function convertMp3ToWav(inputData, settings) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function(ev) { const audioContext = new (window.AudioContext || window.webkitAudioContext)(); audioContext.decodeAudioData(ev.target.result, async function(buffer) { if (buffer.duration > settings.maxDuration) { const numberOfChannels = buffer.numberOfChannels; const newLength = settings.maxDuration * buffer.sampleRate; const newBuffer = audioContext.createBuffer(numberOfChannels, newLength, buffer.sampleRate); for (let channel = 0; channel < numberOfChannels; channel++) { const oldBufferData = buffer.getChannelData(channel); const newBufferData = newBuffer.getChannelData(channel); for (let i = 0; i < newLength; i++) { newBufferData[i] = oldBufferData[i]; } } buffer = newBuffer; } const targetOptions = { sampleRate: Math.round(Number(settings.sampleRate)), bytesPerSample: 8 === Math.round(Number(settings.bitDepth)) ? 1 : 2, channelOpt: settings.channels }; // Inner helper functions const audioResample = (buffer, sampleRate) => { const offlineCtx = new OfflineAudioContext(2, buffer.length / buffer.sampleRate * sampleRate, sampleRate), source = offlineCtx.createBufferSource(); source.buffer = buffer, source.connect(offlineCtx.destination), source.start(); return offlineCtx.startRendering() }; const audioReduceChannels = (buffer, targetChannelOpt) => { if ("both" === targetChannelOpt || buffer.numberOfChannels < 2) return buffer; const outBuffer = new AudioBuffer({ sampleRate: buffer.sampleRate, length: buffer.length, numberOfChannels: 1 }), data = [buffer.getChannelData(0), buffer.getChannelData(1)], newData = new Float32Array(buffer.length); for (let i = 0; i < buffer.length; ++i) newData[i] = "left" === targetChannelOpt ? data[0][i] : "right" === targetChannelOpt ? data[1][i] : (data[0][i] + data[1][i]) / 2; return outBuffer.copyToChannel(newData, 0), outBuffer }; const audioNormalize = buffer => { const data = Array.from(Array(buffer.numberOfChannels)).map((_, idx) => buffer.getChannelData(idx)), maxAmplitude = Math.max(...data.map(chan => chan.reduce((acc, cur) => Math.max(acc, Math.abs(cur)), 0))); if (maxAmplitude >= 1) return buffer; const coeff = 1 / maxAmplitude; return data.forEach(chan => { chan.forEach((v, idx) => chan[idx] = v * coeff), buffer.copyToChannel(chan, 0) }), buffer }; const processAudioFile = async (audioBufferIn, targetChannelOpt, targetSampleRate) => { const resampled = await audioResample(audioBufferIn, targetSampleRate), reduced = audioReduceChannels(resampled, targetChannelOpt); return audioNormalize(reduced) }; const audioToRawWave = (audioChannels, bytesPerSample, mixChannels = false) => { const bufferLength = audioChannels[0].length, numberOfChannels = 1 === audioChannels.length ? 1 : 2, reducedData = new Uint8Array(bufferLength * numberOfChannels * bytesPerSample); for (let i = 0; i < bufferLength; ++i) for (let channel = 0; channel < (mixChannels ? 1 : numberOfChannels); ++channel) { const outputIndex = (i * numberOfChannels + channel) * bytesPerSample; let sample; switch (sample = mixChannels ? audioChannels.reduce((prv, cur) => prv + cur[i], 0) / numberOfChannels : audioChannels[channel][i], sample = sample > 1 ? 1 : sample < -1 ? -1 : sample, bytesPerSample) { case 2: sample *= 32767, reducedData[outputIndex] = sample, reducedData[outputIndex + 1] = sample >> 8; break; case 1: reducedData[outputIndex] = 127 * (sample + 1); break; default: throw "Only 8, 16 bits per sample are supported" } } return reducedData }; const makeWav = (data, channels, sampleRate, bytesPerSample) => { var wav = new Uint8Array(44 + data.length), view = new DataView(wav.buffer); view.setUint32(0, 1380533830, false), view.setUint32(4, 36 + data.length, true), view.setUint32(8, 1463899717, false), view.setUint32(12, 1718449184, false), view.setUint32(16, 16, true), view.setUint16(20, 1, true), view.setUint16(22, channels, true), view.setUint32(24, sampleRate, true), view.setUint32(28, sampleRate * bytesPerSample * channels, true), view.setUint16(32, bytesPerSample * channels, true), view.setUint16(34, 8 * bytesPerSample, true), view.setUint32(36, 1684108385, false), view.setUint32(40, data.length, true), wav.set(data, 44); return new Blob([wav.buffer], { type: "audio/wav" }) }; // Execution starts here const audioBuffer = await processAudioFile(buffer, targetOptions.channelOpt, targetOptions.sampleRate); const rawData = audioToRawWave("both" === targetOptions.channelOpt ? [audioBuffer.getChannelData(0), audioBuffer.getChannelData(1)] : [audioBuffer.getChannelData(0)], targetOptions.bytesPerSample); const blob = makeWav(rawData, "both" === targetOptions.channelOpt ? 2 : 1, targetOptions.sampleRate, targetOptions.bytesPerSample); resolve(blob); }); }; reader.onerror = function(ev) { reject(ev); }; reader.readAsArrayBuffer(inputData); }); } const fileInput = document.querySelector('input[type=file]'); fileInput.addEventListener('change', async function(e) { const file = e.target.files[0]; let startSettings = { sampleRate: 44100, bitDepth: 8, channels: 'mono', maxDuration: 50 }; let blob; let sizeLimit = 400 * 1024; do { blob = await convertMp3ToWav(file, startSettings); // If the blob size is too large, reduce the sample rate by 10% if (blob.size > sizeLimit) { console.log('new file size: ', Math.round(blob.size/1024)+'kb'); console.log('new sample rate', startSettings.sampleRate); if (startSettings.sampleRate * 0.9 >= 3000) { startSettings.sampleRate *= 0.9; } else { console.log('cannot make samle rate any smaller'); } } else { console.log(startSettings.sampleRate); } } while (blob.size > sizeLimit && startSettings.sampleRate * 0.9 >= 3000); console.log('final size: '+Math.round(blob.size / 1024)+'kb'); const url = window.URL.createObjectURL(blob); // Create a temporary downloadable link const tempLink = document.createElement('a'); tempLink.href = url; tempLink.download = 'test.wav'; document.body.appendChild(tempLink); tempLink.click(); // Clean up document.body.removeChild(tempLink); window.URL.revokeObjectURL(url); });