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.setGain(_parent.engine_gain); // All chuff sounds will have this gain 713 count_pre_arrival = 1; 714 queue_limit = 2; 715 wait_loops = 0; 716 if (r) { 717 this.start(); 718 } 719 } 720 721 private void setRunning(boolean r) { 722 is_running = r; 723 } 724 725 private void setThrottle(float t) { 726 // Don't do anything, if engine is not started 727 // Another required value is a S1Notch (should have been set at engine start) 728 if (_parent.isEngineStarted()) { 729 if (t < 0.0f) { 730 // DO something to shut down 731 is_in_rampup_mode = false; // interrupt ramp-up 732 setRpmNominal(0); 733 _parent.accdectime = 0; 734 _parent.startAccDecTimer(); 735 } else { 736 _throttle = t; 737 last_throttle = t; 738 739 // handle half-speed 740 if (is_half_speed) { 741 _throttle = _throttle / 2; 742 } 743 744 // Calculate the nominal speed (Revolutions Per Minute) 745 setRpmNominal(calcRPM(_throttle)); 746 747 // Speeding up or slowing down? 748 if (getRpmNominal() < lastRpm) { 749 // 750 // Slowing down 751 // 752 _parent.accdectime = dec_time; 753 log.debug("decelerate from {} to {}", lastRpm, getRpmNominal()); 754 755 if ((getRpmNominal() < 23) && is_auto_coasting && (count_pre_arrival > 0) && 756 _parent.trigger_sounds.containsKey("pre_arrival") && (dec_time < 250)) { 757 _parent.trigger_sounds.get("pre_arrival").fadeIn(); 758 count_pre_arrival--; 759 } 760 761 // Calculate how long it's been since we lastly checked speed 762 long currentTime = System.currentTimeMillis(); 763 float timePassed = currentTime - timeOfLastSpeedCheck; 764 timeOfLastSpeedCheck = currentTime; 765 // Prove the trigger for decelerating actions (braking, coasting) 766 if (((lastRpm - getRpmNominal()) > _decel_trigger_rpms) && (timePassed < 500.0f)) { 767 log.debug("Time passed {}", timePassed); 768 if ((getRpmNominal() < 30) && (dec_time < 250)) { // Braking sound only when speed is low (, but not to low) 769 if (_parent.trigger_sounds.containsKey("brake")) { 770 _parent.trigger_sounds.get("brake").fadeIn(); 771 is_braking = true; 772 log.debug("braking activ!"); 773 } 774 } else if (notch1.coast_bufs_data.size() > 0 && !is_key_coasting) { 775 is_auto_coasting = true; 776 log.debug("auto-coasting active"); 777 if (!is_chuff_fade_out) { 778 setupChuffFadeOut(); 779 } 780 } 781 } 782 } else { 783 // 784 // Speeding up. 785 // 786 _parent.accdectime = acc_time; 787 log.debug("accelerate from {} to {}", lastRpm, getRpmNominal()); 788 if (is_dynamic_gain) { 789 float new_high_volume = Math.max(dynamic_volume * 0.5f, low_volume) + 790 dynamic_volume * 0.05f * Math.min(getRpmNominal() - getRpm(), 14); 791 if (new_high_volume > high_volume) { 792 high_volume = Math.min(new_high_volume, max_volume); 793 } 794 log.debug("dynamic volume: {}, max volume: {}, high volume: {}", dynamic_volume, max_volume, high_volume); 795 } 796 if (is_braking) { 797 stopBraking(); // Revoke possible brake sound 798 } 799 if (is_auto_coasting) { 800 stopCoasting(); // This makes chuff sound hearable again 801 } 802 } 803 _parent.startAccDecTimer(); // Start, if not already running 804 lastRpm = getRpmNominal(); 805 } 806 } 807 } 808 809 private void stopBraking() { 810 if (is_braking) { 811 if (_parent.trigger_sounds.containsKey("brake")) { 812 _parent.trigger_sounds.get("brake").fadeOut(); 813 is_braking = false; 814 log.debug("braking sound stopped."); 815 } 816 } 817 } 818 819 private void startBoilingSound() { 820 if (_parent.trigger_sounds.containsKey("boiling")) { 821 _parent.trigger_sounds.get("boiling").setLooped(true); 822 _parent.trigger_sounds.get("boiling").play(); 823 log.debug("boiling sound playing"); 824 } 825 } 826 827 private void stopBoilingSound() { 828 if (_parent.trigger_sounds.containsKey("boiling")) { 829 _parent.trigger_sounds.get("boiling").setLooped(false); 830 _parent.trigger_sounds.get("boiling").fadeOut(); 831 log.debug("boiling sound stopped."); 832 } 833 } 834 835 private void stopCoasting() { 836 is_auto_coasting = false; 837 is_key_coasting = false; 838 is_chuff_fade_out = false; 839 if (is_dynamic_gain) { 840 setDynamicVolume(low_volume); 841 } 842 log.debug("coasting sound stopped."); 843 } 844 845 private void getLocoDirection(int d) { 846 // If loco direction was changed we need to set topspeed of the loco to new value 847 // (this is necessary, when topspeed-forward and topspeed-reverse differs) 848 if (d == 1) { // loco is going forward 849 topspeed = _top_speed; 850 } else { 851 topspeed = _top_speed_reverse; 852 } 853 log.debug("loco direction: {}, top speed: {}", d, topspeed); 854 // Re-calculate accel-time and decel-time, hence topspeed may have changed 855 acc_time = calcAccDecTime(_parent.accel_rate); 856 dec_time = calcAccDecTime(_parent.decel_rate); 857 858 // Handle throttle forward and reverse action 859 // nothing to do when loco is not running or just in ramp-up-mode 860 if (getRpm() > 0 && getRpmNominal() > 0 && _parent.isEngineStarted() && !is_in_rampup_mode) { 861 rpm_dirfn = getRpm(); // save rpm for ramp-up 862 log.debug("ramp-up mode - rpm {} saved, rpm nominal: {}", rpm_dirfn, getRpmNominal()); 863 is_in_rampup_mode = true; 864 setRpmNominal(0); // force a stop 865 _parent.startAccDecTimer(); 866 } 867 } 868 869 private void setFunction(String event, boolean is_true, String name) { 870 // This throttle function key handling differs to configurable sounds: 871 // Do something following certain conditions, when a throttle function key is pressed. 872 // Note: throttle will send initial value(s) before thread is started! 873 log.debug("throttle function key pressed: {} is {}, function: {}", event, is_true, name); 874 if (name.equals("COAST")) { 875 // Handle key-coasting on/off. 876 log.debug("COAST key pressed"); 877 is_chuff_fade_out = false; 878 // Set coasting TRUE, if COAST key is pressed. Requires sufficient coasting sounds (chuff_index will rely on that). 879 if (notch1 == null) { 880 notch1 = _parent.getNotch(1); // Because of initial send of throttle key, COAST function key could be "true" 881 } 882 if (is_true && notch1.coast_bufs_data.size() > 0) { 883 is_key_coasting = true; // When idling is active, key-coasting will start after it. 884 if (!is_auto_coasting) { 885 setupChuffFadeOut(); 886 } 887 } else { 888 stopCoasting(); 889 } 890 log.debug("is COAST: {}", is_key_coasting); 891 } 892 893 // Speed change if HALF_SPEED key is pressed 894 if (name.equals("HALF_SPEED")) { 895 log.debug("HALF_SPEED key pressed is {}", is_true); 896 if (_parent.isEngineStarted()) { 897 if (is_true) { 898 is_half_speed = true; 899 } else { 900 is_half_speed = false; 901 } 902 setThrottle(last_throttle); // Trigger a speed update 903 } 904 } 905 906 // Set Accel/Decel off or to lower value 907 if (name.equals("BRAKE_KEY")) { 908 log.debug("BRAKE_KEY pressed is {}", is_true); 909 if (_parent.isEngineStarted()) { 910 if (is_true) { 911 if (_parent.brake_time == 0) { 912 acc_time = 0; 913 dec_time = 0; 914 } else { 915 dec_time = calcAccDecTime(_parent.brake_time); 916 } 917 _parent.accdectime = dec_time; 918 log.debug("accdectime: {}", _parent.accdectime); 919 } else { 920 acc_time = calcAccDecTime(_parent.accel_rate); 921 dec_time = calcAccDecTime(_parent.decel_rate); 922 _parent.accdectime = dec_time; 923 } 924 } 925 } 926 // Other throttle function keys may follow ... 927 } 928 929 private void startEngine() { 930 _sound.unqueueBuffers(); 931 log.debug("thread: start engine ..."); 932 _notch = _parent.getNotch(1); // Initial value 933 notch1 = _parent.getNotch(1); 934 if (_parent.engine_pane != null) { 935 _parent.engine_pane.setThrottle(1); // Set EnginePane (DieselPane) notch 936 } 937 is_dynamic_gain = _parent.is_dynamic_gain; 938 dynamic_volume = 1.0f; 939 _sound.setReferenceDistance(_parent.engine_rd); 940 setRpm(0); 941 _parent.setActualSpeed(0.0f); 942 setRpmNominal(0); 943 helper_index = -1; // Prepare helper buffer start. Index will be incremented before first use 944 setWait(0); 945 startBoilingSound(); 946 startIdling(); 947 acc_time = calcAccDecTime(_parent.accel_rate); // Calculate acceleration time 948 dec_time = calcAccDecTime(_parent.decel_rate); // Calculate deceleration time 949 _parent.initAccDecTimer(); 950 } 951 952 private void stopEngine() { 953 log.debug("thread: stop engine ..."); 954 if (is_looping) { 955 is_looping = false; // Stop the loop player 956 } 957 stopBraking(); 958 stopCoasting(); 959 stopBoilingSound(); 960 stopIdling(); 961 _parent.stopAccDecTimer(); 962 _throttle = 0.0f; // Clear it, just in case the engine was stopped at speed > 0 963 if (_parent.engine_pane != null) { 964 _parent.engine_pane.setThrottle(1); // Set EnginePane (DieselPane) notch 965 } 966 setRpm(0); 967 _parent.setActualSpeed(0.0f); 968 } 969 970 private int calcAccDecTime(int accdec_rate) { 971 // Handle Momentum 972 // Regard topspeed, which may be different on forward or reverse direction 973 int topspeed_rpm = (int) Math.round(topspeed * 1056 / (Math.PI * _driver_diameter_float)); 974 return 896 * accdec_rate / topspeed_rpm; // NMRA value 896 in ms 975 } 976 977 private void startIdling() { 978 is_idling = true; 979 if (_parent.trigger_sounds.containsKey("idle")) { 980 _parent.trigger_sounds.get("idle").play(); 981 } 982 log.debug("start idling ..."); 983 } 984 985 private void stopIdling() { 986 if (is_idling) { 987 is_idling = false; 988 if (_parent.trigger_sounds.containsKey("idle")) { 989 _parent.trigger_sounds.get("idle").fadeOut(); 990 log.debug("idling stopped."); 991 } 992 } 993 } 994 995 private void setupChuffFadeOut() { 996 // discard chuff_fade_out on high acceleration... 997 if (is_looping && _parent.use_chuff_fade_out && getRpmNominal() - getRpm() < 10) { 998 chuff_fade_out_volume = dynamic_volume; 999 chuff_fade_out_factor = 0.7f + (getRpm() * 0.001f); // multiplication 1000 is_chuff_fade_out = true; 1001 } 1002 } 1003 1004 // 1005 // LOOP-PLAYER 1006 // 1007 @Override 1008 public void run() { 1009 try { 1010 while (is_running) { 1011 if (is_looping && AudioUtil.isAudioRunning()) { 1012 if (_sound.getSource().numProcessedBuffers() > 0) { 1013 _sound.unqueueBuffers(); 1014 } 1015 log.debug("run loop. Buffers queued: {}", _sound.getSource().numQueuedBuffers()); 1016 if ((_sound.getSource().numQueuedBuffers() < queue_limit) && (getWait() == 0)) { 1017 setSound(selectData()); // Select appropriate WAV data, handle sound and filler and queue the sound 1018 } 1019 checkAudioState(); 1020 } else { 1021 if (_sound.getSource().numProcessedBuffers() > 0) { 1022 _sound.unqueueBuffers(); 1023 } 1024 } 1025 sleep(_parent.sleep_interval); 1026 updateWait(); 1027 } 1028 _sound.stop(); 1029 } catch (InterruptedException ie) { 1030 // kill thread 1031 log.debug("thread interrupted"); 1032 } 1033 } 1034 1035 private void checkAudioState() { 1036 if (first_start) { 1037 _sound.play(); 1038 first_start = false; 1039 } else { 1040 if (_sound.getSource().getState() != Audio.STATE_PLAYING) { 1041 _sound.play(); 1042 log.info("loop sound re-started"); 1043 } 1044 } 1045 } 1046 1047 private ByteBuffer selectData() { 1048 ByteBuffer data; 1049 updateVolume(); 1050 if ((is_key_coasting || is_auto_coasting) && !is_chuff_fade_out) { 1051 data = notch1.coast_bufs_data.get(incChuffIndex()); // Take the coasting sound 1052 } else { 1053 data = _notch.chuff_bufs_data.get(incChuffIndex()); // Take the standard chuff sound 1054 } 1055 return data; 1056 } 1057 1058 private void changeNotch() { 1059 int new_notch = _notch.getNotch(); 1060 log.debug("changing notch ... rpm: {}, notch: {}, chuff index: {}", 1061 getRpm(), _notch.getNotch(), chuff_index); 1062 if ((getRpm() > _notch.getMaxLimit()) && (new_notch < _parent.notch_sounds.size())) { 1063 // Too fast. Need to go to next notch up 1064 new_notch++; 1065 log.debug("change up. notch: {}", new_notch); 1066 _notch = _parent.getNotch(new_notch); 1067 } else if ((getRpm() < _notch.getMinLimit()) && (new_notch > 1)) { 1068 // Too slow. Need to go to next notch down 1069 new_notch--; 1070 log.debug("change down. notch: {}", new_notch); 1071 _notch = _parent.getNotch(new_notch); 1072 } 1073 _parent.engine_pane.setThrottle(new_notch); // Update EnginePane (DieselPane) notch 1074 } 1075 1076 private int getRpm() { 1077 return rpm; // Actual Revolution per Minute 1078 } 1079 1080 private void setRpm(int r) { 1081 rpm = r; 1082 } 1083 1084 private int getRpmNominal() { 1085 return rpm_nominal; // Nominal Revolution per Minute 1086 } 1087 1088 private void setRpmNominal(int rn) { 1089 rpm_nominal = rn; 1090 } 1091 1092 private void updateRpm() { 1093 if (getRpmNominal() > getRpm()) { 1094 // Actual rpm should not exceed highest max-rpm defined in config.xml 1095 if (getRpm() < _parent.getNotch(_parent.notch_sounds.size()).getMaxLimit()) { 1096 setRpm(getRpm() + 1); 1097 } else { 1098 log.debug("actual rpm not increased. Value: {}", getRpm()); 1099 } 1100 log.debug("accel - nominal RPM: {}, actual RPM: {}", getRpmNominal(), getRpm()); 1101 } else if (getRpmNominal() < getRpm()) { 1102 // deceleration 1103 setRpm(getRpm() - 1); 1104 if (getRpm() < 0) { 1105 setRpm(0); 1106 } 1107 // strong deceleration 1108 if (is_dynamic_gain && (getRpm() - getRpmNominal() > 4) && !is_auto_coasting && !is_key_coasting && !is_chuff_fade_out) { 1109 dynamic_volume = low_volume; 1110 } 1111 log.debug("decel - nominal RPM: {}, actual RPM: {}", getRpmNominal(), getRpm()); 1112 } else { 1113 _parent.stopAccDecTimer(); // Speed is unchanged, nothing to do 1114 } 1115 1116 // calculate actual speed from actual RPM and based on topspeed 1117 _parent.setActualSpeed(getRpm() / (topspeed * 1056 / ((float) Math.PI * _driver_diameter_float))); 1118 log.debug("nominal RPM: {}, actual RPM: {}, actual speed: {}, t: {}, speedcurve(t): {}", 1119 getRpmNominal(), getRpm(), _parent.getActualSpeed(), _throttle, _parent.speedCurve(_throttle)); 1120 1121 // Start or Stop the LOOP-PLAYER 1122 checkState(); 1123 1124 // Are we in the right notch? 1125 if ((getRpm() >= notch1.getMinLimit()) && (!_notch.isInLimits(getRpm()))) { 1126 log.debug("Notch change! Notch: {}, RPM nominal: {}, RPM actual: {}", _notch.getNotch(), getRpmNominal(), getRpm()); 1127 changeNotch(); 1128 } 1129 } 1130 1131 private void checkState() { 1132 if (is_looping) { 1133 if (getRpm() < notch1.getMinLimit()) { 1134 is_looping = false; // Stop the loop player 1135 setWait(0); 1136 if (is_dynamic_gain && !is_key_coasting) { 1137 high_volume = low_volume; 1138 } 1139 log.debug("change from chuff or coast to idle."); 1140 is_auto_coasting = false; 1141 stopBraking(); 1142 startIdling(); 1143 } 1144 } else { 1145 if (_parent.isEngineStarted() && (getRpm() >= notch1.getMinLimit())) { 1146 stopIdling(); 1147 if (is_dynamic_gain && !is_key_coasting) { 1148 dynamic_volume = high_volume; 1149 } 1150 // Now prepare to start the chuff sound (or coasting sound) 1151 _notch = _parent.getNotch(1); // Initial notch value 1152 chuff_index = -1; // Index will be incremented before first usage 1153 count_pre_arrival = 1; 1154 is_chuff_fade_out = false; // Default 1155 first_start = true; 1156 if (is_in_rampup_mode && _sound.getSource().getState() == Audio.STATE_PLAYING) { 1157 _sound.stop(); 1158 } 1159 is_looping = true; // Start the loop player 1160 } 1161 1162 // Handle a throttle forward or reverse change 1163 if (is_in_rampup_mode && getRpm() == 0) { 1164 setRpmNominal(rpm_dirfn); 1165 _parent.accdectime = acc_time; 1166 _parent.startAccDecTimer(); 1167 is_in_rampup_mode = false; 1168 } 1169 } 1170 1171 if (getRpm() > 0) { 1172 queue_limit = Math.max(2, Math.abs(500 / calcChuffInterval(getRpm()))); 1173 log.debug("queue limit: {}", queue_limit); 1174 } 1175 } 1176 1177 private void updateVolume() { 1178 if (is_dynamic_gain && !is_chuff_fade_out && !is_key_coasting && !is_auto_coasting) { 1179 if (getRpmNominal() < getRpm()) { 1180 // deceleration 1181 float inc1 = 0.05f; 1182 if (dynamic_volume >= low_volume) { 1183 dynamic_volume -= inc1; 1184 } 1185 } else { 1186 float inc2 = 0.01f; 1187 float inc3 = 0.005f; 1188 if (dynamic_volume + inc3 < 1.0f && high_volume < 1.0f) { 1189 dynamic_volume += inc3; 1190 } else if (dynamic_volume + inc2 < high_volume) { 1191 dynamic_volume += inc2; 1192 } else if (dynamic_volume - inc3 > 1.0f) { 1193 dynamic_volume -= inc3; 1194 high_volume -= inc2; 1195 } 1196 } 1197 setDynamicVolume(dynamic_volume); 1198 } 1199 } 1200 1201 private void updateWait() { 1202 if (getWait() > 0) { 1203 setWait(getWait() - 1); 1204 } 1205 } 1206 1207 private void setWait(int wait) { 1208 wait_loops = wait; 1209 } 1210 1211 private int getWait() { 1212 return wait_loops; 1213 } 1214 1215 private int incChuffIndex() { 1216 chuff_index++; 1217 // Correct for wrap. 1218 if (chuff_index >= (_num_cylinders * 2)) { 1219 chuff_index = 0; 1220 } 1221 log.debug("new chuff index: {}", chuff_index); 1222 return chuff_index; 1223 } 1224 1225 private int incHelperIndex() { 1226 helper_index++; 1227 // Correct for wrap. 1228 if (helper_index >= notch1.bufs_helper.size()) { 1229 helper_index = 0; 1230 } 1231 return helper_index; 1232 } 1233 1234 private int calcRPM(float t) { 1235 // speed = % of topspeed (mph) 1236 // RPM = speed * ((inches/mile) / (minutes/hour)) / (pi * driver_diameter_float) 1237 return (int) Math.round(_parent.speedCurve(t) * topspeed * 1056 / (Math.PI * _driver_diameter_float)); 1238 } 1239 1240 private int calcChuffInterval(int revpm) { 1241 // chuff interval will be calculated based on revolutions per minute (revpm) 1242 // note: interval time includes the sound duration! 1243 // chuffInterval = time in ms per revolution of the driver wheel: 1244 // 60,000 ms / revpm / number of cylinders / 2 (because cylinders are double-acting) 1245 return (int) Math.round(60000.0 / revpm / _num_cylinders / 2.0); 1246 } 1247 1248 private void setSound(ByteBuffer data) { 1249 AudioBuffer buf = notch1.bufs_helper.get(incHelperIndex()); // buffer for the queue 1250 int sbl = 0; // sound bite length 1251 if (notch1.getBufferFreq() > 0) { 1252 sbl = (1000 * data.limit()/notch1.getBufferFrameSize()) / notch1.getBufferFreq(); // calculate the length of the clip in milliseconds 1253 } 1254 log.debug("sbl: {}", sbl); 1255 // Time in ms from chuff start up to begin of the next chuff, limited to a minimum 1256 int interval = Math.max(calcChuffInterval(getRpm()), _parent.sleep_interval); 1257 int bbufcount = notch1.getBufferFrameSize() * ((interval) * notch1.getBufferFreq() / 1000); 1258 ByteBuffer bbuf = ByteBuffer.allocateDirect(bbufcount); // Target 1259 1260 if (interval > sbl) { 1261 // Regular queueing. Whole sound clip goes to the queue. Low notches 1262 // Prepare the sound and transfer it to the target ByteBuffer bbuf 1263 int bbufcount2 = notch1.getBufferFrameSize() * (sbl * notch1.getBufferFreq() / 1000); 1264 byte[] bbytes2 = new byte[bbufcount2]; 1265 data.get(bbytes2); // Same as: data.get(bbytes2, 0, bbufcount2); 1266 data.rewind(); 1267 1268 // chuff_fade_out 1269 doChuffFadeOut(bbufcount2, bbytes2); 1270 1271 bbuf.order(data.order()); // Set new buffer's byte order to match source buffer. 1272 bbuf.put(bbytes2); // Same as: bbuf.put(bbytes2, 0, bbufcount2); 1273 1274 // Handle filler for the remaining part of the AudioBuffer 1275 if (bbuf.hasRemaining()) { 1276 log.debug("remaining: {}", bbuf.remaining()); 1277 ByteBuffer dataf; 1278 if (is_key_coasting || is_auto_coasting) { 1279 dataf = notch1.getCoastFillerData(); 1280 } else { 1281 dataf = notch1.getNotchFillerData(); 1282 } 1283 if (dataf == null) { 1284 log.debug("No filler sound found"); 1285 // Nothing to do on 16-bit, because 0 is default for "silence"; 8-bit-mono needs 128, otherwise it's "noisy" 1286 if (notch1.getBufferFmt() == com.jogamp.openal.AL.AL_FORMAT_MONO8) { 1287 byte[] bbytesfiller = new byte[bbuf.remaining()]; 1288 for (int i = 0; i < bbytesfiller.length; i++) { 1289 bbytesfiller[i] = (byte) 0x80; // fill array with "silence" 1290 } 1291 bbuf.put(bbytesfiller); 1292 } 1293 } else { 1294 // Filler sound found 1295 log.debug("data limit: {}, remaining: {}", dataf.limit(), bbuf.remaining()); 1296 byte[] bbytesfiller2 = new byte[bbuf.remaining()]; 1297 if (dataf.limit() >= bbuf.remaining()) { 1298 dataf.get(bbytesfiller2); 1299 dataf.rewind(); 1300 bbuf.put(bbytesfiller2); 1301 } else { 1302 log.debug("not enough filler length"); 1303 byte[] bbytesfillerpart = new byte[dataf.limit()]; 1304 dataf.get(bbytesfillerpart); 1305 dataf.rewind(); 1306 int k = 0; 1307 for (int i = 0; i < bbytesfiller2.length; i++) { 1308 bbytesfiller2[i] = bbytesfillerpart[k]; 1309 k++; 1310 if (k == dataf.limit()) { 1311 k = 0; 1312 } 1313 } 1314 bbuf.put(bbytesfiller2); 1315 } 1316 } 1317 } 1318 } else { 1319 // Need to cut the SoundBite to new length of interval 1320 log.debug("need to cut sound clip from {} to length {}", sbl, interval); 1321 byte[] bbytes = new byte[bbufcount]; 1322 data.get(bbytes); // Same as: data.get(bbytes, 0, bbufcount); 1323 data.rewind(); 1324 1325 doChuffFadeOut(bbufcount, bbytes); 1326 1327 bbuf.order(data.order()); // Set new buffer's byte order to match source buffer 1328 bbuf.put(bbytes); // Same as: bbuf.put(bbytes, 0, bbufcount); 1329 } 1330 bbuf.rewind(); 1331 buf.loadBuffer(bbuf, notch1.getBufferFmt(), notch1.getBufferFreq()); 1332 _sound.queueBuffer(buf); 1333 log.debug("buffer queued. Length: {}", (int)SoundBite.calcLength(buf)); 1334 1335 // wait some loops to get up-to-date speed value 1336 setWait((interval - _parent.sleep_interval * _parent.wait_factor) / _parent.sleep_interval); 1337 if (getWait() < 3) { 1338 setWait(0); 1339 } 1340 } 1341 1342 private void doChuffFadeOut(int count, byte[] bbytes) { 1343 // applicable for 16-bit mono sounds only 1344 // (I don't have a solution for volume change on 8-bit sounds) 1345 if (is_chuff_fade_out) { 1346 chuff_fade_out_volume *= chuff_fade_out_factor; 1347 if (chuff_fade_out_volume < 0.15f) { // 0.07f 1348 is_chuff_fade_out = false; // done 1349 if (is_dynamic_gain) { 1350 dynamic_volume = 1.0f; 1351 setDynamicVolume(dynamic_volume); 1352 } 1353 } 1354 for (int i = 0; i < count; ++i) { 1355 bbytes[i] *= chuff_fade_out_volume; // make it quieter 1356 } 1357 } 1358 } 1359 1360 private void mute(boolean m) { 1361 _sound.mute(m); 1362 for (SoundBite ts : _parent.trigger_sounds.values()) { 1363 ts.mute(m); 1364 } 1365 } 1366 1367 // called by the LoopThread on volume changes with active dynamic_gain 1368 private void setDynamicVolume(float v) { 1369 if (_parent.getTunnel()) { 1370 v *= VSDSound.tunnel_volume; 1371 } 1372 1373 if (!_parent.getVsd().isMuted()) { 1374 // v * master_volume * decoder_volume, will be multiplied by gain in SoundBite 1375 // forward volume to SoundBite 1376 _sound.setVolume(v * VSDecoderManager.instance().getMasterVolume() * 0.01f * _parent.getVsd().getDecoderVolume()); 1377 } 1378 } 1379 1380 // triggered by VSDecoder via VSDSound on sound positioning, master or decoder slider changes 1381 // volume v is already multiplied by master_volume and decoder_volume 1382 private void setVolume(float v) { 1383 // handle engine sound (loop sound) 1384 if (! is_dynamic_gain) { 1385 _sound.setVolume(v); // special case on active dynamic_gain 1386 } 1387 // handle trigger sounds (e.g. idle) 1388 for (SoundBite ts : _parent.trigger_sounds.values()) { 1389 ts.setVolume(v); 1390 } 1391 } 1392 1393 private void setPosition(PhysicalLocation p) { 1394 _sound.setPosition(p); 1395 for (SoundBite ts : _parent.trigger_sounds.values()) { 1396 ts.setPosition(p); 1397 } 1398 } 1399 1400 @SuppressWarnings("hiding") // Field has same name as a field in the super class 1401 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(S1LoopThread.class); 1402 1403 } 1404}