Как запрограммировать в реальном времени точный аудио секвенсор на iphone? - PullRequest
23 голосов
/ 25 мая 2009

Я хочу запрограммировать простой аудио секвенсор на iphone, но не могу получить точное время. В последние дни я попробовал все возможные звуковые приемы на iphone, начиная с AudioServicesPlaySystemSound и AVAudioPlayer и OpenAL до AudioQueues.

В моей последней попытке я попробовал звуковой движок CocosDenshion, который использует openAL и позволяет загружать звуки в несколько буферов, а затем воспроизводить их при необходимости. Вот основной код:

INIT:

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?

Теперь в магазине приложений есть десятки приложений звукового секвенсора, и у них есть точное время. I.g. "idrum" имеет идеальный стабильный ритм даже при 180 ударах в минуту, когда масштабирование и рисование сделаны. Так что должно быть решение.

У кого-нибудь есть идеи?

Заранее спасибо за любую помощь!

С уважением,

Walchy


Спасибо за ваш ответ. Это привело меня на шаг вперед, но, к сожалению, не к цели. Вот что я сделал:

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.

Есть ли способ получить ответы в реальном времени с помощью NSThread на iphone?

С уважением,

Walchy

Ответы [ 9 ]

9 голосов
/ 25 мая 2009

NSTimer не имеет абсолютно никаких гарантий при запуске. Он планирует себя для времени срабатывания в runloop, и когда runloop получает доступ к таймерам, он видит, просрочен ли какой-либо из таймеров. Если так, то запускаются их селекторы. Отлично подходит для самых разных задач; бесполезно для этого.

Первый шаг заключается в том, что вам нужно переместить обработку звука в собственный поток и выйти из потока пользовательского интерфейса. Для синхронизации вы можете создать свой собственный механизм синхронизации, используя обычные подходы Си, но я бы начал с рассмотрения CAAnimation и особенно CAMediaTiming.

Имейте в виду, что в Какао есть много вещей, которые предназначены только для работы в главном потоке. Например, не выполняйте работу пользовательского интерфейса в фоновом потоке. В общем, внимательно прочитайте документы, чтобы узнать, что они говорят о безопасности потоков. Но, как правило, если между потоками нет большого взаимодействия (которого в большинстве случаев не должно быть IMO), потоки в Какао довольно просты. Посмотрите на NSThread.

6 голосов
/ 26 июля 2009

я делаю что-то подобное, используя вывод remoteIO. я не полагаюсь на NSTimer. я использую метку времени, предоставленную в обратном вызове рендеринга, чтобы вычислить все мои сроки. Я не знаю, насколько точна частота Гц в iphone, но я уверен, что она довольно близка к 44100 Гц, поэтому я просто рассчитываю, когда мне следует загружать следующий бит, исходя из текущего номера сэмпла.

пример проекта, который использует удаленный ввод-вывод, можно найти здесь взгляните на аргумент обратного вызова рендеринга inTimeStamp.

РЕДАКТИРОВАТЬ: Пример этого подхода работает (и в магазине приложений, можно найти здесь )

4 голосов
/ 15 сентября 2012

У вас было несколько хороших ответов, но я подумал, что предложу код для решения, которое сработало для меня. Когда я начал исследовать это, я действительно искал, как работают циклы выполнения в играх, и нашел хорошее решение, которое было очень эффективным для меня, используя mach_absolute_time.

Вы можете прочитать немного о том, что он делает здесь , но суть в том, что он возвращает время с точностью до наносекунды. Однако возвращаемое число - не совсем время, оно зависит от того, какой у вас процессор, поэтому сначала нужно создать структуру 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];

Весь код, который вы видите здесь, является упрощением проекта, который я открыл, вы можете увидеть его и запустить самостоятельно здесь , если это поможет. Приветствия.

4 голосов
/ 06 ноября 2009

Я решил использовать RemoteIO AudioUnit и фоновый поток, который заполняет свинг-буферы (один буфер для чтения, другой для записи, а затем подкачки) с помощью API AudioFileServices. Затем буферы обрабатываются и смешиваются в потоке AudioUnit. Поток AudioUnit сигнализирует потоку bgnd, когда он должен начать загрузку следующего буфера обмена. Вся обработка была в C и использовала API posix thread. Весь пользовательский интерфейс был в ObjC.

IMO, подход AudioUnit / AudioFileServices обеспечивает наибольшую степень гибкости и контроля.

Приветствия

Ben

1 голос
/ 28 августа 2014

Если построение вашей последовательности заранее не является ограничением, вы можете получить точное время, используя AVMutableComposition. Это будет воспроизводить 4 звука, равномерно распределенных в течение 1 секунды:

// 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

1 голос
/ 05 февраля 2013

Измеряя время, прошедшее для части «Сделай некоторую работу» в цикле, и вычитая эту длительность из следующего времени, значительно повышается точность:

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
    }
}
1 голос
/ 17 сентября 2012

Еще одна вещь, которая может улучшить отзывчивость в реальном времени, - это установить kAudioSessionProperty_PreferredHardwareIOBufferDuration аудиосеанса в несколько миллисекунд (например, 0,005 секунды), прежде чем активировать аудиосеанс. Это заставит RemoteIO чаще запрашивать более короткие буферы обратного вызова (в потоке в реальном времени). Не тратьте много времени на эти обратные вызовы аудио в реальном времени, иначе вы убьете аудиопоток и все аудио для вашего приложения.

Простой подсчет более коротких буферов обратного вызова RemoteIO примерно в 10 раз точнее и с меньшей задержкой, чем при использовании NSTimer. А подсчет сэмплов в буфере звукового обратного вызова для позиционирования начала вашего звукового микса даст вам относительную синхронизацию менее миллисекунды.

1 голос
/ 17 февраля 2011

Действительно, самый точный способ приблизиться ко времени - это посчитать аудиосэмплы и делать все, что вам нужно делать, когда прошло определенное количество семплов. Ваша выходная частота дискретизации является основой для всех вещей, связанных со звуком, так что это главные часы.

Вам не нужно проверять каждый образец, для этого достаточно каждой пары мсек.

0 голосов
/ 26 июля 2009

Я подумал, что лучшим подходом к управлению временем было бы установить значение уд / мин (например, 120) и вместо этого отключить его. Измерения минут и секунд практически бесполезны при написании / создании музыки / музыкальных приложений.

Если вы посмотрите на какое-либо приложение для секвенирования, все они идут по тактам, а не по времени. С другой стороны, если вы посмотрите на редактор сигналов, он использует минуты и секунды.

Я не уверен в наилучшем способе реализации этого кода любым способом, но я думаю, что этот подход избавит вас от многих головных болей в будущем.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...