mediastreamaudiosourcenode-from-context-with-different-rate.https.html (15641B)
1 <!DOCTYPE html> 2 <html class="a"> 3 <head> 4 <title>Connecting to MediaStreamAudioSourceNode from nodes in different rates</title> 5 <script src="/resources/testharness.js"></script> 6 <script src="/resources/testharnessreport.js"></script> 7 </head> 8 <body class="a"> 9 <script> 10 function createSineWaveInput(rate, frequency = 440) { 11 const ctx = new AudioContext({ sampleRate: rate }); 12 const osc = ctx.createOscillator(); 13 osc.type = "sine"; 14 osc.frequency.value = frequency; 15 const dest = new MediaStreamAudioDestinationNode(ctx); 16 osc.connect(dest); 17 return { ctx, osc, dest }; 18 } 19 20 async function waitForMessage(detectorNode, eventChecker = null) { 21 assert_not_equals( 22 detectorNode.context.state, 23 "closed", 24 `state of detector at rate ${detectorNode.context.sampleRate} should not be closed` 25 ); 26 assert_equals( 27 detectorNode.port.onmessage, 28 null, 29 "port.onmessage should be null before calling waitForMessage." 30 ); 31 32 return new Promise((resolve, reject) => { 33 let appendix = []; 34 detectorNode.port.onmessage = (event) => { 35 if (eventChecker && !eventChecker(event.data)) { 36 appendix.push(event.data); 37 return; 38 } 39 // Clear the handler after receiving a message. 40 detectorNode.port.onmessage = null; 41 resolve({ data: event.data, appendix }); 42 }; 43 }); 44 } 45 46 async function createAudioSource(rate, stream, usage) { 47 const ctx = new AudioContext({ sampleRate: rate }); 48 const stm = 49 usage === TRACK_USAGES.CLONED 50 ? new MediaStream(stream.getTracks().map((track) => track.clone())) 51 : stream; 52 const sourceNode = ctx.createMediaStreamSource(stm); 53 sourceNode.connect(ctx.destination); 54 return sourceNode; 55 } 56 57 async function createDetectorNode(ctx) { 58 await ctx.audioWorklet.addModule("silence-detector.js"); 59 const detectorNode = new AudioWorkletNode(ctx, "silence-detector"); 60 return detectorNode; 61 } 62 63 // Helper functions to wait for a message, satisfying eventChecker() if provided, 64 // on each detector node, after performing an action. 65 async function waitForMessagesAfterAction( 66 detectorNodes, 67 action, 68 eventChecker = null 69 ) { 70 const msgPromises = detectorNodes.map((node) => 71 waitForMessage(node, eventChecker) 72 ); 73 await action(); 74 return await Promise.all(msgPromises); 75 } 76 77 // Helper function to create test pairs and wait for them to become non-silent. 78 async function createAndStartTestPairs(input, dstRates, usage) { 79 // Create multiple MediaStreamAudioSourceNodes with different AudioContext sample rates. 80 const pairs = []; 81 for (const rate of dstRates) { 82 const sourceNode = await createAudioSource( 83 rate, 84 input.dest.stream, 85 usage 86 ); 87 const detectorNode = await createDetectorNode(sourceNode.context); 88 sourceNode.connect(detectorNode); 89 90 pairs.push({ sourceNode, detectorNode }); 91 } 92 93 // Make sure all detectors are not silent after starting the oscillator. 94 const msgs = await waitForMessagesAfterAction( 95 pairs.map((p) => p.detectorNode), 96 () => input.osc.start(), 97 (data) => data.isSilentChanged 98 ); 99 msgs.forEach((msg, i) => { 100 assert_false( 101 msg.data.isSilent, 102 `Detector in context with rate ${pairs[i].detectorNode.context.sampleRate} should not be silent after oscillator starts.` 103 ); 104 }); 105 106 return pairs; 107 } 108 109 async function ensureAudioSourceIsNotSilent(sourceNode) { 110 const detectorNode = await createDetectorNode(sourceNode.context); 111 112 const msg = ( 113 await waitForMessagesAfterAction( 114 [detectorNode], 115 () => { 116 sourceNode.connect(detectorNode); 117 }, 118 (data) => data.isSilentChanged 119 ) 120 )[0]; 121 122 assert_false( 123 msg.data.isSilent, 124 `Audio source in context with rate ${sourceNode.context.sampleRate} should not be silent.` 125 ); 126 detectorNode.disconnect(); 127 } 128 129 // Test template that handles setup, execution, and cleanup 130 async function setupAndRunTest(tone, srcRate, dstRates, usage, testFn) { 131 assert_false( 132 dstRates.includes(srcRate), 133 "dstRates should not include srcRate." 134 ); 135 136 const input = createSineWaveInput(srcRate, tone); 137 const pairs = await createAndStartTestPairs(input, dstRates, usage); 138 139 try { 140 await testFn(input, pairs); 141 } finally { 142 // Clean up AudioContexts 143 for (const { sourceNode } of pairs) { 144 if (sourceNode.context.state !== "closed") { 145 await sourceNode.context.close(); 146 } 147 } 148 if (input.ctx.state !== "closed") { 149 await input.ctx.close(); 150 } 151 } 152 } 153 154 // Test that closing a single AudioContext stops only that sourceNode, leaving others unaffected. 155 async function testClosingOneContextStopsOnlyIt( 156 tone, 157 srcRate, 158 dstRates, 159 usage 160 ) { 161 await setupAndRunTest( 162 tone, 163 srcRate, 164 dstRates, 165 usage, 166 async (input, pairs) => { 167 const pairToClose = pairs[0]; 168 const pairsToCheck = pairs.slice(1); 169 170 for (const { sourceNode } of pairsToCheck) { 171 await ensureAudioSourceIsNotSilent(sourceNode); 172 } 173 } 174 ); 175 } 176 177 // Test that suspending a single AudioContext silences only that detector, leaving others unaffected. 178 async function testSuspendingOneContextSilencesOnlyIt( 179 tone, 180 srcRate, 181 dstRates, 182 usage 183 ) { 184 await setupAndRunTest( 185 tone, 186 srcRate, 187 dstRates, 188 usage, 189 async (input, pairs) => { 190 const pairToSuspend = pairs[0]; 191 const pairsToCheck = pairs.slice(1); 192 193 await pairToSuspend.sourceNode.context.suspend(); 194 195 for (const { sourceNode } of pairsToCheck) { 196 await ensureAudioSourceIsNotSilent(sourceNode); 197 } 198 } 199 ); 200 } 201 202 // Test that disconnecting a single source node silences only its detector, leaving others unaffected. 203 async function testDisconnectingOneSourceSilencesOnlyIt( 204 tone, 205 srcRate, 206 dstRates, 207 usage 208 ) { 209 await setupAndRunTest( 210 tone, 211 srcRate, 212 dstRates, 213 usage, 214 async (input, pairs) => { 215 const pairToDisconnect = pairs[0]; 216 const pairsToCheck = pairs.slice(1); 217 218 pairToDisconnect.sourceNode.disconnect(); 219 220 for (const { sourceNode } of pairsToCheck) { 221 await ensureAudioSourceIsNotSilent(sourceNode); 222 } 223 } 224 ); 225 } 226 227 // Test template for operations that silence one MediaStream. 228 async function testSilencingOneMediaStream( 229 tone, 230 srcRate, 231 dstRates, 232 usage, 233 stmOp, 234 silenceChecker = null 235 ) { 236 await setupAndRunTest( 237 tone, 238 srcRate, 239 dstRates, 240 usage, 241 async (input, pairs) => { 242 const pairToOperate = pairs[0]; 243 244 // Determine which pairs should become silent based on track usage 245 const pairsToBecomeSilent = 246 usage === TRACK_USAGES.CLONED ? [pairToOperate] : pairs; 247 const pairsToRemainActive = 248 usage === TRACK_USAGES.CLONED ? pairs.slice(1) : []; 249 250 // Perform the operation and wait for expected detectors to become silent 251 const msgs = await waitForMessagesAfterAction( 252 pairsToBecomeSilent.map((p) => p.detectorNode), 253 async () => { 254 await stmOp(pairToOperate.sourceNode.mediaStream); 255 } 256 ); 257 258 // Verify detectors lost their input 259 msgs.forEach((msg, i) => { 260 const detectorNode = pairsToBecomeSilent[i].detectorNode; 261 silenceChecker 262 ? silenceChecker(msg, detectorNode) 263 : assert_true( 264 !msg.data.hasInput, 265 `Detector in context with rate ${detectorNode.context.sampleRate} should have no input after operation.` 266 ); 267 }); 268 269 // Verify remaining detectors are still active 270 for (const { sourceNode } of pairsToRemainActive) { 271 await ensureAudioSourceIsNotSilent(sourceNode); 272 } 273 } 274 ); 275 } 276 277 // Test the effect of stopping MediaStreamTracks on detectors. 278 // With cloned tracks: stopping one stream affects only its corresponding detector. 279 // With shared tracks: stopping makes all detectors lose their inputs. 280 async function testStoppingOneMediaStream(tone, srcRate, dstRates, usage) { 281 await testSilencingOneMediaStream( 282 tone, 283 srcRate, 284 dstRates, 285 usage, 286 async (stream) => { 287 stream.getTracks().forEach((track) => { 288 track.stop(); 289 }); 290 } 291 ); 292 } 293 294 // Test that disabling a MediaStream's tracks affects either just its own detector or all detectors. 295 // With cloned tracks: disabling affects only its corresponding detector. 296 // With shared tracks: disabling affects all detectors. 297 async function testDisablingOneMediaStream(tone, srcRate, dstRates, usage) { 298 await testSilencingOneMediaStream( 299 tone, 300 srcRate, 301 dstRates, 302 usage, 303 async (stream) => { 304 stream.getTracks().forEach((track) => { 305 track.enabled = false; 306 }); 307 }, 308 // Disabling tracks should make detectors lose input: 309 // https://bugzilla.mozilla.org/show_bug.cgi?id=2005070 310 async (msg, detectorNode) => { 311 assert_true( 312 msg.data.isSilent, 313 `Detector in context with rate ${detectorNode.context.sampleRate} should be silent after disabling tracks.` 314 ); 315 } 316 ); 317 } 318 319 // Test that removing tracks from a single MediaStream affects only that detector, leaving others unaffected. 320 async function testRemovingTracksInOneMediaStream( 321 tone, 322 srcRate, 323 dstRates, 324 usage 325 ) { 326 await setupAndRunTest( 327 tone, 328 srcRate, 329 dstRates, 330 usage, 331 async (input, pairs) => { 332 const pairToModify = pairs[0]; 333 const pairsToCheck = pairs.slice(1); 334 335 const tracks = pairToModify.sourceNode.mediaStream.getTracks(); 336 tracks.forEach((track) => { 337 pairToModify.sourceNode.mediaStream.removeTrack(track); 338 }); 339 340 for (const { sourceNode } of pairsToCheck) { 341 await ensureAudioSourceIsNotSilent(sourceNode); 342 } 343 } 344 ); 345 } 346 347 // Test the impact of stopping the original input stream on detectors. 348 // When tracks are cloned: detectors continue to receive audio. 349 // When tracks are shared: detectors lose their input or become silent after stopping the source stream. 350 async function testStoppingInputStream(tone, srcRate, dstRates, usage) { 351 await setupAndRunTest( 352 tone, 353 srcRate, 354 dstRates, 355 usage, 356 async (input, pairs) => { 357 const stopInputStream = () => { 358 input.dest.stream.getTracks().forEach((track) => track.stop()); 359 }; 360 361 if (usage === TRACK_USAGES.CLONED) { 362 stopInputStream(); 363 for (const { sourceNode } of pairs) { 364 await ensureAudioSourceIsNotSilent(sourceNode); 365 } 366 } else { 367 const msgs = await waitForMessagesAfterAction( 368 pairs.map((p) => p.detectorNode), 369 stopInputStream 370 ); 371 msgs.forEach((msg, i) => { 372 assert_true( 373 msg.data.isSilent || !msg.data.hasInput, 374 `Detector in context with rate ${pairs[i].detectorNode.context.sampleRate} should be silent or have no input after stopping the shared input stream.` 375 ); 376 }); 377 } 378 } 379 ); 380 } 381 382 // Test that suspending the input AudioContext silences all detectors. 383 async function testSuspendingInputContextSilencesAll( 384 tone, 385 srcRate, 386 dstRates, 387 usage 388 ) { 389 await setupAndRunTest( 390 tone, 391 srcRate, 392 dstRates, 393 usage, 394 async (input, pairs) => { 395 const msgs = await waitForMessagesAfterAction( 396 pairs.map((p) => p.detectorNode), 397 async () => { 398 await input.ctx.suspend(); 399 } 400 ); 401 msgs.forEach((msg, i) => { 402 assert_true( 403 msg.data.isSilent, 404 `Detector in context with rate ${pairs[i].detectorNode.context.sampleRate} should be silent after suspending the input context.` 405 ); 406 }); 407 } 408 ); 409 } 410 411 // Test that closing the input AudioContext silences all detectors. 412 async function testClosingInputContextSilencesAll( 413 tone, 414 srcRate, 415 dstRates, 416 usage 417 ) { 418 await setupAndRunTest( 419 tone, 420 srcRate, 421 dstRates, 422 usage, 423 async (input, pairs) => { 424 const msgs = await waitForMessagesAfterAction( 425 pairs.map((p) => p.detectorNode), 426 async () => { 427 await input.ctx.close(); 428 } 429 ); 430 msgs.forEach((msg, i) => { 431 assert_true( 432 msg.data.isSilent, 433 `Detector in context with rate ${pairs[i].detectorNode.context.sampleRate} should be silent after closing the input context.` 434 ); 435 }); 436 } 437 ); 438 } 439 440 const SOURCE_CONTEXT_RATE = 48000; 441 const DEST_CONTEXT_RATES = [32000, 44100, 96000]; 442 443 const TRACK_USAGES = { 444 CLONED: "cloned", 445 SHARED: "shared", 446 }; 447 const SCENARIOS = [ 448 { trackUsage: TRACK_USAGES.CLONED, description: "cloned tracks" }, 449 { trackUsage: TRACK_USAGES.SHARED, description: "shared tracks" }, 450 ]; 451 452 const NOTE_FREQUENCIES = { 453 C4: 261.63, 454 D4: 293.66, 455 E4: 329.63, 456 F4: 349.23, 457 G4: 392.00, 458 A4: 440.00, 459 B4: 493.88, 460 C5: 523.25, 461 D5: 587.33, 462 E5: 659.25, 463 }; 464 465 for (const { trackUsage, description } of SCENARIOS) { 466 // Tests for connection from one source context to multiple destination contexts with different sample rates. 467 468 promise_test(async t => { 469 await testClosingOneContextStopsOnlyIt(NOTE_FREQUENCIES.C4, SOURCE_CONTEXT_RATE, DEST_CONTEXT_RATES, trackUsage); 470 }, `Test closing one AudioContext stops only it (${description})`); 471 472 promise_test(async t => { 473 await testSuspendingOneContextSilencesOnlyIt(NOTE_FREQUENCIES.D4, SOURCE_CONTEXT_RATE, DEST_CONTEXT_RATES, trackUsage); 474 }, `Test suspending one AudioContext silences only it (${description})`); 475 476 promise_test(async t => { 477 await testDisconnectingOneSourceSilencesOnlyIt(NOTE_FREQUENCIES.E4, SOURCE_CONTEXT_RATE, DEST_CONTEXT_RATES, trackUsage); 478 }, `Test disconnecting one MediaStreamAudioSourceNode silences only it (${description})`); 479 480 promise_test(async t => { 481 await testStoppingOneMediaStream(NOTE_FREQUENCIES.F4, SOURCE_CONTEXT_RATE, DEST_CONTEXT_RATES, trackUsage); 482 }, `Test stopping one MediaStream's tracks silences ${trackUsage === TRACK_USAGES.CLONED ? "only its detector" : "all detectors"} (${description})`); 483 484 promise_test(async t => { 485 await testDisablingOneMediaStream(NOTE_FREQUENCIES.G4, SOURCE_CONTEXT_RATE, DEST_CONTEXT_RATES, trackUsage); 486 }, `Test disabling one MediaStream's tracks silences ${trackUsage === TRACK_USAGES.CLONED ? "only its detector" : "all detectors"} (${description})`); 487 488 promise_test(async t => { 489 await testRemovingTracksInOneMediaStream(NOTE_FREQUENCIES.A4, SOURCE_CONTEXT_RATE, DEST_CONTEXT_RATES, trackUsage); 490 }, `Test removing tracks from one MediaStream silences only it} (${description})`); 491 492 promise_test(async t => { 493 await testStoppingInputStream(NOTE_FREQUENCIES.B4, SOURCE_CONTEXT_RATE, DEST_CONTEXT_RATES, trackUsage); 494 }, `Test stopping the input MediaStream's tracks silences ${trackUsage === TRACK_USAGES.CLONED ? "nothing" : "all detectors"} (${description})`); 495 496 promise_test(async t => { 497 await testSuspendingInputContextSilencesAll(NOTE_FREQUENCIES.C5, SOURCE_CONTEXT_RATE, DEST_CONTEXT_RATES, trackUsage); 498 }, `Test suspending the input AudioContext silences all detectors (${description})`); 499 500 promise_test(async t => { 501 await testClosingInputContextSilencesAll(NOTE_FREQUENCIES.D5, SOURCE_CONTEXT_RATE, DEST_CONTEXT_RATES, trackUsage); 502 }, `Test closing the input AudioContext silences all detectors (${description})`); 503 504 // Tests for one source context to multiple destination contexts with identical sample rates. 505 506 const dstRates = [DEST_CONTEXT_RATES[0], DEST_CONTEXT_RATES[0]]; 507 promise_test(async t => { 508 await testRemovingTracksInOneMediaStream(NOTE_FREQUENCIES.E5, SOURCE_CONTEXT_RATE, dstRates, trackUsage); 509 }, `Test removing tracks from one MediaStream silences only its detector when destination rates are the same (${description}, ${SOURCE_CONTEXT_RATE}->${dstRates[0]})`); 510 } 511 512 </script> 513 </body> 514 </html>