Source: https://oliviercouthaud.com/enginesim · Status: In progress · 2026 · Tags: Rust, WebAssembly, Acoustics, GPU, Tauri # EngineSim An engine sound simulator that works the sound out from how gas flows through the engine, instead of playing back recordings. [Image: Spectrogram of a simulated four-cylinder dyno pull: harmonic lines fan out as speed rises from 1500 rpm to the redline, then fall as speed is ramped back down, with a plot of level and rpm below] Figure: What the simulator's output looks like for a generic four-cylinder engine revving up and then back down. Time runs left to right, pitch goes up, brighter means louder. Nothing here is a recording. It shows the sound has structure, not whether it sounds right. ## What I wanted I got into this through [engine-sim community edition](https://github.com/Engine-Simulator/engine-sim-community-edition), the open-source engine simulator that inspired it. The community edition carries on the original engine-sim, which simulates the cylinders as lumped volumes of gas, then takes the pressure in the runners, low-passes it at 1.9 kHz and convolves it with a hand-picked impulse response. So the pipe acoustics are canned. I wanted to simulate the pipes as well, so that torque, exhaust resonances and the crossplane burble come out of the model instead of being drawn in. Most engine sound in games and tools is recordings, played back and blended, which is the other thing I wanted to get away from. Before this I had a JavaScript synthesizer whose cylinder pulses are hand-shaped. It covers 26 vehicles, including turbos, superchargers and jets, and it is still useful as a sketchpad. The Rust simulator is the version where the pulses are the result of actual gas flow. ## How it works Each cylinder is a thermodynamic gas volume: slider-crank geometry, a Wiebe curve for how fast the fuel burns, heat loss to the walls. Every intake and exhaust pipe solves compressible one-dimensional flow, and valves, throttle, plenums, collectors and mufflers are the boundaries between them. The sound is the pressure radiated from the open ends, plus flow noise at the valves, throttle and tailpipe, plus structure noise from valve and piston impacts and combustion pressure. All of it is driven by the simulated flows and pressures. Nothing is looped. **What does one-dimensional mean here?** A pipe is long and thin, so instead of tracking the air in three dimensions I only track pressure, speed and temperature along its length, in a row of small cells. Each step, neighbouring cells swap mass, momentum and energy. That is cheap, and it is enough for sound, because the waves that matter travel along the pipe. **What are MUSCL-Hancock and HLLC?** Two pieces of maths for one job: working out what flows between two neighbouring cells when they hold gas at different pressures. HLLC is an approximate solution of that jump between two states. MUSCL-Hancock makes the values inside each cell vary smoothly, so that sharp pressure fronts stay sharp instead of smearing out. Figure: Diagram. Signal flow in the simulator, with the component names I use in the code. Tap or click a box for a one-line explanation, or tab to it. The orange block is the part that replaces the recorded impulse response. The gas solver runs on its own 32 kHz clock and its grid resolves roughly 2 kHz, with the cell size following the speed of sound in each pipe, so hot pipes cost less. Above that, the sound comes from source models: the standard valve-noise regimes from IEC 60534-8-3, Lighthill jet noise at the tailpipe, and a statistical-energy model of the block. Most of their constants are scaled from physics. A few, like the casting loss factors, the 15 per cent impact share and the silencer volumes, were fitted to recordings in the early rounds, and the later fit rounds (more on that below) left them as they were. For cars, how the sound gets around the body comes from a 3-D wave simulation that I run once on the GPU, about three minutes on my 5070 Ti, and bake into the simulator. [Image: Three rows of paired plots. Left, waveforms in pascals at 1 m for the tailpipe wave, jet, valve, block and intake sources. Right, their power spectra in dB, with the summed mix in black] Figure: Where the sound comes from, source by source, for a V8 at 2500 rpm (full throttle), a V8 at 900 rpm (8% throttle) and a four-cylinder at 4000 rpm (full throttle). Waveforms on the left, spectra on the right. The black line is the sum, the coloured lines are the tailpipe wave, jet, valve, block and intake. From the 21 October build; I have reworked the structure and flow-noise models since. Grid size is the main quality dial. The same four-cylinder on three grids shows what a finer one buys: the low harmonics land on top of each other whatever the cell size, and the finer the grid, the further up the upper harmonics survive. [Image: Left, the tailpipe pressure wave of a four-cylinder at 4000 rpm over three engine cycles on three grid sizes, nearly overlapping. Right, the power spectrum on the same three grids, identical at low frequency and diverging above about 1 kHz] Figure: A four-cylinder at 4000 rpm, full throttle, run with 25 mm, 12.5 mm and 6 mm cells. Left: the pressure wave at the tailpipe over three engine cycles. Right: its spectrum. ## Results Here is the sound behind the spectrogram at the top: the four-cylinder pull, 13 seconds, straight from the simulator with the volume turned down. In that run the throttle is wide open while the dyno, the rig that holds the engine at a set speed, ramps the revs from 1500 rpm to the redline over 9 seconds, then the throttle comes off and the speed ramps back down over 3. The fan of lines in the spectrogram is the engine's harmonics, the whole-number multiples of its firing pitch. It is a fair test of whether the pitch follows the revs, and nothing more. Your browser can't play audio here. [Download the clip](https://oliviercouthaud.com/media/enginesim/i4_pull.mp3). Several things come out of the pipes that I did not draw in. The torque curve ripples where pressure waves arrive at the valves at the right moment. The harmonics brighten through the exhaust resonances, which is the drone. The two banks of a V8 give separate signals. The rev limiter cracks. And the firing order shows up in the sound: below, both V8s do the same pull, idle, then full throttle from 1500 rpm to the limiter, then off. [Image: Spectrogram and level plot of a 4.5 L flat-plane V8 pull: a few clean, widely spaced harmonic lines fanning out] [Image: Spectrogram and level plot of a 5.7 L crossplane V8 pull: a dense ladder of closely spaced lines at low frequency] Crossplane V8Flat-plane V8 Spectrograms of the 5.7 L crossplane V8 (left) and the 4.5 L flat-plane V8 (right) on the same pull, up to 1.5 kHz. The flat-plane fires evenly and its lines are clean. The crossplane fires unevenly on each bank, and that smears into a thick ladder of half orders. The flat-plane also runs to a higher speed (the rpm trace under each plot), so its lines sit higher. **Crossplane or flat-plane?** It is how the crankshaft is built. A crossplane V8 has its crank throws at 90 degrees, which gives the classic burble because the firing gaps on each bank are uneven. A flat-plane crank puts the throws in one plane, the firings alternate evenly between the banks, and the sound is smoother and higher-pitched. **Virtual dyno, full throttle, from the 21 October build. The reference figures are published numbers for similar engines, not the same engine. A dash means no reference.** | Preset | Simulated | Published | |---|---|---| | 2.0 L inline four | 221 Nm, 137 kW | – | | 5.7 L crossplane V8 | 506 Nm, 288 kW | LS1: 465–⁠495 Nm, about 260 kW | | 4.5 L flat-plane V8 | 473 Nm, 338 kW | – | | 3.2 L inline six, throttle bodies | 318 Nm, 211 kW | BMW S54: 365 Nm, 252 kW | | 1.8 L V-twin | 175 Nm, 62 kW | Harley-Davidson Twin Cam: about 150–⁠160 Nm, 65–⁠70 kW | Where there is a reference it is within about 20%. Each preset uses its own geometry and cam data, and I did not tune anything to those dyno figures. The V8 comes out high and the six comes out low; my simulated six has tamer cams and runners than the BMW it is compared with. ### Dyno pull, three engines **Peaks in those pulls, for reading without the chart.** | Engine | Peak torque | Peak power | |---|---|---| | 2.0 L inline four | 221 Nm at about 5400 rpm | 140 kW at about 6400 rpm | | 3.2 L inline six | 321 Nm at about 5900 rpm | 216 kW at about 6900 rpm | | 0.65 L single | 65 Nm at about 2600 rpm | 24 kW at about 4100 rpm | Speed is the catch. These numbers were measured on a Ryzen 9 9900X with a fast build profile that may be a few percent slower than a full release build, on the Mustang 5.0 preset. The CPU rows are 4 seconds of audio at idle, the GPU row is 10 seconds. A value below 1.00 means faster than real time. **Cost of the gas solver: processing time divided by audio time. A dash means not measured.** | Setup | 25 mm cells | 14 mm cells | 10 mm cells | |---|---|---|---| | CPU, one thread | 0.65 | 1.66 | – | | CPU, four threads | 0.53 | 1.10 | – | | GPU, RTX 5070 Ti, Vulkan | – | – | 0.94 27 of 938 chunks late | On the CPU, real time only works on a coarse grid, and the cost climbs steeply as cells shrink (a fitted exponent of 1.63 on one thread). Four threads still leave 14 mm above real time. The middle quality setting in the app is 21 mm cells on four threads, which runs at about 0.64, so there is not much headroom. Finer grids need the graphics card or an offline render, and I render the 4 mm grid I use for benchmarking offline. The GPU run passed narrowly: 27 of 938 chunks came late, so I rate 10 mm as marginal. The kernel is latency-bound, so a bigger GPU would not help much. Whether it sounds right is the open question. A listening test with people is still to come, and in my own benchmark the best machine judges (the ones built on audio embeddings) tell the simulation from real recordings 91 to 100% of the time. That benchmark is my stand-in for a blind listening test. I train judges to separate simulated renders from real clips, and from version 9 on each judge is scored on a vehicle it never saw during training. 50% would mean it can't tell the difference. My target for calling the sound done is 60% or lower on engines the physics was never tuned on, plus a blind listening test that people pass. The recordings belong to other people, so I don't republish them. The scores for each version are my own. **What is an embedding-based judge?** A pretrained audio network turns a few seconds of sound into a list of numbers that captures how it sounds. I then train a simple classifier on those numbers to say real or simulated. If it can, the simulator has some tell that a human ear might catch too, and I can look at which features give it away. ### Benchmark history, version by version ## What I ran into Early on, the engine breathed at half capacity. Coupling a pipe to a volume through a Riemann problem against gas at rest charged a spurious pressure drop about 20 times the physical one. A boundary built on the pipe's characteristics fixed it, and now every boundary is solved together with the pipe's outgoing characteristic, because boundaries that are not smooth at zero flow chattered at the Nyquist frequency. Tuning against real clips showed where the limits are. In the first fit round none of the constants I tried passed my acceptance rule on held-out engines, so every new realism option stays off. Two of them improved the high-frequency slope but added bass excess at 80 to 315 Hz (about 1 to 3 dB on the engines I tuned on, 5 to 9 dB on the held-out ones): one error traded for another. The second round found that each recording has its own level offset, with a spread of 7 to 8 dB between vehicles, that can't be worked out from the references. The apparent gains were that offset. Going further needs a properly calibrated recording. A comparison of a twin-turbo V8 preset against one clip first looked like a big deficit in the harmonics. On closer inspection two of the orders were the clip's own noise floor and a ridge was the microphone chain, not the exhaust. A real gap at higher orders remains to be closed. The tyre and road coefficients in the cabin model are not yet checked. On the GPU path, the four-cylinder run does not match the CPU within the parity check on a software Vulkan driver, with intake pressure about 2% off. DX12 on a software renderer does nothing, so the code falls back to the CPU. A clean-up step in my setup followed directory links into the main folder and emptied the reference recordings and every old benchmark render. I rebuilt the reference clips from the raw downloads and checked them feature for feature. The renders from versions 0 to 13 can't be regenerated, because the code that made them was not kept, which is why the history above is numbers only. ## Where it stands There is a desktop app built with Tauri, around 90,000 lines of Rust across the simulator and the app, tests included. A release build (18.8 MiB) passed an automated launch check of about 20 seconds: engine loaded, then the first-run benchmark, no audio dropouts. That check ran with the master volume at -120 dB, so it shows the app starts and keeps up, not that it sounds right. An unsigned 6.9 MiB Windows installer builds, but it has not been through a full install test yet. I have also run it live myself, driving an engine in the app. It works, and it is in progress. It is compute-heavy because it simulates the engine physically, in line with the speed numbers above. Next: MIDI input, rebuilding the installer with the keyboard and gamepad input and the head-related transfer table (for binaural headphone sound) added since, a ten-minute run with no underruns, and the GPU quality tier measurements. A browser version and the repository will both be public soon. I haven't chosen the licence yet. [Image: The Garage screen of the desktop app: a grid of engine cards for inline four, V8, V6, flat six, single cylinder, twin, diesel and rotary, with a details panel and a Drive button] Figure: The Garage screen: pick an engine, pick a scenario, press Drive. Rendered in a browser with a stand-in engine, so no sound. [Image: The Listener screen of the desktop app: a 3-D view of a simple car with the listener positions around it, and buttons for listening position, microphone format, space and ground] Figure: The Listener screen: put the microphone or head around the car. Same stand-in engine. The bigger next steps are a listening test with people, and calibrated recordings to fit the source gains against. - **Stack**: Rust (dependency-free core), WebAssembly, WGSL GPU kernels, wgpu, Tauri, Python for analysis and the benchmark - **Status**: In progress - **Code**: Not public yet. The repository goes public soon, with a licence still to be chosen. Inspired by [engine-sim community edition](https://github.com/Engine-Simulator/engine-sim-community-edition). [← All work](https://oliviercouthaud.com/projects)