001package jmri.jmrit.audio; 002 003import javax.sound.sampled.Clip; 004import javax.sound.sampled.DataLine; 005import javax.sound.sampled.FloatControl; 006import javax.sound.sampled.LineUnavailableException; 007import javax.sound.sampled.Mixer; 008import javax.vecmath.Vector3f; 009import jmri.InstanceManager; 010import org.slf4j.Logger; 011import org.slf4j.LoggerFactory; 012 013/** 014 * JavaSound implementation of the Audio Source sub-class. 015 * <p> 016 * For now, no system-specific implementations are foreseen - this will remain 017 * internal-only 018 * <p> 019 * For more information about the JavaSound API, visit 020 * <a href="http://java.sun.com/products/java-media/sound/">http://java.sun.com/products/java-media/sound/</a> 021 * 022 * <hr> 023 * This file is part of JMRI. 024 * <p> 025 * JMRI is free software; you can redistribute it and/or modify it under the 026 * terms of version 2 of the GNU General Public License as published by the Free 027 * Software Foundation. See the "COPYING" file for a copy of this license. 028 * <p> 029 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY 030 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 031 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 032 * 033 * @author Matthew Harris copyright (c) 2009 034 */ 035public class JavaSoundAudioSource extends AbstractAudioSource { 036 037 /** 038 * Reference to JavaSound mixer object 039 */ 040 private static Mixer mixer = JavaSoundAudioFactory.getMixer(); 041 042 /** 043 * Reference to current active AudioListener 044 */ 045 private AudioListener activeAudioListener = loadAudioListener(); 046 047 private AudioListener loadAudioListener() { 048 AudioFactory audioFact = InstanceManager.getDefault(jmri.AudioManager.class).getActiveAudioFactory(); 049 if (audioFact != null) { 050 return audioFact.getActiveAudioListener(); 051 } 052 log.error("no AudioListener found"); 053 return null; 054 } 055 056 /** 057 * True if we've been initialised 058 */ 059 private boolean initialised = false; 060 061 /** 062 * Used for playing back sound source 063 */ 064 private transient Clip clip = null; 065 066 /** 067 * Holds reference to the JavaSoundAudioChannel object 068 */ 069 private transient JavaSoundAudioChannel audioChannel = null; 070 071 private boolean jsState; 072 073 /** 074 * Constructor for new JavaSoundAudioSource with system name 075 * 076 * @param systemName AudioSource object system name (e.g. IAS1) 077 */ 078 public JavaSoundAudioSource(String systemName) { 079 super(systemName); 080 log.debug("New JavaSoundAudioSource: {}", systemName); 081 initialised = init(); 082 } 083 084 /** 085 * Constructor for new JavaSoundAudioSource with system name and user name 086 * 087 * @param systemName AudioSource object system name (e.g. IAS1) 088 * @param userName AudioSource object user name 089 */ 090 public JavaSoundAudioSource(String systemName, String userName) { 091 super(systemName, userName); 092 log.debug("New JavaSoundAudioSource: {} ({})", userName, systemName); 093 initialised = init(); 094 } 095 096 /** 097 * Initialise this AudioSource 098 * 099 * @return True if initialised 100 */ 101 private boolean init() { 102 return true; 103 } 104 105// @SuppressWarnings("SleepWhileInLoop") 106 @Override 107 boolean bindAudioBuffer(AudioBuffer audioBuffer) { 108 // First check we've been initialised 109 if (!initialised) { 110 return false; 111 } 112 113 // Wait for AudioBuffer to be loaded, or 20 seconds 114 long startTime = System.currentTimeMillis(); 115 while (audioBuffer.getState() != AudioBuffer.STATE_LOADED 116 && System.currentTimeMillis() - startTime < 20000) { 117 try { 118 Thread.sleep(50); 119 } catch (InterruptedException ex) { 120 log.debug("bindAudioBuffer was interruped"); 121 } 122 } 123 124 if (audioBuffer instanceof JavaSoundAudioBuffer 125 && audioBuffer.getState() == AudioBuffer.STATE_LOADED) { 126 // Cast to JavaSoundAudioBuffer to enable easier access to specific methods 127 JavaSoundAudioBuffer buffer = (JavaSoundAudioBuffer) audioBuffer; 128 129 // Get a JavaSound DataLine and Clip 130 DataLine.Info lineInfo; 131 lineInfo = new DataLine.Info(Clip.class, buffer.getAudioFormat()); 132 Clip newClip; 133 try { 134 newClip = (Clip) mixer.getLine(lineInfo); 135 } catch (LineUnavailableException ex) { 136 log.warn("Error binding JavaSoundSource ({}) to AudioBuffer ({}) ", 137 this.getSystemName(), this.getAssignedBufferName(), ex); 138 return false; 139 } 140 141 this.clip = newClip; 142 143 try { 144 clip.open(buffer.getAudioFormat(), 145 buffer.getDataStorageBuffer(), 146 0, 147 buffer.getDataStorageBuffer().length); 148 } catch (LineUnavailableException ex) { 149 log.warn("Error binding JavaSoundSource ({}) to AudioBuffer ({}) ", 150 this.getSystemName(), this.getAssignedBufferName(), ex); 151 } 152 if (log.isDebugEnabled()) { 153 log.debug("Bind JavaSoundAudioSource ({}) to JavaSoundAudioBuffer ({})", 154 this.getSystemName(), audioBuffer.getSystemName()); 155 } 156 return true; 157 } else { 158 log.warn("AudioBuffer not loaded error when binding JavaSoundSource ({}) to AudioBuffer ({})", 159 this.getSystemName(), this.getAssignedBufferName()); 160 return false; 161 } 162 } 163 164 @Override 165 protected void changePosition(Vector3f pos) { 166 if (initialised && isBound() && audioChannel != null) { 167 calculateGain(); 168 calculatePan(); 169 } 170 } 171 172 @Override 173 public void setGain(float gain) { 174 super.setGain(gain); 175 if (initialised && isBound() && audioChannel != null) { 176 calculateGain(); 177 } 178 } 179 180 @Override 181 public void setPitch(float pitch) { 182 super.setPitch(pitch); 183 if (initialised && isBound() && audioChannel != null) { 184 calculatePitch(); 185 } 186 } 187 188 @Override 189 public void setReferenceDistance(float referenceDistance) { 190 super.setReferenceDistance(referenceDistance); 191 if (initialised && isBound() && audioChannel != null) { 192 calculateGain(); 193 } 194 } 195 196 @Override 197 public void setOffset(long offset) { 198 super.setOffset(offset); 199 if (initialised && isBound() && audioChannel != null) { 200 this.clip.setFramePosition((int) offset); 201 } 202 } 203 204 @Override 205 public int getState() { 206 boolean old = jsState; 207 jsState = (this.clip != null && this.clip.isActive()); 208 if (jsState != old) { 209 if (jsState) { 210 this.setState(STATE_PLAYING); 211 } else { 212 this.setState(STATE_STOPPED); 213 } 214 } 215 return super.getState(); 216 } 217 218 @Override 219 public void stateChanged(int oldState) { 220 super.stateChanged(oldState); 221 if (initialised && isBound() && audioChannel != null) { 222 calculateGain(); 223 calculatePan(); 224 calculatePitch(); 225 } else { 226 initialised = init(); 227 } 228 } 229 230 @Override 231 protected void doPlay() { 232 log.debug("Play JavaSoundAudioSource ({})", this.getSystemName()); 233 if (initialised && isBound()) { 234 doRewind(); 235 doResume(); 236 } 237 } 238 239 @Override 240 protected void doStop() { 241 log.debug("Stop JavaSoundAudioSource ({})", this.getSystemName()); 242 if (initialised && isBound()) { 243 doPause(); 244 doRewind(); 245 } 246 } 247 248 @Override 249 protected void doPause() { 250 if (log.isDebugEnabled()) { 251 log.debug("Pause JavaSoundAudioSource ({})", this.getSystemName()); 252 } 253 if (initialised && isBound()) { 254 this.clip.stop(); 255 if (audioChannel != null) { 256 if (log.isDebugEnabled()) { 257 log.debug("Remove JavaSoundAudioChannel for Source {}", this.getSystemName()); 258 } 259 audioChannel = null; 260 } 261 } 262 this.setState(STATE_STOPPED); 263 } 264 265 @Override 266 protected void doResume() { 267 if (log.isDebugEnabled()) { 268 log.debug("Resume JavaSoundAudioSource ({})", this.getSystemName()); 269 } 270 if (initialised && isBound()) { 271 if (audioChannel == null) { 272 if (log.isDebugEnabled()) { 273 log.debug("Create JavaSoundAudioChannel for Source {}", this.getSystemName()); 274 } 275 audioChannel = new JavaSoundAudioChannel(this); 276 } 277 this.clip.loop(this.getNumLoops()); 278 this.setState(STATE_PLAYING); 279 } 280 } 281 282 @Override 283 protected void doRewind() { 284 if (log.isDebugEnabled()) { 285 log.debug("Rewind JavaSoundAudioSource ({})", this.getSystemName()); 286 } 287 if (initialised && isBound()) { 288 this.clip.setFramePosition(0); 289 } 290 } 291 292 @Override 293 protected void doFadeIn() { 294 if (log.isDebugEnabled()) { 295 log.debug("Fade-in JavaSoundAudioSource ({})", this.getSystemName()); 296 } 297 if (initialised && isBound()) { 298 doPlay(); 299 AudioSourceFadeThread asft = new AudioSourceFadeThread(this); 300 asft.start(); 301 } 302 } 303 304 @Override 305 protected void doFadeOut() { 306 if (log.isDebugEnabled()) { 307 log.debug("Fade-out JavaSoundAudioSource ({})", this.getSystemName()); 308 } 309 if (initialised && isBound()) { 310 AudioSourceFadeThread asft = new AudioSourceFadeThread(this); 311 asft.start(); 312 } 313 } 314 315 @Override 316 protected void cleanup() { 317 if (initialised && isBound()) { 318 this.clip.stop(); 319 this.clip.close(); 320 this.clip = null; 321 } 322 if (log.isDebugEnabled()) { 323 log.debug("Cleanup JavaSoundAudioSource ({})", this.getSystemName()); 324 } 325 } 326 327 /** 328 * Calculate the panning of this Source between fully left (-1.0f) and 329 * fully right (1.0f) 330 * <p> 331 * Calculated internally from the relative positions of this source and the 332 * listener. 333 */ 334 protected void calculatePan() { 335 Vector3f side = new Vector3f(); 336 side.cross(activeAudioListener.getOrientation(UP), activeAudioListener.getOrientation(AT)); 337 side.normalize(); 338 Vector3f vecX = new Vector3f(this.getCurrentPosition()); 339 Vector3f vecZ = new Vector3f(this.getCurrentPosition()); 340 float x = vecX.dot(side); 341 float z = vecZ.dot(activeAudioListener.getOrientation(AT)); 342 float angle = (float) Math.atan2(x, z); 343 float pan = (float) -Math.sin(angle); 344 345 // If playing, update the pan 346 if (audioChannel != null) { 347 audioChannel.setPan(pan); 348 } 349 if (log.isDebugEnabled()) { 350 log.debug("Set pan of JavaSoundAudioSource {} to {}", this.getSystemName(), pan); 351 } 352 } 353 354 @Override 355 protected void calculateGain() { 356 357 // Calculate distance from listener 358 Vector3f distance = new Vector3f(this.getCurrentPosition()); 359 if (!this.isPositionRelative()) { 360 distance.sub(activeAudioListener.getCurrentPosition()); 361 } 362 363 float distanceFromListener 364 = (float) Math.sqrt(distance.dot(distance)); 365 if (log.isDebugEnabled()) { 366 log.debug("Distance of JavaSoundAudioSource {} from Listener = {}", this.getSystemName(), distanceFromListener); 367 } 368 369 // Default value to start with (used for no distance attenuation) 370 float currentGain = 1.0f; 371 372 AudioFactory audioFact = InstanceManager.getDefault(jmri.AudioManager.class).getActiveAudioFactory(); 373 if (audioFact != null && audioFact.isDistanceAttenuated()) { 374 // Calculate gain of this source using clamped inverse distance 375 // attenuation model 376 377 distanceFromListener = Math.max(distanceFromListener, this.getReferenceDistance()); 378 if (log.isDebugEnabled()) { 379 log.debug("After initial clamping, distance of JavaSoundAudioSource {} from Listener = {}", this.getSystemName(), distanceFromListener); 380 } 381 distanceFromListener = Math.min(distanceFromListener, this.getMaximumDistance()); 382 if (log.isDebugEnabled()) { 383 log.debug("After final clamping, distance of JavaSoundAudioSource {} from Listener = {}", this.getSystemName(), distanceFromListener); 384 } 385 386 currentGain 387 = activeAudioListener.getMetersPerUnit() 388 * (this.getReferenceDistance() 389 / (this.getReferenceDistance() + this.getRollOffFactor() 390 * (distanceFromListener - this.getReferenceDistance()))); 391 if (log.isDebugEnabled()) { 392 log.debug("Calculated for JavaSoundAudioSource {} gain = {}", this.getSystemName(), currentGain); 393 } 394 395 // Ensure that gain is between 0 and 1 396 if (currentGain > 1.0f) { 397 currentGain = 1.0f; 398 } else if (currentGain < 0.0f) { 399 currentGain = 0.0f; 400 } 401 } 402 403 // Finally, adjust based on master gain for this source, the gain 404 // of listener and any calculated fade gains 405 currentGain *= this.getGain() * activeAudioListener.getGain() * this.getFadeGain(); 406 407 // If playing, update the gain 408 if (audioChannel != null) { 409 audioChannel.setGain(currentGain); 410 if (log.isDebugEnabled()) { 411 log.debug("Set current gain of JavaSoundAudioSource {} to {}", this.getSystemName(), currentGain); 412 } 413 } 414 } 415 416 /** 417 * Internal method used to calculate the pitch. 418 */ 419 protected void calculatePitch() { 420 // If playing, update the pitch 421 if (audioChannel != null) { 422 audioChannel.setPitch(this.getPitch()); 423 } 424 } 425 426 private static final Logger log = LoggerFactory.getLogger(JavaSoundAudioSource.class); 427 428 private static class JavaSoundAudioChannel { 429 430 /** 431 * Control for changing the gain of this AudioSource 432 */ 433 private FloatControl gainControl = null; 434 435 /** 436 * Control for changing the pan of this AudioSource 437 */ 438 private FloatControl panControl = null; 439 440 /** 441 * Control for changing the sample rate of this AudioSource 442 */ 443 private FloatControl sampleRateControl = null; 444 445 /** 446 * Holds the initial sample rate setting 447 */ 448 private float initialSampleRate = 0.0f; 449 450 /** 451 * Holds the initial gain setting 452 */ 453 private float initialGain = 0.0f; 454 455 /** 456 * Holds reference to the parent AudioSource object 457 */ 458 private final JavaSoundAudioSource audio; 459 460 /** 461 * Holds reference to the JavaSound clip 462 */ 463 private final Clip clip; 464 465 /** 466 * Constructor for creating an AudioChannel for a specific 467 * JavaSoundAudioSource. 468 * 469 * @param audio the specific JavaSoundAudioSource 470 */ 471 public JavaSoundAudioChannel(JavaSoundAudioSource audio) { 472 473 this.audio = audio; 474 this.clip = this.audio.clip; 475 476 // Check if changing gain is supported 477 if (this.clip.isControlSupported(FloatControl.Type.MASTER_GAIN)) { 478 // Yes, so create a new gain control 479 this.gainControl = (FloatControl) this.clip.getControl(FloatControl.Type.MASTER_GAIN); 480 this.initialGain = this.gainControl.getValue(); 481 if (log.isDebugEnabled()) { 482 log.debug("JavaSound gain control created"); 483 log.debug("Initial Gain = {}", this.initialGain); 484 } 485 } else { 486 log.info("Gain control is not supported"); 487 this.gainControl = null; 488 } 489 490 // Check if changing pan is supported 491 if (this.clip.isControlSupported(FloatControl.Type.PAN)) { 492 // Yes, so create a new pan control 493 this.panControl = (FloatControl) this.clip.getControl(FloatControl.Type.PAN); 494 log.debug("JavaSound pan control created"); 495 } else { 496 log.info("Pan control is not supported"); 497 this.panControl = null; 498 } 499 500 // Check if changing pitch is supported 501 if (this.clip.isControlSupported(FloatControl.Type.SAMPLE_RATE)) { 502 // Yes, so create a new pitch control 503 this.sampleRateControl = (FloatControl) this.clip.getControl(FloatControl.Type.SAMPLE_RATE); 504 this.initialSampleRate = this.sampleRateControl.getValue(); 505 if (log.isDebugEnabled()) { 506 log.debug("JavaSound pitch control created"); 507 log.debug("Initial Sample Rate = {}", this.initialSampleRate); 508 } 509 } else { 510 log.info("Sample Rate control is not supported"); 511 this.sampleRateControl = null; 512 this.initialSampleRate = 0; 513 } 514 } 515 516 /** 517 * Set the gain of this AudioChannel. 518 * 519 * @param gain the gain (0.0f to 1.0f) 520 */ 521 protected void setGain(float gain) { 522 if (this.gainControl != null) { 523 // Ensure gain is within limits 524 if (gain <= 0.0f) { 525 gain = 0.0001f; 526 } else if (gain > 1.0f) { 527 gain = 1.0f; 528 } 529 530 // Convert this linear gain to a decibel value 531 float dB = (float) (Math.log(gain) / Math.log(10.0) * 20.0); 532 533 this.gainControl.setValue(dB); 534 if (log.isDebugEnabled()) { 535 log.debug("Actual gain value of JavaSoundAudioSource {} is {}", this.audio.getDebugString(), this.gainControl.getValue()); 536 } 537 } 538 log.debug("Set gain of JavaSoundAudioSource {} to {}", this.audio.getDebugString(), gain); 539 } 540 541 /** 542 * Set the pan of this AudioChannel. 543 * 544 * @param pan the pan (-1.0f to 1.0f) 545 */ 546 protected void setPan(float pan) { 547 if (this.panControl != null) { 548 this.panControl.setValue(pan); 549 } 550 log.debug("Set pan of JavaSoundAudioSource {} to {}", this.audio.getDebugString(), pan); 551 } 552 553 /** 554 * Set the pitch of this AudioChannel. 555 * <p> 556 * Calculated as a ratio of the initial sample rate 557 * 558 * @param pitch the pitch 559 */ 560 protected void setPitch(float pitch) { 561 if (this.sampleRateControl != null) { 562 this.sampleRateControl.setValue(pitch * this.initialSampleRate); 563 } 564 log.debug("Set pitch of JavaSoundAudioSource {} to {}", this.audio.getDebugString(), pitch); 565 } 566 567 } 568 569}