【问题标题】:Audio samples producer multiple threads OSX音频样本生产者多线程 OSX
【发布时间】:2016-03-18 09:55:26
【问题描述】:

此问题是前一个问题 (Audio producer threads with OSX AudioComponent consumer thread and callback in C) 的后续问题,包括一个测试示例,该示例按预期工作和表现,但不能完全回答问题。我已大幅改写该问题,并重新编码示例,使其包含纯 C 代码。 (我发现前一个示例中很少有 Objective-C 代码部分只会引起混淆,并分散读者对问题本质的注意力。)

为了利用多个处理器内核以及使 CoreAudio 拉模型渲染线程尽可能轻量,LPCM 样本的生产者例程显然必须“坐在”一个不同的线程,在实时优先级渲染线程/回调之外。它必须将样本馈送到循环缓冲区(在本例中为 TPCircularBuffer),系统将从该缓冲区中安排以 inNumberFrames 为单位的数据提取。

Grand Central Dispatch API 提供了一个简单的解决方案,这是我根据一些个人研究(包括试错编码)得出的结论。这个解决方案很优雅,因为它不会阻止任何东西,也不会在推拉模型之间产生冲突。然而,应该负责“子线程”的 GCD到目前为止不满足生产者代码的工作线程的特定并行化要求,所以我不得不明确地产生一些POSIX 线程,取决于可用的逻辑核心数。尽管在加速计算方面的结果已经很显着,但我仍然觉得将 POSIX 和 GCD 混合起来有点不舒服。特别是它适用于变量 wait_interval,并正确计算它,而不是通过预测渲染线程下一个周期可能需要多少 PCM 样本。

这是我的测试程序的缩短和简化(伪)代码,用纯 C 语言编写。

控制器声明:

#include "TPCircularBuffer.h"
#include <AudioToolbox/AudioToolbox.h>
#include <AudioUnit/AudioUnit.h>
#include <dispatch/dispatch.h>
#include <sys/sysctl.h>
#include <pthread.h>

typedef struct {
TPCircularBuffer            buffer;
AudioComponentInstance      toneUnit;
Float64                     sampleRate;
AudioStreamBasicDescription streamFormat;   
Float32*                     f;     //array of updated frequencies
Float32*                     a;     //array of updated amps
Float32*                     prevf; //array of prev. frequencies
Float32*                     preva; //array of prev. amps
Float32*                     val;
    int*                    arg;
    int*                    previous_arg;
UInt32                      frames;
int                         state;
Boolean                     midif; //wip
} MyAudioController;

MyAudioController gen;    
dispatch_semaphore_t mSemaphore;
Boolean multithreading, NF;

typedef struct data{
    int tid;
    int cpuCount;
}data;

控制器管理:

void setup (void){
    // Initialize circular buffer
    TPCircularBufferInit(&(self->buffer), kBufferLength);
    // Create the semaphore
    mSemaphore = dispatch_semaphore_create(0);
    // Setup audio
    createToneUnit(&gen);  
}

void dealloc (void) {
    // Release buffer resources
    TPCircularBufferCleanup(&buffer);
    // Clean up semaphore
    dispatch_release(mSemaphore);
    // dispose of audio
    if(gen.toneUnit){
        AudioOutputUnitStop(gen.toneUnit);
        AudioUnitUninitialize(gen.toneUnit);
        AudioComponentInstanceDispose(gen.toneUnit);
    }
}

调度程序调用(从主线程启动生产者队列):

void dproducer (Boolean on, Boolean multithreading, Boolean NF)
{
if (on == true)
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        if((multithreading)||(NF))
            producerSum(on);
        else
            producer(on);
    });
}
return;
}

线程生产者例程:

void producerSum(Boolean on)
{
int rc;
int num = getCPUnum();
pthread_t threads[num];
data thread_args[num];
void* resulT;
static Float32 frames  [FR_MAX];
Float32 wait_interval;
int     bytesToCopy;
Float32 floatmax;

while(on){
    wait_interval = FACT*(gen.frames)/(gen.sampleRate);
    Float32 damp = 1./(Float32)(gen.frames);
    bytesToCopy = gen.frames*sizeof(Float32);
    memset(frames,  0, FR_MAX*sizeof(Float32));
    availableBytes = 0;
    fbuffW  = (Float32**)calloc(num + 1, sizeof(Float32*));
    for (int i=0; i<num; ++i)
    {
        fbuffW[i] = (Float32*)calloc(gen.frames, sizeof(Float32));
        thread_args[i].tid = i; 
        thread_args[i].cpuCount = num;
        rc = pthread_create(&threads[i], NULL, producerTN, (void *) &thread_args[i]);
    }

    for (int i=0; i<num; ++i)  rc = pthread_join(threads[i], &resulT);

    for(UInt32 samp = 0; samp < gen.frames; samp++)
        for(int i = 0; i < num; i++)
            frames[samp] += fbuffW[i][samp];

    //code for managing producer state and GUI updates
    { ... }

    float *head = TPCircularBufferHead(&(gen.buffer), &availableBytes);
    memcpy(head,(const void*)frames,MIN(bytesToCopy, availableBytes));//copies frames to head
    TPCircularBufferProduce(&(gen.buffer),MIN(bytesToCopy,availableBytes));

dispatch_semaphore_wait(mSemaphore, dispatch_time(DISPATCH_TIME_NOW, wait_interval * NSEC_PER_SEC));
if(gen.state == stopped){gen.state = idle; on = false;}

for(int i = 0; i <= num; i++)
        free(fbuffW[i]);
free(fbuffW);
}
return;
}

单个生产者线程可能看起来像这样:

void *producerT  (void *TN)
{
Float32 samples[FR_MAX];
data threadData;
threadData = *((data *)TN);
int tid  = threadData.tid;
int step = threadData.cpuCount;
int *ret = calloc(1,sizeof(int));

do_something(tid, step, &samples);

{ … }
return (void*)ret;
}

这里是渲染回调(CoreAudio 实时消费者线程):

static OSStatus audioRenderCallback(void *inRefCon,
                                  AudioUnitRenderActionFlags *ioActionFlags,
                                  const AudioTimeStamp *inTimeStamp,
                                  UInt32 inBusNumber,
                                  UInt32 inNumberFrames,
                                  AudioBufferList *ioData) {    

MyAudioController *THIS = (MyAudioController *)inRefCon;

// An event happens in the render thread- signal whoever is waiting
if (THIS->state == active) dispatch_semaphore_signal(mSemaphore);

// Mono audio rendering: we only need one target buffer
const int channel = 0;
Float32* targetBuffer = (Float32 *)ioData->mBuffers[channel].mData;
memset(targetBuffer,0,inNumberFrames*sizeof(Float32));

// Pull samples from circular buffer
int32_t availableBytes;    
Float32 *buffer = TPCircularBufferTail(&THIS->buffer, &availableBytes);

//copy circularBuffer content to target buffer
int bytesToCopy = ioData->mBuffers[channel].mDataByteSize;
memcpy(targetBuffer, buffer, MIN(bytesToCopy, availableBytes));
{ … };

TPCircularBufferConsume(&THIS->buffer, availableBytes);    
THIS->frames = inNumberFrames;
return noErr;
}

【问题讨论】:

  • 谢谢,除了一些基本上是总结的 Objective-C 方法之外,所有相关的代码部分都是用 plain-C 编写的,因为 C POSIX 和 CoreAudio API是 C API。很抱歉,这个 C。请原谅我的评论。
  • 简单地说:它用 C 编译器编译吗?不。所以它是 not C。否则它也可能是 C++、Java、Rust 等。只是相似或部分相同的语法并不能使两种语言相同。是的,我知道,ObjC 是 C 的真正超集,但反之则不然!
  • 代码被清理为包含 only plain-C
  • 您对 gcd 和 posix 的混合不用担心,dispatch_semaphorepthreads 面前不会做任何奇怪的事情。渲染线程中所需的样本数量应该是固定的,或者至少是非常可预测的,具体取决于您的硬件采样率。不确定这里的实际问题是什么。
  • 渲染线程中所需的样本数量确实取决于硬件(变量inNumberFrames)。问题是:(1)如何在这个模型中计算和实现 wait_interval 变量,或者(2)如何可能完全摆脱 GCD 以使代码更便携(POSIX/BSD )。在这种情况下如何实现 pthread 条件变量?提前致谢。

标签: c multithreading macos multiprocessing core-audio


【解决方案1】:

Grand Central Dispatch 已经负责将操作分派到多个处理器内核和线程。在典型的实时音频渲染或处理中,人们永远不需要等待信号或信号量,因为循环缓冲区消耗率是非常可预测的,并且随着时间的推移非常缓慢地漂移。 AVAudioSession API(如果可用)和音频单元 API 和回调允许您设置和确定回调缓冲区大小,从而确定循环缓冲区可以更改的最大速率。因此,您可以在计时器上调度所有渲染操作,渲染每个计时器周期所需的确切数量,并让缓冲区大小和状态补偿线程调度时间中的任何抖动。

在超长时间运行的音频渲染中,您可能想要测量计时器操作和实时音频消耗(采样率)之间的偏差,并调整渲染的采样数或计时器偏移量。

【讨论】:

  • 谢谢。事实上,我没有也不能处理 AVAudioSession API,因为这段代码与 iOS 无关,希望从帖子的标题和示例格式中可以看到,我也不打算制作 OSX/iOS 跨平台代码。我也更喜欢自己以编程方式分割示例生成代码。我不打算质疑 GCD 的可靠性,恕我直言。如果可能的话,我更愿意让示例生成例程更兼容 UNIX 纯 C 源代码(版主不让我标记)。这意味着我可能不得不完全放弃 GCD 方法。
  • 使用 AudioSession API 是可选的。类似的计时方案适用于使用 GCD 或 NSTimers 的 OS X。这些计时器回调可以在 OS X 和 iOS 上包装普通的 C 示例生成器(我对两者使用相同的 C 音频合成器代码)。
  • 感谢您的努力和良好的建议,然而,我所写的甚至不是 NSApplicon。这段代码是一个测试。我想尽可能多地保留 UNIX 的可移植性。我在 OSX 上使用 CA 渲染回调只是因为它是本机的、可调试的并且不会引诱编写 NSApp。我宁愿保持样本生成线程“无可可”。这也是因为声化数据的性质。是否有一个简单的示例代码说明 POSIX 生产者线程如何与 CA 消费者线程“协调实时工作”?如果有人能指出我的方向,我将不胜感激。
  • 感谢您的好答案 - 这是一个好答案,我不怀疑。经过几周的研究,我发现不幸的是,GCD 到目前为止还不能满足这些特定的并行化要求——它们太具体了。所以我不得不编写显式并行化代码。因此,我大幅改写了这个问题并重写了示例代码。请善待阅读问题中的新信息。提前致谢。
  • 独立的近实时生产者线程工作的原因是因为固定的音频采样率允许您几乎准确地知道每个计时器滴答声产生多少样本。因此,您不需要计算等待时间,因为它可以是在音频初始化时根据请求的最小音频缓冲区大小设置的常数(计时器时间间隔)。
猜你喜欢
  • 1970-01-01
  • 2017-02-01
  • 1970-01-01
  • 2015-08-27
  • 2014-10-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多