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}