This repository was archived by the owner on Aug 14, 2026. It is now read-only.
forked from TeJota1337/DramaticShapeVoxelMod
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathHordeSfx.lua
More file actions
262 lines (234 loc) · 9.46 KB
/
Copy pathHordeSfx.lua
File metadata and controls
262 lines (234 loc) · 9.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
-- HORDE MODE: the gun, in Game Boy hardware.
--
-- Every sound this mode makes is SYNTHESIZED on the same emulated APU the
-- rest of the game speaks through -- no sample files ship with the mod.
-- That is a deliberate aesthetic choice as much as a legal one: Lavender
-- Town is playing, the cries are the real cries, and a 44kHz foley
-- gunshot dropped on top would read as a different program running in the
-- same window. Authored here with ChipAsm (src/audio/ChipAsm.lua), which
-- assembles note tables into the channel bytecode ChipAudio interprets.
--
-- WHAT A GUNSHOT IS, on this hardware. Channel 4 is a noise generator
-- whose `parameter` byte is NR43: the high nibble is the shift clock (LOW
-- values are BRIGHT, high values are low rumble), bit 3 picks the short
-- 7-bit LFSR (metallic and pitched) over the long 15-bit one (white
-- hiss), and the low three bits divide. A real gunshot is a bright crack
-- collapsing into a body and then a room tail, so each sound here is a
-- STAGED program: three or four noise notes marching down the parameter
-- byte, each shorter-lived than the last. `len` is in frames of 1/60s,
-- `volume` is 0-15, and `fade` is the envelope period -- 1 decays fastest,
-- 7 slowest, 0 holds for the note's whole length.
--
-- The shot also gets two frames of channel 1 underneath it: a square note
-- swept hard downward, which is the only way to put a low thump on this
-- chip. It costs the music its lead channel for 1/30s per shot, which is
-- inaudible as interference and is most of what makes the shot feel like
-- it has weight.
--
-- THREE SHOT VARIANTS, round-robined. Sound.play caches ONE Source per
-- registered name and restarts it (stop then play), so firing twice on
-- one name cuts the first shot's tail off. Three names means three
-- Sources, so a fast trigger finger overlaps its own echoes the way a
-- real one does -- and the variants differ slightly in their tails, which
-- takes the machine-gun sameness off a repeated sound.
-- the mod namespace (see main.lua): V.require loads a sibling module
local V = ...
local HordeSfx = {}
-- the registered names, in the shape the rest of the mode asks for them
HordeSfx.SHOTS = { "DS_HORDE_SHOT_1", "DS_HORDE_SHOT_2", "DS_HORDE_SHOT_3" }
HordeSfx.DRY = "DS_HORDE_DRY"
HordeSfx.MAG_OUT = "DS_HORDE_MAG_OUT"
HordeSfx.MAG_IN = "DS_HORDE_MAG_IN"
HordeSfx.RACK = "DS_HORDE_RACK"
HordeSfx.HIT = "DS_HORDE_HIT"
HordeSfx.HURT = "DS_HORDE_HURT"
HordeSfx.WAVE = "DS_HORDE_WAVE"
-- ------- the programs
-- The shot's noise stage list: bright crack, body, tail, room. `tail`
-- lets the three variants differ in how the last stage rings out without
-- restating the whole program.
local function shotNoise(tail)
return {
-- the crack: one frame, full volume, brightest parameter the chip has
{ noiseNote = { len = 1, volume = 15, fade = 1, parameter = 0x00 } },
-- the body: the shift clock drops, the 7-bit LFSR gives it a metallic
-- edge -- this is the part that reads as "a mechanism did that"
{ noiseNote = { len = 2, volume = 13, fade = 2, parameter = 0x2C } },
-- the tail: lower, softer, longer
{ noiseNote = { len = 3, volume = 8, fade = 3, parameter = tail[1] } },
-- the room: a low breath of noise fading under everything
{ noiseNote = { len = tail[2], volume = 4, fade = 4, parameter = tail[3] } },
}
end
-- The thump under the crack: channel 1's frequency register swept down
-- hard. 0x600 is around 250Hz; the sweep drags it into the floor over the
-- two frames it lives, which is a kick drum by another name.
local THUMP = {
{ pitchSweep = { pace = 2, subtract = true, shift = 3 } },
{ squareNote = { len = 2, volume = 12, fade = 2, frequency = 0x600 } },
}
local function shot(tail)
return {
channels = {
{ hw = 1, program = THUMP },
{ hw = 4, program = shotNoise(tail) },
},
}
end
-- The reload, in three separate sounds the gun fires on its own clock:
-- the magazine dropping out, the fresh one seating, and the slide coming
-- back and going home. Noise only -- these are mechanical clicks, and
-- keeping them off the tone channels leaves the music alone.
local PROGRAMS = {
[HordeSfx.SHOTS[1]] = shot({ 0x55, 5, 0x76 }),
[HordeSfx.SHOTS[2]] = shot({ 0x54, 6, 0x77 }),
[HordeSfx.SHOTS[3]] = shot({ 0x65, 4, 0x86 }),
-- the hammer falling on nothing: one dull tick, no tail
[HordeSfx.DRY] = {
channels = {
{ hw = 4, program = {
{ noiseNote = { len = 1, volume = 7, fade = 1, parameter = 0x38 } },
{ noiseNote = { len = 1, volume = 3, fade = 1, parameter = 0x54 } },
} },
},
},
-- the magazine leaving: a click and a soft drop away from it
[HordeSfx.MAG_OUT] = {
channels = {
{ hw = 4, program = {
{ noiseNote = { len = 1, volume = 10, fade = 1, parameter = 0x1A } },
{ noiseNote = { len = 2, volume = 5, fade = 2, parameter = 0x58 } },
} },
},
},
-- the fresh magazine seating: a firmer, lower clack with a bit of body
[HordeSfx.MAG_IN] = {
channels = {
{ hw = 4, program = {
{ noiseNote = { len = 1, volume = 13, fade = 1, parameter = 0x18 } },
{ noiseNote = { len = 2, volume = 8, fade = 2, parameter = 0x46 } },
{ noiseNote = { len = 2, volume = 3, fade = 3, parameter = 0x67 } },
} },
},
},
-- the slide: back (bright scrape), a frame of nothing, then home (hard)
[HordeSfx.RACK] = {
channels = {
{ hw = 4, program = {
{ noiseNote = { len = 2, volume = 9, fade = 2, parameter = 0x25 } },
{ rest = 1 },
{ noiseNote = { len = 1, volume = 14, fade = 1, parameter = 0x11 } },
{ noiseNote = { len = 2, volume = 6, fade = 2, parameter = 0x44 } },
} },
},
},
-- a bullet arriving: short, bright, gone -- the hit marker's own sound
[HordeSfx.HIT] = {
channels = {
{ hw = 4, program = {
{ noiseNote = { len = 1, volume = 11, fade = 1, parameter = 0x14 } },
{ noiseNote = { len = 1, volume = 5, fade = 2, parameter = 0x42 } },
} },
},
},
-- being hit: a low ugly thud on the noise channel with a square groan
-- under it, sweeping DOWN -- the sound of losing something
[HordeSfx.HURT] = {
channels = {
{ hw = 1, program = {
{ pitchSweep = { pace = 3, subtract = true, shift = 4 } },
{ squareNote = { len = 6, volume = 11, fade = 3, frequency = 0x480 } },
} },
{ hw = 4, program = {
{ noiseNote = { len = 2, volume = 12, fade = 2, parameter = 0x66 } },
{ noiseNote = { len = 4, volume = 6, fade = 3, parameter = 0x78 } },
} },
},
},
-- a wave arriving: two rising square stabs, deliberately not a fanfare
[HordeSfx.WAVE] = {
channels = {
{ hw = 1, program = {
{ squareNote = { len = 3, volume = 10, fade = 2, frequency = 0x5C0 } },
{ rest = 1 },
{ squareNote = { len = 6, volume = 12, fade = 3, frequency = 0x680 } },
} },
},
},
}
-- ------- registration
-- Assemble every program and put it in the sfx registry. Called once from
-- main.lua at load. A malformed note table raises inside ChipAsm; each is
-- assembled under pcall so one bad program is one missing sound rather
-- than a mod that fails to load.
function HordeSfx.register(mod)
local ok, ChipAsm = pcall(require, "src.audio.ChipAsm")
if not (ok and ChipAsm) then return false end
local n = 0
for name, spec in pairs(PROGRAMS) do
local built, out = pcall(ChipAsm.sfx, spec)
if built and out and out.chip then
local reg = pcall(function()
mod.content.sfx:register(name, { chip = out.chip })
end)
if reg then n = n + 1 end
elseif mod.log then
mod.log:error("horde: sfx %s did not assemble: %s", name, tostring(out))
end
end
return n > 0
end
-- ------- playback
--
-- One indirection so callers never touch Sound directly and a headless
-- run (no love.audio) costs a pcall rather than an error.
local function play(name)
pcall(function()
local Game = require("src.core.Game")
require("src.core.Sound").play(Game.data, name)
end)
end
HordeSfx.play = play
local shotIndex = 0
-- The next shot in the round-robin, so consecutive rounds overlap rather
-- than cutting each other off (see the header).
function HordeSfx.shot()
shotIndex = shotIndex % #HordeSfx.SHOTS + 1
play(HordeSfx.SHOTS[shotIndex])
end
-- ------- the cries
--
-- Every mob that dies screams as something from the national dex. The
-- list is built once from the live cry registry -- whatever the game and
-- whatever mods are loaded have between them -- so this needs no data of
-- its own and picks up a total conversion's roster for free.
local cryList = nil
local function cries()
if cryList then return cryList end
local out = {}
pcall(function()
local Game = require("src.core.Game")
local table_ = Game.data and Game.data.audio and Game.data.audio.cries
for species in pairs(table_ or {}) do out[#out + 1] = species end
end)
table.sort(out) -- love.math.random over a stable order, not hash order
cryList = out
return out
end
-- A random cry, at a random-ish pitch. Nothing is more Pokemon than the
-- wrong animal noise coming out of a man in a suit.
function HordeSfx.randomCry()
local list = cries()
if #list == 0 then return nil end
local species = list[love.math.random(#list)]
pcall(function()
local Game = require("src.core.Game")
require("src.core.Sound").playCry(Game.data, species)
end)
return species
end
-- a fresh boot (or a hot reload) rebuilds the species list
function HordeSfx.invalidate()
cryList = nil
end
return HordeSfx