MediaRecorder 本身并没有一个叫做 audio channel configuration 的独立配置项,它的录制数据通道数取决于输入 MediaStreamTrack 的实际设置。换句话说,录音时希望得到单声道还是立体声,必须提前在 getUserMedia 获取音频轨道时通过约束声明,或让 AudioContext 输出的 MediaStreamDestination 携带固定通道数。很多工程项目直接把 channelCount 当作普通 number 处理,结果在切换单声道和立体声时只修改了一处业务常量,却漏掉约束对象或渲染图里的对称处理。为了避免这类问题,可以在 TypeScript 中定义少量字面量类型,把通道数收敛为 1 和 2,同时提供 mono 与 stereo 的语义映射。

先理清 audio channelCount 在 MediaStream 中的位置
浏览器提供的 MediaTrackConstraints 接口允许开发者对音频轨道声明 channelCount、sampleRate、echoCancellation、noiseSuppression 等约束。其中 channelCount 虽然标准中类型写的是 number,但在真实采集场景里,麦克风通常只稳定支持 1 声道和 2 声道。如果把 3、4 或其他数值传入 applyConstraints,浏览器很可能返回 OverconstrainedError,或者静默降级到设备默认值。因此类型约束不能只停留在 number,而要利用 TypeScript 字面量类型创建联合类型。
音频数据在解码后不是一次性传输所有声道,而是按帧交错排列。单声道时每个采样点只有一个 PCM 值,立体声时同一时间点包含左声道和右声道两个 PCM 值。这个差异会影响后续 Web Audio 图中的 ChannelSplitterNode、ChannelMergerNode 以及 AnalyserNode 的频域数据长度。假设录音业务只支持单声道和立体声输出,就可以把类型定义成下面这样:
type AudioChannelCount = 1 | 2;
type AudioChannelMode = 'mono' | 'stereo';
const channelCountMap: Record<AudioChannelMode, AudioChannelCount> = {
mono: 1,
stereo: 2
};
这里的 Record 泛型需要用 < 和 > 转义,避免在 HTML 页面中破坏标签结构。AudioChannelCount 把合法通道数限制为一个闭合集合,任何地方只要接收这个类型,传 3 就会在编译期直接报错。AudioChannelMode 则面向产品层配置,比如用户选择单声道降噪模式还是立体声原声模式,最终通过 channelCountMap 映射到真实约束值。
给 MediaRecorder 链路补充类型安全的工厂函数
MediaRecorder 的构造参数只有 mimeType、audioBitsPerSecond、videoBitsPerSecond 等,并不会读取独立声道字段。要实现单声道或立体声录制,需要在前面创建 MediaStream 时处理约束。可以封装一个工厂函数,接收 AudioChannelMode,内部根据映射函数生成 MediaStreamConstraints,保证输入输出类型一致。
async function createAudioStreamForRecording(mode: AudioChannelMode): Promise<MediaStream> {
const channelCount = channelCountMap[mode];
const constraints: MediaStreamConstraints = {
audio: {
channelCount,
echoCancellation: true,
noiseSuppression: mode === 'mono'
},
video: false
};
return navigator.mediaDevices.getUserMedia(constraints);
}
async function createRecorder(mode: AudioChannelMode): Promise<MediaRecorder> {
const stream = await createAudioStreamForRecording(mode);
const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
? 'audio/webm;codecs=opus'
: 'audio/webm';
const recorder = new MediaRecorder(stream, {
mimeType,
audioBitsPerSecond: mode === 'stereo' ? 128000 : 64000
});
return recorder;
}
这个工厂函数把 mode 作为唯一入口,通道数、降噪策略、比特率都跟随 mode 变化。相比在业务代码中散落 channelCount: 1 或 channelCount: 2,这样的封装更容易验证。例如用户从单声道切到立体声时,不再需要同时修改多个对象,也不容易出现只改了 track.enabled 而没改 channelCount 的情况。
还要注意 constraints 中 channelCount 属于理想值还是强制值的问题。getUserMedia 的普通约束对象默认按理想值处理,如果设备不支持立体声,浏览器会尝试满足其他部分。若业务确实严格要求必须立体声,应使用 advanced 数组并放置 channelCount: 2,但这样会增加失败概率。类型层面同样可以定义严格模式与普通模式两个函数,避免调用方误用。
用类型守卫拦住运行时未知值
来自接口配置、localStorage 或远程下发的声道设置往往被 TypeScript 推断为 string 或 unknown。即便编译期声称是 AudioChannelMode,运行时仍可能因为 JSON 里写了 surround 或 5.1 而进入异常分支。此时需要类型守卫,把未知值安全收窄。
function isAudioChannelMode(value: unknown): value is AudioChannelMode {
return value === 'mono' || value === 'stereo';
}
function parseAudioChannelMode(raw: string | null): AudioChannelMode {
if (isAudioChannelMode(raw)) {
return raw;
}
return 'mono';
}
类型守卫中的 value is AudioChannelMode 使用 is 关键字,不需要尖括号,因此不会破坏 HTML。解析函数把无效值回退到 mono,也可以换成抛错,取决于业务是倾向降级还是快速失败。这里的关键在于收窄后,TypeScript 可以明确后续传入 createRecorder 的参数一定合法,而不会把 string 塞进只接受联合类型的函数。
另一种常见做法是把字符串和数字统一到 channelCount 层面。比如后台返回 { channelCount: 1 },而不是 mono 字符串。此时需要另一层守卫。建议项目中只保留一个权威类型,其他形式都在边界处转换。可以定义 fromChannelCount 函数,让 1 映射为 mono,2 映射为 stereo,其他值返回 null,再由上层决定默认值。
扩展全局类型与 AudioContext 连接时的注意点
有些团队会把 MediaRecorderOptions 或 MediaTrackConstraints 声明合并到全局,以便在构造时直接使用自定义字段。但这样做并不推荐,除非确实对接了自定义浏览器内核或封装了本地插件。因为标准 DOM 类型由 lib.dom.d.ts 管理,声明合并后会影响所有依赖该类型的代码,维护成本较高。更好的方式是把自定义约束类型放在项目内部,例如 AudioRecorderConfig,并让它与实际 DOM 类型保持映射关系。
如果录音源不是麦克风,而是 Web Audio 合成器或播放器,则应通过 AudioContext.createMediaStreamDestination 获取目标流。目标流的 channelCount 取决于上游节点的输出通道。例如 OscillatorNode 默认是单声道,StereoPannerNode 输出立体声,ChannelMergerNode 可以把多个单声道输入合并为立体声。此时不应再给 getUserMedia 传 channelCount,因为音频源已经固定。正确的做法是在 AudioContext 链路中设置节点的 channelCount 或 channelCountMode,再把 destination.stream 交给 MediaRecorder。
const audioContext = new AudioContext();
const oscillator = audioContext.createOscillator();
const gain = audioContext.createGain();
const destination = audioContext.createMediaStreamDestination();
oscillator.connect(gain);
gain.connect(destination);
const recorder = new MediaRecorder(destination.stream, {
mimeType: 'audio/webm;codecs=opus'
});
oscillator.start();
recorder.start();
这里没有直接使用 channelCount 约束,但如果需要强制立体声,可以在 gain 节点上设置 gain.channelCount = 2 并配合 channelCountMode = 'explicit'。这种设置对 OscillatorNode 这类单声道源来说可能仍只有一个声道有数据,所以需要先经过 ChannelMergerNode 或 StereoPannerNode 生成真正的左右声道内容。类型层面可以定义 AudioNodeWithChannelControl 接口,描述这些可配置节点,避免把 channelCount 错误地设置到不支持的节点上。
测试与边界:不能让类型掩盖真实设备差异
单声道和立体声类型定义得再严格,最终仍然受设备能力影响。某些蓝牙耳机的麦克风在通话模式下只提供 8kHz 单声道,某些 USB 麦克风在 Windows 默认驱动下可提供 48kHz 双声道。代码应当在录音前读取 MediaStreamTrack.getSettings() 中的 channelCount,确认实际生效值。该字段的标准类型在 lib.dom.d.ts 中是 number | undefined,因此需要再做一次运行时判断。
function getEffectiveChannelCount(stream: MediaStream): AudioChannelCount {
const track = stream.getAudioTracks()[0];
if (!track) {
return 1;
}
const settings = track.getSettings();
return settings.channelCount === 2 ? 2 : 1;
}
这个函数把 undefined、0 或不支持的数值都归为单声道,符合多数应用场景。若后续需要支持更多声道,可以把 AudioChannelCount 扩展为 1 | 2 | 6,但要注意 Web Audio 中某些处理节点对 6 声道支持并不一致。扩展类型时尽量同步修改 channelCountMap 和运行时守卫,否则联合类型和运行时映射会出现割裂。
总之,TypeScript 中定义单声道与立体声类型不是为了替代浏览器约束,而是为了给 MediaRecorder 录制链路增加一层编译期约束。通过 AudioChannelCount 和 AudioChannelMode 两个字面量联合类型,配合映射表、类型守卫和工厂函数,项目可以将声道配置从分散的 number 比较提升为可追踪的业务语义。这样在后续维护中,单声道与立体声的切换、降级、设备差异处理都会更加直观。
TypeScriptMediaRecorder声道配置修改时间:2026-09-26 22:10:15