1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
|
<!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>
|