Мультимедиа

Аудиоэффекты и обработка звука в реальном времени

Введение

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