Июнь 2016

ТОМ 31, НОМЕР 6

Современные приложения - Эксперименты со звуком в UWP

Фрэнк Ла-Вине | Июнь 2016

Исходный код можно скачать по ссылке bit.ly/1ObYYIb.

Universal Windows Platform (UWP) имеет богатые API для записи звука и видео. Однако набор функциональности не ограничивается только записью. С помощью всего нескольких строк кода разработчики могут применять спецэффекты к звуку в реальном времени. Такие эффекты, как реверберация и эхо, встроены в API и довольно просты в реализации. В этой статье я исследую некоторые основы записи звука и применения спецэффектов. Я создам UWP-приложение, способное записывать звук, сохранять его и применять к нему различные фильтры и спецэффекты.

Подготовка проекта для записи звука

Запись звука требует, чтобы у приложения было разрешение на доступ к микрофону, а это в свою очередь требует модификации файла манифеста приложения. В Solution Explorer дважды щелкните файл Package.appxmanifest. Он всегда находится в корне проекта.

Как только откроется окно редактора файла манифеста, щелкните вкладку Capabilities. В окне списка Capabilities установите флажок рядом с Microphone. Это позволит вашему приложению обращаться к микрофону конечного пользователя. Без этого приложение будет генерировать исключение при попытке доступа к микрофону.

Запись звука

Прежде чем приступать к добавлению спецэффектов к звуку, сначала нужно получить возможность его записывать. Это довольно просто. Первым делом добавьте в свой проект класс для инкапсуляции всего кода записи звука. Назовите этот класс AudioRecorder. В нем будут открытые методы для запуска и остановки записи, а также для воспроизведения только что записанного аудиоклипа. Для этого вам понадобится добавить в класс некоторые члены. Первым из них будет MediaCapture, обеспечивающий захват звука, видео и изображений с устройства захвата, такого как микрофон или веб-камера:

private MediaCapture _mediaCapture;

Вам также потребуется добавить InMemoryRandomAccessStream для захвата ввода с микрофона в память:

private InMemoryRandomAccessStream _memoryBuffer;

Чтобы отслеживать состояние записи, добавьте открытое булево свойство в свой класс:

public bool IsRecording { get; set; }

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

Поскольку класс MediaCapture предоставляет множество функций, вы должны указать, что хотите захватывать звук. Для этого создайте экземпляр MediaCaptureInitializationSettings. Затем код создает экземпляр объекта MediaCapture и передаетMediaCaptureInitializationSettings в метод InitializeAsync, как показано на рис. 1. Наконец, вы сообщаете объекту MediaCapture начать запись, передав параметры, указывающие выбор записи в формате MP3 и место сохранения данных.

Рис. 1. Создание экземпляра объекта MediaCapture

public async void Record()
  {
  if (IsRecording)
  {
    throw new InvalidOperationException("Recording already in progress!");
  }
  await Initialize();
  await DeleteExistingFile();
  MediaCaptureInitializationSettings settings =
    new MediaCaptureInitializationSettings
  {
    StreamingCaptureMode = StreamingCaptureMode.Audio
  };
  _mediaCapture = new MediaCapture();
  await _mediaCapture.InitializeAsync(settings);
  await _mediaCapture.StartRecordToStreamAsync(
    MediaEncodingProfile.CreateMp3(AudioEncodingQuality.Auto), _memoryBuffer);
  IsRecording = true;
}

Остановка записи требует гораздо меньше строк кода:

public async void StopRecording()
{
  await _mediaCapture.StopRecordAsync();
  IsRecording = false;
  SaveAudioToFile();
}

Метод StopRecording делает три вещи: сообщает объекту MediaCapture прекратить запись, присваивает состоянию записи значение false и сохраняет поток аудиоданных в MP3-файл на диске.

Сохранение аудиоданных на диск

Захватив аудиоданные в InMemoryRandomAccessStream, вы сохраняете их на диск (рис. 2). Сохранение аудиоданных из потока в памяти требует копирования содержимого в другой поток и последующего сброса его на диск. Используя вспомогательные средства из пространства имен Windows.ApplicationModel.Package, вы можете получить путь к каталогу установки приложения. (Во время разработки он будет в каталоге \bin\x86\Debug проекта.) Туда вы и запишете файл. Можно было бы легко модифицировать код на сохранение файла в любом другом месте или предоставить выбор пользователю.

Рис. 2. Сохранение аудиоданных на диск

private async void SaveAudioToFile()
{
  IRandomAccessStream audioStream = _memoryBuffer.CloneStream();
  StorageFolder storageFolder = Package.Current.InstalledLocation;
  StorageFile storageFile = await storageFolder.CreateFileAsync(
    DEFAULT_AUDIO_FILENAME, CreationCollisionOption.GenerateUniqueName);
  this._fileName = storageFile.Name;
  using (IRandomAccessStream fileStream =
    await storageFile.OpenAsync(FileAccessMode.ReadWrite))
  {
    await RandomAccessStream.CopyAndCloseAsync(
      audioStream.GetInputStreamAt(0), fileStream.GetOutputStreamAt(0));
    await audioStream.FlushAsync();
    audioStream.Dispose();
  }
}

