package com.indyforge.twod.engine.sound; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.Clip; import javax.sound.sampled.DataLine; import javax.sound.sampled.FloatControl; import javax.sound.sampled.LineEvent; import javax.sound.sampled.LineEvent.Type; import javax.sound.sampled.LineListener; import com.indyforge.twod.engine.graphics.rendering.scenegraph.math.MathExt; import com.indyforge.twod.engine.resources.assets.Asset; import com.indyforge.twod.engine.resources.assets.AssetManager; /** * This class manages/plays game sounds with the Java Sound API. * * @author Matthias Hesse/MrBumble */ public final class SoundManager implements Serializable { /* * This set must be synchroniced because the clips usually are played and * stopped in different threads. */ private static final Set<Clip> currentSounds = new LinkedHashSet<Clip>(); /* * The mute function. */ private static boolean mute = false; /** * Sets mute. * * @param mute * If true no further sounds will be played. */ public static void mute(boolean mute) { if (SoundManager.mute = mute) { closeCurrentSounds(); } } /** * @return the mute flag. */ public static boolean isMute() { return mute; } /** * Closes all active sounds. */ public static void closeCurrentSounds() { for (Clip clip : currentSounds()) { clip.close(); } } /** * @return a set which contains all active clips. */ public static Set<Clip> currentSounds() { synchronized (currentSounds) { return new HashSet<Clip>(currentSounds); } } /** * */ private static final long serialVersionUID = 1L; /* * The asset manager of this sound manager. */ private final AssetManager assetManager; /* * This map maps strings to byte arrays. The byte arrays contain the binary * sound data. When starting a sound these byte arrays are used to create * new clips. */ private final Map<String, Asset<byte[]>> soundMap = new HashMap<String, Asset<byte[]>>(); /* * Used to create / start the clips. */ private transient ExecutorService soundExecutor; /* * The default volume. */ private float defaultVolume = 0.4f; /** * Creates the current sounds other stuff. */ private void initSoundManager() { /* * Use deamon threads. */ soundExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { private final ThreadFactory defaultThreadFactory = Executors .defaultThreadFactory(); @Override public Thread newThread(Runnable r) { Thread thread = defaultThreadFactory.newThread(r); thread.setDaemon(true); return thread; } }); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { // Restore all vars in.defaultReadObject(); if (!AssetManager.isHeadless()) { // Init initSoundManager(); } } public SoundManager(AssetManager assetManager) { if (assetManager == null) { throw new NullPointerException("assetManager"); } this.assetManager = assetManager; if (!AssetManager.isHeadless()) { // Init initSoundManager(); } } /** * @return the asset manager; */ public AssetManager assetManager() { return assetManager; } /** * @return the default volume. */ public float defaultVolume() { return defaultVolume; } /** * * @param defaultVolume * The default volume. * @return this for chaining. */ public SoundManager defaultVolume(float defaultVolume) { this.defaultVolume = defaultVolume; return this; } /** * Uses the given input stream to read the complete sound data and puts it * into the map using the given name. * * @param name * The name of the sound. * @param assetPath * The asset path of the sound. * @return the previous value or null. * @throws Exception * If an exception occurs. */ public Asset<byte[]> putSound(String name, String assetPath) throws Exception { if (name == null) { throw new NullPointerException("name"); } else if (assetPath == null) { throw new NullPointerException("assetPath"); } // Reads the complete stream and puts it into the map return soundMap.put(name, assetManager.loadBytes(assetPath, false)); } /** * Creates a new clip using the given sound data and the * {@link SoundManager#defaultVolume(float) default volume}. * * @param name * The name of the sound. * @param oneshot * If true the sound will be played a single time, otherwise it * will be looped forever. * @return the future which will retrieve the new clip or null (If sound * does not exist, or headless mode, or...) */ public Future<Clip> playSound(final String name, final boolean oneshot) { return playSound(name, defaultVolume, oneshot); } /** * Creates a new clip using the given sound data. * * @param name * The name of the sound. * @param oneshot * If true the sound will be played a single time, otherwise it * will be looped forever. * @param volume * The volume between 0.0f and 1.0f. * @return the future which will retrieve the new clip or null (If sound * does not exist, or headless mode, or...) */ public Future<Clip> playSound(final String name, final float volume, final boolean oneshot) { // No executor ? if (soundExecutor == null || mute) { return null; } /* * Start the new sound in a thread pool. */ return soundExecutor.submit(new Callable<Clip>() { /* * (non-Javadoc) * * @see java.util.concurrent.Callable#call() */ @Override public Clip call() throws Exception { // Get the sound data Asset<byte[]> soundAsset = soundMap.get(name); // Check for null if (soundAsset == null) { return null; } try { // Create new audio input stream AudioInputStream ais = AudioSystem .getAudioInputStream(new ByteArrayInputStream( soundAsset.get())); // Get line (using clip interface) DataLine.Info info = new DataLine.Info(Clip.class, ais .getFormat()); // Create a new clip final Clip clip = (Clip) AudioSystem.getLine(info); // Open with binary data clip.open(ais); // Get the gain control FloatControl gainControl = (FloatControl) clip .getControl(FloatControl.Type.MASTER_GAIN); // Set the volume gainControl.setValue((float) (Math.log(MathExt.clamp( volume, 0, 1)) / Math.log(10.0) * 20.0)); // Add into set synchronized (currentSounds) { currentSounds.add(clip); } /* * Very important: * * This listener stops/removes the created clip. */ clip.addLineListener(new LineListener() { @Override public void update(LineEvent event) { if (event.getType() == Type.STOP) { // If the line is stopped -> close it! event.getLine().close(); } else if (event.getType() == Type.CLOSE) { // If the line is closed -> remove it! synchronized (currentSounds) { currentSounds.remove(clip); } } } }); // Start ! if (oneshot) { clip.start(); } else { clip.loop(Clip.LOOP_CONTINUOUSLY); } return clip; } catch (Exception e) { System.err .println("Failed to create & start sound. Reason: " + e.getMessage()); return null; } } }); } /** * Clears all sound mappings. */ public void clearSounds() { soundMap.clear(); } /** * Removes the mapping with the given name. * * @param name * The name of the mapping you want to remove. * @return the old value or null. */ public Asset<byte[]> removeSound(String name) { return soundMap.remove(name); } /** * @param name * The name of the sound. * @return the binary byte array which contains the sound data of the given * name or null. */ public Asset<byte[]> soundData(String name) { return soundMap.get(name); } }