Safari/Chrome阻止AudioContext,Tone.js循环回调失效问题求助
Tone.js节拍器循环回调不执行及音频上下文阻塞问题
我正在开发一款基于Tone.js的节拍器,通过Tone.js的loop函数触发音效,并控制摆锤、节拍条等视觉元素。已尝试通过直接点击按钮恢复AudioContext,但每次循环回调后音频上下文仍会被阻塞。我考虑过预缓冲音效并单次启动,但这种方式无法动态修改音效(会产生卡顿)。目前无可见报错,代码中的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("🔓 音频上下文已解锁"); } catch (e) { console.error("⚠️ 解锁音频上下文失败", e); return; } } if (metronomeManager.isPlaying) { // metronomeManager.stopMetronome(); } else { metronomeManager.startMetronome(); } }; useHotkeys({ " ": onClick, }); return ( <button id="start-stop-button" onClick={onClick} > {metronomeManager.isPlaying ? "停止" : "开始"} </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: 控制摆锤移动! // this.elementsManager.movePendulum(); } getMetronomeLoopCallback(time) { console.log("循环回调已启动"); this._currentStep = this._count % this._sequence.length; this._isStartOfLoop = this._currentStep === 0; //TODO: 恢复训练模式 // 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("执行到此处"); 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. 启动Tone.Transport
Tone.Loop依赖Tone.Transport驱动运行,你的代码中仅启动了Loop但未启动Transport,这是回调不执行的关键原因。在startMetronome方法中添加:
Tone.getTransport().start();
放在this._loop.start(0);之后。
2. 简化音频上下文解锁逻辑
Tone.start()已经封装了恢复AudioContext的操作,无需额外调用rawContext.resume(),简化后的解锁代码:
await Tone.start(); const silentSynth = new Tone.Synth().toDestination(); silentSynth.triggerAttackRelease("C0", "1ms", Tone.now() + 0.05); audioUnlocked = true;
3. 避免动态修改音效的卡顿
循环中直接修改Synth参数容易导致卡顿,建议提前创建不同预设的Synth实例,在需要时直接调用:
// 初始化时创建预设音效 this.soundPresets = { accent: new Tone.Synth({ envelope: { attack: 0.01, release: 0.1 }, oscillator: { type: 'sine' } }).toDestination(), normal: new Tone.Synth({ envelope: { attack: 0.05, release: 0.2 }, oscillator: { type: 'square' } }).toDestination() }; // 在playMetronomeStep中替换动态修改逻辑 const preset = currentNote.preset || 'normal'; this.soundPresets[preset].triggerAttackRelease('C4', '64n', time);
4. 确保上下文绑定正确
虽然用箭头函数包裹了Loop回调,但为了避免this指向异常,建议在类的构造函数中绑定方法:
constructor() { this.getMetronomeLoopCallback = this.getMetronomeLoopCallback.bind(this); this.playMetronomeStep = this.playMetronomeStep.bind(this); }
5. 调试辅助
在startMetronome中添加日志,确认Transport和Loop的运行状态:
console.log('Transport状态:', Tone.getTransport().state); console.log('Loop状态:', this._loop.state);
内容的提问来源于stack exchange,提问作者Anton Balkouski
相关产品推荐
相关产品推荐

