Вступление

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

Немного теории

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

При обработке сигналов выборка - это преобразование непрерывного сигнала в дискретный. Типичный пример - преобразование звуковой волны (непрерывный сигнал) в последовательность отсчетов (сигнал с дискретным временем). Образец - это значение или набор значений в определенный момент времени и / или в пространстве.

Скорость, с которой берутся эти образцы, называется частотой дискретизации. Для записи звука обычно используется 44100 Гц, что означает 44 100 отсчетов аналогового сигнала в секунду. Самый распространенный метод цифрового представления аналоговых отсчетов называется Импульсно-кодовая модуляция или PCM. Наконец, я должен упомянуть, что наиболее распространенная битовая глубина PCM - это короткое со знаком или 16 бит.

Каждый телефон Android имеет различное оборудование для захвата и воспроизведения звука, а некоторые из них позволяют использовать расширенные режимы, такие как захват стереозвука. Если вас интересует совместимость, вам будет приятно узнать, что все телефоны Android гарантированно поддерживают захват одного канала звука с частотой 44100 Гц в 16-битном кодировании PCM. Вы можете найти более подробную информацию на справочной странице AudioFormat.

Запись сырого звука

Теперь, когда мы рассмотрели основы, пришло время написать код. Запись сырых аудиоданных осуществляется с помощью объекта AudioRecord. Для его настройки требуются источник звука, конфигурация канала, кодирование и размер буфера. Размер буфера выражается в байтах и ​​представляет количество байтов, которое мы можем взять за раз. Существует удобный метод под названием getMinBufferSize (), который может рассчитать его для вас в зависимости от предоставленной конфигурации и оборудования вашего телефона.

int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE,
        AudioFormat.CHANNEL_IN_MONO,
        AudioFormat.ENCODING_PCM_16BIT);
AudioRecord record = new AudioRecord(MediaRecorder.AudioSource.DEFAULT,
        44100,
        AudioFormat.CHANNEL_IN_MONO,
        AudioFormat.ENCODING_PCM_16BIT,
        bufferSize);

На моем Nexus 5 размер буфера составляет 3584 байта. Это означает, что AudioRecord будет выдавать мне 1792 сэмпла (или около 41 мс аудио) за раз. Я должен отметить, что есть также построитель AudioRecord, который вы можете использовать, если конструктор AudioRecord не соответствует вашим потребностям. Фактически, за кулисами конструктор использует конструктор. Вот как настроить тот же экземпляр, но на этот раз с помощью построителя:

AudioRecord record = new AudioRecord.Builder(
        .setAudioSource(MediaRecorder.AudioSource.DEFAULT)
        .setAudioFormat(new AudioFormat.Builder()
                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                .setSampleRate(SAMPLE_RATE)
                .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
                .build())
        .setBufferSizeInBytes(bufferSize)
        .build();

Теперь, когда мы создали его, мы можем начать запись. Получение необработанных аудиосэмплов осуществляется путем опроса. Как и любой непрерывный ввод-вывод, вы должны использовать отдельный выделенный поток. Перед циклом вы должны сигнализировать AudioRecord, вызывая startRecording (). Об окончании записи сигнализирует вызов stop (). Имейте в виду, что AudioRecord должен освободить некоторые собственные объекты, поэтому вы должны в какой-то момент освободить его.

final int SAMPLE_RATE = 44100; // The sampling rate
boolean mShouldContinue; // Indicates if recording / playback should stop
void recordAudio() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
            // buffer size in bytes
            int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE,
                    AudioFormat.CHANNEL_IN_MONO,
                    AudioFormat.ENCODING_PCM_16BIT);
            if (bufferSize == AudioRecord.ERROR || bufferSize == AudioRecord.ERROR_BAD_VALUE) {
                bufferSize = SAMPLE_RATE * 2;
            }
            short[] audioBuffer = new short[bufferSize / 2];
            AudioRecord record = new AudioRecord(MediaRecorder.AudioSource.DEFAULT,
                    SAMPLE_RATE,
                    AudioFormat.CHANNEL_IN_MONO,
                    AudioFormat.ENCODING_PCM_16BIT,
                    bufferSize);
            if (record.getState() != AudioRecord.STATE_INITIALIZED) {
                Log.e(LOG_TAG, "Audio Record can't initialize!");
                return;
            }
            record.startRecording();
            Log.v(LOG_TAG, "Start recording");
            long shortsRead = 0;
            while (mShouldContinue) {
                int numberOfShort = record.read(audioBuffer, 0, audioBuffer.length);
                shortsRead += numberOfShort;
                // Do something with the audioBuffer
            }
            record.stop();
            record.release();
            Log.v(LOG_TAG, String.format("Recording stopped. Samples read: %d", shortsRead));
        }
    }).start();
}

