Merge branch 'overlap_same_clip' into 'master'

Support overlapped reproducing of same AudioClip

See merge request core-developers/forge!2081
This commit is contained in:
Michael Kamensky
2019-09-06 19:22:48 +00:00

View File

@@ -24,6 +24,8 @@ import forge.properties.ForgeConstants;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.MissingResourceException;
import java.util.function.Supplier;
@@ -35,9 +37,10 @@ import java.util.function.Supplier;
* @author Agetian
*/
public class AudioClip implements IAudioClip {
private Clip clip;
private boolean started;
private boolean looping;
private final int maxSize = 16;
private String filename;
private List<ClipWrapper> clips;
private boolean failed;
public static boolean fileExists(String fileName) {
File fSound = new File(ForgeConstants.SOUND_DIR, fileName);
@@ -45,100 +48,62 @@ public class AudioClip implements IAudioClip {
}
public AudioClip(final String filename) {
File fSound = new File(ForgeConstants.SOUND_DIR, filename);
if (!fSound.exists()) {
throw new IllegalArgumentException("Sound file " + fSound.toString() + " does not exist, cannot make a clip of it");
}
try {
AudioInputStream stream = AudioSystem.getAudioInputStream(fSound);
AudioFormat format = stream.getFormat();
DataLine.Info info = new DataLine.Info(Clip.class, stream.getFormat(), ((int) stream.getFrameLength() * format.getFrameSize()));
clip = (Clip) AudioSystem.getLine(info);
clip.addLineListener(this::lineStatusChanged);
clip.open(stream);
return;
} catch (IOException ex) {
System.err.println("Unable to load sound file: " + filename);
} catch (LineUnavailableException ex) {
System.err.println("Error initializing sound system: " + ex);
} catch (UnsupportedAudioFileException ex) {
System.err.println("Unsupported file type of the sound file: " + fSound.toString() + " - " + ex.getMessage());
clip = null;
return;
}
throw new MissingResourceException("Sound clip failed to load", this.getClass().getName(), filename);
this.filename = filename;
clips = new ArrayList<>(maxSize);
addClip();
}
@Override
public final void play() {
if (null == clip) {
return;
}
synchronized (this) {
if (clip.isRunning()) {
// introduce small delay to make a batch sounds more granular,
// e.g. when you auto-tap 4 lands the 4 tap sounds should
// not become completely merged
waitSoundSystemDelay();
}
clip.setMicrosecondPosition(0);
if (!this.looping && clip.isRunning()) {
return;
}
this.started = false;
clip.start();
wait(() -> this.started);
if (clips.stream().anyMatch(ClipWrapper::isRunning)) {
// introduce small delay to make a batch sounds more granular,
// e.g. when you auto-tap 4 lands the 4 tap sounds should
// not become completely merged
waitSoundSystemDelay();
}
getIdleClip().start();
}
@Override
public final void loop() {
if (null == clip) {
return;
}
synchronized (this) {
clip.setMicrosecondPosition(0);
if (this.looping && clip.isRunning()) {
return;
}
this.started = false;
clip.loop(Clip.LOOP_CONTINUOUSLY);
wait(() -> this.started);
this.looping = true;
}
getIdleClip().loop();
}
@Override
public final void stop() {
if (null == clip) {
return;
}
synchronized (this) {
for (ClipWrapper clip: clips) {
clip.stop();
this.looping = false;
}
}
@Override
public final boolean isDone() {
if (null == clip) {
return false;
}
return !clip.isRunning();
return clips.stream().noneMatch(ClipWrapper::isRunning);
}
private void wait(Supplier<Boolean> completed) {
final int attempts = 5;
for (int i = 0; i < attempts; i++) {
if (completed.get() || !waitSoundSystemDelay()) {
break;
private ClipWrapper getIdleClip() {
return clips.stream()
.filter(clip -> !clip.isRunning())
.findFirst()
.orElseGet(this::addClip);
}
private ClipWrapper addClip() {
if (clips.size() < maxSize && !failed) {
ClipWrapper clip = new ClipWrapper(filename);
if (clip.isFailed()) {
failed = true;
} else {
clips.add(clip);
}
return clip;
}
return ClipWrapper.Dummy;
}
private boolean waitSoundSystemDelay() {
private static boolean waitSoundSystemDelay() {
try {
Thread.sleep(SoundSystem.DELAY);
return true;
@@ -148,8 +113,101 @@ public class AudioClip implements IAudioClip {
}
}
private void lineStatusChanged(LineEvent line) {
LineEvent.Type status = line.getType();
this.started |= status == LineEvent.Type.START;
static class ClipWrapper {
private final Clip clip;
private boolean started;
static final ClipWrapper Dummy = new ClipWrapper();
private ClipWrapper() {
clip = null;
}
ClipWrapper(String filename) {
clip = createClip(filename);
if (clip != null) {
clip.addLineListener(this::clipStateChanged);
}
}
boolean isFailed() {
return null == clip;
}
void start() {
if (null == clip) {
return;
}
synchronized (this) {
clip.setMicrosecondPosition(0);
this.started = false;
clip.start();
// with JRE 1.8.0_211 if another thread called clip.setMicrosecondPosition
// just now, it would deadlock. To prevent this we synchronize this method
// and wait
wait(() -> this.started);
}
}
void loop() {
if (null == clip) {
return;
}
synchronized (this) {
clip.setMicrosecondPosition(0);
this.started = false;
clip.loop(Clip.LOOP_CONTINUOUSLY);
wait(() -> this.started);
}
}
void stop() {
if (null == clip) {
return;
}
synchronized (this) {
clip.stop();
}
}
boolean isRunning() {
return clip != null && (clip.isRunning() || clip.isActive());
}
private Clip createClip(String filename) {
File fSound = new File(ForgeConstants.SOUND_DIR, filename);
if (!fSound.exists()) {
throw new IllegalArgumentException("Sound file " + fSound.toString() + " does not exist, cannot make a clip of it");
}
try {
AudioInputStream stream = AudioSystem.getAudioInputStream(fSound);
AudioFormat format = stream.getFormat();
DataLine.Info info = new DataLine.Info(Clip.class, stream.getFormat(), ((int) stream.getFrameLength() * format.getFrameSize()));
Clip clip = (Clip) AudioSystem.getLine(info);
clip.open(stream);
return clip;
} catch (IOException ex) {
System.err.println("Unable to load sound file: " + filename);
} catch (LineUnavailableException ex) {
System.err.println("Error initializing sound system: " + ex);
} catch (UnsupportedAudioFileException ex) {
System.err.println("Unsupported file type of the sound file: " + fSound.toString() + " - " + ex.getMessage());
return null;
}
throw new MissingResourceException("Sound clip failed to load", this.getClass().getName(), filename);
}
private void clipStateChanged(LineEvent lineEvent) {
started |= lineEvent.getType() == LineEvent.Type.START;
}
private void wait(Supplier<Boolean> completed) {
final int attempts = 5;
for (int i = 0; i < attempts; i++) {
if (completed.get() || !waitSoundSystemDelay()) {
break;
}
}
}
}
}