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
@@ -0,0 +1,8 @@
idf_component_register(
SRCS
"src/AudioService.cpp"
INCLUDE_DIRS "include"
REQUIRES core
)
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
@@ -0,0 +1,158 @@
/**
* @file AudioService.hpp
* @brief Application service for ADAU1701 mixer/EQ/master configuration.
*
* 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 "core/AudioProfile.hpp"
#include "core/DspError.hpp"
#include "core/EqBandIndex.hpp"
#include "core/FrequencyHz.hpp"
#include "core/GainDb.hpp"
#include "core/IAudioProfileStore.hpp"
#include "core/IDsp.hpp"
#include "core/MixSource.hpp"
#include "core/StoreError.hpp"
#include <expected>
namespace audio {
/**
* @brief AudioService — intent-level audio path control for HTTP/UI.
*
* @dname AudioService
* @return n/a (type)
* @pubstate Borrows core::IDsp; optionally persists via IAudioProfileStore.
* Tracks the last applied profile in RAM.
*
* @author Michele Bigi
* @date 2026-07-06
*/
class AudioService {
public:
/**
* @brief AudioService — bind DSP and optional profile store.
*
* @dname AudioService
* @param dsp ADAU1701 adapter (must outlive this service).
* @param store Optional NVS store; nullptr skips persistence.
* @pubstate initialises profile_ to factoryDefault().
*
* @author Michele Bigi
* @date 2026-07-06
*/
AudioService(core::IDsp& dsp, core::IAudioProfileStore* store);
/**
* @brief loadAndApply — restore saved profile or factory default.
*
* @dname loadAndApply
* @return Ok on success, or DspError from IDsp.
* @pubstate updates profile_ and safeloads the ADAU1701.
*
* Call once after ADAU1701 boot. When no profile is stored, applies
* AudioProfile::factoryDefault() without writing NVS.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, core::DspError> loadAndApply();
/**
* @brief currentProfile — read the in-memory profile snapshot.
*
* @dname currentProfile
* @return Last applied or pending profile.
* @pubstate reads profile_.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] const core::AudioProfile& currentProfile() const noexcept;
/**
* @brief applyProfile — safeload a full profile and optionally save.
*
* @dname applyProfile
* @param profile Validated user configuration.
* @param persist When true and store is set, write NVS.
* @return Ok on success, DspError, or StoreError::IoFailed.
* @pubstate updates profile_; may persist via store_.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, core::StoreError> applyProfile(
const core::AudioProfile& profile, bool persist);
/**
* @brief setInputVolume — update one input path and apply live.
*
* @dname setInputVolume
* @param source Si4684 or ESP32 path.
* @param left Left gain.
* @param right Right gain.
* @param persist When true and store is set, write NVS.
* @return Ok on success, or StoreError wrapping DspError/IoFailed.
* @pubstate updates profile_.mixer and safeloads.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, core::StoreError> setInputVolume(
core::MixSource source, core::GainDb left, core::GainDb right,
bool persist);
/**
* @brief setMasterVolume — update master output and apply live.
*
* @dname setMasterVolume
* @param left Left master gain.
* @param right Right master gain.
* @param persist When true and store is set, write NVS.
* @return Ok on success, or StoreError.
* @pubstate updates profile_ master fields and safeloads.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, core::StoreError> setMasterVolume(
core::GainDb left, core::GainDb right, bool persist);
/**
* @brief setEqBand — update one PEQ band and apply live.
*
* @dname setEqBand
* @param band Band index 0..5.
* @param gain Band gain in dB.
* @param center Centre frequency.
* @param q Quality factor.
* @param persist When true and store is set, write NVS.
* @return Ok on success, or StoreError.
* @pubstate updates profile_.eq and safeloads.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, core::StoreError> setEqBand(
core::EqBandIndex band, core::GainDb gain, core::FrequencyHz center,
float q, bool persist);
private:
[[nodiscard]] std::expected<void, core::StoreError> persistProfile() const;
core::IDsp& dsp_;
core::IAudioProfileStore* store_;
core::AudioProfile profile_;
};
} // namespace audio
@@ -0,0 +1,118 @@
/**
* @file AudioService.cpp
* @brief AudioService 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 "audio/AudioService.hpp"
namespace audio {
AudioService::AudioService(core::IDsp& dsp, core::IAudioProfileStore* store)
: dsp_(dsp)
, store_(store)
, profile_(core::AudioProfile::factoryDefault())
{
}
std::expected<void, core::DspError> AudioService::loadAndApply()
{
if (store_ != nullptr && store_->hasProfile()) {
if (auto loaded = store_->loadProfile(); loaded) {
profile_ = *loaded;
}
}
if (auto applied = dsp_.applyProfile(profile_); !applied) {
return applied;
}
return {};
}
const core::AudioProfile& AudioService::currentProfile() const noexcept
{
return profile_;
}
std::expected<void, core::StoreError> AudioService::persistProfile() const
{
if (store_ == nullptr) {
return {};
}
return store_->saveProfile(profile_);
}
std::expected<void, core::StoreError> AudioService::applyProfile(
const core::AudioProfile& profile, bool persist)
{
if (auto applied = dsp_.applyProfile(profile); !applied) {
return std::unexpected(core::StoreError::IoFailed);
}
profile_ = profile;
if (persist) {
return persistProfile();
}
return {};
}
std::expected<void, core::StoreError> AudioService::setInputVolume(
core::MixSource source, core::GainDb left, core::GainDb right, bool persist)
{
if (auto applied = dsp_.setInputVolume(source, left, right); !applied) {
return std::unexpected(core::StoreError::IoFailed);
}
if (source == core::MixSource::Si4684) {
profile_.mixer.si4684Left = left;
profile_.mixer.si4684Right = right;
} else {
profile_.mixer.esp32Left = left;
profile_.mixer.esp32Right = right;
}
if (persist) {
return persistProfile();
}
return {};
}
std::expected<void, core::StoreError> AudioService::setMasterVolume(
core::GainDb left, core::GainDb right, bool persist)
{
if (auto applied = dsp_.setMasterVolume(left, right); !applied) {
return std::unexpected(core::StoreError::IoFailed);
}
profile_.masterLeft = left;
profile_.masterRight = right;
if (persist) {
return persistProfile();
}
return {};
}
std::expected<void, core::StoreError> AudioService::setEqBand(
core::EqBandIndex band, core::GainDb gain, core::FrequencyHz center, float q,
bool persist)
{
if (auto applied = dsp_.setEqBand(band, gain, center, q); !applied) {
return std::unexpected(core::StoreError::IoFailed);
}
profile_.eq.setBand(band, core::EqBandSettings{
.gain = gain,
.center = center,
.q = q,
});
if (persist) {
return persistProfile();
}
return {};
}
} // namespace audio