diff --git a/poc/offline-audio-context/helpers.js b/poc/offline-audio-context/helpers.js index 93269b2..804e792 100644 --- a/poc/offline-audio-context/helpers.js +++ b/poc/offline-audio-context/helpers.js @@ -123,6 +123,6 @@ const forceDownload = (blob, filename = 'output.wav') => { document.body.removeChild(link) } -const makeDownload = (abuffer, totalSamples) => { - forceDownload(bufferToWave(abuffer, totalSamples), 'dummy.wav') +const makeDownload = (abuffer, totalSamples, filename) => { + forceDownload(bufferToWave(abuffer, totalSamples), `${filename}-${Date.now()}.wav`) } diff --git a/poc/offline-audio-context/script.js b/poc/offline-audio-context/script.js index c2b35d3..f4f36ab 100644 --- a/poc/offline-audio-context/script.js +++ b/poc/offline-audio-context/script.js @@ -32,7 +32,7 @@ const demo = () => { render(a, ctx) ctx.startRendering().then(abuffer => { - makeDownload(abuffer, ctx.length) + makeDownload(abuffer, ctx.length, 'offline-audio-context') }).catch(e => { console.error(e) }) diff --git a/poc/scheduled-sequencing/helpers.js b/poc/scheduled-sequencing/helpers.js new file mode 100644 index 0000000..804e792 --- /dev/null +++ b/poc/scheduled-sequencing/helpers.js @@ -0,0 +1,128 @@ +/* global Blob, URL */ + +/* +function floatTo16BitPCM(output, offset, input) { + for (let i = 0; i < input.length; i++, offset += 2) { + let s = Math.max(-1, Math.min(1, input[i])); + output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); + } +} + +function writeString(view, offset, string) { + for (let i = 0; i < string.length; i++) { + view.setUint8(offset + i, string.charCodeAt(i)); + } +} + +function encodeWAV(samples) { + let buffer = new ArrayBuffer(44 + samples.length * 2); + let view = new DataView(buffer); + + // RIFF identifier + writeString(view, 0, 'RIFF'); + // RIFF chunk length + view.setUint32(4, 36 + samples.length * 2, true); + // RIFF type + writeString(view, 8, 'WAVE'); + // format chunk identifier + writeString(view, 12, 'fmt '); + // format chunk length + view.setUint32(16, 16, true); + // sample format (raw) + view.setUint16(20, 1, true); + // channel count + view.setUint16(22, numChannels, true); + // sample rate + view.setUint32(24, sampleRate, true); + // byte rate (sample rate * block align) + view.setUint32(28, sampleRate * 4, true); + // block align (channel count * bytes per sample) + view.setUint16(32, numChannels * 2, true); + // bits per sample + view.setUint16(34, 16, true); + // data chunk identifier + writeString(view, 36, 'data'); + // data chunk length + view.setUint32(40, samples.length * 2, true); + + floatTo16BitPCM(view, 44, samples); + + return view; +} +*/ + +// Convert an AudioBuffer to a Blob using WAVE representation +const bufferToWave = (abuffer, len) => { + const numOfChan = abuffer.numberOfChannels + + const length = len * numOfChan * 2 + 44 + + const buffer = new ArrayBuffer(length) + + const view = new DataView(buffer) + + const channels = []; let i; let sample + + let offset = 0 + + let pos = 0 + + // write WAVE header + setUint32(0x46464952) // "RIFF" + setUint32(length - 8) // file length - 8 + setUint32(0x45564157) // "WAVE" + + setUint32(0x20746d66) // "fmt " chunk + setUint32(16) // length = 16 + setUint16(1) // PCM (uncompressed) + setUint16(numOfChan) + setUint32(abuffer.sampleRate) + setUint32(abuffer.sampleRate * 2 * numOfChan) // avg. bytes/sec + setUint16(numOfChan * 2) // block-align + setUint16(16) // 16-bit (hardcoded in this demo) + + setUint32(0x61746164) // "data" - chunk + setUint32(length - pos - 4) // chunk length + + // write interleaved data + for (i = 0; i < abuffer.numberOfChannels; i++) { channels.push(abuffer.getChannelData(i)) } + + while (pos < length) { + for (i = 0; i < numOfChan; i++) { // interleave channels + sample = Math.max(-1, Math.min(1, channels[i][offset])) // clamp + sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0 // scale to 16-bit signed int + view.setInt16(pos, sample, true) // write 16-bit sample + pos += 2 + } + offset++ // next source sample + } + + // create Blob + return new Blob([buffer], { type: 'audio/wav' }) + + function setUint16 (data) { + view.setUint16(pos, data, true) + pos += 2 + } + + function setUint32 (data) { + view.setUint32(pos, data, true) + pos += 4 + } +} + +const forceDownload = (blob, filename = 'output.wav') => { + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + document.body.appendChild(link) + link.style = 'display:none' + link.href = url + link.download = filename + link.click() + URL.revokeObjectURL(url) + document.body.removeChild(link) +} + +const makeDownload = (abuffer, totalSamples, filename) => { + forceDownload(bufferToWave(abuffer, totalSamples), `${filename}-${Date.now()}.wav`) +} diff --git a/poc/scheduled-sequencing/index.html b/poc/scheduled-sequencing/index.html index 9d5ceae..d64cc44 100644 --- a/poc/scheduled-sequencing/index.html +++ b/poc/scheduled-sequencing/index.html @@ -9,10 +9,12 @@ demo index ยป scheduled sequencing

Play a few notes, which were pre-scheduled and not triggered via setTimeout

- + + + diff --git a/poc/scheduled-sequencing/script.js b/poc/scheduled-sequencing/script.js index 2f50534..b66ce8b 100644 --- a/poc/scheduled-sequencing/script.js +++ b/poc/scheduled-sequencing/script.js @@ -1,4 +1,4 @@ -/* global virtualWebaudio, AudioContext, once */ +/* global virtualWebaudio, AudioContext, OfflineAudioContext, once, makeDownload */ const { VirtualAudioContext, render } = virtualWebaudio @@ -35,10 +35,20 @@ const create = () => { const a = create() -const demo = () => { - const ctx = new AudioContext() +const playDemo = () => { + render(a, new AudioContext()) +} + +const renderDemo = () => { + const ctx = new OfflineAudioContext(1, 44100 * 5, 44100) render(a, ctx) + ctx.startRendering().then(abuffer => { + makeDownload(abuffer, ctx.length, 'scheduled-playback') + }).catch(e => { + console.error(e) + }) } -document.getElementsByTagName('button')[0].addEventListener('click', once(demo)) +document.getElementById('play').addEventListener('click', once(playDemo)) +document.getElementById('render').addEventListener('click', once(renderDemo))