Integrate DigiRadioFinale DSP program: exclusive source mux, real

Bass Boost1/SPhat1 enhancement blocks

Replaces the 74-parameter ADAU1701 program with the new 224-parameter
SigmaStudio export. The new netlist has no mixer -- MX1 is an
exclusive selector (DC1) passing through exactly one of three stereo
pairs (radio/bluetooth/beep) instead of blending them with independent
gains. core::MixSource/MixerState are gone; core::ActiveSource plus
IDsp::selectSource() replace applyMixer()/setInputVolume() throughout
the driver/service/API stack. AudioProfile.mixer -> activeSource;
the HTTP "mixer" JSON object -> a single "active_source" string.

DC1 turned out to need a raw 32-bit integer (0/1/2), not the 5.23
fixpoint its compiled TYPE_DC1 macro implies -- confirmed live by
writing both encodings and listening for which one actually switched
sources. Documented in Adau1701Driver::selectSource() and
adau1701::paramSourceIndex().

Bass Boost1 and SPhat1 are real ADI algorithm blocks in this revision,
so core::applyEnhancementsToEq() (the old EQ-band-overwrite hack for
bass/stereo enhancement) is deleted; IDsp::setBassBoostLevel()/
setStereoSpreadLevel() scale each block's compiled coefficients toward
identity/unity instead. Confirmed live: Bass Boost audible on real
program content (not a static test tone -- the algorithm is dynamics-
based), Stereo Spread audible but subtle. The EQ "locked" flag from
2026-08-24 (enhancements silently overwriting manually-set bands) no
longer applies to bands 1-5.

