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, 2025
028 */
029class SteamSound extends EngineSound {
030
031    // Inner class for handling steam RPM sounds
032    class RPMSound {
033
034        private SoundBite sound;
035        private int min_rpm;
036        private int max_rpm;
037        private boolean use_chuff;
038        private javax.swing.Timer t;
039
040        private 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        private void setRPM(int rpm) {
061            if (use_chuff) {
062                t.setDelay(calcChuffInterval(rpm));
063            }
064        }
065
066        private void startChuff() {
067            if (!t.isRunning()) {
068                t.start();
069            }
070        }
071
072        private void stopChuff() {
073            if (t.isRunning()) {
074                t.stop();
075            }
076        }
077    }
078
079    // Engine Sounds
080    private ArrayList<RPMSound> rpm_sounds;
081    int top_speed;
082    private int driver_diameter;
083    private int num_cylinders;
084    private RPMSound current_rpm_sound;
085
086    public SteamSound(String name) {
087        super(name);
088    }
089
090    @Override
091    public void startEngine() {
092        log.debug("Starting Engine");
093        current_rpm_sound = getRPMSound(0);
094        current_rpm_sound.sound.loop();
095    }
096
097    @Override
098    public void stopEngine() {
099        getCurrSound().fadeOut();
100        if (current_rpm_sound.use_chuff) {
101            current_rpm_sound.stopChuff();
102        }
103    }
104
105    private SoundBite getCurrSound() {
106        return current_rpm_sound.sound;
107    }
108
109    private RPMSound getRPMSound(int rpm) {
110        int i = 1;
111        for (RPMSound rps : rpm_sounds) {
112            if ((rps.min_rpm <= rpm) && (rps.max_rpm >= rpm)) {
113                if (engine_pane != null) {
114                    engine_pane.setThrottle(i);
115                }
116                return rps;
117            } else if (rpm > rpm_sounds.get(rpm_sounds.size() - 1).max_rpm) {
118                return rpm_sounds.get(rpm_sounds.size() - 1);
119            }
120            i++;
121        }
122        // Didn't find anything
123        return null;
124    }
125
126    private int calcRPM(float t) {
127        // Speed = % of top_speed (mph)
128        // RPM = speed * ((inches/mile) / (minutes/hour)) / (pi * driver_diameter)
129        double rpm_f = speedCurve(t) * top_speed * 1056 / (Math.PI * driver_diameter);
130        setActualSpeed((float) speedCurve(t));
131        log.debug("RPM Calculated: {}, rounded: {}, actual speed: {}, speedCurve(t): {}", rpm_f, (int) Math.round(rpm_f), getActualSpeed(), speedCurve(t));
132        return (int) Math.round(rpm_f);
133    }
134
135    private int calcChuffInterval(int rpm) {
136        return 30000 / num_cylinders / rpm;
137    }
138
139    @Override
140    public void changeThrottle(float t) {
141        // Don't do anything, if engine is not started or auto-start is active.
142        if (isEngineStarted()) {
143            if (t < 0.0f) {
144                // DO something to shut down
145                //t = 0.0f;
146                setActualSpeed(0.0f);
147                getCurrSound().fadeOut();
148                if (current_rpm_sound.use_chuff) {
149                    current_rpm_sound.stopChuff();
150                }
151                current_rpm_sound = getRPMSound(0);
152                current_rpm_sound.sound.loop();
153            } else {
154                RPMSound rps;
155                rps = getRPMSound(calcRPM(t)); // Get the rpm sound.
156                if (rps != null) {
157                    // Yes, I'm checking to see if rps and current_rpm_sound are the *same object*
158                    if (rps != current_rpm_sound) {
159                        // Stop the current sound
160                        if ((current_rpm_sound != null) && (current_rpm_sound.sound != null)) {
161                            current_rpm_sound.sound.fadeOut();
162                            if (current_rpm_sound.use_chuff) {
163                                current_rpm_sound.stopChuff();
164                            }
165                        }
166                        // Start the new sound.
167                        current_rpm_sound = rps;
168                        if (rps.use_chuff) {
169                            rps.setRPM(calcRPM(t));
170                            rps.startChuff();
171                        }
172                        rps.sound.fadeIn();
173                    } else {
174                        // *same object* - but possibly different rpm (speed) which affects the chuff interval
175                        if (rps.use_chuff) {
176                            rps.setRPM(calcRPM(t)); // Chuff interval need to be recalculated
177                        }
178                    }
179                } else {
180                    log.warn("No adequate sound file found for {}, RPM = {}", this, calcRPM(t));
181                }
182                log.debug("RPS: {}, RPM: {}, current_RPM: {}", rps, calcRPM(t), current_rpm_sound);
183            }
184        }
185    }
186
187    @Override
188    public void shutdown() {
189        for (RPMSound rps : rpm_sounds) {
190            if (rps.use_chuff) rps.stopChuff();
191            rps.sound.stop();
192        }
193    }
194
195    @Override
196    public void mute(boolean m) {
197        for (RPMSound rps : rpm_sounds) {
198            rps.sound.mute(m);
199        }
200    }
201
202    @Override
203    public void setVolume(float v) {
204        for (RPMSound rps : rpm_sounds) {
205            rps.sound.setVolume(v);
206        }
207    }
208
209    @Override
210    public void setPosition(PhysicalLocation p) {
211        for (RPMSound rps : rpm_sounds) {
212            rps.sound.setPosition(p);
213            if (this.getTunnel()) {
214                rps.sound.attachSourcesToEffects();
215            } else {
216                rps.sound.detachSourcesToEffects();
217            }
218        }
219    }
220
221    @Override
222    public Element getXml() {
223        // OUT OF DATE
224        return super.getXml();
225    }
226
227    @Override
228    public void setXml(Element e, VSDFile vf) {
229        Element el;
230        //int num_rpms;
231        String fn, n;
232        SoundBite sb;
233        boolean buffer_ok = true;
234
235        super.setXml(e, vf);
236
237        log.debug("Steam EngineSound: {}, name: {}", e.getAttribute("name").getValue(), name);
238
239        // Required values
240        top_speed = Integer.parseInt(e.getChildText("top-speed"));
241        log.debug("top speed forward: {} MPH", top_speed);
242
243        n = e.getChildText("driver-diameter");
244        if (n != null) {
245            driver_diameter = Integer.parseInt(n);
246            log.debug("Driver diameter: {} inches", driver_diameter);
247        }
248        n = e.getChildText("cylinders");
249        if (n != null) {
250            num_cylinders = Integer.parseInt(n);
251            log.debug("Num Cylinders: {}", num_cylinders);
252        }
253
254        // Optional value
255        // Allows to adjust speed via speedCurve(T).
256        n = e.getChildText("exponent");
257        if (n != null) {
258            exponent = Float.parseFloat(n);
259        } else {
260            exponent = 2.0f; // default
261        }
262        log.debug("exponent: {}", exponent);
263
264        is_auto_start = setXMLAutoStart(e);
265        log.debug("config auto-start: {}", is_auto_start);
266
267        rpm_sounds = new ArrayList<>();
268
269        // Get the RPM steps
270        Iterator<Element> itr = (e.getChildren("rpm-step")).iterator();
271        int i = 0;
272        while (itr.hasNext()) {
273            el = itr.next();
274            fn = el.getChildText("file");
275            int min_r = Integer.parseInt(el.getChildText("min-rpm"));
276            int max_r = Integer.parseInt(el.getChildText("max-rpm"));
277            log.debug("file #: {}, file name: {}", i, fn);
278            sb = new SoundBite(vf, fn, name + "_Steam_n" + i, name + "_Steam_" + i);
279            if (sb.isInitialized()) {
280                sb.setLooped(true);
281                sb.setFadeTimes(100, 100);
282                sb.setReferenceDistance(setXMLReferenceDistance(el)); // Handle reference distance
283                sb.setGain(setXMLGain(el));
284                // Store in the list.
285                boolean chuff = false;
286                Element c;
287                if ((c = el.getChild("use-chuff-gen")) != null) {
288                    log.debug("Use Chuff Generator: {}", c);
289                    chuff = true;
290                }
291
292                rpm_sounds.add(new RPMSound(sb, min_r, max_r, chuff));
293            } else {
294                buffer_ok = false;
295            }
296            i++;
297        }
298
299        if (buffer_ok) {
300            setBuffersFreeState(true);
301            // Check auto-start setting
302            autoStartCheck();
303        } else {
304            setBuffersFreeState(false);
305        }
306    }
307
308    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SteamSound.class);
309
310}