-
Notifications
You must be signed in to change notification settings - Fork 0
/
musicxml2fmf.py
168 lines (147 loc) · 5.46 KB
/
musicxml2fmf.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
#!/usr/bin/env python3
import click
from lxml import etree
downsteps = {
"C": ("B", -1),
"D": ("C", 0),
"E": ("D", 0),
"F": ("E", 0),
"G": ("F", 0),
"A": ("G", 0),
"B": ("A", 0),
}
def getNoteValue(duration, divisions):
"""Convert MusicXML durations to note values.
This method takes as input the MusicXML duration and the current divisions.
The divisions define in how many divisions a measure is seperated.
Division 1 separates a measure in 4 units, division 2 in 8 units and so on.
Accordingly with divisons 2 the smalest note value is an 8th, i.e. duration 1 is an 8th note.
with divisons 1 the duration 1 is a 4th note.
Returns a tuple of the noteValue, a string of dots and the duration that is left over due to rounding error.
for divisions 2 the output is as follows:
durations = {
1: (8, "", None),
2: (4, "", None),
3: (4, ".", None),
4: (2, "", None),
5: (2, "", 1), # rounded
6: (2, ".", None),
7: (2, "..", None),
8: (1, "", None),
}
"""
v = duration
msb = 1 # most significant bit
msb_pos = 1
while (v := v >> 1):
msb = msb << 1
msb_pos += 1
noteValue = int(format(msb, 'b').zfill(2 + divisions)[::-1], base=2)
rest = format(duration - msb, 'b').zfill(msb_pos - 1)
dots = ""
i = 0
while i < len(rest) and int(rest[i]):
i += 1
dots += "."
correction = 0
if rest[i:]:
correction = int(rest[i:], base=2)
return (noteValue, dots, correction)
filetypeHeader = """Filetype: Flipper Music Format
Version: 0
BPM: {bpm}
Duration: {duration}
Octave: {octave}
Notes: """
def noteFactory(divisons):
class Note:
def __init__(self, duration=1, step="C", sharp="", octave=5):
self.duration = int(duration)
self.step = step
self.sharp = sharp
self.octave = int(octave)
def __str__(self):
noteValue, dot, correction = getNoteValue(self.duration, divisons)
correctionStr = ""
if correction:
correctionStr = ", " + str(Rest(correction))
return f"{noteValue}{self.step}{self.sharp}{self.octave}{dot}" + correctionStr
def __repr__(self):
return f"<{self.duration}, {self.step}, {self.sharp}, {self.octave}>"
def __add__(self, other):
if (
isinstance(other, Note)
and self.step == other.step
and self.octave == other.octave
and self.sharp == other.sharp
):
return [
Note(self.duration + other.duration, self.step, self.sharp, self.octave)
]
return [self, other]
class Rest:
def __init__(self, duration=1):
self.duration = int(duration)
def __str__(self):
noteValue, dot, correction = getNoteValue(self.duration, divisons)
correctionStr = ""
if correction:
correctionStr = ", " + str(Rest(correction))
return f"{noteValue}P{dot}" + correctionStr
return Note, Rest
@click.command()
@click.option("--input", help="File to read the MusicXML from (suffix: '.musicxml').")
@click.option("--output", help="File to write the Flipper Music to (suffix: '.fmf').")
@click.option(
"--bpm", default=120, help="Beats per minute for the piece (default: 120)."
)
@click.option("--duration", default=8, help="Default duration of a tone (default: 8).")
@click.option("--octave", default=5, help="Default octave of a note (default: 5).")
def convert(input, output, bpm, duration, octave):
defaultDuration = duration
defaultOctave = octave
flipperNotes = []
overTie = False
tree = etree.parse(input)
divisions = int(tree.xpath("//score-partwise/part/measure/attributes/divisions")[0].text)
Note, Rest = noteFactory(divisions)
musicXmlNotes = tree.xpath("//score-partwise/part/measure/note")
for noteTag in musicXmlNotes:
duration = noteTag.xpath("duration")[0].text
if noteTag.xpath("rest"):
flipperNotes.append(Rest(duration))
continue
alter = False
if noteTag.xpath("pitch/alter"):
alter = int(noteTag.xpath("pitch/alter")[0].text)
step = noteTag.xpath("pitch/step")[0].text
octave = int(noteTag.xpath("pitch/octave")[0].text)
sharp = ""
if alter:
if alter < 0:
step, octaveShift = downsteps[step]
octave = octave + octaveShift
sharp = "#"
note = Note(duration, step, sharp, octave)
if overTie:
tiedNotes = flipperNotes[-1] + note
flipperNotes = flipperNotes[:-1] + tiedNotes
else:
flipperNotes.append(note)
if noteTag.xpath("notations/tied") or noteTag.xpath("notations/slur"):
tieStart = False
tieStop = False
for tied in noteTag.xpath("notations/tied") + noteTag.xpath("notations/slur"):
tieStart = tied.attrib["type"] in "start"
tieStop = tied.attrib["type"] in "stop"
if tieStart:
overTie = True
elif tieStop:
overTie = False
with open(output, "w") as o:
o.write(
filetypeHeader.format(
bpm=bpm, duration=defaultDuration, octave=defaultOctave
)
)
o.write(", ".join(str(n) for n in flipperNotes))