-
Notifications
You must be signed in to change notification settings - Fork 1
/
textgen.py
256 lines (230 loc) · 8.38 KB
/
textgen.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
from util import *
from reference import *
# Note names
CHROMATIC_SCALE_SHARPS = ['c', 'c#', 'd', 'd#', 'e', 'f', 'f#', 'g', 'g#', 'a', 'a#', 'b']
# Change depending on your OS
BACKSLASH = '/'
# What goes between notes in a chord
DELIMITER = ' '
# What goes between timesteps
TIMESTEP = '\n'
# Rest
REST = 'w'
# Measure break
MEASURE = '|'
# BEGIN and END tokens
BEGIN = 'START'
END = 'END'
# Note ON/OFF suffixes
ON = '<'
TAP = '^'
OFF = '>'
# Resolution
SCALE = 6
def index_to_key(index):
''' Convert an index 0-87 to a note (0 --> A0, 3 --> C1, etc) '''
return CHROMATIC_SCALE_SHARPS[(index + 9) % 12] + str((index + 9) // 12)
def key_to_index(key):
''' Convert a note name to an index 0-87 (A0 --> 0, C1 --> 3, etc) '''
try:
return 12 * int(key[-1]) + CHROMATIC_SCALE_SHARPS.index(key[:-1]) - 9
# Return a special index if something can't be parsed
except:
return -1
def numpy_to_text(arr, phrases=False):
'''
Convert a numpy path to text
Details:
arr: piano roll in (timesteps, keys)
phrases: a boolean, if True ==> look for end phrase token (89th key)
returns: a string representing the music
'''
ret = ''
for step in arr:
# Stop early if end of phrase
if phrases and step[88] == 1:
break
# Find where notes are "active"
notes = np.where(step == 1)[0]
if np.size(notes) != 0:
# Add first note
ret += index_to_key(notes[0])
# Delimit additional notes
for note in notes[1:]:
ret += DELIMITER + index_to_key(note)
# Rest
else:
ret += REST
ret += TIMESTEP
return ret
def numpy_to_text_onoff(arr, phrases=False, measureBar=False):
'''
Convert a numpy path to text using on / off notation
Details:
arr: piano roll in (timesteps, keys)
phrases: a boolean, if True ==> look for end phrase token (89th key)
returns: a string representing the music
'''
ret = ''
for i, step in enumerate(arr):
# Stop early if end of phrase
if phrases and step[88] == 1:
break
# Find where notes are "active"
notes = np.where(step == 1)[0]
# String for this timestep
timestep_string = ''
# Go through all notes
for note in notes:
# Four cases: ON, TAP, OFF, or sustained (nothing)
# No previous timestep or not active in previous timestep
if i == 0 or arr[i-1][note] == 0:
# No next timestep or deactive in next timestep (special case)
if i == len(arr)-1 or arr[i+1][note] == 0:
timestep_string += TAP + index_to_key(note) + DELIMITER
else:
timestep_string += ON + index_to_key(note) + DELIMITER
# No next timestep or deactive in next timestep
elif i == len(arr)-1 or arr[i+1][note] == 0:
timestep_string += OFF + index_to_key(note) + DELIMITER
if timestep_string == '':
# Nothing new here
ret += REST
else:
# Cut out the last delimiter
ret += timestep_string[:-len(DELIMITER)]
if measureBar and (i + 1) % (SCALE * 4) == 0:
ret += TIMESTEP + MEASURE
ret += TIMESTEP
if measureBar:
left_in_measure = ((SCALE * 4) - (i + 1)) % (SCALE * 4)
for i in range(left_in_measure):
ret += REST + TIMESTEP
return ret
def make_corpus(verbose=False, input_dir=INPUT_NUMPY_DIR, fname='{}corpus.txt'.format(INPUT_TEXT_DIR), onoff=False, measureBar=False):
'''
Construct corpus from input numpy directory
Details:
verbose: boolean, if True ==> print out the files in the input directory
input_dir: directory to look for npy files
fname: filename to save text file as
onoff: boolean, if True ==> use on/off notation
returns: None
'''
file = open(fname, "a")
# Clear file
file.truncate(0)
for fname in tqdm(os.listdir(input_dir)):
if verbose:
print(fname)
full_path = "{}{}/{}_bin.npy".format(input_dir, fname, fname)
arr = np.transpose(np.load(full_path))
file.write(BEGIN + TIMESTEP)
if onoff:
file.write(numpy_to_text_onoff(arr, measureBar=measureBar))
else:
file.write(numpy_to_text(arr))
file.write(END + TIMESTEP)
file.close()
def make_phrase_corpus(fname='{}phrases.txt'.format(INPUT_TEXT_DIR), onoff=False, load_fname='phrases.npy'):
'''
Construct corpus from input numpy directory using phrases
Details:
fname: filename to save text file as
onoff: boolean, if True ==> use on/off notation
load_fname: filename for phrases npy
returns: None
'''
file = open(fname, "a")
file.truncate(0)
phrases = np.load(load_fname)
for i, phrase in enumerate(phrases):
file.write(BEGIN + TIMESTEP)
if onoff:
file.write(numpy_to_text_onoff(phrase, phrases=True))
else:
file.write(numpy_to_text(phrase, phrases=True))
file.write(END + TIMESTEP)
file.close()
def text_to_numpy(text):
'''
Converts a string of text into a numpy piano roll
Details:
text: a string to be translated
returns: a numpy array
'''
tokens = text.split(TIMESTEP)
arr = np.zeros((1, 88))
for token in tokens:
if token in [BEGIN, END]:
continue
this_timestep = np.zeros(88)
if token != REST:
notes = token.split(DELIMITER)
for note in notes:
index = key_to_index(note)
if index != -1:
this_timestep[index] = 1
arr = np.vstack((arr, [this_timestep]))
return np.transpose(arr[1:])
def text_to_numpy_onoff(text):
'''
Converts a string of text into a numpy piano roll using on/off notation
Details:
text: a string to be translated
returns: a numpy array
'''
tokens = text.split(TIMESTEP)
arr = np.zeros((1, 88))
# Keep track of which notes we're supposed to keep filling in
obligations = np.zeros(88)
for token in tokens:
if token in [BEGIN, END, MEASURE]:
continue
this_timestep = np.zeros(88)
# Set all obligations
for i in range(88):
this_timestep[i] = obligations[i]
# Deal with tokens
if token != REST:
notes = token.split(DELIMITER)
for note in notes:
# Get prefix (on, off, or tap)
prefix = note[0]
index = key_to_index(note[1:])
# Quick validity check, otherwise ignore
if index != -1 and prefix in [ON, OFF, TAP]:
# If on (and valid; obligation should be off) activate and turn on obligation
if prefix == ON and obligations[index] == 0:
obligations[index] = 1
this_timestep[index] = 1
# If off (and valid; obligation should be on) activate and turn off obligation
elif prefix == OFF and obligations[index] == 1:
obligations[index] = 0
this_timestep[index] = 1
# If tap (and valid; obligation should be off) activate and turn off
elif prefix == TAP and obligations[index] == 0:
obligations[index] = 0
this_timestep[index] = 1
arr = np.vstack((arr, [this_timestep]))
return np.transpose(arr[1:])
def text_to_midi(text, suffix, onoff=False):
'''
Convert output text back into a MIDI file
Details:
text: a string to be translated
suffix: a string to be appended to the filename (before the extension)
onoff: boolean, if True ==> use on/off notation
'''
if onoff:
parsed = text_to_numpy_onoff(text)
else:
parsed = text_to_numpy(text)
part = numpy_to_part(parsed)
part_to_midi(part, 'GPT_model_{}'.format(suffix))
''' MAKE CORPUS '''
make_corpus(fname='{}corpus_plain.txt'.format(INPUT_TEXT_DIR), onoff=False, measureBar=True)
# make_phrase_corpus(fname='{}phrases.txt'.format(INPUT_TEXT_DIR), onoff=True)
''' READ OUTPUT '''
# text = open('{}output.txt'.format(INPUT_TEXT_DIR), 'r').read()
# text_to_midi(text, '1', onoff=True)