Ignore:
Timestamp:
2017-06-07T21:41:26+02:00 (2 years 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/io/audio
Files:
5 added
2 edited

Legend:

Unmodified
Added
Removed
  • 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.