Fix root cause of audio profile never persisting; document ADAU1701 registers
Root cause (found via SigmaStudio firmware analysis requested this session): NVS was initialized AFTER HardwareBootstrap::boot(), which internally calls AudioService::loadAndApply() to restore the saved mixer/EQ/master-volume profile. Every nvs_open() inside NvsAudioProfileStore::hasProfile()/loadProfile() failed with ESP_ERR_NVS_NOT_INITIALIZED (0x1101), silently swallowed as "no saved profile" -- the audio profile was never actually restored on any boot, regardless of how many times it was saved via PUT /api/audio/profile. Fixed by moving secure_store::initEncryptedStorage() to the top of app_main(), before HardwareBootstrap::boot() (nvs_flash_init() has no hardware dependency, so this is safe). Second, related bug: HardwareBootstrap::boot() called AudioService::applyRadioFirstMix() unconditionally right after loadAndApply(), discarding any just-restored mixer/master values on every boot. loadAndApply() now returns whether it actually restored a profile from NVS; the radio-first fallback only applies when nothing was saved. Verified live: a distinct mixer+master+5-EQ-band test pattern now survives a full reboot exactly as saved (previously always reset to factory default). DAB/FM/BT unaffected. Also: added a "locked" flag per EQ band in the audio profile JSON -- band 0 is always locked (fixed high-pass, Adau1701Driver::applyEq() never safeloads it) and bands 1-2/3-5 are locked whenever bass_level/stereo_level is active (core::applyEnhancementsToEq() overwrites them with formula-derived values). This was previously undiscoverable from the API -- GET echoed back the stored, inert value with no indication it wasn't what was actually playing. Full register-by-register analysis of the compiled SigmaStudio program (signal chain, all 74 Parameter RAM addresses grouped by function, HTTP API mapping, every endpoint tested live) in docs/adau1701-sigmastudio-analysis.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -220,6 +220,24 @@ std::string serializeAudioProfileJson(const AudioProfile& profile)
|
||||
<< "\"right_db\":" << profile.masterRight.value() << "},"
|
||||
<< "\"eq\":[";
|
||||
|
||||
// Per-band "locked" flag, added 2026-08-24: band 0 is a fixed high-pass
|
||||
// Adau1701Driver::applyEq() never safeloads (SigmaStudio band 1, ST0 in
|
||||
// the compiled program -- whatever gain_db/center_hz/q is stored/sent
|
||||
// for it has zero audible effect, always). Bands 1-2 are overwritten by
|
||||
// core::applyEnhancementsToEq() with formula-derived values whenever
|
||||
// bass_level > 0; bands 3-5 likewise whenever stereo_level > 0 -- a
|
||||
// manual edit to those bands is silently inaudible while the
|
||||
// corresponding enhancement is active. This was previously
|
||||
// undiscoverable from the API response (GET echoed back the stored,
|
||||
// inert value with no indication it wasn't what was actually playing);
|
||||
// "locked":true now tells a client to grey out that slider instead of
|
||||
// letting the user "fix" a value that can't take effect.
|
||||
const bool bassLocks = profile.enhancements.bass.value() > 0U;
|
||||
const bool stereoLocks = profile.enhancements.stereo.value() > 0U;
|
||||
const bool locked[EqBandIndex::kBandCount] = {
|
||||
true, bassLocks, bassLocks, stereoLocks, stereoLocks, stereoLocks,
|
||||
};
|
||||
|
||||
const auto& bands = profile.eq.bands();
|
||||
for (std::size_t i = 0; i < bands.size(); ++i) {
|
||||
if (i > 0U) {
|
||||
@@ -228,6 +246,7 @@ std::string serializeAudioProfileJson(const AudioProfile& profile)
|
||||
const auto& b = bands[i];
|
||||
out << "{\"gain_db\":" << b.gain.value()
|
||||
<< ",\"center_hz\":" << b.center.value() << ",\"q\":" << b.q
|
||||
<< ",\"locked\":" << (locked[i] ? "true" : "false")
|
||||
<< '}';
|
||||
}
|
||||
out << "],\"enhancements\":{"
|
||||
|
||||
@@ -37,7 +37,14 @@ constexpr char kProfileKey[] = "audio_profile";
|
||||
bool NvsAudioProfileStore::hasProfile() const
|
||||
{
|
||||
nvs_handle_t handle = 0;
|
||||
if (nvs_open(kNamespace, NVS_READONLY, &handle) != ESP_OK) {
|
||||
const esp_err_t openErr = nvs_open(kNamespace, NVS_READONLY, &handle);
|
||||
if (openErr != ESP_OK) {
|
||||
// ESP_ERR_NVS_NOT_INITIALIZED (0x1101) here means this was called
|
||||
// before secure_store::initEncryptedStorage() -- see 2026-08-24 fix
|
||||
// in main.cpp's app_main() boot order (NVS must init before
|
||||
// AudioService::loadAndApply(), which calls this).
|
||||
ESP_LOGW(kTag, "hasProfile: nvs_open failed (0x%x)",
|
||||
static_cast<unsigned>(openErr));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,16 +58,25 @@ public:
|
||||
* @brief loadAndApply — restore saved profile or factory default.
|
||||
*
|
||||
* @dname loadAndApply
|
||||
* @return Ok on success, or DspError from IDsp.
|
||||
* @return true if a saved profile was restored from the store, false
|
||||
* if none was found and AudioProfile::factoryDefault() was
|
||||
* applied instead; DspError from IDsp on safeload failure.
|
||||
* @pubstate updates profile_ and safeloads the ADAU1701.
|
||||
*
|
||||
* Call once after ADAU1701 boot. When no profile is stored, applies
|
||||
* AudioProfile::factoryDefault() without writing NVS.
|
||||
* Call once after NVS init (and ADAU1701 boot). Requires NVS to already
|
||||
* be initialized -- see the 2026-08-24 fix in main.cpp's app_main(),
|
||||
* which moved secure_store::initEncryptedStorage() ahead of
|
||||
* HardwareBootstrap::boot() specifically so this call can see a
|
||||
* previously-saved profile instead of always silently falling back to
|
||||
* default. Callers that want a sensible mixer/master fallback when
|
||||
* nothing was saved (see HardwareBootstrap::boot()'s
|
||||
* applyRadioFirstMix() call) should check the returned bool rather than
|
||||
* unconditionally overwriting whatever this restored.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<void, core::DspError> loadAndApply();
|
||||
[[nodiscard]] std::expected<bool, core::DspError> loadAndApply();
|
||||
|
||||
/**
|
||||
* @brief currentProfile — read the in-memory profile snapshot.
|
||||
|
||||
@@ -39,18 +39,26 @@ AudioService::AudioService(core::IDsp& dsp, core::IAudioProfileStore* store)
|
||||
{
|
||||
}
|
||||
|
||||
std::expected<void, core::DspError> AudioService::loadAndApply()
|
||||
std::expected<bool, core::DspError> AudioService::loadAndApply()
|
||||
{
|
||||
bool restored = false;
|
||||
if (store_ != nullptr && store_->hasProfile()) {
|
||||
if (auto loaded = store_->loadProfile(); loaded) {
|
||||
profile_ = *loaded;
|
||||
restored = true;
|
||||
ESP_LOGI(kTag, "audio profile loaded from NVS");
|
||||
} else {
|
||||
ESP_LOGW(kTag, "audio profile present but failed to load — "
|
||||
"using factory default");
|
||||
}
|
||||
} else {
|
||||
ESP_LOGI(kTag, "no saved audio profile — using factory default");
|
||||
}
|
||||
|
||||
if (auto applied = applyProfileToDsp(profile_); !applied) {
|
||||
return std::unexpected(core::DspError::SafeloadFailed);
|
||||
}
|
||||
return {};
|
||||
return restored;
|
||||
}
|
||||
|
||||
const core::AudioProfile& AudioService::currentProfile() const noexcept
|
||||
|
||||
Reference in New Issue
Block a user