diff --git a/Software/components/core/include/core/AudioLevels.hpp b/Software/components/core/include/core/AudioLevels.hpp new file mode 100644 index 0000000..1a6c473 --- /dev/null +++ b/Software/components/core/include/core/AudioLevels.hpp @@ -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 diff --git a/Software/components/core/include/core/AudioProfileJson.hpp b/Software/components/core/include/core/AudioProfileJson.hpp index e29d85a..f491803 100644 --- a/Software/components/core/include/core/AudioProfileJson.hpp +++ b/Software/components/core/include/core/AudioProfileJson.hpp @@ -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 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 diff --git a/Software/components/core/include/core/IDsp.hpp b/Software/components/core/include/core/IDsp.hpp index 483eff5..8ab869a 100644 --- a/Software/components/core/include/core/IDsp.hpp +++ b/Software/components/core/include/core/IDsp.hpp @@ -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 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 readLevels() = 0; }; } // namespace core diff --git a/Software/components/core/src/AudioProfileJson.cpp b/Software/components/core/src/AudioProfileJson.cpp index 21f644b..b3f772d 100644 --- a/Software/components/core/src/AudioProfileJson.cpp +++ b/Software/components/core/src/AudioProfileJson.cpp @@ -338,4 +338,16 @@ std::expected 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 diff --git a/Software/components/core/test/integration_service_test.cpp b/Software/components/core/test/integration_service_test.cpp index 1a0097e..42e1db1 100644 --- a/Software/components/core/test/integration_service_test.cpp +++ b/Software/components/core/test/integration_service_test.cpp @@ -82,6 +82,12 @@ public: return {}; } + [[nodiscard]] std::expected + readLevels() override + { + return core::AudioLevels{}; + } + [[nodiscard]] std::expected writeRawParam( unsigned, float) override { diff --git a/Software/components/drivers/adau1701/include/adau1701/Adau1701Driver.hpp b/Software/components/drivers/adau1701/include/adau1701/Adau1701Driver.hpp index 25cfbeb..4c1983f 100644 --- a/Software/components/drivers/adau1701/include/adau1701/Adau1701Driver.hpp +++ b/Software/components/drivers/adau1701/include/adau1701/Adau1701Driver.hpp @@ -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 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 readLevels(); + private: [[nodiscard]] std::expected ensureBooted() const; [[nodiscard]] std::expected safeloadGain( @@ -277,6 +293,8 @@ private: unsigned paramAddr, std::int32_t fixpoint); [[nodiscard]] std::expected replayProgram( const core::DspProgram& program); + [[nodiscard]] std::expected readCaptureDb( + unsigned progCount, unsigned regSel); Adau1701Pins pins_; core::IDspProgramSource& programSource_; diff --git a/Software/components/drivers/adau1701/include/adau1701/Adau1701Dsp.hpp b/Software/components/drivers/adau1701/include/adau1701/Adau1701Dsp.hpp index baf00cc..8388548 100644 --- a/Software/components/drivers/adau1701/include/adau1701/Adau1701Dsp.hpp +++ b/Software/components/drivers/adau1701/include/adau1701/Adau1701Dsp.hpp @@ -70,6 +70,9 @@ public: [[nodiscard]] std::expected writeRawParam( unsigned address, float value) override; + [[nodiscard]] std::expected + readLevels() override; + private: [[nodiscard]] static core::DspError mapError(Adau1701Error error) noexcept; diff --git a/Software/components/drivers/adau1701/src/Adau1701Driver.cpp b/Software/components/drivers/adau1701/src/Adau1701Driver.cpp index 823f1ff..266a004 100644 --- a/Software/components/drivers/adau1701/src/Adau1701Driver.cpp +++ b/Software/components/drivers/adau1701/src/Adau1701Driver.cpp @@ -18,6 +18,8 @@ #include "core/BiquadDesign.hpp" #include "core/DspProgram.hpp" +#include + #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 Adau1701Driver::readCaptureDb( + unsigned progCount, unsigned regSel) + { + const unsigned char deviceAddr = + static_cast(pins_.i2cAddr7 << 1); + const std::uint16_t config = static_cast( + ((progCount & 0x3FFU) << 2) | (regSel & 0x3U)); + unsigned char configBytes[2U] = { + static_cast((config >> 8) & 0xFFU), + static_cast(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(raw[0]) << 16) + | (static_cast(raw[1]) << 8) + | static_cast(raw[2]); + if ((value & (1 << 23)) != 0) + { + value -= (1 << 24); + } + const float linear = + static_cast(value) / static_cast(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 Adau1701Driver::readLevels() + { + if (auto ready = ensureBooted(); !ready) + { + return std::unexpected(ready.error()); + } + + core::AudioLevels levels{}; + auto read = [this](MeterPoint point, + float &out) -> std::expected + { + 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 Adau1701Driver::applyEq( const core::EqProfile &eq) { diff --git a/Software/components/drivers/adau1701/src/Adau1701Dsp.cpp b/Software/components/drivers/adau1701/src/Adau1701Dsp.cpp index 5614114..67a3735 100644 --- a/Software/components/drivers/adau1701/src/Adau1701Dsp.cpp +++ b/Software/components/drivers/adau1701/src/Adau1701Dsp.cpp @@ -115,4 +115,13 @@ std::expected Adau1701Dsp::writeRawParam( return {}; } +std::expected Adau1701Dsp::readLevels() +{ + auto result = driver_.readLevels(); + if (!result) { + return std::unexpected(mapError(result.error())); + } + return *result; +} + } // namespace adau1701 diff --git a/Software/components/net/src/SetupWebServer.cpp b/Software/components/net/src/SetupWebServer.cpp index 4400422..c308b6e 100644 --- a/Software/components/net/src/SetupWebServer.cpp +++ b/Software/components/net/src/SetupWebServer.cpp @@ -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 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, diff --git a/Software/components/services/audio/include/audio/AudioService.hpp b/Software/components/services/audio/include/audio/AudioService.hpp index 78fe69d..cb68138 100644 --- a/Software/components/services/audio/include/audio/AudioService.hpp +++ b/Software/components/services/audio/include/audio/AudioService.hpp @@ -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 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 + readLevels(); + private: [[nodiscard]] std::expected persistProfile() const; diff --git a/Software/components/services/audio/src/AudioService.cpp b/Software/components/services/audio/src/AudioService.cpp index 2b49cab..787d648 100644 --- a/Software/components/services/audio/src/AudioService.cpp +++ b/Software/components/services/audio/src/AudioService.cpp @@ -193,4 +193,9 @@ std::expected AudioService::writeRawParam( return dsp_.writeRawParam(address, value); } +std::expected AudioService::readLevels() +{ + return dsp_.readLevels(); +} + } // namespace audio diff --git a/Software/docs/adau1701-sigmastudio-analysis.md b/Software/docs/adau1701-sigmastudio-analysis.md index 27ba412..a044a74 100644 --- a/Software/docs/adau1701-sigmastudio-analysis.md +++ b/Software/docs/adau1701-sigmastudio-analysis.md @@ -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":}` | 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. --- diff --git a/Software/docs/app-cursor-brief-2026-08-25.md b/Software/docs/app-cursor-brief-2026-08-25.md index 93150bf..3cf5dc8 100644 --- a/Software/docs/app-cursor-brief-2026-08-25.md +++ b/Software/docs/app-cursor-brief-2026-08-25.md @@ -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.