Using WebAudio to play a sequence of notes - how to stop asynchronously? - javascript

I am using WebAudio to play a sequence of notes. I have a playNote function which works well; I send it note frequency and start and stop times for each note. The generation of the sequence parameters occurs before the actual sound starts, which is a little confusing. The function just creates an oscillator for every note. (I tried other methods and this is the cleanest).
But I would like to stop the sequence asynchronously (e.g. when an external event occurs). I tried setting up a master Gain node that could be used to gate the output, but it seems it needs to be "inside" the function, so it can't be controlled later on. If I try and turn off my gain object inside the function then it is too late - because the start & stop times have already been passed to the function.
Here is my function:
function playNote(audioContext,frequency, startTime, endTime, last) {
gainNode = audioContext.createGain(); //to get smooth rise/fall
oscillator = audioContext.createOscillator();
oscillator.frequency.value=frequency;
oscillator.connect(gainNode);
gainNode.connect(analyserScale); //analyser is global
analyserScale.connect(audioContext.destination);
gainNode.gain.exponentialRampToValueAtTime(toneOn, startTime + trf);
gainNode.gain.exponentialRampToValueAtTime(toneOff, endTime+trf);
oscillator.start(startTime);
oscillator.stop(endTime);
}
Any help appreciated!

This does it: Web Audio API: Stop all scheduled sounds from playing. The solution is to keep track of the scheduled oscillators with an array.
The function now becomes:
var oscs = []; //list of oscillators
function playNote(audioContext,frequency, startTime, endTime, last, index) {
gainNode = audioContext.createGain(); //to get smooth rise/fall
oscillator = audioContext.createOscillator();
oscillator.frequency.value=frequency;
oscillator.connect(gainNode);
//keep track of alll the oscs so that they can be switched off if scale is stopped by user
oscs[index] = oscillator;
gainNode.connect(analyserScale); //analyser is global
analyserScale.connect(audioContext.destination);
gainNode.gain.exponentialRampToValueAtTime(toneOn, startTime + trf);
gainNode.gain.exponentialRampToValueAtTime(toneOff, endTime+trf);
oscillator.start(startTime);
oscillator.stop(endTime);
}
Then code to stop the oscillators:
for(let i=0; i<oscs.length; i++) {
if(oscs[i]){
oscs[i].stop(0);
}
}

Related

AudioContext.currentTime sometimes freezes for ~230ms shortly after start

