第十二章:AI 音频场景专题 —— Web 端音频智能化的完整图谱

这是「Web 音频开发技术知识库」的最后一章,也是最前沿的一章。AI 正在重塑音频处理的每一个环节——语音识别、文字转语音、实时降噪、音频分类、音乐生成……这些能力正在以越来越低的门槛进入 Web 端。本章从 Web Speech API 到 WebAssembly 推理引擎,把 AI 音频场景的完整技术路径打通。


一、Web 端 AI 音频的技术路线全景

在动手之前,先建立一张清晰的技术路线图。Web 端 AI 音频处理有三条主要路径,各有适用场景:

┌─────────────────────────────────────────────────────────────┐
│                   Web AI 音频技术路线                        │
├─────────────────┬──────────────────┬────────────────────────┤
│   浏览器原生 API  │   云端 API 调用   │   本地模型推理          │
│                 │                  │                        │
│ Web Speech API  │ OpenAI Whisper   │ WebAssembly + ONNX     │
│ (识别/合成)      │ TTS API          │ TensorFlow.js          │
│                 │ Azure/Google     │ Transformers.js        │
│                 │ Speech Services  │ whisper.cpp → WASM     │
├─────────────────┼──────────────────┼────────────────────────┤
│ 优点:零延迟     │ 优点:效果最好    │ 优点:离线/隐私/低延迟  │
│ 缺点:兼容性差   │ 缺点:需要网络    │ 缺点:首次加载慢        │
│      效果一般   │      有费用      │      内存占用大         │
└─────────────────┴──────────────────┴────────────────────────┘

选择哪条路径取决于你的具体场景:实时性要求高且网络可靠 → 云端 API隐私敏感或离线场景 → 本地 WASM 推理快速原型或轻量场景 → Web Speech API


二、Web Speech API:浏览器原生语音能力

Web Speech API 是浏览器内置的语音处理接口,分为**语音识别(SpeechRecognition)语音合成(SpeechSynthesis)**两部分,无需任何外部依赖。

2.1 语音识别(Speech-to-Text)

class SpeechRecognizer {
  constructor(options = {}) {
    const SpeechRecognition =
      window.SpeechRecognition || window.webkitSpeechRecognition;
 
    if (!SpeechRecognition) {
      throw new Error('当前浏览器不支持 Web Speech API');
    }
 
    this.recognition = new SpeechRecognition();
 
    // 配置
    this.recognition.lang           = options.lang           || 'zh-CN';
    this.recognition.continuous     = options.continuous     || false; // 连续识别
    this.recognition.interimResults = options.interimResults || true;  // 中间结果
    this.recognition.maxAlternatives = options.maxAlternatives || 1;   // 候选数量
 
    this._onResult   = options.onResult   || (() => {});
    this._onError    = options.onError    || console.error;
    this._onEnd      = options.onEnd      || (() => {});
 
    this._bindEvents();
  }
 
  _bindEvents() {
    // 识别结果
    this.recognition.addEventListener('result', (event) => {
      let interimTranscript = '';
      let finalTranscript   = '';
 
      for (let i = event.resultIndex; i < event.results.length; i++) {
        const result     = event.results[i];
        const transcript = result[0].transcript;
        const confidence = result[0].confidence;
 
        if (result.isFinal) {
          finalTranscript += transcript;
        } else {
          interimTranscript += transcript;
        }
      }
 
      this._onResult({
        final:    finalTranscript,
        interim:  interimTranscript,
        isFinal:  finalTranscript.length > 0,
      });
    });
 
    // 错误处理
    this.recognition.addEventListener('error', (event) => {
      const errorMessages = {
        'not-allowed':       '麦克风权限被拒绝',
        'no-speech':         '未检测到语音输入',
        'audio-capture':     '无法获取麦克风',
        'network':           '网络错误(Web Speech API 需要联网)',
        'aborted':           '识别被中止',
        'language-not-supported': '不支持该语言',
      };
      this._onError(errorMessages[event.error] || `未知错误: `);
    });
 
    this.recognition.addEventListener('end', () => {
      this._onEnd();
      // 连续模式下自动重启
      if (this.isRunning && this.recognition.continuous) {
        this.recognition.start();
      }
    });
  }
 
  start() {
    this.isRunning = true;
    this.recognition.start();
  }
 
  stop() {
    this.isRunning = false;
    this.recognition.stop();
  }
 
  abort() {
    this.isRunning = false;
    this.recognition.abort();
  }
}

实战:实时语音转文字输入框

class VoiceInputField {
  constructor(inputEl, buttonEl) {
    this.input  = inputEl;
    this.button = buttonEl;
    this.isListening = false;
 
    this.recognizer = new SpeechRecognizer({
      lang:           'zh-CN',
      continuous:     true,
      interimResults: true,
 
      onResult: ({ final, interim, isFinal }) => {
        if (isFinal) {
          // 最终结果追加到输入框
          this.input.value += final;
          this._interimSpan.textContent = '';
        } else {
          // 中间结果实时显示(灰色)
          this._interimSpan.textContent = interim;
        }
      },
 
      onError: (msg) => {
        this._showError(msg);
        this._setListening(false);
      },
 
      onEnd: () => {
        if (!this.recognizer.recognition.continuous) {
          this._setListening(false);
        }
      },
    });
 
    // 创建中间结果显示层
    this._interimSpan = document.createElement('span');
    this._interimSpan.style.cssText = 'color:#999; font-style:italic;';
    inputEl.parentNode.insertBefore(this._interimSpan, inputEl.nextSibling);
 
    this.button.addEventListener('click', () => this._toggle());
  }
 
