package itdelatrisu.opsu.audio; import itdelatrisu.opsu.ErrorHandler; import java.io.IOException; import java.util.Iterator; import java.util.LinkedList; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.Clip; import javax.sound.sampled.FloatControl; import javax.sound.sampled.LineListener; import javax.sound.sampled.LineUnavailableException; /** * Extension of Clip that allows playing multiple copies of a Clip simultaneously. * http://stackoverflow.com/questions/1854616/ * * @author fluddokt (https://github.com/fluddokt) */ public class MultiClip { /** Maximum number of extra clips that can be created at one time. */ private static final int MAX_CLIPS = 20; /** A list of all created MultiClips. */ private static final LinkedList ALL_MULTICLIPS = new LinkedList(); /** Size of a single buffer. */ private static final int BUFFER_SIZE = 0x1000; /** Current number of extra clips created. */ private static int extraClips = 0; /** Current number of clip-closing threads in execution. */ private static int closingThreads = 0; /** A list of clips used for this audio sample. */ private LinkedList clips = new LinkedList(); /** The audio input stream. */ private AudioInputStream audioIn; /** The format of this audio sample. */ private AudioFormat format; /** The data for this audio sample. */ private byte[] audioData; /** The name given to this clip. */ private final String name; /** * Constructor. * @param name the clip name * @param audioIn the associated AudioInputStream */ public MultiClip(String name, AudioInputStream audioIn) throws IOException, LineUnavailableException { this.name = name; this.audioIn = audioIn; if (audioIn != null) { format = audioIn.getFormat(); LinkedList allBufs = new LinkedList(); int totalRead = 0; boolean hasData = true; while (hasData) { totalRead = 0; byte[] tbuf = new byte[BUFFER_SIZE]; while (totalRead < tbuf.length) { int read = audioIn.read(tbuf, totalRead, tbuf.length - totalRead); if (read < 0) { hasData = false; break; } totalRead += read; } allBufs.add(tbuf); } audioData = new byte[(allBufs.size() - 1) * BUFFER_SIZE + totalRead]; int cnt = 0; for (byte[] tbuf : allBufs) { int size = BUFFER_SIZE; if (cnt == allBufs.size() - 1) size = totalRead; System.arraycopy(tbuf, 0, audioData, BUFFER_SIZE * cnt, size); cnt++; } } getClip(); ALL_MULTICLIPS.add(this); } /** * Returns the name of the clip. * @return the name */ public String getName() { return name; } /** * Plays the clip with the specified volume. * @param volume the volume the play at * @param listener the line listener */ public void start(float volume, LineListener listener) throws LineUnavailableException { Clip clip = getClip(); if (clip == null) return; // PulseAudio does not support Master Gain if (clip.isControlSupported(FloatControl.Type.MASTER_GAIN)) { // set volume FloatControl gainControl = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN); float dB = (float) (Math.log(volume) / Math.log(10.0) * 20.0); gainControl.setValue(dB); } if (listener != null) clip.addLineListener(listener); clip.setFramePosition(0); clip.start(); } /** * Returns a Clip that is not playing from the list. * If no clip is available, then a new one is created if under MAX_CLIPS. * Otherwise, an existing clip will be returned. * @return the Clip to play */ private Clip getClip() throws LineUnavailableException { // TODO: // Occasionally, even when clips are being closed in a separate thread, // playing any clip will cause the game to hang until all clips are // closed. Why? if (closingThreads > 0) return null; // search for existing stopped clips for (Iterator iter = clips.iterator(); iter.hasNext();) { Clip c = iter.next(); if (!c.isRunning()) { iter.remove(); clips.add(c); return c; } } Clip c = null; if (extraClips >= MAX_CLIPS) { // use an existing clip if (clips.isEmpty()) return null; c = clips.removeFirst(); c.stop(); clips.add(c); } else { // create a new clip c = AudioSystem.getClip(); if (format != null) c.open(format, audioData, 0, audioData.length); clips.add(c); if (clips.size() != 1) extraClips++; } return c; } /** * Destroys the MultiClip and releases all resources. */ public void destroy() { if (clips.size() > 0) { for (Clip c : clips) { c.stop(); c.flush(); c.close(); } extraClips -= clips.size() - 1; clips = new LinkedList(); } audioData = null; if (audioIn != null) { try { audioIn.close(); } catch (IOException e) { ErrorHandler.error(String.format("Could not close AudioInputStream for MultiClip %s.", name), e, true); } } } /** * Destroys all extra clips. */ public static void destroyExtraClips() { if (extraClips == 0) return; // find all extra clips final LinkedList clipsToClose = new LinkedList(); for (MultiClip mc : MultiClip.ALL_MULTICLIPS) { for (Iterator iter = mc.clips.iterator(); iter.hasNext();) { Clip c = iter.next(); if (mc.clips.size() > 1) { // retain last Clip in list iter.remove(); clipsToClose.add(c); } } } // close clips in a new thread new Thread() { @Override public void run() { closingThreads++; for (Clip c : clipsToClose) { c.stop(); c.flush(); c.close(); } closingThreads--; } }.start(); // reset extra clip count extraClips = 0; } }