Files
pluriwave/openspec/specs/eq-audiofocus/spec.md
T
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

5.0 KiB

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