  _toggle() {
    if (this.isListening) {
      this.recognizer.stop();
      this._setListening(false);
    } else {
      this.recognizer.start();
      this._setListening(true);
    }
  }
 
  _setListening(state) {
    this.isListening      = state;
    this.button.textContent = state ? '🔴 停止' : '🎤 语音输入';
    this.button.classList.toggle('listening', state);
  }
 
  _showError(msg) {
    const err = document.createElement('div');
    err.className   = 'voice-error';
    err.textContent = msg;
    this.input.parentNode.appendChild(err);
    setTimeout(() => err.remove(), 3000);
  }
}
 
// 使用
const voiceInput = new VoiceInputField(
  document.getElementById('textInput'),
  document.getElementById('voiceBtn')
);

2.2 语音合成(Text-to-Speech)

class TextToSpeech {
  constructor() {
    this.synth  = window.speechSynthesis;
    this.voices = [];
    this._loadVoices();
  }
 
  _loadVoices() {
    // 声音列表加载是异步的
    const load = () => {
      this.voices = this.synth.getVoices();
    };
    load();
    // Chrome 需要监听事件
    this.synth.addEventListener('voiceschanged', load);
  }
 
  // 获取中文声音列表
  getChineseVoices() {
    return this.voices.filter(v =>
      v.lang.startsWith('zh') || v.lang.startsWith('cmn')
    );
  }
 
  // 合成并播放
  speak(text, options = {}) {
    return new Promise((resolve, reject) => {
      // 停止当前播放
      this.synth.cancel();
 
      const utterance = new SpeechSynthesisUtterance(text);
 
      // 配置
      utterance.lang   = options.lang   || 'zh-CN';
      utterance.rate   = options.rate   || 1.0;   // 语速:0.1 ~ 10
      utterance.pitch  = options.pitch  || 1.0;   // 音调:0 ~ 2
      utterance.volume = options.volume || 1.0;   // 音量:0 ~ 1
 
      // 选择声音
      if (options.voiceName) {
        const voice = this.voices.find(v => v.name === options.voiceName);
        if (voice) utterance.voice = voice;
      } else {
        // 自动选择中文声音
        const cnVoice = this.getChineseVoices()[0];
        if (cnVoice) utterance.voice = cnVoice;
      }
 
      utterance.addEventListener('end',   resolve);
      utterance.addEventListener('error', reject);
 
      // 进度回调
      if (options.onBoundary) {
        utterance.addEventListener('boundary', options.onBoundary);
      }
 
      this.synth.speak(utterance);
    });
  }
 
  // 逐字高亮朗读
  async speakWithHighlight(text, container, options = {}) {
    return new Promise((resolve) => {
      this.synth.cancel();
      const utterance = new SpeechSynthesisUtterance(text);
      utterance.lang  = options.lang || 'zh-CN';
      utterance.rate  = options.rate || 0.9;
 
      // 渲染文字到容器(每个字一个 span)
      container.innerHTML = [...text].map((char, i) =>
        `<span data-index="">`
      ).join('');
 
      let lastHighlighted = -1;
 
      utterance.addEventListener('boundary', (e) => {
        // 清除上一个高亮
        if (lastHighlighted >= 0) {
          container.querySelector(`[data-index=""]`)
            ?.classList.remove('highlight');
        }
        // 高亮当前字
        const charIndex = e.charIndex;
        container.querySelector(`[data-index=""]`)
          ?.classList.add('highlight');
        lastHighlighted = charIndex;
      });
 
      utterance.addEventListener('end', () => {
        // 清除所有高亮
        container.querySelectorAll('.highlight')
          .forEach(el => el.classList.remove('highlight'));
        resolve();
      });
 
      this.synth.speak(utterance);
    });
  }
 
  pause()  { this.synth.pause();  }
  resume() { this.synth.resume(); }
  stop()   { this.synth.cancel(); }
 
  get isSpeaking() { return this.synth.speaking; }
  get isPaused()   { return this.synth.paused;   }
}

三、云端 AI 语音 API 集成

当原生 Web Speech API 的效果不满足需求时,接入云端 AI 语音服务是最快的提升路径。

3.1 OpenAI Whisper API(语音识别)

Whisper 是目前开源效果最好的语音识别模型,通过 API 调用非常简单:

class WhisperTranscriber {
  constructor(apiKey, options = {}) {
    this.apiKey  = apiKey;
    this.options = {
      model:       options.model    || 'whisper-1',
      language:    options.language || 'zh',      // 指定语言加速识别
      temperature: options.temperature || 0,       // 0 = 最确定性
      prompt:      options.prompt  || '',          // 提示词,提高专有名词识别率
    };
  }
 
  // 识别音频文件(File 或 Blob)
  async transcribe(audioBlob, filename = 'audio.webm') {
    const formData = new FormData();
    formData.append('file',        audioBlob, filename);
    formData.append('model',       this.options.model);
    formData.append('language',    this.options.language);
    formData.append('temperature', this.options.temperature);
 
    if (this.options.prompt) {
      formData.append('prompt', this.options.prompt);
    }
 
    // 返回带时间戳的详细结果
    formData.append('response_format', 'verbose_json');
    formData.append('timestamp_granularities[]', 'word');
 
    const response = await fetch(
      'https://api.openai.com/v1/audio/transcriptions',
      {
        method:  'POST',
        headers: { 'Authorization': `Bearer ` },
        body:    formData,
      }
    );
 
    if (!response.ok) {
      const error = await response.json();
      throw new Error(`Whisper API 错误: `);
    }
 
    const result = await response.json();
    return {
      text:     result.text,
      language: result.language,
      duration: result.duration,
      // 词级时间戳
      words:    result.words?.map(w => ({
        word:  w.word,
        start: w.start,
        end:   w.end,
      })) || [],
      // 句子级时间戳
      segments: result.segments?.map(s => ({
        text:  s.text,
        start: s.start,
        end:   s.end,
      })) || [],
    };
  }
 
