Мультимедиа

Запись и обработка аудио с микрофона

Введение

Помимо видео с камеры, частая задача — запись звука с микрофона: голосовые заметки, распознавание речи, мониторинг звукового окружения. В статье разберём запись аудио через QAudioSource (низкоуровневый доступ к необработанным аудиоданным) и через QMediaRecorder с QMediaCaptureSession (более высокоуровневый способ записи в файл), а также обсудим, когда нужен каждый из этих подходов.

Концепция

Для простой записи готового звукового файла удобнее QMediaCaptureSession с QAudioInput в качестве источника, аналогично видеозахвату из предыдущей статьи — этот путь автоматически кодирует звук в выбранный формат файла. Для случаев, когда нужен прямой доступ к необработанным аудио-сэмплам (визуализация звука в реальном времени, собственная обработка сигнала, потоковая передача по сети) используется более низкоуровневый QAudioSource, который отдаёт сырые PCM-данные через QIODevice, не сохраняя их автоматически в файл.

Пример кода: Запись и обработка аудио с микрофона

// Высокоуровневая запись через QMediaCaptureSession — простой случай "записать в файл"
#include <QMediaCaptureSession>
#include <QMediaRecorder>
#include <QAudioInput>
#include <QMediaDevices>

class VoiceMemoRecorder : public QObject
{
    Q_OBJECT
public:
    VoiceMemoRecorder(QObject *parent = nullptr) : QObject(parent)
    {
        m_audioInput = new QAudioInput(this);
        m_captureSession.setAudioInput(m_audioInput);
        m_captureSession.setRecorder(&m_recorder);
    }

    void startRecording(const QString &filePath)
    {
        m_recorder.setOutputLocation(QUrl::fromLocalFile(filePath));
        m_recorder.setQuality(QMediaRecorder::HighQuality);
        m_recorder.record();
    }

    void stopRecording() { m_recorder.stop(); }

private:
    QAudioInput *m_audioInput;
    QMediaCaptureSession m_captureSession;
    QMediaRecorder m_recorder;
};
// Низкоуровневый доступ к сырым аудиоданным через QAudioSource — для визуализации звука
#include <QAudioSource>
#include <QAudioFormat>
#include <QMediaDevices>
#include <QIODevice>

class AudioLevelMonitor : public QObject
{
    Q_OBJECT
public:
    AudioLevelMonitor(QObject *parent = nullptr) : QObject(parent)
    {
        QAudioFormat format;
        format.setSampleRate(44100);
        format.setChannelCount(1);
        format.setSampleFormat(QAudioFormat::Int16);

        const QAudioDevice inputDevice = QMediaDevices::defaultAudioInput();
        if (!inputDevice.isFormatSupported(format)) {
            qWarning() << "Запрошенный формат аудио не поддерживается устройством";
        }

        m_audioSource = std::make_unique<QAudioSource>(inputDevice, format, this);
        m_ioDevice = m_audioSource->start(); // start() возвращает QIODevice для чтения данных

        connect(m_ioDevice, &QIODevice::readyRead, this, &AudioLevelMonitor::processAudioData);
    }

private slots:
    void processAudioData()
    {
        const QByteArray data = m_ioDevice->readAll();
        const qint16 *samples = reinterpret_cast<const qint16 *>(data.constData());
        const int sampleCount = data.size() / sizeof(qint16);

        // Вычисление пикового уровня громкости для простой визуализации
        qint16 peak = 0;
        for (int i = 0; i < sampleCount; ++i) {
            peak = qMax(peak, static_cast<qint16>(qAbs(samples[i])));
        }

        const double normalizedLevel = static_cast<double>(peak) / 32768.0; // нормализация в 0.0–1.0
        emit audioLevelChanged(normalizedLevel);
    }

signals:
    void audioLevelChanged(double level);

private:
    std::unique_ptr<QAudioSource> m_audioSource;
    QIODevice *m_ioDevice = nullptr;
};

Пояснения к коду

VoiceMemoRecorder показывает простой высокоуровневый сценарий — QAudioInput подключается к QMediaCaptureSession точно так же, как камера в предыдущей статье, и QMediaRecorder сам заботится о кодировании потока в выбранный файл с заданным качеством, не давая приложению доступа к промежуточным «сырым» данным — это удобно, когда нужен только результат в виде готового файла.

AudioLevelMonitor показывает принципиально другой подход — QAudioSource::start() возвращает QIODevice, из которого приложение само читает PCM-сэмплы по мере их поступления (сигнал readyRead, аналогичный сетевым сокетам из основного цикла статей). processAudioData() интерпретирует полученные байты как массив 16-битных целых чисел (qint16, согласно заданному QAudioFormat::Int16) и вычисляет пиковый уровень громкости — именно такой прямой доступ к сэмплам необходим для построения визуализации уровня звука в реальном времени, которую невозможно реализовать через высокоуровневый QMediaRecorder.

Подводные камни

  • Несовпадение запрошенного QAudioFormat с реально поддерживаемым устройством форматом. Проверка isFormatSupported() в примере — не просто формальность: если устройство не поддерживает запрошенную комбинацию частоты дискретизации, количества каналов и формата сэмплов, поведение QAudioSource без предварительной проверки может быть некорректным или просто не дать никаких данных — для надёжной работы на разном железе стоит либо запрашивать поддерживаемые форматы у устройства явно, либо использовать QAudioFormat, гарантированно широко поддерживаемый (как в примере — 44100 Гц, моно, 16 бит).
  • Интерпретация байтов как qint16 без учёта порядка байт (endianness) на разных платформах. Хотя на большинстве современных десктопных и мобильных платформ используется little-endian порядок, совпадающий с прямым приведением типа в примере, для полной кросс-платформенной корректности (особенно при работе с записанными на одной платформе и обрабатываемыми на другой данными) порядок байт стоит обрабатывать явно, а не полагаться на совпадение нативного порядка байт платформы с ожидаемым форматом данных.
  • Накопление необработанных данных в буфере QIODevice без своевременного чтения, если обработчик readyRead выполняет слишком долгую обработку (например, тяжёлые вычисления БПФ для частотного анализа) — это может привести к нарастающей задержке между реальным звуком и обработанными данными, и для производительной обработки в реальном времени стоит выносить вычислительно интенсивную часть в отдельный поток (основной цикл статей про потоки), оставляя сам readyRead-обработчик максимально быстрым.
  • Отсутствие обработки сценария отсутствия микрофона вообще или отказа пользователя предоставить доступ к нему. QMediaDevices::defaultAudioInput() может вернуть «пустое» устройство, если на машине физически нет микрофона или системные настройки приватности блокируют доступ — код должен явно проверять валидность полученного QAudioDevice перед использованием, а не предполагать его наличие безусловно.