10. Audio - микшер и формат PAC
16-голосный программный микшер со встроенным ресемплером, вывод через I²S standard-mode (16 бит, стерео). Включается PIPCORE_ENABLE_AUDIO. Доступ - plat->audio().
Модель потоков. Микшер работает в собственном FreeRTOS-таске (стек, приоритет и ядро - в Kconfig; по умолчанию ядро 0, противоположное рендеру). Методы класса Audio защищены мьютексом - play() можно вызывать из любого таска. Пользовательский MixHook выполняется в контексте таска микшера, держите его быстрым и не блокирующим.
Типы
struct SoundConfig
{
uint32_t sampleRate = 44100; // частота I²S-выхода, Гц
int8_t bck = 13; // пины I²S
int8_t ws = 15;
int8_t dataOut = 17;
uint8_t i2sPort = 0;
};
enum class Bus : uint8_t { MUSIC = 0, SFX = 1, UI = 2, AMBIENT = 3 };
struct SoundHandle { uint16_t id = 0; }; // id == 0 - «нет звука»
Шины позволяют регулировать громкость и лимиты голосов по группам звуков.
Методы
| Метод | Описание |
|---|---|
bool configure(const SoundConfig &cfg) |
Пины и частота I²S. Вызывайте до begin(). Если не вызвать, begin() возьмёт SoundConfig{} |
bool begin() |
Запуск вывода и таска микшера. false - не удалось |
void end() |
Остановка |
bool ready() const |
Микшер запущен |
SoundHandle play(const uint8_t *data, size_t dataSize, float volume = 1.0f, uint16_t gainL_q15 = 32767, uint16_t gainR_q15 = 32767, Bus bus = Bus::SFX, uint8_t priority = 5) |
Запустить звук из PAC-данных в памяти. Возвращает {0}, если микшер не запущен, данные битые или не нашлось свободного голоса. data должна быть выровнена на 4 байта |
void stop(SoundHandle h) |
Остановить голос |
void pause(SoundHandle h) / void resume(SoundHandle h) |
Пауза / продолжение |
void setVoiceGains(SoundHandle h, uint16_t gainL_q15, uint16_t gainR_q15) |
Панорама голоса. Формат q15: 32767 = 1.0 |
void setMasterVolume(float v01) / float masterVolume() const |
Общая громкость 0…1 (по умолчанию 0,8) |
void setBusVolume(Bus bus, float v01) / float busVolume(Bus bus) const |
Громкость шины 0…1 |
void setBusQuota(const BusQuota &q) |
Лимит одновременных голосов на шину. BusQuota{ maxVoices[4] }, по умолчанию {2, 10, 2, 2} (MUSIC, SFX, UI, AMBIENT) |
void setMixHook(MixHook hook, void *user) |
Пользовательская пост-обработка микса до мастер-громкости (эквалайзер, реверб) |
uint8_t activeVoices() const |
Число активных (не на паузе) голосов |
uint8_t countActiveOnBus(Bus bus) const |
Активных голосов на шине |
static constexpr uint8_t MaxVoices |
16 |
Тип хука:
using MixHook = void (*)(int32_t *mixL, int32_t *mixR, size_t frames, void *user);
Пример
Audio *audio = plat->audio();
if (!audio)
return;
audio->configure(SoundConfig{}); // пины по умолчанию
if (!audio->begin())
{
log::error("audio init failed");
return;
}
audio->setBusVolume(Bus::MUSIC, 0.8f);
audio->setBusVolume(Bus::SFX, 1.0f);
SoundHandle music = audio->play(musicData, musicSize, 0.9f, 32767, 32767, Bus::MUSIC);
audio->play(laserData, laserSize, 1.0f, 32767, 22000, Bus::SFX); // смещение вправо
// ...
audio->stop(music);
Выровненные данные в прошивке:
alignas(4) static const uint8_t laserData[] = { /* содержимое .pac */ };
Приоритеты и вытеснение
Если квота шины исчерпана, play() вытесняет голос с самым низким приоритетом - при условии, что приоритет нового звука не ниже вытесняемого. Не удалось вытеснить - вернётся {0}. Голос, доигравший до конца, освобождается сам.
Формат PAC
PAC - компактный контейнер озвучки для микшера (генерируется внешним инструментом). Данные лежат во флеш-памяти как есть, без копирования: play() получает указатель и проверяет заголовок.
struct alignas(4) PACHeader // ровно 48 байт
{
char magic[4]; // "PAC!"
uint16_t version;
uint16_t flags; // PAC_FLAG_LOOP
uint32_t sourceRate; // частота исходника, Гц (1000…192000)
uint32_t nativeRate; // частота «как задумано»
uint32_t frameCount; // кадров всего
uint32_t loopStart, loopEnd; // точки цикла, в кадрах
uint32_t blockCount;
uint32_t dataOffset; // смещение полезных данных (>= 48)
uint32_t dataSize;
uint32_t reserved1, reserved2;
bool isValid() const; // magic == "PAC!"
bool isLoop() const; // flags & PAC_FLAG_LOOP
};
Данные разбиты на блоки по 256 кадров (PAC_BLOCK_FRAMES); каждый блок кодируется одним из режимов:
| Режим | Содержимое | Полезная нагрузка блока |
|---|---|---|
PAC_MODE_SILENCE |
Тишина | - |
PAC_MODE_ADPCM2 |
ADPCM, 2 бита на сэмпл | PacPayload2 = 64 байта |
PAC_MODE_ADPCM4 |
ADPCM, 4 бита на сэмпл | PacPayload4 = 128 байт |
PAC_MODE_ADPCM6 |
ADPCM, 6 бит на сэмпл | PacPayload6 = 192 байта |
PAC_MODE_HOLD |
Удержание предыдущего сэмпла | - |
Декодеры сэмплов (decodeAdpcm4 и др. в pipcore::audio) публичны - их можно использовать в собственных инструментах.
Ресемплер линейный: sourceRate автоматически приводится к частоте I²S-выхода. При совпадении частот включается passthrough (пересчёт не тратит CPU).