  // 翻译音频(任意语言 → 英语)
  async translate(audioBlob, filename = 'audio.webm') {
    const formData = new FormData();
    formData.append('file',  audioBlob, filename);
    formData.append('model', this.options.model);
 
    const response = await fetch(
      'https://api.openai.com/v1/audio/translations',
      {
        method:  'POST',
        headers: { 'Authorization': `Bearer ` },
        body:    formData,
      }
    );
 
    const result = await response.json();
    return result.text;
  }
}

录音 + Whisper 识别的完整流程

class VoiceToTextPipeline {
  constructor(apiKey) {
    this.recorder    = new AudioRecorder();
    this.transcriber = new WhisperTranscriber(apiKey, {
      language: 'zh',
      prompt:   '以下是普通话内容,包含技术术语。', // 提示词提高准确率
    });
    this.isRecording = false;
  }
 
  async startRecording() {
    await this.recorder.start({ timeslice: 0 }); // 录完后一次性处理
    this.isRecording = true;
  }
 
  async stopAndTranscribe() {
    return new Promise(async (resolve, reject) => {
      // 停止录音
      this.recorder.recorder.addEventListener('stop', async () => {
        try {
          // 获取录音 Blob
          const mimeType = AudioRecorder.getBestMimeType();
          const blob = new Blob(this.recorder.chunks, { type: mimeType });
 
          console.log(`录音大小: KB,格式: `);
 
          // 发送给 Whisper 识别
          const result = await this.transcriber.transcribe(
            blob,
            `recording.`
          );
 
          resolve(result);
        } catch (err) {
          reject(err);
        }
      }, { once: true });
 
      this.recorder.stop();
      this.isRecording = false;
    });
  }
}
 
// 使用
const pipeline = new VoiceToTextPipeline('your-api-key');
 
recordBtn.addEventListener('mousedown', () => pipeline.startRecording());
recordBtn.addEventListener('mouseup',   async () => {
  const result = await pipeline.stopAndTranscribe();
  console.log('识别结果:', result.text);
  console.log('词级时间戳:', result.words);
  outputEl.textContent = result.text;
});

3.2 云端 TTS API(文字转语音)

class CloudTTSService {
  constructor(provider = 'openai', apiKey) {
    this.provider = provider;
    this.apiKey   = apiKey;
    this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  }
 
  // OpenAI TTS
  async synthesizeOpenAI(text, options = {}) {
    const response = await fetch('https://api.openai.com/v1/audio/speech', {
      method:  'POST',
      headers: {
        'Authorization': `Bearer `,
        'Content-Type':  'application/json',
      },
      body: JSON.stringify({
        model:           options.model || 'tts-1',   // tts-1 或 tts-1-hd
        input:           text,
        voice:           options.voice || 'alloy',   // alloy/echo/fable/onyx/nova/shimmer
        response_format: options.format || 'mp3',
        speed:           options.speed  || 1.0,      // 0.25 ~ 4.0
      }),
    });
 
    if (!response.ok) throw new Error('TTS API 请求失败');
 
    const arrayBuffer = await response.arrayBuffer();
    return arrayBuffer;
  }
 
  // 合成并直接播放
  async speakAndPlay(text, options = {}) {
    const arrayBuffer = await this.synthesizeOpenAI(text, options);
    const audioBuffer = await this.audioCtx.decodeAudioData(arrayBuffer);
 
    const source = this.audioCtx.createBufferSource();
    source.buffer = audioBuffer;
    source.connect(this.audioCtx.destination);
 
    return new Promise((resolve) => {
      source.addEventListener('ended', resolve);
      source.start(0);
    });
  }
 
  // 流式 TTS(边合成边播放,降低首字延迟)
  async speakStreaming(text, options = {}) {
    const response = await fetch('https://api.openai.com/v1/audio/speech', {
      method:  'POST',
      headers: {
        'Authorization': `Bearer `,
        'Content-Type':  'application/json',
      },
      body: JSON.stringify({
        model:           'tts-1',
        input:           text,
        voice:           options.voice || 'nova',
        response_format: 'mp3',
      }),
    });
 
    // 使用 MSE 流式播放(第六章的技术)
    const audio = document.createElement('audio');
    const mediaSource = new MediaSource();
    audio.src = URL.createObjectURL(mediaSource);
 
    mediaSource.addEventListener('sourceopen', async () => {
      const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');
      const reader = response.body.getReader();
 
      const pump = async () => {
        const { done, value } = await reader.read();
        if (done) {
          if (!sourceBuffer.updating) mediaSource.endOfStream();
          return;
        }
 
        // 等待 sourceBuffer 就绪
        if (sourceBuffer.updating) {
          await new Promise(r => sourceBuffer.addEventListener('updateend', r, { once: true }));
        }
        sourceBuffer.appendBuffer(value);
        pump();
      };
 
      pump();
      audio.play();
    });
 
    return audio;
  }
}

四、本地模型推理:WebAssembly + ONNX

当隐私保护或离线能力是核心需求时,需要在浏览器本地运行 AI 模型。

4.1 Transformers.js:浏览器端 Hugging Face

