Audio modding 2: hot swap, hot reload, MixMap inputs, own instances, Csis projects, seed

Completes the "Later" list (doc 16 section M):
- Hot swap: an audio mod turning on / off is swapped into the running audio in place.
  Banks keep their runtime id and constructor order, held posts are re-bound with their
  payload; Splice trees, wheel streams, projects, tuning and speech swap too. Only a
  MixMap or grain-bed change (or a swap error) still restarts the sound.
  sdk.audio.info().swaps / last_change.
- Hot reload: editing audio.json or the files it names swaps the overlay while the
  script keeps running.
- MixMap inputs: sdk.audio.set_mixmap_input / mixmap_inputs drive retail's own
  controllers (validated, owned, 16 per mod / 64 total, released values restored).
- Own instances: world_audio spawn {slots='own'} plays mod cars / peds on a private
  MixMap (16 + 16), retail pools untouched.
- Csis projects: audio.json add.projects ships a mod's own classes, functions and
  globals; name clashes are refused; installed / removed without a restart.
- Seed: sdk.audio.seed / SKATE_AUDIO_SEED seeds all audio generators.
- Rules: play.frame = 'owner' puts offsets in the owner's axes; rule sounds follow
  moving published emitters.
- Capabilities: audio 4, audio_content 3, audio_events 3, world_audio 3.
- Dev test mod: Digit1-6, a second HUD line, its own self-made dev.csi.
- No mod active: e2e bench byte-identical 84 / 84 after every step.
This commit is contained in:
Hails committed 2026-10-04 14:38:56 -06:00
1 parent b321f77799
commit 39debf506d
48 files changed
+2806 -136

No files matched your search

