sound
Everything SENGOKU TRANCE makes a noise with. Three psytrance tracks sequenced to the exact BPM each stage declares, the shared one-shot library every other slice reaches for, the crowd bed, the UI kit, and the mix that keeps them out of each other's way.
game.sound — the music, the mix and the clip library
Everything SENGOKU TRANCE makes a noise with. Three psytrance tracks sequenced to the exact BPM each stage declares, the shared one-shot library every other slice reaches for, the crowd bed, the UI kit, and the mix that keeps them out of each other's way.
local ST = require("game.core")
local S = ST.need("game.sound") -- nil while this slice is unbuilt; handle it
Exports
| call | what it does |
|---|---|
S.startTrack(stageId) -> boolean | play the stage's track and bind ST.clock to it. The one call that starts a duel's music. Returns false when it fell back to the click track. |
S.stopTrack() | stop the deck and release the clock |
S.deck() -> entity? | the entity carrying the track's Audio component |
S.playing() -> boolean | is the deck running right now |
S.clip(name) -> AssetRef? | one clip from the library, or nil |
S.play(name, opts?) | fire a one-shot. opts = { volume, pitch, gap } |
S.names() -> { string } | every name the library answers to |
S.setBed(on) | raise or lower the crowd, faded |
S.duck(seconds, amount?) | push the music down for a moment |
S.mix() | the live levels, plus the ducking multiplier |
S.setChannel(channel, level) | "music" / "sfx" / "ambient" / "voice" |
S.play and S.clip take a library name or an alias, and an unknown
name is a quiet no-op — a slice that guesses cannot take the game down.
The library
| group | names |
|---|---|
| judgement | hit_kiwami · hit_shin · hit_ka · miss |
| holds | hold_begin · hold_end |
| the duel | duel_begin · duel_break · duel_win · duel_lose · combo_up · warn |
| the field kit | ui_move · ui_select · ui_back · ui_start · ui_tick |
| the world | taiko · bell · riser · impact · crowd_roar · shakuhachi |
Aliases that resolve to one of the above: hit kiwami shin ka
perfect great good kuzure fail click hover confirm select
accept back cancel pause start tick metronome cheer crowd
roar drum gong flute win lose begin combo danger sweep
boom.
ui_tick is the metronome the calibration card should tap against — a
60 ms wooden click with a hard front edge.
What answers on the bus
Nobody has to call anything. These fire on their own:
| topic | what sounds |
|---|---|
sfx { name, volume?, pitch? } | that clip |
note.hit { judgement, combo } | the matching judgement, plus combo_up every tenth |
note.miss | miss |
hold.begin / hold.end | the hold pair |
duel.begin | duel_begin |
duel.end { outcome } | break / win / lose, a crowd roar on a win, and the deck stops |
ui.command { action } | the field kit |
phase.change { kind = "drop" } | the impact |
story.line { seconds } | ducks the music to 42% for the length of the line |
The judgement topics are answered directly as well as through sfx, so a
hit sounds whether or not the conductor also emits one; a repeat of the
same name inside its gap is dropped, so the two paths cannot flam.
The mix
Nothing sits at the same level as anything else. Master 0.90; a spoken
line 1.00, the track 0.68, the crowd 0.30, and the field kit quietest
of all, at 0.22–0.30 of the sfx channel, so a menu never argues with the
music. Under a story.line the track drops to 42% and walks back up over
1.6 s.
The tracks — the BPM is the contract
The three records are sequenced, not generated: a bank of one-shots
stamped onto a grid derived from the bpm in ST.STAGES, section by section,
following the psytrance shape that table declares. That is why a chart
authored in beats and a track rendered from beats cannot disagree.
| stage | bpm | length | measured |
|---|---|---|---|
| okehazama | 142 | 96 s | mean kick offset +3.8 ms, sd 9.5 ms; first half +4.2 ms, second half +3.5 ms |
| nagashino | 146 | 108 s | mean +3.8 ms, sd 9.4 ms; halves +3.81 / +3.78 ms |
| honnoji | 150 | 120 s | mean +3.0 ms, sd 8.2 ms; halves +3.04 / +2.94 ms |
Measured by decoding each finished record and finding the loudest sample within ±55 ms of every beat of the grid. The constant few milliseconds is the kick's own attack shape — its peak sits about 4 ms behind its onset — and the two halves agreeing to a third of a millisecond is what says the tempo does not drift.
Each record carries a generated, wholly un-rhythmic atmosphere under it — rain in the gorge, gunsmoke, the temple burning — looped seamlessly, read at two different points for the two sides so the field is decorrelated, and pulled down to a tenth for the drop.
Rebuilding the bank
Everything under /source/audio is rendered by bake.luau. Run it from an
execute, never from a component or the scene entrypoint: an
asset.create made from either writes to the runtime store instead of
/zero/source and the clip does not survive a reload.
execute { file = "/zero/source/game/sound.module/bake.luau",
args = { what = "library",
taikoSrc = "…", fluteSrc = "…", roarSrc = "…", bedSrc = "…" } }
execute { file = "/zero/source/game/sound.module/bake.luau",
args = { what = "track", stage = "okehazama",
taikoSrc = "…", fluteSrc = "…", atmosSrc = "…" } }
The *Src arguments point at generated .soundClip folders and are all
optional — a synthesised voice stands in for each one, so a failed
generation never leaves the game silent. The noise is a seeded LCG, so a
re-bake is byte-identical.
Notes
mount()builds one deck, one crowd bed and a ten-slot one-shot pool underST.anchor("Audio"), and sets the four channel levels. Nothing is synthesised at mount — the bank is on disk.- Every clip reference is a static literal: a computed
asset.resolveon a gameplay path cannot be statically pinned and breaks when the world is imported elsewhere. - The crowd bed is
codec = "pcm", because an Opus clip loses its first few milliseconds on decode and a bed that loops would click at the seam. Loop points do nothing on this build, so the bed is authored seamless end to end and looped by theAudiocomponent's ownloopingfield. - One-shots are Opus, which keeps the whole library under a megabyte.
Scoped to this part · feeds back into the world's score.