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

如何配置VoiceProcessingIO+重采样+低通滤波器的音频单元链?

解决方案:搭建多音频单元处理链路

Hey there! Let's work through setting up your audio processing pipeline step by step—getting multiple audio units connected properly can feel tricky at first, but using Apple's AudioComponentGraph is the standard way to make this happen smoothly.

整体数据流思路

Here's the clear processing chain we'll build:
VoiceProcessingIO (硬件输入,通常为48kHz) → AUConverter (下采样至16kHz) → LowPassFilter (应用低通滤波) → 你的自定义音频处理(比如发送音频消息)


分步实现代码

1. 先声明全局引用

你需要跟踪处理图和每个音频单元的实例:

private AudioComponentGraph _audioGraph;
private AudioUnit _voiceProcessingIO;
private AudioUnit _sampleRateConverter;
private AudioUnit _lowPassFilter;
private AudioStreamBasicDescription _hardwareInputFormat; // 硬件原生采样率格式(一般是48kHz)
private AudioStreamBasicDescription _target16kFormat; // 我们需要的16kHz目标格式

2. 初始化AudioSession(复用你的现有代码,做少量调整)

这里我们会同时捕获硬件原生格式和目标16kHz格式:

private void SetupAudioSession()
{
    AudioSession.Initialize();
    AudioSession.Category = AudioSessionCategory.PlayAndRecord;
    AudioSession.Mode = AudioSessionMode.GameChat;
    AudioSession.PreferredHardwareIOBufferDuration = 0.08f;

    // 捕获硬件输入格式,和VoiceProcessingIO接收的格式匹配
    _hardwareInputFormat = new AudioStreamBasicDescription
    {
        Format = AudioFormatType.LinearPCM,
        FormatFlags = AudioFormatFlags.LinearPCMIsSignedInteger | AudioFormatFlags.LinearPCMIsPacked,
        SampleRate = AudioSession.CurrentHardwareSampleRate,
        FramesPerPacket = SoundSettings.FramesPerPacket,
        BytesPerFrame = SoundSettings.BytesPerSample * SoundSettings.Channels,
        BytesPerPacket = SoundSettings.FramesPerPacket * SoundSettings.BytesPerSample * SoundSettings.Channels,
        BitsPerChannel = SoundSettings.BytesPerSample * 8,
        ChannelsPerFrame = SoundSettings.Channels,
        Reserved = 0
    };

    // 定义目标16kHz格式
    _target16kFormat = new AudioStreamBasicDescription
    {
        Format = AudioFormatType.LinearPCM,
        FormatFlags = AudioFormatFlags.LinearPCMIsSignedInteger | AudioFormatFlags.LinearPCMIsPacked,
        SampleRate = SoundSettings.SampleRate, // 16000
        FramesPerPacket = SoundSettings.FramesPerPacket,
        BytesPerFrame = SoundSettings.BytesPerSample * SoundSettings.Channels,
        BytesPerPacket = SoundSettings.FramesPerPacket * SoundSettings.BytesPerSample * SoundSettings.Channels,
        BitsPerChannel = SoundSettings.BytesPerSample * 8,
        ChannelsPerFrame = SoundSettings.Channels,
        Reserved = 0
    };
}

3. 构建并配置音频处理图

这一步是把所有单元串联起来的核心:

