-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathwriter.py
443 lines (370 loc) · 14.6 KB
/
writer.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
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
#!/usr/bin/env python
from reader import Measure
STEP_TO_NUMBER = {
'C': 1,
'D': 2,
'E': 3,
'F': 4,
'G': 5,
'A': 6,
'B': 7
}
def stepToNumber(step):
return str(STEP_TO_NUMBER[step])
def generateOctaveMark(octave):
if octave >= 4:
return "'" * (octave - 4)
else:
return "," * (4 - octave)
NOTE_DEGREE_TABLE = {
'C': 0, 'B#': 0,
'C#': 1, 'Db': 1,
'D': 2,
'D#': 3, 'Eb': 3,
'E': 4, 'Fb': 4,
'F': 5, 'E#': 5,
'F#': 6, 'Gb': 6,
'G': 7,
'G#': 8, 'Ab': 8,
'A': 9,
'A#': 10, 'Bb': 10,
'B': 11, 'Cb': 11
}
DEGREE_NOTE_TABLE = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
def getTransposedPitch(note_name, octave, offset):
degree = NOTE_DEGREE_TABLE[note_name]
transposed_degree = degree + offset
transposed_octave = octave + transposed_degree // 12
transposed_degree %= 12
return (DEGREE_NOTE_TABLE[transposed_degree], transposed_octave)
def getTransposeOffsetToC(key):
degree = NOTE_DEGREE_TABLE[key]
if degree <= 6:
return -degree
else:
return 12 - degree
def appendForTie(text, char):
if '-' in text: # put char before the first -
idx = text.index('-')
text = f'{text[:idx]}{char} {text[idx:]}'
else:
text = f'{text} {char}'
return text
class WriterError(Exception):
pass
class WriterOptions:
def __init__(self):
self.ignore_key = False
self.max_measures_per_line = 4
self.min_measures_per_line = 2
self.notes_per_line = 0 # rough hint, or disabled if 0
class WriterDict:
def __init__(self):
self.sharp = '#'
self.flat = 'b'
self.tuplet = ('', '')
self.line_suffix = ''
class BaseWriter:
def __init__(self, **kwds):
self._options = WriterOptions()
self._dict = WriterDict()
for key, value in kwds.items():
if value is None:
pass
if hasattr(self._options, key):
setattr(self._options, key, value)
if hasattr(self._dict, key):
setattr(self._dict, key, value)
def toHeader(self, title, key, beats, beat_type, tempo, composer):
raise NotImplementedError()
def toLinePrefix(self, part_index, num_parts):
return ''
def toNote(self, step, accidental, octave_mark):
raise NotImplementedError()
def toTremolo(self, tremolo):
return ''
def toSlide(self, text, slide_up):
return ''
def toTieStart(self, text):
return f'( {text}'
def generate(self, reader):
return self.generateHeader(reader) + '\n' + self.generateBody(reader)
def generateHeader(self, reader):
title = reader.getWorkTitle()
if self._options.ignore_key:
key = 'C'
else:
key = reader.getInitialKeySignature().replace(
'#', self._dict.sharp).replace('b', self._dict.flat)
beats, beat_type = reader.getInitialTime()
tempo = reader.getInitialTempo()
pickup = reader.getPickup()
composer = reader.getComposer()
return self.toHeader(title, key, beats, beat_type, tempo, pickup, composer)
def generateBody(self, reader):
parts = reader.getPartIdList()
part_measures = dict()
for part in parts:
part_measures[part] = list(reader.iterMeasures(part))
lines = []
measure_count = max(len(measures) for measures in part_measures.values())
num_measures_per_line = self.computeNumMeasuresPerLine(part_measures.values())
for i in range(0, measure_count, num_measures_per_line):
begin = i
end = min(i + num_measures_per_line, measure_count)
for part_index, part in enumerate(parts):
line = self.toLinePrefix(part_index, len(parts))
line += self.generateMeasures(part_measures[part][begin:end])
line += self._dict.line_suffix
lines.append(line)
lines.append('') # empty line
return '\n'.join(lines)
def generateMeasures(self, measureList):
result = ''
for i, measure in enumerate(measureList):
result += self.toLeftBarline(i, measure)
result += ' '
result += self.generateMeasure(measure)
result += ' '
result += self.toRightBarline(measure)
return result
def generateMeasure(self, measure):
pieces = [self.generateNote(note) for note in measure]
return ' '.join(pieces)
def generateNote(self, note):
result = self.generateBasicNote(note)
tremolo = note.getTremolo()
if tremolo > 0:
result += self.toTremolo(tremolo)
prefix, suffix = self.generateTimePrefixAndSuffix(*note.getDisplayedDuration())
result = prefix + result + suffix
if note.isTieStart():
result = self.toTieStart(result)
if note.isTupletStart():
result = self._dict.tuplet[0] + result
if note.isTupletStop():
result += self._dict.tuplet[1]
if note.isSlideStart():
result = self.toSlide(result, note.isSlideUp())
if note.isTieStop():
result = appendForTie(result, ')')
return result
def generateBasicNote(self, note):
if note.isRest():
return '0'
else:
pitch = note.getPitch()
(note_name, octave) = note.getPitch()
if not self._options.ignore_key:
keysig = note.getAttributes().getKeySignature()
if keysig != 'C':
offset = getTransposeOffsetToC(keysig)
(note_name, octave) = getTransposedPitch(note_name, octave, offset)
step = note_name[0:1] # C, D, E, F, G, A, B
accidental = note_name[1:2] # sharp (#) and flat (b)
if accidental == '#':
accidental = self._dict.sharp
elif accidental == 'b':
accidental = self._dict.flat
return self.toNote(stepToNumber(step), accidental, generateOctaveMark(octave))
def generateTimePrefixAndSuffix(self, duration, divisions, prefix=''):
if duration < divisions: # less than quarter notes: add / and continue
return self.toShortTimePrefixAndSuffix(duration, divisions, prefix)
elif duration == divisions: # quarter notes
return prefix, ''
elif duration * 2 == divisions * 3: # syncopated notes
return prefix, '.'
else: # sustained more than 1.5 quarter notes: add - and continue
prefix, suffix = self.generateTimePrefixAndSuffix(duration - divisions, divisions)
return prefix, ' -' + suffix
def computeNumMeasuresPerLine(self, collection_of_measures, cutoff=2):
result = self._options.max_measures_per_line
if self._options.notes_per_line > 0:
for measures in collection_of_measures:
num_measures = 0
num_notes = 0
for measure in measures:
count = len(measure.getNotes())
if count > cutoff:
num_measures += 1
num_notes += count
if num_notes > 0:
value = round(self._options.notes_per_line * num_measures / num_notes)
result = min(result, value)
result = max(result, self._options.min_measures_per_line)
return result
class Jianpu99Writer(BaseWriter):
def __init__(self, *args, **kwds):
kwds.update(dict(
flat = '$', # flat is represented by '$' in this format
tuplet = ('(y', ')'),
))
BaseWriter.__init__(self, *args, **kwds)
def toHeader(self, title, key, beats, beat_type, tempo, pickup, composer):
header = 'V: 1.0\n' # jianpu99 version number
if title is not None:
header += f'B: {title}\n'
header += f'D: {key}\n'
header += f'P: {beats}/{beat_type}\n'
if tempo > 0.01:
bpm = round(float(tempo) * beat_type / 4)
header += f'J: {bpm}\n'
if composer is not None:
header += f'Z: {composer}\n'
return header
def toLinePrefix(self, part_index, num_parts):
prefix = ''
if num_parts > 1:
prefix = str(part_index + 1)
return f'Q{prefix}: '
def toNote(self, step, accidental, octave_mark):
return step + accidental + octave_mark
def toTremolo(self, tremolo):
return '"%s"' % ('/' * tremolo)
def toSlide(self, text, slide_up):
if slide_up:
return f'{text}­'
else:
return f'{text}&xhy'
def toShortTimePrefixAndSuffix(self, duration, divisions, prefix):
assert(duration < divisions)
prefix, suffix = self.generateTimePrefixAndSuffix(duration * 2, divisions, prefix)
return prefix, suffix + '/'
def toLeftBarline(self, index, measure):
result = ''
if measure.getLeftBarlineType() == Measure.BARLINE_REPEAT:
if index == 0:
result = '|:'
else:
result = ':'
if measure.isSegno():
result += '&hs'
elif measure.isCoda():
result += '&ty'
return result
def toRightBarline(self, measure):
if measure.getRightBarlineType() == Measure.BARLINE_REPEAT:
result = ':|'
elif measure.getRightBarlineType() == Measure.BARLINE_DOUBLE:
result = '||/'
elif measure.getRightBarlineType() == Measure.BARLINE_FINAL:
result = '||'
else:
result = '|'
if measure.isDalSegno():
result += '&ds'
elif measure.isToCoda():
result += '&ty'
return result
LY_TIME_PREFIXES = ('', 'q', 's', 'd', 'h')
ANAC_TABLE = { # jianpu-ly only has limited support to anac
1: '64', 2: '32', 3: '32.', 4: '16', 6: '16.', 8: '8', 12: '8.',
16: '4', 24: '4.', 32: '2', 48: '2.'
}
def wrapLy(s):
if not s:
return ''
if type(s) in (list, tuple):
s = '\n'.join(s)
return f'\nLP:\n{s}\n:LP\n'
def wrapLyMark(s, raw=False, down=False):
result = (r'\once \override Score.RehearsalMark.break-visibility = #begin-of-line-invisible '
r'\mark \markup { \sans \bold \fontsize #-5 {%s} }' % s)
if down:
result = r'\once \override Score.RehearsalMark.direction = #DOWN ' + result
if not raw:
result = wrapLy(result)
return result
class JianpuLyWriter(BaseWriter):
def __init__(self, *args, **kwds):
kwds.update(dict(
tuplet = ('3[ ', ' ]'),
line_suffix = r'\break',
))
BaseWriter.__init__(self, *args, **kwds)
self.right_after_final_bar = False
def toHeader(self, title, key, beats, beat_type, tempo, pickup, composer):
header = ''
if tempo > 0.01:
header = f'%% tempo: {beat_type}={round(tempo)}\n'
if title is not None:
header += f'title={title}\n'
header += f'1={key}\n'
anac = ''
if pickup < beats - .01:
index = round(pickup * 64 / beat_type)
if index in ANAC_TABLE:
anac = ',' + ANAC_TABLE[index]
header += f'{beats}/{beat_type}{anac}\n'
if composer is not None:
header += f'composer={composer}\n'
ly_lines = [
r'\set Score.barNumberVisibility = #all-bar-numbers-visible',
r'\override Score.BarNumber.font-size = #-6',
r'\override Score.BarNumber.break-visibility = #end-of-line-invisible',
r'\override Score.NonMusicalPaperColumn.padding = #2',
]
if anac:
ly_lines.append(r'\set Score.currentBarNumber = #2')
header += wrapLy(ly_lines)
return header
def toNote(self, step, accidental, octave_mark):
return accidental + step + octave_mark
def toTremolo(self, tremolo):
return wrapLy(fr"-\tweak #'Y-offset #-4.0 -\tweak #'X-offset #0.6 :{4 * 2 ** tremolo}")
def toSlide(self, text, slide_up):
y = [2, -1]
if slide_up:
y.reverse()
return wrapLy(r'\once \override Glissando.bound-details.left.Y = #%d '
r'\once \override Glissando.bound-details.right.Y = #%d '
% (y[0], y[1])) + text + ' \glissando '
def toTieStart(self, text):
return appendForTie(text, '(')
def toShortTimePrefixAndSuffix(self, duration, divisions, prefix):
assert(duration < divisions)
for i in range(len(LY_TIME_PREFIXES) - 1):
if prefix == LY_TIME_PREFIXES[i]:
return self.generateTimePrefixAndSuffix(
duration * 2, divisions, LY_TIME_PREFIXES[i + 1])
raise ValueError('Too short a note duration')
def toLeftBarline(self, index, measure):
ly_lines = []
if measure.getLeftBarlineType() == Measure.BARLINE_REPEAT:
ly_lines.append(r'\bar ".|:"')
if self.right_after_final_bar: # add an invisible measure
beats, beat_type = measure.getAttributes().getTime()
ly_lines = [
r'\once \override Score.BarNumber.break-visibility = ##(#f #f #f)',
' s4' * (beats * 4 // beat_type)
] + ly_lines + [
r'\bar "|"',
fr'\set Score.currentBarNumber = #{measure.getMeasureNumber()}',
]
self.right_after_final_bar = False
if measure.isSegno():
ly_lines.append(wrapLyMark(r'\musicglyph #"scripts.segno"', raw=True))
elif measure.isCoda():
ly_lines.append(wrapLyMark(r'\musicglyph #"scripts.coda"', raw=True))
return wrapLy(ly_lines)
def toRightBarline(self, measure):
ly_lines = []
if measure.getRightBarlineType() == Measure.BARLINE_REPEAT:
ly_lines.append(r'\bar ":|."')
elif measure.getRightBarlineType() == Measure.BARLINE_DOUBLE:
ly_lines.append(r'\bar "||"')
elif measure.getRightBarlineType() == Measure.BARLINE_FINAL:
self.right_after_final_bar = True
ly_lines.append(r'\bar "|."')
# else: use auto barline
if measure.isDalSegno():
ly_lines.insert(0, wrapLyMark(measure.getDalSegno(), raw=True, down=True))
elif measure.isToCoda():
ly_lines.insert(0, wrapLyMark('To \musicglyph #"scripts.coda"', raw=True))
return wrapLy(ly_lines)
def getGrammars():
return 'jianpu99', 'jianpu-ly'
def createWriter(grammar, *args, **kwds):
if grammar == 'jianpu-ly':
return JianpuLyWriter(*args, **kwds)
return Jianpu99Writer(*args, **kwds)