I'm creating a music related app using Web Audio API, which requires precisely synchronizing sound, visuals and MIDI input processing.
The sound production follows the pattern described in this article: requestAnimationFrame regularly calls a function that schedules events on the AudioContext. This works fine most of the time, except in some occasions where the audio inexplicably lags behind the visuals.
After much poking around, I ran into the AudioContext.currentTime specification which hints:
Elapsed time in this system corresponds to elapsed time in the audio stream generated by the BaseAudioContext, which may not be synchronized with other clocks in the system.
And indeed I was able to verify that these sporadic delays in the audio come down to a problem in the AudioContext clock itself, which seems to pause for a bit sometimes just after starting up. Note that this doesn't happen every time, but frequently enough to be an issue (maybe 10-15% of the time...). Each time it happens the pattern is the same: currentTime starts increasing then gets stuck at 23ms for a bit, then starts going again regularly without any further issue after accumulating a total of ~230ms of lag behind the system/wall clock...
I've created a simple script which reproduces the problem (if you want to try it, just open the console to see the output, and press any key to start the test... as the issue is sporadic you may need to retry or sometimes reload multiple times before it happens...):
<script>
const audioContext = new AudioContext();
// reference points for both clocks (JS and audioContext)
var animStartTime = null;
var audioStartTime = null;
// loop function to be called by requestAnimationFrame
function play(timestamp) {
// set animStartTime on first invocation
if (animStartTime == null) animStartTime = timestamp;
// compute elapsed time for both clocks
var animElapsedTime = timestamp - animStartTime;
var audioElapsedTime = (audioContext.currentTime - audioStartTime) * 1000;
console.log('Animation ts: ' + animElapsedTime +
', Audio ts: ' + audioElapsedTime +
', Diff: ' + (animElapsedTime - audioElapsedTime)
);
// keep this going for 1 second
if (animElapsedTime < 1000)
requestAnimationFrame(play);
}
function start() {
audioStartTime = audioContext.currentTime;
animStartTime = null; // use the timestamp provided by requestAnimationFrame
// create a simple oscillator and schedule it to produce a single beep when it starts
const osc = audioContext.createOscillator();
osc.frequency.value = 800;
osc.connect(audioContext.destination);
osc.start(audioStartTime);
osc.stop(audioStartTime + 0.03);
// launch animation loop
requestAnimationFrame(play);
}
// press any key to start the test
window.addEventListener('keypress', function(e) {start()});
</script>
And here is the console output of a bogus run, where you can see the Audio timestamp freezing up to a delay of ~230ms behind the JS main thread timestamp before starting again:
Could someone explain to me:
What is going on? Why is this freezing sporadically?
Is there anything I can do to prevent this? I can think of some ways to mitigate the issue if I can get convinced that it happens only when starting up, but without fully understanding the root cause I fear seeing these freezes happen again at other times...
This is probably https://crbug.com/693978 Resuming the context may take "some" time.
Unfortunately, they don't really wait for the context has started before resolving the Promise returned by context.resume(), so we have to resort to ugly workarounds.
One such workaround would be to wait for currentTime to start updating after context.resume() resolves before starting your animation.
const audioContext = new AudioContext();
// reference points for both clocks (JS and audioContext)
var animStartTime = null;
var audioStartTime = null;
// loop function to be called by requestAnimationFrame
function play(timestamp) {
// set animStartTime on first invocation
if (animStartTime == null) animStartTime = timestamp;
// compute elapsed time for both clocks
var animElapsedTime = timestamp - animStartTime;
var audioElapsedTime = (audioContext.currentTime - audioStartTime) * 1000;
console.log('Animation ts: ' + animElapsedTime +
', Audio ts: ' + audioElapsedTime +
', Diff: ' + (animElapsedTime - audioElapsedTime)
);
// keep this going for 1 second
if (animElapsedTime < 1000)
requestAnimationFrame(play);
}
async function start() {
// wait for the context to resume
// needs to be there, for we still handle the user-gesture
await audioContext.resume();
// though resume() might be a lie in Chrome,
// so we also wait for currentTime to update
const startTime = audioContext.currentTime;
while (startTime === audioContext.currentTime) {
await new Promise((res) => setTimeout(res));
}
// now our AudioContext is ready.
audioStartTime = audioContext.currentTime;
animStartTime = null; // use the timestamp provided by requestAnimationFrame
// create a simple oscillator and schedule it to produce a single beep when it starts
const osc = audioContext.createOscillator();
osc.frequency.value = 800;
osc.connect(audioContext.destination);
osc.start(0);
osc.stop(0.3);
// launch animation loop
requestAnimationFrame(play);
}
// using a button for a clearer snippet
document.querySelector("button")
.addEventListener('click', start);
<button>start</button>

Using while (true) in async?

