diff --git a/src/tests/playwright/pages/index.html b/src/tests/playwright/pages/index.html new file mode 100644 index 000000000..e6c03806a --- /dev/null +++ b/src/tests/playwright/pages/index.html @@ -0,0 +1,16 @@ + + + + + + Document + + + + + + \ No newline at end of file diff --git a/src/tests/playwright/pages/processors/recorder/recorder-main.js b/src/tests/playwright/pages/processors/recorder/recorder-main.js index 555553081..0270da6b0 100644 --- a/src/tests/playwright/pages/processors/recorder/recorder-main.js +++ b/src/tests/playwright/pages/processors/recorder/recorder-main.js @@ -2,7 +2,7 @@ import concat from '../../util/concat.js'; export default async (ctx, length) => { console.assert(ctx instanceof AudioContext); - console.assert(length instanceof Number); + console.assert(typeof length === 'number' && length > 0); const mutex = new Promise((resolve) => setTimeout(resolve, 1000 * length)); @@ -24,14 +24,13 @@ export default async (ctx, length) => { arrays.forEach((array, i) => res[i] = concat(...array)); const buf = new AudioBuffer({ - length: res[0].byteLength, + length: res[0].length, sampleRate: ctx.sampleRate, numberOfChannels: res.length, }); res.forEach((array, i) => buf.copyToChannel(array, i)); - - resolve(res[0]); + resolve(buf); }); return {recorder, buffer}; diff --git a/src/tests/playwright/pages/util/audioBufferToWav.js b/src/tests/playwright/pages/util/audioBufferToWav.js deleted file mode 100644 index ca45c2b87..000000000 --- a/src/tests/playwright/pages/util/audioBufferToWav.js +++ /dev/null @@ -1,162 +0,0 @@ -// 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/src/tests/playwright/pages/util/audioComparer.js b/src/tests/playwright/pages/util/audioComparer.js new file mode 100644 index 000000000..e877c449b --- /dev/null +++ b/src/tests/playwright/pages/util/audioComparer.js @@ -0,0 +1,82 @@ +import { bufferinator } from './bufferinator.js'; +import { createGraphCache } from './graph.js'; + +const SAMPLE_RATE = 48000; +const NUM_CHANNELS = 1; + +// CREATE DSP GRAPH HERE!!! +const createGraph = ctx => { + // My Graph + const osc = new OscillatorNode(ctx); + osc.type = 'sawtooth'; + const gain = new GainNode(ctx); + gain.gain.value = 2.2; + const biq = new BiquadFilterNode(ctx); + biq.type = 'bandpass'; + biq.frequency.value = 1000; + biq.Q.value = 10; + osc.connect(gain).connect(biq).connect(ctx.destination); + + // My render time + const length = 1; + + osc.start(); + osc.stop(ctx.currentTime + length); +} + +/** + * Compares two audio buffers for equality. + * @param {AudioBuffer} myBuf + * @param {AudioBuffer} refBuf + * @returns {number} returns the percentage of samples that are equal + */ +function bufferComparer(myBuf, refBuf) { + let numCorrect = 0; + const numChannels = myBuf.numberOfChannels; + for (let c = 0; c < numChannels; c++) { + const myChannel = myBuf.getChannelData(c); + const refChannel = refBuf.getChannelData(c); + console.log(myChannel.length, refChannel.length) + for (let i = 0; i < myChannel.length; i++) { + if (myChannel[i] === refChannel[i]) { + numCorrect++; + } + } + } + return numCorrect / (numChannels * myBuf.length); +} + +/** + * Takes in an dsp graph to build for realtime and offline context + length of buffer to render. + * Records output of both contexts as audio buffers. + * Compares the real time buffer and the offline buffer for equality. + * @param {(ctx: AudioContext) => void} ctxGraph dsp graph to build in context + * @param {number} length length of buffer to render in seconds + * @returns {boolean} true if buffers are equal + */ +async function evaluateGraph(ctxGraph, length = 1) { + const audioContext = new AudioContext({ sampleRate: SAMPLE_RATE }); + audioContext.suspend(); + const offlineAudioContext = new OfflineAudioContext({ + numberOfChannels: NUM_CHANNELS, + length: length * SAMPLE_RATE, + sampleRate: SAMPLE_RATE + }); + + // create ctxGraph cache + const cache = createGraphCache(); + + // create realtime and offline graphs + ctxGraph(audioContext); + ctxGraph(offlineAudioContext); + + // render graphs to audio buffer + const realtimeBuffer = await bufferinator(audioContext, length, cache) + const offlineBuffer = await bufferinator(offlineAudioContext) + + const score = bufferComparer(realtimeBuffer, offlineBuffer); + + return score; +} + +console.log(await evaluateGraph(createGraph, 1)); \ No newline at end of file diff --git a/src/tests/playwright/pages/util/bufferinator.js b/src/tests/playwright/pages/util/bufferinator.js index 83a35e921..24d8d4af7 100644 --- a/src/tests/playwright/pages/util/bufferinator.js +++ b/src/tests/playwright/pages/util/bufferinator.js @@ -1,18 +1,22 @@ -import record from "../processors/recorder/recorder-main"; +import record from "../processors/recorder/recorder-main.js"; export const bufferinator = async (ctx, length, graph) => { if (ctx instanceof AudioContext) { const {recorder, buffer} = await record(ctx, length); - recorder.connect(ctx.destination); graph.get(ctx)?.forEach(([from, to]) => { if (to instanceof AudioDestinationNode) { - from.disconnect(to); - from.connect(recorder); + from._WAS_disconnect(to); + from._WAS_connect(recorder); } }); + recorder.connect(ctx.destination); + ctx.resume(); + + // for realtime return await buffer; } - return ctx.startRendering(); + // for offline + return ctx.startRendering(); }; diff --git a/src/tests/playwright/pages/util/graph.js b/src/tests/playwright/pages/util/graph.js index 2891f8119..c892f976e 100644 --- a/src/tests/playwright/pages/util/graph.js +++ b/src/tests/playwright/pages/util/graph.js @@ -1,9 +1,17 @@ +/** + * @type {Map} + * @description A map of AudioContexts to a set of connected nodes. + * Build graph to track connections between nodes. + */ const graph = new Map(); export const createGraphCache = () => { const connect = AudioNode.prototype.connect; const disconnect = AudioNode.prototype.disconnect; + AudioNode.prototype._WAS_connect = connect; + AudioNode.prototype._WAS_disconnect = disconnect; + AudioNode.prototype.connect = function () { if (!graph.has(this.context)) { graph.set(this.context, new Set());