Skip to content

Commit 43491b3

Browse files
eriedclaude
andcommitted
Motor sound: more presets, per-option preview, polish
- 9 new synth presets: Boxer 6, Inline-6 (silky), Tractor, Steam locomotive, Lightsaber, Jet turbine, UFO hover, F1 electric whine, Hover bike. - 4 new sampled presets (CC0 from BigSoundBank): Helicopter, Tractor, Lawn mower, Steam locomotive. - Per-option preview buttons on the Muffler and Gearbox rows. Tapping the inline play button triggers a 2.5s preview with that option applied without changing the current selection. - previewProfile now takes optional volume / muffler / gearbox overrides, and SettingsViewModel snapshots current settings so the engine-type preview honors the Engine volume slider. - Drop the cyan source-tag color in the engine combobox; use onSurfaceVariant so it sits behind the selected name. - Rename "Virtual gearbox" to "Gearbox". - Rewrite the engine-sound hint to focus on speed + load mapping; drop the audio-channel sentence. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bbe3317 commit 43491b3

9 files changed

Lines changed: 225 additions & 12 deletions

File tree

app/src/main/java/com/eried/eucplanet/audio/EngineProfile.kt

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,58 @@ data class EngineProfile(
176176
decelPopProbability = 0.2f,
177177
compressionTone = 0.45f
178178
),
179+
// --- Extra ICE flavors ---
180+
EngineProfile(
181+
key = "BOXER_6",
182+
displayName = "Boxer 6",
183+
kind = Kind.ICE,
184+
idleRpm = 850, maxRpm = 7800,
185+
cylinderCount = 6,
186+
// Flat-6 firing pattern (Porsche-ish): even 120° steps in 720° cycle
187+
firingAngles = floatArrayOf(0f, 120f, 240f, 360f, 480f, 600f),
188+
harmonics = floatArrayOf(1f, 0.55f, 0.45f, 0.28f, 0.18f, 0.1f),
189+
exhaustGrit = 0.18f,
190+
decelPopProbability = 0.45f,
191+
compressionTone = 0.5f
192+
),
193+
EngineProfile(
194+
key = "INLINE_6_SILKY",
195+
displayName = "Inline-6 (silky)",
196+
kind = Kind.ICE,
197+
idleRpm = 700, maxRpm = 7000,
198+
cylinderCount = 6,
199+
firingAngles = floatArrayOf(0f, 120f, 240f, 360f, 480f, 600f),
200+
harmonics = floatArrayOf(1f, 0.8f, 0.6f, 0.42f, 0.28f, 0.17f),
201+
exhaustGrit = 0.08f,
202+
decelPopProbability = 0.25f,
203+
compressionTone = 0.4f
204+
),
205+
EngineProfile(
206+
key = "TRACTOR",
207+
displayName = "Tractor",
208+
kind = Kind.ICE, gearless = true,
209+
idleRpm = 450, maxRpm = 2400,
210+
cylinderCount = 1,
211+
firingAngles = floatArrayOf(0f),
212+
harmonics = floatArrayOf(1f, 0.9f, 0.75f, 0.55f, 0.35f, 0.2f),
213+
exhaustGrit = 0.5f,
214+
decelPopProbability = 0.55f,
215+
compressionTone = 0.95f
216+
),
217+
EngineProfile(
218+
key = "STEAM_LOCO",
219+
displayName = "Steam locomotive",
220+
kind = Kind.ICE, gearless = true,
221+
idleRpm = 180, maxRpm = 900,
222+
cylinderCount = 2,
223+
// Two cylinders firing 180° apart — that's the classic "chuff-chuff"
224+
firingAngles = floatArrayOf(0f, 180f),
225+
twoStroke = true,
226+
harmonics = floatArrayOf(1f, 0.95f, 0.85f, 0.7f, 0.5f, 0.3f),
227+
exhaustGrit = 0.7f,
228+
decelPopProbability = 0.0f,
229+
compressionTone = 0.9f
230+
),
179231
// --- Synth / futuristic (gearless) ---
180232
EngineProfile(
181233
key = "TRON_WHINE",
@@ -199,6 +251,61 @@ data class EngineProfile(
199251
exhaustGrit = 0.45f,
200252
decelPopProbability = 0.0f
201253
),
254+
EngineProfile(
255+
key = "LIGHTSABER",
256+
displayName = "Lightsaber",
257+
kind = Kind.SYNTH, gearless = true,
258+
idleRpm = 300, maxRpm = 1800,
259+
synthBaseHz = 55f, synthMaxHz = 220f,
260+
synthShape = 0, // sine — pure hum
261+
synthFmDepth = 0.75f, // heavy modulation gives the wobble
262+
exhaustGrit = 0.02f,
263+
decelPopProbability = 0.0f
264+
),
265+
EngineProfile(
266+
key = "JET_TURBINE",
267+
displayName = "Jet turbine",
268+
kind = Kind.SYNTH, gearless = true,
269+
idleRpm = 1500, maxRpm = 18000,
270+
synthBaseHz = 180f, synthMaxHz = 3800f,
271+
synthShape = 2, // square — bright whine
272+
synthFmDepth = 0.08f, // tight, only a touch of warble
273+
exhaustGrit = 0.55f, // turbine air hiss
274+
decelPopProbability = 0.0f
275+
),
276+
EngineProfile(
277+
key = "UFO_HOVER",
278+
displayName = "UFO hover",
279+
kind = Kind.SYNTH, gearless = true,
280+
idleRpm = 700, maxRpm = 3200,
281+
synthBaseHz = 95f, synthMaxHz = 460f,
282+
synthShape = 0, // sine
283+
synthFmDepth = 1.0f, // huge wobble — the cliche flying-saucer warble
284+
exhaustGrit = 0.1f,
285+
decelPopProbability = 0.0f
286+
),
287+
EngineProfile(
288+
key = "F1_ELECTRIC",
289+
displayName = "F1 electric whine",
290+
kind = Kind.SYNTH, gearless = true,
291+
idleRpm = 800, maxRpm = 20000,
292+
synthBaseHz = 220f, synthMaxHz = 5200f,
293+
synthShape = 1, // saw — rich harmonic content
294+
synthFmDepth = 0.04f,
295+
exhaustGrit = 0.0f,
296+
decelPopProbability = 0.0f
297+
),
298+
EngineProfile(
299+
key = "HOVER_BIKE",
300+
displayName = "Hover bike",
301+
kind = Kind.SYNTH, gearless = true,
302+
idleRpm = 500, maxRpm = 7000,
303+
synthBaseHz = 90f, synthMaxHz = 1400f,
304+
synthShape = 1, // saw + sine FM = thick mid
305+
synthFmDepth = 0.35f,
306+
exhaustGrit = 0.25f,
307+
decelPopProbability = 0.0f
308+
),
202309
// --- Sampled (real recordings, all CC0 from BigSoundBank — see Credits) ---
203310
// For sampled engines the procedural [kind] flags below are unused; the SampledEnginePlayer
204311
// handles render. Idle/max RPM still drive the playback-speed mapping.
@@ -236,6 +343,35 @@ data class EngineProfile(
236343
kind = Kind.ICE, gearless = true,
237344
idleRpm = 850, maxRpm = 6000,
238345
sampleAssetBase = "engine_citycar_saxo"
346+
),
347+
EngineProfile(
348+
key = "SAMPLED_HELICOPTER",
349+
displayName = "Helicopter (sampled)",
350+
kind = Kind.ICE, gearless = true,
351+
// Helicopter rotor "RPM" is metaphorical here — drives playback speed mapping
352+
idleRpm = 250, maxRpm = 1100,
353+
sampleAssetBase = "engine_helicopter"
354+
),
355+
EngineProfile(
356+
key = "SAMPLED_TRACTOR",
357+
displayName = "Tractor (sampled)",
358+
kind = Kind.ICE, gearless = true,
359+
idleRpm = 600, maxRpm = 2600,
360+
sampleAssetBase = "engine_tractor"
361+
),
362+
EngineProfile(
363+
key = "SAMPLED_LAWNMOWER",
364+
displayName = "Lawn mower (sampled)",
365+
kind = Kind.ICE, gearless = true,
366+
idleRpm = 1800, maxRpm = 3600,
367+
sampleAssetBase = "engine_lawnmower"
368+
),
369+
EngineProfile(
370+
key = "SAMPLED_STEAM_LOCO",
371+
displayName = "Steam locomotive (sampled)",
372+
kind = Kind.ICE, gearless = true,
373+
idleRpm = 200, maxRpm = 900,
374+
sampleAssetBase = "engine_steam_loco"
239375
)
240376
)
241377

