package de.lessvoid.nifty.sound.openal; import de.lessvoid.nifty.sound.openal.slick.Audio; import de.lessvoid.nifty.sound.openal.slick.AudioImpl; import de.lessvoid.nifty.sound.openal.slick.SoundStore; import de.lessvoid.nifty.tools.resourceloader.NiftyResourceLoader; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.net.URL; import java.util.ArrayList; import java.util.logging.Logger; /** * A piece of music loaded and playable within the game. Only one piece of music can * play at any given time and a channel is reserved so music will always play. * * @author kevin * @author Nathan Sweet <misc@n4te.com> */ public class Music { private final Logger log = Logger.getLogger(Music.class.getName()); /** * The music currently being played or null if none */ @Nullable private static Music currentMusic; /** * Poll the state of the current music. This causes streaming music * to stream and checks listeners. Note that if you're using a game container * this will be auto-magically called for you. * * @param delta The amount of time since last poll */ public static void poll(int delta) { if (currentMusic != null) { SoundStore.get().poll(delta); if (!SoundStore.get().isMusicPlaying()) { if (!currentMusic.positioning) { Music oldMusic = currentMusic; currentMusic = null; oldMusic.fireMusicEnded(); } } else { currentMusic.update(delta); } } } /** * The sound from FECK representing this music */ private Audio sound; /** * True if the music is playing */ private boolean playing; /** * The list of listeners waiting for notification that the music ended */ @Nonnull private final ArrayList<MusicListener> listeners = new ArrayList<MusicListener>(); /** * The volume of this music */ private float volume = 1.0f; /** * Start gain for fading in/out */ private float fadeStartGain; /** * End gain for fading in/out */ private float fadeEndGain; /** * Countdown for fading in/out */ private int fadeTime; /** * Duration for fading in/out */ private int fadeDuration; /** * True if music should be stopped after fading in/out */ private boolean stopAfterFade; /** * True if the music is being repositioned and it is therefore normal that it's not playing */ private boolean positioning; /** * The position that was requested */ private float requiredPosition = -1; /** * Create and load a piece of music (either OGG or MOD/XM) * * @param ref The location of the music * @throws SlickException */ public Music(@Nonnull String ref, @Nonnull final NiftyResourceLoader resourceLoader) throws Exception { this(ref, false, resourceLoader); } /** * Create and load a piece of music (either OGG or MOD/XM) * * @param ref The location of the music * @throws SlickException */ public Music(@Nonnull URL ref, final NiftyResourceLoader resourceLoader) throws Exception { this(ref, false, resourceLoader); } /** * Create and load a piece of music (either OGG or MOD/XM) * * @param url The location of the music * @param streamingHint A hint to indicate whether streaming should be used if possible * @throws SlickException */ public Music(@Nonnull URL url, boolean streamingHint, final NiftyResourceLoader resourceLoader) throws Exception { SoundStore.get().init(); String ref = url.getFile(); try { if (ref.toLowerCase().endsWith(".ogg")) { if (streamingHint) { sound = SoundStore.get().getOggStream(url, resourceLoader); } else { sound = SoundStore.get().getOgg(url.openStream()); } } else if (ref.toLowerCase().endsWith(".wav")) { sound = SoundStore.get().getWAV(url.openStream()); } else if (ref.toLowerCase().endsWith(".xm") || ref.toLowerCase().endsWith(".mod")) { sound = SoundStore.get().getMOD(url.openStream()); } else if (ref.toLowerCase().endsWith(".aif") || ref.toLowerCase().endsWith(".aiff")) { sound = SoundStore.get().getAIF(url.openStream()); } else { throw new Exception("Only .xm, .mod, .ogg, and .aif/f are currently supported."); } } catch (Exception e) { log.warning(e.toString()); throw new Exception("Failed to load sound: " + url); } } /** * Create and load a piece of music (either OGG or MOD/XM) * * @param ref The location of the music * @param streamingHint A hint to indicate whether streaming should be used if possible * @throws SlickException */ public Music( @Nonnull String ref, boolean streamingHint, @Nonnull final NiftyResourceLoader resourceLoader) throws Exception { SoundStore.get().init(); try { if (ref.toLowerCase().endsWith(".ogg")) { if (streamingHint) { sound = SoundStore.get().getOggStream(ref, resourceLoader); } else { sound = SoundStore.get().getOgg(ref, resourceLoader); } } else if (ref.toLowerCase().endsWith(".wav")) { sound = SoundStore.get().getWAV(ref, resourceLoader); } else if (ref.toLowerCase().endsWith(".xm") || ref.toLowerCase().endsWith(".mod")) { sound = SoundStore.get().getMOD(ref, resourceLoader); } else if (ref.toLowerCase().endsWith(".aif") || ref.toLowerCase().endsWith(".aiff")) { sound = SoundStore.get().getAIF(ref, resourceLoader); } else { throw new Exception("Only .xm, .mod, .ogg, and .aif/f are currently supported."); } } catch (Exception e) { log.warning(e.toString()); throw new Exception("Failed to load sound: " + ref); } } /** * Add a listener to this music * * @param listener The listener to add */ public void addListener(MusicListener listener) { listeners.add(listener); } /** * Remove a listener from this music * * @param listener The listener to remove */ public void removeListener(MusicListener listener) { listeners.remove(listener); } /** * Fire notifications that this music ended */ private void fireMusicEnded() { playing = false; for (int i = 0; i < listeners.size(); i++) { listeners.get(i).musicEnded(this); } } /** * Fire notifications that this music was swapped out * * @param newMusic The new music that will be played */ private void fireMusicSwapped(Music newMusic) { playing = false; for (int i = 0; i < listeners.size(); i++) { listeners.get(i).musicSwapped(this, newMusic); } } /** * Loop the music */ public void loop() { loop(1.0f, 1.0f); } /** * Play the music */ public void play() { play(1.0f, 1.0f); } /** * Play the music at a given pitch and volume * * @param pitch The pitch to play the music at (1.0 = default) * @param volume The volume to play the music at (1.0 = default) */ public void play(float pitch, float volume) { startMusic(pitch, volume, false); } /** * Loop the music at a given pitch and volume * * @param pitch The pitch to play the music at (1.0 = default) * @param volume The volume to play the music at (1.0 = default) */ public void loop(float pitch, float volume) { startMusic(pitch, volume, true); } /** * play or loop the music at a given pitch and volume * * @param pitch The pitch to play the music at (1.0 = default) * @param volume The volume to play the music at (1.0 = default) * @param loop if false the music is played once, the music is looped otherwise */ private void startMusic(float pitch, float volume, boolean loop) { if (currentMusic != null) { currentMusic.stop(); currentMusic.fireMusicSwapped(this); } currentMusic = this; if (volume < 0.0f) { volume = 0.0f; } if (volume > 1.0f) { volume = 1.0f; } sound.playAsMusic(pitch, volume, loop); playing = true; setVolume(volume); if (requiredPosition != -1) { setPosition(requiredPosition); } } /** * Pause the music playback */ public void pause() { playing = false; AudioImpl.pauseMusic(); } /** * Stop the music playing */ public void stop() { sound.stop(); } /** * Resume the music playback */ public void resume() { playing = true; AudioImpl.restartMusic(); } /** * Check if the music is being played * * @return True if the music is being played */ public boolean playing() { return (currentMusic == this) && (playing); } /** * Set the volume of the music as a factor of the global volume setting * * @param volume The volume to play music at. 0 - 1, 1 is Max */ public void setVolume(float volume) { // Bounds check if (volume > 1) { volume = 1; } else if (volume < 0) { volume = 0; } this.volume = volume; // This sound is being played as music if (currentMusic == this) { SoundStore.get().setCurrentMusicVolume(volume); } } /** * Get the individual volume of the music * * @return The volume of this music, still effected by global SoundStore volume. 0 - 1, 1 is Max */ public float getVolume() { return volume; } /** * Fade this music to the volume specified * * @param duration Fade time in milliseconds. * @param endVolume The target volume * @param stopAfterFade True if music should be stopped after fading in/out */ public void fade(int duration, float endVolume, boolean stopAfterFade) { this.stopAfterFade = stopAfterFade; fadeStartGain = volume; fadeEndGain = endVolume; fadeDuration = duration; fadeTime = duration; } /** * Update the current music applying any effects that need to updated per * tick. * * @param delta The amount of time in milliseconds thats passed since last update */ void update(int delta) { if (!playing) { return; } if (fadeTime > 0) { fadeTime -= delta; if (fadeTime < 0) { fadeTime = 0; if (stopAfterFade) { stop(); return; } } float offset = (fadeEndGain - fadeStartGain) * (1 - (fadeTime / (float) fadeDuration)); setVolume(fadeStartGain + offset); } } /** * Seeks to a position in the music. For streaming music, seeking before the current position causes * the stream to be reloaded. * * @param position Position in seconds. * @return True if the seek was successful */ public boolean setPosition(float position) { if (playing) { requiredPosition = -1; positioning = true; playing = false; boolean result = sound.setPosition(position); playing = true; positioning = false; return result; } else { requiredPosition = position; return false; } } /** * The position into the sound thats being played * * @return The current position in seconds. */ public float getPosition() { return sound.getPosition(); } }