package org.erikaredmark.monkeyshines.resource;
import java.beans.PropertyChangeEvent;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.sound.sampled.Clip;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineEvent.Type;
import javax.sound.sampled.LineListener;
import org.erikaredmark.monkeyshines.GameSoundEffect;
import org.erikaredmark.monkeyshines.global.SoundSettings;
import org.erikaredmark.monkeyshines.global.SoundUtils;
import com.google.common.base.Optional;
/**
*
* Provided to all game objects capable of producing sounds; provides methods to indicate to the sound manager
* to play certain sounds.
* <p/>
* The sound played, if at all, depends on the {@code WorldResource} used to construct this manager.
* <p/>
* Methods that change certain properties, such as volume, of a given sound effect, are stateful. All future
* calls to that specific sound effect will use the previously selected properties.
*
* @author Erika Redmark
*
*/
public final class JavaDefaultSoundManager implements SoundManager {
// Use as source of sounds
private final WorldResource rsrc;
private boolean musicOff;
// Set true if music is switched off by volume whilst in the middle of playing.
private boolean musicCut;
private boolean soundOff;
// Intended for playing sounds after a delayed period of time.
private final ScheduledExecutorService delaySound = Executors.newSingleThreadScheduledExecutor();
// Created by WorldResource ONLY. That also handles registering/unregistering it from listening to the
// SoundSettings global.
public JavaDefaultSoundManager(final WorldResource rsrc) {
this.rsrc = rsrc;
setMusicVolume(SoundSettings.getMusicVolumePercent() );
setSoundVolume(SoundSettings.getSoundVolumePercent() );
}
@Override public void playOnce(GameSoundEffect effect) {
if (soundOff) return;
Optional<Clip> clip = rsrc.getSoundFor(effect);
if (clip.isPresent() ) {
Clip c = clip.get();
if (c.isActive() ) c.stop();
c.setFramePosition(0);
c.start();
}
}
@Override public void playOnceDelayed(
final GameSoundEffect effect,
final int delay,
final TimeUnit unit) {
if (soundOff) return;
rsrc.holdSound(effect);
delaySound.schedule(
new Runnable() {
@Override public void run() {
playOnce(effect);
// Block this scheduled thread until sound is over
Optional<Clip> clip = rsrc.getSoundFor(effect);
if (clip.isPresent() ) {
clip.get().addLineListener(new LineListener() {
@Override public void update(LineEvent event) {
if (event.getType() == Type.STOP) {
rsrc.releaseSound(effect);
}
}
});
}
}
},
delay,
unit
);
}
/* (non-Javadoc)
* @see org.erikaredmark.monkeyshines.resource.SoundManager#playMusic()
*/
@Override public void playMusic() {
if (rsrc.backgroundMusic.isPresent() ) {
Clip mus = rsrc.backgroundMusic.get();
if (mus.isActive() ) return;
if (musicOff) return;
mus.setFramePosition(0);
mus.loop(Clip.LOOP_CONTINUOUSLY);
}
}
/* (non-Javadoc)
* @see org.erikaredmark.monkeyshines.resource.SoundManager#stopPlayingMusic()
*/
@Override public void stopPlayingMusic() {
if (rsrc.backgroundMusic.isPresent() ) {
Clip mus = rsrc.backgroundMusic.get();
if (mus.isActive() ) {
mus.stop();
}
}
}
/**
* Automatically called on construction and game setting change to match clip volume to
* user defined levels. Does nothing if there is no background music
*
* @param value
* percentage to set music volume to
*/
private void setMusicVolume(int value) {
if (rsrc.backgroundMusic.isPresent() ) {
Clip mus = rsrc.backgroundMusic.get();
if (value == 0) {
musicOff = true;
// unlike sounds, music must manually be shut off, and then back on again if required.
if (mus.isRunning() ) {
musicCut = true;
mus.stop();
}
return;
} else {
// if the music was previously cut because it was already running, then and only then do
// we resume it.
if (musicCut) {
musicCut = false;
mus.start();
}
}
if (rsrc.backgroundMusic.isPresent() ) {
musicOff = false;
FloatControl gainControl = (FloatControl) rsrc.
backgroundMusic.
get().
getControl(FloatControl.Type.MASTER_GAIN);
float decibelLevelOffset = SoundUtils.resolveDecibelOffsetFromPercentage(value);
// Music seems to be naturally louder than sound effects, so give it a negative nudge.
decibelLevelOffset -= 10;
System.out.println("Decibel offset for music: " + decibelLevelOffset);
gainControl.setValue(decibelLevelOffset);
} else {
musicOff = true;
}
}
}
/**
*
* Automatically called on construction and game setting change to match clip volume to
* user defined levels.
*
* @param value
* percentage to set music volume to
*
*/
private void setSoundVolume(int value) {
if (value == 0) {
soundOff = true;
return;
}
soundOff = false;
float decibelLevelOffset = SoundUtils.resolveDecibelOffsetFromPercentage(value);
System.out.println("Decibel offset for sound: " + decibelLevelOffset);
for (GameSoundEffect effect : GameSoundEffect.values() ) {
Optional<Clip> clip = rsrc.getSoundFor(effect);
if (clip.isPresent() ) {
FloatControl gainControl = (FloatControl)
clip.get().getControl(FloatControl.Type.MASTER_GAIN);
gainControl.setValue(decibelLevelOffset);
}
}
}
/*
* Handles property change events from the settings preferences, whenever the user modifies a sound setting.
*/
@Override public void propertyChange(PropertyChangeEvent event) {
switch (event.getPropertyName() ) {
case SoundSettings.PROPERTY_MUSIC:
setMusicVolume(SoundSettings.getMusicVolumePercent() );
break;
case SoundSettings.PROPERTY_SOUND:
setSoundVolume(SoundSettings.getSoundVolumePercent() );
break;
default:
throw new RuntimeException("Unknown sound manager observer property " + event.getPropertyName() );
}
}
}