OLD | NEW |
1 <!DOCTYPE html> | 1 <!DOCTYPE html> |
2 | |
3 <!-- | 2 <!-- |
4 Tests that AudioBufferSourceNode supports loop-points with .loopStart and .loopE
nd. | 3 Tests that AudioBufferSourceNode supports loop-points with .loopStart and .loopE
nd. |
5 --> | 4 --> |
| 5 <html> |
| 6 <head> |
| 7 <title> |
| 8 audiobuffersource-loop-points.html |
| 9 </title> |
| 10 <script src="../../resources/testharness.js"></script> |
| 11 <script src="../../resources/testharnessreport.js"></script> |
| 12 <script src="../resources/audit-util.js"></script> |
| 13 <script src="../resources/audit.js"></script> |
| 14 </head> |
| 15 <body> |
| 16 <script id="layout-test-code"> |
| 17 let audit = Audit.createTaskRunner(); |
6 | 18 |
7 <html> | 19 let sampleRate = 44100.0; |
8 <head> | 20 let numberOfNotes = 60; // play over a 5 octave range |
9 <script src="../../resources/testharness.js"></script> | 21 let noteDuration = 0.025; |
10 <script src="../../resources/testharnessreport.js"></script> | 22 let noteSpacing = |
11 <script src="../resources/audit-util.js"></script> | 23 noteDuration + 0.005; // leave 5ms of silence between each "note" |
12 <script src="../resources/audit.js"></script> | 24 let lengthInSeconds = numberOfNotes * noteSpacing; |
13 | 25 |
14 </head> | 26 let context = 0; |
15 <body> | 27 let expectedAudio; |
16 | 28 |
17 <script> | 29 function createTestBuffer(frequency, sampleRate) { |
18 let audit = Audit.createTaskRunner(); | 30 // Create a buffer containing two periods at this frequency. |
| 31 // The 1st half is a pure sine wave period scaled by a linear ramp from |
| 32 // 0 -> 1. The 2nd half of the buffer corresponds exactly to one pure |
| 33 // sine wave period. |
| 34 let onePeriodDuration = 1 / frequency; |
| 35 let sampleFrameLength = 2 * onePeriodDuration * sampleRate; |
19 | 36 |
20 let sampleRate = 44100.0; | 37 let audioBuffer = |
21 let numberOfNotes = 60; // play over a 5 octave range | 38 context.createBuffer(1, sampleFrameLength, sampleRate); |
22 let noteDuration = 0.025; | |
23 let noteSpacing = | |
24 noteDuration + 0.005; // leave 5ms of silence between each "note" | |
25 let lengthInSeconds = numberOfNotes * noteSpacing; | |
26 | 39 |
27 let context = 0; | 40 let n = audioBuffer.length; |
28 let expectedAudio; | 41 let channelData = audioBuffer.getChannelData(0); |
29 | 42 |
30 function createTestBuffer(frequency, sampleRate) { | 43 for (let i = 0; i < n; ++i) { |
31 // Create a buffer containing two periods at this frequency. | 44 let sample = Math.sin(frequency * 2.0 * Math.PI * i / sampleRate); |
32 // The 1st half is a pure sine wave period scaled by a linear ramp from 0 -> | |
33 // 1. The 2nd half of the buffer corresponds exactly to one pure sine wave | |
34 // period. | |
35 let onePeriodDuration = 1 / frequency; | |
36 let sampleFrameLength = 2 * onePeriodDuration * sampleRate; | |
37 | 45 |
38 let audioBuffer = context.createBuffer(1, sampleFrameLength, sampleRate); | 46 // Linear ramp from 0 -> 1 for the first period. |
| 47 // Stay at 1 for the 2nd period. |
| 48 let scale = i < n / 2 ? i / (n / 2) : 1; |
| 49 sample *= scale; |
39 | 50 |
40 let n = audioBuffer.length; | 51 channelData[i] = sample; |
41 let channelData = audioBuffer.getChannelData(0); | 52 } |
42 | 53 |
43 for (let i = 0; i < n; ++i) { | 54 return audioBuffer; |
44 let sample = Math.sin(frequency * 2.0 * Math.PI * i / sampleRate); | |
45 | |
46 // Linear ramp from 0 -> 1 for the first period. | |
47 // Stay at 1 for the 2nd period. | |
48 let scale = i < n / 2 ? i / (n / 2) : 1; | |
49 sample *= scale; | |
50 | |
51 channelData[i] = sample; | |
52 } | |
53 | |
54 return audioBuffer; | |
55 } | |
56 | |
57 function playNote(buffer, time, duration, playbackRate) { | |
58 let source = context.createBufferSource(); | |
59 source.buffer = buffer; | |
60 source.playbackRate.value = playbackRate; | |
61 | |
62 let gainNode = context.createGain(); | |
63 source.connect(gainNode); | |
64 gainNode.connect(context.destination); | |
65 | |
66 // Loop the 2nd half of the buffer. | |
67 // We should be able to hear any problems as glitches if the looping | |
68 // incorrectly indexes to anywhere outside of the desired loop-points, since | |
69 // only the 2nd half is a perfect sine-wave cycle, while the 1st half of the | |
70 // buffer contains a linear ramp of a sine-wave cycle. | |
71 source.loop = true; | |
72 source.loopStart = 0.5 * buffer.duration; | |
73 source.loopEnd = buffer.duration; | |
74 | |
75 // Play for the given duration. | |
76 source.start(time); | |
77 source.stop(time + duration); | |
78 | |
79 // Apply a quick linear fade-out to avoid a click at the end of the note. | |
80 gainNode.gain.value = 1; | |
81 gainNode.gain.setValueAtTime(1, time + duration - 0.005); | |
82 gainNode.gain.linearRampToValueAtTime(0, time + duration); | |
83 } | |
84 | |
85 audit.define( | |
86 {label: 'initialize', description: 'Set up context and expected results'}, | |
87 (task, should) => { | |
88 // Create offline audio context. | |
89 should( | |
90 () => {context = new OfflineAudioContext( | |
91 2, sampleRate * lengthInSeconds, sampleRate)}, | |
92 'Creating context for testing') | |
93 .notThrow(); | |
94 | |
95 should( | |
96 Audit.loadFileFromUrl('audiobuffersource-loop-points-expected.wav') | |
97 .then(arrayBuffer => { | |
98 context.decodeAudioData(arrayBuffer).then(audioBuffer => { | |
99 expectedAudio = audioBuffer; | |
100 }); | |
101 }), | |
102 'Fetching expected audio') | |
103 .beResolved() | |
104 .then(() => task.done()); | |
105 }); | |
106 | |
107 audit.define( | |
108 { | |
109 label: 'test', | |
110 description: 'Test loop points and compare with expected results' | |
111 }, | |
112 (task, should) => { | |
113 // Create the test buffer. | |
114 // We'll loop this with the loop-points set for the 2nd half of this | |
115 // buffer. | |
116 let buffer = createTestBuffer(440.0, sampleRate); | |
117 | |
118 // Play all the notes as a chromatic scale. | |
119 for (let i = 0; i < numberOfNotes; ++i) { | |
120 let time = i * noteSpacing; | |
121 // start three octaves down | |
122 let semitone = i - numberOfNotes / 2; | |
123 | |
124 // Convert from semitone to rate. | |
125 let playbackRate = Math.pow(2, semitone / 12); | |
126 | |
127 playNote(buffer, time, noteDuration, playbackRate); | |
128 } | 55 } |
129 | 56 |
130 context.startRendering() | 57 function playNote(buffer, time, duration, playbackRate) { |
131 .then(renderedAudio => { | 58 let source = context.createBufferSource(); |
132 // Compute a threshold based on the maximum error, |maxUlp|, in ULP. | 59 source.buffer = buffer; |
133 // This is experimentally determined. Assuming that the reference | 60 source.playbackRate.value = playbackRate; |
134 // file is a 16-bit wav file, the max values in the wave file | |
135 // are +/- 32768. | |
136 let maxUlp = 0.9999; | |
137 let threshold = maxUlp / 32768; | |
138 | 61 |
139 for (let k = 0; k < renderedAudio.numberOfChannels; ++k) { | 62 let gainNode = context.createGain(); |
140 should( | 63 source.connect(gainNode); |
141 renderedAudio.getChannelData(k), | 64 gainNode.connect(context.destination); |
142 'Rendered audio for channel ' + k) | 65 |
143 .beCloseToArray( | 66 // Loop the 2nd half of the buffer. |
144 expectedAudio.getChannelData(k), | 67 // We should be able to hear any problems as glitches if the looping |
145 {absoluteThreshold: threshold}); | 68 // incorrectly indexes to anywhere outside of the desired loop-points, |
| 69 // since only the 2nd half is a perfect sine-wave cycle, while the 1st |
| 70 // half of the buffer contains a linear ramp of a sine-wave cycle. |
| 71 source.loop = true; |
| 72 source.loopStart = 0.5 * buffer.duration; |
| 73 source.loopEnd = buffer.duration; |
| 74 |
| 75 // Play for the given duration. |
| 76 source.start(time); |
| 77 source.stop(time + duration); |
| 78 |
| 79 // Apply a quick linear fade-out to avoid a click at the end of the |
| 80 // note. |
| 81 gainNode.gain.value = 1; |
| 82 gainNode.gain.setValueAtTime(1, time + duration - 0.005); |
| 83 gainNode.gain.linearRampToValueAtTime(0, time + duration); |
| 84 } |
| 85 |
| 86 audit.define( |
| 87 { |
| 88 label: 'initialize', |
| 89 description: 'Set up context and expected results' |
| 90 }, |
| 91 (task, should) => { |
| 92 // Create offline audio context. |
| 93 should( |
| 94 () => {context = new OfflineAudioContext( |
| 95 2, sampleRate * lengthInSeconds, sampleRate)}, |
| 96 'Creating context for testing') |
| 97 .notThrow(); |
| 98 |
| 99 should( |
| 100 Audit |
| 101 .loadFileFromUrl( |
| 102 'audiobuffersource-loop-points-expected.wav') |
| 103 .then(arrayBuffer => { |
| 104 context.decodeAudioData(arrayBuffer).then(audioBuffer => { |
| 105 expectedAudio = audioBuffer; |
| 106 }); |
| 107 }), |
| 108 'Fetching expected audio') |
| 109 .beResolved() |
| 110 .then(() => task.done()); |
| 111 }); |
| 112 |
| 113 audit.define( |
| 114 { |
| 115 label: 'test', |
| 116 description: 'Test loop points and compare with expected results' |
| 117 }, |
| 118 (task, should) => { |
| 119 // Create the test buffer. |
| 120 // We'll loop this with the loop-points set for the 2nd half of this |
| 121 // buffer. |
| 122 let buffer = createTestBuffer(440.0, sampleRate); |
| 123 |
| 124 // Play all the notes as a chromatic scale. |
| 125 for (let i = 0; i < numberOfNotes; ++i) { |
| 126 let time = i * noteSpacing; |
| 127 // start three octaves down |
| 128 let semitone = i - numberOfNotes / 2; |
| 129 |
| 130 // Convert from semitone to rate. |
| 131 let playbackRate = Math.pow(2, semitone / 12); |
| 132 |
| 133 playNote(buffer, time, noteDuration, playbackRate); |
146 } | 134 } |
147 }) | |
148 .then(() => task.done()); | |
149 }); | |
150 | 135 |
151 audit.run(); | 136 context.startRendering() |
| 137 .then(renderedAudio => { |
| 138 // Compute a threshold based on the maximum error, |maxUlp|, |
| 139 // in ULP. This is experimentally determined. Assuming that |
| 140 // the reference file is a 16-bit wav file, the max values in |
| 141 // the wave file are +/- 32768. |
| 142 let maxUlp = 0.9999; |
| 143 let threshold = maxUlp / 32768; |
152 | 144 |
153 </script> | 145 for (let k = 0; k < renderedAudio.numberOfChannels; ++k) { |
| 146 should( |
| 147 renderedAudio.getChannelData(k), |
| 148 'Rendered audio for channel ' + k) |
| 149 .beCloseToArray( |
| 150 expectedAudio.getChannelData(k), |
| 151 {absoluteThreshold: threshold}); |
| 152 } |
| 153 }) |
| 154 .then(() => task.done()); |
| 155 }); |
154 | 156 |
155 </body> | 157 audit.run(); |
| 158 </script> |
| 159 </body> |
156 </html> | 160 </html> |
OLD | NEW |