feat: Implement audio shutdown procedure for graceful app exit
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -143,7 +143,6 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
DefaultRendererSettingsPersistence();
|
DefaultRendererSettingsPersistence();
|
||||||
final DefaultSaveGamePersistence _savePersistence =
|
final DefaultSaveGamePersistence _savePersistence =
|
||||||
DefaultSaveGamePersistence();
|
DefaultSaveGamePersistence();
|
||||||
Future<void>? _audioShutdownFuture;
|
|
||||||
|
|
||||||
/// Mirrors [WolfRendererSettings.mode] into the Flutter renderer enum.
|
/// Mirrors [WolfRendererSettings.mode] into the Flutter renderer enum.
|
||||||
RendererMode _rendererMode = RendererMode.hardware;
|
RendererMode _rendererMode = RendererMode.hardware;
|
||||||
@@ -213,29 +212,15 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
unawaited(_shutdownAudioForExit());
|
unawaited(widget.wolf3d.shutdownAudio());
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _quitApplication() async {
|
Future<void> _quitApplication() async {
|
||||||
await _shutdownAudioForExit();
|
await widget.wolf3d.shutdownAudio();
|
||||||
await SystemNavigator.pop();
|
await SystemNavigator.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _shutdownAudioForExit() {
|
|
||||||
final existing = _audioShutdownFuture;
|
|
||||||
if (existing != null) {
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
final shutdown = () async {
|
|
||||||
await widget.wolf3d.audio.stopAllAudio();
|
|
||||||
widget.wolf3d.audio.dispose();
|
|
||||||
}();
|
|
||||||
_audioShutdownFuture = shutdown;
|
|
||||||
return shutdown;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopScope(
|
return PopScope(
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ class Wolf3d {
|
|||||||
/// Shared engine audio backend used by menus and gameplay sessions.
|
/// Shared engine audio backend used by menus and gameplay sessions.
|
||||||
final EngineAudio audio;
|
final EngineAudio audio;
|
||||||
|
|
||||||
|
Future<void>? _audioShutdownFuture;
|
||||||
|
|
||||||
/// Engine menu background color as 24-bit RGB.
|
/// Engine menu background color as 24-bit RGB.
|
||||||
int menuBackgroundRgb = 0x890000;
|
int menuBackgroundRgb = 0x890000;
|
||||||
|
|
||||||
@@ -246,6 +248,24 @@ class Wolf3d {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stops and disposes shared audio exactly once for app shutdown.
|
||||||
|
///
|
||||||
|
/// Repeated calls return the same in-flight/completed future so hosts can
|
||||||
|
/// safely invoke shutdown from multiple lifecycle paths.
|
||||||
|
Future<void> shutdownAudio() {
|
||||||
|
final existing = _audioShutdownFuture;
|
||||||
|
if (existing != null) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
final shutdown = () async {
|
||||||
|
await audio.stopAllAudio();
|
||||||
|
audio.dispose();
|
||||||
|
}();
|
||||||
|
_audioShutdownFuture = shutdown;
|
||||||
|
return shutdown;
|
||||||
|
}
|
||||||
|
|
||||||
/// Loads an asset from the Flutter bundle, returning `null` when absent.
|
/// Loads an asset from the Flutter bundle, returning `null` when absent.
|
||||||
Future<ByteData?> _tryLoad(String path) async {
|
Future<ByteData?> _tryLoad(String path) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
||||||
|
|
||||||
|
class _CountingAudio implements EngineAudio {
|
||||||
|
@override
|
||||||
|
WolfensteinData? activeGame;
|
||||||
|
|
||||||
|
int stopAllAudioCallCount = 0;
|
||||||
|
int disposeCallCount = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> debugSoundTest() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> init() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playLevelMusic(Music music) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playMenuMusic() {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playSoundEffect(SoundEffect effect) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playSoundEffectId(int sfxId) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> stopAllAudio() async {
|
||||||
|
stopAllAudioCallCount++;
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void stopMusic() {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
disposeCallCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Wolf3d.shutdownAudio', () {
|
||||||
|
test('stops and disposes audio once', () async {
|
||||||
|
final audio = _CountingAudio();
|
||||||
|
final wolf3d = Wolf3d(audioBackend: audio);
|
||||||
|
|
||||||
|
await wolf3d.shutdownAudio();
|
||||||
|
await wolf3d.shutdownAudio();
|
||||||
|
|
||||||
|
expect(audio.stopAllAudioCallCount, 1);
|
||||||
|
expect(audio.disposeCallCount, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('concurrent calls share the same shutdown work', () async {
|
||||||
|
final audio = _CountingAudio();
|
||||||
|
final wolf3d = Wolf3d(audioBackend: audio);
|
||||||
|
|
||||||
|
await Future.wait<void>([
|
||||||
|
wolf3d.shutdownAudio(),
|
||||||
|
wolf3d.shutdownAudio(),
|
||||||
|
wolf3d.shutdownAudio(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(audio.stopAllAudioCallCount, 1);
|
||||||
|
expect(audio.disposeCallCount, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user