В этой статье мы продолжим знакомиться с Audio Unit. Если вы не читали введение, обязательно прочитайте, т.к. возможно будет трудно понять содержание этой и следующих статей. В данной статье мы быстро пробежимся по AudioFileID, рассмотрим как строить граф из юнитов, а так же как воспроизвести звуковой файл.

Происходить это все будет с помощью двух юнитов — AudioFilePlayer и RemoteIO. Рекомендую загрузить исходный код проекта т.к. весь код из статьи — это пояснения к проекту. Реализация «плеера» находиться в классе Player.

Для статьи я подготовил 6 звуковых файлов, взятых с этого сайта.

AudioFileID

AudioFileID — это еще один тип данных, который может представлять аудио файл в iOS. В контексте данной статьи нам нужно знать, как открыть, закрыть и получить свойства таких файлов. Файл открывается с помощью функции AudioFileOpenURL, которой необходимо передать URL файла, режим доступа и указатель на структуру AudioFileID:

AudioFileID audioFile;
CFURLRef url = ...;
AudioFileOpenURL(url, kAudioFileReadPermission, 0, &audioFile); // открываем в режиме чтения

Закрывается файл функцией AudioFileClose. Для получения свойства нам нужно будет вызвать две функции — узнать размер, а затем и значение свойства. Рассмотрим на примере получения количества пакетов в файле:

UInt32 propertySize;
UInt32 packetsCount;
AudioFileGetPropertyInfo(audioFile, kAudioFilePropertyAudioDataPacketCount, &propertySize, NULL); // узнаем размер
AudioFileGetProperty(audioFile, kAudioFilePropertyAudioDataPacketCount, &propertySize, &packetsCount); // получаем значение свойства

В обоих случаях мы передаем структуру AudioFileID, свойство которое хотим получить и его размер. Последний аргумент функции AudioFileGetPropertyInfo — флаг, который скажет нам о том, можно ли изменять свойство. В данной ситуации он нам не нужен и мы можем передать NULL. Последний аргумент функции AudioFileGetProperty — есть указатель, куда сохранить значение свойства.

AUAudioFilePlayer

AUAudioFilePlayer — это юнит. Данный юнит умеет воспроизводить аудифайлы представленные через AudioFileID. Под «воспроизводить» я имею ввиду чтение аудио-файла и передача звука другому юниту, другими словами AUAudioFilePlayer умеет генерировать звук. Юнит не имеет входных шин, но имеет выходную, через которую он и отправляет дальше считанный из файла(ов) звук. Один AUAudioFilePlayer-юнит может воспроизводить несколько аудиофайлов, которые необходимо обозначить заранее. Но, к сожалению, в один момент времени он может воспроизводить один файл.

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

  1. Установка времени старта юнита
  2. Установка проигрываемых файлов
  3. Указание количества фреймов, которые должны быть загружены перед воспроизведением

Время юнита

Мы начнем с внутреннего времени юнита. У юнита есть свое внутреннее время, которое представляется структурой AudioTimeStamp:

struct AudioTimeStamp
{
    Float64         mSampleTime;
    UInt64          mHostTime;
    Float64         mRateScalar;
    UInt64          mWordClockTime;
    SMPTETime       mSMPTETime;
    UInt32          mFlags;
    UInt32          mReserved;
};

Как видим, структура может вести подсчет времени в нескольких единицах. В каких единицах выражено время, можно определить с помощью поля mFlags. AUAudioFilePlayer использует mSampleTime. Это время, измеряемое количеством отсчетов с некоторого момента (старта юнита или графа). Перед запуском юнита, нужно определиться, в какое время он начнет свою работу. Мы хотим чтобы юнит запустился сразу вместе с графом, поэтому подготовим такую структуру:

AudioTimeStamp startTime;
memset (&startTime, 0, sizeof(AudioTimeStamp));
startTime.mFlags = kAudioTimeStampSampleTimeValid; // обозначим в каких единицах идет отсчет
startTime.mSampleTime = -1; // запустить при первой же возможности

