From c9772cb8e8dbb6a3d77f349f5db7673ca05f6302 Mon Sep 17 00:00:00 2001 From: Argent77 <4519923+Argent77@users.noreply.github.com> Date: Wed, 27 Nov 2024 16:43:33 +0100 Subject: [PATCH] Various improvements and fixes for audio playback Added interfaces for generalized audio player functionality: - AudioPlayback: basic playback controls - BufferedAudioPlayback: controls for prebuffered audio data SoundPanel: - Fixed several loop mode issues - Added option to use combined or separate play/pause controls - Click on progress slider moves playback position to mouse position --- src/org/infinity/datatype/ResourceRef.java | 3 +- src/org/infinity/gui/SoundPanel.java | 186 +++++++++---- .../resource/sound/AudioPlayback.java | 55 ++++ .../infinity/resource/sound/AudioPlayer.java | 2 +- .../resource/sound/BufferedAudioPlayback.java | 39 +++ ...ioPlayer.java => BufferedAudioPlayer.java} | 244 ++++++++++-------- 6 files changed, 358 insertions(+), 171 deletions(-) create mode 100644 src/org/infinity/resource/sound/AudioPlayback.java create mode 100644 src/org/infinity/resource/sound/BufferedAudioPlayback.java rename src/org/infinity/resource/sound/{SingleAudioPlayer.java => BufferedAudioPlayer.java} (86%) diff --git a/src/org/infinity/datatype/ResourceRef.java b/src/org/infinity/datatype/ResourceRef.java index e5df9c8ff..69de2c2a7 100644 --- a/src/org/infinity/datatype/ResourceRef.java +++ b/src/org/infinity/datatype/ResourceRef.java @@ -195,7 +195,8 @@ public void mouseClicked(MouseEvent event) { bView = new JButton("View/Edit", Icons.ICON_ZOOM_16.getIcon()); bView.addActionListener(this); - soundPanel = new SoundPanel(SoundPanel.Option.TIME_LABEL, SoundPanel.Option.PROGRESS_BAR); + soundPanel = new SoundPanel(SoundPanel.Option.COMPACT_CONTROLS, SoundPanel.Option.TIME_LABEL, + SoundPanel.Option.PROGRESS_BAR); soundPanel.setDisplayFormat(SoundPanel.DisplayFormat.ELAPSED_TOTAL_PRECISE); soundPanel.setVisible(ResourceEntry.isSound(types)); diff --git a/src/org/infinity/gui/SoundPanel.java b/src/org/infinity/gui/SoundPanel.java index 7fa151be7..82e16781b 100644 --- a/src/org/infinity/gui/SoundPanel.java +++ b/src/org/infinity/gui/SoundPanel.java @@ -13,6 +13,7 @@ import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.beans.PropertyChangeEvent; @@ -41,6 +42,7 @@ import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; +import javax.swing.plaf.basic.BasicSliderUI; import org.infinity.exceptions.ResourceNotFoundException; import org.infinity.icon.Icons; @@ -48,9 +50,11 @@ import org.infinity.resource.key.ResourceEntry; import org.infinity.resource.sound.AudioBuffer; import org.infinity.resource.sound.AudioFactory; +import org.infinity.resource.sound.AudioPlayback; import org.infinity.resource.sound.AudioStateEvent; import org.infinity.resource.sound.AudioStateListener; -import org.infinity.resource.sound.SingleAudioPlayer; +import org.infinity.resource.sound.BufferedAudioPlayback; +import org.infinity.resource.sound.BufferedAudioPlayer; import org.infinity.resource.sound.WavBuffer; import org.infinity.util.Threading; import org.tinylog.Logger; @@ -64,6 +68,10 @@ public class SoundPanel extends JPanel implements Closeable { /** Optional UI controls for display. */ public enum Option { + /** + * Specifies that the {@code Play} and {@code Pause} buttons are combined to a single button to save space. + */ + COMPACT_CONTROLS, /** * Specifies a label that displays the current playback time. The control is shown below the playback controls and * progress bar if visible. @@ -168,12 +176,13 @@ public String toString(long elapsed, long total) { public static final String PROPERTY_NAME_PAUSED = "soundPanelPaused"; private static final String CMD_PLAY = "play"; + private static final String CMD_PAUSE = "pause"; private static final String CMD_STOP = "stop"; private static final String CMD_LOOP = "loop"; - private static final ImageIcon ICON_PLAY = Icons.ICON_PLAY_16.getIcon(); + private static final ImageIcon ICON_PLAY = Icons.ICON_PLAY_16.getIcon(); private static final ImageIcon ICON_PAUSE = Icons.ICON_PAUSE_16.getIcon(); - private static final ImageIcon ICON_STOP = Icons.ICON_STOP_16.getIcon(); + private static final ImageIcon ICON_STOP = Icons.ICON_STOP_16.getIcon(); private static boolean looped = false; @@ -183,6 +192,7 @@ public String toString(long elapsed, long total) { private final Listeners listener = new Listeners(); private JButton playButton; + private JButton pauseButton; private JButton stopButton; private JLabel displayLabel; private JCheckBox loopCheckBox; @@ -195,6 +205,7 @@ public String toString(long elapsed, long total) { private boolean closed; private boolean progressAdjusting; + private boolean combinedPlayPause; private boolean showProgressLabels; /** @@ -408,6 +419,8 @@ public boolean isOptionEnabled(Option option) { } switch (option) { + case COMPACT_CONTROLS: + return combinedPlayPause; case LOOP_CHECKBOX: return loopCheckBox.isVisible(); case PROGRESS_BAR: @@ -417,6 +430,7 @@ public boolean isOptionEnabled(Option option) { case TIME_LABEL: return displayLabel.isVisible(); } + return false; } @@ -534,6 +548,9 @@ public void setEnabled(boolean enabled) { updateControls(); } else { playButton.setEnabled(false); + if (!combinedPlayPause) { + pauseButton.setEnabled(false); + } stopButton.setEnabled(false); progressSlider.setEnabled(false); } @@ -630,16 +647,21 @@ private void updateLabel() { private void updateControls() { if (runner.isAvailable()) { if (runner.isPlaying()) { - if (runner.isPaused()) { - playButton.setIcon(ICON_PAUSE); + if (combinedPlayPause) { + playButton.setIcon(runner.isPaused() ? ICON_PAUSE : ICON_PLAY); + playButton.setEnabled(true); } else { - playButton.setIcon(ICON_PLAY); + playButton.setEnabled(runner.isPaused()); + pauseButton.setEnabled(!runner.isPaused()); } - playButton.setEnabled(true); stopButton.setEnabled(true); progressSlider.setEnabled(true); } else { - playButton.setIcon(ICON_PLAY); + if (combinedPlayPause) { + playButton.setIcon(ICON_PLAY); + } else { + pauseButton.setEnabled(false); + } playButton.setEnabled(true); stopButton.setEnabled(false); progressSlider.setEnabled(false); @@ -647,6 +669,9 @@ private void updateControls() { } else { playButton.setIcon(ICON_PLAY); playButton.setEnabled(false); + if (!combinedPlayPause) { + pauseButton.setEnabled(false); + } stopButton.setEnabled(false); progressSlider.setEnabled(false); } @@ -721,10 +746,20 @@ private static JLabel createSliderTickLabel(int timeMs) { private void init(Option... options) { closed = false; + combinedPlayPause = isOption(options, Option.COMPACT_CONTROLS); + playButton = new JButton(ICON_PLAY); playButton.setActionCommand(CMD_PLAY); playButton.addActionListener(listener); + if (!combinedPlayPause) { + pauseButton = new JButton(ICON_PAUSE); + pauseButton.setActionCommand(CMD_PAUSE); + pauseButton.addActionListener(listener); + } else { + playButton.setToolTipText("Start or pause playback."); + } + stopButton = new JButton(ICON_STOP); stopButton.setActionCommand(CMD_STOP); stopButton.addActionListener(listener); @@ -732,49 +767,36 @@ private void init(Option... options) { loopCheckBox = new JCheckBox("Loop", looped); loopCheckBox.setActionCommand(CMD_LOOP); loopCheckBox.addActionListener(listener); - loopCheckBox.setVisible(false); + loopCheckBox.setVisible(isOption(options, Option.LOOP_CHECKBOX)); displayLabel = new JLabel(DisplayFormat.ELAPSED_TOTAL.toString(0L, 0L), SwingConstants.LEADING); - displayLabel.setVisible(false); + displayLabel.setVisible(isOption(options, Option.TIME_LABEL)); progressSlider = new FixedSlider(new AdjustingBoundedRangeModel()); progressSlider.setOrientation(SwingConstants.HORIZONTAL); progressSlider.setPaintTicks(true); progressSlider.addChangeListener(listener); - progressSlider.setVisible(false); - - // making selected options visible - for (final Option option : options) { - if (option != null) { - switch (option) { - case LOOP_CHECKBOX: - loopCheckBox.setVisible(true); - break; - case PROGRESS_BAR: - progressSlider.setVisible(true); - break; - case PROGRESS_BAR_LABELS: - showProgressLabels = true; - break; - case TIME_LABEL: - displayLabel.setVisible(true); - break; - } - } - } + progressSlider.setVisible(isOption(options, Option.PROGRESS_BAR)); + showProgressLabels = isOption(options, Option.PROGRESS_BAR_LABELS); progressSlider.setPaintLabels(showProgressLabels); // assembling panel final GridBagConstraints gbc = new GridBagConstraints(); final JPanel playbackPanel = new JPanel(new GridBagLayout()); - ViewerUtil.setGBC(gbc, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + int idx = 0; + ViewerUtil.setGBC(gbc, idx++, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0); playbackPanel.add(playButton, gbc); - ViewerUtil.setGBC(gbc, 1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + if (!combinedPlayPause) { + ViewerUtil.setGBC(gbc, idx++, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(0, 8, 0, 0), 0, 0); + playbackPanel.add(pauseButton, gbc); + } + ViewerUtil.setGBC(gbc, idx++, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(0, 8, 0, 0), 0, 0); playbackPanel.add(stopButton, gbc); - ViewerUtil.setGBC(gbc, 2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, + ViewerUtil.setGBC(gbc, idx++, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 8, 0, 0), 0, 0); playbackPanel.add(loopCheckBox, gbc); @@ -801,6 +823,26 @@ private void init(Option... options) { } } + /** + * Returns whether the specified option is listed in the options array. + * + * @param options Array of {@link Option} enums. + * @param option {@link Option} to test. + * @return {@code true} if {@code option} is found in {@code options}, {@code false} otherwise. + */ + private boolean isOption(Option[] options, Option option) { + boolean retVal = false; + if (options != null) { + for (final Option o : options) { + if (o == option) { + retVal = true; + break; + } + } + } + return retVal; + } + // -------------------------- INNER CLASSES -------------------------- /** Listeners for the SoundPanel class. */ @@ -813,11 +855,20 @@ public void actionPerformed(ActionEvent e) { switch (e.getActionCommand()) { case CMD_PLAY: if (SoundPanel.this.isPlaying()) { - SoundPanel.this.setPaused(!SoundPanel.this.isPaused()); + if (SoundPanel.this.combinedPlayPause) { + SoundPanel.this.setPaused(!SoundPanel.this.isPaused()); + } else if (SoundPanel.this.isPaused()) { + SoundPanel.this.setPaused(false); + } } else { SoundPanel.this.setPlaying(true); } break; + case CMD_PAUSE: + if (SoundPanel.this.isPlaying()) { + SoundPanel.this.setPaused(true); + } + break; case CMD_STOP: SoundPanel.this.setPlaying(false); break; @@ -853,7 +904,7 @@ private class Runner implements Runnable, AudioStateListener { private final ReentrantLock lock = new ReentrantLock(); private final Thread thread; - private SingleAudioPlayer player; + private AudioPlayback player; private boolean running; /** Initializes the class instance and starts a background thread. */ @@ -864,7 +915,7 @@ public Runner() { } /** - * Assigns a new {@link SingleAudioPlayer} instance to the runner. The old player instance, if any, is properly closed + * Assigns a new {@link BufferedAudioPlayer} instance to the runner. The old player instance, if any, is properly closed * before the new instance is assigned. * * @param newBuffer @@ -877,14 +928,15 @@ public void setAudio(AudioBuffer newBuffer) throws Exception{ lock.lock(); try { if (player != null) { - player.stop(); + player.setPlaying(false); player.close(); player = null; } if (newBuffer != null) { - player = new SingleAudioPlayer(newBuffer, this); - player.setLooped(SoundPanel.looped); + // TODO: externalize audio player instantiation to abstract audio player backend + player = new BufferedAudioPlayer(newBuffer, this); + ((BufferedAudioPlayback)player).setLooped(SoundPanel.looped); } } finally { lock.unlock(); @@ -902,13 +954,13 @@ public boolean isAvailable() { /** Returns whether loop mode is enabled. */ @SuppressWarnings("unused") public boolean isLooped() { - return (player != null) ? player.isLooped() : false; + return (player instanceof BufferedAudioPlayback) ? ((BufferedAudioPlayback)player).isLooped() : false; } /** Enables or disable looped playback. */ public void setLooped(boolean loop) { - if (player != null) { - player.setLooped(loop); + if (player instanceof BufferedAudioPlayback) { + ((BufferedAudioPlayback)player).setLooped(loop); } } @@ -926,11 +978,7 @@ public boolean isPlaying() { */ public void setPlaying(boolean play) { if (player != null) { - if (play) { - player.play(); - } else { - player.stop(); - } + player.setPlaying(play); } } @@ -948,11 +996,7 @@ public boolean isPaused() { */ public void setPaused(boolean pause) { if (player != null) { - if (pause) { - player.pause(); - } else { - player.resume(); - } + player.setPaused(pause); } } @@ -961,7 +1005,7 @@ public void setPaused(boolean pause) { * initialized. */ public long getTotalLength() { - return (player != null) ? player.getTotalLength() : 0L; + return (player instanceof BufferedAudioPlayback) ? ((BufferedAudioPlayback)player).getTotalLength() : 0L; } /** @@ -978,8 +1022,8 @@ public long getElapsedTime() { * @param position New playback position in milliseconds. Position is clamped to the available audio clip duration. */ public void setSoundPosition(int position) { - if (player != null) { - player.setSoundPosition(position); + if (player instanceof BufferedAudioPlayback) { + ((BufferedAudioPlayback)player).setSoundPosition(position); } } @@ -1074,30 +1118,36 @@ private static class FixedSlider extends JSlider { @SuppressWarnings("unused") public FixedSlider() { super(); + init(); } @SuppressWarnings("unused") public FixedSlider(int orientation) { super(orientation); + init(); } @SuppressWarnings("unused") public FixedSlider(int min, int max) { super(min, max); + init(); } @SuppressWarnings("unused") public FixedSlider(int min, int max, int value) { super(min, max, value); + init(); } @SuppressWarnings("unused") public FixedSlider(int orientation, int min, int max, int value) { super(orientation, min, max, value); + init(); } public FixedSlider(BoundedRangeModel brm) { super(brm); + init(); } @Override @@ -1131,6 +1181,30 @@ private void clearDragMode() { } } } + + private void processMousePressed(MouseEvent e) { + if (getUI() instanceof BasicSliderUI) { + final BasicSliderUI ui = (BasicSliderUI)getUI(); + final int value; + if (getOrientation() == SwingConstants.VERTICAL) { + value = ui.valueForYPosition(e.getY()); + } else { + value = ui.valueForXPosition(e.getX()); + } + setValue(value); + } else { + Logger.debug("FixedSlider.getUI() not instance of BasicSliderUI"); + } + } + + private void init() { + addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + processMousePressed(e); + } + }); + } } /** diff --git a/src/org/infinity/resource/sound/AudioPlayback.java b/src/org/infinity/resource/sound/AudioPlayback.java new file mode 100644 index 000000000..bc477cdb4 --- /dev/null +++ b/src/org/infinity/resource/sound/AudioPlayback.java @@ -0,0 +1,55 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.resource.sound; + +import org.infinity.resource.Closeable; + +/** + * Provides basic playback functionality for audio data. + */ +public interface AudioPlayback extends Closeable { + /** + * Returns the elapsed playback time of audio data. + * + * @return Elapsed playback time, in milliseconds. + */ + long getElapsedTime(); + + /** + * Returns whether playback is active. Pausing and resuming playback does not affect the result. + * + * @return {@code true} if playback is active, {@code false} otherwise. + */ + boolean isPlaying(); + + /** + * Starts or stops playback of audio data. + * + * @param play Specify {@code true} to start playback or {@code false} to stop playback. + */ + void setPlaying(boolean play); + + /** + * Returns whether if playback is paused. Enabling or disabled the paused state does not affect the result of + * {@link #isPlaying()}.. + * + * @return {@code true} if current playback is paused, {@code false} otherwise. + */ + boolean isPaused(); + + /** + * Enters or leaves paused state when playback is active. Does nothing is playback is stopped. + * + * @param pause Specify {@code true} to pause current playback or {@code false} to resume playback. + */ + void setPaused(boolean pause); + + /** + * Returns whether the player has been closed. A closed audio player does not accept new playback commands. + * + * @return {@code true} if {@link #close()} was called on this audio player instance, {@code false} otherwise. + */ + boolean isClosed(); +} diff --git a/src/org/infinity/resource/sound/AudioPlayer.java b/src/org/infinity/resource/sound/AudioPlayer.java index 83f6e7ffd..9e76f3e5e 100644 --- a/src/org/infinity/resource/sound/AudioPlayer.java +++ b/src/org/infinity/resource/sound/AudioPlayer.java @@ -17,7 +17,7 @@ /** * This class provides a conventional way to play back sound data. It is mostly suited for playing streamed sound data. - * For playback of single sound clips the {@link SingleAudioPlayer} class is more suited. + * For playback of single sound clips the {@link BufferedAudioPlayer} class is more suited. */ public class AudioPlayer { private final byte[] buffer = new byte[8196]; diff --git a/src/org/infinity/resource/sound/BufferedAudioPlayback.java b/src/org/infinity/resource/sound/BufferedAudioPlayback.java new file mode 100644 index 000000000..2b59bb08a --- /dev/null +++ b/src/org/infinity/resource/sound/BufferedAudioPlayback.java @@ -0,0 +1,39 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.resource.sound; + +/** + * Provides specialized playback functionality for prebuffered audio data. + */ +public interface BufferedAudioPlayback extends AudioPlayback { + /** + * Returns the total length of the audio clip. + * + * @return Total sound length, in milliseconds. Returns {@code 0} if unavailable. + */ + long getTotalLength(); + + /** + * Sets an explicit playback position. + * + * @param position New playback position in milliseconds. Position is clamped to the available audio clip duration. + */ + void setSoundPosition(long position); + + /** + * Returns whether loop mode is enabled. + * + * @return {@code true} if sound playback is in loop mode, {@code false} otherwise. + */ + boolean isLooped(); + + /** + * Enables or disables looped playback. + * + * @param loop Specify {@code true} to start playback from the beginning if the end is reached. Specify {@code false} + * to stop playback when the end of the audio data is reached. + */ + void setLooped(boolean loop); +} diff --git a/src/org/infinity/resource/sound/SingleAudioPlayer.java b/src/org/infinity/resource/sound/BufferedAudioPlayer.java similarity index 86% rename from src/org/infinity/resource/sound/SingleAudioPlayer.java rename to src/org/infinity/resource/sound/BufferedAudioPlayer.java index f2dfe77b4..90568288b 100644 --- a/src/org/infinity/resource/sound/SingleAudioPlayer.java +++ b/src/org/infinity/resource/sound/BufferedAudioPlayer.java @@ -4,7 +4,6 @@ package org.infinity.resource.sound; -import java.beans.PropertyChangeEvent; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.Objects; @@ -27,10 +26,10 @@ /** * A class for providing smooth and responsive playback of a single sound clip. *
- * None of the methods block execution. Playback state changes are propagated through {@link PropertyChangeEvent}s. + * None of the methods block execution. Playback state changes are propagated through {@link AudioStateEvent}s. *
*/ - public class SingleAudioPlayer implements AutoCloseable, LineListener { + public class BufferedAudioPlayer implements BufferedAudioPlayback, LineListener { private final EventListenerList listenerList = new EventListenerList(); private final AudioBuffer audioBuffer; @@ -48,7 +47,7 @@ public class SingleAudioPlayer implements AutoCloseable, LineListener { private boolean looped; private boolean listenersEnabled; - public SingleAudioPlayer(AudioBuffer audioBuffer) throws Exception { + public BufferedAudioPlayer(AudioBuffer audioBuffer) throws Exception { this(audioBuffer, null); } @@ -62,7 +61,7 @@ public SingleAudioPlayer(AudioBuffer audioBuffer) throws Exception { * @throws LineUnavailableException if the audio line is not available due to resource restrictions. * @throws IllegalArgumentException if the audio data is invalid. */ - public SingleAudioPlayer(AudioBuffer audioBuffer, AudioStateListener listener) throws Exception { + public BufferedAudioPlayer(AudioBuffer audioBuffer, AudioStateListener listener) throws Exception { this.audioBuffer = Objects.requireNonNull(audioBuffer); addAudioStateListener(listener); audioStream = AudioSystem.getAudioInputStream(new ByteArrayInputStream(this.audioBuffer.getAudioData())); @@ -74,7 +73,7 @@ public SingleAudioPlayer(AudioBuffer audioBuffer, AudioStateListener listener) t audioClip.setLoopPoints(0, -1); } - /** Returns the total length of the audio clip, in milliseconds. */ + @Override public long getTotalLength() { if (isClosed()) { return 0L; @@ -82,7 +81,7 @@ public long getTotalLength() { return audioClip.getMicrosecondLength() / 1_000L; } - /** Returns the elapsed playback time of the current clip, in milliseconds. */ + @Override public long getElapsedTime() { if (isClosed()) { return 0L; @@ -91,11 +90,7 @@ public long getElapsedTime() { return position / 1_000L; } - /** - * Sets an explicit playback position. - * - * @param position New playback position in milliseconds. Position is clamped to the available audio clip duration. - */ + @Override public void setSoundPosition(long position) { if (isClosed()) { return; @@ -113,22 +108,19 @@ public void setSoundPosition(long position) { audioClip.setMicrosecondPosition(position); if (isPlaying) { audioClip.start(); + setLooped(); } } finally { setLineListenersEnabled(true); } } - /** Returns whether loop mode is enabled. */ + @Override public boolean isLooped() { return looped; } - /** - * Enables or disable looped playback. - * - * @param loop Indicates whether to loop playback. - */ + @Override public void setLooped(boolean loop) { if (isClosed()) { return; @@ -142,64 +134,22 @@ public void setLooped(boolean loop) { setLooped(); } - /** Use internally after each call {@link Clip#start()} to set up looping mode. */ - private void setLooped() { - if (isClosed()) { - return; - } - if (isPlaying()) { - audioClip.loop(isLooped() ? Clip.LOOP_CONTINUOUSLY : 0); - } - } - - /** Returns {@code true} if playback is active. Pausing and resuming playback does not affect the result. */ + @Override public boolean isPlaying() { return !isClosed() && playing; } /** - * Starts playback of the associated audio data. Does nothing if the player is closed or already playing. - * Triggers a {@link PropertyChangeEvent} with the name {@link #PROPERTY_NAME_START}. - */ - public void play() { - if (isClosed() || isPlaying()) { - return; - } - - synchronized (audioClip) { - if (audioClip.isRunning()) { - audioClip.stop(); - audioClip.flush(); - } - playing = true; - paused = false; - audioClip.setFramePosition(0); - audioClip.start(); - setLooped(); - } - } - - /** - * Stops active playback and sets position to the start of the clip. Does nothing if the player is closed or has - * stopped playback. Triggers a {@link PropertyChangeEvent} with the name {@link #PROPERTY_NAME_STOP}. + * Starts or stops playback of audio data. Triggers an {@link AudioStateEvent} if playback is started or stopped. + * + * @param play Specify {@code true} to start playback or {@code false} to stop playback. */ - public void stop() { - if (isClosed() || !isPlaying()) { - return; - } - - synchronized (audioClip) { - final boolean isPaused = isPaused(); - playing = false; - paused = false; - if (isPaused) { - // Pause mode is technically "stop" mode, so we need to trigger a "STOP" event manually - update(new LineEvent(audioClip, LineEvent.Type.STOP, audioClip.getLongFramePosition())); - } else { - audioClip.stop(); - } - audioClip.flush(); - audioClip.setFramePosition(0); + @Override + public void setPlaying(boolean play) { + if (play) { + play(); + } else { + stop(); } } @@ -207,57 +157,34 @@ public void stop() { * Returns {@code true} if playback is paused. Enabling or disabled the paused state does not affect playback * activity. */ + @Override public boolean isPaused() { return isPlaying() && paused; } /** - * Pauses active playback. Does nothing if the player is closed, playback is not active, or already in the paused - * state. Triggers a {@link PropertyChangeEvent} with the name {@link #PROPERTY_NAME_PAUSE}. - */ - public void pause() { - if (isClosed()) { - return; - } - - synchronized (audioClip) { - if (isPlaying() && !isPaused()) { - paused = true; - audioClip.stop(); - firePlaybackPaused(); - } - } - } - - /** - * Resumes previously paused playback. Does nothing if the player is closed, playback is not active, or not in the - * paused state. Triggers a {@link PropertyChangeEvent} with the name {@link #PROPERTY_NAME_RESUME}. + * Enters or leaves paused state when playback is active. Does nothing is playback is stopped. + * Triggers an {@link AudioStateEvent} if playback is paused or resumed. + * + * @param pause Specify {@code true} to pause current playback or {@code false} to resume playback. */ - public void resume() { - if (isClosed()) { - return; - } - - synchronized (audioClip) { - if (isPlaying() && isPaused()) { - paused = false; - audioClip.start(); - setLooped(); - firePlaybackResumed(); - } + @Override + public void setPaused(boolean pause) { + if (pause) { + pause(); + } else { + resume(); } } - /** Returns whether the player has been closed. A closed player does not accept any playback commands. */ + @Override public boolean isClosed() { return closed; } - /** - * Closes the player and releases any resources. Playback cannot be used anymore after calling this method. - */ + /** Closes the player and releases all resources. A closed audio player does not accept new playback commands. */ @Override - public void close() { + public void close() throws Exception { if (isClosed()) { return; } @@ -265,6 +192,13 @@ public void close() { closed = true; playing = false; paused = false; + + // removing listeners + final AudioStateListener[] items = getAudioStateListeners(); + for (int i = items.length - 1; i >= 0; i--) { + removeAudioStateListener(items[i]); + } + synchronized (audioClip) { if (audioClip.isRunning()) { audioClip.stop(); @@ -278,12 +212,6 @@ public void close() { Logger.warn(e); } audioStream = null; - - // removing listeners - final AudioStateListener[] items = getAudioStateListeners(); - for (int i = items.length - 1; i >= 0; i--) { - removeAudioStateListener(items[i]); - } } @Override @@ -319,6 +247,96 @@ public void removeAudioStateListener(AudioStateListener l) { } } + /** Use internally after each call {@link Clip#start()} to set up looping mode. */ + private void setLooped() { + if (isClosed()) { + return; + } + if (isPlaying() && !isPaused()) { + audioClip.loop(isLooped() ? Clip.LOOP_CONTINUOUSLY : 0); + } + } + + /** Starts playback of the associated audio data. Does nothing if the player is closed or already playing. */ + private void play() { + if (isClosed() || isPlaying()) { + return; + } + + synchronized (audioClip) { + if (audioClip.isRunning()) { + audioClip.stop(); + audioClip.flush(); + } + playing = true; + paused = false; + audioClip.setFramePosition(0); + audioClip.start(); + setLooped(); + } + } + + /** + * Stops active playback and sets position to the start of the clip. Does nothing if the player is closed or has + * stopped playback. + */ + private void stop() { + if (isClosed() || !isPlaying()) { + return; + } + + synchronized (audioClip) { + final boolean isPaused = isPaused(); + playing = false; + paused = false; + if (isPaused) { + // Pause mode is technically "stop" mode, so we need to trigger a "STOP" event manually + update(new LineEvent(audioClip, LineEvent.Type.STOP, audioClip.getLongFramePosition())); + } else { + audioClip.stop(); + } + audioClip.flush(); + audioClip.setFramePosition(0); + } + } + + /** + * Pauses active playback. Does nothing if the player is closed, playback is not active, or already in the paused + * state. + */ + private void pause() { + if (isClosed()) { + return; + } + + synchronized (audioClip) { + if (isPlaying() && !isPaused()) { + paused = true; + audioClip.stop(); + firePlaybackPaused(); + } + } + } + + /** + * Resumes previously paused playback. Does nothing if the player is closed, playback is not active, or not in the + * paused state. + */ + private void resume() { + if (isClosed()) { + return; + } + + synchronized (audioClip) { + if (isPlaying() && isPaused()) { + paused = false; + audioClip.start(); + setLooped(); + firePlaybackResumed(); + } + } + } + /** Returns whether {@link Line}'s status changes are tracked by this class instance. */ @SuppressWarnings("unused") private boolean isLineListenersEnabled() {