Skip to content

Commit 83e421f

Browse files
eriedclaude
andcommitted
Motor sound: wheel-aware speed curve, About polish, i18n
Engine RPM mapping - New maxSpeedRefKmh field on EngineSoundEngine, wired from WheelRepository.maxSpeedCap, so the speed→RPM curve uses the actual wheel cap instead of a hardcoded 80 km/h. V11 (50 km/h cap), V14 (~100), P6 (~150) each get a curve that runs from idle at 0 km/h to redline near their own top speed. - Gearless mapping switched from linear to sqrt so EUC cruising speeds (10-30 km/h) actually rev the engine — on a 100 km/h wheel, 20 km/h now sits at ~45% rev band instead of 25%, and gearbox per-gear bands scale with the wheel as well. - Gearbox preview scenario uses the same wheel-aware top speed. - Reset smoothing (smoothedRpm/Load, brake/decel envelopes, lastPwm, lastTelemetryAtMs) on preview exit so a real ride starting right after doesn't inherit scenario state. - Drop dead `lastSpeedAtMs` and unused `kotlin.math.max` import. About dialog - Bump max height 680dp → 820dp (~20% taller). - Thin horizontal divider above each subtitle ("Thanks to:" and "Resources & libraries:"), subtitles bumped from titleSmall to titleMedium so the two tables read as discrete sections. - Expand the BigSoundBank credit so every sampled engine in the picker is named. Localization - Add 37 motor-sound UI strings across all 13 locales (es, es-419, de, fr, it, nl, pl, pt-rBR, ru, sv, da, no, uk, zh): section title, description, safety dialog, engine type + source tags, volume, muffler / gearbox / when-parked / decel / engine-brake / voice-duck rows + their options, headphones-only switch. - Each locale now at 435 strings (default 477). Engine preset names (V8 muscle, V12 Italian, Cobra, Aston Martin, etc.) stay English and fall back via Android; they're proper nouns / brand names. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2d13a9b commit 83e421f

17 files changed

Lines changed: 603 additions & 16 deletions

File tree

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

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import javax.inject.Inject
1414
import javax.inject.Singleton
1515
import kotlin.concurrent.thread
1616
import kotlin.math.abs
17-
import kotlin.math.max
17+
import kotlin.math.sqrt
1818

