24

我想在 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 缓冲区已加载(在 init 代码中),然后准备立即开始alSourcePlay(source);- 所以问题可能出在 NSTimer 上?

现在应用商店里有几十个音序器应用程序,它们都有准确的时间。即使在 180 bpm 的缩放和绘图完成时,Ig "idrum" 也具有完美的稳定节拍。所以必须有解决办法。

有人知道吗?

提前感谢您的帮助!

此致,

瓦尔基


感谢您的回答。它让我更进一步,但不幸的是没有达到目标。这是我所做的:

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 获得实时响应?

此致,

瓦尔基

4

9 回答 9

10

NSTimer 绝对不能保证它何时触发。它在 runloop 上安排自己的触发时间,当 runloop 到达计时器时,它会查看是否有任何计时器过期。如果是这样,它会运行它们的选择器。非常适合各种任务;对这个没用。

此处的第一步是您需要将音频处理移至其自己的线程并脱离 UI 线程。对于计时,您可以使用普通的 C 方法构建自己的计时引擎,但我会先查看 CAAnimation,尤其是 CAMediaTiming。

请记住,Cocoa 中有很多东西是设计为只在主线程上运行的。例如,不要在后台线程上做任何 UI 工作。一般来说,请仔细阅读文档以了解他们对线程安全的看法。但一般来说,如果线程之间没有太多的通信(在大多数情况下不应该有),线程在 Cocoa 中非常容易。看看 NSThread。

于 2009-05-25T15:31:49.997 回答
7

我正在使用 remoteIO 输出做类似的事情。我不依赖 NSTimer。我使用渲染回调中提供的时间戳来计算我的所有时间。我不知道 iphone 的 hz 频率有多准确,但我确定它非常接近 44100hz,所以我只是根据当前的样本数来计算我应该何时加载下一个节拍。

可以在此处找到使用远程 io 的示例项目,查看渲染回调 inTimeStamp 参数。

编辑:这种方法的工作示例(在应用商店,可以在这里找到)

于 2009-07-26T05:54:07.273 回答
5

我选择使用 RemoteIO AudioUnit 和使用 AudioFileServices API 填充摆动缓冲区(一个用于读取,一个用于写入,然后交换)的后台线程。然后在 AudioUnit 线程中处理和混合缓冲区。AudioUnit 线程在应该开始加载下一个摆动缓冲区时向 bgnd 线程发出信号。所有的处理都在 C 中并使用了 posix 线程 API。所有 UI 的东西都在 ObjC 中。

IMO,AudioUnit/AudioFileServices 方法提供了最大程度的灵活性和控制。

干杯,

于 2009-11-06T17:00:25.913 回答
4

您在这里得到了一些很好的答案,但我想我会提供一些代码来解决对我有用的问题。当我开始研究这个时,我实际上是在寻找游戏中的运行循环是如何工作的,并找到了一个很好的解决方案,它对我使用mach_absolute_time.

您可以在此处阅读有关它的作用的一些信息,但不足之处在于它以纳秒精度返回时间。但是,它返回的数字并不是时间,它会随着你拥有的 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];

你在这里看到的所有代码都是我开源的一个项目的简化,你可以在这里看到并自己运行它,如果有帮助的话。干杯。

于 2012-09-15T05:15:10.430 回答
2

真正接近时间的最精确方法是计算音频样本并在经过一定数量的样本后执行您需要做的任何事情。无论如何,您的输出采样率是与声音相关的所有事物的基础,因此这是主时钟。

您不必检查每个样本,每隔几毫秒执行一次就足够了。

于 2011-02-17T17:11:28.280 回答
1

如果提前构建序列不是限制,则可以使用 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

更多细节:使用 AVMutableComposition 进行精确计时

于 2014-08-28T15:14:34.460 回答
1

可以提高实时响应能力的另一件事是在激活音频会话之前将音频会话的 kAudioSessionProperty_PreferredHardwareIOBufferDuration 设置为几毫秒(例如 0.005 秒)。这将导致 RemoteIO 更频繁地请求更短的回调缓冲区(在实时线程上)。不要在这些实时音频回调中花费任何时间,否则您将终止应用程序的音频线程和所有音频。

与使用 NSTimer 相比,仅计算较短的 RemoteIO 回调缓冲区的准确度和延迟要低 10 倍左右。并且在音频回调缓冲区中计数样本以定位混音的开始将为您提供亚毫秒的相对时间。

于 2012-09-16T20:46:50.357 回答
1

通过测量循环中“做一些工作”部分所用的时间并从 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
    }
}
于 2013-02-05T16:50:15.540 回答
0

我认为更好的时间管理方法是设置 bpm 设置(例如 120),然后取消设置。在编写/制作音乐/音乐应用程序时,分钟和秒的测量几乎没有用。

如果您查看任何排序应用程序,它们都按节拍而不是时间进行。另一方面,如果您查看波形编辑器,它会使用分钟和秒。

我不确定以任何方式以代码方式实现此代码的最佳方法,但我认为这种方法将为您省去很多麻烦。

于 2009-07-26T05:59:55.840 回答