Safari/Chrome блокируют AudioContext и не позволяют вызвать мою функцию loop из Tone.js

Я пишу метроном, где функция loop из Tone.js воспроизводит звук и управляет маятником, тактовыми полосами и другими визуальными элементами. Я уже пробовал возобновлять AudioContext после прямого нажатия кнопки, но loop функция всё равно не выполняется. У меня есть план Б — буферизовать звук и запускать его один раз, но в этом случае невозможно будет менять звук на лету без полного перерендеринга буфера и без лагов. Есть ли какие-то варианты, как можно решить эту проблему?

Ошибок в консоли нет, просто не заходит в мою функцию getMetronomeLoopCallback.

    let audioUnlocked = false; // Флаг для первого разблокирования контекста

const StartStopButton = observer(({metronomeManager}) => {
    const onClick = async () => {
        if (!audioUnlocked) {
            try {
                // Разблокируем контекст
                await Tone.start();
                await Tone.getContext().rawContext.resume();

                // Играть пустой звук – Safari "разблокирует" звук
                const silentSynth = new Tone.Synth().toDestination();
                silentSynth.triggerAttackRelease("C0", "1ms", Tone.now() + 0.05);

                audioUnlocked = true;
                console.log("? Audio context unlocked");
            } catch (e) {
                console.error("⚠️ Error unlocking audio context", e);
                return;
            }
        }

        if (metronomeManager.isPlaying) {
            // metronomeManager.stopMetronome();
        } else {
            metronomeManager.startMetronome();
        }
    };

    useHotkeys({
        " ": onClick,
    });

    return (
        <button
            id="start-stop-button"
            onClick={onClick}
        >
            {metronomeManager.isPlaying ? "Stop" : "Start"}
        </button>
    );
});
    startMetronome() {
    this._isPlaying = true;

    Tone.getTransport().bpm.value = this.bpm * 3;
    this._sequence = this.generateFixedMetronomeSequence();
    this._skipper = 0;

    this._loop = new Tone.Loop((time) => this.getMetronomeLoopCallback(time), '64n');
    this._loop.start(0);

    //TODO: move pendulum!
    //    this.elementsManager.movePendulum();
}

getMetronomeLoopCallback(time) {
    console.log("loop callback has started");
    this._currentStep = this._count % this._sequence.length;
    this._isStartOfLoop = this._currentStep === 0;

    //TODO: add training mode back
    // if (this.trainingModeManager.getIsTrainingMode()) {
    //     if (this._isStartOfLoop &&
    //         (this.trainingModeManager.getIsFirstLoop() || Math.random() < this.trainingModeManager.getLoopSkipProbability())) {
    //         this._skipper = this._sequence.length;
    //     }
    // }

    if (this._skipper > 0) {
        this._skipper--;
        if (this.trainingModeManager.getIsFirstLoop()) {
            this.playMetronomeStep(this._sequence, this._currentStep, time);
        }
        if (this._skipper === 0) {
            this.trainingModeManager.setIsFirstLoop(false);
        }
    } else {
        this.playMetronomeStep(this._sequence, this._currentStep, time);
    }

    if (this._isStartOfLoop) {
         this._loopCount += 1;
    }

    this._count++;
}

playMetronomeStep(sequence, currentStep, time) {
    const currentNote = sequence[currentStep];
    if (!currentNote || !currentNote.sound) return;
    if (!(this.trainingModeManager.getIsTrainingMode() && Math.random() < this.trainingModeManager.getNoteSkipProbability() && !this.trainingModeManager.getIsFirstLoop())) {
        console.log("stepped here");
        const {sound, settings} = currentNote;

        // Динамически применяем все параметры из settings к sound
        for (const key in settings) {
            if (settings.hasOwnProperty(key)) {
                if (key in sound) {
                    // Если параметр есть в объекте sound (например, volume)
                    sound[key].value = settings[key];
                } else if (key in sound.oscillator) {
                    // Если параметр относится к осциллятору (например, frequency, detune, phase)
                    sound.oscillator[key] = settings[key];
                } else if (key in sound.envelope) {
                    // Если параметр относится к огибающей (например, attack, decay, sustain, release)
                    sound.envelope[key] = settings[key];
                } else if (key in sound.filter) {
                    // Если параметр относится к фильтру (например, filterFrequency, filterQ, filterType)
                    sound.filter[key] = settings[key];
                }
            }
        }

        // Запускаем звук
        sound.triggerAttackRelease('C4', '64n', time);

        // Визуальные эффекты
        elements.flashingBar.style.opacity = 1;
        setTimeout(() => elements.flashingBar.style.opacity = 0, 100);

        const beatElement = document.querySelector(`.beat[data-beat="${currentNote.beatIndex}"]`);
        beatElement.classList.add('playing');
        setTimeout(() => beatElement.classList.remove('playing'), 100);
    }
}

Ответы (1 шт):

Автор решения: Anton Balkouski

из общения с поддержкой аналогичной библиотеки было выявлено, что safari не пропускает tone.loop и tone.sequencer (внутри которого тоже находится loop), способов избежать этого не существует

https://forum.zeroqode.com/t/audio-sound-player-howler-js-lite-loop-audio-not-working/11881/5

→ Ссылка