diff --git a/src/itdelatrisu/opsu/audio/HitSound.java b/src/itdelatrisu/opsu/audio/HitSound.java index 9217e20b..8b7d6f44 100644 --- a/src/itdelatrisu/opsu/audio/HitSound.java +++ b/src/itdelatrisu/opsu/audio/HitSound.java @@ -20,8 +20,6 @@ package itdelatrisu.opsu.audio; import java.util.HashMap; -import javax.sound.sampled.Clip; - /** * Hit sounds. */ @@ -79,7 +77,7 @@ public enum HitSound implements SoundController.SoundComponent { private String filename; /** The Clip associated with the hit sound. */ - private HashMap clips; + private HashMap clips; /** Total number of hit sounds. */ public static final int SIZE = values().length; @@ -90,7 +88,7 @@ public enum HitSound implements SoundController.SoundComponent { */ HitSound(String filename) { this.filename = filename; - this.clips = new HashMap(); + this.clips = new HashMap(); } /** @@ -100,7 +98,7 @@ public enum HitSound implements SoundController.SoundComponent { public String getFileName() { return filename; } @Override - public Clip getClip() { + public MultiClip getClip() { return (currentSampleSet != null) ? clips.get(currentSampleSet) : null; } @@ -109,7 +107,7 @@ public enum HitSound implements SoundController.SoundComponent { * @param s the sample set * @param clip the Clip */ - public void setClip(SampleSet s, Clip clip) { + public void setClip(SampleSet s, MultiClip clip) { clips.put(s, clip); } diff --git a/src/itdelatrisu/opsu/audio/MultiClip.java b/src/itdelatrisu/opsu/audio/MultiClip.java new file mode 100644 index 00000000..c26db9f4 --- /dev/null +++ b/src/itdelatrisu/opsu/audio/MultiClip.java @@ -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 clips = new LinkedList(); + + /** 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 allMultiClips = new LinkedList(); + + /** Constructor */ + public MultiClip(String name, AudioInputStream audioIn) throws IOException, LineUnavailableException { + this.name = name; + if(audioIn != null){ + format = audioIn.getFormat(); + + LinkedList allBufs = new LinkedList(); + + 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 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 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(); + } + } +} diff --git a/src/itdelatrisu/opsu/audio/SoundController.java b/src/itdelatrisu/opsu/audio/SoundController.java index e447551b..f9e1d476 100644 --- a/src/itdelatrisu/opsu/audio/SoundController.java +++ b/src/itdelatrisu/opsu/audio/SoundController.java @@ -27,13 +27,15 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; 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.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.LineUnavailableException; import javax.sound.sampled.UnsupportedAudioFileException; @@ -50,7 +52,7 @@ public class SoundController { * Returns the Clip associated with the sound component. * @return the Clip */ - public Clip getClip(); + public MultiClip getClip(); } /** Sample volume multiplier, from timing points [0, 1]. */ @@ -71,7 +73,7 @@ public class SoundController { * @param isMP3 true if MP3, false if WAV * @return the loaded and opened clip */ - private static Clip loadClip(String ref, boolean isMP3) { + private static MultiClip loadClip(String ref, boolean isMP3) { try { URL url = ResourceLoader.getResource(ref); @@ -79,7 +81,7 @@ public class SoundController { InputStream in = url.openStream(); if (in.available() == 0) { in.close(); - return AudioSystem.getClip(); + return new MultiClip(ref, null); } in.close(); @@ -97,12 +99,10 @@ public class SoundController { audioIn = decodedAudioIn; } DataLine.Info info = new DataLine.Info(Clip.class, format); - if (AudioSystem.isLineSupported(info)) { - Clip clip = (Clip) AudioSystem.getLine(info); - clip.open(audioIn); - return clip; - } else { - // try to find closest matching line + if(AudioSystem.isLineSupported(info)){ + return new MultiClip(ref, audioIn); + }else{ + //Try to find closest matching line Clip clip = AudioSystem.getClip(); AudioFormat[] formats = ((DataLine.Info) clip.getLineInfo()).getFormats(); int bestIndex = -1; @@ -149,11 +149,10 @@ public class SoundController { break; } if (bestIndex >= 0) { - clip.open(AudioSystem.getAudioInputStream(formats[bestIndex], audioIn)); + return new MultiClip(ref, AudioSystem.getAudioInputStream(formats[bestIndex], audioIn)); } else // still couldn't find anything, try the default clip format - clip.open(AudioSystem.getAudioInputStream(clip.getFormat(), audioIn)); - return clip; + return new MultiClip(ref, AudioSystem.getAudioInputStream(clip.getFormat(), audioIn)); } } catch (UnsupportedAudioFileException | IOException | LineUnavailableException | RuntimeException e) { 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 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 return; if (volume > 0f) { - // stop clip if running - if (clip.isRunning()) { - clip.stop(); - clip.flush(); + try { + clip.start(volume); + } catch (LineUnavailableException | IOException e) { + 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)); } + + + /** Max number of clips that can be created */ + static int MAX_CLIP = 100; + + /** List of clips that has been created */ + static HashSet clipList = new HashSet(); + + /** List of clips to be closed */ + static LinkedList clipsToClose = new LinkedList(); + + /** + * 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 ita = clipsToClose.listIterator(); ita.hasNext(); ){ + Clip c = ita.next(); + c.close(); + ita.remove(); + } + } + }.start(); + } } diff --git a/src/itdelatrisu/opsu/audio/SoundEffect.java b/src/itdelatrisu/opsu/audio/SoundEffect.java index 3c7d125f..03cabe3d 100644 --- a/src/itdelatrisu/opsu/audio/SoundEffect.java +++ b/src/itdelatrisu/opsu/audio/SoundEffect.java @@ -18,8 +18,6 @@ package itdelatrisu.opsu.audio; -import javax.sound.sampled.Clip; - /** * Sound effects. */ @@ -47,7 +45,7 @@ public enum SoundEffect implements SoundController.SoundComponent { private String filename; /** The Clip associated with the sound effect. */ - private Clip clip; + private MultiClip clip; /** Total number of sound effects. */ public static final int SIZE = values().length; @@ -67,11 +65,11 @@ public enum SoundEffect implements SoundController.SoundComponent { public String getFileName() { return filename; } @Override - public Clip getClip() { return clip; } + public MultiClip getClip() { return clip; } /** * Sets the Clip for the sound. * @param clip the clip */ - public void setClip(Clip clip) { this.clip = clip; } + public void setClip(MultiClip clip) { this.clip = clip; } } diff --git a/src/itdelatrisu/opsu/states/GameRanking.java b/src/itdelatrisu/opsu/states/GameRanking.java index 4bec2b88..965128c2 100644 --- a/src/itdelatrisu/opsu/states/GameRanking.java +++ b/src/itdelatrisu/opsu/states/GameRanking.java @@ -185,6 +185,7 @@ public class GameRanking extends BasicGameState { retryButton.resetHover(); exitButton.resetHover(); } + SoundController.destroyExtraClips(); } @Override