Transformers.js 是 Hugging Face 的 JavaScript 版本,支持在浏览器中直接运行 Whisper、音频分类等模型:

import { pipeline, env } from '@xenova/transformers';
 
// 配置:使用 WASM 后端,模型缓存到 IndexedDB
env.backends.onnx.wasm.numThreads = 4; // 多线程加速
 
class LocalWhisper {
  constructor() {
    this.transcriber = null;
    this.isLoading   = false;
  }
 
  // 初始化模型(首次需要下载,后续从缓存加载)
  async initialize(onProgress) {
    if (this.transcriber) return;
    this.isLoading = true;
 
    console.log('加载 Whisper 模型...');
 
    this.transcriber = await pipeline(
      'automatic-speech-recognition',
      // tiny 模型:~40MB,速度快但准确率一般
      // small 模型:~250MB,准确率更好
      'Xenova/whisper-tiny',
      {
        // 进度回调
        progress_callback: (progress) => {
          if (progress.status === 'downloading') {
            const pct = Math.round(
              (progress.loaded / progress.total) * 100
            );
            onProgress?.(`下载模型: %`);
          } else if (progress.status === 'loading') {
            onProgress?.('加载模型中...');
          }
        },
      }
    );
 
    this.isLoading = false;
    console.log('Whisper 模型加载完成');
  }
 
  // 识别音频(接受 Float32Array PCM 数据或 URL)
  async transcribe(audioData, options = {}) {
    if (!this.transcriber) {
      throw new Error('模型未初始化,请先调用 initialize()');
    }
 
    const result = await this.transcriber(audioData, {
      language:            options.language || 'chinese',
      task:                options.task     || 'transcribe', // 或 'translate'
      return_timestamps:   options.timestamps || true,
      chunk_length_s:      options.chunkLength || 30,  // 分块处理长音频
      stride_length_s:     options.strideLength || 5,
    });
 
    return {
      text:   result.text,
      chunks: result.chunks || [],
    };
  }
 
  // 从 AudioBuffer 提取 PCM 数据并识别
  async transcribeAudioBuffer(audioBuffer) {
    // Whisper 需要 16kHz 单声道 Float32Array
    const targetSampleRate = 16000;
    let channelData = audioBuffer.getChannelData(0); // 取左声道
 
    // 如果采样率不是 16kHz,需要重采样
    if (audioBuffer.sampleRate !== targetSampleRate) {
      channelData = this._resample(
        channelData,
        audioBuffer.sampleRate,
        targetSampleRate
      );
    }
 
    return this.transcribe(channelData);
  }
 
  // 简单线性插值重采样
  _resample(inputData, inputRate, outputRate) {
    const ratio      = inputRate / outputRate;
    const outputLength = Math.round(inputData.length / ratio);
    const output     = new Float32Array(outputLength);
 
    for (let i = 0; i < outputLength; i++) {
      const srcIndex = i * ratio;
      const srcFloor = Math.floor(srcIndex);
      const srcCeil  = Math.min(srcFloor + 1, inputData.length - 1);
      const frac     = srcIndex - srcFloor;
      output[i] = inputData[srcFloor] * (1 - frac) + inputData[srcCeil] * frac;
    }
    return output;
  }
}

4.2 在 Web Worker 中运行模型推理

模型推理非常耗 CPU,必须放到 Web Worker 中,避免阻塞主线程:

// whisper-worker.js
import { pipeline } from '@xenova/transformers';
 
let transcriber = null;
 
self.addEventListener('message', async (event) => {
  const { type, payload, id } = event.data;
 
  switch (type) {
    case 'init':
      try {
        transcriber = await pipeline(
          'automatic-speech-recognition',
          payload.model || 'Xenova/whisper-tiny',
          {
            progress_callback: (progress) => {
              self.postMessage({ type: 'progress', id, data: progress });
            },
          }
        );
        self.postMessage({ type: 'ready', id });
      } catch (err) {
        self.postMessage({ type: 'error', id, error: err.message });
      }
      break;
 
    case 'transcribe':
      try {
        const result = await transcriber(payload.audio, payload.options);
        self.postMessage({ type: 'result', id, data: result });
      } catch (err) {
        self.postMessage({ type: 'error', id, error: err.message });
      }
      break;
  }
});
// 主线程:与 Worker 通信
class WhisperWorkerClient {
  constructor() {
    this.worker   = new Worker(
      new URL('./whisper-worker.js', import.meta.url),
      { type: 'module' }
    );
    this._pending = new Map(); // id → { resolve, reject }
    this._msgId   = 0;
 
    this.worker.addEventListener('message', (e) => {
      this._handleMessage(e.data);
    });
  }
 
  _handleMessage({ type, id, data, error }) {
    const pending = this._pending.get(id);
 
    if (type === 'progress') {
      pending?.onProgress?.(data);
      return;
    }
 
    if (!pending) return;
    this._pending.delete(id);
 
    if (type === 'error') {
      pending.reject(new Error(error));
    } else {
      pending.resolve(data);
    }
  }
 
  _send(type, payload, onProgress) {
    return new Promise((resolve, reject) => {
      const id = ++this._msgId;
      this._pending.set(id, { resolve, reject, onProgress });
      this.worker.postMessage({ type, payload, id });
    });
  }
 
  async initialize(model = 'Xenova/whisper-tiny', onProgress) {
    return this._send('init', { model }, onProgress);
  }
 
  async transcribe(audioData, options = {}) {
    return this._send('transcribe', { audio: audioData, options });
  }
 
  terminate() {
    this.worker.terminate();
  }
}
 
