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