You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

237 line
8.6KB

  1. /*
  2. WAV2SKETCH utility - converts audio files to Teensy code
  3. Javascript version by Matt Bradshaw, converted from original by Paul Stoffregen
  4. HOW IT WORKS:
  5. File loader listens for a user choosing a file
  6. Check desired sample rate and encoding
  7. If no desired sample rate chosen, file's header is read to see if the sample rate can be determined
  8. Create an OfflineAudioContext with appropriate sample rate
  9. Read data from audio file as a series of floating point numbers
  10. Convert these floating point numbers to unsigned integers
  11. Add padding required by Teensy audio library
  12. Pack unsigned integers into 32-bit words, with u-law encoding if desired
  13. */
  14. var audioFileChooser = document.getElementById('audioFileChooser');
  15. audioFileChooser.addEventListener('change', readFile);
  16. if(!window.OfflineAudioContext) alert("Browser does not support OfflineAudioContext");
  17. function readFile() {
  18. for(var i = 0; i < audioFileChooser.files.length; i++) {
  19. var fileReader = new FileReader();
  20. var sampleRateChoice = document.getElementById('sampleRate').value;
  21. var encodingChoice = document.getElementById('encoding').value;
  22. if(sampleRateChoice == "auto") sampleRateChoice = null;
  23. else sampleRateChoice = parseInt(sampleRateChoice);
  24. fileReader.readAsArrayBuffer(audioFileChooser.files[i]);
  25. fileReader.addEventListener('load', function(fileName, ev) {
  26. processFile(ev.target.result, fileName, sampleRateChoice, encodingChoice);
  27. }.bind(null, audioFileChooser.files[i].name));
  28. }
  29. }
  30. function processFile(file, fileName, sampleRateChoice, encodingChoice) {
  31. // determine sample rate
  32. // ideas borrowed from https://github.com/ffdead/wav.js
  33. var sampleRate = 0;
  34. var encoding = encodingChoice;
  35. if(!sampleRateChoice) {
  36. try {
  37. var sampleRateBytes = new Uint8Array(file, 24, 4);
  38. for(var i = 0; i < sampleRateBytes.length; i ++) {
  39. sampleRate |= sampleRateBytes[i] << (i*8);
  40. }
  41. } catch(err) {
  42. console.log('problem reading sample rate');
  43. }
  44. if([44100, 22050, 11025].indexOf(sampleRate) == -1) {
  45. sampleRate = 44100;
  46. }
  47. } else {
  48. sampleRate = sampleRateChoice;
  49. }
  50. var context = new OfflineAudioContext(1, 100*sampleRate, sampleRate); // arbitrary 100 seconds max length for now, nothing that long would fit on a Teensy anyway
  51. context.decodeAudioData(file, function(buffer) {
  52. var monoData = [];
  53. if(buffer.numberOfChannels == 1) {
  54. monoData = buffer.getChannelData(0);
  55. } else if(buffer.numberOfChannels == 2) {
  56. var leftData = buffer.getChannelData(0);
  57. var rightData = buffer.getChannelData(1);
  58. for(var i=0;i<buffer.length;i++) {
  59. monoData[i] = (leftData[i] + rightData[i]) / 2;
  60. }
  61. } else {
  62. alert("ONLY MONO AND STEREO FILES ARE SUPPORTED");
  63. // NB - would be trivial to add support for n channels, given that everything ends up mono anyway
  64. }
  65. var padLength;
  66. var encodingCode = '0';
  67. var sampleRateCode;
  68. if(encoding == 'u-law') encodingCode = '0';
  69. else encodingCode = '8'; // PCM
  70. if(sampleRate == 44100) {
  71. padLength = padding(monoData.length, 128);
  72. sampleRateCode = '1';
  73. } else if(sampleRate == 22050) {
  74. padLength = padding(monoData.length, 64);
  75. sampleRateCode = '2';
  76. } else if(sampleRate == 11025) {
  77. padLength = padding(monoData.length, 32);
  78. sampleRateCode = '3';
  79. }
  80. var ulawOut = [];
  81. for(var i = 0; i < monoData.length; i ++) {
  82. ulawOut.push(ulaw_encode(toInteger(monoData[i]*0x7fff)));
  83. }
  84. window.ulawOut = ulawOut;
  85. var outputData;
  86. if(encoding == 'u-law') {
  87. outputData = createULawWords(ulawOut, padLength);
  88. } else {
  89. outputData = createWords(monoData, padLength);
  90. }
  91. var statusInt = (monoData.length).toString(16);
  92. while(statusInt.length < 6) statusInt = '0' + statusInt;
  93. if(monoData.length>0xFFFFFF) alert("DATA TOO LONG");
  94. statusInt = '0x' + encodingCode + sampleRateCode + statusInt;
  95. outputData.unshift(statusInt);
  96. var outputFileHolder = document.getElementById('outputFileHolder');
  97. var downloadLink1 = document.createElement('a');
  98. var downloadLink2 = document.createElement('a');
  99. var formattedName = fileName.split('.')[0].split(' ').join('');
  100. formattedName = formattedName.charAt(0).toUpperCase() + formattedName.slice(1).toLowerCase();
  101. downloadLink1.href = generateOutputFile(generateCPPFile(fileName, formattedName, outputData, sampleRate, encoding));
  102. downloadLink1.setAttribute('download', 'AudioSample' + formattedName + '.cpp');
  103. downloadLink1.innerHTML = 'Download AudioSample' + formattedName + '.cpp';
  104. downloadLink2.href = generateOutputFile(generateHeaderFile(formattedName, outputData));
  105. downloadLink2.setAttribute('download', 'AudioSample' + formattedName + '.h');
  106. downloadLink2.innerHTML = 'Download AudioSample' + formattedName + '.h';
  107. outputFileHolder.appendChild(downloadLink1);
  108. outputFileHolder.appendChild(document.createElement('br'));
  109. outputFileHolder.appendChild(downloadLink2);
  110. outputFileHolder.appendChild(document.createElement('br'));
  111. });
  112. }
  113. function ulaw_encode(audio)
  114. {
  115. var mag, neg; // both uint32
  116. // http://en.wikipedia.org/wiki/G.711
  117. if (audio >= 0) {
  118. mag = audio;
  119. neg = 0;
  120. } else {
  121. mag = audio * -1;
  122. neg = 0x80;
  123. }
  124. mag += 128;
  125. if (mag > 0x7FFF) mag = 0x7FFF;
  126. if (mag >= 0x4000) return neg | 0x70 | ((mag >> 10) & 0x0F); // 01wx yz00 0000 0000
  127. if (mag >= 0x2000) return neg | 0x60 | ((mag >> 9) & 0x0F); // 001w xyz0 0000 0000
  128. if (mag >= 0x1000) return neg | 0x50 | ((mag >> 8) & 0x0F); // 0001 wxyz 0000 0000
  129. if (mag >= 0x0800) return neg | 0x40 | ((mag >> 7) & 0x0F); // 0000 1wxy z000 0000
  130. if (mag >= 0x0400) return neg | 0x30 | ((mag >> 6) & 0x0F); // 0000 01wx yz00 0000
  131. if (mag >= 0x0200) return neg | 0x20 | ((mag >> 5) & 0x0F); // 0000 001w xyz0 0000
  132. if (mag >= 0x0100) return neg | 0x10 | ((mag >> 4) & 0x0F); // 0000 0001 wxyz 0000
  133. return neg | 0x00 | ((mag >> 3) & 0x0F); // 0000 0000 1wxy z000
  134. }
  135. function createWords(audioData, padLength) {
  136. var totalLength = audioData.length + padLength;
  137. var outputData = [];
  138. for(var i = 0; i < totalLength; i += 2) {
  139. var a = toUint16(i<audioData.length?audioData[i]*0x7fff:0x0000);
  140. var b = toUint16(i+1<audioData.length?audioData[i+1]*0x7fff:0x0000);
  141. var out = (65536*b + a).toString(16);
  142. while(out.length < 8) out = '0' + out;
  143. out = '0x' + out;
  144. outputData.push(out);
  145. }
  146. return outputData;
  147. }
  148. function createULawWords(audioData, padLength) {
  149. var totalLength = audioData.length + padLength;
  150. var outputData = [];
  151. for(var i = 0; i < totalLength; i += 4) {
  152. var a = i<audioData.length ? audioData[i] : 0;
  153. var b = i+1<audioData.length ? audioData[i+1] : 0;
  154. var c = i+2<audioData.length ? audioData[i+2] : 0;
  155. var d = i+3<audioData.length ? audioData[i+3] : 0;
  156. var out = (a + 0x100*b + 0x10000*c + 0x1000000*d).toString(16);
  157. while(out.length < 8) out = '0' + out;
  158. out = '0x' + out;
  159. outputData.push(out);
  160. }
  161. return outputData;
  162. }
  163. // http://2ality.com/2012/02/js-integers.html
  164. function toInteger(x) {
  165. x = Number(x);
  166. return Math.round(x);
  167. //return x < 0 ? Math.ceil(x) : Math.floor(x);
  168. }
  169. function modulo(a, b) {
  170. return a - Math.floor(a/b)*b;
  171. }
  172. function toUint16(x) {
  173. return modulo(toInteger(x), Math.pow(2, 16));
  174. }
  175. function toUint8(x) {
  176. return modulo(toInteger(x), Math.pow(2, 8));
  177. }
  178. // compute the extra padding needed
  179. function padding(sampleLength, block) {
  180. var extra = sampleLength % block;
  181. if (extra == 0) return 0;
  182. return block - extra;
  183. }
  184. function generateOutputFile(fileContents) {
  185. var textFileURL = null;
  186. var blob = new Blob([fileContents], {type: 'text/plain'});
  187. textFileURL = window.URL.createObjectURL(blob);
  188. return textFileURL;
  189. }
  190. function formatAudioData(audioData) {
  191. var outputString = '';
  192. for(var i = 0; i < audioData.length; i ++) {
  193. if(i%8==0 && i>0) outputString += '\n';
  194. outputString += audioData[i] + ',';
  195. }
  196. return outputString;
  197. }
  198. function generateCPPFile(fileName, formattedName, audioData, sampleRate, encodingType) {
  199. var out = "";
  200. out += '// Audio data converted from audio file by wav2sketch_js\n\n';
  201. out += '#include "AudioSample' + formattedName + '.h"\n\n';
  202. out += '// Converted from ' + fileName + ', using ' + sampleRate + ' Hz, 16 bit ' + encodingType + ' encoding\n';
  203. out += 'PROGMEM\n';
  204. out += 'const unsigned int AudioSample' + formattedName + '[' + audioData.length + '] = {\n';
  205. out += formatAudioData(audioData) + '\n};';
  206. return out;
  207. }
  208. function generateHeaderFile(formattedName, audioData) {
  209. var out = "";
  210. out += '// Audio data converted from audio file by wav2sketch_js\n\n';
  211. out += 'extern const unsigned int AudioSample' + formattedName + '[' + audioData.length + '];';
  212. return out;
  213. }