// 使用
const whisperClient = new WhisperWorkerClient();
 
// 初始化(显示下载进度)
await whisperClient.initialize('Xenova/whisper-tiny', (progress) => {
  if (progress.status === 'downloading') {
    progressBar.style.width =
      `%`;
  }
});
 
// 识别
const result = await whisperClient.transcribe(pcmFloat32Array, {
  language: 'chinese',
  return_timestamps: true,
});
console.log('本地识别结果:', result.text);

五、音频分类与事件检测

5.1 用 TensorFlow.js 实现音频分类

import * as tf from '@tensorflow/tfjs';
import * as speechCommands from '@tensorflow-models/speech-commands';
 
class AudioClassifier {
  constructor() {
    this.recognizer = null;
    this.labels     = [];
  }
 
  async initialize(onProgress) {
    // 使用预训练的 Speech Commands 模型
    // 可识别:'yes', 'no', 'up', 'down', 'left', 'right' 等 18 个命令词
    this.recognizer = speechCommands.create('BROWSER_FFT');
 
    onProgress?.('加载模型...');
    await this.recognizer.ensureModelLoaded();
 
    this.labels = this.recognizer.wordLabels();
    console.log('可识别标签:', this.labels);
  }
 
  // 实时连续识别
  startListening(onResult, options = {}) {
    this.recognizer.listen(
      (result) => {
        const scores    = result.scores;
        const maxScore  = Math.max(...scores);
        const maxIndex  = scores.indexOf(maxScore);
        const label     = this.labels[maxIndex];
        const confidence = maxScore;
 
        // 过滤低置信度结果
        if (confidence > (options.threshold || 0.75)) {
          onResult({ label, confidence, scores });
        }
      },
      {
        includeSpectrogram:  true,
        probabilityThreshold: options.threshold || 0.75,
        invokeCallbackOnNoiseAndUnknown: false,
        overlapFactor: 0.5, // 50% 帧重叠,提高响应速度
      }
    );
  }
 
  stopListening() {
    this.recognizer.stopListening();
  }
}

5.2 自定义音频事件检测(迁移学习)

用少量样本训练自定义音频分类器(如检测掌声、哨声等特定声音):

class CustomAudioEventDetector {
  constructor(baseRecognizer) {
    this.base              = baseRecognizer;
    this.transferRecognizer = null;
    this.customLabels      = [];
    this.samples           = {}; // label → AudioBuffer[]
  }
 
  // 定义自定义标签
  defineLabels(labels) {
    this.customLabels = labels;
    labels.forEach(label => { this.samples[label] = []; });
 
    // 创建迁移学习识别器
    this.transferRecognizer = this.base.createTransfer('custom-detector');
  }
 
  // 采集训练样本(每个标签建议采集 10~20 个样本)
  async collectSample(label, durationMs = 1000) {
    if (!this.customLabels.includes(label)) {
      throw new Error(`未知标签: `);
    }
 
    return new Promise((resolve) => {
      console.log(`开始采集标签 [] 的样本...`);
 
      this.transferRecognizer.collectExample(label)
        .then(() => {
          const count = this.transferRecognizer.countExamples()[label] || 0;
          console.log(`[] 已采集 个样本`);
          resolve(count);
        });
 
      // durationMs 后自动停止
      setTimeout(() => {
        // collectExample 内部会自动停止
      }, durationMs);
    });
  }
 
  // 训练模型
  async train(onProgress) {
    console.log('开始训练自定义模型...');
 
    await this.transferRecognizer.train({
      epochs:         30,
      validationSplit: 0.2,
      callback: {
        onEpochEnd: (epoch, logs) => {
          const progress = ((epoch + 1) / 30) * 100;
          onProgress?.({
            epoch:    epoch + 1,
            loss:     logs.loss.toFixed(4),
            accuracy: (logs.acc * 100).toFixed(1),
            progress,
          });
        },
      },
    });
 
    console.log('训练完成!');
  }
 
  // 保存模型到 IndexedDB
  async saveModel(name = 'custom-audio-model') {
    await this.transferRecognizer.save(`indexeddb://`);
    console.log('模型已保存到 IndexedDB');
  }
 
  // 从 IndexedDB 加载模型
  async loadModel(name = 'custom-audio-model') {
    await this.transferRecognizer.load(`indexeddb://`);
    console.log('模型加载完成');
  }
 
  // 开始检测
  startDetection(onDetect, threshold = 0.85) {
    this.transferRecognizer.listen(
      (result) => {
        const scores   = Array.from(result.scores);
        const maxScore = Math.max(...scores);
        const maxIdx   = scores.indexOf(maxScore);
        const label    = this.customLabels[maxIdx];
 
        if (maxScore >= threshold) {
          onDetect({ label, confidence: maxScore });
        }
      },
      { probabilityThreshold: threshold }
    );
  }
 
  stopDetection() {
    this.transferRecognizer.stopListening();
  }
}
 
// 使用示例:训练一个能识别"鼓掌"和"安静"的检测器
async function trainClapDetector() {
  const classifier = new AudioClassifier();
  await classifier.initialize();
 
  const detector = new CustomAudioEventDetector(classifier.recognizer);
  detector.defineLabels(['clap', 'quiet', '_background_noise_']);
 
  // 引导用户采集样本
  console.log('请拍手 3 次...');
  for (let i = 0; i < 15; i++) {
    await detector.collectSample('clap', 1000);
    await new Promise(r => setTimeout(r, 200));
  }
 
  console.log('请保持安静...');
  for (let i = 0; i < 15; i++) {
    await detector.collectSample('quiet', 1000);
    await new Promise(r => setTimeout(r, 200));
  }
 
  // 训练
  await detector.train((p) => {
    console.log(`训练进度: % | 损失: | 准确率: %`);
  });
 
  // 保存并开始检测
  await detector.saveModel('clap-detector');
  detector.startDetection(({ label, confidence }) => {
    if (label === 'clap') {
      console.log(`检测到鼓掌!置信度: %`);
    }
  });
}