private void PrepareAudioProcessingPipeline()
{
    // 1. 创建音频处理图,用于管理所有音频单元
    AudioComponentGraph.New(out _audioGraph);

    // 2. 添加VoiceProcessingIO节点(获取无回声的输入音频)
    var voiceIOComponent = AudioComponent.FindComponent(AudioTypeOutput.VoiceProcessingIO);
    _audioGraph.AddNode(voiceIOComponent, out var voiceIONode);
    _audioGraph.GetNodeInfo(voiceIONode, out _, out _voiceProcessingIO);

    // 配置VoiceProcessingIO:启用输入,禁用输出(因为你不需要播放音频)
    _voiceProcessingIO.SetEnableIO(true, AudioUnitScopeType.Input, 1);
    _voiceProcessingIO.SetEnableIO(false, AudioUnitScopeType.Output, 0);
    // 设置输入格式匹配硬件原生格式
    _voiceProcessingIO.SetFormat(_hardwareInputFormat, AudioUnitScopeType.Input, 1);

    // 3. 添加AUConverter节点(处理48kHz→16kHz的下采样)
    var converterComponent = AudioComponent.FindComponent(AudioTypeConverter.AUConverter);
    _audioGraph.AddNode(converterComponent, out var converterNode);
    _audioGraph.GetNodeInfo(converterNode, out _, out _sampleRateConverter);

    // 告诉转换器输入格式和输出格式
    _sampleRateConverter.SetFormat(_hardwareInputFormat, AudioUnitScopeType.Input, 0);
    _sampleRateConverter.SetFormat(_target16kFormat, AudioUnitScopeType.Output, 0);

    // 4. 添加LowPassFilter节点(应用低通滤波)
    var filterComponent = AudioComponent.FindComponent(AudioTypeEffect.LowPassFilter);
    _audioGraph.AddNode(filterComponent, out var filterNode);
    _audioGraph.GetNodeInfo(filterNode, out _, out _lowPassFilter);

    // 配置滤波器使用目标16kHz格式作为输入输出
    _lowPassFilter.SetFormat(_target16kFormat, AudioUnitScopeType.Input, 0);
    _lowPassFilter.SetFormat(_target16kFormat, AudioUnitScopeType.Output, 0);
    // 可选:调整截止频率(示例设置为3000Hz,可根据需求修改)
    var cutoffFreq = 3000.0f;
    _lowPassFilter.SetParameter(AudioUnitParameterID.LowPassFilter_CutoffFrequency, 
                               AudioUnitScopeType.Global, 0, cutoffFreq, 0);

    // 5. 连接各个节点:按顺序把前一个单元的输出接到后一个的输入
    // VoiceProcessingIO的输入总线(1)接入转换器的输入总线(0)
    _audioGraph.ConnectNodes(voiceIONode, 1, converterNode, 0);
    // 转换器的输出总线(0)接入滤波器的输入总线(0)
    _audioGraph.ConnectNodes(converterNode, 0, filterNode, 0);

    // 6. 在滤波器的输出端设置回调,获取处理完成的音频
    _lowPassFilter.SetRenderCallback(this.HandleFilteredAudio, AudioUnitScopeType.Output, 0);

    // 7. 初始化并启动处理图
    _audioGraph.Initialize();
    _audioGraph.Start();
}

4. 在回调中处理已完成的音频

这里你会拿到已经下采样到16kHz并完成滤波的音频数据:

private AudioUnitStatus HandleFilteredAudio(
    AudioUnitRenderActionFlags actionFlags, 
    AudioTimeStamp timeStamp, 
    uint busNumber, 
    uint numberFrames, 
    AudioBuffers data)
{
    // 从低通滤波器拉取处理后的音频数据
    var status = _lowPassFilter.Render(ref actionFlags, timeStamp, 0, numberFrames, data);
    if (status != AudioUnitStatus.OK)
    {
        return status;
    }

    // 计算音频数据的字节大小
    int dataByteSize = (int)(numberFrames * _target16kFormat.BytesPerFrame);
    var audioBytes = new byte[dataByteSize];
    Marshal.Copy(data[0].Data, audioBytes, 0, dataByteSize);

    // 执行你的自定义逻辑:创建音频消息并触发事件
    var audioMsg = _msgFactory.CreateAudioMsg(audioBytes, audioBytes.Length, (++_lastIndex));
    this.OnMsgReady(audioMsg);

    // 因为我们禁用了VoiceProcessingIO的输出,这里不需要清零缓冲区
    return AudioUnitStatus.NoError;
}

关于你提到的博客疑问解答

咱们来拆解那篇博客里的特殊用法:

  • 两个转换器:这通常是没必要的!单个AUConverter可以同时处理多种格式转换(采样率、声道数、位深)。博客作者可能是为了代码清晰拆分步骤,或者用了较旧的处理方式,但你的场景完全不需要两个转换器。
  • Remote类型音频单元:Remote单元用于跨进程音频处理(比如连接系统级音频服务),而你是本地处理,完全不需要用到它,用本地单元就足够。
  • 总线顺序:音频单元有固定的总线编号规则:像VoiceProcessingIO这类I/O单元,总线1对应输入,总线0对应输出;而转换器、滤波器这类处理单元,一般用总线0同时作为输入和输出。博客里的顺序只是遵循了这个标准,确保数据能从上游单元的输出流向下游单元的输入。

清理资源的小提示

不用音频处理时,记得销毁处理图和单元,避免内存泄漏:

private void CleanupAudioResources()
{
    _audioGraph?.Stop();
    _audioGraph?.Dispose();
    _voiceProcessingIO?.Dispose();
    _sampleRateConverter?.Dispose();
    _lowPassFilter?.Dispose();
}

另外,记得监听AudioSession.Interrupted事件,处理来电这类音频会话中断的情况,及时暂停/恢复处理图。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.09 11:27:54