jmri.jmrit.vsdecoder.Steam1Sound.java Source code

Java tutorial

Introduction

Here is the source code for jmri.jmrit.vsdecoder.Steam1Sound.java

Source

package jmri.jmrit.vsdecoder;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.nio.ByteBuffer;
import jmri.Audio;
import jmri.AudioException;
import jmri.AudioManager;
import jmri.jmrit.audio.AudioBuffer;
import jmri.util.PhysicalLocation;
import org.jdom2.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <hr>
 * This file is part of JMRI.
 * <P>
 * JMRI is free software; you can redistribute it and/or modify it under 
 * the terms of version 2 of the GNU General Public License as published 
 * by the Free Software Foundation. See the "COPYING" file for a copy 
 * of this license.
 * <P>
 * JMRI is distributed in the hope that it will be useful, but WITHOUT 
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License 
 * for more details.
 * <P>
 *
 * @author Mark Underwood Copyright (C) 2011
 * @author Klaus Killinger Copyright (C) 2017, 2018
 */
class Steam1Sound extends EngineSound {

    // Engine Sounds
    HashMap<Integer, S1Notch> notch_sounds;
    String _soundName;

    // Trigger Sounds
    HashMap<String, SoundBite> trigger_sounds;

    int top_speed;
    int top_speed_reverse;
    private float driver_diameter_float;
    private int num_cylinders;
    private float exponent;
    private int accel_rate;
    private int decel_rate;
    private int brake_time;
    private int decel_trigger_rpms;
    private SoundBite idle_sound;
    private SoundBite boiling_sound;
    private SoundBite brake_sound;
    private SoundBite pre_arrival_sound;
    float engine_rd;
    float engine_gain;

    // Common variables
    boolean is_looping = false;
    S1LoopThread _loopThread = null;

    private javax.swing.Timer rpmTimer;
    int accdectime;

    // Constructor
    public Steam1Sound(String name) {
        super(name);
        log.debug("New Steam1Sound name(param): {}, name(val): {}", name, this.getName());
    }

    private void startThread() {
        _loopThread = new S1LoopThread(this, _soundName, top_speed, top_speed_reverse, driver_diameter_float,
                num_cylinders, decel_trigger_rpms, true);
        log.debug("Loop Thread Started.  Sound name: {}", _soundName);
    }

    @Override
    public void stop() {
        // Stop the loop thread, in case it's running
        if (_loopThread != null) {
            _loopThread.setRunning(false);
        }
        is_looping = false;
    }

    // Responds to "CHANGE" trigger (float)
    @Override
    public void changeThrottle(float s) {
        // This is all we have to do.  The loop thread will handle everything else
        if (_loopThread != null) {
            _loopThread.setThrottle(s);
        }
    }

    // Responds to throttle loco direction key (see EngineSound.java and EngineSoundEvent.java)
    @Override
    public void changeLocoDirection(int dirfn) {
        log.debug("loco IsForward is {}", dirfn);
        if (_loopThread != null) {
            _loopThread.getLocoDirection(dirfn);
        }
    }

    // Responds to throttle function key (see EngineSound.java and EngineSoundEvent.java)
    @Override
    public void functionKey(String event, boolean value, String name) {
        log.debug("throttle function key {} pressed for {}: {}", event, name, value);
        if (_loopThread != null) {
            _loopThread.setFunction(event, value, name);
        }
    }

    @Override
    double speedCurve(float t) {
        return Math.pow(t, exponent);
    }

    // Called from thread
    public S1Notch getNotch(int n) {
        return notch_sounds.get(n);
    }