A full sweep of all 223 non-DC1 named parameters (writing each back to
its own compiled default) completed with zero failures, confirming the
whole new address space is reachable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 21:53:00 +02:00
co-authored by Claude Sonnet 5
parent b55f2dac9d
commit 3e5cfe814d
32 changed files with 5013 additions and 3351 deletions
-2
View File
@@ -19,11 +19,9 @@ idf_component_register(
"src/EqBandIndex.cpp"
"src/BiquadCoefficients.cpp"
"src/BiquadDesign.cpp"
"src/MixerState.cpp"
"src/EqProfile.cpp"
"src/EnhanceLevel.cpp"
"src/AudioEnhancements.cpp"
"src/EnhancementsDesign.cpp"
"src/AudioProfile.cpp"
"src/AudioProfileJson.cpp"
"src/Bt1035At.cpp"
@@ -0,0 +1,46 @@
/**
* @file ActiveSource.hpp
* @brief ADAU1701 MX1 input source selector.
*
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
*
* Copyright 2026 Michele Bigi
* SPDX-License-Identifier: Apache-2.0
*
* @author Michele Bigi
* @date 2026-08-25
*/
#pragma once
namespace core {
/**
* @brief ActiveSource — the one stereo pair MX1 currently passes through.
*
* @dname ActiveSource
* @return n/a (type)
* @pubstate n/a
*
* The DigiRadioFinale SigmaStudio revision (2026-08-25) replaced St Mixer1
* (which combined Si4684 + ESP32 + Beep simultaneously, each at its own
* gain) with MX1, a DC-controlled exclusive mux: only one of the three
* stereo pairs reaches Param EQ1 at a time, selected by the DC1 cell.
* Per-source gain trims (the old Si4674/ESP32 cells) no longer exist —
* level is controlled solely by Master Volume and the EQ, downstream of
* the selected source.
*
* @author Michele Bigi
* @date 2026-08-25
*/
enum class ActiveSource {
Radio, ///< Si4684 tuner (FM/DAB), MX1 pair index 0.
Bluetooth, ///< ESP32 I2S input, MX1 pair index 1.
Beep, ///< Internal test tone, MX1 pair index 2.
};
// DC1 is a raw 32-bit integer index (0/1/2), not the 5.23 fixpoint its
// compiled TYPE_DC1/VALUE_DC1 macros would suggest -- confirmed live
// 2026-08-25 (see adau1701::paramSourceIndex() and Adau1701Driver::
// selectSource()).
} // namespace core
@@ -17,19 +17,20 @@
namespace core {
/**
* @brief AudioEnhancements — psychoacoustic EQ overlays (0100 each).
* @brief AudioEnhancements — dedicated-block enhancement levels (0100).
*
* @dname AudioEnhancements
* @return n/a (type)
* @pubstate Mapped to PEQ bands at runtime (manual chapter sigmastudio).
* No dedicated SigmaStudio blocks; see \c applyEnhancementsToEq.
* @pubstate Applied via IDsp::setStereoSpreadLevel/setBassBoostLevel, which
* scale the SPhat1/Bass Boost1 SigmaStudio blocks toward their
* compiled response; does not touch EqProfile.
*
* @author Michele Bigi
* @date 2026-07-06
*/
struct AudioEnhancements {
EnhanceLevel stereo; ///< Stereo depth / presence (PEQ bands 35).
EnhanceLevel bass; ///< Bass emphasis (PEQ bands 12).
EnhanceLevel stereo; ///< Stereo spread (SPhat1 spatializer).
EnhanceLevel bass; ///< Bass boost intensity (Bass Boost1).
/**
* @brief factoryDefault — both enhancements off.
@@ -12,10 +12,10 @@
*/
#pragma once
#include "core/ActiveSource.hpp"
#include "core/AudioEnhancements.hpp"
#include "core/EqProfile.hpp"
#include "core/GainDb.hpp"
#include "core/MixerState.hpp"
namespace core {
@@ -31,7 +31,7 @@ namespace core {
* @date 2026-07-06
*/
struct AudioProfile {
MixerState mixer; ///< Input and stereo-mixer gains.
ActiveSource activeSource; ///< Which MX1 pair reaches Param EQ1.
EqProfile eq; ///< Six-band parametric EQ.
GainDb masterLeft; ///< Multiple 1 master volume, left.
GainDb masterRight; ///< Multiple 1 master volume, right.
@@ -1,39 +0,0 @@
/**
* @file EnhancementsDesign.hpp
* @brief Map enhancement levels onto Param EQ1 bands (host-testable).
*
* 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/AudioEnhancements.hpp"
#include "core/EqProfile.hpp"
namespace core {
/**
* @brief applyEnhancementsToEq — merge enhancement overlays into an EQ profile.
*
* @dname applyEnhancementsToEq
* @param base User EQ settings (bands not touched stay unchanged).
* @param enhancements Stereo and bass levels (0 = use base band only).
* @return EqProfile with affected PEQ bands updated for safeload.
* @pubstate none
*
* Stereo (bands 35): slight 1\,kHz cut plus 3/8\,kHz lift for depth.
* Bass (bands 12): 100\,Hz and 400\,Hz peaking boost.
* Band 0 (high-pass) is never modified.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] EqProfile applyEnhancementsToEq(
const EqProfile& base, const AudioEnhancements& enhancements) noexcept;
} // namespace core
+44 -24
View File
@@ -12,13 +12,13 @@
*/
#pragma once
#include "core/ActiveSource.hpp"
#include "core/AudioProfile.hpp"
#include "core/DspError.hpp"
#include "core/EnhanceLevel.hpp"
#include "core/EqBandIndex.hpp"
#include "core/FrequencyHz.hpp"
#include "core/GainDb.hpp"
#include "core/MixSource.hpp"
#include "core/MixerState.hpp"
#include "core/EqProfile.hpp"
#include <expected>
@@ -56,18 +56,18 @@ public:
const AudioProfile& profile) = 0;
/**
* @brief applyMixer — safeload input and stereo-mixer gains.
* @brief selectSource — safeload MX1's DC1 control to pick one source.
*
* @dname applyMixer
* @param mixer Per-source and St Mixer1 levels.
* @dname selectSource
* @param source Which stereo pair MX1 passes through to Param EQ1.
* @return Ok on success, or DspError.
* @pubstate writes ADAU1701 parameter RAM via safeload.
*
* @author Michele Bigi
* @date 2026-07-06
* @date 2026-08-25
*/
[[nodiscard]] virtual std::expected<void, DspError> applyMixer(
const MixerState& mixer) = 0;
[[nodiscard]] virtual std::expected<void, DspError> selectSource(
ActiveSource source) = 0;
/**
* @brief applyEq — safeload all six PEQ bands.
@@ -83,22 +83,6 @@ public:
[[nodiscard]] virtual std::expected<void, DspError> applyEq(
const EqProfile& eq) = 0;
/**
* @brief setInputVolume — safeload one input path (Si4684 or ESP32).
*
* @dname setInputVolume
* @param source Tuner or ESP32 I2S path.
* @param left Left channel gain.
* @param right Right channel gain.
* @return Ok on success, or DspError.
* @pubstate writes ADAU1701 parameter RAM via safeload.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] virtual std::expected<void, DspError> setInputVolume(
MixSource source, GainDb left, GainDb right) = 0;
/**
* @brief setMasterVolume — safeload Multiple 1 master output gain.
*
@@ -146,6 +130,42 @@ public:
[[nodiscard]] virtual std::expected<void, DspError> setBeepEnabled(
bool enabled) = 0;
/**
* @brief setBassBoostLevel — scale Bass Boost1 toward its compiled
* curve.
*
* @dname setBassBoostLevel
* @param level 0 = flat bypass, 100 = the full SigmaStudio-tuned
* Dynamic Bass Boost response.
* @return Ok on success, or DspError.
* @pubstate writes ADAU1701 parameter RAM via safeload. Linearly
* interpolates the compiled crossover filter and 33-point
* compander curve toward identity/unity; BASSFREQUENCY and
* the time constant are left at their tuned defaults.
*
* @author Michele Bigi
* @date 2026-08-25
*/
[[nodiscard]] virtual std::expected<void, DspError> setBassBoostLevel(
EnhanceLevel level) = 0;
/**
* @brief setStereoSpreadLevel — scale SPhat1's stereo spread amount.
*
* @dname setStereoSpreadLevel
* @param level 0 = no added spread, 100 = the full SigmaStudio-tuned
* SuperPhat spread.
* @return Ok on success, or DspError.
* @pubstate writes ADAU1701 parameter RAM via safeload. Scales
* SPREAD1/SPREAD2 linearly; the crossover filter and
* compander curve are left at their tuned defaults.
*
* @author Michele Bigi
* @date 2026-08-25
*/
[[nodiscard]] virtual std::expected<void, DspError> setStereoSpreadLevel(
EnhanceLevel level) = 0;
/**
* @brief writeRawParam — safeload any named Parameter RAM cell.
*
@@ -1,32 +0,0 @@
/**
* @file MixSource.hpp
* @brief ADAU1701 input path selector for per-source volume 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
namespace core {
/**
* @brief MixSource — Si4684 tuner or ESP32 I2S input on the ADAU1701.
*
* @dname MixSource
* @return n/a (type)
* @pubstate n/a
*
* @author Michele Bigi
* @date 2026-07-06
*/
enum class MixSource {
Si4684,
Esp32,
};
} // namespace core
@@ -1,64 +0,0 @@
/**
* @file MixerState.hpp
* @brief ADAU1701 input and stereo-mixer gain snapshot.
*
* 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/GainDb.hpp"
namespace core {
/**
* @brief MixerState — per-source and St Mixer1 levels for the ADAU1701.
*
* @dname MixerState
* @return n/a (type)
* @pubstate Immutable snapshot applied by AudioService via IDsp.
*
* Maps to SigmaStudio modules Si4674, ESP32, and St Mixer1.
*
* @author Michele Bigi
* @date 2026-07-06
*/
struct MixerState {
GainDb si4684Left; ///< Si4674 volume control, left channel.
GainDb si4684Right; ///< Si4674 volume control, right channel.
GainDb esp32Left; ///< ESP32 volume control, left channel.
GainDb esp32Right; ///< ESP32 volume control, right channel.
GainDb mixLeft; ///< St Mixer1 ST0 (Si4684) input level.
GainDb mixRight; ///< St Mixer1 ST1 (ESP32) input level.
/**
* @brief factoryDefault — unity gains on all paths.
*
* @dname factoryDefault
* @return MixerState with 0 dB on every control.
* @pubstate none
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] static MixerState factoryDefault() noexcept;
/**
* @brief radioFirst — Si4684 path open, ESP32 path muted.
*
* @dname radioFirst
* @return MixerState for FM/DAB tuner audio to the Bose I2S chain.
* @pubstate none
*
* @author Michele Bigi
* @date 2026-08-05
*/
[[nodiscard]] static MixerState radioFirst() noexcept;
};
} // namespace core
@@ -19,7 +19,7 @@ AudioProfile AudioProfile::factoryDefault() noexcept
{
const GainDb unity = GainDb::zero();
return AudioProfile{
.mixer = MixerState::factoryDefault(),
.activeSource = ActiveSource::Radio,
.eq = EqProfile::factoryDefault(),
.masterLeft = unity,
.masterRight = unity,
@@ -92,35 +92,48 @@ namespace {
return GainDb::tryFromDb(db);
}
[[nodiscard]] std::expected<MixerState, ParseError> parseMixerJson(
[[nodiscard]] std::string_view activeSourceName(ActiveSource source) noexcept
{
switch (source) {
case ActiveSource::Radio:
return "radio";
case ActiveSource::Bluetooth:
return "bluetooth";
case ActiveSource::Beep:
return "beep";
}
return "radio";
}
[[nodiscard]] std::expected<ActiveSource, ParseError> parseActiveSourceJson(
std::string_view json)
{
const std::size_t mixerStart = json.find("\"mixer\"");
if (mixerStart == std::string_view::npos) {
constexpr std::string_view kKey = "\"active_source\"";
const std::size_t needlePos = json.find(kKey);
if (needlePos == std::string_view::npos) {
return std::unexpected(ParseError::MissingField);
}
const std::string_view mixer = json.substr(mixerStart);
const auto si4684Left = parseGainField(mixer, "si4684_left_db");
const auto si4684Right = parseGainField(mixer, "si4684_right_db");
const auto esp32Left = parseGainField(mixer, "esp32_left_db");
const auto esp32Right = parseGainField(mixer, "esp32_right_db");
const auto mixLeft = parseGainField(mixer, "mix_left_db");
const auto mixRight = parseGainField(mixer, "mix_right_db");
if (!si4684Left || !si4684Right || !esp32Left || !esp32Right || !mixLeft
|| !mixRight) {
const std::size_t quoteStart = json.find('"', needlePos + kKey.size());
if (quoteStart == std::string_view::npos) {
return std::unexpected(ParseError::MissingField);
}
const std::size_t quoteEnd = json.find('"', quoteStart + 1U);
if (quoteEnd == std::string_view::npos) {
return std::unexpected(ParseError::MissingField);
}
const std::string_view value =
json.substr(quoteStart + 1U, quoteEnd - quoteStart - 1U);
return MixerState{
.si4684Left = *si4684Left,
.si4684Right = *si4684Right,
.esp32Left = *esp32Left,
.esp32Right = *esp32Right,
.mixLeft = *mixLeft,
.mixRight = *mixRight,
};
if (value == "radio") {
return ActiveSource::Radio;
}
if (value == "bluetooth") {
return ActiveSource::Bluetooth;
}
if (value == "beep") {
return ActiveSource::Beep;
}
return std::unexpected(ParseError::MissingField);
}
[[nodiscard]] std::expected<EqProfile, ParseError> parseEqJson(
@@ -207,14 +220,8 @@ namespace {
std::string serializeAudioProfileJson(const AudioProfile& profile)
{
std::ostringstream out;
const auto& m = profile.mixer;
out << "{\"mixer\":{"
<< "\"si4684_left_db\":" << m.si4684Left.value() << ','
<< "\"si4684_right_db\":" << m.si4684Right.value() << ','
<< "\"esp32_left_db\":" << m.esp32Left.value() << ','
<< "\"esp32_right_db\":" << m.esp32Right.value() << ','
<< "\"mix_left_db\":" << m.mixLeft.value() << ','
<< "\"mix_right_db\":" << m.mixRight.value() << "},"
out << "{\"active_source\":\"" << activeSourceName(profile.activeSource)
<< "\","
<< "\"master\":{"
<< "\"left_db\":" << profile.masterLeft.value() << ','
<< "\"right_db\":" << profile.masterRight.value() << "},"
@@ -223,19 +230,13 @@ std::string serializeAudioProfileJson(const AudioProfile& profile)
// 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;
// for it has zero audible effect, always). Since the 2026-08-25
// DigiRadioFinale revision, bass/stereo enhancement drive the dedicated
// Bass Boost1/SPhat1 blocks directly instead of overwriting EQ bands,
// so bands 1-5 are no longer locked by enhancement levels -- only
// band 0 remains permanently inert.
const bool locked[EqBandIndex::kBandCount] = {
true, bassLocks, bassLocks, stereoLocks, stereoLocks, stereoLocks,
true, false, false, false, false, false,
};
const auto& bands = profile.eq.bands();
@@ -263,9 +264,9 @@ std::expected<AudioProfile, ParseError> parseAudioProfileJson(
return std::unexpected(ParseError::InvalidJson);
}
const auto mixer = parseMixerJson(json);
if (!mixer) {
return std::unexpected(mixer.error());
const auto activeSource = parseActiveSourceJson(json);
if (!activeSource) {
return std::unexpected(activeSource.error());
}
const auto eq = parseEqJson(json);
@@ -290,7 +291,7 @@ std::expected<AudioProfile, ParseError> parseAudioProfileJson(
}
return AudioProfile{
.mixer = *mixer,
.activeSource = *activeSource,
.eq = *eq,
.masterLeft = *masterLeft,
.masterRight = *masterRight,
@@ -1,70 +0,0 @@
/**
* @file EnhancementsDesign.cpp
* @brief Enhancement-to-EQ mapping 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 "core/EnhancementsDesign.hpp"
#include "core/EqBandIndex.hpp"
#include "core/FrequencyHz.hpp"
#include "core/GainDb.hpp"
namespace core {
namespace {
void setBand(EqProfile& profile, std::uint8_t index, GainDb gain,
FrequencyHz center, float q) noexcept
{
const auto band = EqBandIndex::tryFromIndex(index);
if (!band) {
return;
}
profile.setBand(*band, EqBandSettings{
.gain = gain,
.center = center,
.q = q,
});
}
[[nodiscard]] GainDb gainFromDb(float db) noexcept
{
return *GainDb::tryFromDb(db);
}
} // namespace
EqProfile applyEnhancementsToEq(const EqProfile& base,
const AudioEnhancements& enhancements) noexcept
{
EqProfile profile = base;
if (enhancements.bass.value() > 0U) {
const float t = enhancements.bass.fraction();
setBand(profile, 1U, gainFromDb(9.0F * t),
*FrequencyHz::tryFromHz(100U), 0.9F);
setBand(profile, 2U, gainFromDb(3.0F * t),
*FrequencyHz::tryFromHz(400U), 1.0F);
}
if (enhancements.stereo.value() > 0U) {
const float t = enhancements.stereo.fraction();
setBand(profile, 3U, gainFromDb(-1.5F * t),
*FrequencyHz::tryFromHz(1000U), 1.0F);
setBand(profile, 4U, gainFromDb(2.0F * t),
*FrequencyHz::tryFromHz(3000U), 1.0F);
setBand(profile, 5U, gainFromDb(4.0F * t),
*FrequencyHz::tryFromHz(8000U), 1.0F);
}
return profile;
}
} // namespace core
@@ -1,45 +0,0 @@
/**
* @file MixerState.cpp
* @brief MixerState factory defaults.
*
* 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 "core/MixerState.hpp"
namespace core {
MixerState MixerState::factoryDefault() noexcept
{
const GainDb unity = GainDb::zero();
return MixerState{
.si4684Left = unity,
.si4684Right = unity,
.esp32Left = unity,
.esp32Right = unity,
.mixLeft = unity,
.mixRight = unity,
};
}
MixerState MixerState::radioFirst() noexcept
{
const GainDb unity = GainDb::zero();
const GainDb muted = *GainDb::tryFromDb(GainDb::kMinDb);
return MixerState{
.si4684Left = unity,
.si4684Right = unity,
.esp32Left = muted,
.esp32Right = muted,
.mixLeft = unity,
.mixRight = muted,
};
}
} // namespace core
@@ -31,11 +31,9 @@ add_library(digiradio_core STATIC
"${CORE_SRC_DIR}/EqBandIndex.cpp"
"${CORE_SRC_DIR}/BiquadCoefficients.cpp"
"${CORE_SRC_DIR}/BiquadDesign.cpp"
"${CORE_SRC_DIR}/MixerState.cpp"
"${CORE_SRC_DIR}/EqProfile.cpp"
"${CORE_SRC_DIR}/EnhanceLevel.cpp"
"${CORE_SRC_DIR}/AudioEnhancements.cpp"
"${CORE_SRC_DIR}/EnhancementsDesign.cpp"
"${CORE_SRC_DIR}/AudioProfile.cpp"
"${CORE_SRC_DIR}/AudioProfileJson.cpp"
"${CORE_SRC_DIR}/Bt1035At.cpp"
@@ -110,10 +108,6 @@ add_executable(audio_profile_json_test audio_profile_json_test.cpp)
target_link_libraries(audio_profile_json_test PRIVATE digiradio_core)
add_test(NAME audio_profile_json_test COMMAND audio_profile_json_test)
add_executable(enhancements_design_test enhancements_design_test.cpp)
target_link_libraries(enhancements_design_test PRIVATE digiradio_core)
add_test(NAME enhancements_design_test COMMAND enhancements_design_test)
add_executable(station_service_test station_service_test.cpp)
target_include_directories(station_service_test PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/host_stubs"
@@ -69,6 +69,23 @@ namespace {
std::cerr << "enhancements round-trip mismatch\n";
return EXIT_FAILURE;
}
if (parsed->activeSource != core::ActiveSource::Radio) {
std::cerr << "default active_source mismatch\n";
return EXIT_FAILURE;
}
profile.activeSource = core::ActiveSource::Bluetooth;
const std::string json3 = core::serializeAudioProfileJson(profile);
if (json3.find(R"("active_source":"bluetooth")") == std::string::npos) {
std::cerr << "active_source serialization mismatch: " << json3 << '\n';
return EXIT_FAILURE;
}
const auto parsed3 = core::parseAudioProfileJson(json3);
if (!parsed3 || parsed3->activeSource != core::ActiveSource::Bluetooth) {
std::cerr << "active_source round-trip mismatch\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
@@ -1,124 +0,0 @@
/**
* @file enhancements_design_test.cpp
* @brief Host tests for enhancement-to-EQ mapping.
*
* 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 "core/AudioEnhancements.hpp"
#include "core/EnhanceLevel.hpp"
#include "core/EnhancementsDesign.hpp"
#include "core/EqBandIndex.hpp"
#include "core/EqProfile.hpp"
#include <cmath>
#include <cstdlib>
#include <iostream>
namespace {
[[nodiscard]] bool nearlyEqual(float a, float b) noexcept
{
return std::fabs(a - b) < 0.01F;
}
[[nodiscard]] int runBassMappingTest()
{
const auto level = core::EnhanceLevel::tryFromLevel(100U);
if (!level) {
std::cerr << "level setup failed\n";
return EXIT_FAILURE;
}
core::AudioEnhancements enhancements = core::AudioEnhancements::factoryDefault();
enhancements.bass = *level;
const core::EqProfile base = core::EqProfile::factoryDefault();
const core::EqProfile effective =
core::applyEnhancementsToEq(base, enhancements);
const auto band1 = core::EqBandIndex::tryFromIndex(1U);
const auto band2 = core::EqBandIndex::tryFromIndex(2U);
if (!band1 || !band2) {
return EXIT_FAILURE;
}
if (!nearlyEqual(effective.band(*band1).gain.value(), 9.0F)
|| !nearlyEqual(effective.band(*band2).gain.value(), 3.0F)) {
std::cerr << "bass mapping mismatch\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
[[nodiscard]] int runStereoMappingTest()
{
const auto level = core::EnhanceLevel::tryFromLevel(50U);
if (!level) {
std::cerr << "level setup failed\n";
return EXIT_FAILURE;
}
core::AudioEnhancements enhancements = core::AudioEnhancements::factoryDefault();
enhancements.stereo = *level;
const core::EqProfile base = core::EqProfile::factoryDefault();
const core::EqProfile effective =
core::applyEnhancementsToEq(base, enhancements);
const auto band3 = core::EqBandIndex::tryFromIndex(3U);
const auto band4 = core::EqBandIndex::tryFromIndex(4U);
const auto band5 = core::EqBandIndex::tryFromIndex(5U);
if (!band3 || !band4 || !band5) {
return EXIT_FAILURE;
}
if (!nearlyEqual(effective.band(*band3).gain.value(), -0.75F)
|| !nearlyEqual(effective.band(*band4).gain.value(), 1.0F)
|| !nearlyEqual(effective.band(*band5).gain.value(), 2.0F)) {
std::cerr << "stereo mapping mismatch\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
[[nodiscard]] int runOffLeavesBaseTest()
{
const core::EqProfile base = core::EqProfile::factoryDefault();
const core::AudioEnhancements off = core::AudioEnhancements::factoryDefault();
const core::EqProfile effective = core::applyEnhancementsToEq(base, off);
const auto band1 = core::EqBandIndex::tryFromIndex(1U);
if (!band1) {
return EXIT_FAILURE;
}
if (!nearlyEqual(effective.band(*band1).gain.value(),
base.band(*band1).gain.value())) {
std::cerr << "off should leave base unchanged\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
} // namespace
int main()
{
if (runBassMappingTest() != EXIT_SUCCESS) {
return EXIT_FAILURE;
}
if (runStereoMappingTest() != EXIT_SUCCESS) {
return EXIT_FAILURE;
}
if (runOffLeavesBaseTest() != EXIT_SUCCESS) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
@@ -40,8 +40,8 @@ public:
return {};
}
[[nodiscard]] std::expected<void, core::DspError> applyMixer(
const core::MixerState&) override
[[nodiscard]] std::expected<void, core::DspError> selectSource(
core::ActiveSource) override
{
return {};
}
@@ -52,12 +52,6 @@ public:
return {};
}
[[nodiscard]] std::expected<void, core::DspError> setInputVolume(
core::MixSource, core::GainDb, core::GainDb) override
{
return {};
}
[[nodiscard]] std::expected<void, core::DspError> setMasterVolume(
core::GainDb, core::GainDb) override
{
@@ -76,6 +70,18 @@ public:
return {};
}
[[nodiscard]] std::expected<void, core::DspError> setBassBoostLevel(
core::EnhanceLevel) override
{
return {};
}
[[nodiscard]] std::expected<void, core::DspError> setStereoSpreadLevel(
core::EnhanceLevel) override
{
return {};
}
[[nodiscard]] std::expected<void, core::DspError> writeRawParam(
unsigned, float) override
{