fix(eq): re-apply the equalizer after an audio-focus interruption
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.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/servicios/servicio_audio.dart';
|
||||
import 'package:pluriwave/servicios/servicio_audio_session.dart';
|
||||
|
||||
/// EQ audio-focus re-apply — pure decision predicate truth table.
|
||||
///
|
||||
@@ -96,4 +98,192 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ── EQ re-apply after a SHORT audio-focus interruption ──────────────────
|
||||
// debeReaplicarEcualizador only fires on a session-id CHANGE. A short
|
||||
// transient interruption (a nav-app voice prompt) keeps the SAME player
|
||||
// session id, so that trigger never fires and the equalizer stays
|
||||
// silently disabled after Android lets another app's AudioEffect steal
|
||||
// control. Fix: re-assert the equalizer on resume-from-pause and on
|
||||
// un-duck too, via a new no-arg ObjetivoAudioInterrumpible.reaplicarEcualizador()
|
||||
// that the handler implements as a thin delegate to the existing
|
||||
// _activarEcualizador() (setEnabled + band gains, already correct).
|
||||
//
|
||||
// ServicioAudioSession is the orchestration layer under test here (the
|
||||
// same layer servicio_audio_session_test.dart already covers) -- it is
|
||||
// fully unit-testable, unlike PluriWaveAudioHandler itself.
|
||||
group(
|
||||
'ServicioAudioSession re-applies the equalizer on interruption resume '
|
||||
'(no session-id change involved)',
|
||||
() {
|
||||
test(
|
||||
'a pause-interruption cycle (begin -> end/resume) calls '
|
||||
'reaplicarEcualizador exactly once, AFTER reanudar()',
|
||||
() async {
|
||||
final objetivo = _ObjetivoFake()
|
||||
..reproduciendo = true
|
||||
..intencion = true;
|
||||
final servicio = ServicioAudioSession(objetivo: objetivo);
|
||||
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(true, AudioInterruptionType.pause),
|
||||
);
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(false, AudioInterruptionType.pause),
|
||||
);
|
||||
|
||||
expect(objetivo.reaplicaciones, 1);
|
||||
expect(
|
||||
objetivo.eventos,
|
||||
['pausar', 'reanudar', 'reaplicar'],
|
||||
reason:
|
||||
'the re-apply must happen on RESUME, after reanudar() -- '
|
||||
'never before, never on the begin/pause side',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'a duck cycle (begin -> end/un-duck) calls reaplicarEcualizador '
|
||||
'exactly once, AFTER setAtenuado(false)',
|
||||
() async {
|
||||
final objetivo = _ObjetivoFake()
|
||||
..reproduciendo = true
|
||||
..intencion = true;
|
||||
final servicio = ServicioAudioSession(objetivo: objetivo);
|
||||
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(true, AudioInterruptionType.duck),
|
||||
);
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(false, AudioInterruptionType.duck),
|
||||
);
|
||||
|
||||
expect(objetivo.reaplicaciones, 1);
|
||||
expect(
|
||||
objetivo.eventos,
|
||||
['atenuado:true', 'atenuado:false', 'reaplicar'],
|
||||
reason:
|
||||
'the re-apply must happen on UN-DUCK, after '
|
||||
'setAtenuado(false)',
|
||||
);
|
||||
expect(objetivo.pausas, 0, reason: 'a duck never pauses');
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'with the equalizer switched OFF by the user, an interruption '
|
||||
'cycle still only calls the SAME parameterless reassert -- '
|
||||
'ServicioAudioSession has no way to force it on',
|
||||
() async {
|
||||
final objetivo = _ObjetivoFake()
|
||||
..reproduciendo = true
|
||||
..intencion = true
|
||||
..eqActivo = false;
|
||||
final servicio = ServicioAudioSession(objetivo: objetivo);
|
||||
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(true, AudioInterruptionType.pause),
|
||||
);
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(false, AudioInterruptionType.pause),
|
||||
);
|
||||
|
||||
expect(objetivo.reaplicaciones, 1);
|
||||
expect(
|
||||
objetivo.estadosReaplicados,
|
||||
[false],
|
||||
reason:
|
||||
'reaplicarEcualizador takes no boolean argument -- it can '
|
||||
'only ask the handler to reassert whatever state it '
|
||||
'ALREADY holds, never flip it on',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'end without a prior begin/pause never calls reaplicarEcualizador '
|
||||
'(mirrors "end sin pausa previa" -- no resume happened)',
|
||||
() async {
|
||||
final objetivo = _ObjetivoFake();
|
||||
final servicio = ServicioAudioSession(objetivo: objetivo);
|
||||
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(false, AudioInterruptionType.pause),
|
||||
);
|
||||
|
||||
expect(objetivo.reaplicaciones, 0);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'a permanent (unknown-type) focus loss never calls '
|
||||
'reaplicarEcualizador -- there is no resume to re-assert after',
|
||||
() async {
|
||||
final objetivo = _ObjetivoFake()
|
||||
..reproduciendo = true
|
||||
..intencion = true;
|
||||
final servicio = ServicioAudioSession(objetivo: objetivo);
|
||||
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(true, AudioInterruptionType.unknown),
|
||||
);
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(false, AudioInterruptionType.unknown),
|
||||
);
|
||||
|
||||
expect(objetivo.reaplicaciones, 0);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class _ObjetivoFake implements ObjetivoAudioInterrumpible {
|
||||
bool intencion = false;
|
||||
bool reproduciendo = false;
|
||||
bool eqActivo = true;
|
||||
int pausas = 0;
|
||||
int reaplicaciones = 0;
|
||||
final List<bool> atenuaciones = [];
|
||||
final List<bool> estadosReaplicados = [];
|
||||
|
||||
/// Ordering log shared across every method — proves reaplicarEcualizador
|
||||
/// fires at the EXACT point in the sequence the fix requires (after
|
||||
/// reanudar()/setAtenuado(false)), not merely "at some point".
|
||||
final List<String> eventos = [];
|
||||
|
||||
@override
|
||||
bool get intencionReproducir => intencion;
|
||||
|
||||
@override
|
||||
bool get estaReproduciendo => reproduciendo;
|
||||
|
||||
@override
|
||||
Future<void> pausar() async {
|
||||
pausas++;
|
||||
reproduciendo = false;
|
||||
intencion = false;
|
||||
eventos.add('pausar');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reanudar() async {
|
||||
reproduciendo = true;
|
||||
intencion = true;
|
||||
eventos.add('reanudar');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setAtenuado(bool atenuado) async {
|
||||
atenuaciones.add(atenuado);
|
||||
eventos.add('atenuado:$atenuado');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reaplicarEcualizador() async {
|
||||
reaplicaciones++;
|
||||
estadosReaplicados.add(eqActivo);
|
||||
eventos.add('reaplicar');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,13 @@ class _ObjetivoFake implements ObjetivoAudioInterrumpible {
|
||||
Future<void> setAtenuado(bool atenuado) async {
|
||||
atenuaciones.add(atenuado);
|
||||
}
|
||||
|
||||
int reaplicaciones = 0;
|
||||
|
||||
@override
|
||||
Future<void> reaplicarEcualizador() async {
|
||||
reaplicaciones++;
|
||||
}
|
||||
}
|
||||
|
||||
/// S3-R1: audio-session interruptions (phone call, transient loss, duck) and
|
||||
|
||||
Reference in New Issue
Block a user