Очень хотелось написать несколько статей о AudioUnit в iOS. Я уж было написал большую часть о том, как можно применять эффекты к звуку с помощью Audio Unit (далее я буду писать просто юнит). Но в процессе понял, что непосвященному читателю будет тяжело понять написанное и поэтому решил начать с малого.

Для начала разберемся, что такое Audio Unit. Как гласит Wikipedia, AudioUnit — этой некий плагин в OS X (iOS) для генерации, обработки либо другого управления звуком с минимальной задержкой. В коде юнит выступает указателем на struct.

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

Набор юнитов в iOS достаточно ограничен по сравнению с OS X, но с каждой версией iOS их становиться все больше. Все написанное здесь нужно воспринимать в контексте iOS 6, т.к. на сегодняшний день она обладает наибольшим количеством юнитов среди всех версий iOS.

Как мы видим AudioUnit

Как я и говорил, в коде юнит выступает указателем на структуру. Например вот так:

AudioUnit unit;

На самом деле на указатель это не похоже, но где-то в недрах написано:

typedef struct OpaqueAudioComponentInstance *   AudioComponentInstance;
typedef AudioComponentInstance AudioUnit;

Для того, чтобы однозначно определить юнит, нам нужно определиться с тремя параметрами: производитель, тип и подтип. Подтип можно считать названием юнита. Производителем в нашем случае всегда будет выступать Apple. Что касается типов, то в iOS они следующие:

  • Output — юниты ввода/вывода звука, запись/воспроизведение
  • Generator — юниты, которые служат источником звука для других юнитов
  • Mixer — юниты для смешивания звука, например, на вход поступает два потока звука, а выходит один смешанный поток звука
  • Effect — различные звуковые эффекты
  • FormatConverter — юниты, которые могут изменять формат звука

Так же существуют типы юнитов, которые работают с MIDI устройствами, но я их рассматривать не буду. В этот список не вошли типы юнитов, которые присутствуют на OS X, но не присутствуют на iOS. Подтипы юнитов разные для каждой категории. В следующих статьях мы будем рассматривать отдельно некоторые подтипы юнитов.

Итак, как же создать юнит? Как говорилось выше, нам нужно определиться с тремя параметрами. Эти параметры необходимо записать в структуру AudioComponentDescription. Затем необходимо найти нужный компонент и с помощью него создать юнит. Выглядит это вот так:

AudioUnit unit; // юнит

AudioComponentDescription acd; // описание компонента memset(&acd, 0, sizeof(AudioComponentDescription)); acd.componentManufacturer = kAudioUnitManufacturer_Apple; acd.componentType = kAudioUnitType_Effect; acd.componentSubType = kAudioUnitSubType_Delay;

// находим необходимый компонент AudioComponent component = AudioComponentFindNext(NULL, &acd);

// создаем юнит AudioComponentInstanceNew(component, &unit);

Изменяя тип и подтип компонента, мы можем получать другие юниты. Данный код нам предоставит экземпляр юнита, но такой юнит еще не готов к работе. После получения экземпляра юнита, его необходимо инициализировать. При инициализации юнит захватывает требуемые ему ресурсы и производит необходимые действия для того, чтобы перейти в свое основное состояние. Такое разделение обусловлено тем, что некоторые свойства можно устанавливать только когда юнит находится в не инициализированном состоянии, например, формат потока звука. О свойствах юнитов и форматах звука мы поговорим ниже.

Чтобы инициализировать юнит достаточно выполнить:

AudioUnitInitialize(unit);

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

AudioUnitUninitialize(unit);
AudioComponentInstanceDispose(unit);

Шины и Scope

Юнит имеет входные и выходные шины (bus). Есть юниты не имеющие входных шин, например, юнит, который воспроизводит аудио-файл. Шины нумеруют с 0. Например, юнит RemoteIO, который выводит звук на устройство и вводит звук с микрофона, имеет две шины (0 для воспроизведения, 1 для записи). У каждой шины есть scope, чаще всего используются значения Input (вход звука в шину), Output (выход звука из шины), Global (оба случая).

Рассмотрим шину 0 юнита RemoteIO. Через эту шину мы вводим звук в юнит, который необходимо воспроизвести, а юнит через эту шину выводит звук уже на звуковое устройство (наушники, динамики). Когда мы вводим звук в юнит через шину мы используем Input-Scope (вход) шины 0, а когда юнит выводит свой звук используется Output-Scope (выход) шины 0. Такая же история с шиной 1.

