点击按钮初始化Tone.js仍报错:AudioContext未获启动权限
Angular组件中Tone.js在用户点击后仍报错AudioContext未启动的问题及解决
我开发了一个Angular组件,点击按钮可启动动画,同时在高亮数字时播放音符。但在点击按钮触发的escalaPentatonicaToggle方法中初始化Tone.js合成器时,仍收到错误:
The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page
明明是在CLICK事件后尝试播放声音,为何会出现该错误?该如何解决?
组件逻辑代码
import { Component } from '@angular/core'; import { ElementoPentatonica, EscalaPentatonica, NotasEscalaPentatonica } from '../../models/EscalaPentatonica'; import { CommonModule } from '@angular/common'; import { ElementoPentatonicaComponent } from '../elemento-pentatonica/elemento-pentatonica.component'; import * as Tone from 'tone'; @Component({ selector: 'app-escala-pentatonica', standalone: true, imports: [CommonModule, ElementoPentatonicaComponent], templateUrl: './escala-pentatonica.component.html', styleUrl: './escala-pentatonica.component.css' }) export class EscalaPentatonicaComponent { escala: EscalaPentatonica; highlightedIndex: number = 0; timer: any; lapse: number = 600; running: boolean = false; soundEnabled: boolean = true; sonido = "LA"; start = 1; lineasFlat: ElementoPentatonica[] = []; synth: Tone.Synth | null = null; constructor() { this.escala = new EscalaPentatonica(this.start, NotasEscalaPentatonica.SI_MAYOR, this.sonido); this.lineasFlat = this.escala.lineas.flat(); } onSelectionChange(event: Event) { this.stopAnimation(); this.start = Number((event.target as HTMLSelectElement).value); this.escala = new EscalaPentatonica(this.start, NotasEscalaPentatonica.SI_MAYOR, this.sonido); } ngOnInit() { } async escalaPentatonicaToggle() { await Tone.start(); if (!this.running) { await this.startAnimation(); } else { this.stopAnimation(); } this.running = !this.running; } async startAnimation() { this.synth = new Tone.Synth().toDestination(); this.highlightedIndex = 0; this.startTimer(); } stopAnimation() { if (this.timer) { clearInterval(this.timer); } if (this.synth) { this.synth.dispose(); this.synth = null; } } ngOnDestroy() { this.stopAnimation(); } startTimer() { this.timer = setInterval(() => { this.highlightedIndex = (this.highlightedIndex + 1) % this.lineasFlat.length; this.playSound(this.highlightedIndex); }, this.lapse); } getIndex(i: number, j: number): number { let index = 0; for (let x = 0; x < i; x++) { index += this.escala.lineas[x].length; } return index + j; } playSound(index: number) { let note = this.escala.notas[this.lineasFlat[index].numero - this.escala.start]; if (this.synth) { console.log('Playing note:', note); this.synth.triggerAttackRelease(note, '2s'); } } }
HTML模板代码
<div class="escala-container"> <select #escalaSelect (change)="onSelectionChange($event)"> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> <option value="4">4</option> </select> <button class="nextButton" (click)="escalaPentatonicaToggle()"> {{ running ? 'DETENER' : 'INICIAR' }} </button> <div *ngFor="let linea of escala.lineas; let i = index" class="linea" [style.margin-left.px]="(4 - i) * 20"> <app-elemento-pentatonica *ngFor="let elemento of linea; let j = index" [numero]="elemento.numero" [sonido]="elemento.sonido" [highlight]="highlightedIndex === getIndex(i, j)"> </app-elemento-pentatonica> </div> </div>
错误原因
- 异步时序问题:虽然在点击事件中调用了
await Tone.start(),但startAnimation中创建合成器、启动定时器的流程,可能因为异步调度,导致AudioContext的激活没有完全落在用户手势的同步回调链中。 - 定时器脱离交互上下文:
setInterval内的playSound是延迟执行的,第一次调用时已经脱离了用户点击的交互上下文,而浏览器要求AudioContext必须在用户主动触发的同步任务中完成启动/恢复。
解决方法
方法1:在用户手势同步流程中完成AudioContext激活
调整点击事件逻辑,将合成器创建、首次声音播放都放在用户点击的同步环节内,确保AudioContext被完全激活:
async escalaPentatonicaToggle() { if (!this.running) { // 在用户手势上下文内启动Tone并创建合成器 await Tone.start(); this.synth = new Tone.Synth().toDestination(); // 播放极短静音强制激活AudioContext await this.synth.triggerAttackRelease('C0', '1ms'); this.highlightedIndex = 0; this.startTimer(); } else { this.stopAnimation(); } this.running = !this.running; } // 修改startAnimation,移除合成器创建逻辑 async startAnimation() { this.highlightedIndex = 0; this.startTimer(); }
方法2:在播放前检查AudioContext状态
修改playSound方法,每次播放前检查AudioContext是否活跃,未活跃则重新启动:
async playSound(index: number) { if (!this.synth) return; // 检查并激活AudioContext if (Tone.context.state !== 'running') { await Tone.start(); } let note = this.escala.notas[this.lineasFlat[index].numero - this.escala.start]; console.log('Playing note:', note); await this.synth.triggerAttackRelease(note, '2s'); }
方法3:让首次播放落在用户交互上下文内
修改定时器逻辑,先同步执行一次播放,再启动定时器:
startTimer() { // 同步执行首次播放,确保在用户点击上下文内 this.playSound(this.highlightedIndex); // 后续播放基于已激活的上下文 this.timer = setInterval(() => { this.highlightedIndex = (this.highlightedIndex + 1) % this.lineasFlat.length; this.playSound(this.highlightedIndex); }, this.lapse); }
内容的提问来源于stack exchange,提问作者Mauricio Gracia Gutierrez
相关产品推荐
相关产品推荐

