mirror of https://github.com/flutter/pinball.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
352 lines
8.7 KiB
352 lines
8.7 KiB
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';
|
|
|
|
/// Sounds available to play.
|
|
enum PinballAudio {
|
|
/// Google.
|
|
google,
|
|
|
|
/// Bumper.
|
|
bumper,
|
|
|
|
/// Cow moo.
|
|
cowMoo,
|
|
|
|
/// Background music.
|
|
backgroundMusic,
|
|
|
|
/// IO Pinball voice over.
|
|
ioPinballVoiceOver,
|
|
|
|
/// Game over.
|
|
gameOverVoiceOver,
|
|
|
|
/// Launcher.
|
|
launcher,
|
|
|
|
/// Kicker.
|
|
kicker,
|
|
|
|
/// Rollover.
|
|
rollover,
|
|
|
|
/// Sparky.
|
|
sparky,
|
|
|
|
/// Android
|
|
android,
|
|
|
|
/// Dino
|
|
dino,
|
|
|
|
/// Dash
|
|
dash,
|
|
}
|
|
|
|
/// Defines the contract of the creation of an [AudioPool].
|
|
typedef CreateAudioPool = Future<AudioPool> Function(
|
|
String sound, {
|
|
bool? repeating,
|
|
int? maxPlayers,
|
|
int? minPlayers,
|
|
String? prefix,
|
|
});
|
|
|
|
/// Defines the contract for playing a single audio.
|
|
typedef PlaySingleAudio = Future<void> Function(String, {double volume});
|
|
|
|
/// Defines the contract for looping a single audio.
|
|
typedef LoopSingleAudio = Future<void> Function(String, {double volume});
|
|
|
|
/// Defines the contract for pre fetching an audio.
|
|
typedef PreCacheSingleAudio = Future<void> Function(String);
|
|
|
|
/// Defines the contract for configuring an [AudioCache] instance.
|
|
typedef ConfigureAudioCache = void Function(AudioCache);
|
|
|
|
abstract class _Audio {
|
|
void play();
|
|
Future<void> load();
|
|
|
|
String prefixFile(String file) {
|
|
return 'packages/pinball_audio/$file';
|
|
}
|
|
}
|
|
|
|
class _SimplePlayAudio extends _Audio {
|
|
_SimplePlayAudio({
|
|
required this.preCacheSingleAudio,
|
|
required this.playSingleAudio,
|
|
required this.path,
|
|
this.volume,
|
|
});
|
|
|
|
final PreCacheSingleAudio preCacheSingleAudio;
|
|
final PlaySingleAudio playSingleAudio;
|
|
final String path;
|
|
final double? volume;
|
|
|
|
@override
|
|
Future<void> load() => preCacheSingleAudio(prefixFile(path));
|
|
|
|
@override
|
|
void play() {
|
|
playSingleAudio(prefixFile(path), volume: volume ?? 1);
|
|
}
|
|
}
|
|
|
|
class _LoopAudio extends _Audio {
|
|
_LoopAudio({
|
|
required this.preCacheSingleAudio,
|
|
required this.loopSingleAudio,
|
|
required this.path,
|
|
this.volume,
|
|
});
|
|
|
|
final PreCacheSingleAudio preCacheSingleAudio;
|
|
final LoopSingleAudio loopSingleAudio;
|
|
final String path;
|
|
final double? volume;
|
|
|
|
@override
|
|
Future<void> load() => preCacheSingleAudio(prefixFile(path));
|
|
|
|
@override
|
|
void play() {
|
|
loopSingleAudio(prefixFile(path), volume: volume ?? 1);
|
|
}
|
|
}
|
|
|
|
class _SingleLoopAudio extends _LoopAudio {
|
|
_SingleLoopAudio({
|
|
required PreCacheSingleAudio preCacheSingleAudio,
|
|
required LoopSingleAudio loopSingleAudio,
|
|
required String path,
|
|
double? volume,
|
|
}) : super(
|
|
preCacheSingleAudio: preCacheSingleAudio,
|
|
loopSingleAudio: loopSingleAudio,
|
|
path: path,
|
|
volume: volume,
|
|
);
|
|
|
|
bool _playing = false;
|
|
|
|
@override
|
|
void play() {
|
|
if (!_playing) {
|
|
super.play();
|
|
_playing = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
class _RandomABAudio extends _Audio {
|
|
_RandomABAudio({
|
|
required this.createAudioPool,
|
|
required this.seed,
|
|
required this.audioAssetA,
|
|
required this.audioAssetB,
|
|
this.volume,
|
|
});
|
|
|
|
final CreateAudioPool createAudioPool;
|
|
final Random seed;
|
|
final String audioAssetA;
|
|
final String audioAssetB;
|
|
final double? volume;
|
|
|
|
late AudioPool audioA;
|
|
late AudioPool audioB;
|
|
|
|
@override
|
|
Future<void> 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),
|
|
],
|
|
);
|
|
}
|
|
|
|
@override
|
|
void play() {
|
|
(seed.nextBool() ? audioA : audioB).start(volume: volume ?? 1);
|
|
}
|
|
}
|
|
|
|
class _ThrottledAudio extends _Audio {
|
|
_ThrottledAudio({
|
|
required this.preCacheSingleAudio,
|
|
required this.playSingleAudio,
|
|
required this.path,
|
|
required this.duration,
|
|
});
|
|
|
|
final PreCacheSingleAudio preCacheSingleAudio;
|
|
final PlaySingleAudio playSingleAudio;
|
|
final String path;
|
|
final Duration duration;
|
|
|
|
DateTime? _lastPlayed;
|
|
|
|
@override
|
|
Future<void> load() => preCacheSingleAudio(prefixFile(path));
|
|
|
|
@override
|
|
void play() {
|
|
final now = clock.now();
|
|
if (_lastPlayed == null ||
|
|
(_lastPlayed != null && now.difference(_lastPlayed!) > duration)) {
|
|
_lastPlayed = now;
|
|
playSingleAudio(prefixFile(path));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// {@template pinball_audio_player}
|
|
/// Sound manager for the pinball game.
|
|
/// {@endtemplate}
|
|
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,
|
|
_loopSingleAudio = loopSingleAudio ?? FlameAudio.audioCache.loop,
|
|
_preCacheSingleAudio =
|
|
preCacheSingleAudio ?? FlameAudio.audioCache.load,
|
|
_configureAudioCache = configureAudioCache ??
|
|
((AudioCache a) {
|
|
a.prefix = '';
|
|
}),
|
|
_seed = seed ?? Random() {
|
|
audios = {
|
|
PinballAudio.google: _SimplePlayAudio(
|
|
preCacheSingleAudio: _preCacheSingleAudio,
|
|
playSingleAudio: _playSingleAudio,
|
|
path: Assets.sfx.google,
|
|
),
|
|
PinballAudio.sparky: _SimplePlayAudio(
|
|
preCacheSingleAudio: _preCacheSingleAudio,
|
|
playSingleAudio: _playSingleAudio,
|
|
path: Assets.sfx.sparky,
|
|
),
|
|
PinballAudio.dino: _SimplePlayAudio(
|
|
preCacheSingleAudio: _preCacheSingleAudio,
|
|
playSingleAudio: _playSingleAudio,
|
|
path: Assets.sfx.dino,
|
|
),
|
|
PinballAudio.dash: _SimplePlayAudio(
|
|
preCacheSingleAudio: _preCacheSingleAudio,
|
|
playSingleAudio: _playSingleAudio,
|
|
path: Assets.sfx.dash,
|
|
),
|
|
PinballAudio.android: _SimplePlayAudio(
|
|
preCacheSingleAudio: _preCacheSingleAudio,
|
|
playSingleAudio: _playSingleAudio,
|
|
path: Assets.sfx.android,
|
|
),
|
|
PinballAudio.launcher: _SimplePlayAudio(
|
|
preCacheSingleAudio: _preCacheSingleAudio,
|
|
playSingleAudio: _playSingleAudio,
|
|
path: Assets.sfx.launcher,
|
|
),
|
|
PinballAudio.rollover: _SimplePlayAudio(
|
|
preCacheSingleAudio: _preCacheSingleAudio,
|
|
playSingleAudio: _playSingleAudio,
|
|
path: Assets.sfx.rollover,
|
|
volume: 0.3,
|
|
),
|
|
PinballAudio.ioPinballVoiceOver: _SimplePlayAudio(
|
|
preCacheSingleAudio: _preCacheSingleAudio,
|
|
playSingleAudio: _playSingleAudio,
|
|
path: Assets.sfx.ioPinballVoiceOver,
|
|
),
|
|
PinballAudio.gameOverVoiceOver: _SimplePlayAudio(
|
|
preCacheSingleAudio: _preCacheSingleAudio,
|
|
playSingleAudio: _playSingleAudio,
|
|
path: Assets.sfx.gameOverVoiceOver,
|
|
),
|
|
PinballAudio.bumper: _RandomABAudio(
|
|
createAudioPool: _createAudioPool,
|
|
seed: _seed,
|
|
audioAssetA: Assets.sfx.bumperA,
|
|
audioAssetB: Assets.sfx.bumperB,
|
|
volume: 0.6,
|
|
),
|
|
PinballAudio.kicker: _RandomABAudio(
|
|
createAudioPool: _createAudioPool,
|
|
seed: _seed,
|
|
audioAssetA: Assets.sfx.kickerA,
|
|
audioAssetB: Assets.sfx.kickerB,
|
|
volume: 0.6,
|
|
),
|
|
PinballAudio.cowMoo: _ThrottledAudio(
|
|
preCacheSingleAudio: _preCacheSingleAudio,
|
|
playSingleAudio: _playSingleAudio,
|
|
path: Assets.sfx.cowMoo,
|
|
duration: const Duration(seconds: 2),
|
|
),
|
|
PinballAudio.backgroundMusic: _SingleLoopAudio(
|
|
preCacheSingleAudio: _preCacheSingleAudio,
|
|
loopSingleAudio: _loopSingleAudio,
|
|
path: Assets.music.background,
|
|
volume: .6,
|
|
),
|
|
};
|
|
}
|
|
|
|
final CreateAudioPool _createAudioPool;
|
|
|
|
final PlaySingleAudio _playSingleAudio;
|
|
|
|
final LoopSingleAudio _loopSingleAudio;
|
|
|
|
final PreCacheSingleAudio _preCacheSingleAudio;
|
|
|
|
final ConfigureAudioCache _configureAudioCache;
|
|
|
|
final Random _seed;
|
|
|
|
/// Registered audios on the Player.
|
|
@visibleForTesting
|
|
// ignore: library_private_types_in_public_api
|
|
late final Map<PinballAudio, _Audio> audios;
|
|
|
|
/// Loads the sounds effects into the memory.
|
|
List<Future<void> Function()> load() {
|
|
_configureAudioCache(FlameAudio.audioCache);
|
|
|
|
return audios.values.map((a) => a.load).toList();
|
|
}
|
|
|
|
/// Plays the received audio.
|
|
void play(PinballAudio audio) {
|
|
assert(
|
|
audios.containsKey(audio),
|
|
'Tried to play unregistered audio $audio',
|
|
);
|
|
audios[audio]?.play();
|
|
}
|
|
}
|