Add stereo and bass enhance APIs via PEQ overlay.

Expose POST /api/audio/stereo-enhance and bass-enhance with 0–100 levels,
persist enhancements in AudioProfile, and document the virtual EQ mapping in
the manual and SigmaStudio chapter.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 17:00:12 +02:00
co-authored by Cursor
parent 6c350f5c48
commit fa532e068f
22 changed files with 826 additions and 19 deletions
@@ -29,6 +29,9 @@ add_library(digiradio_core STATIC
"${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"
)
@@ -63,3 +66,7 @@ add_test(NAME biquad_design_test COMMAND biquad_design_test)
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)
@@ -13,6 +13,7 @@
#include "core/AudioProfile.hpp"
#include "core/AudioProfileJson.hpp"
#include "core/EnhanceLevel.hpp"
#include "core/EqBandIndex.hpp"
#include "core/FrequencyHz.hpp"
#include "core/GainDb.hpp"
@@ -52,6 +53,22 @@ namespace {
std::cerr << "round-trip mismatch\n";
return EXIT_FAILURE;
}
if (parsed->enhancements.stereo.value() != 0U
|| parsed->enhancements.bass.value() != 0U) {
std::cerr << "default enhancements mismatch\n";
return EXIT_FAILURE;
}
profile.enhancements.stereo = *core::EnhanceLevel::tryFromLevel(40U);
profile.enhancements.bass = *core::EnhanceLevel::tryFromLevel(75U);
const std::string json2 = core::serializeAudioProfileJson(profile);
const auto parsed2 = core::parseAudioProfileJson(json2);
if (!parsed2 || parsed2->enhancements.stereo.value() != 40U
|| parsed2->enhancements.bass.value() != 75U) {
std::cerr << "enhancements round-trip mismatch\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
@@ -0,0 +1,124 @@
/**
* @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;
}