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 final 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 infinite 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}