test_pannerNodeTail.html (7592B)
1 <!DOCTYPE HTML> 2 <html> 3 <head> 4 <title>Test tail time lifetime of PannerNode</title> 5 <script src="/tests/SimpleTest/SimpleTest.js"></script> 6 <script type="text/javascript" src="webaudio.js"></script> 7 <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> 8 </head> 9 <body> 10 <pre id="test"> 11 <script class="testbody" type="text/javascript"> 12 13 // This tests that a PannerNode does not release its reference before 14 // it finishes emitting sound. 15 // 16 // The PannerNode tail time is short, so, when a PannerNode is destroyed on 17 // the main thread, it is unlikely to notify the graph thread before the tail 18 // time expires. However, by adding DelayNodes downstream from the 19 // PannerNodes, the graph thread can have enough time to notice that a 20 // DelayNode has been destroyed. 21 // 22 // In the current implementation, DelayNodes will take a tail-time reference 23 // immediately when they receive the first block of sound from an upstream 24 // node, so this test connects the downstream DelayNodes while the upstream 25 // nodes are finishing, and then runs GC (on the main thread) before the 26 // DelayNodes receive any input (on the graph thread). 27 // 28 // Web Audio doesn't provide a means to precisely time connect()s but we can 29 // test that the output of delay nodes matches the output from a reference 30 // PannerNode that we know will not be GCed. 31 // 32 // Another set of delay nodes is added upstream to ensure that the source node 33 // has removed its self-reference after dispatching its "ended" event. 34 35 SimpleTest.waitForExplicitFinish(); 36 37 const blockSize = 128; 38 // bufferSize should be long enough that to allow an audioprocess event to be 39 // sent to the main thread and a connect message to return to the graph 40 // thread. 41 const bufferSize = 4096; 42 const pannerCount = bufferSize / blockSize; 43 // sourceDelayBufferCount should be long enough to allow the source node 44 // onended to finish and remove the source self-reference. 45 const sourceDelayBufferCount = 3; 46 var gotEnded = false; 47 // ccDelayLength should be long enough to allow CC to run 48 var ccDelayBufferCount = 20; 49 const ccDelayLength = ccDelayBufferCount * bufferSize; 50 51 var ctx = new AudioContext(); 52 var testPanners = []; 53 var referencePanner = new PannerNode(ctx, {panningModel: "HRTF"}); 54 var referenceProcessCount = 0; 55 var referenceOutput = [new Float32Array(bufferSize), 56 new Float32Array(bufferSize)]; 57 var testProcessor; 58 var testProcessCount = 0; 59 60 function onReferenceOutput(e) { 61 switch(referenceProcessCount) { 62 63 case sourceDelayBufferCount - 1: 64 // The panners are about to finish. 65 if (!gotEnded) { 66 todo(false, "Source hasn't ended. Increase sourceDelayBufferCount?"); 67 } 68 69 // Connect each PannerNode output to a downstream DelayNode, 70 // and connect ScriptProcessors to compare test and reference panners. 71 var delayDuration = ccDelayLength / ctx.sampleRate; 72 for (var i = 0; i < pannerCount; ++i) { 73 var delay = ctx.createDelay(delayDuration); 74 delay.delayTime.value = delayDuration; 75 delay.connect(testProcessor); 76 testPanners[i].connect(delay); 77 } 78 testProcessor = null; 79 testPanners = null; 80 81 // The panning effect is linear so only one reference panner is required. 82 // This also checks that the individual panners don't chop their output 83 // too soon. 84 referencePanner.connect(e.target); 85 86 // Assuming the above operations have already scheduled an event to run in 87 // stable state and ask the graph thread to make connections, schedule a 88 // subsequent event to run cycle collection, which should not collect 89 // panners that are still producing sound. 90 SimpleTest.executeSoon(function() { 91 SpecialPowers.forceGC(); 92 SpecialPowers.forceCC(); 93 }); 94 95 break; 96 97 case sourceDelayBufferCount: 98 // Record this buffer during which PannerNode outputs were connected. 99 for (var i = 0; i < 2; ++i) { 100 e.inputBuffer.copyFromChannel(referenceOutput[i], i); 101 } 102 e.target.onaudioprocess = null; 103 e.target.disconnect(); 104 105 // If the buffer is silent, there is probably not much point just 106 // increasing the buffer size, because, with the buffer size already 107 // significantly larger than panner tail time, it demonstrates that the 108 // lag between threads is much greater than the tail time. 109 if (isChannelSilent(referenceOutput[0])) { 110 todo(false, "Connections not detected."); 111 } 112 } 113 114 referenceProcessCount++; 115 } 116 117 function onTestOutput(e) { 118 if (testProcessCount < sourceDelayBufferCount + ccDelayBufferCount) { 119 testProcessCount++; 120 return; 121 } 122 123 for (var i = 0; i < 2; ++i) { 124 compareChannels(e.inputBuffer.getChannelData(i), referenceOutput[i]); 125 } 126 e.target.onaudioprocess = null; 127 e.target.disconnect(); 128 SimpleTest.finish(); 129 } 130 131 function startTest() { 132 // 0.002 is MaxDelayTimeSeconds in HRTFpanner.cpp 133 // and 512 is fftSize() at 48 kHz. 134 const expectedPannerTailTime = 0.002 * ctx.sampleRate + 512; 135 136 // Place the listener to the side of the origin, where the panners are 137 // positioned, to maximize delay in one ear. 138 ctx.listener.setPosition(1,0,0); 139 140 // Create some PannerNodes downstream from DelayNodes with delays long 141 // enough for their source to finish, dispatch its "ended" event 142 // and release its playing reference. The DelayNodes should expire their 143 // tail-time references before the PannerNodes and so only the PannerNode 144 // lifetimes depends on their tail-time references. Many DelayNodes are 145 // created and timed to finish at different times so that one PannerNode 146 // will be finishing the block processed immediately after the connect is 147 // received. 148 var source = ctx.createBufferSource(); 149 // Just short of blockSize here to avoid rounding into the next block 150 var buffer = ctx.createBuffer(1, blockSize - 1, ctx.sampleRate); 151 for (var i = 0; i < buffer.length; ++i) { 152 buffer.getChannelData(0)[i] = Math.cos(Math.PI * i / buffer.length); 153 } 154 source.buffer = buffer; 155 source.start(0); 156 source.onended = function() { 157 gotEnded = true; 158 }; 159 160 // Time the first test panner to finish just before downstream DelayNodes 161 // are about the be connected. Note that DelayNode lifetime depends on 162 // maxDelayTime so set that equal to the delay. 163 var delayDuration = 164 (sourceDelayBufferCount * bufferSize 165 - expectedPannerTailTime - 2 * blockSize) / ctx.sampleRate; 166 167 for (var i = 0; i < pannerCount; ++i) { 168 var delay = ctx.createDelay(delayDuration); 169 delay.delayTime.value = delayDuration; 170 source.connect(delay); 171 delay.connect(referencePanner) 172 173 var panner = ctx.createPanner(); 174 panner.panningModel = "HRTF"; 175 delay.connect(panner); 176 testPanners[i] = panner; 177 178 delayDuration += blockSize / ctx.sampleRate; 179 } 180 181 // Create a ScriptProcessor now to use as a timer to trigger connection of 182 // downstream nodes. It will also be used to record reference output. 183 var referenceProcessor = ctx.createScriptProcessor(bufferSize, 2, 0); 184 referenceProcessor.onaudioprocess = onReferenceOutput; 185 // Start audioprocess events before source delays are connected. 186 referenceProcessor.connect(ctx.destination); 187 188 // The test ScriptProcessor will record output of testPanners. 189 // Create it now so that it is synchronized with the referenceProcessor. 190 testProcessor = ctx.createScriptProcessor(bufferSize, 2, 0); 191 testProcessor.onaudioprocess = onTestOutput; 192 // Start audioprocess events before source delays are connected. 193 testProcessor.connect(ctx.destination); 194 } 195 196 promiseHRTFReady(ctx.sampleRate).then(startTest); 197 </script> 198 </pre> 199 </body> 200 </html>