Аудио: микшер и формат PAC

Настройка аудиомодуля, воспроизведение голосов и структура файлов PAC.

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).