    // Called from thread
    private void initAccDecTimer() {
        rpmTimer = newTimer(1, true, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (_loopThread != null) {
                    rpmTimer.setDelay(accdectime); // Update delay time
                    _loopThread.updateRpm();
                }
            }
        });
        log.debug("timer {} initialized, delay: {}", rpmTimer, accdectime);
    }

    // Called from thread
    void startAccDecTimer() {
        if (!rpmTimer.isRunning()) {
            rpmTimer.start();
            log.debug("timer {} started, delay: {}", rpmTimer, accdectime);
        }
    }

    // Called from thread
    void stopAccDecTimer() {
        if (rpmTimer.isRunning()) {
            rpmTimer.stop();
            log.debug("timer {} stopped, delay: {}", rpmTimer, accdectime);
        }
    }

    @Override
    public void startEngine() {
        log.debug("startEngine. ID = {}", this.getName());
        _loopThread.startEngine();
    }

    @Override
    public void stopEngine() {
        log.debug("stopEngine. ID = {}", this.getName());
        if (_loopThread != null) {
            _loopThread.stopEngine();
        }
    }

    @Override
    public void shutdown() {
        for (VSDSound vs : trigger_sounds.values()) {
            log.debug(" Stopping trigger sound: {}", vs.getName());
            vs.stop(); // SoundBite: Stop playing
        }
        if (rpmTimer != null) {
            stopAccDecTimer();
        }
        this.stop(); // Stop loop thread
    }

    @Override
    public void mute(boolean m) {
        if (_loopThread != null) {
            _loopThread.mute(m);
        }
    }

    @Override
    public void setVolume(float v) {
        if (_loopThread != null) {
            _loopThread.setVolume(v);
        }
    }

    @Override
    public void setPosition(PhysicalLocation p) {
        if (_loopThread != null) {
            _loopThread.setPosition(p);
        }
    }

    @Override
    public Element getXml() {
        Element me = new Element("sound");
        me.setAttribute("name", this.getName());
        me.setAttribute("type", "engine");
        // Do something, eventually...
        return me;
    }

    @Override
    public void setXml(Element e, VSDFile vf) {
        Element el;
        String fn, n;
        S1Notch sb;

        // Handle the common stuff
        super.setXml(e, vf);

        _soundName = this.getName();
        log.debug("Steam1: name: {}, soundName: {}", this.getName(), _soundName);

        // Required values
        top_speed = Integer.parseInt(e.getChildText("top-speed"));
        log.debug("top speed forward: {} MPH", top_speed);

        // Optional value
        // Steam locos can have different top speed reverse.
        n = e.getChildText("top-speed-reverse"); // Optional value.
        if (n != null) {
            top_speed_reverse = Integer.parseInt(n);
        } else {
            top_speed_reverse = top_speed; // Default for top_speed_reverse!
        }
        log.debug("top speed reverse: {} MPH", top_speed_reverse);

        driver_diameter_float = Float.parseFloat(e.getChildText("driver-diameter-float"));
        log.debug("driver diameter: {} inches", driver_diameter_float);
        num_cylinders = Integer.parseInt(e.getChildText("cylinders"));
        log.debug("Number of cylinders defined: {}", num_cylinders);

        // Optional value
        // Allows to adjust speed
        n = e.getChildText("exponent"); // Optional value
        if (n != null) {
            exponent = Float.parseFloat(n);
        } else {
            exponent = 1.0f; // Default
        }
        log.debug("exponent: {}", exponent);

        // Optional value
        // Acceleration and deceleration rate
        n = e.getChildText("accel-rate"); // Optional value
        if (n != null) {
            accel_rate = Integer.parseInt(n);
        } else {
            accel_rate = 35; // Default
        }
        log.debug("accel rate: {}", accel_rate);

        n = e.getChildText("decel-rate"); // Optional value
        if (n != null) {
            decel_rate = Integer.parseInt(n);
        } else {
            decel_rate = 18; // Default
        }
        log.debug("decel rate: {}", decel_rate);

        n = e.getChildText("brake-time"); // Optional value
        if (n != null) {
            brake_time = Integer.parseInt(n);
        } else {
            brake_time = 0; // Default
        }
        log.debug("brake time: {}", brake_time);

        // Optional value 
        // auto-start
        is_auto_start = setXMLAutoStart(e);
        log.debug("config auto-start: {}", is_auto_start);

        // Optional value
        // Allows to adjust OpenAL attenuation
        // Sounds with distance to listener position lower than reference-distance will not have attenuation
        engine_rd = setXMLReferenceDistance(e); // Handle reference distance
        log.debug("engine-sound referenceDistance: {}", engine_rd);

        // Optional value
        // Allows to adjust the engine gain
        n = e.getChildText("engine-gain");
        if ((n != null) && (!n.isEmpty())) {
            engine_gain = Float.parseFloat(n);
            // Make some restrictions, since engine_gain is used for calculations later
            if ((engine_gain < default_gain - 0.4f) || (engine_gain > default_gain + 0.2f)) {
                log.info("Invalid engine gain {} was set to default {}", engine_gain, default_gain);
                engine_gain = default_gain;
            }
        } else {
            engine_gain = default_gain;
        }
        log.debug("engine gain: {}", engine_gain);

        // Optional value
        // Defines how many rpms in 0.5 seconds will trigger decel actions like braking
        n = e.getChildText("decel-trigger-rpms");
        if (n != null) {
            decel_trigger_rpms = Integer.parseInt(n);
        } else {
            decel_trigger_rpms = 999; // Default (need a value)
        }
        log.debug("number of rpms to trigger decelerating actions: {}", decel_trigger_rpms);

        // Get the sounds.
        // Note: each sound must have equal attributes, e.g. 16-bit, 44100 Hz
        // Get the files and create a buffer and byteBuffer for each file
        // For each notch there must be <num_cylinders * 2> chuff files
        notch_sounds = new HashMap<>();
        int i = 0; // notch number (index)
        int j = 0; // chuff or coast number (index)
        int fmt = 0; // Sound sample format
        int nn = 1; // notch number (visual)

        // Get the notch-sounds.
        Iterator<Element> itr = (e.getChildren("s1notch-sound")).iterator();
        while (itr.hasNext()) {
            el = itr.next();
            sb = new S1Notch();
            sb.setNotch(nn);

            // Get the chuff sounds
            List<Element> elist = el.getChildren("notch-file");
            j = 0;
            for (Element fe : elist) {
                fn = fe.getText();
                log.debug("notch: {}, file: {}", nn, fn);
                AudioBuffer b = S1Notch.getBuffer(vf, fn, _soundName + "_NOTCH_" + i + "_" + j,
                        _soundName + "_NOTCH_" + i + "_" + j);
                log.debug("buffer created: {}, name: {}, format: {}", b, b.getSystemName(), b.getFormat());
                sb.addChuffBuffer(b);
                if (fmt == 0) {
                    // Get the format of the (first) WAV file
                    // Since all WAV files of the notches MUST have the same format,
                    // I asume this format for all WAV files for now
                    fmt = AudioUtil.getWavFormat(S1Notch.getWavStream(vf, fn));
                    log.debug("fmt: {}", fmt);
                }
                ByteBuffer data = AudioUtil.getWavData(S1Notch.getWavStream(vf, fn));
                sb.addChuffData(data);
                j++;
            }
            log.debug("Number of chuff sounds for notch {} defined: {}", nn, j);

            // Create a filler Buffer for queueing and a ByteBuffer for length modification
            fn = el.getChildText("notchfiller-file");
            if (fn != null) {
                log.debug("notch filler file: {}", fn);
                AudioBuffer bnf = S1Notch.getBuffer(vf, el.getChildText("notchfiller-file"),
                        _soundName + "_NOTCHFILLER_" + i, _soundName + "_NOTCHFILLER_" + i);
                log.debug("buffer created: {}, name: {}, format: {}", bnf, bnf.getSystemName(), bnf.getFormat());
                sb.setNotchFillerBuffer(bnf);
                sb.setNotchFillerData(AudioUtil.getWavData(S1Notch.getWavStream(vf, fn)));
            } else {
                log.debug("no notchfiller available.");
                sb.setNotchFillerBuffer(null);
            }

            // Coasting sound and helpers are bound to first notch only
            // VSDFile validation makes sure that there is at least one notch
            if (nn == 1) {
                // Get the coasting sounds
                j = 0;
                List<Element> elistc = el.getChildren("coast-file");
                for (Element fe : elistc) {
                    fn = fe.getText();
                    log.debug("coasting file: {}", fn);
                    AudioBuffer bc = S1Notch.getBuffer(vf, fn, _soundName + "_COAST_" + j,
                            _soundName + "_COAST_" + j);
                    log.debug("buffer created: {}, name: {}, format: {}", bc, bc.getSystemName(), bc.getFormat());
                    sb.addCoastBuffer(bc); // WAV in Buffer for queueing
                    ByteBuffer datac = AudioUtil.getWavData(S1Notch.getWavStream(vf, fn));
                    sb.addCoastData(datac); // WAV data in ByteBuffer for length modification
                    j++;
                }
                log.debug("Number of coasting sounds for notch {} defined: {}", nn, j);

                // Create a filler Buffer for queueing and a ByteBuffer for length modification
                fn = el.getChildText("coastfiller-file");
                if (fn != null) {
                    log.debug("coasting filler file: {}", fn);
                    AudioBuffer bcf = S1Notch.getBuffer(vf, fn, _soundName + "_COASTFILLER",
                            _soundName + "_COASTFILLER");
                    log.debug("buffer created: {}, name: {}, format: {}", bcf, bcf.getSystemName(),
                            bcf.getFormat());
                    sb.setCoastFillerBuffer(bcf);
                    sb.setCoastFillerData(AudioUtil.getWavData(S1Notch.getWavStream(vf, fn)));
                } else {
                    log.debug("no coastfiller available.");
                    sb.setCoastFillerBuffer(null);
                }

                // Add some helper Buffers. They are needed for creating
                // variable sound clips in length. Ten helper buffers should
                // serve well for that purpose. These buffers are bound to notch 1
                for (int jk = 0; jk < 10; jk++) {
                    AudioBuffer bh = S1Notch.getBufferHelper(_soundName + "_BUFFERHELPER_" + jk,
                            _soundName + "_BUFFERHELPER_" + jk);
                    log.debug("buffer helper created: {}, name: {}", bh, bh.getSystemName());
                    sb.addHelper(bh);
                }
            }

            sb.setMinLimit(Integer.parseInt(el.getChildText("min-rpm")));
            sb.setMaxLimit(Integer.parseInt(el.getChildText("max-rpm")));
            sb.setBufferFmt(fmt);
            log.debug("sample format for notch {}: {}", nn, fmt);

            // Store in the list
            notch_sounds.put(nn, sb);
            i++;
            nn++;
        }
        log.debug("Number of notches defined: {}", notch_sounds.size());

        // Get the trigger sounds
        // Note: other than notch sounds, trigger sounds can have different attributes
        trigger_sounds = new HashMap<>();

        // Get the idle sound
        el = e.getChild("idle-sound");
        if (el != null) {
            fn = el.getChild("sound-file").getValue();
            log.debug("idle sound: {}", fn);
            idle_sound = new SoundBite(vf, fn, _soundName + "_IDLE", _soundName + "_Idle");
            idle_sound.setGain(setXMLGain(el)); // Handle gain
            log.debug("idle sound gain: {}", idle_sound.getGain());
            idle_sound.setLooped(true);
            idle_sound.setFadeTimes(500, 500);
            idle_sound.setReferenceDistance(setXMLReferenceDistance(el)); // Handle reference distance
            log.debug("idle-sound reference distance: {}", idle_sound.getReferenceDistance());
            trigger_sounds.put("idle", idle_sound);
            log.debug("trigger idle sound: {}", trigger_sounds.get("idle"));
        }

        // Get the boiling sound
        el = e.getChild("boiling-sound");
        if (el != null) {
            fn = el.getChild("sound-file").getValue();
            log.debug("boiling-sound: {}", fn);
            boiling_sound = new SoundBite(vf, fn, name + "_BOILING", name + "_Boiling");
            boiling_sound.setGain(setXMLGain(el)); // Handle gain
            log.debug("boiling-sound gain: {}", boiling_sound.getGain());
            boiling_sound.setLooped(true);
            boiling_sound.setFadeTimes(500, 500);
            boiling_sound.setReferenceDistance(setXMLReferenceDistance(el));
            log.debug("boiling-sound reference distance: {}", boiling_sound.getReferenceDistance());
            trigger_sounds.put("boiling", boiling_sound);
            log.debug("trigger boiling sound: {}", trigger_sounds.get("boiling"));
        }

        // Get the brake sound
        el = e.getChild("brake-sound");
        if (el != null) {
            fn = el.getChild("sound-file").getValue();
            log.debug("brake sound: {}", fn);
            brake_sound = new SoundBite(vf, fn, _soundName + "_BRAKE", _soundName + "_Brake");
            brake_sound.setGain(setXMLGain(el));
            log.debug("brake sound gain: {}", brake_sound.getGain());
            brake_sound.setLooped(false);
            brake_sound.setFadeTimes(500, 500);
            brake_sound.setReferenceDistance(setXMLReferenceDistance(el));
            log.debug("brake-sound reference distance: {}", brake_sound.getReferenceDistance());
            trigger_sounds.put("brake", brake_sound);
            log.debug("trigger brake sound: {}", trigger_sounds.get("brake"));
        }

        // Get the pre-arrival sound
        el = e.getChild("pre-arrival-sound");
        if (el != null) {
            fn = el.getChild("sound-file").getValue();
            log.debug("pre-arrival sound: {}", fn);
            pre_arrival_sound = new SoundBite(vf, fn, _soundName + "_PRE-ARRIVAL", _soundName + "_Pre-arrival");
            pre_arrival_sound.setGain(setXMLGain(el));
            log.debug("pre-arrival sound gain: {}", pre_arrival_sound.getGain());
            pre_arrival_sound.setLooped(false);
            pre_arrival_sound.setFadeTimes(500, 500);
            pre_arrival_sound.setReferenceDistance(setXMLReferenceDistance(el));
            log.debug("pre-arrival-sound reference distance: {}", pre_arrival_sound.getReferenceDistance());
            trigger_sounds.put("pre_arrival", pre_arrival_sound);
            log.debug("trigger pre_arrival sound : {}", trigger_sounds.get("pre_arrival"));
        }

        // Kick-start the loop thread
        this.startThread();

        // Check auto-start setting
        autoStartCheck();
    }

    private static final Logger log = LoggerFactory.getLogger(Steam1Sound.class);

    private static class S1Notch {

        private AudioBuffer filler_bufn;
        private AudioBuffer filler_bufc;
        private int my_notch;
        private int min_rpm, max_rpm;
        private int buffer_fmt;
        private ByteBuffer notchfiller_data;
        private ByteBuffer coastfiller_data;
        private List<AudioBuffer> chuff_bufs = new ArrayList<>();
        private List<AudioBuffer> bufs_helper = new ArrayList<>();
        private List<ByteBuffer> chuff_bufs_data = new ArrayList<>();
        private List<AudioBuffer> coast_bufs = new ArrayList<>();
        private List<ByteBuffer> coast_bufs_data = new ArrayList<>();

        public S1Notch() {
            this(1, null, null, null, null);
        }

        public S1Notch(int notch, AudioBuffer notchfiller, AudioBuffer coastfiller, List<AudioBuffer> chuff,
                List<AudioBuffer> coast) {
            my_notch = notch;
            filler_bufn = notchfiller;
            filler_bufc = coastfiller;
            if (chuff != null) {
                chuff_bufs = chuff;
            }
            if (coast != null) {
                coast_bufs = coast;
            }
        }

        public int getNotch() {
            return my_notch;
        }

        public int getMaxLimit() {
            return max_rpm;
        }

        public int getMinLimit() {
            return min_rpm;
        }

        public void setNotch(int n) {
            my_notch = n;
        }

        public void setMinLimit(int l) {
            min_rpm = l;
        }

        public void setMaxLimit(int l) {
            max_rpm = l;
        }

        public Boolean isInLimits(int val) {
            return val >= min_rpm && val <= max_rpm;
        }

        public void setBufferFmt(int fmt) {
            buffer_fmt = fmt;
        }

        public int getBufferFmt() {
            return buffer_fmt;
        }

        public void setNotchFillerBuffer(AudioBuffer b) {
            filler_bufn = b;
        }

        public AudioBuffer getNotchFillerBuffer() {
            return filler_bufn;
        }

        public void setCoastFillerBuffer(AudioBuffer b) {
            filler_bufc = b;
        }

        public AudioBuffer getCoastFillerBuffer() {
            return filler_bufc;
        }

        public void setNotchFillerData(ByteBuffer dat) {
            notchfiller_data = dat;
        }

        public ByteBuffer getNotchFillerData() {
            return notchfiller_data;
        }

        public void setCoastFillerData(ByteBuffer dat) {
            coastfiller_data = dat;
        }

        public ByteBuffer getCoastFillerData() {
            return coastfiller_data;
        }

        public void addChuffBuffer(AudioBuffer b) {
            chuff_bufs.add(b);
        }

        public void addChuffData(ByteBuffer dat) {
            chuff_bufs_data.add(dat);
        }

        public void addCoastBuffer(AudioBuffer b) {
            coast_bufs.add(b);
        }

        public void addCoastData(ByteBuffer dat) {
            coast_bufs_data.add(dat);
        }

        public void addHelper(AudioBuffer b) {
            bufs_helper.add(b);
        }

        static public AudioBuffer getBuffer(VSDFile vf, String filename, String sname, String uname) {
            AudioBuffer b = null;
            AudioManager am = jmri.InstanceManager.getDefault(jmri.AudioManager.class);
            try {
                b = (AudioBuffer) am.provideAudio(VSDSound.BufSysNamePrefix + sname);
                b.setUserName(VSDSound.BufUserNamePrefix + uname);
                if (vf == null) {
                    log.warn("No VSD File");
                    return null;
                } else {
                    java.io.InputStream ins = vf.getInputStream(filename);
                    if (ins != null) {
                        b.setInputStream(ins);
                    } else {
                        log.warn("input Stream failed for {}", filename);
                        return null;
                    }
                }
            } catch (AudioException | IllegalArgumentException ex) {
                log.warn("problem creating SoundBite", ex);
                return null;
            }

            log.debug("buffer created: {}, name: {}, format: {}", b, b.getSystemName(), b.getFormat());
            return b;
        }

        static public AudioBuffer getBufferHelper(String sname, String uname) {
            AudioBuffer bf = null;
            AudioManager am = jmri.InstanceManager.getDefault(jmri.AudioManager.class);
            try {
                bf = (AudioBuffer) am.provideAudio(VSDSound.BufSysNamePrefix + sname);
                bf.setUserName(VSDSound.BufUserNamePrefix + uname);
            } catch (AudioException | IllegalArgumentException ex) {
                log.warn("problem creating SoundBite", ex);
                return null;
            }
            log.debug("empty buffer created: {}, name: {}", bf, bf.getSystemName());
            return bf;
        }

        static public java.io.InputStream getWavStream(VSDFile vf, String filename) {
            java.io.InputStream ins = vf.getInputStream(filename);
            if (ins != null) {
                return ins;
            } else {
                log.warn("input Stream failed for {}", filename);
                return null;
            }
        }

        private static final Logger log = LoggerFactory.getLogger(S1Notch.class);
    }

    private static class S1LoopThread extends Thread {

        Steam1Sound _parent;
        S1Notch _notch;
        S1Notch coast_notch; // Needed for coasting sounds and buffers
        S1Notch helper_notch; // Needed for helper buffers
        SoundBite _sound;
        float _throttle;

        private boolean is_running = false;
        private boolean is_looping = false;
        private boolean is_dying = false;
        private boolean is_auto_coasting;
        private boolean is_key_coasting;
        private boolean is_idling;
        private boolean is_braking;
        private int lastRpm;
        private long timeOfLastSpeedCheck;
        private int chuff_index;
        private int helper_index;
        private boolean waitForFiller;
        private boolean is_half_speed;
        private int rpm_nominal; // Nominal value
        private int rpm; // Actual value
        private int topspeed;
        private int _top_speed;
        private int _top_speed_reverse;
        private float _driver_diameter_float;
        private int _num_cylinders;
        private int _decel_trigger_rpms;
        private int acc_time;
        private int dec_time;
        private int count_pre_arrival;
        private int queue_limit;
        private int waitFiller;
        private int sbl_fill;
        private int wait_notch;
        public static final int SLEEP_INTERVAL = 50;

        public S1LoopThread(Steam1Sound d, String s, int ts, int tsr, float dd, int nc, int dtr, boolean r) {
            super();
            _parent = d;
            _top_speed = ts;
            _top_speed_reverse = tsr;
            _driver_diameter_float = dd;
            _num_cylinders = nc;
            _decel_trigger_rpms = dtr;
            is_running = r;
            is_looping = false;
            is_dying = false;
            is_auto_coasting = false;
            is_key_coasting = false;
            is_idling = false;
            is_braking = false;
            waitForFiller = false;
            lastRpm = 0;
            timeOfLastSpeedCheck = 0;
            _throttle = 0.0f;
            _notch = null;
            coast_notch = null;
            helper_notch = null;
            _sound = new SoundBite(s + "_QUEUE"); // Sound for queueing
            _sound.setGain(_parent.engine_gain); // All chuff sounds will have this gain
            count_pre_arrival = 1;
            queue_limit = 2;
            waitFiller = 0;
            sbl_fill = 0;
            wait_notch = 1;
            if (r) {
                this.start();
            }
        }

        public void setRunning(boolean r) {
            is_running = r;
        }

        public void setThrottle(float t) {
            // Don't do anything, if engine is not started
            // Another required value is a S1Notch (should have been set at engine start)
            if (_parent.engine_started) {
                if (t < 0.0f) {
                    // DO something to shut down
                    _sound.stop();
                    is_running = false;
                    log.info("emergency Stop");
                    //return; // klk: This will tear down VSD
                    // probably should do something. Not sure what
                    stopBraking();
                    stopAutoCoasting();
                    stopIdling();
                    _throttle = 0.0f;
                    log.info("Throttle set to {}", _throttle);
                } else {
                    _throttle = t;
                }

                if (is_half_speed) {
                    _throttle = _throttle / 2;
                }
                // Calculate the nominal speed (Revolutions Per Minute)
                setRpmNominal(calcRPM(_throttle));

                // Speeding up or slowing down?
                if (getRpmNominal() < lastRpm) {
                    //
                    // Slowing down.
                    //
                    _parent.accdectime = dec_time;
                    log.debug("decelerate from {} to {}", lastRpm, getRpmNominal());

                    if ((getRpmNominal() < 23) && is_auto_coasting && (count_pre_arrival > 0)
                            && _parent.trigger_sounds.containsKey("pre_arrival") && (dec_time < 250)) {
                        _parent.trigger_sounds.get("pre_arrival").fadeIn();
                        count_pre_arrival--;
                    }

                    // Calculate how long it's been since we lastly checked speed
                    long currentTime = System.currentTimeMillis();
                    float timePassed = currentTime - timeOfLastSpeedCheck;
                    timeOfLastSpeedCheck = currentTime;
                    // Prove the trigger for decelerating actions (braking, coasting)
                    if (((lastRpm - getRpmNominal()) > _decel_trigger_rpms) && (timePassed < 500.0f)) {
                        log.debug("Time passed {}", timePassed);
                        if ((getRpmNominal() < 30) && (dec_time < 250)) { // Braking sound only when speed is low (, but not to low)
                            if (_parent.trigger_sounds.containsKey("brake")) {
                                _parent.trigger_sounds.get("brake").fadeIn();
                                is_braking = true;
                                log.debug("braking activ!");
                            }
                        } else if (coast_notch.coast_bufs.size() > 0 && !is_key_coasting) {
                            is_auto_coasting = true;
                            log.debug("auto-coasting active");
                        }
                    }
                } else {
                    //
                    // Speeding up.
                    //
                    _parent.accdectime = acc_time;
                    log.debug("accelerate from {} to {}", lastRpm, getRpmNominal());
                    if (is_braking) {
                        stopBraking(); // Revoke possible brake sound
                    }
                    if (is_auto_coasting) {
                        stopAutoCoasting(); // This makes chuff sound hearable again
                    }
                }
                _parent.startAccDecTimer(); // Start, if not already running
                lastRpm = getRpmNominal();
            }
        }

        private void stopBraking() {
            if (is_braking) {
                if (_parent.trigger_sounds.containsKey("brake")) {
                    _parent.trigger_sounds.get("brake").fadeOut();
                    is_braking = false;
                    log.debug("braking sound stopped.");
                }
            }
        }

        private void startBoilingSound() {
            if (_parent.trigger_sounds.containsKey("boiling")) {
                _parent.trigger_sounds.get("boiling").setLooped(true);
                _parent.trigger_sounds.get("boiling").play();
                log.debug("boiling sound playing");
            }
        }

        private void stopBoilingSound() {
            if (_parent.trigger_sounds.containsKey("boiling")) {
                _parent.trigger_sounds.get("boiling").setLooped(false);
                _parent.trigger_sounds.get("boiling").fadeOut();
                log.debug("boiling sound stopped.");
            }
        }

        private void stopAutoCoasting() {
            if (is_auto_coasting) {
                is_auto_coasting = false;
                log.debug("auto-coasting sound stopped.");
            }
        }

        private void getLocoDirection(int d) {
            // If loco direction was changed we need to set topspeed of the loco to new value 
            // (this is necessary, when topspeed-forward and topspeed-reverse differs)
            if (d == 1) { // loco is going forward
                topspeed = _top_speed;
            } else {
                topspeed = _top_speed_reverse;
            }
            log.debug("loco direction: {}, top speed: {}", d, topspeed);
            // Re-calculate accel-time and decel-time, hence topspeed may have changed
            acc_time = calcAccDecTime(_parent.accel_rate);
            dec_time = calcAccDecTime(_parent.decel_rate);
        }

        private void setFunction(String event, boolean is_true, String name) {
            // This throttle function key handling differs to configurable sounds:
            // Do something following certain conditions, when a throttle function key is pressed.
            // Note: throttle will send initial value(s) before thread is started! 
            log.debug("throttle function key pressed: {} is {}, function: {}", event, is_true, name);
            if (name.equals("COAST")) {
                // Handle key-coasting on/off.
                log.debug("COAST key pressed");
                // Set coasting TRUE, if COAST key is pressed. Requires sufficient coasting sounds (chuff_index will rely on that).
                if (coast_notch == null) {
                    coast_notch = _parent.getNotch(1); // Because of initial send of throttle key, COAST function key could be "true"
                }
                if (is_true && coast_notch.coast_bufs.size() > 0) {
                    is_key_coasting = true; // When idling is active, key-coasting will start after it.
                } else {
                    is_key_coasting = false; // Stop the key-coasting sound
                }
                log.debug("is COAST: {}", is_key_coasting);
            }

            // Speed change if HALF_SPEED key is pressed
            if (name.equals("HALF_SPEED")) {
                log.debug("HALF_SPEED key pressed is {}", is_true);
                if (_parent.engine_started) {
                    if (is_true) {
                        is_half_speed = true;
                    } else {
                        is_half_speed = false;
                    }
                }
            }

            // Set Accel/Decel off or to lower value
            if (name.equals("BRAKE_KEY")) {
                log.debug("BRAKE_KEY pressed is {}", is_true);
                if (_parent.engine_started) {
                    if (is_true) {
                        if (_parent.brake_time == 0) {
                            acc_time = 0;
                            dec_time = 0;
                        } else {
                            dec_time = calcAccDecTime(_parent.brake_time);
                        }
                        _parent.accdectime = dec_time;
                        log.debug("accdectime: {}", _parent.accdectime);
                    } else {
                        acc_time = calcAccDecTime(_parent.accel_rate);
                        dec_time = calcAccDecTime(_parent.decel_rate);
                        _parent.accdectime = dec_time;
                    }
                }
            }
            // Other throttle function keys may follow ...
        }

        private void startEngine() {
            _sound.unqueueBuffers();
            log.debug("thread: start engine ...");
            coast_notch = _parent.getNotch(1); // Coast sounds are bound to notch 1
            helper_notch = _parent.getNotch(1); // Helper buffers are bound to notch 1
            _notch = _parent.getNotch(1); // Initial value
            _parent.engine_pane.setThrottle(1); // Set EnginePane (DieselPane) notch
            _sound.setReferenceDistance(_parent.engine_rd);
            setRpm(0);
            setRpmNominal(0);
            helper_index = -1; // Prepare helper buffer start. Index will be incremented before first use
            setWait(0);
            startBoilingSound();
            startIdling();
            acc_time = calcAccDecTime(_parent.accel_rate); // Calculate acceleration time
            dec_time = calcAccDecTime(_parent.decel_rate); // Calculate deceleration time
            _parent.initAccDecTimer();
        }

        private void stopEngine() {
            log.debug("thread: stop engine ...");
            if (is_looping) {
                is_looping = false; // Stop the loop player
            }
            is_dying = true;
            stopBraking();
            stopAutoCoasting();
            stopBoilingSound();
            stopIdling();
            _parent.stopAccDecTimer();
            _throttle = 0.0f; // Clear it, just in case the engine was stopped at speed > 0
        }

        private int calcAccDecTime(int accdec_rate) {
            // Handle Momentum
            // Regard topspeed, which may be different on forward or reverse direction
            int topspeed_rpm = (int) Math.round(topspeed * 1056 / (Math.PI * _driver_diameter_float));
            return 896 * accdec_rate / topspeed_rpm; // NMRA value 896 in ms
        }

        private void startIdling() {
            is_idling = true;
            if (_parent.trigger_sounds.containsKey("idle")) {
                _parent.trigger_sounds.get("idle").setLooped(true);
                if (!_parent.trigger_sounds.get("idle").isPlaying()) {
                    _parent.trigger_sounds.get("idle").play();
                }
            }
            log.debug("start idling ...");
        }

        private void stopIdling() {
            if (is_idling) {
                is_idling = false;
                if (_parent.trigger_sounds.containsKey("idle")) {
                    _parent.trigger_sounds.get("idle").fadeOut();
                    log.debug("idling stopped.");
                }
            }
        }

        //
        //   LOOP-PLAYER
        //
        @Override
        public void run() {
            try {
                while (is_running) {
                    if (is_looping) {
                        if (_sound.getSource().numProcessedBuffers() > 0) {
                            _sound.unqueueBuffers();
                        }
                        log.debug("run loop. Buffers queued: {}", _sound.getSource().numQueuedBuffers());
                        if ((_sound.getSource().numQueuedBuffers() < queue_limit) && (getWait() == 0)) {
                            AudioBuffer b;
                            if (is_key_coasting || is_auto_coasting) {
                                // Take the coasting sound. Yes, use same index as for chuffs
                                b = coast_notch.coast_bufs.get(incChuffIndex());
                            } else {
                                // Take the standard chuff sound
                                b = _notch.chuff_bufs.get(incChuffIndex());
                            }
                            setSound(b); // Queue the sound and, if necessary, a filler sound
                        }
                        if (_sound.getSource().getState() != Audio.STATE_PLAYING) {
                            _sound.play(); // Starts the Sound. Maybe also re-starts the sound
                            if (getRpm() > _parent.getNotch(1).getMinLimit()) {
                                log.info("loop sound re-started. Possibly queue underrun at rpm: {}", getRpm());
                            }
                        }
                    } else {
                        // Quietly wait for the sound to get turned on again
                        // Once we've stopped playing, kill the thread
                        if (_sound.getSource().numProcessedBuffers() > 0) {
                            _sound.unqueueBuffers();
                        }
                        if (is_dying && (_sound.getSource().getState() != Audio.STATE_PLAYING)) {
                            _sound.stop(); // good reason to get rid of SoundBite.is_playing variable!
                            //return;
                        }
                    }
                    sleep(SLEEP_INTERVAL);
                    updateWait();
                }
                // Note: if (is_running == false) we'll exit the endless while and the Thread will die
                return;
            } catch (InterruptedException ie) {
                is_running = false;
                return;
                // probably should do something. Not sure what
            }
        }

        private void changeNotch() {
            int new_notch = _notch.getNotch();
            log.debug("changing notch ... rpm: {}, notch: {}, chuff index: {}", getRpm(), _notch.getNotch(),
                    chuffIndex());
            if ((getRpm() > _notch.getMaxLimit()) && (new_notch < _parent.notch_sounds.size())) {
                // Too fast. Need to go to next notch up
                new_notch++;
                log.debug("change up. notch: {}", new_notch);
                _notch = _parent.getNotch(new_notch);
            } else if ((getRpm() < _notch.getMinLimit()) && (new_notch > 1)) {
                // Too slow.  Need to go to next notch down
                new_notch--;
                log.debug("change down. notch: {}", new_notch);
                _notch = _parent.getNotch(new_notch);
            }
            _parent.engine_pane.setThrottle(new_notch); // Update EnginePane (DieselPane) notch
            return;
        }

        private int getRpm() {
            return rpm; // Actual Revolution per Minute
        }

        private void setRpm(int r) {
            rpm = r;
        }

        private int getRpmNominal() {
            return rpm_nominal; // Nominal Revolution per Minute
        }

        private void setRpmNominal(int rn) {
            rpm_nominal = rn;
        }

        private void updateRpm() {
            if (getRpmNominal() > getRpm()) {
                // Actual rpm should not exceed highest max-rpm defined in config.xml
                if (getRpm() < _parent.getNotch(_parent.notch_sounds.size()).getMaxLimit()) {
                    setRpm(getRpm() + 1);
                } else {
                    log.debug("actual rpm not increased. Value: {}", getRpm());
                }
                log.debug("accel - nominal RPM: {}, actual RPM: {}", getRpmNominal(), getRpm());
            } else if (getRpmNominal() < getRpm()) {
                setRpm(getRpm() - 1);
                if (getRpm() < 0) {
                    setRpm(0);
                }
                log.debug("decel - nominal RPM: {}, actual RPM: {}", getRpmNominal(), getRpm());
            } else {
                _parent.stopAccDecTimer(); // Speed is unchanged, nothing to do
            }

            // Start or Stop the LOOP-PLAYER
            checkState();

            // Are we in the right notch?
            if ((getRpm() >= _parent.getNotch(1).getMinLimit()) && (!_notch.isInLimits(getRpm()))) {
                log.debug("Notch change! Notch: {}, RPM nominal: {}, RPM actual: {}", _notch.getNotch(),
                        getRpmNominal(), getRpm());
                changeNotch();
            }
        }

        private void checkState() {
            if (is_looping) {
                if (getRpm() < _parent.getNotch(1).getMinLimit()) {
                    is_looping = false; // Stop the loop player
                    setWait(0);
                    log.debug("change from chuff or coast to idle.");
                    stopAutoCoasting(); // Automatic coasting is stopped here
                    stopBraking();
                    startIdling();
                }
            } else {
                if (_parent.engine_started && (getRpm() >= _parent.getNotch(1).getMinLimit())) {
                    stopIdling();
                    // Now prepare to start the chuff sound (or coasting sound)
                    _notch = _parent.getNotch(1); // Initial notch value
                    chuff_index = -1; // Index will be incremented before first usage
                    count_pre_arrival = 1;
                    is_looping = true; // Start the loop player
                }
            }

            if (getRpm() > 62) {
                queue_limit = 3; // Allow more buffers to be queued at higher speed
            } else {
                queue_limit = 2;
            }
        }

        private void updateWait() {
            if (getWait() > 0) {
                setWait(getWait() - 1);
            }
        }

        private void setWait(int wait) {
            waitFiller = wait;
        }

        private int getWait() {
            return waitFiller;
        }

        // Place next two methods here (other than in Diesel3Sound)
        // I want the chuffs in 1-2-3-4 series regardless the notch
        private int chuffIndex() {
            return chuff_index;
        }

        private int incChuffIndex() {
            chuff_index++;
            // Correct for wrap.
            if (chuff_index >= (_num_cylinders * 2)) {
                chuff_index = 0;
            }
            log.debug("new chuff index: {}", chuffIndex());
            return chuff_index;
        }

        private int incHelperIndex() {
            helper_index++;
            // Correct for wrap.
            if (helper_index >= helper_notch.bufs_helper.size()) {
                helper_index = 0;
            }
            return helper_index;
        }

        private int calcRPM(float t) {
            // speed = % of topspeed (mph)
            // RPM = speed * ((inches/mile) / (minutes/hour)) / (pi * driver_diameter_float)
            return (int) Math.round(_parent.speedCurve(t) * topspeed * 1056 / (Math.PI * _driver_diameter_float));
        }

        private int calcChuffInterval(int revpm) {
            //  chuff interval will be calculated based on revolutions per minute (revpm)
            //  note: interval time includes the sound duration!
            //  chuffInterval = time in msec per revolution of the driver wheel: 
            //      60,000 msec / revpm / number of cylinders / 2 (because cylinders are double-acting)
            return (int) Math.round(60000.0 / revpm / _num_cylinders / 2.0);
        }

        private void setSound(AudioBuffer b) {
            int interval = calcChuffInterval(getRpm()); // Time in msec from chuff start up to begin of next chuff
            int sbl = (int) SoundBite.calcLength(b); // Length of WAV file in msec
            // Look if a filler is pending
            if (waitForFiller) {
                // Filler waiting time is over. Go to queue the filler sound now
                //  Only do this if we are in the same notch
                if (wait_notch == _notch.getNotch()) {
                    setFiller(sbl_fill);
                    log.debug("wait filler set");
                }
                waitForFiller = false; // Done.
            }

            if (interval >= sbl) {
                // Regular queueing. Whole sound clip goes to the queue. Low notches
                _sound.queueBuffer(b);
                log.debug("chuff or coast buffer queued. Interval: {}", interval);
                setWait((sbl - SLEEP_INTERVAL * 4) / SLEEP_INTERVAL);
                if (getWait() < 3) {
                    setWait(0);
                } else {
                    sbl_fill = sbl; // Base for filler length calculation
                    waitForFiller = true;
                }
            } else {
                // Need to cut the SoundBite to new length of interval
                // To avoid queue underrun, interval should be a bit longer than SLEEP_INTERVAL
                // SLEEP_INTERVAL + 10 should be lower than interval of max_rpm (e.g. max_rpm=214 -> interval=70)
                if (interval > (SLEEP_INTERVAL + 10)) {
                    log.debug("need to cut sound clip from {} to length {}", (int) SoundBite.calcLength(b),
                            interval);
                    setWait((interval - SLEEP_INTERVAL * 8) / SLEEP_INTERVAL);
                    if (getWait() < 4) {
                        setWait(0);
                    }
                    // Take <interval> ms of the buffer. Regard sample size
                    int bbufcount = b.getFrameSize() * (interval * b.getFrequency() / 1000);
                    // Empty buffer (bound to the coast notch, notch = 1)
                    AudioBuffer buf = helper_notch.bufs_helper.get(incHelperIndex());
                    byte[] bbytes = new byte[bbufcount];
                    ByteBuffer data;
                    if (is_key_coasting || is_auto_coasting) {
                        // Take coasting sound (WAV data)
                        data = coast_notch.coast_bufs_data.get(chuffIndex());
                    } else {
                        // Take chuff sound (WAV data)
                        data = _notch.chuff_bufs_data.get(chuffIndex());
                    }
                    data.get(bbytes); // Same as: data.get(bbytes, 0, bbufcount);
                    data.rewind();
                    ByteBuffer bbuf = ByteBuffer.allocateDirect(bbufcount); // Target
                    bbuf.order(data.order()); // Set new buffer's byte order to match source buffer
                    bbuf.put(bbytes); // Same as: bbuf.put(bbytes, 0, bbufcount);
                    bbuf.rewind();
                    buf.loadBuffer(bbuf, _notch.getBufferFmt(), b.getFrequency());
                    _sound.queueBuffer(buf);
                    log.debug("cut buffer queued. Length: {}", (int) SoundBite.calcLength(buf));
                    // No filler needed here.
                }
            }
        }

        private void setFiller(int lenx) {
            // Fills time after a chuff up to the next chuff with a sound provided by the VSD file
            // Since the filler can be a small amount of time, it might be queued several times
            AudioBuffer fill_buf;
            if (is_key_coasting || is_auto_coasting) {
                fill_buf = coast_notch.getCoastFillerBuffer();
            } else {
                fill_buf = _notch.getNotchFillerBuffer();
            }
            if (fill_buf != null) {
                int filler_length = (int) SoundBite.calcLength(fill_buf);
                int interv_wo_chuff = calcChuffInterval(getRpm()) - lenx;
                log.debug("filler length: {}, sound clip length: {}, interval: {}", filler_length, lenx,
                        calcChuffInterval(getRpm()));
                int im = interv_wo_chuff / filler_length; // How many fill_buf do we need?
                int imrest = interv_wo_chuff - (im * filler_length); // Calculate rest
                log.debug("interval without sound clip: {}, #buffers needed: {}, rest: {}", interv_wo_chuff, im,
                        imrest);
                int k = 0;
                for (int i = 0; i < im; i++) {
                    _sound.queueBuffer(fill_buf);
                    k++;
                }
                log.debug("{} new buffers queued. Total buffers queued now: {}", k,
                        _sound.getSource().numQueuedBuffers());
                // Create a buffer to queue rest of the filling time. Ignore small sound bites
                if (imrest > (SLEEP_INTERVAL + 10)) {
                    setWait((imrest - SLEEP_INTERVAL * 4) / SLEEP_INTERVAL);
                    if (getWait() < 3) {
                        setWait(0);
                    }
                    int bbufcount = fill_buf.getFrameSize() * (imrest * fill_buf.getFrequency() / 1000);
                    log.debug("chuff_index: {}", chuffIndex());
                    // Empty buffer (bound to notch = 1)
                    AudioBuffer buf = helper_notch.bufs_helper.get(incHelperIndex());
                    byte[] bbytes = new byte[bbufcount];
                    ByteBuffer dataf;
                    if (is_key_coasting || is_auto_coasting) {
                        dataf = coast_notch.getCoastFillerData();
                    } else {
                        dataf = _notch.getNotchFillerData();
                    }
                    dataf.get(bbytes); // Same as: data.get(bbytes, 0, bbufcount);
                    dataf.rewind();
                    ByteBuffer bbuf = ByteBuffer.allocate(bbufcount);
                    bbuf.order(dataf.order()); // Set new buffer's byte order to match source buffer
                    bbuf.put(bbytes); // Same as: bbuf.put(bbytes, 0, bbufcount);
                    log.debug("bbuf after put: {}, order: {}", bbuf, bbuf.order());
                    bbuf.rewind();
                    buf.loadBuffer(bbuf, _notch.getBufferFmt(), fill_buf.getFrequency());
                    _sound.queueBuffer(buf);
                    log.debug("filler rest buffer queued. Length: {}", (int) SoundBite.calcLength(buf));
                } else {
                    log.debug("no filler rest buffer queued.");
                }
            } else {
                log.warn("filler buffer missing.");
            }
        }

        public void mute(boolean m) {
            _sound.mute(m);
            for (SoundBite ts : _parent.trigger_sounds.values()) {
                ts.mute(m);
            }
        }

        public void setVolume(float v) {
            _sound.setVolume(v);
            for (SoundBite ts : _parent.trigger_sounds.values()) {
                ts.setVolume(v);
            }
        }

        public void setPosition(PhysicalLocation p) {
            _sound.setPosition(p);
            for (SoundBite ts : _parent.trigger_sounds.values()) {
                ts.setPosition(p);
            }
        }

        private static final Logger log = LoggerFactory.getLogger(S1LoopThread.class);

    }
}