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