Step 1 of 5 · Reading · ~3 min
Read
Resources & Rendering
Audio
A sound card does not play "a gunshot" and "the music"; it plays one stream of numbers. The engine's job is to turn many sounds into that single stream, thousands of times a second. After this lesson your program will be that mixer: it will take sources that are each a list of samples, set their loudness from their distance, add them together without distorting, and hand back a block of samples at a time.
Samples and blocks
Digital audio is a sequence of samples, each the air pressure at one instant, stored as a signed 16-bit integer (-32768 to 32767) and played at a fixed sample rate such as 44,100 or 48,000 per second. At 48,000 Hz a frame of a 60 fps game spans 800 samples.
The audio hardware asks the engine for the next samples in blocks (also called buffers), say 512 at a time, on its own thread, and the engine must answer before the previous block runs out. That deadline is why audio code never allocates memory, takes a lock or reads a file inside the callback. The block size sets the delay between "play this" and hearing it: 512 samples at 48,000 Hz is 10.7 ms; a smaller block lowers latency but leaves less time per block and risks a click when it is missed.
Mixing is addition
Two sounds played together are the sum of their samples, sample by sample. Each source has a position that moves forward one sample per output sample:
- a one-shot sound (gunshot, footstep) is finished after its last sample and should disappear from the mixer;
- a looping sound (engine hum, wind) wraps to its start.
Because sources are added, the sum can leave the 16-bit range. If you let it wrap around, a loud sample becomes a large negative one, which is an ugly crack. The fix is to clamp: anything above 32767 becomes 32767, anything below -32768 becomes -32768. Clamp once, on the finished sum, not on each source; otherwise two loud sources that cancel each other would be clipped before they can.
Gain and distance
Each source has a gain, here a whole percent. A sample is scaled by sample * gain / 100, and integer division drops the fraction towards zero, so -7 at 50% becomes -3 (not -4). A simple linear distance model sets the gain from the distance to the listener: full volume at distance 0, falling in a straight line to silence at max_dist and staying silent beyond it. A negative gain would flip the wave's sign, which is why the floor at 0 matters. Real engines use other falloff curves (inverse distance, logarithmic) and also pan between left and right channels, but the structure is the same.
Worked trace
A one-shot A with samples 20000, -30000, 5 at full gain and a looping B with samples 18000, 18000 at 50% gain (so 9000 each):
output A B sum after clamp
1 20000 9000 29000 29000
2 -30000 9000 -21000 -21000
3 5 9000 9005 9005
4 (done) 9000 9000 9000 A was removed after sample 3
Two sources at 30000 would sum to 60000 and play as 32767.
Common mistakes
- Letting the sum wrap around instead of clamping it.
- Clamping each source before adding.
- Rounding down with floor division on negative samples. -7 at 50% must be -3.
- Letting distance beyond
max_distproduce a negative gain. - Restarting a source at the beginning of every block. Its position persists between blocks.
- Keeping finished one-shot sources, so the mixer slowly fills up.
Your exercise
Implement the four small mixer functions described in the exercise. The starter parses the commands and prints each block. The most likely mistake is dropping the fraction in the wrong direction for negative samples.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…