Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

<!DOCTYPE HTML>
<html>
<head>
<title>Test tail time lifetime of PannerNode</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="webaudio.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<pre id="test">
<script class="testbody" type="text/javascript">
// This tests that a PannerNode does not release its reference before
// it finishes emitting sound.
//
// The PannerNode tail time is short, so, when a PannerNode is destroyed on
// the main thread, it is unlikely to notify the graph thread before the tail
// time expires. However, by adding DelayNodes downstream from the
// PannerNodes, the graph thread can have enough time to notice that a
// DelayNode has been destroyed.
//
// In the current implementation, DelayNodes will take a tail-time reference
// immediately when they receive the first block of sound from an upstream
// node, so this test connects the downstream DelayNodes while the upstream
// nodes are finishing, and then runs GC (on the main thread) before the
// DelayNodes receive any input (on the graph thread).
//
// Web Audio doesn't provide a means to precisely time connect()s but we can
// test that the output of delay nodes matches the output from a reference
// PannerNode that we know will not be GCed.
//
// Another set of delay nodes is added upstream to ensure that the source node
// has removed its self-reference after dispatching its "ended" event.
SimpleTest.waitForExplicitFinish();
const blockSize = 128;
// bufferSize should be long enough that to allow an audioprocess event to be
// sent to the main thread and a connect message to return to the graph
// thread.
const bufferSize = 4096;
const pannerCount = bufferSize / blockSize;
// sourceDelayBufferCount should be long enough to allow the source node
// onended to finish and remove the source self-reference.
const sourceDelayBufferCount = 3;
var gotEnded = false;
// ccDelayLength should be long enough to allow CC to run
var ccDelayBufferCount = 20;
const ccDelayLength = ccDelayBufferCount * bufferSize;
var ctx = new AudioContext();
var testPanners = [];
var referencePanner = new PannerNode(ctx, {panningModel: "HRTF"});
var referenceProcessCount = 0;
var referenceOutput = [new Float32Array(bufferSize),
new Float32Array(bufferSize)];
var testProcessor;
var testProcessCount = 0;
function onReferenceOutput(e) {
switch(referenceProcessCount) {
case sourceDelayBufferCount - 1:
// The panners are about to finish.
if (!gotEnded) {
todo(false, "Source hasn't ended. Increase sourceDelayBufferCount?");
}
// Connect each PannerNode output to a downstream DelayNode,
// and connect ScriptProcessors to compare test and reference panners.
var delayDuration = ccDelayLength / ctx.sampleRate;
for (var i = 0; i < pannerCount; ++i) {
var delay = ctx.createDelay(delayDuration);
delay.delayTime.value = delayDuration;
delay.connect(testProcessor);
testPanners[i].connect(delay);
}
testProcessor = null;
testPanners = null;
// The panning effect is linear so only one reference panner is required.
// This also checks that the individual panners don't chop their output
// too soon.
referencePanner.connect(e.target);
// Assuming the above operations have already scheduled an event to run in
// stable state and ask the graph thread to make connections, schedule a
// subsequent event to run cycle collection, which should not collect
// panners that are still producing sound.
SimpleTest.executeSoon(function() {
SpecialPowers.forceGC();
SpecialPowers.forceCC();
});
break;
case sourceDelayBufferCount:
// Record this buffer during which PannerNode outputs were connected.
for (var i = 0; i < 2; ++i) {
e.inputBuffer.copyFromChannel(referenceOutput[i], i);
}
e.target.onaudioprocess = null;
e.target.disconnect();
// If the buffer is silent, there is probably not much point just
// increasing the buffer size, because, with the buffer size already
// significantly larger than panner tail time, it demonstrates that the
// lag between threads is much greater than the tail time.
if (isChannelSilent(referenceOutput[0])) {
todo(false, "Connections not detected.");
}
}
referenceProcessCount++;
}
function onTestOutput(e) {
if (testProcessCount < sourceDelayBufferCount + ccDelayBufferCount) {
testProcessCount++;
return;
}
for (var i = 0; i < 2; ++i) {
compareChannels(e.inputBuffer.getChannelData(i), referenceOutput[i]);
}
e.target.onaudioprocess = null;
e.target.disconnect();
SimpleTest.finish();
}
function startTest() {
// 0.002 is MaxDelayTimeSeconds in HRTFpanner.cpp
// and 512 is fftSize() at 48 kHz.
const expectedPannerTailTime = 0.002 * ctx.sampleRate + 512;
// Place the listener to the side of the origin, where the panners are
// positioned, to maximize delay in one ear.
ctx.listener.setPosition(1,0,0);
// Create some PannerNodes downstream from DelayNodes with delays long
// enough for their source to finish, dispatch its "ended" event
// and release its playing reference. The DelayNodes should expire their
// tail-time references before the PannerNodes and so only the PannerNode
// lifetimes depends on their tail-time references. Many DelayNodes are
// created and timed to finish at different times so that one PannerNode
// will be finishing the block processed immediately after the connect is
// received.
var source = ctx.createBufferSource();
// Just short of blockSize here to avoid rounding into the next block
var buffer = ctx.createBuffer(1, blockSize - 1, ctx.sampleRate);
for (var i = 0; i < buffer.length; ++i) {
buffer.getChannelData(0)[i] = Math.cos(Math.PI * i / buffer.length);
}
source.buffer = buffer;
source.start(0);
source.onended = function() {
gotEnded = true;
};
// Time the first test panner to finish just before downstream DelayNodes
// are about the be connected. Note that DelayNode lifetime depends on
// maxDelayTime so set that equal to the delay.
var delayDuration =
(sourceDelayBufferCount * bufferSize
- expectedPannerTailTime - 2 * blockSize) / ctx.sampleRate;
for (var i = 0; i < pannerCount; ++i) {
var delay = ctx.createDelay(delayDuration);
delay.delayTime.value = delayDuration;
source.connect(delay);
delay.connect(referencePanner)
var panner = ctx.createPanner();
panner.panningModel = "HRTF";
delay.connect(panner);
testPanners[i] = panner;
delayDuration += blockSize / ctx.sampleRate;
}
// Create a ScriptProcessor now to use as a timer to trigger connection of
// downstream nodes. It will also be used to record reference output.
var referenceProcessor = ctx.createScriptProcessor(bufferSize, 2, 0);
referenceProcessor.onaudioprocess = onReferenceOutput;
// Start audioprocess events before source delays are connected.
referenceProcessor.connect(ctx.destination);
// The test ScriptProcessor will record output of testPanners.
// Create it now so that it is synchronized with the referenceProcessor.
testProcessor = ctx.createScriptProcessor(bufferSize, 2, 0);
testProcessor.onaudioprocess = onTestOutput;
// Start audioprocess events before source delays are connected.
testProcessor.connect(ctx.destination);
}
promiseHRTFReady(ctx.sampleRate).then(startTest);
</script>
</pre>
</body>
</html>