detune-limiting.html (4645B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>Oscillator Detune: High Detune Limits and Automation</title> 5 <script src="/resources/testharness.js"></script> 6 <script src="/resources/testharnessreport.js"></script> 7 <script src="/webaudio/resources/audit-util.js"></script> 8 </head> 9 <body> 10 <script> 11 const sampleRate = 44100; 12 const renderLengthSeconds = 0.125; 13 const totalFrames = sampleRate * renderLengthSeconds; 14 promise_test(async t => { 15 const context = new OfflineAudioContext(2, totalFrames, sampleRate); 16 const merger = new ChannelMergerNode(context, { 17 numberOfInputs: context.destination.channelCount 18 }); 19 merger.connect(context.destination); 20 21 // Set the detune so the oscillator goes beyond the Nyquist frequency 22 // and verify if it produces silence. 23 const oscFrequency = 1; 24 const detunedFrequency = sampleRate; 25 const detuneValue = Math.fround(1200 * Math.log2(detunedFrequency)); 26 const testOsc = new OscillatorNode(context, { 27 frequency: oscFrequency, 28 detune: detuneValue 29 }); 30 testOsc.connect(merger, 0, 1); 31 const computedFrequency 32 = oscFrequency * Math.pow(2, detuneValue / 1200); 33 const refOsc = new OscillatorNode(context, { 34 frequency: computedFrequency 35 }); 36 refOsc.connect(merger, 0, 0); 37 testOsc.start(); 38 refOsc.start(); 39 const renderedBuffer = await context.startRendering(); 40 const expected = renderedBuffer.getChannelData(0); 41 const actual = renderedBuffer.getChannelData(1); 42 assert_greater_than_equal( 43 refOsc.frequency.value, 44 context.sampleRate / 2, 45 'Reference oscillator frequency should be ≥ Nyquist' 46 ); 47 assert_constant_value( 48 expected, 49 0, 50 `Reference output (freq: ${refOsc.frequency.value}) must be zero` 51 ); 52 assert_array_equal_within_eps( 53 actual, 54 expected, 55 0, 56 `Test oscillator output (freq: ${oscFrequency}, ` + 57 `detune: ${detuneValue}) must match reference output` 58 ); 59 }, 'Oscillator with large detune produces 0 output at Nyquist or above'); 60 promise_test(async t => { 61 const context = new OfflineAudioContext(1, totalFrames, sampleRate); 62 const baseFrequency = 1; 63 const rampEndTime = renderLengthSeconds / 2; 64 const detuneEnd = 1e7; 65 const oscillator 66 = new OscillatorNode(context, { frequency: baseFrequency }); 67 oscillator.detune.linearRampToValueAtTime(detuneEnd, rampEndTime); 68 oscillator.connect(context.destination); 69 oscillator.start(); 70 const renderedBuffer = await context.startRendering(); 71 const audio = renderedBuffer.getChannelData(0); 72 // At some point, the computed oscillator frequency will go 73 // above Nyquist. Determine at what time this occurrs. The 74 // computed frequency is f * 2^(d/1200) where |f| is the 75 // oscillator frequency and |d| is the detune value. Thus, 76 // find |d| such that Nyquist = f*2^(d/1200). That is, d = 77 // 1200*log2(Nyquist/f) 78 const nyquist = context.sampleRate / 2; 79 const criticalDetune = 1200 * Math.log2(nyquist / baseFrequency); 80 // Now figure out at what point on the linear ramp does the 81 // detune value reach this critical value. For a linear ramp: 82 // 83 // v(t) = V0+(V1-V0)*(t-T0)/(T1-T0) 84 // 85 // Thus, 86 // 87 // t = ((T1-T0)*v(t) + T0*V1 - T1*V0)/(V1-V0) 88 // 89 // In this test, T0 = 0, V0 = 0, T1 = rampEnd, V1 = 90 // detuneEnd, and v(t) = criticalDetune 91 const criticalTime = (rampEndTime * criticalDetune) / detuneEnd; 92 const criticalFrame = Math.ceil(criticalTime * sampleRate); 93 assert_less_than_equal( 94 criticalFrame, 95 audio.length, 96 'Critical frame should lie within audio buffer length' 97 ); 98 assert_not_constant_value( 99 audio.slice(0, criticalFrame), 100 0, 101 `Oscillator output [0:${criticalFrame - 1}] should not ` + 102 `be zero before exceeding Nyquist` 103 ); 104 assert_constant_value( 105 audio.slice(criticalFrame), 106 0, 107 `Oscillator output [${criticalFrame}:] should be zero ` + 108 `after exceeding Nyquist` 109 ); 110 }, 'Oscillator with detune automation becomes silent above Nyquist'); 111 </script> 112 </body> 113 </html>