Files
pluriwave/openspec/changes/multi-device-eq/spec.md
T
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

13 KiB
Raw Blame History

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