001package jmri.jmrit.vsdecoder; 002 003import java.util.ArrayList; 004import java.util.HashMap; 005import java.util.Iterator; 006import java.util.List; 007import java.nio.ByteBuffer; 008import jmri.Audio; 009import jmri.AudioException; 010import jmri.jmrit.audio.AudioBuffer; 011import jmri.util.PhysicalLocation; 012import org.jdom2.Element; 013 014/** 015 * Diesel Sound version 3. 016 * 017 * <hr> 018 * This file is part of JMRI. 019 * <p> 020 * JMRI is free software; you can redistribute it and/or modify it under 021 * the terms of version 2 of the GNU General Public License as published 022 * by the Free Software Foundation. See the "COPYING" file for a copy 023 * of this license. 024 * <p> 025 * JMRI is distributed in the hope that it will be useful, but WITHOUT 026 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 027 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 028 * for more details. 029 * 030 * @author Mark Underwood Copyright (C) 2011 031 * @author Klaus Killinger Copyright (C) 2018-2021, 2023, 2025, 2026 032 */ 033class Diesel3Sound extends EngineSound { 034 035 // Engine Sounds 036 private HashMap<Integer, D3Notch> notch_sounds; 037 private String _soundName; 038 039 private AudioBuffer start_buffer; 040 private AudioBuffer stop_buffer; 041 private Integer idle_notch; 042 private int first_notch; 043 int top_speed; 044 static final int number_helper_buffers = 5; // in the loop-player is a limit of 2, but the unqueue takes some time too 045 046 // Common variables 047 private int current_notch = 1; // default 048 private D3LoopThread _loopThread = null; 049 050 public Diesel3Sound(String name) { 051 super(name); 052 log.debug("New Diesel3Sound name(param): {}, name(val): {}", name, this.getName()); 053 } 054 055 private void startThread() { 056 _loopThread = new D3LoopThread(this, notch_sounds.get(current_notch), _soundName, true); 057 _loopThread.setName("Diesel3Sound.D3LoopThread"); 058 log.debug("Loop Thread Started. Sound name: {}", _soundName); 059 } 060 061 // Responds to "CHANGE" trigger 062 @Override 063 public void changeThrottle(float s) { 064 // This is all we have to do. The loop thread will handle everything else. 065 if (_loopThread != null) { 066 _loopThread.setThrottle(s); 067 } 068 } 069 070 // Responds to throttle loco direction key (see EngineSound.java and EngineSoundEvent.java) 071 @Override 072 public void changeLocoDirection(int dirfn) { 073 log.debug("loco IsForward is {}", dirfn); 074 if (_loopThread != null) { 075 _loopThread.getLocoDirection(dirfn); 076 } 077 } 078 079 // Responds to throttle function key (see EngineSound.java and EngineSoundEvent.java) 080 @Override 081 public void functionKey(String event, boolean value, String name) { 082 log.debug("throttle function key {} pressed for {}: {}", event, name, value); 083 if (_loopThread != null) { 084 _loopThread.setFunction(event, value, name); 085 } 086 } 087 088 private D3Notch getNotch(int n) { 089 return notch_sounds.get(n); 090 } 091 092 @Override 093 public void startEngine() { 094 log.debug("startEngine. ID: {}", this.getName()); 095 if (_loopThread != null) { 096 _loopThread.startEngine(start_buffer); 097 } 098 } 099 100 @Override 101 public void stopEngine() { 102 log.debug("stopEngine. ID: {}", this.getName()); 103 if (_loopThread != null) { 104 _loopThread.stopEngine(stop_buffer); 105 } 106 } 107 108 @Override 109 public void shutdown() { 110 // Stop the loop thread, in case it's running 111 if (_loopThread != null) { 112 _loopThread.setRunning(false); 113 } 114 } 115 116 @Override 117 public void mute(boolean m) { 118 if (_loopThread != null) { 119 _loopThread.mute(m); 120 } 121 } 122 123 @Override 124 public void setVolume(float v) { 125 if (_loopThread != null) { 126 _loopThread.setVolume(v); 127 } 128 } 129 130 @Override 131 public void setPosition(PhysicalLocation p) { 132 if (_loopThread != null) { 133 _loopThread.setPosition(p); 134 } 135 } 136 137 @Override 138 public Element getXml() { 139 Element me = new Element("sound"); 140 me.setAttribute("name", this.getName()); 141 me.setAttribute("type", "engine"); 142 // Do something, eventually... 143 return me; 144 } 145 146 @Override 147 public void setXml(Element e, VSDFile vf) { 148 Element el; 149 String fn; 150 String in; 151 D3Notch sb; 152 int frame_size = 0; 153 int freq = 0; 154 boolean buffer_ok = true; 155 156 // Handle the common stuff. 157 super.setXml(e, vf); 158 159 _soundName = this.getName() + ":LoopSound"; 160 log.debug("get name: {}, soundName: {}, name: {}", this.getName(), _soundName, name); 161 162 // Optional value 163 in = e.getChildText("top-speed"); 164 if (in != null) { 165 top_speed = Integer.parseInt(in); 166 } else { 167 top_speed = 0; // default 168 } 169 log.debug("top speed forward: {} MPH", top_speed); 170 171 notch_sounds = new HashMap<>(); 172 in = e.getChildText("idle-notch"); 173 idle_notch = null; 174 if (in != null) { 175 idle_notch = Integer.parseInt(in); 176 log.debug("Notch {} is Idle.", idle_notch); 177 } else { 178 // leave idle_notch null for now. We'll use it at the end to trigger a "grandfathering" action 179 log.warn("No Idle Notch Specified!"); 180 } 181 182 is_auto_start = setXMLAutoStart(e); 183 log.debug("config auto-start: {}", is_auto_start); 184 185 // Optional value 186 // Allows to adjust OpenAL attenuation 187 // Sounds with distance to listener position lower than reference-distance will not have attenuation 188 engine_rd = setXMLEngineReferenceDistance(e); // Handle engine reference distance 189 log.debug("engine-sound referenceDistance: {}", engine_rd); 190 191 exponent = setXMLExponent(e); 192 log.debug("exponent: {}", exponent); 193 194 // Optional value 195 // Allows to adjust the engine gain 196 in = e.getChildText("engine-gain"); 197 if ((in != null) && (!in.isEmpty())) { 198 engine_gain = Float.parseFloat(in); 199 } else { 200 engine_gain = default_gain; 201 } 202 log.debug("engine gain: {}", engine_gain); 203 204 sleep_interval = setXMLSleepInterval(e); // Optional value 205 log.debug("sleep interval: {}", sleep_interval); 206 207 // Get the notch sounds 208 Iterator<Element> itr = (e.getChildren("notch-sound")).iterator(); 209 int i = 0; 210 while (itr.hasNext()) { 211 el = itr.next(); 212 int nn = Integer.parseInt(el.getChildText("notch")); 213 sb = new D3Notch(nn); 214 sb.setIdleNotch(false); 215 if ((idle_notch != null) && (nn == idle_notch)) { 216 sb.setIdleNotch(true); 217 log.debug("Notch {} set to Idle.", nn); 218 } 219 220 List<Element> elist = el.getChildren("file"); 221 for (Element fe : elist) { 222 fn = fe.getText(); 223 if (i == 0) { 224 // Take the first notch-file to determine the audio formats (format, frequence and framesize) 225 // All files of notch_sounds must have the same audio formats 226 first_notch = nn; 227 int[] formats; 228 formats = AudioUtil.getWavFormats(D3Notch.getWavStream(vf, fn)); 229 frame_size = formats[2]; 230 freq = formats[1]; 231 sb.setBufferFmt(formats[0]); 232 sb.setBufferFreq(formats[1]); 233 sb.setBufferFrameSize(formats[2]); 234 log.debug("formats: {}", formats); 235 236 // Add some helper Buffers to the first notch 237 for (int j = 0; j < number_helper_buffers; j++) { 238 if (checkForFreeBuffer()) { 239 AudioBuffer bh = D3Notch.getBufferHelper(name + "_BUFFERHELPER_" + j, name + "_BUFFERHELPER_" + j); 240 if (bh != null) { 241 log.debug("helper buffer created: {}, name: {}", bh, bh.getSystemName()); 242 sb.addHelper(bh); 243 } else { 244 buffer_ok = false; 245 } 246 } else { 247 buffer_ok = false; 248 } 249 } 250 } 251 252 // Generate data slices from each notch sound file 253 List<ByteBuffer> l = D3Notch.getDataList(vf, fn, name + "_n" + i); 254 log.debug("{} internal sub buffers created from file {}:", l.size(), fn); 255 for (ByteBuffer b : l) { 256 log.debug(" length: {} ms", (1000 * b.limit() / frame_size) / freq); 257 } 258 sb.addLoopData(l); 259 } 260 261 sb.setNextNotch(el.getChildText("next-notch")); 262 sb.setPrevNotch(el.getChildText("prev-notch")); 263 sb.setAccelLimit(el.getChildText("accel-limit")); 264 sb.setDecelLimit(el.getChildText("decel-limit")); 265 266 if (el.getChildText("accel-file") != null) { 267 if (checkForFreeBuffer()) { 268 sb.setAccelBuffer(D3Notch.getBuffer(vf, el.getChildText("accel-file"), name + "_na" + i, name + "_na" + i)); 269 } else { 270 buffer_ok = false; 271 } 272 } else { 273 sb.setAccelBuffer(null); 274 } 275 if (el.getChildText("decel-file") != null) { 276 if (checkForFreeBuffer()) { 277 sb.setDecelBuffer(D3Notch.getBuffer(vf, el.getChildText("decel-file"), name + "_nd" + i, name + "_nd" + i)); 278 } else { 279 buffer_ok = false; 280 } 281 } else { 282 sb.setDecelBuffer(null); 283 } 284 // Store in the list. 285 notch_sounds.put(nn, sb); 286 i++; 287 } 288 289 // Get the start and stop sounds (both sounds are optional) 290 el = e.getChild("start-sound"); 291 if (el != null) { 292 fn = el.getChild("file").getValue(); 293 if (checkForFreeBuffer()) { 294 start_buffer = D3Notch.getBuffer(vf, fn, name + "_start", name + "_Start"); 295 log.debug("Start sound: {}, buffer {} created, length: {}", fn, start_buffer, SoundBite.calcLength(start_buffer)); 296 } else { 297 buffer_ok = false; 298 } 299 } 300 el = e.getChild("shutdown-sound"); 301 if (el != null) { 302 fn = el.getChild("file").getValue(); 303 if (checkForFreeBuffer()) { 304 stop_buffer = D3Notch.getBuffer(vf, fn, name + "_shutdown", name + "_Shutdown"); 305 log.debug("Shutdown sound: {}, buffer {} created, length: {}", fn, stop_buffer, SoundBite.calcLength(stop_buffer)); 306 } else { 307 buffer_ok = false; 308 } 309 } 310 311 // Handle "grandfathering" the idle notch indication 312 // If the VSD designer did not explicitly designate an idle notch... 313 // Find the Notch with the lowest notch number, and make it the idle notch. 314 // If there's a tie, this will take the first value, but the notches /should/ 315 // all have unique notch numbers. 316 if (idle_notch == null) { 317 D3Notch min_notch = null; 318 // No, this is not a terribly efficient "min" operation. But that's OK. 319 for (D3Notch n : notch_sounds.values()) { 320 if ((min_notch == null) || (n.getNotch() < min_notch.getNotch())) { 321 min_notch = n; 322 } 323 } 324 log.info("No Idle Notch Specified. Choosing Notch {} to be the Idle Notch.", (min_notch != null ? min_notch.getNotch() : "min_notch not set")); 325 if (min_notch != null) { 326 min_notch.setIdleNotch(true); 327 idle_notch = min_notch.getNotch(); 328 } else { 329 log.warn("Could not set idle notch because min_notch was still null"); 330 } 331 } 332 333 if (buffer_ok) { 334 setBuffersFreeState(true); 335 // Kick-start the loop thread. 336 this.startThread(); 337 // Check auto-start setting 338 autoStartCheck(); 339 } else { 340 setBuffersFreeState(false); 341 } 342 } 343 344 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Diesel3Sound.class); 345 346 private static class D3Notch { 347 348 private int my_notch; 349 private int next_notch; 350 private int prev_notch; 351 private int buffer_fmt; 352 private int buffer_freq; 353 private int buffer_frame_size; 354 private int loop_index; 355 private float accel_limit; 356 private float decel_limit; 357 private boolean is_idle; 358 private AudioBuffer accel_buf; 359 private AudioBuffer decel_buf; 360 private List<AudioBuffer> bufs_helper = new ArrayList<>(); 361 private List<ByteBuffer> loop_data = new ArrayList<>(); 362 363 private D3Notch(int notch) { 364 my_notch = notch; 365 loop_index = 0; 366 } 367 368 private int getNotch() { 369 return my_notch; 370 } 371 372 private int getNextNotch() { 373 return next_notch; 374 } 375 376 private int getPrevNotch() { 377 return prev_notch; 378 } 379 380 private AudioBuffer getAccelBuffer() { 381 return accel_buf; 382 } 383 384 private AudioBuffer getDecelBuffer() { 385 return decel_buf; 386 } 387 388 private float getAccelLimit() { 389 return accel_limit; 390 } 391 392 private float getDecelLimit() { 393 return decel_limit; 394 } 395 396 private Boolean isInLimits(float val) { 397 return (val >= decel_limit) && (val <= accel_limit); 398 } 399 400 private void setBufferFmt(int fmt) { 401 buffer_fmt = fmt; 402 } 403 404 private int getBufferFmt() { 405 return buffer_fmt; 406 } 407 408 private void setBufferFreq(int freq) { 409 buffer_freq = freq; 410 } 411 412 private int getBufferFreq() { 413 return buffer_freq; 414 } 415 416 private void setBufferFrameSize(int framesize) { 417 buffer_frame_size = framesize; 418 } 419 420 private int getBufferFrameSize() { 421 return buffer_frame_size; 422 } 423 424 private Boolean isIdleNotch() { 425 return is_idle; 426 } 427 428 private void setNextNotch(String s) { 429 next_notch = setIntegerFromString(s); 430 } 431 432 private void setPrevNotch(String s) { 433 prev_notch = setIntegerFromString(s); 434 } 435 436 private void setAccelLimit(String s) { 437 accel_limit = setFloatFromString(s); 438 } 439 440 private void setDecelLimit(String s) { 441 decel_limit = setFloatFromString(s); 442 } 443 444 private void setAccelBuffer(AudioBuffer b) { 445 accel_buf = b; 446 } 447 448 private void setDecelBuffer(AudioBuffer b) { 449 decel_buf = b; 450 } 451 452 private void addLoopData(List<ByteBuffer> l) { 453 loop_data.addAll(l); 454 } 455 456 private ByteBuffer nextLoopData() { 457 return loop_data.get(incLoopIndex()); 458 } 459 460 private void setIdleNotch(Boolean i) { 461 is_idle = i; 462 } 463 464 private void addHelper(AudioBuffer b) { 465 bufs_helper.add(b); 466 } 467 468 private int incLoopIndex() { 469 // Increment 470 loop_index++; 471 // Correct for wrap. 472 if (loop_index >= loop_data.size()) { 473 loop_index = 0; 474 } 475 return loop_index; 476 } 477 478 private int setIntegerFromString(String s) { 479 if (s == null) { 480 return 0; 481 } 482 try { 483 int n = Integer.parseInt(s); 484 return n; 485 } catch (NumberFormatException e) { 486 d3NotchLog.debug("Invalid integer: {}", s); 487 return 0; 488 } 489 } 490 491 private float setFloatFromString(String s) { 492 if (s == null) { 493 return 0.0f; 494 } 495 try { 496 float f = Float.parseFloat(s) / 100.0f; 497 return f; 498 } catch (NumberFormatException e) { 499 d3NotchLog.debug("Invalid float: {}", s); 500 return 0.0f; 501 } 502 } 503 504 private static List<ByteBuffer> getDataList(VSDFile vf, String filename, String sname) { 505 List<ByteBuffer> datalist = null; 506 java.io.InputStream ins = vf.getInputStream(filename); 507 if (ins != null) { 508 datalist = AudioUtil.getByteBufferList(ins, 250, 150); 509 } else { 510 d3NotchLog.debug("Input Stream failed"); 511 return null; 512 } 513 return datalist; 514 } 515 516 private static AudioBuffer getBuffer(VSDFile vf, String filename, String sname, String uname) { 517 AudioBuffer buf = null; 518 jmri.AudioManager am = jmri.InstanceManager.getDefault(jmri.AudioManager.class); 519 try { 520 buf = (AudioBuffer) am.provideAudio(VSDSound.BufSysNamePrefix + sname); 521 buf.setUserName(VSDSound.BufUserNamePrefix + uname); 522 java.io.InputStream ins = vf.getInputStream(filename); 523 if (ins != null) { 524 buf.setInputStream(ins); 525 } else { 526 d3NotchLog.debug("Input Stream failed"); 527 return null; 528 } 529 } catch (AudioException | IllegalArgumentException ex) { 530 d3NotchLog.error("Problem creating SoundBite", ex); 531 return null; 532 } 533 d3NotchLog.debug("Buffer created: {}, name: {}", buf, buf.getSystemName()); 534 return buf; 535 } 536 537 private static AudioBuffer getBufferHelper(String sname, String uname) { 538 AudioBuffer bf = null; 539 jmri.AudioManager am = jmri.InstanceManager.getDefault(jmri.AudioManager.class); 540 try { 541 bf = (AudioBuffer) am.provideAudio(VSDSound.BufSysNamePrefix + sname); 542 bf.setUserName(VSDSound.BufUserNamePrefix + uname); 543 } catch (AudioException | IllegalArgumentException ex) { 544 d3NotchLog.warn("problem creating SoundBite", ex); 545 return null; 546 } 547 d3NotchLog.debug("empty buffer created: {}, name: {}", bf, bf.getSystemName()); 548 return bf; 549 } 550 551 private static java.io.InputStream getWavStream(VSDFile vf, String filename) { 552 java.io.InputStream ins = vf.getInputStream(filename); 553 if (ins != null) { 554 return ins; 555 } else { 556 d3NotchLog.warn("input Stream failed for {}", filename); 557 return null; 558 } 559 } 560 561 private static final org.slf4j.Logger d3NotchLog = org.slf4j.LoggerFactory.getLogger(D3Notch.class); 562 563 } 564 565 private static class D3LoopThread extends Thread { 566 567 private boolean is_running; 568 private boolean is_looping; 569 private boolean is_in_rampup_mode; 570 private boolean is_key_coasting; 571 private boolean is_key_notching; 572 private boolean is_emergency_stop; 573 private Diesel3Sound _parent; 574 private D3Notch _notch; 575 private D3Notch notch1; 576 private SoundBite _sound; 577 private float _throttle; 578 private float rpm_dirfn; 579 private int helper_index; 580 581 private D3LoopThread(Diesel3Sound d, D3Notch n, String s, boolean r) { 582 super(); 583 is_running = r; 584 is_looping = false; 585 is_in_rampup_mode = false; 586 is_key_coasting = false; 587 is_key_notching = false; 588 is_emergency_stop = false; 589 _parent = d; 590 _notch = n; 591 _sound = new SoundBite(s); 592 _sound.isInitialized(); 593 _sound.setGain(_parent.engine_gain); 594 _throttle = 0.0f; 595 rpm_dirfn = 0.0f; 596 if (r) { 597 this.start(); 598 } 599 } 600 601 private void setRunning(boolean r) { 602 is_running = r; 603 } 604 605 private void setThrottle(float t) { 606 if (_parent.isEngineStarted()) { 607 if (t < 0.0f) { 608 t = 0.0f; 609 _throttle = 0.0f; 610 _parent.setActualSpeed(_throttle); 611 is_in_rampup_mode = false; // interrupt ramp-up 612 is_emergency_stop = true; 613 is_key_coasting = false; 614 } else { 615 _parent.setActualSpeed((float) _parent.speedCurve(t)); 616 if (!is_key_notching && !is_key_coasting) { 617 _throttle = t; 618 d3LoopLog.debug("Throttle set: {}", _throttle); 619 } 620 } 621 } 622 } 623 624 private void getLocoDirection(int d) { 625 d3LoopLog.debug("loco direction: {}", d); 626 627 // React to a change in direction to slow down, 628 // then change direction, then ramp-up to the old speed 629 if (_throttle > 0.0f && _parent.isEngineStarted() && !is_in_rampup_mode) { 630 rpm_dirfn = _throttle; // save rpm for ramp-up 631 d3LoopLog.debug("speed {} saved", rpm_dirfn); 632 is_in_rampup_mode = true; // set a flag for the ramp-up 633 _throttle = 0.0f; 634 _parent.setActualSpeed(_throttle); 635 } 636 } 637 638 private void setFunction(String event, boolean is_true, String name) { 639 // Note: throttle will send initial value(s) before thread is started! 640 d3LoopLog.debug("throttle function key pressed: {} is {}, function: {}", event, is_true, name); 641 if (name.equals("COAST")) { 642 // Handle key-coasting on/off 643 if (is_true) { 644 is_key_coasting = true; 645 _notch = _parent.getNotch(_parent.first_notch); 646 _throttle = _notch.getDecelLimit(); 647 changeNotch(); 648 } else { 649 _throttle = _parent.getActualSpeed(); 650 changeNotch(); 651 is_key_coasting = false; 652 } 653 } 654 655 // Notch change if NOTCH_UP key is pressed 656 if (name.equals("NOTCH_UP")) { 657 // I assume that a NOTCH_DOWN is also declared 658 is_key_notching = true; 659 // the engine must be starting up for key-notching to work 660 if (_parent.isEngineStarted() && is_true) { 661 d3LoopLog.debug("current notch: {}, accel limit: {}", _notch.getNotch(), _notch.getAccelLimit()); 662 _notch = _parent.getNotch(_notch.getNotch()); 663 if (_notch.getNotch() < _parent.notch_sounds.size()) { 664 _throttle = _notch.getAccelLimit() + 0.01f; 665 changeNotch(); 666 } 667 } 668 } 669 // Notch change if NOTCH_DOWN key is pressed 670 if (name.equals("NOTCH_DOWN")) { 671 if (_parent.isEngineStarted() && is_true) { 672 d3LoopLog.debug("current notch: {}, decel limit: {}", _notch.getNotch(), _notch.getDecelLimit()); 673 _notch = _parent.getNotch(_notch.getNotch()); 674 _throttle = _notch.getDecelLimit() - 0.01f; 675 if (_throttle < 0) _throttle = 0.0f; 676 changeNotch(); 677 } 678 } 679 } 680 681 public void startEngine(AudioBuffer start_buf) { 682 _sound.unqueueBuffers(); 683 d3LoopLog.debug("thread: start engine ..."); 684 685 helper_index = -1; // Prepare helper buffer start; index will be incremented before first use 686 notch1 = _parent.getNotch(_parent.first_notch); 687 688 _sound.setReferenceDistance(_parent.engine_rd); 689 d3LoopLog.debug("set reference distance to {} for engine sound", _sound.getReferenceDistance()); 690 691 _notch = _parent.getNotch(_parent.first_notch); 692 d3LoopLog.debug("Notch: {}, prev: {}, next: {}", _notch.getNotch(), _notch.getPrevNotch(), _notch.getNextNotch()); 693 694 if (_parent.engine_pane != null) { 695 _parent.engine_pane.setThrottle(_notch.getNotch()); // Set EnginePane (DieselPane) notch 696 } 697 698 // Only queue the start buffer if we know we're in the idle notch. 699 // This is indicated by prevNotch == self. 700 if (_notch.isIdleNotch()) { 701 _sound.queueBuffer(start_buf); 702 if (_parent.engine_pane != null) { 703 _parent.engine_pane.setButtonDelay(SoundBite.calcLength(start_buf)); 704 } 705 } else { 706 setSound(_notch.nextLoopData()); 707 } 708 709 // Follow up with another loop buffer. 710 setSound(_notch.nextLoopData()); 711 is_looping = true; 712 if (_sound.getSource().getState() != Audio.STATE_PLAYING) { 713 _sound.play(); 714 } 715 } 716 717 public void stopEngine(AudioBuffer stop_buf) { 718 d3LoopLog.debug("thread: stop engine ..."); 719 is_looping = false; // stop the loop player 720 _throttle = 0.0f; // Clear it, just in case the engine was stopped at speed > 0 721 _parent.setActualSpeed(0.0f); 722 if (_parent.engine_pane != null) { 723 _parent.engine_pane.setThrottle(_parent.idle_notch); // Set EnginePane (DieselPane) notch 724 _parent.engine_pane.setButtonDelay(SoundBite.calcLength(stop_buf)); 725 _sound.queueBuffer(stop_buf); 726 if (_sound.getSource().getState() != Audio.STATE_PLAYING) { 727 _sound.play(); 728 } 729 } 730 } 731 732 // loop-player 733 @Override 734 public void run() { 735 try { 736 while (is_running) { 737 if (is_looping && AudioUtil.isAudioRunning()) { 738 if (_sound.getSource().numProcessedBuffers() > 0) { 739 _sound.unqueueBuffers(); 740 } 741 //log.debug("D3Loop {} Run loop. Buffers: {}", _sound.getName(), _sound.getSource().numQueuedBuffers()); 742 if (!_notch.isInLimits(_throttle)) { 743 changeNotch(); 744 } 745 if (_sound.getSource().numQueuedBuffers() < 2) { 746 setSound(_notch.nextLoopData()); 747 } 748 checkAudioState(); 749 } else { 750 if (_sound.getSource().numProcessedBuffers() > 0) { 751 _sound.unqueueBuffers(); 752 } 753 } 754 sleep(_parent.sleep_interval); 755 checkRampup(); 756 } 757 _sound.stop(); 758 } catch (InterruptedException ie) { 759 // kill thread 760 d3LoopLog.debug("thread interrupted"); 761 } 762 } 763 764 private void checkAudioState() { 765 if (_sound.getSource().getState() != Audio.STATE_PLAYING) { 766 _sound.play(); 767 d3LoopLog.info("loop sound re-started"); 768 } 769 } 770 771 private void changeNotch() { 772 AudioBuffer transition_buf = null; 773 int new_notch = _notch.getNotch(); 774 775 d3LoopLog.debug("D3Thread Change Throttle: {}, Accel Limit: {}, Decel Limit: {}", _throttle, _notch.getAccelLimit(), _notch.getDecelLimit()); 776 if (_throttle > _notch.getAccelLimit()) { 777 // Too fast. Need to go to next notch up. 778 if (_notch.getNotch() < _notch.getNextNotch()) { 779 // prepare for next notch up 780 transition_buf = _notch.getAccelBuffer(); 781 new_notch = _notch.getNextNotch(); 782 d3LoopLog.debug("Change up. notch: {}", new_notch); 783 } 784 } else if (_throttle < _notch.getDecelLimit()) { 785 // Too slow. Need to go to next notch down. 786 transition_buf = _notch.getDecelBuffer(); 787 new_notch = _notch.getPrevNotch(); 788 d3LoopLog.debug("Change down. notch: {}", new_notch); 789 } 790 _parent.engine_pane.setThrottle(new_notch); // Update EnginePane (DieselPane) notch 791 // Now, regardless of whether we're going up or down, set the timer, 792 // fade the current sound, and move on. 793 if (transition_buf == null || is_key_coasting || is_emergency_stop) { 794 // No transition sound to play. Skip the timer bit 795 // Recurse directly to try the next notch 796 _notch = _parent.getNotch(new_notch); 797 } else { 798 // queue up the transition sound buffer 799 _notch = _parent.getNotch(new_notch); 800 _sound.queueBuffer(transition_buf); 801 } 802 if (new_notch == 1) { 803 is_emergency_stop = false; 804 } 805 } 806 807 private void setSound(ByteBuffer data) { 808 AudioBuffer buf = notch1.bufs_helper.get(incHelperIndex()); // buffer for the queue 809 int sbl = 0; // sound bite length 810 int bbufcount; // number of bytes for the sound clip 811 ByteBuffer bbuf; 812 byte[] bbytes; 813 814 if (notch1.getBufferFreq() > 0) { 815 sbl = (1000 * data.limit() / notch1.getBufferFrameSize()) / notch1.getBufferFreq(); // calculate the length of the clip in milliseconds 816 // Prepare the sound and transfer it to the target ByteBuffer bbuf 817 bbufcount = notch1.getBufferFrameSize() * (sbl * notch1.getBufferFreq() / 1000); 818 bbuf = ByteBuffer.allocateDirect(bbufcount); // Target 819 bbytes = new byte[bbufcount]; 820 data.get(bbytes); // Same as: data.get(bbytes, 0, bbufcount2); 821 data.rewind(); 822 bbuf.order(data.order()); // Set new buffer's byte order to match source buffer. 823 bbuf.put(bbytes); 824 bbuf.rewind(); 825 buf.loadBuffer(bbuf, notch1.getBufferFmt(), notch1.getBufferFreq()); 826 _sound.queueBuffer(buf); 827 } 828 } 829 830 private void checkRampup() { 831 // Handle a throttle forward or reverse change 832 if (is_in_rampup_mode && _throttle == 0.0f && _notch.getNotch() == _parent.idle_notch) { 833 d3LoopLog.debug("now ramp-up to speed {}", rpm_dirfn); 834 is_in_rampup_mode = false; 835 _throttle = rpm_dirfn; 836 } 837 } 838 839 private int incHelperIndex() { 840 helper_index++; 841 // Correct for wrap. 842 if (helper_index >= Diesel3Sound.number_helper_buffers) { 843 helper_index = 0; 844 } 845 return helper_index; 846 } 847 848 private void mute(boolean m) { 849 _sound.mute(m); 850 } 851 852 private void setVolume(float v) { 853 _sound.setVolume(v); 854 } 855 856 private void setPosition(PhysicalLocation p) { 857 _sound.setPosition(p); 858 if (_parent.getTunnel()) { 859 _sound.attachSourcesToEffects(); 860 } else { 861 _sound.detachSourcesToEffects(); 862 } 863 } 864 865 private static final org.slf4j.Logger d3LoopLog = org.slf4j.LoggerFactory.getLogger(D3LoopThread.class); 866 867 } 868}