Index: /trunk/src/org/openstreetmap/josm/actions/audio/AudioBackAction.java =================================================================== --- /trunk/src/org/openstreetmap/josm/actions/audio/AudioBackAction.java (revision 12327) +++ /trunk/src/org/openstreetmap/josm/actions/audio/AudioBackAction.java (revision 12328) @@ -14,4 +14,5 @@ import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer; import org.openstreetmap.josm.io.audio.AudioPlayer; +import org.openstreetmap.josm.io.audio.AudioUtil; import org.openstreetmap.josm.tools.Shortcut; @@ -40,5 +41,5 @@ MarkerLayer.playAudio(); } catch (IOException | InterruptedException ex) { - AudioPlayer.audioMalfunction(ex); + AudioUtil.audioMalfunction(ex); } } Index: /trunk/src/org/openstreetmap/josm/actions/audio/AudioFastSlowAction.java =================================================================== --- /trunk/src/org/openstreetmap/josm/actions/audio/AudioFastSlowAction.java (revision 12327) +++ /trunk/src/org/openstreetmap/josm/actions/audio/AudioFastSlowAction.java (revision 12328) @@ -8,4 +8,5 @@ import org.openstreetmap.josm.actions.JosmAction; import org.openstreetmap.josm.io.audio.AudioPlayer; +import org.openstreetmap.josm.io.audio.AudioUtil; import org.openstreetmap.josm.tools.Shortcut; @@ -43,5 +44,5 @@ AudioPlayer.play(AudioPlayer.url(), AudioPlayer.position(), speed * multiplier); } catch (IOException | InterruptedException ex) { - AudioPlayer.audioMalfunction(ex); + AudioUtil.audioMalfunction(ex); } } Index: /trunk/src/org/openstreetmap/josm/actions/audio/AudioFwdAction.java =================================================================== --- /trunk/src/org/openstreetmap/josm/actions/audio/AudioFwdAction.java (revision 12327) +++ /trunk/src/org/openstreetmap/josm/actions/audio/AudioFwdAction.java (revision 12328) @@ -13,4 +13,5 @@ import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer; import org.openstreetmap.josm.io.audio.AudioPlayer; +import org.openstreetmap.josm.io.audio.AudioUtil; import org.openstreetmap.josm.tools.Shortcut; @@ -38,5 +39,5 @@ MarkerLayer.playAudio(); } catch (IOException | InterruptedException ex) { - AudioPlayer.audioMalfunction(ex); + AudioUtil.audioMalfunction(ex); } } Index: /trunk/src/org/openstreetmap/josm/actions/audio/AudioPlayPauseAction.java =================================================================== --- /trunk/src/org/openstreetmap/josm/actions/audio/AudioPlayPauseAction.java (revision 12327) +++ /trunk/src/org/openstreetmap/josm/actions/audio/AudioPlayPauseAction.java (revision 12328) @@ -14,4 +14,5 @@ import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer; import org.openstreetmap.josm.io.audio.AudioPlayer; +import org.openstreetmap.josm.io.audio.AudioUtil; import org.openstreetmap.josm.tools.Shortcut; import org.openstreetmap.josm.tools.Utils; @@ -55,5 +56,5 @@ } } catch (IOException | InterruptedException ex) { - AudioPlayer.audioMalfunction(ex); + AudioUtil.audioMalfunction(ex); } } Index: /trunk/src/org/openstreetmap/josm/gui/layer/gpx/ImportAudioAction.java =================================================================== --- /trunk/src/org/openstreetmap/josm/gui/layer/gpx/ImportAudioAction.java (revision 12327) +++ /trunk/src/org/openstreetmap/josm/gui/layer/gpx/ImportAudioAction.java (revision 12328) @@ -41,13 +41,17 @@ private final transient GpxLayer layer; - static final class AudioFileFilter extends FileFilter { + /** + * Audio file filter. + * @since 12328 + */ + public static final class AudioFileFilter extends FileFilter { @Override public boolean accept(File f) { - return f.isDirectory() || Utils.hasExtension(f, "wav"); + return f.isDirectory() || Utils.hasExtension(f, "wav", "mp3", "aac", "aif", "aiff"); } @Override public String getDescription() { - return tr("Wave Audio files (*.wav)"); + return tr("Audio files (*.wav, *.mp3, *.aac, *.aif, *.aiff)"); } } @@ -119,13 +123,13 @@ * which the given audio file is associated with. Markers are derived from the following (a) * explict waypoints in the GPX layer, or (b) named trackpoints in the GPX layer, or (d) - * timestamp on the wav file (e) (in future) voice recognised markers in the sound recording (f) + * timestamp on the audio file (e) (in future) voice recognised markers in the sound recording (f) * a single marker at the beginning of the track - * @param wavFile the file to be associated with the markers in the new marker layer + * @param audioFile the file to be associated with the markers in the new marker layer * @param ml marker layer * @param firstStartTime first start time in milliseconds, used for (d) * @param markers keeps track of warning messages to avoid repeated warnings */ - private void importAudio(File wavFile, MarkerLayer ml, double firstStartTime, Markers markers) { - URL url = Utils.fileToURL(wavFile); + private void importAudio(File audioFile, MarkerLayer ml, double firstStartTime, Markers markers) { + URL url = Utils.fileToURL(audioFile); boolean hasTracks = layer.data.tracks != null && !layer.data.tracks.isEmpty(); boolean hasWaypoints = layer.data.waypoints != null && !layer.data.waypoints.isEmpty(); @@ -212,7 +216,6 @@ // (d) use timestamp of file as location on track if (hasTracks && Main.pref.getBoolean("marker.audiofromwavtimestamps", false)) { - double lastModified = wavFile.lastModified() / 1000.0; // lastModified is in - // milliseconds - double duration = AudioUtil.getCalibratedDuration(wavFile); + double lastModified = audioFile.lastModified() / 1000.0; // lastModified is in milliseconds + double duration = AudioUtil.getCalibratedDuration(audioFile); double startTime = lastModified - duration; startTime = firstStartTime + (startTime - firstStartTime) @@ -242,5 +245,5 @@ (startTime - w1.time) / (w2.time - w1.time))); wayPointFromTimeStamp.time = startTime; - String name = wavFile.getName(); + String name = audioFile.getName(); int dot = name.lastIndexOf('.'); if (dot > 0) { Index: /trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/AudioMarker.java =================================================================== --- /trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/AudioMarker.java (revision 12327) +++ /trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/AudioMarker.java (revision 12328) @@ -13,4 +13,5 @@ import org.openstreetmap.josm.data.gpx.WayPoint; import org.openstreetmap.josm.io.audio.AudioPlayer; +import org.openstreetmap.josm.io.audio.AudioUtil; import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider; @@ -60,5 +61,5 @@ recentlyPlayedMarker = this; } catch (IOException | InterruptedException e) { - AudioPlayer.audioMalfunction(e); + AudioUtil.audioMalfunction(e); } } Index: /trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/DefaultMarkerProducers.java =================================================================== --- /trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/DefaultMarkerProducers.java (revision 12327) +++ /trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/DefaultMarkerProducers.java (revision 12328) @@ -44,5 +44,5 @@ if (url == null) { return Collections.singleton(marker); - } else if (urlStr.endsWith(".wav")) { + } else if (Utils.hasExtension(urlStr, "wav", "mp3", "aac", "aif", "aiff")) { final AudioMarker audioMarker = new AudioMarker(wpt.getCoor(), wpt, url, parentLayer, time, offset); Extensions exts = (Extensions) wpt.get(GpxConstants.META_EXTENSIONS); @@ -55,6 +55,5 @@ } return Arrays.asList(marker, audioMarker); - } else if (urlStr.endsWith(".png") || urlStr.endsWith(".jpg") || urlStr.endsWith(".jpeg") - || urlStr.endsWith(".gif")) { + } else if (Utils.hasExtension(urlStr, "png", "jpg", "jpeg", "gif")) { return Arrays.asList(marker, new ImageMarker(wpt.getCoor(), url, parentLayer, time, offset)); } else { Index: /trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/Marker.java =================================================================== --- /trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/Marker.java (revision 12327) +++ /trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/Marker.java (revision 12328) @@ -53,5 +53,5 @@ * * By default, one the list contains one default "Maker" implementation that - * will create AudioMarkers for .wav files, ImageMarkers for .png/.jpg/.jpeg + * will create AudioMarkers for supported audio files, ImageMarkers for supported image * files, and WebMarkers for everything else. (The creation of a WebMarker will * fail if there's no valid URL in the <link> tag, so it might still make sense Index: /trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/PlayHeadMarker.java =================================================================== --- /trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/PlayHeadMarker.java (revision 12327) +++ /trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/PlayHeadMarker.java (revision 12328) @@ -25,4 +25,5 @@ import org.openstreetmap.josm.gui.layer.GpxLayer; import org.openstreetmap.josm.io.audio.AudioPlayer; +import org.openstreetmap.josm.io.audio.AudioUtil; /** @@ -100,5 +101,5 @@ AudioPlayer.pause(); } catch (IOException | InterruptedException ex) { - AudioPlayer.audioMalfunction(ex); + AudioUtil.audioMalfunction(ex); } } @@ -114,5 +115,5 @@ AudioPlayer.pause(); } catch (IOException | InterruptedException ex) { - AudioPlayer.audioMalfunction(ex); + AudioUtil.audioMalfunction(ex); } } Index: /trunk/src/org/openstreetmap/josm/io/audio/AudioException.java =================================================================== --- /trunk/src/org/openstreetmap/josm/io/audio/AudioException.java (revision 12328) +++ /trunk/src/org/openstreetmap/josm/io/audio/AudioException.java (revision 12328) @@ -0,0 +1,34 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.io.audio; + +/** + * Generic audio exception. Mainly used to wrap backend exceptions varying between implementations. + * @since 12328 + */ +public class AudioException extends Exception { + + /** + * Constructs a new {@code AudioException}. + * @param message the detail message (which is saved for later retrieval by the {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). + */ + public AudioException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new {@code AudioException}. + * @param message the detail message (which is saved for later retrieval by the {@link #getMessage()} method). + */ + public AudioException(String message) { + super(message); + } + + /** + * Constructs a new {@code AudioException}. + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). + */ + public AudioException(Throwable cause) { + super(cause); + } +} Index: /trunk/src/org/openstreetmap/josm/io/audio/AudioListener.java =================================================================== --- /trunk/src/org/openstreetmap/josm/io/audio/AudioListener.java (revision 12328) +++ /trunk/src/org/openstreetmap/josm/io/audio/AudioListener.java (revision 12328) @@ -0,0 +1,17 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.io.audio; + +import java.net.URL; + +/** + * Listener receiving audio playing events. + * @since 12328 + */ +interface AudioListener { + + /** + * Called when a new URL is being played. + * @param playingURL new URL being played + */ + void playing(URL playingURL); +} Index: /trunk/src/org/openstreetmap/josm/io/audio/AudioPlayer.java =================================================================== --- /trunk/src/org/openstreetmap/josm/io/audio/AudioPlayer.java (revision 12327) +++ /trunk/src/org/openstreetmap/josm/io/audio/AudioPlayer.java (revision 12328) @@ -2,22 +2,9 @@ package org.openstreetmap.josm.io.audio; -import static org.openstreetmap.josm.tools.I18n.tr; - -import java.awt.GraphicsEnvironment; import java.io.IOException; import java.net.URL; -import javax.sound.sampled.AudioFormat; -import javax.sound.sampled.AudioInputStream; -import javax.sound.sampled.AudioSystem; -import javax.sound.sampled.DataLine; -import javax.sound.sampled.LineUnavailableException; -import javax.sound.sampled.SourceDataLine; -import javax.sound.sampled.UnsupportedAudioFileException; -import javax.swing.JOptionPane; - import org.openstreetmap.josm.Main; import org.openstreetmap.josm.tools.JosmRuntimeException; -import org.openstreetmap.josm.tools.Utils; /** @@ -28,27 +15,22 @@ * @since 547 */ -public final class AudioPlayer extends Thread { +public final class AudioPlayer extends Thread implements AudioListener { private static volatile AudioPlayer audioPlayer; - private enum State { INITIALIZING, NOTPLAYING, PLAYING, PAUSED, INTERRUPTED } - - private enum Command { PLAY, PAUSE } - - private enum Result { WAITING, OK, FAILED } + enum State { INITIALIZING, NOTPLAYING, PLAYING, PAUSED, INTERRUPTED } + + enum Command { PLAY, PAUSE } + + enum Result { WAITING, OK, FAILED } private State state; + private SoundPlayer soundPlayer; private URL playingUrl; - private final double leadIn; // seconds - private final double calibration; // ratio of purported duration of samples to true duration - private double position; // seconds - private double bytesPerSecond; - private static long chunk = 4000; /* bytes */ - private double speed = 1.0; /** * Passes information from the control thread to the playing thread */ - private class Execute { + class Execute { private Command command; private Result result; @@ -85,5 +67,5 @@ } - private void possiblyInterrupt() throws InterruptedException { + protected void possiblyInterrupt() throws InterruptedException { if (interrupted() || result == Result.WAITING) throw new InterruptedException(); @@ -204,5 +186,5 @@ public static double position() { AudioPlayer instance = AudioPlayer.getInstance(); - return instance == null ? -1 : instance.position; + return instance == null ? -1 : instance.soundPlayer.position(); } @@ -213,5 +195,5 @@ public static double speed() { AudioPlayer instance = AudioPlayer.getInstance(); - return instance == null ? -1 : instance.speed; + return instance == null ? -1 : instance.soundPlayer.speed(); } @@ -251,6 +233,14 @@ command = new Execute(); playingUrl = null; - leadIn = Main.pref.getDouble("audio.leadin", 1.0 /* default, seconds */); - calibration = Main.pref.getDouble("audio.calibration", 1.0 /* default, ratio */); + double leadIn = Main.pref.getDouble("audio.leadin", 1.0 /* default, seconds */); + double calibration = Main.pref.getDouble("audio.calibration", 1.0 /* default, ratio */); + try { + soundPlayer = new JavaFxMediaPlayer(); + } catch (NoClassDefFoundError | InterruptedException e) { + Main.debug(e); + Main.warn("Java FX is unavailable. Falling back to Java Sound API"); + soundPlayer = new JavaSoundPlayer(leadIn, calibration); + } + soundPlayer.addAudioListener(this); start(); while (state == State.INITIALIZING) { @@ -263,12 +253,9 @@ * Not to be used as public, though Thread interface doesn't allow it to be made private */ - @Override public void run() { + @Override + public void run() { /* code running in separate thread */ playingUrl = null; - AudioInputStream audioInputStream = null; - SourceDataLine audioOutputLine = null; - AudioFormat audioFormat; - byte[] abData = new byte[(int) chunk]; for (;;) { @@ -285,27 +272,8 @@ case PLAYING: command.possiblyInterrupt(); - for (;;) { - int nBytesRead = 0; - if (audioInputStream != null) { - nBytesRead = audioInputStream.read(abData, 0, abData.length); - position += nBytesRead / bytesPerSecond; - } - command.possiblyInterrupt(); - if (nBytesRead < 0 || audioInputStream == null || audioOutputLine == null) { - break; - } - audioOutputLine.write(abData, 0, nBytesRead); // => int nBytesWritten - command.possiblyInterrupt(); + if (soundPlayer.playing(command)) { + playingUrl = null; + state = State.NOTPLAYING; } - // end of audio, clean up - if (audioOutputLine != null) { - audioOutputLine.drain(); - audioOutputLine.close(); - } - audioOutputLine = null; - Utils.close(audioInputStream); - audioInputStream = null; - playingUrl = null; - state = State.NOTPLAYING; command.possiblyInterrupt(); break; @@ -319,62 +287,9 @@ switch (command.command()) { case PLAY: - double offset = command.offset(); - speed = command.speed(); - if (playingUrl != command.url() || - stateChange != State.PAUSED || - offset != 0) { - if (audioInputStream != null) { - Utils.close(audioInputStream); - } - playingUrl = command.url(); - audioInputStream = AudioSystem.getAudioInputStream(playingUrl); - audioFormat = audioInputStream.getFormat(); - long nBytesRead; - position = 0.0; - offset -= leadIn; - double calibratedOffset = offset * calibration; - bytesPerSecond = audioFormat.getFrameRate() /* frames per second */ - * audioFormat.getFrameSize() /* bytes per frame */; - if (speed * bytesPerSecond > 256_000.0) { - speed = 256_000 / bytesPerSecond; - } - if (calibratedOffset > 0.0) { - long bytesToSkip = (long) (calibratedOffset /* seconds (double) */ * bytesPerSecond); - // skip doesn't seem to want to skip big chunks, so reduce it to smaller ones - while (bytesToSkip > chunk) { - nBytesRead = audioInputStream.skip(chunk); - if (nBytesRead <= 0) - throw new IOException(tr("This is after the end of the recording")); - bytesToSkip -= nBytesRead; - } - while (bytesToSkip > 0) { - long skippedBytes = audioInputStream.skip(bytesToSkip); - bytesToSkip -= skippedBytes; - if (skippedBytes == 0) { - // Avoid inifinite loop - Main.warn("Unable to skip bytes from audio input stream"); - bytesToSkip = 0; - } - } - position = offset; - } - if (audioOutputLine != null) { - audioOutputLine.close(); - } - audioFormat = new AudioFormat(audioFormat.getEncoding(), - audioFormat.getSampleRate() * (float) (speed * calibration), - audioFormat.getSampleSizeInBits(), - audioFormat.getChannels(), - audioFormat.getFrameSize(), - audioFormat.getFrameRate() * (float) (speed * calibration), - audioFormat.isBigEndian()); - DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat); - audioOutputLine = (SourceDataLine) AudioSystem.getLine(info); - audioOutputLine.open(audioFormat); - audioOutputLine.start(); - } + soundPlayer.play(command, stateChange, playingUrl); stateChange = State.PLAYING; break; case PAUSE: + soundPlayer.pause(command, stateChange, playingUrl); stateChange = State.PAUSED; break; @@ -382,10 +297,9 @@ } command.ok(stateChange); - } catch (LineUnavailableException | IOException | UnsupportedAudioFileException | - SecurityException | IllegalArgumentException startPlayingException) { + } catch (AudioException | IOException | SecurityException | IllegalArgumentException startPlayingException) { Main.error(startPlayingException); command.failed(startPlayingException); // sets state } - } catch (IOException e) { + } catch (AudioException | IOException e) { state = State.NOTPLAYING; Main.error(e); @@ -394,20 +308,7 @@ } - /** - * Shows a popup audio error message for the given exception. - * @param ex The exception used as error reason. Cannot be {@code null}. - */ - public static void audioMalfunction(Exception ex) { - String msg = ex.getMessage(); - if (msg == null) - msg = tr("unspecified reason"); - else - msg = tr(msg); - Main.error(msg); - if (!GraphicsEnvironment.isHeadless()) { - JOptionPane.showMessageDialog(Main.parent, - "
" + msg + "
", - tr("Error playing sound"), JOptionPane.ERROR_MESSAGE); - } + @Override + public void playing(URL playingURL) { + this.playingUrl = playingURL; } } Index: /trunk/src/org/openstreetmap/josm/io/audio/AudioUtil.java =================================================================== --- /trunk/src/org/openstreetmap/josm/io/audio/AudioUtil.java (revision 12327) +++ /trunk/src/org/openstreetmap/josm/io/audio/AudioUtil.java (revision 12328) @@ -2,4 +2,7 @@ package org.openstreetmap.josm.io.audio; +import static org.openstreetmap.josm.tools.I18n.tr; + +import java.awt.GraphicsEnvironment; import java.io.File; import java.io.IOException; @@ -10,4 +13,5 @@ import javax.sound.sampled.AudioSystem; import javax.sound.sampled.UnsupportedAudioFileException; +import javax.swing.JOptionPane; import org.openstreetmap.josm.Main; @@ -46,3 +50,22 @@ } } + + /** + * Shows a popup audio error message for the given exception. + * @param ex The exception used as error reason. Cannot be {@code null}. + * @since 12328 + */ + public static void audioMalfunction(Exception ex) { + String msg = ex.getMessage(); + if (msg == null) + msg = tr("unspecified reason"); + else + msg = tr(msg); + Main.error(msg); + if (!GraphicsEnvironment.isHeadless()) { + JOptionPane.showMessageDialog(Main.parent, + "" + msg + "
", + tr("Error playing sound"), JOptionPane.ERROR_MESSAGE); + } + } } Index: /trunk/src/org/openstreetmap/josm/io/audio/JavaFxMediaPlayer.java =================================================================== --- /trunk/src/org/openstreetmap/josm/io/audio/JavaFxMediaPlayer.java (revision 12328) +++ /trunk/src/org/openstreetmap/josm/io/audio/JavaFxMediaPlayer.java (revision 12328) @@ -0,0 +1,121 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.io.audio; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.concurrent.CountDownLatch; + +import org.openstreetmap.josm.io.audio.AudioPlayer.Execute; +import org.openstreetmap.josm.io.audio.AudioPlayer.State; +import org.openstreetmap.josm.tools.ListenerList; + +import com.sun.javafx.application.PlatformImpl; + +import javafx.scene.media.Media; +import javafx.scene.media.MediaException; +import javafx.scene.media.MediaPlayer; +import javafx.scene.media.MediaPlayer.Status; +import javafx.util.Duration; + +/** + * Default sound player based on the Java FX Media API. + * Used on platforms where Java FX is available. It supports the following audio codecs: