| <!doctype html> |
| <html> |
| <head> |
| <title>Test k-rate AudioParam Inputs for BiquadFilterNode</title> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script src="/webaudio/resources/audit-util.js"></script> |
| <script src="/webaudio/resources/audit.js"></script> |
| </head> |
| |
| <body> |
| <script> |
| // sampleRate and duration are fairly arbitrary. We use low values to |
| // limit the complexity of the test. |
| let sampleRate = 8192; |
| let testDuration = 0.5; |
| |
| let audit = Audit.createTaskRunner(); |
| |
| audit.define( |
| {label: 'Frequency AudioParam', description: 'k-rate input works'}, |
| async (task, should) => { |
| // Test frequency AudioParam using a lowpass filter whose bandwidth |
| // is initially larger than the oscillator frequency. Then automate |
| // the frequency to 0 so that the output of the filter is 0 (because |
| // the cutoff is 0). |
| let oscFrequency = 440; |
| |
| let options = { |
| sampleRate: sampleRate, |
| paramName: 'frequency', |
| oscFrequency: oscFrequency, |
| testDuration: testDuration, |
| filterOptions: {type: 'lowpass', frequency: 0}, |
| autoStart: |
| {method: 'setValueAtTime', args: [2 * oscFrequency, 0]}, |
| autoEnd: { |
| method: 'linearRampToValueAtTime', |
| args: [0, testDuration / 4] |
| } |
| }; |
| |
| let buffer = await doTest(should, options); |
| let expected = buffer.getChannelData(0); |
| let actual = buffer.getChannelData(1); |
| let halfLength = expected.length / 2; |
| |
| // Sanity check. The expected output should not be zero for |
| // the first half, but should be zero for the second half |
| // (because the filter bandwidth is exactly 0). |
| const prefix = 'Expected k-rate frequency with automation'; |
| |
| should( |
| expected.slice(0, halfLength), |
| `${prefix} output[0:${halfLength - 1}]`) |
| .notBeConstantValueOf(0); |
| should( |
| expected.slice(expected.length), |
| `${prefix} output[${halfLength}:]`) |
| .beConstantValueOf(0); |
| |
| // Outputs should be the same. Break the message into two |
| // parts so we can see the expected outputs. |
| checkForSameOutput(should, options.paramName, actual, expected); |
| |
| task.done(); |
| }); |
| |
| audit.define( |
| {label: 'Q AudioParam', description: 'k-rate input works'}, |
| async (task, should) => { |
| // Test Q AudioParam. Use a bandpass filter whose center frequency |
| // is fairly far from the oscillator frequency. Then start with a Q |
| // value of 0 (so everything goes through) and then increase Q to |
| // some large value such that the out-of-band signals are basically |
| // cutoff. |
| let frequency = 440; |
| let oscFrequency = 4 * frequency; |
| |
| let options = { |
| sampleRate: sampleRate, |
| oscFrequency: oscFrequency, |
| testDuration: testDuration, |
| paramName: 'Q', |
| filterOptions: {type: 'bandpass', frequency: frequency, Q: 0}, |
| autoStart: {method: 'setValueAtTime', args: [0, 0]}, |
| autoEnd: { |
| method: 'linearRampToValueAtTime', |
| args: [100, testDuration / 4] |
| } |
| }; |
| |
| const buffer = await doTest(should, options); |
| let expected = buffer.getChannelData(0); |
| let actual = buffer.getChannelData(1); |
| |
| // Outputs should be the same |
| checkForSameOutput(should, options.paramName, actual, expected); |
| |
| task.done(); |
| }); |
| |
| audit.define( |
| {label: 'Gain AudioParam', description: 'k-rate input works'}, |
| async (task, should) => { |
| // Test gain AudioParam. Use a peaking filter with a large Q so the |
| // peak is narrow with a center frequency the same as the oscillator |
| // frequency. Start with a gain of 0 so everything goes through and |
| // then ramp the gain down to -100 so that the oscillator is |
| // filtered out. |
| let oscFrequency = 4 * 440; |
| |
| let options = { |
| sampleRate: sampleRate, |
| oscFrequency: oscFrequency, |
| testDuration: testDuration, |
| paramName: 'gain', |
| filterOptions: |
| {type: 'peaking', frequency: oscFrequency, Q: 100, gain: 0}, |
| autoStart: {method: 'setValueAtTime', args: [0, 0]}, |
| autoEnd: { |
| method: 'linearRampToValueAtTime', |
| args: [-100, testDuration / 4] |
| } |
| }; |
| |
| const buffer = await doTest(should, options); |
| let expected = buffer.getChannelData(0); |
| let actual = buffer.getChannelData(1); |
| |
| // Outputs should be the same |
| checkForSameOutput(should, options.paramName, actual, expected); |
| |
| task.done(); |
| }); |
| |
| audit.define( |
| {label: 'Detune AudioParam', description: 'k-rate input works'}, |
| async (task, should) => { |
| // Test detune AudioParam. The basic idea is the same as the |
| // frequency test above, but insteda of automating the frequency, we |
| // automate the detune value so that initially the filter cutuff is |
| // unchanged and then changing the detune until the cutoff goes to 1 |
| // Hz, which would cause the oscillator to be filtered out. |
| let oscFrequency = 440; |
| let filterFrequency = 5 * oscFrequency; |
| |
| // For a detune value d, the computed frequency, fc, of the filter |
| // is fc = f*2^(d/1200), where f is the frequency of the filter. Or |
| // d = 1200*log2(fc/f). Compute the detune value to produce a final |
| // cutoff frequency of 1 Hz. |
| let detuneEnd = 1200 * Math.log2(1 / filterFrequency); |
| |
| let options = { |
| sampleRate: sampleRate, |
| oscFrequency: oscFrequency, |
| testDuration: testDuration, |
| paramName: 'detune', |
| filterOptions: { |
| type: 'lowpass', |
| frequency: filterFrequency, |
| detune: 0, |
| gain: 0 |
| }, |
| autoStart: {method: 'setValueAtTime', args: [0, 0]}, |
| autoEnd: { |
| method: 'linearRampToValueAtTime', |
| args: [detuneEnd, testDuration / 4] |
| } |
| }; |
| |
| const buffer = await doTest(should, options); |
| let expected = buffer.getChannelData(0); |
| let actual = buffer.getChannelData(1); |
| |
| // Outputs should be the same |
| checkForSameOutput(should, options.paramName, actual, expected); |
| |
| task.done(); |
| }); |
| |
| audit.define('All k-rate inputs', async (task, should) => { |
| // Test the case where all AudioParams are set to k-rate with an input |
| // to each AudioParam. Similar to the above tests except all the params |
| // are k-rate. |
| let testFrames = testDuration * sampleRate; |
| let context = new OfflineAudioContext( |
| {numberOfChannels: 2, sampleRate: sampleRate, length: testFrames}); |
| |
| let merger = new ChannelMergerNode( |
| context, {numberOfInputs: context.destination.channelCount}); |
| merger.connect(context.destination); |
| |
| let src = new OscillatorNode(context); |
| |
| // The peaking filter uses all four AudioParams, so this is the node to |
| // test. |
| let filterOptions = |
| {type: 'peaking', frequency: 0, detune: 0, gain: 0, Q: 0}; |
| let refNode; |
| should( |
| () => refNode = new BiquadFilterNode(context, filterOptions), |
| `Create: refNode = new BiquadFilterNode(context, ${ |
| JSON.stringify(filterOptions)})`) |
| .notThrow(); |
| |
| let tstNode; |
| should( |
| () => tstNode = new BiquadFilterNode(context, filterOptions), |
| `Create: tstNode = new BiquadFilterNode(context, ${ |
| JSON.stringify(filterOptions)})`) |
| .notThrow(); |
| ; |
| |
| // Make all the AudioParams k-rate. |
| ['frequency', 'Q', 'gain', 'detune'].forEach(param => { |
| should( |
| () => refNode[param].automationRate = 'k-rate', |
| `Set rate: refNode[${param}].automationRate = 'k-rate'`) |
| .notThrow(); |
| should( |
| () => tstNode[param].automationRate = 'k-rate', |
| `Set rate: tstNode[${param}].automationRate = 'k-rate'`) |
| .notThrow(); |
| }); |
| |
| // One input for each AudioParam. |
| let mod = {}; |
| ['frequency', 'Q', 'gain', 'detune'].forEach(param => { |
| should( |
| () => mod[param] = new ConstantSourceNode(context, {offset: 0}), |
| `Create: mod[${ |
| param}] = new ConstantSourceNode(context, {offset: 0})`) |
| .notThrow(); |
| ; |
| should( |
| () => mod[param].offset.automationRate = 'a-rate', |
| `Set rate: mod[${param}].offset.automationRate = 'a-rate'`) |
| .notThrow(); |
| }); |
| |
| // Set up automations for refNode. We want to start the filter with |
| // parameters that let the oscillator signal through more or less |
| // untouched. Then change the filter parameters to filter out the |
| // oscillator. What happens in between doesn't reall matter for this |
| // test. Hence, set the initial parameters with a center frequency well |
| // above the oscillator and a Q and gain of 0 to pass everthing. |
| [['frequency', [4 * src.frequency.value, 0]], ['Q', [0, 0]], |
| ['gain', [0, 0]], ['detune', [4 * 1200, 0]]] |
| .forEach(param => { |
| should( |
| () => refNode[param[0]].setValueAtTime(...param[1]), |
| `Automate 0: refNode.${param[0]}.setValueAtTime(${ |
| param[1][0]}, ${param[1][1]})`) |
| .notThrow(); |
| should( |
| () => mod[param[0]].offset.setValueAtTime(...param[1]), |
| `Automate 0: mod[${param[0]}].offset.setValueAtTime(${ |
| param[1][0]}, ${param[1][1]})`) |
| .notThrow(); |
| }); |
| |
| // Now move the filter frequency to the oscillator frequency with a high |
| // Q and very low gain to remove the oscillator signal. |
| [['frequency', [src.frequency.value, testDuration / 4]], |
| ['Q', [40, testDuration / 4]], ['gain', [-100, testDuration / 4]], [ |
| 'detune', [0, testDuration / 4] |
| ]].forEach(param => { |
| should( |
| () => refNode[param[0]].linearRampToValueAtTime(...param[1]), |
| `Automate 1: refNode[${param[0]}].linearRampToValueAtTime(${ |
| param[1][0]}, ${param[1][1]})`) |
| .notThrow(); |
| should( |
| () => mod[param[0]].offset.linearRampToValueAtTime(...param[1]), |
| `Automate 1: mod[${param[0]}].offset.linearRampToValueAtTime(${ |
| param[1][0]}, ${param[1][1]})`) |
| .notThrow(); |
| }); |
| |
| // Connect everything |
| src.connect(refNode).connect(merger, 0, 0); |
| src.connect(tstNode).connect(merger, 0, 1); |
| |
| src.start(); |
| for (let param in mod) { |
| should( |
| () => mod[param].connect(tstNode[param]), |
| `Connect: mod[${param}].connect(tstNode.${param})`) |
| .notThrow(); |
| } |
| |
| for (let param in mod) { |
| should(() => mod[param].start(), `Start: mod[${param}].start()`) |
| .notThrow(); |
| } |
| |
| const buffer = await context.startRendering(); |
| let expected = buffer.getChannelData(0); |
| let actual = buffer.getChannelData(1); |
| |
| // Sanity check that the output isn't all zeroes. |
| should(actual, 'All k-rate AudioParams').notBeConstantValueOf(0); |
| should(actual, 'All k-rate AudioParams').beCloseToArray(expected, { |
| absoluteThreshold: 0 |
| }); |
| |
| task.done(); |
| }); |
| |
| audit.run(); |
| |
| async function doTest(should, options) { |
| // Test that a k-rate AudioParam with an input reads the input value and |
| // is actually k-rate. |
| // |
| // A refNode is created with an automation timeline. This is the |
| // expected output. |
| // |
| // The testNode is the same, but it has a node connected to the k-rate |
| // AudioParam. The input to the node is an a-rate ConstantSourceNode |
| // whose output is automated in exactly the same was as the refNode. If |
| // the test passes, the outputs of the two nodes MUST match exactly. |
| |
| // The options argument MUST contain the following members: |
| // sampleRate - the sample rate for the offline context |
| // testDuration - duration of the offline context, in sec. |
| // paramName - the name of the AudioParam to be tested |
| // oscFrequency - frequency of oscillator source |
| // filterOptions - options used to construct the BiquadFilterNode |
| // autoStart - information about how to start the automation |
| // autoEnd - information about how to end the automation |
| // |
| // The autoStart and autoEnd options are themselves dictionaries with |
| // the following required members: |
| // method - name of the automation method to be applied |
| // args - array of arguments to be supplied to the method. |
| let { |
| sampleRate, |
| paramName, |
| oscFrequency, |
| autoStart, |
| autoEnd, |
| testDuration, |
| filterOptions |
| } = options; |
| |
| let testFrames = testDuration * sampleRate; |
| let context = new OfflineAudioContext( |
| {numberOfChannels: 2, sampleRate: sampleRate, length: testFrames}); |
| |
| let merger = new ChannelMergerNode( |
| context, {numberOfInputs: context.destination.channelCount}); |
| merger.connect(context.destination); |
| |
| // Any calls to |should| are meant to be informational so we can see |
| // what nodes are created and the automations used. |
| let src; |
| |
| // Create the source. |
| should( |
| () => { |
| src = new OscillatorNode(context, {frequency: oscFrequency}); |
| }, |
| `${paramName}: new OscillatorNode(context, {frequency: ${ |
| oscFrequency}})`) |
| .notThrow(); |
| |
| // The refNode automates the AudioParam with k-rate automations, no |
| // inputs. |
| let refNode; |
| should( |
| () => { |
| refNode = new BiquadFilterNode(context, filterOptions); |
| }, |
| `Reference BiquadFilterNode(c, ${JSON.stringify(filterOptions)})`) |
| .notThrow(); |
| |
| refNode[paramName].automationRate = 'k-rate'; |
| |
| // Set up automations for the reference node. |
| should( |
| () => { |
| refNode[paramName][autoStart.method](...autoStart.args); |
| }, |
| `refNode.${paramName}.${autoStart.method}(${autoStart.args})`) |
| .notThrow(); |
| should( |
| () => { |
| refNode[paramName][autoEnd.method](...autoEnd.args); |
| }, |
| `refNode.${paramName}.${autoEnd.method}.(${autoEnd.args})`) |
| .notThrow(); |
| |
| // The tstNode does the same automation, but it comes from the input |
| // connected to the AudioParam. |
| let tstNode; |
| should( |
| () => { |
| tstNode = new BiquadFilterNode(context, filterOptions); |
| }, |
| `Test BiquadFilterNode(context, ${JSON.stringify(filterOptions)})`) |
| .notThrow(); |
| tstNode[paramName].automationRate = 'k-rate'; |
| |
| // Create the input to the AudioParam of the test node. The output of |
| // this node MUST have the same set of automations as the reference |
| // node, and MUST be a-rate to make sure we're handling k-rate inputs |
| // correctly. |
| let mod = new ConstantSourceNode(context); |
| mod.offset.automationRate = 'a-rate'; |
| should( |
| () => { |
| mod.offset[autoStart.method](...autoStart.args); |
| }, |
| `${paramName}: mod.offset.${autoStart.method}(${autoStart.args})`) |
| .notThrow(); |
| should( |
| () => { |
| mod.offset[autoEnd.method](...autoEnd.args); |
| }, |
| `${paramName}: mod.offset.${autoEnd.method}(${autoEnd.args})`) |
| .notThrow(); |
| |
| // Create graph |
| mod.connect(tstNode[paramName]); |
| src.connect(refNode).connect(merger, 0, 0); |
| src.connect(tstNode).connect(merger, 0, 1); |
| |
| // Run! |
| src.start(); |
| mod.start(); |
| return context.startRendering(); |
| } |
| |
| function checkForSameOutput(should, paramName, actual, expected) { |
| let halfLength = expected.length / 2; |
| |
| // Outputs should be the same. We break the check into halves so we can |
| // see the expected outputs. Mostly for a simple visual check that the |
| // output from the second half is small because the tests generally try |
| // to filter out the signal so that the last half of the output is |
| // small. |
| should( |
| actual.slice(0, halfLength), |
| `k-rate ${paramName} with input: output[0,${halfLength}]`) |
| .beCloseToArray( |
| expected.slice(0, halfLength), {absoluteThreshold: 0}); |
| should( |
| actual.slice(halfLength), |
| `k-rate ${paramName} with input: output[${halfLength}:]`) |
| .beCloseToArray(expected.slice(halfLength), {absoluteThreshold: 0}); |
| } |
| </script> |
| </body> |
| </html> |