【发布时间】: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 和输入。
问题:
- 我的音频有问题。渲染和输入似乎可以正常工作,但音频有时会出现故障,有时则不会。从我提供的代码中,你能看出我做错了什么吗?
- 我是否以错误的方式使用核心音频技术来实现实时低延迟信号生成?
- 我应该像上面谈到的那样在单独的线程中进行声音处理吗?在这种情况下如何正确执行线程?有一个专门用于声音的线程是有意义的,对吗?
- 基本的音频处理不应该在核心音频的渲染回调中完成吗?此功能是否仅用于输出提供的声音缓冲区? 如果声音处理应该在这里完成,我如何从回调内部访问键盘状态等信息?
- 你有什么我可能错过的资源吗?
这是我知道的唯一可以在此项目上获得帮助的地方。非常感谢您的帮助。
如果有什么不清楚的地方请告诉我。
谢谢你:)
【问题讨论】:
-
你在关注手工英雄,不是吗?我在 Mac 上制作了一个关于平台层的 Youtube 系列。这会将您链接到我关于正弦波音调生成的视频。 youtu.be/DcL1ON9qWUA
标签: c macos audio real-time core-audio