Drum machine <audio> lag - javascript

I'm putting together a drum machine/sequencer and while the main functionality of the sequencer works fine, the audio that I've embedded in each drum cell does have a noticeable lag when the sequencer is first played. It seems to correct itself after the first beat and everything plays as normal, until other sounds are added to the pattern.
I have three rows of table cells, with each row representing a different drum sound. When the user constructs a drum pattern using all the sounds available, the loop eventually seems to go out of sync, with some sounds playing a fraction of a second later than others, but later corrects itself. My main concern is that the playback of the samples is inconsistent.
I've embedded the tags in elements with a preload attribute set to auto.
<table class="pad">
<tbody>
<tr>
<td class="kick sounds">
<audio preload="auto" src="https://raw.githubusercontent.com/wesbos/JavaScript30/master/01%20-%20JavaScript%20Drum%20Kit/sounds/kick.wav"></audio>
</td>
</tr>
</tbody>
</table>
So each there are 8 table cells to each row and each cell has the same format as above (with an element embedded). Admittedly, I can imagine that the way I've structured this is quite inefficient and that using the web audio API would work better, but I've yet to learn APIs. Is there something I can do in JS that can make the playback of these audio samples quicker?
EDIT: Here is the sequencer code. It cycles through each element and and checks if there is a cell selected. If so, that element's audio file is played. If not, it skips over to the next column.
class Sequencer {
playButton = btn;
clearButton = clear;
sounds = Array.from(sounds);
kicks = Array.from(kicks);
hihats = Array.from(hihats);
snares = Array.from(snares);
currentBeatIndexKick = 0;
currentBeatIndexHiHat = 0;
currentBeatIndexSnare = 0;
isPlaying = false;
constructor(msPerBeat) {
this.msPerBeat = msPerBeat;
this.playButton.addEventListener('click', () => this.toggleStartStop())
this.clearButton.addEventListener('click', () => this.clear())
this.sounds.forEach(sound => {
sound.addEventListener('click', e => {
if (((e.target.classList.contains('kick')) || (e.target.classList.contains('hihat')) || (e.target.classList.contains('snare'))) && !e.target.classList.contains('selected')) {
e.target.classList.add('selected');
} else {
e.target.classList.remove('selected');
}
})
})
}
toggleStartStop() {
if (this.isPlaying) {
this.stop();
} else {
this.start();
}
}
clear() {
this.kicks.forEach(kick => {
if (kick.classList.contains('selected')) {
kick.classList.remove('selected');
}
});
hihats.forEach(hihat => {
if (hihat.classList.contains('selected')) {
hihat.classList.remove('selected');
}
});
snares.forEach(snare => {
if (snare.classList.contains('selected')) {
snare.classList.remove('selected');
}
});
this.stop();
console.clear();
}
stop() {
this.isPlaying = false;
this.currentBeatIndexKick = 0;
this.currentBeatIndexHiHat = 0;
this.currentBeatIndexSnare = 0;
this.playButton.innerText = 'Play';
}
start() {
this.isPlaying = true;
this.playCurrentNoteAndSetTimeoutKick() // kicks
this.playCurrentNoteAndSetTimeoutHiHats() // hihats
this.playCurrentNoteAndSetTimeoutSnares() // snares
this.playButton.innerText = 'Stop';
}
playCurrentNoteAndSetTimeoutKick() {
if (this.isPlaying && this.kicks[this.currentBeatIndexKick].classList.contains('selected')) {
this.kicks[this.currentBeatIndexKick].childNodes[1].play();
setTimeout(() => {
this.toNextBeatKicks();
this.playCurrentNoteAndSetTimeoutKick();
}, this.msPerBeat)
}
if (this.isPlaying && !this.kicks[this.currentBeatIndexKick].classList.contains('selected'))
setTimeout(() => {
this.toNextBeatKicks();
this.playCurrentNoteAndSetTimeoutKick();
}, this.msPerBeat)
}
playCurrentNoteAndSetTimeoutHiHats() {
if (this.isPlaying && this.hihats[this.currentBeatIndexHiHat].classList.contains('selected')) {
this.hihats[this.currentBeatIndexHiHat].childNodes[1].play();
setTimeout(() => {
this.toNextBeatHiHats();
this.playCurrentNoteAndSetTimeoutHiHats();
}, this.msPerBeat)
}
if (this.isPlaying && !this.hihats[this.currentBeatIndexHiHat].classList.contains('selected'))
setTimeout(() => {
this.toNextBeatHiHats();
this.playCurrentNoteAndSetTimeoutHiHats();
}, this.msPerBeat)
}
playCurrentNoteAndSetTimeoutSnares() {
if (this.isPlaying && this.snares[this.currentBeatIndexSnare].classList.contains('selected')) {
this.snares[this.currentBeatIndexSnare].childNodes[1].play();
setTimeout(() => {
this.toNextBeatSnares();
this.playCurrentNoteAndSetTimeoutSnares();
}, this.msPerBeat)
}
if (this.isPlaying && !this.snares[this.currentBeatIndexSnare].classList.contains('selected'))
setTimeout(() => {
this.toNextBeatSnares();
this.playCurrentNoteAndSetTimeoutSnares();
}, this.msPerBeat)
}
toNextBeatKicks() {
this.currentBeatIndexKick = ++this.currentBeatIndexKick % this.kicks.length;
}
toNextBeatHiHats() {
this.currentBeatIndexHiHat = ++this.currentBeatIndexHiHat % this.hihats.length;
}
toNextBeatSnares() {
this.currentBeatIndexSnare = ++this.currentBeatIndexSnare % this.snares.length;
}
}
const sequencer = new Sequencer(213)

