-
Notifications
You must be signed in to change notification settings - Fork 365
/
OcarinaSongs.py
379 lines (315 loc) · 13.1 KB
/
OcarinaSongs.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
PLAYBACK_START = 0xB781DC
PLAYBACK_LENGTH = 0xA0
ACTIVATION_START = 0xB78E5C
ACTIVATION_LENGTH = 0x09
ACTIVATION_TO_PLAYBACK_NOTE = {
0: 0x02, # A
1: 0x05, # Down
2: 0x09, # Right
3: 0x0B, # Left
4: 0x0E, # Up
0xFF: 0xFF, # Rest
}
import random
from Utils import random_choices
# checks if one list is a sublist of the other (in either direction)
# python is magic.....
def subsong(song1, song2):
# convert both lists to strings
s1 = ''.join( map(chr, song1.activation))
s2 = ''.join( map(chr, song2.activation))
# check if either is a substring of the other
return (s1 in s2) or (s2 in s1)
# give random durations and volumes to the notes
def fast_playback(activation):
playback = []
for note_index, note in enumerate(activation):
playback.append( {'note': note, 'duration': 0x03, 'volume': 0x57} )
return playback
# give random durations and volumes to the notes
def random_playback(activation):
playback = []
for note_index, note in enumerate(activation):
duration = random.randint(0x8, 0x20)
# make final note longer on average
if( note_index + 1 >= len(activation) ):
duration = random.randint(0x10, 0x30)
volume = random.randint(0x40, 0x60)
playback.append( {'note': note, 'duration': duration, 'volume': volume} )
# randomly rest
if( random.random() < 0.1 ):
duration = random.randint(0x8, 0x18)
playback.append( {'note': 0xFF, 'duration': duration, 'volume': 0} )
return playback
# gives random volume and duration to the notes of piece
def random_piece_playback(piece):
playback = []
for note in piece:
duration = random.randint(0x8, 0x20)
volume = random.randint(0x40, 0x60)
playback.append( {'note': note, 'duration': duration, 'volume': volume} )
return playback
# takes the volume/duration of playback, and notes of piece, and creates a playback piece
# assumes the lists are the same length
def copy_playback_info(playback, piece):
return [ { 'note': n, 'volume': p['volume'], 'duration': p['duration']} for (p, n) in zip(playback, piece) ]
def identity(x):
return x
def random_piece(count, allowed=range(0,5)):
return random_choices(allowed, k=count)
def invert_piece(piece):
return [4 - note for note in piece]
def reverse_piece(piece):
return piece[::-1]
def clamp(val, low, high):
return max(low, min(high, val))
def transpose_piece(amount):
def transpose(piece):
return [clamp(note + amount, 0, 4) for note in piece]
return transpose
def compose(f, g):
return lambda x: f(g(x))
def add_transform_to_piece(piece, transform):
return piece + transform(piece)
def repeat(piece):
return 2 * piece
# a Song contains it's simple note data, as well as the data to be stored into the rom
class Song():
def increase_duration_to(self, duration):
if self.total_duration >= duration:
return
duration_needed = duration - self.total_duration
last_note_duration = self.playback[-1]['duration']
last_note_adds = 0x7F - last_note_duration
if last_note_adds >= duration_needed:
self.playback[-1]['duration'] += duration_needed
self.format_playback_data()
return
else:
self.playback[-1]['duration'] = 0x7F
duration_needed -= last_note_adds
while duration_needed >= 0x7F:
self.playback.append( {'note': 0xFF, 'duration': 0x7F, 'volume': 0} )
duration_needed -= 0x7F
self.playback.append( {'note': 0xFF, 'duration': duration_needed, 'volume': 0} )
self.format_playback_data()
def two_piece_playback(self, piece, extra_position='none', activation_transform=identity, playback_transform=identity):
piece_length = len(piece)
piece2 = activation_transform( piece )
# add playback parameters
playback_piece1 = random_piece_playback(piece)
playback_piece2 = copy_playback_info(playback_transform(playback_piece1), piece2)
if extra_position == 'none':
self.length = 2 * piece_length
self.activation = piece + piece2
# add a rest between
duration = random.randint(0x8, 0x18)
rest = {'note': 0xFF, 'duration': duration, 'volume': 0}
self.playback = playback_piece1 + [rest] + playback_piece2
else:
self.length = 2 * piece_length + 1
extra = random_piece(1)
extra_playback = random_piece_playback(extra)
if extra_position == 'start':
self.activation = extra + piece + piece2
self.playback = extra_playback + playback_piece1 + playback_piece2
elif extra_position == 'middle':
self.activation = piece + extra + piece2
self.playback = playback_piece1 + extra_playback + playback_piece2
elif extra_position == 'end':
self.activation = piece + piece2 + extra
self.playback = playback_piece1 + playback_piece2 + extra_playback
# add rests between repeated notes in the playback so that they work in-game
def break_repeated_notes(self, duration=0x08):
new_playback = []
for note_index, note in enumerate(self.playback):
new_playback.append(note)
if ( note_index + 1 < len(self.playback) ) and \
( self.playback[note_index]['note'] == self.playback[note_index + 1]['note'] ):
new_playback.append( {'note': 0xFF, 'duration': duration, 'volume': 0} )
self.playback = new_playback
# create the list of bytes that will be written into the rom for the activation
def format_activation_data(self):
# data is 1 byte for the song length,
# len bytes for the song, and the remainder is padding
padding = [0] * (ACTIVATION_LENGTH - (self.length + 1))
self.activation_data = [self.length] + self.activation + padding
# create the list of byte that will be written in to the rom for the playback
def format_playback_data(self):
self.playback_data = []
self.total_duration = 0
for note in self.playback:
pitch = ACTIVATION_TO_PLAYBACK_NOTE[ note['note'] ]
self.playback_data += [pitch, 0, 0, note['duration'], note['volume'], 0, 0, 0]
self.total_duration += note['duration']
# add a rest at the end
self.playback_data += [0xFF, 0, 0, 0, 0x5A, 0, 0, 0]
# pad the rest of the song
padding = [0] * (PLAYBACK_LENGTH - len(self.playback_data))
self.playback_data += padding
def display(self):
activation_string = 'Activation Data:\n\t' + ' '.join( map( "{:02x}".format, self.activation_data) )
# break playback into groups of 8...
index = 0
broken_up_playback = []
while index < len(self.playback_data):
broken_up_playback.append( self.playback_data[index:index + 8])
index += 8
playback_string = 'Playback Data:\n\t' + '\n\t'.join( map( lambda line: ' '.join( map( "{:02x}".format, line) ), broken_up_playback ) )
return activation_string + '\n' + playback_string
# create a song, based on a given scheme
def __init__(self, rand_song=True, piece_size=3, extra_position='none', starting_range=range(0,5), activation_transform=identity, playback_transform=identity, activation=None):
if activation:
self.length = len(activation)
self.activation = activation
self.playback = fast_playback(self.activation)
self.break_repeated_notes(0x03)
self.format_playback_data()
self.increase_duration_to(45)
return
if rand_song:
self.length = random.randint(4, 8)
self.activation = random_choices(range(0,5), k=self.length)
self.playback = random_playback(self.activation)
else:
if extra_position != 'none':
piece_size = 3
piece = random_piece(piece_size, starting_range)
self.two_piece_playback(piece, extra_position, activation_transform, playback_transform)
self.break_repeated_notes()
self.format_activation_data()
self.format_playback_data()
__str__ = __repr__ = display
# randomly choose song parameters
def get_random_song():
rand_song = random_choices([True, False], [1, 9])[0]
piece_size = random_choices([3, 4], [5, 2])[0]
extra_position = random_choices(['none', 'start', 'middle', 'end'], [12, 1, 1, 1])[0]
activation_transform = identity
playback_transform = identity
weight_damage = 0
should_transpose = random_choices([True, False], [1, 4])[0]
starting_range=range(0,5)
if should_transpose:
weight_damage = 2
direction = random_choices(['up', 'down'], [1, 1])[0]
if direction == 'up':
starting_range=range(0,4)
activation_transform = transpose_piece(1)
elif direction == 'down':
starting_range=range(1,5)
activation_transform = transpose_piece(-1)
should_invert = random_choices([True, False], [3 - weight_damage, 6])[0]
if should_invert:
weight_damage += 1
activation_transform = compose(invert_piece, activation_transform)
should_reflect = random_choices([True, False], [5 - weight_damage, 4])[0]
if should_reflect:
activation_transform = compose(reverse_piece, activation_transform)
playback_transform = reverse_piece
# print([rand_song, piece_size, extra_position, starting_range, should_transpose, should_invert, should_reflect])
song = Song(rand_song, piece_size, extra_position, starting_range, activation_transform, playback_transform)
# rate its difficulty
difficulty = 0
difficulty = piece_size * 12
if extra_position != 'none':
difficulty += 12
if should_transpose:
difficulty += 25
if should_reflect:
difficulty += 10
if should_invert:
difficulty += 20
if rand_song:
difficulty = 11 * len(song.activation)
song.difficulty = difficulty
return song
# create a list of 12 songs, none of which are sub-strings of any other song
def generate_song_list(scarecrow_song=None):
songs = []
for _ in range(12):
while True:
# generate a completely random song
song = get_random_song()
# test the song against all existing songs
is_good = True
for other_song in songs + [scarecrow_song] if scarecrow_song else songs:
if subsong(song, other_song):
is_good = False
if is_good:
songs.append(song)
break
# sort the songs by length
songs.sort(key=lambda s: s.difficulty)
return songs
# replace the playback and activation requirements for the ocarina songs
def replace_songs(rom, scarecrow_song=None):
songs = generate_song_list(scarecrow_song)
#print('\n\n'.join(map(str, songs)))
song_order = [
8, # zelda's lullaby
6, # saria's song
7, # epona's song
11, # song of storms
10, # song of time
9, # sun's song
5, # prelude of light
0, # minuet of forest
1, # bolero of fire
2, # serenade of water
3, # requiem of spirit
4, # nocturne of shadow
]
for index, song in enumerate(songs):
# fix the song of time
if song_order[index] == 10:
song.increase_duration_to(260)
# write the song to the activation table
cur_offset = ACTIVATION_START + song_order[index] * ACTIVATION_LENGTH
rom.write_bytes(cur_offset, song.activation_data)
# write the songs to the playback table
song_offset = PLAYBACK_START + song_order[index] * PLAYBACK_LENGTH
rom.write_bytes(song_offset, song.playback_data)
original_songs = [
'LURLUR',
'ULRULR',
'DRLDRL',
'RDURDU',
'RADRAD',
'ADUADU',
'AULRLR',
'DADALDLD',
'ADRRL',
'ADALDA',
'LRRALRD',
'URURLU'
]
note_map = {
'A': 0,
'D': 1,
'R': 2,
'L': 3,
'U': 4
}
def verify_scarecrow_song_str(scarecrow_song_str:str, randomize_ocarina_songs:bool):
if len(scarecrow_song_str) != 8:
raise Exception('Scarecrow Song must be 8 notes long')
if len(set(scarecrow_song_str.upper())) == 1:
raise Exception('Scarecrow Song must contain at least two different notes')
scarecrow_song = str_to_song(scarecrow_song_str)
if not randomize_ocarina_songs:
for original_song in original_songs:
song_notes = []
for c in original_song:
song_notes.append(note_map[c])
song = Song(activation=song_notes)
if subsong(scarecrow_song, song):
raise Exception('You may not have the Scarecrow Song contain an existing song')
return scarecrow_song
def str_to_song(song:str):
notes = []
for c in song.upper():
if c not in note_map:
raise Exception('Invalid note %s. Valid notes are A, D, R, L, U' % c)
notes.append(note_map[c])
return Song(activation=notes)