001package jmri.jmrit.vsdecoder;
002
003import java.util.ArrayList;
004import java.util.HashMap;
005import java.util.Iterator;
006import java.util.List;
007import java.nio.ByteBuffer;
008import jmri.Audio;
009import jmri.AudioException;
010import jmri.jmrit.audio.AudioBuffer;
011import jmri.util.PhysicalLocation;
012import org.jdom2.Element;
013
014/**
015 * Diesel Sound version 3.
016 *
017 * <hr>
018 * This file is part of JMRI.
019 * <p>
020 * JMRI is free software; you can redistribute it and/or modify it under
021 * the terms of version 2 of the GNU General Public License as published
022 * by the Free Software Foundation. See the "COPYING" file for a copy
023 * of this license.
024 * <p>
025 * JMRI is distributed in the hope that it will be useful, but WITHOUT
026 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
027 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
028 * for more details.
029 *
030 * @author Mark Underwood Copyright (C) 2011
031 * @author Klaus Killinger Copyright (C) 2018-2021, 2023, 2025, 2026
032 */
033class Diesel3Sound extends EngineSound {
034
035    // Engine Sounds
036    private HashMap<Integer, D3Notch> notch_sounds;
037    private String _soundName;
038
039    private AudioBuffer start_buffer;
040    private AudioBuffer stop_buffer;
041    private Integer idle_notch;
042    private int first_notch;
043    int top_speed;
044    static final int number_helper_buffers = 5; // in the loop-player is a limit of 2, but the unqueue takes some time too
045
046    // Common variables
047    private int current_notch = 1; // default
048    private D3LoopThread _loopThread = null;
049
050    public Diesel3Sound(String name) {
051        super(name);
052        log.debug("New Diesel3Sound name(param): {}, name(val): {}", name, this.getName());
053    }
054
055    private void startThread() {
056        _loopThread = new D3LoopThread(this, notch_sounds.get(current_notch), _soundName, true);
057        _loopThread.setName("Diesel3Sound.D3LoopThread");
058        log.debug("Loop Thread Started.  Sound name: {}", _soundName);
059    }
060
061    // Responds to "CHANGE" trigger
062    @Override
063    public void changeThrottle(float s) {
064        // This is all we have to do.  The loop thread will handle everything else.
065        if (_loopThread != null) {
066            _loopThread.setThrottle(s);
067        }
068    }
069
070    // Responds to throttle loco direction key (see EngineSound.java and EngineSoundEvent.java)
071    @Override
072    public void changeLocoDirection(int dirfn) {
073        log.debug("loco IsForward is {}", dirfn);
074        if (_loopThread != null) {
075            _loopThread.getLocoDirection(dirfn);
076        }
077    }
078
079    // Responds to throttle function key (see EngineSound.java and EngineSoundEvent.java)
080    @Override
081    public void functionKey(String event, boolean value, String name) {
082        log.debug("throttle function key {} pressed for {}: {}", event, name, value);
083        if (_loopThread != null) {
084            _loopThread.setFunction(event, value, name);
085        }
086    }
087
088    private D3Notch getNotch(int n) {
089        return notch_sounds.get(n);
090    }
091
092    @Override
093    public void startEngine() {
094        log.debug("startEngine.  ID: {}", this.getName());
095        if (_loopThread != null) {
096            _loopThread.startEngine(start_buffer);
097        }
098    }
099
100    @Override
101    public void stopEngine() {
102        log.debug("stopEngine.  ID: {}", this.getName());
103        if (_loopThread != null) {
104            _loopThread.stopEngine(stop_buffer);
105        }
106    }
107
108    @Override
109    public void shutdown() {
110        // Stop the loop thread, in case it's running
111        if (_loopThread != null) {
112            _loopThread.setRunning(false);
113        }
114    }
115
116    @Override
117    public void mute(boolean m) {
118        if (_loopThread != null) {
119            _loopThread.mute(m);
120        }
121    }
122
123    @Override
124    public void setVolume(float v) {
125        if (_loopThread != null) {
126            _loopThread.setVolume(v);
127        }
128    }
129
130    @Override
131    public void setPosition(PhysicalLocation p) {
132        if (_loopThread != null) {
133            _loopThread.setPosition(p);
134        }
135    }
136
137    @Override
138    public Element getXml() {
139        Element me = new Element("sound");
140        me.setAttribute("name", this.getName());
141        me.setAttribute("type", "engine");
142        // Do something, eventually...
143        return me;
144    }
145
146    @Override
147    public void setXml(Element e, VSDFile vf) {
148        Element el;
149        String fn;
150        String in;
151        D3Notch sb;
152        int frame_size = 0;
153        int freq = 0;
154        boolean buffer_ok = true;
155
156        // Handle the common stuff.
157        super.setXml(e, vf);
158
159        _soundName = this.getName() + ":LoopSound";
160        log.debug("get name: {}, soundName: {}, name: {}", this.getName(), _soundName, name);
161
162        // Optional value
163        in = e.getChildText("top-speed");
164        if (in != null) {
165            top_speed = Integer.parseInt(in);
166        } else {
167            top_speed = 0; // default
168        }
169        log.debug("top speed forward: {} MPH", top_speed);
170
171        notch_sounds = new HashMap<>();
172        in = e.getChildText("idle-notch");
173        idle_notch = null;
174        if (in != null) {
175            idle_notch = Integer.parseInt(in);
176            log.debug("Notch {} is Idle.", idle_notch);
177        } else {
178            // leave idle_notch null for now. We'll use it at the end to trigger a "grandfathering" action
179            log.warn("No Idle Notch Specified!");
180        }
181
182        is_auto_start = setXMLAutoStart(e);
183        log.debug("config auto-start: {}", is_auto_start);
184
185        // Optional value
186        // Allows to adjust OpenAL attenuation
187        // Sounds with distance to listener position lower than reference-distance will not have attenuation
188        engine_rd = setXMLEngineReferenceDistance(e); // Handle engine reference distance
189        log.debug("engine-sound referenceDistance: {}", engine_rd);
190
191        exponent = setXMLExponent(e);
192        log.debug("exponent: {}", exponent);
193
194        // Optional value
195        // Allows to adjust the engine gain
196        in = e.getChildText("engine-gain");
197        if ((in != null) && (!in.isEmpty())) {
198            engine_gain = Float.parseFloat(in);
199        } else {
200            engine_gain = default_gain;
201        }
202        log.debug("engine gain: {}", engine_gain);
203
204        sleep_interval = setXMLSleepInterval(e); // Optional value
205        log.debug("sleep interval: {}", sleep_interval);
206
207        // Get the notch sounds
208        Iterator<Element> itr = (e.getChildren("notch-sound")).iterator();
209        int i = 0;
210        while (itr.hasNext()) {
211            el = itr.next();
212            int nn = Integer.parseInt(el.getChildText("notch"));
213            sb = new D3Notch(nn);
214            sb.setIdleNotch(false);
215            if ((idle_notch != null) && (nn == idle_notch)) {
216                sb.setIdleNotch(true);
217                log.debug("Notch {} set to Idle.", nn);
218            }
219
220            List<Element> elist = el.getChildren("file");
221            for (Element fe : elist) {
222                fn = fe.getText();
223                if (i == 0) {
224                    // Take the first notch-file to determine the audio formats (format, frequence and framesize)
225                    // All files of notch_sounds must have the same audio formats
226                    first_notch = nn;
227                    int[] formats;
228                    formats = AudioUtil.getWavFormats(D3Notch.getWavStream(vf, fn));
229                    frame_size = formats[2];
230                    freq = formats[1];
231                    sb.setBufferFmt(formats[0]);
232                    sb.setBufferFreq(formats[1]);
233                    sb.setBufferFrameSize(formats[2]);
234                    log.debug("formats: {}", formats);
235
236                    // Add some helper Buffers to the first notch
237                    for (int j = 0; j < number_helper_buffers; j++) {
238                        if (checkForFreeBuffer()) {
239                            AudioBuffer bh = D3Notch.getBufferHelper(name + "_BUFFERHELPER_" + j, name + "_BUFFERHELPER_" + j);
240                            if (bh != null) {
241                                log.debug("helper buffer created: {}, name: {}", bh, bh.getSystemName());
242                                sb.addHelper(bh);
243                            } else {
244                                buffer_ok = false;
245                            }
246                        } else {
247                           buffer_ok = false;
248                        }
249                    }
250                }
251
252                // Generate data slices from each notch sound file
253                List<ByteBuffer> l = D3Notch.getDataList(vf, fn, name + "_n" + i);
254                log.debug("{} internal sub buffers created from file {}:", l.size(), fn);
255                for (ByteBuffer b : l) {
256                    log.debug(" length: {} ms", (1000 * b.limit() / frame_size) / freq);
257                }
258                sb.addLoopData(l);
259            }
260
261            sb.setNextNotch(el.getChildText("next-notch"));
262            sb.setPrevNotch(el.getChildText("prev-notch"));
263            sb.setAccelLimit(el.getChildText("accel-limit"));
264            sb.setDecelLimit(el.getChildText("decel-limit"));
265
266            if (el.getChildText("accel-file") != null) {
267                if (checkForFreeBuffer()) {
268                    sb.setAccelBuffer(D3Notch.getBuffer(vf, el.getChildText("accel-file"), name + "_na" + i, name + "_na" + i));
269                } else {
270                    buffer_ok = false;
271                }
272            } else {
273                sb.setAccelBuffer(null);
274            }
275            if (el.getChildText("decel-file") != null) {
276                if (checkForFreeBuffer()) {
277                    sb.setDecelBuffer(D3Notch.getBuffer(vf, el.getChildText("decel-file"), name + "_nd" + i, name + "_nd" + i));
278                } else {
279                    buffer_ok = false;
280                }
281            } else {
282                sb.setDecelBuffer(null);
283            }
284            // Store in the list.
285            notch_sounds.put(nn, sb);
286            i++;
287        }
288
289        // Get the start and stop sounds (both sounds are optional)
290        el = e.getChild("start-sound");
291        if (el != null) {
292            fn = el.getChild("file").getValue();
293            if (checkForFreeBuffer()) {
294                start_buffer = D3Notch.getBuffer(vf, fn, name + "_start", name + "_Start");
295                log.debug("Start sound: {}, buffer {} created, length: {}", fn, start_buffer, SoundBite.calcLength(start_buffer));
296            } else {
297                buffer_ok = false;
298            }
299        }
300        el = e.getChild("shutdown-sound");
301        if (el != null) {
302            fn = el.getChild("file").getValue();
303            if (checkForFreeBuffer()) {
304                stop_buffer = D3Notch.getBuffer(vf, fn, name + "_shutdown", name + "_Shutdown");
305                log.debug("Shutdown sound: {}, buffer {} created, length: {}", fn, stop_buffer, SoundBite.calcLength(stop_buffer));
306            } else {
307                buffer_ok = false;
308            }
309        }
310
311        // Handle "grandfathering" the idle notch indication
312        // If the VSD designer did not explicitly designate an idle notch...
313        // Find the Notch with the lowest notch number, and make it the idle notch.
314        // If there's a tie, this will take the first value, but the notches /should/
315        // all have unique notch numbers.
316        if (idle_notch == null) {
317            D3Notch min_notch = null;
318            // No, this is not a terribly efficient "min" operation.  But that's OK.
319            for (D3Notch n : notch_sounds.values()) {
320                if ((min_notch == null) || (n.getNotch() < min_notch.getNotch())) {
321                    min_notch = n;
322                }
323            }
324            log.info("No Idle Notch Specified.  Choosing Notch {} to be the Idle Notch.", (min_notch != null ? min_notch.getNotch() : "min_notch not set"));
325            if (min_notch != null) {
326                min_notch.setIdleNotch(true);
327                idle_notch = min_notch.getNotch();
328            } else {
329                log.warn("Could not set idle notch because min_notch was still null");
330            }
331        }
332
333        if (buffer_ok) {
334            setBuffersFreeState(true);
335            // Kick-start the loop thread.
336            this.startThread();
337            // Check auto-start setting
338            autoStartCheck();
339        } else {
340            setBuffersFreeState(false);
341        }
342    }
343
344    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Diesel3Sound.class);
345
346    private static class D3Notch {
347
348        private int my_notch;
349        private int next_notch;
350        private int prev_notch;
351        private int buffer_fmt;
352        private int buffer_freq;
353        private int buffer_frame_size;
354        private int loop_index;
355        private float accel_limit;
356        private float decel_limit;
357        private boolean is_idle;
358        private AudioBuffer accel_buf;
359        private AudioBuffer decel_buf;
360        private List<AudioBuffer> bufs_helper = new ArrayList<>();
361        private List<ByteBuffer> loop_data = new ArrayList<>();
362
363        private D3Notch(int notch) {
364            my_notch = notch;
365            loop_index = 0;
366        }
367
368        private int getNotch() {
369            return my_notch;
370        }
371
372        private int getNextNotch() {
373            return next_notch;
374        }
375
376        private int getPrevNotch() {
377            return prev_notch;
378        }
379
380        private AudioBuffer getAccelBuffer() {
381            return accel_buf;
382        }
383
384        private AudioBuffer getDecelBuffer() {
385            return decel_buf;
386        }
387
388        private float getAccelLimit() {
389            return accel_limit;
390        }
391
392        private float getDecelLimit() {
393            return decel_limit;
394        }
395
396        private Boolean isInLimits(float val) {
397            return (val >= decel_limit) && (val <= accel_limit);
398        }
399
400        private void setBufferFmt(int fmt) {
401            buffer_fmt = fmt;
402        }
403
404        private int getBufferFmt() {
405            return buffer_fmt;
406        }
407
408        private void setBufferFreq(int freq) {
409            buffer_freq = freq;
410        }
411
412        private int getBufferFreq() {
413            return buffer_freq;
414        }
415
416        private void setBufferFrameSize(int framesize) {
417            buffer_frame_size = framesize;
418        }
419
420        private int getBufferFrameSize() {
421            return buffer_frame_size;
422        }
423
424        private Boolean isIdleNotch() {
425            return is_idle;
426        }
427
428        private void setNextNotch(String s) {
429            next_notch = setIntegerFromString(s);
430        }
431
432        private void setPrevNotch(String s) {
433            prev_notch = setIntegerFromString(s);
434        }
435
436        private void setAccelLimit(String s) {
437            accel_limit = setFloatFromString(s);
438        }
439
440        private void setDecelLimit(String s) {
441            decel_limit = setFloatFromString(s);
442        }
443
444        private void setAccelBuffer(AudioBuffer b) {
445            accel_buf = b;
446        }
447
448        private void setDecelBuffer(AudioBuffer b) {
449            decel_buf = b;
450        }
451
452        private void addLoopData(List<ByteBuffer> l) {
453            loop_data.addAll(l);
454        }
455
456        private ByteBuffer nextLoopData() {
457            return loop_data.get(incLoopIndex());
458        }
459
460        private void setIdleNotch(Boolean i) {
461            is_idle = i;
462        }
463
464        private void addHelper(AudioBuffer b) {
465            bufs_helper.add(b);
466        }
467
468        private int incLoopIndex() {
469            // Increment
470            loop_index++;
471            // Correct for wrap.
472            if (loop_index >= loop_data.size()) {
473                loop_index = 0;
474            }
475            return loop_index;
476        }
477
478        private int setIntegerFromString(String s) {
479            if (s == null) {
480                return 0;
481            }
482            try {
483                int n = Integer.parseInt(s);
484                return n;
485            } catch (NumberFormatException e) {
486                d3NotchLog.debug("Invalid integer: {}", s);
487                return 0;
488            }
489        }
490
491        private float setFloatFromString(String s) {
492            if (s == null) {
493                return 0.0f;
494            }
495            try {
496                float f = Float.parseFloat(s) / 100.0f;
497                return f;
498            } catch (NumberFormatException e) {
499                d3NotchLog.debug("Invalid float: {}", s);
500                return 0.0f;
501            }
502        }
503
504        private static List<ByteBuffer> getDataList(VSDFile vf, String filename, String sname) {
505            List<ByteBuffer> datalist = null;
506            java.io.InputStream ins = vf.getInputStream(filename);
507            if (ins != null) {
508                datalist = AudioUtil.getByteBufferList(ins, 250, 150);
509            } else {
510                d3NotchLog.debug("Input Stream failed");
511                return null;
512            }
513            return datalist;
514        }
515
516        private static AudioBuffer getBuffer(VSDFile vf, String filename, String sname, String uname) {
517            AudioBuffer buf = null;
518            jmri.AudioManager am = jmri.InstanceManager.getDefault(jmri.AudioManager.class);
519            try {
520                buf = (AudioBuffer) am.provideAudio(VSDSound.BufSysNamePrefix + sname);
521                buf.setUserName(VSDSound.BufUserNamePrefix + uname);
522                java.io.InputStream ins = vf.getInputStream(filename);
523                if (ins != null) {
524                    buf.setInputStream(ins);
525                } else {
526                    d3NotchLog.debug("Input Stream failed");
527                    return null;
528                }
529            } catch (AudioException | IllegalArgumentException ex) {
530                d3NotchLog.error("Problem creating SoundBite", ex);
531                return null;
532            }
533            d3NotchLog.debug("Buffer created: {}, name: {}", buf, buf.getSystemName());
534            return buf;
535        }
536
537        private static AudioBuffer getBufferHelper(String sname, String uname) {
538            AudioBuffer bf = null;
539            jmri.AudioManager am = jmri.InstanceManager.getDefault(jmri.AudioManager.class);
540            try {
541                bf = (AudioBuffer) am.provideAudio(VSDSound.BufSysNamePrefix + sname);
542                bf.setUserName(VSDSound.BufUserNamePrefix + uname);
543            } catch (AudioException | IllegalArgumentException ex) {
544                d3NotchLog.warn("problem creating SoundBite", ex);
545                return null;
546            }
547            d3NotchLog.debug("empty buffer created: {}, name: {}", bf, bf.getSystemName());
548            return bf;
549        }
550
551        private static java.io.InputStream getWavStream(VSDFile vf, String filename) {
552            java.io.InputStream ins = vf.getInputStream(filename);
553            if (ins != null) {
554                return ins;
555            } else {
556                d3NotchLog.warn("input Stream failed for {}", filename);
557                return null;
558            }
559        }
560
561        private static final org.slf4j.Logger d3NotchLog = org.slf4j.LoggerFactory.getLogger(D3Notch.class);
562
563    }
564
565    private static class D3LoopThread extends Thread {
566
567        private boolean is_running;
568        private boolean is_looping;
569        private boolean is_in_rampup_mode;
570        private boolean is_key_coasting;
571        private boolean is_key_notching;
572        private boolean is_emergency_stop;
573        private Diesel3Sound _parent;
574        private D3Notch _notch;
575        private D3Notch notch1;
576        private SoundBite _sound;
577        private float _throttle;
578        private float rpm_dirfn;
579        private int helper_index;
580
581        private D3LoopThread(Diesel3Sound d, D3Notch n, String s, boolean r) {
582            super();
583            is_running = r;
584            is_looping = false;
585            is_in_rampup_mode = false;
586            is_key_coasting = false;
587            is_key_notching = false;
588            is_emergency_stop = false;
589            _parent = d;
590            _notch = n;
591            _sound = new SoundBite(s);
592            _sound.isInitialized();
593            _sound.setGain(_parent.engine_gain);
594            _throttle = 0.0f;
595            rpm_dirfn = 0.0f;
596            if (r) {
597                this.start();
598            }
599        }
600
601        private void setRunning(boolean r) {
602            is_running = r;
603        }
604
605        private void setThrottle(float t) {
606            if (_parent.isEngineStarted()) {
607                if (t < 0.0f) {
608                    t = 0.0f;
609                    _throttle = 0.0f;
610                    _parent.setActualSpeed(_throttle);
611                    is_in_rampup_mode = false; // interrupt ramp-up
612                    is_emergency_stop = true;
613                    is_key_coasting = false;
614                } else {
615                    _parent.setActualSpeed((float) _parent.speedCurve(t));
616                    if (!is_key_notching && !is_key_coasting) {
617                        _throttle = t;
618                        d3LoopLog.debug("Throttle set: {}", _throttle);
619                    }
620                }
621            }
622        }
623
624        private void getLocoDirection(int d) {
625            d3LoopLog.debug("loco direction: {}", d);
626
627            // React to a change in direction to slow down,
628            // then change direction, then ramp-up to the old speed
629            if (_throttle > 0.0f && _parent.isEngineStarted() && !is_in_rampup_mode) {
630                rpm_dirfn = _throttle; // save rpm for ramp-up
631                d3LoopLog.debug("speed {} saved", rpm_dirfn);
632                is_in_rampup_mode = true; // set a flag for the ramp-up
633                _throttle = 0.0f;
634                _parent.setActualSpeed(_throttle);
635            }
636        }
637
638        private void setFunction(String event, boolean is_true, String name) {
639            // Note: throttle will send initial value(s) before thread is started!
640            d3LoopLog.debug("throttle function key pressed: {} is {}, function: {}", event, is_true, name);
641            if (name.equals("COAST")) {
642                // Handle key-coasting on/off
643                if (is_true) {
644                    is_key_coasting = true;
645                    _notch = _parent.getNotch(_parent.first_notch);
646                    _throttle = _notch.getDecelLimit();
647                    changeNotch();
648                } else {
649                    _throttle = _parent.getActualSpeed();
650                    changeNotch();
651                    is_key_coasting = false;
652                }
653            }
654
655            // Notch change if NOTCH_UP key is pressed
656            if (name.equals("NOTCH_UP")) {
657                // I assume that a NOTCH_DOWN is also declared
658                is_key_notching = true;
659                // the engine must be starting up for key-notching to work
660                if (_parent.isEngineStarted() && is_true) {
661                    d3LoopLog.debug("current notch: {}, accel limit: {}", _notch.getNotch(), _notch.getAccelLimit());
662                    _notch = _parent.getNotch(_notch.getNotch());
663                    if (_notch.getNotch() < _parent.notch_sounds.size()) {
664                        _throttle = _notch.getAccelLimit() + 0.01f;
665                        changeNotch();
666                    }
667                }
668            }
669            // Notch change if NOTCH_DOWN key is pressed
670            if (name.equals("NOTCH_DOWN")) {
671                if (_parent.isEngineStarted() && is_true) {
672                    d3LoopLog.debug("current notch: {}, decel limit: {}", _notch.getNotch(), _notch.getDecelLimit());
673                    _notch = _parent.getNotch(_notch.getNotch());
674                    _throttle = _notch.getDecelLimit() - 0.01f;
675                    if (_throttle < 0) _throttle = 0.0f;
676                    changeNotch();
677                }
678            }
679        }
680
681        public void startEngine(AudioBuffer start_buf) {
682            _sound.unqueueBuffers();
683            d3LoopLog.debug("thread: start engine ...");
684
685            helper_index = -1; // Prepare helper buffer start; index will be incremented before first use
686            notch1 = _parent.getNotch(_parent.first_notch);
687
688            _sound.setReferenceDistance(_parent.engine_rd);
689            d3LoopLog.debug("set reference distance to {} for engine sound", _sound.getReferenceDistance());
690
691            _notch = _parent.getNotch(_parent.first_notch);
692            d3LoopLog.debug("Notch: {}, prev: {}, next: {}", _notch.getNotch(), _notch.getPrevNotch(), _notch.getNextNotch());
693
694            if (_parent.engine_pane != null) {
695                _parent.engine_pane.setThrottle(_notch.getNotch()); // Set EnginePane (DieselPane) notch
696            }
697
698            // Only queue the start buffer if we know we're in the idle notch.
699            // This is indicated by prevNotch == self.
700            if (_notch.isIdleNotch()) {
701                _sound.queueBuffer(start_buf);
702                if (_parent.engine_pane != null) {
703                    _parent.engine_pane.setButtonDelay(SoundBite.calcLength(start_buf));
704                }
705            } else {
706                setSound(_notch.nextLoopData());
707            }
708
709            // Follow up with another loop buffer.
710            setSound(_notch.nextLoopData());
711            is_looping = true;
712            if (_sound.getSource().getState() != Audio.STATE_PLAYING) {
713                _sound.play();
714            }
715        }
716
717        public void stopEngine(AudioBuffer stop_buf) {
718            d3LoopLog.debug("thread: stop engine ...");
719            is_looping = false; // stop the loop player
720            _throttle = 0.0f; // Clear it, just in case the engine was stopped at speed > 0
721            _parent.setActualSpeed(0.0f);
722            if (_parent.engine_pane != null) {
723                _parent.engine_pane.setThrottle(_parent.idle_notch); // Set EnginePane (DieselPane) notch
724                _parent.engine_pane.setButtonDelay(SoundBite.calcLength(stop_buf));
725                _sound.queueBuffer(stop_buf);
726                if (_sound.getSource().getState() != Audio.STATE_PLAYING) {
727                    _sound.play();
728                }
729            }
730        }
731
732        // loop-player
733        @Override
734        public void run() {
735            try {
736                while (is_running) {
737                    if (is_looping && AudioUtil.isAudioRunning()) {
738                        if (_sound.getSource().numProcessedBuffers() > 0) {
739                            _sound.unqueueBuffers();
740                        }
741                        //log.debug("D3Loop {} Run loop. Buffers: {}", _sound.getName(), _sound.getSource().numQueuedBuffers());
742                        if (!_notch.isInLimits(_throttle)) {
743                            changeNotch();
744                        }
745                        if (_sound.getSource().numQueuedBuffers() < 2) {
746                            setSound(_notch.nextLoopData());
747                        }
748                        checkAudioState();
749                    } else {
750                        if (_sound.getSource().numProcessedBuffers() > 0) {
751                            _sound.unqueueBuffers();
752                        }
753                    }
754                    sleep(_parent.sleep_interval);
755                    checkRampup();
756                }
757                _sound.stop();
758            } catch (InterruptedException ie) {
759                // kill thread
760                d3LoopLog.debug("thread interrupted");
761            }
762        }
763
764        private void checkAudioState() {
765            if (_sound.getSource().getState() != Audio.STATE_PLAYING) {
766                _sound.play();
767                d3LoopLog.info("loop sound re-started");
768            }
769        }
770
771        private void changeNotch() {
772            AudioBuffer transition_buf = null;
773            int new_notch = _notch.getNotch();
774
775            d3LoopLog.debug("D3Thread Change Throttle: {}, Accel Limit: {}, Decel Limit: {}", _throttle, _notch.getAccelLimit(), _notch.getDecelLimit());
776            if (_throttle > _notch.getAccelLimit()) {
777                // Too fast. Need to go to next notch up.
778                if (_notch.getNotch() < _notch.getNextNotch()) {
779                    // prepare for next notch up
780                    transition_buf = _notch.getAccelBuffer();
781                    new_notch = _notch.getNextNotch();
782                    d3LoopLog.debug("Change up. notch: {}", new_notch);
783                }
784            } else if (_throttle < _notch.getDecelLimit()) {
785                // Too slow.  Need to go to next notch down.
786                transition_buf = _notch.getDecelBuffer();
787                new_notch = _notch.getPrevNotch();
788                d3LoopLog.debug("Change down. notch: {}", new_notch);
789            }
790            _parent.engine_pane.setThrottle(new_notch); // Update EnginePane (DieselPane) notch
791            // Now, regardless of whether we're going up or down, set the timer,
792            // fade the current sound, and move on.
793            if (transition_buf == null || is_key_coasting || is_emergency_stop) {
794                // No transition sound to play.  Skip the timer bit
795                // Recurse directly to try the next notch
796                _notch = _parent.getNotch(new_notch);
797            } else {
798                // queue up the transition sound buffer
799                _notch = _parent.getNotch(new_notch);
800                _sound.queueBuffer(transition_buf);
801            }
802            if (new_notch == 1) {
803                is_emergency_stop = false;
804            }
805        }
806
807        private void setSound(ByteBuffer data) {
808            AudioBuffer buf = notch1.bufs_helper.get(incHelperIndex()); // buffer for the queue
809            int sbl = 0; // sound bite length
810            int bbufcount; // number of bytes for the sound clip
811            ByteBuffer bbuf;
812            byte[] bbytes;
813
814            if (notch1.getBufferFreq() > 0) {
815                sbl = (1000 * data.limit() / notch1.getBufferFrameSize()) / notch1.getBufferFreq(); // calculate the length of the clip in milliseconds
816                // Prepare the sound and transfer it to the target ByteBuffer bbuf
817                bbufcount = notch1.getBufferFrameSize() * (sbl * notch1.getBufferFreq() / 1000);
818                bbuf = ByteBuffer.allocateDirect(bbufcount); // Target
819                bbytes = new byte[bbufcount];
820                data.get(bbytes); // Same as: data.get(bbytes, 0, bbufcount2);
821                data.rewind();
822                bbuf.order(data.order()); // Set new buffer's byte order to match source buffer.
823                bbuf.put(bbytes);
824                bbuf.rewind();
825                buf.loadBuffer(bbuf, notch1.getBufferFmt(), notch1.getBufferFreq());
826                _sound.queueBuffer(buf);
827            }
828        }
829
830        private void checkRampup() {
831            // Handle a throttle forward or reverse change
832            if (is_in_rampup_mode && _throttle == 0.0f && _notch.getNotch() == _parent.idle_notch) {
833                d3LoopLog.debug("now ramp-up to speed {}", rpm_dirfn);
834                is_in_rampup_mode = false;
835                _throttle = rpm_dirfn;
836            }
837        }
838
839        private int incHelperIndex() {
840            helper_index++;
841            // Correct for wrap.
842            if (helper_index >= Diesel3Sound.number_helper_buffers) {
843                helper_index = 0;
844            }
845            return helper_index;
846        }
847
848        private void mute(boolean m) {
849            _sound.mute(m);
850        }
851
852        private void setVolume(float v) {
853            _sound.setVolume(v);
854        }
855
856        private void setPosition(PhysicalLocation p) {
857            _sound.setPosition(p);
858            if (_parent.getTunnel()) {
859                _sound.attachSourcesToEffects();
860            } else {
861                _sound.detachSourcesToEffects();
862            }
863        }
864
865        private static final org.slf4j.Logger d3LoopLog = org.slf4j.LoggerFactory.getLogger(D3LoopThread.class);
866
867    }
868}