第十二章: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,
}
);九、本章知识图谱
十、系列总结:Web 音频技术全景回顾
走过十二章,我们从最基础的 <audio> 标签出发,一路深入到 AI 音频推理。回头看这整条技术路线: