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

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

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

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

С наилучшими пожеланиями,

Walchy


person Walchy    schedule 25.05.2009    source источник
comment
Я думаю, что @Walchy ушел на обед ... Интересно, каким образом он в конце концов решил это сделать?   -  person Aran Mulholland    schedule 17.10.2014
comment
К сожалению, я не могу опубликовать образец кода, но для приложения Playback в магазине приложений мы используем обратный вызов, чтобы предоставить аудиоданные, которые считываются из файлов. Каждый файл имеет одинаковые звуковые характеристики, поэтому для обеспечения точной синхронизации мы просто переходим к этой точке выборки в данных аудиофайла. Это очень точно (скачайте приложение и получите бесплатную игру дня). Мы можем зацикливать разделы и переходить к разделам в файле, одновременно воспроизводя более 20 треков (еще не достигнув лимита треков).   -  person Matt H    schedule 29.10.2015


Ответы (9)


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

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

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

person Rob Napier    schedule 25.05.2009

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

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

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

person Aran Mulholland    schedule 26.07.2009
comment
конечно, вам понадобится bpm, но чтобы запланировать воспроизведение следующего сэмпла, вам нужно будет найти сэмплы до следующего удара (он будет определяться вашим bpm) и использовать для этого текущую отметку времени. - person Aran Mulholland; 28.07.2009
comment
Какую часть структуры AudioTimeStamp вы используете для расчета времени? Я использую mSampleTime. Есть какие-либо мнения о том, что лучше всего использовать и почему? - person Will Pragnell; 12.02.2012
comment
@WillPragnell, я тоже использую mSampleTime. - person Aran Mulholland; 13.02.2012

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

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

Ваше здоровье,

Бен

person Ben Dyer    schedule 06.11.2009

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

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

person pwightman    schedule 15.09.2012
comment
Имейте в виду, что системные часы (например, возвращенные mach_absolute_time) не обязательно синхронизированы с часами звука, по которым сэмплы синхронизируются с кодеком. Использование счетчика семплов в обратном вызове рендеринга звука для генерации таймингов секвенсора - единственный надежный способ. - person marko; 16.10.2014
comment
Да, я обнаружил несколько ошибок в этом подходе, включая то, что на него влияют обновления часов с серверов часовых поясов и т. Д. У вас есть ссылка / справочник для подключения к счетчику образцов в обратном вызове рендеринга звука ? - person pwightman; 22.10.2014
comment
Практически во всех обработчиках аудио-рендеринга (включая CoreAudio) вам дается либо количество отсчетов, либо вы можете обратно преобразовать его из параметров времени, которые он предоставляет. Следствием этого является то, что вам нужно обслуживать очередь событий и обрабатывать элементы, которые должны быть выполнены (и решать, что делать с теми, которые уже в прошлом) в обработчике рендеринга. В VST SDK есть пример, на который вы, возможно, захотите взглянуть. Это принципиально очень похоже на AU. - person marko; 22.10.2014
comment
@pwightman Спасибо за этот код и ваш проект на github BBGroover. Мне не удалось заставить его работать, но я смог использовать его в качестве примера, и у меня есть простая рабочая версия точного таймера. Мне любопытно, что еще не так с этим подходом, о котором вы упомянули выше? У меня также есть другая версия, работающая с CoreAudio под управлением SuperTimer github.com/timwredwards/iOS-Core- Audio-Timer - но я не могу получить столь же точную синхронизацию BPM, потому что интервалы должны делиться на длину буфера устройства по умолчанию (512). Мне хочется просто пойти с mach_absolute_time. @marko приветствуется любой ввод. - person Gadget Blaster; 24.01.2016
comment
Я нигде не могу найти информацию, на которую mach_absolute_time влияют обновления clcok с серверов часовых поясов. В нем указано, что он не меняется при обновлении сервера или изменении администратора: nadeausoftware.com/ статьи / 2012/04 /. Также похоже, что создатель TAAE предполагает, что я не на 100% уверен, что частота дискретизации кадров в секунду является абсолютно надежной: forum.theamazingaudioengine.com/discussion/comment/1504/. Он также предлагает использовать mach_absolute_time для метронома. - person Gadget Blaster; 30.01.2016

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

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

person tahome    schedule 17.02.2011

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

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

person hotpaw2    schedule 16.09.2012

Измерение времени, прошедшего для части цикла «Выполните некоторую работу», и вычитание этой длительности из 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
    }
}
person user1694772    schedule 05.02.2013

Если создание вашей последовательности заранее не является ограничением, вы можете получить точное время с помощью 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

person sabajt    schedule 28.08.2014

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

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

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

person Sneakyness    schedule 26.07.2009
comment
Как вы думаете, вы собираетесь это измерить? NSTimer страдает от джиттера, как и любой другой механизм, который полагается на планирование потока. Вот почему единственный способ, который работает, - это синхронизация с синхронизацией выходных отсчетов. - person marko; 24.01.2016