-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmusescore_drumkit.py
More file actions
244 lines (179 loc) · 7.73 KB
/
musescore_drumkit.py
File metadata and controls
244 lines (179 loc) · 7.73 KB
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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
drm file creator for musescore drumkit based on SFZ file.
Created on Wed 3rd Sep 19:19:27 2025
@author: luiz
"""
from sfzCreatorClass import Sample, SfzCreator
from typing import Iterable, Set
from lxml import etree
from dataclasses import dataclass, fields
import inspect
import numpy as np
musescore_version = '4.60'
def all_common_substrings(strings: Iterable[str], min_len: int = 1) -> Set[str]:
"""Return every substring that is present in *all* supplied strings."""
strings = list(strings)
if not strings:
return set()
# 1. candidates come from the shortest string
shortest = min(strings, key=len)
candidates = {
shortest[i:j]
for i in range(len(shortest))
for j in range(i + min_len, len(shortest) + 1)
}
# 2. keep only those that occur in every other string
for s in strings:
candidates = {sub for sub in candidates if sub in s}
if not candidates: # early exit
break
return candidates
def maximal_common_substrings(strings: Iterable[str], min_len: int = 1) -> Set[str]:
"""Same as above, but drop substrings that are already part of a longer common one."""
common = all_common_substrings(strings, min_len)
# discard any substrings that are *inside* a longer common substring
return {sub for sub in common
if not any(other != sub and sub in other for other in common)}
@dataclass
class Drum():
_pitch: int = None
head: str = 'normal'
line: int = 0
voice: int = 0
name: str = ''
stem: int = 1
shortcut: str = None
panelRow: int = None
panelColumn: int = None
def to_lxml(self) -> etree._Element:
"""
Return an lxml Element whose tag-names are the class-variable names
and whose text content is the string representation of the values.
"""
root = etree.Element(self.__class__.__name__) # <MyClass> … </MyClass>
for name, value in self.__dict__.items():
# skip private / callable stuff
if value is None:
continue
if not name.startswith('_') and not callable(value) and not inspect.isroutine(value):
child = etree.SubElement(root, name)
child.text = str(value)
root.set('pitch', str(self._pitch))
return root
@dataclass
class MusescoreDrumkit():
percussionPanelColumns: int = None
_Drums = []
def to_lxml(self) -> etree._Element:
root = etree.Element('museScore')
root.set('version', musescore_version)
# get elements (percussionPanelColumns)
for name, value in self.__dict__.items():
# skip private / callable stuff
if value is None:
continue
if not name.startswith('_') and not callable(value) and not inspect.isroutine(value):
child = etree.SubElement(root, name)
child.text = str(value)
for drum in self._Drums:
child = drum.to_lxml()
root.append(child)
return root
def to_file(self, output_filepath: str,
incl_header=True,
append_extension=True):
if append_extension:
if not output_filepath.endswith('.drm'):
output_filepath = output_filepath + '.drm'
root = self.to_lxml()
et = etree.ElementTree(root)
if incl_header:
header = '<?xml version="1.0" encoding="UTF-8"?>\n'
out_str = etree.tostring(root, pretty_print=True, encoding='unicode')
with open(output_filepath, 'w') as out_file:
out_file.write(header)
out_file.write(out_str)
else:
et.write(output_filepath, pretty_print=True)
def sfz_class_to_drumkit_class(sfz_creator_obj: SfzCreator,
panelColumns: int = 8):
# shorter alias
sfz = sfz_creator_obj
# get lists of lowkeys, hikeys and pitch keycentres
lokeys = np.array([sample.Lokey for sample in sfz.Samples])
hikeys = np.array([sample.Hikey for sample in sfz.Samples])
pitch_keycentres = np.array([sample.Pkeycenter for sample in sfz.Samples])
# list of drums
drums = []
# Getting filenames to identify common strings
fnames = [sample.Fname for sample in sfz.Samples]
# identitying which strings are in all filenames
common_to_all = maximal_common_substrings(fnames)
# removing common substrings
# fnames_diff = fnames[:] # copy
fnames_diff = []
for fname in fnames:
for common_str in common_to_all:
fname = fname.replace(common_str, '')
fnames_diff.append(fname)
# going through all midi notes and extracting which
# ones have a sound in the SFZ
idx_drum = 0
for idx_midi_key in range(128): # all 128 pitches, from 0 to 127
samples_current_key = []
idx_match_lokeys = idx_midi_key >= lokeys
idx_match_hikeys = idx_midi_key <= hikeys
idx_match_both = np.where(np.logical_and(
idx_match_lokeys,
idx_match_hikeys))[0]
samples_match_lokey = [sfz.Samples[idx] for idx in idx_match_lokeys]
samples_match_hikey = [sfz.Samples[idx] for idx in idx_match_hikeys]
samples_match_both = [sfz.Samples[idx] for idx in idx_match_both]
if len(samples_match_both) > 0:
drum = Drum()
drum._pitch = idx_midi_key
drum_fnames = [fnames_diff[idx] for idx in idx_match_both]
common_to_drum = maximal_common_substrings(drum_fnames)
drum_name = ' '.join(common_to_drum).replace('_', ' ').strip()
drum.name = drum_name
drum.head = 'normal' # hard-coded for now
drum.line = 0 # hard-coded for now
# this will be the horizontal position of the drum in the musescore Ui table selection
drum.panelColumn = idx_drum%panelColumns # mod, rest of division, to get the column
# this will be the vertical position of the drum in the musescore Ui table selection
drum.panelRow = idx_drum//panelColumns # integer division to get the row
drum.stem = 1 # hard-coded for now
drum.voice = 0 # hard-coded for now
drums.append(drum)
idx_drum = idx_drum + 1
musescore_drumkit = MusescoreDrumkit()
musescore_drumkit.percussionPanelColumns = panelColumns
musescore_drumkit._Drums = drums
return musescore_drumkit
def sfz_to_musescore_drumkit_drm(sfz_fp: str, outfile: str = None, panelColumns=8):
if outfile is None:
outfile = sfz_fp.removesuffix('.sfz') + '.drm'
sfz = SfzCreator()
sfz.loadSfzFile(sfz_fp)
drumkit = sfz_class_to_drumkit_class(sfz, panelColumns=panelColumns)
drumkit.to_file(outfile)
print(f'MuseScore Drumkit drm generated successfully, saved to {outfile}')
if __name__ == '__main__': # tests - how to use
# drum class examples
dr = Drum()
dr2 = Drum()
dr._pitch = 50
dr2._pitch = 60
drm = MusescoreDrumkit()
drm.percussionPanelColumns = 8
drm._Drums = [dr, dr2]
root = drm.to_lxml()
print(etree.tostring(root, pretty_print=True, encoding='unicode'))
# sfz class to drumkit class
sfz = SfzCreator()
sfz.loadSfzFile('my_sfz.sfz')
musescore_drumkit = sfz_class_to_drumkit_class(sfz)
# direct file conversion
sfz_to_musescore_drumkit_drm('my_sfz.sfz', 'mu_drumkit.drm')