|
1 <!DOCTYPE HTML> |
|
2 <html> |
|
3 <head> |
|
4 <title>Test tail time lifetime of PannerNode</title> |
|
5 <script type="text/javascript" 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; |
|
52 var testPanners = []; |
|
53 var referencePanner; |
|
54 var referenceProcessCount = 0; |
|
55 var referenceOutput = [new Float32Array(bufferSize), |
|
56 new Float32Array(bufferSize)]; |
|
57 var testProcessor; |
|
58 var testProcessCount = 0; |
|
59 |
|
60 function isChannelSilent(channel) { |
|
61 for (var i = 0; i < channel.length; ++i) { |
|
62 if (channel[i] != 0.0) { |
|
63 return false; |
|
64 } |
|
65 } |
|
66 return true; |
|
67 } |
|
68 |
|
69 function onReferenceOutput(e) { |
|
70 switch(referenceProcessCount) { |
|
71 |
|
72 case sourceDelayBufferCount - 1: |
|
73 // The panners are about to finish. |
|
74 if (!gotEnded) { |
|
75 todo(false, "Source hasn't ended. Increase sourceDelayBufferCount?"); |
|
76 } |
|
77 |
|
78 // Connect each PannerNode output to a downstream DelayNode, |
|
79 // and connect ScriptProcessors to compare test and reference panners. |
|
80 var delayDuration = ccDelayLength / ctx.sampleRate; |
|
81 for (var i = 0; i < pannerCount; ++i) { |
|
82 var delay = ctx.createDelay(delayDuration); |
|
83 delay.delayTime.value = delayDuration; |
|
84 delay.connect(testProcessor); |
|
85 testPanners[i].connect(delay); |
|
86 } |
|
87 testProcessor = null; |
|
88 testPanners = null; |
|
89 |
|
90 // The panning effect is linear so only one reference panner is required. |
|
91 // This also checks that the individual panners don't chop their output |
|
92 // too soon. |
|
93 referencePanner.connect(e.target); |
|
94 |
|
95 // Assuming the above operations have already scheduled an event to run in |
|
96 // stable state and ask the graph thread to make connections, schedule a |
|
97 // subsequent event to run cycle collection, which should not collect |
|
98 // panners that are still producing sound. |
|
99 SimpleTest.executeSoon(function() { |
|
100 SpecialPowers.forceGC(); |
|
101 SpecialPowers.forceCC(); |
|
102 }); |
|
103 |
|
104 break; |
|
105 |
|
106 case sourceDelayBufferCount: |
|
107 // Record this buffer during which PannerNode outputs were connected. |
|
108 for (var i = 0; i < 2; ++i) { |
|
109 e.inputBuffer.copyFromChannel(referenceOutput[i], i); |
|
110 } |
|
111 e.target.onaudioprocess = null; |
|
112 e.target.disconnect(); |
|
113 |
|
114 // If the buffer is silent, there is probably not much point just |
|
115 // increasing the buffer size, because, with the buffer size already |
|
116 // significantly larger than panner tail time, it demonstrates that the |
|
117 // lag between threads is much greater than the tail time. |
|
118 if (isChannelSilent(referenceOutput[0])) { |
|
119 todo(false, "Connections not detected."); |
|
120 } |
|
121 } |
|
122 |
|
123 referenceProcessCount++; |
|
124 } |
|
125 |
|
126 function onTestOutput(e) { |
|
127 if (testProcessCount < sourceDelayBufferCount + ccDelayBufferCount) { |
|
128 testProcessCount++; |
|
129 return; |
|
130 } |
|
131 |
|
132 for (var i = 0; i < 2; ++i) { |
|
133 compareChannels(e.inputBuffer.getChannelData(i), referenceOutput[i]); |
|
134 } |
|
135 e.target.onaudioprocess = null; |
|
136 e.target.disconnect(); |
|
137 SimpleTest.finish(); |
|
138 } |
|
139 |
|
140 function startTest() { |
|
141 // 0.002 is MaxDelayTimeSeconds in HRTFpanner.cpp |
|
142 // and 512 is fftSize() at 48 kHz. |
|
143 const expectedPannerTailTime = 0.002 * ctx.sampleRate + 512; |
|
144 |
|
145 // Create some PannerNodes downstream from DelayNodes with delays long |
|
146 // enough for their source to finish, dispatch its "ended" event |
|
147 // and release its playing reference. The DelayNodes should expire their |
|
148 // tail-time references before the PannerNodes and so only the PannerNode |
|
149 // lifetimes depends on their tail-time references. Many DelayNodes are |
|
150 // created and timed to finish at different times so that one PannerNode |
|
151 // will be finishing the block processed immediately after the connect is |
|
152 // received. |
|
153 var source = ctx.createBufferSource(); |
|
154 // Just short of blockSize here to avoid rounding into the next block |
|
155 var buffer = ctx.createBuffer(1, blockSize - 1, ctx.sampleRate); |
|
156 for (var i = 0; i < buffer.length; ++i) { |
|
157 buffer.getChannelData(0)[i] = Math.cos(Math.PI * i / buffer.length); |
|
158 } |
|
159 source.buffer = buffer; |
|
160 source.start(0); |
|
161 source.onended = function(e) { |
|
162 gotEnded = true; |
|
163 }; |
|
164 |
|
165 // Time the first test panner to finish just before downstream DelayNodes |
|
166 // are about the be connected. Note that DelayNode lifetime depends on |
|
167 // maxDelayTime so set that equal to the delay. |
|
168 var delayDuration = |
|
169 (sourceDelayBufferCount * bufferSize |
|
170 - expectedPannerTailTime - 2 * blockSize) / ctx.sampleRate; |
|
171 |
|
172 for (var i = 0; i < pannerCount; ++i) { |
|
173 var delay = ctx.createDelay(delayDuration); |
|
174 delay.delayTime.value = delayDuration; |
|
175 source.connect(delay); |
|
176 delay.connect(referencePanner) |
|
177 |
|
178 var panner = ctx.createPanner(); |
|
179 delay.connect(panner); |
|
180 testPanners[i] = panner; |
|
181 |
|
182 delayDuration += blockSize / ctx.sampleRate; |
|
183 } |
|
184 |
|
185 // Create a ScriptProcessor now to use as a timer to trigger connection of |
|
186 // downstream nodes. It will also be used to record reference output. |
|
187 var referenceProcessor = ctx.createScriptProcessor(bufferSize, 2, 0); |
|
188 referenceProcessor.onaudioprocess = onReferenceOutput; |
|
189 // Start audioprocess events before source delays are connected. |
|
190 referenceProcessor.connect(ctx.destination); |
|
191 |
|
192 // The test ScriptProcessor will record output of testPanners. |
|
193 // Create it now so that it is synchronized with the referenceProcessor. |
|
194 testProcessor = ctx.createScriptProcessor(bufferSize, 2, 0); |
|
195 testProcessor.onaudioprocess = onTestOutput; |
|
196 // Start audioprocess events before source delays are connected. |
|
197 testProcessor.connect(ctx.destination); |
|
198 } |
|
199 |
|
200 function prepareTest() { |
|
201 ctx = new AudioContext(); |
|
202 // Place the listener to the side of the origin, where the panners are |
|
203 // positioned, to maximize delay in one ear. |
|
204 ctx.listener.setPosition(1,0,0); |
|
205 |
|
206 // A PannerNode will produce no output until it has loaded its HRIR |
|
207 // database. Wait for this to load before starting the test. |
|
208 var processor = ctx.createScriptProcessor(bufferSize, 2, 0); |
|
209 referencePanner = ctx.createPanner(); |
|
210 referencePanner.connect(processor); |
|
211 var oscillator = ctx.createOscillator(); |
|
212 oscillator.connect(referencePanner); |
|
213 oscillator.start(0); |
|
214 |
|
215 processor.onaudioprocess = function(e) { |
|
216 if (isChannelSilent(e.inputBuffer.getChannelData(0))) |
|
217 return; |
|
218 |
|
219 oscillator.stop(0); |
|
220 oscillator.disconnect(); |
|
221 referencePanner.disconnect(); |
|
222 e.target.onaudioprocess = null; |
|
223 SimpleTest.executeSoon(startTest); |
|
224 }; |
|
225 } |
|
226 prepareTest(); |
|
227 </script> |
|
228 </pre> |
|
229 </body> |
|
230 </html> |