六、实时 AI 降噪

6.1 RNNoise:基于深度学习的实时降噪

RNNoise 是 Mozilla 开发的基于 RNN 的实时降噪算法,编译为 WebAssembly 后可在浏览器中实时运行,延迟极低(每帧 10ms):

// rnnoise-worklet.js(AudioWorklet 中运行)
// 需要先将 rnnoise.wasm 编译并加载
 
class RNNoiseProcessor extends AudioWorkletProcessor {
  constructor(options) {
    super();
    this.ready    = false;
    this.rnnoise  = null;
    this.denoise  = null;
    this.state    = null;
 
    // 从主线程接收 WASM 模块
    this.port.onmessage = async (e) => {
      if (e.data.type === 'init') {
        await this._initWasm(e.data.wasmBuffer);
        this.port.postMessage({ type: 'ready' });
      }
    };
  }
 
  async _initWasm(wasmBuffer) {
    // 加载编译好的 RNNoise WASM
    const wasmModule = await WebAssembly.instantiate(wasmBuffer, {
      env: {
        memory:          new WebAssembly.Memory({ initial: 256 }),
        __memory_base:   0,
        __table_base:    0,
        _abort:          () => {},
      },
    });
 
    this.rnnoise = wasmModule.instance.exports;
    // 创建降噪状态
    this.state   = this.rnnoise.rnnoise_create(0);
    this.ready   = true;
  }
 
  process(inputs, outputs) {
    if (!this.ready) {
      // 未就绪时透传
      outputs[0][0]?.set(inputs[0][0] || new Float32Array(128));
      return true;
    }
 
    const input  = inputs[0][0];
    const output = outputs[0][0];
    if (!input || !output) return true;
 
    // RNNoise 每次处理 480 个采样点(10ms @ 48kHz)
    // AudioWorklet 每次给 128 个采样点,需要缓冲
    // 这里简化处理,实际需要实现采样点缓冲队列
    const FRAME_SIZE = 480;
 
    if (!this._inputBuffer) {
      this._inputBuffer  = new Float32Array(FRAME_SIZE);
      this._outputBuffer = new Float32Array(FRAME_SIZE);
      this._bufferPtr    = 0;
    }
 
    for (let i = 0; i < input.length; i++) {
      this._inputBuffer[this._bufferPtr++] = input[i] * 32768; // 转 int16 范围
 
      if (this._bufferPtr >= FRAME_SIZE) {
        // 分配 WASM 内存并处理
        const ptr = this.rnnoise.malloc(FRAME_SIZE * 4);
        const buf = new Float32Array(
          this.rnnoise.memory.buffer, ptr, FRAME_SIZE
        );
        buf.set(this._inputBuffer);
 
        // 执行降噪
        this.rnnoise.rnnoise_process_frame(this.state, ptr, ptr);
 
        // 读回结果
        this._outputBuffer.set(buf);
        this.rnnoise.free(ptr);
        this._bufferPtr = 0;
      }
 
      // 输出降噪后的数据
      output[i] = (this._outputBuffer[i] || 0) / 32768;
    }
 
    return true;
  }
}
 
registerProcessor('rnnoise-processor', RNNoiseProcessor);

主线程集成:

class RealtimeDenoiser {
  constructor(audioCtx) {
    this.ctx     = audioCtx;
    this.ready   = false;
    this.node    = null;
  }
 
  async initialize() {
    // 加载 Worklet
    await this.ctx.audioWorklet.addModule('rnnoise-worklet.js');
 
    // 创建节点
    this.node = new AudioWorkletNode(this.ctx, 'rnnoise-processor', {
      numberOfInputs:  1,
      numberOfOutputs: 1,
      channelCount:    1,
    });
 
    // 加载 WASM 并发送给 Worklet
    const response    = await fetch('rnnoise.wasm');
    const wasmBuffer  = await response.arrayBuffer();
 
    await new Promise((resolve) => {
      this.node.port.onmessage = (e) => {
        if (e.data.type === 'ready') {
          this.ready = true;
          resolve();
        }
      };
      this.node.port.postMessage({ type: 'init', wasmBuffer });
    });
 
    console.log('RNNoise 降噪已就绪');
  }
 
  // 将降噪节点插入音频图
  process(sourceNode, destinationNode) {
    sourceNode.connect(this.node);
    this.node.connect(destinationNode);
    return this.node;
  }
}

七、AI 音频生成:Web 端音乐合成

7.1 Magenta.js:浏览器端 AI 音乐生成

Magenta.js 是 Google 的浏览器端 AI 音乐生成库,基于 TensorFlow.js:

import * as mm from '@magenta/music';
 
class AIMusician {
  constructor() {
    this.player    = new mm.SoundFontPlayer(
      'https://storage.googleapis.com/magentadata/js/soundfonts/sgm_plus'
    );
    this.rnn       = null;
    this.vae       = null;
  }
 
