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