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

使用Rust的cpal混合双音频流并实时写入WAV的异常问题

混合麦克风与系统音频并写入WAV的修复方案

原代码的核心问题

  • 设备配置不兼容:强制用麦克风的配置给系统音频捕获流,两者采样率、通道数可能不一致,导致流运行异常、丢采样。
  • 无同步的双缓冲区:两个独立环形缓冲区无法保证采样同步,读取时频繁跳过采样,最终写入的总采样数远不足5秒。
  • WAV格式错误:硬编码单通道,但实际输入可能是立体声,导致文件格式无效。
  • 时长控制逻辑错误:依赖时间循环而非采样数计算,主循环单采样读取效率低,容易丢帧。
  • 系统音频捕获方式错误:Windows下需用Loopback设备捕获系统输出,直接用默认输出设备作为输入无法正常工作。

修复后的完整代码

use std::time::Duration;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{StreamConfig, SupportedStreamConfig};
use ringbuf::HeapRb;
use anyhow::Result;

fn main() -> Result<()> {
    let host = cpal::default_host();

    // 获取麦克风输入设备
    let mic_device = host.default_input_device().expect("找不到麦克风设备");
    println!("使用麦克风设备: \"{}\"", mic_device.name()?);

    // 获取Windows系统音频Loopback捕获设备(用于捕获系统输出)
    let loopback_device = host.input_devices()?
        .find(|d| {
            let name = d.name().unwrap_or_default();
            name.contains("Loopback") || name.contains("回环")
        })
        .expect("找不到系统音频回环设备,请确保已启用Windows音频回环功能");
    println!("使用系统回环设备: \"{}\"", loopback_device.name()?);

    // 获取两个设备都支持的共同音频配置
    let mic_supported_configs = mic_device.supported_input_configs()?;
    let loopback_supported_configs = loopback_device.supported_input_configs()?;
    
    let common_config = find_common_config(mic_supported_configs, loopback_supported_configs)
        .expect("找不到两个设备都支持的音频配置");
    let config: StreamConfig = common_config.into();
    println!("使用统一配置: {:?}", config);

    // 计算缓冲区大小:按200ms延迟计算
    let latency_ms = 200.0;
    let latency_frames = (latency_ms / 1000.0) * config.sample_rate.0 as f32;
    let buffer_size = (latency_frames * config.channels as f32) as usize * 2;

    // 创建共享环形缓冲区(存储混合后的采样)
    let ring = HeapRb::<f32>::new(buffer_size);
    let (mut producer, mut consumer) = ring.split();

    // 预填充静音,避免初始阶段无采样
    for _ in 0..(buffer_size / 2) {
        producer.push(0.0).unwrap();
    }

    // 麦克风输入回调:将采样写入缓冲区
    let mut mic_buffer = Vec::with_capacity(config.channels as usize);
    let mic_callback = move |data: &[f32], _: &cpal::InputCallbackInfo| {
        mic_buffer.clear();
        mic_buffer.extend_from_slice(data);
        
        let mut fell_behind = false;
        for &sample in &mic_buffer {
            if producer.push(sample).is_err() {
                fell_behind = true;
            }
        }
        if fell_behind {
            eprintln!("麦克风流缓冲区已满,增加延迟或缓冲区大小");
        }
    };

    // 系统音频回环回调:与麦克风采样混合后写入缓冲区
    let mut loopback_buffer = Vec::with_capacity(config.channels as usize);
    let loopback_callback = move |data: &[f32], _: &cpal::InputCallbackInfo| {
        loopback_buffer.clear();
        loopback_buffer.extend_from_slice(data);
        
        let mut fell_behind = false;
        for (_, &sample) in loopback_buffer.iter().enumerate() {
            // 读取对应位置的麦克风采样(无数据则用静音)
            let mic_sample = consumer.pop().unwrap_or(0.0);
            // 混合采样:平均处理避免音频过载
            let mixed_sample = (sample + mic_sample) / 2.0;
            if producer.push(mixed_sample).is_err() {
                fell_behind = true;
            }
        }
        if fell_behind {
            eprintln!("系统音频流缓冲区已满,增加延迟或缓冲区大小");
        }
    };

    // 创建并启动两个输入流
    let mic_stream = mic_device.build_input_stream(&config, mic_callback, err_fn, None)?;
    let loopback_stream = loopback_device.build_input_stream(&config, loopback_callback, err_fn, None)?;

    println!("启动音频流,延迟 {}ms", latency_ms);
    mic_stream.play()?;
    loopback_stream.play()?;

    // 计算5秒需要的总采样数
    let total_samples = config.sample_rate.0 as usize * config.channels as usize * 5;
    println!("需要写入 {} 个采样", total_samples);

    // 创建WAV写入器,匹配实际音频配置
    let spec = hound::WavSpec {
        channels: config.channels,
        sample_rate: config.sample_rate.0,
        bits_per_sample: 32,
        sample_format: hound::SampleFormat::Float,
    };
    let mut writer = hound::WavWriter::create("mixed_output.wav", spec)?;

    // 读取混合后的采样并写入WAV
    let mut written_samples = 0;
    while written_samples < total_samples {
        if let Some(sample) = consumer.pop() {
            writer.write_sample(sample)?;
            written_samples += 1;
        } else {
            // 缓冲区无数据时短暂休眠,降低CPU占用
            std::thread::sleep(Duration::from_micros(10));
        }
    }

    // 收尾工作
    writer.finalize()?;
    println!("WAV文件生成完成");

    drop(mic_stream);
    drop(loopback_stream);
    Ok(())
}

// 辅助函数:查找两个设备都支持的共同音频配置
fn find_common_config(
    mic_configs: impl Iterator<Item = SupportedStreamConfig>,
    loopback_configs: impl Iterator<Item = SupportedStreamConfig>,
) -> Option<SupportedStreamConfig> {
    let mic_configs: Vec<_> = mic_configs.collect();
    for lb_config in loopback_configs {
        if mic_configs.iter().any(|mc| {
            mc.sample_rate() == lb_config.sample_rate()
            && mc.channels() == lb_config.channels()
            && mc.sample_format() == lb_config.sample_format()
        }) {
            return Some(lb_config);
        }
    }
    None
}

fn err_fn(err: cpal::StreamError) {
    eprintln!("音频流错误: {}", err);
}

关键修复说明

  • Loopback设备捕获:通过遍历输入设备找到Windows回环设备,确保能正确捕获系统音频输出。
  • 统一配置:通过辅助函数找到两个设备都支持的采样率、通道数和格式,避免配置不兼容。
  • 同步混合:在系统音频回调中读取对应位置的麦克风采样,实时混合后写入缓冲区,保证采样同步。
  • 采样数控制时长:根据采样率和通道数精确计算5秒所需的总采样数,确保写入足够数据。
  • 优化缓冲区处理:批量处理采样,缓冲区无数据时短暂休眠,降低CPU占用。

内容的提问来源于stack exchange,提问作者Jake

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 23:19:51