  async initialize() {
    // MusicRNN:基于 LSTM 的旋律续写模型
    this.rnn = new mm.MusicRNN(
      'https://storage.googleapis.com/magentadata/js/checkpoints/music_rnn/melody_rnn'
    );
    await this.rnn.initialize();
 
    // MusicVAE:变分自编码器,用于旋律插值和生成
    this.vae = new mm.MusicVAE(
      'https://storage.googleapis.com/magentadata/js/checkpoints/music_vae/mel_4bar_small_q2'
    );
    await this.vae.initialize();
 
    console.log('AI 音乐模型加载完成');
  }
 
  // 根据种子旋律续写
  async continuemelody(seedNotes, steps = 64) {
    // seedNotes 格式:NoteSequence
    const seed = {
      notes: seedNotes.map(n => ({
        pitch:           n.pitch,        // MIDI 音高(0~127)
        startTime:       n.startTime,    // 开始时间(秒)
        endTime:         n.endTime,      // 结束时间(秒)
        velocity:        n.velocity || 80,
        instrument:      0,
        program:         0,
      })),
      totalTime:      seedNotes[seedNotes.length - 1].endTime,
      tempos:         [{ time: 0, qpm: 120 }],
      quantizationInfo: { stepsPerQuarter: 4 },
    };
 
    // 量化
    const quantized = mm.sequences.quantizeNoteSequence(seed, 4);
 
    // 续写
    const continuation = await this.rnn.continueSequence(
      quantized,
      steps,
      1.0,    // temperature:越高越随机
    );
 
    return continuation;
  }
 
  // 随机生成旋律
  async generateMelody(numSamples = 1, temperature = 1.0) {
    const samples = await this.vae.sample(numSamples, temperature);
    return samples[0]; // 返回第一个样本
  }
 
  // 在两段旋律之间插值(生成过渡旋律)
  async interpolate(melodyA, melodyB, steps = 4) {
    const interpolated = await this.vae.interpolate(
      [melodyA, melodyB],
      steps
    );
    return interpolated; // 返回 steps 个过渡旋律
  }
 
  // 播放 NoteSequence
  async play(noteSequence) {
    await this.player.loadSamples(noteSequence);
    this.player.start(noteSequence);
  }
 
  stop() {
    this.player.stop();
  }
}
 
// 使用
const musician = new AIMusician();
await musician.initialize();
 
// 提供种子旋律(C大调音阶)
const seed = [
  { pitch: 60, startTime: 0,   endTime: 0.5 }, // C4
  { pitch: 62, startTime: 0.5, endTime: 1.0 }, // D4
  { pitch: 64, startTime: 1.0, endTime: 1.5 }, // E4
  { pitch: 65, startTime: 1.5, endTime: 2.0 }, // F4
];
 
// 续写 64 步
const continuation = await musician.continuemelody(seed, 64);
await musician.play(continuation);

八、实时字幕系统:完整实战

把语音识别、时间戳对齐、字幕渲染整合成一个完整的实时字幕系统——这是 AI 音频最有实用价值的场景之一:

class RealtimeSubtitleSystem {
  constructor(container, options = {}) {
    this.container   = container;
    this.options     = {
      mode:     options.mode     || 'browser', // 'browser' | 'cloud' | 'local'
      lang:     options.lang     || 'zh-CN',
      maxLines: options.maxLines || 3,
      ...options,
    };
 
    this.subtitles   = [];  // 历史字幕
    this.currentLine = '';  // 当前正在识别的行
    this._buildUI();
    this._initRecognizer();
  }
 
  _buildUI() {
    this.container.innerHTML = `
      <div class="subtitle-display">
        <div class="subtitle-history" id="subtitleHistory"></div>
        <div class="subtitle-current" id="subtitleCurrent"></div>
      </div>
      <div class="subtitle-controls">
        <button id="subStartBtn">▶ 开始字幕</button>
        <button id="subStopBtn"  disabled>⏹ 停止</button>
        <button id="subClearBtn">🗑 清空</button>
        <button id="subExportBtn">📄 导出 SRT</button>
      </div>
    `;
 
    document.getElementById('subStartBtn').addEventListener('click', () => this.start());
    document.getElementById('subStopBtn').addEventListener('click',  () => this.stop());
    document.getElementById('subClearBtn').addEventListener('click', () => this.clear());
    document.getElementById('subExportBtn').addEventListener('click', () => this.exportSRT());
 
    this.historyEl = document.getElementById('subtitleHistory');
    this.currentEl = document.getElementById('subtitleCurrent');
  }
 
  _initRecognizer() {
    if (this.options.mode === 'browser') {
      this.recognizer = new SpeechRecognizer({
        lang:           this.options.lang,
        continuous:     true,
        interimResults: true,
 
        onResult: ({ final, interim, isFinal }) => {
          if (isFinal && final.trim()) {
            this._addSubtitle(final.trim());
          } else {
            this._updateCurrent(interim);
          }
        },
 
        onError: (msg) => console.error('识别错误:', msg),
        onEnd:   ()    => this._onRecognitionEnd(),
      });
    }
    // cloud / local 模式在 start() 中初始化
  }
 
  async start() {
    document.getElementById('subStartBtn').disabled = true;
    document.getElementById('subStopBtn').disabled  = false;
    this.startTime = Date.now();
 
    if (this.options.mode === 'browser') {
      this.recognizer.start();
 
    } else if (this.options.mode === 'cloud') {
      // 云端模式:实时录音 + 定时发送给 Whisper
      this._startCloudMode();
 
    } else if (this.options.mode === 'local') {
      // 本地模式:Transformers.js Whisper
      await this._startLocalMode();
    }
  }
 