Далее необходимо установить данную структуру, как значение свойства kAudioUnitProperty_ScheduleStartTimeStamp, которое и обозначает время старта юнита:

AudioUnit playerUnit = ...;
CA(AudioUnitSetProperty(playerUnit, kAudioUnitProperty_ScheduleStartTimeStamp, kAudioUnitScope_Global, 0, &startTime, sizeof(AudioTimeStamp)));

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

AudioTimeStamp timeStamp;
AudioUnit playerUnit = ...;
UInt32 propSize = sizeof(AudioTimeStamp);
CA(AudioUnitGetProperty(playerUnit, kAudioUnitProperty_CurrentPlayTime, kAudioUnitScope_Global, 0, &timeStamp, &propSize));

Установка проигрываемых файлов

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

AudioUnit playerUnit = ...;
AudioFileID *files = ...; // массив из AudioFileId
int filesCount = ...; // количество файлов
CA(AudioUnitSetProperty(playerUnit, kAudioUnitProperty_ScheduledFileIDs, kAudioUnitScope_Global, 0, files, sizeof(AudioFileID) * filesCount));

Количество фреймов для старта

Юниту нужно указать, сколько фреймов загрузить перед началом воспроизведения. Это можно сделать так:

UInt32 prime = 0; // ни одного фрейма
AudioUnit playerUnit = ...;
CA(AudioUnitSetProperty(playerUnit, kAudioUnitProperty_ScheduledFilePrime, kAudioUnitScope_Global, 0, &prime, sizeof(prime)));

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

Воспроизведение файлов

Чтобы файл начал воспроизводиться нужно запланировать (schedule) данное деяние. Для этого нужно заполнить структуру ScheduledAudioFileRegion и установить ее в свойство kAudioUnitProperty_ScheduledFileRegion. Структура выглядит так:

struct ScheduledAudioFileRegion {
	AudioTimeStamp mTimeStamp; // внутреннее время юнита, в которое должно начаться воспроизведение
	ScheduledAudioFileRegionCompletionProc  mCompletionProc; // коллбэк для уведомления о событиях
	void * mCompletionProcUserData; // любые данные
	struct OpaqueAudioFileID * mAudioFile; // AudioFileID который хотим играть
	UInt32 mLoopCount; // сколько раз повторить проигрывание
	SInt64 mStartFrame; // с какого фрейма начать
	UInt32 mFramesToPlay; // сколько фреймов играть
};

Благодаря полям mStartFrame и mFramesToPlay мы можем не воспроизводить весь файл целиком, а лишь некоторые его части. Если мы хотим воспроизвести файл целиком, нужно выставить mStartFrame в 0, а mFramesToPlay в полное количество фреймов в файле. Узнать количество фреймов в файле можно умножив поле mFramesPerPacket структуры AudioStreamBasicDescription на количество пакетов в файле. О том, как узнать количество пакетов в файле мы рассматривали выше. Так же все это вы сможете увидеть в исходном коде проекта, прикрепленном к статье.

Если мы хотим, чтобы воспроизведение началось как можно быстрее, «прямо сейчас», то нам нужно выставить поле mTimeStamp в текущее время юнита (о том как его узнать написано выше). Для подстраховки, можно добавить некоторое количество фреймов.

AUGraph

Как я и говорил в введении — мы можем строить граф из юнитов. В данной статье мы построим граф из двух юнитов — AudioFilePlayer и RemoteIO. По-сути, граф содержит в себе вершины (nodes), которые связанны между собой. Как связаны вершины — решаем мы сами. Звук «идет» от одной вершине к другой, и в каждой вершине он обрабатывается юнитом, который находится в вершине. На звук может быть наложен эффект, звук может отправиться на динамики и т.д. Мы построим вот такой граф:

