001package jmri.jmrit.vsdecoder; 002 003import java.awt.event.ActionEvent; 004import java.awt.event.ActionListener; 005import java.util.ArrayList; 006import java.util.HashMap; 007import java.util.Iterator; 008import java.util.List; 009import java.nio.ByteBuffer; 010import jmri.Audio; 011import jmri.AudioException; 012import jmri.jmrit.audio.AudioBuffer; 013import jmri.util.PhysicalLocation; 014import org.jdom2.Element; 015 016/** 017 * Steam Sound version 1 (adapted from Diesel3Sound). 018 * 019 * <hr> 020 * This file is part of JMRI. 021 * <p> 022 * JMRI is free software; you can redistribute it and/or modify it under 023 * the terms of version 2 of the GNU General Public License as published 024 * by the Free Software Foundation. See the "COPYING" file for a copy 025 * of this license. 026 * <p> 027 * JMRI is distributed in the hope that it will be useful, but WITHOUT 028 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 029 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 030 * for more details. 031 * 032 * @author Mark Underwood Copyright (C) 2011 033 * @author Klaus Killinger Copyright (C) 2017-2021, 2023, 2025 034 */ 035class Steam1Sound extends EngineSound { 036 037 // Engine Sounds 038 private HashMap<Integer, S1Notch> notch_sounds; 039 040 // Trigger Sounds 041 private HashMap<String, SoundBite> trigger_sounds; 042 043 private String _soundName; 044 int top_speed; 045 int top_speed_reverse; 046 private float driver_diameter_float; 047 private int num_cylinders; 048 private int accel_rate; 049 private int decel_rate; 050 private int brake_time; 051 private int decel_trigger_rpms; 052 private int wait_factor; 053 private boolean is_dynamic_gain; 054 private boolean use_chuff_fade_out; 055 056 private SoundBite idle_sound; 057 private SoundBite boiling_sound; 058 private SoundBite brake_sound; 059 private SoundBite pre_arrival_sound; 060 061 private S1LoopThread _loopThread = null; 062 063 private javax.swing.Timer rpmTimer; 064 private int accdectime; 065 066 // Constructor 067 public Steam1Sound(String name) { 068 super(name); 069 log.debug("New Steam1Sound name(param): {}, name(val): {}", name, this.getName()); 070 } 071 072 private void startThread() { 073 _loopThread = new S1LoopThread(this, _soundName, top_speed, top_speed_reverse, 074 driver_diameter_float, num_cylinders, decel_trigger_rpms, true); 075 _loopThread.setName("Steam1Sound.S1LoopThread"); 076 log.debug("Loop Thread Started. Sound name: {}", _soundName); 077 } 078 079 // Responds to "CHANGE" trigger (float) 080 @Override 081 public void changeThrottle(float s) { 082 // This is all we have to do. The loop thread will handle everything else 083 if (_loopThread != null) { 084 _loopThread.setThrottle(s); 085 } 086 } 087 088 @Override 089 public void changeLocoDirection(int dirfn) { 090 log.debug("loco IsForward is {}", dirfn); 091 if (_loopThread != null) { 092 _loopThread.getLocoDirection(dirfn); 093 } 094 } 095 096 @Override 097 public void functionKey(String event, boolean value, String name) { 098 log.debug("throttle function key {} pressed for {}: {}", event, name, value); 099 if (_loopThread != null) { 100 _loopThread.setFunction(event, value, name); 101 } 102 } 103 104 private S1Notch getNotch(int n) { 105 return notch_sounds.get(n); 106 } 107 108 private void initAccDecTimer() { 109 rpmTimer = newTimer(1, true, new ActionListener() { 110 @Override 111 public void actionPerformed(ActionEvent e) { 112 if (_loopThread != null) { 113 rpmTimer.setDelay(accdectime); // Update delay time 114 _loopThread.updateRpm(); 115 } 116 } 117 }); 118 log.debug("timer {} initialized, delay: {}", rpmTimer, accdectime); 119 } 120 121 private void startAccDecTimer() { 122 if (!rpmTimer.isRunning()) { 123 rpmTimer.start(); 124 log.debug("timer {} started, delay: {}", rpmTimer, accdectime); 125 } 126 } 127 128 private void stopAccDecTimer() { 129 if (rpmTimer.isRunning()) { 130 rpmTimer.stop(); 131 log.debug("timer {} stopped, delay: {}", rpmTimer, accdectime); 132 } 133 } 134 135 private VSDecoder getVsd() { 136 return VSDecoderManager.instance().getVSDecoderByID(_soundName.substring(0, _soundName.indexOf("ENGINE") - 1)); 137 } 138 139 @Override 140 public void startEngine() { 141 log.debug("startEngine. ID: {}", this.getName()); 142 if (_loopThread != null) { 143 _loopThread.startEngine(); 144 } 145 } 146 147 @Override 148 public void stopEngine() { 149 log.debug("stopEngine. ID = {}", this.getName()); 150 if (_loopThread != null) { 151 _loopThread.stopEngine(); 152 } 153 } 154 155 // Called when deleting a VSDecoder or closing the VSDecoder Manager 156 // There is one thread for every VSDecoder 157 @Override 158 public void shutdown() { 159 for (VSDSound vs : trigger_sounds.values()) { 160 log.debug(" Stopping trigger sound: {}", vs.getName()); 161 vs.stop(); // SoundBite: Stop playing 162 } 163 if (rpmTimer != null) { 164 stopAccDecTimer(); 165 } 166 167 // Stop the loop thread, in case it's running 168 if (_loopThread != null) { 169 _loopThread.setRunning(false); 170 } 171 } 172 173 @Override 174 public void mute(boolean m) { 175 if (_loopThread != null) { 176 _loopThread.mute(m); 177 } 178 } 179 180 @Override 181 public void setVolume(float v) { 182 if (_loopThread != null) { 183 _loopThread.setVolume(v); 184 } 185 } 186 187 @Override 188 public void setPosition(PhysicalLocation p) { 189 if (_loopThread != null) { 190 _loopThread.setPosition(p); 191 } 192 } 193 194 @Override 195 public Element getXml() { 196 Element me = new Element("sound"); 197 me.setAttribute("name", this.getName()); 198 me.setAttribute("type", "engine"); 199 // Do something, eventually... 200 return me; 201 } 202 203 @Override 204 public void setXml(Element e, VSDFile vf) { 205 boolean buffer_ok = true; 206 Element el; 207 String fn; 208 String n; 209 S1Notch sb; 210 211 // Handle the common stuff 212 super.setXml(e, vf); 213 214 _soundName = this.getName() + ":LoopSound"; 215 log.debug("Steam1: name: {}, soundName: {}", this.getName(), _soundName); 216 217 top_speed = Integer.parseInt(e.getChildText("top-speed")); // Required value 218 log.debug("top speed forward: {} MPH", top_speed); 219 220 // Steam locos can have different top speed reverse 221 n = e.getChildText("top-speed-reverse"); // Optional value 222 if ((n != null) && !(n.isEmpty())) { 223 top_speed_reverse = Integer.parseInt(n); 224 } else { 225 top_speed_reverse = top_speed; // Default for top_speed_reverse 226 } 227 log.debug("top speed reverse: {} MPH", top_speed_reverse); 228 229 // Required values 230 driver_diameter_float = Float.parseFloat(e.getChildText("driver-diameter-float")); 231 log.debug("driver diameter: {} inches", driver_diameter_float); 232 num_cylinders = Integer.parseInt(e.getChildText("cylinders")); 233 log.debug("Number of cylinders defined: {}", num_cylinders); 234 235 // Allows to adjust speed 236 exponent = setXMLExponent(e); 237 log.debug("exponent: {}", exponent); 238 239 // Acceleration and deceleration rate 240 n = e.getChildText("accel-rate"); // Optional value 241 if ((n != null) && !(n.isEmpty())) { 242 accel_rate = Integer.parseInt(n); 243 } else { 244 accel_rate = 35; // Default 245 } 246 log.debug("accel rate: {}", accel_rate); 247 248 n = e.getChildText("decel-rate"); // Optional value 249 if ((n != null) && !(n.isEmpty())) { 250 decel_rate = Integer.parseInt(n); 251 } else { 252 decel_rate = 18; // Default 253 } 254 log.debug("decel rate: {}", decel_rate); 255 256 n = e.getChildText("brake-time"); // Optional value 257 if ((n != null) && !(n.isEmpty())) { 258 brake_time = Integer.parseInt(n); 259 } else { 260 brake_time = 0; // Default 261 } 262 log.debug("brake time: {}", brake_time); 263 264 // auto-start 265 is_auto_start = setXMLAutoStart(e); // Optional value 266 log.debug("config auto-start: {}", is_auto_start); 267 268 // Allows to adjust OpenAL attenuation 269 // Sounds with distance to listener position lower than reference-distance will not have attenuation 270 engine_rd = setXMLEngineReferenceDistance(e); // Optional value 271 log.debug("engine-sound referenceDistance: {}", engine_rd); 272 273 // Allows to adjust the engine gain 274 n = e.getChildText("engine-gain"); // Optional value 275 if ((n != null) && !(n.isEmpty())) { 276 engine_gain = Float.parseFloat(n); 277 // Make some restrictions, since engine_gain is used for calculations later 278 if ((engine_gain < default_gain - 0.4f) || (engine_gain > default_gain + 0.2f)) { 279 log.info("Invalid engine gain {} was set to default {}", engine_gain, default_gain); 280 engine_gain = default_gain; 281 } 282 } else { 283 engine_gain = default_gain; 284 } 285 log.debug("engine gain: {}", engine_gain); 286 287 // Allows to handle dynamic gain for chuff sounds 288 n = e.getChildText("dynamic-gain"); // Optional value 289 if ((n != null) && (n.equals("yes"))) { 290 is_dynamic_gain = true; 291 } else { 292 is_dynamic_gain = false; 293 } 294 log.debug("dynamic gain: {}", is_dynamic_gain); 295 296 // Allows to fade out from chuff to coast sounds 297 n = e.getChildText("chuff-fade-out"); // Optional value 298 if ((n != null) && (n.equals("yes"))) { 299 use_chuff_fade_out = true; 300 } else { 301 use_chuff_fade_out = false; // Default 302 } 303 log.debug("chuff fade out: {}", use_chuff_fade_out); 304 305 // Defines how many loops (50ms) to be subtracted from interval to calculate wait-time 306 // The lower the wait-factor, the more effect it has 307 // Better to take a higher value when running VSD on old/slow computers 308 n = e.getChildText("wait-factor"); // Optional value 309 if ((n != null) && !(n.isEmpty())) { 310 wait_factor = Integer.parseInt(n); 311 // Make some restrictions to protect the loop-player 312 if (wait_factor < 5 || wait_factor > 40) { 313 log.info("Invalid wait-factor {} was set to default 18", wait_factor); 314 wait_factor = 18; 315 } 316 } else { 317 wait_factor = 18; // Default 318 } 319 log.debug("number of loops to subtract from interval: {}", wait_factor); 320 321 // Defines how many rpms in 0.5 seconds will trigger decel actions like braking 322 n = e.getChildText("decel-trigger-rpms"); // Optional value 323 if ((n != null) && !(n.isEmpty())) { 324 decel_trigger_rpms = Integer.parseInt(n); 325 } else { 326 decel_trigger_rpms = 999; // Default (need a value) 327 } 328 log.debug("number of rpms to trigger decelerating actions: {}", decel_trigger_rpms); 329 330 sleep_interval = setXMLSleepInterval(e); // Optional value 331 log.debug("sleep interval: {}", sleep_interval); 332 333 // Get the sounds 334 // Note: each sound must have equal attributes, e.g. 16-bit, 44100 Hz 335 // Get the files and create a buffer and byteBuffer for each file 336 // For each notch there must be <num_cylinders * 2> chuff files 337 notch_sounds = new HashMap<>(); 338 int nn = 1; // notch number (visual) 339 340 // Get the notch-sounds 341 Iterator<Element> itr = (e.getChildren("s1notch-sound")).iterator(); 342 while (itr.hasNext()) { 343 el = itr.next(); 344 sb = new S1Notch(nn); 345 346 // Get the medium/standard chuff sounds 347 List<Element> elist = el.getChildren("notch-file"); 348 for (Element fe : elist) { 349 fn = fe.getText(); 350 log.debug("notch: {}, file: {}", nn, fn); 351 sb.addChuffData(AudioUtil.getWavData(S1Notch.getWavStream(vf, fn))); 352 } 353 log.debug("Number of chuff medium/standard sounds for notch {} defined: {}", nn, elist.size()); 354 355 // Filler sound, coasting sound and helpers are bound to the first notch only 356 // VSDFile validation makes sure that there is at least one notch 357 if (nn == 1) { 358 // Take the first notch-file to determine the audio formats (format, frequence and framesize) 359 // All files of notch_sounds must have the same audio formats 360 fn = el.getChildText("notch-file"); 361 int[] formats; 362 formats = AudioUtil.getWavFormats(S1Notch.getWavStream(vf, fn)); 363 sb.setBufferFmt(formats[0]); 364 sb.setBufferFreq(formats[1]); 365 sb.setBufferFrameSize(formats[2]); 366 367 log.debug("WAV audio formats - format: {}, frequence: {}, frame size: {}", 368 sb.getBufferFmt(), sb.getBufferFreq(), sb.getBufferFrameSize()); 369 370 // Revert chuff_fade_out if audio format is wrong 371 if (use_chuff_fade_out && sb.getBufferFmt() != com.jogamp.openal.AL.AL_FORMAT_MONO16) { 372 use_chuff_fade_out = false; // Default 373 log.warn("chuff-fade-out disabled; 16-bit sounds needed"); 374 } 375 376 // Create a filler Buffer for queueing and a ByteBuffer for length modification 377 fn = el.getChildText("notchfiller-file"); 378 if (fn != null) { 379 log.debug("notch filler file: {}", fn); 380 sb.setNotchFillerData(AudioUtil.getWavData(S1Notch.getWavStream(vf, fn))); 381 } else { 382 log.debug("no notchfiller available."); 383 sb.setNotchFillerData(null); 384 } 385 386 // Get the coasting sounds. 387 List<Element> elistc = el.getChildren("coast-file"); 388 for (Element fe : elistc) { 389 fn = fe.getText(); 390 log.debug("coasting file: {}", fn); 391 sb.addCoastData(AudioUtil.getWavData(S1Notch.getWavStream(vf, fn))); 392 } 393 log.debug("Number of coasting sounds for notch {} defined: {}", nn, elistc.size()); 394 395 // Create a filler Buffer for queueing and a ByteBuffer for length modification 396 fn = el.getChildText("coastfiller-file"); 397 if (fn != null) { 398 log.debug("coasting filler file: {}", fn); 399 sb.setCoastFillerData(AudioUtil.getWavData(S1Notch.getWavStream(vf, fn))); 400 } else { 401 log.debug("no coastfiller available."); 402 sb.setCoastFillerData(null); 403 } 404 405 // Add some helper Buffers. They are needed for creating 406 // variable sound clips in length. Twelve helper buffers should 407 // serve well for that purpose. 408 for (int j = 0; j < 12; j++) { 409 if (checkForFreeBuffer()) { 410 AudioBuffer bh = S1Notch.getBufferHelper(name + "_BUFFERHELPER_" + j, name + "_BUFFERHELPER_" + j); 411 if (bh != null) { 412 log.debug("buffer helper created: {}, name: {}", bh, bh.getSystemName()); 413 sb.addHelper(bh); 414 } else { 415 buffer_ok = false; 416 } 417 } else { 418 buffer_ok = false; 419 } 420 } 421 } 422 423 sb.setMinLimit(Integer.parseInt(el.getChildText("min-rpm"))); 424 sb.setMaxLimit(Integer.parseInt(el.getChildText("max-rpm"))); 425 426 // Store in the list 427 notch_sounds.put(nn, sb); 428 nn++; 429 } 430 log.debug("Number of notches defined: {}", notch_sounds.size()); 431 432 // Get the trigger sounds 433 // Note: other than notch sounds, trigger sounds can have different attributes 434 trigger_sounds = new HashMap<>(); 435 436 // Get the idle sound 437 el = e.getChild("idle-sound"); 438 if (el != null) { 439 fn = el.getChild("sound-file").getValue(); 440 log.debug("idle sound: {}", fn); 441 idle_sound = new SoundBite(vf, fn, _soundName + "_IDLE", _soundName + "_Idle"); 442 if (idle_sound.isInitialized()) { 443 idle_sound.setGain(setXMLGain(el)); // Handle gain 444 log.debug("idle sound gain: {}", idle_sound.getGain()); 445 idle_sound.setLooped(true); 446 idle_sound.setFadeTimes(500, 500); 447 idle_sound.setReferenceDistance(setXMLReferenceDistance(el)); // Handle reference distance 448 log.debug("idle-sound reference distance: {}", idle_sound.getReferenceDistance()); 449 trigger_sounds.put("idle", idle_sound); 450 log.debug("trigger idle sound: {}", trigger_sounds.get("idle")); 451 } else { 452 buffer_ok = false; 453 } 454 } 455 456 // Get the boiling sound 457 el = e.getChild("boiling-sound"); 458 if (el != null) { 459 fn = el.getChild("sound-file").getValue(); 460 boiling_sound = new SoundBite(vf, fn, name + "_BOILING", name + "_Boiling"); 461 if (boiling_sound.isInitialized()) { 462 boiling_sound.setGain(setXMLGain(el)); // Handle gain 463 boiling_sound.setLooped(true); 464 boiling_sound.setFadeTimes(500, 500); 465 boiling_sound.setReferenceDistance(setXMLReferenceDistance(el)); 466 trigger_sounds.put("boiling", boiling_sound); 467 } else { 468 buffer_ok = false; 469 } 470 } 471 472 // Get the brake sound 473 el = e.getChild("brake-sound"); 474 if (el != null) { 475 fn = el.getChild("sound-file").getValue(); 476 brake_sound = new SoundBite(vf, fn, _soundName + "_BRAKE", _soundName + "_Brake"); 477 if (brake_sound.isInitialized()) { 478 brake_sound.setGain(setXMLGain(el)); 479 brake_sound.setLooped(false); 480 brake_sound.setFadeTimes(500, 500); 481 brake_sound.setReferenceDistance(setXMLReferenceDistance(el)); 482 trigger_sounds.put("brake", brake_sound); 483 } else { 484 buffer_ok = false; 485 } 486 } 487 488 // Get the pre-arrival sound 489 el = e.getChild("pre-arrival-sound"); 490 if (el != null) { 491 fn = el.getChild("sound-file").getValue(); 492 pre_arrival_sound = new SoundBite(vf, fn, _soundName + "_PRE-ARRIVAL", _soundName + "_Pre-arrival"); 493 if (pre_arrival_sound.isInitialized()) { 494 pre_arrival_sound.setGain(setXMLGain(el)); 495 pre_arrival_sound.setLooped(false); 496 pre_arrival_sound.setFadeTimes(500, 500); 497 pre_arrival_sound.setReferenceDistance(setXMLReferenceDistance(el)); 498 trigger_sounds.put("pre_arrival", pre_arrival_sound); 499 } else { 500 buffer_ok = false; 501 } 502 } 503 504 if (buffer_ok) { 505 setBuffersFreeState(true); 506 // Kick-start the loop thread 507 this.startThread(); 508 // Check auto-start setting 509 autoStartCheck(); 510 } else { 511 setBuffersFreeState(false); 512 } 513 } 514 515 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Steam1Sound.class); 516 517 private static class S1Notch { 518 519 private int my_notch; 520 private int min_rpm, max_rpm; 521 private int buffer_fmt; 522 private int buffer_freq; 523 private int buffer_frame_size; 524 private ByteBuffer notchfiller_data; 525 private ByteBuffer coastfiller_data; 526 private List<AudioBuffer> bufs_helper = new ArrayList<>(); 527 private List<ByteBuffer> chuff_bufs_data = new ArrayList<>(); 528 private List<ByteBuffer> coast_bufs_data = new ArrayList<>(); 529 530 private S1Notch(int notch) { 531 my_notch = notch; 532 } 533 534 private int getNotch() { 535 return my_notch; 536 } 537 538 private int getMaxLimit() { 539 return max_rpm; 540 } 541 542 private int getMinLimit() { 543 return min_rpm; 544 } 545 546 private void setMinLimit(int l) { 547 min_rpm = l; 548 } 549 550 private void setMaxLimit(int l) { 551 max_rpm = l; 552 } 553 554 private Boolean isInLimits(int val) { 555 return val >= min_rpm && val <= max_rpm; 556 } 557 558 private void setBufferFmt(int fmt) { 559 buffer_fmt = fmt; 560 } 561 562 private int getBufferFmt() { 563 return buffer_fmt; 564 } 565 566 private void setBufferFreq(int freq) { 567 buffer_freq = freq; 568 } 569 570 private int getBufferFreq() { 571 return buffer_freq; 572 } 573 574 private void setBufferFrameSize(int framesize) { 575 buffer_frame_size = framesize; 576 } 577 578 private int getBufferFrameSize() { 579 return buffer_frame_size; 580 } 581 582 private void setNotchFillerData(ByteBuffer dat) { 583 notchfiller_data = dat; 584 } 585 586 private ByteBuffer getNotchFillerData() { 587 return notchfiller_data; 588 } 589 590 private void setCoastFillerData(ByteBuffer dat) { 591 coastfiller_data = dat; 592 } 593 594 private ByteBuffer getCoastFillerData() { 595 return coastfiller_data; 596 } 597 598 private void addChuffData(ByteBuffer dat) { 599 chuff_bufs_data.add(dat); 600 } 601 602 private void addCoastData(ByteBuffer dat) { 603 coast_bufs_data.add(dat); 604 } 605 606 private void addHelper(AudioBuffer b) { 607 bufs_helper.add(b); 608 } 609 610 private static AudioBuffer getBufferHelper(String sname, String uname) { 611 AudioBuffer bf = null; 612 jmri.AudioManager am = jmri.InstanceManager.getDefault(jmri.AudioManager.class); 613 try { 614 bf = (AudioBuffer) am.provideAudio(VSDSound.BufSysNamePrefix + sname); 615 bf.setUserName(VSDSound.BufUserNamePrefix + uname); 616 } catch (AudioException | IllegalArgumentException ex) { 617 log.warn("problem creating SoundBite", ex); 618 return null; 619 } 620 log.debug("empty buffer created: {}, name: {}", bf, bf.getSystemName()); 621 return bf; 622 } 623 624 private static java.io.InputStream getWavStream(VSDFile vf, String filename) { 625 java.io.InputStream ins = vf.getInputStream(filename); 626 if (ins != null) { 627 return ins; 628 } else { 629 log.warn("input Stream failed for {}", filename); 630 return null; 631 } 632 } 633 634 @SuppressWarnings("hiding") // Field has same name as a field in the super class 635 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(S1Notch.class); 636 637 } 638 639 private static class S1LoopThread extends Thread { 640 641 private Steam1Sound _parent; 642 private S1Notch _notch; 643 private S1Notch notch1; 644 private SoundBite _sound; 645 private boolean is_running = false; 646 private boolean is_looping = false; 647 private boolean is_auto_coasting; 648 private boolean is_key_coasting; 649 private boolean is_idling; 650 private boolean is_braking; 651 private boolean is_half_speed; 652 private boolean is_in_rampup_mode; 653 private boolean first_start; 654 private boolean is_dynamic_gain; 655 private boolean is_chuff_fade_out; 656 private long timeOfLastSpeedCheck; 657 private float _throttle; 658 private float last_throttle; 659 private float _driver_diameter_float; 660 private float low_volume; 661 private float high_volume; 662 private float dynamic_volume; 663 private float max_volume; 664 private float chuff_fade_out_factor; 665 private float chuff_fade_out_volume; 666 private int chuff_index; 667 private int helper_index; 668 private int lastRpm; 669 private int rpm_dirfn; 670 private int rpm_nominal; // Nominal value 671 private int rpm; // Actual value 672 private int topspeed; 673 private int _top_speed; 674 private int _top_speed_reverse; 675 private int _num_cylinders; 676 private int _decel_trigger_rpms; 677 private int acc_time; 678 private int dec_time; 679 private int count_pre_arrival; 680 private int queue_limit; 681 private int wait_loops; 682 683 private S1LoopThread(Steam1Sound d, String s, int ts, int tsr, float dd, 684 int nc, int dtr, boolean r) { 685 super(); 686 _parent = d; 687 _top_speed = ts; 688 _top_speed_reverse = tsr; 689 _driver_diameter_float = dd; 690 _num_cylinders = nc; 691 _decel_trigger_rpms = dtr; 692 is_running = r; 693 is_looping = false; 694 is_auto_coasting = false; 695 is_key_coasting = false; 696 is_idling = false; 697 is_braking = false; 698 is_in_rampup_mode = false; 699 is_dynamic_gain = false; 700 is_chuff_fade_out = false; 701 lastRpm = 0; 702 rpm_dirfn = 0; 703 timeOfLastSpeedCheck = 0; 704 _throttle = 0.0f; 705 last_throttle = 0.0f; 706 _notch = null; 707 high_volume = 0.0f; 708 low_volume = 0.85f; 709 dynamic_volume = 1.0f; 710 max_volume = 1.0f / _parent.engine_gain; 711 _sound = new SoundBite(s); // Soundsource for queueing 712 _sound.isInitialized(); 713 _sound.setGain(_parent.engine_gain); // All chuff sounds will have this gain 714 count_pre_arrival = 1; 715 queue_limit = 2; 716 wait_loops = 0; 717 if (r) { 718 this.start(); 719 } 720 } 721 722 private void setRunning(boolean r) { 723 is_running = r; 724 } 725 726 private void setThrottle(float t) { 727 // Don't do anything, if engine is not started 728 // Another required value is a S1Notch (should have been set at engine start) 729 if (_parent.isEngineStarted()) { 730 if (t < 0.0f) { 731 // DO something to shut down 732 is_in_rampup_mode = false; // interrupt ramp-up 733 setRpmNominal(0); 734 _parent.accdectime = 0; 735 _parent.startAccDecTimer(); 736 } else { 737 _throttle = t; 738 last_throttle = t; 739 740 // handle half-speed 741 if (is_half_speed) { 742 _throttle = _throttle / 2; 743 } 744 745 // Calculate the nominal speed (Revolutions Per Minute) 746 setRpmNominal(calcRPM(_throttle)); 747 748 // Speeding up or slowing down? 749 if (getRpmNominal() < lastRpm) { 750 // 751 // Slowing down 752 // 753 _parent.accdectime = dec_time; 754 log.debug("decelerate from {} to {}", lastRpm, getRpmNominal()); 755 756 if ((getRpmNominal() < 23) && is_auto_coasting && (count_pre_arrival > 0) && 757 _parent.trigger_sounds.containsKey("pre_arrival") && (dec_time < 250)) { 758 _parent.trigger_sounds.get("pre_arrival").fadeIn(); 759 count_pre_arrival--; 760 } 761 762 // Calculate how long it's been since we lastly checked speed 763 long currentTime = System.currentTimeMillis(); 764 float timePassed = currentTime - timeOfLastSpeedCheck; 765 timeOfLastSpeedCheck = currentTime; 766 // Prove the trigger for decelerating actions (braking, coasting) 767 if (((lastRpm - getRpmNominal()) > _decel_trigger_rpms) && (timePassed < 500.0f)) { 768 log.debug("Time passed {}", timePassed); 769 if ((getRpmNominal() < 30) && (dec_time < 250)) { // Braking sound only when speed is low (, but not to low) 770 if (_parent.trigger_sounds.containsKey("brake")) { 771 _parent.trigger_sounds.get("brake").fadeIn(); 772 is_braking = true; 773 log.debug("braking activ!"); 774 } 775 } else if (notch1.coast_bufs_data.size() > 0 && !is_key_coasting) { 776 is_auto_coasting = true; 777 log.debug("auto-coasting active"); 778 if (!is_chuff_fade_out) { 779 setupChuffFadeOut(); 780 } 781 } 782 } 783 } else { 784 // 785 // Speeding up. 786 // 787 _parent.accdectime = acc_time; 788 log.debug("accelerate from {} to {}", lastRpm, getRpmNominal()); 789 if (is_dynamic_gain) { 790 float new_high_volume = Math.max(dynamic_volume * 0.5f, low_volume) + 791 dynamic_volume * 0.05f * Math.min(getRpmNominal() - getRpm(), 14); 792 if (new_high_volume > high_volume) { 793 high_volume = Math.min(new_high_volume, max_volume); 794 } 795 log.debug("dynamic volume: {}, max volume: {}, high volume: {}", dynamic_volume, max_volume, high_volume); 796 } 797 if (is_braking) { 798 stopBraking(); // Revoke possible brake sound 799 } 800 if (is_auto_coasting) { 801 stopCoasting(); // This makes chuff sound hearable again 802 } 803 } 804 _parent.startAccDecTimer(); // Start, if not already running 805 lastRpm = getRpmNominal(); 806 } 807 } 808 } 809 810 private void stopBraking() { 811 if (is_braking) { 812 if (_parent.trigger_sounds.containsKey("brake")) { 813 _parent.trigger_sounds.get("brake").fadeOut(); 814 is_braking = false; 815 log.debug("braking sound stopped."); 816 } 817 } 818 } 819 820 private void startBoilingSound() { 821 if (_parent.trigger_sounds.containsKey("boiling")) { 822 _parent.trigger_sounds.get("boiling").setLooped(true); 823 _parent.trigger_sounds.get("boiling").play(); 824 log.debug("boiling sound playing"); 825 } 826 } 827 828 private void stopBoilingSound() { 829 if (_parent.trigger_sounds.containsKey("boiling")) { 830 _parent.trigger_sounds.get("boiling").setLooped(false); 831 _parent.trigger_sounds.get("boiling").fadeOut(); 832 log.debug("boiling sound stopped."); 833 } 834 } 835 836 private void stopCoasting() { 837 is_auto_coasting = false; 838 is_key_coasting = false; 839 is_chuff_fade_out = false; 840 if (is_dynamic_gain) { 841 setDynamicVolume(low_volume); 842 } 843 log.debug("coasting sound stopped."); 844 } 845 846 private void getLocoDirection(int d) { 847 // If loco direction was changed we need to set topspeed of the loco to new value 848 // (this is necessary, when topspeed-forward and topspeed-reverse differs) 849 if (d == 1) { // loco is going forward 850 topspeed = _top_speed; 851 } else { 852 topspeed = _top_speed_reverse; 853 } 854 log.debug("loco direction: {}, top speed: {}", d, topspeed); 855 // Re-calculate accel-time and decel-time, hence topspeed may have changed 856 acc_time = calcAccDecTime(_parent.accel_rate); 857 dec_time = calcAccDecTime(_parent.decel_rate); 858 859 // Handle throttle forward and reverse action 860 // nothing to do when loco is not running or just in ramp-up-mode 861 if (getRpm() > 0 && getRpmNominal() > 0 && _parent.isEngineStarted() && !is_in_rampup_mode) { 862 rpm_dirfn = getRpm(); // save rpm for ramp-up 863 log.debug("ramp-up mode - rpm {} saved, rpm nominal: {}", rpm_dirfn, getRpmNominal()); 864 is_in_rampup_mode = true; 865 setRpmNominal(0); // force a stop 866 _parent.startAccDecTimer(); 867 } 868 } 869 870 private void setFunction(String event, boolean is_true, String name) { 871 // This throttle function key handling differs to configurable sounds: 872 // Do something following certain conditions, when a throttle function key is pressed. 873 // Note: throttle will send initial value(s) before thread is started! 874 log.debug("throttle function key pressed: {} is {}, function: {}", event, is_true, name); 875 if (name.equals("COAST")) { 876 // Handle key-coasting on/off. 877 log.debug("COAST key pressed"); 878 is_chuff_fade_out = false; 879 // Set coasting TRUE, if COAST key is pressed. Requires sufficient coasting sounds (chuff_index will rely on that). 880 if (notch1 == null) { 881 notch1 = _parent.getNotch(1); // Because of initial send of throttle key, COAST function key could be "true" 882 } 883 if (is_true && notch1.coast_bufs_data.size() > 0) { 884 is_key_coasting = true; // When idling is active, key-coasting will start after it. 885 if (!is_auto_coasting) { 886 setupChuffFadeOut(); 887 } 888 } else { 889 stopCoasting(); 890 } 891 log.debug("is COAST: {}", is_key_coasting); 892 } 893 894 // Speed change if HALF_SPEED key is pressed 895 if (name.equals("HALF_SPEED")) { 896 log.debug("HALF_SPEED key pressed is {}", is_true); 897 if (_parent.isEngineStarted()) { 898 if (is_true) { 899 is_half_speed = true; 900 } else { 901 is_half_speed = false; 902 } 903 setThrottle(last_throttle); // Trigger a speed update 904 } 905 } 906 907 // Set Accel/Decel off or to lower value 908 if (name.equals("BRAKE_KEY")) { 909 log.debug("BRAKE_KEY pressed is {}", is_true); 910 if (_parent.isEngineStarted()) { 911 if (is_true) { 912 if (_parent.brake_time == 0) { 913 acc_time = 0; 914 dec_time = 0; 915 } else { 916 dec_time = calcAccDecTime(_parent.brake_time); 917 } 918 _parent.accdectime = dec_time; 919 log.debug("accdectime: {}", _parent.accdectime); 920 } else { 921 acc_time = calcAccDecTime(_parent.accel_rate); 922 dec_time = calcAccDecTime(_parent.decel_rate); 923 _parent.accdectime = dec_time; 924 } 925 } 926 } 927 // Other throttle function keys may follow ... 928 } 929 930 private void startEngine() { 931 _sound.unqueueBuffers(); 932 log.debug("thread: start engine ..."); 933 _notch = _parent.getNotch(1); // Initial value 934 notch1 = _parent.getNotch(1); 935 if (_parent.engine_pane != null) { 936 _parent.engine_pane.setThrottle(1); // Set EnginePane (DieselPane) notch 937 } 938 is_dynamic_gain = _parent.is_dynamic_gain; 939 dynamic_volume = 1.0f; 940 _sound.setReferenceDistance(_parent.engine_rd); 941 setRpm(0); 942 _parent.setActualSpeed(0.0f); 943 setRpmNominal(0); 944 helper_index = -1; // Prepare helper buffer start. Index will be incremented before first use 945 setWait(0); 946 startBoilingSound(); 947 startIdling(); 948 acc_time = calcAccDecTime(_parent.accel_rate); // Calculate acceleration time 949 dec_time = calcAccDecTime(_parent.decel_rate); // Calculate deceleration time 950 _parent.initAccDecTimer(); 951 } 952 953 private void stopEngine() { 954 log.debug("thread: stop engine ..."); 955 if (is_looping) { 956 is_looping = false; // Stop the loop player 957 } 958 stopBraking(); 959 stopCoasting(); 960 stopBoilingSound(); 961 stopIdling(); 962 _parent.stopAccDecTimer(); 963 _throttle = 0.0f; // Clear it, just in case the engine was stopped at speed > 0 964 if (_parent.engine_pane != null) { 965 _parent.engine_pane.setThrottle(1); // Set EnginePane (DieselPane) notch 966 } 967 setRpm(0); 968 _parent.setActualSpeed(0.0f); 969 } 970 971 private int calcAccDecTime(int accdec_rate) { 972 // Handle Momentum 973 // Regard topspeed, which may be different on forward or reverse direction 974 int topspeed_rpm = (int) Math.round(topspeed * 1056 / (Math.PI * _driver_diameter_float)); 975 return 896 * accdec_rate / topspeed_rpm; // NMRA value 896 in ms 976 } 977 978 private void startIdling() { 979 is_idling = true; 980 if (_parent.trigger_sounds.containsKey("idle")) { 981 _parent.trigger_sounds.get("idle").play(); 982 } 983 log.debug("start idling ..."); 984 } 985 986 private void stopIdling() { 987 if (is_idling) { 988 is_idling = false; 989 if (_parent.trigger_sounds.containsKey("idle")) { 990 _parent.trigger_sounds.get("idle").fadeOut(); 991 log.debug("idling stopped."); 992 } 993 } 994 } 995 996 private void setupChuffFadeOut() { 997 // discard chuff_fade_out on high acceleration... 998 if (is_looping && _parent.use_chuff_fade_out && getRpmNominal() - getRpm() < 10) { 999 chuff_fade_out_volume = dynamic_volume; 1000 chuff_fade_out_factor = 0.7f + (getRpm() * 0.001f); // multiplication 1001 is_chuff_fade_out = true; 1002 } 1003 } 1004 1005 // 1006 // LOOP-PLAYER 1007 // 1008 @Override 1009 public void run() { 1010 try { 1011 while (is_running) { 1012 if (is_looping && AudioUtil.isAudioRunning()) { 1013 if (_sound.getSource().numProcessedBuffers() > 0) { 1014 _sound.unqueueBuffers(); 1015 } 1016 log.debug("run loop. Buffers queued: {}", _sound.getSource().numQueuedBuffers()); 1017 if ((_sound.getSource().numQueuedBuffers() < queue_limit) && (getWait() == 0)) { 1018 setSound(selectData()); // Select appropriate WAV data, handle sound and filler and queue the sound 1019 } 1020 checkAudioState(); 1021 } else { 1022 if (_sound.getSource().numProcessedBuffers() > 0) { 1023 _sound.unqueueBuffers(); 1024 } 1025 } 1026 sleep(_parent.sleep_interval); 1027 updateWait(); 1028 } 1029 _sound.stop(); 1030 } catch (InterruptedException ie) { 1031 // kill thread 1032 log.debug("thread interrupted"); 1033 } 1034 } 1035 1036 private void checkAudioState() { 1037 if (first_start) { 1038 _sound.play(); 1039 first_start = false; 1040 } else { 1041 if (_sound.getSource().getState() != Audio.STATE_PLAYING) { 1042 _sound.play(); 1043 log.info("loop sound re-started"); 1044 } 1045 } 1046 } 1047 1048 private ByteBuffer selectData() { 1049 ByteBuffer data; 1050 updateVolume(); 1051 if ((is_key_coasting || is_auto_coasting) && !is_chuff_fade_out) { 1052 data = notch1.coast_bufs_data.get(incChuffIndex()); // Take the coasting sound 1053 } else { 1054 data = _notch.chuff_bufs_data.get(incChuffIndex()); // Take the standard chuff sound 1055 } 1056 return data; 1057 } 1058 1059 private void changeNotch() { 1060 int new_notch = _notch.getNotch(); 1061 log.debug("changing notch ... rpm: {}, notch: {}, chuff index: {}", 1062 getRpm(), _notch.getNotch(), chuff_index); 1063 if ((getRpm() > _notch.getMaxLimit()) && (new_notch < _parent.notch_sounds.size())) { 1064 // Too fast. Need to go to next notch up 1065 new_notch++; 1066 log.debug("change up. notch: {}", new_notch); 1067 _notch = _parent.getNotch(new_notch); 1068 } else if ((getRpm() < _notch.getMinLimit()) && (new_notch > 1)) { 1069 // Too slow. Need to go to next notch down 1070 new_notch--; 1071 log.debug("change down. notch: {}", new_notch); 1072 _notch = _parent.getNotch(new_notch); 1073 } 1074 _parent.engine_pane.setThrottle(new_notch); // Update EnginePane (DieselPane) notch 1075 } 1076 1077 private int getRpm() { 1078 return rpm; // Actual Revolution per Minute 1079 } 1080 1081 private void setRpm(int r) { 1082 rpm = r; 1083 } 1084 1085 private int getRpmNominal() { 1086 return rpm_nominal; // Nominal Revolution per Minute 1087 } 1088 1089 private void setRpmNominal(int rn) { 1090 rpm_nominal = rn; 1091 } 1092 1093 private void updateRpm() { 1094 if (getRpmNominal() > getRpm()) { 1095 // Actual rpm should not exceed highest max-rpm defined in config.xml 1096 if (getRpm() < _parent.getNotch(_parent.notch_sounds.size()).getMaxLimit()) { 1097 setRpm(getRpm() + 1); 1098 } else { 1099 log.debug("actual rpm not increased. Value: {}", getRpm()); 1100 } 1101 log.debug("accel - nominal RPM: {}, actual RPM: {}", getRpmNominal(), getRpm()); 1102 } else if (getRpmNominal() < getRpm()) { 1103 // deceleration 1104 setRpm(getRpm() - 1); 1105 if (getRpm() < 0) { 1106 setRpm(0); 1107 } 1108 // strong deceleration 1109 if (is_dynamic_gain && (getRpm() - getRpmNominal() > 4) && !is_auto_coasting && !is_key_coasting && !is_chuff_fade_out) { 1110 dynamic_volume = low_volume; 1111 } 1112 log.debug("decel - nominal RPM: {}, actual RPM: {}", getRpmNominal(), getRpm()); 1113 } else { 1114 _parent.stopAccDecTimer(); // Speed is unchanged, nothing to do 1115 } 1116 1117 // calculate actual speed from actual RPM and based on topspeed 1118 _parent.setActualSpeed(getRpm() / (topspeed * 1056 / ((float) Math.PI * _driver_diameter_float))); 1119 log.debug("nominal RPM: {}, actual RPM: {}, actual speed: {}, t: {}, speedcurve(t): {}", 1120 getRpmNominal(), getRpm(), _parent.getActualSpeed(), _throttle, _parent.speedCurve(_throttle)); 1121 1122 // Start or Stop the LOOP-PLAYER 1123 checkState(); 1124 1125 // Are we in the right notch? 1126 if ((getRpm() >= notch1.getMinLimit()) && (!_notch.isInLimits(getRpm()))) { 1127 log.debug("Notch change! Notch: {}, RPM nominal: {}, RPM actual: {}", _notch.getNotch(), getRpmNominal(), getRpm()); 1128 changeNotch(); 1129 } 1130 } 1131 1132 private void checkState() { 1133 if (is_looping) { 1134 if (getRpm() < notch1.getMinLimit()) { 1135 is_looping = false; // Stop the loop player 1136 setWait(0); 1137 if (is_dynamic_gain && !is_key_coasting) { 1138 high_volume = low_volume; 1139 } 1140 log.debug("change from chuff or coast to idle."); 1141 is_auto_coasting = false; 1142 stopBraking(); 1143 startIdling(); 1144 } 1145 } else { 1146 if (_parent.isEngineStarted() && (getRpm() >= notch1.getMinLimit())) { 1147 stopIdling(); 1148 if (is_dynamic_gain && !is_key_coasting) { 1149 dynamic_volume = high_volume; 1150 } 1151 // Now prepare to start the chuff sound (or coasting sound) 1152 _notch = _parent.getNotch(1); // Initial notch value 1153 chuff_index = -1; // Index will be incremented before first usage 1154 count_pre_arrival = 1; 1155 is_chuff_fade_out = false; // Default 1156 first_start = true; 1157 if (is_in_rampup_mode && _sound.getSource().getState() == Audio.STATE_PLAYING) { 1158 _sound.stop(); 1159 } 1160 is_looping = true; // Start the loop player 1161 } 1162 1163 // Handle a throttle forward or reverse change 1164 if (is_in_rampup_mode && getRpm() == 0) { 1165 setRpmNominal(rpm_dirfn); 1166 _parent.accdectime = acc_time; 1167 _parent.startAccDecTimer(); 1168 is_in_rampup_mode = false; 1169 } 1170 } 1171 1172 if (getRpm() > 0) { 1173 queue_limit = Math.max(2, Math.abs(500 / calcChuffInterval(getRpm()))); 1174 log.debug("queue limit: {}", queue_limit); 1175 } 1176 } 1177 1178 private void updateVolume() { 1179 if (is_dynamic_gain && !is_chuff_fade_out && !is_key_coasting && !is_auto_coasting) { 1180 if (getRpmNominal() < getRpm()) { 1181 // deceleration 1182 float inc1 = 0.05f; 1183 if (dynamic_volume >= low_volume) { 1184 dynamic_volume -= inc1; 1185 } 1186 } else { 1187 float inc2 = 0.01f; 1188 float inc3 = 0.005f; 1189 if (dynamic_volume + inc3 < 1.0f && high_volume < 1.0f) { 1190 dynamic_volume += inc3; 1191 } else if (dynamic_volume + inc2 < high_volume) { 1192 dynamic_volume += inc2; 1193 } else if (dynamic_volume - inc3 > 1.0f) { 1194 dynamic_volume -= inc3; 1195 high_volume -= inc2; 1196 } 1197 } 1198 setDynamicVolume(dynamic_volume); 1199 } 1200 } 1201 1202 private void updateWait() { 1203 if (getWait() > 0) { 1204 setWait(getWait() - 1); 1205 } 1206 } 1207 1208 private void setWait(int wait) { 1209 wait_loops = wait; 1210 } 1211 1212 private int getWait() { 1213 return wait_loops; 1214 } 1215 1216 private int incChuffIndex() { 1217 chuff_index++; 1218 // Correct for wrap. 1219 if (chuff_index >= (_num_cylinders * 2)) { 1220 chuff_index = 0; 1221 } 1222 log.debug("new chuff index: {}", chuff_index); 1223 return chuff_index; 1224 } 1225 1226 private int incHelperIndex() { 1227 helper_index++; 1228 // Correct for wrap. 1229 if (helper_index >= notch1.bufs_helper.size()) { 1230 helper_index = 0; 1231 } 1232 return helper_index; 1233 } 1234 1235 private int calcRPM(float t) { 1236 // speed = % of topspeed (mph) 1237 // RPM = speed * ((inches/mile) / (minutes/hour)) / (pi * driver_diameter_float) 1238 return (int) Math.round(_parent.speedCurve(t) * topspeed * 1056 / (Math.PI * _driver_diameter_float)); 1239 } 1240 1241 private int calcChuffInterval(int revpm) { 1242 // chuff interval will be calculated based on revolutions per minute (revpm) 1243 // note: interval time includes the sound duration! 1244 // chuffInterval = time in ms per revolution of the driver wheel: 1245 // 60,000 ms / revpm / number of cylinders / 2 (because cylinders are double-acting) 1246 return (int) Math.round(60000.0 / revpm / _num_cylinders / 2.0); 1247 } 1248 1249 private void setSound(ByteBuffer data) { 1250 AudioBuffer buf = notch1.bufs_helper.get(incHelperIndex()); // buffer for the queue 1251 int sbl = 0; // sound bite length 1252 if (notch1.getBufferFreq() > 0) { 1253 sbl = (1000 * data.limit()/notch1.getBufferFrameSize()) / notch1.getBufferFreq(); // calculate the length of the clip in milliseconds 1254 } 1255 log.debug("sbl: {}", sbl); 1256 // Time in ms from chuff start up to begin of the next chuff, limited to a minimum 1257 int interval = Math.max(calcChuffInterval(getRpm()), _parent.sleep_interval); 1258 int bbufcount = notch1.getBufferFrameSize() * ((interval) * notch1.getBufferFreq() / 1000); 1259 ByteBuffer bbuf = ByteBuffer.allocateDirect(bbufcount); // Target 1260 1261 if (interval > sbl) { 1262 // Regular queueing. Whole sound clip goes to the queue. Low notches 1263 // Prepare the sound and transfer it to the target ByteBuffer bbuf 1264 int bbufcount2 = notch1.getBufferFrameSize() * (sbl * notch1.getBufferFreq() / 1000); 1265 byte[] bbytes2 = new byte[bbufcount2]; 1266 data.get(bbytes2); // Same as: data.get(bbytes2, 0, bbufcount2); 1267 data.rewind(); 1268 1269 // chuff_fade_out 1270 doChuffFadeOut(bbufcount2, bbytes2); 1271 1272 bbuf.order(data.order()); // Set new buffer's byte order to match source buffer. 1273 bbuf.put(bbytes2); // Same as: bbuf.put(bbytes2, 0, bbufcount2); 1274 1275 // Handle filler for the remaining part of the AudioBuffer 1276 if (bbuf.hasRemaining()) { 1277 log.debug("remaining: {}", bbuf.remaining()); 1278 ByteBuffer dataf; 1279 if (is_key_coasting || is_auto_coasting) { 1280 dataf = notch1.getCoastFillerData(); 1281 } else { 1282 dataf = notch1.getNotchFillerData(); 1283 } 1284 if (dataf == null) { 1285 log.debug("No filler sound found"); 1286 // Nothing to do on 16-bit, because 0 is default for "silence"; 8-bit-mono needs 128, otherwise it's "noisy" 1287 if (notch1.getBufferFmt() == com.jogamp.openal.AL.AL_FORMAT_MONO8) { 1288 byte[] bbytesfiller = new byte[bbuf.remaining()]; 1289 for (int i = 0; i < bbytesfiller.length; i++) { 1290 bbytesfiller[i] = (byte) 0x80; // fill array with "silence" 1291 } 1292 bbuf.put(bbytesfiller); 1293 } 1294 } else { 1295 // Filler sound found 1296 log.debug("data limit: {}, remaining: {}", dataf.limit(), bbuf.remaining()); 1297 byte[] bbytesfiller2 = new byte[bbuf.remaining()]; 1298 if (dataf.limit() >= bbuf.remaining()) { 1299 dataf.get(bbytesfiller2); 1300 dataf.rewind(); 1301 bbuf.put(bbytesfiller2); 1302 } else { 1303 log.debug("not enough filler length"); 1304 byte[] bbytesfillerpart = new byte[dataf.limit()]; 1305 dataf.get(bbytesfillerpart); 1306 dataf.rewind(); 1307 int k = 0; 1308 for (int i = 0; i < bbytesfiller2.length; i++) { 1309 bbytesfiller2[i] = bbytesfillerpart[k]; 1310 k++; 1311 if (k == dataf.limit()) { 1312 k = 0; 1313 } 1314 } 1315 bbuf.put(bbytesfiller2); 1316 } 1317 } 1318 } 1319 } else { 1320 // Need to cut the SoundBite to new length of interval 1321 log.debug("need to cut sound clip from {} to length {}", sbl, interval); 1322 byte[] bbytes = new byte[bbufcount]; 1323 data.get(bbytes); // Same as: data.get(bbytes, 0, bbufcount); 1324 data.rewind(); 1325 1326 doChuffFadeOut(bbufcount, bbytes); 1327 1328 bbuf.order(data.order()); // Set new buffer's byte order to match source buffer 1329 bbuf.put(bbytes); // Same as: bbuf.put(bbytes, 0, bbufcount); 1330 } 1331 bbuf.rewind(); 1332 buf.loadBuffer(bbuf, notch1.getBufferFmt(), notch1.getBufferFreq()); 1333 _sound.queueBuffer(buf); 1334 log.debug("buffer queued. Length: {}", (int)SoundBite.calcLength(buf)); 1335 1336 // wait some loops to get up-to-date speed value 1337 setWait((interval - _parent.sleep_interval * _parent.wait_factor) / _parent.sleep_interval); 1338 if (getWait() < 3) { 1339 setWait(0); 1340 } 1341 } 1342 1343 private void doChuffFadeOut(int count, byte[] bbytes) { 1344 // applicable for 16-bit mono sounds only 1345 // (I don't have a solution for volume change on 8-bit sounds) 1346 if (is_chuff_fade_out) { 1347 chuff_fade_out_volume *= chuff_fade_out_factor; 1348 if (chuff_fade_out_volume < 0.15f) { // 0.07f 1349 is_chuff_fade_out = false; // done 1350 if (is_dynamic_gain) { 1351 dynamic_volume = 1.0f; 1352 setDynamicVolume(dynamic_volume); 1353 } 1354 } 1355 for (int i = 0; i < count; ++i) { 1356 bbytes[i] *= chuff_fade_out_volume; // make it quieter 1357 } 1358 } 1359 } 1360 1361 private void mute(boolean m) { 1362 _sound.mute(m); 1363 for (SoundBite ts : _parent.trigger_sounds.values()) { 1364 ts.mute(m); 1365 } 1366 } 1367 1368 // called by the LoopThread on volume changes with active dynamic_gain 1369 private void setDynamicVolume(float v) { 1370 if (_parent.getTunnel()) { 1371 v *= VSDSound.tunnel_volume; 1372 } 1373 1374 if (!_parent.getVsd().isMuted()) { 1375 // v * master_volume * decoder_volume, will be multiplied by gain in SoundBite 1376 // forward volume to SoundBite 1377 _sound.setVolume(v * VSDecoderManager.instance().getMasterVolume() * 0.01f * _parent.getVsd().getDecoderVolume()); 1378 } 1379 } 1380 1381 // triggered by VSDecoder via VSDSound on sound positioning, master or decoder slider changes 1382 // volume v is already multiplied by master_volume and decoder_volume 1383 private void setVolume(float v) { 1384 // handle engine sound (loop sound) 1385 if (! is_dynamic_gain) { 1386 _sound.setVolume(v); // special case on active dynamic_gain 1387 } 1388 // handle trigger sounds (e.g. idle) 1389 for (SoundBite ts : _parent.trigger_sounds.values()) { 1390 ts.setVolume(v); 1391 } 1392 } 1393 1394 private void setPosition(PhysicalLocation p) { 1395 _sound.setPosition(p); 1396 if (_parent.getTunnel()) { 1397 _sound.attachSourcesToEffects(); 1398 } else { 1399 _sound.detachSourcesToEffects(); 1400 } 1401 1402 for (SoundBite ts : _parent.trigger_sounds.values()) { 1403 ts.setPosition(p); 1404 if (_parent.getTunnel()) { 1405 ts.attachSourcesToEffects(); 1406 } else { 1407 ts.detachSourcesToEffects(); 1408 } 1409 } 1410 } 1411 1412 @SuppressWarnings("hiding") // Field has same name as a field in the super class 1413 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(S1LoopThread.class); 1414 1415 } 1416}