Handle remaining samples < 4 correctly(?).
[calfbox.git] / py / drumkit_editor.py
blob7d42352465960095ce15243ee4bff18277a62007
1 import cbox
2 import glob
3 import os
4 from gui_tools import *
5 import sfzparser
7 #sample_dir = "/media/resources/samples/dooleydrums/"
8 sample_dir = cbox.Config.get("init", "sample_dir")
10 ####################################################################################################################################################
12 class SampleDirsModel(Gtk.ListStore):
13 def __init__(self):
14 Gtk.ListStore.__init__(self, GObject.TYPE_STRING, GObject.TYPE_STRING)
15 found = False
16 for entry in cbox.Config.keys("sample_dirs"):
17 path = cbox.Config.get("sample_dirs", entry)
18 self.append((entry, path))
19 found = True
20 if not found:
21 print ("Warning: no sample directories defined. Please add one or more entries of a form: 'name=/path/to/my/samples' to [sample_dirs] section of .cboxrc")
22 self.append(("home", os.getenv("HOME")))
23 self.append(("/", "/"))
24 def has_dir(self, dir):
25 return dir in [path for entry, path in self]
27 ####################################################################################################################################################
29 class SampleFilesModel(Gtk.ListStore):
30 def __init__(self, dirs_model):
31 self.dirs_model = dirs_model
32 self.is_refreshing = False
33 Gtk.ListStore.__init__(self, GObject.TYPE_STRING, GObject.TYPE_STRING)
35 def refresh(self, sample_dir):
36 try:
37 self.is_refreshing = True
38 self.clear()
39 if sample_dir is not None:
40 if not self.dirs_model.has_dir(sample_dir):
41 self.append((os.path.dirname(sample_dir.rstrip("/")) + "/", "(up)"))
42 filelist = sorted(glob.glob("%s/*" % sample_dir))
43 for f in sorted(filelist):
44 if os.path.isdir(f) and not self.dirs_model.has_dir(f + "/"):
45 self.append((f + "/", os.path.basename(f)+"/"))
46 for f in sorted(filelist):
47 if f.lower().endswith(".wav") and not os.path.isdir(f):
48 self.append((f,os.path.basename(f)))
49 finally:
50 self.is_refreshing = False
52 ####################################################################################################################################################
54 class KeyModelPath(object):
55 def __init__(self, controller, var = None):
56 self.controller = controller
57 self.var = var
58 self.args = []
59 def plus(self, var):
60 if self.var is not None:
61 print ("Warning: key model plus used twice with %s and %s" % (self.var, var))
62 return KeyModelPath(self.controller, var)
63 def set(self, value):
64 model = self.controller.get_current_layer_model()
65 oldval = model.attribs[self.var]
66 model.attribs[self.var] = value
67 if value != oldval and not self.controller.no_sfz_update:
68 print ("%s: set %s to %s" % (self.controller, self.var, value))
69 self.controller.update_kit_later()
71 ####################################################################################################################################################
73 layer_attribs = {
74 'volume' : 0.0,
75 'pan' : 0.0,
76 'ampeg_attack' : 0.001,
77 'ampeg_hold' : 0.001,
78 'ampeg_decay' : 0.001,
79 'ampeg_sustain' : 100.0,
80 'ampeg_release' : 0.1,
81 'tune' : 0.0,
82 'transpose' : 0,
83 'cutoff' : 22000.0,
84 'resonance' : 0.7,
85 'fileg_depth' : 0.0,
86 'fileg_attack' : 0.001,
87 'fileg_hold' : 0.001,
88 'fileg_decay' : 0.001,
89 'fileg_sustain' : 100.0,
90 'fileg_release' : 0.1,
91 'lovel' : 1,
92 'hivel' : 127,
93 'group' : 0,
94 'off_by' : 0,
95 'effect1' : 0,
96 'effect2' : 0,
97 'output' : 0,
100 ####################################################################################################################################################
102 class KeySampleModel(object):
103 def __init__(self, key, sample, filename):
104 self.key = key
105 self.sample = sample
106 self.filename = filename
107 self.mode = "one_shot"
108 self.attribs = layer_attribs.copy()
109 def set_sample(self, sample, filename):
110 self.sample = sample
111 self.filename = filename
112 def to_sfz(self):
113 if self.filename == '':
114 return ""
115 s = "<region> key=%d sample=%s loop_mode=%s" % (self.key, self.filename, self.mode)
116 s += "".join([" %s=%s" % item for item in self.attribs.items()])
117 return s + "\n"
118 def to_markup(self):
119 return "<small>%s</small>" % self.sample
121 ####################################################################################################################################################
123 class KeyModel(Gtk.ListStore):
124 def __init__(self, key):
125 self.key = key
126 Gtk.ListStore.__init__(self, GObject.TYPE_STRING, GObject.TYPE_PYOBJECT)
127 def to_sfz(self):
128 return "".join([ksm.to_sfz() for name, ksm in self])
129 def to_markup(self):
130 return "\n".join([ksm.to_markup() for name, ksm in self])
132 ####################################################################################################################################################
134 class BankModel(dict):
135 def __init__(self):
136 dict.__init__(self)
137 self.clear()
138 def to_sfz(self):
139 s = ""
140 for key in self:
141 s += self[key].to_sfz()
142 return s
143 def clear(self):
144 dict.clear(self)
145 for b in range(36, 36 + 16):
146 self[b] = KeyModel(b)
147 def from_sfz(self, data, path):
148 self.clear()
149 sfz = sfzparser.SFZ()
150 sfz.parse(data)
151 for r in sfz.regions:
152 rdata = r.merged()
153 if ('key' in rdata) and ('sample' in rdata) and (rdata['sample'] != ''):
154 key = sfznote2value(rdata['key'])
155 sample = rdata['sample']
156 sample_short = os.path.basename(sample)
157 if key in self:
158 ksm = KeySampleModel(key, sample_short, sfzparser.find_sample_in_path(path, sample))
159 for k, v in rdata.items():
160 if k in ksm.attribs:
161 if type(layer_attribs[k]) is float:
162 ksm.attribs[k] = float(v)
163 elif type(layer_attribs[k]) is int:
164 ksm.attribs[k] = int(float(v))
165 else:
166 ksm.attribs[k] = v
167 self[key].append((sample_short, ksm))
169 ####################################################################################################################################################
171 class LayerListView(Gtk.TreeView):
172 def __init__(self, controller):
173 Gtk.TreeView.__init__(self, None)
174 self.controller = controller
175 self.insert_column_with_attributes(0, "Name", Gtk.CellRendererText(), text=0)
176 self.set_cursor((0,))
177 #self.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [("text/plain", 0, 1)], Gdk.DragAction.COPY)
178 self.connect('cursor-changed', self.cursor_changed)
179 #self.connect('drag-data-get', self.drag_data_get)
180 self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
181 self.drag_dest_set_target_list([])
182 self.drag_dest_add_text_targets()
183 self.connect('drag_data_received', self.drag_data_received)
184 def cursor_changed(self, w):
185 self.controller.on_layer_changed()
186 def drag_data_received(self, widget, context, x, y, selection, info, etime):
187 sample, filename = selection.get_text().split("|")
188 pad_model = self.controller.get_current_pad_model()
189 pad_model.append((sample, KeySampleModel(pad_model.key, sample, filename)))
190 self.controller.current_pad.update_label()
191 self.controller.on_sample_dragged(self)
193 ####################################################################################################################################################
195 class LayerEditor(Gtk.VBox):
196 def __init__(self, controller, bank_model):
197 Gtk.VBox.__init__(self)
198 self.table = Gtk.Table(len(self.fields) + 1, 2)
199 self.table.set_size_request(240, -1)
200 self.controller = controller
201 self.bank_model = bank_model
202 self.name_widget = Gtk.Label()
203 self.table.attach(self.name_widget, 0, 2, 0, 1)
204 self.refreshers = []
205 for i in range(len(self.fields)):
206 self.refreshers.append(self.fields[i].add_row(self.table, i + 1, KeyModelPath(controller), None))
207 #self.table.attach(left_label(self.fields[i].label), 0, 1, i + 1, i + 2)
208 self.pack_start(self.table, False, False, 0)
210 def refresh(self):
211 data = self.controller.get_current_layer_model()
212 if data is None:
213 self.name_widget.set_text("")
214 else:
215 self.name_widget.set_text(data.sample)
216 data = data.attribs
217 for r in self.refreshers:
218 r(data)
220 fields = [
221 SliderRow("Volume", "volume", -100, 0),
222 SliderRow("Pan", "pan", -100, 100),
223 SliderRow("Effect 1", "effect1", 0, 100),
224 SliderRow("Effect 2", "effect2", 0, 100),
225 IntSliderRow("Output", "output", 0, 7),
226 SliderRow("Tune", "tune", -100, 100),
227 IntSliderRow("Transpose", "transpose", -48, 48),
228 IntSliderRow("Low velocity", "lovel", 1, 127),
229 IntSliderRow("High velocity", "hivel", 1, 127),
230 MappedSliderRow("Amp Attack", "ampeg_attack", env_mapper),
231 MappedSliderRow("Amp Hold", "ampeg_hold", env_mapper),
232 MappedSliderRow("Amp Decay", "ampeg_decay", env_mapper),
233 SliderRow("Amp Sustain", "ampeg_sustain", 0, 100),
234 MappedSliderRow("Amp Release", "ampeg_release", env_mapper),
235 MappedSliderRow("Flt Cutoff", "cutoff", filter_freq_mapper),
236 MappedSliderRow("Flt Resonance", "resonance", LogMapper(0.707, 16, "%0.1f x")),
237 SliderRow("Flt Depth", "fileg_depth", -4800, 4800),
238 MappedSliderRow("Flt Attack", "fileg_attack", env_mapper),
239 MappedSliderRow("Flt Hold", "fileg_hold", env_mapper),
240 MappedSliderRow("Flt Decay", "fileg_decay", env_mapper),
241 SliderRow("Flt Sustain", "fileg_sustain", 0, 100),
242 MappedSliderRow("Flt Release", "fileg_release", env_mapper),
243 IntSliderRow("Group", "group", 0, 15),
244 IntSliderRow("Off by group", "off_by", 0, 15),
247 ####################################################################################################################################################
249 class PadButton(Gtk.RadioButton):
250 def __init__(self, controller, bank_model, key):
251 Gtk.RadioButton.__init__(self, use_underline = False)
252 self.set_mode(False)
253 self.controller = controller
254 self.bank_model = bank_model
255 self.key = key
256 self.set_size_request(100, 100)
257 self.update_label()
258 self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
259 self.drag_dest_set_target_list([])
260 self.drag_dest_add_text_targets()
261 self.connect('drag_data_received', self.drag_data_received)
262 #self.connect('toggled', lambda widget: widget.controller.on_pad_selected(widget) if widget.get_active() else None)
263 self.connect('pressed', self.on_clicked)
264 def get_key_model(self):
265 return self.bank_model[self.key]
266 def drag_data_received(self, widget, context, x, y, selection, info, etime):
267 sample, filename = selection.get_text().split("|")
268 self.get_key_model().clear()
269 self.get_key_model().append((sample, KeySampleModel(self.key, sample, filename)))
270 self.update_label()
271 self.controller.on_sample_dragged(self)
272 def update_label(self):
273 data = self.get_key_model()
274 if data == None:
275 self.set_label("-")
276 else:
277 self.set_label("-")
278 self.get_child().set_markup(data.to_markup())
279 self.get_child().set_line_wrap(True)
280 def on_clicked(self, w):
281 self.controller.play_note(self.key)
282 w.controller.on_pad_selected(w)
284 ####################################################################################################################################################
286 class PadTable(Gtk.Table):
287 def __init__(self, controller, bank_model, rows, columns):
288 Gtk.Table.__init__(self, rows, columns, True)
290 self.keys = {}
291 group = None
292 for r in range(0, rows):
293 for c in range(0, columns):
294 key = 36 + (rows - r - 1) * columns + c
295 b = PadButton(controller, bank_model, key)
296 if group is not None:
297 b.set_group(group)
298 self.attach(standard_align(b, 0.5, 0.5, 0, 0), c, c + 1, r, r + 1)
299 self.keys[key] = b
300 def refresh(self):
301 for pad in self.keys.values():
302 pad.update_label()
304 ####################################################################################################################################################
306 class FileView(Gtk.TreeView):
307 def __init__(self, dirs_model, controller):
308 self.controller = controller
309 self.is_playing = True
310 self.dirs_model = dirs_model
311 self.files_model = SampleFilesModel(dirs_model)
312 Gtk.TreeView.__init__(self, self.files_model)
313 self.insert_column_with_attributes(0, "Name", Gtk.CellRendererText(), text=1)
314 self.set_cursor((0,))
315 self.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
316 self.drag_source_add_text_targets()
317 self.cursor_changed_handler = self.connect('cursor-changed', self.cursor_changed)
318 self.connect('drag-data-get', self.drag_data_get)
319 self.connect('row-activated', self.on_row_activated)
321 def stop_playing(self):
322 if self.is_playing:
323 self.controller.stop_preview()
324 self.is_playing = False
326 def start_playing(self, fn):
327 self.is_playing = True
328 self.controller.start_preview(fn)
330 def cursor_changed(self, w):
331 if self.files_model.is_refreshing:
332 self.stop_playing()
333 return
335 c = self.get_cursor()
336 if c[0] is not None:
337 fn = self.files_model[c[0].get_indices()[0]][0]
338 if fn.endswith("/"):
339 return
340 if fn != "":
341 self.start_playing(fn)
342 else:
343 self.stop_playing(fn)
345 def drag_data_get(self, treeview, context, selection, target_id, etime):
346 cursor = treeview.get_cursor()
347 if cursor is not None:
348 c = cursor[0].get_indices()[0]
349 fr = self.files_model[c]
350 selection.set_text(str(fr[1]+"|"+fr[0]), -1)
352 def on_row_activated(self, treeview, path, column):
353 c = self.get_cursor()
354 fn, label = self.files_model[c[0].get_indices()[0]]
355 if fn.endswith("/"):
356 self.files_model.refresh(fn)
358 ####################################################################################################################################################
360 class EditorDialog(Gtk.Dialog):
361 def __init__(self, parent):
362 self.prepare_scene()
363 Gtk.Dialog.__init__(self, "Drum kit editor", parent, Gtk.DialogFlags.DESTROY_WITH_PARENT,
366 self.menu_bar = Gtk.MenuBar()
367 self.menu_bar.append(create_menu("_Kit", [
368 ("_New", self.on_kit_new),
369 ("_Open...", self.on_kit_open),
370 ("_Save as...", self.on_kit_save_as),
371 ("_Close", lambda w: self.response(Gtk.ResponseType.OK)),
373 self.menu_bar.append(create_menu("_Layer", [
374 ("_Delete", self.on_layer_delete),
376 self.vbox.pack_start(self.menu_bar, False, False, 0)
378 self.hbox = Gtk.HBox(spacing = 5)
380 self.update_source = None
381 self.current_pad = None
382 self.dirs_model = SampleDirsModel()
383 self.bank_model = BankModel()
384 self.tree = FileView(self.dirs_model, self)
385 self.layer_list = LayerListView(self)
386 self.layer_editor = LayerEditor(self, self.bank_model)
387 self.no_sfz_update = False
389 combo = Gtk.ComboBox(model = self.dirs_model)
390 cell = Gtk.CellRendererText()
391 combo.pack_start(cell, True)
392 combo.add_attribute(cell, 'text', 0)
393 combo.connect('changed', lambda combo, tree_model, combo_model: tree_model.refresh(combo_model[combo.get_active()][1] if combo.get_active() >= 0 else None), self.tree.get_model(), combo.get_model())
394 combo.set_active(0)
395 sw = Gtk.ScrolledWindow()
396 sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
397 sw.add(self.tree)
399 left_box = Gtk.VBox(spacing = 5)
400 left_box.pack_start(combo, False, False, 0)
401 left_box.pack_start(sw, True, True, 5)
402 self.hbox.pack_start(left_box, True, True, 0)
403 sw.set_size_request(200, -1)
405 self.pads = PadTable(self, self.bank_model, 4, 4)
406 self.hbox.pack_start(self.pads, True, True, 5)
408 right_box = Gtk.VBox(spacing = 5)
409 sw = Gtk.ScrolledWindow()
410 sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
411 sw.set_size_request(320, 100)
412 sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
413 sw.add(self.layer_list)
414 right_box.pack_start(sw, True, True, 0)
415 sw = Gtk.ScrolledWindow()
416 sw.set_size_request(320, 200)
417 sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.ALWAYS)
418 sw.add_with_viewport(self.layer_editor)
419 right_box.pack_start(sw, True, True, 0)
420 self.hbox.pack_start(right_box, True, True, 0)
422 self.vbox.pack_start(self.hbox, False, False, 0)
423 self.vbox.show_all()
424 widget = self.pads.keys[36]
425 widget.set_active(True)
427 self.update_kit()
429 def prepare_scene(self):
430 found_scene = None
431 for scene in cbox.Document.get_engine().status().scenes:
432 scene_status = scene.status()
433 layers = [layer.status().instrument_name for layer in scene_status.layers]
434 if '_preview_sample' in layers and '_preview_kit' in layers:
435 found_scene = scene
436 break
437 if found_scene is None:
438 self.scene = cbox.Document.get_engine().new_scene()
439 self.scene.add_new_instrument_layer("_preview_sample", "stream_player", pos = 0)
440 ps = self.scene.status().instruments['_preview_sample'][1]
441 ps.cmd('/output/1/gain', None, -12.0)
442 self.scene.add_new_instrument_layer("_preview_kit", "sampler", pos = 1)
443 else:
444 self.scene = found_scene
445 _, self._preview_kit = self.scene.status().instruments['_preview_kit']
446 _, self._preview_sample = self.scene.status().instruments['_preview_sample']
448 def update_kit(self):
449 self._preview_kit.engine.load_patch_from_string(0, "", self.bank_model.to_sfz(), "Preview")
450 self.update_source = None
451 return False
453 def update_kit_later(self):
454 if self.update_source is not None:
455 glib.source_remove(self.update_source)
456 self.update_source = glib.idle_add(self.update_kit)
458 def on_sample_dragged(self, widget):
459 self.update_kit()
460 if widget == self.current_pad:
461 self.layer_list.set_cursor(len(self.layer_list.get_model()) - 1)
462 # self.pad_editor.refresh()
464 def refresh_layers(self):
465 try:
466 self.no_sfz_update = True
467 if self.current_pad is not None:
468 self.layer_list.set_model(self.bank_model[self.current_pad.key])
469 self.layer_list.set_cursor(0)
470 else:
471 self.layer_list.set_model(None)
472 self.layer_editor.refresh()
473 finally:
474 self.no_sfz_update = False
476 def on_pad_selected(self, widget):
477 self.current_pad = widget
478 self.refresh_layers()
480 def on_layer_changed(self):
481 self.layer_editor.refresh()
483 def on_layer_delete(self, w):
484 if self.layer_list.get_cursor()[0] is None:
485 return None
486 model = self.layer_list.get_model()
487 model.remove(model.get_iter(self.layer_list.get_cursor()[0]))
488 self.current_pad.update_label()
489 self.layer_editor.refresh()
490 self.update_kit()
491 self.layer_list.set_cursor(0)
493 def get_current_pad_model(self):
494 return self.current_pad.get_key_model()
496 def get_current_layer_model(self):
497 if self.layer_list.get_cursor()[0] is None:
498 return None
499 return self.layer_list.get_model()[self.layer_list.get_cursor()[0]][1]
501 def on_kit_new(self, widget):
502 self.bank_model.from_sfz('', '')
503 self.pads.refresh()
504 self.refresh_layers()
505 self.update_kit()
507 def on_kit_open(self, widget):
508 dlg = Gtk.FileChooserDialog('Open a pad bank', self, Gtk.FileChooserAction.OPEN,
509 (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.APPLY))
510 dlg.add_filter(standard_filter(["*.sfz", "*.SFZ"], "SFZ files"))
511 dlg.add_filter(standard_filter(["*"], "All files"))
512 try:
513 if dlg.run() == Gtk.ResponseType.APPLY:
514 sfz_data = open(dlg.get_filename(), "r").read()
515 self.bank_model.from_sfz(sfz_data, dlg.get_current_folder())
516 self.pads.refresh()
517 self.refresh_layers()
518 self.update_kit()
519 finally:
520 dlg.destroy()
522 def on_kit_save_as(self, widget):
523 dlg = Gtk.FileChooserDialog('Save a pad bank', self, Gtk.FileChooserAction.SAVE,
524 (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.APPLY))
525 dlg.add_filter(standard_filter(["*.sfz", "*.SFZ"], "SFZ files"))
526 dlg.add_filter(standard_filter(["*"], "All files"))
527 try:
528 if dlg.run() == Gtk.ResponseType.APPLY:
529 open(dlg.get_filename(), "w").write(self.bank_model.to_sfz())
530 finally:
531 dlg.destroy()
533 def start_preview(self, filename):
534 self._preview_sample.engine.load(filename)
535 self._preview_sample.engine.play()
536 def stop_preview(self):
537 self._preview_sample.engine.unload()
538 def play_note(self, note, vel = 127):
539 self.scene.send_midi_event(0x90, note, vel)
540 self.scene.send_midi_event(0x80, note, vel)