001package jmri.jmrit.vsdecoder;
002
003import java.awt.event.ActionEvent;
004import java.awt.event.ActionListener;
005import java.util.ArrayList;
006import java.util.Iterator;
007import jmri.util.PhysicalLocation;
008import org.jdom2.Element;
009import org.slf4j.Logger;
010import org.slf4j.LoggerFactory;
011
012/**
013 * Steam Sound initial version.
014 *
015 * <hr>
016 * This file is part of JMRI.
017 * <p>
018 * JMRI is free software; you can redistribute it and/or modify it under 
019 * the terms of version 2 of the GNU General Public License as published 
020 * by the Free Software Foundation. See the "COPYING" file for a copy
021 * of this license.
022 * <p>
023 * JMRI is distributed in the hope that it will be useful, but WITHOUT 
024 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 
025 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License 
026 * for more details.
027 *
028 * @author Mark Underwood Copyright (C) 2011
029 * @author Klaus Killinger Copyright (C) 2018-2021
030 */
031// Usage:
032// SteamSound() : constructor
033class SteamSound extends EngineSound {
034
035    // Inner class for handling steam RPM sounds
036    class RPMSound {
037
038        public SoundBite sound;
039        public int min_rpm;
040        public int max_rpm;
041        public boolean use_chuff;
042        private javax.swing.Timer t;
043
044        public RPMSound(SoundBite sb, int min_r, int max_r, boolean chuff) {
045            sound = sb;
046            min_rpm = min_r;
047            max_rpm = max_r;
048            use_chuff = chuff;
049            if (use_chuff) {
050                sound.setLooped(false);
051                t = newTimer(1, true, new ActionListener() {
052                    @Override
053                    public void actionPerformed(ActionEvent e) {
054                        doChuff();
055                    }
056                });
057            }
058        }
059
060        private void doChuff() {
061            sound.play();
062        }
063
064        public void setRPM(int rpm) {
065            if (use_chuff) {
066                t.setDelay(calcChuffInterval(rpm));
067            }
068        }
069
070        public void startChuff() {
071            if (!t.isRunning()) {
072                t.start();
073            }
074        }
075
076        public void stopChuff() {
077            t.stop();
078        }
079    }
080
081    // Engine Sounds
082    ArrayList<RPMSound> rpm_sounds;
083    int top_speed;
084    private int driver_diameter;
085    private int num_cylinders;
086    RPMSound current_rpm_sound;
087    private float exponent;
088
089    public SteamSound(String name) {
090        super(name);
091    }
092
093    // Responds to throttle loco direction key (see EngineSound.java and EngineSoundEvent.java)
094    @Override
095    public void changeLocoDirection(int d) {
096        // If loco direction was changed we need to set topspeed of the loco to new value 
097        // (this is necessary, when topspeed-forward and topspeed-reverse differs)
098        log.debug("loco direction: {}", d);
099    }
100
101    @Override
102    public void startEngine() {
103        log.debug("Starting Engine");
104        current_rpm_sound = getRPMSound(0);
105        current_rpm_sound.sound.loop();
106    }
107
108    @Override
109    public void stopEngine() {
110        current_rpm_sound.sound.fadeOut();
111        if (current_rpm_sound.use_chuff) {
112            current_rpm_sound.stopChuff();
113        }
114    }
115
116    private RPMSound getRPMSound(int rpm) {
117        int i = 1;
118        for (RPMSound rps : rpm_sounds) {
119            if ((rps.min_rpm <= rpm) && (rps.max_rpm >= rpm)) {
120                if (engine_pane != null) {
121                    engine_pane.setThrottle(i);
122                }
123                return rps;
124            }
125            i++;
126        }
127        // Didn't find anything
128        return null;
129    }
130
131    private int calcRPM(float t) {
132        // Speed = % of top_speed (mph)
133        // RPM = speed * ((inches/mile) / (minutes/hour)) / (pi * driver_diameter)
134        double rpm_f = speedCurve(t) * top_speed * 1056 / (Math.PI * driver_diameter);
135        log.debug("RPM Calculated: {}, (int) {}", rpm_f, (int) Math.round(rpm_f));
136        return (int) Math.round(rpm_f);
137    }
138
139    @Override
140    double speedCurve(float t) {
141        return Math.pow(t, exponent) / 1.0;
142    }
143
144    private int calcChuffInterval(int rpm) {
145        return 30000 / num_cylinders / rpm;
146    }
147
148    @Override
149    public void changeThrottle(float t) {
150        // Don't do anything, if engine is not started or auto-start is active.
151        if (isEngineStarted()) {
152            if (t < 0.0f) {
153                // DO something to shut down
154                //t = 0.0f;
155                current_rpm_sound.sound.fadeOut();
156                if (current_rpm_sound.use_chuff) {
157                    current_rpm_sound.stopChuff();
158                }
159                current_rpm_sound = getRPMSound(0);
160                current_rpm_sound.sound.loop();
161            } else {
162                RPMSound rps;
163                rps = getRPMSound(calcRPM(t)); // Get the rpm sound.
164                if (rps != null) {
165                    // Yes, I'm checking to see if rps and current_rpm_sound are the *same object*
166                    if (rps != current_rpm_sound) {
167                        // Stop the current sound
168                        if ((current_rpm_sound != null) && (current_rpm_sound.sound != null)) {
169                            current_rpm_sound.sound.fadeOut();
170                            if (current_rpm_sound.use_chuff) {
171                                current_rpm_sound.stopChuff();
172                            }
173                        }
174                        // Start the new sound.
175                        current_rpm_sound = rps;
176                        if (rps.use_chuff) {
177                            rps.setRPM(calcRPM(t));
178                            rps.startChuff();
179                        }
180                        rps.sound.fadeIn();
181                    } else {
182                        // *same object* - but possibly different rpm (speed) which affects the chuff interval
183                        if (rps.use_chuff) {
184                            rps.setRPM(calcRPM(t)); // Chuff interval need to be recalculated
185                        }
186                    }
187                } else {
188                    log.warn("No adequate sound file found for RPM = {}", calcRPM(t));
189                }
190                log.debug("RPS: {}, RPM: {}, current_RPM: {}", rps, calcRPM(t), current_rpm_sound);
191            }
192        }
193    }
194
195    @Override
196    public void shutdown() {
197        for (RPMSound rps : rpm_sounds) {
198            if (rps.use_chuff) rps.stopChuff();
199            rps.sound.stop();
200        }
201    }
202
203    @Override
204    public void mute(boolean m) {
205        for (RPMSound rps : rpm_sounds) {
206            rps.sound.mute(m);
207        }
208    }
209
210    @Override
211    public void setVolume(float v) {
212        for (RPMSound rps : rpm_sounds) {
213            rps.sound.setVolume(v);
214        }
215    }
216
217    @Override
218    public void setPosition(PhysicalLocation p) {
219        for (RPMSound rps : rpm_sounds) {
220            rps.sound.setPosition(p);
221        }
222    }
223
224    @Override
225    public Element getXml() {
226        // OUT OF DATE
227        return super.getXml();
228    }
229
230    @Override
231    public void setXml(Element e, VSDFile vf) {
232        Element el;
233        //int num_rpms;
234        String fn, n;
235        SoundBite sb;
236
237        super.setXml(e, vf);
238
239        log.debug("Steam EngineSound: {}, name: {}", e.getAttribute("name").getValue(), name);
240
241        // Required values
242        top_speed = Integer.parseInt(e.getChildText("top-speed"));
243        log.debug("top speed forward: {} MPH", top_speed);
244
245        n = e.getChildText("driver-diameter");
246        if (n != null) {
247            driver_diameter = Integer.parseInt(n);
248            log.debug("Driver diameter: {} inches", driver_diameter);
249        }
250        n = e.getChildText("cylinders");
251        if (n != null) {
252            num_cylinders = Integer.parseInt(n);
253            log.debug("Num Cylinders: {}", num_cylinders);
254        }
255
256        // Optional value
257        // Allows to adjust speed via speedCurve(T).
258        n = e.getChildText("exponent");
259        if (n != null) {
260            exponent = Float.parseFloat(n);
261        } else {
262            exponent = 2.0f; // default
263        }
264        log.debug("exponent: {}", exponent);
265
266        // For now, num_rpms is not used.  
267        /*
268         n = e.getChild("rpm-steps").getValue();
269         if (n != null) {
270         num_rpms = Integer.parseInt(n);
271         //log.debug("Number of rpm steps: {}", num_rpms);
272         }
273         */
274
275        is_auto_start = setXMLAutoStart(e);
276        log.debug("config auto-start: {}", is_auto_start);
277
278        rpm_sounds = new ArrayList<>();
279
280        // Get the RPM steps
281        Iterator<Element> itr = (e.getChildren("rpm-step")).iterator();
282        int i = 0;
283        while (itr.hasNext()) {
284            el = itr.next();
285            fn = el.getChildText("file");
286            int min_r = Integer.parseInt(el.getChildText("min-rpm"));
287            int max_r = Integer.parseInt(el.getChildText("max-rpm"));
288            log.debug("file #: {}, file name: {}", i, fn);
289            sb = new SoundBite(vf, fn, name + "_Steam_n" + i, name + "_Steam_" + i);
290            sb.setLooped(true);
291            sb.setFadeTimes(100, 100);
292            sb.setReferenceDistance(setXMLReferenceDistance(el)); // Handle reference distance
293            sb.setGain(setXMLGain(el));
294            // Store in the list.
295            boolean chuff = false;
296            Element c;
297            if ((c = el.getChild("use-chuff-gen")) != null) {
298                log.debug("Use Chuff Generator: {}", c);
299                chuff = true;
300            }
301
302            rpm_sounds.add(new RPMSound(sb, min_r, max_r, chuff));
303            i++;
304        }
305
306        /*
307         // Get the start and stop sounds
308         el = e.getChild("start-sound");
309         if (el != null) {
310         fn = el.getChild("file").getValue();
311         log.debug("Start sound: {}", fn);
312         start_sound = new SoundBite(vf, fn, "Engine_start", 
313         "Engine_Start");
314         // Handle gain
315         start_sound.setGain(setXMLGain(el));
316         start_sound.setLooped(false);
317         }
318         */
319
320        // Check auto-start setting
321        autoStartCheck();
322    }
323
324    private static final Logger log = LoggerFactory.getLogger(SteamSound.class);
325
326}