You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

点击按钮初始化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>

错误原因

  1. 异步时序问题:虽然在点击事件中调用了await Tone.start(),但startAnimation中创建合成器、启动定时器的流程,可能因为异步调度,导致AudioContext的激活没有完全落在用户手势的同步回调链中。
  2. 定时器脱离交互上下文: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 12:34:59