I think what you might have to do is to play the audio file programmatically. It's kind of hard to understand how your sequencer works without giving us more access to your code, the way I would approach this is by programmatically playing the audio file once the user has selected the sequence. I would assign a variable to play once the steps are selected on your sequencer.
var audio = new Audio('audio_file.mp3');
audio.play();
audio.play would be called on the sequence. Not sure if this helps but that's part of the api I think you were referring to.

Related

Javascript navigator.mediasession image not showing in navigator / media controls disappearing

Again, working on my audio player...
I've rediscovered a mediasession updater in my code that adds a little notification from the browser with controls for my music, and it used to work ok, a little finicky, but now...
Nothing. I tinkered around a bit and got my play/pause buttons working, along with the next/prev songs and seek buttons, and the music title and artist. But I have now found that if you press the play button, the next/prev button, or sometimes the pause button, that the options and the song details disappear, leaving me with only the play/pause button, which works perfectly...
Now, to the code. I have a function that loads the songs in my player (loadSong(songIndex)), and near the end, I have a few lines of code that reset the navigator's metadata:
navigator.mediaSession.metadata.title = song.songname;
navigator.mediaSession.metadata.author = song.artistname;
navigator.mediaSession.metadata.artwork = ("./Music/" + song.thumbnail);
The artist and name work perfectly, but the thumbnail doesn't... More on that in a separate question.
I set up the actual controls like this:
/*media controls*/
let index = 0;
let skipTime = 10;
const actionHandlers = [
['play', () => { main.playPauseControl.click(); navigator.mediaSession.playbackState = "playing"; updatePositionState();}],
['pause', () => { main.playPauseControl.click(); navigator.mediaSession.playbackState = "paused"; updatePositionState();}],
['previoustrack', () => { main.prevControl.click(); updatePositionState();}],
['nexttrack', () => { main.nextControl.click(); updatePositionState();}],
['seekbackward', (details) => { main.audio.currentTime = Math.max(main.audio.currentTime - skipTime, 0); updatePositionState();}],
['seekforward', (details) => { main.audio.currentTime = Math.min(main.audio.currentTime + skipTime, main.audio.duration); updatePositionState();}],
];
for (const [action, handler] of actionHandlers) {
try {
navigator.mediaSession.setActionHandler(action, handler);
} catch (error) {
console.log(`The media session action "${action}" is not supported yet.`);
}
}
I have some other things linked to the mediaSession in order to register interactions outside the navigator
main.audio.addEventListener('play', function() {
navigator.mediaSession.playbackState = 'playing';
main.playPauseControl.classList.remove("paused");
});
main.audio.addEventListener('pause', function() {
navigator.mediaSession.playbackState = 'paused';
main.playPauseControl.classList.add("paused");
});
Now this all works, initially, but as soon as I interact with the navigator, BOOM, everything disappears. Can someone PLEASE tell me what's happening... this has been bugging me for a while...
P.S. sorry for the long question.
This answer is going to sound weird but have you tried not touching the media session from inside the callbacks? I know the Google Chrome example calls updatePositionState, but it actually has the same play/pause issue as your code.
So concretely, try this:
const actionHandlers = [
['play', () => { main.playPauseControl.click(); }],
['pause', () => { main.playPauseControl.click(); }],
['previoustrack', () => { main.prevControl.click(); }],
['nexttrack', () => { main.nextControl.click(); }],
['seekbackward', (details) => { main.audio.currentTime = Math.max(main.audio.currentTime - skipTime, 0); }],
['seekforward', (details) => { main.audio.currentTime = Math.min(main.audio.currentTime + skipTime, main.audio.duration); }],
];
I've done this for readtomyshoe and it no longer has this issue.

