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

使用EZOutput输出音频时出现机器人音及卡顿问题求助

问题:EZOutput输出音频出现机器人音、卡顿及丢音问题

我使用EZAudio框架的EZOutput组件实现类似LiveListen的功能,从self.myAudioBufferList获取音频流输出,同时持有音频基本描述self.asbd!。但实现EZOutput数据源的shouldFill方法后,输出声音呈现机器人音,还存在卡顿、部分音频丢失的情况。

当前实现的shouldFill方法代码:

func output(_ output: EZOutput!, shouldFill audioBufferList: UnsafeMutablePointer<AudioBufferList>!, withNumberOfFrames frames: UInt32, timestamp: UnsafePointer<AudioTimeStamp>!) -> OSStatus {
    
    if self.asbd != nil {
        output.inputFormat = self.asbd!
    }
    
    if self.audioBufferList != nil {
        audioBufferList.pointee.mNumberBuffers = self.myAudioBufferList!.mNumberBuffers
        audioBufferList.pointee.mBuffers.mNumberChannels = self.myAudioBufferList!.mBuffers.mNumberChannels
        audioBufferList.pointee.mBuffers.mData = self.myAudioBufferList!.mBuffers.mData
    } else {
        print("it is nil, and that is why it is making low noise")
        audioBufferList.pointee.mBuffers.mData = nil
    }
    
    return noErr;
}

另外,当设置audioBufferList的mDataByteSize时,音质会变得更差。当前配置中dataByteSize为4096,但myAudioBufferList的大小不固定,波动但从未超过512。设置代码如下:

audioBufferList.pointee.mBuffers.mDataByteSize = self.myAudioBufferList!.mBuffers.mDataByteSize

数据源方法传入的frames值为1024,我也曾尝试创建EZOutput实例时传入inputFormat,但音质没有改善:

// Assign a delegate and datasource to the shared instance of the output to provide the output audio data
self.ezOutput = EZOutput.init(dataSource: self, inputFormat: asbd)
self.ezOutput?.delegate = self

问题原因分析

  1. 音频帧数量不匹配:shouldFill要求提供1024帧数据,但myAudioBufferList实际帧数量远小于这个值(按16bit单声道计算,512字节仅对应256帧),直接赋值会导致输出端重复读取不足的数据,产生机器人音。
  2. 重复修改输入格式:每次回调都设置output.inputFormat会打乱EZOutput的内部状态,音频格式应仅在初始化时设置一次。
  3. 缓冲区指针直接赋值风险:直接将myAudioBufferList的mData指针传给输出缓冲区,既没处理数据长度不匹配的问题,还可能因原缓冲区被释放/覆盖导致异常。

修复方案

  1. 初始化时固定输入格式:移除回调内的output.inputFormat设置,确保创建EZOutput时传入正确的asbd,后续不再修改。
  2. 新增数据缓存处理帧不匹配:维护一个音频数据缓存,将myAudioBufferList的新数据存入缓存,当缓存数据量满足frames要求时再拷贝到输出缓冲区;不足时填充静音数据避免卡顿。
  3. 拷贝数据而非直接赋值指针:使用memcpy将原缓冲区数据拷贝到输出缓冲区,根据实际数据量设置mDataByteSize,剩余部分填充静音。

修复后的示例代码:

// 新增缓存用的音频数据容器,需匹配asbd格式初始化
private var audioDataCache = Data()

func output(_ output: EZOutput!, shouldFill audioBufferList: UnsafeMutablePointer<AudioBufferList>!, withNumberOfFrames frames: UInt32, timestamp: UnsafePointer<AudioTimeStamp>!) -> OSStatus {
    guard let asbd = self.asbd else { return noErr }
    
    // 计算单帧字节数:声道数 × 位深/8
    let bytesPerFrame = Int(asbd.mChannelsPerFrame) * Int(asbd.mBitsPerChannel / 8)
    let requiredBytes = Int(frames) * bytesPerFrame
    
    // 将新的音频数据加入缓存
    if let sourceBufferList = self.myAudioBufferList {
        let sourceBytes = Int(sourceBufferList.mBuffers.mDataByteSize)
        if let sourceData = sourceBufferList.mBuffers.mData {
            audioDataCache.append(UnsafeRawBufferPointer(start: sourceData, count: sourceBytes))
        }
    }
    
    // 取出需要的字节数,剩余数据留在缓存中
    let outputData = audioDataCache.prefix(requiredBytes)
    audioDataCache = audioDataCache.dropFirst(requiredBytes)
    
    // 填充输出缓冲区
    let buffer = &audioBufferList.pointee.mBuffers[0]
    buffer.mNumberChannels = asbd.mChannelsPerFrame
    
    if outputData.count > 0 {
        memcpy(buffer.mData, outputData.baseAddress, outputData.count)
    }
    
    // 数据不足时填充静音(以16bit单声道为例,静音值为0)
    let missingBytes = requiredBytes - outputData.count
    if missingBytes > 0 {
        let muteData = Data(repeating: 0, count: missingBytes)
        memcpy(buffer.mData?.advanced(by: outputData.count), muteData.baseAddress, missingBytes)
        buffer.mDataByteSize = UInt32(requiredBytes)
    } else {
        buffer.mDataByteSize = UInt32(outputData.count)
    }
    
    return noErr
}

额外注意事项

  • 确保asbd的采样率、声道数、位深等参数,与输入输出格式完全一致,格式不匹配也会导致音质异常。
  • 避免在shouldFill回调中执行耗时操作,该回调运行在音频线程,阻塞会引发卡顿。
  • 注意myAudioBufferList的生命周期,确保使用时数据有效,不会被提前释放。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 19:12:50