-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.py
385 lines (302 loc) · 11.2 KB
/
main.py
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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
import os, json, math, struct, aifc, chunk, re
# SM64 Audio Manager v0.1
# Made by Arthurtilly
"""
Check if the streamed audio bank has been created, and create it if not.
"""
def check_if_bank_exists(decompFolder):
if not os.path.exists(os.path.join(decompFolder, "sound/sound_banks/streamed_audio.json")):
print("")
print("This is the first time you have opened this decomp.\nInitializing streamed audio sound bank...")
create_streamed_audio_bank(decompFolder)
"""
Set up the streamed audio sound bank for the first time.
"""
def create_streamed_audio_bank(decompFolder):
# Create json file for streamed audio sound bank
jsonBankData = {
"date": "1996-03-19",
"sample_bank": "streamed_audio",
"envelopes": {
"envelope": [
[6, 32700],
[6, 32700],
[32700, 29430],
"hang"
]
},
"instruments": {},
"instrument_list": []
}
j = open(os.path.join(decompFolder, "sound/sound_banks/streamed_audio.json"), "w")
json.dump(jsonBankData, j, indent=4)
j.close()
print("ADDED: Created soundbank file 'sound/sound_banks/streamed_audio.json'")
# Create directory for streamed audio samples
try:
os.mkdir(os.path.join(decompFolder, "sound/samples/streamed_audio"))
print("ADDED: Created sample directory 'sound/samples/streamed_audio'")
except FileExistsError:
pass
print("Finished initalizing streamed audio bank.")
print("")
"""
Add a new instrument to the streamed audio sound bank.
"""
def add_streamed_audio_inst(soundName, decompFolder):
jsonPath = os.path.join(decompFolder, "sound/sound_banks/streamed_audio.json")
j = open(jsonPath, "r")
jsonBankData = json.load(j)
j.close()
# Get number of existing instruments
instNo = len(jsonBankData["instruments"])
# Add new instrument
jsonBankData["instruments"]["inst%d" % instNo] = {
"release_rate": 10,
"envelope": "envelope",
"sound": "%s" % soundName
}
jsonBankData["instrument_list"].append("inst%d" % instNo)
j = open(jsonPath, "w")
json.dump(jsonBankData, j, indent=4)
j.close()
print("CHANGED: Modified 'sound/sound_banks/streamed_audio.json'")
return instNo
"""
Create a new m64 file for the streamed audio.
"""
def create_m64(m64Name, instNo, volume, decompFolder):
volume = struct.pack(">B", volume)
instNo = struct.pack(">B", instNo)
m64Bytes = b"\
\xD3\x00\
\xD7\x00\x01\
\xDB%b\
\x90\x00\x16\
\xDD\x01\
\xFD\xFF\xFF\
\xFB\x00\x07\
\xD6\x00\x01\
\xFF\
\xC4\
\x90\x00\x20\
\xC1%b\
\xFD\xFF\xFF\
\xFF\
\xC2\x00\
\x67\xFF\xFF\x7F\
\xFF" % (volume, instNo)
m64 = open(os.path.join(decompFolder, "sound/sequences/us/%s.m64" % m64Name), "wb")
m64.write(m64Bytes)
m64.close()
print("ADDED: Created sequence file 'sound/sequences/us/%s.m64'" % m64Name)
"""
Add a new sequence to the sequences json.
"""
def create_new_sequence(seqName, decompFolder):
jsonPath = os.path.join(decompFolder, "sound/sequences.json")
j = open(jsonPath, "r")
jsonSeqData = json.load(j)
j.close()
# m64s must be numbered correctly here otherwise compilation will fail
m64Num = hex(len(jsonSeqData) - 1)[2:].upper()
# Add "custom" so git will pick it up
m64Name = "%s_custom_%s" % (m64Num, seqName)
jsonSeqData[m64Name] = ["streamed_audio"]
j = open(jsonPath, "w")
json.dump(jsonSeqData, j, indent=4)
j.close()
print("CHANGED: Modified 'sound/sequences.json'")
return m64Name
"""
Add loop points to an aiff file.
"""
def add_loop_to_aiff(aiffPath, loopStart, loopEnd):
# Defines the looping
instChunk = b"\
INST\
\x00\x00\x00\x14\
\x3C\x00\x00\x7F\x00\x7F\x00\x00\
\x00\x01\x00\x01\x00\x02\
\x00\x00\x00\x00\x00\x00\
"
# Defines the loop points
markChunk = b"\
MARK\
\x00\x00\x00\x22\x00\x02\
\x00\x01%b\x08beg loop\x00\
\x00\x02%b\x08end loop\x00\
" % (loopStart, loopEnd)
f = open(aiffPath, "ab+")
f.write(instChunk)
f.write(markChunk)
f.close()
def get_loop_point(string, length, framerate, default):
while True:
ans = input(string)
if ans == "":
return default
try:
ans = float(ans) * framerate
except ValueError:
print("Invalid input. Please enter a float.")
continue
if ans > length or ans < -length:
print("Input out of range.")
continue
if ans < 0:
ans += length
return ans
"""
Copy over the aiff data from the source file and add loop points.
"""
def copy_aiff_data(aiffPath, m64Name, decompFolder):
newPath = os.path.join(decompFolder, "sound/samples/streamed_audio/%s.aiff" % m64Name)
aRead = aifc.open(aiffPath, "r")
aWrite = aifc.open(newPath, "w")
frames = aRead.getnframes()
framerate = aRead.getframerate()
# Copy data
aWrite.setnchannels(1)
aWrite.setsampwidth(aRead.getsampwidth())
aWrite.setframerate(framerate)
aWrite.writeframes(aRead.readframes(frames))
aWrite.close()
seconds = frames / framerate
ans = input("Would you like to specify custom loop points? (y/n) ")
if ans.upper() == "Y":
print("\nEnter a number of seconds between 0 and %.3f.\nYou can use a negative number to offset backwards from the beginning,\nor leave it blank to use the default." % seconds)
loopStart = int(get_loop_point("Enter timestamp for start of loop (default 0.000): ", frames, framerate, 0))
loopEnd = int(get_loop_point("Enter timestamp for end of loop (default %.3f): " % seconds, frames, framerate, frames))
else:
loopStart = 0
loopEnd = frames
print("Looping from frame %d (%.3fs) to frame %d (%.3fs)...\n" % (loopStart, loopStart/framerate, loopEnd, loopEnd/framerate))
# Add loop points (beginning and end)
loopStart = struct.pack(">L", loopStart)
loopEnd = struct.pack(">L", loopEnd)
add_loop_to_aiff(newPath, loopStart, loopEnd)
print("ADDED: Created sample file 'sound/samples/streamed_audio/%s.aiff'" % m64Name)
aRead.close()
"""
Add a new sequence to the sequence IDs enum in include/seq_ids.h.
"""
def append_seq_id(seqName, decompFolder):
seqIdsPath = os.path.join(decompFolder, "include/seq_ids.h")
seqIdsFile = open(seqIdsPath, "r")
seqIdsLines = seqIdsFile.readlines()
seqIdsFile.close()
listFound = False
for i in range(len(seqIdsLines)):
# Check for end of enum
if "SEQ_COUNT" in seqIdsLines[i]:
seqIdsLines.insert(i, " %s,\n" % seqName)
listFound = True
break
if not listFound:
raise ValueError("'SEQ_COUNT' end macro missing from include/seq_ids.h! Aborted")
seqIdsFile = open(seqIdsPath, "w", newline="\n")
seqIdsFile.writelines(seqIdsLines)
seqIdsFile.close()
print("CHANGED: Modified 'include/seq_ids.h'")
"""
Add corresponding entry for the new sequence in the volume table in src/audio/external.c.
"""
def append_default_volume_table(volume, decompFolder):
externalPath = os.path.join(decompFolder, "src/audio/external.c")
externalFile = open(externalPath, "r")
externalLines = externalFile.readlines()
externalFile.close()
inTable = False
for i in range(len(externalLines)):
if not inTable:
# Check for start of table
if "sBackgroundMusicDefaultVolume" in externalLines[i]:
inTable = True
# Check for end of table
elif "}" in externalLines[i]:
externalLines.insert(i, " %d,\n" % volume)
break
if not inTable:
raise ValueError("'sBackgroundMusicDefaultVolume' table missing from src/audio/external.c! Aborted")
externalFile = open(externalPath, "w", newline="\n")
externalFile.writelines(externalLines)
externalFile.close()
print("CHANGED: Modified 'src/audio/external.c'")
"""
Verify a given AIFF file path is valid.
"""
def verify_aiff(aiffPath):
if not os.path.exists(aiffPath):
print("Error: aiff file does not exist.")
return False
try: a = aifc.open(aiffPath, "r")
except aifc.Error:
print("Error: file was not a valid AIFF file.\nIf you are exporting with Audacity 2.4 or later,\nplease use an earlier version (2.3.x or earlier)\nas AIFF exporting is broken.")
return False
if a.getnchannels() > 1:
print("Error: tool currently only supports mono audio.")
return False
return True
"""
Verify a given sequence name is valid.
"""
def verify_name(name):
if not re.fullmatch("[0-9A-Za-z_ ]+", name):
print("Error: Invalid name entered, must contain only letters, numbers, underscores and spaces")
return False
return True
"""
Verify a given decomp folder is valid.
"""
def verify_decomp(decompFolder):
# Check that the folder is decomp
if not os.path.exists(os.path.join(decompFolder, "Makefile")):
print("Error: Given directory does not exist / is not a decomp folder.")
return False
# Check that the required files are there
if not os.path.exists(os.path.join(decompFolder, "include/seq_ids.h")):
print("Error: 'include/seq_ids.h' is missing!")
return False
if not os.path.exists(os.path.join(decompFolder, "src/audio/external.c")):
print("Error: 'src/audio/external.c' is missing!")
return False
return True
"""
Add a new piece of streamed audio to the game.
"""
def add_streamed_audio(aiffPath, name, decompFolder):
# Todo:
# Custom volume
# Custom loop points
# Option to keep existing loop points (detect loop points?)
print("")
name = name.lower().replace(" ","_")
m64Name = create_new_sequence(name, decompFolder)
copy_aiff_data(aiffPath, m64Name, decompFolder)
instNo = add_streamed_audio_inst(m64Name, decompFolder)
create_m64(m64Name, instNo, 127, decompFolder)
seqName = "SEQ_STREAMED_" + name.upper()
append_seq_id(seqName, decompFolder)
append_default_volume_table(127, decompFolder)
print("")
print("Done! Your sequence has been added as %s." % seqName)
"""
Main user input function.
"""
def main():
decompFolder = os.path.expanduser(input("Enter decomp directory: "))
if not verify_decomp(decompFolder): return
check_if_bank_exists(decompFolder)
aiffPath = os.path.expanduser(input("Enter path for the .aiff file: "))
if not verify_aiff(aiffPath): return
a = aifc.open(aiffPath, "r")
print("\nAIFF data:\n\tSample rate: %dHZ\n\tTotal samples: %d\n\tLength: %.1f seconds\n" % (a.getframerate(), a.getnframes(), a.getnframes()/a.getframerate()))
if (a.getframerate() > 32000):
ans = input("Warning: sample rate (%dHZ) exceeds recommended rate of 32000HZ.\nConsider reducing sample rate to save space.\nContinue? (y/n) ")
if ans.upper() != "Y":
return
name = input("Enter a name for the streamed audio: ")
if not verify_name(name): return
add_streamed_audio(aiffPath, name, decompFolder)
main()