package com.github.czyzby.lml.uedi.music; import com.badlogic.gdx.audio.Music; import com.badlogic.gdx.audio.Music.OnCompletionListener; import com.badlogic.gdx.audio.Sound; import com.badlogic.gdx.math.Interpolation; import com.badlogic.gdx.scenes.scene2d.Stage; import com.badlogic.gdx.scenes.scene2d.actions.Actions; import com.badlogic.gdx.scenes.scene2d.ui.Button; import com.badlogic.gdx.scenes.scene2d.ui.ProgressBar; import com.badlogic.gdx.utils.Array; import com.github.czyzby.kiwi.util.gdx.collection.GdxArrays; import com.github.czyzby.kiwi.util.gdx.preference.ApplicationPreferences; import com.github.czyzby.lml.parser.LmlData; import com.github.czyzby.lml.parser.action.ActorConsumer; import com.github.czyzby.uedi.stereotype.Singleton; /** Allows to manage music and sounds settings. Eases playing of {@link Music} and {@link Sound} instances. * * @author MJ */ public class MusicService implements Singleton { private final Stage stage; private final MusicOnPreference musicOn; private final SoundOnPreference soundOn; private final MusicVolumePreference musicVolume; private final SoundVolumePreference soundVolume; // Music utilities: private Music currentTheme; private final float duration = 0.5f; private final Array<Music> themes = GdxArrays.newArray(); private final OnCompletionListener listener = new OnCompletionListener() { @Override public void onCompletion(final Music music) { music.setOnCompletionListener(null); if (music == currentTheme) { changeTheme(); } } }; /** @param stage will be used to add music transition actions. * @param musicOn decides whether music is on. * @param soundOn decides whether sounds are on. * @param musicVolume decides how loud the music is played. * @param soundVolume decides how loud the sounds are played. */ public MusicService(final Stage stage, final MusicOnPreference musicOn, final SoundOnPreference soundOn, final MusicVolumePreference musicVolume, final SoundVolumePreference soundVolume) { this.stage = stage; this.musicOn = musicOn; this.soundOn = soundOn; this.musicVolume = musicVolume; this.soundVolume = soundVolume; } /** @param data will include a few useful {@link ActorConsumer} instances that allow to set up current music and * sound preferences. */ public void addDefaultActions(final LmlData data) { // Allows to query current music settings in LML templates. data.addActorConsumer("isMusicOn", new ActorConsumer<Boolean, Object>() { @Override public Boolean consume(final Object actor) { return isMusicOn(); } }); data.addActorConsumer("isSoundOn", new ActorConsumer<Boolean, Object>() { @Override public Boolean consume(final Object actor) { return isSoundOn(); } }); data.addActorConsumer("getMusicVolume", new ActorConsumer<Float, Object>() { @Override public Float consume(final Object actor) { return getMusicVolume(); } }); data.addActorConsumer("getSoundVolume", new ActorConsumer<Float, Object>() { @Override public Float consume(final Object actor) { return getSoundVolume(); } }); // Allows to turn the music on and off. data.addActorConsumer("setMusicOn", new ActorConsumer<Void, Button>() { @Override public Void consume(final Button actor) { setMusicOn(actor.isChecked()); return null; } }); // Allows to turn the sounds on and off. data.addActorConsumer("setSoundOn", new ActorConsumer<Void, Button>() { @Override public Void consume(final Button actor) { setSoundOn(actor.isChecked()); return null; } }); // Changes current music volume (aimed at sliders): data.addActorConsumer("setMusicVolume", new ActorConsumer<Void, ProgressBar>() { @Override public Void consume(final ProgressBar actor) { setMusicVolume(actor.getValue()); return null; } }); // Changes current sounds volume (aimed at sliders): data.addActorConsumer("setSoundVolume", new ActorConsumer<Void, ProgressBar>() { @Override public Void consume(final ProgressBar actor) { setSoundVolume(actor.getValue()); return null; } }); } /** @return true if sounds are currently on. */ public boolean isSoundOn() { return soundOn.isOn(); } /** @param on true to turn on, false to turn off. */ public void setSoundOn(final boolean on) { soundOn.setOn(on); } /** @return current sound volume in range of [0, 1]. */ public float getSoundVolume() { return soundVolume.getPercent(); } /** @param volume should be in range of [0, 1]. */ public void setSoundVolume(final float volume) { soundVolume.setPercent(volume); } /** @param sound will be played with the current volume setting if sounds are turned on. * @return the id of the sound instance if successful, -1 on failure or if sounds are turned off. */ public long play(final Sound sound) { if (soundOn.isOn()) { return sound.play(soundVolume.getPercent()); } return -1L; } /** @param sound will be played if sounds are turned on. * @param volume the volume in the range of [0,1]. Will multiply current sound volume setting. * @return the id of the sound instance if successful, -1 on failure or if sounds are turned off. */ public long play(final Sound sound, final float volume) { if (soundOn.isOn()) { return sound.play(soundVolume.getPercent() * volume); } return -1L; } /** @param sound will be played if sounds are turned on. * @param volume the volume in the range of [0,1]. Will multiply current sound volume setting. * @param pitch the pitch multiplier. 1 stands for default, larger than 1 is faster, smaller than 1 is slower, the * value should be between 0.5 and 2.0. * @return the id of the sound instance if successful, -1 on failure or if sounds are turned off. */ public long play(final Sound sound, final float volume, final float pitch) { return play(sound, volume, pitch, 0f); } /** @param sound will be played if sounds are turned on. * @param volume the volume in the range of [0,1]. Will multiply current sound volume setting. * @param pitch the pitch multiplier. 1 stands for default, larger than 1 is faster, smaller than 1 is slower, the * value should be between 0.5 and 2.0. * @param pan panning in the range -1 (full left) to 1 (full right). 0 is the center. * @return the id of the sound instance if successful, -1 on failure or if sounds are turned off. */ public long play(final Sound sound, final float volume, final float pitch, final float pan) { if (soundOn.isOn()) { return sound.play(soundVolume.getPercent() * volume, pitch, pan); } return -1L; } /** @return true if music are currently on. */ public boolean isMusicOn() { return musicOn.isOn(); } /** @param on true to turn on, false to turn off. */ public void setMusicOn(final boolean on) { musicOn.setOn(on); if (on) { currentTheme = getNextTheme(null); fadeInCurrentTheme(); } else { fadeOutCurrentTheme(); } } /** @return current music volume in range of [0, 1]. */ public float getMusicVolume() { return musicVolume.getPercent(); } /** @param volume should be in range of [0, 1]. */ public void setMusicVolume(final float volume) { musicVolume.setPercent(volume); if (currentTheme != null) { currentTheme.setVolume(volume); } } /** @param music is music is turned on, will be smoothly started and played. Replaces current music theme, if any. * Will be played once with the current volume setting. */ public void play(final Music music) { if (musicOn.isOn()) { fadeOutCurrentTheme(); currentTheme = music; fadeInCurrentTheme(); } } /** Smoothly fades out current theme. */ protected void fadeOutCurrentTheme() { if (currentTheme != null && currentTheme.isPlaying()) { currentTheme.setOnCompletionListener(null); stage.addAction(Actions.sequence( VolumeAction.setVolume(currentTheme, currentTheme.getVolume(), 0f, duration, Interpolation.fade), MusicStopAction.stop(currentTheme))); } currentTheme = null; } /** Smoothly fades in current theme. */ protected void fadeInCurrentTheme() { if (currentTheme != null) { currentTheme.setLooping(false); currentTheme.setVolume(0f); currentTheme.setOnCompletionListener(listener); stage.addAction( VolumeAction.setVolume(currentTheme, 0f, musicVolume.getPercent(), duration, Interpolation.fade)); currentTheme.play(); } } /** Forces a change of current theme. Assumes a theme was already played - there are no smooth fade ins of any * kind. */ protected void changeTheme() { currentTheme = getNextTheme(currentTheme); if (currentTheme != null) { currentTheme.setLooping(false); currentTheme.setOnCompletionListener(listener); currentTheme.setVolume(musicVolume.getPercent()); currentTheme.play(); } } /** @param previous was played previously. * @return the new music theme. */ protected Music getNextTheme(final Music previous) { return themes.size == 1 ? themes.first() : themes.random(); } /** @param theme will be added to the current themes collection, which are constantly played on a loop. By default, * themes order is random. If no theme is currently played, this music instance will start playing * smoothly. */ public void addTheme(final Music theme) { if (!themes.contains(theme, true)) { themes.add(theme); } if (currentTheme == null && musicOn.isOn()) { currentTheme = theme; fadeInCurrentTheme(); } } /** @param themes will be added to the current themes collection, which are constantly played on a loop. By default, * themes order is random. If no theme is currently played, one of the passed music instances will start * playing smoothly. */ public void addThemes(final Music... themes) { for (final Music theme : themes) { if (!this.themes.contains(theme, true)) { this.themes.add(theme); } } if (currentTheme == null && musicOn.isOn()) { currentTheme = getNextTheme(null); fadeInCurrentTheme(); } } /** @param themes will be added to the current themes collection, which are constantly played on a loop. By default, * themes order is random. If no theme is currently played, one of the passed music instances will start * playing smoothly. */ public void addThemes(final Iterable<Music> themes) { for (final Music theme : themes) { if (!this.themes.contains(theme, true)) { this.themes.add(theme); } } if (currentTheme == null && musicOn.isOn()) { currentTheme = getNextTheme(null); fadeInCurrentTheme(); } } /** @param theme will be removed from the current themes collection. If is currently played, will be smoothly * stopped and replaced with another theme (if any are left). */ public void removeTheme(final Music theme) { themes.removeValue(theme, true); if (currentTheme == theme) { fadeOutCurrentTheme(); currentTheme = getNextTheme(theme); fadeInCurrentTheme(); } } /** @param themes will be removed from the current themes collection. If one of them is currently played, it will be * smoothly stopped and replaced with another theme (if any are left). */ public void removeThemes(final Music... themes) { boolean removeCurrent = false; for (final Music theme : themes) { this.themes.removeValue(theme, true); removeCurrent |= currentTheme == theme; } if (removeCurrent) { final Music nextTheme = getNextTheme(currentTheme); fadeOutCurrentTheme(); currentTheme = nextTheme; fadeInCurrentTheme(); } } /** @param themes will be removed from the current themes collection. If one of them is currently played, it will be * smoothly stopped and replaced with another theme (if any are left). */ public void removeThemes(final Iterable<Music> themes) { boolean removeCurrent = false; for (final Music theme : themes) { this.themes.removeValue(theme, true); removeCurrent |= currentTheme == theme; } if (removeCurrent) { final Music nextTheme = getNextTheme(currentTheme); fadeOutCurrentTheme(); currentTheme = nextTheme; fadeInCurrentTheme(); } } /** Removes all themes. Stops currently played music instance. */ public void clearThemes() { themes.clear(); fadeOutCurrentTheme(); } /** Saves music preferences. Will flush the global preferences: any other settings will be permanently saved. */ public void savePreferences() { ApplicationPreferences.getPreferences().flush(); } }