The equalizer stopped applying after another app interrupted audio (e.g. a navigation app's voice prompt): play a station with EQ working, let the prompt speak, resume -- the audio sounds flat until the station is re-tapped. debeReaplicarEcualizador only re-attaches the equalizer when the native player session id actually changes. A short transient interruption keeps the SAME session (no id rotation), so that trigger never fires, while Android's AudioEffect framework can let a higher-priority client silently disable this app's effect instance in the meantime. Add reaplicarEcualizador() to ObjetivoAudioInterrumpible, implemented as a thin delegate to the existing _activarEcualizador() (already the correct idempotent setEnabled + re-push-gains path). ServicioAudioSession calls it on resume-from-pause (after reanudar()) and on un-duck (after setAtenuado(false)) -- additive to the existing session-id trigger, not a replacement. The method takes no argument, so it can only re-assert whatever enabled/disabled state the handler already holds -- an interruption cycle with the equalizer OFF stays OFF.
134 lines
5.0 KiB
Dart
134 lines
5.0 KiB
Dart
import 'dart:async';
|
|
import 'dart:developer' as developer;
|
|
|
|
import 'package:audio_session/audio_session.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
/// Minimal playback contract this service needs from the audio handler
|
|
/// (Design 3.1). `PluriWaveAudioHandler` implements it; tests use a fake.
|
|
abstract class ObjetivoAudioInterrumpible {
|
|
/// Intent-to-play flag (Designs 3.1/7.2): true while the user wants audio
|
|
/// playing. The S7 reconnect logic reads the same flag, so an interruption
|
|
/// pause also disarms reconnection attempts.
|
|
bool get intencionReproducir;
|
|
|
|
bool get estaReproduciendo;
|
|
|
|
Future<void> pausar();
|
|
|
|
Future<void> reanudar();
|
|
|
|
/// Temporarily lowers ("ducks") the output volume without pausing.
|
|
Future<void> setAtenuado(bool atenuado);
|
|
|
|
/// Re-attaches the equalizer effect and re-pushes the current preset's
|
|
/// gains (fix "EQ Re-Apply After Audio-Focus Interruption"). Called after
|
|
/// resuming from a transient interruption pause and after un-ducking,
|
|
/// because Android's AudioEffect framework can let a higher-priority
|
|
/// client silently disable this app's effect instance while the
|
|
/// underlying player session id never changes — the existing session-id
|
|
/// rotation trigger (`ServicioAudio.debeReaplicarEcualizador`) therefore
|
|
/// never fires for a SHORT interruption (e.g. a nav-app voice prompt).
|
|
/// Idempotent and cheap (a `setEnabled` plus band `setGain` calls); takes
|
|
/// no argument by design — it re-asserts whatever enabled/disabled state
|
|
/// the handler ALREADY holds, so a caller here can never force the
|
|
/// equalizer on. Never restarts or repositions playback.
|
|
Future<void> reaplicarEcualizador();
|
|
}
|
|
|
|
/// Wrapper around `package:audio_session` (S3-R1): configures the session
|
|
/// for music playback and translates interruption / becoming-noisy events
|
|
/// into pause, duck and auto-resume calls on the audio handler.
|
|
class ServicioAudioSession {
|
|
ServicioAudioSession({
|
|
required ObjetivoAudioInterrumpible objetivo,
|
|
Future<AudioSession> Function()? obtenerSesion,
|
|
}) : _objetivo = objetivo,
|
|
_obtenerSesion = obtenerSesion ?? (() => AudioSession.instance);
|
|
|
|
final ObjetivoAudioInterrumpible _objetivo;
|
|
final Future<AudioSession> Function() _obtenerSesion;
|
|
StreamSubscription<AudioInterruptionEvent>? _interrupcionesSub;
|
|
StreamSubscription<void>? _ruidoSub;
|
|
|
|
/// True when WE paused because of an interruption; only then does an
|
|
/// interruption end with shouldResume restart playback.
|
|
bool _pausadoPorInterrupcion = false;
|
|
|
|
Future<void> configurar() async {
|
|
try {
|
|
final sesion = await _obtenerSesion();
|
|
await sesion.configure(
|
|
const AudioSessionConfiguration.music().copyWith(
|
|
androidWillPauseWhenDucked: true,
|
|
),
|
|
);
|
|
await _interrupcionesSub?.cancel();
|
|
await _ruidoSub?.cancel();
|
|
_interrupcionesSub = sesion.interruptionEventStream.listen(
|
|
(evento) => unawaited(manejarInterrupcion(evento)),
|
|
);
|
|
_ruidoSub = sesion.becomingNoisyEventStream.listen(
|
|
(_) => unawaited(manejarDesconexionSalida()),
|
|
);
|
|
} catch (e) {
|
|
developer.log(
|
|
'[PluriWave] No se pudo configurar la sesion de audio: $e',
|
|
name: 'ServicioAudioSession',
|
|
level: 900,
|
|
);
|
|
}
|
|
}
|
|
|
|
@visibleForTesting
|
|
Future<void> manejarInterrupcion(AudioInterruptionEvent evento) async {
|
|
if (evento.begin) {
|
|
switch (evento.type) {
|
|
case AudioInterruptionType.duck:
|
|
await _objetivo.setAtenuado(true);
|
|
case AudioInterruptionType.pause:
|
|
case AudioInterruptionType.unknown:
|
|
if (_objetivo.estaReproduciendo || _objetivo.intencionReproducir) {
|
|
_pausadoPorInterrupcion = true;
|
|
await _objetivo.pausar();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
switch (evento.type) {
|
|
case AudioInterruptionType.duck:
|
|
await _objetivo.setAtenuado(false);
|
|
// Un-ducking never rotates the native player session id, so the
|
|
// session-id-change trigger never fires for this case — re-assert
|
|
// here too (belt-and-braces, additive to that trigger).
|
|
await _objetivo.reaplicarEcualizador();
|
|
case AudioInterruptionType.pause:
|
|
// Transient loss ended and the OS says we may resume.
|
|
if (_pausadoPorInterrupcion) {
|
|
_pausadoPorInterrupcion = false;
|
|
await _objetivo.reanudar();
|
|
// Same rationale as the duck branch above: a short transient
|
|
// interruption keeps the SAME player session id.
|
|
await _objetivo.reaplicarEcualizador();
|
|
}
|
|
case AudioInterruptionType.unknown:
|
|
// Permanent focus loss: never auto-resume.
|
|
_pausadoPorInterrupcion = false;
|
|
}
|
|
}
|
|
|
|
@visibleForTesting
|
|
Future<void> manejarDesconexionSalida() async {
|
|
// Headphones unplugged: hard pause, never auto-resume afterwards.
|
|
_pausadoPorInterrupcion = false;
|
|
if (_objetivo.estaReproduciendo) {
|
|
await _objetivo.pausar();
|
|
}
|
|
}
|
|
|
|
Future<void> dispose() async {
|
|
await _interrupcionesSub?.cancel();
|
|
await _ruidoSub?.cancel();
|
|
}
|
|
}
|