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.

224 lines
8.0KB

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