Permalink
Browse files

Audio Editor

  • Loading branch information...
1 parent e45bcb9 commit 730dbd1aa04144e3dba4bd90bbf7e2f99ce06acf @cpojer cpojer committed Nov 13, 2012
@@ -168,3 +168,72 @@ Controller.define('/recording/{id}', showOne);
Controller.define('/recording/new/chapter/:id:', function(req) {
form.show('chapters', req.id);
});
+
+var loadEditor = function(object, data) {
+ var element = object.toElement().getElement('.editor');
+ var editor = new AudioEditor(element.getElement('.audio-editor'));
+ editor.loadFromArrayBuffer(Base64.decodeAsArrayBuffer(data.substr(24)));
+
+ element.getElement('.editor-copy').addEvent('click', editor.bound('copy'));
+ element.getElement('.editor-cut').addEvent('click', editor.bound('cut'));
+ element.getElement('.editor-paste').addEvent('click', editor.bound('paste'));
+ element.getElement('.editor-remove').addEvent('click', editor.bound('remove'));
+ element.getElement('.editor-select-all').addEvent('click', editor.bound('selectAll'));
+ element.getElement('.editor-zoom').addEvent('click', editor.bound('zoom'));
+ element.getElement('.editor-show-all').addEvent('click', editor.bound('showAll'));
+
+ element.getElement('.editor-play').addEvent('click', editor.bound('play'));
+ element.getElement('.editor-pause').addEvent('click', editor.bound('pause'));
+ element.getElement('.editor-stop').addEvent('click', editor.bound('stop'));
+ element.getElement('.editor-toggle-loop').addEvent('click', editor.bound('toggleLoop'));
+ element.getElement('.editor-save').addEvent('click', function() {
+ editor.render('application/octet-stream', function(url) {
+ window.location.href = url;
+ });
+ });
+
+ element.getElement('.editor-filter-normalize').addEvent('click', editor.bound('filterNormalize'));
+ element.getElement('.editor-filter-silence').addEvent('click', editor.bound('filterSilence'));
+ element.getElement('.editor-filter-fade-in').addEvent('click', editor.bound('filterFadeIn'));
+ element.getElement('.editor-filter-fade-out').addEvent('click', editor.bound('filterFadeOut'));
+};
+
+var AudioEditor = require('Editor/AudioEditor');
+var Base64 = require('Utility/Base64');
+var Request = require('Request');
+Controller.define('/edit', function() {
+ new Request({
+ url: '../../file.base64',
+ onSuccess: function(data) {
+ var object = new View.Object({
+ title: 'Editor',
+ content: UI.render('audio-editor'),
+ onShow: function() {
+ loadEditor(object, data);
+ }
+ });
+
+ View.getMain().push(object);
+ }
+ }).send();
+});
+
+Controller.define('/editor/{id}', function(req) {
+ var recording = Recording.findById(req.id);
+ var object = new View.Object({
+ title: recording.display_name,
+ content: UI.render('audio-editor', recording),
+ onShow: function() {
+ Recording.read(req.id, function(entry) {
+ entry.file(function(file) {
+ var reader = new FileReader();
+ reader.onloadend = function(event) {
+ loadEditor(object, reader.result);
+ };
+ reader.readAsDataURL(file);
+ });
+ });
+ }
+ });
+ View.getMain().push(object);
+});
@@ -0,0 +1,108 @@
+var AudioPlayback = require('./AudioPlayback');
+var AudioSequence = require('./AudioSequence');
+var Binds = require('./Binds');
+var SequenceEditor = require('./SequenceEditor');
+var WAVEncoder = require('./WAVEncoder');
+
+var invoke = function(array, methodName){
+ var args = Array.prototype.slice.call(arguments, 2);
+ return array.map(function(item){
+ return item[methodName].apply(item, args);
+ });
+};
+
+var AudioEditor = module.exports = function(element) {
+ Binds.mixin(this);
+ this.element = element;
+ this.editors = [];
+ this.linkMode = false;
+ this.playLoop = false;
+
+ this.audioPlayback = new AudioPlayback();
+ this.audioPlayback.addListener(this.bound('onPlaybackUpdate'));
+};
+
+AudioEditor.prototype.onPlaybackUpdate = function() {
+ this.editors.each(function(editor) {
+ editor.setPosition(this.audioPlayback.currentPlayPosition);
+ }, this);
+
+ var node = this.audioPlayback.analyserNode;
+ node.getFloatFrequencyData(new Float32Array(node.frequencyBinCount));
+};
+
+AudioEditor.prototype.addSequence = function(sequence) {
+ var editor = new SequenceEditor(sequence, this.element);
+ this.editors.push(editor);
+ this.setLinkMode(this.linkMode);
+ return editor;
+};
+
+AudioEditor.prototype.removeSequences = function() {
+ invoke(this.editors, 'remove');
+ this.editors = [];
+};
+
+AudioEditor.prototype.setLinkMode = function(linkMode) {
+ this.linkmode = linkMode;
+ if (!this.linkMode) return;
+
+ var editors = this.editors;
+ for (var i = 0; i < editors.length - 1; ++i) {
+ for (var j = i + 1; j < editors.length; ++j) {
+ editors[i].link(editors[j]);
+ }
+ }
+};
+
+AudioEditor.prototype.selectAll = function() { invoke(this.editors, 'selectAll'); };
+AudioEditor.prototype.filterNormalize = function() { invoke(this.editors, 'filterNormalize'); };
+AudioEditor.prototype.filterFadeIn = function() { invoke(this.editors, 'filterFade', true); };
+AudioEditor.prototype.filterFadeOut = function() { invoke(this.editors, 'filterFade', false); };
+AudioEditor.prototype.filterGain = function(decibel) { invoke(this.editors, 'filterGain', false); };
+AudioEditor.prototype.filterSilence = function() { invoke(this.editors, 'filterSilence'); };
+AudioEditor.prototype.copy = function() { invoke(this.editors, 'copy', false); };
+AudioEditor.prototype.paste = function() { invoke(this.editors, 'paste', false); };
+AudioEditor.prototype.cut = function() { invoke(this.editors, 'cut', false); };
+AudioEditor.prototype.remove = function() { invoke(this.editors, 'remove', false); };
+AudioEditor.prototype.zoom = function() { invoke(this.editors, 'zoom'); };
+AudioEditor.prototype.showAll = function() { invoke(this.editors, 'showAll'); };
+AudioEditor.prototype.pause = function() { this.audioPlayback.pause(); };
+AudioEditor.prototype.stop = function() { this.audioPlayback.stop(); };
+AudioEditor.prototype.toggleLoop = function() { this.playLoop = !this.playLoop; };
+
+AudioEditor.prototype.play = function() {
+ // fast version
+ var audioData = this.editors.map(function(editor) {
+ return editor.audioSequence.data;
+ });
+ var selectionStart = this.editors[0].selectionStart;
+ var selectionEnd = this.editors[0].selectionEnd;
+ if (selectionStart != selectionEnd) this.audioPlayback.play(audioData, this.editors[0].audioSequence.sampleRate, this.playLoop, selectionStart, selectionEnd);
+ else this.audioPlayback.play(audioData, this.editors[0].audioSequence.sampleRate, this.playLoop);
+
+ /* slow version
+ this.render('audio/wav', (function(url) {
+ this.audioPlayback.src = url;
+ this.audioPlayback.play();
+ }).bind(this));
+ */
+};
+
+AudioEditor.prototype.render = function(encoding, fn) {
+ new WAVEncoder(this.editors.map(function(editor) {
+ return editor.audioSequence;
+ })).toBlobURL(encoding, fn);
+};
+
+var decode = function(buffer) {
+ for (var i = 0; i < buffer.numberOfChannels; i++)
+ this.addSequence(new AudioSequence(buffer.sampleRate, buffer.getChannelData(i)));
+};
+
+AudioEditor.prototype.loadFromArrayBuffer = function(arrayBuffer) {
+ this.removeSequences();
+ this.audioPlayback.audioContext.decodeAudioData(arrayBuffer, decode.bind(this), function() {
+ console.log('error');
+ });
+};
@@ -0,0 +1,166 @@
+var AudioContext = window.AudioContext || window.webkitAudioContext;
+
+var AudioPlayback = module.exports = function() {
+ // Creation of a new audio context
+ this.audioBufferSize = 1024;
+ this.sampleRate = 0;
+ this.audioContext = new AudioContext();
+
+ this.audioNode = this.audioContext.createJavaScriptNode(this.audioBufferSize, 1, 2);
+ this.audioNode.onaudioprocess = this.onAudioUpdate.bind(this);
+
+ this.analyserNode = this.audioContext.createAnalyser();
+ this.analyserNode.minDecibels = -100;
+ this.analyserNode.maxDecibels = 0;
+ this.analyserNode.smoothingTimeConstant = 0.0;
+ this.analyserNode.connect(this.audioContext.destination);
+
+ this.audioData = undefined;
+
+ // Playback information
+ this.playStart = 0;
+ this.playEnd = 0;
+ this.isLooped = false;
+ this.currentPlayPosition = 0;
+ this.isPlaying = false;
+
+ // Callback information
+ this.updateListener = [];
+ this.playbackUpdateInterval = 0.0; // in Seconds
+ this.lastPlaybackUpdate = 0;
+};
+
+AudioPlayback.prototype.onAudioUpdate = function(evt) {
+ var bufferSize = this.audioBufferSize;
+ var elapsedTime = bufferSize / this.sampleRate;
+
+ if (!this.isPlaying) return;
+
+ var audioData = this.audioData;
+ var leftBuffer = evt.outputBuffer.getChannelData(0);
+ var rightBuffer = evt.outputBuffer.getChannelData(1);
+
+ this.copyChannelDataToBuffer(leftBuffer, audioData[0], this.currentPlayPosition, bufferSize, this.playStart, this.playEnd, this.isLooped);
+ if (audioData.length == 1) // mono
+ this.currentPlayPosition = this.copyChannelDataToBuffer(rightBuffer, audioData[0], this.currentPlayPosition, bufferSize, this.playStart, this.playEnd, this.isLooped);
+ else if (audioData.length == 2) // stereo
+ this.currentPlayPosition = this.copyChannelDataToBuffer(rightBuffer, audioData[1], this.currentPlayPosition, bufferSize, this.playStart, this.playEnd, this.isLooped);
+
+ // the playback is done
+ if (this.currentPlayPosition === undefined) {
+ this.stop();
+ } else {
+ this.lastPlaybackUpdate -= elapsedTime;
+ if (this.lastPlaybackUpdate < 0.0) {
+ this.lastPlaybackUpdate = this.playbackUpdateInterval;
+ this.notifyListener();
+ }
+ }
+};
+
+/**
+ * Copies the audio data to a channel buffer and sets the new play position. If looping is enabled,
+ * the position is set automaticly.
+ * @param bufferReference Reference to the channel buffer
+ * @param dataReference Reference to the audio data
+ * @param position Current position of the playback
+ * @param len Length of the chunk
+ * @param startPosition Start position for looping
+ * @param endPosition End position for looping
+ * @param isLooped Enable looping.
+ */
+AudioPlayback.prototype.copyChannelDataToBuffer = function(bufferReference, dataReference, position, len, startPosition, endPosition, isLooped) {
+ /* In order to enable looping, we should need to split up when the end of the audio data is reached
+ * to begin with the first position. Therefore is a split into two ranges if neccessary
+ */
+ var firstSplitStart = position;
+ var firstSplitEnd = (position + len > dataReference.length) ? dataReference.length : (position + len > endPosition) ? endPosition : (position + len);
+ var firstSplitLen = firstSplitEnd - firstSplitStart;
+
+ var secondSplitStart = (firstSplitLen < bufferReference.length) ? (isLooped) ? startPosition : 0 : undefined;
+ var secondSplitEnd = (secondSplitStart !== undefined) ? bufferReference.length - firstSplitLen + secondSplitStart : undefined;
+
+ if (secondSplitStart === undefined) {
+ this.copyIntoBuffer(bufferReference, 0, dataReference, firstSplitStart, firstSplitEnd);
+ return firstSplitEnd;
+ }
+
+ this.copyIntoBuffer(bufferReference, 0, dataReference, firstSplitStart, firstSplitEnd);
+ if (isLooped) {
+ this.copyIntoBuffer(bufferReference, firstSplitLen, dataReference, secondSplitStart, secondSplitEnd);
+ return secondSplitEnd;
+ }
+
+ return null;
+};
+
+AudioPlayback.prototype.copyIntoBuffer = function(bufferReference, bufferOffset, dataReference, dataOffset, end) {
+ bufferReference.set(dataReference.slice(dataOffset, end), bufferOffset);
+};
+
+AudioPlayback.prototype.play = function(audioData, sampleRate, isLooped, start, end) {
+ if (this.isPlaying || audioData === undefined || audioData.length < 1 ||
+ sampleRate === undefined || sampleRate <= 0) return;
+
+ if (this.currentPlayPosition) {
+ this.resume();
+ return;
+ }
+
+ this.audioData = audioData;
+ this.sampleRate = sampleRate;
+ this.isLooped = (isLooped === undefined) ? false : isLooped;
+ this.playStart = (start === undefined || start < 0 || start >= audioData[0].length) ? 0 : start;
+ this.playEnd = (end === undefined || end - this.audioBufferSize < start || end >= audioData[0].length) ? audioData[0].length : end;
+ this.currentPlayPosition = this.playStart;
+ this.isPlaying = true;
+
+ this.audioNode.connect(this.analyserNode);
+
+ this.notifyListener();
+};
+
+AudioPlayback.prototype.stop = function() {
+ if (!this.isPlaying) return;
+
+ this.audioNode.disconnect(this.analyserNode);
+
+ this.playStart = 0;
+ this.playEnd = 0;
+ this.isLooped = false;
+ this.currentPlayPosition = 0;
+ this.isPlaying = false;
+ this.lastPlaybackUpdate = 0;
+ this.audioData = undefined;
+ this.sampleRate = 0;
+
+ this.notifyListener();
+};
+
+AudioPlayback.prototype.pause = function() {
+ if (!this.isPlaying) return;
+
+ this.audioNode.disconnect(this.analyserNode);
+
+ this.isPlaying = false;
+ this.lastPlaybackUpdate = 0;
+ this.notifyListener();
+};
+
+AudioPlayback.prototype.resume = function() {
+ if (this.isPlaying || this.audioData === undefined || this.audioData.length < 1) return;
+ this.isPlaying = true;
+
+ this.audioNode.connect(this.analyserNode);
+
+ this.notifyListener();
+};
+
+AudioPlayback.prototype.addListener = function(updateCallback) {
+ this.updateListener.push(updateCallback);
+};
+
+AudioPlayback.prototype.notifyListener = function() {
+ for (var i = 0; i < this.updateListener.length; ++i)
+ this.updateListener[i]();
+};
Oops, something went wrong.

0 comments on commit 730dbd1

Please sign in to comment.