waveshaper.html (4358B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title> 5 waveshaper.html 6 </title> 7 <script src="/resources/testharness.js"></script> 8 <script src="/resources/testharnessreport.js"></script> 9 </head> 10 <body> 11 <script> 12 13 const sampleRate = 44100; 14 const lengthInSeconds = 4; 15 const numberOfRenderFrames = sampleRate * lengthInSeconds; 16 const numberOfCurveFrames = 65536; 17 18 let context; 19 let inputBuffer; 20 let waveShapingCurve; 21 22 function generateInputBuffer() { 23 // Create mono input buffer. 24 const buffer = new AudioBuffer({ 25 length: numberOfRenderFrames, 26 numberOfChannels: 1, 27 sampleRate: context.sampleRate 28 }); 29 const data = buffer.getChannelData(0); 30 31 // Generate an input vector with values from -1 -> +1 over a duration of 32 // lengthInSeconds. This exercises the full nominal input range and will 33 // touch every point of the shaping curve. 34 for (let i = 0; i < numberOfRenderFrames; ++i) { 35 let x = i / numberOfRenderFrames; 36 x = 2 * x - 1; 37 data[i] = x; 38 } 39 40 return buffer; 41 } 42 43 // Generates a symmetric curve: Math.atan(5 * x) / (0.5 * Math.PI) 44 // (with x == 0 corresponding to the center of the array) 45 // This curve is arbitrary, but would be useful in the real-world. 46 // To some extent, the actual curve we choose is not important in this 47 // test, since the input vector walks through all possible curve values. 48 function generateWaveShapingCurve() { 49 const curve = new Float32Array(numberOfCurveFrames); 50 const n = numberOfCurveFrames; 51 const n2 = n / 2; 52 53 for (let i = 0; i < n; ++i) { 54 const x = (i - n2) / n2; 55 curve[i] = Math.atan(5 * x) / (0.5 * Math.PI); 56 } 57 return curve; 58 } 59 // Following spec algorithm from 60 // https://webaudio.github.io/web-audio-api/#WaveShaperNode-attributes 61 function checkShapedCurve(buffer) { 62 const inputData = inputBuffer.getChannelData(0); 63 const outputData = buffer.getChannelData(0); 64 const c = waveShapingCurve; 65 const N = numberOfCurveFrames; 66 const tolerance = 1e-6; 67 68 let firstMismatchIndex = -1; 69 let actual = 0; 70 let expected = 0; 71 72 // Go through every sample and make sure it has been shaped exactly 73 // according to the shaping curve we gave it. 74 for (let i = 0; i < buffer.length; ++i) { 75 const x = inputData[i]; 76 77 const v = ((N - 1) * 0.5) * (x + 1); 78 const k = Math.floor(v); 79 const f = v - k; 80 81 let y; 82 if (v < 0) { 83 y = c[0]; 84 } else if (v >= N - 1) { 85 y = c[N - 1]; 86 } else { 87 y = (1 - f) * c[k] + f * c[k + 1]; 88 } 89 90 if (Math.abs(outputData[i] - y) > tolerance) { 91 firstMismatchIndex = i; 92 actual = outputData[i]; 93 expected = y; 94 break; 95 } 96 } 97 98 assert_equals( 99 firstMismatchIndex, 100 -1, 101 firstMismatchIndex === -1 102 ? 'WaveShaperNode output should match the spec.' 103 : `Mismatch at sample ${firstMismatchIndex}: ` + 104 `actual = ${actual}, expected = ${expected}, ` + 105 `tolerance = ${tolerance}`); 106 } 107 108 promise_test(async t => { 109 // Create offline audio context. 110 context = new OfflineAudioContext(1, numberOfRenderFrames, sampleRate); 111 112 // source -> waveshaper -> destination 113 const source = new AudioBufferSourceNode(context); 114 const waveshaper = new WaveShaperNode(context); 115 source.connect(waveshaper); 116 waveshaper.connect(context.destination); 117 118 // Create an input test vector. 119 inputBuffer = generateInputBuffer(); 120 source.buffer = inputBuffer; 121 122 // We'll apply non-linear distortion according to this shaping curve. 123 waveShapingCurve = generateWaveShapingCurve(); 124 waveshaper.curve = waveShapingCurve; 125 126 source.start(); 127 128 const renderedBuffer = await context.startRendering(); 129 checkShapedCurve(renderedBuffer); 130 }, 'WaveShaperNode applies non-linear distortion correctly'); 131 </script> 132 </body> 133 </html>