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 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