mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
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:
48 files changed
+2806
-136
No files matched your search
@@ -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.
@@ -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,
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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")
|
||||
Reference in new issue
Block a user