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.
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user