part of skysprites; enum SoundFadeMode { crossFade, fadeOutThenPlay, fadeOutThenFadeIn, } enum SoundEventSimultaneousPolicy { dontPlay, stopOldest, } enum SoundEventMinimumOverlapPolicy { dontPlay, delay, } class SoundEvent { SoundEvent(SoundEffect effect) { effects = [effect]; } SoundEvent.withList(this.effects); List<SoundEffect> effects; double pitchVariance = 0.0; double volumeVariance = 0.0; double panVariance = 0.0; SoundEventSimultaneousPolicy simultaneousLimitPolicy = SoundEventSimultaneousPolicy.stopOldest; int simultaneousLimit = 0; SoundEventMinimumOverlapPolicy minimumOverlapPolicy = SoundEventMinimumOverlapPolicy.dontPlay; double minimumOverlap = 0.0; } class _PlayingSoundEvent { SoundEvent event; SoundEffectStream stream; int startTime; } SoundManager _sharedSoundManager; class SoundManager { static SoundManager sharedInstance() { if (_sharedSoundManager == null) { _sharedSoundManager = new SoundManager(); } return _sharedSoundManager; } static void purgeSharedInstance() { if (_sharedSoundManager == null) return; _sharedSoundManager = null; } SoundManager() { new Timer.periodic(new Duration(milliseconds:10), _update); } Map<SoundEvent, List<_PlayingSoundEvent>> _playingEvents = {}; SoundTrack _backgroundMusicTrack; SoundEffectPlayer _effectPlayer = SoundEffectPlayer.sharedInstance(); SoundTrackPlayer _trackPlayer = SoundTrackPlayer.sharedInstance(); ActionController actions = new ActionController(); bool enableBackgroundMusic; bool enableSoundEffects; int _lastTimeStamp; void playEvent(SoundEvent evt, [double volume = 1.0, double pitch = 1.0, double pan = 0.0]) { List<_PlayingSoundEvent> playingList = _playingEvents[evt]; if (playingList == null) playingList = []; // Check simultaneousLimit if (evt.simultaneousLimit != 0 && evt.simultaneousLimit >= playingList.length) { // We have too many sounds playing if (evt.simultaneousLimitPolicy == SoundEventSimultaneousPolicy.dontPlay) { // Skip this sound event return; } else { // Stop the oldest sound _effectPlayer.stop(playingList[0].stream); } } // Check for overlap int playTime = new DateTime.now().millisecondsSinceEpoch; if (evt.minimumOverlap != 0.0 && playingList.length > 0) { int overlap = playTime - playingList.last.startTime; if (overlap.toDouble() / 1000.0 < evt.minimumOverlap) { // Sounds are overlapping if (evt.minimumOverlapPolicy == SoundEventMinimumOverlapPolicy.dontPlay) { return; } else { // TODO: try to play the sound a little bit later return; } } } // Create a new entry for the event _PlayingSoundEvent newPlaying = new _PlayingSoundEvent(); newPlaying.startTime = playTime; newPlaying.event = evt; // Pick a sound effect to play SoundEffect effect = evt.effects.elementAt(randomInt(evt.effects.length)); // Add the entry playingList.add(newPlaying); // Play the event newPlaying.stream = _effectPlayer.play( effect, false, (volume + evt.volumeVariance * randomSignedDouble()).clamp(0.0, 2.0), (pitch + evt.pitchVariance * randomSignedDouble()).clamp(0.5, 2.0), (pan + evt.panVariance * randomSignedDouble()).clamp(-1.0, 1.0), (SoundEffectStream s) { // Completion callback - remove the entry playingList.remove(newPlaying); } ); } void stopAllEvents([double fadeDuration]) { for (List<_PlayingSoundEvent> playingList in _playingEvents) { for (_PlayingSoundEvent playing in playingList) { if (fadeDuration > 0.0) { // Fade out and stop ActionTween fadeOut = new ActionTween((a) => playing.stream.volume = a, playing.stream.volume, 0.0, fadeDuration); ActionCallFunction stop = new ActionCallFunction(() { _effectPlayer.stop(playing.stream); }); ActionSequence seq = new ActionSequence([fadeOut, stop]); actions.run(seq); } else { // Stop right away _effectPlayer.stop(playing.stream); } } } } void playBackgroundMusic(SoundTrack track, [double fadeDuration = 0.0, SoundFadeMode fadeMode = SoundFadeMode.fadeOutThenPlay]) { double fadeInDuration = 0.0; double fadeInDelay = 0.0; double fadeOutDuration = 0.0; // Calculate durations if (fadeDuration > 0.0) { if (fadeMode == SoundFadeMode.crossFade) { fadeOutDuration = fadeDuration; fadeInDuration = fadeDuration; } else if (fadeMode == SoundFadeMode.fadeOutThenPlay) { fadeOutDuration = fadeDuration; fadeInDelay = fadeDuration; } else if (fadeMode == SoundFadeMode.fadeOutThenFadeIn) { fadeOutDuration = fadeDuration / 2.0; fadeInDuration = fadeDuration / 2.0; fadeInDelay = fadeDuration / 2.0; } } if (_backgroundMusicTrack != null) { // Stop the current track if (fadeOutDuration == 0.0) { _trackPlayer.stop(_backgroundMusicTrack); } else { ActionTween fadeOut = new ActionTween((a) => _backgroundMusicTrack.volume = a, _backgroundMusicTrack.volume, 0.0, fadeOutDuration); ActionCallFunction stop = new ActionCallFunction(() { _trackPlayer.stop(_backgroundMusicTrack); }); ActionSequence seq = new ActionSequence([fadeOut, stop]); actions.run(seq); } } else { fadeInDelay = 0.0; } // Fade in new sound if (fadeInDelay == 0.0) { _fadeInTrack(track, fadeInDuration); } else { ActionDelay delay = new ActionDelay(fadeInDelay); ActionCallFunction fadeInCall = new ActionCallFunction(() { _fadeInTrack(track, fadeInDuration); }); ActionSequence seq = new ActionSequence([delay, fadeInCall]); actions.run(seq); } } void _fadeInTrack(SoundTrack track, double duration) { _backgroundMusicTrack = track; if (duration == 0.0) { _trackPlayer.play(track); } else { _trackPlayer.play(track, true, 0.0); actions.run(new ActionTween((a) => track.volume = a, 0.0, 1.0, duration)); } } void stopBackgroundMusic([double fadeDuration = 0.0]) { if (fadeDuration == 0.0) { _trackPlayer.stop(_backgroundMusicTrack); } else { ActionTween fadeOut = new ActionTween( (a) => _backgroundMusicTrack.volume = a, _backgroundMusicTrack.volume, 0.0, fadeDuration); ActionCallFunction stopCall = new ActionCallFunction(() { _trackPlayer.stop(_backgroundMusicTrack); }); ActionSequence seq = new ActionSequence([fadeOut, stopCall]); actions.run(seq); } _backgroundMusicTrack = null; } void _update(Timer timer) { int delta = 0; int timestamp = new DateTime.now().millisecondsSinceEpoch; if (_lastTimeStamp != null) { delta = timestamp - _lastTimeStamp; } _lastTimeStamp = timestamp; actions.step(delta / 1000.0); } }