从 Audio-Unit 渲染回调实时触发事件并获得严重失真

Posted

技术标签:

【中文标题】从 Audio-Unit 渲染回调实时触发事件并获得严重失真【英文标题】:Triggering events in real-time from an Audio-Unit render callback and getting severe distortion 【发布时间】:2016-04-30 22:59:56 【问题描述】:

我正在创建一个基于AUSampleraudio 单元的轻量级音序器。本质上,在每个“滴答”上,我都想执行一些操作。这里的核心技术是利用音频单元渲染回调来实现实时精确回调,并调用任意定义的进一步功能。基于这个特定的回调:

static OSStatus renderCallback (void                        *inRefCon,
                                AudioUnitRenderActionFlags  *ioActionFlags,
                                const AudioTimeStamp        *inTimeStamp,
                                UInt32                      inBusNumber,
                                UInt32                      inNumberFrames,
                                AudioBufferList             *ioData)



   SuperTimer *_superTimer   = (__bridge SuperTimer*)inRefCon;

   _superTimer->samplesSinceLastCall += inNumberFrames;

   if (_superTimer->samplesSinceLastCall > 10000) 

      _superTimer->tickMethod();
      _superTimer->samplesSinceLastCall = 0;
   
   return 0;

这是OSX 上默认输出音频单元的常规渲染回调。最终我应该用一个相对于当前缓冲区大小inNumberFrame 的数字替换硬编码的10000,但这有助于我对tickMethod() 的一致调用,这是一个定义为的块:

void (^tickMethod)(void);

以 44100kHZ 的采样率(硬件似乎默认为该采样率)及其 512 帧的缓冲区大小,我可以在每秒两次调用的块中选择定义的任何代码。 (虽然我现在不太关心这个值,并且可以用我想象的渲染函数中的相对值逻辑来修复)。

在 main 中,我实例化处理音频单元初始化和渲染的对象,并将一些代码分配给初始化程序中的块。

int main(int argc, const char * argv[]) 


    Dispatch * dispatch = [[Dispatch alloc] init];

   Sampler * sampler = [[Sampler alloc] init];

   SuperTimer * time = [[SuperTimer alloc] initWithCompletion:^
      printf("test");
      [dispatch sequencer];
   ];

   CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1000, false);

我要求运行循环来延长我的应用程序的生命周期,瞧,我每秒两次将“测试”实时打印到控制台。然而,sequencer 方法调用立即引起了磨碎的失真声音,把我的邻居吵醒了。即使在最小音量设置下,扬声器发出的声音也非常可怕。

因此,我的问题是如何在不导致音频系统停止的情况下获得进入 Objective-C 领域的“入口点”?一件事是我需要立即发生样本命中,但我还需要进行数据更改(例如我的节拍计数等)。这些事情可能不完全实时发生,但至少在每个回调的时间范围内发生。

所以,我认为我需要跳出我的回调所在的实时线程,并在缓冲速率限制内将执行返回给它的调用者,并且可能在另一个线程上继续我的逻辑。但是,作为并发编程的 n00b,我不太确定如何以及是否应该尝试这种方法。

任何cmets或建议将不胜感激。

【问题讨论】:

【参考方案1】:

如果不走在 *** good answering practice 的边缘,我很难对您的问题做出公平实用的回答。 尽管我确信传输定时事件与静态噪声无关(您必须使缓冲区静音,如示例代码所示),但我不确定您的建议是否是获得您想要的东西的最佳方式要得到。尽管CoreAudio 渲染时间的精确度是独一无二的,但请不要忘记它是一个回调,而不是一个调用。换句话说,系统拥有它需要的所有时间信息,并在需要时调用回调过程,具有实时优先级。您所需要提供的只是输出数据。 但是,此实时优先级仅对音频缓冲区有保证,否则您会听到咔嗒声和故障。对于控制渲染回调参数但发生在实时渲染线程之外的用户数据,这种实时优先级远不能保证。只有音频 I/O 具有实时优先级,AFAIK。然而,回调本身是基于时间的。如果你分析这段代码:

Boolean debug;

