diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 71c54903..8c82b1d2 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -42,7 +42,8 @@ "unawaited", "unfocus", "unlayered", - "vsync" + "vsync", + "microtask" ], "ignorePaths": [ ".github/workflows/**" diff --git a/packages/pinball_audio/lib/src/pinball_audio.dart b/packages/pinball_audio/lib/src/pinball_audio.dart index 4a3d04d8..796c8cdc 100644 --- a/packages/pinball_audio/lib/src/pinball_audio.dart +++ b/packages/pinball_audio/lib/src/pinball_audio.dart @@ -2,10 +2,10 @@ import 'dart:math'; import 'package:audioplayers/audioplayers.dart'; import 'package:clock/clock.dart'; -import 'package:flame_audio/audio_pool.dart'; import 'package:flame_audio/flame_audio.dart'; import 'package:flutter/material.dart'; import 'package:pinball_audio/gen/assets.gen.dart'; +import 'package:pinball_audio/src/pinball_audio_pool.dart'; /// Sounds available to play. enum PinballAudio { @@ -52,17 +52,8 @@ enum PinballAudio { flipper, } -/// Defines the contract of the creation of an [AudioPool]. -typedef CreateAudioPool = Future Function( - String sound, { - bool? repeating, - int? maxPlayers, - int? minPlayers, - String? prefix, -}); - /// Defines the contract for playing a single audio. -typedef PlaySingleAudio = Future Function(String, {double volume}); +typedef PlaySingleAudio = Future Function(String, {double volume}); /// Defines the contract for looping a single audio. typedef LoopSingleAudio = Future Function(String, {double volume}); @@ -94,13 +85,20 @@ class _SimplePlayAudio extends _Audio { final PlaySingleAudio playSingleAudio; final String path; final double? volume; + AudioPlayer? _player; @override Future load() => preCacheSingleAudio(prefixFile(path)); @override - void play() { - playSingleAudio(prefixFile(path), volume: volume ?? 1); + Future play() async { + final url = prefixFile(path); + final volume = this.volume ?? 1; + if (_player == null) { + _player = await playSingleAudio(url, volume: volume); + } else { + await _player!.play(url, volume: volume); + } } } @@ -153,81 +151,94 @@ class _SingleLoopAudio extends _LoopAudio { class _SingleAudioPool extends _Audio { _SingleAudioPool({ required this.path, - required this.createAudioPool, + required this.duration, required this.maxPlayers, + required this.preCacheSingleAudio, + required this.playSingleAudio, }); final String path; - final CreateAudioPool createAudioPool; final int maxPlayers; - late AudioPool pool; + final Duration duration; + final PreCacheSingleAudio preCacheSingleAudio; + final PlaySingleAudio playSingleAudio; + late PinballAudioPool pool; @override Future load() async { - pool = await createAudioPool( - prefixFile(path), - maxPlayers: maxPlayers, - prefix: '', + pool = PinballAudioPool( + path: prefixFile(path), + poolSize: maxPlayers, + preCacheSingleAudio: preCacheSingleAudio, + playSingleAudio: playSingleAudio, + duration: duration, ); + await pool.load(); } @override - void play() => pool.start(); + void play() => pool.play(); } class _RandomABAudio extends _Audio { _RandomABAudio({ - required this.createAudioPool, + required this.preCacheSingleAudio, + required this.playSingleAudio, required this.seed, required this.audioAssetA, required this.audioAssetB, + required this.duration, this.volume, }); - final CreateAudioPool createAudioPool; + final PreCacheSingleAudio preCacheSingleAudio; + final PlaySingleAudio playSingleAudio; final Random seed; final String audioAssetA; final String audioAssetB; + final Duration duration; final double? volume; - late AudioPool audioA; - late AudioPool audioB; + late PinballAudioPool audioA; + late PinballAudioPool audioB; @override Future load() async { - await Future.wait( - [ - createAudioPool( - prefixFile(audioAssetA), - maxPlayers: 4, - prefix: '', - ).then((pool) => audioA = pool), - createAudioPool( - prefixFile(audioAssetB), - maxPlayers: 4, - prefix: '', - ).then((pool) => audioB = pool), - ], + audioA = PinballAudioPool( + path: prefixFile(audioAssetA), + poolSize: 4, + preCacheSingleAudio: preCacheSingleAudio, + playSingleAudio: playSingleAudio, + duration: duration, ); + audioB = PinballAudioPool( + path: prefixFile(audioAssetB), + poolSize: 4, + preCacheSingleAudio: preCacheSingleAudio, + playSingleAudio: playSingleAudio, + duration: duration, + ); + await Future.wait([audioA.load(), audioB.load()]); } @override void play() { - (seed.nextBool() ? audioA : audioB).start(volume: volume ?? 1); + (seed.nextBool() ? audioA : audioB).play(volume: volume ?? 1); } } -class _ThrottledAudio extends _Audio { +class _ThrottledAudio extends _SimplePlayAudio { _ThrottledAudio({ - required this.preCacheSingleAudio, - required this.playSingleAudio, - required this.path, + required PreCacheSingleAudio preCacheSingleAudio, + required PlaySingleAudio playSingleAudio, + required String path, required this.duration, - }); + }) : super( + preCacheSingleAudio: preCacheSingleAudio, + playSingleAudio: playSingleAudio, + path: path, + ); - final PreCacheSingleAudio preCacheSingleAudio; - final PlaySingleAudio playSingleAudio; - final String path; final Duration duration; DateTime? _lastPlayed; @@ -236,12 +247,12 @@ class _ThrottledAudio extends _Audio { Future load() => preCacheSingleAudio(prefixFile(path)); @override - void play() { + Future play() async { final now = clock.now(); if (_lastPlayed == null || (_lastPlayed != null && now.difference(_lastPlayed!) > duration)) { _lastPlayed = now; - playSingleAudio(prefixFile(path)); + await super.play(); } } } @@ -252,14 +263,12 @@ class _ThrottledAudio extends _Audio { class PinballAudioPlayer { /// {@macro pinball_audio_player} PinballAudioPlayer({ - CreateAudioPool? createAudioPool, PlaySingleAudio? playSingleAudio, LoopSingleAudio? loopSingleAudio, PreCacheSingleAudio? preCacheSingleAudio, ConfigureAudioCache? configureAudioCache, Random? seed, - }) : _createAudioPool = createAudioPool ?? AudioPool.create, - _playSingleAudio = playSingleAudio ?? FlameAudio.audioCache.play, + }) : _playSingleAudio = playSingleAudio ?? FlameAudio.audioCache.play, _loopSingleAudio = loopSingleAudio ?? FlameAudio.audioCache.loop, _preCacheSingleAudio = preCacheSingleAudio ?? FlameAudio.audioCache.load, @@ -308,8 +317,10 @@ class PinballAudioPlayer { ), PinballAudio.flipper: _SingleAudioPool( path: Assets.sfx.flipper, - createAudioPool: _createAudioPool, - maxPlayers: 2, + maxPlayers: 4, + preCacheSingleAudio: _preCacheSingleAudio, + playSingleAudio: _playSingleAudio, + duration: const Duration(milliseconds: 200), ), PinballAudio.ioPinballVoiceOver: _SimplePlayAudio( preCacheSingleAudio: _preCacheSingleAudio, @@ -322,17 +333,21 @@ class PinballAudioPlayer { path: Assets.sfx.gameOverVoiceOver, ), PinballAudio.bumper: _RandomABAudio( - createAudioPool: _createAudioPool, + preCacheSingleAudio: _preCacheSingleAudio, + playSingleAudio: _playSingleAudio, seed: _seed, audioAssetA: Assets.sfx.bumperA, audioAssetB: Assets.sfx.bumperB, + duration: const Duration(seconds: 1), volume: 0.6, ), PinballAudio.kicker: _RandomABAudio( - createAudioPool: _createAudioPool, + preCacheSingleAudio: _preCacheSingleAudio, + playSingleAudio: _playSingleAudio, seed: _seed, audioAssetA: Assets.sfx.kickerA, audioAssetB: Assets.sfx.kickerB, + duration: const Duration(seconds: 1), volume: 0.6, ), PinballAudio.cowMoo: _ThrottledAudio( @@ -350,8 +365,6 @@ class PinballAudioPlayer { }; } - final CreateAudioPool _createAudioPool; - final PlaySingleAudio _playSingleAudio; final LoopSingleAudio _loopSingleAudio; diff --git a/packages/pinball_audio/lib/src/pinball_audio_pool.dart b/packages/pinball_audio/lib/src/pinball_audio_pool.dart new file mode 100644 index 00000000..659d325e --- /dev/null +++ b/packages/pinball_audio/lib/src/pinball_audio_pool.dart @@ -0,0 +1,87 @@ +import 'dart:async'; + +import 'package:audioplayers/audioplayers.dart'; +import 'package:pinball_audio/pinball_audio.dart'; + +class _PlayerEntry { + _PlayerEntry({ + required this.available, + required this.player, + }); + + bool available; + final AudioPlayer player; +} + +/// {@template pinball_audio_pool} +/// Creates an audio player pool used to trigger many sounds at the same time. +/// {@endtemplate} +class PinballAudioPool { + /// {@macro pinball_audio_pool} + PinballAudioPool({ + required this.path, + required this.poolSize, + required this.preCacheSingleAudio, + required this.playSingleAudio, + required this.duration, + }); + + /// Sounds path. + final String path; + + /// Max size of this pool. + final int poolSize; + + /// Function to cache audios. + final PreCacheSingleAudio preCacheSingleAudio; + + /// Function to play audios. + final PlaySingleAudio playSingleAudio; + + /// How long the sound lasts. + final Duration duration; + + final List<_PlayerEntry> _players = []; + + /// Loads the pool. + Future load() async { + await preCacheSingleAudio(path); + } + + /// Plays the pool. + Future play({double volume = 1}) async { + AudioPlayer? player; + if (_players.length < poolSize) { + _players.add( + _PlayerEntry( + available: false, + player: player = await playSingleAudio(path, volume: volume), + ), + ); + } else { + final entries = _players.where((entry) => entry.available); + if (entries.isNotEmpty) { + final entry = entries.first..available = false; + + player = entry.player; + unawaited(entry.player.play(path, volume: volume)); + } + } + + if (player != null) { + unawaited( + Future.delayed(duration).then( + (_) { + _returnEntryAvailability(player!); + }, + ), + ); + } else {} + } + + void _returnEntryAvailability( + AudioPlayer player, + ) { + _players.where((entry) => entry.player == player).single.available = true; + } +} diff --git a/packages/pinball_audio/test/src/pinball_audio_pool_test.dart b/packages/pinball_audio/test/src/pinball_audio_pool_test.dart new file mode 100644 index 00000000..3b132808 --- /dev/null +++ b/packages/pinball_audio/test/src/pinball_audio_pool_test.dart @@ -0,0 +1,74 @@ +// ignore_for_file: one_member_abstracts + +import 'package:audioplayers/audioplayers.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_audio/src/pinball_audio_pool.dart'; + +class _MockAudioPlayer extends Mock implements AudioPlayer {} + +class _MockPlaySingleAudio extends Mock { + Future onCall(String path, {double volume}); +} + +abstract class _PreCacheSingleAudio { + Future onCall(String path); +} + +class _MockPreCacheSingleAudio extends Mock implements _PreCacheSingleAudio {} + +void main() { + group('PinballAudioPool', () { + late _PreCacheSingleAudio preCacheSingleAudio; + late _MockPlaySingleAudio playSingleAudio; + late PinballAudioPool pool; + late AudioPlayer audioPlayer; + + setUp(() { + preCacheSingleAudio = _MockPreCacheSingleAudio(); + when(() => preCacheSingleAudio.onCall(any())).thenAnswer((_) async {}); + + audioPlayer = _MockAudioPlayer(); + when(() => audioPlayer.play(any(), volume: any(named: 'volume'))) + .thenAnswer((_) async => 1); + + playSingleAudio = _MockPlaySingleAudio(); + when(() => playSingleAudio.onCall(any(), volume: any(named: 'volume'))) + .thenAnswer((_) async => audioPlayer); + + pool = PinballAudioPool( + path: 'path', + poolSize: 1, + preCacheSingleAudio: preCacheSingleAudio.onCall, + playSingleAudio: playSingleAudio.onCall, + duration: const Duration(milliseconds: 10), + ); + }); + + test('pre cache the sound', () async { + await pool.load(); + verify(() => preCacheSingleAudio.onCall('path')).called(1); + }); + + test('plays a fresh sound', () async { + await pool.load(); + await pool.play(); + + verify( + () => playSingleAudio.onCall( + 'path', + volume: any(named: 'volume'), + ), + ).called(1); + }); + + test('plays from the pool after it returned', () async { + await pool.load(); + await pool.play(); + await Future.delayed(const Duration(milliseconds: 12)); + await pool.play(); + + verify(() => audioPlayer.play('path')).called(1); + }); + }); +} diff --git a/packages/pinball_audio/test/src/pinball_audio_test.dart b/packages/pinball_audio/test/src/pinball_audio_test.dart index 1c82815d..9fd5b8e7 100644 --- a/packages/pinball_audio/test/src/pinball_audio_test.dart +++ b/packages/pinball_audio/test/src/pinball_audio_test.dart @@ -3,33 +3,22 @@ import 'dart:math'; import 'package:audioplayers/audioplayers.dart'; import 'package:clock/clock.dart'; -import 'package:flame_audio/audio_pool.dart'; import 'package:flame_audio/flame_audio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_audio/gen/assets.gen.dart'; import 'package:pinball_audio/pinball_audio.dart'; -class _MockAudioPool extends Mock implements AudioPool {} - class _MockAudioCache extends Mock implements AudioCache {} -class _MockCreateAudioPool extends Mock { - Future onCall( - String sound, { - bool? repeating, - int? maxPlayers, - int? minPlayers, - String? prefix, - }); -} +class _MockAudioPlayer extends Mock implements AudioPlayer {} class _MockConfigureAudioCache extends Mock { void onCall(AudioCache cache); } class _MockPlaySingleAudio extends Mock { - Future onCall(String path, {double volume}); + Future onCall(String path, {double volume}); } class _MockLoopSingleAudio extends Mock { @@ -48,7 +37,6 @@ class _MockClock extends Mock implements Clock {} void main() { group('PinballAudio', () { - late _MockCreateAudioPool createAudioPool; late _MockConfigureAudioCache configureAudioCache; late _MockPlaySingleAudio playSingleAudio; late _MockLoopSingleAudio loopSingleAudio; @@ -61,21 +49,12 @@ void main() { }); setUp(() { - createAudioPool = _MockCreateAudioPool(); - when( - () => createAudioPool.onCall( - any(), - maxPlayers: any(named: 'maxPlayers'), - prefix: any(named: 'prefix'), - ), - ).thenAnswer((_) async => _MockAudioPool()); - configureAudioCache = _MockConfigureAudioCache(); when(() => configureAudioCache.onCall(any())).thenAnswer((_) {}); playSingleAudio = _MockPlaySingleAudio(); when(() => playSingleAudio.onCall(any(), volume: any(named: 'volume'))) - .thenAnswer((_) async {}); + .thenAnswer((_) async => _MockAudioPlayer()); loopSingleAudio = _MockLoopSingleAudio(); when(() => loopSingleAudio.onCall(any(), volume: any(named: 'volume'))) @@ -88,7 +67,6 @@ void main() { audioPlayer = PinballAudioPlayer( configureAudioCache: configureAudioCache.onCall, - createAudioPool: createAudioPool.onCall, playSingleAudio: playSingleAudio.onCall, loopSingleAudio: loopSingleAudio.onCall, preCacheSingleAudio: preCacheSingleAudio.onCall, @@ -101,64 +79,6 @@ void main() { }); group('load', () { - test('creates the bumpers pools', () async { - await Future.wait( - audioPlayer.load().map((loadableBuilder) => loadableBuilder()), - ); - - verify( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.bumperA}', - maxPlayers: 4, - prefix: '', - ), - ).called(1); - - verify( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.bumperB}', - maxPlayers: 4, - prefix: '', - ), - ).called(1); - }); - - test('creates the kicker pools', () async { - await Future.wait( - audioPlayer.load().map((loadableBuilder) => loadableBuilder()), - ); - - verify( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.kickerA}', - maxPlayers: 4, - prefix: '', - ), - ).called(1); - - verify( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.kickerB}', - maxPlayers: 4, - prefix: '', - ), - ).called(1); - }); - - test('creates the flipper pool', () async { - await Future.wait( - audioPlayer.load().map((loadableBuilder) => loadableBuilder()), - ); - - verify( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.flipper}', - maxPlayers: 2, - prefix: '', - ), - ).called(1); - }); - test('configures the audio cache instance', () async { await Future.wait( audioPlayer.load().map((loadableBuilder) => loadableBuilder()), @@ -170,7 +90,6 @@ void main() { test('sets the correct prefix', () async { audioPlayer = PinballAudioPlayer( - createAudioPool: createAudioPool.onCall, playSingleAudio: playSingleAudio.onCall, preCacheSingleAudio: preCacheSingleAudio.onCall, ); @@ -186,6 +105,26 @@ void main() { audioPlayer.load().map((loadableBuilder) => loadableBuilder()), ); + verify( + () => preCacheSingleAudio + .onCall('packages/pinball_audio/assets/sfx/bumper_a.mp3'), + ).called(1); + verify( + () => preCacheSingleAudio + .onCall('packages/pinball_audio/assets/sfx/bumper_b.mp3'), + ).called(1); + verify( + () => preCacheSingleAudio + .onCall('packages/pinball_audio/assets/sfx/kicker_a.mp3'), + ).called(1); + verify( + () => preCacheSingleAudio + .onCall('packages/pinball_audio/assets/sfx/kicker_b.mp3'), + ).called(1); + verify( + () => preCacheSingleAudio + .onCall('packages/pinball_audio/assets/sfx/flipper.mp3'), + ).called(1); verify( () => preCacheSingleAudio .onCall('packages/pinball_audio/assets/sfx/google.mp3'), @@ -236,33 +175,6 @@ void main() { }); group('bumper', () { - late AudioPool bumperAPool; - late AudioPool bumperBPool; - - setUp(() { - bumperAPool = _MockAudioPool(); - when(() => bumperAPool.start(volume: any(named: 'volume'))) - .thenAnswer((_) async => () {}); - when( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.bumperA}', - maxPlayers: any(named: 'maxPlayers'), - prefix: any(named: 'prefix'), - ), - ).thenAnswer((_) async => bumperAPool); - - bumperBPool = _MockAudioPool(); - when(() => bumperBPool.start(volume: any(named: 'volume'))) - .thenAnswer((_) async => () {}); - when( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.bumperB}', - maxPlayers: any(named: 'maxPlayers'), - prefix: any(named: 'prefix'), - ), - ).thenAnswer((_) async => bumperBPool); - }); - group('when seed is true', () { test('plays the bumper A sound pool', () async { when(seed.nextBool).thenReturn(true); @@ -271,7 +183,12 @@ void main() { ); audioPlayer.play(PinballAudio.bumper); - verify(() => bumperAPool.start(volume: 0.6)).called(1); + verify( + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.bumperA}', + volume: 0.6, + ), + ).called(1); }); }); @@ -283,39 +200,17 @@ void main() { ); audioPlayer.play(PinballAudio.bumper); - verify(() => bumperBPool.start(volume: 0.6)).called(1); + verify( + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.bumperB}', + volume: 0.6, + ), + ).called(1); }); }); }); group('kicker', () { - late AudioPool kickerAPool; - late AudioPool kickerBPool; - - setUp(() { - kickerAPool = _MockAudioPool(); - when(() => kickerAPool.start(volume: any(named: 'volume'))) - .thenAnswer((_) async => () {}); - when( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.kickerA}', - maxPlayers: any(named: 'maxPlayers'), - prefix: any(named: 'prefix'), - ), - ).thenAnswer((_) async => kickerAPool); - - kickerBPool = _MockAudioPool(); - when(() => kickerBPool.start(volume: any(named: 'volume'))) - .thenAnswer((_) async => () {}); - when( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.kickerB}', - maxPlayers: any(named: 'maxPlayers'), - prefix: any(named: 'prefix'), - ), - ).thenAnswer((_) async => kickerBPool); - }); - group('when seed is true', () { test('plays the kicker A sound pool', () async { when(seed.nextBool).thenReturn(true); @@ -324,7 +219,12 @@ void main() { ); audioPlayer.play(PinballAudio.kicker); - verify(() => kickerAPool.start(volume: 0.6)).called(1); + verify( + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.kickerA}', + volume: 0.6, + ), + ).called(1); }); }); @@ -336,27 +236,17 @@ void main() { ); audioPlayer.play(PinballAudio.kicker); - verify(() => kickerBPool.start(volume: 0.6)).called(1); + verify( + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.kickerB}', + volume: 0.6, + ), + ).called(1); }); }); }); group('flipper', () { - late AudioPool pool; - - setUp(() { - pool = _MockAudioPool(); - when(() => pool.start(volume: any(named: 'volume'))) - .thenAnswer((_) async => () {}); - when( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.flipper}', - maxPlayers: any(named: 'maxPlayers'), - prefix: any(named: 'prefix'), - ), - ).thenAnswer((_) async => pool); - }); - test('plays the flipper sound pool', () async { when(seed.nextBool).thenReturn(true); await Future.wait( @@ -364,7 +254,12 @@ void main() { ); audioPlayer.play(PinballAudio.flipper); - verify(() => pool.start()).called(1); + verify( + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.flipper}', + volume: any(named: 'volume'), + ), + ).called(1); }); }); @@ -376,14 +271,21 @@ void main() { audioPlayer.play(PinballAudio.cowMoo); verify( - () => playSingleAudio - .onCall('packages/pinball_audio/${Assets.sfx.cowMoo}'), + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.cowMoo}', + volume: any(named: 'volume'), + ), ).called(1); }); test('only plays the sound again after 2 seconds', () async { final clock = _MockClock(); await withClock(clock, () async { + final audioPlayerInstance = _MockAudioPlayer(); + when( + () => playSingleAudio.onCall(any(), volume: any(named: 'volume')), + ).thenAnswer((_) async => audioPlayerInstance); + when(clock.now).thenReturn(DateTime(2022)); await Future.wait( audioPlayer.load().map((loadableBuilder) => loadableBuilder()), @@ -393,16 +295,20 @@ void main() { ..play(PinballAudio.cowMoo); verify( - () => playSingleAudio - .onCall('packages/pinball_audio/${Assets.sfx.cowMoo}'), + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.cowMoo}', + volume: any(named: 'volume'), + ), ).called(1); when(clock.now).thenReturn(DateTime(2022, 1, 1, 1, 2)); audioPlayer.play(PinballAudio.cowMoo); verify( - () => playSingleAudio - .onCall('packages/pinball_audio/${Assets.sfx.cowMoo}'), + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.cowMoo}', + volume: any(named: 'volume'), + ), ).called(1); }); }); @@ -422,6 +328,44 @@ void main() { ), ).called(1); }); + + test('uses the cached player on the second time', () async { + final audioPlayerCache = _MockAudioPlayer(); + when(() => audioPlayerCache.play(any(), volume: any(named: 'volume'))) + .thenAnswer((_) async => 0); + + when(() => playSingleAudio.onCall(any(), volume: any(named: 'volume'))) + .thenAnswer((_) async => audioPlayerCache); + audioPlayer = PinballAudioPlayer( + configureAudioCache: configureAudioCache.onCall, + playSingleAudio: playSingleAudio.onCall, + loopSingleAudio: loopSingleAudio.onCall, + preCacheSingleAudio: preCacheSingleAudio.onCall, + seed: seed, + ); + + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); + audioPlayer.play(PinballAudio.google); + + verify( + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.google}', + volume: any(named: 'volume'), + ), + ).called(1); + + await Future.microtask(() {}); + + audioPlayer.play(PinballAudio.google); + verify( + () => audioPlayerCache.play( + 'packages/pinball_audio/${Assets.sfx.google}', + volume: any(named: 'volume'), + ), + ).called(1); + }); }); group('sparky', () { @@ -467,16 +411,20 @@ void main() { ..play(PinballAudio.dino); verify( - () => playSingleAudio - .onCall('packages/pinball_audio/${Assets.sfx.dino}'), + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.dino}', + volume: any(named: 'volume'), + ), ).called(1); when(clock.now).thenReturn(DateTime(2022, 1, 1, 1, 6)); audioPlayer.play(PinballAudio.dino); verify( - () => playSingleAudio - .onCall('packages/pinball_audio/${Assets.sfx.dino}'), + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.dino}', + volume: any(named: 'volume'), + ), ).called(1); }); });