1919
/**
2020
* Motor sound generator. Synthesises a virtual engine driven by real-time
@@ -50,6 +50,13 @@ class EngineSoundEngine @Inject constructor(
5050
private val synth = EngineSynth(sampleRate)
5151
private val sampledPlayer = SampledEnginePlayer(context)
5252

53+
/**
54+
* Reference top speed used to map km/h → rev band. Updated from
55+
* WheelRepository.maxSpeedCap so a 50 km/h wheel revs out at 50 km/h and
56+
* a 150 km/h wheel takes longer to hit redline.
57+
*/
58+
@Volatile private var maxSpeedRefKmh: Float = 90f
59+
5360
// --- Main-thread state used to derive params from raw telemetry ---
5461
private var profile: EngineProfile = EngineProfile.byKey("FOUR_STROKE_SINGLE")
5562
private var masterVolume: Float = 0.6f
@@ -66,7 +73,6 @@ class EngineSoundEngine @Inject constructor(
6673
private var lastPwm: Float = 0f
6774
private var pwmEverNonZero: Boolean = false // becomes true once we see real PWM, so we don't fall back forever
6875
private var lastSpeedKmh: Float = 0f
69-
private var lastSpeedAtMs: Long = 0L
7076
private var lastMovingAtMs: Long = 0L
7177
private var lastTelemetryAtMs: Long = 0L
7278
private var idleEnvelope: Float = 0f // 0 = silent, 1 = full idle
@@ -122,6 +128,14 @@ class EngineSoundEngine @Inject constructor(
122128
voiceActive = active
123129
}
124130

131+
/**
132+
* Tell the engine what the wheel's top speed is so the speed→RPM map matches
133+
* the wheel. WheelService wires this to WheelRepository.maxSpeedCap.
134+
*/
135+
fun setMaxSpeedRef(kmh: Float) {
136+
if (kmh > 5f) maxSpeedRefKmh = kmh
137+
}
138+
125139
/**
126140
* Live preview while user is on the settings page (no telemetry yet).
127141
*
@@ -172,6 +186,14 @@ class EngineSoundEngine @Inject constructor(
172186
mufflerKey = savedMuffler
173187
gearboxKey = savedGearbox
174188
pwmEverNonZero = savedPwmEverNonZero
189+
// Reset smoothing state so the next real telemetry tick doesn't carry over
190+
// the scenario's rpm/load — avoids a transient when the user goes riding right after.
191+
smoothedRpm = 0f
192+
smoothedLoad = 0f
193+
brakeEnvelope = 0f
194+
decelEnvelope = 0f
195+
lastPwm = 0f
196+
lastTelemetryAtMs = 0L
175197
}
176198
}.start()
177199
}
@@ -191,12 +213,12 @@ class EngineSoundEngine @Inject constructor(
191213
}
192214
}
193215

194-
/** Speed sweep 0..80 km/h so the gearbox actually crosses gear thresholds. */
216+
/** Speed sweep 0..maxSpeedRef km/h so the gearbox actually crosses gear thresholds. */
195217
private fun runGearboxScenario(durationMs: Long) {
196-
// computeTargetRpm assumes 0..80 km/h band, so we span that.
218+
val topKmh = maxSpeedRefKmh
197219
val steps = 50
198220
for (i in 0..steps) {
199-
val s = 80f * (i.toFloat() / steps)
221+
val s = topKmh * (i.toFloat() / steps)
200222
pushTelemetry(speedKmh = s, pwmPercent = 55f)
201223
Thread.sleep(durationMs / steps)
202224
}
@@ -331,26 +353,27 @@ class EngineSoundEngine @Inject constructor(
331353
voiceDuckGain += (targetDuck - voiceDuckGain) * (dt * 10f).coerceAtMost(1f)
332354

333355
lastSpeedKmh = speedKmh
334-
lastSpeedAtMs = now
335356

336357
emit()
337358
}
338359

339360
private fun computeTargetRpm(speedKmh: Float, load: Float): Float {
340361
val absSpeed = abs(speedKmh)
362+
val maxRef = maxSpeedRefKmh
341363
val gearless = profile.gearless || gearboxKey == "OFF"
342364
val gearCount = when (gearboxKey) {
343365
"SIX" -> 6
344366
"FOUR" -> 4
345367
else -> 0
346368
}
347369
val baseFrac = if (gearless || gearCount == 0) {
348-
// Linear map speed → 0..1 across an assumed 0..80 km/h band
349-
(absSpeed / 80f).coerceIn(0f, 1f)
370+
// sqrt curve against the wheel's top speed — typical EUC cruise (10-30 km/h) is a
371+
// small fraction of a fast wheel's max (90-150). Linear would leave the engine at
372+
// idle most of the time; sqrt pushes 20 km/h on a 100 km/h wheel up to ~45% rev band.
373+
sqrt((absSpeed / maxRef).coerceIn(0f, 1f))
350374
} else {
351375
// Gearbox: each gear covers a band of speed; RPM ramps 0..1 within each, then drops on shift.
352-
val maxSpeed = 80f
353-
val perGear = maxSpeed / gearCount
376+
val perGear = maxRef / gearCount
354377
val gearIdx = (absSpeed / perGear).toInt().coerceAtMost(gearCount - 1)
355378
val withinGear = (absSpeed - gearIdx * perGear) / perGear
356379
withinGear.coerceIn(0f, 1f)

app/src/main/java/com/eried/eucplanet/service/WheelService.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ class WheelService : LifecycleService() {
120120
}
121121
}
122122

123+
// Speed→RPM mapping uses the wheel's top speed as the reference so 30 km/h on a
124+
// V11 (max 50) revs harder than 30 km/h on a P6 (max 150).
125+
lifecycleScope.launch {
126+
wheelRepository.maxSpeedCap.collect { cap ->
127+
engineSoundEngine.setMaxSpeedRef(cap)
128+
}
129+
}
130+
123131
// Start periodic voice announcements
124132
startVoiceLoop()
125133

app/src/main/java/com/eried/eucplanet/ui/dashboard/DashboardScreen.kt

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,7 @@ fun DashboardScreen(
828828
Surface(
829829
modifier = Modifier
830830
.fillMaxWidth(0.95f)
831-
.heightIn(max = 680.dp),
831+
.heightIn(max = 820.dp),
832832
shape = RoundedCornerShape(20.dp),
833833
color = MaterialTheme.colorScheme.surface
834834
) {
@@ -1000,12 +1000,17 @@ fun DashboardScreen(
10001000
color = MaterialTheme.colorScheme.onSurfaceVariant
10011001
)
10021002
Spacer(Modifier.height(12.dp))
1003+
androidx.compose.material3.HorizontalDivider(
1004+
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.4f),
1005+
thickness = 1.dp
1006+
)
1007+
Spacer(Modifier.height(10.dp))
10031008
Text(
10041009
"Thanks to:",
1005-
style = MaterialTheme.typography.titleSmall,
1010+
style = MaterialTheme.typography.titleMedium,
10061011
color = MaterialTheme.colorScheme.onSurface
10071012
)
1008-
Spacer(Modifier.height(4.dp))
1013+
Spacer(Modifier.height(6.dp))
10091014
// Credits table. Two columns — name on the left,
10101015
// why on the right — no headers since the format
10111016
// is self-evident. Hardcoded English because the
@@ -1046,14 +1051,19 @@ fun DashboardScreen(
10461051
}
10471052
}
10481053
Spacer(Modifier.height(14.dp))
1054+
androidx.compose.material3.HorizontalDivider(
1055+
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.4f),
1056+
thickness = 1.dp
1057+
)
1058+
Spacer(Modifier.height(10.dp))
10491059
Text(
10501060
"Resources & libraries:",
1051-
style = MaterialTheme.typography.titleSmall,
1061+
style = MaterialTheme.typography.titleMedium,
10521062
color = MaterialTheme.colorScheme.onSurface
10531063
)
1054-
Spacer(Modifier.height(4.dp))
1064+
Spacer(Modifier.height(6.dp))
10551065
val resources = listOf(
1056-
"BigSoundBank — engine samples" to "Joseph SARDIN. CC0 / public domain. V8 Cobra, V-twin Ducati, diesel truck, motorcycle and city car recordings used in the Motor sound generator.",
1066+
"BigSoundBank — engine samples" to "Joseph SARDIN. CC0 / public domain. All sampled engines in the Motor sound generator (V8 Cobra, V-twin Ducati, diesel truck, motorcycle, city car, helicopter, tractor, lawn mower, steam locomotive, Aston Martin, big diesel, car cruise, broken exhaust, quad ATV).",
10571067
"Jetpack Compose, Material 3" to "Google. Apache 2.0. UI toolkit and design system.",
10581068
"Hilt, Room, WorkManager, Navigation" to "Google. Apache 2.0. DI, persistence, background jobs, navigation graph.",
10591069
"Kotlin & coroutines" to "JetBrains. Apache 2.0. Language and structured concurrency.",

app/src/main/res/values-b+es+419/strings.xml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,4 +459,43 @@
459459
<string name="watch_screen_button_2">Botón 2</string>
460460
<string name="watch_haptic_on_action">Vibrar al activar</string>
461461
<string name="watch_haptic_on_action_desc">Vibración breve del reloj cada vez que se dispara una acción de los botones</string>
462+
463+
<!-- Motor sound generator -->
464+
<string name="section_engine_sound">Sonido del motor</string>
465+
<string name="engine_sound_enabled">Sonido del motor</string>
466+
<string name="engine_sound_enabled_hint">Las RPM del motor siguen en tiempo real la velocidad y la carga de tu rueda. Más carga sube las vueltas, soltar el acelerador y la regeneración las bajan.</string>
467+
<string name="engine_safety_title">Atención</string>
468+
<string name="engine_safety_message">El sonido del motor puede tapar el ruido del tráfico. Usa volúmenes bajos en la ciudad y considera un auricular en un solo oído para mantenerte atento al entorno.</string>
469+
<string name="engine_safety_ok">Entendido</string>
470+
<string name="engine_type_label">Tipo de motor</string>
471+
<string name="engine_preview">Vista previa</string>
472+
<string name="engine_source_sampled">Muestra</string>
473+
<string name="engine_source_synth">Sintet.</string>
474+
<string name="engine_volume">Volumen del motor</string>
475+
<string name="engine_muffler_label">Escape</string>
476+
<string name="engine_muffler_open">Abierto</string>
477+
<string name="engine_muffler_half">Medio</string>
478+
<string name="engine_muffler_muffled">Silenciado</string>
479+
<string name="engine_gearbox_label">Caja de cambios</string>
480+
<string name="engine_gearbox_off">Sin</string>
481+
<string name="engine_gearbox_four">4 marchas</string>
482+
<string name="engine_gearbox_six">6 marchas</string>
483+
<string name="engine_idle_label">Cuando está detenido</string>
484+
<string name="engine_idle_always">Ralentí continuo</string>
485+
<string name="engine_idle_fade">Desvanecer</string>
486+
<string name="engine_idle_moving">Solo en movimiento</string>
487+
<string name="engine_decel_label">Al desacelerar</string>
488+
<string name="engine_decel_smooth">Suave</string>
489+
<string name="engine_decel_standard">Estándar</string>
490+
<string name="engine_decel_backfire">Petardeo</string>
491+
<string name="engine_brake_label">Freno motor</string>
492+
<string name="engine_brake_off">Apagado</string>
493+
<string name="engine_brake_light">Ligero</string>
494+
<string name="engine_brake_strong">Fuerte</string>
495+
<string name="engine_duck_label">Cuando habla la voz</string>
496+
<string name="engine_duck_duck">Bajar</string>
497+
<string name="engine_duck_pause">Pausar</string>
498+
<string name="engine_duck_mix">Mezclar</string>
499+
<string name="engine_headphones_only">Solo con auriculares</string>
500+
<string name="engine_headphones_only_hint">Omite el altavoz del teléfono. El motor suena solo cuando hay audio por cable o Bluetooth.</string>
462501
</resources>

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,4 +423,43 @@
423423
<string name="watch_screen_button_2">Knap 2</string>
424424
<string name="watch_haptic_on_action">Vibrér ved handling</string>
425425
<string name="watch_haptic_on_action_desc">Kort vibration på uret hver gang en knaphandling udløses</string>
426+
427+
<!-- Motor sound generator -->
428+
<string name="section_engine_sound">Motorlyd</string>
429+
<string name="engine_sound_enabled">Motorlyd</string>
430+
<string name="engine_sound_enabled_hint">Motoromdrejningerne følger i realtid hjulets hastighed og motorbelastning. Mere belastning hæver omdrejningerne, udrul og regen trækker dem ned igen.</string>
431+
<string name="engine_safety_title">Obs</string>
432+
<string name="engine_safety_message">Motorlyd kan dække trafikstøj. Hold volumen lav i byen og overvej kun ét høretelefonstykke, så du stadig hører omgivelserne.</string>
433+
<string name="engine_safety_ok">OK</string>
434+
<string name="engine_type_label">Motortype</string>
435+
<string name="engine_preview">Forhåndsvis</string>
436+
<string name="engine_source_sampled">Sample</string>
437+
<string name="engine_source_synth">Synt.</string>
438+
<string name="engine_volume">Motorlydstyrke</string>
439+
<string name="engine_muffler_label">Lyddæmper</string>
440+
<string name="engine_muffler_open">Åben</string>
441+
<string name="engine_muffler_half">Halv</string>
442+
<string name="engine_muffler_muffled">Dæmpet</string>
443+
<string name="engine_gearbox_label">Gearkasse</string>
444+
<string name="engine_gearbox_off">Fra</string>
445+
<string name="engine_gearbox_four">4-gears</string>
446+
<string name="engine_gearbox_six">6-gears</string>
447+
<string name="engine_idle_label">Når parkeret</string>
448+
<string name="engine_idle_always">Altid tomgang</string>
449+
<string name="engine_idle_fade">Fade ud</string>
450+
<string name="engine_idle_moving">Kun i bevægelse</string>
451+
<string name="engine_decel_label">Ved opbremsning</string>
452+
<string name="engine_decel_smooth">Blød</string>
453+
<string name="engine_decel_standard">Standard</string>
454+
<string name="engine_decel_backfire">Knald</string>
455+
<string name="engine_brake_label">Motorbremse</string>
456+
<string name="engine_brake_off">Fra</string>
457+
<string name="engine_brake_light">Let</string>
458+
<string name="engine_brake_strong">Stærk</string>
459+
<string name="engine_duck_label">Når stemmen taler</string>
460+
<string name="engine_duck_duck">Sænk</string>
461+
<string name="engine_duck_pause">Pause</string>
462+
<string name="engine_duck_mix">Miks</string>
463+
<string name="engine_headphones_only">Kun med hovedtelefoner</string>
464+
<string name="engine_headphones_only_hint">Springer telefonens højttaler over. Motor spiller kun, når kablet eller Bluetooth-lyd er tilsluttet.</string>
426465
</resources>

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,4 +439,43 @@
439439
<string name="watch_screen_button_2">Taste 2</string>
440440
<string name="watch_haptic_on_action">Bei Aktion vibrieren</string>
441441
<string name="watch_haptic_on_action_desc">Kurze Vibration der Uhr, sobald eine Tasten-Aktion ausgelöst wird</string>
442+
443+
<!-- Motor sound generator -->
444+
<string name="section_engine_sound">Motorklang</string>
445+
<string name="engine_sound_enabled">Motorklang</string>
446+
<string name="engine_sound_enabled_hint">Die Motordrehzahl folgt in Echtzeit der Geschwindigkeit und Motorlast deines Rads. Mehr Last dreht hoch, Ausrollen und Regen ziehen sie wieder zurück.</string>
447+
<string name="engine_safety_title">Achtung</string>
448+
<string name="engine_safety_message">Motorklang kann den Straßenverkehr überdecken. Im Stadtverkehr leiser stellen und ein einzelnes Headset erwägen, damit du die Umgebung weiter wahrnimmst.</string>
449+
<string name="engine_safety_ok">Verstanden</string>
450+
<string name="engine_type_label">Motortyp</string>
451+
<string name="engine_preview">Vorschau</string>
452+
<string name="engine_source_sampled">Sample</string>
453+
<string name="engine_source_synth">Synth</string>
454+
<string name="engine_volume">Motorlautstärke</string>
455+
<string name="engine_muffler_label">Auspuff</string>
456+
<string name="engine_muffler_open">Offen</string>
457+
<string name="engine_muffler_half">Halb</string>
458+
<string name="engine_muffler_muffled">Gedämpft</string>
459+
<string name="engine_gearbox_label">Getriebe</string>
460+
<string name="engine_gearbox_off">Aus</string>
461+
<string name="engine_gearbox_four">4-Gang</string>
462+
<string name="engine_gearbox_six">6-Gang</string>
463+
<string name="engine_idle_label">Im Stand</string>
464+
<string name="engine_idle_always">Immer Leerlauf</string>
465+
<string name="engine_idle_fade">Ausblenden</string>
466+
<string name="engine_idle_moving">Nur in Bewegung</string>
467+
<string name="engine_decel_label">Beim Verzögern</string>
468+
<string name="engine_decel_smooth">Sanft</string>
469+
<string name="engine_decel_standard">Standard</string>
470+
<string name="engine_decel_backfire">Fehlzündung</string>
471+
<string name="engine_brake_label">Motorbremse</string>
472+
<string name="engine_brake_off">Aus</string>
473+
<string name="engine_brake_light">Leicht</string>
474+
<string name="engine_brake_strong">Stark</string>
475+
<string name="engine_duck_label">Bei Sprachausgabe</string>
476+
<string name="engine_duck_duck">Leiser</string>
477+
<string name="engine_duck_pause">Pause</string>
478+
<string name="engine_duck_mix">Mischen</string>
479+
<string name="engine_headphones_only">Nur mit Kopfhörern</string>
480+
<string name="engine_headphones_only_hint">Spielt nicht über den Lautsprecher. Motorklang läuft nur, wenn kabelgebundenes oder Bluetooth-Audio verbunden ist.</string>
442481
</resources>

0 commit comments

Comments
 (0)