Audio & Sound
Audio & Sound
Cade gives a table two ways to make noise: sample clips — .wav or .ogg files you trigger from gameplay — and synthesis — oscillator patches the engine generates in real time, with no audio files to ship. Both feed the same mixer, so you can mix recorded and generated sound freely.
Defining a clip
Declare each sound once with a top-level audio_clip block, then reference it by name wherever you want it to play:
audio_clip "bumper" {
file = "sounds/bumper_hit.wav"
preload = true
volume = 0.8
priority = 600
}| Attribute | Default | Meaning |
|---|---|---|
file | (required) | Path to the sound file, relative to your table directory |
preload | false | Load into memory at startup instead of on first play |
volume | 1.0 | Playback level, 0.0–1.0 |
priority | 500 | Which sound wins when voices are limited; higher takes precedence |
loop | false | Repeat until stopped |
Supported formats are WAV (.wav, .wave) and Ogg Vorbis (.ogg, .oga).
Attaching sound to a device
Device sound is authored with two top-level blocks — a route that lists the candidate sounds an owner can make, and a selection that decides which one plays. Both are addressed to their owner with owners =:
device "switch" "bumper" "pop_bumper_1" {
id = 32
}
route {
owners = device.pop_bumper_1
source "audio_clip" "bumper" { weight = 1 }
}
selection {
owners = device.pop_bumper_1
}For a pool of clips, list several source candidates and pick a decision method:
route {
owners = device.drop_target_1
source "audio_clip" "target_1" { weight = 1 }
source "audio_clip" "target_2" { weight = 1 }
source "audio_clip" "target_3" { weight = 1 }
}
selection {
owners = device.drop_target_1
selection_method = "random"
}The same owners reference can also point a route/selection at a synth source or at scoring owners. For the full model — owner kinds, weights, synth sources, decision methods, and conditions — see Routing & Selection.
Migrating from a device
audio {}block? A nestedaudio {}under adeviceno longer parses — move each one to a top-levelroute/selectionpair keyed byowners = device.<name>. See Routing & Selection.
Playing a sound when something scores
Scoring rules take an audio sub-block, so the sound and the points fire together:
score "event" "jackpot" {
when = device.ramp.activated
points = 50000
audio {
clip = jackpot
volume = 1.0
}
}Setting levels
Master level and the device-audio system are controlled by a top-level audio block:
audio {
master_volume = 0.9
device_audio = true
}master_volume scales everything (default 0.9); device_audio enables the device-attached sounds described above (default true).
Generating sound with synth patches
Instead of a file, you can define a synth patch — oscillators shaped by ADSR envelopes, optionally filtered and modulated — and trigger it from an event. This is ideal for bumpers, targets, and UI blips where you want a tight, tunable effect without recording anything.
synth "bumper_pop" {
oscillator "carrier" {
wave = "square"
freq = 880
}
oscillator "mod" {
wave = "sine"
freq = 370
amp = 450
}
modulate {
source = "mod"
target = "carrier.freq"
}
envelope "amp" {
attack = 1
decay = 440
sustain = 0.0
release = 220
}
filter {
type = "lowpass"
cutoff = 700
resonance = 0.9
in = node.carrier.out
}
output {
in = node.filter.out
gain = node.amp.out
}
}
event_handler "bumper_synth" {
when = device.pop_bumper_1.hit
}
route {
owners = [event_handler.bumper_synth]
source "synth" "bumper_pop" {
weight = 1
pitch = 880
velocity = 1.0
}
}A voice is fired by a top-level route whose owners name the handler, carrying a source "synth" labelled with the patch name. The source takes a pitch in Hz and a velocity from 0.0 to 1.0 (default 1.0). Note that the handler here owns nothing but its route — a handler with no actions is valid, and its route still fires.
Envelope times are plain numbers in milliseconds, and every synth wires its signal path with an output block. The amp envelope shown above is optional — omit it (and the output’s gain) for a bare oscillator → output patch and cade supplies a default amplitude gate. Add duration = "0.5s" to hold the note for a fixed time, or sustain = true to hold it until the triggering switch releases; the two are mutually exclusive.
Reactive pitch and velocity
pitch and velocity don’t have to be fixed numbers. Each accepts a binding that resolves the moment the event fires, so a sound can react to the game — a bumper that climbs in pitch as the score grows, or a hit that gets louder as a combo builds:
event_handler "bumper_synth" {
when = device.pop_bumper_1.hit
}
route {
owners = [event_handler.bumper_synth]
source "synth" "bumper_pop" {
weight = 1
pitch = 440 + score / 100000 # rises with the score
velocity = min(var.combo / 5.0, 1.0) # louder as the combo builds
}
}A binding can be a literal number, an expression over live game state (score, var.<name>, signal.<name>), or a property of the triggering event (from:event.<field>). See Runtime Bindings for the forms, the full namespace, and how they resolve.
For the full oscillator, envelope, filter, and modulation reference, see Synthesis.
Workflow
- Drop your
.wav/.oggfiles in asounds/directory beside your.cadefile. - Declare an
audio_clipfor each, settingvolumeandpriority. - Attach clips to devices with a
route/selectionpair (see Routing & Selection), and to scoring rules with anaudiosub-block. - Set the overall level with the global
audio { master_volume = … }block. - For generated sound, add a
synthpatch and fire it from aroutewith asource "synth". - Trigger devices and events from the console to hear the result, and watch the cascade to confirm the audio action fired.