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}