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.
9.4 KiB
Archive Report: EQ Re-Apply After Audio-Focus Ducking
Change: eq-audiofocus-reapply
Archived: 2026-07-10
Artifact Store: hybrid (Engram + openspec)
Overall Verdict: PASS — Ready for Merging (Phase 5 Manual QA Pending)
Executive Summary
The eq-audiofocus-reapply change has completed all automated phases (1-4) and passed verification with 0 CRITICAL and 0 WARNING issues. Implementation adds 29 lines to lib/servicios/servicio_audio.dart plus one new test file. The equalizer now automatically re-attaches and re-applies gains when the native Android audio session id rotates during playback (e.g., after ducking by another app).
Phase 5 Manual On-Device QA (4 tasks) is intentionally PENDING — human verification required before shipping to users. This is NOT a blocker for code review or merge, but MUST complete before release.
Artifact Traceability
All SDD artifacts retrieved from Engram with observation IDs for full audit trail:
| Artifact | Topic Key | Observation ID | Retrieved |
|---|---|---|---|
| Proposal | sdd/eq-audiofocus-reapply/proposal |
#2300 | ✅ |
| Specification | sdd/eq-audiofocus-reapply/spec |
#2309 | ✅ |
| Design | sdd/eq-audiofocus-reapply/design |
#2307 | ✅ |
| Tasks | sdd/eq-audiofocus-reapply/tasks |
#2312 | ✅ |
| Apply Progress | sdd/eq-audiofocus-reapply/apply-progress |
#2317 | ✅ |
| Verify Report | sdd/eq-audiofocus-reapply/verify-report |
#2321 | ✅ |
Implementation Summary
Files Changed
-
lib/servicios/servicio_audio.dart (MODIFIED)
- Added
int? _ultimaSessionIdEqfield (change-guard, dedicated to EQ re-apply) - Extended existing
_androidAudioSessionIdSublistener with conditional re-apply logic - Added static pure predicate:
debeReaplicarEcualizador(...)for testing - Reset
_ultimaSessionIdEqin_recrearPlayer()to close teardown race - +29 lines net
- Added
-
test/servicios/servicio_audio_eq_reapply_test.dart (CREATED)
- 5 test cases covering predicate truth table (rotation, same-id guard, first-activation no-op, null id, teardown gate)
- ~50-70 lines
Constraints Respected
- ✅ Only target files modified — no changes to servicio_audio_session.dart, estado_ecualizador.dart, or Kotlin
- ✅ Station-switch path unchanged — _cambiarFuente, line 449 call site intact
- ✅ Manual slider path unchanged — setBanda path untouched
- ✅ Broadcast semantics preserved — _androidAudioSessionIdStream broadcast (line 270) unchanged; new logic uses separate _ultimaSessionIdEq field per design
- ✅ Multi-device orthogonality — fix lives below EstadoEcualizador layer, applies regardless of _eqMultiDeviceEnabled toggle
Verification Results
Verdict: PASS
Date Verified: 2026-07-10
Critical Issues: 0
Warnings: 0
Suggestions: 2
Test Coverage
| Category | Result | Details |
|---|---|---|
| flutter analyze | ✅ PASS | No issues found (0 issues) |
| Unit tests (predicate) | ✅ PASS | 5/5 truth-table cases (13 tests from 3 files) |
| Regression suite (explicit) | ✅ PASS | 13/13 tests across 4 files (eq_reapply + source_switch + session + reconnect) |
| Regression suite (full) | ⚠️ HANG | 245 pass + 1 unrelated intermittent flake in estado_alarmas_ejecuciones_test.dart (alarm module, pre-existing, not introduced here) |
| estado_ecualizador_test.dart (multi-device orthogonality) | ✅ PASS | 38/38 tests confirm toggle layer unaffected |
Design-to-Code Verification
- ✅ Predicate signature exact match:
debeReaplicarEcualizador({required int? sessionId, required int? ultimaSessionIdEq, required bool eqDisponible}) - ✅ Change-guard field dedicated:
int? _ultimaSessionIdEqseparate from_androidAudioSessionId - ✅ Listener extension location correct: lines 265-280, after existing broadcast (line 270)
- ✅ Re-apply call:
unawaited(_activarEcualizador())at line 278 (correct choke-point) - ✅ Guard reset location: line 520 (
_ultimaSessionIdEq = null), after line 518 (_eqDisponible = false), before line 523 (_conectarStreamsPlayer resubscribe) — race safely closed per design
Requirements Coverage
Requirement: Session Id Rotation Triggers EQ Re-Apply
- ✅ All 3 scenarios pass (rotation while playing, same-id no-redundant, first-activation no-double)
- ✅ Dedicated field requirement satisfied
- Evidence: predicate tests + listener wiring verified at lines 265-280
Requirement: Re-Apply Is Gated On EQ Availability
- ✅ Teardown race scenario passes
- ✅ Null-id precondition passes
- Evidence: predicate test + _recrearPlayer() ordering verified
Requirement: Re-Apply Is Independent Of Multi-Device Toggle
- ✅ Design/absence verified (zero _eqMultiDeviceEnabled mentions in diff)
- ✅ estado_ecualizador_test.dart 38/38 confirm toggle layer unaffected
- Evidence: regression tests + code inspection
Phase 5: Manual On-Device QA (PENDING)
The following 4 tasks are intentionally left unchecked (marked [ ] in tasks.md). They require a physical/emulated Android device with MethodChannels and cannot be automated in this environment. These MUST complete before shipping to users, but do NOT block code review or merge.
-
5.1 Real ducking interruption (Google Maps voice prompt or similar) during playback with non-flat preset — confirm music resumes EQUALIZED, not flat.
-
5.2 Repeat 5.1 with
_eqMultiDeviceEnabledtoggled ON and OFF — confirm identical behavior (validates Requirement "Re-Apply Is Independent Of Multi-Device Toggle"). -
5.3 Rapid station switch DURING an active ducking event — confirm no crash/exception, new station ends up correctly equalized (validates teardown race gate under real conditions).
-
5.4 Manual slider (setBanda) still works; explicit station switch still activates EQ via existing line-449 path with no double-apply audible artifact (validates first-activation no-double-apply under real conditions).
Next steps: After merging to main, assign Phase 5 QA to human tester with access to physical device and Google Maps/Radarbot. Phase 5 is a release gate, not a merge gate.
Suggestions
SUGGESTION 1 (from verify-report #2321):
Full flutter test suite unusable as a clean single-command gate until pre-existing, unrelated estado_alarmas_ejecuciones_test.dart hang is fixed separately (out of this change's scope).
SUGGESTION 2 (from verify-report #2321):
Two predicate test cases ("first activation" and "same id re-emitted") have textually identical bodies — intentional per design's testing table to document two distinct intents with the same input shape. Doc comments already clarify; no action needed, noted for awareness only.
Risk Assessment
| Risk | Likelihood | Impact | Mitigation | Status |
|---|---|---|---|---|
| Redundant re-fire on first/legit activation | Low | Low | Change-guard (_ultimaSessionIdEq), mirrors visualizer pattern | ✅ Tested |
| Race with _recrearPlayer() rebuild | Low | Medium | Gate on _eqDisponible == false; teardown sets false + nulls id BEFORE resubscribe | ✅ Tested |
| Regress station-switch / manual slider path | Low | High | Call sites unchanged; full regression suite passes | ✅ Tested |
Specs Synced to Main
Domain: eq-audiofocus (audio equalization)
Action: Create new main spec (no pre-existing spec for this domain)
Source: openspec/changes/eq-audiofocus-reapply/spec.md → openspec/specs/eq-audiofocus/spec.md
Content: 3 requirements (Session Id Rotation, Re-Apply Gate, Multi-Device Orthogonality) + 6 scenarios + testability matrix + non-goals
Rollback Plan
Self-contained addition to one listener block plus one new test file. Single-commit revert to restore prior behavior. No migration, schema, or API surface change; no downstream persistence impact.
Archive Contents Verified
- ✅ proposal.md (from Engram #2300)
- ✅ spec.md (from Engram #2309, copied to openspec/specs/eq-audiofocus/spec.md)
- ✅ design.md (from Engram #2307)
- ✅ tasks.md (from Engram #2312, 20/22 tasks marked [x], Phase 5 marked [ ])
- ✅ verify-report.md (from Engram #2321)
- ✅ state.yaml (generated by archive phase)
SDD Cycle Complete
- ✅ Phase 1: Proposal — Intent, scope, risks, rollback documented
- ✅ Phase 2: Specification — Requirements, scenarios, non-goals, testability matrix
- ✅ Phase 3: Design — Technical approach, architecture decisions, race analysis, testing strategy
- ✅ Phase 4: Tasks — Hierarchical breakdown, RED-GREEN-REFACTOR sequencing, delivery strategy
- ✅ Phase 5A: Apply — Implementation complete, Strict TDD followed (RED→GREEN→REFACTOR→Regression)
- ✅ Phase 5B: Verify — PASS verdict, 0 CRITICAL, 0 WARNING, diff-to-design exact match
- ✅ Phase 6: Archive — Specs synced to main, change folder moved to archive, audit trail persisted
- ⏳ Phase 5C: Manual QA — PENDING (human gate, device-dependent, not a merge blocker)
For the Next Session
- Assign Phase 5 manual on-device QA (4 tasks) to human tester with Android device
- After Phase 5 completes, mark tasks 5.1-5.4 as
[x]and update state.yaml status toqa-complete - No further automated work remains for this change
Generated by sdd-archive phase on 2026-07-10.
Artifact store: hybrid (Engram + openspec).
Topic key: sdd/eq-audiofocus-reapply/archive-report.