fix(eq): resolve base-speaker preset live instead of pinning a stale copy
Build & Deploy PluriWave / Análisis de código (push) Successful in 36s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m30s

_onDispositivoCambiado() bootstrapped a device-level preset entry for
every never-seen device id, including the built-in speaker. That
persistent level-3 entry masked later global-preset edits (level 3
beats level 4 on every resolution), so disconnecting a BT device or
cold-starting without one could leave the EQ stuck on an outdated
copy instead of the current global preset.

The base speaker is now excluded from the first-seen bootstrap:
disconnect and cold start always resolve through the live hierarchy.
BT/wired/USB devices keep their bootstrap behavior unchanged.
This commit is contained in:
2026-07-10 23:54:01 +02:00
parent bfa95a1e57
commit 8f7ca8059b
10 changed files with 1026 additions and 2 deletions
@@ -0,0 +1,62 @@
# Proposal: EQ Device Disconnect / Cold-Start Revert
## Intent
When an audio device disconnects — or the app opens with no external device connected — the EQ must return to the phone/base configuration, not stay on a stale device preset. Today `_onDispositivoCambiado()` (`estado_ecualizador.dart:210-227`) treats every device event identically: there is no explicit disconnect path, no cold-start guarantee, and ZERO disconnect test coverage. On disconnect it re-resolves the hierarchy for the now-active device, which only *coincidentally* looks like a revert when no station/matrix override interferes. This closes a correctness gap, not a regression.
## Scope
### In Scope
- Explicit disconnect handling: re-resolve + APPLY the preset for the now-active device (typically `builtin_speaker`).
- Cold-start guarantee: app open with no device connected applies the phone/base resolution, never a stale persisted device preset.
- Verify the resolved preset reaches the native EQ via the existing `audio.aplicarPreset()` choke-point (not state-only mutation).
- Test coverage: disconnect event, cold start without device, connect→disconnect→reconnect cycle, station/matrix override interaction.
- Respect `_eqMultiDeviceEnabled`: when OFF, behavior is unchanged (2-level resolution).
### Out of Scope
- Audio-focus ducking EQ re-apply (separate change `eq-audiofocus-reapply`, Part B).
- Native Kotlin/Swift device detection (`MainActivity.kt`, `pluriwave/audio_devices`) — already emits add/remove correctly.
- New persistence fields or migration; matrix/station/device preset storage is unchanged.
## Capabilities
### New Capabilities
- None.
### Modified Capabilities
- `multi-device-eq`: revert-on-disconnect and cold-start resolution become explicit, guaranteed requirements. "Revert" = deterministic re-resolution of the existing 4-level hierarchy (matrix → station → device → global) for the now-active device — NOT a snapshot/restore. This is stateless, self-healing, and the only mechanism covering the cold-start case (no snapshot exists to restore).
## Approach
Make the disconnect path explicit inside the device-event handler (`_onDispositivoCambiado`, or a split connect/disconnect dispatch). On any device event — connect, disconnect, or cold-start seed — set `_dispositivoActualId` to the now-active device, run `_resolverPresetActivo()`, assign `_presetActual`, and push it through `audio.aplicarPreset()`. Cold start already flows through `_sembrarDispositivoActual()``obtenerDispositivoActual()` (fallback `builtin_speaker`); the change guarantees the resolved base preset is APPLIED on that path, not just seeded. The first-seen bootstrap (`presetsDispositivo` write) must NOT hijack revert resolution for the base device.
## Affected Areas
| Area | Impact | Description |
|------|--------|-------------|
| `lib/estado/estado_ecualizador.dart` | Modified | Explicit disconnect/cold-start resolve+apply in the device handler |
| `test/estado/estado_ecualizador_test.dart` | Modified | New disconnect, cold-start, reconnect-cycle, override-matrix cases |
## Risks
| Risk | Likelihood | Mitigation |
|------|------------|------------|
| Regress passing connect/4-level suite (Phase 5, tests 5.15.8, 2.x) | Med | Reuse `_resolverPresetActivo()`; run full `estado_ecualizador_test.dart` green before/after |
| First-seen bootstrap overwrites base on disconnect | Med | Guard bootstrap so revert to `builtin_speaker` resolves from hierarchy, not a forced copy |
| Double-apply / redundant native pushes on rapid events | Low | Idempotent apply choke-point; skip when resolved preset unchanged |
## Rollback Plan
Single-file logic change plus tests. Revert the commit(s) touching `estado_ecualizador.dart` and its test file; no persistence schema, native, or public-API change means rollback restores the prior coincidental-revert behavior with zero migration.
## Dependencies
- Shared exploration `sdd/eq-apply-lifecycle/explore` (Part A section). Locked decision: re-resolution, not snapshot/restore.
## Success Criteria
- [ ] Disconnect event re-resolves and applies the base-device preset via `aplicarPreset()`.
- [ ] Cold start with no device applies phone/base resolution, not a stale device preset.
- [ ] connect→disconnect→reconnect cycle lands on the correct preset at each step, including station/matrix overrides.
- [ ] With `_eqMultiDeviceEnabled=false`, behavior is byte-for-byte unchanged (2-level).
- [ ] Full `estado_ecualizador_test.dart` suite stays green (no connect-path regression).