diff --git a/apps/wolf_3d_gui/lib/screens/game_screen.dart b/apps/wolf_3d_gui/lib/screens/game_screen.dart index 4f7546c..4896fbe 100644 --- a/apps/wolf_3d_gui/lib/screens/game_screen.dart +++ b/apps/wolf_3d_gui/lib/screens/game_screen.dart @@ -143,7 +143,6 @@ class _GameScreenState extends State { DefaultRendererSettingsPersistence(); final DefaultSaveGamePersistence _savePersistence = DefaultSaveGamePersistence(); - Future? _audioShutdownFuture; /// Mirrors [WolfRendererSettings.mode] into the Flutter renderer enum. RendererMode _rendererMode = RendererMode.hardware; @@ -213,29 +212,15 @@ class _GameScreenState extends State { @override void dispose() { - unawaited(_shutdownAudioForExit()); + unawaited(widget.wolf3d.shutdownAudio()); super.dispose(); } Future _quitApplication() async { - await _shutdownAudioForExit(); + await widget.wolf3d.shutdownAudio(); await SystemNavigator.pop(); } - Future _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 Widget build(BuildContext context) { return PopScope( diff --git a/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart b/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart index f92f098..585db39 100644 --- a/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart +++ b/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart @@ -26,6 +26,8 @@ class Wolf3d { /// Shared engine audio backend used by menus and gameplay sessions. final EngineAudio audio; + Future? _audioShutdownFuture; + /// Engine menu background color as 24-bit RGB. int menuBackgroundRgb = 0x890000; @@ -246,6 +248,24 @@ class Wolf3d { 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 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. Future _tryLoad(String path) async { try { diff --git a/packages/wolf_3d_flutter/test/wolf_3d_flutter_shutdown_audio_test.dart b/packages/wolf_3d_flutter/test/wolf_3d_flutter_shutdown_audio_test.dart new file mode 100644 index 0000000..42047ba --- /dev/null +++ b/packages/wolf_3d_flutter/test/wolf_3d_flutter_shutdown_audio_test.dart @@ -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 debugSoundTest() async {} + + @override + Future init() async {} + + @override + void playLevelMusic(Music music) {} + + @override + void playMenuMusic() {} + + @override + void playSoundEffect(SoundEffect effect) {} + + @override + void playSoundEffectId(int sfxId) {} + + @override + Future stopAllAudio() async { + stopAllAudioCallCount++; + await Future.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([ + wolf3d.shutdownAudio(), + wolf3d.shutdownAudio(), + wolf3d.shutdownAudio(), + ]); + + expect(audio.stopAllAudioCallCount, 1); + expect(audio.disposeCallCount, 1); + }); + }); +}