static OSStatus renderCallback ( void *inRefCon,
                       AudioUnitRenderActionFlags *ioActionFlags,
                       const AudioTimeStamp *inTimeStamp, //observe this!
                       UInt32 inBusNumber,
                       UInt32 inNumberFrames,
                       AudioBufferList *ioData )

  OSStatus lErr = noErr;
  static Float64 prev_stime;  //prev. sample time
  static UInt64  prev_htime;  //prev. host time  
  Float64 scycle = inTimeStamp->mHostTime   - prev_htime;
  Float64 hcycle = inTimeStamp->mHostTime   - prev_htime; 
  if(debug) //use *only* for printout debugging!
      printf("frames: %d\thcycle: %lld\tscycle: %7.2lf\n",
            (unsigned int)inNumberFrames,hcycle,scycle);
  prev_htime = inTimeStamp->mHostTime;
  prev_stime = inTimeStamp->mSampleTime;

  Float32 *lBuffer0 = (Float32 *)gAudioBufferList->mBuffers[0].mData;
  Float32 *lBuffer1 = (Float32 *)gAudioBufferList->mBuffers[1].mData;
  memset(lBuffer0,  0, inNumberFrames*sizeof(Float32));
  memset(lBuffer1,  0, inNumberFrames*sizeof(Float32));   
  return lErr;

您会发现,您不需要样本即可获得精确的时间信息。所有时序变量也可以是 C 中的 SuperTimer 控制器结构(或 Obj-C 中的接口)的成员,就是这样。无法保证在渲染周期的哪个精确时刻,您的时序变量将使用新值进行更新,并对程序的其余部分及其用户可见。有根据的猜测是在渲染周期结束时。 adjusting buffer length 有一个 CoreAudio API,在您的情况下,可以最大限度地减少延迟。 所以,我想只有当您需要为您的乐器提供精确的视听同步时,才值得这样做。还请考虑其他延迟,例如可视化延迟、MIDI 延迟(如果您使用它)到您项目的“可行性计算”中。 CoreAudio 开发人员 Michael Tyson 也有一篇非常有启发性的读物,处理时序问题,可以向 example code 学习。 最后,请对个人研究感到鼓舞。

更新

最重要的事情之一:应该永远在实时线程上使用阻塞函数或内存管理(alloc、release 和其他 Obj-C “魔法”,例如 Sampler * sampler = [[Sampler alloc] init];) - 你打开您的代码面临priority inversion 的风险,这在音频中意味着点击、丢失、失真以及同样未定义的行为here's one example!出于这个原因,CA 回调是用纯 C 编写的!

【讨论】:

您正确指出失真问题(来自内存垃圾中未初始化的音频缓冲区)是一个单独的问题。关于定序器的基础,我发现在渲染回调中运行逻辑,它每隔 (x % n == 0) 调用一个外部函数,其中 n 确定不调用外部函数的次数。从那里我可以对我的音序器的基本“时钟”进行一定程度的控制。理想情况下,缓冲区帧大小应该非常低,以使这种方法更准确和可微调 如果我没有得到充分的动力,我将继续阅读您的帖子。我将不得不玩弄主机计时并观察时间戳来测试这些实时渲染回调如何与程序执行的其余部分互操作。我相信你已经指出这个回调本身的时间并不精确?或者从它调用的外部函数本身不能保证是“实时的”。无论如何,使用我在第一条评论中概述的方法让我在我的音序器上获得了非常好的声音量化,并将 NSTimer 从水中吹了出来。 回调本身与系统调用一样精确,适用于音频缓冲区 - 并且是实时。我不会打赌“欺骗系统”让“认为”从它调用的外部函数本身并不能保证是“实时的”。它是一个回调,外部函数不是它的子进程。另请注意,回调中的inTimeStamp 变量与NSTimer API 无关。这是纯C。最好的办法是研究 M. Tyson 的一些关于计时的示例代码。希望这个答案能满足。 我将不得不阅读这篇文章并重新阅读您所说的内容,因为我对系统级时序和函数执行的细节并不了解。然而,就其价值而言,我当前的代码确实为我编写的音序器组件提供了基础,由此产生的鼓声在节拍上听起来很完美。 请为陡峭的学习曲线和大量的个人研究做好准备。

以上是关于从 Audio-Unit 渲染回调实时触发事件并获得严重失真的主要内容,如果未能解决你的问题,请参考以下文章

防抖和节流方法实现

(原创!)彻底理解JS中的事件,事件处理函数,钩子函数,回调函数。

触发 React 渲染输入的更改事件(类型=范围)

为啥间隔回调属于第一次渲染不能在每次间隔触发时向 React 发送更新指令(计数 +1)?

Vue跨路由触发事件,Vue监听sessionStorage

在Vue中使用GSAP完成动画(三)动画事件