Game audio basics for a small 2D game, from sources to code

Audio is the part of a small game that gets left until the last evening, and it is also the part that changes how the game feels more than almost anything else you can do in a few hours. A jump with a sound is a jump. A jump without one is a sprite moving upwards. You do not need to be a sound designer to get this right. You need a handful of decent sounds, a clear idea of how loud each should be, a bit of variation so they do not grate, and a music loop that does not click. This piece covers all of that, plus how to wire it up in a Rust game with an audio crate.
Where sound effects come from
There are four realistic sources for a small team.
Record them yourself. A phone, a quiet room and a cupboard of household objects will get you further than you think. Snapping celery, a zip, a book dropped on a cushion, keys shaken in a sock. Record a few takes of everything, because you will want variation later. The main enemy is room noise, so record close to the object and far from the fridge.
Generate them. Retro-style effect generators such as sfxr and its descendants make coin pickups, jumps, lasers and explosions from a few sliders and a randomise button. They are perfect for pixel art games and jam entries, and what they produce is usually yours to use. Check each tool's own terms, but most are made exactly for this.
Use a library. Free sound sites like Freesound host a huge range of recordings, and there are paid packs from many sellers. This is where licences start to matter, so read the next section before downloading anything.
Make them in a synth or audio editor. Free editors like Audacity let you trim, layer, fade and pitch shift. Layering two simple sounds (a thump plus a click, say) often gives a better impact than any single sample.
Licences, the boring part that bites
Every sound you did not make comes with a licence, and the licence decides what you can do with it. The common ones you will meet on free libraries:
| Licence | Can I use it in a commercial game? | What I have to do |
|---|---|---|
| CC0 (public domain dedication) | Yes | Nothing required, credit is polite |
| CC BY | Yes | Credit the creator as the licence asks |
| CC BY-SA | Usually, with care | Credit, and think hard about share-alike terms |
| CC BY-NC | No, non-commercial only | Do not use in anything you sell |
| Pack licence from a seller | Read it | Often fine for games, sometimes with limits |
Two habits will save you pain. First, keep a credits file from day one, a plain text file with each sound's filename, where it came from, who made it and the licence. Write the line when you download the file, not when you ship. Second, avoid non-commercial sounds even in free jam games, because jam games have a way of turning into store releases. If one does, the plain guide to releasing on Steam is a lot less fun if you also have to replace half your audio.
Mixing levels: decide before you tune
Most bad game mixes have the same problem: every sound was normalised to full volume, so the coin pickup is as loud as the explosion and the footsteps drown out the music. Fix this with a simple plan, written down before you start nudging sliders.
Work in decibels relative to full scale, where 0 dB is as loud as a sound can get and lower numbers are quieter. Every 6 dB down is roughly half the amplitude. Here is a starting mix for a small 2D platformer. These are my own starting points, not rules:
| Sound group | Level |
|---|---|
| Player actions (jump, land, attack) | -6 dB |
| Important feedback (hit, pickup, death) | -4 dB |
| Enemies and world (doors, enemy hits) | -10 dB |
| Footsteps and small foley | -16 dB |
| Ambience (wind, birds, hum) | -20 dB |
| Music | -14 dB |
| User interface clicks | -12 dB |
The idea is a hierarchy. The things the player must notice sit on top, the texture sits underneath. Put each group on its own bus (a channel with its own volume), so you can turn a whole category up or down at once, and so your options menu can offer separate music and effects sliders.
Converting decibels to the linear volume most audio APIs want is one line:
/// Convert decibels to a linear amplitude factor.
/// 0 dB gives 1.0, -6 dB gives about 0.5, -20 dB gives 0.1.
fn db_to_amp(db: f32) -> f32 {
10.0_f32.powf(db / 20.0)
}
fn main() {
for db in [0.0, -6.0, -14.0, -20.0] {
println!("{db:>6} dB = {:.3}", db_to_amp(db));
}
}
That prints roughly 1.000, 0.501, 0.200 and 0.100. Keep your mix table in decibels in a config file and convert at load time. Decibels are how ears work, so "two steps quieter" means the same thing at every level.
Variation: the cure for the machine-gun effect
Play the same footstep sample twelve times in a row and the brain notices immediately. It sounds mechanical, a bit like a typewriter. Real sounds never repeat exactly. Two cheap tricks fix most of it.
The first is pitch randomisation. Every time a sound plays, nudge its playback rate a small random amount, say between 0.94 and 1.06. That is a few semitone fractions either way, small enough to sound like the same action, large enough to stop repetition. Volume can wobble a little too, a decibel or so.
The second is round robin: record or generate three to five versions of a frequent sound and pick a different one each time, never the same one twice in a row.
Here is both together, in plain std Rust with a tiny random number generator so nothing depends on a particular crate:
struct Rng(u32);
impl Rng {
// xorshift: fine for audio jitter, not for anything secure
fn next_f32(&mut self) -> f32 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
self.0 = x;
(x as f32) / (u32::MAX as f32)
}
fn range(&mut self, lo: f32, hi: f32) -> f32 {
lo + (hi - lo) * self.next_f32()
}
}
That generator is all the randomness audio jitter needs. The variant picker sits on top of it:
struct SoundVariants {
clips: Vec<&'static str>,
last: Option<usize>,
}
impl SoundVariants {
/// Pick a clip that is not the previous one, plus a pitch factor.
fn pick(&mut self, rng: &mut Rng) -> (&'static str, f32) {
let n = self.clips.len();
let mut i = (rng.next_f32() * n as f32) as usize % n;
if n > 1 && Some(i) == self.last {
i = (i + 1) % n;
}
self.last = Some(i);
(self.clips[i], rng.range(0.94, 1.06))
}
}
The pick method returns a clip name and a pitch factor, and your audio layer plays that clip at that rate. Seed the generator from anything that changes, such as the frame count. If you use an ECS, this fits neatly as a small resource that a sound system reads from, in the spirit of ECS for 2D games.
One more trick: limit how many copies of the same sound can play at once. Ten coins collected in one frame should not produce ten overlapping coin sounds at full volume. Cap it at two or three and drop the rest, or let a new one cut off the oldest.
Music loops that do not click
A music loop has to end exactly where it begins, or you hear a click or a stumble every time it wraps. Two things make a loop clean.
The first is length. Loops should be a whole number of bars. Here is the arithmetic for a common case:
Tempo: 120 beats per minute
Time signature: 4 beats per bar
Loop length: 8 bars = 32 beats
Seconds per beat: 60 / 120 = 0.5
Loop duration: 32 x 0.5 = 16 seconds
Sample rate: 44100 samples per second
Loop in samples: 16 x 44100 = 705600 samples
If your exported file is 705,600 samples long, it will loop perfectly in time. If your editor added a little silence at the end, or a reverb tail, the loop drifts and stutters. Trim to the exact sample count.
The second is the tail. Reverb and long notes ring past the end of the last bar. The usual fix is to render the loop twice in a row, then cut the middle copy out, so the tail from the first pass is baked into the start. Some audio crates also let you set loop points inside a longer file, which handles intro sections nicely: the track plays the intro once, then loops between two markers forever.
For transitions between tracks, a short crossfade of a second or two is enough for most small games. For a jam, one loop for the whole game is fine. Seriously. One good loop beats three rushed ones, and the jam polish order in scoping a game jam puts music last for a reason.
Playing sound in Rust
There are several audio crates in the Rust ecosystem. kira is a popular choice for games: it is built around the idea of an audio manager that owns the output, sound data you load once, and handles you get back when you play something so you can change volume, pitch or stop it later. It also supports mixer tracks, which map neatly onto the buses described above, and tweened changes so a volume change can fade smoothly instead of jumping. Engines such as Bevy and macroquad have their own audio support too, so check what the engine you use already provides before adding a crate.
I will not paste exact kira calls here, because the API has moved between versions and the crate's own examples are the reliable reference. The structure of a small audio layer looks the same whichever crate you pick:
on startup:
create the audio manager
create a track for each bus: effects, music, ui, ambience
set each track volume from the mix table (dB converted to amp)
load every sound file once into a map keyed by name
when gameplay wants a sound:
look up the SoundVariants for that event
pick a clip and a pitch factor
play it on the right track with that pitch
if too many copies are playing, skip it
for music:
play the loop on the music track, looping between markers
on a scene change, fade the old one out and the new one in
Keep the gameplay code ignorant of the crate. Gameplay sends events like "player jumped" or "coin collected", and one audio module turns those into playback. That way swapping crates, or moving from an engine's built-in audio to a dedicated one, touches one file.
Timing matters a little too. If your game uses a fixed update step, trigger sounds from the update where the event happens, not from rendering, or you can get doubled or skipped sounds when frames and updates do not line up one to one. The fixed timestep loop piece explains why those two clocks drift.
A small exercise
Take one action in your game, the one players do most often. Make or find four variations of its sound, write them into a SoundVariants, add pitch jitter between 0.94 and 1.06, and set it at -6 dB on an effects bus. Play the game for two minutes with your eyes half closed. If it still sounds repetitive, widen the pitch range slightly or add a fifth variation. If it sounds like a different action each time, narrow it. You will find the sweet spot in about ten tries, and then you will want to do the same for every sound in the game.
Filed in Dev Desk

Written by Felicity Marrable
Felicity writes about finishing and shipping: game jams, store pages, trailers, audio and the unglamorous checklists between a prototype and a release. She produces small games with friends from a spare room in Leeds.