Changeset 12328 in josm


Ignore:
Timestamp:
2017-06-07T21:41:26+02:00 (4 months ago)
Author:
Don-vip
Message:

fix #2089 - Add support for MP3, AIFF and AAC audio codecs (.mp3, .aac, .aif, .aiff files) if Java FX is on the classpath (i.e. Windows, macOS, nearly all major Linux distributions). The classes are not public on purpose, as the whole system will have to be simplified when all Linux distributions propose Java FX and so we can get rid of old Java Sound implementation.

Location:
trunk/src/org/openstreetmap/josm
Files:
5 added
11 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/actions/audio/AudioBackAction.java

    r12326 r12328  
    1414import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
    1515import org.openstreetmap.josm.io.audio.AudioPlayer;
     16import org.openstreetmap.josm.io.audio.AudioUtil;
    1617import org.openstreetmap.josm.tools.Shortcut;
    1718
     
    4041                MarkerLayer.playAudio();
    4142        } catch (IOException | InterruptedException ex) {
    42             AudioPlayer.audioMalfunction(ex);
     43            AudioUtil.audioMalfunction(ex);
    4344        }
    4445    }
  • trunk/src/org/openstreetmap/josm/actions/audio/AudioFastSlowAction.java

    r12326 r12328  
    88import org.openstreetmap.josm.actions.JosmAction;
    99import org.openstreetmap.josm.io.audio.AudioPlayer;
     10import org.openstreetmap.josm.io.audio.AudioUtil;
    1011import org.openstreetmap.josm.tools.Shortcut;
    1112
     
    4344                AudioPlayer.play(AudioPlayer.url(), AudioPlayer.position(), speed * multiplier);
    4445        } catch (IOException | InterruptedException ex) {
    45             AudioPlayer.audioMalfunction(ex);
     46            AudioUtil.audioMalfunction(ex);
    4647        }
    4748    }
  • trunk/src/org/openstreetmap/josm/actions/audio/AudioFwdAction.java

    r12326 r12328  
    1313import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
    1414import org.openstreetmap.josm.io.audio.AudioPlayer;
     15import org.openstreetmap.josm.io.audio.AudioUtil;
    1516import org.openstreetmap.josm.tools.Shortcut;
    1617
     
    3839                MarkerLayer.playAudio();
    3940        } catch (IOException | InterruptedException ex) {
    40             AudioPlayer.audioMalfunction(ex);
     41            AudioUtil.audioMalfunction(ex);
    4142        }
    4243    }
  • trunk/src/org/openstreetmap/josm/actions/audio/AudioPlayPauseAction.java

    r12326 r12328  
    1414import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
    1515import org.openstreetmap.josm.io.audio.AudioPlayer;
     16import org.openstreetmap.josm.io.audio.AudioUtil;
    1617import org.openstreetmap.josm.tools.Shortcut;
    1718import org.openstreetmap.josm.tools.Utils;
     
    5556            }
    5657        } catch (IOException | InterruptedException ex) {
    57             AudioPlayer.audioMalfunction(ex);
     58            AudioUtil.audioMalfunction(ex);
    5859        }
    5960    }
  • trunk/src/org/openstreetmap/josm/gui/layer/gpx/ImportAudioAction.java

    r12326 r12328  
    4141    private final transient GpxLayer layer;
    4242
    43     static final class AudioFileFilter extends FileFilter {
     43    /**
     44     * Audio file filter.
     45     * @since 12328
     46     */
     47    public static final class AudioFileFilter extends FileFilter {
    4448        @Override
    4549        public boolean accept(File f) {
    46             return f.isDirectory() || Utils.hasExtension(f, "wav");
     50            return f.isDirectory() || Utils.hasExtension(f, "wav", "mp3", "aac", "aif", "aiff");
    4751        }
    4852
    4953        @Override
    5054        public String getDescription() {
    51             return tr("Wave Audio files (*.wav)");
     55            return tr("Audio files (*.wav, *.mp3, *.aac, *.aif, *.aiff)");
    5256        }
    5357    }
     
    119123     * which the given audio file is associated with. Markers are derived from the following (a)
    120124     * explict waypoints in the GPX layer, or (b) named trackpoints in the GPX layer, or (d)
    121      * timestamp on the wav file (e) (in future) voice recognised markers in the sound recording (f)
     125     * timestamp on the audio file (e) (in future) voice recognised markers in the sound recording (f)
    122126     * a single marker at the beginning of the track
    123      * @param wavFile the file to be associated with the markers in the new marker layer
     127     * @param audioFile the file to be associated with the markers in the new marker layer
    124128     * @param ml marker layer
    125129     * @param firstStartTime first start time in milliseconds, used for (d)
    126130     * @param markers keeps track of warning messages to avoid repeated warnings
    127131     */
    128     private void importAudio(File wavFile, MarkerLayer ml, double firstStartTime, Markers markers) {
    129         URL url = Utils.fileToURL(wavFile);
     132    private void importAudio(File audioFile, MarkerLayer ml, double firstStartTime, Markers markers) {
     133        URL url = Utils.fileToURL(audioFile);
    130134        boolean hasTracks = layer.data.tracks != null && !layer.data.tracks.isEmpty();
    131135        boolean hasWaypoints = layer.data.waypoints != null && !layer.data.waypoints.isEmpty();
     
    212216        // (d) use timestamp of file as location on track
    213217        if (hasTracks && Main.pref.getBoolean("marker.audiofromwavtimestamps", false)) {
    214             double lastModified = wavFile.lastModified() / 1000.0; // lastModified is in
    215             // milliseconds
    216             double duration = AudioUtil.getCalibratedDuration(wavFile);
     218            double lastModified = audioFile.lastModified() / 1000.0; // lastModified is in milliseconds
     219            double duration = AudioUtil.getCalibratedDuration(audioFile);
    217220            double startTime = lastModified - duration;
    218221            startTime = firstStartTime + (startTime - firstStartTime)
     
    242245                        (startTime - w1.time) / (w2.time - w1.time)));
    243246                wayPointFromTimeStamp.time = startTime;
    244                 String name = wavFile.getName();
     247                String name = audioFile.getName();
    245248                int dot = name.lastIndexOf('.');
    246249                if (dot > 0) {
  • trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/AudioMarker.java

    r12326 r12328  
    1313import org.openstreetmap.josm.data.gpx.WayPoint;
    1414import org.openstreetmap.josm.io.audio.AudioPlayer;
     15import org.openstreetmap.josm.io.audio.AudioUtil;
    1516import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider;
    1617
     
    6061            recentlyPlayedMarker = this;
    6162        } catch (IOException | InterruptedException e) {
    62             AudioPlayer.audioMalfunction(e);
     63            AudioUtil.audioMalfunction(e);
    6364        }
    6465    }
  • trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/DefaultMarkerProducers.java

    r11892 r12328  
    4444        if (url == null) {
    4545            return Collections.singleton(marker);
    46         } else if (urlStr.endsWith(".wav")) {
     46        } else if (Utils.hasExtension(urlStr, "wav", "mp3", "aac", "aif", "aiff")) {
    4747            final AudioMarker audioMarker = new AudioMarker(wpt.getCoor(), wpt, url, parentLayer, time, offset);
    4848            Extensions exts = (Extensions) wpt.get(GpxConstants.META_EXTENSIONS);
     
    5555            }
    5656            return Arrays.asList(marker, audioMarker);
    57         } else if (urlStr.endsWith(".png") || urlStr.endsWith(".jpg") || urlStr.endsWith(".jpeg")
    58                 || urlStr.endsWith(".gif")) {
     57        } else if (Utils.hasExtension(urlStr, "png", "jpg", "jpeg", "gif")) {
    5958            return Arrays.asList(marker, new ImageMarker(wpt.getCoor(), url, parentLayer, time, offset));
    6059        } else {
  • trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/Marker.java

    r11892 r12328  
    5353 *
    5454 * By default, one the list contains one default "Maker" implementation that
    55  * will create AudioMarkers for .wav files, ImageMarkers for .png/.jpg/.jpeg
     55 * will create AudioMarkers for supported audio files, ImageMarkers for supported image
    5656 * files, and WebMarkers for everything else. (The creation of a WebMarker will
    5757 * fail if there's no valid URL in the <link> tag, so it might still make sense
  • trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/PlayHeadMarker.java

    r12326 r12328  
    2525import org.openstreetmap.josm.gui.layer.GpxLayer;
    2626import org.openstreetmap.josm.io.audio.AudioPlayer;
     27import org.openstreetmap.josm.io.audio.AudioUtil;
    2728
    2829/**
     
    100101                AudioPlayer.pause();
    101102            } catch (IOException | InterruptedException ex) {
    102                 AudioPlayer.audioMalfunction(ex);
     103                AudioUtil.audioMalfunction(ex);
    103104            }
    104105        }
     
    114115                AudioPlayer.pause();
    115116            } catch (IOException | InterruptedException ex) {
    116                 AudioPlayer.audioMalfunction(ex);
     117                AudioUtil.audioMalfunction(ex);
    117118            }
    118119        }
  • trunk/src/org/openstreetmap/josm/io/audio/AudioPlayer.java

    r12326 r12328  
    22package org.openstreetmap.josm.io.audio;
    33
    4 import static org.openstreetmap.josm.tools.I18n.tr;
    5 
    6 import java.awt.GraphicsEnvironment;
    74import java.io.IOException;
    85import java.net.URL;
    96
    10 import javax.sound.sampled.AudioFormat;
    11 import javax.sound.sampled.AudioInputStream;
    12 import javax.sound.sampled.AudioSystem;
    13 import javax.sound.sampled.DataLine;
    14 import javax.sound.sampled.LineUnavailableException;
    15 import javax.sound.sampled.SourceDataLine;
    16 import javax.sound.sampled.UnsupportedAudioFileException;
    17 import javax.swing.JOptionPane;
    18 
    197import org.openstreetmap.josm.Main;
    208import org.openstreetmap.josm.tools.JosmRuntimeException;
    21 import org.openstreetmap.josm.tools.Utils;
    229
    2310/**
     
    2815 * @since 547
    2916 */
    30 public final class AudioPlayer extends Thread {
     17public final class AudioPlayer extends Thread implements AudioListener {
    3118
    3219    private static volatile AudioPlayer audioPlayer;
    3320
    34     private enum State { INITIALIZING, NOTPLAYING, PLAYING, PAUSED, INTERRUPTED }
    35 
    36     private enum Command { PLAY, PAUSE }
    37 
    38     private enum Result { WAITING, OK, FAILED }
     21    enum State { INITIALIZING, NOTPLAYING, PLAYING, PAUSED, INTERRUPTED }
     22
     23    enum Command { PLAY, PAUSE }
     24
     25    enum Result { WAITING, OK, FAILED }
    3926
    4027    private State state;
     28    private SoundPlayer soundPlayer;
    4129    private URL playingUrl;
    42     private final double leadIn; // seconds
    43     private final double calibration; // ratio of purported duration of samples to true duration
    44     private double position; // seconds
    45     private double bytesPerSecond;
    46     private static long chunk = 4000; /* bytes */
    47     private double speed = 1.0;
    4830
    4931    /**
    5032     * Passes information from the control thread to the playing thread
    5133     */
    52     private class Execute {
     34    class Execute {
    5335        private Command command;
    5436        private Result result;
     
    8567        }
    8668
    87         private void possiblyInterrupt() throws InterruptedException {
     69        protected void possiblyInterrupt() throws InterruptedException {
    8870            if (interrupted() || result == Result.WAITING)
    8971                throw new InterruptedException();
     
    204186    public static double position() {
    205187        AudioPlayer instance = AudioPlayer.getInstance();
    206         return instance == null ? -1 : instance.position;
     188        return instance == null ? -1 : instance.soundPlayer.position();
    207189    }
    208190
     
    213195    public static double speed() {
    214196        AudioPlayer instance = AudioPlayer.getInstance();
    215         return instance == null ? -1 : instance.speed;
     197        return instance == null ? -1 : instance.soundPlayer.speed();
    216198    }
    217199
     
    251233        command = new Execute();
    252234        playingUrl = null;
    253         leadIn = Main.pref.getDouble("audio.leadin", 1.0 /* default, seconds */);
    254         calibration = Main.pref.getDouble("audio.calibration", 1.0 /* default, ratio */);
     235        double leadIn = Main.pref.getDouble("audio.leadin", 1.0 /* default, seconds */);
     236        double calibration = Main.pref.getDouble("audio.calibration", 1.0 /* default, ratio */);
     237        try {
     238            soundPlayer = new JavaFxMediaPlayer();
     239        } catch (NoClassDefFoundError | InterruptedException e) {
     240            Main.debug(e);
     241            Main.warn("Java FX is unavailable. Falling back to Java Sound API");
     242            soundPlayer = new JavaSoundPlayer(leadIn, calibration);
     243        }
     244        soundPlayer.addAudioListener(this);
    255245        start();
    256246        while (state == State.INITIALIZING) {
     
    263253     * Not to be used as public, though Thread interface doesn't allow it to be made private
    264254     */
    265     @Override public void run() {
     255    @Override
     256    public void run() {
    266257        /* code running in separate thread */
    267258
    268259        playingUrl = null;
    269         AudioInputStream audioInputStream = null;
    270         SourceDataLine audioOutputLine = null;
    271         AudioFormat audioFormat;
    272         byte[] abData = new byte[(int) chunk];
    273260
    274261        for (;;) {
     
    285272                    case PLAYING:
    286273                        command.possiblyInterrupt();
    287                         for (;;) {
    288                             int nBytesRead = 0;
    289                             if (audioInputStream != null) {
    290                                 nBytesRead = audioInputStream.read(abData, 0, abData.length);
    291                                 position += nBytesRead / bytesPerSecond;
    292                             }
    293                             command.possiblyInterrupt();
    294                             if (nBytesRead < 0 || audioInputStream == null || audioOutputLine == null) {
    295                                 break;
    296                             }
    297                             audioOutputLine.write(abData, 0, nBytesRead); // => int nBytesWritten
    298                             command.possiblyInterrupt();
     274                        if (soundPlayer.playing(command)) {
     275                            playingUrl = null;
     276                            state = State.NOTPLAYING;
    299277                        }
    300                         // end of audio, clean up
    301                         if (audioOutputLine != null) {
    302                             audioOutputLine.drain();
    303                             audioOutputLine.close();
    304                         }
    305                         audioOutputLine = null;
    306                         Utils.close(audioInputStream);
    307                         audioInputStream = null;
    308                         playingUrl = null;
    309                         state = State.NOTPLAYING;
    310278                        command.possiblyInterrupt();
    311279                        break;
     
    319287                    switch (command.command()) {
    320288                        case PLAY:
    321                             double offset = command.offset();
    322                             speed = command.speed();
    323                             if (playingUrl != command.url() ||
    324                                     stateChange != State.PAUSED ||
    325                                     offset != 0) {
    326                                 if (audioInputStream != null) {
    327                                     Utils.close(audioInputStream);
    328                                 }
    329                                 playingUrl = command.url();
    330                                 audioInputStream = AudioSystem.getAudioInputStream(playingUrl);
    331                                 audioFormat = audioInputStream.getFormat();
    332                                 long nBytesRead;
    333                                 position = 0.0;
    334                                 offset -= leadIn;
    335                                 double calibratedOffset = offset * calibration;
    336                                 bytesPerSecond = audioFormat.getFrameRate() /* frames per second */
    337                                 * audioFormat.getFrameSize() /* bytes per frame */;
    338                                 if (speed * bytesPerSecond > 256_000.0) {
    339                                     speed = 256_000 / bytesPerSecond;
    340                                 }
    341                                 if (calibratedOffset > 0.0) {
    342                                     long bytesToSkip = (long) (calibratedOffset /* seconds (double) */ * bytesPerSecond);
    343                                     // skip doesn't seem to want to skip big chunks, so reduce it to smaller ones
    344                                     while (bytesToSkip > chunk) {
    345                                         nBytesRead = audioInputStream.skip(chunk);
    346                                         if (nBytesRead <= 0)
    347                                             throw new IOException(tr("This is after the end of the recording"));
    348                                         bytesToSkip -= nBytesRead;
    349                                     }
    350                                     while (bytesToSkip > 0) {
    351                                         long skippedBytes = audioInputStream.skip(bytesToSkip);
    352                                         bytesToSkip -= skippedBytes;
    353                                         if (skippedBytes == 0) {
    354                                             // Avoid inifinite loop
    355                                             Main.warn("Unable to skip bytes from audio input stream");
    356                                             bytesToSkip = 0;
    357                                         }
    358                                     }
    359                                     position = offset;
    360                                 }
    361                                 if (audioOutputLine != null) {
    362                                     audioOutputLine.close();
    363                                 }
    364                                 audioFormat = new AudioFormat(audioFormat.getEncoding(),
    365                                         audioFormat.getSampleRate() * (float) (speed * calibration),
    366                                         audioFormat.getSampleSizeInBits(),
    367                                         audioFormat.getChannels(),
    368                                         audioFormat.getFrameSize(),
    369                                         audioFormat.getFrameRate() * (float) (speed * calibration),
    370                                         audioFormat.isBigEndian());
    371                                 DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
    372                                 audioOutputLine = (SourceDataLine) AudioSystem.getLine(info);
    373                                 audioOutputLine.open(audioFormat);
    374                                 audioOutputLine.start();
    375                             }
     289                            soundPlayer.play(command, stateChange, playingUrl);
    376290                            stateChange = State.PLAYING;
    377291                            break;
    378292                        case PAUSE:
     293                            soundPlayer.pause(command, stateChange, playingUrl);
    379294                            stateChange = State.PAUSED;
    380295                            break;
     
    382297                    }
    383298                    command.ok(stateChange);
    384                 } catch (LineUnavailableException | IOException | UnsupportedAudioFileException |
    385                         SecurityException | IllegalArgumentException startPlayingException) {
     299                } catch (AudioException | IOException | SecurityException | IllegalArgumentException startPlayingException) {
    386300                    Main.error(startPlayingException);
    387301                    command.failed(startPlayingException); // sets state
    388302                }
    389             } catch (IOException e) {
     303            } catch (AudioException | IOException e) {
    390304                state = State.NOTPLAYING;
    391305                Main.error(e);
     
    394308    }
    395309
    396     /**
    397      * Shows a popup audio error message for the given exception.
    398      * @param ex The exception used as error reason. Cannot be {@code null}.
    399      */
    400     public static void audioMalfunction(Exception ex) {
    401         String msg = ex.getMessage();
    402         if (msg == null)
    403             msg = tr("unspecified reason");
    404         else
    405             msg = tr(msg);
    406         Main.error(msg);
    407         if (!GraphicsEnvironment.isHeadless()) {
    408             JOptionPane.showMessageDialog(Main.parent,
    409                     "<html><p>" + msg + "</p></html>",
    410                     tr("Error playing sound"), JOptionPane.ERROR_MESSAGE);
    411         }
     310    @Override
     311    public void playing(URL playingURL) {
     312        this.playingUrl = playingURL;
    412313    }
    413314}
  • trunk/src/org/openstreetmap/josm/io/audio/AudioUtil.java

    r12326 r12328  
    22package org.openstreetmap.josm.io.audio;
    33
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.awt.GraphicsEnvironment;
    47import java.io.File;
    58import java.io.IOException;
     
    1013import javax.sound.sampled.AudioSystem;
    1114import javax.sound.sampled.UnsupportedAudioFileException;
     15import javax.swing.JOptionPane;
    1216
    1317import org.openstreetmap.josm.Main;
     
    4650        }
    4751    }
     52
     53    /**
     54     * Shows a popup audio error message for the given exception.
     55     * @param ex The exception used as error reason. Cannot be {@code null}.
     56     * @since 12328
     57     */
     58    public static void audioMalfunction(Exception ex) {
     59        String msg = ex.getMessage();
     60        if (msg == null)
     61            msg = tr("unspecified reason");
     62        else
     63            msg = tr(msg);
     64        Main.error(msg);
     65        if (!GraphicsEnvironment.isHeadless()) {
     66            JOptionPane.showMessageDialog(Main.parent,
     67                    "<html><p>" + msg + "</p></html>",
     68                    tr("Error playing sound"), JOptionPane.ERROR_MESSAGE);
     69        }
     70    }
    4871}
Note: See TracChangeset for help on using the changeset viewer.