+2 -1
View File
@@ -8,6 +8,7 @@
},
"add": {
"banks": {"DEV_siren": {"samples": ["audio/siren.wav"], "group": "world"}, "DEV_fade": {"samples": ["audio/fade.wav"], "group": "world"}},
"projects": ["audio/dev.csi"],
"random_sets": {
"00000000000DE501": {"name": "dev_sirens", "sounds": [{"bank": "DEV_siren", "volume": 0.8, "seconds": 4, "weight": 1}], "min_interval": 5, "max_interval": 10}
},
@@ -28,7 +29,7 @@
},
"rules": {
"pop_click": {"match": {"tag": "pop"}, "action": "layer", "play": {"path": "audio/beep.wav", "volume": 0.3, "at": "owner"}, "min_interval": 0.2},
"honk_beep": {"match": {"tag": "horn"}, "action": "layer", "play": {"path": "audio/beep.wav", "volume": 0.5, "group": "world", "offset": [0, 1.5, 0]}, "min_interval": 0.5}
"honk_beep": {"match": {"tag": "horn"}, "action": "layer", "play": {"path": "audio/beep.wav", "volume": 0.5, "group": "world", "offset": [0, 1.5, 2], "frame": "owner"}, "min_interval": 0.5}
},
"maps": {
"DownTown": {"crossfade_bank": "DEV_fade"},
Binary file not shown.
+92 -2
View File
@@ -8,6 +8,8 @@ local native_on = false
local tuned = false
local placed = false
local quiet = false
-- audio/moddability-2, third pass (doc 16 L1-L7): Digit1..Digit6.
local ducked, seeded, own_cars, dev_class, nose, orbit = false, false, false, false, false, nil
local pressed = {}
local function key(name)
@@ -17,6 +19,10 @@ local function key(name)
return edge
end
local function dev_level()
return sdk.audio.global("g_dev_level")
end
local function hud()
local info = sdk.audio.info()
local h = sdk.audio.handle("emit")
@@ -31,6 +37,17 @@ local function hud()
placed and ((sdk.world_audio.read("emitter") or {}).audible and "playing" or "placed") or "-",
tostring(info.rules or 0) .. (quiet and " (grinds muted, landing beacon)" or ""),
m and tostring(m.level) or "-", table.concat(tags, ", "), table.concat(last, " / ")))
local own = 0
for i = 1, 6 do
local r = sdk.world_audio.read("own_car" .. i)
if r and r.own then own = own + 1 end
end
sdk.ui.text("audio-content-test-2", string.format(
"Content changes: swaps %s, restarts %s, last %s | duck %s | seed %s | own-instance cars %s (%d own) | c_dev_mod %s, g_dev_level %s | nose beep %s | orbiting emitter %s [1 duck (Master inputs), 2 seed, 3 own-instance taxis, 4 mod Csis class + global, 5 pop beep 2 m ahead of the board, 6 orbiting emitter; edit audio.json while running: swapped, no restart]",
tostring(info.swaps), tostring(info.restarts), tostring(info.last_change),
ducked and "on" or "-", seeded and "1234" or "-", own_cars and "on" or "-", own,
dev_class and "posted" or "-", tostring(dev_level()), nose and "on" or "-",
orbit and ((sdk.world_audio.read("orbit") or {}).audible and "playing" or "placed") or "-"))
end
return {
@@ -40,10 +57,13 @@ return {
return
end
if sdk.settings.events then sdk.audio.subscribe{tags = {}} end
sdk.audio.watch{mixmap = {{slot = "emitter", object = 0, instance = 0, output = 4}}}
-- g_dev_level is the global of this mod's own Csis project (audio.json add.projects, doc 16
-- L4); without it (an older engine) the watch fails, so it goes through a request.
sdk.commands.request("watch", {kind = "audio_watch", globals = (sdk.capabilities.audio_content or 0) >= 3 and {"g_dev_level"} or {},
mixmap = {{slot = "emitter", object = 0, instance = 0, output = 4}}})
hud()
end,
on_update = function()
on_update = function(event)
if (sdk.capabilities.audio or 0) < 2 then return end
for _, e in ipairs(sdk.audio.events()) do
local name = e.tag or (e.kind .. ":" .. (e.slot ~= "" and e.slot or e.class))
@@ -124,6 +144,75 @@ return {
play = {path = "audio/beep.wav", volume = 0.8, at = "world", position = {p[1] + 10, p[2], p[3]},
falloff = {radius = 30}}} or nil)
end
if (sdk.capabilities.audio or 0) >= 4 then
if key("Digit1") then
-- L2: duck the world through retail's own controllers: the Master category gains
-- (Global object 2, inputs 1..4; the host writes 32767) held at 8192 (-12 dB).
ducked = not ducked
for i = 1, 4 do
sdk.commands.request("duck" .. i, {kind = "audio_set_mixmap_input", slot = "global", object = 2, instance = 0, input = i, value = ducked and 8192 or nil})
end
end
if key("Digit2") then
-- L5: seed every audio generator (the same draws from here on each time); again
-- releases it.
seeded = not seeded
sdk.audio.seed(seeded and 1234 or nil)
end
end
if key("Digit3") and (sdk.capabilities.world_audio or 0) >= 3 then
-- L3: six idling taxis around the skater, each on its own MixMap instance (retail's 4
-- traffic instances stay for the map's cars, and all six are heard).
own_cars = not own_cars
local p = sdk.player.read().position
for i = 1, 6 do
if own_cars then
local a = i * math.pi / 3
sdk.commands.request("own" .. i, {kind = "world_audio_spawn", key = "own_car" .. i, object = "traffic", options = {
engine = "c04_taxi01", slots = "own", speed = 0, position = {p[1] + 9 * math.cos(a), p[2], p[3] + 9 * math.sin(a)}}})
else
sdk.world_audio.remove("own_car" .. i)
end
end
end
if key("Digit4") and (sdk.capabilities.audio_content or 0) >= 3 then
-- L4: this mod's Csis project: post its class c_dev_mod (no bank binds it: a post that
-- makes nothing, as retail's posts to an unbound class) and set its global g_dev_level
-- (default 7) to 9; again releases both.
dev_class = not dev_class
if dev_class then
sdk.commands.request("dev_post", {kind = "audio_post", key = "dev_class", class = "c_dev_mod", words = {1}})
else
sdk.audio.release("dev_class")
end
sdk.commands.request("dev_global", {kind = "audio_set_global", name = "g_dev_level", value = dev_class and 9 or nil})
end
if key("Digit5") and (sdk.capabilities.audio_events or 0) >= 3 then
-- The offset in the owner's axes: a beep on every pop 2 m ahead of the board's nose
-- (frame = 'owner'), wherever the skater faces.
nose = not nose
sdk.audio.rule("nose_beep", nose and {match = {tag = "pop"}, action = "layer",
play = {path = "audio/beep.wav", volume = 0.6, offset = {0, 0, 2}, frame = "owner"}, min_interval = 0.2} or nil)
end
if key("Digit6") and (sdk.capabilities.world_audio or 0) >= 2 then
-- A published emitter that keeps moving (orbiting the skater at 8 m every 6 s): its
-- sound follows it.
if orbit then
sdk.world_audio.remove("orbit")
orbit = nil
else
orbit = 0
local p = sdk.player.read().position
sdk.commands.request("orbit", {kind = "world_audio_spawn", key = "orbit", object = "emitter", options = {
bank = "Baby_Cry_1", patch = 22, position = {p[1] + 8, p[2], p[3]}, extent = {20, 20, 20}, volume = 0.9}})
end
end
if orbit then
orbit = orbit + (event and event.dt or 0.016)
local p = sdk.player.read().position
local a = orbit * 2 * math.pi / 6
sdk.world_audio.update("orbit", {position = {p[1] + 8 * math.cos(a), p[2], p[3] + 8 * math.sin(a)}})
end
if key("F5") then
global_set = not global_set
sdk.commands.request("global", {kind = "audio_set_global", name = "babycry_1_sel_snd", value = global_set and 1 or nil})
@@ -132,5 +221,6 @@ return {
end,
on_unload = function()
sdk.ui.text("audio-content-test", "")
sdk.ui.text("audio-content-test-2", "")
end,
}
+1 -1
View File
@@ -4,7 +4,7 @@
"name": "Audio Content Test (dev)",
"version": "0.1.0",
"author": "Hails",
"description": "Dev only (PR #36), off by default: enable it in the mod menu, or run with SKATE3_MODS_ENABLE=dev-audio-content-test. Its audio.json replaces DownTown's main zone bed with soft noise, the Baby_Cry_1 emitter bank with a beep, a tunnel zone's volume, a speech take, a taxi's idle rpm, makes a self-made crossfade bank DownTown's zone-transition crossfade (a declared layout), and adds a siren location set on the format-demo map (district DownTown). The script posts a retail c_emitter with F6 and releases it with F7, sets a global with F5 (again to restore), and shows the last audio events and a watched MixMap output on the HUD. audio/moddability-2: F8 plays a siren through the native mixer (the default routing, default reach) and a beep, F9 toggles tuning writes (taxi idle rpm, a reverb preset value), F10 places a mod emitter (its own emitter instance, the default) and a reverb zone at the skater, F11 toggles rules muting grind starts and replacing the landing with a beep at a fixed spot 10 m east; audio.json layers a quiet beep on every pop at the skater and one above every honking car (positional rules). Turning the mod on or off restarts the game's sound (a short cut).",
"description": "Dev only (PR #36), off by default: enable it in the mod menu, or run with SKATE3_MODS_ENABLE=dev-audio-content-test. Its audio.json replaces DownTown's main zone bed with soft noise, the Baby_Cry_1 emitter bank with a beep, a tunnel zone's volume, a speech take, a taxi's idle rpm, makes a self-made crossfade bank DownTown's zone-transition crossfade (a declared layout), and adds a siren location set on the format-demo map (district DownTown). The script posts a retail c_emitter with F6 and releases it with F7, sets a global with F5 (again to restore), and shows the last audio events and a watched MixMap output on the HUD. audio/moddability-2: F8 plays a siren through the native mixer (the default routing, default reach) and a beep, F9 toggles tuning writes (taxi idle rpm, a reverb preset value), F10 places a mod emitter (its own emitter instance, the default) and a reverb zone at the skater, F11 toggles rules muting grind starts and replacing the landing with a beep at a fixed spot 10 m east; audio.json layers a quiet beep on every pop at the skater and one above every honking car (positional rules). audio.json's honk_beep plays 1.5 m above and 2 m ahead of each honking car (offset in the car's own axes, frame = owner). Third pass (doc 16 L1-L7): turning the mod on or off, or editing audio.json or its files while it runs, is swapped into the running audio without a restart (only the changed banks; the HUD's second line shows swaps / restarts / the last change); audio.json adds the mod's own Csis project audio/dev.csi (class c_dev_mod, global g_dev_level). Digit1 ducks the world through retail's Master MixMap inputs (L2), Digit2 seeds the audio random state (L5), Digit3 places six idling taxis on their own MixMap instances (L3), Digit4 posts the mod's own class and sets its global to 9 (L4), Digit5 adds a beep 2 m ahead of the board's nose on every pop (frame = owner), Digit6 places an emitter that orbits the skater (its sound follows it).",
"entry": "main.lua",
"enabled_by_default": false,
"settings": {
+27 -1
View File
@@ -7,6 +7,8 @@ two-tone beep at 22.05 kHz (replaces an emitter bank's samples: another rate and
retail ones, so the rebuilt sample headers are exercised), warn.wav: a short falling tone (a speech
take), siren.wav: a 3 s rising-falling tone (an added location-set bank), fade.wav: 2 s of looping
band-passed noise with a slow flutter (a mod crossfade bank's sample, played by a declared layout).
dev.csi: a Csis project of the mod's own (doc 16 L4): the class c_dev_mod, the function f_dev_msg
and the global g_dev_level (default 7); the layout skate-audio's `Project::parse` reads.
"""
from __future__ import annotations
@@ -71,10 +73,34 @@ def fade() -> list[float]:
return out
def csi(path: Path) -> None:
"""A MOIR project: header 0x28 (counts at 0x0A / 0x0C / 0x0E, id at 0x10, big-endian), the
function and class records (12 bytes: name offset at +4, name id at +8), the global records (16
bytes: default at +4, name offset at +8, name id at +12), then the names."""
tables = [[("f_dev_msg", 1, 0)], [("c_dev_mod", 1, 0)], [("g_dev_level", 1, 7)]]
records = sum(len(t) * (16 if i == 2 else 12) for i, t in enumerate(tables))
head = bytearray(0x28)
head[0:4] = b"MOIR"
for i, at in enumerate((0x0A, 0x0C, 0x0E)):
head[at:at + 2] = struct.pack(">H", len(tables[i]))
head[0x10:0x12] = struct.pack(">H", 0x4445)
rec, names = bytearray(), bytearray()
for i, table in enumerate(tables):
for name, name_id, default in table:
off = 0x28 + records + len(names)
names += name.encode() + b"\0"
if i == 2:
rec += b"\0" * 4 + struct.pack(">iIH", default, off, name_id) + b"\0" * 2
else:
rec += b"\0" * 4 + struct.pack(">IH", off, name_id) + b"\0" * 2
path.write_bytes(bytes(head + rec + names))
if __name__ == "__main__":
write("bed.wav", bed())
write("beep.wav", beep(22050), 22050)
write("warn.wav", warn())
write("siren.wav", siren())
write("fade.wav", fade())
print("wrote", sorted(p.name for p in HERE.glob("*.wav")))
csi(HERE / "dev.csi")
print("wrote", sorted(p.name for p in HERE.glob("*.wav")), "dev.csi")