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:
@@ -5,9 +5,13 @@ SigmaStudio project **DigiRadio**, IC1 = ADAU1701.
|
|||||||
Key files:
|
Key files:
|
||||||
- `DigiRadio_IC_1.h` — program/param RAM data + `default_download_IC_1()`
|
- `DigiRadio_IC_1.h` — program/param RAM data + `default_download_IC_1()`
|
||||||
- `DigiRadio_IC_1_REG.h` — register map
|
- `DigiRadio_IC_1_REG.h` — register map
|
||||||
- `DigiRadio_IC_1_PARAM.h` — parameter handles (safeload, Slice 5+)
|
- `DigiRadio_IC_1_PARAM.h` — parameter handles (safeload addresses)
|
||||||
|
|
||||||
The ESP32 replays `default_download_IC_1()` over I2C on every boot via
|
The ESP32 replays `default_download_IC_1()` over I2C on every boot via
|
||||||
`adau1701::Adau1701Driver` (see `components/drivers/adau1701/`).
|
`adau1701::Adau1701Driver`. Runtime mixer/EQ/master changes use the
|
||||||
|
ADAU1701 safeload mechanism (`sigma_safeload_param` /
|
||||||
|
`sigma_safeload_block` in `SigmaStudioFW.c`).
|
||||||
|
|
||||||
I2C address: 7-bit `0x34` (ADDR0=ADDR1=GND), matching `board_pins.hpp`.
|
I2C address: 7-bit `0x34` (ADDR0=ADDR1=GND), matching `board_pins.hpp`.
|
||||||
|
|
||||||
|
Sample rate: **48 kHz** (see `DigiRadio_NetList.xml`).
|
||||||
|
|||||||
@@ -49,6 +49,35 @@ void SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress,
|
|||||||
*/
|
*/
|
||||||
void adau1701_run_default_download(void);
|
void adau1701_run_default_download(void);
|
||||||
|
|
||||||
|
/** Safeload data register base (0x0810..0x0814). */
|
||||||
|
#define ADAU1701_SAFELOAD_DATA_BASE 0x0810U
|
||||||
|
/** Safeload address register base (0x0815..0x0819). */
|
||||||
|
#define ADAU1701_SAFELOAD_ADDR_BASE 0x0815U
|
||||||
|
/** DSP core control register (IST bit triggers safeload). */
|
||||||
|
#define ADAU1701_CORE_CONTROL_REG 0x081CU
|
||||||
|
/** IST bit value OR-ed into core control to commit safeload. */
|
||||||
|
#define ADAU1701_CORE_IST_TRIGGER 0x003CU
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Safeload one 8.23 fixpoint parameter (click-free update).
|
||||||
|
*
|
||||||
|
* @param paramAddr Parameter RAM address from DigiRadio_IC_1_PARAM.h.
|
||||||
|
* @param fixpoint 32-bit SigmaStudio fixpoint value.
|
||||||
|
* @return 0 on success, non-zero on I2C failure.
|
||||||
|
*/
|
||||||
|
int sigma_safeload_param(unsigned int paramAddr, int fixpoint);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Safeload up to five parameters in one IST transfer (e.g. biquad).
|
||||||
|
*
|
||||||
|
* @param count Number of pairs (1..5).
|
||||||
|
* @param paramAddrs Parameter RAM addresses.
|
||||||
|
* @param fixpoints 32-bit fixpoint values.
|
||||||
|
* @return 0 on success, non-zero on I2C failure.
|
||||||
|
*/
|
||||||
|
int sigma_safeload_block(unsigned char count, const unsigned int* paramAddrs,
|
||||||
|
const int* fixpoints);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ idf_component_register(
|
|||||||
"src/EmbeddedBlobReader.cpp"
|
"src/EmbeddedBlobReader.cpp"
|
||||||
"src/TunerJson.cpp"
|
"src/TunerJson.cpp"
|
||||||
"src/FrequencyKHz.cpp"
|
"src/FrequencyKHz.cpp"
|
||||||
|
"src/GainDb.cpp"
|
||||||
|
"src/FrequencyHz.cpp"
|
||||||
|
"src/EqBandIndex.cpp"
|
||||||
|
"src/BiquadCoefficients.cpp"
|
||||||
|
"src/BiquadDesign.cpp"
|
||||||
|
"src/MixerState.cpp"
|
||||||
|
"src/EqProfile.cpp"
|
||||||
|
"src/AudioProfile.cpp"
|
||||||
|
"src/AudioProfileJson.cpp"
|
||||||
INCLUDE_DIRS "include"
|
INCLUDE_DIRS "include"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* @file AudioProfile.hpp
|
||||||
|
* @brief Combined mixer, EQ, and master volume 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/EqProfile.hpp"
|
||||||
|
#include "core/GainDb.hpp"
|
||||||
|
#include "core/MixerState.hpp"
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief AudioProfile — full ADAU1701 user configuration snapshot.
|
||||||
|
*
|
||||||
|
* @dname AudioProfile
|
||||||
|
* @return n/a (type)
|
||||||
|
* @pubstate Plain aggregate persisted by IAudioProfileStore and applied by
|
||||||
|
* AudioService after boot.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
struct AudioProfile {
|
||||||
|
MixerState mixer; ///< Input and stereo-mixer gains.
|
||||||
|
EqProfile eq; ///< Six-band parametric EQ.
|
||||||
|
GainDb masterLeft; ///< Multiple 1 master volume, left.
|
||||||
|
GainDb masterRight; ///< Multiple 1 master volume, right.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief factoryDefault — factory-flat audio path (0 dB everywhere).
|
||||||
|
*
|
||||||
|
* @dname factoryDefault
|
||||||
|
* @return AudioProfile suitable for first boot and reset.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] static AudioProfile factoryDefault() noexcept;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* @file AudioProfileJson.hpp
|
||||||
|
* @brief JSON parse/serialise for audio profile API (pure core).
|
||||||
|
*
|
||||||
|
* 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/ParseError.hpp"
|
||||||
|
|
||||||
|
#include <expected>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief serializeAudioProfileJson — serialise profile for GET /api/audio.
|
||||||
|
*
|
||||||
|
* @dname serializeAudioProfileJson
|
||||||
|
* @param profile Domain snapshot from AudioService.
|
||||||
|
* @return JSON object string for the HTTP response body.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::string serializeAudioProfileJson(
|
||||||
|
const AudioProfile& profile);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief parseAudioProfileJson — parse PUT /api/audio/profile body.
|
||||||
|
*
|
||||||
|
* @dname parseAudioProfileJson
|
||||||
|
* @param json Untrusted request body.
|
||||||
|
* @return AudioProfile on success, or ParseError.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::expected<AudioProfile, ParseError> parseAudioProfileJson(
|
||||||
|
std::string_view json);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief serializeAudioSavedJson — success response after profile apply.
|
||||||
|
*
|
||||||
|
* @dname serializeAudioSavedJson
|
||||||
|
* @return JSON object \texttt{\{"status":"saved"\}}.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::string serializeAudioSavedJson();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief serializeAudioErrorJson — error response for audio routes.
|
||||||
|
*
|
||||||
|
* @dname serializeAudioErrorJson
|
||||||
|
* @param reason Safe token (never includes secrets).
|
||||||
|
* @return JSON object with status and reason fields.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::string serializeAudioErrorJson(std::string_view reason);
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* @file BiquadCoefficients.hpp
|
||||||
|
* @brief Float and fixpoint biquad coefficients for ADAU1701 PEQ.
|
||||||
|
*
|
||||||
|
* 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 <array>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief BiquadCoefficients — five PEQ coefficients (b0..a1).
|
||||||
|
*
|
||||||
|
* @dname BiquadCoefficients
|
||||||
|
* @return n/a (type)
|
||||||
|
* @pubstate Plain value type matching SigmaStudio Param EQ cell order.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
struct BiquadCoefficients {
|
||||||
|
float b0; ///< Numerator b0.
|
||||||
|
float b1; ///< Numerator b1.
|
||||||
|
float b2; ///< Numerator b2.
|
||||||
|
float a0; ///< Denominator a0 (ADI stores feedback terms).
|
||||||
|
float a1; ///< Denominator a1.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief toFixpoint823 — convert each coefficient to 8.23 fixpoint.
|
||||||
|
*
|
||||||
|
* @dname toFixpoint823
|
||||||
|
* @return Array of five 32-bit fixpoint words for safeload.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::array<std::int32_t, 5> toFixpoint823() const noexcept;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* @file BiquadDesign.hpp
|
||||||
|
* @brief Host-testable biquad coefficient design for ADAU1701 PEQ.
|
||||||
|
*
|
||||||
|
* 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/BiquadCoefficients.hpp"
|
||||||
|
#include "core/FrequencyHz.hpp"
|
||||||
|
#include "core/GainDb.hpp"
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
/** Sample rate of the DigiRadio SigmaStudio project (Hz). */
|
||||||
|
inline constexpr std::uint32_t kAdauSampleRateHz = 48000U;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief designPeakingEq — compute PEQ coefficients for one band.
|
||||||
|
*
|
||||||
|
* @dname designPeakingEq
|
||||||
|
* @param center Validated centre frequency.
|
||||||
|
* @param gain Band gain in dB (0 dB yields flat response).
|
||||||
|
* @param q Quality factor (typically 0.5..10).
|
||||||
|
* @return BiquadCoefficients in SigmaStudio Param EQ order.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* Uses the Robert Bristow-Johnson peaking EQ formulae at kAdauSampleRateHz.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] BiquadCoefficients designPeakingEq(FrequencyHz center, GainDb gain,
|
||||||
|
float q) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief designFlatEq — unity-gain bypass coefficients for a PEQ band.
|
||||||
|
*
|
||||||
|
* @dname designFlatEq
|
||||||
|
* @return Identity biquad (b0=1, others 0) as in SigmaStudio defaults.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] BiquadCoefficients designFlatEq() noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief gainDbToLinearFixpoint — map dB to ADAU 8.23 linear gain word.
|
||||||
|
*
|
||||||
|
* @dname gainDbToLinearFixpoint
|
||||||
|
* @param gain Validated gain in dB.
|
||||||
|
* @return 32-bit fixpoint suitable for safeload (0 dB = 0x00800000).
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::int32_t gainDbToLinearFixpoint(GainDb gain) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief floatToFixpoint823 — convert a float to ADAU 8.23 fixpoint.
|
||||||
|
*
|
||||||
|
* @dname floatToFixpoint823
|
||||||
|
* @param value Floating value (typically -16..+16 for filter coeffs).
|
||||||
|
* @return Rounded 32-bit fixpoint word.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::int32_t floatToFixpoint823(float value) noexcept;
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* @file DspError.hpp
|
||||||
|
* @brief Typed errors for DSP control operations.
|
||||||
|
*
|
||||||
|
* 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 DspError — failure causes for IDsp operations.
|
||||||
|
*
|
||||||
|
* @dname DspError
|
||||||
|
* @return n/a (type)
|
||||||
|
* @pubstate n/a
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
enum class DspError {
|
||||||
|
NotBooted,
|
||||||
|
SafeloadFailed,
|
||||||
|
InvalidParameter,
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* @file EqBandIndex.hpp
|
||||||
|
* @brief Strong index for ADAU1701 Param EQ1 bands (0–5).
|
||||||
|
*
|
||||||
|
* 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/ParseError.hpp"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <expected>
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief EqBandIndex — validated index into the 6-band Param EQ1 module.
|
||||||
|
*
|
||||||
|
* @dname EqBandIndex
|
||||||
|
* @return n/a (type)
|
||||||
|
* @pubstate Owns index_ in 0..kBandCount-1 after construction.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
class EqBandIndex {
|
||||||
|
public:
|
||||||
|
/** Number of parametric EQ bands in the SigmaStudio export. */
|
||||||
|
static constexpr std::uint8_t kBandCount = 6U;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief tryFromIndex — validate a band index at the boundary.
|
||||||
|
*
|
||||||
|
* @dname tryFromIndex
|
||||||
|
* @param index Untrusted band index from JSON input.
|
||||||
|
* @return EqBandIndex on success, or ParseError::MissingField.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] static std::expected<EqBandIndex, ParseError> tryFromIndex(
|
||||||
|
std::uint32_t index) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief value — read the stored band index.
|
||||||
|
*
|
||||||
|
* @dname value
|
||||||
|
* @return Band index 0..5.
|
||||||
|
* @pubstate reads index_.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::uint8_t value() const noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit EqBandIndex(std::uint8_t index) noexcept;
|
||||||
|
|
||||||
|
std::uint8_t index_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* @file EqBandSettings.hpp
|
||||||
|
* @brief Per-band EQ settings for EqProfile.
|
||||||
|
*
|
||||||
|
* 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/FrequencyHz.hpp"
|
||||||
|
#include "core/GainDb.hpp"
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief EqBandSettings — gain, centre frequency, and Q for one PEQ band.
|
||||||
|
*
|
||||||
|
* @dname EqBandSettings
|
||||||
|
* @return n/a (type)
|
||||||
|
* @pubstate Plain value type; q in 0.2..10.0 when validated via EqProfile.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
struct EqBandSettings {
|
||||||
|
GainDb gain; ///< Band gain in dB.
|
||||||
|
FrequencyHz center; ///< Centre frequency in Hz.
|
||||||
|
float q; ///< Quality factor.
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* @file EqProfile.hpp
|
||||||
|
* @brief Six-band parametric EQ profile for the ADAU1701.
|
||||||
|
*
|
||||||
|
* 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/EqBandIndex.hpp"
|
||||||
|
#include "core/EqBandSettings.hpp"
|
||||||
|
#include "core/FrequencyHz.hpp"
|
||||||
|
#include "core/GainDb.hpp"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief EqProfile — six PEQ bands persisted and applied via AudioService.
|
||||||
|
*
|
||||||
|
* @dname EqProfile
|
||||||
|
* @return n/a (type)
|
||||||
|
* @pubstate Owns bands_ array; default centres match typical listening curve.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
class EqProfile {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief factoryDefault — flat EQ at standard centre frequencies.
|
||||||
|
*
|
||||||
|
* @dname factoryDefault
|
||||||
|
* @return EqProfile with 0 dB on all bands.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] static EqProfile factoryDefault() noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief band — read settings for one PEQ band.
|
||||||
|
*
|
||||||
|
* @dname band
|
||||||
|
* @param index Band index 0..5.
|
||||||
|
* @return Const reference to band settings.
|
||||||
|
* @pubstate reads bands_.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] const EqBandSettings& band(EqBandIndex index) const noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief setBand — replace settings for one PEQ band.
|
||||||
|
*
|
||||||
|
* @dname setBand
|
||||||
|
* @param index Band index 0..5.
|
||||||
|
* @param settings New gain, centre, and Q.
|
||||||
|
* @pubstate writes bands_.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
void setBand(EqBandIndex index, EqBandSettings settings) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief bands — access the full band array.
|
||||||
|
*
|
||||||
|
* @dname bands
|
||||||
|
* @return Const reference to all six band settings.
|
||||||
|
* @pubstate reads bands_.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] const std::array<EqBandSettings, EqBandIndex::kBandCount>&
|
||||||
|
bands() const noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit EqProfile(std::array<EqBandSettings, EqBandIndex::kBandCount> bands)
|
||||||
|
noexcept;
|
||||||
|
|
||||||
|
std::array<EqBandSettings, EqBandIndex::kBandCount> bands_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* @file FrequencyHz.hpp
|
||||||
|
* @brief Strong type for audio centre frequencies (EQ bands).
|
||||||
|
*
|
||||||
|
* 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/ParseError.hpp"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <expected>
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief FrequencyHz — validated centre frequency for parametric EQ.
|
||||||
|
*
|
||||||
|
* @dname FrequencyHz
|
||||||
|
* @return n/a (type)
|
||||||
|
* @pubstate Owns hz_ within 20..20\,000 Hz (48 kHz SigmaStudio project).
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
class FrequencyHz {
|
||||||
|
public:
|
||||||
|
/** Minimum EQ centre frequency (Hz). */
|
||||||
|
static constexpr std::uint32_t kMinHz = 20U;
|
||||||
|
/** Maximum EQ centre frequency (Hz). */
|
||||||
|
static constexpr std::uint32_t kMaxHz = 20000U;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief tryFromHz — validate a centre frequency at the boundary.
|
||||||
|
*
|
||||||
|
* @dname tryFromHz
|
||||||
|
* @param hz Untrusted frequency in hertz.
|
||||||
|
* @return FrequencyHz on success, or ParseError::MissingField.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] static std::expected<FrequencyHz, ParseError> tryFromHz(
|
||||||
|
std::uint32_t hz) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief value — read the stored frequency in hertz.
|
||||||
|
*
|
||||||
|
* @dname value
|
||||||
|
* @return Validated centre frequency in Hz.
|
||||||
|
* @pubstate reads hz_.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::uint32_t value() const noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit FrequencyHz(std::uint32_t hz) noexcept;
|
||||||
|
|
||||||
|
std::uint32_t hz_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* @file GainDb.hpp
|
||||||
|
* @brief Strong type for decibel gain/attenuation values.
|
||||||
|
*
|
||||||
|
* 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/ParseError.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <expected>
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief GainDb — validated gain in decibels for DSP volume and EQ.
|
||||||
|
*
|
||||||
|
* @dname GainDb
|
||||||
|
* @return n/a (type)
|
||||||
|
* @pubstate Owns db_ within kMinDb..kMaxDb after construction.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
class GainDb {
|
||||||
|
public:
|
||||||
|
/** Minimum attenuation (effectively muted on the linear path). */
|
||||||
|
static constexpr float kMinDb = -96.0F;
|
||||||
|
/** Maximum boost allowed on the DSP path. */
|
||||||
|
static constexpr float kMaxDb = 12.0F;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief tryFromDb — validate a gain value at the HTTP boundary.
|
||||||
|
*
|
||||||
|
* @dname tryFromDb
|
||||||
|
* @param db Untrusted gain in decibels.
|
||||||
|
* @return GainDb on success, or ParseError::MissingField.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] static std::expected<GainDb, ParseError> tryFromDb(
|
||||||
|
float db) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief zero — unity gain (0 dB).
|
||||||
|
*
|
||||||
|
* @dname zero
|
||||||
|
* @return GainDb at 0 dB.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] static GainDb zero() noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief value — read the stored gain in decibels.
|
||||||
|
*
|
||||||
|
* @dname value
|
||||||
|
* @return Validated gain in dB.
|
||||||
|
* @pubstate reads db_.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] float value() const noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief linear — convert to linear amplitude (not dB).
|
||||||
|
*
|
||||||
|
* @dname linear
|
||||||
|
* @return Linear gain factor (1.0 at 0 dB).
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] float linear() const noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit GainDb(float db) noexcept;
|
||||||
|
|
||||||
|
float db_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* @file IAudioProfileStore.hpp
|
||||||
|
* @brief Persistence boundary for user audio 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/StoreError.hpp"
|
||||||
|
|
||||||
|
#include <expected>
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief IAudioProfileStore — load/save AudioProfile in NVS (non-secret).
|
||||||
|
*
|
||||||
|
* @dname IAudioProfileStore
|
||||||
|
* @return n/a (type)
|
||||||
|
* @pubstate Shell implementations own NVS handles; core uses fakes in tests.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
class IAudioProfileStore {
|
||||||
|
public:
|
||||||
|
virtual ~IAudioProfileStore() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief hasProfile — check whether a saved profile exists.
|
||||||
|
*
|
||||||
|
* @dname hasProfile
|
||||||
|
* @return true when loadProfile would succeed.
|
||||||
|
* @pubstate reads backing storage via implementation.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] virtual bool hasProfile() const = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief saveProfile — persist a validated audio profile.
|
||||||
|
*
|
||||||
|
* @dname saveProfile
|
||||||
|
* @param profile User mixer/EQ/master snapshot.
|
||||||
|
* @return Ok on success, or StoreError::IoFailed.
|
||||||
|
* @pubstate writes backing storage via implementation.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] virtual std::expected<void, StoreError> saveProfile(
|
||||||
|
const AudioProfile& profile) = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief loadProfile — read the stored audio profile.
|
||||||
|
*
|
||||||
|
* @dname loadProfile
|
||||||
|
* @return AudioProfile on success, or StoreError::NotFound / IoFailed.
|
||||||
|
* @pubstate reads backing storage via implementation.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] virtual std::expected<AudioProfile, StoreError> loadProfile()
|
||||||
|
const = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief clearProfile — erase the stored audio profile.
|
||||||
|
*
|
||||||
|
* @dname clearProfile
|
||||||
|
* @return Ok on success, or StoreError::IoFailed.
|
||||||
|
* @pubstate clears backing storage via implementation.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] virtual std::expected<void, StoreError> clearProfile() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* @file IDsp.hpp
|
||||||
|
* @brief Abstract ADAU1701 DSP control boundary (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/AudioProfile.hpp"
|
||||||
|
#include "core/DspError.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>
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief IDsp — hardware abstraction for ADAU1701 runtime control.
|
||||||
|
*
|
||||||
|
* @dname IDsp
|
||||||
|
* @return n/a (type)
|
||||||
|
* @pubstate Implemented by adau1701::Adau1701Dsp on device; fakes in tests.
|
||||||
|
*
|
||||||
|
* All parameter updates use safeload on the implementation side.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
class IDsp {
|
||||||
|
public:
|
||||||
|
virtual ~IDsp() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief applyProfile — safeload mixer, EQ, and master from a snapshot.
|
||||||
|
*
|
||||||
|
* @dname applyProfile
|
||||||
|
* @param profile Validated user configuration.
|
||||||
|
* @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> applyProfile(
|
||||||
|
const AudioProfile& profile) = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief applyMixer — safeload input and stereo-mixer gains.
|
||||||
|
*
|
||||||
|
* @dname applyMixer
|
||||||
|
* @param mixer Per-source and St Mixer1 levels.
|
||||||
|
* @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> applyMixer(
|
||||||
|
const MixerState& mixer) = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief applyEq — safeload all six PEQ bands.
|
||||||
|
*
|
||||||
|
* @dname applyEq
|
||||||
|
* @param eq Six-band parametric EQ settings.
|
||||||
|
* @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> 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.
|
||||||
|
*
|
||||||
|
* @dname setMasterVolume
|
||||||
|
* @param left Left master gain.
|
||||||
|
* @param right Right master 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> setMasterVolume(
|
||||||
|
GainDb left, GainDb right) = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 DspError.
|
||||||
|
* @pubstate writes five coefficients via a single safeload transfer.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] virtual std::expected<void, DspError> setEqBand(
|
||||||
|
EqBandIndex band, GainDb gain, FrequencyHz center, float q) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* @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
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* @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 left blend level.
|
||||||
|
GainDb mixRight; ///< St Mixer1 right blend level.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief factoryDefault — unity gains on all paths (radio-first mix).
|
||||||
|
*
|
||||||
|
* @dname factoryDefault
|
||||||
|
* @return MixerState with 0 dB on every control.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] static MixerState factoryDefault() noexcept;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* @file AudioProfile.cpp
|
||||||
|
* @brief AudioProfile 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/AudioProfile.hpp"
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
AudioProfile AudioProfile::factoryDefault() noexcept
|
||||||
|
{
|
||||||
|
const GainDb unity = GainDb::zero();
|
||||||
|
return AudioProfile{
|
||||||
|
.mixer = MixerState::factoryDefault(),
|
||||||
|
.eq = EqProfile::factoryDefault(),
|
||||||
|
.masterLeft = unity,
|
||||||
|
.masterRight = unity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
/**
|
||||||
|
* @file AudioProfileJson.cpp
|
||||||
|
* @brief AudioProfileJson 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/AudioProfileJson.hpp"
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] bool extractJsonFloat(std::string_view json,
|
||||||
|
std::string_view key,
|
||||||
|
float& out)
|
||||||
|
{
|
||||||
|
const std::string needle =
|
||||||
|
std::string("\"") + std::string(key) + "\":";
|
||||||
|
const std::size_t start = json.find(needle);
|
||||||
|
if (start == std::string_view::npos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const std::size_t valueStart = start + needle.size();
|
||||||
|
char* end = nullptr;
|
||||||
|
out = std::strtof(json.data() + valueStart, &end);
|
||||||
|
return end != json.data() + valueStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool extractJsonUint(std::string_view json,
|
||||||
|
std::string_view key,
|
||||||
|
unsigned long& out)
|
||||||
|
{
|
||||||
|
const std::string needle =
|
||||||
|
std::string("\"") + std::string(key) + "\":";
|
||||||
|
const std::size_t start = json.find(needle);
|
||||||
|
if (start == std::string_view::npos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const std::size_t valueStart = start + needle.size();
|
||||||
|
char* end = nullptr;
|
||||||
|
out = std::strtoul(json.data() + valueStart, &end, 10);
|
||||||
|
return end != json.data() + valueStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::expected<GainDb, ParseError> parseGainField(
|
||||||
|
std::string_view json, std::string_view key)
|
||||||
|
{
|
||||||
|
float db = 0.0F;
|
||||||
|
if (!extractJsonFloat(json, key, db)) {
|
||||||
|
return std::unexpected(ParseError::MissingField);
|
||||||
|
}
|
||||||
|
return GainDb::tryFromDb(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::expected<MixerState, ParseError> parseMixerJson(
|
||||||
|
std::string_view json)
|
||||||
|
{
|
||||||
|
const std::size_t mixerStart = json.find("\"mixer\"");
|
||||||
|
if (mixerStart == 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) {
|
||||||
|
return std::unexpected(ParseError::MissingField);
|
||||||
|
}
|
||||||
|
|
||||||
|
return MixerState{
|
||||||
|
.si4684Left = *si4684Left,
|
||||||
|
.si4684Right = *si4684Right,
|
||||||
|
.esp32Left = *esp32Left,
|
||||||
|
.esp32Right = *esp32Right,
|
||||||
|
.mixLeft = *mixLeft,
|
||||||
|
.mixRight = *mixRight,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::expected<EqProfile, ParseError> parseEqJson(
|
||||||
|
std::string_view json)
|
||||||
|
{
|
||||||
|
EqProfile profile = EqProfile::factoryDefault();
|
||||||
|
const std::size_t eqStart = json.find("\"eq\"");
|
||||||
|
if (eqStart == std::string_view::npos) {
|
||||||
|
return std::unexpected(ParseError::MissingField);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view rest = json.substr(eqStart);
|
||||||
|
for (std::uint8_t band = 0; band < EqBandIndex::kBandCount; ++band) {
|
||||||
|
const std::size_t objStart = rest.find('{');
|
||||||
|
if (objStart == std::string_view::npos) {
|
||||||
|
return std::unexpected(ParseError::MissingField);
|
||||||
|
}
|
||||||
|
const std::size_t objEnd = rest.find('}', objStart);
|
||||||
|
if (objEnd == std::string_view::npos) {
|
||||||
|
return std::unexpected(ParseError::MissingField);
|
||||||
|
}
|
||||||
|
const std::string_view obj = rest.substr(objStart, objEnd - objStart + 1U);
|
||||||
|
|
||||||
|
float gainDb = 0.0F;
|
||||||
|
unsigned long centerHz = 0U;
|
||||||
|
float q = 0.0F;
|
||||||
|
if (!extractJsonFloat(obj, "gain_db", gainDb)
|
||||||
|
|| !extractJsonUint(obj, "center_hz", centerHz)
|
||||||
|
|| !extractJsonFloat(obj, "q", q)) {
|
||||||
|
return std::unexpected(ParseError::MissingField);
|
||||||
|
}
|
||||||
|
if (q < 0.2F || q > 10.0F) {
|
||||||
|
return std::unexpected(ParseError::MissingField);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto gain = GainDb::tryFromDb(gainDb);
|
||||||
|
const auto center = FrequencyHz::tryFromHz(
|
||||||
|
static_cast<std::uint32_t>(centerHz));
|
||||||
|
const auto index = EqBandIndex::tryFromIndex(band);
|
||||||
|
if (!gain || !center || !index) {
|
||||||
|
return std::unexpected(ParseError::MissingField);
|
||||||
|
}
|
||||||
|
|
||||||
|
profile.setBand(*index, EqBandSettings{
|
||||||
|
.gain = *gain,
|
||||||
|
.center = *center,
|
||||||
|
.q = q,
|
||||||
|
});
|
||||||
|
rest = rest.substr(objEnd + 1U);
|
||||||
|
}
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // 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() << "},"
|
||||||
|
<< "\"master\":{"
|
||||||
|
<< "\"left_db\":" << profile.masterLeft.value() << ','
|
||||||
|
<< "\"right_db\":" << profile.masterRight.value() << "},"
|
||||||
|
<< "\"eq\":[";
|
||||||
|
|
||||||
|
const auto& bands = profile.eq.bands();
|
||||||
|
for (std::size_t i = 0; i < bands.size(); ++i) {
|
||||||
|
if (i > 0U) {
|
||||||
|
out << ',';
|
||||||
|
}
|
||||||
|
const auto& b = bands[i];
|
||||||
|
out << "{\"gain_db\":" << b.gain.value()
|
||||||
|
<< ",\"center_hz\":" << b.center.value() << ",\"q\":" << b.q
|
||||||
|
<< '}';
|
||||||
|
}
|
||||||
|
out << "]}";
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::expected<AudioProfile, ParseError> parseAudioProfileJson(
|
||||||
|
std::string_view json)
|
||||||
|
{
|
||||||
|
if (json.find('{') == std::string_view::npos) {
|
||||||
|
return std::unexpected(ParseError::InvalidJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto mixer = parseMixerJson(json);
|
||||||
|
if (!mixer) {
|
||||||
|
return std::unexpected(mixer.error());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto eq = parseEqJson(json);
|
||||||
|
if (!eq) {
|
||||||
|
return std::unexpected(eq.error());
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t masterStart = json.find("\"master\"");
|
||||||
|
if (masterStart == std::string_view::npos) {
|
||||||
|
return std::unexpected(ParseError::MissingField);
|
||||||
|
}
|
||||||
|
const std::string_view master = json.substr(masterStart);
|
||||||
|
const auto masterLeft = parseGainField(master, "left_db");
|
||||||
|
const auto masterRight = parseGainField(master, "right_db");
|
||||||
|
if (!masterLeft || !masterRight) {
|
||||||
|
return std::unexpected(ParseError::MissingField);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AudioProfile{
|
||||||
|
.mixer = *mixer,
|
||||||
|
.eq = *eq,
|
||||||
|
.masterLeft = *masterLeft,
|
||||||
|
.masterRight = *masterRight,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string serializeAudioSavedJson()
|
||||||
|
{
|
||||||
|
return R"({"status":"saved"})";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string serializeAudioErrorJson(std::string_view reason)
|
||||||
|
{
|
||||||
|
std::ostringstream out;
|
||||||
|
out << R"({"status":"error","reason":")" << reason << R"("})";
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* @file BiquadCoefficients.cpp
|
||||||
|
* @brief BiquadCoefficients fixpoint conversion.
|
||||||
|
*
|
||||||
|
* 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/BiquadCoefficients.hpp"
|
||||||
|
#include "core/BiquadDesign.hpp"
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
std::array<std::int32_t, 5> BiquadCoefficients::toFixpoint823() const noexcept
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
floatToFixpoint823(b0),
|
||||||
|
floatToFixpoint823(b1),
|
||||||
|
floatToFixpoint823(b2),
|
||||||
|
floatToFixpoint823(a0),
|
||||||
|
floatToFixpoint823(a1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* @file BiquadDesign.cpp
|
||||||
|
* @brief Biquad coefficient design and ADAU fixpoint helpers.
|
||||||
|
*
|
||||||
|
* 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/BiquadDesign.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <numbers>
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr float kPi = std::numbers::pi_v<float>;
|
||||||
|
|
||||||
|
[[nodiscard]] float clampQ(float q) noexcept
|
||||||
|
{
|
||||||
|
if (q < 0.2F) {
|
||||||
|
return 0.2F;
|
||||||
|
}
|
||||||
|
if (q > 10.0F) {
|
||||||
|
return 10.0F;
|
||||||
|
}
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::int32_t floatToFixpoint823(float value) noexcept
|
||||||
|
{
|
||||||
|
const double scaled =
|
||||||
|
static_cast<double>(value) * static_cast<double>(1U << 23);
|
||||||
|
const auto rounded = static_cast<std::int64_t>(std::llround(scaled));
|
||||||
|
return static_cast<std::int32_t>(rounded);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::int32_t gainDbToLinearFixpoint(GainDb gain) noexcept
|
||||||
|
{
|
||||||
|
return floatToFixpoint823(gain.linear());
|
||||||
|
}
|
||||||
|
|
||||||
|
BiquadCoefficients designFlatEq() noexcept
|
||||||
|
{
|
||||||
|
return BiquadCoefficients{
|
||||||
|
.b0 = 1.0F,
|
||||||
|
.b1 = 0.0F,
|
||||||
|
.b2 = 0.0F,
|
||||||
|
.a0 = 0.0F,
|
||||||
|
.a1 = 0.0F,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
BiquadCoefficients designPeakingEq(FrequencyHz center, GainDb gain,
|
||||||
|
float q) noexcept
|
||||||
|
{
|
||||||
|
if (std::fabs(gain.value()) < 0.001F) {
|
||||||
|
return designFlatEq();
|
||||||
|
}
|
||||||
|
|
||||||
|
const float qClamped = clampQ(q);
|
||||||
|
const float fs = static_cast<float>(kAdauSampleRateHz);
|
||||||
|
const float f0 = static_cast<float>(center.value());
|
||||||
|
const float a = std::pow(10.0F, gain.value() / 40.0F);
|
||||||
|
const float omega = 2.0F * kPi * f0 / fs;
|
||||||
|
const float sinOmega = std::sin(omega);
|
||||||
|
const float cosOmega = std::cos(omega);
|
||||||
|
const float alpha = sinOmega / (2.0F * qClamped);
|
||||||
|
|
||||||
|
const float b0 = 1.0F + alpha * a;
|
||||||
|
const float b1 = -2.0F * cosOmega;
|
||||||
|
const float b2 = 1.0F - alpha * a;
|
||||||
|
const float a0 = 1.0F + alpha / a;
|
||||||
|
const float a1 = -2.0F * cosOmega;
|
||||||
|
const float a2 = 1.0F - alpha / a;
|
||||||
|
|
||||||
|
return BiquadCoefficients{
|
||||||
|
.b0 = b0 / a0,
|
||||||
|
.b1 = b1 / a0,
|
||||||
|
.b2 = b2 / a0,
|
||||||
|
.a0 = a1 / a0,
|
||||||
|
.a1 = a2 / a0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* @file EqBandIndex.cpp
|
||||||
|
* @brief EqBandIndex 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/EqBandIndex.hpp"
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
EqBandIndex::EqBandIndex(std::uint8_t index) noexcept
|
||||||
|
: index_(index)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::expected<EqBandIndex, ParseError> EqBandIndex::tryFromIndex(
|
||||||
|
std::uint32_t index) noexcept
|
||||||
|
{
|
||||||
|
if (index >= kBandCount) {
|
||||||
|
return std::unexpected(ParseError::MissingField);
|
||||||
|
}
|
||||||
|
return EqBandIndex(static_cast<std::uint8_t>(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint8_t EqBandIndex::value() const noexcept
|
||||||
|
{
|
||||||
|
return index_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* @file EqProfile.cpp
|
||||||
|
* @brief EqProfile 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/EqProfile.hpp"
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr std::array<std::uint32_t, EqBandIndex::kBandCount> kDefaultCenters{
|
||||||
|
40U, 100U, 250U, 1000U, 4000U, 12000U};
|
||||||
|
|
||||||
|
[[nodiscard]] std::array<EqBandSettings, EqBandIndex::kBandCount>
|
||||||
|
makeDefaultBands() noexcept
|
||||||
|
{
|
||||||
|
return std::array<EqBandSettings, EqBandIndex::kBandCount>{
|
||||||
|
EqBandSettings{GainDb::zero(), *FrequencyHz::tryFromHz(kDefaultCenters[0]),
|
||||||
|
1.414F},
|
||||||
|
EqBandSettings{GainDb::zero(), *FrequencyHz::tryFromHz(kDefaultCenters[1]),
|
||||||
|
1.414F},
|
||||||
|
EqBandSettings{GainDb::zero(), *FrequencyHz::tryFromHz(kDefaultCenters[2]),
|
||||||
|
1.414F},
|
||||||
|
EqBandSettings{GainDb::zero(), *FrequencyHz::tryFromHz(kDefaultCenters[3]),
|
||||||
|
1.414F},
|
||||||
|
EqBandSettings{GainDb::zero(), *FrequencyHz::tryFromHz(kDefaultCenters[4]),
|
||||||
|
1.414F},
|
||||||
|
EqBandSettings{GainDb::zero(), *FrequencyHz::tryFromHz(kDefaultCenters[5]),
|
||||||
|
1.414F},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
EqProfile::EqProfile(
|
||||||
|
std::array<EqBandSettings, EqBandIndex::kBandCount> bands) noexcept
|
||||||
|
: bands_(bands)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
EqProfile EqProfile::factoryDefault() noexcept
|
||||||
|
{
|
||||||
|
return EqProfile(makeDefaultBands());
|
||||||
|
}
|
||||||
|
|
||||||
|
const EqBandSettings& EqProfile::band(EqBandIndex index) const noexcept
|
||||||
|
{
|
||||||
|
return bands_[index.value()];
|
||||||
|
}
|
||||||
|
|
||||||
|
void EqProfile::setBand(EqBandIndex index, EqBandSettings settings) noexcept
|
||||||
|
{
|
||||||
|
bands_[index.value()] = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::array<EqBandSettings, EqBandIndex::kBandCount>& EqProfile::bands()
|
||||||
|
const noexcept
|
||||||
|
{
|
||||||
|
return bands_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* @file FrequencyHz.cpp
|
||||||
|
* @brief FrequencyHz 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/FrequencyHz.hpp"
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
FrequencyHz::FrequencyHz(std::uint32_t hz) noexcept
|
||||||
|
: hz_(hz)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::expected<FrequencyHz, ParseError> FrequencyHz::tryFromHz(
|
||||||
|
std::uint32_t hz) noexcept
|
||||||
|
{
|
||||||
|
if (hz < kMinHz || hz > kMaxHz) {
|
||||||
|
return std::unexpected(ParseError::MissingField);
|
||||||
|
}
|
||||||
|
return FrequencyHz(hz);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t FrequencyHz::value() const noexcept
|
||||||
|
{
|
||||||
|
return hz_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* @file GainDb.cpp
|
||||||
|
* @brief GainDb 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/GainDb.hpp"
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
GainDb::GainDb(float db) noexcept
|
||||||
|
: db_(db)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::expected<GainDb, ParseError> GainDb::tryFromDb(float db) noexcept
|
||||||
|
{
|
||||||
|
if (db < kMinDb || db > kMaxDb) {
|
||||||
|
return std::unexpected(ParseError::MissingField);
|
||||||
|
}
|
||||||
|
return GainDb(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
GainDb GainDb::zero() noexcept
|
||||||
|
{
|
||||||
|
return GainDb(0.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
float GainDb::value() const noexcept
|
||||||
|
{
|
||||||
|
return db_;
|
||||||
|
}
|
||||||
|
|
||||||
|
float GainDb::linear() const noexcept
|
||||||
|
{
|
||||||
|
return std::pow(10.0F, db_ / 20.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* @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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -22,6 +22,15 @@ add_library(digiradio_core STATIC
|
|||||||
"${CORE_SRC_DIR}/EmbeddedBlobReader.cpp"
|
"${CORE_SRC_DIR}/EmbeddedBlobReader.cpp"
|
||||||
"${CORE_SRC_DIR}/TunerJson.cpp"
|
"${CORE_SRC_DIR}/TunerJson.cpp"
|
||||||
"${CORE_SRC_DIR}/FrequencyKHz.cpp"
|
"${CORE_SRC_DIR}/FrequencyKHz.cpp"
|
||||||
|
"${CORE_SRC_DIR}/GainDb.cpp"
|
||||||
|
"${CORE_SRC_DIR}/FrequencyHz.cpp"
|
||||||
|
"${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}/AudioProfile.cpp"
|
||||||
|
"${CORE_SRC_DIR}/AudioProfileJson.cpp"
|
||||||
)
|
)
|
||||||
target_include_directories(digiradio_core PUBLIC
|
target_include_directories(digiradio_core PUBLIC
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/../include"
|
"${CMAKE_CURRENT_SOURCE_DIR}/../include"
|
||||||
@@ -46,3 +55,11 @@ add_test(NAME tuner_json_test COMMAND tuner_json_test)
|
|||||||
add_executable(frequency_khz_test frequency_khz_test.cpp)
|
add_executable(frequency_khz_test frequency_khz_test.cpp)
|
||||||
target_link_libraries(frequency_khz_test PRIVATE digiradio_core)
|
target_link_libraries(frequency_khz_test PRIVATE digiradio_core)
|
||||||
add_test(NAME frequency_khz_test COMMAND frequency_khz_test)
|
add_test(NAME frequency_khz_test COMMAND frequency_khz_test)
|
||||||
|
|
||||||
|
add_executable(biquad_design_test biquad_design_test.cpp)
|
||||||
|
target_link_libraries(biquad_design_test PRIVATE digiradio_core)
|
||||||
|
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)
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* @file audio_profile_json_test.cpp
|
||||||
|
* @brief Host tests for AudioProfile JSON round-trip.
|
||||||
|
*
|
||||||
|
* 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/AudioProfile.hpp"
|
||||||
|
#include "core/AudioProfileJson.hpp"
|
||||||
|
#include "core/EqBandIndex.hpp"
|
||||||
|
#include "core/FrequencyHz.hpp"
|
||||||
|
#include "core/GainDb.hpp"
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] int runRoundTripTest()
|
||||||
|
{
|
||||||
|
core::AudioProfile profile = core::AudioProfile::factoryDefault();
|
||||||
|
const auto band = core::EqBandIndex::tryFromIndex(2U);
|
||||||
|
const auto gain = core::GainDb::tryFromDb(3.0F);
|
||||||
|
const auto center = core::FrequencyHz::tryFromHz(500U);
|
||||||
|
if (!band || !gain || !center) {
|
||||||
|
std::cerr << "test setup failed\n";
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
profile.eq.setBand(*band,
|
||||||
|
core::EqBandSettings{
|
||||||
|
.gain = *gain,
|
||||||
|
.center = *center,
|
||||||
|
.q = 2.0F,
|
||||||
|
});
|
||||||
|
|
||||||
|
const std::string json = core::serializeAudioProfileJson(profile);
|
||||||
|
const auto parsed = core::parseAudioProfileJson(json);
|
||||||
|
if (!parsed) {
|
||||||
|
std::cerr << "parse failed\n";
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& b = parsed->eq.band(*band);
|
||||||
|
if (b.gain.value() != 3.0F || b.center.value() != 500U
|
||||||
|
|| b.q != 2.0F) {
|
||||||
|
std::cerr << "round-trip mismatch\n";
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
if (runRoundTripTest() != EXIT_SUCCESS) {
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* @file biquad_design_test.cpp
|
||||||
|
* @brief Host tests for ADAU biquad design and fixpoint conversion.
|
||||||
|
*
|
||||||
|
* 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/BiquadDesign.hpp"
|
||||||
|
#include "core/FrequencyHz.hpp"
|
||||||
|
#include "core/GainDb.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] int runUnityGainFixpointTest()
|
||||||
|
{
|
||||||
|
const auto gain = core::GainDb::zero();
|
||||||
|
const std::int32_t fix = core::gainDbToLinearFixpoint(gain);
|
||||||
|
if (fix != 0x00800000) {
|
||||||
|
std::cerr << "expected 0 dB fixpoint 0x00800000, got 0x"
|
||||||
|
<< std::hex << fix << std::dec << '\n';
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] int runFlatBiquadTest()
|
||||||
|
{
|
||||||
|
const core::BiquadCoefficients flat = core::designFlatEq();
|
||||||
|
if (std::fabs(flat.b0 - 1.0F) > 0.001F || std::fabs(flat.b1) > 0.001F
|
||||||
|
|| std::fabs(flat.b2) > 0.001F || std::fabs(flat.a0) > 0.001F
|
||||||
|
|| std::fabs(flat.a1) > 0.001F) {
|
||||||
|
std::cerr << "flat biquad coefficients unexpected\n";
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto center = core::FrequencyHz::tryFromHz(1000U);
|
||||||
|
const auto zero = core::GainDb::zero();
|
||||||
|
const core::BiquadCoefficients peaking =
|
||||||
|
core::designPeakingEq(*center, zero, 1.414F);
|
||||||
|
if (std::fabs(peaking.b0 - 1.0F) > 0.001F) {
|
||||||
|
std::cerr << "0 dB peaking should match flat b0\n";
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
if (runUnityGainFixpointTest() != EXIT_SUCCESS) {
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
if (runFlatBiquadTest() != EXIT_SUCCESS) {
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
@@ -3,12 +3,13 @@ set(ADAU_FW_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../Firmware/ADAU1701-Firmware")
|
|||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS
|
SRCS
|
||||||
"src/Adau1701Driver.cpp"
|
"src/Adau1701Driver.cpp"
|
||||||
|
"src/Adau1701Dsp.cpp"
|
||||||
"src/SigmaStudioFW.c"
|
"src/SigmaStudioFW.c"
|
||||||
"src/adau1701_program.c"
|
"src/adau1701_program.c"
|
||||||
INCLUDE_DIRS
|
INCLUDE_DIRS
|
||||||
"include"
|
"include"
|
||||||
"${ADAU_FW_DIR}"
|
"${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)
|
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* @file Adau1701Driver.hpp
|
* @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
|
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||||
*
|
*
|
||||||
@@ -14,6 +14,15 @@
|
|||||||
|
|
||||||
#include "adau1701/Adau1701Error.hpp"
|
#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>
|
#include <expected>
|
||||||
|
|
||||||
namespace adau1701 {
|
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
|
* @dname Adau1701Driver
|
||||||
* @param pins Board wiring for I2C and RESET#.
|
* @param pins Board wiring for I2C and RESET#.
|
||||||
@@ -45,7 +54,8 @@ struct Adau1701Pins {
|
|||||||
* successful default_download replay.
|
* successful default_download replay.
|
||||||
*
|
*
|
||||||
* Writes the SigmaStudio export from Firmware/ADAU1701-Firmware on every
|
* 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
|
* @author Michele Bigi
|
||||||
* @date 2026-07-06
|
* @date 2026-07-06
|
||||||
@@ -102,7 +112,104 @@ public:
|
|||||||
*/
|
*/
|
||||||
[[nodiscard]] bool isBooted() const noexcept;
|
[[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:
|
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_;
|
Adau1701Pins pins_;
|
||||||
bool booted_;
|
bool booted_;
|
||||||
void* i2cBus_;
|
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,
|
I2cInitFailed,
|
||||||
ResetFailed,
|
ResetFailed,
|
||||||
DownloadFailed,
|
DownloadFailed,
|
||||||
|
NotBooted,
|
||||||
|
SafeloadFailed,
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace adau1701
|
} // 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/Adau1701Driver.hpp"
|
||||||
|
|
||||||
|
#include "adau1701/Adau1701ParamMap.hpp"
|
||||||
|
|
||||||
|
#include "core/BiquadDesign.hpp"
|
||||||
|
|
||||||
|
#include "DigiRadio_IC_1_PARAM.h"
|
||||||
#include "SigmaStudioFW.h"
|
#include "SigmaStudioFW.h"
|
||||||
|
|
||||||
#include "driver/gpio.h"
|
#include "driver/gpio.h"
|
||||||
@@ -115,4 +120,139 @@ bool Adau1701Driver::isBooted() const noexcept
|
|||||||
return booted_;
|
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
|
} // 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);
|
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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ idf_component_register(
|
|||||||
"src/NetBootstrap.cpp"
|
"src/NetBootstrap.cpp"
|
||||||
INCLUDE_DIRS "include"
|
INCLUDE_DIRS "include"
|
||||||
EMBED_FILES "www/index.html.gz"
|
EMBED_FILES "www/index.html.gz"
|
||||||
REQUIRES core esp_wifi esp_netif esp_event nvs_flash esp_http_server tuner
|
REQUIRES core esp_wifi esp_netif esp_event nvs_flash esp_http_server tuner audio
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
|
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
|
||||||
|
|||||||
@@ -27,6 +27,10 @@
|
|||||||
#include <expected>
|
#include <expected>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
|
namespace audio {
|
||||||
|
class AudioService;
|
||||||
|
} // namespace audio
|
||||||
|
|
||||||
namespace tuner {
|
namespace tuner {
|
||||||
class TunerService;
|
class TunerService;
|
||||||
} // namespace tuner
|
} // namespace tuner
|
||||||
@@ -52,6 +56,7 @@ public:
|
|||||||
* @dname start
|
* @dname start
|
||||||
* @param store Secure store consulted for saved STA credentials.
|
* @param store Secure store consulted for saved STA credentials.
|
||||||
* @param tuner Tuner service exposed by the HTTP API.
|
* @param tuner Tuner service exposed by the HTTP API.
|
||||||
|
* @param audio Audio service exposed by the HTTP API.
|
||||||
* @return NetBootstrap on success, or a NetError.
|
* @return NetBootstrap on success, or a NetError.
|
||||||
* @pubstate none
|
* @pubstate none
|
||||||
*
|
*
|
||||||
@@ -59,7 +64,8 @@ public:
|
|||||||
* @date 2026-07-06
|
* @date 2026-07-06
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] static std::expected<NetBootstrap, NetError>
|
[[nodiscard]] static std::expected<NetBootstrap, NetError>
|
||||||
start(core::ISecureStore& store, tuner::TunerService& tuner);
|
start(core::ISecureStore& store, tuner::TunerService& tuner,
|
||||||
|
audio::AudioService& audio);
|
||||||
|
|
||||||
NetBootstrap(const NetBootstrap&) = delete;
|
NetBootstrap(const NetBootstrap&) = delete;
|
||||||
NetBootstrap& operator=(const NetBootstrap&) = delete;
|
NetBootstrap& operator=(const NetBootstrap&) = delete;
|
||||||
|
|||||||
@@ -25,6 +25,10 @@
|
|||||||
|
|
||||||
struct httpd_req;
|
struct httpd_req;
|
||||||
|
|
||||||
|
namespace audio {
|
||||||
|
class AudioService;
|
||||||
|
} // namespace audio
|
||||||
|
|
||||||
namespace tuner {
|
namespace tuner {
|
||||||
class TunerService;
|
class TunerService;
|
||||||
} // namespace tuner
|
} // namespace tuner
|
||||||
@@ -47,6 +51,7 @@ namespace net {
|
|||||||
struct HttpRouteContext {
|
struct HttpRouteContext {
|
||||||
core::ISecureStore* store; ///< Secure store for Wi-Fi provisioning.
|
core::ISecureStore* store; ///< Secure store for Wi-Fi provisioning.
|
||||||
tuner::TunerService* tuner; ///< Tuner service for tuner REST routes.
|
tuner::TunerService* tuner; ///< Tuner service for tuner REST routes.
|
||||||
|
audio::AudioService* audio; ///< Audio service for ADAU1701 REST routes.
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,21 +124,24 @@ public:
|
|||||||
* @param store Secure store for POST /api/wifi persistence.
|
* @param store Secure store for POST /api/wifi persistence.
|
||||||
* @param netState Active network phase exposed to handlers.
|
* @param netState Active network phase exposed to handlers.
|
||||||
* @param tuner Tuner service for the tuner REST routes.
|
* @param tuner Tuner service for the tuner REST routes.
|
||||||
|
* @param audio Audio service for the audio REST routes.
|
||||||
* @return Ok on success, or NetError::HttpServerStartFailed.
|
* @return Ok on success, or NetError::HttpServerStartFailed.
|
||||||
* @pubstate writes server_, store_, netState_, and tuner_ on success.
|
* @pubstate writes server_, store_, netState_, tuner_, and audio_ on success.
|
||||||
*
|
*
|
||||||
* @author Michele Bigi
|
* @author Michele Bigi
|
||||||
* @date 2026-07-06
|
* @date 2026-07-06
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] std::expected<void, NetError> start(core::ISecureStore& store,
|
[[nodiscard]] std::expected<void, NetError> start(core::ISecureStore& store,
|
||||||
NetState netState,
|
NetState netState,
|
||||||
tuner::TunerService& tuner);
|
tuner::TunerService& tuner,
|
||||||
|
audio::AudioService& audio);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
httpd_handle* server_;
|
httpd_handle* server_;
|
||||||
core::ISecureStore* store_;
|
core::ISecureStore* store_;
|
||||||
NetState netState_;
|
NetState netState_;
|
||||||
tuner::TunerService* tuner_;
|
tuner::TunerService* tuner_;
|
||||||
|
audio::AudioService* audio_;
|
||||||
HttpRouteContext routeContext_;
|
HttpRouteContext routeContext_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include "esp_netif.h"
|
#include "esp_netif.h"
|
||||||
#include "esp_wifi.h"
|
#include "esp_wifi.h"
|
||||||
#include "nvs_flash.h"
|
#include "nvs_flash.h"
|
||||||
|
#include "audio/AudioService.hpp"
|
||||||
#include "tuner/TunerService.hpp"
|
#include "tuner/TunerService.hpp"
|
||||||
|
|
||||||
namespace net {
|
namespace net {
|
||||||
@@ -98,7 +99,8 @@ constexpr char kTag[] = "NetBootstrap";
|
|||||||
* @date 2026-07-06
|
* @date 2026-07-06
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] std::expected<NetBootstrap, NetError>
|
[[nodiscard]] std::expected<NetBootstrap, NetError>
|
||||||
startSetupMode(core::ISecureStore& store, tuner::TunerService& tuner)
|
startSetupMode(core::ISecureStore& store, tuner::TunerService& tuner,
|
||||||
|
audio::AudioService& audio)
|
||||||
{
|
{
|
||||||
esp_netif_create_default_wifi_ap();
|
esp_netif_create_default_wifi_ap();
|
||||||
|
|
||||||
@@ -108,7 +110,8 @@ startSetupMode(core::ISecureStore& store, tuner::TunerService& tuner)
|
|||||||
}
|
}
|
||||||
|
|
||||||
SetupWebServer webServer;
|
SetupWebServer webServer;
|
||||||
if (auto webResult = webServer.start(store, NetState::SoftApSetup, tuner);
|
if (auto webResult =
|
||||||
|
webServer.start(store, NetState::SoftApSetup, tuner, audio);
|
||||||
!webResult) {
|
!webResult) {
|
||||||
return std::unexpected(webResult.error());
|
return std::unexpected(webResult.error());
|
||||||
}
|
}
|
||||||
@@ -130,7 +133,8 @@ startSetupMode(core::ISecureStore& store, tuner::TunerService& tuner)
|
|||||||
* @date 2026-07-06
|
* @date 2026-07-06
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] std::expected<NetBootstrap, NetError>
|
[[nodiscard]] std::expected<NetBootstrap, NetError>
|
||||||
startStaMode(core::ISecureStore& store, tuner::TunerService& tuner)
|
startStaMode(core::ISecureStore& store, tuner::TunerService& tuner,
|
||||||
|
audio::AudioService& audio)
|
||||||
{
|
{
|
||||||
auto credsResult = store.loadWifiCredentials();
|
auto credsResult = store.loadWifiCredentials();
|
||||||
if (!credsResult) {
|
if (!credsResult) {
|
||||||
@@ -146,7 +150,8 @@ startStaMode(core::ISecureStore& store, tuner::TunerService& tuner)
|
|||||||
}
|
}
|
||||||
|
|
||||||
SetupWebServer webServer;
|
SetupWebServer webServer;
|
||||||
if (auto webResult = webServer.start(store, NetState::StaConnected, tuner);
|
if (auto webResult =
|
||||||
|
webServer.start(store, NetState::StaConnected, tuner, audio);
|
||||||
!webResult) {
|
!webResult) {
|
||||||
return std::unexpected(webResult.error());
|
return std::unexpected(webResult.error());
|
||||||
}
|
}
|
||||||
@@ -159,7 +164,8 @@ startStaMode(core::ISecureStore& store, tuner::TunerService& tuner)
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::expected<NetBootstrap, NetError>
|
std::expected<NetBootstrap, NetError>
|
||||||
NetBootstrap::start(core::ISecureStore& store, tuner::TunerService& tuner)
|
NetBootstrap::start(core::ISecureStore& store, tuner::TunerService& tuner,
|
||||||
|
audio::AudioService& audio)
|
||||||
{
|
{
|
||||||
if (auto platform = initPlatform(); !platform) {
|
if (auto platform = initPlatform(); !platform) {
|
||||||
return std::unexpected(platform.error());
|
return std::unexpected(platform.error());
|
||||||
@@ -170,14 +176,14 @@ NetBootstrap::start(core::ISecureStore& store, tuner::TunerService& tuner)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (store.hasWifiCredentials()) {
|
if (store.hasWifiCredentials()) {
|
||||||
auto staResult = startStaMode(store, tuner);
|
auto staResult = startStaMode(store, tuner, audio);
|
||||||
if (staResult) {
|
if (staResult) {
|
||||||
return staResult;
|
return staResult;
|
||||||
}
|
}
|
||||||
ESP_LOGW(kTag, "STA join failed — falling back to setup SoftAP");
|
ESP_LOGW(kTag, "STA join failed — falling back to setup SoftAP");
|
||||||
}
|
}
|
||||||
|
|
||||||
return startSetupMode(store, tuner);
|
return startSetupMode(store, tuner, audio);
|
||||||
}
|
}
|
||||||
|
|
||||||
NetBootstrap::NetBootstrap(std::optional<SoftApHost> softAp,
|
NetBootstrap::NetBootstrap(std::optional<SoftApHost> softAp,
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
#include "net/SetupWebServer.hpp"
|
#include "net/SetupWebServer.hpp"
|
||||||
|
|
||||||
|
#include "core/AudioProfile.hpp"
|
||||||
|
#include "core/AudioProfileJson.hpp"
|
||||||
#include "core/FirmwareVersion.hpp"
|
#include "core/FirmwareVersion.hpp"
|
||||||
#include "core/HealthStatus.hpp"
|
#include "core/HealthStatus.hpp"
|
||||||
#include "core/HealthStatusJson.hpp"
|
#include "core/HealthStatusJson.hpp"
|
||||||
@@ -26,6 +28,7 @@
|
|||||||
#include "core/TunerJson.hpp"
|
#include "core/TunerJson.hpp"
|
||||||
#include "core/WifiProvisionJson.hpp"
|
#include "core/WifiProvisionJson.hpp"
|
||||||
#include "tuner/TunerService.hpp"
|
#include "tuner/TunerService.hpp"
|
||||||
|
#include "audio/AudioService.hpp"
|
||||||
|
|
||||||
#include "esp_http_server.h"
|
#include "esp_http_server.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
@@ -40,7 +43,7 @@ namespace net {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr char kTag[] = "SetupWebServer";
|
constexpr char kTag[] = "SetupWebServer";
|
||||||
constexpr char kFirmwareVersion[] = "0.4.0";
|
constexpr char kFirmwareVersion[] = "0.5.0";
|
||||||
constexpr unsigned kRebootDelaySec = 3;
|
constexpr unsigned kRebootDelaySec = 3;
|
||||||
|
|
||||||
extern const uint8_t www_index_html_gz_start[] asm(
|
extern const uint8_t www_index_html_gz_start[] asm(
|
||||||
@@ -363,6 +366,109 @@ esp_err_t tunerSeekPostHandler(httpd_req_t* req)
|
|||||||
return httpd_resp_send(req, json.c_str(), json.size());
|
return httpd_resp_send(req, json.c_str(), json.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief audioProfileGetHandler — serve GET /api/audio/profile JSON.
|
||||||
|
*
|
||||||
|
* @dname audioProfileGetHandler
|
||||||
|
* @param req HTTP request handle from esp_http_server.
|
||||||
|
* @return ESP_OK on success, or an esp_err_t error code.
|
||||||
|
* @pubstate reads route context audio service snapshot.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
esp_err_t audioProfileGetHandler(httpd_req_t* req)
|
||||||
|
{
|
||||||
|
auto* ctx = routeContextFrom(req);
|
||||||
|
if (ctx == nullptr || ctx->audio == nullptr) {
|
||||||
|
httpd_resp_set_status(req, "503 Service Unavailable");
|
||||||
|
return httpd_resp_send(req, nullptr, 0);
|
||||||
|
}
|
||||||
|
const std::string json =
|
||||||
|
core::serializeAudioProfileJson(ctx->audio->currentProfile());
|
||||||
|
httpd_resp_set_type(req, "application/json");
|
||||||
|
return httpd_resp_send(req, json.c_str(), json.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief audioProfilePutHandler — accept PUT /api/audio/profile JSON.
|
||||||
|
*
|
||||||
|
* @dname audioProfilePutHandler
|
||||||
|
* @param req HTTP request handle from esp_http_server.
|
||||||
|
* @return ESP_OK on success, or an esp_err_t error code.
|
||||||
|
* @pubstate parses profile, safeloads ADAU1701, persists to NVS.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
esp_err_t audioProfilePutHandler(httpd_req_t* req)
|
||||||
|
{
|
||||||
|
auto* ctx = routeContextFrom(req);
|
||||||
|
if (ctx == nullptr || ctx->audio == nullptr) {
|
||||||
|
httpd_resp_set_status(req, "503 Service Unavailable");
|
||||||
|
return httpd_resp_send(req, nullptr, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<char, 2048> body{};
|
||||||
|
if (!readRequestBody(req, body)) {
|
||||||
|
httpd_resp_set_status(req, "400 Bad Request");
|
||||||
|
return httpd_resp_send(req, nullptr, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto parsed =
|
||||||
|
core::parseAudioProfileJson(std::string_view(body.data()));
|
||||||
|
if (!parsed) {
|
||||||
|
const std::string json =
|
||||||
|
core::serializeAudioErrorJson(parseErrorToken(parsed.error()));
|
||||||
|
httpd_resp_set_status(req, "400 Bad Request");
|
||||||
|
httpd_resp_set_type(req, "application/json");
|
||||||
|
return httpd_resp_send(req, json.c_str(), json.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto applied = ctx->audio->applyProfile(*parsed, true); !applied) {
|
||||||
|
const std::string json = core::serializeAudioErrorJson("store_failed");
|
||||||
|
httpd_resp_set_status(req, "500 Internal Server Error");
|
||||||
|
httpd_resp_set_type(req, "application/json");
|
||||||
|
return httpd_resp_send(req, json.c_str(), json.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string json = core::serializeAudioSavedJson();
|
||||||
|
httpd_resp_set_type(req, "application/json");
|
||||||
|
return httpd_resp_send(req, json.c_str(), json.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief audioResetPostHandler — restore factory-flat profile.
|
||||||
|
*
|
||||||
|
* @dname audioResetPostHandler
|
||||||
|
* @param req HTTP request handle from esp_http_server.
|
||||||
|
* @return ESP_OK on success, or an esp_err_t error code.
|
||||||
|
* @pubstate applies AudioProfile::factoryDefault() and persists to NVS.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
esp_err_t audioResetPostHandler(httpd_req_t* req)
|
||||||
|
{
|
||||||
|
auto* ctx = routeContextFrom(req);
|
||||||
|
if (ctx == nullptr || ctx->audio == nullptr) {
|
||||||
|
httpd_resp_set_status(req, "503 Service Unavailable");
|
||||||
|
return httpd_resp_send(req, nullptr, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const core::AudioProfile defaults = core::AudioProfile::factoryDefault();
|
||||||
|
if (auto applied = ctx->audio->applyProfile(defaults, true); !applied) {
|
||||||
|
const std::string json = core::serializeAudioErrorJson("store_failed");
|
||||||
|
httpd_resp_set_status(req, "500 Internal Server Error");
|
||||||
|
httpd_resp_set_type(req, "application/json");
|
||||||
|
return httpd_resp_send(req, json.c_str(), json.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string json = core::serializeAudioSavedJson();
|
||||||
|
httpd_resp_set_type(req, "application/json");
|
||||||
|
return httpd_resp_send(req, json.c_str(), json.size());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief indexGetHandler — serve gzipped setup page from flash.
|
* @brief indexGetHandler — serve gzipped setup page from flash.
|
||||||
*
|
*
|
||||||
@@ -450,7 +556,8 @@ SetupWebServer::SetupWebServer()
|
|||||||
, store_(nullptr)
|
, store_(nullptr)
|
||||||
, netState_(NetState::Uninitialized)
|
, netState_(NetState::Uninitialized)
|
||||||
, tuner_(nullptr)
|
, tuner_(nullptr)
|
||||||
, routeContext_{nullptr, nullptr}
|
, audio_(nullptr)
|
||||||
|
, routeContext_{nullptr, nullptr, nullptr}
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,13 +566,15 @@ SetupWebServer::SetupWebServer(SetupWebServer&& other) noexcept
|
|||||||
, store_(other.store_)
|
, store_(other.store_)
|
||||||
, netState_(other.netState_)
|
, netState_(other.netState_)
|
||||||
, tuner_(other.tuner_)
|
, tuner_(other.tuner_)
|
||||||
|
, audio_(other.audio_)
|
||||||
, routeContext_(other.routeContext_)
|
, routeContext_(other.routeContext_)
|
||||||
{
|
{
|
||||||
other.server_ = nullptr;
|
other.server_ = nullptr;
|
||||||
other.store_ = nullptr;
|
other.store_ = nullptr;
|
||||||
other.netState_ = NetState::Uninitialized;
|
other.netState_ = NetState::Uninitialized;
|
||||||
other.tuner_ = nullptr;
|
other.tuner_ = nullptr;
|
||||||
other.routeContext_ = {nullptr, nullptr};
|
other.audio_ = nullptr;
|
||||||
|
other.routeContext_ = {nullptr, nullptr, nullptr};
|
||||||
}
|
}
|
||||||
|
|
||||||
SetupWebServer& SetupWebServer::operator=(SetupWebServer&& other) noexcept
|
SetupWebServer& SetupWebServer::operator=(SetupWebServer&& other) noexcept
|
||||||
@@ -478,12 +587,14 @@ SetupWebServer& SetupWebServer::operator=(SetupWebServer&& other) noexcept
|
|||||||
store_ = other.store_;
|
store_ = other.store_;
|
||||||
netState_ = other.netState_;
|
netState_ = other.netState_;
|
||||||
tuner_ = other.tuner_;
|
tuner_ = other.tuner_;
|
||||||
|
audio_ = other.audio_;
|
||||||
routeContext_ = other.routeContext_;
|
routeContext_ = other.routeContext_;
|
||||||
other.server_ = nullptr;
|
other.server_ = nullptr;
|
||||||
other.store_ = nullptr;
|
other.store_ = nullptr;
|
||||||
other.netState_ = NetState::Uninitialized;
|
other.netState_ = NetState::Uninitialized;
|
||||||
other.tuner_ = nullptr;
|
other.tuner_ = nullptr;
|
||||||
other.routeContext_ = {nullptr, nullptr};
|
other.audio_ = nullptr;
|
||||||
|
other.routeContext_ = {nullptr, nullptr, nullptr};
|
||||||
}
|
}
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
@@ -494,13 +605,14 @@ SetupWebServer::~SetupWebServer()
|
|||||||
httpd_stop(server_);
|
httpd_stop(server_);
|
||||||
server_ = nullptr;
|
server_ = nullptr;
|
||||||
}
|
}
|
||||||
routeContext_ = {nullptr, nullptr};
|
routeContext_ = {nullptr, nullptr, nullptr};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::expected<void, NetError> SetupWebServer::start(
|
std::expected<void, NetError> SetupWebServer::start(
|
||||||
core::ISecureStore& store,
|
core::ISecureStore& store,
|
||||||
NetState netState,
|
NetState netState,
|
||||||
tuner::TunerService& tuner)
|
tuner::TunerService& tuner,
|
||||||
|
audio::AudioService& audio)
|
||||||
{
|
{
|
||||||
if (server_ != nullptr) {
|
if (server_ != nullptr) {
|
||||||
return {};
|
return {};
|
||||||
@@ -509,8 +621,10 @@ std::expected<void, NetError> SetupWebServer::start(
|
|||||||
store_ = &store;
|
store_ = &store;
|
||||||
netState_ = netState;
|
netState_ = netState;
|
||||||
tuner_ = &tuner;
|
tuner_ = &tuner;
|
||||||
|
audio_ = &audio;
|
||||||
routeContext_.store = &store;
|
routeContext_.store = &store;
|
||||||
routeContext_.tuner = &tuner;
|
routeContext_.tuner = &tuner;
|
||||||
|
routeContext_.audio = &audio;
|
||||||
|
|
||||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||||
config.server_port = 80;
|
config.server_port = 80;
|
||||||
@@ -518,7 +632,7 @@ std::expected<void, NetError> SetupWebServer::start(
|
|||||||
|
|
||||||
if (httpd_start(&server_, &config) != ESP_OK) {
|
if (httpd_start(&server_, &config) != ESP_OK) {
|
||||||
ESP_LOGE(kTag, "httpd_start failed");
|
ESP_LOGE(kTag, "httpd_start failed");
|
||||||
routeContext_ = {nullptr, nullptr};
|
routeContext_ = {nullptr, nullptr, nullptr};
|
||||||
return std::unexpected(NetError::HttpServerStartFailed);
|
return std::unexpected(NetError::HttpServerStartFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,6 +702,30 @@ std::expected<void, NetError> SetupWebServer::start(
|
|||||||
};
|
};
|
||||||
httpd_register_uri_handler(server_, &tunerSeekUri);
|
httpd_register_uri_handler(server_, &tunerSeekUri);
|
||||||
|
|
||||||
|
const httpd_uri_t audioProfileGetUri = {
|
||||||
|
.uri = "/api/audio/profile",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = audioProfileGetHandler,
|
||||||
|
.user_ctx = routeCtx,
|
||||||
|
};
|
||||||
|
httpd_register_uri_handler(server_, &audioProfileGetUri);
|
||||||
|
|
||||||
|
const httpd_uri_t audioProfilePutUri = {
|
||||||
|
.uri = "/api/audio/profile",
|
||||||
|
.method = HTTP_PUT,
|
||||||
|
.handler = audioProfilePutHandler,
|
||||||
|
.user_ctx = routeCtx,
|
||||||
|
};
|
||||||
|
httpd_register_uri_handler(server_, &audioProfilePutUri);
|
||||||
|
|
||||||
|
const httpd_uri_t audioResetUri = {
|
||||||
|
.uri = "/api/audio/reset",
|
||||||
|
.method = HTTP_POST,
|
||||||
|
.handler = audioResetPostHandler,
|
||||||
|
.user_ctx = routeCtx,
|
||||||
|
};
|
||||||
|
httpd_register_uri_handler(server_, &audioResetUri);
|
||||||
|
|
||||||
ESP_LOGI(kTag, "HTTP server listening on port 80");
|
ESP_LOGI(kTag, "HTTP server listening on port 80");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
input[type="range"] { padding: 0; }
|
||||||
button {
|
button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
@@ -196,6 +197,22 @@
|
|||||||
|
|
||||||
<p class="msg" id="tuner-msg" aria-live="polite"></p>
|
<p class="msg" id="tuner-msg" aria-live="polite"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section id="audio-section">
|
||||||
|
<h2>Audio</h2>
|
||||||
|
<p>ADAU1701 mixer and master volume (safeload at runtime).</p>
|
||||||
|
<label for="master-db">Master (dB)</label>
|
||||||
|
<input id="master-db" type="range" min="-60" max="12" step="0.5" value="0">
|
||||||
|
<label for="si4684-db">Radio path Si4684 (dB)</label>
|
||||||
|
<input id="si4684-db" type="range" min="-60" max="12" step="0.5" value="0">
|
||||||
|
<label for="esp32-db">ESP32 path (dB)</label>
|
||||||
|
<input id="esp32-db" type="range" min="-60" max="12" step="0.5" value="0">
|
||||||
|
<div class="row">
|
||||||
|
<button type="button" id="save-audio">Save profile</button>
|
||||||
|
<button type="button" class="secondary" id="reset-audio">Reset flat</button>
|
||||||
|
</div>
|
||||||
|
<p class="msg" id="audio-msg" aria-live="polite"></p>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<script>
|
<script>
|
||||||
function showMsg(el, text, ok) {
|
function showMsg(el, text, ok) {
|
||||||
@@ -397,6 +414,70 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
refreshTunerStatus();
|
refreshTunerStatus();
|
||||||
|
|
||||||
|
var audioProfile = null;
|
||||||
|
|
||||||
|
function loadAudioProfile() {
|
||||||
|
var msg = document.getElementById("audio-msg");
|
||||||
|
return fetch("/api/audio/profile")
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (d) {
|
||||||
|
audioProfile = d;
|
||||||
|
document.getElementById("master-db").value = d.master.left_db;
|
||||||
|
document.getElementById("si4684-db").value = d.mixer.si4684_left_db;
|
||||||
|
document.getElementById("esp32-db").value = d.mixer.esp32_left_db;
|
||||||
|
showMsg(msg, "", true);
|
||||||
|
})
|
||||||
|
.catch(function () { showMsg(msg, "Audio profile load failed.", false); });
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("save-audio").addEventListener("click", function () {
|
||||||
|
var msg = document.getElementById("audio-msg");
|
||||||
|
if (!audioProfile) {
|
||||||
|
showMsg(msg, "Profile not loaded.", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var master = parseFloat(document.getElementById("master-db").value, 10);
|
||||||
|
var si4684 = parseFloat(document.getElementById("si4684-db").value, 10);
|
||||||
|
var esp32 = parseFloat(document.getElementById("esp32-db").value, 10);
|
||||||
|
audioProfile.master.left_db = master;
|
||||||
|
audioProfile.master.right_db = master;
|
||||||
|
audioProfile.mixer.si4684_left_db = si4684;
|
||||||
|
audioProfile.mixer.si4684_right_db = si4684;
|
||||||
|
audioProfile.mixer.esp32_left_db = esp32;
|
||||||
|
audioProfile.mixer.esp32_right_db = esp32;
|
||||||
|
fetch("/api/audio/profile", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(audioProfile)
|
||||||
|
})
|
||||||
|
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
|
||||||
|
.then(function (res) {
|
||||||
|
if (res.ok && res.d.status === "saved") {
|
||||||
|
showMsg(msg, "Audio profile saved.", true);
|
||||||
|
} else {
|
||||||
|
showMsg(msg, "Save failed: " + (res.d.reason || "unknown"), false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () { showMsg(msg, "Save request failed.", false); });
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("reset-audio").addEventListener("click", function () {
|
||||||
|
var msg = document.getElementById("audio-msg");
|
||||||
|
fetch("/api/audio/reset", { method: "POST" })
|
||||||
|
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
|
||||||
|
.then(function (res) {
|
||||||
|
if (res.ok && res.d.status === "saved") {
|
||||||
|
showMsg(msg, "Factory-flat profile applied.", true);
|
||||||
|
loadAudioProfile();
|
||||||
|
} else {
|
||||||
|
showMsg(msg, "Reset failed: " + (res.d.reason || "unknown"), false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () { showMsg(msg, "Reset request failed.", false); });
|
||||||
|
});
|
||||||
|
|
||||||
|
loadAudioProfile();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Binary file not shown.
@@ -1,5 +1,7 @@
|
|||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS "src/NvsSecureStore.cpp"
|
SRCS
|
||||||
|
"src/NvsSecureStore.cpp"
|
||||||
|
"src/NvsAudioProfileStore.cpp"
|
||||||
INCLUDE_DIRS "include"
|
INCLUDE_DIRS "include"
|
||||||
REQUIRES core nvs_flash
|
REQUIRES core nvs_flash
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* @file NvsAudioProfileStore.hpp
|
||||||
|
* @brief NVS-backed IAudioProfileStore for user audio settings.
|
||||||
|
*
|
||||||
|
* 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/IAudioProfileStore.hpp"
|
||||||
|
|
||||||
|
namespace secure_store {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief NvsAudioProfileStore — persists AudioProfile JSON in NVS.
|
||||||
|
*
|
||||||
|
* @dname NvsAudioProfileStore
|
||||||
|
* @return n/a (type)
|
||||||
|
* @pubstate Uses namespace digiradio, key audio_profile_json.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
class NvsAudioProfileStore final : public core::IAudioProfileStore {
|
||||||
|
public:
|
||||||
|
[[nodiscard]] bool hasProfile() const override;
|
||||||
|
|
||||||
|
[[nodiscard]] std::expected<void, core::StoreError> saveProfile(
|
||||||
|
const core::AudioProfile& profile) override;
|
||||||
|
|
||||||
|
[[nodiscard]] std::expected<core::AudioProfile, core::StoreError>
|
||||||
|
loadProfile() const override;
|
||||||
|
|
||||||
|
[[nodiscard]] std::expected<void, core::StoreError> clearProfile() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace secure_store
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* @file NvsAudioProfileStore.cpp
|
||||||
|
* @brief NvsAudioProfileStore 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 "secure_store/NvsAudioProfileStore.hpp"
|
||||||
|
|
||||||
|
#include "core/AudioProfileJson.hpp"
|
||||||
|
|
||||||
|
#include "nvs.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace secure_store {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr char kNamespace[] = "digiradio";
|
||||||
|
constexpr char kProfileKey[] = "audio_profile_json";
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool NvsAudioProfileStore::hasProfile() const
|
||||||
|
{
|
||||||
|
nvs_handle_t handle = 0;
|
||||||
|
if (nvs_open(kNamespace, NVS_READONLY, &handle) != ESP_OK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t len = 0;
|
||||||
|
const esp_err_t err = nvs_get_str(handle, kProfileKey, nullptr, &len);
|
||||||
|
nvs_close(handle);
|
||||||
|
return err == ESP_OK && len > 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::expected<void, core::StoreError> NvsAudioProfileStore::saveProfile(
|
||||||
|
const core::AudioProfile& profile)
|
||||||
|
{
|
||||||
|
const std::string json = core::serializeAudioProfileJson(profile);
|
||||||
|
|
||||||
|
nvs_handle_t handle = 0;
|
||||||
|
if (nvs_open(kNamespace, NVS_READWRITE, &handle) != ESP_OK) {
|
||||||
|
return std::unexpected(core::StoreError::IoFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t err = nvs_set_str(handle, kProfileKey, json.c_str());
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
err = nvs_commit(handle);
|
||||||
|
}
|
||||||
|
nvs_close(handle);
|
||||||
|
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return std::unexpected(core::StoreError::IoFailed);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::expected<core::AudioProfile, core::StoreError>
|
||||||
|
NvsAudioProfileStore::loadProfile() const
|
||||||
|
{
|
||||||
|
nvs_handle_t handle = 0;
|
||||||
|
if (nvs_open(kNamespace, NVS_READONLY, &handle) != ESP_OK) {
|
||||||
|
return std::unexpected(core::StoreError::NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t len = 0;
|
||||||
|
if (nvs_get_str(handle, kProfileKey, nullptr, &len) != ESP_OK || len == 0U) {
|
||||||
|
nvs_close(handle);
|
||||||
|
return std::unexpected(core::StoreError::NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<char> buf(len);
|
||||||
|
if (nvs_get_str(handle, kProfileKey, buf.data(), &len) != ESP_OK) {
|
||||||
|
nvs_close(handle);
|
||||||
|
return std::unexpected(core::StoreError::IoFailed);
|
||||||
|
}
|
||||||
|
nvs_close(handle);
|
||||||
|
|
||||||
|
if (auto parsed = core::parseAudioProfileJson(buf.data()); parsed) {
|
||||||
|
return *parsed;
|
||||||
|
}
|
||||||
|
return std::unexpected(core::StoreError::InvalidData);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::expected<void, core::StoreError> NvsAudioProfileStore::clearProfile()
|
||||||
|
{
|
||||||
|
nvs_handle_t handle = 0;
|
||||||
|
if (nvs_open(kNamespace, NVS_READWRITE, &handle) != ESP_OK) {
|
||||||
|
return std::unexpected(core::StoreError::IoFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t err = nvs_erase_key(handle, kProfileKey);
|
||||||
|
if (err == ESP_OK || err == ESP_ERR_NVS_NOT_FOUND) {
|
||||||
|
err = nvs_commit(handle);
|
||||||
|
}
|
||||||
|
nvs_close(handle);
|
||||||
|
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return std::unexpected(core::StoreError::IoFailed);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace secure_store
|
||||||
@@ -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
|
||||||
@@ -6,7 +6,7 @@ implemented in \texttt{SetupWebServer}. Request bodies are parsed into
|
|||||||
domain types in the pure core (\texttt{components/core}) before any
|
domain types in the pure core (\texttt{components/core}) before any
|
||||||
persistence or driver call. Exact C++ signatures live in the generated
|
persistence or driver call. Exact C++ signatures live in the generated
|
||||||
Doxygen output under \texttt{docs/api/}; this chapter documents the
|
Doxygen output under \texttt{docs/api/}; this chapter documents the
|
||||||
wire protocol and behaviour as shipped in firmware~0.4.0 (Slices~1--4).
|
wire protocol and behaviour as shipped in firmware~0.5.0 (Slices~1--5).
|
||||||
|
|
||||||
\section{Transport and reachability}
|
\section{Transport and reachability}
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ Returns a health-check DTO serialised by
|
|||||||
|
|
||||||
\begin{drnote}[Response schema]
|
\begin{drnote}[Response schema]
|
||||||
\begin{drcode}[JSON]
|
\begin{drcode}[JSON]
|
||||||
{"status":"ok","fw":"0.4.0"}
|
{"status":"ok","fw":"0.5.0"}
|
||||||
\end{drcode}
|
\end{drcode}
|
||||||
\begin{itemize}
|
\begin{itemize}
|
||||||
\item \texttt{status} --- coarse indicator; \texttt{ok} when the
|
\item \texttt{status} --- coarse indicator; \texttt{ok} when the
|
||||||
@@ -172,10 +172,55 @@ Seeks FM upward (no request body). Returns
|
|||||||
\texttt{\{"frequency\_khz":...\}} on success. HTTP status: \textbf{200 OK};
|
\texttt{\{"frequency\_khz":...\}} on success. HTTP status: \textbf{200 OK};
|
||||||
\textbf{409} on seek failure.
|
\textbf{409} on seek failure.
|
||||||
|
|
||||||
|
\subsection{\texttt{GET /api/audio/profile}}
|
||||||
|
\label{sec:api-audio-profile-get}
|
||||||
|
|
||||||
|
Returns the current ADAU1701 audio snapshot serialised by
|
||||||
|
\texttt{core::serializeAudioProfileJson()} from \texttt{core::AudioProfile}.
|
||||||
|
The handler reads \texttt{audio::AudioService::currentProfile()}.
|
||||||
|
|
||||||
|
\begin{drnote}[Response schema (excerpt)]
|
||||||
|
\begin{drcode}[JSON]
|
||||||
|
{"mixer":{"si4684_left_db":0,"si4684_right_db":0,"esp32_left_db":0,
|
||||||
|
"esp32_right_db":0,"mix_left_db":0,"mix_right_db":0},
|
||||||
|
"master":{"left_db":0,"right_db":0},
|
||||||
|
"eq":[{"gain_db":0,"center_hz":40,"q":1.414}, ...]}
|
||||||
|
\end{drcode}
|
||||||
|
Six EQ bands are always present (\texttt{eq} array length~6).
|
||||||
|
\end{drnote}
|
||||||
|
|
||||||
|
HTTP status: \textbf{200 OK}; \textbf{503} when the audio service is
|
||||||
|
unavailable.
|
||||||
|
|
||||||
|
\subsection{\texttt{PUT /api/audio/profile}}
|
||||||
|
\label{sec:api-audio-profile-put}
|
||||||
|
|
||||||
|
Applies a full audio profile. Body parsed by
|
||||||
|
\texttt{core::parseAudioProfileJson()}; on success
|
||||||
|
\texttt{audio::AudioService::applyProfile(..., persist=true)} safeloads
|
||||||
|
the ADAU1701 and writes NVS key \texttt{audio\_profile\_json}.
|
||||||
|
|
||||||
|
\begin{drnote}[Success response]
|
||||||
|
\begin{drcode}[JSON]
|
||||||
|
{"status":"saved"}
|
||||||
|
\end{drcode}
|
||||||
|
\end{drnote}
|
||||||
|
|
||||||
|
HTTP status: \textbf{200 OK}; \textbf{400} for parse/validation failures;
|
||||||
|
\textbf{500} when safeload or NVS persistence fails.
|
||||||
|
|
||||||
|
\subsection{\texttt{POST /api/audio/reset}}
|
||||||
|
\label{sec:api-audio-reset}
|
||||||
|
|
||||||
|
Restores \texttt{AudioProfile::factoryDefault()} (flat EQ, 0\,dB gains),
|
||||||
|
applies it to the DSP, and persists to NVS. Success response:
|
||||||
|
\texttt{\{"status":"saved"\}}. HTTP status: \textbf{200 OK}; \textbf{500}
|
||||||
|
on apply/persist failure.
|
||||||
|
|
||||||
\section{Boot and network state machine}
|
\section{Boot and network state machine}
|
||||||
\label{sec:api-boot-flow}
|
\label{sec:api-boot-flow}
|
||||||
|
|
||||||
At boot, \texttt{net::NetBootstrap::start(store, tuner)} consults
|
At boot, \texttt{net::NetBootstrap::start(store, tuner, audio)} consults
|
||||||
\texttt{ISecureStore::hasWifiCredentials()}:
|
\texttt{ISecureStore::hasWifiCredentials()}:
|
||||||
|
|
||||||
\begin{enumerate}
|
\begin{enumerate}
|
||||||
@@ -193,7 +238,9 @@ This explicit \texttt{enum class NetState} replaces ad-hoc flags; see
|
|||||||
\label{sec:api-storage}
|
\label{sec:api-storage}
|
||||||
|
|
||||||
Wi-Fi credentials are stored in NVS namespace \texttt{digiradio}, keys
|
Wi-Fi credentials are stored in NVS namespace \texttt{digiradio}, keys
|
||||||
\texttt{wifi\_ssid} and \texttt{wifi\_pwd}. Passwords are wrapped in
|
\texttt{wifi\_ssid} and \texttt{wifi\_pwd}. Audio profiles (non-secret) use
|
||||||
|
the same namespace, key \texttt{audio\_profile\_json}, via
|
||||||
|
\texttt{secure\_store::NvsAudioProfileStore}. Passwords are wrapped in
|
||||||
\texttt{core::Secret} in RAM and are never logged or returned by the API.
|
\texttt{core::Secret} in RAM and are never logged or returned by the API.
|
||||||
|
|
||||||
\begin{drcaution}[Encryption at rest]
|
\begin{drcaution}[Encryption at rest]
|
||||||
|
|||||||
@@ -56,16 +56,17 @@ configured SoftAP. Imperative shell; no business logic.
|
|||||||
|
|
||||||
\section{SetupWebServer}\label{cls:SetupWebServer}
|
\section{SetupWebServer}\label{cls:SetupWebServer}
|
||||||
Minimal HTTP server: gzipped setup UI, \texttt{GET /api/health},
|
Minimal HTTP server: gzipped setup UI, \texttt{GET /api/health},
|
||||||
\texttt{POST /api/wifi}, and tuner routes (\texttt{/api/tuner/*}).
|
\texttt{POST /api/wifi}, tuner routes (\texttt{/api/tuner/*}), and audio
|
||||||
JSON parsing and serialisation delegate to the pure core; credentials
|
routes (\texttt{/api/audio/*}). JSON parsing and serialisation delegate to
|
||||||
persist via \texttt{ISecureStore}; tuner operations via
|
the pure core; credentials persist via \texttt{ISecureStore}; tuner via
|
||||||
\texttt{tuner::TunerService}.
|
\texttt{tuner::TunerService}; audio via \texttt{audio::AudioService}.
|
||||||
|
|
||||||
\section{NetBootstrap}\label{cls:NetBootstrap}
|
\section{NetBootstrap}\label{cls:NetBootstrap}
|
||||||
Owns network resources for setup or STA mode.
|
Owns network resources for setup or STA mode.
|
||||||
\texttt{start(store, tuner)} initialises the platform, joins stored Wi-Fi
|
\texttt{start(store, tuner)} initialises the platform, joins stored Wi-Fi
|
||||||
when credentials exist, or falls back to the \texttt{DigiRadio-setup}
|
when credentials exist, or falls back to the \texttt{DigiRadio-setup}
|
||||||
SoftAP. Must outlive \texttt{app\_main} for the process lifetime.
|
SoftAP. Must outlive \texttt{app\_main} for the process lifetime.
|
||||||
|
\texttt{start(store, tuner, audio)} also wires the audio REST routes.
|
||||||
|
|
||||||
% ------------------------------------------------------------------
|
% ------------------------------------------------------------------
|
||||||
% Domain core + secure store (Slice 2)
|
% Domain core + secure store (Slice 2)
|
||||||
@@ -135,10 +136,47 @@ DTOs.
|
|||||||
RAII I2C driver for the ADAU1701 SigmaDSP. \texttt{boot()} asserts RESET\#,
|
RAII I2C driver for the ADAU1701 SigmaDSP. \texttt{boot()} asserts RESET\#,
|
||||||
initialises the shared I2C bus, and replays the SigmaStudio export from
|
initialises the shared I2C bus, and replays the SigmaStudio export from
|
||||||
\texttt{Firmware/ADAU1701-Firmware/} on every power-up (no EEPROM self-boot
|
\texttt{Firmware/ADAU1701-Firmware/} on every power-up (no EEPROM self-boot
|
||||||
on DigiRadio).
|
on DigiRadio). Runtime mixer, EQ, and master volume updates use the
|
||||||
|
ADAU1701 safeload mechanism via \texttt{applyProfile()} and related methods.
|
||||||
|
|
||||||
|
\section{Adau1701Dsp}\label{cls:Adau1701Dsp}
|
||||||
|
\texttt{core::IDsp} adapter over \texttt{Adau1701Driver}. Maps domain-level
|
||||||
|
audio control to safeload I2C transactions without exposing parameter RAM
|
||||||
|
addresses to services.
|
||||||
|
|
||||||
% ------------------------------------------------------------------
|
% ------------------------------------------------------------------
|
||||||
% Application services (Slice 4)
|
% Domain core — audio (Slice 5)
|
||||||
|
% ------------------------------------------------------------------
|
||||||
|
|
||||||
|
\section{GainDb}\label{cls:GainDb}
|
||||||
|
Validated decibel gain/attenuation ($-96$\,dB to $+12$\,dB). Used for
|
||||||
|
input volumes, master level, and EQ band gain. Converted to ADAU 8.23
|
||||||
|
fixpoint via \texttt{gainDbToLinearFixpoint()} before safeload.
|
||||||
|
|
||||||
|
\section{FrequencyHz}\label{cls:FrequencyHz}
|
||||||
|
Validated PEQ centre frequency (20--20\,000\,Hz) for the 48\,kHz
|
||||||
|
SigmaStudio project. Parsed at the HTTP boundary before coefficient design.
|
||||||
|
|
||||||
|
\section{EqBandIndex}\label{cls:EqBandIndex}
|
||||||
|
Strong index into the six-band \texttt{Param EQ1} module (0--5). Maps to
|
||||||
|
parameter RAM via \texttt{adau1701::paramAddrEqBandBase()}.
|
||||||
|
|
||||||
|
\section{EqProfile}\label{cls:EqProfile}
|
||||||
|
Six-band parametric EQ snapshot with default centre frequencies
|
||||||
|
(40\,Hz--12\,kHz). Designed in the pure core; coefficients safeloaded by
|
||||||
|
\texttt{Adau1701Driver::applyEq()}.
|
||||||
|
|
||||||
|
\section{IDsp}\label{cls:IDsp}
|
||||||
|
Abstract ADAU1701 control boundary. Defines \texttt{applyProfile()},
|
||||||
|
\texttt{setInputVolume()}, \texttt{setEqBand()}, and related intent-level
|
||||||
|
operations without ESP-IDF types.
|
||||||
|
|
||||||
|
\section{IAudioProfileStore}\label{cls:IAudioProfileStore}
|
||||||
|
Persistence boundary for \texttt{AudioProfile} (mixer, EQ, master). Device
|
||||||
|
implementation: \texttt{NvsAudioProfileStore}; host tests use fakes.
|
||||||
|
|
||||||
|
% ------------------------------------------------------------------
|
||||||
|
% Application services (Slice 4–5)
|
||||||
% ------------------------------------------------------------------
|
% ------------------------------------------------------------------
|
||||||
|
|
||||||
\section{ITuner}\label{cls:ITuner}
|
\section{ITuner}\label{cls:ITuner}
|
||||||
@@ -156,3 +194,13 @@ target and volume, and maps driver failures to \texttt{core::TunerError}.
|
|||||||
\texttt{ITuner} adapter over \texttt{Si4684Driver}. Translates domain calls
|
\texttt{ITuner} adapter over \texttt{Si4684Driver}. Translates domain calls
|
||||||
into SPI commands and maps \texttt{Si4684Error} to \texttt{TunerError}.
|
into SPI commands and maps \texttt{Si4684Error} to \texttt{TunerError}.
|
||||||
Constructed once in \texttt{HardwareBootstrap} alongside the driver.
|
Constructed once in \texttt{HardwareBootstrap} alongside the driver.
|
||||||
|
|
||||||
|
\section{AudioService}\label{cls:AudioService}
|
||||||
|
Application service for ADAU1701 mixer, EQ, and master volume. Holds a
|
||||||
|
reference to \texttt{core::IDsp}, tracks the in-memory \texttt{AudioProfile},
|
||||||
|
loads from \texttt{IAudioProfileStore} after boot, and applies changes via
|
||||||
|
safeload. Exposed on \texttt{/api/audio/*} and the web UI Audio section.
|
||||||
|
|
||||||
|
\section{NvsAudioProfileStore}\label{cls:NvsAudioProfileStore}
|
||||||
|
\texttt{IAudioProfileStore} implementation storing serialised
|
||||||
|
\texttt{AudioProfile} JSON in NVS namespace \texttt{digiradio}.
|
||||||
|
|||||||
@@ -194,6 +194,9 @@ bring-up:
|
|||||||
(\texttt{DigiRadio\_IC\_1.h}) replayed through
|
(\texttt{DigiRadio\_IC\_1.h}) replayed through
|
||||||
\texttt{SIGMA\_WRITE\_REGISTER\_BLOCK} after hardware reset. The DSP
|
\texttt{SIGMA\_WRITE\_REGISTER\_BLOCK} after hardware reset. The DSP
|
||||||
program lives in RAM only; download runs on every boot.
|
program lives in RAM only; download runs on every boot.
|
||||||
|
\item \textbf{Audio profile}: \texttt{audio::AudioService::loadAndApply()}
|
||||||
|
restores the saved \texttt{core::AudioProfile} from NVS (or factory
|
||||||
|
defaults) via ADAU1701 safeload before network bring-up.
|
||||||
\end{enumerate}
|
\end{enumerate}
|
||||||
|
|
||||||
If either driver returns an error, the firmware logs the failure and stops
|
If either driver returns an error, the firmware logs the failure and stops
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ Repository: https://github.com/manvalan/DigiRadio
|
|||||||
3. **Companion-chip boot** — done (Slice 3): Si4684 DAB + ADAU1701 RAM load.
|
3. **Companion-chip boot** — done (Slice 3): Si4684 DAB + ADAU1701 RAM load.
|
||||||
4. Station/frequency list model + persistence + UI.
|
4. Station/frequency list model + persistence + UI.
|
||||||
5. Si4684 tuning: RSQ, station list, DAB properties.
|
5. Si4684 tuning: RSQ, station list, DAB properties.
|
||||||
6. ADAU1701 runtime: safeload EQ + input mixer.
|
6. **ADAU1701 runtime** — done (Slice 5): safeload EQ + input mixer + HTTP.
|
||||||
7. FSC-BT1035 driver: AT init (incl. `AT+AUXCFG=1`), audio out.
|
7. FSC-BT1035 driver: AT init (incl. `AT+AUXCFG=1`), audio out.
|
||||||
8. Integration: TunerService + AudioService end to end.
|
8. Integration: TunerService + AudioService end to end.
|
||||||
|
|
||||||
@@ -119,3 +119,23 @@ Acceptance criteria:
|
|||||||
- [x] ADAU1701 reset + I2C + `default_download_IC_1()` replay.
|
- [x] ADAU1701 reset + I2C + `default_download_IC_1()` replay.
|
||||||
- [x] Host test for `EmbeddedBlobReader`; manual sync green.
|
- [x] Host test for `EmbeddedBlobReader`; manual sync green.
|
||||||
- [ ] Device flash verified (requires ESP-IDF toolchain on build host).
|
- [ ] Device flash verified (requires ESP-IDF toolchain on build host).
|
||||||
|
|
||||||
|
## Slice 5 — ADAU1701 runtime + audio API (complete)
|
||||||
|
|
||||||
|
Goal: safeload mixer/EQ/master on the ADAU1701 at runtime; persist user
|
||||||
|
profiles in NVS; expose REST and web UI controls.
|
||||||
|
|
||||||
|
Build on Slice 3–4:
|
||||||
|
- Pure core: `GainDb`, `EqProfile`, `AudioProfile`, `IDsp`, biquad design,
|
||||||
|
`parseAudioProfileJson` / `serializeAudioProfileJson`.
|
||||||
|
- Driver: `sigma_safeload_*`, extended `Adau1701Driver`, `Adau1701Dsp`.
|
||||||
|
- Service: `audio::AudioService`, `secure_store::NvsAudioProfileStore`.
|
||||||
|
- HTTP: `GET/PUT /api/audio/profile`, `POST /api/audio/reset`; Audio
|
||||||
|
section in the web UI. Firmware **0.5.0**.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
- [x] Safeload volume/mixer/EQ without direct param RAM writes during audio.
|
||||||
|
- [x] Profile load/apply after ADAU boot; NVS round-trip via JSON.
|
||||||
|
- [x] Host tests for biquad fixpoint and audio profile JSON.
|
||||||
|
- [x] Doxygen green; manual sync green; `ch-api.tex` documents audio routes.
|
||||||
|
- [ ] Device flash verified on hardware.
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ idf_component_register(
|
|||||||
"main.cpp"
|
"main.cpp"
|
||||||
"hardware_bootstrap.cpp"
|
"hardware_bootstrap.cpp"
|
||||||
INCLUDE_DIRS "."
|
INCLUDE_DIRS "."
|
||||||
REQUIRES core net secure_store adau1701 si4684 tuner
|
REQUIRES core net secure_store adau1701 si4684 tuner audio
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,7 +14,10 @@
|
|||||||
#include "hardware_bootstrap.hpp"
|
#include "hardware_bootstrap.hpp"
|
||||||
|
|
||||||
#include "adau1701/Adau1701Driver.hpp"
|
#include "adau1701/Adau1701Driver.hpp"
|
||||||
|
#include "adau1701/Adau1701Dsp.hpp"
|
||||||
|
#include "audio/AudioService.hpp"
|
||||||
#include "board_pins.hpp"
|
#include "board_pins.hpp"
|
||||||
|
#include "secure_store/NvsAudioProfileStore.hpp"
|
||||||
#include "si4684/Si4684Band.hpp"
|
#include "si4684/Si4684Band.hpp"
|
||||||
#include "si4684/Si4684Driver.hpp"
|
#include "si4684/Si4684Driver.hpp"
|
||||||
#include "si4684/Si4684EmbeddedImages.hpp"
|
#include "si4684/Si4684EmbeddedImages.hpp"
|
||||||
@@ -51,6 +54,9 @@ adau1701::Adau1701Driver gAdau1701(
|
|||||||
.resetGpio = board::pins::Adau1701Reset,
|
.resetGpio = board::pins::Adau1701Reset,
|
||||||
.i2cAddr7 = board::pins::Adau1701Addr,
|
.i2cAddr7 = board::pins::Adau1701Addr,
|
||||||
});
|
});
|
||||||
|
adau1701::Adau1701Dsp gAdau1701Dsp(gAdau1701);
|
||||||
|
secure_store::NvsAudioProfileStore gAudioStore;
|
||||||
|
audio::AudioService gAudioService(gAdau1701Dsp, &gAudioStore);
|
||||||
|
|
||||||
bool gReady = false;
|
bool gReady = false;
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -74,6 +80,10 @@ std::expected<void, HardwareBootError> HardwareBootstrap::boot()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (auto audioResult = gAudioService.loadAndApply(); !audioResult) {
|
||||||
|
ESP_LOGW(kTag, "ADAU1701 profile apply failed");
|
||||||
|
}
|
||||||
|
|
||||||
gReady = true;
|
gReady = true;
|
||||||
ESP_LOGI(kTag, "companion chips ready");
|
ESP_LOGI(kTag, "companion chips ready");
|
||||||
return {};
|
return {};
|
||||||
@@ -84,4 +94,9 @@ si4684::Si4684Tuner& HardwareBootstrap::si4684Tuner()
|
|||||||
return gSi4684Tuner;
|
return gSi4684Tuner;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
audio::AudioService& HardwareBootstrap::audioService()
|
||||||
|
{
|
||||||
|
return gAudioService;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace hardware
|
} // namespace hardware
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
|
|
||||||
#include <expected>
|
#include <expected>
|
||||||
|
|
||||||
|
namespace audio {
|
||||||
|
class AudioService;
|
||||||
|
} // namespace audio
|
||||||
|
|
||||||
namespace si4684 {
|
namespace si4684 {
|
||||||
class Si4684Tuner;
|
class Si4684Tuner;
|
||||||
} // namespace si4684
|
} // namespace si4684
|
||||||
@@ -75,6 +79,20 @@ public:
|
|||||||
* @date 2026-07-06
|
* @date 2026-07-06
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] static si4684::Si4684Tuner& si4684Tuner();
|
[[nodiscard]] static si4684::Si4684Tuner& si4684Tuner();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief audioService — borrow the audio orchestration service after boot.
|
||||||
|
*
|
||||||
|
* @dname audioService
|
||||||
|
* @return Reference to the static AudioService instance.
|
||||||
|
* @pubstate reads static storage initialised by boot().
|
||||||
|
*
|
||||||
|
* Valid only after a successful boot() call in the same process.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-06
|
||||||
|
*/
|
||||||
|
[[nodiscard]] static audio::AudioService& audioService();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace hardware
|
} // namespace hardware
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ void heartbeatTask(void* arg)
|
|||||||
*/
|
*/
|
||||||
extern "C" void app_main()
|
extern "C" void app_main()
|
||||||
{
|
{
|
||||||
ESP_LOGI(kTag, "DigiRadio firmware boot — Slice 4");
|
ESP_LOGI(kTag, "DigiRadio firmware boot — Slice 5");
|
||||||
|
|
||||||
auto hwResult = hardware::HardwareBootstrap::boot();
|
auto hwResult = hardware::HardwareBootstrap::boot();
|
||||||
if (!hwResult) {
|
if (!hwResult) {
|
||||||
@@ -60,7 +60,8 @@ extern "C" void app_main()
|
|||||||
|
|
||||||
static secure_store::NvsSecureStore store;
|
static secure_store::NvsSecureStore store;
|
||||||
|
|
||||||
auto netResult = net::NetBootstrap::start(store, tunerService);
|
auto netResult = net::NetBootstrap::start(
|
||||||
|
store, tunerService, hardware::HardwareBootstrap::audioService());
|
||||||
if (!netResult) {
|
if (!netResult) {
|
||||||
ESP_LOGE(kTag, "network bootstrap failed");
|
ESP_LOGE(kTag, "network bootstrap failed");
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user