Index: src/org/openstreetmap/josm/actions/audio/AudioBackAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/audio/AudioBackAction.java	(revision 12326)
+++ src/org/openstreetmap/josm/actions/audio/AudioBackAction.java	(working copy)
@@ -13,6 +13,7 @@
 import org.openstreetmap.josm.actions.JosmAction;
 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;
 
 /**
@@ -39,7 +40,7 @@
             else
                 MarkerLayer.playAudio();
         } catch (IOException | InterruptedException ex) {
-            AudioPlayer.audioMalfunction(ex);
+            AudioUtil.audioMalfunction(ex);
         }
     }
 }
Index: src/org/openstreetmap/josm/actions/audio/AudioFastSlowAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/audio/AudioFastSlowAction.java	(revision 12326)
+++ src/org/openstreetmap/josm/actions/audio/AudioFastSlowAction.java	(working copy)
@@ -7,6 +7,7 @@
 import org.openstreetmap.josm.Main;
 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;
 
 /**
@@ -42,7 +43,7 @@
             if (AudioPlayer.playing() || AudioPlayer.paused())
                 AudioPlayer.play(AudioPlayer.url(), AudioPlayer.position(), speed * multiplier);
         } catch (IOException | InterruptedException ex) {
-            AudioPlayer.audioMalfunction(ex);
+            AudioUtil.audioMalfunction(ex);
         }
     }
 }
Index: src/org/openstreetmap/josm/actions/audio/AudioFwdAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/audio/AudioFwdAction.java	(revision 12326)
+++ src/org/openstreetmap/josm/actions/audio/AudioFwdAction.java	(working copy)
@@ -12,6 +12,7 @@
 import org.openstreetmap.josm.actions.JosmAction;
 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;
 
 /**
@@ -37,7 +38,7 @@
             else
                 MarkerLayer.playAudio();
         } catch (IOException | InterruptedException ex) {
-            AudioPlayer.audioMalfunction(ex);
+            AudioUtil.audioMalfunction(ex);
         }
     }
 }
Index: src/org/openstreetmap/josm/actions/audio/AudioPlayPauseAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/audio/AudioPlayPauseAction.java	(revision 12326)
+++ src/org/openstreetmap/josm/actions/audio/AudioPlayPauseAction.java	(working copy)
@@ -13,6 +13,7 @@
 import org.openstreetmap.josm.gui.layer.markerlayer.AudioMarker;
 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;
 
@@ -54,7 +55,7 @@
                 }
             }
         } catch (IOException | InterruptedException ex) {
-            AudioPlayer.audioMalfunction(ex);
+            AudioUtil.audioMalfunction(ex);
         }
     }
 }
Index: src/org/openstreetmap/josm/gui/layer/markerlayer/AudioMarker.java
===================================================================
--- src/org/openstreetmap/josm/gui/layer/markerlayer/AudioMarker.java	(revision 12326)
+++ src/org/openstreetmap/josm/gui/layer/markerlayer/AudioMarker.java	(working copy)
@@ -12,6 +12,7 @@
 import org.openstreetmap.josm.data.gpx.GpxLink;
 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;
 
 /**
@@ -59,7 +60,7 @@
             AudioPlayer.play(audioUrl, offset + syncOffset + after);
             recentlyPlayedMarker = this;
         } catch (IOException | InterruptedException e) {
-            AudioPlayer.audioMalfunction(e);
+            AudioUtil.audioMalfunction(e);
         }
     }
 
Index: src/org/openstreetmap/josm/gui/layer/markerlayer/PlayHeadMarker.java
===================================================================
--- src/org/openstreetmap/josm/gui/layer/markerlayer/PlayHeadMarker.java	(revision 12326)
+++ src/org/openstreetmap/josm/gui/layer/markerlayer/PlayHeadMarker.java	(working copy)
@@ -24,6 +24,7 @@
 import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.gui.layer.GpxLayer;
 import org.openstreetmap.josm.io.audio.AudioPlayer;
+import org.openstreetmap.josm.io.audio.AudioUtil;
 
 /**
  * Singleton marker class to track position of audio.
@@ -99,7 +100,7 @@
             try {
                 AudioPlayer.pause();
             } catch (IOException | InterruptedException ex) {
-                AudioPlayer.audioMalfunction(ex);
+                AudioUtil.audioMalfunction(ex);
             }
         }
     }
@@ -113,7 +114,7 @@
             try {
                 AudioPlayer.pause();
             } catch (IOException | InterruptedException ex) {
-                AudioPlayer.audioMalfunction(ex);
+                AudioUtil.audioMalfunction(ex);
             }
         }
         if (reset) {
Index: src/org/openstreetmap/josm/io/audio/AudioException.java
===================================================================
--- src/org/openstreetmap/josm/io/audio/AudioException.java	(nonexistent)
+++ src/org/openstreetmap/josm/io/audio/AudioException.java	(working copy)
@@ -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 xxx
+ */
+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: src/org/openstreetmap/josm/io/audio/AudioListener.java
===================================================================
--- src/org/openstreetmap/josm/io/audio/AudioListener.java	(nonexistent)
+++ src/org/openstreetmap/josm/io/audio/AudioListener.java	(working copy)
@@ -0,0 +1,19 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.io.audio;
+
+import java.net.URL;
+
+/**
+ * Listener receiving audio playing events.
+ * @since xxx
+ */
+interface AudioListener {
+
+    /**
+     * Called when a new URL is being played.
+     * @param playingURL new URL being played
+     * @param position position, in seconds
+     * @param speed speed factor
+     */
+    void playing(URL playingURL, double position, double speed);
+}
Index: src/org/openstreetmap/josm/io/audio/AudioPlayer.java
===================================================================
--- src/org/openstreetmap/josm/io/audio/AudioPlayer.java	(revision 12326)
+++ src/org/openstreetmap/josm/io/audio/AudioPlayer.java	(working copy)
@@ -1,24 +1,11 @@
 // License: GPL. For details, see LICENSE file.
 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;
 
 /**
  * Creates and controls a separate audio player thread.
@@ -27,29 +14,26 @@
  * @since 12326 (move to new package)
  * @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 }
+    enum State { INITIALIZING, NOTPLAYING, PLAYING, PAUSED, INTERRUPTED }
 
-    private enum Command { PLAY, PAUSE }
+    enum Command { PLAY, PAUSE }
 
-    private enum Result { WAITING, OK, FAILED }
+    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;
         private Exception exception;
@@ -84,7 +68,7 @@
                 throw new IOException(exception);
         }
 
-        private void possiblyInterrupt() throws InterruptedException {
+        protected void possiblyInterrupt() throws InterruptedException {
             if (interrupted() || result == Result.WAITING)
                 throw new InterruptedException();
         }
@@ -250,8 +234,16 @@
         state = State.INITIALIZING;
         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 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) {
             yield();
@@ -262,14 +254,11 @@
      * Starts the thread to actually play the audio, per Thread interface
      * 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 (;;) {
             try {
@@ -284,27 +273,7 @@
                         break;
                     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();
-                        }
-                        // end of audio, clean up
-                        if (audioOutputLine != null) {
-                            audioOutputLine.drain();
-                            audioOutputLine.close();
-                        }
-                        audioOutputLine = null;
-                        Utils.close(audioInputStream);
-                        audioInputStream = null;
+                        soundPlayer.play(command);
                         playingUrl = null;
                         state = State.NOTPLAYING;
                         command.possiblyInterrupt();
@@ -318,61 +287,7 @@
                 try {
                     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.playInterrupted(command, stateChange, playingUrl);
                             stateChange = State.PLAYING;
                             break;
                         case PAUSE:
@@ -381,12 +296,11 @@
                         default: // Do nothing
                     }
                     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);
             }
@@ -393,21 +307,10 @@
         }
     }
 
-    /**
-     * 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, double position, double speed) {
+        this.playingUrl = playingURL;
+        this.position = position;
+        this.speed = speed;
     }
 }
Index: src/org/openstreetmap/josm/io/audio/AudioUtil.java
===================================================================
--- src/org/openstreetmap/josm/io/audio/AudioUtil.java	(revision 12326)
+++ src/org/openstreetmap/josm/io/audio/AudioUtil.java	(working copy)
@@ -1,6 +1,9 @@
 // License: GPL. For details, see LICENSE file.
 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;
 import java.net.URL;
@@ -9,6 +12,7 @@
 import javax.sound.sampled.AudioInputStream;
 import javax.sound.sampled.AudioSystem;
 import javax.sound.sampled.UnsupportedAudioFileException;
+import javax.swing.JOptionPane;
 
 import org.openstreetmap.josm.Main;
 
@@ -45,4 +49,22 @@
             return 0.0;
         }
     }
+
+    /**
+     * 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);
+        }
+    }
 }
Index: src/org/openstreetmap/josm/io/audio/JavaFxMediaPlayer.java
===================================================================
--- src/org/openstreetmap/josm/io/audio/JavaFxMediaPlayer.java	(nonexistent)
+++ src/org/openstreetmap/josm/io/audio/JavaFxMediaPlayer.java	(working copy)
@@ -0,0 +1,43 @@
+// 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;
+import org.openstreetmap.josm.tools.ListenerList;
+
+/**
+ * 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 xxx
+ */
+class JavaFxMediaPlayer implements SoundPlayer {
+
+    private final ListenerList<AudioListener> listeners = ListenerList.create();
+
+    JavaFxMediaPlayer() {
+        throw new NoClassDefFoundError("test legacy first");
+    }
+
+    @Override
+    public void play(Execute command) throws AudioException, IOException {
+        // TODO Auto-generated method stub
+    }
+
+    @Override
+    public void playInterrupted(Execute command, State stateChange, URL playingUrl) throws AudioException, IOException {
+        // TODO Auto-generated method stub
+    }
+
+    @Override
+    public void addAudioListener(AudioListener listener) {
+        listeners.addWeakListener(listener);
+    }
+}
Index: src/org/openstreetmap/josm/io/audio/JavaSoundPlayer.java
===================================================================
--- src/org/openstreetmap/josm/io/audio/JavaSoundPlayer.java	(nonexistent)
+++ src/org/openstreetmap/josm/io/audio/JavaSoundPlayer.java	(working copy)
@@ -0,0 +1,153 @@
+// 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 xxx
+ */
+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) throws AudioException, IOException, InterruptedException {
+        for (;;) {
+            int nBytesRead = 0;
+            if (audioInputStream != null) {
+                nBytesRead = audioInputStream.read(abData, 0, abData.length);
+                position += nBytesRead / bytesPerSecond;
+                listeners.fireEvent(l -> l.playing(command.url(), position, speed));
+            }
+            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;
+    }
+
+    @Override
+    public void playInterrupted(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, position, speed));
+            try {
+                audioInputStream = AudioSystem.getAudioInputStream(url);
+            } catch (UnsupportedAudioFileException e) {
+                throw new AudioException(e);
+            }
+            audioFormat = audioInputStream.getFormat();
+            long nBytesRead;
+            position = 0.0;
+            listeners.fireEvent(l -> l.playing(url, position, speed));
+            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;
+                listeners.fireEvent(l -> l.playing(url, position, speed));
+            }
+            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;
+                listeners.fireEvent(l -> l.playing(url, position, speed));
+            }
+            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 addAudioListener(AudioListener listener) {
+        listeners.addWeakListener(listener);
+    }
+}
Index: src/org/openstreetmap/josm/io/audio/SoundPlayer.java
===================================================================
--- src/org/openstreetmap/josm/io/audio/SoundPlayer.java	(nonexistent)
+++ src/org/openstreetmap/josm/io/audio/SoundPlayer.java	(working copy)
@@ -0,0 +1,21 @@
+// 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 xxx
+ */
+interface SoundPlayer {
+
+    void play(Execute command) throws AudioException, IOException, InterruptedException;
+
+    void playInterrupted(Execute command, State stateChange, URL playingUrl) throws AudioException, IOException;
+
+    void addAudioListener(AudioListener listener);
+}