Каждый раз, когда мы читаем аудиоданные, система блокирует нас, пока не наберет достаточно аудиосэмплов для заполнения буфера. В моем случае он блокирует поток примерно на 41 мс. Что бы вы ни делали в цикле опроса, вы должны сделать это менее чем за эти 41 мс, иначе вы можете пропустить некоторые образцы. Это очень похоже на onDraw (). Избегайте ввода-вывода в этой ветке аудиозаписи. Если вам нужно сохранить аудиоданные, лучше всего использовать классический шаблон «Производитель - Потребитель».

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

Воспроизведение сырого звука очень похоже. Для этого нам понадобится объект AudioTrack. Нам снова нужен размер буфера, и для этого есть похожий удобный метод getMinBufferSize (). AudioTrack настраивается так же, как AudioRecord, но на этот раз мы предоставляем тип выходного потока и конфигурацию канала вместо ввода. Также нам нужно решить, как мы будем передавать семплы в AudioTrack: Static или Stream. Статический означает, что весь аудиофайл загружается в память и может воспроизводиться несколько раз без перезагрузки. Это может быть полезно для небольших аудиофайлов, которые часто воспроизводятся. Для больших файлов потоковая передача более эффективна.

int mBufferSize = AudioTrack.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_OUT_MONO,
        AudioFormat.ENCODING_PCM_16BIT);
if (mBufferSize == AudioTrack.ERROR || mBufferSize == AudioTrack.ERROR_BAD_VALUE) {
	// For some readon we couldn't obtain a buffer size
    mBufferSize = SAMPLE_RATE * CHANNELS * 2;
}
AudioTrack mAudioTrack = new AudioTrack(
        AudioManager.STREAM_MUSIC,
        SAMPLE_RATE,
        AudioFormat.CHANNEL_OUT_MONO,
        AudioFormat.ENCODING_PCM_16BIT,
        mBufferSize,
        AudioTrack.MODE_STREAM);

Опять же, как и в случае с AudioRecord, для этого есть построитель. На этот раз он дает несколько больший контроль над произведенной звуковой дорожкой.

AudioTrack audioTrack = new AudioTrack.Builder()
        .setAudioAttributes(new AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_MEDIA)
                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                .build())
        .setAudioFormat(new AudioFormat.Builder()
                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                .setSampleRate(SAMPLE_RATE)
                .setChannelMask(AudioFormat.CHANNEL_OUT_MONO).build())
        .setBufferSizeInBytes(bufferSize)
        .setTransferMode(AudioTrack.MODE_STREAM)
        .build();

Последний шаг - подготовка AudioTrack. Я собираюсь показать вам, как можно транслировать образцы звука в цикле. Использование ShortBuffer удобно, потому что он отслеживает позицию и имеет методы для получения заданного количества элементов.

