Аудиозапись с микрофона

122

Класс CaptureSource является простой оболочкой, предоставляющей минимальный набор базовых функций. Для решения более сложных задач необходимо познакомиться с конвейером обработки потока и написать собственный приемник звука или видео.

Приемник звука или видео — это пользовательский класс, обрабатывающий "сырые" данные, полученные от аудио- или видеоустройства. Обработка выполняется по одной порции за раз. Пользовательский класс приемника видео должен быть производным от базового класса VideoSink, а приемника звука — от AudioSink.

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

Запись и воспроизведение аудиоклипа

Можно ли решить эту простую задачу с помощью класса CaptureSource? Оказывается, нельзя. Он жестко ограничен воспроизведением реального видео и захватом статических снимков. Для решения данной задачи вы должны будете применить довольно неуклюжий подход — обработать исходные данные самостоятельно.

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

public class MemoryStreamAudioSink : AudioSink
{
   ...

В теле MemoryStreamAudioSink все аудиоданные записываются в объект MemoryStream. Затем поток становится доступным для кода приложения посредством свойства AudioData:

private MemoryStream stream;
public MemoryStream AudioData
{
            get
            {
                return stream;
            }
}

Аудиоприемник должен предоставить реализацию свойства AudioData и переопределить несколько абстрактных членов. Свойство AudioFormat отслеживает формат исходных аудиоданных. Метод OnFormatChange() реагирует на изменение формата:

private AudioFormat audioFormat;
public AudioFormat AudioFormat
{
            get
            {
                return audioFormat;
            }
}

protected override void OnFormatChange(AudioFormat audioFormat)
{
            if (this.audioFormat == null)
            {
                this.audioFormat = audioFormat;
            }
            else
            {
                // Нельзя разрешить изменение, влияющее на текущую запись
                throw new InvalidOperationException();
            }
}

Необходимо переопределить метод OnCaptureStarted(), автоматически вызываемый перед началом записи. В приемнике MemoryStreamAudioSink этот метод всего лишь инициирует новый поток MemoryStream:

protected override void OnCaptureStarted()
{
            // Подготовка потока в памяти для хранения аудиоданных
            stream = new MemoryStream();
}

Очень важен метод OnSamples(), который запускается периодически в процессе записи каждый раз при получении нового сэмпла аудиоданных. Сэмпл передается в код как массив типа byte. В приемнике MemoryStreamAudioSink он всего лишь записывается в память:

protected override void OnSamples(long sampleTime, long sampleDuration, byte[] sampleData)
{
            // При каждом получении сэмпла он записывается в поток 
            // в памяти (можно также определить поток в сети) 
            stream.Write(sampleData, 0, sampleData.Length);
}

При остановке записи автоматически вызывается метод OnCaptureStopped(). В этот момент приемник MemoryStreamAudioSink должен выполнить некоторые операции. Внутренний поток содержит исходные волновые данные. Для их преобразования в формат WAV (другие приложения и проигрыватели не принимают исходные данные, но могут работать с данными формата WAV) нужно добавить заголовок в начало файла.

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

Для решения этой проблемы метод OnCaptureStopped(), приведенный ниже, сначала создает и записывает правильный заголовок и только после этого копирует все данные из одного потока в другой. Затем код метода заменяет старый поток (сборщик мусора освобождает его). Данная методика вполне работоспособна, но требует большого объема памяти для хранения двух копий записанных аудиоданных. Лучшая, но более сложная методика состоит в том, чтобы оставить в потоке пространство для заголовка и добавить его позже:

protected override void OnCaptureStopped()
{
            // Генерация заголовка
            byte[] wavFileHeader = WavFileHelper.GetWavFileHeader(AudioData.Length,
              AudioFormat);

            // Запись заголовка в новый поток
            MemoryStream wavStream = new MemoryStream();
            wavStream.Write(wavFileHeader, 0, wavFileHeader.Length);

            // Запись данных порциями по 4096 байт за раз
            byte[] buffer = new byte[4096];
            int read = 0;
            stream.Seek(0, SeekOrigin.Begin);
            while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
            {
                wavStream.Write(buffer, 0, read);
            }

            // Замена старого потока новым
            stream = wavStream;
            stream.Seek(0, SeekOrigin.Begin);
}

В Silverlight не встроена поддержка создания и записи заголовка файла WAV. В приведенном выше коде используется вспомогательный класс WavFileHelper для записи заголовков, созданный Майклом Толти (Mike Taulty):

// Вспомогательный класс для создания заголовка файла WAV
public static class WavFileHelper
{
        public static byte[] GetWavFileHeader(long audioLength,
          AudioFormat audioFormat)
        {
            MemoryStream stream = new MemoryStream(44);
            stream.Write(new byte[] { 0x52, 0x49, 0x46, 0x46 }, 0, 4); 
            stream.Write(BitConverter.GetBytes((UInt32)(audioLength + 44 - 8)), 0, 4);
            stream.Write(new byte[] { 0x57, 0x41, 0x56, 0x45 }, 0, 4);
            stream.Write(new byte[] { 0x66, 0x6D, 0x74, 0x20 }, 0, 4);
            stream.Write(BitConverter.GetBytes((UInt32)16), 0, 4);
            stream.Write(BitConverter.GetBytes((UInt16)1), 0, 2);
            stream.Write(BitConverter.GetBytes((UInt16)audioFormat.Channels), 0, 2);
            stream.Write(BitConverter.GetBytes((UInt32)audioFormat.SamplesPerSecond), 0, 4);
            stream.Write(BitConverter.GetBytes((UInt32)
                ((audioFormat.SamplesPerSecond *
                audioFormat.Channels * audioFormat.BitsPerSample) / 8)), 0, 4);
            stream.Write(BitConverter.GetBytes((UInt16)
                ((audioFormat.Channels * audioFormat.BitsPerSample) / 8)), 0, 2);  
            stream.Write(BitConverter.GetBytes((UInt16)audioFormat.BitsPerSample), 0, 2); 
            stream.Write(new byte[] { 0x64, 0x61, 0x74, 0x61 }, 0, 4); 
            stream.Write(BitConverter.GetBytes((UInt32)audioLength), 0, 4);

            return (stream.GetBuffer());
        }
}

Теперь можно применить класс MemoryStreamAudioSink в примере, показанном на рисунке выше:

<StackPanel Margin="10">
        <Button x:Name="cmdStartRecord" Margin="3" Padding="5" Click="cmdStartRecord_Click" Content="Начать запись"></Button>
        <Button x:Name="cmdStopRecord" Margin="3" Padding="5" Click="cmdStopRecord_Click" Content="Остановить" IsEnabled="False"></Button>
        <Button x:Name="cmdPlayClip" Margin="3" Padding="5" Click="cmdPlayClip_Click" Content="Воспроизвести" IsEnabled="False"></Button>
        <TextBlock x:Name="lblStatus" Margin="3"></TextBlock>
        <MediaElement x:Name="media"></MediaElement>
</StackPanel>
private MemoryStreamAudioSink audioSink;
private CaptureSource capture;

private void cmdStartRecord_Click(object sender, RoutedEventArgs e)
{
            if (CaptureDeviceConfiguration.AllowedDeviceAccess || CaptureDeviceConfiguration.RequestDeviceAccess())
            {
                if (audioSink == null)
                {
                    capture = new CaptureSource();
                    capture.AudioCaptureDevice = CaptureDeviceConfiguration.GetDefaultAudioCaptureDevice();

                    audioSink = new MemoryStreamAudioSink();
                    audioSink.CaptureSource = capture;
                }
                else
                {
                    audioSink.CaptureSource.Stop();
                }

                audioSink.CaptureSource.Start();
                cmdStartRecord.IsEnabled = false;

                // Добавление задержки, чтобы гарантировать правильную инициализацию (иначе, 
                // если пользователь немедленно остановит запись, будет сгенерировано исключение)
                System.Threading.Thread.Sleep(TimeSpan.FromSeconds(0.5));
                cmdStopRecord.IsEnabled = true;

                lblStatus.Text = "Выполняется запись ...";
            }
}

private void cmdStopRecord_Click(object sender, RoutedEventArgs args)
{
            audioSink.CaptureSource.Stop();

            cmdPlayClip.IsEnabled = true;
            cmdStopRecord.IsEnabled = false;
            cmdStartRecord.IsEnabled = true;
            lblStatus.Text = "Запись завершена. Клип можно воспроизвести.";
}

private void cmdPlayClip_Click(object sender, RoutedEventArgs args)
{
            // Проиграть записанную аудиозапись
            WaveMSS.WaveMediaStreamSource wavMss = new WaveMSS.WaveMediaStreamSource(audioSink.AudioData);
            media.SetSource(wavMss);
}

В данном коде для проигрывания аудиозаписи используется класс WaveMediaStreamSource, который определен в сборке WAVMss.dll.

Данный пример лишь поверхностно иллюстрирует использование исходных аудио- и видеоданных в приложении Silverlight, однако в реальных приложениях возникают более сложные задачи. Ключевая проблема состоит в том, что "сырые" данные WAV занимают огромный объем памяти. Если нужно хранить много аудиофайлов или часто передавать аудиоклипы по сети, использование формата WAV не практично.

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

Пройди тесты
Лучший чат для C# программистов