Воспроизведение звука

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

Код для проигрывания звука из памяти весьма прост. Вы создаете новый экземпляр элемента управления MediaElement, указываете в качестве его источника буфер в памяти, передаете ему MIME-тип, а затем вызываете метод Play:

public void Play()
{
  MediaElement playbackMediaElement = new MediaElement();
  playbackMediaElement.SetSource(_memoryBuffer, "MP3");
  playbackMediaElement.Play();
}

Воспроизведение с диска требует немного дополнительного кода, так как открытие файлов является асинхронной задачей. Чтобы UI-поток взаимодействовал с задачей, выполняемой в другом потоке, вам понадобится задействовать CoreDispatcher. Он передает сообщения между потоком, где выполняется данный код, и UI-потоком. С его помощью код может получить UI-контекст из другого потока. Отличное описание CoreDispatcher см. в блоге Дэвида Крука (David Crook) в статье по ссылке bit.ly/1SbJ6up.

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

public async Task PlayFromDisk(CoreDispatcher dispatcher)
{
  await dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
  {
    MediaElement playbackMediaElement = new MediaElement();
    StorageFolder storageFolder = Package.Current.InstalledLocation;
    StorageFile storageFile = await storageFolder.GetFileAsync(this._fileName);
    IRandomAccessStream stream = await storageFile.OpenAsync(FileAccessMode.Read);
    playbackMediaElement.SetSource(stream, storageFile.FileType);
    playbackMediaElement.Play();
  });
}

Создание UI

Закончив класс AudioRecorder, остается лишь создать UI для приложения. UI в этом проекте довольно прост, так как вам достаточно кнопки для записи и кнопки для воспроизведения записанного звука (рис. 3). Соответственно XAML-код очень прост: TextBlock и StackPanel с двумя кнопками:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
  <Grid.RowDefinitions>
    <RowDefinition Height="43"/>
    <RowDefinition Height="*"/>
  </Grid.RowDefinitions>
<TextBlock FontSize="24">Audio in UWP</TextBlock>
<StackPanel HorizontalAlignment="Center" Grid.Row="1" >
  <Button Name="btnRecord" Click="btnRecord_Click">Record</Button>
  <Button Name="btnPlay" Click="btnPlay_Click">Play</Button>
</StackPanel>
</Grid>

AudioRecorder UI
Рис. 3. UI в AudioRecorder

В классе отделенного кода вы создаете переменную-член AudioRecorder. Это будет объект, используемый приложением для записи и воспроизведения звука:

AudioRecorder _audioRecorder;

Экземпляр класса AudioRecorder создается в конструкторе MainPage приложения:

public MainPage()
{
  this.InitializeComponent();
  this._audioRecorder = new AudioRecorder();
}

Кнопка btnRecord на самом деле является переключателем «старт-стоп» записи звука. Чтобы держать пользователя в курсе текущего состояния AudioRecorder, метод btnRecord_Click меняет контент кнопки btnRecord, а также запускает и останавливает запись.

Есть два варианта применительно к обработчику событий для кнопки btnPlay: воспроизводить из буфера в памяти или из файла на диске.

Для воспроизведения из буфера вы пишете такой код:

private void btnPlay_Click(object sender, RoutedEventArgs e)
{
  this._audioRecorder.Play();
}

Как я уже упоминал, проигрывание файлов с диска происходит асинхронно. То есть эта задача будет выполняться в потоке, отличном от UI-потока. Планировщик ОС определит, какой поток будет обрабатывать эту задачу в период выполнения. Передача объекта Dispatcher методу PlayFromDisk позволяет потоку получить доступ к UI-контексту UI-потока:

private async void btnPlay_Click(object sender, RoutedEventArgs e)
{
  await this._audioRecorder.PlayFromDisk(Dispatcher);
}

Применение спецэффектов

Теперь, когда у вас есть приложение, записывающее и воспроизводящее звук, пора исследовать некоторые из менее известных возможностей в UWP: спецэффекты, применяемые к звуку в реальном времени. В наборе API из пространства имен Windows.Media.Audio поддерживается ряд спецэффектов, которые способны добавить дополнительный штрих к приложению.

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

public async Task<StorageFile>
   GetStorageFile(CoreDispatcher dispatcher)
{
  StorageFolder storageFolder =
    Package.Current.InstalledLocation;
  StorageFile storageFile =
    await storageFolder.GetFileAsync(this._fileName);
  return storageFile;
}

Метод GetStorageFile возвращает объект StorageFile для сохраненного аудиофайла. Через него класс спецэффектов будет обращаться к аудиоданным.

Введение в AudioGraph

