Files
FreeTLab 0ab63731d0
Build & Deploy PluriWave / Análisis de código (push) Successful in 41s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m33s
fix(eq): re-apply equalizer when the native audio session rotates
ExoPlayer assigns a new audio session id after transient audio-focus
interruptions (navigation prompts, radar warnings), leaving the
AndroidEqualizer attached to the dead session so playback resumed
without equalization until the next station switch. The session-id
listener now detects genuine rotations through a dedicated guard and
re-activates the equalizer with the current preset, gated on EQ
availability to stay clear of player teardown/rebuild.
2026-07-10 18:51:45 +02:00

100 lines
5.0 KiB
Markdown

# EQ Audio-Focus Re-Apply Specification
## Purpose
Keep the native Android equalizer attached and equalized after ExoPlayer
rotates the audio session id mid-playback (audio-focus transitions, ducking by
other apps). Today re-attachment only happens on explicit station switch, so
music resumed after an interruption is silently un-equalized. Covers
`PluriWaveAudioHandler` (`lib/servicios/servicio_audio.dart`) only.
## Requirements
### Requirement: Session Id Rotation Triggers EQ Re-Apply
The system MUST re-attach the equalizer and re-push the current preset's gains
whenever `androidAudioSessionIdStream` emits a session id that differs from the
last one processed for EQ purposes, is non-null, and `_eqDisponible == true`.
Change tracking MUST use a dedicated field (`_ultimaSessionIdEq`), separate
from the broadcast field `_androidAudioSessionId`, so existing broadcast
semantics on `androidAudioSessionIdStream` stay unchanged.
#### Scenario: Session id rotates while playing
- GIVEN the player is playing, EQ attached/enabled (`_eqDisponible == true`),
a non-flat preset applied
- WHEN `androidAudioSessionIdStream` emits a new non-null id different from
`_ultimaSessionIdEq`
- THEN the system re-attaches the EQ (re-probes `_eq.parameters`, re-enables
per `_ecualizadorActivo`) and re-applies the current preset's gains
- AND `_ultimaSessionIdEq` updates to the new id
#### Scenario: Same id re-emitted produces no redundant re-apply
- GIVEN `_ultimaSessionIdEq` already equals the last processed id
- WHEN the same id is emitted again
- THEN the system MUST NOT call the re-attach/re-apply choke-point again
#### Scenario: First legitimate activation is not double-applied
- GIVEN a station switch just completed `_recrearPlayer()` and its own
`_activarEcualizador()` call (`servicio_audio.dart:449`)
- WHEN the resulting first session-id emission for the new player reaches this
listener
- THEN the listener MAY invoke the same idempotent choke-point, but this MUST
NOT produce a different final gain state than the station-switch path alone
### Requirement: Re-Apply Is Gated On EQ Availability
The system MUST NOT attempt to re-attach or re-apply gains while
`_eqDisponible == false`, to avoid racing `_recrearPlayer()`'s teardown/rebuild.
#### Scenario: Rotation during player teardown is safely skipped
- GIVEN `_recrearPlayer()` has set `_eqDisponible = false`, nulled
`_androidAudioSessionId`, and created a fresh `AndroidEqualizer()`
- WHEN a session-id emission reaches the listener while `_eqDisponible` is
still `false`
- THEN the system skips the re-apply choke-point entirely
- AND no exception is thrown
### Requirement: Re-Apply Is Independent Of Multi-Device Toggle
The system MUST re-apply gains on session id rotation regardless of
`_eqMultiDeviceEnabled`, since this fix lives below the `EstadoEcualizador`
multi-device layer.
#### Scenario: Re-apply works with the toggle on or off
- GIVEN either state of `_eqMultiDeviceEnabled`
- WHEN a session id rotates under the "rotates while playing" preconditions
- THEN the re-apply behaves identically in both states
## Non-Goals
- Station-switch EQ path (`_cambiarFuente``_recrearPlayer`
`_activarEcualizador()` at line 449) is unchanged.
- Manual slider path (`cambiarBanda`/`setBanda`) is unchanged.
- No new localized strings — no l10n keys added, removed, or modified.
## Testability Matrix
| Scenario | Testable how |
|---|---|
| Rotates while playing / same-id guard / first-activation no double-apply / teardown-race skip / toggle-independence | **Manual QA only.** `PluriWaveAudioHandler` is never instantiated anywhere in `test/` — confirmed zero matches for `PluriWaveAudioHandler(`. Its `_player` is a real `just_audio.AudioPlayer` requiring platform MethodChannels; `androidAudioSessionIdStream` is fed directly from it, with no fake/injectable seam. Verify on-device: real ducking (e.g. Google Maps prompt) during playback, and rapid station-switch during a ducking event for the teardown race. |
| Station-switch path / manual slider path unchanged | **Regression only, not targeted unit tests.** Same instantiation blocker applies to both call sites; confidence comes from the full `flutter test` suite plus manual QA, not new automated tests against these paths. |
**Correction to proposal**: the proposal's downstream notes claim
`servicio_audio_source_switch_test.dart` shows "fake/stream patterns" for this
handler. Verified against the file: it explicitly states *"We cannot
instantiate the handler in unit tests (MethodChannels)"* and only exercises
`ControladorReconexion` in isolation — not `PluriWaveAudioHandler`. No
fake/injectable `AudioPlayer`/`AndroidEqualizer` seam exists in `test/`. Every
scenario above is therefore manual on-device QA under the current harness, not
automated. This reframes the proposal's Success Criteria item "covered by
passing tests" as "passing tests where the harness allows, manual QA
otherwise" — `sdd-design`/`sdd-tasks` should decide whether to accept that, or
add a DI seam for `_player`/`_eq` as a separate prerequisite (out of this
change's scope).