Javascript How to remove event that was added, and be able to add it again when need to?

I don't know to explain this, but let me try. I'm currently doing a snooker scorekeeper project, and I'll explain a problem that I have.
//Add point when player pots balls
buttons.yellowButton.addEventListener('click', () => {
coloredBall(2); // add 2 points to player
})
buttons.greenButton.addEventListener('click', () => {
coloredBall(3); // add 3 points to player
})
.
.
.
.
.
buttons.blackButton.addEventListener('click', () => {
coloredBall(7); // add 7 points to player
})
The code above works fine, It just updates the player score. Now, when all the reds are potted, I want to disable all of the buttons except the button that the players suppose to play. So I create a function that will add a new event to the buttons. The thing is that when I restart a game, I want to be able to remove those events, and to adds the same event when all the reds are potted again. How can I do this?
allRedArePotted = function() => {
buttons.yellowButton.disabled = false;
buttons.yellowButton.addEventListener('click', () => {
disableAllButtons();
buttons.greenButton.disabled = false;
yellow = 0;
})
buttons.greenButton.addEventListener('click', () => {
disableAllButtons();
buttons.brownButton.disabled = false;
green = 0;
})
buttons.brownButton.addEventListener('click', () => {
disableAllButtons();
buttons.blueButton.disabled = false;
brown = 0;
})
buttons.blueButton.addEventListener('click', () => {
disableAllButtons();
buttons.pinkButton.disabled = false;
blue = 0;
})
buttons.pinkButton.addEventListener('click', () => {
disableAllButtons();
buttons.blackButton.disabled = false;
pink = 0;
})
buttons.blackButton.addEventListener('click', () => {
black = 0;
checkWinner();
})
}
Use named functions (not anonymous) for event handlers and remove them anytime you want. For example:
// adding
document.addEventListener('click', clickHandler);
// removing
document.removeEventListener('click', clickHandler);
As mentioned in the comments, It is better to keep the state of your program somewhere and act according to that. Adding and Removing handlers is not a good approach.
someHandler(e) {
if(condition) {
// act1
}
else {
//act2
}
}

Counter gets incremented twice

I am writing a simple JavaScript game in which the user clicks on a div that plays sounds and if they guess the animal sound correct, a counter keeps track of their score and increments.
It works, but if the same sound is played again and the user guesses it increments the counter with two not one and sometimes even by three and I can't figure out why and how to fix it, here is my HTML:
<div id="counter">Score:<span id="counter-score"> 0</span> </div>
and here is the JavaScript code:
var sounds = [
{
animalType: 'horse',
sound: new Audio('../sounds/Horse-neigh.mp3')
},
{
animalType: 'bear',
sound: new Audio('../sounds/grizzlybear.mp3')
},
{
animalType: 'goat',
sound: new Audio('../sounds/Goat-noise.mp3'),
}
];
var player = document.getElementById('player');
var enteredWord = document.getElementById('entered-word');
var counter = document.getElementById('counter-score');
startGame();
function startGame() {
player.addEventListener('click', function() {
var sound = sounds[Math.floor(Math.random() * sounds.length)];
var currentSound = sound.animalType;
sound['sound'].play();
enteredWord.addEventListener('keydown', function() {
if (event.key === 'Enter') {
if (enteredWord.value === currentSound) {
counter.textContent++;
}
} else {
}
});
});
}
Why does it happen like that?
I tried using the += operator but it gives the same result.
As #Ibu said in comments, each time the click event occur, a new event listener is added to keydown event.
You should extract the enteredWord.addEventListener part outside of player.addEventListener callback, like so:
function startGame() {
var currentSound;
player.addEventListener('click', function() {
var sound = sounds[Math.floor(Math.random()*sounds.length)];
currentSound = sound.animalType;
sound['sound'].play();
})
enteredWord.addEventListener('keydown', function() {
if(event.key === 'Enter') {
if(enteredWord.value === currentSound) {
counter.textContent ++;
}
}
})
}
Hi can you replace your code with this one, I think the key-down event is executing twice so you are getting this issue.
enteredWord.addEventListener('keydown', function(e) {
if( (e.keyCode ==13) && (enteredWord.value === currentSound)){
e.stopImmediatePropagation();
counter.textContent ++;
}
})

ReactJS ProgressBar value is not updating on par with state.progressValue

I'm currently using
import { Progress } from 'react-sweet-progress';
and before I also tried import { Progress } from 'reactstrap'; which just uses bootstrap 4 ProgressBar.
I have a state that maintains the progressValue, and using audio HTML tag to call in an online audio src and playing it. During timeupdate eventListener, I update my progressValue in my state, and reflect it back by setting the value of <Progress> as this.state.progressValue.
class FooBar extends Component {
state = {
progressValue: 0
}
handleProgress = () => {
this.currentTimeInterval = null;
this.audio.onplay = () => {
this.setState({
progressValue: 0,
play: true
});
this.audio.addEventListener('timeupdate', () => {
this.setState({
progressValue: Math.floor(this.audio.currentTime / this.audio.duration * 100)
}, function() {
console.log(this.state.progressValue);
})
}, true)
this.audio.onpause = () => {
clearInterval(this.currentTimeInterval);
}
}
render() {
return(
<audio
ref={(audio) => { this.audio = audio }}
src={http://www.music.helsinki.fi/tmt/opetus/uusmedia/esim/a2002011001-e02.wav}
autoPlay
onLoadedData={this.handleProgress}
/>
<Progress value={this.state.progressValue} />
);
}
}
The timing, however, doesn't seem to match up where the audio will be playing and the progressValue will be delayed in the sense that audio will be ahead of the progressValue. Hence, by the time audio finishes, it would likely take another 2~3 seconds for the progressBar to reach 100%.
I also tried:
this.currentTimeInterval = setInterval(() => {
this.setState({
progressValue: Math.floor(this.audio.currentTime / this.audio.duration * 100)
}))
}, 100)
and tried manipulating the timeInterval of 100ms to a smaller number, which makes it closer but the delay still exists.
3 Questions:
1) What is causing this to happen?
2) Is there a way to fix this?
3) If the answer is 'no' to 2), is there another component I can use to display the progress of an audio file? (Not the default controls from audio tag).
Thank you!

HTML5 audio won't play sometimes

In my application, I am creating 10 audio object and I store them in a browser variable. Based on the scenario, I will pick one of them and will assign it to one global variable. Then I play that sound. After some time I will stop and clear that audio from the global variable.
I am verifying the readystate whenever I play the sound. I capture Play & Pause events.
The problem is that sometimes the sound is not audible but play & pause events are still fired. I am not able to reproduce it all the times (it happens randomly).
Please let me know if anyone faced this kind of issue.
function Sound(){}
try
{
if(new Audio().canPlayType("audio/wav") != "" && new Audio().canPlayType("audio/wav") != "no" ){
Sound.extn="wav";//No I18N
}
else{
Sound.extn="mp3";//No I18N
}
}
catch (e) {
}
Sound.init=function(soundsobj)
{
for (var tunename in soundsobj)
{
var audioobj = new Audio(soundsobj[tunename]);
audioobj.setAttribute("autobuffer", "true");
audioobj.addEventListener('loadeddata', function() {
Sound.cache[tunename]=this;
});
audioobj.addEventListener('loadedmetadata', function() {
this.currentTime = 0;
});
Sound._SOUND_PLAYER.addEventListener( "play", function(){console.log(this.readyState);}, false);
Sound._SOUND_PLAYER.addEventListener( "pause", function(){console.log(this.readyState);}, false);
}
}
Sound.playTune=function(tunename,duration)
{
if(Sound.cache[tunename])
{
var cachedsound = Sound.cache[tunename];
if(cachedsound.readyState == 4)
{
cachedsound.currentTime = 0;
Sound._SOUND_PLAYER = cachedsound;
Sound.play(duration);
}
else
{
delete Sound.cache[tunename];
var audioobj = new Audio(soundsobj[tunename]);
audioobj.setAttribute("autobuffer", "true");
audioobj.addEventListener('loadeddata', function() {
Sound.cache[tunename]=this;
Sound._SOUND_PLAYER = this;
Sound.play(duration);
});
}
}
}
Sound.play = function(duration)
{
Sound._SOUND_PLAYER.loop=true;
Sound._SOUND_PLAYER.play();
clearTimeout(Sound.cleartimer);
Sound.cleartimer = setTimeout(function(){Sound.stop()},duration*1000);
}
Sound.stop=function()
{
try
{
if(Sound._SOUND_PLAYER)
{
Sound._SOUND_PLAYER.pause();
}
Sound._SOUND_PLAYER = undefined;
}
catch(e){}
}

Categories