ShortBuffer mSamples; // the samples to play
int mNumSamples; // number of samples to play
void playAudio() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            int bufferSize = AudioTrack.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_OUT_MONO,
                    AudioFormat.ENCODING_PCM_16BIT);
            if (bufferSize == AudioTrack.ERROR || bufferSize == AudioTrack.ERROR_BAD_VALUE) {
                bufferSize = SAMPLE_RATE * 2;
            }
            AudioTrack audioTrack = new AudioTrack(
                    AudioManager.STREAM_MUSIC,
                    SAMPLE_RATE,
                    AudioFormat.CHANNEL_OUT_MONO,
                    AudioFormat.ENCODING_PCM_16BIT,
                    bufferSize,
                    AudioTrack.MODE_STREAM);
            audioTrack.play();
            Log.v(LOG_TAG, "Audio streaming started");
            short[] buffer = new short[bufferSize];
            mSamples.rewind();
            int limit = mNumSamples;
            int totalWritten = 0;
            while (mSamples.position() < limit && mShouldContinue) {
                int numSamplesLeft = limit - mSamples.position();
                int samplesToWrite;
                if (numSamplesLeft >= buffer.length) {
                    mSamples.get(buffer);
                    samplesToWrite = buffer.length;
                } else {
                    for (int i = numSamplesLeft; i < buffer.length; i++) {
                        buffer[i] = 0;
                    }
                    mSamples.get(buffer, 0, numSamplesLeft);
                    samplesToWrite = numSamplesLeft;
                }
                totalWritten += samplesToWrite;
                audioTrack.write(buffer, 0, samplesToWrite);
            }
            if (!mShouldContinue) {
                audioTrack.release();
            }
            Log.v(LOG_TAG, "Audio streaming finished. Samples written: " + totalWritten);
        }
    }).start();
}

Как вы видите здесь, процесс аналогичен тому, что мы делали при записи звука. Вы снова должны вызвать play (), чтобы сигнализировать AudioTrack о начале воспроизведения. Однако есть одно отличие: метод записи аудиосэмплов имеет только неблокирующую версию до уровня API 23. Это означает, что при вызове функции write () фактические сэмплы помещаются в очередь для последующего воспроизведения. Это создает одну сложность: когда я должен выпустить звуковую дорожку? AudioTrack дает вам возможность разместить маркер в заданное время и уведомить вас, когда он будет достигнут. Также он может отправлять периодические уведомления через определенные промежутки времени. Вы можете разместить маркер на последнем семпле, и AudioTrack сообщит вам, когда он закончил воспроизведение, чтобы вы могли его отпустить.

audioTrack.setPlaybackPositionUpdateListener(new AudioTrack.OnPlaybackPositionUpdateListener() {
    @Override
    public void onPeriodicNotification(AudioTrack track) {
        if (track.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
            int currentFrame = track.getPlaybackHeadPosition();
            int elapsedSeconds = (currentFrame * 1000) / SAMPLE_RATE;
        }
    }
    @Override
    public void onMarkerReached(AudioTrack track) {
        Log.v(LOG_TAG, "Audio file end reached");
        track.release();
    }
});
audioTrack.setPositionNotificationPeriod(SAMPLE_RATE / 30); // 30 times per second
audioTrack.setNotificationMarkerPosition(mNumSamples);

Обратите внимание, что вызов release () неявно останавливает воспроизведение, поэтому, если вы сделаете это слишком рано, воспроизведение внезапно остановится.

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

  • Pause () временно приостановит воспроизведение. Аудиоданные, которые не будут отброшены;
  • GetPlayState () предоставит вам состояние воспроизведения (воспроизведение / пауза / остановка);
  • GetPlaybackHeadPosition () даст вам количество воспроизведенных кадров;
  • Flush () отбрасывает все аудиоданные, которые поставлены в очередь на воспроизведение, но еще не воспроизведены;

Визуализация сырых данных

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

Я решил разделить его на два режима из-за разницы в контексте. Во время записи я рисую отснятые сэмплы непрерывной линией. Поскольку аудиоданные поступают короткими пакетами, я храню 6 пакетов аудиоданных (всего около 240 мс аудиоданных на моем телефоне). Это позволяет мне визуализировать больше данных, а также дает приятный эффект затухания старых данных.

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

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

Эпилог

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

Изначально этот пост был опубликован в блоге New Venture Software.