  async _startCloudMode() {
    this.cloudRecorder = new AudioRecorder();
    this.transcriber   = new WhisperTranscriber(this.options.apiKey);
 
    // 每 5 秒发送一次音频片段
    await this.cloudRecorder.start({ timeslice: 5000 });
 
    this.cloudRecorder.recorder.addEventListener('dataavailable', async (e) => {
      if (e.data.size < 1000) return; // 太小的片段跳过
 
      try {
        const result = await this.transcriber.transcribe(e.data);
        if (result.text.trim()) {
          this._addSubtitle(result.text.trim());
        }
      } catch (err) {
        console.warn('云端识别失败:', err.message);
      }
    });
  }
 
  async _startLocalMode() {
    if (!this.localWhisper) {
      this.localWhisper = new WhisperWorkerClient();
      this.currentEl.textContent = '加载本地模型...';
      await this.localWhisper.initialize(
        'Xenova/whisper-tiny',
        (p) => {
          if (p.status === 'downloading') {
            this.currentEl.textContent =
              `下载模型: %`;
          }
        }
      );
      this.currentEl.textContent = '';
    }
 
    // 实时录音 + 本地识别
    this.localStream  = await getMicrophoneStream();
    this.localAudioCtx = new AudioContext({ sampleRate: 16000 });
    const source      = this.localAudioCtx.createMediaStreamSource(this.localStream);
    const processor   = this.localAudioCtx.createScriptProcessor(4096, 1, 1);
 
    const pcmBuffer   = [];
    const CHUNK_SECS  = 5; // 每 5 秒识别一次
    const chunkSamples = 16000 * CHUNK_SECS;
 
    processor.onaudioprocess = async (e) => {
      const data = e.inputBuffer.getChannelData(0);
      pcmBuffer.push(...data);
 
      if (pcmBuffer.length >= chunkSamples) {
        const chunk = new Float32Array(pcmBuffer.splice(0, chunkSamples));
        try {
          const result = await this.localWhisper.transcribe(chunk, {
            language: 'chinese',
          });
          if (result.text.trim()) {
            this._addSubtitle(result.text.trim());
          }
        } catch (err) {
          console.warn('本地识别失败:', err);
        }
      }
    };
 
    source.connect(processor);
    processor.connect(this.localAudioCtx.destination);
    this._localProcessor = processor;
    this._localSource    = source;
  }
 
  stop() {
    if (this.options.mode === 'browser') {
      this.recognizer.stop();
    } else if (this.options.mode === 'cloud') {
      this.cloudRecorder?.stop();
    } else if (this.options.mode === 'local') {
      this._localSource?.disconnect();
      this._localProcessor?.disconnect();
      this.localStream?.getTracks().forEach(t => t.stop());
    }
 
    document.getElementById('subStartBtn').disabled = false;
    document.getElementById('subStopBtn').disabled  = true;
  }
 
  _onRecognitionEnd() {
    // 浏览器识别意外结束时自动重启
    if (document.getElementById('subStopBtn').disabled === false) {
      setTimeout(() => this.recognizer.start(), 300);
    }
  }
 
  _addSubtitle(text) {
    const timestamp = this._formatTimestamp(Date.now() - this.startTime);
    const entry     = { text, timestamp, time: Date.now() - this.startTime };
    this.subtitles.push(entry);
 
    // 更新历史显示
    const div = document.createElement('div');
    div.className   = 'subtitle-line';
    div.innerHTML   = `<span class="sub-time"></span><span class="sub-text"></span>`;
    this.historyEl.appendChild(div);
 
    // 保留最近 N 行
    const lines = this.historyEl.querySelectorAll('.subtitle-line');
    if (lines.length > this.options.maxLines) {
      lines[0].remove();
    }
 
    this.historyEl.scrollTop = this.historyEl.scrollHeight;
    this._updateCurrent('');
  }
 
  _updateCurrent(text) {
    this.currentEl.textContent = text;
  }
 
  clear() {
    this.subtitles  = [];
    this.historyEl.innerHTML = '';
    this.currentEl.textContent = '';
  }
 
  // 导出 SRT 格式字幕文件
  exportSRT() {
    const lines = this.subtitles.map((entry, i) => {
      const start = this._msToSRT(entry.time);
      const end   = this._msToSRT(
        entry.time + Math.max(2000, entry.text.length * 80)
      );
      return `\n-->\n\n`;
    });
 
    const blob = new Blob([lines.join('\n')], { type: 'text/plain' });
    const a    = document.createElement('a');
    a.href     = URL.createObjectURL(blob);
    a.download = `subtitle-.srt`;
    a.click();
    URL.revokeObjectURL(a.href);
  }
 
  _formatTimestamp(ms) {
    const s = Math.floor(ms / 1000);
    const m = Math.floor(s / 60);
    return `:`;
  }
 
  _msToSRT(ms) {
    const h   = Math.floor(ms / 3600000);
    const m   = Math.floor((ms % 3600000) / 60000);
    const s   = Math.floor((ms % 60000) / 1000);
    const ms3 = ms % 1000;
    return `::,`;
  }
}
 
// 使用
const subtitleSystem = new RealtimeSubtitleSystem(
  document.getElementById('subtitleContainer'),
  {
    mode:    'browser',  // 快速启动
    lang:    'zh-CN',
    maxLines: 4,
  }
);

九、本章知识图谱

流程图画布 · 115%
Mermaid 流程图加载中...

十、系列总结:Web 音频技术全景回顾

走过十二章,我们从最基础的 &lt;audio&gt; 标签出发,一路深入到 AI 音频推理。回头看这整条技术路线:

流程图画布 · 115%
Mermaid 流程图加载中...