test_seamless_looping.html (6374B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>Test for seamless loop of HTMLAudioElements</title> 5 <script src="/tests/SimpleTest/SimpleTest.js"></script> 6 <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> 7 <script type="text/javascript" src="manifest.js"></script> 8 </head> 9 <body> 10 <canvas id="canvas" width="300" height="300"></canvas> 11 <script type="application/javascript"> 12 /** 13 * This test is used to ensure every time we loop audio, the audio can loop 14 * seamlessly which means there won't have any silenece or noise between the 15 * end and the start. 16 */ 17 18 SimpleTest.waitForExplicitFinish(); 19 20 // Set DEBUG to true to add a canvas with a little drawing of what is going 21 // on, and actually outputs the audio to the speakers. 22 var DEBUG = true; 23 var LOOPING_COUNT = 0; 24 var MAX_LOOPING_COUNT = 10; 25 // Test files are at 44100Hz, files are one second long, and contain therefore 26 // 100 periods 27 var TONE_FREQUENCY = 441; 28 29 (async function testSeamlesslooping() { 30 let wavFileURL = { 31 name: URL.createObjectURL(new Blob([createSrcBuffer()], { type: 'audio/wav' 32 })), 33 type: "audio/wav" 34 }; 35 36 let testURLs = gSeamlessLoopingTests.splice(0) 37 testURLs.push(wavFileURL); 38 for (let testFile of testURLs) { 39 LOOPING_COUNT = 0; 40 info(`- create looping audio element ${testFile.name}`); 41 let audio = createAudioElement(testFile.name); 42 43 info(`- start audio and analyze audio wave data to ensure looping audio without any silence or noise -`); 44 await playAudioAndStartAnalyzingWaveData(audio); 45 46 info(`- test seamless looping multiples times -`); 47 for (LOOPING_COUNT = 0; LOOPING_COUNT < MAX_LOOPING_COUNT; LOOPING_COUNT++) { 48 await once(audio, "seeked"); 49 info(`- the round ${LOOPING_COUNT} of the seamless looping succeeds -`); 50 } 51 window.audio.remove(); 52 window.ac.close(); 53 } 54 55 info(`- end of seamless looping test -`); 56 SimpleTest.finish(); 57 })(); 58 59 /** 60 * Test utility functions 61 */ 62 function createSrcBuffer() { 63 // Generate the sine in floats, then convert, for simplicity. 64 let channels = 1; 65 let sampleRate = 44100; 66 let buffer = new Float32Array(sampleRate * channels); 67 let phase = 0; 68 const TAU = 2 * Math.PI; 69 for (let i = 0; i < buffer.length; i++) { 70 // Adjust the gain a little so we're sure it's not going to clip. This is 71 // important because we're converting to 16bit integer right after, and 72 // clipping will clearly introduce a discontinuity that will be 73 // mischaracterized as a looping click. 74 buffer[i] = Math.sin(phase) * 0.99; 75 phase += TAU * TONE_FREQUENCY / 44100; 76 if (phase > 2 * TAU) { 77 phase -= TAU; 78 } 79 } 80 81 // Make a RIFF header, it's 23 bytes 82 let buf = new Int16Array(buffer.length + 23); 83 buf[0] = 0x4952; 84 buf[1] = 0x4646; 85 buf[2] = (2 * buffer.length + 15) & 0x0000ffff; 86 buf[3] = ((2 * buffer.length + 15) & 0xffff0000) >> 16; 87 buf[4] = 0x4157; 88 buf[5] = 0x4556; 89 buf[6] = 0x6d66; 90 buf[7] = 0x2074; 91 buf[8] = 0x0012; 92 buf[9] = 0x0000; 93 buf[10] = 0x0001; 94 buf[11] = 1; 95 buf[12] = 44100 & 0x0000ffff; 96 buf[13] = (44100 & 0xffff0000) >> 16; 97 buf[14] = (2 * channels * sampleRate) & 0x0000ffff; 98 buf[15] = ((2 * channels * sampleRate) & 0xffff0000) >> 16; 99 buf[16] = 0x0004; 100 buf[17] = 0x0010; 101 buf[18] = 0x0000; 102 buf[19] = 0x6164; 103 buf[20] = 0x6174; 104 buf[21] = (2 * buffer.length) & 0x0000ffff; 105 buf[22] = ((2 * buffer.length) & 0xffff0000) >> 16; 106 107 // convert to int16 and copy. 108 for (let i = 0; i < buffer.length; i++) { 109 buf[i + 23] = Math.round(buffer[i] * (1 << 15)); 110 } 111 return buf; 112 } 113 114 function createAudioElement(url) { 115 /* global audio */ 116 window.audio = document.createElement("audio"); 117 audio.src = url; 118 audio.controls = true; 119 audio.loop = true; 120 document.body.appendChild(audio); 121 return audio; 122 } 123 124 async function playAudioAndStartAnalyzingWaveData(audio) { 125 createAudioWaveAnalyser(audio); 126 ok(await once(audio, "canplay").then(() => true, () => false), 127 `audio can start playing.`) 128 ok(await audio.play().then(() => true, () => false), 129 `audio started playing successfully.`); 130 } 131 132 function createAudioWaveAnalyser(source) { 133 /* global ac, analyser */ 134 window.ac = new AudioContext(); 135 window.analyser = ac.createAnalyser(); 136 analyser.frequencyBuf = new Float32Array(analyser.frequencyBinCount); 137 analyser.smoothingTimeConstant = 0; 138 analyser.fftSize = 2048; // 1024 bins 139 140 let sourceNode = ac.createMediaElementSource(source); 141 sourceNode.connect(analyser); 142 143 if (DEBUG) { 144 analyser.connect(ac.destination); 145 analyser.timeDomainBuf = new Float32Array(analyser.frequencyBinCount); 146 let cvs = document.querySelector("canvas"); 147 analyser.c = cvs.getContext("2d"); 148 analyser.w = cvs.width; 149 analyser.h = cvs.height; 150 } 151 152 analyser.notifyAnalysis = () => { 153 if (LOOPING_COUNT >= MAX_LOOPING_COUNT) { 154 return; 155 } 156 let {frequencyBuf} = analyser; 157 analyser.getFloatFrequencyData(frequencyBuf); 158 // Let things stabilize at the beginning. See bug 1441509. 159 if (LOOPING_COUNT > 1) { 160 analyser.doAnalysis(frequencyBuf, ac.sampleRate); 161 } 162 163 if (DEBUG) { 164 let {c, w, h, timeDomainBuf} = analyser; 165 c.clearRect(0, 0, w, h); 166 analyser.getFloatTimeDomainData(timeDomainBuf); 167 for (let i = 0; i < frequencyBuf.length; i++) { 168 c.fillRect(i, h, 1, -frequencyBuf[i] + analyser.minDecibels); 169 } 170 171 for (let i = 0; i < timeDomainBuf.length; i++) { 172 c.fillRect(i, h / 2, 1, -timeDomainBuf[i] * h / 2); 173 } 174 } 175 176 requestAnimationFrame(analyser.notifyAnalysis); 177 } 178 179 analyser.doAnalysis = (buf, ctxSampleRate) => { 180 // The size of an FFT is twice the number of bins in its output. 181 let fftSize = 2 * buf.length; 182 // first find a peak where we expect one. 183 let binIndexTone = 1 + Math.round(TONE_FREQUENCY * fftSize / ctxSampleRate); 184 ok(buf[binIndexTone] > -35, 185 `Could not find a peak: ${buf[binIndexTone]} db at ${TONE_FREQUENCY}Hz 186 (${source.src})`); 187 188 // check that the energy some octaves higher is very low. 189 let binIndexOutsidePeak = 1 + Math.round(TONE_FREQUENCY * 4 * buf.length / ctxSampleRate); 190 ok(buf[binIndexOutsidePeak] < -84, 191 `Found unexpected high frequency content: ${buf[binIndexOutsidePeak]}db 192 at ${TONE_FREQUENCY * 4}Hz (${source.src})`); 193 } 194 195 analyser.notifyAnalysis(); 196 } 197 </script> 198 </body> 199 </html>