001package jmri.jmrit.audio; 002 003import java.util.LinkedList; 004import java.util.Queue; 005import java.util.concurrent.ThreadLocalRandom; 006import javax.annotation.Nonnull; 007import javax.vecmath.Vector3f; 008import jmri.Audio; 009import jmri.AudioManager; 010import jmri.InstanceManager; 011import jmri.implementation.AbstractAudio; 012 013/** 014 * Base implementation of the AudioSource class. 015 * <p> 016 * Specific implementations will extend this base class. 017 * <br> 018 * <hr> 019 * This file is part of JMRI. 020 * <p> 021 * JMRI is free software; you can redistribute it and/or modify it under the 022 * terms of version 2 of the GNU General Public License as published by the Free 023 * Software Foundation. See the "COPYING" file for a copy of this license. 024 * <p> 025 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY 026 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 027 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 028 * 029 * @author Matthew Harris copyright (c) 2009 030 */ 031public abstract class AbstractAudioSource extends AbstractAudio implements AudioSource { 032 033 private Vector3f position = new Vector3f(0.0f, 0.0f, 0.0f); 034 private Vector3f currentPosition = new Vector3f(0.0f, 0.0f, 0.0f); 035 private Vector3f velocity = new Vector3f(0.0f, 0.0f, 0.0f); 036 private float gain = 1.0f; 037 private float pitch = 1.0f; 038 private float referenceDistance = 1.0f; 039 private float maximumDistance = Audio.MAX_DISTANCE; 040 private float rollOffFactor = 1.0f; 041 private int minLoops = LOOP_NONE; 042 private int maxLoops = LOOP_NONE; 043 private int numLoops = 0; 044 // private int minLoopDelay = 0; 045 // private int maxLoopDelay = 0; 046 // private int loopDelay = 0; 047 private int fadeInTime = 1000; 048 private int fadeOutTime = 1000; 049 private float fadeGain = 1.0f; 050 private long timeOfLastFadeCheck = 0; 051 private long timeOfLastPositionCheck = 0; 052 private int fading = Audio.FADE_NONE; 053 private boolean bound = false; 054 private boolean positionRelative = false; 055 private boolean queued = false; 056 private long offset = 0; 057 private AudioBuffer buffer; 058 // private AudioSourceDelayThread asdt = null; 059 private LinkedList<AudioBuffer> pendingBufferQueue = new LinkedList<>(); 060 061 private static final AudioFactory activeAudioFactory = InstanceManager.getDefault(jmri.AudioManager.class).getActiveAudioFactory(); 062 private static float metersPerUnit; 063 064 /** 065 * Abstract constructor for new AudioSource with system name 066 * 067 * @param systemName AudioSource object system name (e.g. IAS1) 068 */ 069 public AbstractAudioSource(String systemName) { 070 super(systemName); 071 AudioListener al = activeAudioFactory.getActiveAudioListener(); 072 if (al != null) { 073 storeMetersPerUnit(al.getMetersPerUnit()); 074 } 075 } 076 077 /** 078 * Abstract constructor for new AudioSource with system name and user name 079 * 080 * @param systemName AudioSource object system name (e.g. IAS1) 081 * @param userName AudioSource object user name 082 */ 083 public AbstractAudioSource(String systemName, String userName) { 084 super(systemName, userName); 085 AudioListener al = activeAudioFactory.getActiveAudioListener(); 086 if (al != null) { 087 storeMetersPerUnit(al.getMetersPerUnit()); 088 } 089 } 090 091 private static void storeMetersPerUnit(float newVal) { 092 metersPerUnit = newVal; 093 } 094 095 public boolean isAudioAlive() { 096 return ((AudioThread) activeAudioFactory.getCommandThread()).isThreadAlive(); 097 } 098 099 @Override 100 public char getSubType() { 101 return SOURCE; 102 } 103 104 @Override 105 public boolean queueBuffers(Queue<AudioBuffer> audioBuffers) { 106 // Note: Cannot queue buffers to a Source that has a bound buffer. 107 if (!bound) { 108 this.pendingBufferQueue = new LinkedList<>(audioBuffers); 109 activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_QUEUE_BUFFERS)); 110 activeAudioFactory.getCommandThread().interrupt(); 111 if (log.isDebugEnabled() && (audioBuffers.peek() != null)) { 112 log.debug("Queued Buffer {} to Source {}", audioBuffers.peek().getSystemName(), this.getSystemName()); 113 } 114 return true; 115 } else { 116 if (audioBuffers.peek() != null) { 117 log.error("Attempted to queue buffers {} (etc) to Bound Source {}", audioBuffers.peek().getSystemName(), this.getSystemName()); 118 } 119 return false; 120 } 121 } 122 123 @Override 124 public boolean queueBuffer(AudioBuffer audioBuffer) { 125 if (!bound) { 126 //this.pendingBufferQueue.clear(); 127 this.pendingBufferQueue.add(audioBuffer); 128 activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_QUEUE_BUFFERS)); 129 activeAudioFactory.getCommandThread().interrupt(); 130 if (log.isDebugEnabled()) { 131 log.debug("Queued Buffer {} to Source {}", audioBuffer.getSystemName(), this.getSystemName()); 132 } 133 return true; 134 } else { 135 log.error("Attempted to queue buffer {} to Bound Source {}", audioBuffer.getSystemName(), this.getSystemName()); 136 return false; 137 } 138 } 139 140 @Override 141 public boolean unqueueBuffers() { 142 if (bound) { 143 log.error("Attempted to unqueue buffers on Bound Source {}", this.getSystemName()); 144 return false; 145 } else if (queued) { 146 activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_UNQUEUE_BUFFERS)); 147 activeAudioFactory.getCommandThread().interrupt(); 148 if (log.isDebugEnabled()) { 149 log.debug("Unqueued Processed Buffers on Source {}", this.getSystemName()); 150 } 151 return true; 152 } else { 153 log.debug("Source neither queued nor bound. Not an error. {}", this.getSystemName()); 154 return false; 155 } 156 } 157 158 public Queue<AudioBuffer> getQueuedBuffers() { 159 return this.pendingBufferQueue; 160 } 161 162 @Override 163 public void setAssignedBuffer(AudioBuffer audioBuffer) { 164 if (!queued) { 165 this.buffer = audioBuffer; 166 // Ensure that the source is stopped 167 this.stop(false); 168 activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_BIND_BUFFER)); 169 activeAudioFactory.getCommandThread().interrupt(); 170 if (log.isDebugEnabled()) { 171 log.debug("Assigned Buffer {} to Source {}", audioBuffer.getSystemName(), this.getSystemName()); 172 } 173 } else { 174 log.error("Attempted to assign buffer {} to Queued Source {}", audioBuffer.getSystemName(), this.getSystemName()); 175 } 176 } 177 178 @Override 179 public void setAssignedBuffer(String bufferSystemName) { 180 if (!queued) { 181 AudioManager am = InstanceManager.getDefault(jmri.AudioManager.class); 182 Audio a = am.getBySystemName(bufferSystemName); 183 if (a != null && a.getSubType() == Audio.BUFFER) { 184 setAssignedBuffer((AudioBuffer) a); 185 } else { 186 log.warn("Attempt to assign incorrect object type to buffer - AudioBuffer expected."); 187 this.buffer = null; 188 this.bound = false; 189 } 190 } else { 191 log.error("Attempted to assign buffer {} to Queued Source {}", bufferSystemName, this.getSystemName()); 192 } 193 } 194 195 @Override 196 public AudioBuffer getAssignedBuffer() { 197 return this.buffer; 198 } 199 200 @Override 201 public String getAssignedBufferName() { 202 return (buffer != null) ? buffer.getSystemName() : "[none]"; 203 } 204 205 @Override 206 public void setPosition(Vector3f pos) { 207 this.position = pos; 208 this.currentPosition = pos; 209 changePosition(pos); 210 if (log.isDebugEnabled()) { 211 log.debug("Set position of Source {} to {}", this.getSystemName(), pos); 212 } 213 } 214 215 @Override 216 public void setPosition(float x, float y, float z) { 217 this.setPosition(new Vector3f(x, y, z)); 218 } 219 220 @Override 221 public void setPosition(float x, float y) { 222 this.setPosition(new Vector3f(x, y, 0.0f)); 223 } 224 225 @Override 226 public Vector3f getPosition() { 227 return this.position; 228 } 229 230 @Override 231 public Vector3f getCurrentPosition() { 232 return this.currentPosition; 233 } 234 235 @Override 236 public void setPositionRelative(boolean relative) { 237 this.positionRelative = relative; 238 } 239 240 @Override 241 public boolean isPositionRelative() { 242 return this.positionRelative; 243 } 244 245 @Override 246 public void setVelocity(Vector3f vel) { 247 this.velocity = vel; 248 if (log.isDebugEnabled()) { 249 log.debug("Set velocity of Source {} to {}", this.getSystemName(), vel); 250 } 251 } 252 253 @Override 254 public Vector3f getVelocity() { 255 return this.velocity; 256 } 257 258 /** 259 * Calculate current position based on velocity. 260 */ 261 protected void calculateCurrentPosition() { 262 263 // Calculate how long it's been since we lasted checked position 264 long currentTime = System.currentTimeMillis(); 265 float timePassed = (currentTime - this.timeOfLastPositionCheck); 266 this.timeOfLastPositionCheck = currentTime; 267 268 log.debug("timePassed = {} metersPerUnit = {} source = {} state = {}", timePassed, metersPerUnit, this.getSystemName(), this.getState()); 269 if (this.velocity.length() != 0) { 270 this.currentPosition.scaleAdd((timePassed / 1000) * metersPerUnit, 271 this.velocity, 272 this.currentPosition); 273 changePosition(this.currentPosition); 274 if (log.isDebugEnabled()) { 275 log.debug("Set current position of Source {} to {}", this.getSystemName(), this.currentPosition); 276 } 277 } 278 } 279 280 @Override 281 public void resetCurrentPosition() { 282 activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_RESET_POSITION)); 283 activeAudioFactory.getCommandThread().interrupt(); 284 } 285 286 /** 287 * Reset the current position. 288 */ 289 protected void doResetCurrentPosition() { 290 this.currentPosition = this.position; 291 } 292 293 /** 294 * Change the current position of this source. 295 * 296 * @param pos new position 297 */ 298 abstract protected void changePosition(Vector3f pos); 299 300 @Override 301 public void setGain(float gain) { 302 this.gain = gain; 303 if (log.isDebugEnabled()) { 304 log.debug("Set gain of Source {} to {}", this.getSystemName(), gain); 305 } 306 } 307 308 @Override 309 public float getGain() { 310 return this.gain; 311 } 312 313 /** 314 * Calculate the gain of this AudioSource based on distance from 315 * listener and fade levels. 316 */ 317 abstract protected void calculateGain(); 318 319 @Override 320 public void setPitch(float pitch) { 321 if (pitch < 0.5f) { 322 pitch = 0.5f; 323 } 324 if (pitch > 2.0f) { 325 pitch = 2.0f; 326 } 327 this.pitch = pitch; 328 if (log.isDebugEnabled()) { 329 log.debug("Set pitch of Source {} to {}", this.getSystemName(), pitch); 330 } 331 } 332 333 @Override 334 public float getPitch() { 335 return this.pitch; 336 } 337 338 @Override 339 public void setReferenceDistance(float referenceDistance) { 340 if (referenceDistance < 0.0f) { 341 referenceDistance = 0.0f; 342 } 343 this.referenceDistance = referenceDistance; 344 if (log.isDebugEnabled()) { 345 log.debug("Set reference distance of Source {} to {}", this.getSystemName(), referenceDistance); 346 } 347 } 348 349 @Override 350 public float getReferenceDistance() { 351 return this.referenceDistance; 352 } 353 354 @Override 355 public void setOffset(long offset) { 356 if (offset < 0) { 357 offset = 0; 358 } 359 if (offset > this.buffer.getLength()) { 360 offset = this.buffer.getLength(); 361 } 362 this.offset = offset; 363 if (log.isDebugEnabled()) { 364 log.debug("Set byte offset of Source {}to {}", this.getSystemName(), offset); 365 } 366 } 367 368 @Override 369 public long getOffset() { 370 return this.offset; 371 } 372 373 @Override 374 public void setMaximumDistance(float maximumDistance) { 375 if (maximumDistance < 0.0f) { 376 maximumDistance = 0.0f; 377 } 378 this.maximumDistance = maximumDistance; 379 if (log.isDebugEnabled()) { 380 log.debug("Set maximum distance of Source {} to {}", this.getSystemName(), maximumDistance); 381 } 382 } 383 384 @Override 385 public float getMaximumDistance() { 386 return this.maximumDistance; 387 } 388 389 @Override 390 public void setRollOffFactor(float rollOffFactor) { 391 this.rollOffFactor = rollOffFactor; 392 if (log.isDebugEnabled()) { 393 log.debug("Set roll-off factor of Source {} to {}", this.getSystemName(), rollOffFactor); 394 } 395 } 396 397 @Override 398 public float getRollOffFactor() { 399 return this.rollOffFactor; 400 } 401 402 @Override 403 public void setLooped(boolean loop) { 404 if (loop) { 405 this.minLoops = LOOP_CONTINUOUS; 406 this.maxLoops = LOOP_CONTINUOUS; 407 } else { 408 this.minLoops = LOOP_NONE; 409 this.maxLoops = LOOP_NONE; 410 } 411 calculateLoops(); 412 } 413 414 @Override 415 public boolean isLooped() { 416 return (this.minLoops != LOOP_NONE || this.maxLoops != LOOP_NONE); 417 } 418 419 @Override 420 public void setMinLoops(int loops) { 421 if (this.maxLoops < loops) { 422 this.maxLoops = loops; 423 } 424 this.minLoops = loops; 425 calculateLoops(); 426 } 427 428 @Override 429 public int getMinLoops() { 430 return this.minLoops; 431 } 432 433 @Override 434 public void setMaxLoops(int loops) { 435 if (this.minLoops > loops) { 436 this.minLoops = loops; 437 } 438 this.maxLoops = loops; 439 calculateLoops(); 440 } 441 442 /** 443 * Calculate the number of times to loop playback of this sound. 444 */ 445 protected void calculateLoops() { 446 if (this.minLoops != this.maxLoops) { 447 this.numLoops = this.minLoops + ThreadLocalRandom.current().nextInt(this.maxLoops - this.minLoops); 448 } else { 449 this.numLoops = this.minLoops; 450 } 451 } 452 453 @Override 454 public int getMaxLoops() { 455 return this.maxLoops; 456 } 457 458 @Override 459 public int getNumLoops() { 460 // Call the calculate method each time so as to ensure 461 // randomness when min and max are not equal 462 calculateLoops(); 463 return this.numLoops; 464 } 465 466 @Override 467 public int getLastNumLoops() { 468 return this.numLoops; 469 } 470 471// public void setMinLoopDelay(int loopDelay) { 472// if (this.maxLoopDelay < loopDelay) { 473// this.maxLoopDelay = loopDelay; 474// } 475// this.minLoopDelay = loopDelay; 476// calculateLoopDelay(); 477// } 478// 479// public int getMinLoopDelay() { 480// return this.minLoopDelay; 481// } 482// 483// public void setMaxLoopDelay(int loopDelay) { 484// if (this.minLoopDelay > loopDelay) { 485// this.minLoopDelay = loopDelay; 486// } 487// this.maxLoopDelay = loopDelay; 488// calculateLoopDelay(); 489// } 490// 491// public int getMaxLoopDelay() { 492// return this.maxLoopDelay; 493// } 494// 495// public int getLoopDelay() { 496// // Call the calculate method each time so as to ensure 497// // randomness when min and max are not equal 498// calculateLoopDelay(); 499// return this.loopDelay; 500// } 501// 502// /** 503// * Method to calculate the delay between subsequent loops of this source 504// */ 505// protected void calculateLoopDelay() { 506// if (this.minLoopDelay != this.maxLoopDelay) { 507// Random r = new Random(); 508// this.loopDelay = this.minLoopDelay + r.nextInt(this.maxLoopDelay-this.minLoopDelay); 509// } else { 510// this.loopDelay = this.minLoopDelay; 511// } 512// } 513 @Override 514 public void setFadeIn(int fadeInTime) { 515 this.fadeInTime = fadeInTime; 516 } 517 518 @Override 519 public int getFadeIn() { 520 return this.fadeInTime; 521 } 522 523 @Override 524 public void setFadeOut(int fadeOutTime) { 525 this.fadeOutTime = fadeOutTime; 526 } 527 528 @Override 529 public int getFadeOut() { 530 return this.fadeOutTime; 531 } 532 533 /** 534 * Used to return the current calculated fade gain for this AudioSource. 535 * 536 * @return current fade gain 537 */ 538 protected float getFadeGain() { 539 return this.fadeGain; 540 } 541 542 /** 543 * Calculate the fade gains. 544 */ 545 protected void calculateFades() { 546 547 // Calculate how long it's been since we lasted checked fade gains 548 long currentTime = System.currentTimeMillis(); 549 long timePassed = currentTime - this.timeOfLastFadeCheck; 550 this.timeOfLastFadeCheck = currentTime; 551 552 switch (this.fading) { 553 case Audio.FADE_NONE: 554 // Reset fade gain 555 this.fadeGain = 1.0f; 556 break; 557 case Audio.FADE_OUT: 558 // Calculate fade-out gain 559 this.fadeGain -= roundDecimal(timePassed) / (this.getFadeOut()); 560 561 // Ensure that fade-out gain is not less than 0.0f 562 if (this.fadeGain < 0.0f) { 563 this.fadeGain = 0.0f; 564 565 // If so, we're done fading 566 this.fading = Audio.FADE_NONE; 567 } 568 if (log.isDebugEnabled()) { 569 log.debug("Set fade out gain of AudioSource {} to {}", this.getSystemName(), this.fadeGain); 570 } 571 break; 572 case Audio.FADE_IN: 573 // Calculate fade-in gain 574 this.fadeGain += roundDecimal(timePassed) / (this.getFadeIn()); 575 576 // Ensure that fade-in gain is not greater than 1.0f 577 if (this.fadeGain >= 1.0f) { 578 this.fadeGain = 1.0f; 579 580 // If so, we're done fading 581 this.fading = Audio.FADE_NONE; 582 } 583 if (log.isDebugEnabled()) { 584 log.debug("Set fade in gain of AudioSource {} to {}", this.getSystemName(), this.fadeGain); 585 } 586 break; 587 default: 588 throw new IllegalArgumentException(); 589 } 590 } 591 592 // Probably aught to be abstract, but I don't want to force the non-JOAL Source 593 // types to implement this (yet). So default to failing. 594 public boolean queueAudioBuffers(Queue<AudioBuffer> audioBuffers) { 595 log.debug("Abstract queueAudioBuffers() called."); 596 return false; 597 } 598 599 // Probably aught to be abstract, but I don't want to force the non-JOAL Source 600 // types to implement this (yet). So default to failing. 601 public boolean queueAudioBuffer(AudioBuffer audioBuffer) { 602 return false; 603 } 604 605 public boolean unqueueAudioBuffers() { 606 return false; 607 } 608 609 // Probably aught to be abstract, but I don't want to force the non-JOAL Source 610 // types to implement this (yet). So default to failing. 611 @Override 612 public int numQueuedBuffers() { 613 return 0; 614 } 615 616 // Probably aught to be abstract, but I don't want to force the non-JOAL Source 617 // types to implement this (yet). So default to failing. 618 @Override 619 public int numProcessedBuffers() { 620 return 0; 621 } 622 623 /** 624 * Binds this AudioSource with the specified AudioBuffer. 625 * <p> 626 * Applies only to sub-types: 627 * <ul> 628 * <li>Source 629 * </ul> 630 * 631 * @param buffer The AudioBuffer to bind to this AudioSource 632 * @return true if successful 633 */ 634 abstract boolean bindAudioBuffer(AudioBuffer buffer); 635 636 /** 637 * Method to define if this AudioSource has been bound to an AudioBuffer. 638 * 639 * @param bound True if bound to an AudioBufferr 640 */ 641 protected void setBound(boolean bound) { 642 this.bound = bound; 643 } 644 645 protected void setQueued(boolean queued) { 646 this.queued = queued; 647 } 648 649 @Override 650 public boolean isBound() { 651 return this.bound; 652 } 653 654 @Override 655 public boolean isQueued() { 656 return this.queued; 657 } 658 659 @Override 660 public void stateChanged(int oldState) { 661 // Get the current state 662 int i = this.getState(); 663 664 // Check if the current state has changed to playing 665 if (i != oldState && i == STATE_PLAYING) { 666 // We've changed to playing so start the move thread 667 this.timeOfLastPositionCheck = System.currentTimeMillis(); 668 AudioSourceMoveThread asmt = new AudioSourceMoveThread(this); 669 asmt.start(); 670 } 671 672// // Check if the current state has changed to stopped 673// if (i!=oldState && i==STATE_STOPPED) { 674// // We've changed to stopped so determine if we need to start the 675// // loop delay thread 676// if (isLooped() && getMinLoops()!=LOOP_CONTINUOUS) { 677// // Yes, we need to 678// if (asdt!=null) { 679// asdt.cleanup(); 680// asdt = null; 681// } 682// asdt = new AudioSourceDelayThread(this); 683// asdt.start(); 684// } 685// } 686 } 687 688 @Override 689 public void play() { 690 this.fading = Audio.FADE_NONE; 691// if (asdt!=null) { 692// asdt.interrupt(); 693// } 694 if (this.getState() != STATE_PLAYING) { 695 this.setState(STATE_PLAYING); 696 activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_PLAY)); 697 activeAudioFactory.getCommandThread().interrupt(); 698 } 699 } 700 701 /** 702 * Play the clip from the beginning. If looped, start looping. 703 */ 704 abstract protected void doPlay(); 705 706 @Override 707 public void stop() { 708 stop(true); 709 } 710 711 private void stop(boolean interruptThread) { 712 this.fading = Audio.FADE_NONE; 713 this.setState(STATE_STOPPED); 714 activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_STOP)); 715 if (interruptThread) { 716 activeAudioFactory.getCommandThread().interrupt(); 717 } 718 } 719 720 /** 721 * Stop playing the clip and rewind to the beginning. 722 */ 723 abstract protected void doStop(); 724 725 @Override 726 public void togglePlay() { 727 this.fading = Audio.FADE_NONE; 728 activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_PLAY_TOGGLE)); 729 activeAudioFactory.getCommandThread().interrupt(); 730 } 731 732 /** 733 * Toggle the current playing status. Will always start at/return to the 734 * beginning of the sample. 735 */ 736 protected void doTogglePlay() { 737 if (this.getState() == STATE_PLAYING) { 738 stop(); 739 } else { 740 play(); 741 } 742 } 743 744 @Override 745 public void pause() { 746 this.fading = Audio.FADE_NONE; 747 this.setState(STATE_STOPPED); 748 activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_PAUSE)); 749 activeAudioFactory.getCommandThread().interrupt(); 750 } 751 752 /** 753 * Stop playing the clip but retain the current position. 754 */ 755 abstract protected void doPause(); 756 757 @Override 758 public void resume() { 759 this.fading = Audio.FADE_NONE; 760 this.setState(STATE_PLAYING); 761 activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_RESUME)); 762 activeAudioFactory.getCommandThread().interrupt(); 763 } 764 765 /** 766 * Play the clip from the current position. 767 */ 768 abstract protected void doResume(); 769 770 @Override 771 public void togglePause() { 772 this.fading = Audio.FADE_NONE; 773 activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_PAUSE_TOGGLE)); 774 activeAudioFactory.getCommandThread().interrupt(); 775 } 776 777 /** 778 * Toggle the current playing status. Will retain the playback position of 779 * the sample. 780 */ 781 protected void doTogglePause() { 782 if (this.getState() == STATE_PLAYING) { 783 pause(); 784 } else { 785 resume(); 786 } 787 } 788 789 @Override 790 public void rewind() { 791 this.fading = Audio.FADE_NONE; 792 this.setState(STATE_STOPPED); 793 activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_REWIND)); 794 activeAudioFactory.getCommandThread().interrupt(); 795 } 796 797 /** 798 * Rewind clip to the beginning. 799 */ 800 abstract protected void doRewind(); 801 802 @Override 803 public void fadeIn() { 804 if (this.getState() != STATE_PLAYING && this.fading != Audio.FADE_IN) { 805 this.fading = Audio.FADE_IN; 806 this.fadeGain = 0.0f; 807 this.timeOfLastFadeCheck = System.currentTimeMillis(); 808 this.setState(STATE_PLAYING); 809 activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_FADE_IN)); 810 activeAudioFactory.getCommandThread().interrupt(); 811 } 812 } 813 814 /** 815 * Fade in then play this AudioSource. 816 */ 817 abstract protected void doFadeIn(); 818 819 @Override 820 public void fadeOut() { 821 if (this.getState() == STATE_PLAYING && this.fading != Audio.FADE_OUT) { 822 this.fading = Audio.FADE_OUT; 823 this.fadeGain = 1.0f; 824 this.timeOfLastFadeCheck = System.currentTimeMillis(); 825 this.setState(STATE_PLAYING); 826 activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_FADE_OUT)); 827 activeAudioFactory.getCommandThread().interrupt(); 828 } 829 } 830 831 /** 832 * Fade out then stop this AudioSource. 833 */ 834 abstract protected void doFadeOut(); 835 836 /** 837 * Get the current fading status. 838 * 839 * @return fading status 840 */ 841 protected int getFading() { 842 return this.fading; 843 } 844 845 // Probably aught to be abstract, but I don't want to force the non-JOAL Source 846 // types to implement this (yet). So default to failing. 847 @Override 848 public int attachSourcesToEffects() { 849 return 0; 850 } 851 852 // Probably aught to be abstract, but I don't want to force the non-JOAL Source 853 // types to implement this (yet). So default to failing. 854 @Override 855 public int detachSourcesToEffects() { 856 return 0; 857 } 858 859 @Override 860 @Nonnull 861 public String getDebugString() { 862 return "Pos: " + this.getPosition().toString() 863 + ", bound to: " + this.getAssignedBufferName() 864 + ", loops: " 865 + ((this.getMinLoops() == LOOP_CONTINUOUS) ? "infinite" 866 : ((!this.isLooped()) ? "none" 867 : "(min=" + this.getMinLoops() + " max=" + this.getMaxLoops() + ")")); 868 } 869 870 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractAudioSource.class); 871 872 /** 873 * An internal class used to create a new thread to monitor and maintain 874 * fade in and fade out levels. 875 * <p> 876 * Will exist only as long as this source is in the process of fading in or 877 * out. 878 */ 879 protected static class AudioSourceFadeThread extends AbstractAudioThread { 880 881 /** 882 * Reference to the AudioSource object being monitored. 883 */ 884 private AbstractAudioSource audioSource; 885 886 /** 887 * Internal variable to hold the fade direction. 888 */ 889 private final int fadeDirection; 890 891 /** 892 * Constructor that takes handle to looping AudioSource to monitor. 893 * 894 * @param audioSource looping AudioSource to monitor 895 */ 896 AudioSourceFadeThread(AbstractAudioSource audioSource) { 897 super(); 898 this.setName("fadesrc-" + super.getName()); 899 this.audioSource = audioSource; 900 this.fadeDirection = audioSource.getFading(); 901 if (log.isDebugEnabled()) { 902 log.debug("Created AudioSourceFadeThread for AudioSource {}", audioSource.getSystemName()); 903 } 904 } 905 906 /** 907 * Main processing loop. 908 */ 909 @Override 910 public void run() { 911 912 while (!dying()) { 913 914 // Recalculate the fade levels 915 audioSource.calculateFades(); 916 917 // Recalculate the gain levels 918 audioSource.calculateGain(); 919 920 // Check if we've done fading 921 if (audioSource.getFading() == Audio.FADE_NONE) { 922 die(); 923 } 924 925 // sleep for a while so as not to overload CPU 926 snooze(20); 927 } 928 929 // Reset fades 930 audioSource.calculateFades(); 931 932 // Check if we were fading out and, if so, stop. 933 // Otherwise reset gain 934 if (this.fadeDirection == Audio.FADE_OUT) { 935 audioSource.doStop(); 936 } else { 937 audioSource.calculateGain(); 938 } 939 940 // Finish up 941 if (log.isDebugEnabled()) { 942 log.debug("Clean up thread {}", this.getName()); 943 } 944 cleanup(); 945 } 946 947 /** 948 * Shut down this thread and clear references to created objects. 949 */ 950 @Override 951 protected void cleanup() { 952 // Thread is to shutdown 953 die(); 954 955 // Clear references to objects 956 this.audioSource = null; 957 958 // Finalise cleanup in super-class 959 super.cleanup(); 960 } 961 } 962 963 /** 964 * An internal class used to create a new thread to monitor and maintain 965 * current source position with respect to velocity. 966 */ 967 protected static class AudioSourceMoveThread extends AbstractAudioThread { 968 969 /** 970 * Reference to the AudioSource object being monitored. 971 */ 972 private AbstractAudioSource audioSource; 973 974 /** 975 * Constructor that takes handle to looping AudioSource to monitor. 976 * 977 * @param audioSource looping AudioSource to monitor 978 */ 979 AudioSourceMoveThread(AbstractAudioSource audioSource) { 980 super(); 981 this.setName("movesrc-" + super.getName()); 982 this.audioSource = audioSource; 983 if (log.isDebugEnabled()) { 984 log.debug("Created AudioSourceMoveThread for AudioSource {}", audioSource.getSystemName()); 985 } 986 } 987 988 /** 989 * Main processing loop. 990 */ 991 @Override 992 public void run() { 993 994 while (!dying()) { 995 996 // Recalculate the position 997 audioSource.calculateCurrentPosition(); 998 999 // Check state and die if not playing 1000 if (audioSource.getState() != STATE_PLAYING) { 1001 die(); 1002 } 1003 1004 // sleep for a while so as not to overload CPU 1005 snooze(100); 1006 } 1007 1008// // Reset the current position 1009// audioSource.resetCurrentPosition(); 1010 // Finish up 1011 if (log.isDebugEnabled()) { 1012 log.debug("Clean up thread {}", this.getName()); 1013 } 1014 cleanup(); 1015 } 1016 1017 /** 1018 * Shuts this thread down and clears references to created objects. 1019 */ 1020 @Override 1021 protected void cleanup() { 1022 // Thread is to shutdown 1023 die(); 1024 1025 // Clear references to objects 1026 this.audioSource = null; 1027 1028 // Finalise cleanup in super-class 1029 super.cleanup(); 1030 } 1031 } 1032 1033// /** 1034// * An internal class used to create a new thread to delay subsequent 1035// * playbacks of a non-continuous looped source. 1036// */ 1037// private class AudioSourceDelayThread extends Thread { 1038// 1039// /** 1040// * Reference to the AudioSource object being monitored 1041// */ 1042// private AbstractAudioSource audioSource; 1043// 1044// /** 1045// * Constructor that takes handle to looping AudioSource to monitor 1046// * 1047// * @param audioSource looping AudioSource to monitor 1048// */ 1049// AudioSourceDelayThread(AbstractAudioSource audioSource) { 1050// super(); 1051// this.setName("delaysrc-"+super.getName()); 1052// this.audioSource = audioSource; 1053// if (log.isDebugEnabled()) log.debug("Created AudioSourceDelayThread for AudioSource " + audioSource.getSystemName()); 1054// } 1055// 1056// /** 1057// * Main processing loop 1058// */ 1059// @Override 1060// public void run() { 1061// 1062// // Sleep for the required period of time 1063// try { 1064// Thread.sleep(audioSource.getLoopDelay()); 1065// } catch (InterruptedException ex) {} 1066// 1067// // Restart playing this AudioSource 1068// this.audioSource.play(); 1069// 1070// // Finish up 1071// if (log.isDebugEnabled()) log.debug("Clean up thread " + this.getName()); 1072// cleanup(); 1073// } 1074// 1075// /** 1076// * Shuts this thread down and clears references to created objects 1077// */ 1078// protected void cleanup() { 1079// // Clear references to objects 1080// this.audioSource = null; 1081// } 1082// } 1083 1084}