Add live VU-meter readback via ADAU1701 Data Capture Register
Implements GET /api/audio/levels, reading all six 1×RTA level-detector cells on demand (no background polling, no caching -- only runs when called, per explicit request this session). The read mechanism is the ADAU1701's documented Data Capture Register (address 2074/0x081A, datasheet Rev.0 pp.30,36: write a (program-step, register-select) pair to configure what the register mirrors, then read back a 3-byte 5.19 twos-complement value). The per-meter program- step indices are taken verbatim from SigmaStudio's own compiler output (Firmware/ADAU1701-Firmware/IC 1_DigiRadioFinale/net_list_out2/ trap.dat), not invented -- the datasheet explicitly says these indices must come from that compiler-generated file. Confirmed live: all six points return distinct, plausible dBFS values, and the output meters track a master-volume change. Also folds two pieces of live user feedback into the app brief: the volume slider should reach +12 dB (the firmware's real ceiling), not stop at 0 dB, and the new levels endpoint is what a "VU meter" UI section should poll instead of faking one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @file AudioLevels.hpp
|
||||
* @brief Snapshot of the six ADAU1701 Data Capture level meters.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-25
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief AudioLevels — one on-demand read of all six 1×RTA meters.
|
||||
*
|
||||
* @dname AudioLevels
|
||||
* @return n/a (type)
|
||||
* @pubstate Read-only snapshot; not part of AudioProfile, never persisted.
|
||||
*
|
||||
* Each field is in dBFS, read live from the ADAU1701 Data Capture Register
|
||||
* (address 2074, MAC_out tap) at the program-step index SigmaStudio's
|
||||
* compiler assigned to that 1×RTA cell (see
|
||||
* Firmware/ADAU1701-Firmware/IC 1_DigiRadioFinale/net_list_out2/trap.dat).
|
||||
* Captured only when requested by an API call -- no background polling.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-25
|
||||
*/
|
||||
struct AudioLevels {
|
||||
float radioInLeftDb; ///< 1×RTA1, post-compressor Si4684 L.
|
||||
float radioInRightDb; ///< 1×RTA2, post-compressor Si4684 R.
|
||||
float bluetoothInLeftDb; ///< 1×RTA3, ESP32 L.
|
||||
float bluetoothInRightDb; ///< 1×RTA4, ESP32 R.
|
||||
float outputLeftDb; ///< 1×RTA1_2, post Bass Boost1, L.
|
||||
float outputRightDb; ///< 1×RTA2_2, post Bass Boost1, R.
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -12,6 +12,7 @@
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/AudioLevels.hpp"
|
||||
#include "core/AudioProfile.hpp"
|
||||
#include "core/ParseError.hpp"
|
||||
|
||||
@@ -104,4 +105,17 @@ namespace core {
|
||||
[[nodiscard]] std::expected<bool, ParseError> parseBeepEnabledJson(
|
||||
std::string_view json);
|
||||
|
||||
/**
|
||||
* @brief serializeAudioLevelsJson — serialise a level-meter snapshot.
|
||||
*
|
||||
* @dname serializeAudioLevelsJson
|
||||
* @param levels On-demand read from IDsp::readLevels().
|
||||
* @return JSON object string for GET /api/audio/levels.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-25
|
||||
*/
|
||||
[[nodiscard]] std::string serializeAudioLevelsJson(const AudioLevels& levels);
|
||||
|
||||
} // namespace core
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/ActiveSource.hpp"
|
||||
#include "core/AudioLevels.hpp"
|
||||
#include "core/AudioProfile.hpp"
|
||||
#include "core/DspError.hpp"
|
||||
#include "core/EnhanceLevel.hpp"
|
||||
@@ -183,6 +184,20 @@ public:
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<void, DspError> writeRawParam(
|
||||
unsigned address, float value) = 0;
|
||||
|
||||
/**
|
||||
* @brief readLevels — on-demand read of all six 1×RTA level meters.
|
||||
*
|
||||
* @dname readLevels
|
||||
* @return AudioLevels snapshot, or DspError.
|
||||
* @pubstate Reads live from the ADAU1701; not part of AudioProfile,
|
||||
* never persisted. Only runs when called, no background
|
||||
* polling.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-25
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<AudioLevels, DspError> readLevels() = 0;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
|
||||
@@ -338,4 +338,16 @@ std::expected<bool, ParseError> parseBeepEnabledJson(std::string_view json)
|
||||
return enabled;
|
||||
}
|
||||
|
||||
std::string serializeAudioLevelsJson(const AudioLevels& levels)
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << "{\"radio_in_left_db\":" << levels.radioInLeftDb
|
||||
<< ",\"radio_in_right_db\":" << levels.radioInRightDb
|
||||
<< ",\"bluetooth_in_left_db\":" << levels.bluetoothInLeftDb
|
||||
<< ",\"bluetooth_in_right_db\":" << levels.bluetoothInRightDb
|
||||
<< ",\"output_left_db\":" << levels.outputLeftDb
|
||||
<< ",\"output_right_db\":" << levels.outputRightDb << "}";
|
||||
return out.str();
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
|
||||
@@ -82,6 +82,12 @@ public:
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<core::AudioLevels, core::DspError>
|
||||
readLevels() override
|
||||
{
|
||||
return core::AudioLevels{};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<void, core::DspError> writeRawParam(
|
||||
unsigned, float) override
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include "adau1701/Adau1701Error.hpp"
|
||||
|
||||
#include "core/AudioLevels.hpp"
|
||||
#include "core/AudioProfile.hpp"
|
||||
#include "core/EnhanceLevel.hpp"
|
||||
#include "core/EqBandIndex.hpp"
|
||||
@@ -269,6 +270,21 @@ public:
|
||||
[[nodiscard]] std::expected<void, Adau1701Error> writeRawParam(
|
||||
unsigned address, float value);
|
||||
|
||||
/**
|
||||
* @brief readLevels — on-demand read of all six 1×RTA level meters.
|
||||
*
|
||||
* @dname readLevels
|
||||
* @return AudioLevels snapshot, or Adau1701Error.
|
||||
* @pubstate Reads the ADAU1701 Data Capture Register (address 2074) six
|
||||
* times in sequence, reconfiguring it for each meter's
|
||||
* program-step/register-select pair before each read; no
|
||||
* background polling, only runs when called.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-25
|
||||
*/
|
||||
[[nodiscard]] std::expected<core::AudioLevels, Adau1701Error> readLevels();
|
||||
|
||||
private:
|
||||
[[nodiscard]] std::expected<void, Adau1701Error> ensureBooted() const;
|
||||
[[nodiscard]] std::expected<void, Adau1701Error> safeloadGain(
|
||||
@@ -277,6 +293,8 @@ private:
|
||||
unsigned paramAddr, std::int32_t fixpoint);
|
||||
[[nodiscard]] std::expected<void, Adau1701Error> replayProgram(
|
||||
const core::DspProgram& program);
|
||||
[[nodiscard]] std::expected<float, Adau1701Error> readCaptureDb(
|
||||
unsigned progCount, unsigned regSel);
|
||||
|
||||
Adau1701Pins pins_;
|
||||
core::IDspProgramSource& programSource_;
|
||||
|
||||
@@ -70,6 +70,9 @@ public:
|
||||
[[nodiscard]] std::expected<void, core::DspError> writeRawParam(
|
||||
unsigned address, float value) override;
|
||||
|
||||
[[nodiscard]] std::expected<core::AudioLevels, core::DspError>
|
||||
readLevels() override;
|
||||
|
||||
private:
|
||||
[[nodiscard]] static core::DspError mapError(Adau1701Error error) noexcept;
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#include "core/BiquadDesign.hpp"
|
||||
#include "core/DspProgram.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "DigiRadio_IC_1_PARAM.h"
|
||||
#include "SigmaStudioFW.h"
|
||||
|
||||
@@ -559,6 +561,121 @@ namespace adau1701
|
||||
return safeloadFixpoint(address, core::floatToFixpoint823(value));
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// ADAU1701 Data Capture Register, address 2074 (0x081A) -- see
|
||||
// datasheet Rev.0 "2074 TO 2075 (0x081A TO 0x081B)--DATA CAPTURE
|
||||
// REGISTERS" (p.36) and Table 32's register-map bit layout (p.30):
|
||||
// D[11:2]=PC[9:0] (program-step index), D[1:0]=RS[1:0] (register
|
||||
// select). Reading the same address afterward returns a 3-byte,
|
||||
// 24-bit two's-complement 5.19 value (5.23 with the 4 LSBs
|
||||
// truncated, per the datasheet). The per-meter (progCount, regSel)
|
||||
// pairs below are read verbatim from SigmaStudio's own compiler
|
||||
// output (Firmware/ADAU1701-Firmware/IC 1_DigiRadioFinale/
|
||||
// net_list_out2/trap.dat), not invented.
|
||||
constexpr unsigned kDataCaptureAddr = 0x081AU;
|
||||
constexpr unsigned kRegSelMacOut = 2U; // "mult_out" in trap.dat
|
||||
|
||||
struct MeterPoint
|
||||
{
|
||||
unsigned progCount;
|
||||
};
|
||||
|
||||
constexpr MeterPoint kRadioInLeft{221}; // SingleBandLevelDet1
|
||||
constexpr MeterPoint kRadioInRight{254}; // SingleBandLevelDet2
|
||||
constexpr MeterPoint kBluetoothInLeft{155}; // SingleBandLevelDet3
|
||||
constexpr MeterPoint kBluetoothInRight{188}; // SingleBandLevelDet4
|
||||
constexpr MeterPoint kOutputLeft{717}; // SingleBandLevelDet5
|
||||
constexpr MeterPoint kOutputRight{684}; // SingleBandLevelDet6
|
||||
} // namespace
|
||||
|
||||
std::expected<float, Adau1701Error> Adau1701Driver::readCaptureDb(
|
||||
unsigned progCount, unsigned regSel)
|
||||
{
|
||||
const unsigned char deviceAddr =
|
||||
static_cast<unsigned char>(pins_.i2cAddr7 << 1);
|
||||
const std::uint16_t config = static_cast<std::uint16_t>(
|
||||
((progCount & 0x3FFU) << 2) | (regSel & 0x3U));
|
||||
unsigned char configBytes[2U] = {
|
||||
static_cast<unsigned char>((config >> 8) & 0xFFU),
|
||||
static_cast<unsigned char>(config & 0xFFU),
|
||||
};
|
||||
if (SIGMA_WRITE_REGISTER_BLOCK(deviceAddr, kDataCaptureAddr, 2U,
|
||||
configBytes) != 0)
|
||||
{
|
||||
return std::unexpected(Adau1701Error::SafeloadFailed);
|
||||
}
|
||||
|
||||
unsigned char raw[3U] = {0U, 0U, 0U};
|
||||
if (sigma_i2c_read(kDataCaptureAddr, raw, sizeof(raw)) != 0)
|
||||
{
|
||||
return std::unexpected(Adau1701Error::SafeloadFailed);
|
||||
}
|
||||
std::int32_t value = (static_cast<std::int32_t>(raw[0]) << 16)
|
||||
| (static_cast<std::int32_t>(raw[1]) << 8)
|
||||
| static_cast<std::int32_t>(raw[2]);
|
||||
if ((value & (1 << 23)) != 0)
|
||||
{
|
||||
value -= (1 << 24);
|
||||
}
|
||||
const float linear =
|
||||
static_cast<float>(value) / static_cast<float>(1 << 19);
|
||||
constexpr float kFloorDb = -96.0F;
|
||||
if (std::fabs(linear) < 1e-5F)
|
||||
{
|
||||
return kFloorDb;
|
||||
}
|
||||
const float db = 20.0F * std::log10(std::fabs(linear));
|
||||
return db < kFloorDb ? kFloorDb : db;
|
||||
}
|
||||
|
||||
std::expected<core::AudioLevels, Adau1701Error> Adau1701Driver::readLevels()
|
||||
{
|
||||
if (auto ready = ensureBooted(); !ready)
|
||||
{
|
||||
return std::unexpected(ready.error());
|
||||
}
|
||||
|
||||
core::AudioLevels levels{};
|
||||
auto read = [this](MeterPoint point,
|
||||
float &out) -> std::expected<void, Adau1701Error>
|
||||
{
|
||||
const auto db = readCaptureDb(point.progCount, kRegSelMacOut);
|
||||
if (!db)
|
||||
{
|
||||
return std::unexpected(db.error());
|
||||
}
|
||||
out = *db;
|
||||
return {};
|
||||
};
|
||||
|
||||
if (auto r = read(kRadioInLeft, levels.radioInLeftDb); !r)
|
||||
{
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
if (auto r = read(kRadioInRight, levels.radioInRightDb); !r)
|
||||
{
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
if (auto r = read(kBluetoothInLeft, levels.bluetoothInLeftDb); !r)
|
||||
{
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
if (auto r = read(kBluetoothInRight, levels.bluetoothInRightDb); !r)
|
||||
{
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
if (auto r = read(kOutputLeft, levels.outputLeftDb); !r)
|
||||
{
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
if (auto r = read(kOutputRight, levels.outputRightDb); !r)
|
||||
{
|
||||
return std::unexpected(r.error());
|
||||
}
|
||||
return levels;
|
||||
}
|
||||
|
||||
std::expected<void, Adau1701Error> Adau1701Driver::applyEq(
|
||||
const core::EqProfile &eq)
|
||||
{
|
||||
|
||||
@@ -115,4 +115,13 @@ std::expected<void, core::DspError> Adau1701Dsp::writeRawParam(
|
||||
return {};
|
||||
}
|
||||
|
||||
std::expected<core::AudioLevels, core::DspError> Adau1701Dsp::readLevels()
|
||||
{
|
||||
auto result = driver_.readLevels();
|
||||
if (!result) {
|
||||
return std::unexpected(mapError(result.error()));
|
||||
}
|
||||
return *result;
|
||||
}
|
||||
|
||||
} // namespace adau1701
|
||||
|
||||
@@ -971,6 +971,39 @@ esp_err_t audioBeepPostHandler(httpd_req_t* req)
|
||||
return httpd_resp_send(req, json.c_str(), json.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief audioLevelsGetHandler — serve GET /api/audio/levels as JSON.
|
||||
*
|
||||
* @dname audioLevelsGetHandler
|
||||
* @param req HTTP request handle from esp_http_server.
|
||||
* @return ESP_OK on success, or an esp_err_t error code.
|
||||
* @pubstate Reads all six 1×RTA level meters live on every call -- no
|
||||
* background polling, no caching.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-25
|
||||
*/
|
||||
esp_err_t audioLevelsGetHandler(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 auto levels = ctx->audio->readLevels();
|
||||
if (!levels) {
|
||||
const std::string json = core::serializeAudioErrorJson("dsp_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::serializeAudioLevelsJson(*levels);
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
return httpd_resp_send(req, json.c_str(), json.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief dspParamsGetHandler — serve GET /api/dsp/params as JSON.
|
||||
*
|
||||
@@ -2252,6 +2285,14 @@ std::expected<void, NetError> SetupWebServer::start(
|
||||
};
|
||||
httpd_register_uri_handler(server_, &audioBeepUri);
|
||||
|
||||
const httpd_uri_t audioLevelsUri = {
|
||||
.uri = "/api/audio/levels",
|
||||
.method = HTTP_GET,
|
||||
.handler = audioLevelsGetHandler,
|
||||
.user_ctx = routeCtx,
|
||||
};
|
||||
httpd_register_uri_handler(server_, &audioLevelsUri);
|
||||
|
||||
const httpd_uri_t dspParamsUri = {
|
||||
.uri = "/api/dsp/params",
|
||||
.method = HTTP_GET,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/ActiveSource.hpp"
|
||||
#include "core/AudioLevels.hpp"
|
||||
#include "core/AudioProfile.hpp"
|
||||
#include "core/DspError.hpp"
|
||||
#include "core/EnhanceLevel.hpp"
|
||||
@@ -229,6 +230,20 @@ public:
|
||||
[[nodiscard]] std::expected<void, core::DspError> writeRawParam(
|
||||
unsigned address, float value);
|
||||
|
||||
/**
|
||||
* @brief readLevels — on-demand read of all six level meters.
|
||||
*
|
||||
* @dname readLevels
|
||||
* @return AudioLevels snapshot, or DspError.
|
||||
* @pubstate live-only: not part of AudioProfile, never persisted, does
|
||||
* not touch profile_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-25
|
||||
*/
|
||||
[[nodiscard]] std::expected<core::AudioLevels, core::DspError>
|
||||
readLevels();
|
||||
|
||||
private:
|
||||
[[nodiscard]] std::expected<void, core::StoreError> persistProfile() const;
|
||||
|
||||
|
||||
@@ -193,4 +193,9 @@ std::expected<void, core::DspError> AudioService::writeRawParam(
|
||||
return dsp_.writeRawParam(address, value);
|
||||
}
|
||||
|
||||
std::expected<core::AudioLevels, core::DspError> AudioService::readLevels()
|
||||
{
|
||||
return dsp_.readLevels();
|
||||
}
|
||||
|
||||
} // namespace audio
|
||||
|
||||
@@ -112,6 +112,7 @@ un intero grezzo a 32 bit (vedi sopra).
|
||||
| `/api/audio/stereo-enhance` | POST | `{"level":0-100}` | `SPHAT1_SPREAD1/2` (scala proporzionale) | ✅ | ✅ (effetto soggettivo/sottile) |
|
||||
| `/api/audio/bass-enhance` | POST | `{"level":0-100}` | `BASSBOOST1_*` filtro+tabella (scala verso identità) | ✅ | ✅ (confermato su radio, non su tono fisso — l'algoritmo è dinamico, reagisce a contenuto con dinamica reale) |
|
||||
| `/api/audio/beep` | POST | `{"enabled":true/false}` | `BEEP1_ENABLE/KICK` | ❌ (live-only, per design) | ✅ |
|
||||
| `/api/audio/levels` | GET | — | legge live i 6 VU-meter (Data Capture Register 2074) | — (nessuna cache/polling) | ✅ |
|
||||
| `/api/dsp/params` | GET | — | elenco statico nome→indirizzo (230 celle, incluse le 6 speciali readback) | — | ✅ |
|
||||
| `/api/dsp/param` | PUT | `{"name":"...","value":<float>}` | qualunque cella per nome — **eccetto `DC1`, che qui va scritto già come intero puro, non tramite `selectSource()`** | ❌ (live-only) | ✅ (sweep completo di 223/224 celle scrivibili, 0 errori) |
|
||||
|
||||
@@ -126,16 +127,39 @@ Boost1/SPhat1.
|
||||
|
||||
---
|
||||
|
||||
## 4. VU-meter (readback) — non ancora implementato
|
||||
## 4. VU-meter (readback) — implementato 2026-08-25
|
||||
|
||||
Le 6 celle `1×RTA*` sono veri VU-meter (level detector) posizionati in 4 punti
|
||||
d'ingresso e 2 punti d'uscita. Il loro valore letto NON sta al normale indirizzo
|
||||
Parameter RAM di ciascuna — condividono tutte l'indirizzo speciale **2074**, con
|
||||
un codice di selezione diverso per ciascuna (`VALUES_1XRTA1` = 0x0376, ecc., tipo
|
||||
`SIGMASTUDIOTYPE_SPECIAL`/`SIGMASTUDIOTYPE_10_14`). Questo è il meccanismo di
|
||||
"Data Capture" dell'ADAU1701 (registro indiretto), diverso dal semplice
|
||||
read/safeload usato ovunque altrove in questo firmware. **Va ancora reverse-
|
||||
engineerato e implementato** — non è stato inventato né testato in questa sessione.
|
||||
d'ingresso e 2 punti d'uscita. Il loro valore NON sta al normale indirizzo
|
||||
Parameter RAM di ciascuna — usano il **Data Capture Register** dell'ADAU1701
|
||||
(indirizzo 2074/0x081A, datasheet Rev.0 pag. 30 e 36, Table 28/29/32/44/45):
|
||||
|
||||
1. Si scrive un word di 16 bit a 2074: bit[11:2] = indice del passo di
|
||||
programma (`PC[9:0]`) dove campionare, bit[1:0] = quale registro interno
|
||||
del core trasferire (`RS[1:0]`; `10`=MAC_out).
|
||||
2. Si rilegge lo stesso indirizzo 2074: risponde con 3 byte (24 bit,
|
||||
complemento a due, formato 5.19 — il 5.23 interno con i 4 LSB troncati).
|
||||
|
||||
Gli indici `PC[9:0]` per ciascuno dei 6 meter **non sono stati inventati**:
|
||||
vengono letti testualmente dal file che il compilatore SigmaStudio genera
|
||||
apposta per questo (`IC 1_DigiRadioFinale/net_list_out2/trap.dat`):
|
||||
|
||||
| Meter | Punto | Program-step (trap.dat) |
|
||||
|---|---|---|
|
||||
| `1×RTA1` | Radio L, post-compressore | 221 |
|
||||
| `1×RTA2` | Radio R, post-compressore | 254 |
|
||||
| `1×RTA3` | ESP32 L | 155 |
|
||||
| `1×RTA4` | ESP32 R | 188 |
|
||||
| `1×RTA1_2` | Uscita L (post Bass Boost1) | 717 |
|
||||
| `1×RTA2_2` | Uscita R (post Bass Boost1) | 684 |
|
||||
|
||||
Il chip ha **solo 2 registri di cattura hardware** (2074, 2075); il firmware
|
||||
ne usa uno solo, riconfigurandolo e rileggendolo in sequenza per i 6 punti —
|
||||
**solo quando arriva una richiesta** `GET /api/audio/levels`, nessun polling
|
||||
in background. Testato dal vivo: valori plausibili e distinti per ciascun
|
||||
punto, e l'uscita scende coerentemente abbassando il master volume.
|
||||
|
||||
Vedi `Adau1701Driver::readLevels()`/`readCaptureDb()` per l'implementazione.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,118 +1,214 @@
|
||||
# Brief per Cursor — allineamento app iOS al nuovo firmware DSP (DigiRadioFinale)
|
||||
# Brief per Cursor — allineamento app iOS al firmware DigiRadioFinale
|
||||
|
||||
Il firmware ADAU1701 è stato sostituito con un programma DSP molto più ricco
|
||||
(224 parametri contro 74 prima). Questo documento descrive **cosa è cambiato
|
||||
lato API HTTP** e **quali controlli nuovi puoi costruire in app**, più qualche
|
||||
indicazione di UX. I dettagli implementativi Swift (struttura file, ViewModels
|
||||
esistenti, tab bar) restano una tua scelta — qui do solo il contratto dati e
|
||||
gli obiettivi funzionali.
|
||||
Questo documento è vincolante: ogni punto marcato **DEVE** è un requisito, non
|
||||
un suggerimento. Se qualcosa non è chiaro o sembra in conflitto con codice
|
||||
esistente, chiedi prima di improvvisare una soluzione diversa — non inventare
|
||||
comportamenti non specificati qui.
|
||||
|
||||
Dispositivo di test: `http://192.168.1.62` (mDNS `digiradio-CC4DB4.local`).
|
||||
Dispositivo di test reale: `http://192.168.1.62` (mDNS `digiradio-CC4DB4.local`).
|
||||
Verifica ogni funzionalità contro il dispositivo vero prima di considerarla
|
||||
finita, non solo con dati mock.
|
||||
|
||||
---
|
||||
|
||||
## 1. Breaking change: `mixer` → `active_source`
|
||||
## 0. Problemi riportati oggi dall'utente (da risolvere tutti)
|
||||
|
||||
**Prima**: `GET/PUT /api/audio/profile` aveva un oggetto `"mixer"` con guadagni
|
||||
indipendenti per Si4684, ESP32 e i due leg del mixer — permetteva di
|
||||
"mescolare" radio e Bluetooth insieme.
|
||||
1. L'interfaccia non è "in stile Apple" — troppo grezza/disorganizzata.
|
||||
2. I nuovi controlli DSP (selettore sorgente, Bass Boost, Stereo Spread) non
|
||||
sono stati implementati.
|
||||
3. Non è possibile selezionare la sorgente di ingresso (radio/Bluetooth).
|
||||
4. Il controllo del volume è sbagliato/inconsistente.
|
||||
5. Le stazioni: uno scan completo produce una lista lunga sia FM che DAB, ma
|
||||
aprendo il tab FM o il tab DAB separatamente dice "nessuna stazione" —
|
||||
vanno unificate in un'unica lista.
|
||||
|
||||
**Ora**: il firmware non ha più un mixer. C'è un **selettore di sorgente
|
||||
esclusivo** — si ascolta una sorgente alla volta, come su un vero stereo.
|
||||
Le sezioni seguenti danno il contratto dati esatto e i requisiti UI per
|
||||
risolvere ciascuno di questi punti. Non è accettabile implementare solo una
|
||||
parte e considerare il resto "per dopo" senza dirlo esplicitamente.
|
||||
|
||||
---
|
||||
|
||||
## 1. Selettore sorgente (sostituisce il vecchio mixer) — OBBLIGATORIO
|
||||
|
||||
Il firmware non ha più un mixer che combina sorgenti. **DEVE** esserci un
|
||||
controllo a scelta singola (es. `Picker` segmented, non due slider separati)
|
||||
con tre opzioni: **Radio**, **Bluetooth**, e opzionalmente **Test tone**
|
||||
(quest'ultimo solo in una sezione diagnostica, non nella UI principale).
|
||||
|
||||
Contratto API — `PUT /api/audio/profile`, corpo completo (tutti i campi sono
|
||||
obbligatori nella richiesta, il firmware non li fa opzionali):
|
||||
|
||||
```json
|
||||
{
|
||||
"active_source": "radio",
|
||||
"master": {"left_db": 0, "right_db": 0},
|
||||
"eq": [ ... 6 bande, invariato ... ],
|
||||
"eq": [
|
||||
{"gain_db": 0, "center_hz": 20, "q": 1.414},
|
||||
{"gain_db": 0, "center_hz": 100, "q": 1},
|
||||
{"gain_db": 0, "center_hz": 400, "q": 1},
|
||||
{"gain_db": 0, "center_hz": 1000, "q": 1},
|
||||
{"gain_db": 0, "center_hz": 3000, "q": 1},
|
||||
{"gain_db": 0, "center_hz": 8000, "q": 1}
|
||||
],
|
||||
"enhancements": {"stereo_level": 0, "bass_level": 0}
|
||||
}
|
||||
```
|
||||
|
||||
`active_source` accetta esattamente `"radio"`, `"bluetooth"`, `"beep"`.
|
||||
`"beep"` è il tono di test interno (usato per diagnostica firmware, probabilmente
|
||||
da nascondere in UI di produzione o mettere in una sezione "Diagnostica").
|
||||
`active_source` accetta **esattamente** le stringhe `"radio"`, `"bluetooth"`,
|
||||
`"beep"` (minuscolo, invariato). Nessun altro valore.
|
||||
|
||||
**Azione richiesta**: sostituire ogni UI che oggi mostra due slider indipendenti
|
||||
(volume radio / volume BT) con un **selettore a scelta singola** (segmented
|
||||
control o lista) tra Radio e Bluetooth. Non esiste più un modo per sentirli
|
||||
mescolati.
|
||||
`GET /api/audio/profile` restituisce lo stesso oggetto — usalo per
|
||||
inizializzare il Picker all'avvio schermata, non assumere sempre "radio".
|
||||
|
||||
**Errore comune da evitare**: NON costruire il body facendo merge parziale di
|
||||
un vecchio oggetto `"mixer"` — quel campo non esiste più, se lo mandi il
|
||||
parser lo ignora e basta (non causa errore, ma non fa nulla).
|
||||
|
||||
---
|
||||
|
||||
## 2. `enhancements` ora pilota algoritmi DSP reali, non più un trucco EQ
|
||||
## 2. Volume — contratto esatto
|
||||
|
||||
**Prima**: `bass_level`/`stereo_level` sovrascrivevano silenziosamente alcune
|
||||
bande dell'equalizzatore manuale (da cui il campo `"locked"` per banda, per
|
||||
segnalarlo).
|
||||
Il volume master è **nello stesso oggetto profilo**, campo `"master"`:
|
||||
|
||||
**Ora**: pilotano due blocchi DSP dedicati e indipendenti dall'EQ:
|
||||
- `bass_level` (0-100) → **Bass Boost1**, un vero algoritmo ADI di "Dynamic
|
||||
Bass Boost" (filtro crossover + compander dinamico). Effetto udibile solo su
|
||||
contenuto reale con dinamica (radio, streaming) — su un tono fisso costante
|
||||
non si sente quasi nulla, è normale (l'algoritmo reagisce a variazioni di
|
||||
livello nel tempo).
|
||||
- `stereo_level` (0-100) → **SPhat1** ("SuperPhat" Spatializer/stereo widener),
|
||||
un secondo algoritmo ADI dedicato.
|
||||
```json
|
||||
"master": {"left_db": -6, "right_db": -6}
|
||||
```
|
||||
|
||||
Il campo `"locked"` per banda EQ **ora è sempre `false`** tranne la banda 0
|
||||
(passa-alto fisso, sempre `true`, invariato da prima). Puoi quindi rimuovere
|
||||
qualunque logica "banda grigia perché l'enhancement l'ha sovrascritta" —
|
||||
l'EQ manuale ora è sempre indipendente dagli enhancement.
|
||||
- Range valido: **-96.0 .. +12.0** (dB). Valori fuori range vengono rifiutati
|
||||
dal firmware (risposta di errore, non clamping silenzioso) — clampa lato
|
||||
client PRIMA di inviare.
|
||||
- **Non fermare lo slider a 0 dB** — il firmware supporta fino a **+12 dB** di
|
||||
guadagno reale sul master. Se lo slider attuale si ferma a 0 e "sembra
|
||||
basso", è un limite arbitrario della UI, non del firmware: estendi il range
|
||||
fino a +12 dB.
|
||||
- Normalmente L e R vanno impostati **uguali** con un unico slider "Volume"
|
||||
(non serve un controllo stereo separato per il volume master, a meno che
|
||||
non venga esplicitamente richiesto un bilanciamento L/R).
|
||||
- Ogni `PUT /api/audio/profile` **sostituisce l'intero profilo** — se lo slider
|
||||
del volume invia solo `{"master": {...}}` senza gli altri campi
|
||||
(`active_source`, `eq`, `enhancements`), il firmware risponde con errore di
|
||||
campo mancante. **DEVI sempre inviare l'oggetto completo**, leggendo prima lo
|
||||
stato corrente con `GET /api/audio/profile` (o tenendolo in uno store locale
|
||||
sempre sincronizzato) e poi cambiando solo il campo che l'utente ha toccato.
|
||||
|
||||
**Azione richiesta**: nessun cambio di forma dati per `enhancements` (stessi
|
||||
due slider 0-100 di prima), ma puoi rimuovere la UI "banda bloccata" per le
|
||||
bande 1-5 (resta solo per la banda 0, che era già così).
|
||||
Questo "invia sempre tutto l'oggetto" è quasi certamente la causa del
|
||||
"volume sbagliato": se il codice attuale manda solo il volume da solo, il
|
||||
firmware lo rifiuta o (se altri campi arrivano con default sbagliati) resetta
|
||||
sorgente/EQ/enhancement senza che l'utente l'abbia chiesto.
|
||||
|
||||
---
|
||||
|
||||
## 3. Non ancora disponibile lato firmware (in arrivo)
|
||||
## 3. Bass Boost e Stereo Spread — nuovi controlli, OBBLIGATORI
|
||||
|
||||
- **VU-meter / readback dei livelli** (in ingresso e in uscita) — il firmware
|
||||
ha 6 sensori di livello nel DSP ma il meccanismo di lettura (un registro
|
||||
indiretto dell'ADAU1701) non è ancora implementato. Non costruire ancora una
|
||||
UI che dipende da dati di livello in tempo reale dal firmware — se vuoi una
|
||||
sezione "grafica" ora, usa un placeholder o un'animazione generica non
|
||||
agganciata a dati reali, finché non arriva l'endpoint.
|
||||
- **Voice Clarifier** — la cella DSP dedicata (`Gen Filter1`) esiste nella
|
||||
catena del segnale ma non è ancora tarata (passa tutto invariato). Nessuna
|
||||
API la pilota ancora. Non esporre ancora questo controllo in UI, o mettilo
|
||||
disabilitato/"prossimamente".
|
||||
- **Mute in uscita** (pre-Output1/Output2) — non presente in questo export del
|
||||
firmware. Se serve, va aggiunto lato SigmaStudio prima di poter esporlo via API.
|
||||
Due slider 0-100, indipendenti da sorgente/volume/EQ:
|
||||
|
||||
Ti avviso appena questi sono pronti lato firmware con l'endpoint esatto.
|
||||
```
|
||||
POST /api/audio/bass-enhance {"level": 0-100}
|
||||
POST /api/audio/stereo-enhance {"level": 0-100}
|
||||
```
|
||||
|
||||
- Questi sono endpoint **a sé stanti**, non fanno parte del body di
|
||||
`/api/audio/profile` per la scrittura (ma il loro stato corrente torna
|
||||
dentro `GET /api/audio/profile` → `"enhancements": {"bass_level":.., "stereo_level":..}`).
|
||||
- Bass Boost è un algoritmo **dinamico**: su un tono fisso di test non si
|
||||
sente quasi nulla, è normale — non è un bug se durante un test con la
|
||||
radio spenta sembra "non fare niente".
|
||||
- Non esiste più il vecchio comportamento per cui alzare questi livelli
|
||||
"bloccava" delle bande dell'equalizzatore manuale — l'EQ è ora sempre
|
||||
indipendente. Il campo `"locked"` per banda nell'array `eq[]` (in
|
||||
`GET /api/audio/profile`) è ora sempre `false` tranne la banda 0 (che è
|
||||
sempre `true`, invariato da prima — è un passa-alto fisso, non toccarlo).
|
||||
|
||||
---
|
||||
|
||||
## 4. Riepilogo controlli disponibili ORA (tutti testati dal vivo sul dispositivo)
|
||||
## 4. Lista stazioni unificata — OBBLIGATORIO
|
||||
|
||||
| Controllo | Endpoint | Corpo |
|
||||
|---|---|---|
|
||||
| Sorgente attiva | `PUT /api/audio/profile` (campo `active_source`) | `"radio"` \| `"bluetooth"` \| `"beep"` |
|
||||
| Master volume | `PUT /api/audio/profile` (campo `master`) | `{"left_db":..,"right_db":..}`, range tipico -96..+12 dB |
|
||||
| Equalizzatore (6 bande) | `PUT /api/audio/profile` (campo `eq`) | invariato: `gain_db`, `center_hz`, `q` per banda; banda 0 sempre inerte |
|
||||
| Bass Boost | `POST /api/audio/bass-enhance` | `{"level":0-100}` |
|
||||
| Stereo Spread | `POST /api/audio/stereo-enhance` | `{"level":0-100}` |
|
||||
| Tono di test (diagnostica) | `POST /api/audio/beep` | `{"enabled":true/false}` — richiede anche `active_source:"beep"` per essere udibile |
|
||||
| Lettura stato completo | `GET /api/audio/profile` | risposta con tutti i campi sopra |
|
||||
Il backend ha **un solo elenco stazioni**, non due:
|
||||
|
||||
```
|
||||
GET /api/stations
|
||||
```
|
||||
|
||||
restituisce un array dove ogni stazione ha un campo `"band"` che vale
|
||||
`"fm"` oppure `"dab"`, più i campi specifici di banda (`fm_frequency_khz` per
|
||||
FM; `dab_freq_index`, `dab_service_id`, `dab_component_id` per DAB).
|
||||
|
||||
**Il bug riportato oggi** (lo scan produce una lista lunga, ma il tab FM o il
|
||||
tab DAB dicono "nessuna stazione") indica che l'app sta usando due sorgenti
|
||||
dati diverse — una per la lista generale/scan e una diversa (vuota o rotta)
|
||||
per i tab FM/DAB filtrati. **DEVI unificare**: un solo `StationListStore` (o
|
||||
equivalente) alimentato da `GET /api/stations`, con i tab FM e DAB che sono
|
||||
semplicemente **filtri client-side** (`station.band == .fm` / `.dab`) sulla
|
||||
STESSA lista, non due chiamate/store separati.
|
||||
|
||||
UI richiesta: un'unica lista in stile Apple (`List` con sezioni, o una vista
|
||||
tipo Impostazioni), non due implementazioni diverse per FM e DAB — stesso
|
||||
componente di riga, stesso stile, filtrato per banda.
|
||||
|
||||
---
|
||||
|
||||
## 5. Indicazioni di stile (dalla richiesta dell'utente)
|
||||
## 5. Stile UI — Apple, minimalista ma con una sezione grafica curata
|
||||
|
||||
- Stile Apple/HIG nativo: niente slider/bottoni "grezzi" o accozzaglia in
|
||||
un'unica schermata. Raggruppa per tab/sezione logica (es. "Ascolto" per
|
||||
sorgente+volume, "Suono" per EQ+Bass Boost+Stereo Spread, "Bluetooth" per
|
||||
pairing, "Diagnostica" per tono di test/dettagli tecnici).
|
||||
- Preferisci componenti nativi SwiftUI (`Picker` segmented per la sorgente,
|
||||
`Slider` con `.tint()` per i livelli, liste con `Form`/`List` in stile
|
||||
Impostazioni) piuttosto che controlli custom pesanti.
|
||||
- La sezione "grafica" (visualizzazione carina) può oggi mostrare solo dati
|
||||
già disponibili (EQ come curva, o un'animazione leggera legata allo stato
|
||||
sorgente/tono) — non agganciarla a VU-meter reali finché non sono pronti
|
||||
(punto 3).
|
||||
- Dato che non esiste più il mix simultaneo radio+BT, il cambio sorgente è
|
||||
un'azione "netta" (come cambiare stazione) — vale la pena un feedback visivo
|
||||
chiaro (es. breve transizione/fade nell'interfaccia, non nell'audio: il
|
||||
cambio DSP è istantaneo) quando l'utente lo seleziona.
|
||||
- Componenti nativi SwiftUI: `Picker` segmented per la sorgente, `Slider` con
|
||||
`.tint()` per volume/bass/stereo, `List`/`Form` in stile Impostazioni per le
|
||||
stazioni e le opzioni tecniche. Niente controlli custom pesanti o griglie di
|
||||
bottoni non standard.
|
||||
- Organizza per tab/sezione logica, non tutto in una schermata:
|
||||
- **Ascolto**: sorgente attiva, volume, stazione corrente.
|
||||
- **Suono**: EQ 6 bande, Bass Boost, Stereo Spread.
|
||||
- **Stazioni**: lista unificata FM+DAB (vedi §4), con scan.
|
||||
- **Bluetooth**: pairing, dispositivo connesso.
|
||||
- **Diagnostica**: tono di test, dettagli tecnici/versione firmware — non
|
||||
mescolare con i controlli quotidiani.
|
||||
- Una sezione "grafica" curata è benvenuta (es. una card "Now Playing" con
|
||||
sfondo sfumato/blur, animazione leggera sul cambio sorgente). Ora **puoi**
|
||||
agganciarla a dati reali di livello audio — vedi §7, l'endpoint VU-meter è
|
||||
disponibile da oggi.
|
||||
|
||||
---
|
||||
|
||||
## 7. VU-meter — nuovo, disponibile da oggi
|
||||
|
||||
```
|
||||
GET /api/audio/levels
|
||||
```
|
||||
|
||||
Risposta:
|
||||
|
||||
```json
|
||||
{
|
||||
"radio_in_left_db": -1.5,
|
||||
"radio_in_right_db": -1.5,
|
||||
"bluetooth_in_left_db": -0.9,
|
||||
"bluetooth_in_right_db": -1.0,
|
||||
"output_left_db": -1.9,
|
||||
"output_right_db": -2.0
|
||||
}
|
||||
```
|
||||
|
||||
Valori reali in dBFS, letti dal vivo dal DSP **ad ogni chiamata** — il
|
||||
firmware non fa polling in background né cache. Se vuoi un meter che si
|
||||
aggiorna in tempo reale, il polling lo fai tu lato app (es. ogni 200-500ms
|
||||
mentre la schermata è visibile) — **fermalo quando la schermata non è a
|
||||
video**, per non generare traffico I2C continuo inutile sul dispositivo.
|
||||
Questa è la sezione "grafica" giusta per barre VU vere (radio in / BT in /
|
||||
uscita), non un'animazione finta.
|
||||
|
||||
---
|
||||
|
||||
## 8. Checklist di autoverifica prima di considerare il lavoro finito
|
||||
|
||||
- [ ] Cambiare sorgente da Radio a Bluetooth nell'app cambia davvero l'audio
|
||||
sul dispositivo reale (non solo lo stato locale dell'app).
|
||||
- [ ] Cambiare il volume aggiorna il volume reale e **non** resetta
|
||||
accidentalmente sorgente/EQ/enhancement.
|
||||
- [ ] Bass Boost e Stereo Spread hanno uno slider visibile, funzionante, e il
|
||||
valore torna corretto dopo un refresh/riavvio app (persistente lato
|
||||
firmware).
|
||||
- [ ] Il tab FM mostra le stazioni FM dello scan; il tab DAB mostra quelle
|
||||
DAB; entrambe vengono dalla stessa fonte dati.
|
||||
- [ ] Nessuna schermata mostra contemporaneamente controlli tecnici
|
||||
(indirizzi DSP, tono di test) mescolati a quelli quotidiani.
|
||||
- [ ] Lo slider del volume arriva fino a +12 dB, non si ferma a 0 dB.
|
||||
- [ ] I VU-meter (se implementati) mostrano numeri che cambiano nel tempo con
|
||||
l'audio reale, e il polling si ferma quando la schermata non è visibile.
|
||||
|
||||
Reference in New Issue
Block a user