Merge pull request #25 from fluddokt/AudioTest

Allows multiple instance of the same clip to be played simultaneously.
This commit is contained in:
Jeffrey Han 2015-02-28 21:08:21 -05:00
commit 540b1c49f0
5 changed files with 230 additions and 41 deletions

View File

@ -20,8 +20,6 @@ package itdelatrisu.opsu.audio;
import java.util.HashMap; import java.util.HashMap;
import javax.sound.sampled.Clip;
/** /**
* Hit sounds. * Hit sounds.
*/ */
@ -79,7 +77,7 @@ public enum HitSound implements SoundController.SoundComponent {
private String filename; private String filename;
/** The Clip associated with the hit sound. */ /** The Clip associated with the hit sound. */
private HashMap<SampleSet, Clip> clips; private HashMap<SampleSet, MultiClip> clips;
/** Total number of hit sounds. */ /** Total number of hit sounds. */
public static final int SIZE = values().length; public static final int SIZE = values().length;
@ -90,7 +88,7 @@ public enum HitSound implements SoundController.SoundComponent {
*/ */
HitSound(String filename) { HitSound(String filename) {
this.filename = filename; this.filename = filename;
this.clips = new HashMap<SampleSet, Clip>(); this.clips = new HashMap<SampleSet, MultiClip>();
} }
/** /**
@ -100,7 +98,7 @@ public enum HitSound implements SoundController.SoundComponent {
public String getFileName() { return filename; } public String getFileName() { return filename; }
@Override @Override
public Clip getClip() { public MultiClip getClip() {
return (currentSampleSet != null) ? clips.get(currentSampleSet) : null; return (currentSampleSet != null) ? clips.get(currentSampleSet) : null;
} }
@ -109,7 +107,7 @@ public enum HitSound implements SoundController.SoundComponent {
* @param s the sample set * @param s the sample set
* @param clip the Clip * @param clip the Clip
*/ */
public void setClip(SampleSet s, Clip clip) { public void setClip(SampleSet s, MultiClip clip) {
clips.put(s, clip); clips.put(s, clip);
} }

View File

@ -0,0 +1,156 @@
package itdelatrisu.opsu.audio;
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.Clip;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
//http://stackoverflow.com/questions/1854616/in-java-how-can-i-play-the-same-audio-clip-multiple-times-simultaneously
public class MultiClip {
/** A list of clips used for this audio sample */
LinkedList<Clip> clips = new LinkedList<Clip>();
/** The format of this audio sample */
AudioFormat format;
/** The data for this audio sample */
byte[] audioData;
/** The name given to this clip */
String name;
/** Size of a single buffer */
final int BUFFER_SIZE = 0x1000;
static LinkedList<MultiClip> allMultiClips = new LinkedList<MultiClip>();
/** Constructor */
public MultiClip(String name, AudioInputStream audioIn) throws IOException, LineUnavailableException {
this.name = name;
if(audioIn != null){
format = audioIn.getFormat();
LinkedList<byte[]> allBufs = new LinkedList<byte[]>();
int readed = 0;
boolean hasData = true;
while (hasData) {
readed = 0;
byte[] tbuf = new byte[BUFFER_SIZE];
while (readed < tbuf.length) {
int read = audioIn.read(tbuf, readed, tbuf.length - readed);
if (read < 0) {
hasData = false;
break;
}
readed += read;
}
allBufs.add(tbuf);
}
audioData = new byte[(allBufs.size() - 1) * BUFFER_SIZE + readed];
int cnt = 0;
for (byte[] tbuf : allBufs) {
int size = BUFFER_SIZE;
if (cnt == allBufs.size() - 1) {
size = readed;
}
System.arraycopy(tbuf, 0, audioData, BUFFER_SIZE * cnt, size);
cnt++;
}
}
getClip();
allMultiClips.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
* @throws IOException
* @throws LineUnavailableException
*/
public void start(float volume) throws LineUnavailableException, IOException {
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);
}
clip.setFramePosition(0);
clip.start();
}
/**
* Returns a Clip that is not playing from the list
* if one is not available a new one is created if able
* @return the Clip
*/
private Clip getClip() throws LineUnavailableException, IOException{
for(Iterator<Clip> ita = clips.listIterator(); ita.hasNext(); ) {
Clip c = ita.next();
if(!c.isRunning()){
ita.remove();
clips.add(c);
return c;
}
}
Clip t = SoundController.newClip();
if(t == null){
if(clips.isEmpty()){
return null;
}
t = clips.removeFirst();
t.stop();
clips.add(t);
} else {
if (format != null)
t.open(format, audioData, 0, audioData.length);
clips.add(t);
}
return t;
}
/**
* Destroys all but one clip
*/
protected void destroyAllButOne(){
for(Iterator<Clip> ita = clips.listIterator(); ita.hasNext(); ) {
Clip c = ita.next();
if(clips.size()>1){
ita.remove();
SoundController.destroyClip(c);
}
}
}
/**
* Destroys all but one clip for all MultiClips
*/
protected static void destroyExtraClips() {
for(MultiClip mc : MultiClip.allMultiClips){
mc.destroyAllButOne();
}
}
}

View File

@ -27,13 +27,15 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.ListIterator;
import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem; import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip; import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine; import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException; import javax.sound.sampled.UnsupportedAudioFileException;
@ -50,7 +52,7 @@ public class SoundController {
* Returns the Clip associated with the sound component. * Returns the Clip associated with the sound component.
* @return the Clip * @return the Clip
*/ */
public Clip getClip(); public MultiClip getClip();
} }
/** Sample volume multiplier, from timing points [0, 1]. */ /** Sample volume multiplier, from timing points [0, 1]. */
@ -71,7 +73,7 @@ public class SoundController {
* @param isMP3 true if MP3, false if WAV * @param isMP3 true if MP3, false if WAV
* @return the loaded and opened clip * @return the loaded and opened clip
*/ */
private static Clip loadClip(String ref, boolean isMP3) { private static MultiClip loadClip(String ref, boolean isMP3) {
try { try {
URL url = ResourceLoader.getResource(ref); URL url = ResourceLoader.getResource(ref);
@ -79,7 +81,7 @@ public class SoundController {
InputStream in = url.openStream(); InputStream in = url.openStream();
if (in.available() == 0) { if (in.available() == 0) {
in.close(); in.close();
return AudioSystem.getClip(); return new MultiClip(ref, null);
} }
in.close(); in.close();
@ -97,12 +99,10 @@ public class SoundController {
audioIn = decodedAudioIn; audioIn = decodedAudioIn;
} }
DataLine.Info info = new DataLine.Info(Clip.class, format); DataLine.Info info = new DataLine.Info(Clip.class, format);
if (AudioSystem.isLineSupported(info)) { if(AudioSystem.isLineSupported(info)){
Clip clip = (Clip) AudioSystem.getLine(info); return new MultiClip(ref, audioIn);
clip.open(audioIn); }else{
return clip; //Try to find closest matching line
} else {
// try to find closest matching line
Clip clip = AudioSystem.getClip(); Clip clip = AudioSystem.getClip();
AudioFormat[] formats = ((DataLine.Info) clip.getLineInfo()).getFormats(); AudioFormat[] formats = ((DataLine.Info) clip.getLineInfo()).getFormats();
int bestIndex = -1; int bestIndex = -1;
@ -149,11 +149,10 @@ public class SoundController {
break; break;
} }
if (bestIndex >= 0) { if (bestIndex >= 0) {
clip.open(AudioSystem.getAudioInputStream(formats[bestIndex], audioIn)); return new MultiClip(ref, AudioSystem.getAudioInputStream(formats[bestIndex], audioIn));
} else } else
// still couldn't find anything, try the default clip format // still couldn't find anything, try the default clip format
clip.open(AudioSystem.getAudioInputStream(clip.getFormat(), audioIn)); return new MultiClip(ref, AudioSystem.getAudioInputStream(clip.getFormat(), audioIn));
return clip;
} }
} catch (UnsupportedAudioFileException | IOException | LineUnavailableException | RuntimeException e) { } catch (UnsupportedAudioFileException | IOException | LineUnavailableException | RuntimeException e) {
ErrorHandler.error(String.format("Failed to load file '%s'.", ref), e, true); ErrorHandler.error(String.format("Failed to load file '%s'.", ref), e, true);
@ -231,28 +230,16 @@ public class SoundController {
* @param clip the Clip to play * @param clip the Clip to play
* @param volume the volume [0, 1] * @param volume the volume [0, 1]
*/ */
private static void playClip(Clip clip, float volume) { private static void playClip(MultiClip clip, float volume) {
if (clip == null) // clip failed to load properly if (clip == null) // clip failed to load properly
return; return;
if (volume > 0f) { if (volume > 0f) {
// stop clip if running try {
if (clip.isRunning()) { clip.start(volume);
clip.stop(); } catch (LineUnavailableException | IOException e) {
clip.flush(); ErrorHandler.error(String.format("Could not start a clip '%s'.", clip.getName()), e, true);
} }
// 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);
}
// play clip
clip.setFramePosition(0);
clip.start();
} }
} }
@ -314,4 +301,53 @@ public class SoundController {
return currentFileIndex * 100 / (SoundEffect.SIZE + (HitSound.SIZE * SampleSet.SIZE)); return currentFileIndex * 100 / (SoundEffect.SIZE + (HitSound.SIZE * SampleSet.SIZE));
} }
/** Max number of clips that can be created */
static int MAX_CLIP = 100;
/** List of clips that has been created */
static HashSet<Clip> clipList = new HashSet<Clip>();
/** List of clips to be closed */
static LinkedList<Clip> clipsToClose = new LinkedList<Clip>();
/**
* Returns a new clip if it still below the max number of clips
*/
protected static Clip newClip() throws LineUnavailableException{
if(clipList.size() < MAX_CLIP) {
Clip c = AudioSystem.getClip();
clipList.add(c);
return c;
} else {
System.out.println("Can't newClip");
return null;
}
}
/**
* Adds a clip to be closed
*/
protected static void destroyClip(Clip c) {
if(clipList.remove(c)){
clipsToClose.add(c);
}
}
/**
* Destroys all extra Clips
*/
public static void destroyExtraClips() {
MultiClip.destroyExtraClips();
new Thread(){
public void run(){
for(ListIterator<Clip> ita = clipsToClose.listIterator(); ita.hasNext(); ){
Clip c = ita.next();
c.close();
ita.remove();
}
}
}.start();
}
} }

