我想使用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显示出来,我也在那里进行按键检查,因为是的,它是功能的主要功能。将来,我想在这里处理多个振荡器,滤波器和其他东西的控件。
无论如何,即使我停止每次迭代都调用Rendering和Input处理,我仍然会遇到音频故障。
我尝试将OSXProcessFrameAndRunGameLogic
中的所有声音处理拉入自己的函数void* RunSound(void *GameData)
中,并将其更改为:
pthread_t soundThread;
pthread_create(&soundThread, NULL, RunSound, GameData);
pthread_join(soundThread, NULL);
但是我得到的结果好坏参半,甚至不确定多线程是否像这样完成。每秒创建和销毁线程60次似乎并不可行。
我还想到了在应用程序实际进入主循环之前,让声音处理在完全不同的线程上进行。类似于两个同时运行的while循环,第一个循环处理音频,第二个UI和输入循环。
问题:
这是我所知道的唯一可以获得该项目帮助的地方。非常感谢您的帮助。
如果您不清楚某些事情,请告诉我。
谢谢:)
答案 0 :(得分:2)
通常,在处理低延迟音频时,您想要实现尽可能确定性的行为。
例如,它翻译为:
问题1 :
当您想要获得连续的,实时的,无故障的音频时,您的代码确实存在一些问题。
1。两个不同的时钟域。
您提供的音频数据来自(我称呼)的时钟域不同于要求数据的时钟域。在这种情况下,时钟域1由您的TargetFramesPerSecond
值定义,而时钟域2由Core Audio定义。但是,由于调度的工作原理,也不能保证线程按时完成。您尝试将渲染目标定为每秒n帧,但是如果您不及时将其渲染呢?据我所知,与理想时序相比,您无法补偿渲染周期所产生的偏差。
线程的工作方式最终是OS调度程序决定线程何时处于活动状态。从来没有保证,这会导致渲染周期不是很精确(就音频渲染而言,需要精度)。
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
答案 1 :(得分:0)
您的基本问题是,您试图从游戏循环中推送音频,而不是让音频系统将其拉出。例如而不是总是拥有(或快速能够创建*)足够的音频样本,以准备由音频回调请求的音频回调所请求的数量。 “总是”必须考虑到足够的斜率,以覆盖游戏循环中的定时抖动(被称为“晚”或“早”或次数太少)。
(*,没有锁,信号量,内存分配或Objective C消息)