Как видим, AudioFilePlayer не имеет входных шин, зато имеет выходную. RemoteIO имеет входную шину 0 (поданный в нее звук отправится на динамики) и выходную шину 1 (это звук, записанный с микрофона, в этой статье не пригодится). Количество входов и выходов в/из вершины в точности повторяет количество шин в юните, который находиться в этой вершине. AudioFilePlayer будет читать аудиофайл и отправлять в RemoteIO, который уже отправит звук на динамики.

Граф представляется структурой AUGraph. Граф может быть открыт/закрыт, запущен/остановлен, инициализирован/не инициализирован. Процедура построения графа такая:

  1. Создать граф
  2. Добавить в него вершины (юниты)
  3. Соединить вершины
  4. Открыть граф
  5. Инициализировать
  6. Настроить юниты
  7. Запустить

Итак, мы хотим создать граф:

AUGraph graph;
CA(NewAUGraph(&graph));

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

AudioComponentDescription acd;
acd.componentManufacturer = kAudioUnitManufacturer_Apple;
acd.componentType = kAudioUnitType_Output;
acd.componentSubType = kAudioUnitSubType_RemoteIO;
AUNode rioNode;
CA(AUGraphAddNode(graph, &acd, &rioNode));

А так же для AudioFilePlayer:

acd.componentManufacturer = kAudioUnitManufacturer_Apple;
acd.componentType = kAudioUnitType_Generator;
acd.componentSubType = kAudioUnitSubType_AudioFilePlayer;
AUNode playerNode;
CA(AUGraphAddNode(graph, &acd, &playerNode));

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

Следует обратить внимание, что мы не создаем юниты как таковые, а создаем вершины, которые будут содержать в себе юниты (поэтому нам и необходим AudioComponentDescription).

После того, как вершины созданы, можно их соединить:

CA(AUGraphConnectNodeInput(graph, playerNode, 0, rioNode, 0));

В данном случае мы направляем звук из выходной шины 0 вершины playerNode во входную шину 0 вершины rioNode.

Далее необходимо открыть граф:

CA(AUGraphOpen(graph));

После открытия юниты в вершинах будут созданы, но еще не будут инициализированны. На данном этапе самое время получить указатели на юниты, которые находятся в вершинах графа. Для этих целей существует функция AUGraphNodeInfo. Для RemoteIO юнита:

AudioUnit rioUnit; // сам юнит
CA(AUGraphNodeInfo(graph, rioNode, NULL, &rioUnit));

Для AudioFilePlayer:

AudioUnit playerUnit;
CA(AUGraphNodeInfo(graph, playerNode, NULL, &playerUnit));

Как видим, функция принимает на вход граф и вершину. В качестве третьего аргумента можно передать указатель на AudioComponentDescription, в который запишется описание юнита, но в данном случае нам это не нужно и мы передаем NULL. Последний аргумент — указатель на AudioUnit, куда будет записан юнит.

Далее можно инициализировать граф:

CA(AUGraphInitialize(graph));

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

CA(AUGraphStart(graph));

После вызова AUGraphStart граф начнет работать. Но все же некоторые юниты нуждаются в «настройке» и поэтому есть риск вообще ничего не услышать. Например, AudioFilePlayer нужно настраивать так, как описано было выше.

Если мы больше не нуждаемся в графе, мы должны сделать вот так:

AUGraphStop (graph);
AUGraphUninitialize (graph);
AUGraphClose(graph);
DisposeAUGraph(graph);

Граф необязательно всегда должен находиться в запущенном состоянии. Мы безболезненно можем его останавливать и запускать функциями AUGraphStop и AUGraphStart. Например, если хотим приостановить звук. Так останавливать граф нужно, если мы хотим изменить связи вершин.

Заключение

Исходный код проекта находится тут. Там вы найдете реализацию графа с двумя юнитами — RemoteIO и AudioFilePlayer.

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

Если у вас есть вопросы — задавайте, с радостью отвечу в комментариях.

  • Audio Unit в iOS. Часть 1, введение
  • Audio Unit в iOS. Часть 3, накладываем эффект Delay

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

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