Юнит с эффектом Delay имеет одну шину, ее номер — 0. Когда мы отправляем звук на эту шину мы используем Input-Scope (вход) шины 0, звук выходит из юнита через Output-Scope (выход) шины 0.

Обработка ошибок

Почти все функции, которые мы вызываем для работы с юнитами (и не только они) возвращают код ошибки (OSStatus), если она случилась. Если ошибки не было, функция возвращает noErr. Если произошла ошибка стоит разобраться почему она возникла, поэтому каждый вызов следует оборачивать в проверку. Для этого я определяю функцию:

OSStatus CAError(OSStatus result, const char *file, int line)
{
    if (result == noErr) return noErr;
    fprintf(stderr, "Error in %s in %dn", file, line);
    return result;
}

И определяю макрос:

#define CA(x) CAError(x,__FILE__,__LINE__)

Теперь каждый вызов можно оборачивать в макрос CA. Например, вот так:

CA(AudioUnitInitialize(unit));

Если произойдет ошибка, то в консоль будет выведен файл и строка, где произошла ошибка.

Форматы звука

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

struct AudioStreamBasicDescription
{
    Float64 mSampleRate;
    UInt32  mFormatID;
    UInt32  mFormatFlags;
    UInt32  mBytesPerPacket;
    UInt32  mFramesPerPacket;
    UInt32  mBytesPerFrame;
    UInt32  mChannelsPerFrame;
    UInt32  mBitsPerChannel;
    UInt32  mReserved;
};

По большей части, вам не прийдется заполнять поля этой структуры вручную. Назначение некоторых полей понятно из названия, а вот некоторых нет. Далее приведено описание полей структуры, но для ясности стоит определиться что такое packet (пакет), frame (фрейм), sample (отсчет). Мы с вами (и юниты) будем работать с форматом PCM. В таком случае, файл состоит из отсчетов, которые объединяются в фреймы, а фреймы объединяются в пакеты. Количество фреймов в пакете, их размер, порядок отсчетов — все это задается в структуре AudioStreamBasicDescription.

  • mSampleRate — частота дискретизации звука. Показывает количество отсчетов (samples), которое содержит одна секунда звука. Каждый отсчет имеет конкретное количественное значение и представлен конкретным типом (например, Float32 или SInt16).
  • mFormatID — определяет основной вид данных в потоке звука. Мы будем работать с импульсно-кодовой модуляцией (PCM). Это поле в таком случае равно kAudioFormatLinearPCM
  • mFormatFlags — флаги, описывающие формат. В основном описывает тип, которым представлен звук (kAudioFormatFlagIsFloat или kAudioFormatFlagIsSignedInteger), порядок байтов (kAudioFormatFlagIsBigEndian), порядок расположения каналов (kAudioFormatFlagIsNonInterleaved). Non-Interleaved означает что каждый фрейм (frame) звука содержит один отсчет канала (sample). Interleaved означает, что фрейм содержит в себе отсчет для каждого канала звука (для моно — один, для стерео — два и т.д.).
  • mBytesPerPacket — размер пакета в байтах
  • mFramesPerPacket — количество фреймов в пакете, чаще всего один фрейм на один пакет. В сжатом формате может быть другое количество.
  • mBytesPerFrame — размер фрейма в байтах. Если звук Non-Interleaved то это будет и размером одного отсчета.
  • mChannelsPerFrame — количество каналов звука в фрейме. Если звук Non-Interleaved то это поле будет равно еденице т.к. такой звук несет в каждом фрейме один канал.
  • mBitsPerChannel — бит на канал
  • mReserved — не используется

Свойства

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

Например, возмем свойство kAudioUnitProperty_StreamFormat. Это — формат звука, который будет входить/выходить из юнита. Свойства устанавливаются либо получаются для конкретной шины и для конкретного Scope. Можно использовать Global-Scope для обоих случаев.

Функция установки свойства выглядит вот так:

OSStatus AudioUnitGetProperty(
                     AudioUnit                  inUnit, // юнит
                     AudioUnitPropertyID	inID, // свойство
                     AudioUnitScope             inScope, // scope
                     AudioUnitElement           inElement, // шина
                     void *                     outData, // куда записать значение свойства
                     UInt32 *                   ioDataSize // на входе - размер передаваемых данных, на выходе - кол-во записанных данных
); 

Получим формат звука из юнита:

