Reputation: 2306
I'm using the WebAudio API to crossfade between multiple sources. Fades are queued using setValueCurveAtTime(curve, time, duration). The WebAudio spec indicates that any subsequent call to setValueCurveAtTime with overlapping durations is not allowed. So I'm calling cancelScheduledValues(time) before queuing up new fades. Both Firefox v68 and Chrome v77 throw errors on the second setValueCurveAtTime call however.
The attached snippet contains the minimum amount of code to trigger the errors in either browser. Click on Start to trigger the error. Note that it doesn't play any audio as it's not needed for the error to be thrown. The select dropdown allow control of the time
argument to both functions. In Chrome v77 time=0
doesn't trigger an error.
Any ideas on how to get this to work in both browsers would be much appreciated!
Update: As Raymond Toy pointed out cancelScheduledValues(t) appears to cancel automations which started at t
or later (not simply active during t
). By using cancelScheduledValues(Math.max(t - duration, 0))
the code now appears to work in Chrome. Firefox still fails with a Operation is not supported
error however.
<!DOCTYPE html>
<html>
<body>
<button id="start">Start</button>
<select id="time">
<option value="0">time=0</option>
<option value="1">time=currentTime</option>
</select>
<pre id="log"></pre>
<script>
const select = document.querySelector('#time')
const log = document.querySelector('#log')
function start() {
const ctx = new AudioContext()
ctx.resume()
const gain = ctx.createGain()
gain.connect(ctx.destination)
// Fade in
gain.gain.setValueCurveAtTime(new Float32Array([0, 1]), 0, 1)
setTimeout(() => {
const time = select.options[select.selectedIndex].value === '0' ? 0 : ctx.currentTime
// Replace fade in with fade out
// THIS IS THE CALL THAT DOESN'T WORK =====
// Doesn't work in Firefox nor Chrome:
// gain.gain.cancelScheduledValues(time)
// Doesn't work in Firefox:
gain.gain.cancelScheduledValues(Math.max(time - 1 /* duration of previous fade */, 0))
try {
// ERROR IS THROWN HERE =================
gain.gain.setValueCurveAtTime(new Float32Array([0, 1]), time, 1)
} catch (error) {
log.prepend(error.message + '\n')
throw error
}
log.prepend('No error!\n')
}, 100)
}
document.querySelector('#start').addEventListener('click', start)
</script>
</body>
</html>
Upvotes: 0
Views: 322
Reputation: 2306
In the end I ended up ditching setValueCurveAtTime()
in favour of multiple setValueAtTime
calls:
function fade(gainNode, fadeIn, startTime = 0) {
const duration = 1 // seconds
const delta = 1 / 100 // number of volume changes per second
const targetVolume = fadeIn ? 1 : 0
const currentTime = audioContext.currentTime
if (!startTime) { startTime = currentTime }
// We can only read current volume for current startTime, so if queueing a fade
// we'll have to assume it starts in the other end
const startingVolume = startTime && startTime !== currentTime
? 1 - targetVolume
: gainNode.gain.value
// Offset to start at when startingVolume isn't 0 or 1
let tOffset = equalPowerEasingInverse(startingVolume)
if (fadeIn) tOffset = 1 - tOffset
let t = 0 // time iterator [0..1]
try {
// Cancel any potentially overlapping fade automations
gainNode.gain.cancelScheduledValues(Math.max(startTime - duration, 0))
for (; t + tOffset <= 1; t += delta) {
// Queue volume change
gainNode.gain.setValueAtTime(
equalPowerEasing(t + tOffset, fadeIn),
startTime + t*duration
)
}
// Ensure final value is exact
gainNode.gain.setValueAtTime(targetVolume, startTime + t*duration)
} catch (error) {
if (/Failed to execute 'setValueCurveAtTime'|Operation is not supported/.test(error.message)) {
// Ignore Chrome + Firefox errors
} else {
throw error
}
}
}
function equalPowerEasing(t, invert = true) {
if (invert) t = 1 - t
return Math.cos(t * 0.5 * Math.PI)
}
function equalPowerEasingInverse(x) {
return Math.acos(x) / 0.5 / Math.PI
}
Upvotes: 0
Reputation: 6048
My reading of cancelScheduledValues shows that this is working as intended. The event time for a setValueCurveAtTime(curve, time, duration)
is time
. cancelScheduledValues(t2)
removes all events whose event time is t2
or greater. In your test case, time
= 0, and t2
is currentTime
which is greater than 0. Thus, nothing is removed from the timeline. The second call to setValueCurveAtTime
inserts a new event that does overlap the previous one. Hence, you get an error.
Having said that, I think this is kind of unexpected. This could be an error in the WebAudio spec.
Upvotes: 1