5 <script src=
"../resources/js-test.js"></script>
6 <script src=
"resources/compatibility.js"></script>
7 <script type=
"text/javascript" src=
"resources/audio-testing.js"></script>
11 <div id=
"description"></div>
12 <div id=
"console"></div>
15 description("Channel mixing rules for AudioNodes.");
18 var sampleRate
= 44100;
19 var renderNumberOfChannels
= 8;
20 var singleTestFrameLength
= 8;
23 // A list of connections to an AudioNode input, each of which is to be used in one or more specific test cases.
24 // Each element in the list is a string, with the number of connections corresponding to the length of the string,
25 // and each character in the string is from '1' to '8' representing a 1 to 8 channel connection (from an AudioNode output).
26 // For example, the string "128" means 3 connections, having 1, 2, and 8 channels respectively.
27 var connectionsList
= ["1", "2", "3", "4", "5", "6", "7", "8", "11", "12", "14", "18", "111", "122", "123", "124", "128"];
29 // A list of mixing rules, each of which will be tested against all of the connections in connectionsList.
30 var mixingRulesList
= [
31 {channelCount
: 2, channelCountMode
: "max", channelInterpretation
: "speakers"},
32 {channelCount
: 4, channelCountMode
: "clamped-max", channelInterpretation
: "speakers"},
34 // Test up-down-mix to some explicit speaker layouts.
35 {channelCount
: 1, channelCountMode
: "explicit", channelInterpretation
: "speakers"},
36 {channelCount
: 2, channelCountMode
: "explicit", channelInterpretation
: "speakers"},
37 {channelCount
: 4, channelCountMode
: "explicit", channelInterpretation
: "speakers"},
38 {channelCount
: 6, channelCountMode
: "explicit", channelInterpretation
: "speakers"},
40 {channelCount
: 2, channelCountMode
: "max", channelInterpretation
: "discrete"},
41 {channelCount
: 4, channelCountMode
: "clamped-max", channelInterpretation
: "discrete"},
42 {channelCount
: 4, channelCountMode
: "explicit", channelInterpretation
: "discrete"},
43 {channelCount
: 8, channelCountMode
: "explicit", channelInterpretation
: "discrete"},
46 var numberOfTests
= mixingRulesList
.length
* connectionsList
.length
;
48 // Create an n-channel buffer, with all sample data zero except for a shifted impulse.
49 // The impulse position depends on the channel index.
50 // For example, for a 4-channel buffer:
51 // channel0: 1 0 0 0 0 0 0 0
52 // channel1: 0 1 0 0 0 0 0 0
53 // channel2: 0 0 1 0 0 0 0 0
54 // channel3: 0 0 0 1 0 0 0 0
55 function createTestBuffer(numberOfChannels
) {
56 var buffer
= context
.createBuffer(numberOfChannels
, singleTestFrameLength
, context
.sampleRate
);
57 for (var i
= 0; i
< numberOfChannels
; ++i
) {
58 var data
= buffer
.getChannelData(i
);
64 // Discrete channel interpretation mixing:
65 // https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html#UpMix
66 // up-mix by filling channels until they run out then ignore remaining dest channels.
67 // down-mix by filling as many channels as possible, then dropping remaining source channels.
68 function discreteSum(sourceBuffer
, destBuffer
) {
69 if (sourceBuffer
.length
!= destBuffer
.length
) {
70 alert("discreteSum(): invalid AudioBuffer!");
74 var numberOfChannels
= sourceBuffer
.numberOfChannels
< destBuffer
.numberOfChannels
? sourceBuffer
.numberOfChannels
: destBuffer
.numberOfChannels
;
75 var length
= numberOfChannels
;
77 for (var c
= 0; c
< numberOfChannels
; ++c
) {
78 var source
= sourceBuffer
.getChannelData(c
);
79 var dest
= destBuffer
.getChannelData(c
);
80 for (var i
= 0; i
< length
; ++i
) {
86 // Speaker channel interpretation mixing:
87 // https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html#UpMix
88 function speakersSum(sourceBuffer
, destBuffer
)
90 var numberOfSourceChannels
= sourceBuffer
.numberOfChannels
;
91 var numberOfDestinationChannels
= destBuffer
.numberOfChannels
;
92 var length
= destBuffer
.length
;
94 if (numberOfDestinationChannels
== 2 && numberOfSourceChannels
== 1) {
95 // Handle mono -> stereo case (summing mono channel into both left and right).
96 var source
= sourceBuffer
.getChannelData(0);
97 var destL
= destBuffer
.getChannelData(0);
98 var destR
= destBuffer
.getChannelData(1);
100 for (var i
= 0; i
< length
; ++i
) {
101 destL
[i
] += source
[i
];
102 destR
[i
] += source
[i
];
104 } else if (numberOfDestinationChannels
== 1 && numberOfSourceChannels
== 2) {
105 // Handle stereo -> mono case. output += 0.5 * (input.L + input.R).
106 var sourceL
= sourceBuffer
.getChannelData(0);
107 var sourceR
= sourceBuffer
.getChannelData(1);
108 var dest
= destBuffer
.getChannelData(0);
110 for (var i
= 0; i
< length
; ++i
) {
111 dest
[i
] += 0.5 * (sourceL
[i
] + sourceR
[i
]);
113 } else if (numberOfDestinationChannels
== 6 && numberOfSourceChannels
== 1) {
114 // Handle mono -> 5.1 case, sum mono channel into center.
115 var source
= sourceBuffer
.getChannelData(0);
116 var dest
= destBuffer
.getChannelData(2);
118 for (var i
= 0; i
< length
; ++i
) {
119 dest
[i
] += source
[i
];
121 } else if (numberOfDestinationChannels
== 1 && numberOfSourceChannels
== 6) {
122 // Handle 5.1 -> mono.
123 var sourceL
= sourceBuffer
.getChannelData(0);
124 var sourceR
= sourceBuffer
.getChannelData(1);
125 var sourceC
= sourceBuffer
.getChannelData(2);
126 // skip LFE for now, according to current spec.
127 var sourceSL
= sourceBuffer
.getChannelData(4);
128 var sourceSR
= sourceBuffer
.getChannelData(5);
129 var dest
= destBuffer
.getChannelData(0);
131 for (var i
= 0; i
< length
; ++i
) {
132 dest
[i
] += 0.7071 * (sourceL
[i
] + sourceR
[i
]) + sourceC
[i
] + 0.5 * (sourceSL
[i
] + sourceSR
[i
]);
135 // Fallback for unknown combinations.
136 discreteSum(sourceBuffer
, destBuffer
);
140 function scheduleTest(testNumber
, connections
, channelCount
, channelCountMode
, channelInterpretation
) {
141 var mixNode
= context
.createGain();
142 mixNode
.channelCount
= channelCount
;
143 mixNode
.channelCountMode
= channelCountMode
;
144 mixNode
.channelInterpretation
= channelInterpretation
;
145 mixNode
.connect(context
.destination
);
147 for (var i
= 0; i
< connections
.length
; ++i
) {
148 var connectionNumberOfChannels
= connections
.charCodeAt(i
) - "0".charCodeAt(0);
150 var source
= context
.createBufferSource();
151 // Get a buffer with the right number of channels, converting from 1-based to 0-based index.
152 var buffer
= testBuffers
[connectionNumberOfChannels
- 1];
153 source
.buffer
= buffer
;
154 source
.connect(mixNode
);
156 // Start at the right offset.
157 var sampleFrameOffset
= testNumber
* singleTestFrameLength
;
158 var time
= sampleFrameOffset
/ sampleRate
;
163 function computeNumberOfChannels(connections
, channelCount
, channelCountMode
) {
164 if (channelCountMode
== "explicit")
167 var computedNumberOfChannels
= 1; // Must have at least one channel.
169 // Compute "computedNumberOfChannels" based on all the connections.
170 for (var i
= 0; i
< connections
.length
; ++i
) {
171 var connectionNumberOfChannels
= connections
.charCodeAt(i
) - "0".charCodeAt(0);
172 computedNumberOfChannels
= Math
.max(computedNumberOfChannels
, connectionNumberOfChannels
);
175 if (channelCountMode
== "clamped-max")
176 computedNumberOfChannels
= Math
.min(computedNumberOfChannels
, channelCount
);
178 return computedNumberOfChannels
;
181 function checkTestResult(renderedBuffer
, testNumber
, connections
, channelCount
, channelCountMode
, channelInterpretation
) {
182 var s
= "connections: " + connections
+ ", " + channelCountMode
;
184 // channelCount is ignored in "max" mode.
185 if (channelCountMode
== "clamped-max" || channelCountMode
== "explicit") {
186 s
+= "(" + channelCount
+ ")";
189 s
+= ", " + channelInterpretation
;
191 var computedNumberOfChannels
= computeNumberOfChannels(connections
, channelCount
, channelCountMode
);
193 // Show rendered output for this test:
196 // var sampleFrameOffset = testNumber * singleTestFrameLength;
197 // for (var c = 0; c < renderNumberOfChannels; ++c) {
198 // var data = renderedBuffer.getChannelData(c);
200 // for (var sampleFrame = 0; sampleFrame < singleTestFrameLength; ++sampleFrame) {
201 // s += data[sampleFrame + sampleFrameOffset] + " ";
208 // Create a zero-initialized silent AudioBuffer with computedNumberOfChannels.
209 var destBuffer
= context
.createBuffer(computedNumberOfChannels
, singleTestFrameLength
, context
.sampleRate
);
211 // Mix all of the connections into the destination buffer.
212 for (var i
= 0; i
< connections
.length
; ++i
) {
213 var connectionNumberOfChannels
= connections
.charCodeAt(i
) - "0".charCodeAt(0);
214 var sourceBuffer
= testBuffers
[connectionNumberOfChannels
- 1]; // convert from 1-based to 0-based index
216 if (channelInterpretation
== "speakers") {
217 speakersSum(sourceBuffer
, destBuffer
);
218 } else if (channelInterpretation
== "discrete") {
219 discreteSum(sourceBuffer
, destBuffer
);
221 alert("Invalid channel interpretation!");
225 // Validate that destBuffer matches the rendered output.
226 // We need to check the rendered output at a specific sample-frame-offset corresponding
227 // to the specific test case we're checking for based on testNumber.
229 var sampleFrameOffset
= testNumber
* singleTestFrameLength
;
230 for (var c
= 0; c
< renderNumberOfChannels
; ++c
) {
231 var renderedData
= renderedBuffer
.getChannelData(c
);
232 for (var frame
= 0; frame
< singleTestFrameLength
; ++frame
) {
233 var renderedValue
= renderedData
[frame
+ sampleFrameOffset
];
235 var expectedValue
= 0;
236 if (c
< destBuffer
.numberOfChannels
) {
237 var expectedData
= destBuffer
.getChannelData(c
);
238 expectedValue
= expectedData
[frame
];
241 // We may need to add an epsilon in the comparison if we add more test vectors.
242 if (renderedValue
!= expectedValue
) {
243 var message
= s
+ "rendered: " + renderedValue
+ " expected: " + expectedValue
+ " channel: " + c
+ " frame: " + frame
;
253 function checkResult(event
) {
254 var buffer
= event
.renderedBuffer
;
256 // Sanity check result.
257 if (buffer
.length
!= numberOfTests
* singleTestFrameLength
|| buffer
.numberOfChannels
!= renderNumberOfChannels
) {
258 testFailed("OfflineAudioContext result not of expected size!");
263 // Check all the tests.
265 for (var m
= 0; m
< mixingRulesList
.length
; ++m
) {
266 var mixingRules
= mixingRulesList
[m
];
267 for (var i
= 0; i
< connectionsList
.length
; ++i
, ++testNumber
) {
268 checkTestResult(buffer
, testNumber
, connectionsList
[i
], mixingRules
.channelCount
, mixingRules
.channelCountMode
, mixingRules
.channelInterpretation
);
276 if (window
.testRunner
) {
277 testRunner
.dumpAsText();
278 testRunner
.waitUntilDone();
281 window
.jsTestIsAsync
= true;
283 // Create 8-channel offline audio context.
284 // Each test will render 8 sample-frames starting at sample-frame position testNumber * 8.
285 var totalFrameLength
= numberOfTests
* singleTestFrameLength
;
286 context
= new OfflineAudioContext(renderNumberOfChannels
, totalFrameLength
, sampleRate
);
288 // Set destination to discrete mixing.
289 context
.destination
.channelCount
= renderNumberOfChannels
;
290 context
.destination
.channelCountMode
= "explicit";
291 context
.destination
.channelInterpretation
= "discrete";
293 // Create test buffers from 1 to 8 channels.
294 testBuffers
= new Array();
295 for (var i
= 0; i
< renderNumberOfChannels
; ++i
) {
296 testBuffers
[i
] = createTestBuffer(i
+ 1);
299 // Schedule all the tests.
301 for (var m
= 0; m
< mixingRulesList
.length
; ++m
) {
302 var mixingRules
= mixingRulesList
[m
];
303 for (var i
= 0; i
< connectionsList
.length
; ++i
, ++testNumber
) {
304 scheduleTest(testNumber
, connectionsList
[i
], mixingRules
.channelCount
, mixingRules
.channelCountMode
, mixingRules
.channelInterpretation
);
308 // Render then check results.
309 context
.oncomplete
= checkResult
;
310 context
.startRendering();