466 lines
14 KiB
JavaScript
466 lines
14 KiB
JavaScript
// 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();
|
|
data.close();
|
|
assert_equals(data.sampleRate, 0);
|
|
assert_equals(data.numberOfFrames, 0);
|
|
assert_equals(data.numberOfChannels, 0);
|
|
assert_equals(data.format, null);
|
|
}, 'AudioData close');
|
|
|
|
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 => {
|
|
var data = new Float32Array([0]);
|
|
let audio_data_init = {
|
|
timestamp: 0,
|
|
data: data,
|
|
numberOfFrames: 1,
|
|
numberOfChannels: 1,
|
|
sampleRate: 44100,
|
|
format: 'f32',
|
|
};
|
|
let audioData = new AudioData(audio_data_init);
|
|
assert_not_equals(data.length, 0, "Input data is copied when constructing an AudioData");
|
|
}, 'Test input array is copied on construction');
|
|
|
|
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],
|
|
testInterleavedResult: [MIN, NEGATIVE_HALF, BIAS, BIAS, MAX, MAX, MIN, BIAS, HALF, 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.testInterleavedResult = Array.from(
|
|
cloned.testInterleavedResult,
|
|
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
|
|
);
|
|
// Copy to interleaved from planar: all channels are copied
|
|
data.copyTo(destInterleaved, {planeIndex: 0, format: destinationType});
|
|
check_array_equality(
|
|
destInterleaved,
|
|
result.testInterleavedResult,
|
|
sourceType,
|
|
"planar to interleaved",
|
|
assert_func
|
|
);
|
|
}, `Test conversion of ${sourceType} to ${destinationType}`);
|
|
}
|
|
|
|
const TYPES = ["u8", "s16", "s32", "f32"];
|
|
TYPES.forEach(sourceType => {
|
|
TYPES.forEach(destinationType => {
|
|
conversionTest(sourceType, destinationType);
|
|
});
|
|
});
|