001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io.audio;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.IOException;
007import java.net.URL;
008
009import javax.sound.sampled.AudioFormat;
010import javax.sound.sampled.AudioInputStream;
011import javax.sound.sampled.AudioSystem;
012import javax.sound.sampled.DataLine;
013import javax.sound.sampled.LineUnavailableException;
014import javax.sound.sampled.SourceDataLine;
015import javax.sound.sampled.UnsupportedAudioFileException;
016
017import org.openstreetmap.josm.io.audio.AudioPlayer.Execute;
018import org.openstreetmap.josm.io.audio.AudioPlayer.State;
019import org.openstreetmap.josm.tools.ListenerList;
020import org.openstreetmap.josm.tools.Logging;
021import org.openstreetmap.josm.tools.Utils;
022
023/**
024 * Legacy sound player based on the Java Sound API.
025 * Used on platforms where Java FX is not yet available. It supports only WAV files.
026 * @since 12328
027 */
028class JavaSoundPlayer implements SoundPlayer {
029
030    private static int chunk = 4000; /* bytes */
031
032    private AudioInputStream audioInputStream;
033    private SourceDataLine audioOutputLine;
034
035    private final double leadIn; // seconds
036    private final double calibration; // ratio of purported duration of samples to true duration
037
038    private double bytesPerSecond;
039    private final byte[] abData = new byte[chunk];
040
041    private double position; // seconds
042    private double speed = 1.0;
043
044    private final ListenerList<AudioListener> listeners = ListenerList.create();
045
046    JavaSoundPlayer(double leadIn, double calibration) {
047        this.leadIn = leadIn;
048        this.calibration = calibration;
049    }
050
051    @Override
052    public void play(Execute command, State stateChange, URL playingUrl) throws AudioException, IOException {
053        final URL url = command.url();
054        double offset = command.offset();
055        speed = command.speed();
056        if (playingUrl != url ||
057                stateChange != State.PAUSED ||
058                offset != 0) {
059            if (audioInputStream != null) {
060                Utils.close(audioInputStream);
061            }
062            listeners.fireEvent(l -> l.playing(url));
063            try {
064                audioInputStream = AudioSystem.getAudioInputStream(url);
065            } catch (UnsupportedAudioFileException e) {
066                throw new AudioException(e);
067            }
068            AudioFormat audioFormat = audioInputStream.getFormat();
069            long nBytesRead;
070            position = 0.0;
071            offset -= leadIn;
072            double calibratedOffset = offset * calibration;
073            bytesPerSecond = audioFormat.getFrameRate() /* frames per second */
074            * audioFormat.getFrameSize() /* bytes per frame */;
075            if (speed * bytesPerSecond > 256_000.0) {
076                speed = 256_000 / bytesPerSecond;
077            }
078            if (calibratedOffset > 0.0) {
079                long bytesToSkip = (long) (calibratedOffset /* seconds (double) */ * bytesPerSecond);
080                // skip doesn't seem to want to skip big chunks, so reduce it to smaller ones
081                while (bytesToSkip > chunk) {
082                    nBytesRead = audioInputStream.skip(chunk);
083                    if (nBytesRead <= 0)
084                        throw new IOException(tr("This is after the end of the recording"));
085                    bytesToSkip -= nBytesRead;
086                }
087                while (bytesToSkip > 0) {
088                    long skippedBytes = audioInputStream.skip(bytesToSkip);
089                    bytesToSkip -= skippedBytes;
090                    if (skippedBytes == 0) {
091                        // Avoid inifinite loop
092                        Logging.warn("Unable to skip bytes from audio input stream");
093                        bytesToSkip = 0;
094                    }
095                }
096                position = offset;
097            }
098            if (audioOutputLine != null) {
099                audioOutputLine.close();
100            }
101            audioFormat = new AudioFormat(audioFormat.getEncoding(),
102                    audioFormat.getSampleRate() * (float) (speed * calibration),
103                    audioFormat.getSampleSizeInBits(),
104                    audioFormat.getChannels(),
105                    audioFormat.getFrameSize(),
106                    audioFormat.getFrameRate() * (float) (speed * calibration),
107                    audioFormat.isBigEndian());
108            try {
109                DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
110                audioOutputLine = (SourceDataLine) AudioSystem.getLine(info);
111                audioOutputLine.open(audioFormat);
112                audioOutputLine.start();
113            } catch (LineUnavailableException e) {
114                throw new AudioException(e);
115            }
116        }
117    }
118
119    @Override
120    public void pause(Execute command, State stateChange, URL playingUrl) throws AudioException, IOException {
121        // Do nothing. As we are very low level, the playback is paused if we stop writing to audio output line
122    }
123
124    @Override
125    public boolean playing(Execute command) throws AudioException, IOException, InterruptedException {
126        for (;;) {
127            int nBytesRead = 0;
128            if (audioInputStream != null) {
129                nBytesRead = audioInputStream.read(abData, 0, abData.length);
130                position += nBytesRead / bytesPerSecond;
131            }
132            command.possiblyInterrupt();
133            if (nBytesRead < 0 || audioInputStream == null || audioOutputLine == null) {
134                break;
135            }
136            audioOutputLine.write(abData, 0, nBytesRead); // => int nBytesWritten
137            command.possiblyInterrupt();
138        }
139        // end of audio, clean up
140        if (audioOutputLine != null) {
141            audioOutputLine.drain();
142            audioOutputLine.close();
143        }
144        audioOutputLine = null;
145        Utils.close(audioInputStream);
146        audioInputStream = null;
147        speed = 0;
148        return true;
149    }
150
151    @Override
152    public double position() {
153        return position;
154    }
155
156    @Override
157    public double speed() {
158        return speed;
159    }
160
161    @Override
162    public void addAudioListener(AudioListener listener) {
163        listeners.addWeakListener(listener);
164    }
165}