c - 使用Core Audio实时生成正弦音

标签 c macos audio real-time core-audio

我想使用Apples核心音频框架创建一个实时正弦发生器。我想做低水平的工作,以便我可以学习和理解基本知识。

我知道使用PortAudio或Jack可能会更容易,并且我会在某个时候使用它们,但是我想先使其工作,这样我才能自信地了解基本原理。

我现在在这个主题上搜索了几天,但是似乎没有人使用核心音频创建实时波形发生器,试图在使用C而不是Swift或Objective-C时获得低延迟。

为此,我使用了我之前设置的项目。它最初被设计为游戏。因此,在应用程序启动后,它将进入运行循环。我认为这非常合适,因为我可以使用主循环将样本复制到音频缓冲区中,并进行渲染和输入处理。

到目前为止,我知道了。有时它会工作一段时间,然后开始出现故障,有时会立即出现故障。

这是我的代码。我尝试简化并且仅介绍重要部分。

我有多个问题。它们位于这篇文章的底部。

应用程序主运行循环。这是在创建窗口,缓冲区和内存初始化之后所有开始的地方:

    while (OSXIsGameRunning())
    {
       OSXProcessPendingMessages(&GameData);            

       [GlobalGLContext makeCurrentContext];

       CGRect WindowFrame = [window frame];
       CGRect ContentViewFrame = [[window contentView] frame];

       CGPoint MouseLocationInScreen = [NSEvent mouseLocation];
       BOOL MouseInWindowFlag = NSPointInRect(MouseLocationInScreen, WindowFrame);
       CGPoint MouseLocationInView = {};

       if (MouseInWindowFlag)
       {
          NSRect RectInWindow = [window convertRectFromScreen:NSMakeRect(MouseLocationInScreen.x,                                                                        MouseLocationInScreen.y,                                                                 1,                                                                         1)];
          NSPoint PointInWindow = RectInWindow.origin;
          MouseLocationInView= [[window contentView] convertPoint:PointInWindow fromView:nil];
       }
       u32 MouseButtonMask = [NSEvent pressedMouseButtons];

       OSXProcessFrameAndRunGameLogic(&GameData, ContentViewFrame,
                                           MouseInWindowFlag, MouseLocationInView,
                                           MouseButtonMask);

#if ENGINE_USE_VSYNC
       [GlobalGLContext flushBuffer];
#else        
       glFlush();
#endif

     }

通过使用VSYNC,我可以将循环速度降低到60 FPS。时机不是很紧,但是很稳定。我也有一些代码使用更精确的马赫定时手动调节它。我出于可读性而省略了它。
不使用VSYNC或不使用马赫定时来获得每秒60次迭代,也会使音频故障。

时序日志:
CyclesElapsed: 8154360866, TimeElapsed: 0.016624, FPS: 60.155666
CyclesElapsed: 8174382119, TimeElapsed: 0.020021, FPS: 49.946926
CyclesElapsed: 8189041370, TimeElapsed: 0.014659, FPS: 68.216309
CyclesElapsed: 8204363633, TimeElapsed: 0.015322, FPS: 65.264511
CyclesElapsed: 8221230959, TimeElapsed: 0.016867, FPS: 59.286217
CyclesElapsed: 8237971921, TimeElapsed: 0.016741, FPS: 59.733719
CyclesElapsed: 8254861722, TimeElapsed: 0.016890, FPS: 59.207333
CyclesElapsed: 8271667520, TimeElapsed: 0.016806, FPS: 59.503273
CyclesElapsed: 8292434135, TimeElapsed: 0.020767, FPS: 48.154209

这里重要的是函数OSXProcessFrameAndRunGameLogic。它每秒被调用60次,并传递一个包含基本信息的结构,例如用于渲染的缓冲区,键盘状态和声音缓冲区,如下所示:
    typedef struct osx_sound_output
    {
       game_sound_output_buffer SoundBuffer;
       u32 SoundBufferSize;
       s16* CoreAudioBuffer;
       s16* ReadCursor;
       s16* WriteCursor;

       AudioStreamBasicDescription AudioDescriptor;
       AudioUnit AudioUnit;  
    } osx_sound_output;

其中game_sound_output_buffer是:
    typedef struct game_sound_output_buffer
    {
       real32 tSine;
       int SamplesPerSecond;
       int SampleCount;
       int16 *Samples;
    } game_sound_output_buffer;

这些在应用程序进入其运行循环之前进行设置。
SoundBuffer本身的大小为SamplesPerSecond * sizeof(uint16) * 2,其中SamplesPerSecond = 48000

因此OSXProcessFrameAndRunGameLogic内部是声音的产生:
void OSXProcessFrameAndRunGameLogic(osx_game_data *GameData, CGRect WindowFrame,
                                    b32 MouseInWindowFlag, CGPoint MouseLocation,
                                    int MouseButtonMask)
{
    GameData->SoundOutput.SoundBuffer.SampleCount = GameData->SoundOutput.SoundBuffer.SamplesPerSecond / GameData->TargetFramesPerSecond;

    // Oszi 1

    OutputTestSineWave(GameData, &GameData->SoundOutput.SoundBuffer, GameData->SynthesizerState.ToneHz);

    int16* CurrentSample = GameData->SoundOutput.SoundBuffer.Samples;
    for (int i = 0; i < GameData->SoundOutput.SoundBuffer.SampleCount; ++i)
    {
        *GameData->SoundOutput.WriteCursor++ = *CurrentSample++;
        *GameData->SoundOutput.WriteCursor++ = *CurrentSample++;

        if ((char*)GameData->SoundOutput.WriteCursor >= ((char*)GameData->SoundOutput.CoreAudioBuffer + GameData->SoundOutput.SoundBufferSize))
        {
            //printf("Write cursor wrapped!\n");
            GameData->SoundOutput.WriteCursor  = GameData->SoundOutput.CoreAudioBuffer;
        }
    }
}

其中OutputTestSineWave是缓冲区实际充满数据的部分:
void OutputTestSineWave(osx_game_data *GameData, game_sound_output_buffer *SoundBuffer, int ToneHz)
{
    int16 ToneVolume = 3000;
    int WavePeriod = SoundBuffer->SamplesPerSecond/ToneHz;

    int16 *SampleOut = SoundBuffer->Samples;
    for(int SampleIndex = 0;
        SampleIndex < SoundBuffer->SampleCount;
        ++SampleIndex)
    {
        real32 SineValue = sinf(SoundBuffer->tSine);
        int16 SampleValue = (int16)(SineValue * ToneVolume);

        *SampleOut++ = SampleValue;
        *SampleOut++ = SampleValue;

        SoundBuffer->tSine += Tau32*1.0f/(real32)WavePeriod;
        if(SoundBuffer->tSine > Tau32)
        {
            SoundBuffer->tSine -= Tau32;
        }
    }
}

因此,当在启动时创建缓冲区时,我也会像这样初始化Core音频:
void OSXInitCoreAudio(osx_sound_output* SoundOutput)
{
    AudioComponentDescription acd;
    acd.componentType         = kAudioUnitType_Output;
    acd.componentSubType      = kAudioUnitSubType_DefaultOutput;
    acd.componentManufacturer = kAudioUnitManufacturer_Apple;

    AudioComponent outputComponent = AudioComponentFindNext(NULL, &acd);

    AudioComponentInstanceNew(outputComponent, &SoundOutput->AudioUnit);
    AudioUnitInitialize(SoundOutput->AudioUnit);

    // uint16
    //AudioStreamBasicDescription asbd;
    SoundOutput->AudioDescriptor.mSampleRate       = SoundOutput->SoundBuffer.SamplesPerSecond;
    SoundOutput->AudioDescriptor.mFormatID         = kAudioFormatLinearPCM;
    SoundOutput->AudioDescriptor.mFormatFlags      = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsNonInterleaved | kAudioFormatFlagIsPacked;
    SoundOutput->AudioDescriptor.mFramesPerPacket  = 1;
    SoundOutput->AudioDescriptor.mChannelsPerFrame = 2; // Stereo
    SoundOutput->AudioDescriptor.mBitsPerChannel   = sizeof(int16) * 8;
    SoundOutput->AudioDescriptor.mBytesPerFrame    = sizeof(int16); // don't multiply by channel count with non-interleaved!
    SoundOutput->AudioDescriptor.mBytesPerPacket   = SoundOutput->AudioDescriptor.mFramesPerPacket * SoundOutput->AudioDescriptor.mBytesPerFrame;



    AudioUnitSetProperty(SoundOutput->AudioUnit,
                         kAudioUnitProperty_StreamFormat,
                         kAudioUnitScope_Input,
                         0,
                         &SoundOutput->AudioDescriptor,
                         sizeof(SoundOutput->AudioDescriptor));

    AURenderCallbackStruct cb;
    cb.inputProc = OSXAudioUnitCallback;
    cb.inputProcRefCon = SoundOutput;

    AudioUnitSetProperty(SoundOutput->AudioUnit,
                         kAudioUnitProperty_SetRenderCallback,
                         kAudioUnitScope_Global,
                         0,
                         &cb,
                         sizeof(cb));

    AudioOutputUnitStart(SoundOutput->AudioUnit);
}

核心音频的初始化代码将渲染回调设置为OSXAudioUnitCallback
OSStatus OSXAudioUnitCallback(void * inRefCon,
                              AudioUnitRenderActionFlags * ioActionFlags,
                              const AudioTimeStamp * inTimeStamp,
                              UInt32 inBusNumber,
                              UInt32 inNumberFrames,
                              AudioBufferList * ioData)
{
#pragma unused(ioActionFlags)
#pragma unused(inTimeStamp)
#pragma unused(inBusNumber)

    //double currentPhase = *((double*)inRefCon);

    osx_sound_output* SoundOutput = ((osx_sound_output*)inRefCon);


    if (SoundOutput->ReadCursor == SoundOutput->WriteCursor)
    {
        SoundOutput->SoundBuffer.SampleCount = 0;
        //printf("AudioCallback: No Samples Yet!\n");
    }

    //printf("AudioCallback: SampleCount = %d\n", SoundOutput->SoundBuffer.SampleCount);

    int SampleCount = inNumberFrames;
    if (SoundOutput->SoundBuffer.SampleCount < inNumberFrames)
    {
        SampleCount = SoundOutput->SoundBuffer.SampleCount;
    }

    int16* outputBufferL = (int16 *)ioData->mBuffers[0].mData;
    int16* outputBufferR = (int16 *)ioData->mBuffers[1].mData;

    for (UInt32 i = 0; i < SampleCount; ++i)
    {
        outputBufferL[i] = *SoundOutput->ReadCursor++;
        outputBufferR[i] = *SoundOutput->ReadCursor++;

        if ((char*)SoundOutput->ReadCursor >= (char*)((char*)SoundOutput->CoreAudioBuffer + SoundOutput->SoundBufferSize))
        {
            //printf("Callback: Read cursor wrapped!\n");
            SoundOutput->ReadCursor = SoundOutput->CoreAudioBuffer;
        }
    }

    for (UInt32 i = SampleCount; i < inNumberFrames; ++i)
    {
        outputBufferL[i] = 0.0;
        outputBufferR[i] = 0.0;
    }

    return noErr;
}

这基本上就是全部。这很长,但是我没有找到一种以更紧凑的方式呈现所有所需信息的方法。我想展示所有内容,因为我绝不是专业的程序员。如果您觉得缺少某些东西,请告诉我。

我的感觉告诉我,时间安排有问题。我觉得OSXProcessFrameAndRunGameLogic函数有时需要更多时间,以便核心音频回调在OutputTestSineWave完全写入之前已经将样本从缓冲区中拉出。

实际上,在OSXProcessFrameAndRunGameLogic中还有更多的事情在进行,我没有在这里显示。我是将非常基本的东西“软件渲染”到帧缓冲区中,然后由OpenGL显示出来,我也在那里进行按键检查,因为是的,它是功能的主要功能。将来,我想在这里处理多个振荡器,滤波器和其他东西的控件。
无论如何,即使我停止了每次迭代都调用“渲染”和“输入”处理,我仍然会遇到音频故障。

我尝试将OSXProcessFrameAndRunGameLogic中的所有声音处理拉入自己的函数void* RunSound(void *GameData)中,并将其更改为:
pthread_t soundThread;
pthread_create(&soundThread, NULL, RunSound, GameData);
pthread_join(soundThread, NULL);

但是我得到的结果参差不齐,甚至不确定多线程是否像这样完成。每秒创建和销毁线程60次似乎并不可行。

我还想到了让声音处理在应用程序实际进入主循环之前在完全不同的线程上进行。类似于两个同时运行的while循环,第一个循环处理音频,第二个UI和输入。

问题:
  • 我的音频出现故障。渲染和输入似乎正常工作,但音频有时会出现毛刺,有时则不会。从我提供的代码中,您也许可以看到我做错了什么?
  • 我是否以错误的方式使用核心音频技术以实现实时低延迟信号生成?
  • 是否应该像上面所说的那样在单独的线程中进行声音处理?在这种情况下如何正确进行穿线?有一个专门用于声音的线程是有意义的吗?
  • 我是否应该在核心音频的渲染回调中不进行基本音频处理?此功能仅用于输出提供的声音缓冲区吗?
    如果应该在此处进行声音处理,如何从回调内部访问诸如键盘状态之类的信息?
  • 有什么资源可以指出我可能会错过的地方吗?

  • 这是我所知道的唯一可以获得该项目帮助的地方。我将衷心感谢您的帮助。

    如果您不清楚某些事情,请告诉我。

    谢谢 :)

    最佳答案

    通常,在处理低延迟音频时,您希望实现尽可能确定的行为。

    例如,这可以转换为:

  • 不要在音频线程上拥有任何锁(优先级反转)
  • 音频线程上没有内存分配(通常花费太多时间)
  • 音频线程上没有文件/网络IO(通常花费太多时间)

  • 问题1 :

    当您想要获得连续的,实时的,无干扰的音频时,您的代码确实存在一些问题。

    1.两个不同的时钟域。
    您提供的音频数据来自(我称呼为)与时钟域不同的时钟域,用于请求数据。在这种情况下,时钟域1由TargetFramesPerSecond值定义,时钟域2由Core Audio定义。但是,由于调度的工作原理,也不能保证线程按时完成。您尝试将渲染目标定为每秒n帧,但是如果您不及时将其渲染呢?据我所知,与理想时序相比,您无法补偿渲染周期所产生的偏差。
    线程的工作方式最终是OS调度程序决定线程何时处于 Activity 状态。从来没有保证,这会导致渲染周期不是很精确(就音频渲染而言,需要精度)。

    2.渲染线程与Core Audio rendercallback线程之间没有同步。 OSXAudioUnitCallback运行的线程与OSXProcessFrameAndRunGameLogicOutputTestSineWave运行的线程不同。您正在从主线程提供数据,并且正在从Core Audio渲染线程读取数据。通常,您会使用一些互斥锁来保护数据,但是在这种情况下这是不可能的,因为您会遇到优先级倒置的问题。
    处理竞争条件的一种方法是使用一个缓冲区,该缓冲区使用原子变量来存储缓冲区的用法和指针,并仅允许1个生产者和1个使用者使用此缓冲区。
    此类缓冲区的好例子是:
    https://github.com/michaeltyson/TPCircularBuffer
    https://github.com/andrewrk/libsoundio/blob/master/src/ring_buffer.h

    3.音频渲染线程中有很多调用会阻止确定性行为。
    在撰写本文时,您在同一个音频渲染线程中做了很多工作。变化很大,以至于会发生一些事情(在后台),这会阻止您的线程按时进行。通常,您应该避免花费过多时间或不确定性的 call 。使用所有OpenGL / keypres / framebuffer渲染,无法确定线程是否“准时到达”。
    以下是一些值得研究的资源。

    问题2 :

    AFAICT一般来说,您正在正确使用Core Audio技术。我认为您遇到的唯一问题是提供方。

    问题3 :

    是。绝对!虽然,有多种方法可以做到这一点。
    在您的情况下,您有一个运行正常优先级的线程来进行渲染,还有一个高性能的实时线程,正在其上调用音频渲染回调。查看您的代码,我建议将正弦波的生成放入渲染回调函数中(或从渲染回调中调用OutputTestSineWave)。这样,您就可以在可靠的高prio线程中运行音频生成,没有其他渲染会影响定时精度,也不需要环形缓冲区。

    在其他情况下,您需要执行“非实时”处理以准备好音频数据(例如从文件中读取,从网络中读取,甚至从另一个物理音频设备中读取),则无法在Core Audio线程中运行此逻辑。解决此问题的一种方法是启动一个单独的专用线程来执行此处理。要将数据传递到实时音频线程,您将使用前面提到的环形缓冲区。
    它基本上可以归结为两个简单的目标:对于实时线程,必须始终保持音频数据可用(所有渲染调用),如果失败,您将最终发送无效的(或更好地归零)音频数据。
    辅助线程的主要目标是尽快填充环形缓冲区,并保持环形缓冲区尽可能满。因此,只要有空间将新的音频数据放入环形缓冲区,线程就应该这样做。

    在这种情况下,环形缓冲区的大小将决定延迟的容忍度。环形缓冲区的大小将是确定性(较大的缓冲区)和延迟(较小的缓冲区)之间的平衡。

    顺便说一句。我敢肯定Core Audio具有为您完成所有这些操作的所有功能。

    问题4 :

    有多种方法可以实现您的目标,并且从Core Audio中渲染渲染回调中的内容绝对是其中一种。您应该记住的一件事是必须确保函数及时返回。
    为了更改参数来控制音频渲染,您必须找到一种传递消息的方法,该方法使阅读器(音频渲染器功能)无需锁定和等待即可获取消息。我这样做的方法是创建第二个环形缓冲区,其中包含音频渲染器可以使用的消息。这可以像保存数据结构(甚至指向数据的指针)的环形缓冲区一样简单。只要您遵守不锁定的规则。

    问题5 :

    我不知道您知道哪些资源,但是这里有一些必读内容:
    http://atastypixel.com/blog/four-common-mistakes-in-audio-development/
    http://www.rossbencina.com/code/real-time-audio-programming-101-time-waits-for-nothing
    https://developer.apple.com/library/archive/qa/qa1467/_index.html

    关于c - 使用Core Audio实时生成正弦音,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52577890/

    相关文章:

    c - 对于整数模计算,fmod 是否比 % 快

    c - 使用格式字符串打印返回地址

    macos - 使用原生 Mac OS X 对话框为 pcap 请求权限

    ios - 使用iOS检测中等(17 kHz-20 kHz)音频频率

    延长音频文件持续时间的Linux命令

    c - 什么是 scanf ("%*[\n] %[^\n]", input_string);做?

    macos - 要从 OSX 中完全删除 MAMP 安装,需要做什么?

    swift - 允许混合状态,但不允许使用 NSButton 单击(复选框)

    ios - Apple 推送通知声音

    camshift 对象跟踪问题