app/src/main/java/com/eried/eucplanet/audio/EngineSoundEngine.kt

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,28 @@ class EngineSoundEngine @Inject constructor(
122122
voiceActive = active
123123
}
124124

125-
/** Live preview while user is on the settings page (no telemetry yet). */
126-
fun previewProfile(key: String, durationMs: Long = 2500L) {
127-
val saved = profile
125+
/**
126+
* Live preview while user is on the settings page (no telemetry yet).
127+
*
128+
* Optional overrides let the caller A/B-test a specific knob (volume slider,
129+
* muffler/gearbox option) without committing the change first. When null,
130+
* the engine's current state (last [applySettings]) is used.
131+
*/
132+
fun previewProfile(
133+
key: String,
134+
durationMs: Long = 2500L,
135+
volume: Float? = null,
136+
muffler: String? = null,
137+
gearbox: String? = null
138+
) {
139+
val savedProfile = profile
140+
val savedVolume = masterVolume
141+
val savedMuffler = mufflerKey
142+
val savedGearbox = gearboxKey
128143
profile = EngineProfile.byKey(key)
144+
if (volume != null) masterVolume = volume.coerceIn(0f, 1f)
145+
if (muffler != null) mufflerKey = muffler
146+
if (gearbox != null) gearboxKey = gearbox
129147
// Synthesize a small acceleration sweep: idle → mid → idle
130148
Thread {
131149
try {
@@ -145,7 +163,10 @@ class EngineSoundEngine @Inject constructor(
145163
Thread.sleep(250)
146164
stop()
147165
} finally {
148-
profile = saved
166+
profile = savedProfile
167+
masterVolume = savedVolume
168+
mufflerKey = savedMuffler
169+
gearboxKey = savedGearbox
149170
}
150171
}.start()
151172
}

app/src/main/java/com/eried/eucplanet/ui/settings/SettingsScreen.kt

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1951,7 +1951,8 @@ private fun EngineSoundSection(
19511951
"MUFFLED" to stringResource(R.string.engine_muffler_muffled)
19521952
),
19531953
current = settings.engineMuffler,
1954-
onChange = { viewModel.updateEngineMuffler(it) }
1954+
onChange = { viewModel.updateEngineMuffler(it) },
1955+
onPreview = { viewModel.previewMufflerOption(it) }
19551956
)
19561957

19571958
SegmentedChoice(
@@ -1962,7 +1963,8 @@ private fun EngineSoundSection(
19621963
"SIX" to stringResource(R.string.engine_gearbox_six)
19631964
),
19641965
current = settings.engineGearbox,
1965-
onChange = { viewModel.updateEngineGearbox(it) }
1966+
onChange = { viewModel.updateEngineGearbox(it) },
1967+
onPreview = { viewModel.previewGearboxOption(it) }
19661968
)
19671969

19681970
SegmentedChoice(
@@ -2087,7 +2089,7 @@ private fun EngineTypePicker(
20872089
Text(
20882090
sourceLabel,
20892091
style = MaterialTheme.typography.labelSmall,
2090-
color = MaterialTheme.colorScheme.primary
2092+
color = MaterialTheme.colorScheme.onSurfaceVariant
20912093
)
20922094
},
20932095
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
@@ -2212,7 +2214,8 @@ private fun SegmentedChoice(
22122214
label: String,
22132215
options: List<Pair<String, String>>,
22142216
current: String,
2215-
onChange: (String) -> Unit
2217+
onChange: (String) -> Unit,
2218+
onPreview: ((String) -> Unit)? = null
22162219
) {
22172220
Column(modifier = Modifier.padding(vertical = 4.dp)) {
22182221
Text(
@@ -2232,7 +2235,13 @@ private fun SegmentedChoice(
22322235
selected = current == key,
22332236
onClick = { onChange(key) },
22342237
shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size),
2235-
icon = {},
2238+
// When onPreview is wired, the leading icon slot becomes a per-option ▶ button.
2239+
// IconButton consumes the click, so the SegmentedButton onChange doesn't fire.
2240+
icon = {
2241+
if (onPreview != null) {
2242+
PlayButton(onClick = { onPreview(key) })
2243+
}
2244+
},
22362245
label = {
22372246
Text(
22382247
optLabel,

app/src/main/java/com/eried/eucplanet/ui/settings/SettingsViewModel.kt

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,41 @@ class SettingsViewModel @Inject constructor(
131131
fun markEngineSafetyShown() = update { copy(engineSafetyShown = true) }
132132

133133
fun previewEngine(key: String) {
134-
engineSoundEngine.previewProfile(key)
134+
viewModelScope.launch {
135+
val s = settingsRepository.get()
136+
engineSoundEngine.previewProfile(
137+
key = key,
138+
volume = s.engineVolume,
139+
muffler = s.engineMuffler,
140+
gearbox = s.engineGearbox
141+
)
142+
}
143+
}
144+
145+
/** Preview the currently selected engine with [mufflerOverride] applied — for the per-option ▶ buttons. */
146+
fun previewMufflerOption(mufflerOverride: String) {
147+
viewModelScope.launch {
148+
val s = settingsRepository.get()
149+
engineSoundEngine.previewProfile(
150+
key = s.engineType,
151+
volume = s.engineVolume,
152+
muffler = mufflerOverride,
153+
gearbox = s.engineGearbox
154+
)
155+
}
156+
}
157+
158+
/** Preview the currently selected engine with [gearboxOverride] applied — for the per-option ▶ buttons. */
159+
fun previewGearboxOption(gearboxOverride: String) {
160+
viewModelScope.launch {
161+
val s = settingsRepository.get()
162+
engineSoundEngine.previewProfile(
163+
key = s.engineType,
164+
volume = s.engineVolume,
165+
muffler = s.engineMuffler,
166+
gearbox = gearboxOverride
167+
)
168+
}
135169
}
136170
fun updateAutoRecord(v: Boolean) = update { copy(autoRecord = v) }
137171
fun updateAutoRecordStartInMotion(v: Boolean) = update { copy(autoRecordStartInMotion = v) }
154 KB
Binary file not shown.
1.08 MB
Binary file not shown.
3.15 MB
Binary file not shown.
1.46 MB
Binary file not shown.

app/src/main/res/values/strings.xml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@
242242
<!-- Motor sound generator -->
243243
<string name="section_engine_sound">Motor sound</string>
244244
<string name="engine_sound_enabled">Engine sound</string>
245-
<string name="engine_sound_enabled_hint">Synthesised engine driven by your wheel\'s speed and load. Plays through the media channel so it mixes with music.</string>
245+
<string name="engine_sound_enabled_hint">Drives engine RPM in real time from your wheel\'s speed and motor load. Higher load pushes the revs up, coasting and regen pull them back.</string>
246246
<string name="engine_safety_title">Heads up</string>
247247
<string name="engine_safety_message">Engine sound can mask traffic noise. Use lower volumes in city riding, and consider a single-ear headphone so you stay aware of your surroundings.</string>
248248
<string name="engine_safety_ok">Got it</string>
@@ -255,7 +255,7 @@
255255
<string name="engine_muffler_open">Open</string>
256256
<string name="engine_muffler_half">Half</string>
257257
<string name="engine_muffler_muffled">Muffled</string>
258-
<string name="engine_gearbox_label">Virtual gearbox</string>
258+
<string name="engine_gearbox_label">Gearbox</string>
259259
<string name="engine_gearbox_off">Off</string>
260260
<string name="engine_gearbox_four">4-speed</string>
261261
<string name="engine_gearbox_six">6-speed</string>
@@ -286,13 +286,26 @@
286286
<string name="engine_preset_v8_muscle">V8 muscle</string>
287287
<string name="engine_preset_v12_italian">V12 Italian</string>
288288
<string name="engine_preset_v16_luxury">V16 luxury</string>
289+
<string name="engine_preset_boxer_6">Boxer 6</string>
290+
<string name="engine_preset_inline_6_silky">Inline-6 (silky)</string>
291+
<string name="engine_preset_tractor">Tractor</string>
292+
<string name="engine_preset_steam_loco">Steam locomotive</string>
289293
<string name="engine_preset_tron_whine">Tron whine</string>
290294
<string name="engine_preset_pod_racer">Pod racer</string>
295+
<string name="engine_preset_lightsaber">Lightsaber</string>
296+
<string name="engine_preset_jet_turbine">Jet turbine</string>
297+
<string name="engine_preset_ufo_hover">UFO hover</string>
298+
<string name="engine_preset_f1_electric">F1 electric whine</string>
299+
<string name="engine_preset_hover_bike">Hover bike</string>
291300
<string name="engine_preset_sampled_v8_cobra">V8 Cobra</string>
292301
<string name="engine_preset_sampled_vtwin_ducati">V-twin Ducati</string>
293302
<string name="engine_preset_sampled_diesel_iveco">Diesel truck</string>
294303
<string name="engine_preset_sampled_motorcycle">Motorcycle</string>
295304
<string name="engine_preset_sampled_city_car">City car</string>
305+
<string name="engine_preset_sampled_helicopter">Helicopter</string>
306+
<string name="engine_preset_sampled_tractor">Tractor</string>
307+
<string name="engine_preset_sampled_lawnmower">Lawn mower</string>
308+
<string name="engine_preset_sampled_steam_loco">Steam locomotive</string>
296309

297310
<string name="col_periodic">Periodic</string>
298311
<string name="col_trigger">Trigger</string>

0 commit comments

Comments
 (0)