Game sound without sound files.
Brusverk is a procedural audio library for games, written in Rust. Engines, weather, weapons and interface sounds are computed while the game runs, from the game's own state: throttle, speed, damage, distance. It plugs into Godot 4 as an audio stream, and it is running in this tab as WebAssembly.
- 43 built-in generators
- about 1 MB of WebAssembly
- nothing is played from a recording
- MIT or Apache-2.0
Pick something to hear.
Each picture is the spectrogram of the sound under it, computed in this tab when the page loaded. Press play and it is computed again, so the event sounds come out a little different each time. Nothing is downloaded except the engine.
Every sound in the library opens in the Sound Lab, with its inputs as sliders and all of its parameters:
Your game already has the inputs.
A recorded engine loop has one speed. A generated one follows the game: the throttle, revs, slip and damage your physics already computes become the sound's inputs, every frame. In Godot 4 a generator is an AudioStream, so players, buses, 3D position and effects work the way they do for a sample.
- Build the extension.
cargo build -p brusverk-godot --release, then point a.gdextensionfile at the library it produces. - Give a player a generator.Pick a built-in one and a preset, or load a model file of your own.
- Feed it your game state.Set the inputs every physics frame. They all run from 0 to 1.
- Tune it by ear.Parameters show up in the Godot inspector. Or shape the sound in the Sound Lab and copy the values back with
set_params_json().
Tested with Godot 4.7 on macOS. Linux and Windows build from the same source but have not been tested here. Not using Godot? brusverk-core is plain Rust with no engine dependency, and the WebAssembly build has a C interface.
# once: an engine on any AudioStreamPlayer, 2D or 3D
var gen := SoundGenerator.create("piston")
gen.preset = "Heavy truck"
gen.set_param("engine/external_rpm", 1) # the game owns the revs
player.stream = gen
player.play()
# every frame: hand it what the physics already knows
func _physics_process(_dt):
var pb := player.get_stream_playback() as SoundGeneratorPlayback
pb.set_inputs({"throttle": throttle, "rpm": rpm, "load": load})
# an event: fired with what the game knows at that moment
var boom := SoundGenerator.create("explosion")
boom.preset = "Ship destroyed"
boom_player.stream = boom
func explode(power: float, distance: float) -> void:
# power: glancing hit to full blast. distance: 0 point blank, 1 far off
boom.set_start_input("power", power)
boom.set_start_input("distance", distance)
boom_player.play() # `finished` fires when the tail has rung out
Need a sound that isn't there? Write it as a text file.
A model file is a graph of nodes (noise, filters, resonators, delays, a reverb) plus formulas that turn the inputs into control signals. It loads while the game runs, shows its parameters in the Godot inspector like a built-in generator, and costs about three times as much to compute.
This is campfire.toml, the file behind the campfire demo above. Sparks are random impulses, shaped into short bursts of noise and coloured by a filter that moves a little each time.
The file appears here once the engine has loaded.
What's inside.
One Rust crate does the synthesis. Around it are a Godot 4 extension, the WebAssembly build this page runs, and the tools used to check every sound.
Engines
- Piston
- Each cylinder fires in order into one of two exhaust banks. The header and exhaust pipes are resonators with lengths in metres. Turbo, engine brake and diesel clatter for the heavy ones.
- Layouts
- Cross-plane and flat-plane V8, inline four and six, boxer four, V-twin, single, V12.
- Fitted to
- A recording of a Rover 3.5 V8: third-octave levels within about 4 dB at idle and at 3000 rpm.
- Jet
- A turbine with spool inertia, afterburner, wind and damage layers.
Event sounds
- Built from
- Up to eight layers each: cracks, swept bodies, sub drops, debris and a reverb tail.
- At trigger
- The game passes power and distance. Every trigger varies slightly, so the tenth explosion is not the first one again.
- Stereo
- Sub and crack stay centred, noise and tail are spread. Width 0 gives exactly the mono sound.
- Ending
- The player emits
finishedwhen the tail has rung out, as it would for a sample.
Synth voice
- Voice
- Three oscillators, a state-variable filter, two envelopes and an LFO. 32 voices.
- Patches
- 59 parameters, stored as JSON or as a Godot resource.
- Chip effects
- 11 presets in the sfxr tradition (pickup, laser, jump and so on), each reproducible from a seed.
- Try it
- The Instrument and Sound FX tabs of the Sound Lab.
Under the hood
- Audio thread
- No allocation, locks or blocking while rendering. Parameter changes arrive over lock-free queues.
- Speed
- Every built-in generator running at once renders 26 times faster than real time on one core of an Apple-silicon laptop.
- Checked
- More than 200 automated tests. Every sound is reviewed by spectrogram for dropouts, clipping and ringing, and the audio of this site is measured in a real browser.
- Size
- The core crate has no engine dependencies. The web build is about 1 MB.