Просматривая рунет в поисках статей на эту тему, удивляешься их отсутствию. Возможно, людям хватает описания в хэдере класса, а может, все просто предпочитают изучать его опытным путём. Так или иначе, для тех, кто первый раз сталкивается с этим классом или чувствует потребность в наглядных примерах, мы предлагаем это несложное практическое руководство от наших партнеров, Music Topia.

В плане работы со звуком Apple находится впереди своих конкурентов, и это неспроста. Компания предлагает хороший инструментарий для проигрывания, записи и обработки треков. Благодаря этому на AppStore можно увидеть огромное количество приложений, которые так или иначе работают с аудиоконтентом. В их число входят проигрыватели с хорошим звуком (Vox), аудиоредакторы с инструментами для редактирования и наложения эффектов (Sound Editor), приложения, изменяющие голос (Voicy Helium), различные симуляторы музыкальных инструментов, которые дают довольно точную имитацию соответствующего звучания (Virtuoso Piano), и даже симуляторы DJ-установок (X Djing).



Для работы с аудио Apple предоставляет фреймворк AVFoundation, который объединяет в себе несколько инструментов. Например, AVAudioPlayer используется для проигрывания одиночного трека, а AVAudioRecorder — для записи звука с микрофона. И если вам нужны только функции, предоставляемые этими классами, то просто используйте их. Если же вам необходимо наложить какой-либо эффект, проигрывать несколько дорожек одновременно, микшировать их, заняться обработкой или редактированием аудио или записать звук с выхода определенного аудиоузла, то с этим вам поможет AVAudioEngine. Больше всего данный класс привлекает возможностью наложения эффектов на трек. Как раз на основе этих эффектов построена уйма приложений с эквалайзерами и возможностью изменить голос. Кроме того, Apple позволяет разработчикам создавать свои эффекты, генераторы звука и инструменты.

Основные элементы и их взаимосвязь


Начнём, пожалуй, с основного движка AVAudioEngine. Этот движок представляет собой группу соединений аудиоузлов, которые генерируют и обрабатывают аудиосигналы и представляют аудиовходы и выходы. Его также можно описать как схему аудиоузлов, которые расположены определенным образом для достижения необходимого результата. То есть можно сказать, что AVAudioEngine — это материнская плата, на которой расставлены микросхемы (в нашем случае аудиоузлы).

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

Работая с этим классом, необходимо в определенном порядке выполнять инициализации и методы, чтобы все операции осуществлялись корректно. Приведём последовательность действий по пунктам:

  1. Для начала выставляем аудиосессию AVAudioSession.
  2. Создаем движок.
  3. Создаем по отдельности узлы AVAudioNode.
  4. Прикрепляем узлы к движку (attach).
  5. Соединяем узлы между собой (connect).
  6. Запускаем движок.

1. Настройка аудиосессии

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

[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil];
[[AVAudioSession sharedInstance] setActive:YES error:nil];

Чтобы настроить на запись, необходимо выставить категорию AVAudioSessionCategoryRecord. Со всеми категориями можно ознакомиться здесь.

2. Создание движка

Для работы этого движка нужно импортировать фреймворк AVFoundation и инициализировать класс.

#import <AVFoundation/AVFoundation.h>

Инициализация происходит так:

AVAudioEngine *engine = [[AVAudioEngine alloc] init];

3. Создание узлов

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

Рассмотрим каждый вид аудиоузлов по отдельности.

Самым распространенным типом аудиоузлов является AVAudioPlayerNode. Эта разновидность, как понятно из названия, используется для воспроизведения аудио. Узел проигрывает звук либо из заданного буфера представления AVAudioBuffer, либо из сегментов аудиофайла, открытого посредством AVAudioFile. Буферы и сегменты могут быть запланированы для воспроизведения в определенный момент или же проигрываться сразу после предшествующего сегмента.

Пример инициализации и проигрывания:

AVAudioPlayerNode *playerNode = [[AVAudioPlayerNode alloc] init];
…
NSURL *url = [[NSBundle mainBundle] URLForResource:@"sample" withExtension:@"wav"];
AVAudioFile *audioFile = [[AVAudioFile alloc] initForReading:url error:nil];
[playerNode scheduleFile:audioFile atTime:0 completionHandler:nil];
[playerNode play];

Далее рассмотрим класс AVAudioUnit, который также является дочерним классом AVAudioNode. AVAudioUnitEffect (дочерний класс AVAudioUnit) выступает в качестве родительного класса для классов эффектов. Наиболее часто используются следующие эффекты: AVAudioUnitEQ (эквалайзер), AVAudioUnitDelay (задержка), AVAudioUnitReverb (эффект эхо) и AVAudioUnitTimePitch (эффект ускорения или замедления, высота тона). Каждый из них имеет свой набор настроек для изменения и обработки звука.

