diff --git a/audio-worklet/basic/audio-worklet-node-options/index.html b/audio-worklet/basic/audio-worklet-node-options/index.html index dd23e655e..a01537376 100644 --- a/audio-worklet/basic/audio-worklet-node-options/index.html +++ b/audio-worklet/basic/audio-worklet-node-options/index.html @@ -134,7 +134,7 @@

Using Options for AudioWorkletNode

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/audio-worklet/basic/bit-crusher/index.html b/audio-worklet/basic/bit-crusher/index.html index 776cd286c..d354fd4df 100644 --- a/audio-worklet/basic/bit-crusher/index.html +++ b/audio-worklet/basic/bit-crusher/index.html @@ -118,7 +118,7 @@

BitCrusher with AudioParam

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/audio-worklet/basic/handling-errors/index.html b/audio-worklet/basic/handling-errors/index.html index 42e68fd9e..1a12c46cf 100644 --- a/audio-worklet/basic/handling-errors/index.html +++ b/audio-worklet/basic/handling-errors/index.html @@ -118,7 +118,7 @@

Handling Errors

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/audio-worklet/basic/hello-audio-worklet/index.html b/audio-worklet/basic/hello-audio-worklet/index.html index 4d5c13d36..c10fcb547 100644 --- a/audio-worklet/basic/hello-audio-worklet/index.html +++ b/audio-worklet/basic/hello-audio-worklet/index.html @@ -117,7 +117,7 @@

Hello Audio Worklet!

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/audio-worklet/basic/message-port/index.html b/audio-worklet/basic/message-port/index.html index 9a707dd33..5e3bef6d9 100644 --- a/audio-worklet/basic/message-port/index.html +++ b/audio-worklet/basic/message-port/index.html @@ -117,7 +117,7 @@

MessagePort

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/audio-worklet/basic/noise-generator/index.html b/audio-worklet/basic/noise-generator/index.html index b862925a4..ea8126e0c 100644 --- a/audio-worklet/basic/noise-generator/index.html +++ b/audio-worklet/basic/noise-generator/index.html @@ -118,7 +118,7 @@

Noise generator with modulation

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/audio-worklet/basic/one-pole-filter/index.html b/audio-worklet/basic/one-pole-filter/index.html index d27ff665b..564863fdc 100644 --- a/audio-worklet/basic/one-pole-filter/index.html +++ b/audio-worklet/basic/one-pole-filter/index.html @@ -118,7 +118,7 @@

One Pole Filter

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/audio-worklet/basic/volume-meter/index.html b/audio-worklet/basic/volume-meter/index.html index 92aa5ac45..1ec92e908 100644 --- a/audio-worklet/basic/volume-meter/index.html +++ b/audio-worklet/basic/volume-meter/index.html @@ -120,7 +120,7 @@

Volume Meter

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/audio-worklet/design-pattern/shared-buffer/index.html b/audio-worklet/design-pattern/shared-buffer/index.html index 88df1e1dd..fa44c7d87 100644 --- a/audio-worklet/design-pattern/shared-buffer/index.html +++ b/audio-worklet/design-pattern/shared-buffer/index.html @@ -122,7 +122,7 @@

AudioWorklet, SharedArrayBuffer, and Worker

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/audio-worklet/design-pattern/wasm-ring-buffer/index.html b/audio-worklet/design-pattern/wasm-ring-buffer/index.html index 25c4d6dd5..cb2cd3506 100644 --- a/audio-worklet/design-pattern/wasm-ring-buffer/index.html +++ b/audio-worklet/design-pattern/wasm-ring-buffer/index.html @@ -119,7 +119,7 @@

Ring Buffer in AudioWorkletProcessor

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/audio-worklet/design-pattern/wasm-supersaw/index.html b/audio-worklet/design-pattern/wasm-supersaw/index.html index 9cf774474..432e44934 100644 --- a/audio-worklet/design-pattern/wasm-supersaw/index.html +++ b/audio-worklet/design-pattern/wasm-supersaw/index.html @@ -130,7 +130,7 @@

