| OLD | NEW |
| 1 <!DOCTYPE html> | 1 <!DOCTYPE html> |
| 2 <html> | 2 <html> |
| 3 <head> |
| 4 <title> |
| 5 stereopannernode-no-glitch.html |
| 6 </title> |
| 7 <script src="../../resources/testharness.js"></script> |
| 8 <script src="../../resources/testharnessreport.js"></script> |
| 9 <script src="../resources/audit-util.js"></script> |
| 10 <script src="../resources/audit.js"></script> |
| 11 </head> |
| 12 <body> |
| 13 <script id="layout-test-code"> |
| 14 let sampleRate = 44100; |
| 15 let renderDuration = 0.5; |
| 3 | 16 |
| 4 <head> | 17 // The threshold for glitch detection. This was experimentally determined. |
| 5 <script src="../../resources/testharness.js"></script> | 18 let GLITCH_THRESHOLD = 0.0005; |
| 6 <script src="../../resources/testharnessreport.js"></script> | |
| 7 <script src="../resources/audit-util.js"></script> | |
| 8 <script src="../resources/audit.js"></script> | |
| 9 </head> | |
| 10 | 19 |
| 11 <body> | 20 // The maximum threshold for the error between the actual and the expected |
| 12 <script> | 21 // sample values. Experimentally determined. |
| 13 var sampleRate = 44100; | 22 let MAX_ERROR_ALLOWED = 0.0000001; |
| 14 var renderDuration = 0.5; | |
| 15 | 23 |
| 16 // The threshold for glitch detection. This was experimentally determined. | 24 // Option for |Should| test util. The number of array elements to be |
| 17 var GLITCH_THRESHOLD = 0.0005; | 25 // printed out is arbitrary. |
| 26 let SHOULD_OPTS = {numberOfArrayLog: 2}; |
| 18 | 27 |
| 19 // The maximum threshold for the error between the actual and the expected | 28 let audit = Audit.createTaskRunner(); |
| 20 // sample values. Experimentally determined. | |
| 21 var MAX_ERROR_ALLOWED = 0.0000001; | |
| 22 | 29 |
| 23 // Option for |Should| test util. The number of array elements to be printed | 30 // Extract a transitional region from the AudioBuffer. If no transition |
| 24 // out is arbitrary. | 31 // found, fail this test. |
| 25 var SHOULD_OPTS = { | 32 function extractPanningTransition(should, input, prefix) { |
| 26 numberOfArrayLog: 2 | 33 let chanL = input.getChannelData(0); |
| 27 }; | 34 let chanR = input.getChannelData(1); |
| 35 let start, end; |
| 36 let index = 1; |
| 28 | 37 |
| 29 var audit = Audit.createTaskRunner(); | 38 // Find transition by comparing two consecutive samples. If two |
| 39 // consecutive samples are identical, the transition has not started. |
| 40 while (chanL[index - 1] === chanL[index] || |
| 41 chanR[index - 1] === chanR[index]) { |
| 42 if (++index >= input.length) { |
| 43 should(false, prefix + ': Transition in the channel data') |
| 44 .summarize('found', 'not found'); |
| 45 return null; |
| 46 } |
| 47 } |
| 48 start = index - 1; |
| 30 | 49 |
| 31 // Extract a transitional region from the AudioBuffer. If no transition | 50 // Find the end of transition. If two consecutive samples are not equal, |
| 32 // found, fail this test. | 51 // the transition is still ongoing. |
| 33 function extractPanningTransition(should, input, prefix) { | 52 while (chanL[index - 1] !== chanL[index] || |
| 34 var chanL = input.getChannelData(0); | 53 chanR[index - 1] !== chanR[index]) { |
| 35 var chanR = input.getChannelData(1); | 54 if (++index >= input.length) { |
| 36 var start, end; | 55 should(false, 'Transition found') |
| 37 var index = 1; | 56 .summarize('', 'but the buffer ended prematurely'); |
| 57 return null; |
| 58 } |
| 59 } |
| 60 end = index; |
| 38 | 61 |
| 39 // Find transition by comparing two consecutive samples. If two consecutiv
e | 62 return { |
| 40 // samples are identical, the transition has not started. | 63 left: chanL.subarray(start, end), |
| 41 while (chanL[index-1] === chanL[index] || chanR[index-1] === chanR[index])
{ | 64 right: chanR.subarray(start, end), |
| 42 if (++index >= input.length) { | 65 length: end - start |
| 43 should(false, prefix + ': Transition in the channel data') | 66 }; |
| 44 .summarize('found', 'not found'); | |
| 45 return null; | |
| 46 } | |
| 47 } | |
| 48 start = index - 1; | |
| 49 | |
| 50 // Find the end of transition. If two consecutive samples are not equal, | |
| 51 // the transition is still ongoing. | |
| 52 while (chanL[index-1] !== chanL[index] || chanR[index-1] !== chanR[index])
{ | |
| 53 if (++index >= input.length) { | |
| 54 should(false, 'Transition found') | |
| 55 .summarize('', 'but the buffer ended prematurely'); | |
| 56 return null; | |
| 57 } | |
| 58 } | |
| 59 end = index; | |
| 60 | |
| 61 return { | |
| 62 left: chanL.subarray(start, end), | |
| 63 right: chanR.subarray(start, end), | |
| 64 length: end - start | |
| 65 }; | |
| 66 } | |
| 67 | |
| 68 // JS implementation of stereo equal power panning. | |
| 69 function panStereoEqualPower(pan, inputL, inputR) { | |
| 70 pan = Math.min(1.0, Math.max(-1.0, pan)); | |
| 71 var output = []; | |
| 72 var panRadian; | |
| 73 if (!inputR) { // mono case. | |
| 74 panRadian = (pan * 0.5 + 0.5) * Math.PI / 2; | |
| 75 output[0] = inputL * Math.cos(panRadian); | |
| 76 output[1] = inputR * Math.sin(panRadian); | |
| 77 } else { // stereo case. | |
| 78 panRadian = (pan <= 0 ? pan + 1 : pan) * Math.PI / 2; | |
| 79 var gainL = Math.cos(panRadian); | |
| 80 var gainR = Math.sin(panRadian); | |
| 81 if (pan <= 0) { | |
| 82 output[0] = inputL + inputR * gainL; | |
| 83 output[1] = inputR * gainR; | |
| 84 } else { | |
| 85 output[0] = inputL * gainL; | |
| 86 output[1] = inputR + inputL * gainR; | |
| 87 } | |
| 88 } | |
| 89 return output; | |
| 90 } | |
| 91 | |
| 92 // Generate the expected result of stereo equal panning. |input| is an | |
| 93 // AudioBuffer to be panned. | |
| 94 function generateStereoEqualPanningResult(input, startPan, endPan, length) { | |
| 95 | |
| 96 // Smoothing constant time is 0.05 second. | |
| 97 var smoothingConstant = 1 - Math.exp(-1 / (sampleRate * 0.05)); | |
| 98 | |
| 99 var inputL = input.getChannelData(0); | |
| 100 var inputR = input.getChannelData(1); | |
| 101 var pan = startPan; | |
| 102 var outputL = [], outputR = []; | |
| 103 | |
| 104 for (var i = 0; i < length; i++) { | |
| 105 var samples = panStereoEqualPower(pan, inputL[i], inputR[i]); | |
| 106 outputL[i] = samples[0]; | |
| 107 outputR[i] = samples[1]; | |
| 108 pan += (endPan - pan) * smoothingConstant; | |
| 109 } | 67 } |
| 110 | 68 |
| 111 return { | 69 // JS implementation of stereo equal power panning. |
| 112 left: outputL, | 70 function panStereoEqualPower(pan, inputL, inputR) { |
| 113 right: outputR | 71 pan = Math.min(1.0, Math.max(-1.0, pan)); |
| 114 }; | 72 let output = []; |
| 115 } | 73 let panRadian; |
| 74 if (!inputR) { // mono case. |
| 75 panRadian = (pan * 0.5 + 0.5) * Math.PI / 2; |
| 76 output[0] = inputL * Math.cos(panRadian); |
| 77 output[1] = inputR * Math.sin(panRadian); |
| 78 } else { // stereo case. |
| 79 panRadian = (pan <= 0 ? pan + 1 : pan) * Math.PI / 2; |
| 80 let gainL = Math.cos(panRadian); |
| 81 let gainR = Math.sin(panRadian); |
| 82 if (pan <= 0) { |
| 83 output[0] = inputL + inputR * gainL; |
| 84 output[1] = inputR * gainR; |
| 85 } else { |
| 86 output[0] = inputL * gainL; |
| 87 output[1] = inputR + inputL * gainR; |
| 88 } |
| 89 } |
| 90 return output; |
| 91 } |
| 116 | 92 |
| 117 // Build audio graph and render. Change the pan parameter in the middle of | 93 // Generate the expected result of stereo equal panning. |input| is an |
| 118 // rendering. | 94 // AudioBuffer to be panned. |
| 119 function panAndVerify(should, options) { | 95 function generateStereoEqualPanningResult( |
| 120 var context = new OfflineAudioContext(2, renderDuration * sampleRate, samp
leRate); | 96 input, startPan, endPan, length) { |
| 121 var source = context.createBufferSource(); | 97 // Smoothing constant time is 0.05 second. |
| 122 var panner = context.createStereoPanner(); | 98 let smoothingConstant = 1 - Math.exp(-1 / (sampleRate * 0.05)); |
| 123 var stereoBuffer = createConstantBuffer(context, renderDuration * sampleRa
te, [1.0, 1.0]); | |
| 124 | 99 |
| 125 source.buffer = stereoBuffer; | 100 let inputL = input.getChannelData(0); |
| 101 let inputR = input.getChannelData(1); |
| 102 let pan = startPan; |
| 103 let outputL = [], outputR = []; |
| 126 | 104 |
| 127 panner.pan.value = options.startPanValue; | 105 for (let i = 0; i < length; i++) { |
| 106 let samples = panStereoEqualPower(pan, inputL[i], inputR[i]); |
| 107 outputL[i] = samples[0]; |
| 108 outputR[i] = samples[1]; |
| 109 pan += (endPan - pan) * smoothingConstant; |
| 110 } |
| 128 | 111 |
| 129 source.connect(panner); | 112 return {left: outputL, right: outputR}; |
| 130 panner.connect(context.destination); | 113 } |
| 131 source.start(); | |
| 132 | 114 |
| 133 // Schedule the parameter transition by the setter at 1/10 of the render | 115 // Build audio graph and render. Change the pan parameter in the middle of |
| 134 // duration. | 116 // rendering. |
| 135 context.suspend(0.1 * renderDuration).then(function () { | 117 function panAndVerify(should, options) { |
| 136 panner.pan.value = options.endPanValue; | 118 let context = |
| 137 context.resume(); | 119 new OfflineAudioContext(2, renderDuration * sampleRate, sampleRate); |
| 120 let source = context.createBufferSource(); |
| 121 let panner = context.createStereoPanner(); |
| 122 let stereoBuffer = createConstantBuffer( |
| 123 context, renderDuration * sampleRate, [1.0, 1.0]); |
| 124 |
| 125 source.buffer = stereoBuffer; |
| 126 |
| 127 panner.pan.value = options.startPanValue; |
| 128 |
| 129 source.connect(panner); |
| 130 panner.connect(context.destination); |
| 131 source.start(); |
| 132 |
| 133 // Schedule the parameter transition by the setter at 1/10 of the render |
| 134 // duration. |
| 135 context.suspend(0.1 * renderDuration).then(function() { |
| 136 panner.pan.value = options.endPanValue; |
| 137 context.resume(); |
| 138 }); |
| 139 |
| 140 return context.startRendering().then(function(buffer) { |
| 141 let actual = |
| 142 extractPanningTransition(should, buffer, options.message); |
| 143 let expected = generateStereoEqualPanningResult( |
| 144 stereoBuffer, options.startPanValue, options.endPanValue, |
| 145 actual.length); |
| 146 |
| 147 // |notGlitch| tests are redundant if the actual and expected results |
| 148 // match and if the expected results themselves don't glitch. |
| 149 should(actual.left, options.message + ': Channel #0') |
| 150 .notGlitch(GLITCH_THRESHOLD); |
| 151 should(actual.right, options.message + ': Channel #1') |
| 152 .notGlitch(GLITCH_THRESHOLD); |
| 153 |
| 154 should(actual.left, options.message + ': Channel #0', SHOULD_OPTS) |
| 155 .beCloseToArray( |
| 156 expected.left, {absoluteThreshold: MAX_ERROR_ALLOWED}); |
| 157 should(actual.right, options.message + ': Channel #1', SHOULD_OPTS) |
| 158 .beCloseToArray( |
| 159 expected.right, {absoluteThreshold: MAX_ERROR_ALLOWED}); |
| 160 }); |
| 161 } |
| 162 |
| 163 // Task: move pan from negative (-0.1) to positive (0.1) value to check if |
| 164 // there is a glitch during the transition. See crbug.com/470559. |
| 165 audit.define('negative-to-positive', (task, should) => { |
| 166 panAndVerify( |
| 167 should, {startPanValue: -0.1, endPanValue: 0.1, message: 'L->R'}) |
| 168 .then(() => task.done()); |
| 138 }); | 169 }); |
| 139 | 170 |
| 140 return context.startRendering().then(function (buffer) { | |
| 141 var actual = extractPanningTransition(should, buffer, options.message); | |
| 142 var expected = generateStereoEqualPanningResult(stereoBuffer, | |
| 143 options.startPanValue, options.endPanValue, actual.length); | |
| 144 | 171 |
| 145 // |notGlitch| tests are redundant if the actual and expected results | 172 // Task: move pan from positive (0.1) to negative (-0.1) value to check if |
| 146 // match and if the expected results themselves don't glitch. | 173 // there is a glitch during the transition. |
| 147 should(actual.left, options.message + ': Channel #0').notGlitch(GLITCH_T
HRESHOLD); | 174 audit.define('positive-to-negative', (task, should) => { |
| 148 should(actual.right, options.message + ': Channel #1').notGlitch(GLITCH_
THRESHOLD); | 175 panAndVerify( |
| 176 should, {startPanValue: 0.1, endPanValue: -0.1, message: 'R->L'}) |
| 177 .then(() => task.done()); |
| 178 }); |
| 149 | 179 |
| 150 should(actual.left, options.message + ': Channel #0', SHOULD_OPTS) | 180 audit.run(); |
| 151 .beCloseToArray(expected.left, {absoluteThreshold: MAX_ERROR_ALLOWED})
; | 181 </script> |
| 152 should(actual.right, options.message + ': Channel #1', SHOULD_OPTS) | 182 </body> |
| 153 .beCloseToArray(expected.right, {absoluteThreshold: MAX_ERROR_ALLOWED}
); | |
| 154 }); | |
| 155 } | |
| 156 | |
| 157 // Task: move pan from negative (-0.1) to positive (0.1) value to check if | |
| 158 // there is a glitch during the transition. See crbug.com/470559. | |
| 159 audit.define('negative-to-positive', (task, should) => { | |
| 160 panAndVerify(should, { startPanValue: -0.1, endPanValue: 0.1, message: "L-
>R" }) | |
| 161 .then(() => task.done()); | |
| 162 }); | |
| 163 | |
| 164 | |
| 165 // Task: move pan from positive (0.1) to negative (-0.1) value to check if | |
| 166 // there is a glitch during the transition. | |
| 167 audit.define('positive-to-negative', (task, should) => { | |
| 168 panAndVerify(should, { startPanValue: 0.1, endPanValue: -0.1, message: "R-
>L" }) | |
| 169 .then(() => task.done()); | |
| 170 }); | |
| 171 | |
| 172 audit.run(); | |
| 173 </script> | |
| 174 </body> | |
| 175 | |
| 176 </html> | 183 </html> |
| OLD | NEW |