Skip to content

About

Combustion engine simulator that generates realistic audio.

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Latest commit

 

History

214 Commits

Folders and files

Repository files navigation

Engine Sim Lite

A lightweight, cross-platform rebuild of AngeTheGreat's Engine Sim. The original physics solver, dynamic audio synthesis pipeline, and .mr engine-scripting loader are carried over unmodified (see include/, src/, scripting/) — only the Windows/DirectX "Delta Studio" rendering layer has been stripped out and replaced with a minimal SDL2 + Dear ImGui shell (app/).

UI

  • Window is freely resizable (drag any edge/corner) with a 420x480 minimum — the single ImGui panel re-reads the actual OS window size every frame and rebuilds its layout to fill it, so scaling happens live while dragging, not just on next launch. Runs without SDL_WINDOW_ALLOW_HIGHDPI on purpose: with it set, the window's point-size and the renderer's actual pixel-size can diverge on a Retina display, and any mismatch between when each gets queried renders the whole UI at the wrong scale (content stuck in a quarter of the window, rest black) — leaving HighDPI off keeps the two always exactly 1:1, at the cost of a slightly soft (not crisp-Retina) render for this small utility GUI. An SDL_AddEventWatch callback also keeps rendering during an active resize drag itself (macOS otherwise blocks the whole event loop until you let go of the window edge)
  • Live engine RPM
  • Throttle slider, a "GAS" pedal button (hold to accelerate, let off to return to idle), plus the original Q/W/E/R throttle-preset keys, and hold S to crank the starter. Every input source sets the throttle directly with no app-level ramp/interpolation — GAS held snaps straight to full, released snaps straight back to idle
  • Idle RPM slider (500-2500). Idle isn't app-managed anywhere in the original engine-sim (confirmed by grepping the upstream app source — it never references "idle"); it's entirely emergent from Intake::process()'s constant idle-circuit flow (idle_flow_rate), independent of the main throttle plate and tuned per engine script — there's no direct "set idle RPM" knob in the physics core to bind a slider to. So this works by closed-loop control instead: the app's own idle-throttle floor is nudged up or down every ~2 seconds based on which way actual RPM is off from the slider's target, the same way a real idle-air-control valve hunts for a setpoint. It reacts slowly on purpose (matching how long this engine's own RPM takes to settle after a throttle change; correcting harder/faster just fights that lag and can overshoot into a stall), plus a separate, much faster-reacting safety net that pulls throttle back up the instant RPM actually drops near stalling, regardless of the target or the normal correction cadence — verified by testing an earlier, untethered version of this controller straight into a dead stall on N/A V12 (chasing a target below what it can sustain) and confirming it just sat there raising throttle uselessly forever afterward, same as a real stalled engine not restarting itself. Some engines can't sustain every value on the slider — they'll settle short of an unreachably low target rather than actually stalling chasing it. A screaming F1 V10 redlining at 18,300 RPM may still legitimately struggle to idle even so — same as the real car historically did
  • Gear readout with Shift Up/Shift Down buttons (or the ↑/↓ arrow keys — one shift per press, not continuous while held), driving the original (unmodified) Transmission class. Clutch is fully locked whenever in gear (matching the original's own default — it only ever slips there if you hold its manual clutch-pedal keys), except for a brief ~250ms cut-and- ramp right after each shift to simulate the mechanical time a real shift takes rather than slamming straight to full lockup at the new gear ratio. Locking the rest of the time (instead of the throttle-proportional slip an earlier version used) keeps the engine and vehicle coupled through the gear ratio while coasting off-throttle, so lifting off at speed shows real engine braking instead of the crank snapping back to idle as if the vehicle had no inertia
  • Selected engine readout
  • "Select Engine" button — lists every self-contained .mr scene under assets/engines/ by its own in-script engine name (parsed from the scene's engine(name: "...", ...) call, not the asset file path): four naturally-aspirated engines ship by default (N/A V12, Formula 1 V10, N/A I6, N/A V8)
  • Simulation frequency slider (400 Hz – 400,000 Hz)
  • Engine load slider — a continuous float from 0.00 to 1.00 (not a toggle), applied as a proportional drag via the (unmodified) Dynamometer, same class the original app's dyno/load- simulation cluster used. It targets a fraction below the engine's own current speed each instant rather than always braking toward a dead stop (the constraint solver saturates at the torque ceiling whenever it can't fully satisfy its velocity target, so a fixed zero-speed target made any nonzero load either do nothing or stall the engine outright, never anything smooth in between)
  • 5-band graphic equalizer (60 Hz / 250 Hz / 1 kHz / 4 kHz / 12 kHz, ±24/+12 dB) — a new post-processing stage (app/equalizer.h/.cpp) applied to the final PCM output in the audio callback, not part of the original engine-sim DSP
  • External Control panel (button in the main window): three tabs driving the engine directly, in-process — no second app, no network round-trip. Simulated (a spring-loaded accelerator slider driving a small simulated engine + 6-speed automatic transmission, smooth ~0.45s shifts, 9000 RPM shift point, harder to accelerate each gear up), Manual (direct RPM/load sliders, sent as-is), and OBD2 (reads live RPM and throttle position from a real BMW F3x over its factory ENET diagnostic port — protocol verified live against a B48 LCI, see app/obd2_client.cpp's header comment for the full reverse-engineering trail — and forwards them, RPM scaled 1.3x so the B48's 7000 RPM redline lines up with the V12's 9100 RPM redline). RPM is forced via the dynamometer's own hold mode (minimal/zero smoothing), load drives the throttle plate; closing the panel hands control back to the local GUI/keyboard immediately. A Blend In slider controls how long (0.1-5s) the engine's audio takes to fade in once RPM/throttle clear a threshold (OBD2: real RPM > 2400 and throttle > 60%; Simulated: RPM > 3000 and load > 60%; Manual isn't gated) — the engine itself is always driven continuously either way, only whether you actually hear it is gated. The app also still listens on UDP 127.0.0.1:50505 (app/external_control.h) for any third-party sender that wants to drive it the old way, independent of this panel.
  • Batch Renderer panel (button in the main window) plus an Optimized playback mode: the live physics/audio core pegs a full CPU core, which is the whole reason this exists — a "Generate .RKF" pipeline (app/rkf/offline_renderer.h/.cpp) drives the same core faster than real time to record real acceleration sweeps (1000 RPM to the engine's own redline, at (redline-1000)/1500 seconds each) across a throttle x load grid, plus fixed-RPM hold recordings for steady cruise, at one of four quality presets (LOW 4x4 up to HQ 32x32 — see app/rkf/rkf_format.h). Everything is packed into a single .rkf container file (raw PCM, metadata, and the original .mr script text, all in one place) via app/rkf/rkf_writer.cpp. Loading a .rkf and switching to Optimized mode plays it back through app/rkf/rkf_player.cpp (nearest-grid-cell selection with a short crossfade, plus its own virtual-RPM model driven by throttle) at near-zero CPU, driven by the same gas pedal/load slider as Real-Time mode; flipping back to Real-Time mode recompiles the embedded script and runs the normal physics core instead — same file, either way. External Control's Simulated/Manual/OBD2 tabs (they force a literal target RPM onto a real physics sim) stay Real-Time-only. This is a v1: cell selection isn't a continuous bilinear blend and crossfades aren't phase-aligned to the outgoing combustion cycle yet — see the comments atop app/rkf/rkf_player.h for exactly what's simplified and why.

Building

Prerequisites

  • CMake 3.16+
  • A C++17 compiler
  • SDL2 development headers
  • Flex 2.6+ and Bison 3.2+ (needed to build the Piranha .mr script compiler)

macOS:

brew install sdl2 flex bison

Linux (Debian/Ubuntu):

sudo apt install libsdl2-dev flex bison cmake build-essential

macOS note: Apple ships an ancient GPLv2 bison (2.3) and its own patched flex build that are both incompatible with Piranha's grammar/scanner. dependencies/CMakeLists.txt automatically prefers Homebrew's bison/flex (brew install bison flex) over the system ones when present.

Build

cmake -B build -S . -DCMAKE_POLICY_VERSION_MINIMUM=3.5
cmake --build build -j

(CMAKE_POLICY_VERSION_MINIMUM=3.5 works around a very old cmake_minimum_required pinned inside the vendored GoogleTest snapshot used for unit tests; it has no effect on this project's own code.)

Run

./build/engine-sim-lite

External control (Simulated/Manual/OBD2 telemetry) is built into engine-sim-lite itself — see the "External Control" button in its own window, no separate binary to run alongside it.

Everything else (simple-2d-constraint-solver, csv-io, piranha, Dear ImGui) is vendored under dependencies/submodules/ and built from source — no other dependencies to install.

Tests

./build/engine-sim-test

Runs the original (unmodified) gas_system/function/synthesizer unit tests against the ported physics/audio core.

Android

engine-sim-lite also builds as an Android APK, reusing this same top-level CMakeLists.txt (see its if (ANDROID) branches) via Gradle's CMake integration under android/. SDL2's own Android support handles the JNI/window/audio glue; the only app code that differs is app/main.cpp's startup, which on Android extracts the assets/+es/ trees bundled into the APK (android/app/src/main/assets/) out to the app's private storage on first launch, then chdirs there -- after that every relative-path file access in the untouched engine-sim/piranha code works exactly as it does on desktop, since the process's cwd is a real filesystem in both cases.

Prerequisites

  • A JDK (17+)
  • Android SDK cmdline-tools/platform-tools, NDK (28+ -- tested with 28.2.13676358), and the SDK's own CMake package, all installable via sdkmanager. minSdkVersion is 28 (Android 9): piranha's standard_allocator.h calls std::aligned_alloc, only present in bionic libc from API 28 on.
  • Everything the desktop build needs too (flex/bison, since piranha's grammar is generated by the same host-run flex/bison regardless of target platform)

Build

cd android
./gradlew assembleDebug

Produces android/app/build/outputs/apk/debug/app-debug.apk. Install with adb install -r android/app/build/outputs/apk/debug/app-debug.apk, or open android/ in Android Studio.

Notes

  • No forced orientation -- the UI is a single vertical stack of controls (throttle, idle-RPM, gear buttons, sim-frequency, engine load, equalizer), which already suits a phone's portrait aspect better than a forced landscape lock. Whatever orientation the OS gives the app, the existing per-frame SetNextWindowSize(ImGui::GetIO().DisplaySize) layout (the same mechanism driving desktop's live-resize support) fills it.
  • Porting to Android surfaced (and fixed) two real, pre-existing, platform-independent bugs in the vendored dependencies that desktop's Apple-clang build happened not to trigger: simple-2d-constraint-solver/piranha's __forceinline/__int64 MSVC-only macros were only ever given a fallback for __APPLE__, not other non-MSVC compilers (fixed by keying the fallback on _MSC_VER instead); and PistonEngineSimulator::writeToSynthesizer() wrote a per-cylinder value into a hardcoded 8-element array regardless of actual cylinder count -- a real global-buffer-overflow for any engine with more than 8 cylinders (e.g. the bundled V12), caught with AddressSanitizer and fixed by deleting the array (it was write-only, never read).

About

Combustion engine simulator that generates realistic audio.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages