Routing & Selection

audio sound routing selection

Routing & Selection

Every sound a device or scoring rule makes flows through two top-level blocks:

  • A route lists the candidate sounds an owner can make — one or more source sub-blocks, each a clip or a synth voice with a routing weight.
  • A selection decides which candidate plays when the owner fires — random, weighted, round-robin, and so on.

Both are addressed to their owner with owners =, so the blocks live at the top level of your table rather than nested inside a device. This is the only way to attach sound to a device — a nested audio {} block under a device no longer parses.

The smallest example

A bumper that plays one clip on every hit needs a route naming the clip and a selection to fire it:

audio_clip "bumper_hit" {
  file   = "sounds/bumper_hit.wav"
  volume = 0.8
}

device "switch" "bumper" "pop_bumper_1" {
  id = 32
}

route {
  owners = device.pop_bumper_1
  source "audio_clip" "bumper_hit" { weight = 1 }
}

selection {
  owners = device.pop_bumper_1
}

With a single source and no selection_method, that one clip plays every time.

Owners

owners names who the route or selection serves. It takes a single reference or a bank — a list of references that share the same candidates and decision:

owners = device.pop_bumper_1                          # one owner
owners = [device.pop_1, device.pop_2, device.pop_3]   # a bank

Owners are written bare (not quoted). Five kinds can own sound:

Owner kindReference formFires when…
devicedevice.<name>the device switch/coil triggers
scorescore.<name>the scoring rule awards
modifiermodifier.<name>the scoring modifier applies
accumulatoraccumulator.<name>the accumulator advances
event_handlerevent_handler.<name>the event handler runs

Because scoring owners can drive audio too, a jackpot’s fanfare and a bumper’s pop are authored the same way.

Candidate sources

Each source inside a route is one candidate the owner can play. The block header is source "<kind>" "<name>", where kind is "audio_clip" or "synth":

route {
  owners = device.drop_target_1

  source "audio_clip" "target_1" { weight = 3 }   # played most often
  source "audio_clip" "target_2" { weight = 1 }
  source "synth"      "zap"      {
    weight   = 1
    pitch    = 880
    velocity = 0.4 + score / 200000   # synth-only payload
    duration = "0.5s"                 # timed note-off
    position { x = 0.5 y = 0.9 }      # place the voice in space
  }

  mix { channel = "fx" }              # optional mixer destination
}
AttributeApplies toMeaning
weightany sourceRelative routing weight — higher is chosen more often
pitch"synth" onlyVoice frequency in Hz (number or expression)
velocity"synth" onlyVoice amplitude 0.01.0 (number or expression)
duration"synth" onlyTimed note-off — hold the voice this long, then release ("0.5s")
sustain"synth" onlyHold the voice until the triggering switch opens; excludes duration
position"synth" onlySub-block placing the voice in space: position { x = … y = … z = … }

The four voice attributes — pitch, velocity, duration, sustain — and the position block are only valid on "synth" sources; an "audio_clip" source plays at its own declared volume/priority. duration and sustain are mutually exclusive: duration gives a fixed hold, sustain holds the note for as long as the owning switch stays closed and releases it on the open edge. An optional mix { channel = "<name>" } sub-block sends the route’s output to a named mixer channel.

Choosing a candidate

The selection block decides which source plays. With one candidate you can omit selection_method; with several, name one:

selection {
  owners           = device.drop_target_1
  selection_method = "random"
  no_repeat        = 1                       # never repeat the last 1 pick
  condition        = var.multiball_active == false
}
AttributeTypeMeaning
ownersreferenceThe owner(s) this decision serves
selection_methodstringHow a candidate is picked (see below); default random
no_repeatintSuppress repeats — avoid replaying the last N picks
shuffle_memoryintFor shuffle, how many recent picks the shuffle bag remembers
clip_probabilityexpressionA live expression that biases the pick toward higher-weighted sources
conditionexpressionGate — the owner stays silent unless this evaluates true

condition and clip_probability are bare expressions over live game state (score, var.<name>, signal.<name>) — no ${…} interpolation.

Selection methods

MethodPicks by…
randomA weighted random draw each trigger (the default)
weightedStrict proportional weighting across sources
rotateRound-robin through the sources in order
shuffleA shuffle bag — every source before any repeats
balancedEvening out play counts over time
probabilityDriven by the clip_probability expression
coordinatedCoordinated across the owners in a bank so they don’t collide

Sharing candidates across a bank

When several owners should sound alike — a row of pop bumpers, a bank of drop targets — list them all in one owners bank. They share the candidate pool and the decision, and coordinated selection keeps them from all picking the same clip at once:

route {
  owners = [device.pop_1, device.pop_2, device.pop_3]
  source "audio_clip" "pop_a" { weight = 1 }
  source "audio_clip" "pop_b" { weight = 1 }
  source "audio_clip" "pop_c" { weight = 1 }
}

selection {
  owners           = [device.pop_1, device.pop_2, device.pop_3]
  selection_method = "coordinated"
}

Migrating from device audio {} blocks

A nested audio {} block under a device is now a parse error. To migrate:

  1. Move the block out of the device to a top-level route keyed by owners = device.<name>.
  2. Turn each clip / clips entry into a source "audio_clip" "<name>" { weight = … }.
  3. Add a selection for the same owner; carry selection/selection_method and any condition onto it.

Two capabilities from the old device-audio schema have no direct replacement: state-keyed variations maps (a route lists static candidates, not state-keyed alternates) and per-trigger volume/priority/delay overrides (each clip’s own declared volume/priority applies instead).

See also