Audio & Sound

audio sound synth music

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
}
AttributeDefaultMeaning
file(required)Path to the sound file, relative to your table directory
preloadfalseLoad into memory at startup instead of on first play
volume1.0Playback level, 0.01.0
priority500Which sound wins when voices are limited; higher takes precedence
loopfalseRepeat 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 nested audio {} under a device no longer parses — move each one to a top-level route/selection pair keyed by owners = 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

  1. Drop your .wav/.ogg files in a sounds/ directory beside your .cade file.
  2. Declare an audio_clip for each, setting volume and priority.
  3. Attach clips to devices with a route/selection pair (see Routing & Selection), and to scoring rules with an audio sub-block.
  4. Set the overall level with the global audio { master_volume = … } block.
  5. For generated sound, add a synth patch and fire it from a route with a source "synth".
  6. Trigger devices and events from the console to hear the result, and watch the cascade to confirm the audio action fired.