AudioUnit unit;
UInt32 propertySize = sizeof(AudioStreamBasicDescription);
AudioStreamBasicDescription format;
CA(AudioUnitGetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &format, &propertySize));

В данном случае мы получили формат звука, который необходимо подавать юниту на вход шины 0. Обратите внимание, что мы передаем и получаем размер данных, которые передали для сохранения значения свойства.

Установка свойства выглядит так:

OSStatus AudioUnitSetProperty(
                     AudioUnit	            inUnit, // юнит
                     AudioUnitPropertyID    inID, // свойство
                     AudioUnitScope         inScope, // scope
                     AudioUnitElement       inElement, // шина
                     const void *           inData, // значение свойства, которое необходимо установить
                     UInt32                 inDataSize // размер передаваемых данных
);
UInt32 shouldAllocateBuffer = 1;
CA(AudioUnitSetProperty(unit, kAudioUnitProperty_ShouldAllocateBuffer, kAudioUnitScope_Global, 0, &shouldAllocateBuffer, sizeof(UInt32)));

Как видим, отличий от получения свойства не так уж и много. Разница лишь в том, что мы не получаем размер назад.

Параметры

Параметры, по сути, являются теми же свойствами, только они влияют на то, что юнит делает со звуком. Например, параметр kDelayParam_DelayTime для юнита AUDelay задает время в секундах, через которое повториться звук. Установка и получение параметра очень похожа на установку и получение свойства:

OSStatus AudioUnitSetParameter(
                      AudioUnit                 inUnit, // юнит
                      AudioUnitParameterID	inID, // ID параметра
                      AudioUnitScope            inScope, // scope (вход или выход)
                      AudioUnitElement          inElement, // шина
                      AudioUnitParameterValue   inValue, // значение параметра
                      UInt32                    inBufferOffsetInFrames // будет всегда равно 0
);

AudioUnitParameterValue delayValue = 1.5;
CA(AudioUnitSetParameter(unit, kDelayParam_DelayTime, kAudioUnitScope_Global, 0, delayValue, 0));

Получение параметра:

AudioUnitParameterValue value;
CA(AudioUnitGetParameter(unit, kDelayParam_DelayTime, kAudioUnitScope_Global, 0, &value));

Аудиофайлы

Так как мы будем работать со звуком, и довольно часто этот звук будет сохранен в аудиофайлы, нам необходимо будет читать эти самые файлы со звуком. Для этого мы будем использовать ExtendedAudioFile API. Данное API позволяет «на лету» переводить звук из одного формата в другой, что нам будет очень полезно. API имеет небольшой набор функций: открыть/создать файл, запись/чтение, получить позицию/сдвинуть позицию.

Звук для чтения и записи всегда будет передаваться через структуру AudioBufferList:

struct AudioBufferList
{
    UInt32      mNumberBuffers; // количество буферов
    AudioBuffer mBuffers[1]; // массив буферов, на самом деле количество переменное
};

struct AudioBuffer
{
    UInt32  mNumberChannels; // количество каналов в буфере
    UInt32  mDataByteSize; // размер буфера в байтах
    void*   mData; // указатель на данные
};

Для работы с аудио-файлами будет использоваться специальная структура ExtAudioFileRef. Как и юнитам, аудиофайлам можно выставлять свойства. Нам очень сильно понадобиться свойство kExtAudioFileProperty_ClientDataFormat. Оно позволяет установить формат, в который мы хотим конвертировать звук при чтении, и формат звука, который мы будем передавать для записи. Так же очень полезным оказывается свойство kExtAudioFileProperty_FileLengthFrames, которое предоставляет нам количество фреймов в файле.

Утилита afconvert

Данная утилита на OS X позволит конвертировать любой аудиофайл в формат, который нам нужен будет для работы. Делается это так:

afconvert -d F32@44100 -o MyFaforiteSong.mp3 MyFavoriteSong.caf

Выше я писал о том, что мы будем работать с не сжатым звуком. Команда выше как раз позволяет преобразовать сжатый звук в не сжатый и пригодный для работы. В итоге мы получим звук, который представлен форматом Float32 и имеет частоту дискретизации 44100.

Это было небольшое введение, которое поможет лучше понимать то, что будет написано в следующих статьях. В следующих статьях мы рассмотрим некоторые юниты, а так же их совместное использование в графе. Спасибо за внимание!

  • Audio Unit в iOS. Часть 2, строим граф и проигрываем файлы
  • Audio Unit в iOS. Часть 3, накладываем эффект Delay

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *