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

使用AVSampleBufferAudioRenderer播放Opus解码PCM流音频遇问题

编辑: 根据建议更新了代码,修复了ASBD并尝试修正PTS。目前仍无音频输出,但至少已无报错。


iOS Opus音频解码播放问题排查

我正在开发一个iOS项目,接收Opus音频数据包,尝试使用AVSampleBufferAudioRenderer进行播放。当前使用Opus自带的解码器,最终只需让解码后的PCM数据包播放即可。整个流程缺乏详尽文档,但我认为已接近目标。以下是目前的代码(经过精简,部分值硬编码以简化逻辑):

static AVSampleBufferAudioRenderer* audioRenderer;
static AVSampleBufferRenderSynchronizer* renderSynchronizer;

int samplesPerFrame = 240;
int channelCount    = 2;
int sampleRate      = 48000;
int streams         = 1;
int coupledStreams  = 1;
char mapping[8] = ['\0','\x01','\0','\0','\0','\0','\0','\0'];

CMTime startPTS;

// called when the stream is about to start
void AudioInit()
{
    renderSynchronizer = [[AVSampleBufferRenderSynchronizer alloc] init];
    audioRenderer = [[AVSampleBufferAudioRenderer alloc] init];
    [renderSynchronizer addRenderer:audioRenderer];
    
    int decodedPacketSize = samplesPerFrame * sizeof(short) * channelCount; // 240 samples per frame * 2 channels
    decodedPacketBuffer = SDL_malloc(decodedPacketSize);
    
    int err;
    opusDecoder = opus_multistream_decoder_create(sampleRate,       // 48000
                                                  channelCount,     // 2
                                                  streams,          // 1
                                                  coupledStreams,   // 1
                                                  mapping,
                                                  &err);

    [renderSynchronizer setRate:1.0 time:kCMTimeZero atHostTime:CMClockGetTime(CMClockGetHostTimeClock())];
    startPTS = CMClockGetTime(CMClockGetHostTimeClock());
}

// called every X milliseconds with a new packet of audio data to play, IF there's audio. (while testing, X = 5)
void AudioDecodeAndPlaySample(char* sampleData, int sampleLength)
{
    // decode the packet from Opus to (I think??) Linear PCM
    int numSamples;
    numSamples = opus_multistream_decode(opusDecoder,
                                         (unsigned char *)sampleData,
                                         sampleLength,
                                         (short*)decodedPacketBuffer,
                                         samplesPerFrame, // 240
                                         0);

    int bufferSize = sizeof(short) * numSamples * channelCount; // 240 samples * 2 channels

    CMTime currentPTS = CMTimeSubtract(CMClockGetTime(CMClockGetHostTimeClock()), startPTS);

    // LPCM stream description
    AudioStreamBasicDescription asbd = {
        .mFormatID          = kAudioFormatLinearPCM,
        .mFormatFlags       = kLinearPCMFormatFlagIsSignedInteger,
        .mBytesPerPacket    = sizeof(short) * channelCount,
        .mFramesPerPacket   = 1,
        .mBytesPerFrame     = sizeof(short) * channelCount,
        .mChannelsPerFrame  = channelCount, // 2
        .mBitsPerChannel    = 16,
        .mSampleRate        = sampleRate // 48000,
        .mReserved          = 0
    };
    
    // audio format description wrapper around asbd
    CMAudioFormatDescriptionRef audioFormatDesc;
    OSStatus status = CMAudioFormatDescriptionCreate(kCFAllocatorDefault,
                                                     &asbd,
                                                     0,
                                                     NULL,
                                                     0,
                                                     NULL,
                                                     NULL,
                                                     &audioFormatDesc);
    
    // data block to store decoded packet into
    CMBlockBufferRef blockBuffer;
    status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault,
                                                decodedPacketBuffer,
                                                bufferSize,
                                                kCFAllocatorNull,
                                                NULL,
                                                0,
                                                bufferSize,
                                                0,
                                                &blockBuffer);
    
    // data block converted into a sample buffer
    CMSampleBufferRef sampleBuffer;
    status = CMAudioSampleBufferCreateReadyWithPacketDescriptions(kCFAllocatorDefault,
                                                                  blockBuffer,
                                                                  audioFormatDesc,
                                                                  numSamples,
                                                                  currentPTS,
                                                                  NULL,
                                                                  &sampleBuffer);
    
    
    // queueing sample buffer onto audio renderer
    [audioRenderer enqueueSampleBuffer:sampleBuffer];
}

AudioDecodeAndPlaySample函数来自我使用的库,如注释所述,每隔X毫秒(测试时X=5)会被调用一次,传入一份音频数据包(无音频时不会调用)。

我可能存在多处错误——我认为Opus解码器解码后输出的是交错式Linear PCM,也希望自己构建的AudioStreamBasicDescription是正确的。我不确定在CMAudioSampleBufferCreateReadyWithPacketDescriptions中该如何设置PTS(显示时间戳),尝试用当前主机时间 - 初始化主机时间来计算,但不确定是否可行。

我看到的大多数enqueueSampleBuffer示例代码都用requestMediaDataWhenReady搭配调度队列进行包裹,我也尝试过但无效果(我认为这只是最佳实践而非必需,所以先尝试最简实现;若确实必需可重新添加)。

若您更习惯Swift语言也可使用Swift回复,两种语言我都能处理(无奈我现在必须使用Objective-C 😀)。现寻求帮助排查问题,确认ASBD配置、PTS设置是否正确,以及是否遗漏关键步骤导致无音频输出。


问题排查关键点

1. AudioStreamBasicDescription 配置修正

Opus解码出的PCM是小端字节序的交错式PCM,你的ASBD缺少关键格式标记,需补充kLinearPCMFormatFlagIsPacked | kLinearPCMFormatFlagIsLittleEndian,同时建议先清零结构体避免内存脏值:

AudioStreamBasicDescription asbd;
memset(&asbd, 0, sizeof(asbd));
asbd.mFormatID          = kAudioFormatLinearPCM;
asbd.mFormatFlags       = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked | kLinearPCMFormatFlagIsLittleEndian;
asbd.mBytesPerPacket    = sizeof(short) * channelCount;
asbd.mFramesPerPacket   = 1;
asbd.mBytesPerFrame     = sizeof(short) * channelCount;
asbd.mChannelsPerFrame  = channelCount;
asbd.mBitsPerChannel    = 16;
asbd.mSampleRate        = sampleRate;

2. PTS 时间戳逻辑调整

用主机时间计算PTS会导致同步混乱,AVSampleBufferRenderSynchronizer需要的是基于采样数累加的媒体时间戳:

  • 新增全局变量:CMTime currentMediaTime = kCMTimeZero;
  • 每次解码后计算帧持续时间并更新时间戳:
CMTime frameDuration = CMTimeMake(numSamples, sampleRate);
CMTime currentPTS = currentMediaTime;
currentMediaTime = CMTimeAdd(currentMediaTime, frameDuration);

3. 渲染器启动与状态检查

  • 必须调用启动方法:[renderSynchronizer startRenderingAtTime:kCMTimeZero];
  • 添加状态监听排查错误:
// 在AudioInit中添加监听
[audioRenderer addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];

// 实现监听回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"status"]) {
        AVSampleBufferRendererStatus status = [change[NSKeyValueChangeNewKey] integerValue];
        if (status == AVSampleBufferRendererStatusFailed) {
            NSLog(@"Audio renderer error: %@", audioRenderer.error);
        }
    }
}

4. Core Foundation 对象内存管理

创建的CMAudioFormatDescriptionRef、CMBlockBufferRef、CMSampleBufferRef需手动释放,避免内存泄漏和数据异常:

// 在enqueue后添加释放逻辑
CFRelease(sampleBuffer);
CFRelease(blockBuffer);
CFRelease(audioFormatDesc);

5. requestMediaDataWhenReady 的必要性

AVSampleBufferAudioRenderer是按需拉取数据的模型,直接调用enqueueSampleBuffer可能导致数据堆积或丢失,正确写法:

// 在AudioInit中初始化调度队列
dispatch_queue_t audioQueue = dispatch_queue_create("com.your.app.audio.render", DISPATCH_QUEUE_SERIAL);
[audioRenderer requestMediaDataWhenReadyOnQueue:audioQueue usingBlock:^{
    while ([audioRenderer isReadyForMoreMediaData]) {
        // 此处处理解码并调用enqueueSampleBuffer,需确保数据供给速率匹配播放速率
    }
}];

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 16:04:51