Olivier Couthaud

EngineSim

An engine sound simulator that works the sound out from how gas flows through the engine, instead of playing back recordings.

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
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.

This started with engine-sim community edition, an open-source engine simulator. I played with it, liked it a lot, and wanted to see how far the idea goes if you simulate more of the engine than it does. Everything below is my own take on that, and the inspiration is theirs.

Most engine sounds in games and apps are recordings. Someone records a real car at a few speeds and the software blends between the clips. That works for the car they recorded and falls apart for everything else: another engine, another gear, a stab of throttle nobody captured.

I wanted to build the engine instead of recording it. EngineSim models each cylinder, the pipes that carry the exhaust away, the muffler, and the way air pressure moves through all of it. Most of the sound is what that pressure does when it reaches open air at the end of the tailpipe. The rest is the hiss of air rushing through valves and pipes, and the ticking of the engine itself. It is a bit like a pipe organ. If you know how long the pipes are and how the air is pushed through them, you can work out the note without recording anything. An engine is the same, except the air is hot and arrives in bangs.

Here is thirteen seconds of it, a four-cylinder revving up and back down, straight from the simulator with the volume turned down. It is a fair test of whether the pitch follows the revs, and nothing more.

You pick an engine from a library that runs from a lawnmower motor to V8s, motorcycles, diesels and light aircraft, press the throttle and it runs. There is a desktop app around it where you drive the engine and choose where you are standing: in the cabin, by the exhaust, or as the car goes past. I have run it live in the app, and it works, but it is still a work in progress and it asks a lot of the computer.

The Drive screen of the desktop app: an engine speed ruler along the top, a throttle slider on the left, a 10 second output history in the middle and a master volume dial on the right
The Drive screen of the desktop app: engine speed along the top, throttle on the left, master volume on the right. This is a preview in a browser with a stand-in engine, so it makes no sound.

The physics holds up better than I expected. I ran several engines on a virtual dynamometer, the machine that measures how hard an engine pulls. For the three where I could find published figures for a similar real engine, a V8, a six and a motorcycle twin, the power and torque land within about 20 per cent. I did not tune anything to those figures.

Whether it sounds right is the harder question, and I can't say it is solved. To test it without a room full of listeners I built a stand-in: a program that hears short clips and guesses whether each one is the simulator or a real recording, scored on engines it has never heard. 50 per cent would mean it can't tell. The best of my judges still gets 91 to 100 per cent. I haven't run a listening test with people yet, so I don't know how it sounds to an ear.

Fitting the sound to real recordings mostly didn't work. Each change that helped one part of the sound made another part worse, and when I looked closer, a lot of the apparent improvement was just differences in how the recordings themselves were made. It also needs a lot of computing. On an ordinary processor it keeps up in real time only with a coarse model, and the finer ones need the graphics card or have to be rendered ahead of time.

What's next: a proper listening test with real people, a carefully calibrated recording to fit against, and a graphics-card path that can run finer models live. The app also still needs MIDI keyboard support and a rebuilt installer. A browser version and the code itself will be public soon, under a licence I haven't chosen yet.

What I wanted

I got into this through 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.

Controlleridle, fuel cut Crankpressure, friction Cylinders, 0-DWiebe, wall heat Pipes, 1-D EulerMUSCL-Hancock, HLLC Volumes, 0-Dplenum, muffler BCBC Structure noiseimpacts, modes Open-end radiationrho0 dQ/dt / 4 pi r Flow noise+ acoustic networkvalve, throttle, jet Mix48 kHz audio Listener modelmic, ground,recording chain Gas solverclock: 32 kHz.Cell size followssound speed.
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) changed nothing. 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.

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
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.

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
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.

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.

Spectrogram and level plot of a 4.5 L flat-plane V8 pull: a few clean, widely spaced harmonic lines fanning outSpectrogram 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.
PresetSimulatedPublished
2.0 L inline four221 Nm, 137 kW–
5.7 L crossplane V8506 Nm, 288 kWLS1: 465–⁠495 Nm, about 260 kW
4.5 L flat-plane V8473 Nm, 338 kW–
3.2 L inline six, throttle bodies318 Nm, 211 kWBMW S54: 365 Nm, 252 kW
1.8 L V-twin175 Nm, 62 kWHarley-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.

Peaks in those pulls, for reading without the chart.
EnginePeak torquePeak power
2.0 L inline four221 Nm at about 5400 rpm140 kW at about 6400 rpm
3.2 L inline six321 Nm at about 5900 rpm216 kW at about 6900 rpm
0.65 L single65 Nm at about 2600 rpm24 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.
Setup25 mm cells14 mm cells10 mm cells
CPU, one thread0.651.66–
CPU, four threads0.531.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 only just passed: 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 not proven. I have not run a listening test yet, and in my own benchmark, the best machine judges (the ones built on audio embeddings) still 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.

What didn't work

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 mostly failed. 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 deficit at higher orders remains. The tyre and road coefficients in the cabin model are unchecked.

On the GPU path, the four-cylinder run fails the parity check against the CPU on a software Vulkan driver, with intake pressure about 2% off. DX12 on a software renderer silently did nothing, so the code falls back to the CPU.

I also lost work. 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 are gone for good, and the code that made them was not kept anywhere I can get it back from. That is why the history above is only numbers.

What is unfinished

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, but it is a work in progress and pretty compute-heavy, in line with the speed numbers above. Still open: 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.

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
The Garage screen: pick an engine, pick a scenario, press Drive. Rendered in a browser with a stand-in engine, so no sound.
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
The Listener screen: put the microphone or head around the car. Same stand-in engine.

The bigger gaps 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.

← All work