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