Метроном. Таймер, музыка и анимация

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

Я пытался реализовать таймер через NSTimer, dispatch_after, performSelector:afterDelay:, CADisplayLink и dispatch_source_t. В любом случае у меня возникают проблемы во время анимации. Я даже пытался реализовать собственную анимацию через CADisplayLink, где я вычисляю кадры анимированных видов, это тоже не помогло.


person Valentin Shamardin    schedule 02.08.2018    source источник


Ответы (2)


Единственный 100% надежный способ сделать это, который я нашел, — это настроить либо через CoreAudio, либо через AudioToolbox: https://developer.apple.com/documentation/audiotoolbox поставщик данных аудиопотока, который вызывается iOS через регулярные фиксированные интервалы для предоставления аудиосистеме образцов аудио.

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

Это код, который я использовал для настройки AudioUnit с помощью AudioToolbox:

static AudioComponentInstance _audioUnit;
static int _outputAudioBus;

...

#pragma mark - Audio Unit

+(void)_activateAudioUnit
{
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil];
    if([self _createAudioUnitInstance]
       && [self _setupAudioUnitOutput]
       && [self _setupAudioUnitFormat]
       && [self _setupAudioUnitRenderCallback]
       && [self _initializeAudioUnit]
       && [self _startAudioUnit]
       )
    {
        [self _adjustOutputLatency];
//        NSLog(@"Audio unit initialized");
    }
}

+(BOOL)_createAudioUnitInstance
{
    // Describe audio component
    AudioComponentDescription desc;
    desc.componentType = kAudioUnitType_Output;
    desc.componentSubType = kAudioUnitSubType_RemoteIO;
    desc.componentFlags = 0;
    desc.componentFlagsMask = 0;
    desc.componentManufacturer = kAudioUnitManufacturer_Apple;
    AudioComponent inputComponent = AudioComponentFindNext(NULL, &desc);

    // Get audio units
    OSStatus status = AudioComponentInstanceNew(inputComponent, &_audioUnit);
    [self _logStatus:status step:@"instantiate"];
    return (status == noErr );
}

+(BOOL)_setupAudioUnitOutput
{
    UInt32 flag = 1;
    OSStatus status = AudioUnitSetProperty(_audioUnit,
                                  kAudioOutputUnitProperty_EnableIO,
                                  kAudioUnitScope_Output,
                                  _outputAudioBus,
                                  &flag,
                                  sizeof(flag));
    [self _logStatus:status step:@"set output bus"];
    return (status == noErr );
}

+(BOOL)_setupAudioUnitFormat
{
    AudioStreamBasicDescription audioFormat = {0};
    audioFormat.mSampleRate         = 44100.00;
    audioFormat.mFormatID           = kAudioFormatLinearPCM;
    audioFormat.mFormatFlags        = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
    audioFormat.mFramesPerPacket    = 1;
    audioFormat.mChannelsPerFrame   = 2;
    audioFormat.mBitsPerChannel     = 16;
    audioFormat.mBytesPerPacket     = 4;
    audioFormat.mBytesPerFrame      = 4;

    OSStatus status = AudioUnitSetProperty(_audioUnit,
                                           kAudioUnitProperty_StreamFormat,
                                           kAudioUnitScope_Input,
                                           _outputAudioBus,
                                           &audioFormat,
                                           sizeof(audioFormat));
    [self _logStatus:status step:@"set audio format"];
    return (status == noErr );
}

+(BOOL)_setupAudioUnitRenderCallback
{
    AURenderCallbackStruct audioCallback;
    audioCallback.inputProc = playbackCallback;
    audioCallback.inputProcRefCon = (__bridge void *)(self);
    OSStatus status = AudioUnitSetProperty(_audioUnit,
                                           kAudioUnitProperty_SetRenderCallback,
                                           kAudioUnitScope_Global,
                                           _outputAudioBus,
                                           &audioCallback,
                                           sizeof(audioCallback));
    [self _logStatus:status step:@"set render callback"];
    return (status == noErr);
}


+(BOOL)_initializeAudioUnit
{
    OSStatus status = AudioUnitInitialize(_audioUnit);
    [self _logStatus:status step:@"initialize"];
    return (status == noErr);
}

+(void)start
{
    [self clearFeeds];
    [self _startAudioUnit];
}

+(void)stop
{
    [self _stopAudioUnit];
}

+(BOOL)_startAudioUnit
{
    OSStatus status = AudioOutputUnitStart(_audioUnit);
    [self _logStatus:status step:@"start"];
    return (status == noErr);
}

+(BOOL)_stopAudioUnit
{
    OSStatus status = AudioOutputUnitStop(_audioUnit);
    [self _logStatus:status step:@"stop"];
    return (status == noErr);
}

+(void)_logStatus:(OSStatus)status step:(NSString *)step
{
    if( status != noErr )
    {
        NSLog(@"AudioUnit failed to %@, error: %d", step, (int)status);
    }
}

Наконец, как только это будет запущено, мой зарегистрированный звуковой обратный вызов будет тем, который обеспечивает звук:

static OSStatus playbackCallback(void *inRefCon,
                                 AudioUnitRenderActionFlags *ioActionFlags,
                                 const AudioTimeStamp *inTimeStamp,
                                 UInt32 inBusNumber,
                                 UInt32 inNumberFrames,
                                 AudioBufferList *ioData) {

    @autoreleasepool {
        AudioBuffer *audioBuffer = ioData->mBuffers;

        // .. fill in audioBuffer with Metronome sample data, fill the in-between ticks with 0s
    }
    return noErr;
}

Вы можете использовать звуковой редактор, например Audacity: https://www.audacityteam.org/download/mac/, чтобы отредактировать и сохранить файл в файле моно/стерео данных RAW PCM, или вы можете использовать одну из библиотек AVFoundation для извлечения аудиосэмплов из любого из поддерживаемых форматов аудиофайлов. Загрузите сэмплы в буфер, отслеживайте, где вы остановились между кадрами аудио обратного вызова, и подайте сэмпл метронома, чередующийся с 0.

Прелесть этого заключается в том, что теперь вы можете положиться на AudioToolbox iOS для определения приоритетов вашего кода, чтобы аудио и анимация просмотра не мешали друг другу.

Здоровья и удачи!

person ekscrypto    schedule 02.08.2018
comment
_adjustOutputLatency — это функция, которая регулирует опережение звука по отношению к визуальным эффектам, когда звук маршрутизируется через Bluetooth или трансляцию. Без этого изображение на экране и слышимый звук больше не синхронизированы. Может быть или не быть необходимым в зависимости от ваших требований. clearFeeds остался от моего приложения, которое смешивает несколько аудиопотоков вместе с метрономом, оно просто удаляет предыдущие аудиопотоки перед включением звука, что, скорее всего, не нужно в вашем случае. - person ekscrypto; 02.08.2018
comment
Как заполнить AudioBuffer? Если у меня есть звуковой файл tick.caf - person Valentin Shamardin; 02.08.2018
comment
К счастью, на этот вопрос уже несколько раз отвечали: stackoverflow.com/questions/7537505/ - person ekscrypto; 02.08.2018
comment
Если я помещу код для воспроизведения звука тика в playbackCallback, анимация все равно замедлит таймер. - person Valentin Shamardin; 02.08.2018
comment
@ValentinShamardin, можете ли вы показать свой обновленный код? В частности, функцияplayCallback - person ekscrypto; 02.08.2018
comment
Давайте продолжим обсуждение в чате. - person Valentin Shamardin; 02.08.2018

Я нашел решение, играя с Apple AVAudioEngine пример HelloMetronome. Я понял основную мысль. Мне нужно планировать звуки и обрабатывать обратные вызовы в пользовательском интерфейсе. Использование любых таймеров для запуска воспроизведения звуков и обновления пользовательского интерфейса было абсолютно неправильным.

person Valentin Shamardin    schedule 14.08.2018