Введение
Помимо видео с камеры, частая задача — запись звука с микрофона: голосовые заметки, распознавание речи, мониторинг звукового окружения. В статье разберём запись аудио через 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перед использованием, а не предполагать его наличие безусловно.