Введение
Помимо простого воспроизведения и записи, иногда нужна обработка звукового сигнала на лету — изменение громкости динамически в зависимости от условий, простые эффекты (эквалайзер, эффект эха), или анализ сигнала для визуализации. В статье разберём, как встроиться в поток аудиоданных для их обработки, опираясь на низкоуровневый доступ через QAudioSource/QAudioSink из статьи 253.
Концепция
Qt Multimedia не предоставляет встроенной библиотеки готовых аудиоэффектов (в отличие от некоторых специализированных аудио-фреймворков) — для реальной обработки сигнала приложение само читает сырые PCM-сэмплы через QAudioSource, применяет к ним собственный алгоритм обработки (на уровне массива чисел), и записывает результат через QAudioSink для воспроизведения. Это даёт полный контроль, но требует от разработчика самостоятельной реализации даже базовых эффектов на уровне арифметики над сэмплами.
Пример кода: Аудиоэффекты и обработка
// audioeffectprocessor.h — простой пример: программное усиление громкости и базовый эффект эха
#pragma once
#include <QAudioSource>
#include <QAudioSink>
#include <QAudioFormat>
#include <QMediaDevices>
#include <QByteArray>
#include <QVector>
class AudioEffectProcessor : public QObject
{
Q_OBJECT
public:
AudioEffectProcessor(QObject *parent = nullptr) : QObject(parent)
{
QAudioFormat format;
format.setSampleRate(44100);
format.setChannelCount(1);
format.setSampleFormat(QAudioFormat::Int16);
m_source = std::make_unique<QAudioSource>(QMediaDevices::defaultAudioInput(), format, this);
m_sink = std::make_unique<QAudioSink>(QMediaDevices::defaultAudioOutput(), format, this);
m_inputDevice = m_source->start();
m_outputDevice = m_sink->start();
// Буфер задержки для эффекта эха — половина секунды при 44100 Гц
m_echoBuffer.resize(22050, 0);
connect(m_inputDevice, &QIODevice::readyRead, this, &AudioEffectProcessor::processChunk);
}
void setVolumeGain(double gain) { m_volumeGain = gain; }
void setEchoEnabled(bool enabled) { m_echoEnabled = enabled; }
private slots:
void processChunk()
{
QByteArray rawData = m_inputDevice->readAll();
const int sampleCount = rawData.size() / sizeof(qint16);
auto *samples = reinterpret_cast<qint16 *>(rawData.data());
for (int i = 0; i < sampleCount; ++i) {
double sample = samples[i] * m_volumeGain;
if (m_echoEnabled) {
// Простейший эффект эха: смешиваем текущий сэмпл с задержанным из буфера
const int echoIndex = m_echoPosition % m_echoBuffer.size();
sample += m_echoBuffer[echoIndex] * 0.4; // затухание эха 40%
m_echoBuffer[echoIndex] = static_cast<qint16>(qBound(-32768.0, sample, 32767.0));
m_echoPosition++;
}
// Ограничение (clipping) во избежание искажений при переполнении диапазона
samples[i] = static_cast<qint16>(qBound(-32768.0, sample, 32767.0));
}
m_outputDevice->write(rawData);
}
private:
std::unique_ptr<QAudioSource> m_source;
std::unique_ptr<QAudioSink> m_sink;
QIODevice *m_inputDevice = nullptr;
QIODevice *m_outputDevice = nullptr;
double m_volumeGain = 1.0;
bool m_echoEnabled = false;
QVector<qint16> m_echoBuffer;
int m_echoPosition = 0;
};
Пояснения к коду
processChunk() — центр всей обработки: каждый раз, когда от QAudioSource поступает новая порция сырых данных, они интерпретируются как массив qint16-сэмплов, к каждому применяется усиление громкости (m_volumeGain) и, опционально, эффект эха, после чего результат записывается напрямую в m_outputDevice, связанный с QAudioSink, что немедленно воспроизводит обработанный звук.
Эффект эха реализован через кольцевой буфер (m_echoBuffer) фиксированного размера, соответствующего половине секунды задержки при заданной частоте дискретизации — текущий сэмпл смешивается с сэмплом, записанным в этот же буфер примерно полсекунды назад, создавая классический эффект повторения звука с затуханием. Явное ограничение (qBound) после каждой операции — обязательный шаг для предотвращения переполнения 16-битного диапазона при суммировании усиленного сигнала с эхом, которое без такого ограничения создавало бы резкие, неприятные щелчки искажения.
Подводные камни
- Задержка между записью входного сигнала и воспроизведением обработанного результата (латентность) может быть заметной для пользователя, особенно если размер буфера обработки слишком велик — для приложений, требующих минимальной задержки (живой эффект во время разговора или исполнения музыки), нужно явно настраивать размер внутренних буферов
QAudioSource/QAudioSinkна минимально приемлемое значение, балансируя между задержкой и риском прерываний звука (audio dropouts) при слишком маленьком буфере. - Обработка сэмплов в обработчике
readyRead, выполняющаяся в основном потоке событий Qt, может конкурировать с другой работой основного потока (обновление интерфейса) за процессорное время — для более сложных, вычислительно дорогих эффектов обработку стоит переносить в отдельный поток, чтобы не создавать заметных пользователю задержек в остальном интерфейсе приложения. - Простое арифметическое смешивание сэмплов без правильной нормализации и ограничения диапазона (забытый
qBoundили его эквивалент) приводит к характерным резким щелчкам и треску в звуке при переполнении 16-битного целочисленного диапазона — это одна из самых частых ошибок при первой попытке реализовать собственную обработку звука, и её симптом (треск при громких звуках) почти всегда указывает именно на эту причину. - Жёстко закодированные параметры формата (частота дискретизации, количество каналов) без проверки совместимости с реально доступными устройствами, аналогично подводному камню из статьи 253 — для надёжной работы на разнообразном пользовательском оборудовании нужна явная проверка поддержки выбранного формата перед началом обработки, а не предположение, что любое устройство поддерживает ровно те же параметры, что были использованы при разработке и тестировании.