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 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 AudioBuffer bh = S1Notch.getBufferHelper(name + "_BUFFERHELPER_" + j, name + "_BUFFERHELPER_" + j); 410 if (bh != null) { 411 log.debug("buffer helper created: {}, name: {}", bh, bh.getSystemName()); 412 sb.addHelper(bh); 413 } else { 414 buffer_ok = false; 415 } 416 } 417 } 418 419 sb.setMinLimit(Integer.parseInt(el.getChildText("min-rpm"))); 420 sb.setMaxLimit(Integer.parseInt(el.getChildText("max-rpm"))); 421 422 // Store in the list 423 notch_sounds.put(nn, sb); 424 nn++; 425 } 426 log.debug("Number of notches defined: {}", notch_sounds.size()); 427 428 // Get the trigger sounds 429 // Note: other than notch sounds, trigger sounds can have different attributes 430 trigger_sounds = new HashMap<>(); 431 432 // Get the idle sound 433 el = e.getChild("idle-sound"); 434 if (el != null) { 435 fn = el.getChild("sound-file").getValue(); 436 log.debug("idle sound: {}", fn); 437 idle_sound = new SoundBite(vf, fn, _soundName + "_IDLE", _soundName + "_Idle"); 438 idle_sound.setGain(setXMLGain(el)); // Handle gain 439 log.debug("idle sound gain: {}", idle_sound.getGain()); 440 idle_sound.setLooped(true); 441 idle_sound.setFadeTimes(500, 500); 442 idle_sound.setReferenceDistance(setXMLReferenceDistance(el)); // Handle reference distance 443 log.debug("idle-sound reference distance: {}", idle_sound.getReferenceDistance()); 444 trigger_sounds.put("idle", idle_sound); 445 log.debug("trigger idle sound: {}", trigger_sounds.get("idle")); 446 } 447 448 // Get the boiling sound 449 el = e.getChild("boiling-sound"); 450 if (el != null) { 451 fn = el.getChild("sound-file").getValue(); 452 boiling_sound = new SoundBite(vf, fn, name + "_BOILING", name + "_Boiling"); 453 boiling_sound.setGain(setXMLGain(el)); // Handle gain 454 boiling_sound.setLooped(true); 455 boiling_sound.setFadeTimes(500, 500); 456 boiling_sound.setReferenceDistance(setXMLReferenceDistance(el)); 457 trigger_sounds.put("boiling", boiling_sound); 458 } 459 460 // Get the brake sound 461 el = e.getChild("brake-sound"); 462 if (el != null) { 463 fn = el.getChild("sound-file").getValue(); 464 brake_sound = new SoundBite(vf, fn, _soundName + "_BRAKE", _soundName + "_Brake"); 465 brake_sound.setGain(setXMLGain(el)); 466 brake_sound.setLooped(false); 467 brake_sound.setFadeTimes(500, 500); 468 brake_sound.setReferenceDistance(setXMLReferenceDistance(el)); 469 trigger_sounds.put("brake", brake_sound); 470 } 471 472 // Get the pre-arrival sound 473 el = e.getChild("pre-arrival-sound"); 474 if (el != null) { 475 fn = el.getChild("sound-file").getValue(); 476 pre_arrival_sound = new SoundBite(vf, fn, _soundName + "_PRE-ARRIVAL", _soundName + "_Pre-arrival"); 477 pre_arrival_sound.setGain(setXMLGain(el)); 478 pre_arrival_sound.setLooped(false); 479 pre_arrival_sound.setFadeTimes(500, 500); 480 pre_arrival_sound.setReferenceDistance(setXMLReferenceDistance(el)); 481 trigger_sounds.put("pre_arrival", pre_arrival_sound); 482 } 483 484 if (buffer_ok) { 485 // Kick-start the loop thread 486 this.startThread(); 487 488 // Check auto-start setting 489 autoStartCheck(); 490 } else { 491 log.warn("Engine cannot be started due to buffer issues"); 492 } 493 } 494 495 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Steam1Sound.class); 496 497 private static class S1Notch { 498 499 private int my_notch; 500 private int min_rpm, max_rpm; 501 private int buffer_fmt; 502 private int buffer_freq; 503 private int buffer_frame_size; 504 private ByteBuffer notchfiller_data; 505 private ByteBuffer coastfiller_data; 506 private List<AudioBuffer> bufs_helper = new ArrayList<>(); 507 private List<ByteBuffer> chuff_bufs_data = new ArrayList<>(); 508 private List<ByteBuffer> coast_bufs_data = new ArrayList<>(); 509 510 private S1Notch(int notch) { 511 my_notch = notch; 512 } 513 514 private int getNotch() { 515 return my_notch; 516 } 517 518 private int getMaxLimit() { 519 return max_rpm; 520 } 521 522 private int getMinLimit() { 523 return min_rpm; 524 } 525 526 private void setMinLimit(int l) { 527 min_rpm = l; 528 } 529 530 private void setMaxLimit(int l) { 531 max_rpm = l; 532 } 533 534 private Boolean isInLimits(int val) { 535 return val >= min_rpm && val <= max_rpm; 536 } 537 538 private void setBufferFmt(int fmt) { 539 buffer_fmt = fmt; 540 } 541 542 private int getBufferFmt() { 543 return buffer_fmt; 544 } 545 546 private void setBufferFreq(int freq) { 547 buffer_freq = freq; 548 } 549 550 private int getBufferFreq() { 551 return buffer_freq; 552 } 553 554 private void setBufferFrameSize(int framesize) { 555 buffer_frame_size = framesize; 556 } 557 558 private int getBufferFrameSize() { 559 return buffer_frame_size; 560 } 561 562 private void setNotchFillerData(ByteBuffer dat) { 563 notchfiller_data = dat; 564 } 565 566 private ByteBuffer getNotchFillerData() { 567 return notchfiller_data; 568 } 569 570 private void setCoastFillerData(ByteBuffer dat) { 571 coastfiller_data = dat; 572 } 573 574 private ByteBuffer getCoastFillerData() { 575 return coastfiller_data; 576 } 577 578 private void addChuffData(ByteBuffer dat) { 579 chuff_bufs_data.add(dat); 580 } 581 582 private void addCoastData(ByteBuffer dat) { 583 coast_bufs_data.add(dat); 584 } 585 586 private void addHelper(AudioBuffer b) { 587 bufs_helper.add(b); 588 } 589 590 static private AudioBuffer getBufferHelper(String sname, String uname) { 591 AudioBuffer bf = null; 592 jmri.AudioManager am = jmri.InstanceManager.getDefault(jmri.AudioManager.class); 593 try { 594 bf = (AudioBuffer) am.provideAudio(VSDSound.BufSysNamePrefix + sname); 595 bf.setUserName(VSDSound.BufUserNamePrefix + uname); 596 } catch (AudioException | IllegalArgumentException ex) { 597 log.warn("problem creating SoundBite", ex); 598 return null; 599 } 600 log.debug("empty buffer created: {}, name: {}", bf, bf.getSystemName()); 601 return bf; 602 } 603 604 static private java.io.InputStream getWavStream(VSDFile vf, String filename) { 605 java.io.InputStream ins = vf.getInputStream(filename); 606 if (ins != null) { 607 return ins; 608 } else { 609 log.warn("input Stream failed for {}", filename); 610 return null; 611 } 612 } 613 614 @SuppressWarnings("hiding") // Field has same name as a field in the super class 615 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(S1Notch.class); 616 617 } 618 619 private static class S1LoopThread extends Thread { 620 621 private Steam1Sound _parent; 622 private S1Notch _notch; 623 private S1Notch notch1; 624 private SoundBite _sound; 625 private boolean is_running = false; 626 private boolean is_looping = false; 627 private boolean is_auto_coasting; 628 private boolean is_key_coasting; 629 private boolean is_idling; 630 private boolean is_braking; 631 private boolean is_half_speed; 632 private boolean is_in_rampup_mode; 633 private boolean first_start; 634 private boolean is_dynamic_gain; 635 private boolean is_chuff_fade_out; 636 private long timeOfLastSpeedCheck; 637 private float _throttle; 638 private float last_throttle; 639 private float _driver_diameter_float; 640 private float low_volume; 641 private float high_volume; 642 private float dynamic_volume; 643 private float max_volume; 644 private float chuff_fade_out_factor; 645 private float chuff_fade_out_volume; 646 private int chuff_index; 647 private int helper_index; 648 private int lastRpm; 649 private int rpm_dirfn; 650 private int rpm_nominal; // Nominal value 651 private int rpm; // Actual value 652 private int topspeed; 653 private int _top_speed; 654 private int _top_speed_reverse; 655 private int _num_cylinders; 656 private int _decel_trigger_rpms; 657 private int acc_time; 658 private int dec_time; 659 private int count_pre_arrival; 660 private int queue_limit; 661 private int wait_loops; 662 663 private S1LoopThread(Steam1Sound d, String s, int ts, int tsr, float dd, 664 int nc, int dtr, boolean r) { 665 super(); 666 _parent = d; 667 _top_speed = ts; 668 _top_speed_reverse = tsr; 669 _driver_diameter_float = dd; 670 _num_cylinders = nc; 671 _decel_trigger_rpms = dtr; 672 is_running = r; 673 is_looping = false; 674 is_auto_coasting = false; 675 is_key_coasting = false; 676 is_idling = false; 677 is_braking = false; 678 is_in_rampup_mode = false; 679 is_dynamic_gain = false; 680 is_chuff_fade_out = false; 681 lastRpm = 0; 682 rpm_dirfn = 0; 683 timeOfLastSpeedCheck = 0; 684 _throttle = 0.0f; 685 last_throttle = 0.0f; 686 _notch = null; 687 high_volume = 0.0f; 688 low_volume = 0.85f; 689 dynamic_volume = 1.0f; 690 max_volume = 1.0f / _parent.engine_gain; 691 _sound = new SoundBite(s); // Soundsource for queueing 692 _sound.setGain(_parent.engine_gain); // All chuff sounds will have this gain 693 count_pre_arrival = 1; 694 queue_limit = 2; 695 wait_loops = 0; 696 if (r) { 697 this.start(); 698 } 699 } 700 701 private void setRunning(boolean r) { 702 is_running = r; 703 } 704 705 private void setThrottle(float t) { 706 // Don't do anything, if engine is not started 707 // Another required value is a S1Notch (should have been set at engine start) 708 if (_parent.isEngineStarted()) { 709 if (t < 0.0f) { 710 // DO something to shut down 711 is_in_rampup_mode = false; // interrupt ramp-up 712 setRpmNominal(0); 713 _parent.accdectime = 0; 714 _parent.startAccDecTimer(); 715 } else { 716 _throttle = t; 717 last_throttle = t; 718 719 // handle half-speed 720 if (is_half_speed) { 721 _throttle = _throttle / 2; 722 } 723 724 // Calculate the nominal speed (Revolutions Per Minute) 725 setRpmNominal(calcRPM(_throttle)); 726 727 // Speeding up or slowing down? 728 if (getRpmNominal() < lastRpm) { 729 // 730 // Slowing down 731 // 732 _parent.accdectime = dec_time; 733 log.debug("decelerate from {} to {}", lastRpm, getRpmNominal()); 734 735 if ((getRpmNominal() < 23) && is_auto_coasting && (count_pre_arrival > 0) && 736 _parent.trigger_sounds.containsKey("pre_arrival") && (dec_time < 250)) { 737 _parent.trigger_sounds.get("pre_arrival").fadeIn(); 738 count_pre_arrival--; 739 } 740 741 // Calculate how long it's been since we lastly checked speed 742 long currentTime = System.currentTimeMillis(); 743 float timePassed = currentTime - timeOfLastSpeedCheck; 744 timeOfLastSpeedCheck = currentTime; 745 // Prove the trigger for decelerating actions (braking, coasting) 746 if (((lastRpm - getRpmNominal()) > _decel_trigger_rpms) && (timePassed < 500.0f)) { 747 log.debug("Time passed {}", timePassed); 748 if ((getRpmNominal() < 30) && (dec_time < 250)) { // Braking sound only when speed is low (, but not to low) 749 if (_parent.trigger_sounds.containsKey("brake")) { 750 _parent.trigger_sounds.get("brake").fadeIn(); 751 is_braking = true; 752 log.debug("braking activ!"); 753 } 754 } else if (notch1.coast_bufs_data.size() > 0 && !is_key_coasting) { 755 is_auto_coasting = true; 756 log.debug("auto-coasting active"); 757 if (!is_chuff_fade_out) { 758 setupChuffFadeOut(); 759 } 760 } 761 } 762 } else { 763 // 764 // Speeding up. 765 // 766 _parent.accdectime = acc_time; 767 log.debug("accelerate from {} to {}", lastRpm, getRpmNominal()); 768 if (is_dynamic_gain) { 769 float new_high_volume = Math.max(dynamic_volume * 0.5f, low_volume) + 770 dynamic_volume * 0.05f * Math.min(getRpmNominal() - getRpm(), 14); 771 if (new_high_volume > high_volume) { 772 high_volume = Math.min(new_high_volume, max_volume); 773 } 774 log.debug("dynamic volume: {}, max volume: {}, high volume: {}", dynamic_volume, max_volume, high_volume); 775 } 776 if (is_braking) { 777 stopBraking(); // Revoke possible brake sound 778 } 779 if (is_auto_coasting) { 780 stopCoasting(); // This makes chuff sound hearable again 781 } 782 } 783 _parent.startAccDecTimer(); // Start, if not already running 784 lastRpm = getRpmNominal(); 785 } 786 } 787 } 788 789 private void stopBraking() { 790 if (is_braking) { 791 if (_parent.trigger_sounds.containsKey("brake")) { 792 _parent.trigger_sounds.get("brake").fadeOut(); 793 is_braking = false; 794 log.debug("braking sound stopped."); 795 } 796 } 797 } 798 799 private void startBoilingSound() { 800 if (_parent.trigger_sounds.containsKey("boiling")) { 801 _parent.trigger_sounds.get("boiling").setLooped(true); 802 _parent.trigger_sounds.get("boiling").play(); 803 log.debug("boiling sound playing"); 804 } 805 } 806 807 private void stopBoilingSound() { 808 if (_parent.trigger_sounds.containsKey("boiling")) { 809 _parent.trigger_sounds.get("boiling").setLooped(false); 810 _parent.trigger_sounds.get("boiling").fadeOut(); 811 log.debug("boiling sound stopped."); 812 } 813 } 814 815 private void stopCoasting() { 816 is_auto_coasting = false; 817 is_key_coasting = false; 818 is_chuff_fade_out = false; 819 if (is_dynamic_gain) { 820 setDynamicVolume(low_volume); 821 } 822 log.debug("coasting sound stopped."); 823 } 824 825 private void getLocoDirection(int d) { 826 // If loco direction was changed we need to set topspeed of the loco to new value 827 // (this is necessary, when topspeed-forward and topspeed-reverse differs) 828 if (d == 1) { // loco is going forward 829 topspeed = _top_speed; 830 } else { 831 topspeed = _top_speed_reverse; 832 } 833 log.debug("loco direction: {}, top speed: {}", d, topspeed); 834 // Re-calculate accel-time and decel-time, hence topspeed may have changed 835 acc_time = calcAccDecTime(_parent.accel_rate); 836 dec_time = calcAccDecTime(_parent.decel_rate); 837 838 // Handle throttle forward and reverse action 839 // nothing to do when loco is not running or just in ramp-up-mode 840 if (getRpm() > 0 && getRpmNominal() > 0 && _parent.isEngineStarted() && !is_in_rampup_mode) { 841 rpm_dirfn = getRpm(); // save rpm for ramp-up 842 log.debug("ramp-up mode - rpm {} saved, rpm nominal: {}", rpm_dirfn, getRpmNominal()); 843 is_in_rampup_mode = true; 844 setRpmNominal(0); // force a stop 845 _parent.startAccDecTimer(); 846 } 847 } 848 849 private void setFunction(String event, boolean is_true, String name) { 850 // This throttle function key handling differs to configurable sounds: 851 // Do something following certain conditions, when a throttle function key is pressed. 852 // Note: throttle will send initial value(s) before thread is started! 853 log.debug("throttle function key pressed: {} is {}, function: {}", event, is_true, name); 854 if (name.equals("COAST")) { 855 // Handle key-coasting on/off. 856 log.debug("COAST key pressed"); 857 is_chuff_fade_out = false; 858 // Set coasting TRUE, if COAST key is pressed. Requires sufficient coasting sounds (chuff_index will rely on that). 859 if (notch1 == null) { 860 notch1 = _parent.getNotch(1); // Because of initial send of throttle key, COAST function key could be "true" 861 } 862 if (is_true && notch1.coast_bufs_data.size() > 0) { 863 is_key_coasting = true; // When idling is active, key-coasting will start after it. 864 if (!is_auto_coasting) { 865 setupChuffFadeOut(); 866 } 867 } else { 868 stopCoasting(); 869 } 870 log.debug("is COAST: {}", is_key_coasting); 871 } 872 873 // Speed change if HALF_SPEED key is pressed 874 if (name.equals("HALF_SPEED")) { 875 log.debug("HALF_SPEED key pressed is {}", is_true); 876 if (_parent.isEngineStarted()) { 877 if (is_true) { 878 is_half_speed = true; 879 } else { 880 is_half_speed = false; 881 } 882 setThrottle(last_throttle); // Trigger a speed update 883 } 884 } 885 886 // Set Accel/Decel off or to lower value 887 if (name.equals("BRAKE_KEY")) { 888 log.debug("BRAKE_KEY pressed is {}", is_true); 889 if (_parent.isEngineStarted()) { 890 if (is_true) { 891 if (_parent.brake_time == 0) { 892 acc_time = 0; 893 dec_time = 0; 894 } else { 895 dec_time = calcAccDecTime(_parent.brake_time); 896 } 897 _parent.accdectime = dec_time; 898 log.debug("accdectime: {}", _parent.accdectime); 899 } else { 900 acc_time = calcAccDecTime(_parent.accel_rate); 901 dec_time = calcAccDecTime(_parent.decel_rate); 902 _parent.accdectime = dec_time; 903 } 904 } 905 } 906 // Other throttle function keys may follow ... 907 } 908 909 private void startEngine() { 910 _sound.unqueueBuffers(); 911 log.debug("thread: start engine ..."); 912 _notch = _parent.getNotch(1); // Initial value 913 notch1 = _parent.getNotch(1); 914 if (_parent.engine_pane != null) { 915 _parent.engine_pane.setThrottle(1); // Set EnginePane (DieselPane) notch 916 } 917 is_dynamic_gain = _parent.is_dynamic_gain; 918 dynamic_volume = 1.0f; 919 _sound.setReferenceDistance(_parent.engine_rd); 920 setRpm(0); 921 _parent.setActualSpeed(0.0f); 922 setRpmNominal(0); 923 helper_index = -1; // Prepare helper buffer start. Index will be incremented before first use 924 setWait(0); 925 startBoilingSound(); 926 startIdling(); 927 acc_time = calcAccDecTime(_parent.accel_rate); // Calculate acceleration time 928 dec_time = calcAccDecTime(_parent.decel_rate); // Calculate deceleration time 929 _parent.initAccDecTimer(); 930 } 931 932 private void stopEngine() { 933 log.debug("thread: stop engine ..."); 934 if (is_looping) { 935 is_looping = false; // Stop the loop player 936 } 937 stopBraking(); 938 stopCoasting(); 939 stopBoilingSound(); 940 stopIdling(); 941 _parent.stopAccDecTimer(); 942 _throttle = 0.0f; // Clear it, just in case the engine was stopped at speed > 0 943 if (_parent.engine_pane != null) { 944 _parent.engine_pane.setThrottle(1); // Set EnginePane (DieselPane) notch 945 } 946 setRpm(0); 947 _parent.setActualSpeed(0.0f); 948 } 949 950 private int calcAccDecTime(int accdec_rate) { 951 // Handle Momentum 952 // Regard topspeed, which may be different on forward or reverse direction 953 int topspeed_rpm = (int) Math.round(topspeed * 1056 / (Math.PI * _driver_diameter_float)); 954 return 896 * accdec_rate / topspeed_rpm; // NMRA value 896 in ms 955 } 956 957 private void startIdling() { 958 is_idling = true; 959 if (_parent.trigger_sounds.containsKey("idle")) { 960 _parent.trigger_sounds.get("idle").play(); 961 } 962 log.debug("start idling ..."); 963 } 964 965 private void stopIdling() { 966 if (is_idling) { 967 is_idling = false; 968 if (_parent.trigger_sounds.containsKey("idle")) { 969 _parent.trigger_sounds.get("idle").fadeOut(); 970 log.debug("idling stopped."); 971 } 972 } 973 } 974 975 private void setupChuffFadeOut() { 976 // discard chuff_fade_out on high acceleration... 977 if (is_looping && _parent.use_chuff_fade_out && getRpmNominal() - getRpm() < 10) { 978 chuff_fade_out_volume = dynamic_volume; 979 chuff_fade_out_factor = 0.7f + (getRpm() * 0.001f); // multiplication 980 is_chuff_fade_out = true; 981 } 982 } 983 984 // 985 // LOOP-PLAYER 986 // 987 @Override 988 public void run() { 989 try { 990 while (is_running) { 991 if (is_looping && AudioUtil.isAudioRunning()) { 992 if (_sound.getSource().numProcessedBuffers() > 0) { 993 _sound.unqueueBuffers(); 994 } 995 log.debug("run loop. Buffers queued: {}", _sound.getSource().numQueuedBuffers()); 996 if ((_sound.getSource().numQueuedBuffers() < queue_limit) && (getWait() == 0)) { 997 setSound(selectData()); // Select appropriate WAV data, handle sound and filler and queue the sound 998 } 999 checkAudioState(); 1000 } else { 1001 if (_sound.getSource().numProcessedBuffers() > 0) { 1002 _sound.unqueueBuffers(); 1003 } 1004 } 1005 sleep(_parent.sleep_interval); 1006 updateWait(); 1007 } 1008 _sound.stop(); 1009 } catch (InterruptedException ie) { 1010 // kill thread 1011 log.debug("thread interrupted"); 1012 } 1013 } 1014 1015 private void checkAudioState() { 1016 if (first_start) { 1017 _sound.play(); 1018 first_start = false; 1019 } else { 1020 if (_sound.getSource().getState() != Audio.STATE_PLAYING) { 1021 _sound.play(); 1022 log.info("loop sound re-started"); 1023 } 1024 } 1025 } 1026 1027 private ByteBuffer selectData() { 1028 ByteBuffer data; 1029 updateVolume(); 1030 if ((is_key_coasting || is_auto_coasting) && !is_chuff_fade_out) { 1031 data = notch1.coast_bufs_data.get(incChuffIndex()); // Take the coasting sound 1032 } else { 1033 data = _notch.chuff_bufs_data.get(incChuffIndex()); // Take the standard chuff sound 1034 } 1035 return data; 1036 } 1037 1038 private void changeNotch() { 1039 int new_notch = _notch.getNotch(); 1040 log.debug("changing notch ... rpm: {}, notch: {}, chuff index: {}", 1041 getRpm(), _notch.getNotch(), chuff_index); 1042 if ((getRpm() > _notch.getMaxLimit()) && (new_notch < _parent.notch_sounds.size())) { 1043 // Too fast. Need to go to next notch up 1044 new_notch++; 1045 log.debug("change up. notch: {}", new_notch); 1046 _notch = _parent.getNotch(new_notch); 1047 } else if ((getRpm() < _notch.getMinLimit()) && (new_notch > 1)) { 1048 // Too slow. Need to go to next notch down 1049 new_notch--; 1050 log.debug("change down. notch: {}", new_notch); 1051 _notch = _parent.getNotch(new_notch); 1052 } 1053 _parent.engine_pane.setThrottle(new_notch); // Update EnginePane (DieselPane) notch 1054 } 1055 1056 private int getRpm() { 1057 return rpm; // Actual Revolution per Minute 1058 } 1059 1060 private void setRpm(int r) { 1061 rpm = r; 1062 } 1063 1064 private int getRpmNominal() { 1065 return rpm_nominal; // Nominal Revolution per Minute 1066 } 1067 1068 private void setRpmNominal(int rn) { 1069 rpm_nominal = rn; 1070 } 1071 1072 private void updateRpm() { 1073 if (getRpmNominal() > getRpm()) { 1074 // Actual rpm should not exceed highest max-rpm defined in config.xml 1075 if (getRpm() < _parent.getNotch(_parent.notch_sounds.size()).getMaxLimit()) { 1076 setRpm(getRpm() + 1); 1077 } else { 1078 log.debug("actual rpm not increased. Value: {}", getRpm()); 1079 } 1080 log.debug("accel - nominal RPM: {}, actual RPM: {}", getRpmNominal(), getRpm()); 1081 } else if (getRpmNominal() < getRpm()) { 1082 // deceleration 1083 setRpm(getRpm() - 1); 1084 if (getRpm() < 0) { 1085 setRpm(0); 1086 } 1087 // strong deceleration 1088 if (is_dynamic_gain && (getRpm() - getRpmNominal() > 4) && !is_auto_coasting && !is_key_coasting && !is_chuff_fade_out) { 1089 dynamic_volume = low_volume; 1090 } 1091 log.debug("decel - nominal RPM: {}, actual RPM: {}", getRpmNominal(), getRpm()); 1092 } else { 1093 _parent.stopAccDecTimer(); // Speed is unchanged, nothing to do 1094 } 1095 1096 // calculate actual speed from actual RPM and based on topspeed 1097 _parent.setActualSpeed(getRpm() / (topspeed * 1056 / ((float) Math.PI * _driver_diameter_float))); 1098 log.debug("nominal RPM: {}, actual RPM: {}, actual speed: {}, t: {}, speedcurve(t): {}", 1099 getRpmNominal(), getRpm(), _parent.getActualSpeed(), _throttle, _parent.speedCurve(_throttle)); 1100 1101 // Start or Stop the LOOP-PLAYER 1102 checkState(); 1103 1104 // Are we in the right notch? 1105 if ((getRpm() >= notch1.getMinLimit()) && (!_notch.isInLimits(getRpm()))) { 1106 log.debug("Notch change! Notch: {}, RPM nominal: {}, RPM actual: {}", _notch.getNotch(), getRpmNominal(), getRpm()); 1107 changeNotch(); 1108 } 1109 } 1110 1111 private void checkState() { 1112 if (is_looping) { 1113 if (getRpm() < notch1.getMinLimit()) { 1114 is_looping = false; // Stop the loop player 1115 setWait(0); 1116 if (is_dynamic_gain && !is_key_coasting) { 1117 high_volume = low_volume; 1118 } 1119 log.debug("change from chuff or coast to idle."); 1120 is_auto_coasting = false; 1121 stopBraking(); 1122 startIdling(); 1123 } 1124 } else { 1125 if (_parent.isEngineStarted() && (getRpm() >= notch1.getMinLimit())) { 1126 stopIdling(); 1127 if (is_dynamic_gain && !is_key_coasting) { 1128 dynamic_volume = high_volume; 1129 } 1130 // Now prepare to start the chuff sound (or coasting sound) 1131 _notch = _parent.getNotch(1); // Initial notch value 1132 chuff_index = -1; // Index will be incremented before first usage 1133 count_pre_arrival = 1; 1134 is_chuff_fade_out = false; // Default 1135 first_start = true; 1136 if (is_in_rampup_mode && _sound.getSource().getState() == Audio.STATE_PLAYING) { 1137 _sound.stop(); 1138 } 1139 is_looping = true; // Start the loop player 1140 } 1141 1142 // Handle a throttle forward or reverse change 1143 if (is_in_rampup_mode && getRpm() == 0) { 1144 setRpmNominal(rpm_dirfn); 1145 _parent.accdectime = acc_time; 1146 _parent.startAccDecTimer(); 1147 is_in_rampup_mode = false; 1148 } 1149 } 1150 1151 if (getRpm() > 0) { 1152 queue_limit = Math.max(2, Math.abs(500 / calcChuffInterval(getRpm()))); 1153 log.debug("queue limit: {}", queue_limit); 1154 } 1155 } 1156 1157 private void updateVolume() { 1158 if (is_dynamic_gain && !is_chuff_fade_out && !is_key_coasting && !is_auto_coasting) { 1159 if (getRpmNominal() < getRpm()) { 1160 // deceleration 1161 float inc1 = 0.05f; 1162 if (dynamic_volume >= low_volume) { 1163 dynamic_volume -= inc1; 1164 } 1165 } else { 1166 float inc2 = 0.01f; 1167 float inc3 = 0.005f; 1168 if (dynamic_volume + inc3 < 1.0f && high_volume < 1.0f) { 1169 dynamic_volume += inc3; 1170 } else if (dynamic_volume + inc2 < high_volume) { 1171 dynamic_volume += inc2; 1172 } else if (dynamic_volume - inc3 > 1.0f) { 1173 dynamic_volume -= inc3; 1174 high_volume -= inc2; 1175 } 1176 } 1177 setDynamicVolume(dynamic_volume); 1178 } 1179 } 1180 1181 private void updateWait() { 1182 if (getWait() > 0) { 1183 setWait(getWait() - 1); 1184 } 1185 } 1186 1187 private void setWait(int wait) { 1188 wait_loops = wait; 1189 } 1190 1191 private int getWait() { 1192 return wait_loops; 1193 } 1194 1195 private int incChuffIndex() { 1196 chuff_index++; 1197 // Correct for wrap. 1198 if (chuff_index >= (_num_cylinders * 2)) { 1199 chuff_index = 0; 1200 } 1201 log.debug("new chuff index: {}", chuff_index); 1202 return chuff_index; 1203 } 1204 1205 private int incHelperIndex() { 1206 helper_index++; 1207 // Correct for wrap. 1208 if (helper_index >= notch1.bufs_helper.size()) { 1209 helper_index = 0; 1210 } 1211 return helper_index; 1212 } 1213 1214 private int calcRPM(float t) { 1215 // speed = % of topspeed (mph) 1216 // RPM = speed * ((inches/mile) / (minutes/hour)) / (pi * driver_diameter_float) 1217 return (int) Math.round(_parent.speedCurve(t) * topspeed * 1056 / (Math.PI * _driver_diameter_float)); 1218 } 1219 1220 private int calcChuffInterval(int revpm) { 1221 // chuff interval will be calculated based on revolutions per minute (revpm) 1222 // note: interval time includes the sound duration! 1223 // chuffInterval = time in ms per revolution of the driver wheel: 1224 // 60,000 ms / revpm / number of cylinders / 2 (because cylinders are double-acting) 1225 return (int) Math.round(60000.0 / revpm / _num_cylinders / 2.0); 1226 } 1227 1228 private void setSound(ByteBuffer data) { 1229 AudioBuffer buf = notch1.bufs_helper.get(incHelperIndex()); // buffer for the queue 1230 int sbl = 0; // sound bite length 1231 if (notch1.getBufferFreq() > 0) { 1232 sbl = (1000 * data.limit()/notch1.getBufferFrameSize()) / notch1.getBufferFreq(); // calculate the length of the clip in milliseconds 1233 } 1234 log.debug("sbl: {}", sbl); 1235 // Time in ms from chuff start up to begin of the next chuff, limited to a minimum 1236 int interval = Math.max(calcChuffInterval(getRpm()), _parent.sleep_interval); 1237 int bbufcount = notch1.getBufferFrameSize() * ((interval) * notch1.getBufferFreq() / 1000); 1238 ByteBuffer bbuf = ByteBuffer.allocateDirect(bbufcount); // Target 1239 1240 if (interval > sbl) { 1241 // Regular queueing. Whole sound clip goes to the queue. Low notches 1242 // Prepare the sound and transfer it to the target ByteBuffer bbuf 1243 int bbufcount2 = notch1.getBufferFrameSize() * (sbl * notch1.getBufferFreq() / 1000); 1244 byte[] bbytes2 = new byte[bbufcount2]; 1245 data.get(bbytes2); // Same as: data.get(bbytes2, 0, bbufcount2); 1246 data.rewind(); 1247 1248 // chuff_fade_out 1249 doChuffFadeOut(bbufcount2, bbytes2); 1250 1251 bbuf.order(data.order()); // Set new buffer's byte order to match source buffer. 1252 bbuf.put(bbytes2); // Same as: bbuf.put(bbytes2, 0, bbufcount2); 1253 1254 // Handle filler for the remaining part of the AudioBuffer 1255 if (bbuf.hasRemaining()) { 1256 log.debug("remaining: {}", bbuf.remaining()); 1257 ByteBuffer dataf; 1258 if (is_key_coasting || is_auto_coasting) { 1259 dataf = notch1.getCoastFillerData(); 1260 } else { 1261 dataf = notch1.getNotchFillerData(); 1262 } 1263 if (dataf == null) { 1264 log.debug("No filler sound found"); 1265 // Nothing to do on 16-bit, because 0 is default for "silence"; 8-bit-mono needs 128, otherwise it's "noisy" 1266 if (notch1.getBufferFmt() == com.jogamp.openal.AL.AL_FORMAT_MONO8) { 1267 byte[] bbytesfiller = new byte[bbuf.remaining()]; 1268 for (int i = 0; i < bbytesfiller.length; i++) { 1269 bbytesfiller[i] = (byte) 0x80; // fill array with "silence" 1270 } 1271 bbuf.put(bbytesfiller); 1272 } 1273 } else { 1274 // Filler sound found 1275 log.debug("data limit: {}, remaining: {}", dataf.limit(), bbuf.remaining()); 1276 byte[] bbytesfiller2 = new byte[bbuf.remaining()]; 1277 if (dataf.limit() >= bbuf.remaining()) { 1278 dataf.get(bbytesfiller2); 1279 dataf.rewind(); 1280 bbuf.put(bbytesfiller2); 1281 } else { 1282 log.debug("not enough filler length"); 1283 byte[] bbytesfillerpart = new byte[dataf.limit()]; 1284 dataf.get(bbytesfillerpart); 1285 dataf.rewind(); 1286 int k = 0; 1287 for (int i = 0; i < bbytesfiller2.length; i++) { 1288 bbytesfiller2[i] = bbytesfillerpart[k]; 1289 k++; 1290 if (k == dataf.limit()) { 1291 k = 0; 1292 } 1293 } 1294 bbuf.put(bbytesfiller2); 1295 } 1296 } 1297 } 1298 } else { 1299 // Need to cut the SoundBite to new length of interval 1300 log.debug("need to cut sound clip from {} to length {}", sbl, interval); 1301 byte[] bbytes = new byte[bbufcount]; 1302 data.get(bbytes); // Same as: data.get(bbytes, 0, bbufcount); 1303 data.rewind(); 1304 1305 doChuffFadeOut(bbufcount, bbytes); 1306 1307 bbuf.order(data.order()); // Set new buffer's byte order to match source buffer 1308 bbuf.put(bbytes); // Same as: bbuf.put(bbytes, 0, bbufcount); 1309 } 1310 bbuf.rewind(); 1311 buf.loadBuffer(bbuf, notch1.getBufferFmt(), notch1.getBufferFreq()); 1312 _sound.queueBuffer(buf); 1313 log.debug("buffer queued. Length: {}", (int)SoundBite.calcLength(buf)); 1314 1315 // wait some loops to get up-to-date speed value 1316 setWait((interval - _parent.sleep_interval * _parent.wait_factor) / _parent.sleep_interval); 1317 if (getWait() < 3) { 1318 setWait(0); 1319 } 1320 } 1321 1322 private void doChuffFadeOut(int count, byte[] bbytes) { 1323 // applicable for 16-bit mono sounds only 1324 // (I don't have a solution for volume change on 8-bit sounds) 1325 if (is_chuff_fade_out) { 1326 chuff_fade_out_volume *= chuff_fade_out_factor; 1327 if (chuff_fade_out_volume < 0.15f) { // 0.07f 1328 is_chuff_fade_out = false; // done 1329 if (is_dynamic_gain) { 1330 dynamic_volume = 1.0f; 1331 setDynamicVolume(dynamic_volume); 1332 } 1333 } 1334 for (int i = 0; i < count; ++i) { 1335 bbytes[i] *= chuff_fade_out_volume; // make it quieter 1336 } 1337 } 1338 } 1339 1340 private void mute(boolean m) { 1341 _sound.mute(m); 1342 for (SoundBite ts : _parent.trigger_sounds.values()) { 1343 ts.mute(m); 1344 } 1345 } 1346 1347 // called by the LoopThread on volume changes with active dynamic_gain 1348 private void setDynamicVolume(float v) { 1349 if (_parent.getTunnel()) { 1350 v *= VSDSound.tunnel_volume; 1351 } 1352 1353 if (!_parent.getVsd().isMuted()) { 1354 // v * master_volume * decoder_volume, will be multiplied by gain in SoundBite 1355 // forward volume to SoundBite 1356 _sound.setVolume(v * VSDecoderManager.instance().getMasterVolume() * 0.01f * _parent.getVsd().getDecoderVolume()); 1357 } 1358 } 1359 1360 // triggered by VSDecoder via VSDSound on sound positioning, master or decoder slider changes 1361 // volume v is already multiplied by master_volume and decoder_volume 1362 private void setVolume(float v) { 1363 // handle engine sound (loop sound) 1364 if (! is_dynamic_gain) { 1365 _sound.setVolume(v); // special case on active dynamic_gain 1366 } 1367 // handle trigger sounds (e.g. idle) 1368 for (SoundBite ts : _parent.trigger_sounds.values()) { 1369 ts.setVolume(v); 1370 } 1371 } 1372 1373 private void setPosition(PhysicalLocation p) { 1374 _sound.setPosition(p); 1375 for (SoundBite ts : _parent.trigger_sounds.values()) { 1376 ts.setPosition(p); 1377 } 1378 } 1379 1380 @SuppressWarnings("hiding") // Field has same name as a field in the super class 1381 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(S1LoopThread.class); 1382 1383 } 1384}