Инициализация и настройка эффекта на примере эффекта задержки:

AVAudioUnitDelay *delay = [[AVAudioUnitDelay alloc] init];
delay.delayTime = 1.0f;//Время затрачиваемое входным сигналом для достижения выхода, от 0 до 2 секунд

Следующий нод, AVAudioMixerNode, используется для микширования. В отличие от остальных узлов, у которых строго по одному входному узлу, микшер может иметь несколько. Здесь вы можете объединить узлы-проигрыватели с узлами эффектов и эквалайзером, после чего выход узла подключается уже к AVAudioOutputNode для вывода итогового звука.

Пример инициализации узлов микшера и вывода:

AVAudioMixerNode *mixerNode = [[AVAudioMixerNode alloc] init];
AVAudioOutputNode *outputNode = engine.outputNode;

4. Прикрепление узлов к движку

Однако одного лишь объявления узла недостаточно, чтобы он заработал. Его нужно прикрепить к движку и соединить с другими узлами посредством шин ввода и вывода. Движок поддерживает динамические подключение, отключение и удаление узлов во время запуска с небольшими ограничениями:

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

Для прикрепления узлов к движку используется метод «attachNode:», а для открепления от движка — «detachNode:».

[engine attachNode:playerNode];
[engine detachNode:delay];

5. Соединение узлов между собой

Все прикрепленные узлы необходимо соединить в общую схему для вывода звука. Соединения зависят от того, что вы хотите выводить. Нужны эффекты — подключайте узлы эффектов к узлу проигрывателя, нужно объединить два трека — присоединяйте два узла-проигрывателя к микшеру. Или, может быть, вам нужно два отдельных трека: один с чистым звуком, а второй с обработанным — это тоже можно реализовать. Порядок соединения не так важен, однако последовательность соединений в итоге должна быть подключена к основному микшеру, который, в свою очередь, подключается к узлу вывода звука. Исключением являются разве что входные узлы, которые обычно используются для записи с микрофона.

Приведу примеры на схемах и в виде кода:

1) Проигрыватель с эффектами



[engine connect:playerNode to:delay format:nil];
[engine connect:delay to:reverb format:nil];
[engine connect:reverb to:engine.mainMixerNode format:nil];
[engine connect:engine.mainMixerNode to:outputNode format:nil];
[engine prepare];

2) Два проигрывателя, после одного из которых осуществляется обработка звука



[engine connect:player1 to:eq format:nil];
[engine connect:eq to:pitch format:nil];
[engine connect:pitch to:mixerNode fromBus:0 toBus:0 format:nil];

[engine connect:player2 to:mixerNode fromBus:0 toBus:1 format:nil];

[engine connect:mixerNode to:outputNode format:nil];
[engine prepare];

3) Четыре проигрывателя, два из которых обрабатываются эквалайзером



[engine connect:player1 to:mixer1 fromBus:0 toBus:0 format:nil];
[engine connect:player2 to:mixer1 fromBus:0 toBus:1 format:nil];
[engine connect:mixer1 to:eq format:nil];
[engine connect:eq to:engine.mainMixerNode fromBus:0 toBus:0 format:nil];

[engine connect:player3 to: engine.mainMixerNode fromBus:0 toBus:1 format:nil];
[engine connect:player4 to: engine.mainMixerNode fromBus:0 toBus:2 format:nil];

[engine connect:engine.mainMixerNode to:engine.outputNode format:nil];
[engine prepare];

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

- (instancetype)init
{
    self = [super init];
    if (self) {
        [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
        [[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil];
        [[AVAudioSession sharedInstance] setActive:YES error:nil];
        
        engine = [[AVAudioEngine alloc] init];
        playerNode = [[AVAudioPlayerNode alloc] init];
        AVAudioMixerNode *mixerNode = engine.mainMixerNode;
        AVAudioOutputNode *outputNode = engine.outputNode;
        AVAudioUnitEQ *eq = [[AVAudioUnitEQ alloc] initWithNumberOfBands:3];
        eq.bands[0].filterType =  AVAudioUnitEQFilterTypeParametric;
        eq.bands[0].frequency = 100.f;
        
        eq.bands[1].filterType =  AVAudioUnitEQFilterTypeParametric;
        eq.bands[1].frequency = 1000.f;
        
        eq.bands[2].filterType =  AVAudioUnitEQFilterTypeParametric;
        eq.bands[2].frequency = 10000.f;
        
        [engine attachNode:playerNode];
        [engine attachNode:eq];
        
        [engine connect:playerNode to:eq format:nil];
        [engine connect:eq to:engine.mainMixerNode format:nil];
        [engine connect:engine.mainMixerNode to:engine.outputNode format:nil];
        [engine prepare];
        
        if (!engine.isRunning) {
            [engine startAndReturnError:nil];
        }
    }
    return self;
}

Ну и пример метода проигрывания:

-(void)playFromURL:(NSURL *)url
{
    AVAudioFile *audioFile = [[AVAudioFile alloc] initForReading:url error:nil];
    [playerNode scheduleFile:audioFile atTime:0 completionHandler:nil];
    [playerNode play];
}

Возможные проблемы


При разработке с AVAudioEngine не стоит забывать о некоторых вещах, без которых приложение будет работает некорректно или крашиться.

1. Аудиосессия и проверка на запуск перед проигрыванием

Перед проигрыванием всегда стоит выставлять аудиосессию: по тем или иным причинам она может сброситься или изменить категорию в процессе использования приложения. Также перед проигрыванием следует проверять, запущен ли движок, так как он может оказаться остановленным (например, в результате переключения на другое приложение с тем же движком). Иначе есть риск, что приложение вылетит.

[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil];
[[AVAudioSession sharedInstance] setActive:YES error:nil];

if (!engine.isRunning) {
    [engine startAndReturnError:nil];
}

2. Переключение гарнитуры

Ещё один важный момент — это подключение/отключение наушников или bluetooth-гарнитуры. При этом движок тоже останавливается, и зачастую проигрываемый трек перестает играть. Поэтому стоит этот момент отлавливать, добавив наблюдателя на получение уведомления «AVAudioSessionRouteChangeNotification». В селекторе нужно будет выставить аудиосессию и перезапустить движок.

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioRouteChangeListenerCallback:) name:AVAudioSessionRouteChangeNotification object:nil];

Заключение


В данной статье мы ознакомились с AVAudioEngine, по пунктам рассмотрели, как с ним работать, и привели несколько простых примеров. Это хороший инструмент в арсенале Apple, с которым вы можете сделать много интересных приложений разной степени сложности, в той или иной мере работающих со звуком.

Комментарии (11)


  1. sacred
    29.12.2017 12:14

    Работал с ним недавно и столкнулся с такой особенностью — если вам нужно обработать ассет с помощью того же pitch эффекта и сохранит в файл, то сделать это без проигрывания через плеер ноду вроде как и нельзя. А цель — записать видео и изменить в нем аудиодорожку и сохранить на диск уже обработанное видео, и чтобы юзер ничего не слышал до конца обработки. До iOS 11. Там добавили так называемый оффлайн рендеринг аудио.


    1. EverydayTools Автор
      29.12.2017 14:05

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


      1. sacred
        29.12.2017 14:07

        сталкивался с этим решением на стеке. оно уже не работает. да и от AVAudioEngine там мизер, основном коде — это core media и core audio.


    1. kirill_aust
      30.12.2017 14:44

      Тоже сталкивался с такой проблемой, у AVAudioEngine есть рендер, но чтоб он работал нужно делать примерно так:


      1. Вызвать play у плеера
      2. Вызвать pause у engine
      3. Вызвать рендер

      Работало на 9 iOS точно, если интересно постараюсь на днях скинуть пример


      1. sacred
        30.12.2017 17:12

        да было бы интересно глянуть. и странно, раз есть такое решение, то зачем с ios11 они сделали отдельное апи для оффлайн рендеринга


  1. barkalov
    29.12.2017 14:29

    Подскажите, какую минимальную задержку (latency) дает аудиотракт у яблочных девайсов?
    Сколько мсек проходит от импульса на входе, до импульса на выходе?


    1. alsakharov
      29.12.2017 23:40

      Если у вас возник этот вопрос, вам нужна нормальная внешняя аудиокарта.


      1. barkalov
        29.12.2017 23:51

        А у меня есть. ASIO, все дела.
        Просто интересно, все эти гитарные эффект-процессоры в appstore — баловство или реальность? Android, вот, и близко не могёт. У него 100+ мсек только на воспроизведение задержка.


        1. V1tol
          30.12.2017 02:22

          Как-то девелопил одно приложение с аудио и для тестирования input с output соединил — у меня получился натуральный мегафон. Вообще неощутимая задержка. Например, если верить измерениям по этой сссылке, программная задержка в пределах десятка миллисекунд. Вот это исследование говорит, что все iOS устройства программно укладываются в предел 6-8 миллисекунд.


          1. barkalov
            30.12.2017 08:26

            Ого, весьма недурно! Меньше 5 мсек и из «железного» DSP зачастую бывает не выжать, особеннно если есть какой-нибудь look ahead compressor в цепи. Спасибо за ссылки, изучил.


  1. iWheelBuy
    30.12.2017 06:31

    Пожалуйста, напишите публикацию про AUGraph и все что с этим связано. У вас хорошо получается (: