Follow-up to #25.
- Moved all MultiClip-handling methods into the MultiClip class. - Destroy extra clips when starting and finishing new beatmaps. - Reduced maximum number of extra clips to 20. - Attempted fixes at multithreading issues. Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
@@ -6,87 +6,101 @@ import java.util.LinkedList;
|
|||||||
|
|
||||||
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.Clip;
|
import javax.sound.sampled.Clip;
|
||||||
import javax.sound.sampled.FloatControl;
|
import javax.sound.sampled.FloatControl;
|
||||||
import javax.sound.sampled.LineUnavailableException;
|
import javax.sound.sampled.LineUnavailableException;
|
||||||
|
|
||||||
//http://stackoverflow.com/questions/1854616/in-java-how-can-i-play-the-same-audio-clip-multiple-times-simultaneously
|
/**
|
||||||
|
* 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 {
|
public class MultiClip {
|
||||||
/** A list of clips used for this audio sample */
|
/** Maximum number of extra clips that can be created at one time. */
|
||||||
LinkedList<Clip> clips = new LinkedList<Clip>();
|
private static final int MAX_CLIPS = 20;
|
||||||
|
|
||||||
/** The format of this audio sample */
|
/** A list of all created MultiClips. */
|
||||||
AudioFormat format;
|
private static final LinkedList<MultiClip> ALL_MULTICLIPS = new LinkedList<MultiClip>();
|
||||||
|
|
||||||
/** The data for this audio sample */
|
/** Size of a single buffer. */
|
||||||
byte[] audioData;
|
private static final int BUFFER_SIZE = 0x1000;
|
||||||
|
|
||||||
/** The name given to this clip */
|
/** Current number of extra clips created. */
|
||||||
String name;
|
private static int extraClips = 0;
|
||||||
|
|
||||||
/** Size of a single buffer */
|
/** Current number of clip-closing threads in execution. */
|
||||||
final int BUFFER_SIZE = 0x1000;
|
private static int closingThreads = 0;
|
||||||
|
|
||||||
static LinkedList<MultiClip> allMultiClips = new LinkedList<MultiClip>();
|
/** A list of clips used for this audio sample. */
|
||||||
|
private LinkedList<Clip> clips = new LinkedList<Clip>();
|
||||||
|
|
||||||
/** Constructor */
|
/** 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 String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
* @param name the clip name
|
||||||
|
* @param audioIn the associated AudioInputStream
|
||||||
|
*/
|
||||||
public MultiClip(String name, AudioInputStream audioIn) throws IOException, LineUnavailableException {
|
public MultiClip(String name, AudioInputStream audioIn) throws IOException, LineUnavailableException {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
if(audioIn != null){
|
if (audioIn != null) {
|
||||||
format = audioIn.getFormat();
|
format = audioIn.getFormat();
|
||||||
|
|
||||||
LinkedList<byte[]> allBufs = new LinkedList<byte[]>();
|
LinkedList<byte[]> allBufs = new LinkedList<byte[]>();
|
||||||
|
|
||||||
int readed = 0;
|
int totalRead = 0;
|
||||||
boolean hasData = true;
|
boolean hasData = true;
|
||||||
while (hasData) {
|
while (hasData) {
|
||||||
readed = 0;
|
totalRead = 0;
|
||||||
byte[] tbuf = new byte[BUFFER_SIZE];
|
byte[] tbuf = new byte[BUFFER_SIZE];
|
||||||
while (readed < tbuf.length) {
|
while (totalRead < tbuf.length) {
|
||||||
int read = audioIn.read(tbuf, readed, tbuf.length - readed);
|
int read = audioIn.read(tbuf, totalRead, tbuf.length - totalRead);
|
||||||
if (read < 0) {
|
if (read < 0) {
|
||||||
hasData = false;
|
hasData = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
readed += read;
|
totalRead += read;
|
||||||
}
|
}
|
||||||
allBufs.add(tbuf);
|
allBufs.add(tbuf);
|
||||||
}
|
}
|
||||||
|
|
||||||
audioData = new byte[(allBufs.size() - 1) * BUFFER_SIZE + readed];
|
audioData = new byte[(allBufs.size() - 1) * BUFFER_SIZE + totalRead];
|
||||||
|
|
||||||
int cnt = 0;
|
int cnt = 0;
|
||||||
for (byte[] tbuf : allBufs) {
|
for (byte[] tbuf : allBufs) {
|
||||||
int size = BUFFER_SIZE;
|
int size = BUFFER_SIZE;
|
||||||
if (cnt == allBufs.size() - 1) {
|
if (cnt == allBufs.size() - 1)
|
||||||
size = readed;
|
size = totalRead;
|
||||||
}
|
|
||||||
System.arraycopy(tbuf, 0, audioData, BUFFER_SIZE * cnt, size);
|
System.arraycopy(tbuf, 0, audioData, BUFFER_SIZE * cnt, size);
|
||||||
cnt++;
|
cnt++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getClip();
|
getClip();
|
||||||
allMultiClips.add(this);
|
ALL_MULTICLIPS.add(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the name of the clip
|
* Returns the name of the clip.
|
||||||
* @return the name
|
* @return the name
|
||||||
*/
|
*/
|
||||||
public String getName() {
|
public String getName() { return name; }
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays the clip with the specified volume.
|
* Plays the clip with the specified volume.
|
||||||
* @param volume the volume the play at
|
* @param volume the volume the play at
|
||||||
* @throws IOException
|
|
||||||
* @throws LineUnavailableException
|
|
||||||
*/
|
*/
|
||||||
public void start(float volume) throws LineUnavailableException, IOException {
|
public void start(float volume) throws LineUnavailableException {
|
||||||
Clip clip = getClip();
|
Clip clip = getClip();
|
||||||
|
if (clip == null)
|
||||||
if(clip == null)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// PulseAudio does not support Master Gain
|
// PulseAudio does not support Master Gain
|
||||||
@@ -100,57 +114,85 @@ public class MultiClip {
|
|||||||
clip.setFramePosition(0);
|
clip.setFramePosition(0);
|
||||||
clip.start();
|
clip.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a Clip that is not playing from the list
|
* Returns a Clip that is not playing from the list.
|
||||||
* if one is not available a new one is created if able
|
* If no clip is available, then a new one is created if under MAX_CLIPS.
|
||||||
* @return the Clip
|
* Otherwise, an existing clip will be returned.
|
||||||
|
* @return the Clip to play
|
||||||
*/
|
*/
|
||||||
private Clip getClip() throws LineUnavailableException, IOException{
|
private Clip getClip() throws LineUnavailableException {
|
||||||
for(Iterator<Clip> ita = clips.listIterator(); ita.hasNext(); ) {
|
// TODO:
|
||||||
Clip c = ita.next();
|
// Occasionally, even when clips are being closed in a separate thread,
|
||||||
if(!c.isRunning()){
|
// playing any clip will cause the game to hang until all clips are
|
||||||
ita.remove();
|
// 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);
|
clips.add(c);
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Clip t = SoundController.newClip();
|
Clip c = null;
|
||||||
if(t == null){
|
if (extraClips >= MAX_CLIPS) {
|
||||||
if(clips.isEmpty()){
|
// use an existing clip
|
||||||
|
if (clips.isEmpty())
|
||||||
return null;
|
return null;
|
||||||
}
|
c = clips.removeFirst();
|
||||||
t = clips.removeFirst();
|
c.stop();
|
||||||
t.stop();
|
clips.add(c);
|
||||||
clips.add(t);
|
|
||||||
} else {
|
} else {
|
||||||
|
// create a new clip
|
||||||
|
c = AudioSystem.getClip();
|
||||||
if (format != null)
|
if (format != null)
|
||||||
t.open(format, audioData, 0, audioData.length);
|
c.open(format, audioData, 0, audioData.length);
|
||||||
clips.add(t);
|
clips.add(c);
|
||||||
|
if (clips.size() != 1)
|
||||||
|
extraClips++;
|
||||||
}
|
}
|
||||||
return t;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroys all but one clip
|
* Destroys all extra clips.
|
||||||
*/
|
*/
|
||||||
protected void destroyAllButOne(){
|
public static void destroyExtraClips() {
|
||||||
for(Iterator<Clip> ita = clips.listIterator(); ita.hasNext(); ) {
|
if (extraClips == 0)
|
||||||
Clip c = ita.next();
|
return;
|
||||||
if(clips.size()>1){
|
|
||||||
ita.remove();
|
// find all extra clips
|
||||||
SoundController.destroyClip(c);
|
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
|
||||||
* Destroys all but one clip for all MultiClips
|
extraClips = 0;
|
||||||
*/
|
|
||||||
protected static void destroyExtraClips() {
|
|
||||||
for(MultiClip mc : MultiClip.allMultiClips){
|
|
||||||
mc.destroyAllButOne();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ 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;
|
||||||
@@ -102,7 +99,7 @@ public class SoundController {
|
|||||||
if(AudioSystem.isLineSupported(info)){
|
if(AudioSystem.isLineSupported(info)){
|
||||||
return new MultiClip(ref, audioIn);
|
return new MultiClip(ref, audioIn);
|
||||||
}else{
|
}else{
|
||||||
//Try to find closest matching line
|
// 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;
|
||||||
@@ -237,7 +234,7 @@ public class SoundController {
|
|||||||
if (volume > 0f) {
|
if (volume > 0f) {
|
||||||
try {
|
try {
|
||||||
clip.start(volume);
|
clip.start(volume);
|
||||||
} catch (LineUnavailableException | IOException e) {
|
} catch (LineUnavailableException e) {
|
||||||
ErrorHandler.error(String.format("Could not start a clip '%s'.", clip.getName()), e, true);
|
ErrorHandler.error(String.format("Could not start a clip '%s'.", clip.getName()), e, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,53 +298,4 @@ 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ public class LinearBezier extends Curve {
|
|||||||
curPoint = 0;
|
curPoint = 0;
|
||||||
} else {
|
} else {
|
||||||
curPoint = curBezier.points() - 1;
|
curPoint = curBezier.points() - 1;
|
||||||
if(lastDistanceAt == distanceAt){
|
if (lastDistanceAt == distanceAt) {
|
||||||
//out of points even though we haven't reached the preferred distance.
|
// out of points even though the preferred distance hasn't been reached
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,7 +185,6 @@ public class GameRanking extends BasicGameState {
|
|||||||
retryButton.resetHover();
|
retryButton.resetHover();
|
||||||
exitButton.resetHover();
|
exitButton.resetHover();
|
||||||
}
|
}
|
||||||
SoundController.destroyExtraClips();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import itdelatrisu.opsu.ScoreData;
|
|||||||
import itdelatrisu.opsu.SongSort;
|
import itdelatrisu.opsu.SongSort;
|
||||||
import itdelatrisu.opsu.Utils;
|
import itdelatrisu.opsu.Utils;
|
||||||
import itdelatrisu.opsu.audio.HitSound;
|
import itdelatrisu.opsu.audio.HitSound;
|
||||||
|
import itdelatrisu.opsu.audio.MultiClip;
|
||||||
import itdelatrisu.opsu.audio.MusicController;
|
import itdelatrisu.opsu.audio.MusicController;
|
||||||
import itdelatrisu.opsu.audio.SoundController;
|
import itdelatrisu.opsu.audio.SoundController;
|
||||||
import itdelatrisu.opsu.audio.SoundEffect;
|
import itdelatrisu.opsu.audio.SoundEffect;
|
||||||
@@ -917,6 +918,9 @@ public class SongMenu extends BasicGameState {
|
|||||||
if (resetGame) {
|
if (resetGame) {
|
||||||
((Game) game.getState(Opsu.STATE_GAME)).resetGameData();
|
((Game) game.getState(Opsu.STATE_GAME)).resetGameData();
|
||||||
|
|
||||||
|
// destroy extra Clips
|
||||||
|
MultiClip.destroyExtraClips();
|
||||||
|
|
||||||
// destroy skin images, if any
|
// destroy skin images, if any
|
||||||
for (GameImage img : GameImage.values()) {
|
for (GameImage img : GameImage.values()) {
|
||||||
if (img.isSkinnable())
|
if (img.isSkinnable())
|
||||||
@@ -1261,6 +1265,7 @@ public class SongMenu extends BasicGameState {
|
|||||||
Display.setTitle(String.format("%s - %s", game.getTitle(), osu.toString()));
|
Display.setTitle(String.format("%s - %s", game.getTitle(), osu.toString()));
|
||||||
OsuParser.parseHitObjects(osu);
|
OsuParser.parseHitObjects(osu);
|
||||||
HitSound.setSampleSet(osu.sampleSet);
|
HitSound.setSampleSet(osu.sampleSet);
|
||||||
|
MultiClip.destroyExtraClips();
|
||||||
((Game) game.getState(Opsu.STATE_GAME)).setRestart(Game.Restart.NEW);
|
((Game) game.getState(Opsu.STATE_GAME)).setRestart(Game.Restart.NEW);
|
||||||
game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user