Подтвердить что ты не робот

Как запрограммировать точный аудио секвенсор в реальном времени на iphone?

Я хочу запрограммировать простой аудио секвенсор на 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?

Теперь в appstore есть десятки приложений для звукового секвенсера, и они имеют точное время. 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

4b9b3361

Ответ 1

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

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

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

Ответ 2

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

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

EDIT: пример этого подхода, работающего (и в хранилище приложений, можно найти здесь)

Ответ 3

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

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

Приветствия,

Бен

Ответ 4

У вас было несколько хороших ответов здесь, но я подумал, что предлагаю какой-то код для решения, которое сработало для меня. Когда я начал исследовать это, я действительно искал, как работают циклы в играх, и нашел хорошее решение, которое было очень эффективным для меня, используя 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];

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

Ответ 5

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

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

Ответ 6

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

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

Ответ 7

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

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. Это будет воспроизводить 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

Ответ 9

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

Если вы посмотрите на любое приложение для секвенирования, все они будут выполняться вместо битов. На противоположной стороне вещей, если вы посмотрите на редактор формы волны, он использует минуты и секунды.

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