audio-data.any.js (14327B)
1 // META: global=window,dedicatedworker 2 // META: script=/common/media.js 3 // META: script=/webcodecs/utils.js 4 5 var defaultInit = { 6 timestamp: 1234, 7 channels: 2, 8 sampleRate: 8000, 9 frames: 100, 10 }; 11 12 function createDefaultAudioData() { 13 return make_audio_data( 14 defaultInit.timestamp, 15 defaultInit.channels, 16 defaultInit.sampleRate, 17 defaultInit.frames 18 ); 19 } 20 21 test(t => { 22 let local_data = new Float32Array(defaultInit.channels * defaultInit.frames); 23 24 let audio_data_init = { 25 timestamp: defaultInit.timestamp, 26 data: local_data, 27 numberOfFrames: defaultInit.frames, 28 numberOfChannels: defaultInit.channels, 29 sampleRate: defaultInit.sampleRate, 30 format: 'f32-planar', 31 } 32 33 let data = new AudioData(audio_data_init); 34 35 assert_equals(data.timestamp, defaultInit.timestamp, 'timestamp'); 36 assert_equals(data.numberOfFrames, defaultInit.frames, 'frames'); 37 assert_equals(data.numberOfChannels, defaultInit.channels, 'channels'); 38 assert_equals(data.sampleRate, defaultInit.sampleRate, 'sampleRate'); 39 assert_equals( 40 data.duration, defaultInit.frames / defaultInit.sampleRate * 1_000_000, 41 'duration'); 42 assert_equals(data.format, 'f32-planar', 'format'); 43 44 // Create an Int16 array of the right length. 45 let small_data = new Int16Array(defaultInit.channels * defaultInit.frames); 46 47 let wrong_format_init = {...audio_data_init}; 48 wrong_format_init.data = small_data; 49 50 // Creating `f32-planar` AudioData from Int16 from should throw. 51 assert_throws_js(TypeError, () => { 52 let data = new AudioData(wrong_format_init); 53 }, `AudioDataInit.data needs to be big enough`); 54 55 var members = [ 56 'timestamp', 57 'data', 58 'numberOfFrames', 59 'numberOfChannels', 60 'sampleRate', 61 'format', 62 ]; 63 64 for (const member of members) { 65 let incomplete_init = {...audio_data_init}; 66 delete incomplete_init[member]; 67 68 assert_throws_js( 69 TypeError, () => {let data = new AudioData(incomplete_init)}, 70 'AudioData requires \'' + member + '\''); 71 } 72 73 let invalid_init = {...audio_data_init}; 74 invalid_init.numberOfFrames = 0 75 76 assert_throws_js( 77 TypeError, () => {let data = new AudioData(invalid_init)}, 78 'AudioData requires numberOfFrames > 0'); 79 80 invalid_init = {...audio_data_init}; 81 invalid_init.numberOfChannels = 0 82 83 assert_throws_js( 84 TypeError, () => {let data = new AudioData(invalid_init)}, 85 'AudioData requires numberOfChannels > 0'); 86 87 }, 'Verify AudioData constructors'); 88 89 test(t => { 90 let data = createDefaultAudioData(); 91 data.close(); 92 assert_equals(data.sampleRate, 0); 93 assert_equals(data.numberOfFrames, 0); 94 assert_equals(data.numberOfChannels, 0); 95 assert_equals(data.format, null); 96 }, 'AudioData close'); 97 98 test(t => { 99 let data = createDefaultAudioData(); 100 101 let clone = data.clone(); 102 103 // Verify the parameters match. 104 assert_equals(data.timestamp, clone.timestamp, 'timestamp'); 105 assert_equals(data.numberOfFrames, clone.numberOfFrames, 'frames'); 106 assert_equals(data.numberOfChannels, clone.numberOfChannels, 'channels'); 107 assert_equals(data.sampleRate, clone.sampleRate, 'sampleRate'); 108 assert_equals(data.format, clone.format, 'format'); 109 110 const data_copyDest = new Float32Array(defaultInit.frames); 111 const clone_copyDest = new Float32Array(defaultInit.frames); 112 113 // Verify the data matches. 114 for (var channel = 0; channel < defaultInit.channels; channel++) { 115 data.copyTo(data_copyDest, {planeIndex: channel}); 116 clone.copyTo(clone_copyDest, {planeIndex: channel}); 117 118 assert_array_equals( 119 data_copyDest, clone_copyDest, 'Cloned data ch=' + channel); 120 } 121 122 // Verify closing the original data doesn't close the clone. 123 data.close(); 124 assert_equals(data.numberOfFrames, 0, 'data.buffer (closed)'); 125 assert_not_equals(clone.numberOfFrames, 0, 'clone.buffer (not closed)'); 126 127 clone.close(); 128 assert_equals(clone.numberOfFrames, 0, 'clone.buffer (closed)'); 129 130 // Verify closing a closed AudioData does not throw. 131 data.close(); 132 }, 'Verify closing and cloning AudioData'); 133 134 test(t => { 135 let data = make_audio_data( 136 -10, defaultInit.channels, defaultInit.sampleRate, defaultInit.frames); 137 assert_equals(data.timestamp, -10, 'timestamp'); 138 data.close(); 139 }, 'Test we can construct AudioData with a negative timestamp.'); 140 141 test(t => { 142 var data = new Float32Array([0]); 143 let audio_data_init = { 144 timestamp: 0, 145 data: data, 146 numberOfFrames: 1, 147 numberOfChannels: 1, 148 sampleRate: 44100, 149 format: 'f32', 150 }; 151 let audioData = new AudioData(audio_data_init); 152 assert_not_equals(data.length, 0, "Input data is copied when constructing an AudioData"); 153 }, 'Test input array is copied on construction'); 154 155 test(t => { 156 let audio_data_init = { 157 timestamp: 0, 158 data: new Float32Array([1,2,3,4,5,6,7,8]), 159 numberOfFrames: 4, 160 numberOfChannels: 2, 161 sampleRate: 44100, 162 format: 'f32', 163 }; 164 let audioData = new AudioData(audio_data_init); 165 let dest = new Float32Array(8); 166 assert_throws_js( 167 RangeError, () => audioData.copyTo(dest, {planeIndex: 1}), 168 'copyTo from interleaved data with non-zero planeIndex throws'); 169 audioData.close(); 170 }, 'Test that copyTo throws if copying from interleaved with a non-zero planeIndex'); 171 172 // Indices to pick a particular specific value in a specific sample-format 173 const MIN = 0; // Minimum sample value, max amplitude 174 const MAX = 1; // Maximum sample value, max amplitude 175 const HALF = 2; // Half the maximum sample value, positive 176 const NEGATIVE_HALF = 3; // Half the maximum sample value, negative 177 const BIAS = 4; // Center of the range, silence 178 const DISCRETE_STEPS = 5; // Number of different value for a type. 179 180 function pow2(p) { 181 return 2 ** p; 182 } 183 // Rounding operations for conversion, currently always floor (round towards 184 // zero). 185 let r = Math.floor.bind(this); 186 187 const TEST_VALUES = { 188 u8: [0, 255, 191, 64, 128, 256], 189 s16: [ 190 -pow2(15), 191 pow2(15) - 1, 192 r((pow2(15) - 1) / 2), 193 r(-pow2(15) / 2), 194 0, 195 pow2(16), 196 ], 197 s32: [ 198 -pow2(31), 199 pow2(31) - 1, 200 r((pow2(31) - 1) / 2), 201 r(-pow2(31) / 2), 202 0, 203 pow2(32), 204 ], 205 f32: [-1.0, 1.0, 0.5, -0.5, 0, pow2(24)], 206 }; 207 208 const TEST_TEMPLATE = { 209 channels: 2, 210 frames: 5, 211 // Each test is run with an element of the cartesian product of a pair of 212 // elements of the set of type in [u8, s16, s32, f32] 213 // For each test, this template is copied and the values replaced with the 214 // appropriate values for this particular type. 215 // For each test, copy this template and replace the number by the appropriate 216 // number for this type 217 testInput: [MIN, BIAS, MAX, MIN, HALF, NEGATIVE_HALF, BIAS, MAX, BIAS, BIAS], 218 testInterleavedResult: [MIN, NEGATIVE_HALF, BIAS, BIAS, MAX, MAX, MIN, BIAS, HALF, BIAS], 219 testVectorInterleavedResult: [ 220 [MIN, MAX, HALF, BIAS, BIAS], 221 [BIAS, MIN, NEGATIVE_HALF, MAX, BIAS], 222 ], 223 testVectorPlanarResult: [ 224 [MIN, BIAS, MAX, MIN, HALF], 225 [NEGATIVE_HALF, BIAS, MAX, BIAS, BIAS], 226 ], 227 }; 228 229 function isInteger(type) { 230 switch (type) { 231 case "u8": 232 case "s16": 233 case "s32": 234 return true; 235 case "f32": 236 return false; 237 default: 238 throw "invalid type"; 239 } 240 } 241 242 // This is the complex part: carefully select an acceptable error value 243 // depending on various factors: expected destination value, source type, 244 // destination type. This is designed to be strict but reachable with simple 245 // sample format transformation (no dithering or complex transformation). 246 function epsilon(expectedDestValue, sourceType, destType) { 247 // Strict comparison if not converting 248 if (sourceType == destType) { 249 return 0.0; 250 } 251 // There are three cases in which the maximum value cannot be reached, when 252 // converting from a smaller integer sample type to a wider integer sample 253 // type: 254 // - u8 to s16 255 // - u8 to s32 256 // - s16 to u32 257 if (expectedDestValue == TEST_VALUES[destType][MAX]) { 258 if (sourceType == "u8" && destType == "s16") { 259 return expectedDestValue - 32511; // INT16_MAX - 2 << 7 + 1 260 } else if (sourceType == "u8" && destType == "s32") { 261 return expectedDestValue - 2130706432; // INT32_MAX - (2 << 23) + 1 262 } else if (sourceType == "s16" && destType == "s32") { 263 return expectedDestValue - 2147418112; // INT32_MAX - UINT16_MAX 264 } 265 } 266 // Min and bias value are correctly mapped for all integer sample-types 267 if (isInteger(sourceType) && isInteger(destType)) { 268 if (expectedDestValue == TEST_VALUES[destType][MIN] || 269 expectedDestValue == TEST_VALUES[destType][BIAS]) { 270 return 0.0; 271 } 272 } 273 // If converting from float32 to u8 or s16, allow choosing the rounding 274 // direction. s32 has higher resolution than f32 in [-1.0,1.0] (24 bits of 275 // mantissa) 276 if (!isInteger(sourceType) && isInteger(destType) && destType != "s32") { 277 return 1.0; 278 } 279 // In all other cases, expect an accuracy that depends on the source type and 280 // the destination type. 281 // The resolution of the source type. 282 var sourceResolution = TEST_VALUES[sourceType][DISCRETE_STEPS]; 283 // The resolution of the destination type. 284 var destResolution = TEST_VALUES[destType][DISCRETE_STEPS]; 285 // Computations should be exact if going from high resolution to low resolution. 286 if (sourceResolution > destResolution) { 287 return 0.0; 288 } else { 289 // Something that approaches the precision imbalance 290 return destResolution / sourceResolution; 291 } 292 } 293 294 // Fill the template above with the values for a particular type 295 function get_type_values(type) { 296 let cloned = structuredClone(TEST_TEMPLATE); 297 cloned.testInput = Array.from( 298 cloned.testInput, 299 idx => TEST_VALUES[type][idx] 300 ); 301 cloned.testInterleavedResult = Array.from( 302 cloned.testInterleavedResult, 303 idx => TEST_VALUES[type][idx] 304 ); 305 cloned.testVectorInterleavedResult = Array.from( 306 cloned.testVectorInterleavedResult, 307 c => { 308 return Array.from(c, idx => { 309 return TEST_VALUES[type][idx]; 310 }); 311 } 312 ); 313 cloned.testVectorPlanarResult = Array.from( 314 cloned.testVectorPlanarResult, 315 c => { 316 return Array.from(c, idx => { 317 return TEST_VALUES[type][idx]; 318 }); 319 } 320 ); 321 return cloned; 322 } 323 324 function typeToArrayType(type) { 325 switch (type) { 326 case "u8": 327 return Uint8Array; 328 case "s16": 329 return Int16Array; 330 case "s32": 331 return Int32Array; 332 case "f32": 333 return Float32Array; 334 default: 335 throw "Unexpected"; 336 } 337 } 338 339 function arrayTypeToType(array) { 340 switch (array.constructor) { 341 case Uint8Array: 342 return "u8"; 343 case Int16Array: 344 return "s16"; 345 case Int32Array: 346 return "s32"; 347 case Float32Array: 348 return "f32"; 349 default: 350 throw "Unexpected"; 351 } 352 } 353 354 function check_array_equality(values, expected, sourceType, message, assert_func) { 355 if (values.length != expected.length) { 356 throw "Array not of the same length"; 357 } 358 for (var i = 0; i < values.length; i++) { 359 var eps = epsilon(expected[i], sourceType, arrayTypeToType(values)); 360 assert_func( 361 Math.abs(expected[i] - values[i]) <= eps, 362 `Got ${values[i]} but expected result ${ 363 expected[i] 364 } at index ${i} when converting from ${sourceType} to ${arrayTypeToType( 365 values 366 )}, epsilon ${eps}` 367 ); 368 } 369 assert_func( 370 true, 371 `${values} is equal to ${expected} when converting from ${sourceType} to ${arrayTypeToType( 372 values 373 )}` 374 ); 375 } 376 377 function conversionTest(sourceType, destinationType) { 378 test(function (t) { 379 var test = get_type_values(sourceType); 380 var result = get_type_values(destinationType); 381 382 var sourceArrayCtor = typeToArrayType(sourceType); 383 var destArrayCtor = typeToArrayType(destinationType); 384 385 let data = new AudioData({ 386 timestamp: defaultInit.timestamp, 387 data: new sourceArrayCtor(test.testInput), 388 numberOfFrames: test.frames, 389 numberOfChannels: test.channels, 390 sampleRate: defaultInit.sampleRate, 391 format: sourceType, 392 }); 393 394 // All conversions can be supported, but conversion of any type to f32-planar 395 // MUST be supported. 396 var assert_func = destinationType == "f32" ? assert_true : assert_implements_optional; 397 let dest = new destArrayCtor(data.numberOfFrames); 398 data.copyTo(dest, { planeIndex: 0, format: destinationType + "-planar" }); 399 check_array_equality( 400 dest, 401 result.testVectorInterleavedResult[0], 402 sourceType, 403 "interleaved channel 0", 404 assert_func 405 ); 406 data.copyTo(dest, { planeIndex: 1, format: destinationType + "-planar" }); 407 check_array_equality( 408 dest, 409 result.testVectorInterleavedResult[1], 410 sourceType, 411 "interleaved channel 0", 412 assert_func 413 ); 414 let destInterleaved = new destArrayCtor(data.numberOfFrames * data.numberOfChannels); 415 data.copyTo(destInterleaved, { planeIndex: 0, format: destinationType }); 416 check_array_equality( 417 destInterleaved, 418 result.testInput, 419 sourceType, 420 "copyTo from interleaved to interleaved (conversion only)", 421 assert_implements_optional 422 ); 423 424 data = new AudioData({ 425 timestamp: defaultInit.timestamp, 426 data: new sourceArrayCtor(test.testInput), 427 numberOfFrames: test.frames, 428 numberOfChannels: test.channels, 429 sampleRate: defaultInit.sampleRate, 430 format: sourceType + "-planar", 431 }); 432 433 data.copyTo(dest, { planeIndex: 0, format: destinationType + "-planar" }); 434 check_array_equality( 435 dest, 436 result.testVectorPlanarResult[0], 437 sourceType, 438 "planar channel 0", 439 assert_func, 440 ); 441 data.copyTo(dest, { planeIndex: 1, format: destinationType + "-planar" }); 442 check_array_equality( 443 dest, 444 result.testVectorPlanarResult[1], 445 sourceType, 446 "planar channel 1", 447 assert_func 448 ); 449 // Copy to interleaved from planar: all channels are copied 450 data.copyTo(destInterleaved, {planeIndex: 0, format: destinationType}); 451 check_array_equality( 452 destInterleaved, 453 result.testInterleavedResult, 454 sourceType, 455 "planar to interleaved", 456 assert_func 457 ); 458 }, `Test conversion of ${sourceType} to ${destinationType}`); 459 } 460 461 const TYPES = ["u8", "s16", "s32", "f32"]; 462 TYPES.forEach(sourceType => { 463 TYPES.forEach(destinationType => { 464 conversionTest(sourceType, destinationType); 465 }); 466 });