View File

@ -18,8 +18,6 @@
package itdelatrisu.opsu.audio; package itdelatrisu.opsu.audio;
import javax.sound.sampled.Clip;
/** /**
* Sound effects. * Sound effects.
*/ */
@ -47,7 +45,7 @@ public enum SoundEffect implements SoundController.SoundComponent {
private String filename; private String filename;
/** The Clip associated with the sound effect. */ /** The Clip associated with the sound effect. */
private Clip clip; private MultiClip clip;
/** Total number of sound effects. */ /** Total number of sound effects. */
public static final int SIZE = values().length; public static final int SIZE = values().length;
@ -67,11 +65,11 @@ public enum SoundEffect implements SoundController.SoundComponent {
public String getFileName() { return filename; } public String getFileName() { return filename; }
@Override @Override
public Clip getClip() { return clip; } public MultiClip getClip() { return clip; }
/** /**
* Sets the Clip for the sound. * Sets the Clip for the sound.
* @param clip the clip * @param clip the clip
*/ */
public void setClip(Clip clip) { this.clip = clip; } public void setClip(MultiClip clip) { this.clip = clip; }
} }

View File

@ -185,6 +185,7 @@ public class GameRanking extends BasicGameState {
retryButton.resetHover(); retryButton.resetHover();
exitButton.resetHover(); exitButton.resetHover();
} }
SoundController.destroyExtraClips();
} }
@Override @Override