// META: global=window,dedicatedworker // META: script=/common/media.js // META: script=/webcodecs/utils.js var defaultInit = { timestamp: 1234, channels: 2, sampleRate: 8000, frames: 100, }; function createDefaultAudioData() { return make_audio_data( defaultInit.timestamp, defaultInit.channels, defaultInit.sampleRate, defaultInit.frames ); } test(t => { let local_data = new Float32Array(defaultInit.channels * defaultInit.frames); let audio_data_init = { timestamp: defaultInit.timestamp, data: local_data, numberOfFrames: defaultInit.frames, numberOfChannels: defaultInit.channels, sampleRate: defaultInit.sampleRate, format: 'f32-planar', } let data = new AudioData(audio_data_init); assert_equals(data.timestamp, defaultInit.timestamp, 'timestamp'); assert_equals(data.numberOfFrames, defaultInit.frames, 'frames'); assert_equals(data.numberOfChannels, defaultInit.channels, 'channels'); assert_equals(data.sampleRate, defaultInit.sampleRate, 'sampleRate'); assert_equals( data.duration, defaultInit.frames / defaultInit.sampleRate * 1_000_000, 'duration'); assert_equals(data.format, 'f32-planar', 'format'); // Create an Int16 array of the right length. let small_data = new Int16Array(defaultInit.channels * defaultInit.frames); let wrong_format_init = {...audio_data_init}; wrong_format_init.data = small_data; // Creating `f32-planar` AudioData from Int16 from should throw. assert_throws_js(TypeError, () => { let data = new AudioData(wrong_format_init); }, `AudioDataInit.data needs to be big enough`); var members = [ 'timestamp', 'data', 'numberOfFrames', 'numberOfChannels', 'sampleRate', 'format', ]; for (const member of members) { let incomplete_init = {...audio_data_init}; delete incomplete_init[member]; assert_throws_js( TypeError, () => {let data = new AudioData(incomplete_init)}, 'AudioData requires \'' + member + '\''); } let invalid_init = {...audio_data_init}; invalid_init.numberOfFrames = 0 assert_throws_js( TypeError, () => {let data = new AudioData(invalid_init)}, 'AudioData requires numberOfFrames > 0'); invalid_init = {...audio_data_init}; invalid_init.numberOfChannels = 0 assert_throws_js( TypeError, () => {let data = new AudioData(invalid_init)}, 'AudioData requires numberOfChannels > 0'); }, 'Verify AudioData constructors'); test(t => { let data = createDefaultAudioData(); let clone = data.clone(); // Verify the parameters match. assert_equals(data.timestamp, clone.timestamp, 'timestamp'); assert_equals(data.numberOfFrames, clone.numberOfFrames, 'frames'); assert_equals(data.numberOfChannels, clone.numberOfChannels, 'channels'); assert_equals(data.sampleRate, clone.sampleRate, 'sampleRate'); assert_equals(data.format, clone.format, 'format'); const data_copyDest = new Float32Array(defaultInit.frames); const clone_copyDest = new Float32Array(defaultInit.frames); // Verify the data matches. for (var channel = 0; channel < defaultInit.channels; channel++) { data.copyTo(data_copyDest, {planeIndex: channel}); clone.copyTo(clone_copyDest, {planeIndex: channel}); assert_array_equals( data_copyDest, clone_copyDest, 'Cloned data ch=' + channel); } // Verify closing the original data doesn't close the clone. data.close(); assert_equals(data.numberOfFrames, 0, 'data.buffer (closed)'); assert_not_equals(clone.numberOfFrames, 0, 'clone.buffer (not closed)'); clone.close(); assert_equals(clone.numberOfFrames, 0, 'clone.buffer (closed)'); // Verify closing a closed AudioData does not throw. data.close(); }, 'Verify closing and cloning AudioData'); test(t => { let data = make_audio_data( -10, defaultInit.channels, defaultInit.sampleRate, defaultInit.frames); assert_equals(data.timestamp, -10, 'timestamp'); data.close(); }, 'Test we can construct AudioData with a negative timestamp.'); test(t => { let audio_data_init = { timestamp: 0, data: new Float32Array([1,2,3,4,5,6,7,8]), numberOfFrames: 4, numberOfChannels: 2, sampleRate: 44100, format: 'f32', }; let audioData = new AudioData(audio_data_init); let dest = new Float32Array(8); assert_throws_js( RangeError, () => audioData.copyTo(dest, {planeIndex: 1}), 'copyTo from interleaved data with non-zero planeIndex throws'); audioData.close(); }, 'Test that copyTo throws if copying from interleaved with a non-zero planeIndex'); // Indices to pick a particular specific value in a specific sample-format const MIN = 0; // Minimum sample value, max amplitude const MAX = 1; // Maximum sample value, max amplitude const HALF = 2; // Half the maximum sample value, positive const NEGATIVE_HALF = 3; // Half the maximum sample value, negative const BIAS = 4; // Center of the range, silence const DISCRETE_STEPS = 5; // Number of different value for a type. function pow2(p) { return 2 ** p; } // Rounding operations for conversion, currently always floor (round towards // zero). let r = Math.floor.bind(this); const TEST_VALUES = { u8: [0, 255, 191, 64, 128, 256], s16: [ -pow2(15), pow2(15) - 1, r((pow2(15) - 1) / 2), r(-pow2(15) / 2), 0, pow2(16), ], s32: [ -pow2(31), pow2(31) - 1, r((pow2(31) - 1) / 2), r(-pow2(31) / 2), 0, pow2(32), ], f32: [-1.0, 1.0, 0.5, -0.5, 0, pow2(24)], }; const TEST_TEMPLATE = { channels: 2, frames: 5, // Each test is run with an element of the cartesian product of a pair of // elements of the set of type in [u8, s16, s32, f32] // For each test, this template is copied and the values replaced with the // appropriate values for this particular type. // For each test, copy this template and replace the number by the appropriate // number for this type testInput: [MIN, BIAS, MAX, MIN, HALF, NEGATIVE_HALF, BIAS, MAX, BIAS, BIAS], testVectorInterleavedResult: [ [MIN, MAX, HALF, BIAS, BIAS], [BIAS, MIN, NEGATIVE_HALF, MAX, BIAS], ], testVectorPlanarResult: [ [MIN, BIAS, MAX, MIN, HALF], [NEGATIVE_HALF, BIAS, MAX, BIAS, BIAS], ], }; function isInteger(type) { switch (type) { case "u8": case "s16": case "s32": return true; case "f32": return false; default: throw "invalid type"; } } // This is the complex part: carefully select an acceptable error value // depending on various factors: expected destination value, source type, // destination type. This is designed to be strict but reachable with simple // sample format transformation (no dithering or complex transformation). function epsilon(expectedDestValue, sourceType, destType) { // Strict comparison if not converting if (sourceType == destType) { return 0.0; } // There are three cases in which the maximum value cannot be reached, when // converting from a smaller integer sample type to a wider integer sample // type: // - u8 to s16 // - u8 to s32 // - s16 to u32 if (expectedDestValue == TEST_VALUES[destType][MAX]) { if (sourceType == "u8" && destType == "s16") { return expectedDestValue - 32511; // INT16_MAX - 2 << 7 + 1 } else if (sourceType == "u8" && destType == "s32") { return expectedDestValue - 2130706432; // INT32_MAX - (2 << 23) + 1 } else if (sourceType == "s16" && destType == "s32") { return expectedDestValue - 2147418112; // INT32_MAX - UINT16_MAX } } // Min and bias value are correctly mapped for all integer sample-types if (isInteger(sourceType) && isInteger(destType)) { if (expectedDestValue == TEST_VALUES[destType][MIN] || expectedDestValue == TEST_VALUES[destType][BIAS]) { return 0.0; } } // If converting from float32 to u8 or s16, allow choosing the rounding // direction. s32 has higher resolution than f32 in [-1.0,1.0] (24 bits of // mantissa) if (!isInteger(sourceType) && isInteger(destType) && destType != "s32") { return 1.0; } // In all other cases, expect an accuracy that depends on the source type and // the destination type. // The resolution of the source type. var sourceResolution = TEST_VALUES[sourceType][DISCRETE_STEPS]; // The resolution of the destination type. var destResolution = TEST_VALUES[destType][DISCRETE_STEPS]; // Computations should be exact if going from high resolution to low resolution. if (sourceResolution > destResolution) { return 0.0; } else { // Something that approaches the precision imbalance return destResolution / sourceResolution; } } // Fill the template above with the values for a particular type function get_type_values(type) { let cloned = structuredClone(TEST_TEMPLATE); cloned.testInput = Array.from( cloned.testInput, idx => TEST_VALUES[type][idx] ); cloned.testVectorInterleavedResult = Array.from( cloned.testVectorInterleavedResult, c => { return Array.from(c, idx => { return TEST_VALUES[type][idx]; }); } ); cloned.testVectorPlanarResult = Array.from( cloned.testVectorPlanarResult, c => { return Array.from(c, idx => { return TEST_VALUES[type][idx]; }); } ); return cloned; } function typeToArrayType(type) { switch (type) { case "u8": return Uint8Array; case "s16": return Int16Array; case "s32": return Int32Array; case "f32": return Float32Array; default: throw "Unexpected"; } } function arrayTypeToType(array) { switch (array.constructor) { case Uint8Array: return "u8"; case Int16Array: return "s16"; case Int32Array: return "s32"; case Float32Array: return "f32"; default: throw "Unexpected"; } } function check_array_equality(values, expected, sourceType, message, assert_func) { if (values.length != expected.length) { throw "Array not of the same length"; } for (var i = 0; i < values.length; i++) { var eps = epsilon(expected[i], sourceType, arrayTypeToType(values)); assert_func( Math.abs(expected[i] - values[i]) <= eps, `Got ${values[i]} but expected result ${ expected[i] } at index ${i} when converting from ${sourceType} to ${arrayTypeToType( values )}, epsilon ${eps}` ); } assert_func( true, `${values} is equal to ${expected} when converting from ${sourceType} to ${arrayTypeToType( values )}` ); } function conversionTest(sourceType, destinationType) { test(function (t) { var test = get_type_values(sourceType); var result = get_type_values(destinationType); var sourceArrayCtor = typeToArrayType(sourceType); var destArrayCtor = typeToArrayType(destinationType); let data = new AudioData({ timestamp: defaultInit.timestamp, data: new sourceArrayCtor(test.testInput), numberOfFrames: test.frames, numberOfChannels: test.channels, sampleRate: defaultInit.sampleRate, format: sourceType, }); // All conversions can be supported, but conversion of any type to f32-planar // MUST be supported. var assert_func = destinationType == "f32" ? assert_true : assert_implements_optional; let dest = new destArrayCtor(data.numberOfFrames); data.copyTo(dest, { planeIndex: 0, format: destinationType + "-planar" }); check_array_equality( dest, result.testVectorInterleavedResult[0], sourceType, "interleaved channel 0", assert_func ); data.copyTo(dest, { planeIndex: 1, format: destinationType + "-planar" }); check_array_equality( dest, result.testVectorInterleavedResult[1], sourceType, "interleaved channel 0", assert_func ); let destInterleaved = new destArrayCtor(data.numberOfFrames * data.numberOfChannels); data.copyTo(destInterleaved, { planeIndex: 0, format: destinationType }); check_array_equality( destInterleaved, result.testInput, sourceType, "copyTo from interleaved to interleaved (conversion only)", assert_implements_optional ); data = new AudioData({ timestamp: defaultInit.timestamp, data: new sourceArrayCtor(test.testInput), numberOfFrames: test.frames, numberOfChannels: test.channels, sampleRate: defaultInit.sampleRate, format: sourceType + "-planar", }); data.copyTo(dest, { planeIndex: 0, format: destinationType + "-planar" }); check_array_equality( dest, result.testVectorPlanarResult[0], sourceType, "planar channel 0", assert_func, ); data.copyTo(dest, { planeIndex: 1, format: destinationType + "-planar" }); check_array_equality( dest, result.testVectorPlanarResult[1], sourceType, "planar channel 1", assert_func ); // Planar to interleaved isn't supported }, `Test conversion of ${sourceType} to ${destinationType}`); } const TYPES = ["u8", "s16", "s32", "f32"]; TYPES.forEach(sourceType => { TYPES.forEach(destinationType => { conversionTest(sourceType, destinationType); }); });