I have a task in Javascript that requires continuous execution. I have a list of audio files stored as buffers (audioBuffer), I wish to play. However there are audio files being constantly appended to that list, so I use a while loop to monitor it. I constantly get the first buffer, play its audio, and dequeue it. However, when I run this function, it makes my browser hang, even though the loop is in an async wrapper. Why is this? Shouldn't async largely prevent my browser from freezing?
Code:
function playAudioQueue() {
const player = new Audio();
(async () => {
while (true) {
const audioBuffer = audioQueue[0];
if (audioBuffer) {
const base64Audio = base64Prefix + arrayBufferToBase64(audioBuffer);
player.src = base64Audio;
await player.play();
audioQueue.shift();
};
};
})();
};
You are basically creating an infinite loop. It will lock up the browser. You need to basically do a queue type thing where it keeps checking without using a loop. You can use ended to know when the file is done playing
const audioQueue = [];
function playNext() {
// grab the next thing to play
const nextFile = audioQueue.shift();
// if nothing there, check again in a short time
if (!nextFile) {
window.setTimeout(playNext, 1);
return;
}
// Create a new player
const player = new Audio();
const base64Audio = base64Prefix + arrayBufferToBase64(nextFile);
// when done, load up next thing to play
player.addEventListener("ended", playNext);
player.src = base64Audio;
player.play();
}
playNext();
According to MDN Web Docs:
A Promise which is resolved when playback has been started, or is rejected if for any reason playback cannot be started.
One problem might be that in your browser it immediately resolves since you're using a data: URL which doesn't need to wait for any network requests.
A bigger logical problem in your code is that you seem to expect that await player.play() will wait until the song finished playing. This is not the case, which most likely is not what you expect.

How to effectively loop short audio samples with vanilla JS to create a drum sequencer?

Trying to create a simple drum machine sequencing app without using any libraries. Basic purpose of the app is to loop audio at specified intervals while some sort of condition is met e.g. pressing the spacebar.
I figured out that looping audio with setInterval isn't really a good idea since it's pretty inconsistent. Found another solution using new Date() but the example requires calling the function with a set duration as an arg. What I'd like to do is use this same code, but have the loop run infinitely until a spacebar keydown or some other condition is met.
Is this possible? Any thoughts on how I can adapt or re-write this to get what I want? I know how to create my trigger for the loop with event handlers, but each keydown or whatever event will trigger a new audio event (causing loops on top of loops) vs. killing the process of the last one which once set to "loop forever" does just that...
function doTimer(totalms, interval, oninstance, kill) {
var time = 0,
start = new Date().getTime();
function instance() {
time += interval;
if (kill) {
} else {
var diff = (new Date().getTime() - start) - time;
window.setTimeout(instance, (interval - diff));
oninstance(time + diff);
}
}
window.setTimeout(instance, interval);
}
function playSample(str){
soundStr = `./audio808/${str}.mp3`;
sound = new Audio(soundStr);
sound.play();
}
doTimer(0, // duration
125, // sets the interval
function() {
playSample("ch");}, // calls the sample to play
true // whether or not it should play
);
You can set the .loop property of AudioNode to true
sound.loop = true;

Java script -All audio plays at once when attempting to play audio one by one in a sequence (through a for loop)

