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