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.
100 lines
5.0 KiB
Markdown
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).
|