Commit 2cbc3cc4 authored by Thomas Guilbert's avatar Thomas Guilbert Committed by Commit Bot

Reland preservesPitch test deflake, fix timeouts

This CL relands "Deflake preservesPitch tests", which was reverted by:
commit f19f3404.

It also attempts to incorporate some of the WPT changes made upstream,
but not yet landed, in:
https://github.com/web-platform-tests/wpt/pull/24599

Specifically, it fixes typos in the Safari prefixes, and explicitly
starts the audio context. However, it is different from the PR, since
it reuses the same Audio element, MediaElementAudioSourceNode and
AudioContext. This is an attempt to cut down on overhead costs and
fix test timeouts. We instead reset the audio.currentTime to 0, and
only create a new analyser node for each test.

Bug: 1105877, 1096238
Change-Id: Ie528ec0b7c38d9df59fcb04696c810e6d1c232f6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2300929
Commit-Queue: Thomas Guilbert <tguilbert@chromium.org>
Auto-Submit: Thomas Guilbert <tguilbert@chromium.org>
Reviewed-by: default avatarPhilip Jägenstedt <foolip@chromium.org>
Cr-Commit-Position: refs/heads/master@{#789262}
parent 16a88111
...@@ -4,11 +4,14 @@ window.AudioContext = window.AudioContext || window.webkitAudioContext; ...@@ -4,11 +4,14 @@ window.AudioContext = window.AudioContext || window.webkitAudioContext;
var FFT_SIZE = 2048; var FFT_SIZE = 2048;
function getPitchDetector(media, t) { var audioContext;
var audioContext = new AudioContext(); var sourceNode;
t.add_cleanup(() => audioContext.close());
var sourceNode = audioContext.createMediaElementSource(media); function getPitchDetector(media) {
if(!audioContext) {
audioContext = new AudioContext();
sourceNode = audioContext.createMediaElementSource(media);
}
var analyser = audioContext.createAnalyser(); var analyser = audioContext.createAnalyser();
analyser.fftSize = FFT_SIZE; analyser.fftSize = FFT_SIZE;
...@@ -16,14 +19,22 @@ function getPitchDetector(media, t) { ...@@ -16,14 +19,22 @@ function getPitchDetector(media, t) {
sourceNode.connect(analyser); sourceNode.connect(analyser);
analyser.connect(audioContext.destination); analyser.connect(audioContext.destination);
// Returns the frequency value for the nth FFT bin. return {
var binConverter = (bin) => audioContext.sampleRate*(bin/FFT_SIZE); ensureStart() { return audioContext.resume(); },
detect() { return getPitch(analyser); },
return () => getPitch(analyser, binConverter); cleanup() {
sourceNode.disconnect();
analyser.disconnect();
},
};
} }
function getPitch(analyser, binConverter) { function getPitch(analyser) {
var buf = new Uint8Array(FFT_SIZE/2); // Returns the frequency value for the nth FFT bin.
var binConverter = (bin) =>
(audioContext.sampleRate/2)*((bin)/(analyser.frequencyBinCount-1));
var buf = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(buf); analyser.getByteFrequencyData(buf);
return findDominantFrequency(buf, binConverter); return findDominantFrequency(buf, binConverter);
} }
...@@ -40,9 +51,8 @@ function findDominantFrequency(buf, binConverter) { ...@@ -40,9 +51,8 @@ function findDominantFrequency(buf, binConverter) {
} }
} }
// The distance between bins is always constant and corresponds to // The spread of frequencies within bins is constant and corresponds to
// (1/FFT_SIZE)th of the sample rate. Use the frequency value of the 1st bin // (1/(FFT_SIZE-1))th of the sample rate. Use the value of bin #1 as a
// as the margin directly, instead of calculating an average from the values // shorthand for that value.
// of the neighboring bins.
return { value:binConverter(bin), margin:binConverter(1) }; return { value:binConverter(bin), margin:binConverter(1) };
} }
\ No newline at end of file
...@@ -15,8 +15,8 @@ function getPreservesPitch(audio) { ...@@ -15,8 +15,8 @@ function getPreservesPitch(audio) {
if ("mozPreservesPitch" in HTMLAudioElement.prototype) { if ("mozPreservesPitch" in HTMLAudioElement.prototype) {
return audio.mozPreservesPitch; return audio.mozPreservesPitch;
} }
if ("wekbitPreservesPitch" in HTMLAudioElement.prototype) { if ("webkitPreservesPitch" in HTMLAudioElement.prototype) {
return audio.wekbitPreservesPitch; return audio.webkitPreservesPitch;
} }
return undefined; return undefined;
} }
...@@ -27,8 +27,8 @@ function setPreservesPitch(audio, value) { ...@@ -27,8 +27,8 @@ function setPreservesPitch(audio, value) {
audio.preservesPitch = value; audio.preservesPitch = value;
} else if ("mozPreservesPitch" in HTMLAudioElement.prototype) { } else if ("mozPreservesPitch" in HTMLAudioElement.prototype) {
audio.mozPreservesPitch = value; audio.mozPreservesPitch = value;
} else if ("wekbitPreservesPitch" in HTMLAudioElement.prototype) { } else if ("webkitPreservesPitch" in HTMLAudioElement.prototype) {
audio.wekbitPreservesPitch = value; audio.webkitPreservesPitch = value;
} }
} }
...@@ -37,51 +37,82 @@ test(function(t) { ...@@ -37,51 +37,82 @@ test(function(t) {
}, "Test that preservesPitch is present and unprefixed."); }, "Test that preservesPitch is present and unprefixed.");
test(function(t) { test(function(t) {
let audio = document.createElement('audio'); let defaultAudio = document.createElement('audio');
assert_true(getPreservesPitch(audio)); assert_true(getPreservesPitch(defaultAudio));
setPreservesPitch(audio, false); setPreservesPitch(defaultAudio, false);
assert_false(getPreservesPitch(audio)); assert_false(getPreservesPitch(defaultAudio));
}, "Test that presevesPitch is on by default"); }, "Test that preservesPitch is on by default");
function testPreservesPitch(preservesPitch, playbackRate, expectedPitch, description) {
promise_test(async t => {
let audio = document.createElement('audio');
var detector = getPitchDetector(audio, t); var audio;
function addTestCleanups(t, detector) {
t.add_cleanup(() => {
audio.pause();
audio.currentTime = 0;
});
t.add_cleanup(() => detector.cleanup());
}
// This file contains 5 seconds of a 440hz sine wave. function testPreservesPitch(preservesPitch, playbackRate, expectedPitch, description) {
audio.src = "/media/sine440.mp3"; promise_test(async t => {
let detector = getPitchDetector(audio);
addTestCleanups(t, detector);
audio.playbackRate = playbackRate; audio.playbackRate = playbackRate;
setPreservesPitch(audio, preservesPitch); setPreservesPitch(audio, preservesPitch);
function promiseTimeUpdate() { function waitUntil(time) {
return new Promise((resolve) => audio.ontimeupdate = resolve); return new Promise((resolve) => {
audio.ontimeupdate = () => {
if (audio.currentTime >= time) {
resolve();
}
};
});
} }
function verifyPitch() { // Wait until we have played some audio. Otherwise, the detector
var pitch = detector(); // might return a pitch of 0Hz.
audio.play();
await waitUntil(0.25);
// 25Hz is larger than the margin we get from 48kHz and 44.1kHz var pitch = detector.detect();
// audio being analyzed by a FFT of size 2048. If we get something
// different, there is an error within the test's calculations (or
// we might be dealing a larger sample rate).
assert_less_than(pitch.margin, 25,
"Test error: the margin should be reasonably small.")
assert_approx_equals(pitch.value, expectedPitch, pitch.margin, // 25Hz is larger than the margin we get from 48kHz and 44.1kHz
"The actual pitch should be close to the expected pitch."); // audio being analyzed by a FFT of size 2048. If we get something
} // different, there is an error within the test's calculations (or
// we might be dealing a larger sample rate).
assert_less_than(pitch.margin, 25,
"Test error: the margin should be reasonably small.")
assert_approx_equals(pitch.value, expectedPitch, pitch.margin,
"The actual pitch should be close to the expected pitch.");
await test_driver.bless("Play audio element", () => audio.play() )
.then(promiseTimeUpdate)
.then(verifyPitch);
}, description); }, description);
} }
var REFERENCE_PITCH = 440; var REFERENCE_PITCH = 440;
promise_test(async t => {
// Create the audio element only once, in order to lower the chances of
// tests timing out.
audio = document.createElement('audio');
// This file contains 5 seconds of a 440hz sine wave.
audio.src = "/media/sine440.mp3";
let detector = getPitchDetector(audio);
addTestCleanups(t, detector);
// The first time we run the test, we need to interact with the
// AudioContext and Audio element via user gestures.
await test_driver.bless("Play audio element", () => {
return Promise.all([audio.play(), detector.ensureStart()]);
});
}, "Setup Audio element and AudioContext")
testPreservesPitch(true, 1.0, REFERENCE_PITCH, testPreservesPitch(true, 1.0, REFERENCE_PITCH,
"The default playbackRate should not affect pitch") "The default playbackRate should not affect pitch")
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment