【问题标题】:Realtime sine tone generation with Core Audio使用 Core Audio 实时生成正弦音
【发布时间】:2019-03-05 18:47:27
【问题描述】:

我想使用苹果核心音频框架创建一个实时正弦发生器。我想做低级的,这样我就可以学习和理解基础知识。

我知道使用 PortAudio 或 Jack 可能会更容易,我会在某个时候使用它们,但我想先让它发挥作用,这样我才能有信心理解基本原理。

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

为此,我使用了我不久前建立的一个项目。它最初被设计成一个游戏。所以Application启动后会进入一个run loop。我认为这非常适合,因为我可以使用主循环将样本复制到音频缓冲区并处理渲染和输入处理。

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

这是我的代码。我试图简化如果且仅呈现重要部分。

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

应用程序主运行循环。这是创建窗口并初始化缓冲区和内存后一切开始的地方:

    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;
        }
    }
}

因此,当缓冲区在启动时创建时,核心音频也会被初始化,我这样做是这样的:

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 和输入。

问题:

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

这是我知道的唯一可以在此项目上获得帮助的地方。非常感谢您的帮助。

如果有什么不清楚的地方请告诉我。

谢谢你:)

【问题讨论】:

  • 你在关注手工英雄,不是吗?我在 Mac 上制作了一个关于平台层的 Youtube 系列。这会将您链接到我关于正弦波音调生成的视频。 youtu.be/DcL1ON9qWUA

标签: c macos audio real-time core-audio


【解决方案1】:

您的基本问题是您试图从游戏循环中推送音频,而不是让音频系统拉取它;例如而不是总是拥有(或能够快速创建 *)足够的音频样本,以便为音频回调请求的音频回调量做好准备。 “总是”必须考虑足够的斜率来覆盖游戏循环中的时间抖动(被称为迟到或早或太少的时间)。

(* 没有锁、信号量、内存分配或 Objective C 消息)

【讨论】:

    【解决方案2】:

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

    例如,这翻译为:

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

    问题 1

    当您想要实现连续、实时、无故障的音频时,您的代码确实存在一些问题。

    1.两个不同的时钟域。
    您正在提供来自(我称之为)与请求数据的时钟域不同的时钟域的音频数据。在这种情况下,时钟域 1 由您的 TargetFramesPerSecond 值定义,时钟域 2 由 Core Audio 定义。但是,由于调度的工作原理,您无法保证线程按时按时完成。您尝试将渲染目标设置为每秒 n 帧,但是如果您不及时处理会发生什么?据我所见,与理想时间相比,您没有补偿渲染周期的偏差。 线程的工作方式是最终由操作系统调度程序决定您的线程何时处于活动状态。永远无法保证,这会导致您的渲染周期不是很精确(就音频渲染所需的精度而言)。

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

    3.您的音频渲染线程中有很多调用会阻止确定性行为。
    正如你所写的,你在同一个音频渲染线程中做了更多的事情。变化非常大,会发生一些事情(在引擎盖下),这会阻止你的线程按时进行。通常,您应该避免调用花费太多时间或不确定的调用。使用所有 OpenGL/keypres/framebuffer 渲染,无法确定您的线程将“准时到达”。
    以下是一些值得研究的资源。

    问题 2

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

    问题 3

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

    在您需要进行“非实时”处理以准备好音频数据的其他情况下(考虑从文件读取、从网络读取,甚至从另一个物理音频设备读取),您无法在 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

    【讨论】:

    • 非常感谢!您在这里提供的信息和知识绝对是一流的。它对我有很大帮助。真的。现在事情变得更有意义了。谢谢你!我完全可以看到如何将音频生成内容放在回调中并通过缓冲区处理控件仍然是非常实时的。但是您将如何处理来自数据的样本或音频流的播放?那么你必须在核心音频线程之外生成音频吗?你将如何同步它们?或者像你所说的那样:“处理线程和核心音频渲染线程之间的耦合”。
    • 我扩展了我的原始答案以解决此处的后续问题。
    • 感恩!现在这一切都为我解决了。感谢您的宝贵时间!
    猜你喜欢
    • 2015-02-20
    • 2012-01-08
    • 1970-01-01
    • 2015-02-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多