Files
FreeTLab bccc5c48b8
Build & Deploy PluriWave / Análisis de código (push) Successful in 38s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m30s
docs(openspec): add SDD artifact trail for recent alarm and EQ changes
Persist the exploration, proposal, spec, design, tasks, and
verify/archive reports produced during the multi-device EQ,
alarm-countdown, and notification-visual-polish SDD cycles.
2026-07-04 12:42:11 +02:00

342 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Spec: Multi-Device Equalizer
## New Capability: audio-device-detection
### Purpose
Platform channel bridge that enumerates current audio output devices and streams change events to Dart. Enables the rest of the system to react to device connect/disconnect without polling.
### Requirements
#### Requirement: Device enumeration on demand
The system MUST expose a synchronous query that returns the list of currently connected audio output devices as a typed list of device IDs.
##### Scenario: enumerate at startup — Android
- GIVEN the feature toggle is enabled
- WHEN `ServicioDispositivoAudio.dispositivosActuales()` is called on Android
- THEN it MUST return a list where each entry has a stable `deviceId` string
- AND built-in speaker MUST appear as `"builtin_speaker"`
- AND BT A2DP devices MUST appear as `"bt_a2dp:<MAC>"`
- AND USB audio MUST appear as `"usb_headset:<address>"`
- AND wired headset MUST appear as `"wired_headset"`
##### Scenario: enumerate at startup — iOS
- GIVEN the feature toggle is enabled
- WHEN `ServicioDispositivoAudio.dispositivosActuales()` is called on iOS
- THEN it MUST return at least the active route output
- AND the `deviceId` MUST be stable within the session using `portType+uid`
- AND calling it again before a route change MUST return the same IDs
##### Scenario: enumerate when toggle is disabled
- GIVEN the feature toggle is disabled
- WHEN any method of `ServicioDispositivoAudio` is called
- THEN it MUST return an empty list and an empty stream without errors
---
#### Requirement: Device change stream
The system MUST provide a `Stream<DispositivoAudio>` that emits an event whenever an audio output device is connected or disconnected.
##### Scenario: Bluetooth device connects — Android
- GIVEN the feature toggle is enabled and stream is subscribed
- WHEN a Bluetooth A2DP device connects
- THEN the stream MUST emit a `DispositivoAudio` with the correct MAC-based `deviceId`
- AND the event MUST arrive within 1 second of the OS callback
##### Scenario: wired headset disconnects
- GIVEN a wired headset is connected and the stream is subscribed
- WHEN the headset is unplugged
- THEN the stream MUST emit a disconnect event for `"wired_headset"`
##### Scenario: stream delivers no events when toggle is off
- GIVEN the feature toggle is disabled
- WHEN a device is connected or disconnected
- THEN the stream MUST NOT emit any events
---
#### Requirement: Stable device key derivation
The system MUST derive a deterministic, collision-free device key from platform device metadata such that the same physical device always yields the same key across app restarts.
##### Scenario: BT MAC key is stable across restarts — Android
- GIVEN a Bluetooth A2DP device with MAC `AA:BB:CC:DD:EE:FF`
- WHEN the app restarts and the device reconnects
- THEN `deviceId` MUST be `"bt_a2dp:AA:BB:CC:DD:EE:FF"` on both launches
##### Scenario: iOS uid fallback on uid instability
- GIVEN a Bluetooth device whose `uid` differs across sessions on iOS
- WHEN the key is derived
- THEN the system MUST use `portType+portName` as the fallback key
- AND MUST NOT produce an empty or null key
---
#### Requirement: Fakeable service interface
`ServicioDispositivoAudio` MUST be abstracted behind an interface so a `FakeServicioDispositivoAudio` can inject controlled device streams in unit tests without platform channels.
##### Scenario: unit test uses fake
- GIVEN a test instantiates `EstadoEcualizador` with `FakeServicioDispositivoAudio`
- WHEN the fake emits a device ID via its controller
- THEN `EstadoEcualizador` MUST react as if a real device change occurred
- AND no platform channel code MUST be invoked
---
## New Capability: multi-device-eq
### Purpose
Device-aware EQ resolution, persistence of per-device and matrix presets, and automatic preset swap when the active output device changes. Gated by a feature toggle that defaults to off.
### Requirements
#### Requirement: Feature toggle defaults off
The system MUST gate all multi-device-eq behavior behind a persistent boolean flag `eq_multi_device_enabled_v1` that defaults to `false`.
##### Scenario: toggle defaults to off on first install
- GIVEN the app is installed fresh with no prior SharedPreferences data
- WHEN `EstadoEcualizador` initializes
- THEN `eqMultiDeviceEnabled` MUST be `false`
- AND EQ behavior MUST be identical to the pre-feature release
##### Scenario: enabling the toggle persists across restarts
- GIVEN the user enables multi-device EQ in Settings
- WHEN the app is killed and relaunched
- THEN `eqMultiDeviceEnabled` MUST still be `true`
##### Scenario: disabling the toggle restores legacy behavior immediately
- GIVEN the toggle is currently `true`
- WHEN the user sets it to `false`
- THEN all EQ resolution MUST immediately fall back to the 2-level hierarchy (station → global)
- AND no per-device or matrix presets MUST be applied
---
#### Requirement: 4-level EQ resolution hierarchy
When the feature toggle is enabled, the system MUST resolve the active EQ preset using the following priority order, stopping at the first non-null match:
1. `presetsMatriz["stationUuid:deviceId"]` — station × device
2. `presetsEmisoraMap[stationUuid]` — station-only
3. `presetsDispositivo[deviceId]` — device-only
4. `presetPrincipal` — global fallback
##### Scenario: station+device preset takes top priority
- GIVEN a matrix entry exists for `stationUuid:deviceId`
- WHEN EQ is resolved for that station playing on that device
- THEN the matrix preset MUST be returned
##### Scenario: falls back to station when no matrix entry
- GIVEN no matrix entry for `stationUuid:deviceId`
- AND a station-only preset exists for `stationUuid`
- WHEN EQ is resolved
- THEN the station-only preset MUST be returned
##### Scenario: falls back to device when no station preset
- GIVEN no matrix entry and no station preset
- AND a device-only preset exists for `deviceId`
- WHEN EQ is resolved
- THEN the device preset MUST be returned
##### Scenario: falls back to global when nothing else matches
- GIVEN no matrix, station, or device preset
- WHEN EQ is resolved
- THEN `presetPrincipal` MUST be returned
##### Scenario: toggle off collapses to 2-level hierarchy
- GIVEN `eqMultiDeviceEnabled` is `false`
- WHEN EQ is resolved for any station/device combination
- THEN the system MUST use only station → global resolution (original behavior)
---
#### Requirement: First-device initialization copies current preset
When a device is seen for the first time (no entry in `presetsDispositivo`), the system MUST copy the currently active resolved preset as the device's default starting point.
##### Scenario: new device gets a copy of current preset
- GIVEN a device with ID `"bt_a2dp:AA:BB:CC:DD:EE:FF"` has never been seen
- WHEN that device connects
- THEN `presetsDispositivo["bt_a2dp:AA:BB:CC:DD:EE:FF"]` MUST be initialized to the current resolved preset
- AND no prompt or confirmation MUST be required from the user
##### Scenario: subsequent connections do not overwrite user-saved preset
- GIVEN the user has previously customized the device preset
- WHEN the same device reconnects
- THEN the user's preset MUST be preserved unchanged
---
#### Requirement: Automatic EQ swap on device change
When a device change event arrives and the feature toggle is on, the system MUST re-resolve and re-apply the EQ preset within 500 ms of the event.
##### Scenario: EQ swaps when BT device connects
- GIVEN the feature toggle is enabled and a station is playing via built-in speaker
- WHEN a BT A2DP device connects
- THEN the EQ preset resolved for `currentStation × "bt_a2dp:<MAC>"` MUST be applied
- AND the swap MUST complete within 500 ms
##### Scenario: EQ reverts when BT device disconnects
- GIVEN a station is playing via BT device with a matrix preset
- WHEN the BT device disconnects
- THEN EQ resolution MUST fall back to built-in speaker (or next best level)
- AND the player MUST receive the updated preset without user action
##### Scenario: no playback active during device change
- GIVEN no station is currently playing
- WHEN a device change event arrives
- THEN the system MUST update internal device state silently
- AND the next playback MUST use the resolved preset for the new device
---
#### Requirement: Per-device and matrix preset persistence
The system MUST persist per-device presets under `eq_preset_por_dispositivo_v1` and matrix presets under `eq_presets_matriz_v1` in SharedPreferences.
##### Scenario: device preset survives app restart
- GIVEN the user has saved a custom preset for device `"bt_a2dp:AA:BB:CC:DD:EE:FF"`
- WHEN the app restarts and the device reconnects
- THEN the same preset MUST be loaded from SharedPreferences
##### Scenario: matrix preset survives app restart
- GIVEN a `"stationUuid:deviceId"` entry exists in the matrix
- WHEN the app restarts
- THEN `presetsMatriz` MUST contain that entry after initialization
##### Scenario: storage budget stays within limits
- GIVEN up to 50 stations × 5 device types (250 matrix entries) are stored
- WHEN all entries are populated
- THEN total SharedPreferences storage for device/matrix presets MUST remain under 50 KB
---
#### Requirement: Settings UI for feature toggle and device preset list
The Settings screen MUST expose an "Advanced Equalization Options" section that is visible only when advanced EQ is relevant to the user.
##### Scenario: toggle appears in Settings
- GIVEN the user navigates to Settings
- WHEN they scroll to the EQ section
- THEN an "Advanced Equalization Options" group MUST be visible
- AND it MUST contain a toggle labeled to enable per-device EQ
##### Scenario: device list appears when toggle is on
- GIVEN the toggle is enabled and at least one audio device has been detected
- WHEN the user views the Advanced EQ section
- THEN a list of known devices with their associated preset names MUST be visible
##### Scenario: Settings section is absent when toggle is off
- GIVEN the toggle is `false`
- WHEN the user views Settings
- THEN device preset list and matrix controls MUST NOT be displayed
---
## Delta for equalizer
### MODIFIED Requirements
#### Requirement: EQ resolution logic
The system MUST resolve the active EQ preset using the 4-level hierarchy (station×device → station → device → global) when `eqMultiDeviceEnabled` is `true`, and MUST fall back to the existing 2-level hierarchy (station → global) when the toggle is `false`. `_recrearPlayer()` MUST re-apply the device-resolved preset, not a stale `_presetActual`.
(Previously: resolution used station → global only; `_recrearPlayer()` re-applied `_presetActual` directly.)
##### Scenario: player recreation re-applies device-resolved preset
- GIVEN a station is playing and a device-resolved preset differs from `_presetActual`
- WHEN `_recrearPlayer()` is called (e.g., source change)
- THEN the player MUST receive the fully-resolved 4-level preset, not the stale principal
##### Scenario: toggle-off preserves original resolution
- GIVEN `eqMultiDeviceEnabled` is `false`
- WHEN a station starts playing
- THEN resolution MUST use station preset → global preset (unchanged from prior behavior)
- AND no device query MUST be made
##### Scenario: device subscription is set up at initialization
- GIVEN `EstadoEcualizador` is initialized with `eqMultiDeviceEnabled = true`
- WHEN `ServicioDispositivoAudio` emits a device change
- THEN `EstadoEcualizador` MUST receive it and re-resolve the active preset
- AND listeners MUST be notified via `notifyListeners()`
---
## Delta for export-import
### MODIFIED Requirements
#### Requirement: Export/import schema versioned at v3
The system MUST export and import configuration at schema version 3, adding `presetsPorDispositivo` and `presetsMatriz` fields. Import of v1/v2 files MUST succeed, treating missing device fields as empty maps.
(Previously: schema was v2 with `presetPrincipalEcualizador` and `presetsEcualizador` only.)
##### Scenario: v3 export includes device fields
- GIVEN the user triggers a config export with device presets stored
- WHEN the export file is generated
- THEN the JSON MUST contain `"schemaVersion": 3`
- AND MUST contain `"presetsPorDispositivo"` as a non-null map
- AND MUST contain `"presetsMatriz"` as a non-null map
##### Scenario: v3 round-trip preserves device presets
- GIVEN a user has device and matrix presets configured
- WHEN they export and then import the same file
- THEN all device presets MUST be restored exactly
- AND all matrix presets MUST be restored exactly
##### Scenario: v2 file imports cleanly into v3 app
- GIVEN an export file from a previous app version (schema v2, no device fields)
- WHEN the user imports it into the v3 app
- THEN the import MUST succeed without errors
- AND `presetsDispositivo` MUST default to an empty map
- AND `presetsMatriz` MUST default to an empty map
- AND previously-existing global and station presets MUST be restored correctly
##### Scenario: v3 file ignored by v2 app
- GIVEN an export file from the v3 app (with device fields)
- WHEN a v2 app attempts to import it
- THEN the v2 app MUST either succeed by ignoring unknown fields, or fail with a clear version mismatch error
- AND MUST NOT corrupt or partially apply EQ state