mscore3 1.0.2__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mscore/__init__.py +302 -0
- mscore/res/score.mscx +5490 -0
- mscore/res/score.mscz +0 -0
- mscore/scripts/__init__.py +0 -0
- mscore/scripts/channels.py +30 -0
- mscore/scripts/clear_synth.py +33 -0
- mscore/scripts/port_partition.py +58 -0
- mscore/test.py +91 -0
- mscore3-1.0.2.dist-info/LICENSE +619 -0
- mscore3-1.0.2.dist-info/METADATA +41 -0
- mscore3-1.0.2.dist-info/RECORD +13 -0
- mscore3-1.0.2.dist-info/WHEEL +5 -0
- mscore3-1.0.2.dist-info/entry_points.txt +5 -0
mscore/__init__.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# mscore/__init__.py
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2024 liyang <liyang@veronica>
|
|
4
|
+
#
|
|
5
|
+
# This program is free software; you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation; either version 2 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with this program; if not, write to the Free Software
|
|
17
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
18
|
+
# MA 02110-1301, USA.
|
|
19
|
+
#
|
|
20
|
+
"""
|
|
21
|
+
A python library for opening/inspecting/modifying MuseScore3 files.
|
|
22
|
+
"""
|
|
23
|
+
import os, sys, logging, configparser, glob, io
|
|
24
|
+
import xml.etree.ElementTree as et
|
|
25
|
+
from zipfile import ZipFile
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from sf2utils.sf2parse import Sf2File
|
|
28
|
+
from console_quiet import ConsoleQuiet
|
|
29
|
+
from node_soso import SmartNode, dump
|
|
30
|
+
|
|
31
|
+
__version__ = "1.0.2"
|
|
32
|
+
|
|
33
|
+
CHANNEL_NAMES = [ 'normal', 'open', 'mute', 'arco', 'tremolo', 'crescendo',
|
|
34
|
+
'marcato', 'staccato', 'flageoletti', 'slap', 'pop', 'pizzicato']
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ----------------------------
|
|
38
|
+
# MuseScore classes
|
|
39
|
+
|
|
40
|
+
class Part(SmartNode):
|
|
41
|
+
|
|
42
|
+
def instrument(self):
|
|
43
|
+
return Instrument.from_element(self.find('Instrument'))
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def name(self):
|
|
47
|
+
return self.element_text('trackName')
|
|
48
|
+
|
|
49
|
+
def __str__(self):
|
|
50
|
+
return f'<Part "{self.name}">'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Instrument(SmartNode):
|
|
54
|
+
|
|
55
|
+
def channels(self):
|
|
56
|
+
return Channel.from_elements(self.findall('Channel'))
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def name(self):
|
|
60
|
+
name = self.element_text('longName')
|
|
61
|
+
if name is None:
|
|
62
|
+
name = self.element_text('trackName')
|
|
63
|
+
return name
|
|
64
|
+
|
|
65
|
+
def musicxml_id(self):
|
|
66
|
+
return self.element_text('instrumentId')
|
|
67
|
+
|
|
68
|
+
def __str__(self):
|
|
69
|
+
return f'<Instrument "{self.name}">'
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Channel(SmartNode):
|
|
73
|
+
|
|
74
|
+
CC_VOLUME = 7;
|
|
75
|
+
CC_PAN = 10;
|
|
76
|
+
CC_BANK_MSB = 0;
|
|
77
|
+
CC_BANK_LSB = 32;
|
|
78
|
+
|
|
79
|
+
def program(self):
|
|
80
|
+
el = self.find('program')
|
|
81
|
+
return None if el is None else el.attrib['value']
|
|
82
|
+
|
|
83
|
+
def bank_msb(self):
|
|
84
|
+
msb = self.controller_value(self.CC_BANK_MSB)
|
|
85
|
+
return -1 if msb is None else msb
|
|
86
|
+
|
|
87
|
+
def bank_lsb(self):
|
|
88
|
+
lsb = self.controller_value(self.CC_BANK_LSB)
|
|
89
|
+
return -1 if lsb is None else lsb
|
|
90
|
+
|
|
91
|
+
def controller_value(self, ccid):
|
|
92
|
+
el = self.find('controller[@ctrl="%s"]' % ccid)
|
|
93
|
+
return None if el is None else el.attrib['value']
|
|
94
|
+
|
|
95
|
+
def idstring(self):
|
|
96
|
+
return '%02d:%02d:%02d' % (
|
|
97
|
+
int(self.bank_msb()),
|
|
98
|
+
int(self.bank_lsb()),
|
|
99
|
+
int(self.program())
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def name(self):
|
|
104
|
+
xmlname = self.attribute_value('name')
|
|
105
|
+
return 'normal' if xmlname is None else xmlname
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def midi_port(self):
|
|
109
|
+
"""
|
|
110
|
+
Always returns the public (1-based) channel number.
|
|
111
|
+
"""
|
|
112
|
+
text = self.element_text('midiPort')
|
|
113
|
+
return -1 if text is None else int(text) + 1
|
|
114
|
+
|
|
115
|
+
@midi_port.setter
|
|
116
|
+
def midi_port(self, value):
|
|
117
|
+
"""
|
|
118
|
+
"value" must be the public (1-based) channel number.
|
|
119
|
+
The actual node value is set to one less.
|
|
120
|
+
"""
|
|
121
|
+
node = self.find('midiPort')
|
|
122
|
+
if node is None:
|
|
123
|
+
node = et.SubElement(self.element, 'midiPort')
|
|
124
|
+
node.text = str(int(value) - 1)
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def midi_channel(self):
|
|
128
|
+
"""
|
|
129
|
+
Always returns the public (1-based) channel number.
|
|
130
|
+
"""
|
|
131
|
+
text = self.element_text('midiChannel')
|
|
132
|
+
return -1 if text is None else int(text) + 1
|
|
133
|
+
|
|
134
|
+
@midi_channel.setter
|
|
135
|
+
def midi_channel(self, value):
|
|
136
|
+
"""
|
|
137
|
+
"value" must be the public (1-based) channel number.
|
|
138
|
+
The actual node value is set to one less.
|
|
139
|
+
"""
|
|
140
|
+
node = self.find('midiChannel')
|
|
141
|
+
if node is None:
|
|
142
|
+
node = et.SubElement(self.element, 'midiChannel')
|
|
143
|
+
node.text = str(int(value) - 1)
|
|
144
|
+
|
|
145
|
+
def __str__(self):
|
|
146
|
+
return f'<Channel "{self.name}">'
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class Score():
|
|
150
|
+
|
|
151
|
+
__default_sfnames = None
|
|
152
|
+
__user_sfpaths = None
|
|
153
|
+
__sys_sfpaths = None
|
|
154
|
+
__sf2s = {}
|
|
155
|
+
|
|
156
|
+
__zip_entries = None
|
|
157
|
+
__zip_mscx_index = None
|
|
158
|
+
|
|
159
|
+
USER_SF2 = 0
|
|
160
|
+
SYSTEM_SF2 = 1
|
|
161
|
+
MISSING_SF2 = 3
|
|
162
|
+
|
|
163
|
+
def __init__(self, filename):
|
|
164
|
+
self.filename = filename
|
|
165
|
+
self.ext = os.path.splitext(filename)[-1]
|
|
166
|
+
if self.ext == '.mscx':
|
|
167
|
+
self.tree = et.parse(filename)
|
|
168
|
+
elif self.ext == '.mscz':
|
|
169
|
+
with ZipFile(self.filename, 'r') as zipfile:
|
|
170
|
+
self.__zip_entries = [
|
|
171
|
+
{
|
|
172
|
+
'info' :info,
|
|
173
|
+
'data' :zipfile.read(info.filename)
|
|
174
|
+
} for info in zipfile.infolist()
|
|
175
|
+
]
|
|
176
|
+
for idx in range(len(self.__zip_entries)):
|
|
177
|
+
if os.path.splitext(self.__zip_entries[idx]['info'].filename)[-1] == '.mscx':
|
|
178
|
+
self.__zip_mscx_index = idx
|
|
179
|
+
break
|
|
180
|
+
if self.__zip_mscx_index is None:
|
|
181
|
+
raise Exception("No mscx entries found in zip file")
|
|
182
|
+
with io.BytesIO(self.__zip_entries[self.__zip_mscx_index]['data']) as bob:
|
|
183
|
+
self.tree = et.parse(bob)
|
|
184
|
+
else:
|
|
185
|
+
raise Exception("Unsupported file extension: " + self.ext)
|
|
186
|
+
|
|
187
|
+
def save(self):
|
|
188
|
+
if self.ext == '.mscx':
|
|
189
|
+
self.tree.write(self.filename, xml_declaration=True, encoding='utf-8')
|
|
190
|
+
elif self.ext == '.mscz':
|
|
191
|
+
with io.BytesIO() as bob:
|
|
192
|
+
self.tree.write(bob)
|
|
193
|
+
self.__zip_entries[self.__zip_mscx_index]['data'] = bob.getvalue()
|
|
194
|
+
with ZipFile(self.filename, 'w') as zipfile:
|
|
195
|
+
for entry in self.__zip_entries:
|
|
196
|
+
zipfile.writestr(entry['info'], entry['data'])
|
|
197
|
+
|
|
198
|
+
def dump(self):
|
|
199
|
+
dump(self.tree)
|
|
200
|
+
|
|
201
|
+
def find(self, path):
|
|
202
|
+
return self.tree.find(path)
|
|
203
|
+
|
|
204
|
+
def findall(self, path):
|
|
205
|
+
return self.tree.findall(path)
|
|
206
|
+
|
|
207
|
+
def parts(self):
|
|
208
|
+
return Part.from_elements(self.findall('./Score/Part'))
|
|
209
|
+
|
|
210
|
+
def instruments(self):
|
|
211
|
+
return Instrument.from_elements(self.findall('./Score/Part/Instrument'))
|
|
212
|
+
|
|
213
|
+
def channels(self):
|
|
214
|
+
return Channel.from_elements(self.findall('./Score/Part/Instrument/Channel'))
|
|
215
|
+
|
|
216
|
+
def part(self, name):
|
|
217
|
+
for p in self.parts():
|
|
218
|
+
if p.name == name:
|
|
219
|
+
return p
|
|
220
|
+
|
|
221
|
+
def part_names(self):
|
|
222
|
+
return [ p.name for p in self.parts() ]
|
|
223
|
+
|
|
224
|
+
def duplicate_part_names(self):
|
|
225
|
+
a = self.part_names()
|
|
226
|
+
return [ name for name in list(set(a)) if a.count(name) > 1]
|
|
227
|
+
|
|
228
|
+
def has_duplicate_part_names(self):
|
|
229
|
+
return len(self.duplicate_part_names()) > 0
|
|
230
|
+
|
|
231
|
+
def instrument_names(self):
|
|
232
|
+
return [ p.instrument().name for p in self.parts() ]
|
|
233
|
+
|
|
234
|
+
def sound_fonts(self):
|
|
235
|
+
return list(set( el.text for el in self.findall('.//Trackesizer/Fluid/val') ))
|
|
236
|
+
|
|
237
|
+
@classmethod
|
|
238
|
+
def default_sound_fonts(cls):
|
|
239
|
+
if cls.__default_sfnames is None:
|
|
240
|
+
filename = os.path.join(str(Path.home()), '.local/share/MuseScore/MuseScore3/trackesizer.xml')
|
|
241
|
+
cls.__default_sfnames = [ node.text for node in et.parse(filename).findall('.//Fluid/val') ]
|
|
242
|
+
return cls.__default_sfnames
|
|
243
|
+
|
|
244
|
+
@classmethod
|
|
245
|
+
def ini_file(cls):
|
|
246
|
+
filename = os.path.join(str(Path.home()), '.config/MuseScore/MuseScore3.ini')
|
|
247
|
+
config = configparser.ConfigParser()
|
|
248
|
+
config.read(filename)
|
|
249
|
+
return config
|
|
250
|
+
|
|
251
|
+
@classmethod
|
|
252
|
+
def user_soundfont_dirs(cls):
|
|
253
|
+
return cls.ini_file()['application']['paths\mySoundfonts'].strip('"').split(';')
|
|
254
|
+
|
|
255
|
+
@classmethod
|
|
256
|
+
def system_soundfont_dirs(cls):
|
|
257
|
+
return ['/usr/share/sounds/sf2']
|
|
258
|
+
|
|
259
|
+
@classmethod
|
|
260
|
+
def _iter_sf_paths(cls, dirs):
|
|
261
|
+
for d in dirs:
|
|
262
|
+
yield from glob.glob(f'{d}/*.sf2')
|
|
263
|
+
|
|
264
|
+
@classmethod
|
|
265
|
+
def user_soundfonts(cls):
|
|
266
|
+
return list(cls._iter_sf_paths(cls.user_soundfont_dirs()))
|
|
267
|
+
|
|
268
|
+
@classmethod
|
|
269
|
+
def system_soundfonts(cls):
|
|
270
|
+
return list(cls._iter_sf_paths(cls.system_soundfont_dirs()))
|
|
271
|
+
|
|
272
|
+
@classmethod
|
|
273
|
+
def sf2(cls, sf_name):
|
|
274
|
+
if cls.__user_sfpaths is None:
|
|
275
|
+
cls.__user_sfpaths = { os.path.basename(path):path for path in cls.user_soundfonts() }
|
|
276
|
+
cls.__sys_sfpaths = { os.path.basename(path):path for path in cls.system_soundfonts() }
|
|
277
|
+
if sf_name not in cls.__sf2s:
|
|
278
|
+
if sf_name in cls.__user_sfpaths:
|
|
279
|
+
logging.debug('Inspecting user soundfont "%s"', sf_name)
|
|
280
|
+
cls.__sf2s[sf_name] = cls._get_parsed_sf2(cls.__user_sfpaths[sf_name])
|
|
281
|
+
elif sf_name in cls.__sys_sfpaths:
|
|
282
|
+
logging.debug('Inspecting user system "%s"', sf_name)
|
|
283
|
+
cls.__sf2s[sf_name] = cls._get_parsed_sf2(cls.__sys_sfpaths[sf_name])
|
|
284
|
+
else:
|
|
285
|
+
raise Exception('SoundFont "%s" not found', sf_name)
|
|
286
|
+
return cls.__sf2s[sf_name]
|
|
287
|
+
|
|
288
|
+
@classmethod
|
|
289
|
+
def _get_parsed_sf2(cls, filename):
|
|
290
|
+
with open(filename, 'rb') as file:
|
|
291
|
+
with ConsoleQuiet():
|
|
292
|
+
return Sf2File(file)
|
|
293
|
+
|
|
294
|
+
@classmethod
|
|
295
|
+
def is_score(cls, filename):
|
|
296
|
+
return os.path.splitext(filename)[-1] in ['.mscx', '.mscz']
|
|
297
|
+
|
|
298
|
+
def __str__(self):
|
|
299
|
+
return f'<Score "{self.filename}">'
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# end mscore/__init__.py
|