Мультимедиа

Обработка аудиосигнала: эффекты, эквализация, визуализация спектра

Введение

Статья 470 показала доступ к «сырым» PCM-данным захваченного звука — в этой статье применим эти данные для практической обработки сигнала: простой эффект усиления/затухания, базовая эквализация через цифровую фильтрацию, и визуализация спектра через быстрое преобразование Фурье (FFT), часто используемая для анимированных индикаторов уровня звука в музыкальных плеерах.

Концепция

Прямой доступ к PCM-сэмплам (статья 470) даёт возможность математически модифицировать значение каждого сэмпла перед его дальнейшим использованием (воспроизведением или записью) — простейший эффект, изменение громкости, достигается простым умножением каждого сэмпла на коэффициент усиления. Эквализация (усиление/подавление определённых частотных диапазонов) требует цифровой фильтрации сигнала, типично реализуемой через свёртку с импульсной характеристикой фильтра или через более сложные IIR/FIR-фильтры. Визуализация спектра требует преобразования сигнала из временной области (последовательность амплитуд сэмплов во времени) в частотную область (распределение энергии сигнала по разным частотам) через быстрое преобразование Фурье — на практике для C++ обычно используется готовая библиотека (например, KissFFT или FFTW) вместо самостоятельной реализации алгоритма FFT.

Пример кода

// Простейший эффект — программное изменение громкости через умножение сэмплов
void applyGain(qint16 *samples, int sampleCount, float gainFactor)
{
    for (int i = 0; i < sampleCount; ++i) {
        int amplified = static_cast<int>(samples[i] * gainFactor);
        // Явное ограничение (clamping) — без него переполнение типа qint16 дало бы
        // искажённый, "обёрнутый" через переполнение результат вместо простого ограничения сверху
        samples[i] = static_cast<qint16>(std::clamp(amplified, -32768, 32767));
    }
}
// Простой однополюсный low-pass фильтр (концептуальный пример эквализации) —
// подавление высоких частот, оставляя преимущественно низкие
class SimpleLowPassFilter
{
public:
    explicit SimpleLowPassFilter(float alpha) : m_alpha(alpha) {}

    qint16 process(qint16 inputSample)
    {
        // alpha близко к 1.0 — слабая фильтрация (почти исходный сигнал)
        // alpha близко к 0.0 — сильная фильтрация (медленно "сглаженный" сигнал)
        m_previousOutput = m_alpha * inputSample + (1.0f - m_alpha) * m_previousOutput;
        return static_cast<qint16>(m_previousOutput);
    }

private:
    float m_alpha;
    float m_previousOutput = 0.0f;
}; // ^ каждый выходной сэмпл зависит от текущего входного И предыдущего выходного значения
// Концептуальная визуализация спектра через FFT (использование внешней библиотеки, например KissFFT)
#include "kiss_fft.h"

QVector<float> computeSpectrum(const qint16 *samples, int sampleCount)
{
    kiss_fft_cfg cfg = kiss_fft_alloc(sampleCount, 0, nullptr, nullptr);
    std::vector<kiss_fft_cpx> input(sampleCount), output(sampleCount);

    for (int i = 0; i < sampleCount; ++i) {
        input[i].r = samples[i] / 32768.0f; // нормализация в диапазон [-1.0, 1.0]
        input[i].i = 0;
    }

    kiss_fft(cfg, input.data(), output.data());

    QVector<float> magnitudes;
    for (int i = 0; i < sampleCount / 2; ++i) { // только первая половина — вторая симметрична для реального сигнала
        float magnitude = std::sqrt(output[i].r * output[i].r + output[i].i * output[i].i);
        magnitudes.append(magnitude); // каждое значение соответствует энергии в конкретном частотном диапазоне
    }

    kiss_fft_free(cfg);
    return magnitudes;
}

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

applyGain показывает простейшую, но практически значимую деталь — явное ограничение (std::clamp) результата умножения перед обратным приведением к qint16, без которого превышение допустимого диапазона значений привело бы к переполнению целочисленного типа, дающему резкий, слышимый щелчок или искажение звука вместо ожидаемого «мягкого» ограничения громкости на максимальном уровне. SimpleLowPassFilter демонстрирует базовый принцип цифровой фильтрации через рекуррентную формулу — каждый выходной сэмпл представляет собой взвешенную комбинацию текущего входного значения и предыдущего выходного, что эффективно «сглаживает» быстрые изменения сигнала (высокие частоты), оставляя медленные изменения (низкие частоты) относительно нетронутыми. computeSpectrum показывает использование внешней FFT-библиотеки для преобразования временного сигнала в частотный спектр — взятие только первой половины результата (sampleCount / 2) использует математическое свойство преобразования Фурье для вещественного (не комплексного) входного сигнала, для которого вторая половина результата всегда симметрична первой и не несёт дополнительной информации.

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

  • Применение эффектов обработки непосредственно к данным, предназначенным одновременно и для воспроизведения, и для записи в файл, без явного разделения этих двух путей — если нужен разный результат обработки для прослушивания (например, с применённым эквалайзером) и для сохранения в файл (без изменений, оригинальный сигнал), необходимо явно поддерживать раздельные копии или пути обработки данных, а не модифицировать единственный, общий буфер сэмплов «на месте», что сделало бы последующее сохранение исходного, неизменённого сигнала уже невозможным.
  • Игнорирование вычислительной стоимости обработки сигнала в реальном времени — обработка (фильтрация, FFT) каждого поступающего блока сэмплов должна укладываться в доступное время до прихода следующего блока данных (определяемое размером буфера и частотой дискретизации), и недостаточно эффективная реализация обработки может привести к отставанию обработки от реального темпа поступления данных, что проявляется как заикания, пропуски или нарастающая задержка воспроизведения/визуализации.
  • Применение FFT к блоку сэмплов без оконной функции (windowing), что приводит к так называемому эффекту «спектральной утечки» (spectral leakage) — резкие границы блока данных (без специальной, плавно убывающей к краям оконной функции, как Hamming или Hanning window) интерпретируются преобразованием Фурье как дополнительные, ложные высокочастотные составляющие, искажающие реальную картину спектра сигнала; для качественной визуализации спектра входной блок сэмплов обычно умножается на подходящую оконную функцию перед применением FFT.
  • Самостоятельная, «наивная» реализация сложных алгоритмов цифровой обработки сигнала (полноценного многополосного эквалайзера, продвинутых эффектов) вместо использования существующих, тщательно протестированных библиотек цифровой обработки сигналов — корректная, численно устойчивая реализация таких алгоритмов требует специализированных знаний в области цифровой обработки сигналов (выбор и проектирование коэффициентов фильтра, борьба с числовой нестабильностью), и для серьёзных, продвинутых задач обработки звука использование готовых, проверенных библиотек значительно надёжнее самостоятельной реализации с нуля.