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 &lt;link&gt; 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,
-                    "<html><p>" + msg + "</p></html>",
-                    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,
+                    "<html><p>" + msg + "</p></html>",
+                    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:<ul>
+ * <li>MP3</li>
+ * <li>AIFF containing uncompressed PCM</li>
+ * <li>WAV containing uncompressed PCM</li>
+ * <li>MPEG-4 multimedia container with Advanced Audio Coding (AAC) audio</li>
+ * </ul>
+ * @since 12328
+ */
+class JavaFxMediaPlayer implements SoundPlayer {
+
+    private final ListenerList<AudioListener> listeners = ListenerList.create();
+
+    private URL url;
+    private Media media;
+    private MediaPlayer mediaPlayer;
+
+    JavaFxMediaPlayer() throws InterruptedException {
+        initFxPlatform();
+    }
+
+    /**
+     * Initializes the JavaFX platform runtime.
+     * @throws InterruptedException if the current thread is interrupted while waiting
+     */
+    public static void initFxPlatform() throws InterruptedException {
+        final CountDownLatch startupLatch = new CountDownLatch(1);
+
+        // Note, this method is called on the FX Application Thread
+        PlatformImpl.startup(() -> startupLatch.countDown());
+
+        // Wait for FX platform to start
+        startupLatch.await();
+    }
+
+    @Override
+    public void play(Execute command, State stateChange, URL playingUrl) throws AudioException, IOException {
+        try {
+            url = command.url();
+            if (playingUrl != url) {
+                if (mediaPlayer != null) {
+                    mediaPlayer.stop();
+                }
+                // Fail fast in case of invalid local URI (JavaFX Media locator retries 5 times with a 1 second delay)
+                if ("file".equals(url.getProtocol()) && !new File(url.toURI()).exists()) {
+                    throw new FileNotFoundException(url.toString());
+                }
+                media = new Media(url.toString());
+                mediaPlayer = new MediaPlayer(media);
+                mediaPlayer.setOnPlaying(() -> {
+                    listeners.fireEvent(l -> l.playing(url));
+                });
+            }
+            mediaPlayer.setRate(command.speed());
+            if (Status.PLAYING == mediaPlayer.getStatus()) {
+                Duration seekTime = Duration.seconds(command.offset());
+                if (!seekTime.equals(mediaPlayer.getCurrentTime())) {
+                    mediaPlayer.seek(seekTime);
+                }
+            }
+            mediaPlayer.play();
+        } catch (MediaException | URISyntaxException e) {
+            throw new AudioException(e);
+        }
+    }
+
+    @Override
+    public void pause(Execute command, State stateChange, URL playingUrl) throws AudioException, IOException {
+        if (mediaPlayer != null) {
+            try {
+                mediaPlayer.pause();
+            } catch (MediaException e) {
+                throw new AudioException(e);
+            }
+        }
+    }
+
+    @Override
+    public boolean playing(Execute command) throws AudioException, IOException, InterruptedException {
+        // Not used: JavaFX handles the low-level audio playback
+        return false;
+    }
+
+    @Override
+    public double position() {
+        return mediaPlayer.getCurrentTime().toSeconds();
+    }
+
+    @Override
+    public double speed() {
+        return mediaPlayer.getCurrentRate();
+    }
+
+    @Override
+    public void addAudioListener(AudioListener listener) {
+        listeners.addWeakListener(listener);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/io/audio/JavaSoundPlayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/audio/JavaSoundPlayer.java	(revision 12328)
+++ trunk/src/org/openstreetmap/josm/io/audio/JavaSoundPlayer.java	(revision 12328)
@@ -0,0 +1,166 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.io.audio;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+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 org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.io.audio.AudioPlayer.Execute;
+import org.openstreetmap.josm.io.audio.AudioPlayer.State;
+import org.openstreetmap.josm.tools.ListenerList;
+import org.openstreetmap.josm.tools.Utils;
+
+/**
+ * Legacy sound player based on the Java Sound API.
+ * Used on platforms where Java FX is not yet available. It supports only WAV files.
+ * @since 12328
+ */
+class JavaSoundPlayer implements SoundPlayer {
+
+    private static int chunk = 4000; /* bytes */
+
+    private AudioInputStream audioInputStream;
+    private SourceDataLine audioOutputLine;
+    private AudioFormat audioFormat;
+
+    private final double leadIn; // seconds
+    private final double calibration; // ratio of purported duration of samples to true duration
+
+    private double bytesPerSecond;
+    private byte[] abData = new byte[chunk];
+
+    private double position; // seconds
+    private double speed = 1.0;
+
+    private final ListenerList<AudioListener> listeners = ListenerList.create();
+
+    JavaSoundPlayer(double leadIn, double calibration) {
+        this.leadIn = leadIn;
+        this.calibration = calibration;
+    }
+
+    @Override
+    public void play(Execute command, State stateChange, URL playingUrl) throws AudioException, IOException {
+        final URL url = command.url();
+        double offset = command.offset();
+        speed = command.speed();
+        if (playingUrl != url ||
+                stateChange != State.PAUSED ||
+                offset != 0) {
+            if (audioInputStream != null) {
+                Utils.close(audioInputStream);
+            }
+            listeners.fireEvent(l -> l.playing(url));
+            try {
+                audioInputStream = AudioSystem.getAudioInputStream(url);
+            } catch (UnsupportedAudioFileException e) {
+                throw new AudioException(e);
+            }
+            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());
+            try {
+                DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
+                audioOutputLine = (SourceDataLine) AudioSystem.getLine(info);
+                audioOutputLine.open(audioFormat);
+                audioOutputLine.start();
+            } catch (LineUnavailableException e) {
+                throw new AudioException(e);
+            }
+        }
+    }
+
+    @Override
+    public void pause(Execute command, State stateChange, URL playingUrl) throws AudioException, IOException {
+        // Do nothing. As we are very low level, the playback is paused if we stop writing to audio output line
+    }
+
+    @Override
+    public boolean playing(Execute command) throws AudioException, IOException, InterruptedException {
+        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();
+        }
+        // end of audio, clean up
+        if (audioOutputLine != null) {
+            audioOutputLine.drain();
+            audioOutputLine.close();
+        }
+        audioOutputLine = null;
+        Utils.close(audioInputStream);
+        audioInputStream = null;
+        speed = 0;
+        return true;
+    }
+
+    @Override
+    public double position() {
+        return position;
+    }
+
+    @Override
+    public double speed() {
+        return speed;
+    }
+
+    @Override
+    public void addAudioListener(AudioListener listener) {
+        listeners.addWeakListener(listener);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/io/audio/SoundPlayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/audio/SoundPlayer.java	(revision 12328)
+++ trunk/src/org/openstreetmap/josm/io/audio/SoundPlayer.java	(revision 12328)
@@ -0,0 +1,63 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.io.audio;
+
+import java.io.IOException;
+import java.net.URL;
+
+import org.openstreetmap.josm.io.audio.AudioPlayer.Execute;
+import org.openstreetmap.josm.io.audio.AudioPlayer.State;
+
+/**
+ * Sound player interface. Implementations can be backed up by Java Sound API or Java FX Media API.
+ * @since 12328
+ */
+interface SoundPlayer {
+
+    /**
+     * Ask player to play a new media.
+     * @param command Command containing media information
+     * @param stateChange the previous state
+     * @param playingUrl the currently playing URL, if any
+     * @throws AudioException if an audio error occurs
+     * @throws IOException if an I/O error occurs
+     */
+    void play(Execute command, State stateChange, URL playingUrl) throws AudioException, IOException;
+
+    /**
+     * Ask player to pause the current playing media.
+     * @param command Command containing media information
+     * @param stateChange the previous state
+     * @param playingUrl the currently playing URL, if any
+     * @throws AudioException if an audio error occurs
+     * @throws IOException if an I/O error occurs
+     */
+    void pause(Execute command, State stateChange, URL playingUrl) throws AudioException, IOException;
+
+    /**
+     * Method called when a media is being played.
+     * @param command Command containing media information
+     * @return {@code true} if the playing call was blocking, and the playback is finished when this method returns
+     * @throws AudioException if an audio error occurs
+     * @throws IOException if an I/O error occurs
+     * @throws InterruptedException if the play is interrupted
+     */
+    boolean playing(Execute command) throws AudioException, IOException, InterruptedException;
+
+    /**
+     * Returns the media playback position, in seconds.
+     * @return the media playback position, in seconds
+     */
+    double position();
+
+    /**
+     * Returns the media playback speed ratio.
+     * @return the media playback speed ratio
+     */
+    double speed();
+
+    /**
+     * Adds a listener that will be notified of audio playback events.
+     * @param listener audio listener
+     */
+    void addAudioListener(AudioListener listener);
+}
