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}