Я хочу создать генератор синуса в реальном времени, используя базовую звуковую систему яблок.Я хочу сделать это на низком уровне, чтобы я мог учиться и понимать основы.
Я знаю, что использовать 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, когда первый обрабатывает звук, а второй - пользовательский интерфейс и ввод.
Вопросы:
- Я получаю громкий звук.Рендеринг и ввод, кажется, работают правильно, но звук иногда дает сбой, иногда нет.Из предоставленного мною кода, возможно, вы видите, что я что-то не так делаю?
- Неправильно ли я использую базовую аудиотехнологию для получения в реальном времени сигнала с низкой задержкой?
- Следует лиЯ делаю обработку звука в отдельной теме, как я говорил выше?Как в этом контексте можно было бы правильно выполнять многопоточность?Имеет ли смысл иметь поток, предназначенный только для звука, я прав?
- Прав ли я, что базовая обработка звука не должна выполняться при обратном вызове рендеринга основного звука?Эта функция предназначена только для вывода предоставленного звукового буфера?И если обработка звука должна быть выполнена прямо здесь, как я могу получить доступ к информации, такой как состояние клавиатуры, из-за обратного вызова?
- Есть ли какие-либо ресурсы, на которые вы могли бы указать мне, что я, возможно, пропустил?
Это единственное место, где я знаю, где я могу получить помощь по этому проекту.Буду очень признателен за вашу помощь.
И если вам что-то не понятно, пожалуйста, дайте мне знать.
Спасибо:)