Files
pluriwave/lib/servicios/servicio_audio.dart
ShanaiaBot 5fd3d6deb9
Some checks failed
Flutter CI/CD — PluriWave / Test + Build (push) Has been cancelled
feat(v0.3.0): ecualizador + favoritos en tarjeta + emisoras custom + export/import + fix MainActivity
- MainActivity: extiende AudioServiceActivity (fix pantalla en blanco)
- ServicioAudio: AndroidEqualizer en AudioPipeline, aplicarPreset(), setBanda()
- PresetEcualizador: modelo independiente (Flat/Rock/Pop/BassBoost/Jazz/Voz)
- EcualizadorWidget: 5 sliders verticales + PresetsEcualizadorWidget
- TarjetaEmisora: botón favorito visible en grid y lista (toggle con SnackBar)
- EstadoRadio: emisoras custom (CRUD), export/import JSON v1, presets por emisora
- PantallaAjustes: ecualizador interactivo, form añadir emisora, backup export/import
- pubspec: +file_picker ^8.1.7, +uuid ^4.5.1
2026-04-04 19:17:40 +02:00

229 lines
7.5 KiB
Dart

import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart';
import '../modelos/emisora.dart';
import '../modelos/preset_ecualizador.dart';
/// Estado de reproducción expuesto al UI.
enum EstadoReproduccion { detenido, cargando, reproduciendo, pausado, error }
// ─────────────────────────────────────────────────────────────
// Handler global — inicializado en main.dart con AudioService.init
// ─────────────────────────────────────────────────────────────
PluriWaveAudioHandler? _handlerGlobal;
void registrarHandler(PluriWaveAudioHandler handler) {
_handlerGlobal = handler;
}
/// Wrapper de alto nivel para el UI.
class ServicioAudio {
PluriWaveAudioHandler get _handler {
assert(_handlerGlobal != null, 'registrarHandler() no fue llamado en main.dart');
return _handlerGlobal!;
}
Emisora? get emisoraActual => _handler.emisoraActual;
Stream<EstadoReproduccion> get estadoStream =>
_handler.playbackState.map((s) {
if (s.processingState == AudioProcessingState.loading ||
s.processingState == AudioProcessingState.buffering) {
return EstadoReproduccion.cargando;
}
if (s.playing) return EstadoReproduccion.reproduciendo;
if (s.processingState == AudioProcessingState.idle) {
return EstadoReproduccion.detenido;
}
return EstadoReproduccion.pausado;
});
Future<void> reproducir(Emisora emisora) async {
final item = MediaItem(
id: emisora.url,
title: emisora.nombre,
artist: emisora.pais ?? '',
album: 'PluriWave',
artUri: emisora.favicon != null && emisora.favicon!.isNotEmpty
? Uri.tryParse(emisora.favicon!)
: null,
extras: {'uuid': emisora.uuid},
);
await _handler.playMediaItem(item);
}
Future<void> pausar() => _handler.pause();
Future<void> reanudar() => _handler.play();
Future<void> togglePlay() async {
if (_handler.playbackState.value.playing) {
await pausar();
} else {
await reanudar();
}
}
Future<void> detener() => _handler.stop();
Future<void> setVolumen(double vol) => _handler.setVolumen(vol);
double get volumen => _handler.volumen;
bool get estaSonando => _handler.playbackState.value.playing;
Future<void> dispose() async {}
// ── Ecualizador ──────────────────────────────────────────────────────────
AndroidEqualizer? get ecualizador => _handler.ecualizador;
bool get ecualizadorDisponible => _handler.ecualizadorDisponible;
PresetEcualizador get presetActual => _handler.presetActual;
Future<void> aplicarPreset(PresetEcualizador preset) =>
_handler.aplicarPreset(preset);
Future<void> setBanda(int index, double db) =>
_handler.setBanda(index, db);
}
// ─────────────────────────────────────────────────────────────
// AudioHandler
// ─────────────────────────────────────────────────────────────
class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
final AndroidEqualizer _eq = AndroidEqualizer();
late final AudioPlayer _player = AudioPlayer(
audioPipeline: AudioPipeline(androidAudioEffects: [_eq]),
);
Emisora? emisoraActual;
double _volumen = 1.0;
double get volumen => _volumen;
AndroidEqualizer? get ecualizador => _eq;
bool _eqDisponible = false;
bool get ecualizadorDisponible => _eqDisponible;
PresetEcualizador _presetActual = PresetEcualizador.flat;
PresetEcualizador get presetActual => _presetActual;
PluriWaveAudioHandler() {
_setupStreams();
}
void _setupStreams() {
_player.playerStateStream.listen((state) {
final playing = state.playing;
final proc = state.processingState;
playbackState.add(playbackState.value.copyWith(
controls: [
if (playing) MediaControl.pause else MediaControl.play,
MediaControl.stop,
],
systemActions: const {MediaAction.seek, MediaAction.stop},
androidCompactActionIndices: const [0],
processingState: _mapProcState(proc),
playing: playing,
bufferedPosition: _player.bufferedPosition,
speed: _player.speed,
));
});
_player.bufferedPositionStream.listen((pos) {
playbackState.add(playbackState.value.copyWith(bufferedPosition: pos));
});
}
AudioProcessingState _mapProcState(ProcessingState state) {
return switch (state) {
ProcessingState.idle => AudioProcessingState.idle,
ProcessingState.loading => AudioProcessingState.loading,
ProcessingState.buffering => AudioProcessingState.buffering,
ProcessingState.ready => AudioProcessingState.ready,
ProcessingState.completed => AudioProcessingState.completed,
};
}
@override
Future<void> playMediaItem(MediaItem item) async {
mediaItem.add(item);
try {
await _player.stop();
await _player.setUrl(item.id);
await _player.play();
// Habilitar ecualizador tras reproducir (necesita audio activo)
await _activarEcualizador();
} on PlayerException catch (e) {
playbackState.add(playbackState.value.copyWith(
processingState: AudioProcessingState.error,
errorMessage: e.message ?? 'Error de reproducción',
errorCode: e.code,
));
rethrow;
}
}
Future<void> _activarEcualizador() async {
try {
final params = await _eq.parameters;
_eqDisponible = params.bands.isNotEmpty;
if (_eqDisponible) {
await _eq.setEnabled(true);
await aplicarPreset(_presetActual);
}
} catch (_) {
_eqDisponible = false;
}
}
/// Aplica un preset al ecualizador nativo Android.
Future<void> aplicarPreset(PresetEcualizador preset) async {
_presetActual = preset;
if (!_eqDisponible) return;
try {
final params = await _eq.parameters;
for (int i = 0; i < params.bands.length && i < preset.bandas.length; i++) {
await params.bands[i].setGain(preset.bandas[i]);
}
} catch (_) {}
}
/// Ajusta una banda individual.
Future<void> setBanda(int index, double db) async {
if (!_eqDisponible) return;
final bandas = List<double>.from(_presetActual.bandas);
if (index >= 0 && index < bandas.length) {
bandas[index] = db;
_presetActual = _presetActual.copyWithBandas(bandas);
}
try {
final params = await _eq.parameters;
if (index < params.bands.length) {
await params.bands[index].setGain(db);
}
} catch (_) {}
}
Future<void> setVolumen(double vol) async {
_volumen = vol.clamp(0.0, 1.0);
await _player.setVolume(_volumen);
}
@override
Future<void> play() => _player.play();
@override
Future<void> pause() => _player.pause();
@override
Future<void> stop() async {
await _player.stop();
emisoraActual = null;
mediaItem.add(null);
await super.stop();
}
@override
Future<void> seek(Duration position) => _player.seek(position);
@override
Future<void> onTaskRemoved() async {
await stop();
await _player.dispose();
}
}