source: josm/trunk/src/org/openstreetmap/josm/io/audio/AudioPlayer.java@ 14628

Last change on this file since 14628 was 14195, checked in by Don-vip, 6 years ago

spotbugs - LI_LAZY_INIT_STATIC

  • Property svn:eol-style set to native
File size: 12.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io.audio;
3
4import java.io.IOException;
5import java.net.URL;
6import java.util.Objects;
7
8import org.openstreetmap.josm.spi.preferences.Config;
9import org.openstreetmap.josm.tools.JosmRuntimeException;
10import org.openstreetmap.josm.tools.Logging;
11
12/**
13 * Creates and controls a separate audio player thread.
14 *
15 * @author David Earl <david@frankieandshadow.com>
16 * @since 12326 (move to new package)
17 * @since 547
18 */
19public final class AudioPlayer extends Thread implements AudioListener {
20
21 private static volatile AudioPlayer audioPlayer;
22
23 /**
24 * Audio player state.
25 */
26 public enum State {
27 /** Initializing */
28 INITIALIZING,
29 /** Not playing */
30 NOTPLAYING,
31 /** Playing */
32 PLAYING,
33 /** Paused */
34 PAUSED,
35 /** Interrupted */
36 INTERRUPTED
37 }
38
39 /**
40 * Audio player command.
41 */
42 public enum Command { /** Audio play */ PLAY, /** Audio pause */ PAUSE }
43
44 /**
45 * Audio player result.
46 */
47 public enum Result { /** In progress */ WAITING, /** Success */ OK, /** Failure */ FAILED }
48
49 private State state;
50 private static volatile Class<? extends SoundPlayer> soundPlayerClass;
51 private SoundPlayer soundPlayer;
52 private URL playingUrl;
53
54 /**
55 * Passes information from the control thread to the playing thread
56 */
57 public class Execute {
58 private Command command;
59 private Result result;
60 private Exception exception;
61 private URL url;
62 private double offset; // seconds
63 private double speed; // ratio
64
65 /*
66 * Called to execute the commands in the other thread
67 */
68 protected void play(URL url, double offset, double speed) throws InterruptedException, IOException {
69 this.url = url;
70 this.offset = offset;
71 this.speed = speed;
72 command = Command.PLAY;
73 result = Result.WAITING;
74 send();
75 }
76
77 protected void pause() throws InterruptedException, IOException {
78 command = Command.PAUSE;
79 send();
80 }
81
82 private void send() throws InterruptedException, IOException {
83 result = Result.WAITING;
84 interrupt();
85 while (result == Result.WAITING) {
86 sleep(10);
87 }
88 if (result == Result.FAILED)
89 throw new IOException(exception);
90 }
91
92 protected void possiblyInterrupt() throws InterruptedException {
93 if (interrupted() || result == Result.WAITING)
94 throw new InterruptedException();
95 }
96
97 protected void failed(Exception e) {
98 exception = e;
99 result = Result.FAILED;
100 state = State.NOTPLAYING;
101 }
102
103 protected void ok(State newState) {
104 result = Result.OK;
105 state = newState;
106 }
107
108 /**
109 * Returns the offset.
110 * @return the offset, in seconds
111 */
112 public double offset() {
113 return offset;
114 }
115
116 /**
117 * Returns the speed.
118 * @return the speed (ratio)
119 */
120 public double speed() {
121 return speed;
122 }
123
124 /**
125 * Returns the URL.
126 * @return The resource to play, which must be a WAV file or stream
127 */
128 public URL url() {
129 return url;
130 }
131
132 /**
133 * Returns the command.
134 * @return the command
135 */
136 public Command command() {
137 return command;
138 }
139 }
140
141 private final Execute command;
142
143 /**
144 * Plays a WAV audio file from the beginning. See also the variant which doesn't
145 * start at the beginning of the stream
146 * @param url The resource to play, which must be a WAV file or stream
147 * @throws InterruptedException thread interrupted
148 * @throws IOException audio fault exception, e.g. can't open stream, unhandleable audio format
149 */
150 public static void play(URL url) throws InterruptedException, IOException {
151 AudioPlayer instance = AudioPlayer.getInstance();
152 if (instance != null)
153 instance.command.play(url, 0.0, 1.0);
154 }
155
156 /**
157 * Plays a WAV audio file from a specified position.
158 * @param url The resource to play, which must be a WAV file or stream
159 * @param seconds The number of seconds into the audio to start playing
160 * @throws InterruptedException thread interrupted
161 * @throws IOException audio fault exception, e.g. can't open stream, unhandleable audio format
162 */
163 public static void play(URL url, double seconds) throws InterruptedException, IOException {
164 AudioPlayer instance = AudioPlayer.getInstance();
165 if (instance != null)
166 instance.command.play(url, seconds, 1.0);
167 }
168
169 /**
170 * Plays a WAV audio file from a specified position at variable speed.
171 * @param url The resource to play, which must be a WAV file or stream
172 * @param seconds The number of seconds into the audio to start playing
173 * @param speed Rate at which audio playes (1.0 = real time, &gt; 1 is faster)
174 * @throws InterruptedException thread interrupted
175 * @throws IOException audio fault exception, e.g. can't open stream, unhandleable audio format
176 */
177 public static void play(URL url, double seconds, double speed) throws InterruptedException, IOException {
178 AudioPlayer instance = AudioPlayer.getInstance();
179 if (instance != null)
180 instance.command.play(url, seconds, speed);
181 }
182
183 /**
184 * Pauses the currently playing audio stream. Does nothing if nothing playing.
185 * @throws InterruptedException thread interrupted
186 * @throws IOException audio fault exception, e.g. can't open stream, unhandleable audio format
187 */
188 public static void pause() throws InterruptedException, IOException {
189 AudioPlayer instance = AudioPlayer.getInstance();
190 if (instance != null)
191 instance.command.pause();
192 }
193
194 /**
195 * To get the Url of the playing or recently played audio.
196 * @return url - could be null
197 */
198 public static URL url() {
199 AudioPlayer instance = AudioPlayer.getInstance();
200 return instance == null ? null : instance.playingUrl;
201 }
202
203 /**
204 * Whether or not we are paused.
205 * @return boolean whether or not paused
206 */
207 public static boolean paused() {
208 AudioPlayer instance = AudioPlayer.getInstance();
209 return instance != null && instance.state == State.PAUSED;
210 }
211
212 /**
213 * Whether or not we are playing.
214 * @return boolean whether or not playing
215 */
216 public static boolean playing() {
217 AudioPlayer instance = AudioPlayer.getInstance();
218 return instance != null && instance.state == State.PLAYING;
219 }
220
221 /**
222 * How far we are through playing, in seconds.
223 * @return double seconds
224 */
225 public static double position() {
226 AudioPlayer instance = AudioPlayer.getInstance();
227 return instance == null ? -1 : instance.soundPlayer.position();
228 }
229
230 /**
231 * Speed at which we will play.
232 * @return double, speed multiplier
233 */
234 public static double speed() {
235 AudioPlayer instance = AudioPlayer.getInstance();
236 return instance == null ? -1 : instance.soundPlayer.speed();
237 }
238
239 /**
240 * Returns the singleton object, and if this is the first time, creates it along with
241 * the thread to support audio
242 * @return the unique instance
243 */
244 private static AudioPlayer getInstance() {
245 if (audioPlayer != null)
246 return audioPlayer;
247 try {
248 audioPlayer = new AudioPlayer();
249 return audioPlayer;
250 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException ex) {
251 Logging.error(ex);
252 return null;
253 }
254 }
255
256 /**
257 * Resets the audio player.
258 */
259 public static void reset() {
260 if (audioPlayer != null) {
261 try {
262 pause();
263 } catch (InterruptedException | IOException e) {
264 Logging.warn(e);
265 }
266 audioPlayer.playingUrl = null;
267 }
268 }
269
270 @SuppressWarnings("unchecked")
271 private AudioPlayer() {
272 state = State.INITIALIZING;
273 command = new Execute();
274 playingUrl = null;
275 double leadIn = Config.getPref().getDouble("audio.leadin", 1.0 /* default, seconds */);
276 double calibration = Config.getPref().getDouble("audio.calibration", 1.0 /* default, ratio */);
277 try {
278 if (soundPlayerClass == null) {
279 // To remove when switching to Java 11
280 soundPlayerClass = (Class<? extends SoundPlayer>) Class.forName(
281 "org.openstreetmap.josm.io.audio.fx.JavaFxMediaPlayer");
282 }
283 soundPlayer = soundPlayerClass.getDeclaredConstructor().newInstance();
284 } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException e) {
285 Logging.debug(e);
286 Logging.warn("JOSM compiled without Java FX support. Falling back to Java Sound API");
287 } catch (NoClassDefFoundError | JosmRuntimeException e) {
288 Logging.debug(e);
289 Logging.warn("Java FX is unavailable. Falling back to Java Sound API");
290 }
291 if (soundPlayer == null) {
292 soundPlayer = new JavaSoundPlayer(leadIn, calibration);
293 }
294 soundPlayer.addAudioListener(this);
295 start();
296 while (state == State.INITIALIZING) {
297 yield();
298 }
299 }
300
301 /**
302 * Starts the thread to actually play the audio, per Thread interface
303 * Not to be used as public, though Thread interface doesn't allow it to be made private
304 */
305 @Override
306 public void run() {
307 /* code running in separate thread */
308
309 playingUrl = null;
310
311 for (;;) {
312 try {
313 switch (state) {
314 case INITIALIZING:
315 // we're ready to take interrupts
316 state = State.NOTPLAYING;
317 break;
318 case NOTPLAYING:
319 case PAUSED:
320 sleep(200);
321 break;
322 case PLAYING:
323 command.possiblyInterrupt();
324 if (soundPlayer.playing(command)) {
325 playingUrl = null;
326 state = State.NOTPLAYING;
327 }
328 command.possiblyInterrupt();
329 break;
330 default: // Do nothing
331 }
332 } catch (InterruptedException e) {
333 interrupted(); // just in case we get an interrupt
334 State stateChange = state;
335 state = State.INTERRUPTED;
336 try {
337 switch (command.command()) {
338 case PLAY:
339 soundPlayer.play(command, stateChange, playingUrl);
340 stateChange = State.PLAYING;
341 break;
342 case PAUSE:
343 soundPlayer.pause(command, stateChange, playingUrl);
344 stateChange = State.PAUSED;
345 break;
346 default: // Do nothing
347 }
348 command.ok(stateChange);
349 } catch (AudioException | IOException | SecurityException | IllegalArgumentException startPlayingException) {
350 Logging.error(startPlayingException);
351 command.failed(startPlayingException); // sets state
352 }
353 } catch (AudioException | IOException e) {
354 state = State.NOTPLAYING;
355 Logging.error(e);
356 }
357 }
358 }
359
360 @Override
361 public void playing(URL playingUrl) {
362 this.playingUrl = playingUrl;
363 }
364
365 /**
366 * Returns the custom sound player class, if any.
367 * @return the custom sound player class, or {@code null}
368 * @since 14183
369 */
370 public static Class<? extends SoundPlayer> getSoundPlayerClass() {
371 return soundPlayerClass;
372 }
373
374 /**
375 * Sets the custom sound player class to override default core player.
376 * Must be called before the first audio method invocation.
377 * @param playerClass custom sound player class to override default core player
378 * @since 14183
379 */
380 public static void setSoundPlayerClass(Class<? extends SoundPlayer> playerClass) {
381 if (audioPlayer != null) {
382 throw new IllegalStateException("Audio player already initialized");
383 }
384 soundPlayerClass = Objects.requireNonNull(playerClass);
385 }
386}
Note: See TracBrowser for help on using the repository browser.