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