如何在 iPhone 上编写实时准确的音频音序器?
Posted
技术标签:
【中文标题】如何在 iPhone 上编写实时准确的音频音序器?【英文标题】:How to program a real-time accurate audio sequencer on the iphone? 【发布时间】:2010-10-28 18:33:27 【问题描述】:我想在 iphone 上编写一个简单的音频音序器,但我无法获得准确的时间。最近几天我在 iphone 上尝试了所有可能的音频技术,从 AudioservicesPlaySystemSound 和 AVAudioPlayer 和 OpenAL 到 AudioQueues。
在我最后一次尝试中,我尝试了 CocosDenshion 声音引擎,它使用 openAL 并允许将声音加载到多个缓冲区中,然后在需要时播放它们。这是基本代码:
初始化:
int channelGroups[1];
channelGroups[0] = 8;
soundEngine = [[CDSoundEngine alloc] init:channelGroups channelGroupTotal:1];
int i=0;
for(NSString *soundName in [NSArray arrayWithObjects:@"base1", @"snare1", @"hihat1", @"dit", @"snare", nil])
[soundEngine loadBuffer:i fileName:soundName fileType:@"wav"];
i++;
[NSTimer scheduledTimerWithTimeInterval:0.14 target:self selector:@selector(drumLoop:) userInfo:nil repeats:YES];
在初始化过程中,我创建了声音引擎,将一些声音加载到不同的缓冲区,然后使用 NSTimer 建立音序器循环。
音频循环:
- (void)drumLoop:(NSTimer *)timer
for(int track=0; track<4; track++)
unsigned char note=pattern[track][step];
if(note)
[soundEngine playSound:note-1 channelGroupId:0 pitch:1.0f pan:.5 gain:1.0 loop:NO];
if(++step>=16)
step=0;
就是这样,它可以正常工作,但时间不稳定且不稳定。一旦发生其他事情(例如在视图中绘图),它就会不同步。
据我了解声音引擎和 openAL 缓冲区已加载(在初始化代码中),然后准备立即使用 alSourcePlay(source);
开始 - 所以问题可能出在 NSTimer 上?
现在应用商店里有几十个音序器应用程序,它们都有准确的时间。举例“idrum”在完成缩放和绘图时即使在 180 bpm 也有完美的稳定节拍。所以一定有办法。
有人知道吗?
提前感谢您的帮助!
最好的问候,
瓦尔基
感谢您的回答。它让我更进一步,但不幸的是没有达到目标。这是我所做的:
nextBeat=[[NSDate alloc] initWithTimeIntervalSinceNow:0.1];
[NSThread detachNewThreadSelector:@selector(drumLoop:) toTarget:self withObject:nil];
在初始化中,我存储下一个节拍的时间并创建一个新线程。
- (void)drumLoop:(id)info
[NSThread setThreadPriority:1.0];
while(1)
for(int track=0; track<4; track++)
unsigned char note=pattern[track][step];
if(note)
[soundEngine playSound:note-1 channelGroupId:0 pitch:1.0f pan:.5 gain:1.0 loop:NO];
if(++step>=16)
step=0;
NSDate *newNextBeat=[[NSDate alloc] initWithTimeInterval:0.1 sinceDate:nextBeat];
[nextBeat release];
nextBeat=newNextBeat;
[NSThread sleepUntilDate:nextBeat];
在序列循环中,我将线程优先级设置得尽可能高,然后进入无限循环。播放完声音后,我计算下一个节拍的下一个绝对时间,并让线程休眠直到这个时间。
这再次有效,并且它比我在没有 NSThread 的情况下尝试更稳定,但如果发生其他事情,它仍然不稳定,尤其是 GUI 的东西。
有没有办法在 iPhone 上使用 NSThread 获得实时响应?
最好的问候,
瓦尔基
【问题讨论】:
我想@Walchy 出去吃午饭了...想知道他最终选择了哪种方式? 很遗憾我不能发布示例代码,但是对于应用商店中的播放应用,我们使用回调来提供从文件中读取的音频数据。每个文件都具有相同的音频特征,因此为了提供精确的时序,我们只需跳转到音频文件数据中的那个采样点。它非常准确(下载应用程序并获得当天的免费游戏)。我们可以循环播放部分并跳转到文件中的部分,同时播放 20 多首曲目(尚未达到曲目限制)。 【参考方案1】:NSTimer 绝对不能保证它何时触发。它在 runloop 上安排自己的触发时间,当 runloop 到达计时器时,它会查看是否有任何计时器过期。如果是这样,它会运行它们的选择器。非常适合各种任务;对这个没用。
这里的第一步是您需要将音频处理移至其自己的线程并脱离 UI 线程。对于时序,您可以使用普通的 C 方法构建自己的时序引擎,但我会从 CAAnimation 开始,尤其是 CAMediaTiming。
请记住,Cocoa 中有很多东西是设计为只在主线程上运行的。例如,不要在后台线程上做任何 UI 工作。一般来说,请仔细阅读文档以了解他们对线程安全的看法。但一般来说,如果线程之间没有太多的通信(在大多数情况下不应该有),线程在 Cocoa 中非常容易。看看 NSThread。
【讨论】:
【参考方案2】:我正在使用 remoteIO 输出做类似的事情。我不依赖 NSTimer。我使用渲染回调中提供的时间戳来计算我的所有时间。我不知道 iphone 的 hz 频率有多准确,但我确定它非常接近 44100hz,所以我只是根据当前的采样数来计算我应该何时加载下一个节拍。
可以找到使用远程 io 的示例项目 here 看看渲染回调 inTimeStamp 参数。
编辑:这种方法的工作示例(在应用商店,可以找到here)
【讨论】:
当然你需要一个 bpm,但是要安排你的下一个样本播放,你必须找到样本直到下一个节拍(这将由你的 bpm 定义)并使用当前时间戳来这样做。 您使用 AudioTimeStamp 结构的哪一部分来计算时间?我一直在使用 mSampleTime。对哪个最好使用以及为什么使用有任何意见? @WillPragnell 我也使用 mSampleTime。【参考方案3】:我选择使用 RemoteIO AudioUnit 和使用 AudioFileServices API 填充摆动缓冲区(一个用于读取,一个用于写入,然后交换)的后台线程。然后在 AudioUnit 线程中处理和混合缓冲区。 AudioUnit 线程在应该开始加载下一个摆动缓冲区时向 bgnd 线程发出信号。所有的处理都在 C 中并使用了 posix 线程 API。所有 UI 的东西都在 ObjC 中。
IMO,AudioUnit/AudioFileServices 方法提供了最大程度的灵活性和控制力。
干杯,
本
【讨论】:
【参考方案4】:您在这里得到了一些很好的答案,但我想我会提供一些代码来解决对我有用的问题。当我开始研究这个时,我实际上是在寻找游戏中的运行循环是如何工作的,并找到了一个很好的解决方案,它使用mach_absolute_time
对我来说非常高效。
您可以阅读一些关于它的作用 here 的信息,但不足之处在于它以纳秒精度返回时间。但是,它返回的数字并不是时间,它会随着你的 CPU 而变化,所以你必须先创建一个 mach_timebase_info_data_t
结构体,然后使用它来标准化时间。
// Gives a numerator and denominator that you can apply to mach_absolute_time to
// get the actual nanoseconds
mach_timebase_info_data_t info;
mach_timebase_info(&info);
uint64_t currentTime = mach_absolute_time();
currentTime *= info.numer;
currentTime /= info.denom;
如果我们希望它每 16 分音符打勾,你可以这样做:
uint64_t interval = (1000 * 1000 * 1000) / 16;
uint64_t nextTime = currentTime + interval;
此时,currentTime
将包含一定数量的纳秒,并且您希望它在每次经过 interval
纳秒时打勾,我们将其存储在 nextTime
中。然后,您可以设置一个 while 循环,如下所示:
while (_running)
if (currentTime >= nextTime)
// Do some work, play the sound files or whatever you like
nextTime += interval;
currentTime = mach_absolute_time();
currentTime *= info.numer;
currentTime /= info.denom;
mach_timebase_info
的东西有点令人困惑,但是一旦你把它放进去,它就会很好地工作。它对我的应用程序来说非常高效。还值得注意的是,您不想在主线程上运行它,因此将其放到自己的线程中是明智的。你可以把上面所有的代码放在它自己的名为run
的方法中,然后用类似这样的东西开始它:
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
你在这里看到的所有代码都是我开源的一个项目的简化,你可以看到它并自己运行它here,如果有帮助的话。干杯。
【讨论】:
请注意,系统时钟(例如mach_absolute_time
返回的)不一定与将样本输出到编解码器的音频时钟同步。使用音频渲染回调中的样本计数器来生成音序器计时是唯一可靠的方法。
是的,我发现这种方法有一些问题,包括它受到时区服务器时钟更新的影响等。你有链接/参考吗?音频渲染回调中的示例计数器?
在几乎所有的音频渲染处理程序(包括 CoreAudio 的)中,您都可以得到样本计数 - 或者可以从它提供的时间参数中反向转换它。这样做的结果是您需要在渲染处理程序中为事件队列提供服务并处理到期的项目(并决定如何处理已经过去的任何项目)。 VST SDK 中有一个您可能想查看的示例。它基本上与 AU 非常相似。
@pwightman 感谢此代码和您的 BBGroover github 项目。我无法让它运行,但我可以将它用作示例,并且我有一个精确计时器的简单工作版本。我很好奇你上面提到的这种方法还有什么问题?我还有另一个与 CoreAudio 一起运行 SuperTimer github.com/timwredwards/iOS-Core-Audio-Timer 的版本——但我似乎无法获得准确的计时 BPM,因为“间隔必须能被设备的默认缓冲区长度整除”(512)。我很想选择mach_absolute_time
。 @marko 任何意见表示赞赏。
我在任何地方都找不到mach_absolute_time
受来自时区服务器的 clcok 更新影响的信息。这表明,它“不会按时更改服务器更新或管理员更改”:nadeausoftware.com/articles/2012/04/…。此外,TAAE 的创建者似乎暗示“我不能 100% 确定每秒采样率帧是完全可靠的”:forum.theamazingaudioengine.com/discussion/comment/1504/…。他还建议使用mach_absolute_time
作为节拍器。【参考方案5】:
真正接近时间的最精确方法是计算音频样本,并在经过一定数量的样本后执行您需要做的任何事情。无论如何,您的输出采样率是与声音相关的所有事物的基础,因此这是主时钟。
您不必检查每个样本,每隔几毫秒检查一次就足够了。
【讨论】:
【参考方案6】:另一件可以提高实时响应能力的事情是在激活音频会话之前将音频会话的 kAudioSessionProperty_PreferredHardwareIOBufferDuration 设置为几毫秒(例如 0.005 秒)。这将导致 RemoteIO 更频繁地请求更短的回调缓冲区(在实时线程上)。不要在这些实时音频回调中花费任何时间,否则您将杀死应用程序的音频线程和所有音频。
与使用 NSTimer 相比,仅计算较短的 RemoteIO 回调缓冲区的准确度和延迟要低 10 倍左右。并且在音频回调缓冲区中计数样本以定位混音的开始将为您提供亚毫秒的相对时间。
【讨论】:
【参考方案7】:通过测量循环中“做一些工作”部分所用的时间并从 nextTime 中减去此持续时间,大大提高了准确性:
while (loop == YES)
timerInterval = adjustedTimerInterval ;
startTime = CFAbsoluteTimeGetCurrent() ;
if (delegate != nil)
[delegate timerFired] ; // do some work
endTime = CFAbsoluteTimeGetCurrent() ;
diffTime = endTime - startTime ; // measure how long the call took. This result has to be subtracted from the interval!
endTime = CFAbsoluteTimeGetCurrent() + timerInterval-diffTime ;
while (CFAbsoluteTimeGetCurrent() < endTime)
// wait until the waiting interval has elapsed
【讨论】:
【参考方案8】:如果提前构建序列不是限制,您可以使用 AVMutableComposition 获得精确的时序。这将在 1 秒内均匀地播放 4 种声音:
// setup your composition
AVMutableComposition *composition = [[AVMutableComposition alloc] init];
NSDictionary *options = @AVURLAssetPreferPreciseDurationAndTimingKey : @YES;
for (NSInteger i = 0; i < 4; i++)
AVMutableCompositionTrack* track = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
NSURL *url = [[NSBundle mainBundle] URLForResource:[NSString stringWithFormat:@"sound_file_%i", i] withExtension:@"caf"];
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:options];
AVAssetTrack *assetTrack = [asset tracksWithMediaType:AVMediaTypeAudio].firstObject;
CMTimeRange timeRange = [assetTrack timeRange];
Float64 t = i * 1.0;
NSError *error;
BOOL success = [track insertTimeRange:timeRange ofTrack:assetTrack atTime:CMTimeMake(t, 4) error:&error];
NSAssert(success && !error, @"error creating composition");
AVPlayerItem* playerItem = [AVPlayerItem playerItemWithAsset:composition];
self.avPlayer = [[AVPlayer alloc] initWithPlayerItem:playerItem];
// later when you want to play
[self.avPlayer seekToTime:kCMTimeZero];
[self.avPlayer play];
此解决方案的原始信用:http://forum.theamazingaudioengine.com/discussion/638#Item_5
更多详情:precise timing with AVMutableComposition
【讨论】:
【参考方案9】:我认为更好的时间管理方法是设置 bpm 设置(例如 120),然后取消设置。在编写/制作音乐/音乐应用程序时,分钟和秒的测量几乎毫无用处。
如果您查看任何排序应用程序,它们都会按节拍而不是时间进行。另一方面,如果您查看波形编辑器,它会使用分钟和秒。
我不确定以任何方式实现此代码的最佳方法,但我认为这种方法会为您省去很多麻烦。
【讨论】:
你认为你将如何衡量这个?NSTimer
受到抖动的影响,任何其他依赖于线程被调度的机制也会受到影响。这就是为什么唯一可行的方法是根据输出采样时钟计时。以上是关于如何在 iPhone 上编写实时准确的音频音序器?的主要内容,如果未能解决你的问题,请参考以下文章