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 шт):
из общения с поддержкой аналогичной библиотеки было выявлено, что safari не пропускает tone.loop и tone.sequencer (внутри которого тоже находится loop), способов избежать этого не существует
https://forum.zeroqode.com/t/audio-sound-player-howler-js-lite-loop-audio-not-working/11881/5