TypeScript项目里接入语音活动检测(Voice Activity Detection,简称VAD)时,最让人头疼的往往不是算法本身,而是浏览器API缺少官方类型声明。Media Track Processing 提供的 VAD 能力可以让前端在不引入第三方模型的前提下判断当前音频轨道中是否有人说话,但相关接口在 lib.dom.d.ts 中大多没有定义,直接调用会报属性不存在的错误。这篇文章就来完整梳理如何为这套 API 设计一套清晰、可维护的TypeScript类型。

语音活动检测的核心概念与类型建模思路
在动手写类型之前,先要理解VAD API的数据流。检测器附着在一个MediaStreamTrack上,持续分析音频帧,每当检测到语音开始或结束,就会通过回调或事件把一个带有时间戳的区间推给调用方。围绕这条链路,我们需要为三类对象建模:检测器本身、检测结果(语音段区间)、以及创建检测器时传入的配置项。
建模的第一步是把“检测状态”抽象出来。语音检测通常只有两个稳定态:有声(voiced)和无声(unvoiced),再加上一个初始的“未开始”状态。用字符串字面量联合类型比枚举更轻量,也能获得更好的提示体验。检测结果的核心是时间区间,用秒还是用媒体时间戳要提前定好,这里建议统一用毫秒级的 DOMHighResTimeStamp,与 performance.now() 保持一致,方便和音频上下文对齐。
核心类型定义代码
下面是一套可以直接放进项目的类型定义。它包含了状态联合类型、语音段接口、配置项接口,以及检测器类的签名。
/** 检测状态:未检测 / 有语音 / 无语音 */
export type VadState = 'silence' | 'voice' | 'unknown';
/** 一次语音活动区间 */
export interface VoiceActivitySegment {
/** 语音开始时间,毫秒 */
readonly startTime: DOMHighResTimeStamp;
/** 语音结束时间,毫秒,进行中的段为 null */
readonly endTime: DOMHighResTimeStamp | null;
/** 区间状态 */
readonly state: VadState;
/** 该区间内的平均音量,0 到 1 之间 */
readonly level: number;
}
/** 检测器配置 */
export interface VadOptions {
/** 检测灵敏度,越大越容易判定为语音,默认 0.6 */
sensitivity?: number;
/** 判定为语音结束前的静音等待毫秒数 */
hangoverMs?: number;
/** 状态变化回调 */
onSegment?: (segment: VoiceActivitySegment) => void;
}
/** 语音活动检测器 */
export interface VoiceActivityDetector {
readonly track: MediaStreamTrack;
start(options?: VadOptions): Promise<void>;
stop(): void;
/** 获取最近一次检测状态 */
getState(): VadState;
/** 订阅语音段,返回取消订阅函数 */
onSegmentChange(listener: (segment: VoiceActivitySegment) => void): () => void;
}这套定义里有两个细节值得注意。第一,endTime 被设计成可为 null,用来表示“语音正在进行中”,这样实时渲染字幕条之类的场景不需要等区间闭合就能拿到反馈。第二,onSegmentChange 返回一个取消订阅函数,这是现代前端管理事件生命周期的惯用模式,配合React的useEffect清理逻辑非常顺手。
通过declare module补全浏览器原生API声明
如果你希望直接调用浏览器原生暴露在 MediaStreamTrack 上的检测方法,而不是自己封装一层,那么就需要用声明合并(declaration merging)扩展内置类型。做法是在项目里新建一个 vad.d.ts 文件,通过 declare global 把接口挂到全局命名空间。
// vad.d.ts
declare global {
interface MediaStreamTrack {
/** 对当前轨道开启语音活动检测 */
createVoiceActivityDetector?(
init?: VoiceActivityDetectorInit
): Promise<VoiceActivityDetector>;
}
interface VoiceActivityDetectorInit {
sensitivity?: number;
hangoverMs?: number;
}
interface Window {
VoiceActivityDetector?: new () => VoiceActivityDetector;
}
}
export {};声明合并生效后,track.createVoiceActivityDetector 就不会再报错。这里把方法定义为可选属性是一个防御性设计:即便用户浏览器不支持该API,TypeScript也不会诱导开发者写出必然抛异常的代码。实际调用处配合特性检测判断即可:
async function attachVad(track: MediaStreamTrack) {
if (typeof track.createVoiceActivityDetector !== 'function') {
throw new Error('当前浏览器不支持语音活动检测');
}
const detector = await track.createVoiceActivityDetector({
sensitivity: 0.7,
hangoverMs: 300,
});
return detector;
}另一个常见的坑是把声明文件写在了普通目录却没有被 tsconfig 包含。要确认 vad.d.ts 落在 include 覆盖的路径内,否则声明合并不会生效,编辑器依旧会提示方法不存在。
封装类型安全的检测器实现与使用示例
类型定义只是骨架,真正让它发挥作用的是一套带泛型的封装。比如你希望检测结果里额外携带自定义数据(说话人ID、房间号等),可以用泛型参数扩展区间类型:
export class TypedVadDetector<T = unknown> implements VoiceActivityDetector {
private listeners = new Set<(s: VoiceActivitySegment & Partial<T>) => void>();
private currentState: VadState = 'unknown';
constructor(public readonly track: MediaStreamTrack) {}
async start(options?: VadOptions): Promise<void> {
const detector = await attachVad(this.track);
// 省略内部绑定逻辑
void detector;
}
stop(): void {
this.listeners.clear();
this.currentState = 'unknown';
}
getState(): VadState {
return this.currentState;
}
onSegmentChange(
listener: (segment: VoiceActivitySegment & Partial<T>) => void
): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
}使用时只需要一行泛型标注,就能让回调里的参数带上完整类型推断:
const detector = new TypedVadDetector<{ speakerId: string }>(micTrack);
detector.onSegmentChange((segment) => {
// segment.speakerId 和 segment.startTime 都有完整类型提示
console.log(segment.state, segment.speakerId, segment.startTime);
});最后补充一点工程建议:把类型定义、声明合并和实现类拆成三个文件(types.ts、vad.d.ts、detector.ts),对外只通过 index.ts 导出公共类型。这样当浏览器规范更新、接口签名变化时,你只需要改动声明层,业务代码基本不用动。类型定义做得扎实,后续接入实时字幕、静音检测统计、说话人切换提醒等功能时都会轻松很多。
TypeScript Voice Activity Detection Web Audio API修改时间:2026-09-03 17:50:57