Skip to content

Commit

Permalink
add save() and getBlob() methods to SoundFile. Move .wav conversi…
Browse files Browse the repository at this point in the history
…on to helpers.js (#315)

* add save and saveBlob to p5.SoundFile. Move convertToWav to helpers.js
* saveBlob --> getBlob, update inline example
  • Loading branch information
therewasaguy authored Aug 18, 2018
1 parent feb4bd3 commit dd0779e
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 90 deletions.
80 changes: 79 additions & 1 deletion src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ define(function (require) {
};

// This method converts ANSI notes specified as a string "C4", "Eb3" to a frequency
noteToFreq = function(note) {
var noteToFreq = function(note) {
if (typeof note !== 'string') {
return note;
}
Expand Down Expand Up @@ -224,7 +224,85 @@ define(function (require) {
return o;
};


// helper methods to convert audio file as .wav format,
// will use as saving .wav file and saving blob object
// Thank you to Matt Diamond's RecorderJS (MIT License)
// https://github.com/mattdiamond/Recorderjs
function convertToWav(audioBuffer) {
var leftChannel, rightChannel;
leftChannel = audioBuffer.getChannelData(0);

// handle mono files
if (audioBuffer.numberOfChannels > 1) {
rightChannel = audioBuffer.getChannelData(1);
} else {
rightChannel = leftChannel;
}

var interleaved = interleave(leftChannel, rightChannel);

// create the buffer and view to create the .WAV file
var buffer = new window.ArrayBuffer(44 + interleaved.length * 2);
var view = new window.DataView(buffer);

// write the WAV container,
// check spec at: https://web.archive.org/web/20171215131933/http://tiny.systems/software/soundProgrammer/WavFormatDocs.pdf

// RIFF chunk descriptor
writeUTFBytes(view, 0, 'RIFF');
view.setUint32(4, 36 + interleaved.length * 2, true);
writeUTFBytes(view, 8, 'WAVE');
// FMT sub-chunk
writeUTFBytes(view, 12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
// stereo (2 channels)
view.setUint16(22, 2, true);
view.setUint32(24, 44100, true);
view.setUint32(28, 44100 * 4, true);
view.setUint16(32, 4, true);
view.setUint16(34, 16, true);
// data sub-chunk
writeUTFBytes(view, 36, 'data');
view.setUint32(40, interleaved.length * 2, true);

// write the PCM samples
var lng = interleaved.length;
var index = 44;
var volume = 1;
for (var i = 0; i < lng; i++) {
view.setInt16(index, interleaved[i] * (0x7FFF * volume), true);
index += 2;
}

return view;
}

// helper methods to save waves
function interleave(leftChannel, rightChannel) {
var length = leftChannel.length + rightChannel.length;
var result = new Float32Array(length);

var inputIndex = 0;

for (var index = 0; index < length;) {
result[index++] = leftChannel[inputIndex];
result[index++] = rightChannel[inputIndex];
inputIndex++;
}
return result;
}

function writeUTFBytes(view, offset, string) {
var lng = string.length;
for (var i = 0; i < lng; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}

return {
convertToWav: convertToWav,
midiToFreq: midiToFreq,
noteToFreq: noteToFreq
};
Expand Down
100 changes: 11 additions & 89 deletions src/soundRecorder.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ define(function (require) {
// inspiration: recorder.js, Tone.js & typedarray.org

var p5sound = require('master');
var convertToWav = require('helpers').convertToWav;
var ac = p5sound.audiocontext;

/**
Expand Down Expand Up @@ -244,101 +245,22 @@ define(function (require) {
this._jsNode = null;
};

/**
* Export a usable url for blob object.
*
* @method saveSoundToBlob
* @param {p5.SoundFile} soundFile p5.SoundFile that you wish to export
*/
p5.prototype.saveSoundToBlob = function(soundFile) {
const dataView = convertToWav(soundFile)
const audioBlob = new Blob([dataView], {type: 'audio/wav'})
return URL.createObjectURL(audioBlob)
}

/**
* Save a p5.SoundFile as a .wav audio file.
* Save a p5.SoundFile as a .wav file. The browser will prompt the user
* to download the file to their device.
* For uploading audio to a server, use
* <a href="/docs/reference/#/p5.SoundFile/saveBlob">`p5.SoundFile.saveBlob`</a>.
*
* @for p5
* @method saveSound
* @param {p5.SoundFile} soundFile p5.SoundFile that you wish to save
* @param {String} name name of the resulting .wav file.
* @param {String} fileName name of the resulting .wav file.
*/
p5.prototype.saveSound = function(soundFile, name) {
const dataView = convertToWav(soundFile)
p5.prototype.writeFile( [ dataView ], name, 'wav');
// add to p5.prototype as this is used by the p5 `save()` method.
p5.prototype.saveSound = function (soundFile, fileName) {
const dataView = convertToWav(soundFile.buffer);
p5.prototype.writeFile([dataView], fileName, 'wav');
};

// helper methods to convert audio file as .wav format,
// will use as saving .wav file and saving blob object
function convertToWav(soundFile){
var leftChannel, rightChannel;
leftChannel = soundFile.buffer.getChannelData(0);

// handle mono files
if (soundFile.buffer.numberOfChannels > 1) {
rightChannel = soundFile.buffer.getChannelData(1);
} else {
rightChannel = leftChannel;
}

var interleaved = interleave(leftChannel,rightChannel);

// create the buffer and view to create the .WAV file
var buffer = new window.ArrayBuffer(44 + interleaved.length * 2);
var view = new window.DataView(buffer);

// write the WAV container,
// check spec at: https://ccrma.stanford.edu/courses/422/projects/WaveFormat/
// RIFF chunk descriptor
writeUTFBytes(view, 0, 'RIFF');
view.setUint32(4, 36 + interleaved.length * 2, true);
writeUTFBytes(view, 8, 'WAVE');
// FMT sub-chunk
writeUTFBytes(view, 12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
// stereo (2 channels)
view.setUint16(22, 2, true);
view.setUint32(24, 44100, true);
view.setUint32(28, 44100 * 4, true);
view.setUint16(32, 4, true);
view.setUint16(34, 16, true);
// data sub-chunk
writeUTFBytes(view, 36, 'data');
view.setUint32(40, interleaved.length * 2, true);

// write the PCM samples
var lng = interleaved.length;
var index = 44;
var volume = 1;
for (var i = 0; i < lng; i++) {
view.setInt16(index, interleaved[i] * (0x7FFF * volume), true);
index += 2;
}

return view
}

// helper methods to save waves
function interleave(leftChannel, rightChannel) {
var length = leftChannel.length + rightChannel.length;
var result = new Float32Array(length);

var inputIndex = 0;

for (var index = 0; index < length; ) {
result[index++] = leftChannel[inputIndex];
result[index++] = rightChannel[inputIndex];
inputIndex++;
}
return result;
}

function writeUTFBytes(view, offset, string) {
var lng = string.length;
for (var i = 0; i < lng; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}

});
69 changes: 69 additions & 0 deletions src/soundfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ define(function (require) {
var p5sound = require('master');
var ac = p5sound.audiocontext;
var midiToFreq = require('helpers').midiToFreq;
var convertToWav = require('helpers').convertToWav;

/**
* <p>SoundFile object with a path to a file.</p>
Expand Down Expand Up @@ -1640,4 +1641,72 @@ define(function (require) {
this._prevTime = playbackTime;
};

/**
* Save a p5.SoundFile as a .wav file. The browser will prompt the user
* to download the file to their device.
*
* @method save
* @param {String} [fileName] name of the resulting .wav file.
*/
p5.SoundFile.prototype.save = function(fileName) {
const dataView = convertToWav(this.buffer);
p5.prototype.writeFile([dataView], fileName, 'wav');
};

/**
* This method is useful for sending a SoundFile to a server. It returns the
* .wav-encoded audio data as a "<a target="_blank" title="Blob reference at
* MDN" href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>".
* A Blob is a file-like data object that can be uploaded to a server
* with an <a href="/docs/reference/#/p5/httpDo">http</a> request. We'll
* use the `httpDo` options object to send a POST request with some
* specific options: we encode the request as `multipart/form-data`,
* and attach the blob as one of the form values using `FormData`.
*
*
* @method getBlob
* @returns {Blob} A file-like data object
* @example
* <div><code>
*
* function preload() {
* mySound = loadSound('assets/doorbell.mp3');
* }
*
* function setup() {
* noCanvas();
* var soundBlob = mySound.getBlob();
*
* // Now we can send the blob to a server...
* var serverUrl = 'https://jsonplaceholder.typicode.com/posts';
* var httpRequestOptions = {
* method: 'POST',
* body: new FormData().append('soundBlob', soundBlob),
* headers: new Headers({
* 'Content-Type': 'multipart/form-data'
* })
* };
* httpDo(serverUrl, httpRequestOptions);
*
* // We can also create an `ObjectURL` pointing to the Blob
* var blobUrl = URL.createObjectURL(soundBlob);
*
* // The `<Audio>` Element accepts Object URL's
* var htmlAudioElt = createAudio(blobUrl).showControls();
*
* createDiv();
*
* // The ObjectURL exists as long as this tab is open
* var input = createInput(blobUrl);
* input.attribute('readonly', true);
* input.mouseClicked(function() { input.elt.select() });
* }
*
* </code></div>
*/
p5.SoundFile.prototype.getBlob = function() {
const dataView = convertToWav(this.buffer);
return new Blob([dataView], { type: 'audio/wav' });
};

});

0 comments on commit dd0779e

Please sign in to comment.