# 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).