Here is the link to code pen to my project https://codepen.io/RajTheSHow/pen/vZZPqZ
when you press the wrong button it is supposed to play the sequence(with audio and button press in order).however what actually happens is when you press the wrong button it plays all the audio and changes the color at once and doesn't execute what is in the sleep function.you can see the problem in the pen.
the function that runs the sequence when you press the wrong button is below
cal is where the order is stored of the sequence.
//plays the sequence
function callBut(){
onr=0;
for(i=0;i<cal.length;i++){
// or eval ("s"+cal[i])
window["s"+cal[i]].play();
// set the but# to Clicked Color Ccol# then after 1 sec go back to normal color
$("[but="+cal[i]+"]").css("background",window["Ccol"+cal[i]])
sleep(500).then(() => {
// set the button# to Standard color Scol#
$("[but="+cal[i]+"]").css("background",window["Scol"+cal[i]])
});
}
What'd you expect? The loop does not wait for the audio to finish. It's better to use recursion to play the next song.
let arr = cal.map(element => window[`s${element}`]); // asuming 'cal' is something significant
let index = 0;
playAudio(arr[index]);
function playAudio(audio) {
if (!audio || !(audio instanceof Audio)) return;
audio.addEventListener('ended', function() {
index++;
playAudio(arr[index]);
})
audio.play();
}
This is the correct and expected behaviour.
What you want to achieve requires to wait for the end event and only then invoke play on the next clip.
This can be achieved by properly handling the audio events you can look up online and "chaining" them playing one at a time, much like this:
var nextSound;
function playNext()
{
var audio;
if (nextSound >= cal.length)
{
return;
}
audio = $(window["s"+cal[nextSound++]]);
audio.on("ended", playNext);
audio[0].play();
}
nextSound = 0;
playNext();

Adding a timer to my game

I am currently working on a html5 canvas game. I want to add a timer, so that you have a certain amount of time to collect as many items as possible in. I have tried a few ways of doing it, but due to my lack of skill and experience when it comes to javascript I have not yet made it work. So my question is how to do this in an as simple as possible way?
My code:
Thanks in advance!!
requestAnimationFrame is a very efficient way of doing timers in the browser.
Some Benefits of requestAnimationFrame:
automatically synchronizes canvas drawings with the current display refresh cycle,
multiple requestAnimationFrame calls are coordinated,
automatically suspends the loop if the user changes do a different browser tab
each loop automatically receives a highly precise timestamp. Use this timestamp to determine when to trigger animation work (like your end-of-game) and/or to determine how much animation work to do.
Here's how to make it work:
Create 1+ javascript objects. Each object is one timer.
Each timer object defines some work that should be done after a specified delay.
var timers=[];
timers.push({
// this timer fires every 500ms
delay:500,
// fire this timer when requestAnimationFrame's timestamp
// reaches nextFireTime
nextFireTime:0,
// What does this timer do?
// It execute the 'doTimers' function when this timer fires
doFunction:doTimers,
// just for testing: accumulate how many times this timer has fired
counter:0
});
Create an animation loop with requestAnimationFrame
// the animation loop
// The loop automatically receives the currentTime
function timerLoop(currentTime){
// schedule another frame
// this is required to make the loop continue
// (without another requestAnimationFrame the loop stops)
requestAnimationFrame(timerLoop);
// iterate through each timer object
for(var i=0;i<timers.length;i++){
// if the currentTime > this timer's nextFireTime...
// then do the work specified by this timer
if(currentTime>timers[i].nextFireTime){
var t=timers[i];
// increment nextFireTime
t.nextFireTime=currentTime+t.delay;
// do the work specified in this timer
// This timer will call 'doFunction' & send arguments: t,i
t.doFunction(t,i);
}
}
}
Start the animation loop
requestAnimationFrame(timerLoop);
Here's example code and a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var timers=[];
timers.push({delay:50,nextFireTime:0,doFunction:doTimers,counter:0});
timers.push({delay:500,nextFireTime:0,doFunction:doTimers,counter:0});
timers.push({delay:5000,nextFireTime:0,doFunction:doTimers,counter:0});
requestAnimationFrame(timerLoop);
function timerLoop(currentTime){
requestAnimationFrame(timerLoop);
for(var i=0;i<timers.length;i++){
if(currentTime>timers[i].nextFireTime){
var t=timers[i];
t.nextFireTime=currentTime+t.delay;
t.doFunction(t,i);
}
}
}
function doTimers(t,i){
ctx.clearRect(0,100+i*20-20,cw,20);
ctx.fillText('Timer#'+i+' with '+t.delay+'ms delay has fired '+(++t.counter)+' times.',20,100+20*i);
}
body{ background-color: ivory; padding:10px; }
#canvas{border:1px solid red;}
<canvas id="canvas" width=300 height=300></canvas>
When the game begins set a timer using the setTimeout() function.
Since your game is currently running indefinitely I'd change the last bit of your code to give it an ending.
var time = Date.now();
var running = setInterval(run, 10); // Save this so we can clear/cancel it later
setTimeout(function() { // Set a timer
clearInterval(running); // Stop the running loop
alert('Game over!'); // Let the user know, do other stuff here
}, 30000); // time in miliseconds before the game ends

Categories