249 lines
6.7 KiB
Java
249 lines
6.7 KiB
Java
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<MultiClip> ALL_MULTICLIPS = new LinkedList<MultiClip>();
|
|
|
|
/** 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<Clip> clips = new LinkedList<Clip>();
|
|
|
|
/** 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
|
|
* @throws IOException if an input or output error occurs
|
|
* @throws LineUnavailableException if a clip object is not available or
|
|
* if the line cannot be opened due to resource restrictions
|
|
*/
|
|
public MultiClip(String name, AudioInputStream audioIn) throws IOException, LineUnavailableException {
|
|
this.name = name;
|
|
this.audioIn = audioIn;
|
|
if (audioIn != null) {
|
|
format = audioIn.getFormat();
|
|
|
|
LinkedList<byte[]> allBufs = new LinkedList<byte[]>();
|
|
|
|
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
|
|
* @throws LineUnavailableException if a clip object is not available or
|
|
* if the line cannot be opened due to resource restrictions
|
|
*/
|
|
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
|
|
* @throws LineUnavailableException if a clip object is not available or
|
|
* if the line cannot be opened due to resource restrictions
|
|
*/
|
|
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<Clip> 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<Clip>();
|
|
}
|
|
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<Clip> clipsToClose = new LinkedList<Clip>();
|
|
for (MultiClip mc : MultiClip.ALL_MULTICLIPS) {
|
|
for (Iterator<Clip> 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;
|
|
}
|
|
|
|
/**
|
|
* Mute the Clip (because destroying it, won't stop it)
|
|
*/
|
|
public void mute() {
|
|
try {
|
|
((FloatControl) getClip().getControl(FloatControl.Type.MASTER_GAIN)).setValue((float) (Math.log(Float.MIN_VALUE) / Math.log(10.0) * 20.0));
|
|
} catch (Exception e) {
|
|
}
|
|
}
|
|
}
|