Files
pluriwave/lib/servicios/servicio_dispositivo_audio.dart
T
FreeTLab 4632d53eb8 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
2026-06-27 11:33:53 +02:00

113 lines
3.6 KiB
Dart

import 'dart:async';
import 'package:flutter/services.dart';
import '../modelos/dispositivo_audio.dart';
/// Abstract service for audio device detection.
///
/// Implementations:
/// - [ServicioDispositivoAudioReal] — platform channel (Android/iOS)
/// - `FakeServicioDispositivoAudio` (test/helpers/fakes.dart) — for unit tests
abstract class ServicioDispositivoAudio {
/// The last known active audio output device, or null if none detected yet.
DispositivoAudio? get dispositivoActual;
/// A broadcast stream that emits whenever the active audio output device
/// changes (connect or disconnect event from the platform).
Stream<DispositivoAudio> get onDispositivoCambiado;
/// Requests the current active device from the platform synchronously
/// (method channel round-trip). Returns the cached value if already known.
Future<DispositivoAudio> obtenerDispositivoActual();
/// Cancels the device-change subscription and releases resources.
Future<void> dispose();
}
/// Platform channel implementation of [ServicioDispositivoAudio].
///
/// Talks to `pluriwave/audio_devices` on Android (Kotlin) and iOS (Swift).
/// Type int constants (from platform channel protocol):
/// 2 → altavozInterno (builtin_speaker)
/// 3 → auricularesCable (wired_headset)
/// 8 → bluetoothA2dp (`bt_a2dp:<MAC>`)
/// 14 → usbAudio (`usb_headset:<address>`)
class ServicioDispositivoAudioReal extends ServicioDispositivoAudio {
static const _channelName = 'pluriwave/audio_devices';
final MethodChannel _methodChannel;
final EventChannel _eventChannel;
final _controller = StreamController<DispositivoAudio>.broadcast();
StreamSubscription<dynamic>? _eventSub;
DispositivoAudio? _dispositivoActual;
ServicioDispositivoAudioReal({
MethodChannel? methodChannel,
EventChannel? eventChannel,
}) : _methodChannel = methodChannel ?? const MethodChannel(_channelName),
_eventChannel = eventChannel ?? const EventChannel(_channelName) {
_subscribeToEvents();
}
void _subscribeToEvents() {
_eventSub = _eventChannel.receiveBroadcastStream().listen(
(dynamic event) {
if (event is Map) {
final device = _mapToDispositivo(Map<String, dynamic>.from(event));
_dispositivoActual = device;
_controller.add(device);
}
},
onError: (_) {
/* swallow platform errors; stream continues */
},
cancelOnError: false,
);
}
@override
DispositivoAudio? get dispositivoActual => _dispositivoActual;
@override
Stream<DispositivoAudio> get onDispositivoCambiado => _controller.stream;
@override
Future<DispositivoAudio> obtenerDispositivoActual() async {
final raw = await _methodChannel.invokeMethod<Map>('getActiveDevice');
final map = Map<String, dynamic>.from(raw ?? {});
final device = _mapToDispositivo(map);
_dispositivoActual = device;
return device;
}
@override
Future<void> dispose() async {
await _eventSub?.cancel();
await _controller.close();
}
static DispositivoAudio _mapToDispositivo(Map<String, dynamic> map) {
final id = map['id'] as String? ?? 'builtin_speaker';
final type = map['type'] as int? ?? 2;
final name = map['name'] as String? ?? '';
return DispositivoAudio(id: id, tipo: _tipoDesdeInt(type), nombre: name);
}
static TipoDispositivo _tipoDesdeInt(int type) {
switch (type) {
case 2:
return TipoDispositivo.altavozInterno;
case 3:
return TipoDispositivo.auricularesCable;
case 8:
return TipoDispositivo.bluetoothA2dp;
case 14:
return TipoDispositivo.usbAudio;
default:
return TipoDispositivo.desconocido;
}
}
}