Генерация синусоидального тона в реальном времени с Core Audio - PullRequest
0 голосов
/ 30 сентября 2018

Я хочу создать генератор синуса в реальном времени, используя базовую звуковую систему яблок.Я хочу сделать это на низком уровне, чтобы я мог учиться и понимать основы.

Я знаю, что использовать 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 или синхронизацию по времени Mach для получения 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, когда первый обрабатывает звук, а второй - пользовательский интерфейс и ввод.

Вопросы:

  1. Я получаю громкий звук.Рендеринг и ввод, кажется, работают правильно, но звук иногда дает сбой, иногда нет.Из предоставленного мною кода, возможно, вы видите, что я что-то не так делаю?
  2. Неправильно ли я использую базовую аудиотехнологию для получения в реальном времени сигнала с низкой задержкой?
  3. Следует лиЯ делаю обработку звука в отдельной теме, как я говорил выше?Как в этом контексте можно было бы правильно выполнять многопоточность?Имеет ли смысл иметь поток, предназначенный только для звука, я прав?
  4. Прав ли я, что базовая обработка звука не должна выполняться при обратном вызове рендеринга основного звука?Эта функция предназначена только для вывода предоставленного звукового буфера?И если обработка звука должна быть выполнена прямо здесь, как я могу получить доступ к информации, такой как состояние клавиатуры, из-за обратного вызова?
  5. Есть ли какие-либо ресурсы, на которые вы могли бы указать мне, что я, возможно, пропустил?

Это единственное место, где я знаю, где я могу получить помощь по этому проекту.Буду очень признателен за вашу помощь.

И если вам что-то не понятно, пожалуйста, дайте мне знать.

Спасибо:)

Ответы [ 2 ]

0 голосов
/ 13 октября 2018

Ваша основная проблема заключается в том, что вы пытаетесь вытолкнуть звук из игрового цикла вместо того, чтобы позволить аудиосистеме тянуть его;например, вместо того, чтобы всегда иметь (или быстро создавать *) достаточное количество аудиосэмплов, готовых к объему, запрашиваемому аудио-обратным вызовом, для извлечения аудио-обратного вызова.«Всегда» должно учитывать достаточное отклонение, чтобы покрыть дрожание синхронизации (вызываемое поздно, рано или слишком редко) в игровом цикле.

(* без блокировок, семафоров, выделения памяти или сообщений Objective C)

0 голосов
/ 01 октября 2018

Обычно при работе с аудио с низкой задержкой вы хотите добиться максимально детерминированного поведения.

Это, например, означает:

  • Не удерживатьблокировки аудиопотока (инверсия приоритета)
  • Нет выделения памяти в аудиопотоке (часто занимает слишком много времени)
  • Нет файлового / сетевого ввода-вывода в аудиопотоке (часто занимает слишком много времени)

Вопрос 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 из обратного вызова рендеринга).Таким образом, вы генерируете аудио в надежном потоке с высоким значением 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

...