Add Slice 5 ADAU1701 runtime audio control (firmware 0.5.0).

Safeload mixer/EQ/master on the ADAU1701, persist AudioProfile in NVS,
expose /api/audio routes and web UI controls, with host tests and manual sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 16:47:58 +02:00
co-authored by Cursor
parent 4b931b0aad
commit ab3651e678
58 changed files with 3191 additions and 41 deletions
@@ -3,12 +3,13 @@ set(ADAU_FW_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../Firmware/ADAU1701-Firmware")
idf_component_register(
SRCS
"src/Adau1701Driver.cpp"
"src/Adau1701Dsp.cpp"
"src/SigmaStudioFW.c"
"src/adau1701_program.c"
INCLUDE_DIRS
"include"
"${ADAU_FW_DIR}"
REQUIRES driver esp_driver_gpio esp_driver_i2c
REQUIRES core driver esp_driver_gpio esp_driver_i2c
)
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
@@ -1,6 +1,6 @@
/**
* @file Adau1701Driver.hpp
* @brief ADAU1701 SigmaDSP driver — RAM boot on every power-up.
* @brief ADAU1701 SigmaDSP driver — RAM boot and runtime safeload control.
*
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
*
@@ -14,6 +14,15 @@
#include "adau1701/Adau1701Error.hpp"
#include "core/AudioProfile.hpp"
#include "core/EqBandIndex.hpp"
#include "core/EqProfile.hpp"
#include "core/FrequencyHz.hpp"
#include "core/GainDb.hpp"
#include "core/MixSource.hpp"
#include "core/MixerState.hpp"
#include <cstdint>
#include <expected>
namespace adau1701 {
@@ -36,7 +45,7 @@ struct Adau1701Pins {
};
/**
* @brief Adau1701Driver — owns I2C + reset and loads SigmaStudio RAM.
* @brief Adau1701Driver — owns I2C + reset, boot, and safeload runtime.
*
* @dname Adau1701Driver
* @param pins Board wiring for I2C and RESET#.
@@ -45,7 +54,8 @@ struct Adau1701Pins {
* successful default_download replay.
*
* Writes the SigmaStudio export from Firmware/ADAU1701-Firmware on every
* boot (no EEPROM self-boot on DigiRadio).
* boot (no EEPROM self-boot on DigiRadio). Runtime EQ and mixer changes
* use the ADAU1701 safeload mechanism.
*
* @author Michele Bigi
* @date 2026-07-06
@@ -102,7 +112,104 @@ public:
*/
[[nodiscard]] bool isBooted() const noexcept;
/**
* @brief applyProfile — safeload mixer, EQ, and master from snapshot.
*
* @dname applyProfile
* @param profile User audio configuration.
* @return Ok on success, or Adau1701Error.
* @pubstate writes parameter RAM via safeload.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, Adau1701Error> applyProfile(
const core::AudioProfile& profile);
/**
* @brief applyMixer — safeload input and stereo-mixer gains.
*
* @dname applyMixer
* @param mixer Per-source and St Mixer1 levels.
* @return Ok on success, or Adau1701Error.
* @pubstate writes parameter RAM via safeload.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, Adau1701Error> applyMixer(
const core::MixerState& mixer);
/**
* @brief applyEq — safeload all six PEQ bands.
*
* @dname applyEq
* @param eq Six-band parametric EQ settings.
* @return Ok on success, or Adau1701Error.
* @pubstate writes parameter RAM via safeload.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, Adau1701Error> applyEq(
const core::EqProfile& eq);
/**
* @brief setInputVolume — safeload one input path gain.
*
* @dname setInputVolume
* @param source Si4684 or ESP32 I2S path.
* @param left Left channel gain.
* @param right Right channel gain.
* @return Ok on success, or Adau1701Error.
* @pubstate writes parameter RAM via safeload.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, Adau1701Error> setInputVolume(
core::MixSource source, core::GainDb left, core::GainDb right);
/**
* @brief setMasterVolume — safeload Multiple 1 master output gain.
*
* @dname setMasterVolume
* @param left Left master gain.
* @param right Right master gain.
* @return Ok on success, or Adau1701Error.
* @pubstate writes parameter RAM via safeload.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, Adau1701Error> setMasterVolume(
core::GainDb left, core::GainDb right);
/**
* @brief setEqBand — design and safeload one PEQ band.
*
* @dname setEqBand
* @param band Band index 0..5.
* @param gain Band gain in dB.
* @param center Centre frequency.
* @param q Quality factor.
* @return Ok on success, or Adau1701Error.
* @pubstate writes five coefficients in one safeload transfer.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, Adau1701Error> setEqBand(
core::EqBandIndex band, core::GainDb gain, core::FrequencyHz center,
float q);
private:
[[nodiscard]] std::expected<void, Adau1701Error> ensureBooted() const;
[[nodiscard]] std::expected<void, Adau1701Error> safeloadGain(
unsigned paramAddr, core::GainDb gain);
[[nodiscard]] std::expected<void, Adau1701Error> safeloadFixpoint(
unsigned paramAddr, std::int32_t fixpoint);
Adau1701Pins pins_;
bool booted_;
void* i2cBus_;
@@ -0,0 +1,70 @@
/**
* @file Adau1701Dsp.hpp
* @brief core::IDsp adapter over Adau1701Driver.
*
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
*
* Copyright 2026 Michele Bigi
* SPDX-License-Identifier: Apache-2.0
*
* @author Michele Bigi
* @date 2026-07-06
*/
#pragma once
#include "adau1701/Adau1701Driver.hpp"
#include "core/IDsp.hpp"
namespace adau1701 {
/**
* @brief Adau1701Dsp — maps core::IDsp to Adau1701Driver safeload API.
*
* @dname Adau1701Dsp
* @return n/a (type)
* @pubstate Borrows Adau1701Driver for the process lifetime.
*
* @author Michele Bigi
* @date 2026-07-06
*/
class Adau1701Dsp final : public core::IDsp {
public:
/**
* @brief Adau1701Dsp — bind to the board driver instance.
*
* @dname Adau1701Dsp
* @param driver Booted ADAU1701 driver (must outlive this adapter).
* @pubstate stores driver reference.
*
* @author Michele Bigi
* @date 2026-07-06
*/
explicit Adau1701Dsp(Adau1701Driver& driver);
[[nodiscard]] std::expected<void, core::DspError> applyProfile(
const core::AudioProfile& profile) override;
[[nodiscard]] std::expected<void, core::DspError> applyMixer(
const core::MixerState& mixer) override;
[[nodiscard]] std::expected<void, core::DspError> applyEq(
const core::EqProfile& eq) override;
[[nodiscard]] std::expected<void, core::DspError> setInputVolume(
core::MixSource source, core::GainDb left, core::GainDb right) override;
[[nodiscard]] std::expected<void, core::DspError> setMasterVolume(
core::GainDb left, core::GainDb right) override;
[[nodiscard]] std::expected<void, core::DspError> setEqBand(
core::EqBandIndex band, core::GainDb gain, core::FrequencyHz center,
float q) override;
private:
[[nodiscard]] static core::DspError mapError(Adau1701Error error) noexcept;
Adau1701Driver& driver_;
};
} // namespace adau1701
@@ -28,6 +28,8 @@ enum class Adau1701Error {
I2cInitFailed,
ResetFailed,
DownloadFailed,
NotBooted,
SafeloadFailed,
};
} // namespace adau1701
@@ -0,0 +1,77 @@
/**
* @file Adau1701ParamMap.hpp
* @brief SigmaStudio parameter RAM addresses for DigiRadio runtime control.
*
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
*
* Copyright 2026 Michele Bigi
* SPDX-License-Identifier: Apache-2.0
*
* @author Michele Bigi
* @date 2026-07-06
*/
#pragma once
#include "DigiRadio_IC_1_PARAM.h"
#include "core/EqBandIndex.hpp"
#include "core/MixSource.hpp"
#include <cstdint>
namespace adau1701 {
/**
* @brief paramAddrEqBandBase — first coefficient address for a PEQ band.
*
* @dname paramAddrEqBandBase
* @param band Band index 0..5.
* @return ADDR_PARAMEQ1_STn_B0 from the SigmaStudio export.
* @pubstate none
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] inline unsigned paramAddrEqBandBase(std::uint8_t band) noexcept
{
return static_cast<unsigned>(ADDR_PARAMEQ1_ST0_B0)
+ static_cast<unsigned>(band) * 5U;
}
/**
* @brief paramAddrInputLeft — left volume address for an input source.
*
* @dname paramAddrInputLeft
* @param source Si4684 or ESP32 path.
* @return Parameter RAM address for the left channel gain cell.
* @pubstate none
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] inline unsigned paramAddrInputLeft(core::MixSource source) noexcept
{
return source == core::MixSource::Si4684
? static_cast<unsigned>(ADDR_SI4674)
: static_cast<unsigned>(ADDR_ESP32);
}
/**
* @brief paramAddrInputRight — right volume address for an input source.
*
* @dname paramAddrInputRight
* @param source Si4684 or ESP32 path.
* @return Parameter RAM address for the right channel gain cell.
* @pubstate none
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] inline unsigned paramAddrInputRight(core::MixSource source) noexcept
{
return source == core::MixSource::Si4684
? static_cast<unsigned>(ADDR_SI4674_1)
: static_cast<unsigned>(ADDR_ESP32_1);
}
} // namespace adau1701
@@ -13,6 +13,11 @@
#include "adau1701/Adau1701Driver.hpp"
#include "adau1701/Adau1701ParamMap.hpp"
#include "core/BiquadDesign.hpp"
#include "DigiRadio_IC_1_PARAM.h"
#include "SigmaStudioFW.h"
#include "driver/gpio.h"
@@ -115,4 +120,139 @@ bool Adau1701Driver::isBooted() const noexcept
return booted_;
}
std::expected<void, Adau1701Error> Adau1701Driver::ensureBooted() const
{
if (!booted_) {
return std::unexpected(Adau1701Error::NotBooted);
}
return {};
}
std::expected<void, Adau1701Error> Adau1701Driver::safeloadFixpoint(
unsigned paramAddr, std::int32_t fixpoint)
{
if (sigma_safeload_param(paramAddr, fixpoint) != 0) {
return std::unexpected(Adau1701Error::SafeloadFailed);
}
return {};
}
std::expected<void, Adau1701Error> Adau1701Driver::safeloadGain(
unsigned paramAddr, core::GainDb gain)
{
return safeloadFixpoint(paramAddr, core::gainDbToLinearFixpoint(gain));
}
std::expected<void, Adau1701Error> Adau1701Driver::setInputVolume(
core::MixSource source, core::GainDb left, core::GainDb right)
{
if (auto ready = ensureBooted(); !ready) {
return ready;
}
if (auto result = safeloadGain(paramAddrInputLeft(source), left); !result) {
return result;
}
return safeloadGain(paramAddrInputRight(source), right);
}
std::expected<void, Adau1701Error> Adau1701Driver::setMasterVolume(
core::GainDb left, core::GainDb right)
{
if (auto ready = ensureBooted(); !ready) {
return ready;
}
if (auto result = safeloadGain(static_cast<unsigned>(ADDR_MULTIPLE1), left);
!result) {
return result;
}
return safeloadGain(static_cast<unsigned>(ADDR_MULTIPLE1_1), right);
}
std::expected<void, Adau1701Error> Adau1701Driver::applyMixer(
const core::MixerState& mixer)
{
if (auto ready = ensureBooted(); !ready) {
return ready;
}
if (auto result = setInputVolume(core::MixSource::Si4684, mixer.si4684Left,
mixer.si4684Right);
!result) {
return result;
}
if (auto result = setInputVolume(core::MixSource::Esp32, mixer.esp32Left,
mixer.esp32Right);
!result) {
return result;
}
if (auto result =
safeloadGain(static_cast<unsigned>(ADDR_STMIXER1_ST0_VOLUME),
mixer.mixLeft);
!result) {
return result;
}
return safeloadGain(static_cast<unsigned>(ADDR_STMIXER1_ST1_VOLUME),
mixer.mixRight);
}
std::expected<void, Adau1701Error> Adau1701Driver::setEqBand(
core::EqBandIndex band, core::GainDb gain, core::FrequencyHz center, float q)
{
if (auto ready = ensureBooted(); !ready) {
return ready;
}
const core::BiquadCoefficients coeffs =
core::designPeakingEq(center, gain, q);
const auto fixpoints = coeffs.toFixpoint823();
const unsigned baseAddr = paramAddrEqBandBase(band.value());
unsigned addrs[5U];
int values[5U];
for (unsigned i = 0U; i < 5U; ++i) {
addrs[i] = baseAddr + i;
values[i] = fixpoints[i];
}
if (sigma_safeload_block(5U, addrs, values) != 0) {
return std::unexpected(Adau1701Error::SafeloadFailed);
}
return {};
}
std::expected<void, Adau1701Error> Adau1701Driver::applyEq(
const core::EqProfile& eq)
{
if (auto ready = ensureBooted(); !ready) {
return ready;
}
for (std::uint8_t i = 0; i < core::EqBandIndex::kBandCount; ++i) {
const auto index = core::EqBandIndex::tryFromIndex(i);
if (!index) {
return std::unexpected(Adau1701Error::SafeloadFailed);
}
const core::EqBandSettings& band = eq.band(*index);
if (auto result = setEqBand(*index, band.gain, band.center, band.q);
!result) {
return result;
}
}
return {};
}
std::expected<void, Adau1701Error> Adau1701Driver::applyProfile(
const core::AudioProfile& profile)
{
if (auto ready = ensureBooted(); !ready) {
return ready;
}
if (auto result = applyMixer(profile.mixer); !result) {
return result;
}
if (auto result = applyEq(profile.eq); !result) {
return result;
}
return setMasterVolume(profile.masterLeft, profile.masterRight);
}
} // namespace adau1701
@@ -0,0 +1,90 @@
/**
* @file Adau1701Dsp.cpp
* @brief Adau1701Dsp implementation.
*
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
*
* Copyright 2026 Michele Bigi
* SPDX-License-Identifier: Apache-2.0
*
* @author Michele Bigi
* @date 2026-07-06
*/
#include "adau1701/Adau1701Dsp.hpp"
namespace adau1701 {
Adau1701Dsp::Adau1701Dsp(Adau1701Driver& driver)
: driver_(driver)
{
}
core::DspError Adau1701Dsp::mapError(Adau1701Error error) noexcept
{
switch (error) {
case Adau1701Error::NotBooted:
return core::DspError::NotBooted;
case Adau1701Error::SafeloadFailed:
return core::DspError::SafeloadFailed;
default:
return core::DspError::SafeloadFailed;
}
}
std::expected<void, core::DspError> Adau1701Dsp::applyProfile(
const core::AudioProfile& profile)
{
if (auto result = driver_.applyProfile(profile); !result) {
return std::unexpected(mapError(result.error()));
}
return {};
}
std::expected<void, core::DspError> Adau1701Dsp::applyMixer(
const core::MixerState& mixer)
{
if (auto result = driver_.applyMixer(mixer); !result) {
return std::unexpected(mapError(result.error()));
}
return {};
}
std::expected<void, core::DspError> Adau1701Dsp::applyEq(
const core::EqProfile& eq)
{
if (auto result = driver_.applyEq(eq); !result) {
return std::unexpected(mapError(result.error()));
}
return {};
}
std::expected<void, core::DspError> Adau1701Dsp::setInputVolume(
core::MixSource source, core::GainDb left, core::GainDb right)
{
if (auto result = driver_.setInputVolume(source, left, right); !result) {
return std::unexpected(mapError(result.error()));
}
return {};
}
std::expected<void, core::DspError> Adau1701Dsp::setMasterVolume(
core::GainDb left, core::GainDb right)
{
if (auto result = driver_.setMasterVolume(left, right); !result) {
return std::unexpected(mapError(result.error()));
}
return {};
}
std::expected<void, core::DspError> Adau1701Dsp::setEqBand(
core::EqBandIndex band, core::GainDb gain, core::FrequencyHz center,
float q)
{
if (auto result = driver_.setEqBand(band, gain, center, q); !result) {
return std::unexpected(mapError(result.error()));
}
return {};
}
} // namespace adau1701
@@ -58,3 +58,78 @@ void SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress,
remaining = (unsigned char)(remaining - chunk);
}
}
static int sigma_i2c_write(unsigned int reg, const unsigned char* data,
unsigned char length)
{
if (s_dev == NULL || data == NULL || length == 0U) {
return -1;
}
unsigned char buf[2U + 4U];
if ((size_t)length + 2U > sizeof(buf)) {
return -1;
}
buf[0] = (unsigned char)((reg >> 8) & 0xFFU);
buf[1] = (unsigned char)(reg & 0xFFU);
memcpy(buf + 2U, data, length);
const esp_err_t err =
i2c_master_transmit(s_dev, buf, (size_t)(2U + length), 1000);
return err == ESP_OK ? 0 : -1;
}
static int sigma_write_fixpoint_reg(unsigned int reg, int fixpoint)
{
unsigned char payload[4U];
payload[0] = 0U;
payload[1] = (unsigned char)((fixpoint >> 16) & 0xFFU);
payload[2] = (unsigned char)((fixpoint >> 8) & 0xFFU);
payload[3] = (unsigned char)(fixpoint & 0xFFU);
return sigma_i2c_write(reg, payload, 4U);
}
static int sigma_write_param_addr(unsigned int reg, unsigned int paramAddr)
{
unsigned char payload[2U];
payload[0] = (unsigned char)((paramAddr >> 8) & 0xFFU);
payload[1] = (unsigned char)(paramAddr & 0xFFU);
return sigma_i2c_write(reg, payload, 2U);
}
static int sigma_trigger_safeload(void)
{
unsigned char payload[2U];
payload[0] = (unsigned char)((ADAU1701_CORE_IST_TRIGGER >> 8) & 0xFFU);
payload[1] = (unsigned char)(ADAU1701_CORE_IST_TRIGGER & 0xFFU);
return sigma_i2c_write(ADAU1701_CORE_CONTROL_REG, payload, 2U);
}
int sigma_safeload_param(unsigned int paramAddr, int fixpoint)
{
const unsigned int addrs[1U] = {paramAddr};
const int values[1U] = {fixpoint};
return sigma_safeload_block(1U, addrs, values);
}
int sigma_safeload_block(unsigned char count, const unsigned int* paramAddrs,
const int* fixpoints)
{
if (count == 0U || count > 5U || paramAddrs == NULL || fixpoints == NULL) {
return -1;
}
for (unsigned char i = 0U; i < count; ++i) {
const unsigned int dataReg =
ADAU1701_SAFELOAD_DATA_BASE + (unsigned int)i;
const unsigned int addrReg =
ADAU1701_SAFELOAD_ADDR_BASE + (unsigned int)i;
if (sigma_write_fixpoint_reg(dataReg, fixpoints[i]) != 0) {
return -1;
}
if (sigma_write_param_addr(addrReg, paramAddrs[i]) != 0) {
return -1;
}
}
return sigma_trigger_safeload();
}