feat(eq): add per-device equalizer with 4-level preset resolution

Introduce multi-device EQ support allowing each audio output device
(built-in speaker, wired, USB, individual Bluetooth by MAC) to have
its own equalizer preset, combined with existing per-station presets
for a full station×device matrix.

- Add DispositivoAudio model and ServicioDispositivoAudio interface
- Add Android platform channel (AudioDeviceCallback) for device detection
- Add iOS AudioDevicesPlugin (AVAudioSession route tracking)
- Extend ServicioEcualizador with device and matrix persistence keys
- Implement 4-level resolution: matrix > station > device > global
- Add advanced EQ settings section with feature toggle (off by default)
- Extend export/import to v3 with backward compatibility
- 184 tests passing, zero analyzer issues
This commit is contained in:
2026-06-27 11:33:53 +02:00
parent 8f42e67b48
commit 4632d53eb8
36 changed files with 2850 additions and 64 deletions
+44 -14
View File
@@ -6,21 +6,29 @@ import '../modelos/preset_ecualizador.dart';
/// Owns the backup (export/import) JSON serialization (S4-R4).
///
/// The v2 envelope produced here is byte-compatible with the legacy format
/// previously assembled inline by `EstadoRadio.exportarConfig` and
/// pretty-printed by `pantalla_ajustes.dart`, so existing exports keep
/// round-tripping. State APPLICATION (writing favorites, EQ, alarms back
/// into the app) stays in `EstadoRadio.importarConfig` — this service only
/// owns serialization, parsing and the envelope shape.
/// v3 extends v2 with `presetsPorDispositivo`, `presetsMatriz`, and
/// `eqMultiDeviceEnabled`. When those optional parameters are omitted the
/// export stays at v2 for backward compat with the old app. State APPLICATION
/// (writing favorites, EQ, alarms back into the app) stays in
/// `EstadoRadio.importarConfig` — this service only owns serialization,
/// parsing and the envelope shape.
class ServicioExportImport {
const ServicioExportImport();
/// Current backup schema version (v2full portability).
static const int versionActual = 2;
/// Current backup schema version (v3multi-device EQ).
static const int versionActual = 3;
/// Builds the v2 export envelope. Key set and semantics must stay exactly
/// as the legacy inline export so old backups remain importable:
/// the `alarmas` block is the RAW JSON map persisted by ServicioAlarmas
/// Legacy v2 version constant kept for clarity.
static const int versionV2 = 2;
/// Builds the export envelope.
///
/// When [presetsPorDispositivo] or [presetsMatriz] are provided (non-null),
/// [versionActual] (3) is written. When both are omitted the call behaves
/// identically to the original v2 format (version key stays 2) so old
/// backups keep round-tripping without version bumps.
///
/// The `alarmas` block is the RAW JSON map persisted by ServicioAlarmas
/// and passes through untouched (no re-parsing here).
Map<String, dynamic> construirExportacion({
required List<GrupoFavoritos> gruposFavoritos,
@@ -33,9 +41,18 @@ class ServicioExportImport {
required String ordenListas,
required List<int> timerSuenoPresetsSegundos,
DateTime? exportadoEn,
// v3 extensions — omitting these produces a v2-compatible export.
Map<String, PresetEcualizador>? presetsPorDispositivo,
Map<String, PresetEcualizador>? presetsMatriz,
bool? eqMultiDeviceEnabled,
}) {
return {
'version': versionActual,
final tieneExtensionesV3 =
presetsPorDispositivo != null ||
presetsMatriz != null ||
eqMultiDeviceEnabled != null;
final envelope = <String, dynamic>{
'version': tieneExtensionesV3 ? versionActual : versionV2,
'exportedAt': (exportadoEn ?? DateTime.now()).toIso8601String(),
// Favorites + groups (preserves grupo_id assignments per station).
// The protected "sin asignar" group is implicit and never exported.
@@ -47,7 +64,7 @@ class ServicioExportImport {
'favoritos': favoritos.map((e) => e.toMap()).toList(),
// Custom stations.
'emisorasCustom': emisorasCustom.map((e) => e.toMap()).toList(),
// Equalizer.
// Equalizer (base fields — present in all versions).
'presetPrincipalEcualizador': presetPrincipal.toJson(),
'presetsEcualizador': presetsPorEmisora.map(
(uuid, preset) => MapEntry(uuid, preset.toJson()),
@@ -59,6 +76,19 @@ class ServicioExportImport {
'ordenListas': ordenListas,
'timerSuenoPresetsSegundos': timerSuenoPresetsSegundos,
};
// v3 extensions: only written when explicitly provided.
if (tieneExtensionesV3) {
envelope['presetsPorDispositivo'] = (presetsPorDispositivo ?? {}).map(
(deviceId, preset) => MapEntry(deviceId, preset.toJson()),
);
envelope['presetsMatriz'] = (presetsMatriz ?? {}).map(
(clave, preset) => MapEntry(clave, preset.toJson()),
);
envelope['eqMultiDeviceEnabled'] = eqMultiDeviceEnabled ?? false;
}
return envelope;
}
/// Serializes an export envelope to pretty-printed JSON (same formatting