diff --git a/forge-gui-desktop/src/main/java/forge/sound/AudioClip.java b/forge-gui-desktop/src/main/java/forge/sound/AudioClip.java index 831dc2806c6..105dadd8882 100644 --- a/forge-gui-desktop/src/main/java/forge/sound/AudioClip.java +++ b/forge-gui-desktop/src/main/java/forge/sound/AudioClip.java @@ -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 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 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 completed) { + final int attempts = 5; + for (int i = 0; i < attempts; i++) { + if (completed.get() || !waitSoundSystemDelay()) { + break; + } + } + } } }