Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Введение в объекты обработки звука
Исходный код можно скачать по ссылке.
Компонент XAudio2 в DirectX представляет собой нечто гораздо большее, чем просто средство для воспроизведения звуков и музыки в приложениях Windows 8. Я рассматриваю его скорее как универсальный конструктор для обработки звука. Благодаря использованию нескольких экземпляров IXAudio2SourceVoice и IXAudio2SubmixVoice программисты могут распределять звук между раздельными конвейерами для собственной обработки, а затем объединять их в конечный IXAudio2MasteringVoice.
Как я продемонстрировал в прошлой статье из этой рубрики (msdn.microsoft.com/magazine/dn198248), XAudio2 позволяет применять стандартные фильтры звука к голосам-источникам и к субмиксовым голосам (submix voices). Эти фильтры ослабляют диапазоны частот и соответственно изменяют гармоническое содержание и тембр звуков.
Но куда более мощным является механизм, предоставляющий доступ к аудиопотокам, передаваемым через голоса. Вы можете просто анализировать этот аудиопоток или модифицировать его.
Данный механизм известен под неформальным названием «аудиоэффект» (audio effect). С более формальной точки зрения, он включает создание Audio Processing Object (APO), также известного как кросс-платформенный APO, или XAPO, если его можно использовать в приложениях не только Windows, но и Xbox 360.
XAudio2 включает два предопределенных APO для распространенных задач. Функция XAudio2CreateVolumeMeter создает APO, позволяющий программе динамически получать пиковую амплитуду аудиопотока через интервалы, подходящие приложению. Функция XAudio2CreateReverb создает APO, применяющий эхо или реверберацию к какому-либо голосу на основе 23 параметров периода выполнения, и под периодом выполнения в этом контексте я имею в виду параметры, которые можно динамически изменять, пока APO активно обрабатывает звук. Кроме того, библиотека XAPOFX предоставляет эффекты эха и реверберации, а также ограничитель громкости (volume limiter) и четырехполосный эквалайзер.
APO реализует интерфейс IXAPO, но APO с параметрами периода выполнения — интерфейс IXAPOParameters. Однако более простой подход к созданию собственных APO включает наследование от классов CXAPOBase и CXAPOParametersBase, которые реализуют эти интерфейсы и берут на себя большую часть работы.
В этой статье я выбрал именно такую стратегию: наследование от этих двух классов. Кроме заголовочных файлов и важных библиотек, о которых я рассказывал в предыдущих статьях, в проекты, реализующие APO, нужно включать ссылку на заголовочный файл xapobase.h и библиотеку импорта xapobase.lib.
Использование объектов обработки звука
Прежде чем обсуждать внутреннее устройство APO-класса, позвольте мне показать, как применяются эффекты к голосам XAudio2. Проект SimpleEffectDemo в пакете исходного кода для этой статьи позволяет загрузить файл из вашей библиотеки музыки в Windows 8 и воспроизвести его. Код в этом проекте аналогичен продемонстрированному в предыдущих статьях: файл загружается и декодируется с помощью классов Media Foundation, а проигрывается средствами XAudio2. SimpleEffectDemo создает только два голоса XAudio2: голос-источник для генерации звука и обязательный мастеринговый голос, который отправляет аудиоданные в звуковое оборудование.
SimpleEffectDemo также содержит два не параметризованных производных от CXAPOBase класса: OneSecondTremoloEffect (применяет тремоло, или колебание громкости, основанное на простой модуляции амплитуды) и OneSecondEchoEffect. На рис. 1 показана программа с загруженным музыкальным файлом. Каждый из двух эффектов включается и отключается кнопкой ToggleButton. На этом экранном снимке видно, что в данный момент включен эффект эха, а эффект тремоло отключен.
Рис. 1. Программа SimpleEffectDemo с двумя аудиоэффектами
APO могут применяться к любому типу голоса XAudio2. При применении к голосам-источникам или субмиксовым голосам обработка звука осуществляется после встроенных фильтров, заданных вами с помощью SetFilterParameters, но до фильтров, применяемых к аудиоданным, которые передаются другим голосам с использованием SetOutputFilterParameters.
Я решил применить эти два эффекта к мастеринговому голосу. Код, который создает экземпляры этих эффектов и подключает их к мастеринговому голосу, показан на рис. 2. На каждый эффект нужно ссылаться с помощью XAUDIO2_EFFECT_DESCRIPTOR. Если у вас больше одного эффекта (как в этом случае), вы используете массив таких структур. Затем на эту структуру (или массив) ссылается структура XAUDIO2_EFFECT_CHAIN, передаваемая методу SetEffectChain, который поддерживается всеми голосами XAudio2. Порядок эффектов важен: в данном случае эффект эха получит аудиопоток, к которому уже применен эффект тремоло.
Рис. 2. Применение двух аудиоэффектов к мастеринговому голосу
// Создаем эффект тремоло
ComPtr<OneSecondTremoloEffect> pTremoloEffect = new OneSecondTremoloEffect();
// Создаем эффект эха
ComPtr<OneSecondEchoEffect> pEchoEffect = new OneSecondEchoEffect();
// Ссылаемся на эти эффекты с помощью массива дескрипторов
std::array<XAUDIO2_EFFECT_DESCRIPTOR, 2> effectDescriptors;
effectDescriptors[0].pEffect = pTremoloEffect.Get();
effectDescriptors[0].InitialState = tremoloToggle->IsChecked->Value;
effectDescriptors[0].OutputChannels = 2;
effectDescriptors[1].pEffect = pEchoEffect.Get();
effectDescriptors[1].InitialState = echoToggle->IsChecked->Value;
effectDescriptors[1].OutputChannels = 2;
// Ссылаемся на этот массив с помощью цепочки эффектов
XAUDIO2_EFFECT_CHAIN effectChain;
effectChain.EffectCount = effectDescriptors.size();
effectChain.pEffectDescriptors = effectDescriptors.data();
hresult = pMasteringVoice->SetEffectChain(&effectChain);
if (FAILED(hresult))
throw ref new COMException(hresult, "pMasteringVoice->SetEffectChain failure");
После вызова SetEffectChain программа больше не должна ссылаться на экземпляры эффектов. XAudio2 уже добавил ссылку на эти экземпляры, и программа может освободить свои копии — либо ComPtr сделает это за вас. С этого момента эффекты идентифицируются по индексам: в нашем случае 0 обозначает эффект тремоло, а 1 — эффект эха. Возможно, вы предпочтете использовать перечисление для этих констант.
Для обоих эффектов я присваиваю полю InitialState структуры XAUDIO2_EFFECT_DESCRIPTOR состояние «checked» кнопки ToggleButton. Это позволяет управлять тем, будет ли эффект изначально включен или отключен. Впоследствии эффекты включаются и отключаются обработчиками Checked и Unchecked двух элементов управления ToggleButton (рис. 3).
Рис. 3. Включение и отключение аудиоэффектов
void MainPage::OnTremoloToggleChecked(Object^ sender,
RoutedEventArgs^ args)
{
EnableDisableEffect(safe_cast<ToggleButton^>(sender), 0);
}
void MainPage::OnEchoToggleChecked(Object^ sender,
RoutedEventArgs^ args)
{
EnableDisableEffect(safe_cast<ToggleButton^>(sender), 1);
}
void MainPage::EnableDisableEffect(ToggleButton^ toggle, int index)
{
HRESULT hresult = toggle->IsChecked->Value ?
pMasteringVoice->EnableEffect(index) :
pMasteringVoice->DisableEffect(index);
if (FAILED(hresult))
throw ref new COMException(hresult, "pMasteringVoice->Enable/DisableEffect " +
index.ToString());
}
Создание экземпляров и инициализация
И OneSecondTremoloEffect, и OneSecondEchoEffect наследуют от CXAPOBase. Возможно, первая головоломка, с которой вы столкнетесь при наследовании от этого класса, окажется вопрос: как работать с конструктором CXAPOBase? Этому конструктору надо передать указатель на инициализированную структуру XAPO_REGISTRATION_PROPERTIES, но как ее инициализировать? C++ требует, чтобы конструктор базового класса выполнялся до того, как начнет выполняться любой код в производном классе.
Это создает слегка затруднительное положение, из которого вы можете выйти определением и инициализацией структуры как глобальной переменной или статического поля, либо выполнением этих операций в статическом методе. Я предпочел в данном случае подход со статическим полем, как видно в заголовочном файле OneSecondTremoloEffect.h на рис. 4.
Рис. 4. Заголовочный файл OneSecondTremoloEffect.h
#pragma once
class OneSecondTremoloEffect sealed : public CXAPOBase
{
private:
static const XAPO_REGISTRATION_PROPERTIES RegistrationProps;
WAVEFORMATEX waveFormat;
int tremoloIndex;
public:
OneSecondTremoloEffect() : CXAPOBase(&RegistrationProps),
tremoloIndex(0)
{
}
protected:
virtual HRESULT __stdcall LockForProcess(
UINT32 inpParamCount,
const XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS *pInpParams,
UINT32 outParamCount,
const XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS *pOutParam) override;
virtual void __stdcall Process(
UINT32 inpParameterCount,
const XAPO_PROCESS_BUFFER_PARAMETERS *pInpParams,
UINT32 outParamCount,
XAPO_PROCESS_BUFFER_PARAMETERS *pOutParams,
BOOL isEnabled) override;
};
class __declspec(uuid("6FB2EBA3-7DCB-4ADF-9335-686782C49911"))
OneSecondTremoloEffect;
Поле RegistrationProperties инициализируется в файле кода (об этом чуть позже). Указатель на него передается в конструктор CXAPOBase. Очень часто в производном от CXAPOBase классе также определяется поле типа WAVEFORMATEX (как в нашем примере) или WAVEFORMATEXTENSIBLE (в общем случае) для сохранения формата волнового сигнала аудиопотока, пропускаемого через эффект.
Обратите внимание и на __declspec (спецификатор объявления) в конце файла, который сопоставляет класс OneSecondTremoloEffect с GUID. Вы можете сгенерировать GUID для своих классов эффектов с помощью Create GUID в меню Tools в Visual Studio.
Производный от CXAPOBase класс должен переопределять метод Process и обычно также переопределяет метод LockForProcess. Метод LockForProcess позволяет APO выполнять инициализацию на основе конкретного формата звука, включая частоту дискретизации, количество каналов и тип данных выборок. Метод Process выполняет анализ или модификацию аудиоданных.
На рис. 5 демонстрируются оба метода, а также показана инициализация поля RegistrationProperties. Заметьте, что первое поле структуры XAPO_REGISTRATION_PROPERTIES — это GUID, идентифицируемый с помощью класса.
Рис. 5. Файл OneSecondTremoloEffect.cpp
#include "pch.h"
#include "OneSecondTremoloEffect.h"
const XAPO_REGISTRATION_PROPERTIES OneSecondTremoloEffect::RegistrationProps =
{
__uuidof(OneSecondTremoloEffect),
L"One-Second Tremolo Effect",
L"Coded by Charles Petzold",
1, // основной номер версии
0, // дополнительный номер версии
XAPOBASE_DEFAULT_FLAG | XAPO_FLAG_INPLACE_REQUIRED,
1, // Минимальный размер входного буфера
1, // Максимальный размер входного буфера
1, // Минимальный размер выходного буфера
1 // Максимальный размер выходного буфера
};
HRESULT OneSecondTremoloEffect::LockForProcess(
UINT32 inpParamCount,
const XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS *pInpParams,
UINT32 outParamCount,
const XAPO_LOCKFORPROCESS_BUFFER_PARAMETERS *pOutParams)
{
waveFormat = * pInpParams[0].pFormat;
return CXAPOBase::LockForProcess(inpParamCount, pInpParams,
outParamCount, pOutParams);
}
void OneSecondTremoloEffect::Process(UINT32 inpParamCount,
const XAPO_PROCESS_BUFFER_PARAMETERS *pInpParams,
UINT32 outParamCount,
XAPO_PROCESS_BUFFER_PARAMETERS *pOutParams,
BOOL isEnabled)
{
XAPO_BUFFER_FLAGS flags = pInpParams[0].BufferFlags;
int frameCount = pInpParams[0].ValidFrameCount;
const float * pSrc = static_cast<float *>(pInpParams[0].pBuffer);
float * pDst = static_cast<float *>(pOutParams[0].pBuffer);
int numChannels = waveFormat.nChannels;
switch(flags)
{
case XAPO_BUFFER_VALID:
for (int frame = 0; frame < frameCount; frame++)
{
float sin = 1;
if (isEnabled)
{
sin = fabs(DirectX::XMScalarSin(DirectX::XM_PI * tremoloIndex /
waveFormat.nSamplesPerSec));
tremoloIndex = (tremoloIndex + 1) % waveFormat.nSamplesPerSec;
}
for (int channel = 0; channel < numChannels; channel++)
{
int index = numChannels * frame + channel;
pDst[index] = sin * pSrc[index];
}
}
break;
case XAPO_BUFFER_SILENT:
break;
}
pOutParams[0].ValidFrameCount = pInpParams[0].ValidFrameCount;
pOutParams[0].BufferFlags = pInpParams[0].BufferFlags;
}
Теоретически APO могут иметь дело с несколькими входными и выходными буферами. Однако в настоящее время APO ограничены одним входным и одним выходным буфером. Это ограничение касается последних четырех полей структуры XAPO_REGISTRATION_PROPERTIES и параметров, передаваемых методам LockForProcess и Process. В обоих методах параметры inpParamCount и outParamCount всегда равны 1, а аргументы-указатели всегда указывают только на один экземпляр структуры.
Метод Process в APO принимает входной буфер аудиоданных и подготавливает выходной буфер при частоте 100 вызовов в секунду. APO могут выполнять преобразования формата, например изменять частоту дискретизации между входным и выходным буферами, количество каналов или тип данных выборок.
Эти преобразования формата могут быть трудными, поэтому в шестом поле структуры XAPO_REGISTRATION_PROPERTIES вы указываете, какие преобразования вы не будете реализовать. XAPOBASE_DEFAULT_FLAG сообщает, что вы не хотите выполнять преобразования частоты дискретизации, числа каналов, размеров выборок или фреймов (количества выборок при каждом вызове Process).
Формат аудиоданных, передаваемых через APO, доступен из параметров переопределенной версии LockForProcess в виде стандартной структуры WAVEFORMATEX. Обычно LockForProcess вызывается лишь раз. Большинству APO нужно знать частоту дискретизации и количество каналов, поэтому лучше всего обобщить ваш APO под любые возможные значения.
Также критичен тип данных самих выборок. При работе с XAudio2 вы чаще всего имеете дело с выборками, которые являются 16-битными целыми или 32-битными числами с плавающей точкой. Однако на внутреннем уровне XAudio2 предпочитает использовать данные с плавающей точкой (C++-тип float), и именно это вы увидите в своих APO. Если хотите, можете проверить тип данных выборок в методе LockForProcess. Однако, судя по моему опыту, поле wFormatTag структуры WAVEFORMATEX не содержит WAVE_FORMAT_IEEE_FLOAT, как можно было бы ожидать. Вместо этого оно равно WAVE_FORMAT_EXTENSIBLE (значение 65534), а это значит, что вы на самом деле имеете дело со структурой WAVEFORMATEXTENSIBLE, в каковом случае поле SubFormat указывает тип данных KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.
Если метод LockForProcess встречает формат, с которым он не может работать, он должен вернуть HRESULT, сообщающий об ошибке, например E_NOTIMPL для указания «not implemented» (не реализован).
Обработка аудиоданных
Метод LockForProcess может тратить любое время, необходимое на инициализацию, но метод Process выполняется в потоке, обрабатывающем аудиоданные, и поэтому не может быть медлительным. Вы обнаружите, что при частоте дискретизации 44 100 Гц поле ValidFrameCount в буферах содержит значение 441, а это говорит о том, что Process вызывается 100 раз в секунду — и каждый раз с порцией аудиоданных на 10 мс. В случае двухканального стереозвука этот буфер содержит 882 значения с плавающей точкой; при этом каналы чередуются: за левым каналом следует правый.
Поле BufferFlags содержит либо XAPO_BUFFER_VALID, либо XAPO_BUFFER_SILENT. Этот флаг позволяет пропускать обработку, если реальных аудиоданных не поступает. Кроме того, параметр isEnabled указывает, был ли этот эффект включен через методы EnableEffect/DisableEffect, которые вы уже видели.
Если буфер действителен, OneSecondTremoloEffect APO перебирает фреймы и каналы, вычисляет индекс для буфера и перемещает значения с плавающей точкой из буфера-источника (pSrc) в буфер-приемник (pDst). Если эффект отключен, к значениям источника применяется множитель 1, а если включен — значение синуса, вычисляемое с применением быстрой функции XMScalarSin из библиотеки DirectX Math.
В конце метода Process полям ValidFrameCount и BufferFlags в структуре выходных параметров присваиваются соответствующие значения из структуры входных параметров.
Хотя код обрабатывает входной и выходной буферы как раздельные объекты, на самом деле это не так. Среди флагов, которые вы можете установить в структуре XAPO_REGISTRATION_PROPERTIES, — XAPO_FLAG_INPLACE_SUPPORTED (включен в XAPOBASE_DEFAULT_FLAG) и XAPO_FLAG_INPLACE_REQUIRED. Слово «inplace» означает, что указатели на входной и выходной буферы (в моем коде это pSrc и pDst) на самом деле одинаковы. То есть для ввода и вывода используется только один буфер. Вы должны четко понимать этот факт при написании своего кода.
Будьте осторожны: если эти флаги убрать, то, как показывает мой опыт, раздельные буферы действительно присутствуют, но лишь входной буфер является действительным как для ввода, так и для вывода.
Сохранение предыдущих выборок
Эффекту тремоло нужно просто изменять выборки, а эффекту эхо — сохранять предыдущие выборки, так как вывод эффекта односекундного эха является текущими аудиоданными плюс аудиоданные секунду назад.
Это означает, что в классе OneSecondEchoEffect требуется поддерживать собственный буфер аудиоданных, который определяется как std::vector типа float, а его размер задается в методе LockForProcess:
delayLength = waveFormat.nSamplesPerSec;
int numDelaySamples = waveFormat.nChannels *
waveFormat.nSamplesPerSec;delayBuffer.resize(numDelaySamples);
Этот вектор delayBuffer достаточен для хранения одной секунды аудиоданных и обрабатывается как круговой буфер. Метод LockForProcess инициализирует буфер нулевыми значениями, а затем инициализирует индекс этого буфера:
delayIndex = 0;
На рис. 6 показан метод Process в OneSecondEchoEffect. Поскольку эффект эха должен звучать и по завершении исходного звука, вы больше не можете пропускать обработку, когда флаг XAPO_BUFFER_SILENT указывает на отсутствие входных аудиоданных. Вместо этого по завершении звукового файла выходные аудиоданные должны воспроизводиться вплоть до конца эха. Поэтому переменная source содержит либо входные аудиоданные, либо 0 в зависимости от наличия флага XAPO_BUFFER_SILENT. Половина значения source объединяется с половиной значения, хранящегося в буфере задержки, и результат записывается обратно в буфер задержки. В любой момент вы слышите половину текущих аудиоданных плюс четверть аудиоданных секунду назад, плюс одну восьмую аудиоданных две секунды назад и т. д. Вы можете подстраивать баланс для разных эффектов, включая эхо, которое становится громче с каждым повторением.
Рис. 6. Метод Process в OneSecondEchoEffect
void OneSecondEchoEffect::Process(UINT32 inpParamCount,
const XAPO_PROCESS_BUFFER_PARAMETERS *pInpParams,
UINT32 outParamCount,
XAPO_PROCESS_BUFFER_PARAMETERS *pOutParams,
BOOL isEnabled)
{
const float * pSrc = static_cast<float *>(pInpParams[0].pBuffer);
float * pDst = static_cast<float *>(pOutParams[0].pBuffer);
int frameCount = pInpParams[0].ValidFrameCount;
int numChannels = waveFormat.nChannels;
bool isSourceValid = pInpParams[0].BufferFlags == XAPO_BUFFER_VALID;
for (int frame = 0; frame < frameCount; frame++)
{
for (int channel = 0; channel < numChannels; channel++)
{
// Получаем выборку на основе флага XAPO_BUFFER_VALID
int index = numChannels * frame + channel;
float source = isSourceValid ? pSrc[index] : 0.0f;
// Объединяем выборку с содержимым буфера задержки
// и записываем результат обратно в него
int delayBufferIndex = numChannels * delayIndex + channel;
float echo = 0.5f * source + 0.5f * delayBuffer[delayBufferIndex];
delayBuffer[delayBufferIndex] = echo;
// Перемещаем в буфер-приемник
pDst[index] = isEnabled ? echo : source;
}
delayIndex = (delayIndex + 1) % delayLength;
}
pOutParams[0].BufferFlags = XAPO_BUFFER_VALID;
pOutParams[0].ValidFrameCount = pInpParams[0].ValidFrameCount;
}
Попробуйте установить длину буфера задержки в одну десятую секунды:
delayLength = waveFormat.nSamplesPerSec / 10;
Теперь вы получите не столько эхо, сколько эффект реверберации. Конечно, в настоящем APO вы предпочтете программным способом управлять всеми параметрами, и вот почему настоящий APO для создания эффекта эха и реверберации контролируется структурой XAUDIO2FX_REVERB_PARAMETERS с 23 полями.
APO с параметрами
Большинство APO позволяет изменять их поведение с помощью параметров периода выполнения, которые можно задавать программным способом. Для всех классов голосов определяется метод SetEffectParameters, и он ссылается на конкретный APO по индексу. Параметризованный APO несколько хитроумнее в реализации, но не намного.
В предыдущей статье из этой рубрики я демонстрировал, как использовать встроенный полосовой фильтр, реализованный в голосах-источниках и субмиксовых голосах XAudio2, для создания 26-полосного графического эквалайзера, в котором каждая полоса охватывает треть октавы в общем диапазоне звуковых частот. Та программа GraphicEqualizer разделяла звук на 26 частей для применения этих фильтров, а затем снова соединяла аудиопотоки. Эта методика может оказаться довольно неэффективной.
В одном APO можно реализовать алгоритм всего графического эквалайзера и добиться того же эффекта, что и в предыдущей программе, используя всего один голос-источник и один мастеринговый голос. Это я и сделал в программе GraphicEqualizer2. Новая программа выглядит и работает со звуком так же, как и более ранняя, но ее внутреннее устройство сильно отличается.
Одна из проблем в передаче параметров в APO — синхронизация между потоками. Метод Process выполняется в потоке, обрабатывающем аудиоданные, а параметры скорее всего передаются из UI-потока. К счастью, класс CXAPOParametersBase берет синхронизацию на себя.
Сначала вам нужно определить структуру для параметров. В случае эффекта 26-полосного эквалайзера структура содержит только одно поле — массив из 26 уровней амплитуд:
struct OneThirdOctaveEqualizerParameters
{
std::array<float, 26> Amplitude;};
В программе члены этого массива вычисляются по значениям ползунков (в децибелах).
Чтобы инициализировать CXAPOParametersBase, вы должны передать в его конструктор массив из трех структур параметров. CXAPOParametersBase использует этот блок памяти для синхронизации между потоками.
Мы вновь столкнулись с проблемой передачи инициализированных данных из производного класса конструктору базового класса. На этот раз я выбрал другое решение: определил конструктор производного класса как защищенный (protected) и создал экземпляр класса из открытого статического метода Create, как показано на рис. 7.
Рис. 7. Статический метод Create для OneThirdOctaveEqualizerEffect
OneThirdOctaveEqualizerEffect * OneThirdOctaveEqualizerEffect::Create()
{
// Создаем и инициализируем три параметра эффекта
OneThirdOctaveEqualizerParameters * pParameterBlocks =
new OneThirdOctaveEqualizerParameters[3];
for (int i = 0; i < 3; i++)
for (int band = 0; band < 26; band++)
pParameterBlocks[i].Amplitude[band] = 1.0f;
// Создаем эффект
return new OneThirdOctaveEqualizerEffect(
&RegistrationProps,
(byte *) pParameterBlocks,
sizeof(OneThirdOctaveEqualizerParameters),
false);
}
Цифровые биквадратные фильтры, реализованные в XAudio2 (которые эмулируются в этом APO), используют следующую формулу:
y = (b0·x + b1·x’ + b2·x’’ – a1·y’ – a2·y’’) / a0
В этой формуле x — это входная выборка, x' — предыдущая входная выборка, а x'' — выборка до нее. Буквы y означают то же самое, но применительно к выводу.
Таким образом, эффекту эквалайзера нужно сохранять два предыдущих входных значения для каждого канала и два предыдущих выходных значения для каждого канала и каждой полосы.
Шесть констант в этой формуле зависят от типа фильтра, частоты среза (или, как в данном случае с полосовым фильтром, от центральной частоты) относительно частоты дискретизации и Q — коэффициента добротности фильтра. В случае нашего графического эквалайзера каждый фильтр имеет Q, соответствующую полосе пропускания в треть октавы, или 4.318. Каждая полоса имеет уникальный набор констант, вычисляемых в методе LockForProcess с помощью кода, представленного на рис. 8.
Рис. 8. Вычисление констант фильтра эквалайзера
Q = 4.318f; // треть октавы
static float frequencies[26] =
{
20.0f, 25.0f, 31.5f, 40.0f, 50.0f, 63.0f, 80.0f, 100.0f, 125.0f,
160.0f, 200.0f, 250.0f, 320.0f, 400.0f, 500.0f, 630.0f, 800.0f, 1000.0f,
1250.0f, 1600.0f, 2000.0f, 2500.0f, 3150.0f, 4000.0f, 5000.0f, 6300.0f
};
for (int band = 0; band < 26; band++)
{
float frequency = frequencies[band];
float omega = 2 * 3.14159f * frequency / waveFormat.nSamplesPerSec;
float alpha = sin(omega) / (2 * Q);
a0[band] = 1 + alpha;
a1[band] = -2 * cos(omega);
a2[band] = 1 - alpha;
b0[band] = Q * alpha; // == sin(omega) / 2;
b1[band] = 0;
b2[band] = -Q * alpha; // == -sin(omega) / 2;
}
В методе Process объект APO получает указатель на текущую структуру параметров вызовом CXAPOParametersBase::BeginProcess, причем в данном случае осуществляется приведение возвращаемого значения к структуре типа OneThirdOctaveEqualizerParameters. В конце метода Process вызов CXAPOParametersBase::EndProcess освобождает структуру параметров, удерживаемую методом. Полный исходный код метода Process показан на рис. 9.
Рис. 9. Метод Process в OneThirdOctaveEqualizerEffect
void OneThirdOctaveEqualizerEffect::Process(UINT32 inpParamCount,
const XAPO_PROCESS_BUFFER_PARAMETERS *pInpParam,
UINT32 outParamCount,
XAPO_PROCESS_BUFFER_PARAMETERS *pOutParam,
BOOL isEnabled)
{
// Получаем параметры эффекта
OneThirdOctaveEqualizerParameters * pEqualizerParams =
(OneThirdOctaveEqualizerParameters *) CXAPOParametersBase::BeginProcess();
// Получаем указатели на буферы и другую информацию
const float * pSrc = static_cast<float *>(pInpParam[0].pBuffer);
float * pDst = static_cast<float *>(pOutParam[0].pBuffer);
int frameCount = pInpParam[0].ValidFrameCount;
int numChannels = waveFormat.nChannels;
switch(pInpParam[0].BufferFlags)
{
case XAPO_BUFFER_VALID:
for (int frame = 0; frame < frameCount; frame++)
{
for (int channel = 0; channel < numChannels; channel++)
{
int index = numChannels * frame + channel;
// Если фильтр отключен, почти ничего не делаем
if (!isEnabled)
{
pDst[index] = pSrc[index];
continue;
}
// Получаем предыдущие входные значения
float x = pSrc[index];
float xp = pxp[channel];
float xpp = pxpp[channel];
// Инициализируем накопленное значение
float accum = 0;
for (int band = 0; band < 26; band++)
{
int bandIndex = numChannels * band + channel;
// Получаем предыдущие выходные значения
float yp = pyp[bandIndex];
float ypp = pypp[bandIndex];
// Вычисляем вывод фильтра
float y = (b0[band] * x + b1[band] * xp + b2[band] * xpp
- a1[band] * yp - a2[band] * ypp) / a0[band];
// Накапливаем вывод фильтра
// с подстройкой по амплитуде
accum += y * pEqualizerParams->Amplitude[band];
// Сохраняем предыдущие выходные значения
pypp[bandIndex] = yp;
pyp[bandIndex] = y;
}
// Сохраняем предыдущие входные значения
pxpp[channel] = xp;
pxp[channel] = x;
// Сохраняем конечное значение, подстраиваемое
// под коэффициент усиления фильтра
pDst[index] = accum / Q;
}
}
break;
case XAPO_BUFFER_SILENT:
break;
}
// Задаем выходные параметры
pOutParam[0].ValidFrameCount = pInpParam[0].ValidFrameCount;
pOutParam[0].BufferFlags = pInpParam[0].BufferFlags;
CXAPOParametersBase::EndProcess();
}
Одна из особенностей в программировании, которая всегда мне нравилась, заключается в том, что проблемы зачастую можно решать несколькими способами. Иногда трудное решение оказывается более эффективным, а иногда — нет. Определенно, замена 26 экземпляров IXAudio2SubmixVoice одним APO является радикальным изменением. Но если вы решили, что это изменение дает значительное повышение скорости работы, то вы заблуждаетесь. Task Manager в Windows 8 показывает, что обе программы GraphicEqualizer примерно эквивалентны, а значит, разделение аудиопотока на 26 субмиксовых голосов оказалось в конечном счете не столь уж бредовой идеей.
Чарльз Петцольд (Charles Petzold) — давний «пишущий» редактор MSDN Magazine и автор книги «Programming Windows, 6th edition» (O’Reilly Media, 2012) о написании приложений для Windows 8. Его веб-сайт находится по адресу is charlespetzold.com.
Выражаю благодарность за рецензирование статьи экспертам Microsoft Дункану Маккею (Duncan McKay) и Джеймсу Макнеллису (James McNellis).