WebAssembly Supersaw

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/audio-worklet/design-pattern/wasm/index.html b/audio-worklet/design-pattern/wasm/index.html index 78b1033e1..9f29b3f15 100644 --- a/audio-worklet/design-pattern/wasm/index.html +++ b/audio-worklet/design-pattern/wasm/index.html @@ -119,7 +119,7 @@

Audio Worklet and WebAssembly

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/audio-worklet/free-queue/examples/simple-passthrough/index.html b/audio-worklet/free-queue/examples/simple-passthrough/index.html index f4785fd11..c04b2962c 100644 --- a/audio-worklet/free-queue/examples/simple-passthrough/index.html +++ b/audio-worklet/free-queue/examples/simple-passthrough/index.html @@ -122,7 +122,7 @@

Simple Passthrough Example

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/audio-worklet/index.html b/audio-worklet/index.html index 10fd3311d..5860face7 100644 --- a/audio-worklet/index.html +++ b/audio-worklet/index.html @@ -270,7 +270,7 @@

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/audio-worklet/migration/spn-recorder/index.html b/audio-worklet/migration/spn-recorder/index.html index 0beead084..29164fa26 100644 --- a/audio-worklet/migration/spn-recorder/index.html +++ b/audio-worklet/migration/spn-recorder/index.html @@ -158,7 +158,7 @@

ScriptProcessorNode Audio Recorder

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/audio-worklet/migration/worklet-recorder/index.html b/audio-worklet/migration/worklet-recorder/index.html index 63d92500f..ac28a9bca 100644 --- a/audio-worklet/migration/worklet-recorder/index.html +++ b/audio-worklet/migration/worklet-recorder/index.html @@ -158,7 +158,7 @@

AudioWorklet Recorder

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/demos/mld-drum-sampler/index.html b/demos/mld-drum-sampler/index.html index 00410ef01..8f6a13395 100644 --- a/demos/mld-drum-sampler/index.html +++ b/demos/mld-drum-sampler/index.html @@ -116,7 +116,7 @@

Web Audio API 드럼머신 만들기

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/experiments/index.html b/experiments/index.html index 7247c7794..f07794351 100644 --- a/experiments/index.html +++ b/experiments/index.html @@ -113,7 +113,7 @@

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/experiments/webgpuaudio/index.html b/experiments/webgpuaudio/index.html index 9f8ab8b9c..d2b92b7e7 100644 --- a/experiments/webgpuaudio/index.html +++ b/experiments/webgpuaudio/index.html @@ -115,7 +115,7 @@

WebGPUAudio Experiment

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/index.html b/index.html index f48ca4b86..21f55d704 100644 --- a/index.html +++ b/index.html @@ -258,7 +258,7 @@

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/tests/index.html b/tests/index.html index 4fbe05278..8c087d2c2 100644 --- a/tests/index.html +++ b/tests/index.html @@ -121,7 +121,7 @@

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/tests/pannernode/index.html b/tests/pannernode/index.html index 995b77e2c..338696d70 100644 --- a/tests/pannernode/index.html +++ b/tests/pannernode/index.html @@ -131,7 +131,7 @@

Glitches in PannerNode

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/tests/playwright/pages/realtime-sine.html b/tests/playwright/pages/realtime-sine.html index da28beec7..ea65c1b3f 100644 --- a/tests/playwright/pages/realtime-sine.html +++ b/tests/playwright/pages/realtime-sine.html @@ -1,32 +1,8 @@ - - - Hello Sine Test + + - -

Realtime Sine Test

-

Play 440Hz sine wave for 1 second, 880Hz for 1 second, then stop

- - - - \ No newline at end of file + + diff --git a/tests/playwright/pages/realtime-sine/index.html b/tests/playwright/pages/realtime-sine/index.html index da28beec7..ea65c1b3f 100644 --- a/tests/playwright/pages/realtime-sine/index.html +++ b/tests/playwright/pages/realtime-sine/index.html @@ -1,32 +1,8 @@ - - - Hello Sine Test + + - -

Realtime Sine Test

-

Play 440Hz sine wave for 1 second, 880Hz for 1 second, then stop

