|
|
@@ -0,0 +1,235 @@ |
|
|
|
/* |
|
|
|
WAV2SKETCH utility - converts audio files to Teensy code |
|
|
|
Javascript version by Matt Bradshaw, converted from original by Paul Stoffregen |
|
|
|
|
|
|
|
HOW IT WORKS: |
|
|
|
File loader listens for a user choosing a file |
|
|
|
Check desired sample rate and encoding |
|
|
|
If no desired sample rate chosen, file's header is read to see if the sample rate can be determined |
|
|
|
Create an OfflineAudioContext with appropriate sample rate |
|
|
|
Read data from audio file as a series of floating point numbers |
|
|
|
Convert these floating point numbers to unsigned integers |
|
|
|
Add padding required by Teensy audio library |
|
|
|
Pack unsigned integers into 32-bit words, with u-law encoding if desired |
|
|
|
*/ |
|
|
|
|
|
|
|
var audioFileChooser = document.getElementById('audioFileChooser'); |
|
|
|
|
|
|
|
audioFileChooser.addEventListener('change', readFile); |
|
|
|
|
|
|
|
if(!window.OfflineAudioContext) alert("Browser does not support OfflineAudioContext"); |
|
|
|
|
|
|
|
function readFile() { |
|
|
|
for(var i = 0; i < audioFileChooser.files.length; i++) { |
|
|
|
var fileReader = new FileReader(); |
|
|
|
var sampleRateChoice = document.getElementById('sampleRate').value; |
|
|
|
var encodingChoice = document.getElementById('encoding').value; |
|
|
|
if(sampleRateChoice == "auto") sampleRateChoice = null; |
|
|
|
else sampleRateChoice = parseInt(sampleRateChoice); |
|
|
|
fileReader.readAsArrayBuffer(audioFileChooser.files[i]); |
|
|
|
fileReader.addEventListener('load', function(fileName, ev) { |
|
|
|
processFile(ev.target.result, fileName, sampleRateChoice, encodingChoice); |
|
|
|
}.bind(null, audioFileChooser.files[i].name)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function processFile(file, fileName, sampleRateChoice, encodingChoice) { |
|
|
|
// determine sample rate |
|
|
|
// ideas borrowed from https://github.com/ffdead/wav.js |
|
|
|
var sampleRate = 0; |
|
|
|
var encoding = encodingChoice; |
|
|
|
if(!sampleRateChoice) { |
|
|
|
try { |
|
|
|
var sampleRateBytes = new Uint8Array(file, 24, 4); |
|
|
|
for(var i = 0; i < sampleRateBytes.length; i ++) { |
|
|
|
sampleRate |= sampleRateBytes[i] << (i*8); |
|
|
|
} |
|
|
|
} catch(err) { |
|
|
|
console.log('problem reading sample rate'); |
|
|
|
} |
|
|
|
if([44100, 22050, 11025].indexOf(sampleRate) == -1) { |
|
|
|
sampleRate = 44100; |
|
|
|
} |
|
|
|
} else { |
|
|
|
sampleRate = sampleRateChoice; |
|
|
|
} |
|
|
|
|
|
|
|
var context = new OfflineAudioContext(1, 100*sampleRate, sampleRate); // arbitrary 100 seconds max length for now, nothing that long would fit on a Teensy anyway |
|
|
|
context.decodeAudioData(file, function(buffer) { |
|
|
|
var monoData = []; |
|
|
|
if(buffer.numberOfChannels == 1) { |
|
|
|
monoData = buffer.getChannelData(0); |
|
|
|
} else if(buffer.numberOfChannels == 2) { |
|
|
|
var leftData = buffer.getChannelData(0); |
|
|
|
var rightData = buffer.getChannelData(1); |
|
|
|
for(var i=0;i<buffer.length;i++) { |
|
|
|
monoData[i] = (leftData[i] + rightData[i]) / 2; |
|
|
|
} |
|
|
|
} else { |
|
|
|
alert("ONLY MONO AND STEREO FILES ARE SUPPORTED"); |
|
|
|
// NB - would be trivial to add support for n channels, given that everything ends up mono anyway |
|
|
|
} |
|
|
|
var padLength; |
|
|
|
var encodingCode = '0'; |
|
|
|
var sampleRateCode; |
|
|
|
if(encoding == 'u-law') encodingCode = '0'; |
|
|
|
else encodingCode = '8'; // PCM |
|
|
|
if(sampleRate == 44100) { |
|
|
|
padLength = padding(monoData.length, 128); |
|
|
|
sampleRateCode = '1'; |
|
|
|
} else if(sampleRate == 22050) { |
|
|
|
padLength = padding(monoData.length, 64); |
|
|
|
sampleRateCode = '2'; |
|
|
|
} else if(sampleRate == 11025) { |
|
|
|
padLength = padding(monoData.length, 32); |
|
|
|
sampleRateCode = '3'; |
|
|
|
} |
|
|
|
|
|
|
|
var ulawOut = []; |
|
|
|
for(var i = 0; i < monoData.length; i ++) { |
|
|
|
ulawOut.push(ulaw_encode(toInteger(monoData[i]*0x7fff))); |
|
|
|
} |
|
|
|
window.ulawOut = ulawOut; |
|
|
|
var outputData; |
|
|
|
if(encoding == 'u-law') { |
|
|
|
outputData = createULawWords(ulawOut, padLength); |
|
|
|
} else { |
|
|
|
outputData = createWords(monoData, padLength); |
|
|
|
} |
|
|
|
|
|
|
|
var statusInt = (monoData.length).toString(16); |
|
|
|
while(statusInt.length < 6) statusInt = '0' + statusInt; |
|
|
|
if(monoData.length>0xFFFFFF) alert("DATA TOO LONG"); |
|
|
|
statusInt = '0x' + encodingCode + sampleRateCode + statusInt; |
|
|
|
outputData.unshift(statusInt); |
|
|
|
|
|
|
|
var outputFileHolder = document.getElementById('outputFileHolder'); |
|
|
|
var downloadLink1 = document.createElement('a'); |
|
|
|
var downloadLink2 = document.createElement('a'); |
|
|
|
var formattedName = fileName.split('.')[0].split(' ').join(''); |
|
|
|
formattedName = formattedName.charAt(0).toUpperCase() + formattedName.slice(1).toLowerCase(); |
|
|
|
downloadLink1.href = generateOutputFile(generateCPPFile(fileName, formattedName, outputData, sampleRate, encoding)); |
|
|
|
downloadLink1.setAttribute('download', 'AudioSample' + formattedName + '.cpp'); |
|
|
|
downloadLink1.innerHTML = 'Download AudioSample' + formattedName + '.cpp'; |
|
|
|
downloadLink2.href = generateOutputFile(generateHeaderFile(formattedName, outputData)); |
|
|
|
downloadLink2.setAttribute('download', 'AudioSample' + formattedName + '.h'); |
|
|
|
downloadLink2.innerHTML = 'Download AudioSample' + formattedName + '.h'; |
|
|
|
outputFileHolder.appendChild(downloadLink1); |
|
|
|
outputFileHolder.appendChild(document.createElement('br')); |
|
|
|
outputFileHolder.appendChild(downloadLink2); |
|
|
|
outputFileHolder.appendChild(document.createElement('br')); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
function ulaw_encode(audio) |
|
|
|
{ |
|
|
|
var mag, neg; // both uint32 |
|
|
|
|
|
|
|
// http://en.wikipedia.org/wiki/G.711 |
|
|
|
if (audio >= 0) { |
|
|
|
mag = audio; |
|
|
|
neg = 0; |
|
|
|
} else { |
|
|
|
mag = audio * -1; |
|
|
|
neg = 0x80; |
|
|
|
} |
|
|
|
mag += 128; |
|
|
|
if (mag > 0x7FFF) mag = 0x7FFF; |
|
|
|
if (mag >= 0x4000) return neg | 0x70 | ((mag >> 10) & 0x0F); // 01wx yz00 0000 0000 |
|
|
|
if (mag >= 0x2000) return neg | 0x60 | ((mag >> 9) & 0x0F); // 001w xyz0 0000 0000 |
|
|
|
if (mag >= 0x1000) return neg | 0x50 | ((mag >> 8) & 0x0F); // 0001 wxyz 0000 0000 |
|
|
|
if (mag >= 0x0800) return neg | 0x40 | ((mag >> 7) & 0x0F); // 0000 1wxy z000 0000 |
|
|
|
if (mag >= 0x0400) return neg | 0x30 | ((mag >> 6) & 0x0F); // 0000 01wx yz00 0000 |
|
|
|
if (mag >= 0x0200) return neg | 0x20 | ((mag >> 5) & 0x0F); // 0000 001w xyz0 0000 |
|
|
|
if (mag >= 0x0100) return neg | 0x10 | ((mag >> 4) & 0x0F); // 0000 0001 wxyz 0000 |
|
|
|
return neg | 0x00 | ((mag >> 3) & 0x0F); // 0000 0000 1wxy z000 |
|
|
|
} |
|
|
|
|
|
|
|
function createWords(audioData, padLength) { |
|
|
|
var totalLength = audioData.length + padLength; |
|
|
|
var outputData = []; |
|
|
|
for(var i = 0; i < totalLength; i += 2) { |
|
|
|
var a = toUint16(i<audioData.length?audioData[i]*0x7fff:0x0000); |
|
|
|
var b = toUint16(i+1<audioData.length?audioData[i+1]*0x7fff:0x0000); |
|
|
|
var out = (65536*b + a).toString(16); |
|
|
|
while(out.length < 8) out = '0' + out; |
|
|
|
out = '0x' + out; |
|
|
|
outputData.push(out); |
|
|
|
} |
|
|
|
return outputData; |
|
|
|
} |
|
|
|
|
|
|
|
function createULawWords(audioData, padLength) { |
|
|
|
var totalLength = audioData.length + padLength; |
|
|
|
var outputData = []; |
|
|
|
for(var i = 0; i < totalLength; i += 4) { |
|
|
|
var a = i<audioData.length ? audioData[i] : 0; |
|
|
|
var b = i+1<audioData.length ? audioData[i+1] : 0; |
|
|
|
var c = i+2<audioData.length ? audioData[i+2] : 0; |
|
|
|
var d = i+3<audioData.length ? audioData[i+3] : 0; |
|
|
|
var out = (a + 0x100*b + 0x10000*c + 0x1000000*d).toString(16); |
|
|
|
while(out.length < 8) out = '0' + out; |
|
|
|
out = '0x' + out; |
|
|
|
outputData.push(out); |
|
|
|
} |
|
|
|
return outputData; |
|
|
|
} |
|
|
|
|
|
|
|
// http://2ality.com/2012/02/js-integers.html |
|
|
|
function toInteger(x) { |
|
|
|
x = Number(x); |
|
|
|
return Math.round(x); |
|
|
|
//return x < 0 ? Math.ceil(x) : Math.floor(x); |
|
|
|
} |
|
|
|
|
|
|
|
function modulo(a, b) { |
|
|
|
return a - Math.floor(a/b)*b; |
|
|
|
} |
|
|
|
|
|
|
|
function toUint16(x) { |
|
|
|
return modulo(toInteger(x), Math.pow(2, 16)); |
|
|
|
} |
|
|
|
|
|
|
|
function toUint8(x) { |
|
|
|
return modulo(toInteger(x), Math.pow(2, 8)); |
|
|
|
} |
|
|
|
|
|
|
|
// compute the extra padding needed |
|
|
|
function padding(sampleLength, block) { |
|
|
|
var extra = sampleLength % block; |
|
|
|
if (extra == 0) return 0; |
|
|
|
return block - extra; |
|
|
|
} |
|
|
|
|
|
|
|
function generateOutputFile(fileContents) { |
|
|
|
var textFileURL = null; |
|
|
|
var blob = new Blob([fileContents], {type: 'text/plain'}); |
|
|
|
textFileURL = window.URL.createObjectURL(blob); |
|
|
|
return textFileURL; |
|
|
|
} |
|
|
|
|
|
|
|
function formatAudioData(audioData) { |
|
|
|
var outputString = ''; |
|
|
|
for(var i = 0; i < audioData.length; i ++) { |
|
|
|
if(i%8==0 && i>0) outputString += '\n'; |
|
|
|
outputString += audioData[i] + ','; |
|
|
|
} |
|
|
|
return outputString; |
|
|
|
} |
|
|
|
|
|
|
|
function generateCPPFile(fileName, formattedName, audioData, sampleRate, encodingType) { |
|
|
|
var out = ""; |
|
|
|
out += '// Audio data converted from audio file by wav2sketch_js\n\n'; |
|
|
|
out += '#include "AudioSample' + formattedName + '.h"\n\n'; |
|
|
|
out += '// Converted from ' + fileName + ', using ' + sampleRate + ' Hz, 16 bit ' + encodingType + ' encoding\n'; |
|
|
|
out += 'const unsigned int AudioSample' + formattedName + '[' + audioData.length + '] = {\n'; |
|
|
|
out += formatAudioData(audioData) + '\n};'; |
|
|
|
return out; |
|
|
|
} |
|
|
|
|
|
|
|
function generateHeaderFile(formattedName, audioData) { |
|
|
|
var out = ""; |
|
|
|
out += '// Audio data converted from audio file by wav2sketch_js\n\n'; |
|
|
|
out += 'extern const unsigned int AudioSample' + formattedName + '[' + audioData.length + '];'; |
|
|
|
return out; |
|
|
|
} |