-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathflashprep.py
239 lines (191 loc) · 7.03 KB
/
flashprep.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
"""
Usage: flashprep.py path_to_audio_directory
This script will take the audio files in the passed directory,
sort them lexicographically, pick a sample rate to fit them
into the provided memory space, and then output:
- flash.bin, containing a header plus the audio streams
- data/flash_constants.h, containing constants
- data/flash_constants.c, containing an TrackMetadata array
"""
import os
import sys
import math
import struct
from glob import glob
from tqdm import tqdm
import subprocess
SECRET_HEADER_TEXT = """
Hello world! If you're reading this, you've somehow
disassembled this custom Game Boy cartridge and are
reverse engineering it or something. Cool. My name's
Peter, and I built this in 2017/2018. I'd love to
hear from you - send me an email at:
[email protected] or find me on most social
networks at @psobot.
What follows is a concatenation of multiple audio
files encoded at 8bits and some arbitrary sample rate.
Their bounds are specified in a rudimentary header format:
uint32_t: number of files
uint32_t: start of file 0
uint32_t: start of file 1...
After the newline at the end of the next sentence,
the header will begin. Happy hacking!
"""
SECRET_HEADER_TEXT = SECRET_HEADER_TEXT.lstrip()
DEFAULT_SIZE_IN_MBITS = 128
MAX_NUMBER_OF_HEX_DIGITS = math.log(DEFAULT_SIZE_IN_MBITS, 2) - 1
def generate_file_offsets(files_with_sizes):
offset = get_header_size(files_with_sizes)
for file, size in files_with_sizes:
yield (file, offset)
offset += size
def generate_header(files_with_sizes):
return "".join([
SECRET_HEADER_TEXT,
struct.pack("<i", len(files_with_sizes)),
] + [
struct.pack("<i", offset)
for _, offset
in generate_file_offsets(files_with_sizes)
])
def get_header_table_size(files):
return 4 + (len(files) * 4)
def get_header_table_start():
return len(SECRET_HEADER_TEXT)
def get_header_size(files):
return get_header_table_start() + get_header_table_size(files)
def to_raw_audio(file, sample_rate=None):
if sample_rate:
sample_rate_args = ['-ar', str(sample_rate)]
else:
sample_rate_args = []
command = [
'ffmpeg', '-i', file, '-f', 'u8', '-ac', '1'
] + sample_rate_args + ['-']
return subprocess.check_output(
command,
stderr=open(os.devnull, 'w')
)
def normalize_name(fname):
return (
fname
.split('/')[-1]
.split('.')[0]
.upper()
.replace(' ', '_')
.replace("(", "")
.replace(")", ""))
def generate_c_defines(offsets, files_with_sizes, sample_rate, buffer_size):
yield "#ifndef flash_constants"
yield "#define flash_constants"
yield "#define BUFFER_SIZE_BYTE {}".format(buffer_size)
yield "#define AUDIO_TRACK_COUNT {}".format(len(files_with_sizes))
yield "#define AUDIO_SAMPLE_RATE {}".format(sample_rate)
for (file, offset), (_, size) in zip(offsets, files_with_sizes):
yield "#define AUDIO_{}_START {}".format(normalize_name(file), offset)
yield "#define AUDIO_{}_END {}".format(
normalize_name(file), offset + size)
yield "typedef struct {"
yield " unsigned int start_offset;"
yield " unsigned int end_offset;"
yield "} TrackMetadataEntry;"
yield "TrackMetadataEntry TrackMetadata[AUDIO_TRACK_COUNT];"
yield ("#define TrackMetadataForLevel(level) "
"(TrackMetadata[level % AUDIO_TRACK_COUNT])")
yield "#endif"
def generate_c_arrays(offsets, files_with_sizes):
yield '#include "flash_constants.h"'
"""
Note: The original Super Mario Land cart uses this music mapping:
.musicByLevel
db 7, 7, 3
db 8, 8, 5
db 7, 3, 3
db 6, 6 ,5
"""
yield "TrackMetadataEntry TrackMetadata[AUDIO_TRACK_COUNT] = {"
for (file, offset), (_, size) in zip(offsets, files_with_sizes):
yield "\t{{ AUDIO_{}_START, AUDIO_{}_END }},".format(
normalize_name(file),
normalize_name(file))
yield "};"
def get_total_audio_length(files):
# assume original files are at 44100
sample_rate = 44100
converted_audio_files = [to_raw_audio(f, sample_rate) for f in tqdm(files)]
return float(sum(len(file) for file in converted_audio_files)) \
/ float(sample_rate)
def as_hex_address(num):
return ("0x{:0" + str(int(MAX_NUMBER_OF_HEX_DIGITS)) + "X}").format(num)
def flashprep(
directory,
outfile='flash.bin',
maxsize=DEFAULT_SIZE_IN_MBITS * 1024 * 1024 / 8,
sample_rate=None,
buffer_size=512,
):
"""
Given a list of files, generate one binary with all of them concatenated,
along with C defines specifying their bounds.
"""
files = sorted(glob(os.path.join(directory, '*')))
if sample_rate is None:
quantize = 50
available_space = maxsize - get_header_size(files)
audio_length_seconds = get_total_audio_length(files)
float_sample_rate = (float(available_space) / audio_length_seconds)
sample_rate = math.floor(float_sample_rate / quantize) * quantize
print "Using sample rate of {:,} to fit {:,}s into {:,} bytes.".format(
sample_rate, audio_length_seconds, available_space)
converted_audio_files = [to_raw_audio(f, sample_rate) for f in tqdm(files)]
files_with_sizes = [
(f, len(converted_audio_file))
for f, converted_audio_file in tqdm(zip(files, converted_audio_files))
]
header = generate_header(files_with_sizes)
offsets = list(generate_file_offsets(files_with_sizes))
for (file, offset), (_, size) in zip(offsets, files_with_sizes):
print (
"File {} starts at byte {:,} ({}) and "
"ends at byte {:,} ({})").format(
file,
offset,
as_hex_address(offset + size),
offset + size,
as_hex_address(offset + size))
with open('data/flash_constants.h', 'w') as f:
for line in generate_c_defines(
offsets,
files_with_sizes,
sample_rate,
buffer_size
):
f.write(line)
f.write("\n")
with open('data/flash_constants.c', 'w') as f:
for line in generate_c_arrays(offsets, files_with_sizes):
f.write(line)
f.write("\n")
with open(outfile, 'w') as out:
out.write(header)
for f in tqdm(converted_audio_files):
out.write(f)
size = out.tell()
# pad with zero bytes until we hit the size of the flash
out.write("\x00" * (maxsize - size))
if size > maxsize:
raise ValueError((
"Size of requested data ({:,} bytes) is greater "
"than max flash size ({:,} bytes)!").format(size, maxsize))
print "Wrote {:,} bytes ({}% of max) to {}.".format(
size,
100. * (float(size) / float(maxsize)),
outfile
)
if __name__ == "__main__":
input_folder = sys.argv[-1]
if not os.path.isdir(input_folder):
raise ValueError(
"Usage: %s path_to_directory_with_sorted_audio_files" %
sys.argv[0])
flashprep(input_folder)