- - - - \ No newline at end of file + + diff --git a/tests/playwright/pages/src/audioBufferToWav.js b/tests/playwright/pages/src/audioBufferToWav.js new file mode 100644 index 000000000..ca45c2b87 --- /dev/null +++ b/tests/playwright/pages/src/audioBufferToWav.js @@ -0,0 +1,162 @@ +// REF: https://github.com/hoch/canopy/blob/master/docs/js/canopy-exporter.js + +/** + * Writes a string to an array starting at a specified offset. + * + * @param {string} aString - The string to write to the array. + * @param {Uint8Array} targetArray - The array to write to. + * @param {number} offset - The offset in the array to start writing at. + */ +const _writeStringToArray = (aString, targetArray, offset) => { + for (let i = 0; i < aString.length; ++i) { + targetArray[offset + i] = aString.charCodeAt(i); + } +}; + +/** + * Writes a 16-bit integer to an array at the specified offset. + * + * @param {number} aNumber - The 16-bit integer to be written. + * @param {Uint8Array} targetArray - The array to write the integer to. + * @param {number} offset - The offset at which to write the integer in the + * array. + */ +const _writeInt16ToArray = (aNumber, targetArray, offset) => { + aNumber = Math.floor(aNumber); + targetArray[offset] = aNumber & 255; // byte 1 + targetArray[offset + 1] = (aNumber >> 8) & 255; // byte 2 +}; + +/** + * Writes a 32-bit integer to a target array at the specified offset. + * + * @param {number} aNumber - The number to be written. + * @param {Uint8Array} targetArray - The array to write the number to. + * @param {number} offset - The offset at which to start writing. + */ +const _writeInt32ToArray = (aNumber, targetArray, offset) => { + aNumber = Math.floor(aNumber); + targetArray[offset] = aNumber & 255; // byte 1 + targetArray[offset + 1] = (aNumber >> 8) & 255; // byte 2 + targetArray[offset + 2] = (aNumber >> 16) & 255; // byte 3 + targetArray[offset + 3] = (aNumber >> 24) & 255; // byte 4 +}; + +// Return the bits of the float as a 32-bit integer value. This +// produces the raw bits; no intepretation of the value is done. +const _floatBits = (f) => { + const buf = new ArrayBuffer(4); + (new Float32Array(buf))[0] = f; + const bits = (new Uint32Array(buf))[0]; + // Return as a signed integer. + return bits | 0; +}; + +/** + * Converts an audio buffer to an array with the specified bit depth. + * + * @param {AudioBuffer} audioBuffer - The audio buffer to convert. + * @param {Uint8Array} targetArray - The array to store the converted samples. + * @param {number} offset - The offset in the targetArray to start writing the + * converted samples. + * @param {number} bitDepth - The desired bit depth of the converted samples + * (16 or 32). + */ +const _writeAudioBufferToArray = + (audioBuffer, targetArray, offset, bitDepth) => { + let index; let channel = 0; + const length = audioBuffer.length; + const channels = audioBuffer.numberOfChannels; + let channelData; let sample; + + // Clamping samples onto the 16-bit resolution. + for (index = 0; index < length; ++index) { + for (channel = 0; channel < channels; ++channel) { + channelData = audioBuffer.getChannelData(channel); + + // Branches upon the requested bit depth + if (bitDepth === 16) { + sample = channelData[index] * 32768.0; + if (sample < -32768) { + sample = -32768; + } else if (sample > 32767) { + sample = 32767; + } + _writeInt16ToArray(sample, targetArray, offset); + offset += 2; + } else if (bitDepth === 32) { + // This assumes we're going to out 32-float, not 32-bit linear. + sample = _floatBits(channelData[index]); + _writeInt32ToArray(sample, targetArray, offset); + offset += 4; + } else { + console.error('Invalid bit depth for PCM encoding.'); + return; + } + } + } + }; + +/** + * Converts an AudioBuffer object into a WAV file in the form of a binary blob. + * The resulting WAV file can be used for audio playback or further processing. + * The function takes two parameters: audioBuffer which represents the audio + * data, and as32BitFloat which indicates whether the WAV file should be encoded + * as 32-bit float or 16-bit integer PCM. The unction performs various + * calculations and writes the necessary headers and data to create the WAV + * file. Finally, it returns the WAV file as a Blob object with the MIME type + * audio/wave. + * + * @param {AudioBuffer} audioBuffer + * @param {Boolean} as32BitFloat + * @return {Blob} Resulting binary blob. + */ +const audioBufferToWav = (audioBuffer, as32BitFloat) => { + // Encoding setup. + const frameLength = audioBuffer.length; + const numberOfChannels = audioBuffer.numberOfChannels; + const sampleRate = audioBuffer.sampleRate; + const bitsPerSample = as32BitFloat ? 32 : 16; + const bytesPerSample = bitsPerSample / 8; + const byteRate = sampleRate * numberOfChannels * bitsPerSample / 8; + const blockAlign = numberOfChannels * bitsPerSample / 8; + const wavDataByteLength = frameLength * numberOfChannels * bytesPerSample; + const headerByteLength = 44; + const totalLength = headerByteLength + wavDataByteLength; + const waveFileData = new Uint8Array(totalLength); + const subChunk1Size = 16; + const subChunk2Size = wavDataByteLength; + const chunkSize = 4 + (8 + subChunk1Size) + (8 + subChunk2Size); + + _writeStringToArray('RIFF', waveFileData, 0); + _writeInt32ToArray(chunkSize, waveFileData, 4); + _writeStringToArray('WAVE', waveFileData, 8); + _writeStringToArray('fmt ', waveFileData, 12); + + // SubChunk1Size (4) + _writeInt32ToArray(subChunk1Size, waveFileData, 16); + // AudioFormat (2): 3 means 32-bit float, 1 means integer PCM. + _writeInt16ToArray(as32BitFloat ? 3 : 1, waveFileData, 20); + // NumChannels (2) + _writeInt16ToArray(numberOfChannels, waveFileData, 22); + // SampleRate (4) + _writeInt32ToArray(sampleRate, waveFileData, 24); + // ByteRate (4) + _writeInt32ToArray(byteRate, waveFileData, 28); + // BlockAlign (2) + _writeInt16ToArray(blockAlign, waveFileData, 32); + // BitsPerSample (4) + _writeInt32ToArray(bitsPerSample, waveFileData, 34); + _writeStringToArray('data', waveFileData, 36); + // SubChunk2Size (4) + _writeInt32ToArray(subChunk2Size, waveFileData, 40); + + // Write actual audio data starting at offset 44. + _writeAudioBufferToArray(audioBuffer, waveFileData, 44, bitsPerSample); + + return new Blob([waveFileData], { + type: 'audio/wav', + }); +}; + +export default audioBufferToWav; diff --git a/tests/playwright/pages/src/concat.js b/tests/playwright/pages/src/concat.js new file mode 100644 index 000000000..7be0fcc18 --- /dev/null +++ b/tests/playwright/pages/src/concat.js @@ -0,0 +1,16 @@ +export default (...arrays) => { + // Calculate the total length of the new Float32Array + const totalLength = arrays.reduce((acc, curr) => acc + curr.length, 0); + + // Create a new Float32Array with the total length + const result = new Float32Array(totalLength); + + // Copy elements from each input array into the new array + let offset = 0; + arrays.forEach((array) => { + result.set(array, offset); + offset += array.length; + }); + + return result; +}; diff --git a/tests/playwright/pages/src/dom.js b/tests/playwright/pages/src/dom.js new file mode 100644 index 000000000..5814fddea --- /dev/null +++ b/tests/playwright/pages/src/dom.js @@ -0,0 +1,10 @@ +export default { + id: { + helloSine: { + btn: document.getElementById('hello-sine:btn'), + res: document.getElementById('hello-sine:res'), + time: document.getElementById('hello-sine:time'), + output: document.getElementById('hello-sine:output'), + }, + }, +}; diff --git a/tests/playwright/pages/src/download.js b/tests/playwright/pages/src/download.js new file mode 100644 index 000000000..c4852b226 --- /dev/null +++ b/tests/playwright/pages/src/download.js @@ -0,0 +1,6 @@ +export default (blob) => { + const download = document.createElement('a'); + download.href = URL.createObjectURL(blob); + download.download = 'out.wav'; + download.click(); +}; diff --git a/tests/playwright/pages/src/realtime-sine.js b/tests/playwright/pages/src/realtime-sine.js new file mode 100644 index 000000000..320e9028c --- /dev/null +++ b/tests/playwright/pages/src/realtime-sine.js @@ -0,0 +1,27 @@ +import audioBufferToWav from './audioBufferToWav.js'; +import record from './recorder/main.js'; + +// eslint-disable-next-line no-async-promise-executor +window.updateFrequencyPromise = new Promise(async (resolve) => { + const ctx = new AudioContext(); + const helloSine = new OscillatorNode(ctx); + const {recorder, buffer} = await record(ctx, helloSine); + + helloSine.connect(recorder).connect(ctx.destination); + + const start = performance.now(); + + helloSine.start(); + helloSine.stop(ctx.currentTime + 1); + + const latency = await new Promise((resolve) => + helloSine.onended = () => resolve(ctx.baseLatency)); + + const end = performance.now(); + + const blob = audioBufferToWav(await buffer, false); + + await ctx.close(); + + resolve({latency, time: end - start, blob}); +}); diff --git a/tests/playwright/pages/src/recorder/main.js b/tests/playwright/pages/src/recorder/main.js new file mode 100644 index 000000000..a6c6e382b --- /dev/null +++ b/tests/playwright/pages/src/recorder/main.js @@ -0,0 +1,38 @@ +import concat from '../concat.js'; + +export default async (ctx, scheduleNode) => { + console.assert(ctx instanceof AudioContext); + console.assert(scheduleNode instanceof AudioScheduledSourceNode); + + const mutex = new Promise((resolve) => + scheduleNode.addEventListener('ended', resolve)); + + await ctx.audioWorklet.addModule('./scripts/recorder/worker.js'); + + const recorder = new AudioWorkletNode(ctx, 'recorder'); + + const arrays = []; + recorder.port.onmessage = (e) => { + !(e.data.channel in arrays) && (arrays[e.data.channel] = []); + arrays[e.data.channel].push(e.data.data); + }; + + // eslint-disable-next-line no-async-promise-executor + const buffer = new Promise(async (resolve) => { + await mutex; + const res = []; + arrays.forEach((array, i) => res[i] = concat(...array)); + + const buf = new AudioBuffer({ + length: res[0].byteLength, + sampleRate: ctx.sampleRate, + numberOfChannels: res.length, + }); + + res.forEach((array, i) => buf.copyToChannel(array, i)); + + resolve(buf); + }); + + return {recorder, buffer}; +}; diff --git a/tests/playwright/pages/src/recorder/worker.js b/tests/playwright/pages/src/recorder/worker.js new file mode 100644 index 000000000..782f7a983 --- /dev/null +++ b/tests/playwright/pages/src/recorder/worker.js @@ -0,0 +1,20 @@ +// bypass-processor.js +class RecorderProcessor extends AudioWorkletProcessor { + constructor() { + super(); + } + + process(inputs, outputs) { + const input = inputs[0]; + const output = outputs[0]; + + for (let channel = 0; channel < input.length; channel++) { + output[channel].set(input[channel]); + this.port.postMessage({channel, data: input[channel]}); + } + + return true; + } +} + +registerProcessor('recorder', RecorderProcessor); diff --git a/tests/resampler/index.html b/tests/resampler/index.html index 2788c180b..79eb79ccc 100644 --- a/tests/resampler/index.html +++ b/tests/resampler/index.html @@ -123,7 +123,7 @@

Resampler Verificaiton

2010-2024 Chromium authors - (4920120) + (3d5ca23)
diff --git a/tests/setsinkid/index.html b/tests/setsinkid/index.html index cab296e97..870a084d3 100644 --- a/tests/setsinkid/index.html +++ b/tests/setsinkid/index.html @@ -122,7 +122,7 @@

AudioContext.setSinkId(): Manual Test

2010-2024 Chromium authors - (4920120) + (3d5ca23)