Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

<!doctype html>
<html>
<head>
<title>
Oscillator Detune Limits
</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/webaudio/resources/audit.js"></script>
</head>
<body>
<script>
const sampleRate = 44100;
const renderLengthSeconds = 0.125;
let audit = Audit.createTaskRunner();
audit.define(
{
label: 'detune limits',
description:
'Oscillator with detune and frequency at Nyquist or above'
},
(task, should) => {
let context = new OfflineAudioContext(
2, renderLengthSeconds * sampleRate, sampleRate);
let merger = new ChannelMergerNode(
context, {numberOfInputs: context.destination.channelCount});
merger.connect(context.destination);
// For test oscillator, set the oscillator frequency to -Nyquist and
// set detune to be a large number that would cause the detuned
// frequency to be way above Nyquist.
const oscFrequency = 1;
const detunedFrequency = sampleRate;
const detuneValue = Math.fround(1200 * Math.log2(detunedFrequency));
let testOsc = new OscillatorNode(
context, {frequency: oscFrequency, detune: detuneValue});
testOsc.connect(merger, 0, 1);
// For the reference oscillator, determine the computed oscillator
// frequency using the values above and set that as the oscillator
// frequency.
let computedFreq = oscFrequency * Math.pow(2, detuneValue / 1200);
let refOsc = new OscillatorNode(context, {frequency: computedFreq});
refOsc.connect(merger, 0, 0);
// Start 'em up and render
testOsc.start();
refOsc.start();
context.startRendering()
.then(renderedBuffer => {
let expected = renderedBuffer.getChannelData(0);
let actual = renderedBuffer.getChannelData(1);
// Let user know about the smaple rate so following messages
// make more sense.
should(context.sampleRate, 'Context sample rate')
.beEqualTo(context.sampleRate);
// Since the frequency is at Nyquist, the reference oscillator
// output should be zero.
should(
refOsc.frequency.value, 'Reference oscillator frequency')
.beGreaterThanOrEqualTo(context.sampleRate / 2);
should(
expected, `Osc(freq: ${refOsc.frequency.value}) output`)
.beConstantValueOf(0);
// The output from each oscillator should be the same.
should(
actual,
'Osc(freq: ' + oscFrequency + ', detune: ' + detuneValue +
') output')
.beCloseToArray(expected, {absoluteThreshold: 0});
})
.then(() => task.done());
});
audit.define(
{
label: 'detune automation',
description:
'Oscillator output with detune automation should be zero ' +
'above Nyquist'
},
(task, should) => {
let context = new OfflineAudioContext(
1, renderLengthSeconds * sampleRate, sampleRate);
const baseFrequency = 1;
const rampEnd = renderLengthSeconds / 2;
const detuneEnd = 1e7;
let src = new OscillatorNode(context, {frequency: baseFrequency});
src.detune.linearRampToValueAtTime(detuneEnd, rampEnd);
src.connect(context.destination);
src.start();
context.startRendering()
.then(renderedBuffer => {
let audio = renderedBuffer.getChannelData(0);
// At some point, the computed oscillator frequency will go
// above Nyquist. Determine at what time this occurrs. The
// computed frequency is f * 2^(d/1200) where |f| is the
// oscillator frequency and |d| is the detune value. Thus,
// find |d| such that Nyquist = f*2^(d/1200). That is, d =
// 1200*log2(Nyquist/f)
let criticalDetune =
1200 * Math.log2(context.sampleRate / 2 / baseFrequency);
// Now figure out at what point on the linear ramp does the
// detune value reach this critical value. For a linear ramp:
//
// v(t) = V0+(V1-V0)*(t-T0)/(T1-T0)
//
// Thus,
//
// t = ((T1-T0)*v(t) + T0*V1 - T1*V0)/(V1-V0)
//
// In this test, T0 = 0, V0 = 0, T1 = rampEnd, V1 =
// detuneEnd, and v(t) = criticalDetune
let criticalTime = (rampEnd * criticalDetune) / detuneEnd;
let criticalFrame =
Math.ceil(criticalTime * context.sampleRate);
should(
criticalFrame,
`Frame where detuned oscillator reaches Nyquist`)
.beEqualTo(criticalFrame);
should(
audio.slice(0, criticalFrame),
`osc[0:${criticalFrame - 1}]`)
.notBeConstantValueOf(0);
should(audio.slice(criticalFrame), `osc[${criticalFrame}:]`)
.beConstantValueOf(0);
})
.then(() => task.done());
});
audit.run();
</script>
</body>
</html>