Класс AudioGraph занимает центральное место в сценариях продвинутой обработки звука в UWP. AudioGraph может направлять аудиоданные из узлов входных источников в узлы выходных источников через различные узлы микширования. Полный функционал AudioGraph выходит далеко за рамки этой статьи, но в будущем я планирую исследовать его глубже. А пока важный момент заключается в том, что к каждому узлу в аудиографе можно применять несколько звуковых эффектов. Подробнее об AudioGraph см. статью в Windows Dev Center по ссылке bit.ly/1VCIBfD.

Сначала вы добавляете в проект класс AudioEffects и включаете следующие члены:

private AudioGraph _audioGraph;
private AudioFileInputNode _fileInputNode;
private AudioDeviceOutputNode _deviceOutputNode;

Чтобы создать экземпляр класса AudioGraph, вы должны создать объект AudioGraphSettings, содержащий конфигурационные параметры для AudioGraph. Затем вызывается метод AudioGraph.CreateAsync, которому передаются эти конфигурационные параметры. Метод CreateAsync возвращает объект CreateAudioGraphResult. Этот класс обеспечивает доступ к созданному аудиографу и значению состояния, указывающему, было ли создание аудиографа успешным или неудачным.

Кроме того, нужно создать выходной узел для воспроизведения звука. Для этого вызовите метод CreateDeviceOutputNodeAsync класса AudioGraph и присвойте переменную-член свойству DeviceOutputNode класса CreateAudioDeviceOutputNodeResult. Код, инициализирующий AudioGraph и AudioDeviceOutputNode целиком находится в методе InitializeAudioGraph:

public async Task InitializeAudioGraph()
{
  AudioGraphSettings settings = new AudioGraphSettings(AudioRenderCategory.Media);
  CreateAudioGraphResult result = await AudioGraph.CreateAsync(settings);
  this._audioGraph = result.Graph;
  CreateAudioDeviceOutputNodeResult outputDeviceNodeResult =
    await this._audioGraph.CreateDeviceOutputNodeAsync();
  _deviceOutputNode = outputDeviceNodeResult.DeviceOutputNode;
}

Воспроизвести звукозапись из объекта AudioGraph очень легко: достаточно вызвать метод Play. Поскольку AudioGraph является закрытым членом вашего класса AudioEffects, вам понадобится обернуть его в открытый метод, чтобы он был доступен:

public void Play()
{
this._audioGraph.Start();
}

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

public async Task LoadFileIntoGraph(StorageFile audioFile)
{
  CreateAudioFileInputNodeResult audioFileInputResult =
    await this._audioGraph.CreateFileInputNodeAsync(audioFile);
  _fileInputNode = audioFileInputResult.FileInputNode;
  _fileInputNode.AddOutgoingConnection(_deviceOutputNode);
  CreateAndAddEchoEffect();
}

Вы также заметите ссылку на метод CreateAndAddEchoEffect, который мы обсудим в следующем разделе.

Добавление звукового эффекта

В API аудиографа есть четыре встроенных звуковых эффекта: эхо, реверберация, эквалайзер и амплитудный ограничитель (limiter). В данном случае вам нужно добавить эхо к записанному звуку. Добавление этого эффекта требует лишь создания объекта EchoEffectDefition и задания свойств эффекта. После этого вы должны добавить определение эффекта в узел. В нашем примере эффект добавляется в _fileInputNode, который содержит аудиоданные, записанные и сохраненные на диск:

private void CreateAndAddEchoEffect()
{
  EchoEffectDefinition echoEffectDefinition = new EchoEffectDefinition(this._audioGraph);
  echoEffectDefinition.Delay = 100.0f;
  echoEffectDefinition.WetDryMix = 0.7f;
  echoEffectDefinition.Feedback = 0.5f;
  _fileInputNode.EffectDefinitions.Add(echoEffectDefinition);
}

Собираем все воедино

Закончив с классом AudioEffect, вы можете использовать его из UI. Сначала добавьте кнопку на начальную страницу приложения:

<Button Content="Play with Special Effect" Click="btnSpecialEffectPlay_Click" />

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

private async void btnSpecialEffectPlay_Click(object sender, RoutedEventArgs e)
{
  var storageFile = await this._audioRecorder.GetStorageFile(Dispatcher);
  AudioEffects effects = new AudioEffects();
  await effects.InitializeAudioGraph();
  await effects.LoadFileIntoGraph(storageFile);
  effects.Play();
}

Запустите приложение и щелкните Record для записи небольшого аудиоклипа. Чтобы прослушать его в том виде, в каком он был записан, щелкните кнопку Play. Чтобы услышать его с добавленным эхом, щелкните Play with Special Effect.

Заключение

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


Фрэнк Ла-Вине (Frank La Vigne) — идеолог технологий в группе Microsoft Technology and Civic Engagement, где он помогает пользователям применять технологии и формировать соответствующие сообщества. Регулярно ведет блог на FranksWorld.com и имеет канал на YouTube под названием «Frank’s World TV» (youtube.com/FranksWorldTV).

Выражаю благодарность за рецензирование статьи экспертам Дрю Бэтчелор (Drew Batchelor) и Хосе Луису Маннерсу (Jose Luis Manners).