Tonejs / Tone.js

A Web Audio framework for making interactive music in the browser.
https://tonejs.github.io
MIT License
13.52k stars 983 forks source link

Issues trying to create a metronome using Transport repeat schedule #910

Closed cordial closed 3 years ago

cordial commented 3 years ago

Describe the bug

When I try and create a metronome - The metronome works ok first time I play it, but when I turn on the metronome for a second time i.e. start, stop the transport and then restart, I am getting the following error -

"Unhandled Runtime Error Error: Start time must be strictly greater than previous start time"

To Reproduce

Here is the code causing the issue (called for on and off) -

   function initialisePlayer() {
    const player = new Player(sound).toDestination();
    return player;
}

function handleMetronomeToggle() {
    if (!isMetroPlaying) {
        Transport.bpm.value = bpm;
        storage
            .ref("audio")
            .child(sound)
            .getDownloadURL()
            .then((fireBaseUrl) => {
                player.load(fireBaseUrl).then(() => {
                    Transport.scheduleRepeat(function (time) {
                        player.start(time);
                    }, "4n");

                    Transport.start();
                });
            });
    } else {
        Transport.stop();
    }
    setIsMetroPlaying(!isMetroPlaying);
}

So to explain it - method 1 just initialises the player and method 2 gets the right URL from firebase and then loads it into the player and runs it..

Expected behavior

  1. No error on repeated start/stop

What I've tried Not really sure what to do, I don't understand the error.

tambien commented 3 years ago

This seems like an autoplay error to me. Are you invoking Tone.start() anywhere after a user click? Because of autoplay restrictions of nearly all browsers you can't just start the audio after it loads, it needs to be started by a user gesture like a click.

cordial commented 3 years ago

yes, this all occurs on the on / off button click, here is the method that is called each time you click the button to turn the metronome on / off as well as adjusting the bpm value method (its in react, all the set methods are for useState hooks and its loading the player file from firebase) -

function handleTempoChange(newTempo) {
        setTempo(newTempo);
        Transport.bpm.value = newTempo;
}
function handleMetronomeClick() {
        if (!isMetroPlaying) {
            Transport.bpm.value = tempo;
            storage
                .ref("audio")
                .child(sound)
                .getDownloadURL()
                .then((fireBaseUrl) => {
                    player.load(fireBaseUrl).then(() => {
                        Transport.scheduleRepeat(function (time) {
                            player.start(time);
                        }, "4n");
                        Transport.start();
                    });
                });
        } else {
            Transport.stop();
        }
        setIsMetroPlaying(!isMetroPlaying);
    }
cordial commented 3 years ago

Interestingly, the error only happens when I run locally, if I deploy the site (to vercel) no error is thrown..not sure if this is relevant.

tambien commented 3 years ago

Autoplay issues are often very flaky and can be different on different domains. Is handleMetronomeClick invoked through a button? if it is, add Tone.start() at the top of the function call

cordial commented 3 years ago

Thanks for the quick reply. That doesn't seem to fix it unfortunately (yes handleMetro... is a button click).

It is also worth noting, and again i'm not sure if this is relevant, that the time variable doesn't seem to be resetting to zero upon Transport.stop() and then start again. Is that right? I would have thought it would do but the only thing that is doing that is refreshing the page and starting over.

tambien commented 3 years ago

If you're referring to the AudioContext time, that does not ever go back to 0 unless you dispose and recreate the AudioContext. Transport has a variable called "seconds" which does reset to 0 on stop.

One issue might be that you're calling Transport.scheduleRepeat multiple times. This would lead to many events being scheduled all for nearly the same time, which might cause that error. Try preloading the audio and setting up the scheduleRepeat callback outside of your button call to avoid scheduling things multiple times.

tambien commented 3 years ago

Is this still an issue?

cordial commented 3 years ago

hey,

yes sorry it is. Unfortunately, the user is able to change the metronome noise by using a select field that has various different files (in firestore) to use. The schedule repeat number is only happening every time you button click to start and stop, is there a way to reset the transport object? I don't really see how schedule repeat can be outside the button click either as it is starting and stopping the player.