Handle remaining samples < 4 correctly(?).
[calfbox.git] / py / drum_pattern_editor.py
blobfc32e77778d2819666b238a2a572e16138b0748b
1 #from gui_tools import *
2 from gi.repository import GObject, Gdk, Gtk, GooCanvas, GLib
3 import gui_tools
5 def guint(x):
6 value = GObject.Value()
7 value.init(GObject.TYPE_UINT)
8 value.set_uint(x)
9 return value
11 PPQN = 48
13 def standard_filter(patterns, name):
14 f = Gtk.FileFilter()
15 for p in patterns:
16 f.add_pattern(p)
17 f.set_name(name)
18 return f
20 def hide_item(citem):
21 citem.visibility = GooCanvas.CanvasItemVisibility.HIDDEN
23 def show_item(citem):
24 citem.visibility = GooCanvas.CanvasItemVisibility.VISIBLE
26 def polygon_to_path(points):
27 assert len(points) % 2 == 0, "Invalid number of points (%d) in %s" % (len(points), repr(points))
28 path = ""
29 if len(points) > 0:
30 path += "M %s %s" % (points[0], points[1])
31 if len(points) > 1:
32 for i in range(2, len(points), 2):
33 path += " L %s %s" % (points[i], points[i + 1])
34 return path
36 class NoteModel(object):
37 def __init__(self, pos, channel, row, vel, len = 1):
38 self.pos = int(pos)
39 self.channel = int(channel)
40 self.row = int(row)
41 self.vel = int(vel)
42 self.len = int(len)
43 self.item = None
44 self.selected = False
45 def __str__(self):
46 return "pos=%s row=%s vel=%s len=%s" % (self.pos, self.row, self.vel, self.len)
48 # This is stupid and needs rewriting using a faster data structure
49 class DrumPatternModel(GObject.GObject):
50 def __init__(self, beats, bars):
51 GObject.GObject.__init__(self)
52 self.ppqn = PPQN
53 self.beats = beats
54 self.bars = bars
55 self.notes = []
57 def import_data(self, data):
58 self.clear()
59 active_notes = {}
60 for t in data:
61 cmd = t[1] & 0xF0
62 if len(t) == 4 and (cmd == 0x90) and (t[3] > 0):
63 note = NoteModel(t[0], (t[1] & 15) + 1, t[2], t[3])
64 active_notes[t[2]] = note
65 self.add_note(note)
66 if len(t) == 4 and ((cmd == 0x90 and t[3] == 0) or cmd == 0x80):
67 if t[2] in active_notes:
68 active_notes[t[2]].len = t[0] - active_notes[t[2]].pos
69 del active_notes[t[2]]
70 end = self.get_length()
71 for n in active_notes.values():
72 n.len = end - n.pos
74 def clear(self):
75 self.notes = []
76 self.changed()
78 def add_note(self, note, send_changed = True):
79 self.notes.append(note)
80 if send_changed:
81 self.changed()
83 def remove_note(self, pos, row, channel):
84 self.notes = [note for note in self.notes if note.pos != pos or note.row != row or (channel is not None and note.channel != channel)]
85 self.changed()
87 def set_note_vel(self, note, vel):
88 note.vel = int(vel)
89 self.changed()
91 def set_note_len(self, note, len):
92 note.len = int(len)
93 self.changed()
95 def has_note(self, pos, row, channel):
96 for n in self.notes:
97 if n.pos == pos and n.row == row and (channel is None or n.channel == channel):
98 return True
99 return False
101 def get_note(self, pos, row, channel):
102 for n in self.notes:
103 if n.pos == pos and n.row == row and (channel is None or n.channel == channel):
104 return n
105 return None
107 def items(self):
108 return self.notes
110 def get_length(self):
111 return int(self.beats * self.bars * self.ppqn)
113 def changed(self):
114 self.emit('changed')
116 def delete_selected(self):
117 self.notes = [note for note in self.notes if not note.selected]
118 self.changed()
120 def group_set(self, **vals):
121 for n in self.notes:
122 if not n.selected:
123 continue
124 for k, v in vals.items():
125 setattr(n, k, v)
126 self.changed()
128 def transpose_selected(self, amount):
129 for n in self.notes:
130 if not n.selected:
131 continue
132 if n.row + amount < 0 or n.row + amount > 127:
133 continue
134 n.row += amount
135 self.changed()
137 GObject.type_register(DrumPatternModel)
138 GObject.signal_new("changed", DrumPatternModel, GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, ())
140 channel_ls = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT)
141 for ch in range(1, 17):
142 channel_ls.append((str(ch), ch))
143 snap_settings_ls = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT)
144 for row in [("1/4", PPQN), ("1/8", PPQN // 2), ("1/8T", PPQN//3), ("1/16", PPQN//4), ("1/16T", PPQN//6), ("1/32", PPQN//8), ("1/32T", PPQN//12), ("1/64", PPQN//4)]:
145 snap_settings_ls.append(row)
146 edit_mode_ls = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING)
147 for row in [("Drum", "D"), ("Melodic", "M")]:
148 edit_mode_ls.append(row)
150 class DrumEditorToolbox(Gtk.HBox):
151 def __init__(self, canvas):
152 Gtk.HBox.__init__(self, spacing = 5)
153 self.canvas = canvas
154 self.vel_adj = Gtk.Adjustment(100, 1, 127, 1, 10, 0)
156 self.pack_start(Gtk.Label("Channel:"), False, False, 0)
157 self.channel_setting = gui_tools.standard_combo(channel_ls, active_item_lookup = self.canvas.channel, lookup_column = 1)
158 self.channel_setting.connect('changed', lambda w: self.canvas.set_channel(w.get_model()[w.get_active()][1]))
159 self.pack_start(self.channel_setting, False, True, 0)
161 self.pack_start(Gtk.Label("Mode:"), False, False, 0)
162 self.mode_setting = gui_tools.standard_combo(edit_mode_ls, active_item_lookup = self.canvas.edit_mode, lookup_column = 1)
163 self.mode_setting.connect('changed', lambda w: self.canvas.set_edit_mode(w.get_model()[w.get_active()][1]))
164 self.pack_start(self.mode_setting, False, True, 0)
166 self.pack_start(Gtk.Label("Snap:"), False, False, 0)
167 self.snap_setting = gui_tools.standard_combo(snap_settings_ls, active_item_lookup = self.canvas.grid_unit, lookup_column = 1)
168 self.snap_setting.connect('changed', lambda w: self.canvas.set_grid_unit(w.get_model()[w.get_active()][1]))
169 self.pack_start(self.snap_setting, False, True, 0)
171 self.pack_start(Gtk.Label("Velocity:"), False, False, 0)
172 self.pack_start(Gtk.SpinButton(adjustment = self.vel_adj, climb_rate = 0, digits = 0), False, False, 0)
173 button = Gtk.Button("Load")
174 button.connect('clicked', self.load_pattern)
175 self.pack_start(button, True, True, 0)
176 button = Gtk.Button("Save")
177 button.connect('clicked', self.save_pattern)
178 self.pack_start(button, True, True, 0)
179 button = Gtk.Button("Double")
180 button.connect('clicked', self.double_pattern)
181 self.pack_start(button, True, True, 0)
182 self.pack_start(Gtk.Label("--"), False, False, 0)
184 def update_edit_mode(self):
185 self.mode_setting.set_active(gui_tools.ls_index(edit_mode_ls, self.canvas.edit_mode, 1))
187 def load_pattern(self, w):
188 dlg = Gtk.FileChooserDialog('Open a drum pattern', self.get_toplevel(), Gtk.FileChooserAction.OPEN,
189 (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.APPLY))
190 dlg.add_filter(standard_filter(["*.cbdp"], "Drum patterns"))
191 dlg.add_filter(standard_filter(["*"], "All files"))
192 try:
193 if dlg.run() == Gtk.ResponseType.APPLY:
194 pattern = self.canvas.pattern
195 f = file(dlg.get_filename(), "r")
196 pattern.clear()
197 pattern.beats, pattern.bars = [int(v) for v in f.readline().strip().split(";")]
198 for line in f.readlines():
199 line = line.strip()
200 if not line.startswith("n:"):
201 pos, row, vel = line.split(";")
202 row = int(row) + 36
203 channel = 10
204 len = 1
205 else:
206 pos, channel, row, vel, len = line[2:].split(";")
207 self.canvas.pattern.add_note(NoteModel(pos, channel, row, vel, len), send_changed = False)
208 f.close()
209 self.canvas.pattern.changed()
210 self.canvas.update_grid()
211 self.canvas.update_notes()
212 finally:
213 dlg.destroy()
214 def save_pattern(self, w):
215 dlg = Gtk.FileChooserDialog('Save a drum pattern', self.get_toplevel(), Gtk.FileChooserAction.SAVE,
216 (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.APPLY))
217 dlg.add_filter(standard_filter(["*.cbdp"], "Drum patterns"))
218 dlg.add_filter(standard_filter(["*"], "All files"))
219 dlg.set_current_name("pattern.cbdp")
220 try:
221 if dlg.run() == Gtk.ResponseType.APPLY:
222 pattern = self.canvas.pattern
223 f = file(dlg.get_filename(), "w")
224 f.write("%s;%s\n" % (pattern.beats, pattern.bars))
225 for i in self.canvas.pattern.items():
226 f.write("n:%s;%s;%s;%s;%s\n" % (i.pos, i.channel, i.row, i.vel, i.len))
227 f.close()
228 finally:
229 dlg.destroy()
230 def double_pattern(self, w):
231 len = self.canvas.pattern.get_length()
232 self.canvas.pattern.bars *= 2
233 new_notes = []
234 for note in self.canvas.pattern.items():
235 new_notes.append(NoteModel(note.pos + len, note.channel, note.row, note.vel, note.len))
236 for note in new_notes:
237 self.canvas.pattern.add_note(note, send_changed = False)
238 self.canvas.pattern.changed()
239 self.canvas.update_size()
240 self.canvas.update_grid()
241 self.canvas.update_notes()
243 class DrumCanvasCursor(object):
244 def __init__(self, canvas):
245 self.canvas = canvas
246 self.canvas_root = canvas.get_root_item()
247 self.frame = GooCanvas.CanvasRect(parent = self.canvas_root, x = -6, y = -6, width = 12, height = 12 , stroke_color = "gray", line_width = 1)
248 hide_item(self.frame)
249 self.vel = GooCanvas.CanvasText(parent = self.canvas_root, x = 0, y = 0, fill_color = "blue", stroke_color = "blue", anchor = GooCanvas.CanvasAnchorType.S)
250 hide_item(self.vel)
251 self.rubberband = False
252 self.rubberband_origin = None
253 self.rubberband_current = None
255 def hide(self):
256 hide_item(self.frame)
257 hide_item(self.vel)
259 def show(self):
260 show_item(self.frame)
261 show_item(self.vel)
263 def move_to_note(self, note):
264 self.move(note.pos, note.row, note)
266 def move(self, pulse, row, note):
267 x = self.canvas.pulse_to_screen_x(pulse)
268 y = self.canvas.row_to_screen_y(row) + self.canvas.row_height / 2
269 dx = 0
270 if note is not None:
271 dx = self.canvas.pulse_to_screen_x(pulse + note.len) - x
272 self.frame.set_properties(x = x - 6, width = 12 + dx, y = y - 6, height = 12)
273 cy = y - self.canvas.row_height * 1.5 if y >= self.canvas.rows * self.canvas.row_height / 2 else y + self.canvas.row_height * 1.5
274 if note is None:
275 text = ""
276 else:
277 text = "[%s] %s" % (note.channel, note.vel)
278 self.vel.set_properties(x = x, y = cy, text = text)
280 def start_rubberband(self, x, y):
281 self.rubberband = True
282 self.rubberband_origin = (x, y)
283 self.rubberband_current = (x, y)
284 self.update_rubberband_frame()
285 show_item(self.frame)
287 def update_rubberband(self, x, y):
288 self.rubberband_current = (x, y)
289 self.update_rubberband_frame()
291 def end_rubberband(self, x, y):
292 self.rubberband_current = (x, y)
293 self.update_rubberband_frame()
294 hide_item(self.frame)
295 self.rubberband = False
297 def cancel_rubberband(self):
298 hide_item(self.frame)
299 self.rubberband = False
301 def update_rubberband_frame(self):
302 self.frame.set_properties(x = self.rubberband_origin[0],
303 y = self.rubberband_origin[1],
304 width = self.rubberband_current[0] - self.rubberband_origin[0],
305 height = self.rubberband_current[1] - self.rubberband_origin[1])
307 def get_rubberband_box(self):
308 x1, y1 = self.rubberband_origin
309 x2, y2 = self.rubberband_current
310 return (min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2))
312 class DrumCanvas(GooCanvas.Canvas):
313 def __init__(self, rows, pattern):
314 GooCanvas.Canvas.__init__(self)
315 self.rows = rows
316 self.pattern = pattern
317 self.row_height = 24
318 self.grid_unit = PPQN / 4 # unit in pulses
319 self.zoom_in = 2
320 self.instr_width = 120
321 self.edited_note = None
322 self.orig_velocity = None
323 self.orig_length = None
324 self.orig_y = None
325 self.channel_modes = ['D' if ch == 10 else 'M' for ch in range(1, 17)]
327 self.update_size()
329 self.grid = GooCanvas.CanvasGroup(parent = self.get_root_item(), x = self.instr_width)
330 self.update_grid()
332 self.notes = GooCanvas.CanvasGroup(parent = self.get_root_item(), x = self.instr_width)
334 self.names = GooCanvas.CanvasGroup(parent = self.get_root_item(), x = 0)
335 self.update_names()
337 self.cursor = DrumCanvasCursor(self)
338 hide_item(self.cursor)
340 self.connect('event', self.on_grid_event)
342 self.channel = 10
343 self.edit_mode = self.channel_modes[self.channel - 1]
344 self.toolbox = DrumEditorToolbox(self)
346 self.add_events(Gdk.EventMask.POINTER_MOTION_HINT_MASK)
348 self.grab_focus(self.grid)
349 self.update_notes()
351 def set_edit_mode(self, mode):
352 self.edit_mode = mode
353 self.channel_modes[self.channel - 1] = mode
355 def calc_size(self):
356 return (self.instr_width + self.pattern.get_length() * self.zoom_in + 1, self.rows * self.row_height + 1)
358 def set_grid_unit(self, grid_unit):
359 self.grid_unit = grid_unit
360 self.update_grid()
362 def set_channel(self, channel):
363 self.channel = channel
364 self.set_edit_mode(self.channel_modes[self.channel - 1])
365 self.update_notes()
366 self.toolbox.update_edit_mode()
368 def update_size(self):
369 sx, sy = self.calc_size()
370 self.set_bounds(0, 0, sx, self.rows * self.row_height)
371 self.set_size_request(sx, sy)
373 def update_names(self):
374 for i in self.names.items:
375 i.destroy()
376 for i in range(0, self.rows):
377 #GooCanvas.CanvasText(parent = self.names, text = gui_tools.note_to_name(i), x = self.instr_width - 10, y = (i + 0.5) * self.row_height, anchor = Gtk.AnchorType.E, size_points = 10, font = "Sans", size_set = True)
378 GooCanvas.CanvasText(parent = self.names, text = gui_tools.note_to_name(i), x = self.instr_width - 10, y = (i + 0.5) * self.row_height, anchor = GooCanvas.CanvasAnchorType.E, font = "Sans")
380 def update_grid(self):
381 for i in self.grid.items:
382 i.destroy()
383 bg = GooCanvas.CanvasRect(parent = self.grid, x = 0, y = 0, width = self.pattern.get_length() * self.zoom_in, height = self.rows * self.row_height, fill_color = "white")
384 bar_fg = "blue"
385 beat_fg = "darkgray"
386 grid_fg = "lightgray"
387 row_grid_fg = "lightgray"
388 row_ext_fg = "black"
389 for i in range(0, self.rows + 1):
390 color = row_ext_fg if (i == 0 or i == self.rows) else row_grid_fg
391 GooCanvas.CanvasPath(parent = self.grid, data = "M %s %s L %s %s" % (0, i * self.row_height, self.pattern.get_length() * self.zoom_in, i * self.row_height), stroke_color = color, line_width = 1)
392 for i in range(0, self.pattern.get_length() + 1, int(self.grid_unit)):
393 color = grid_fg
394 if i % self.pattern.ppqn == 0:
395 color = beat_fg
396 if (i % (self.pattern.ppqn * self.pattern.beats)) == 0:
397 color = bar_fg
398 GooCanvas.CanvasPath(parent = self.grid, data = "M %s %s L %s %s" % (i * self.zoom_in, 1, i * self.zoom_in, self.rows * self.row_height - 1), stroke_color = color, line_width = 1)
400 def update_notes(self):
401 while self.notes.get_n_children() > 0:
402 self.notes.remove_child(0)
403 for item in self.pattern.items():
404 x = self.pulse_to_screen_x(item.pos) - self.instr_width
405 y = self.row_to_screen_y(item.row + 0.5)
406 if item.channel == self.channel:
407 fill_color = 0xC0C0C0 - int(item.vel * 1.5) * 0x000101
408 stroke_color = 0x808080
409 if item.selected:
410 stroke_color = 0xFF8080
411 else:
412 fill_color = 0xE0E0E0
413 stroke_color = 0xE0E0E0
414 if item.len > 1:
415 x2 = self.pulse_to_screen_x(item.pos + item.len) - self.pulse_to_screen_x(item.pos)
416 polygon = [-2, 0, 0, -5, x2 - 5, -5, x2, 0, x2 - 5, 5, 0, 5]
417 else:
418 polygon = [-5, 0, 0, -5, 5, 0, 0, 5, -5, 0]
419 item.item = GooCanvas.CanvasPath(parent = self.notes, data = polygon_to_path(polygon), line_width = 1, fill_color = ("#%06x" % fill_color), stroke_color = ("#%06x" % stroke_color))
420 #item.item.set_property('stroke_color_rgba', guint(stroke_color))
421 item.item.translate(x, y)
423 def set_selection_from_rubberband(self):
424 sx, sy, ex, ey = self.cursor.get_rubberband_box()
425 for item in self.pattern.items():
426 x = self.pulse_to_screen_x(item.pos)
427 y = self.row_to_screen_y(item.row + 0.5)
428 item.selected = (x >= sx and x <= ex and y >= sy and y <= ey)
429 self.update_notes()
431 def on_grid_event(self, item, event):
432 if event.type == Gdk.EventType.KEY_PRESS:
433 return self.on_key_press(item, event)
434 if event.type in [Gdk.EventType.BUTTON_PRESS, Gdk.EventType._2BUTTON_PRESS, Gdk.EventType.LEAVE_NOTIFY, Gdk.EventType.MOTION_NOTIFY, Gdk.EventType.BUTTON_RELEASE]:
435 return self.on_mouse_event(item, event)
437 def on_key_press(self, item, event):
438 keyval, state = event.keyval, event.state
439 kvname = Gdk.keyval_name(keyval)
440 if kvname == 'Delete':
441 self.pattern.delete_selected()
442 self.update_notes()
443 elif kvname == 'c':
444 self.pattern.group_set(channel = self.channel)
445 self.update_notes()
446 elif kvname == 'v':
447 self.pattern.group_set(vel = int(self.toolbox.vel_adj.get_value()))
448 self.update_notes()
449 elif kvname == 'plus':
450 self.pattern.transpose_selected(1)
451 self.update_notes()
452 elif kvname == 'minus':
453 self.pattern.transpose_selected(-1)
454 self.update_notes()
455 #else:
456 # print kvname
458 def on_mouse_event(self, item, event):
459 ex, ey = self.convert_to_item_space(self.get_root_item(), event.x, event.y)
460 column = self.screen_x_to_column(ex)
461 row = self.screen_y_to_row(ey)
462 pulse = column * self.grid_unit
463 epulse = (ex - self.instr_width) / self.zoom_in
464 unit = self.grid_unit * self.zoom_in
465 if self.cursor.rubberband:
466 if event.type == Gdk.EventType.MOTION_NOTIFY:
467 self.cursor.update_rubberband(ex, ey)
468 return
469 button = event.get_button()[1]
470 if event.type == Gdk.EventType.BUTTON_RELEASE and button == 1:
471 self.cursor.end_rubberband(ex, ey)
472 self.set_selection_from_rubberband()
473 self.request_update()
474 return
475 return
476 if event.type == Gdk.EventType.BUTTON_PRESS:
477 button = event.get_button()[1]
478 self.grab_focus(self.grid)
479 if ((event.state & Gdk.ModifierType.SHIFT_MASK) == Gdk.ModifierType.SHIFT_MASK) and button == 1:
480 self.cursor.start_rubberband(ex, ey)
481 return
482 if pulse < 0 or pulse >= self.pattern.get_length():
483 return
484 note = self.pattern.get_note(pulse, row, self.channel)
485 if note is not None:
486 if button == 3:
487 vel = int(self.toolbox.vel_adj.get_value())
488 self.pattern.set_note_vel(note, vel)
489 self.cursor.move(pulse, row, note)
490 self.update_notes()
491 return
492 self.toolbox.vel_adj.set_value(note.vel)
493 else:
494 note = NoteModel(pulse, self.channel, row, int(self.toolbox.vel_adj.get_value()), self.grid_unit if self.edit_mode == 'M' else 1)
495 self.pattern.add_note(note)
496 self.edited_note = note
497 self.orig_length = note.len
498 self.orig_velocity = note.vel
499 self.orig_y = ey
500 self.grab_add()
501 self.cursor.move(self.edited_note.pos, self.edited_note.row, note)
502 self.update_notes()
503 return
504 if event.type == Gdk.EventType._2BUTTON_PRESS:
505 if pulse < 0 or pulse >= self.pattern.get_length():
506 return
507 if self.pattern.has_note(pulse, row, self.channel):
508 self.pattern.remove_note(pulse, row, self.channel)
509 self.cursor.move(pulse, row, None)
510 self.update_notes()
511 if self.edited_note is not None:
512 self.grab_remove()
513 self.edited_note = None
514 return
515 if event.type == Gdk.EventType.LEAVE_NOTIFY and self.edited_note is None:
516 hide_item(self.cursor)
517 return
518 if event.type == Gdk.EventType.MOTION_NOTIFY and self.edited_note is None:
519 if pulse < 0 or pulse >= self.pattern.get_length():
520 hide_item(self.cursor)
521 return
523 if abs(pulse - epulse) > 5:
524 hide_item(self.cursor)
525 return
526 note = self.pattern.get_note(column * self.grid_unit, row, self.channel)
527 self.cursor.move(pulse, row, note)
528 show_item(self.cursor)
529 return
530 if event.type == Gdk.EventType.MOTION_NOTIFY and self.edited_note is not None:
531 vel = int(self.orig_velocity - self.snap(ey - self.orig_y) / 2)
532 if vel < 1: vel = 1
533 if vel > 127: vel = 127
534 self.pattern.set_note_vel(self.edited_note, vel)
535 len = pulse - self.edited_note.pos
536 if self.edit_mode == 'D':
537 len = 1
538 elif len <= -self.grid_unit:
539 len = self.orig_length
540 elif len <= self.grid_unit:
541 len = self.grid_unit
542 else:
543 len = int((len + 1) / self.grid_unit) * self.grid_unit - 1
544 self.pattern.set_note_len(self.edited_note, len)
545 self.toolbox.vel_adj.set_value(vel)
546 self.cursor.move_to_note(self.edited_note)
547 self.update_notes()
548 self.request_update()
549 return
550 if event.type == Gdk.EventType.BUTTON_RELEASE and self.edited_note is not None:
551 self.edited_note = None
552 self.grab_remove()
553 return
555 def screen_x_to_column(self, x):
556 unit = self.grid_unit * self.zoom_in
557 return int((x - self.instr_width + unit / 2) / unit)
559 def screen_y_to_row(self, y):
560 return int((y - 1) / self.row_height)
562 def pulse_to_screen_x(self, pulse):
563 return pulse * self.zoom_in + self.instr_width
565 def column_to_screen_x(self, column):
566 unit = self.grid_unit * self.zoom_in
567 return column * unit + self.instr_width
569 def row_to_screen_y(self, row):
570 return row * self.row_height + 1
572 def snap(self, val):
573 if val > -10 and val < 10:
574 return 0
575 if val >= 10:
576 return val - 10
577 if val <= -10:
578 return val + 10
579 assert False
581 class DrumSeqWindow(Gtk.Window):
582 def __init__(self, length, pat_data):
583 Gtk.Window.__init__(self, Gtk.WindowType.TOPLEVEL)
584 self.vbox = Gtk.VBox(spacing = 5)
585 self.pattern = DrumPatternModel(4, length / (4 * PPQN))
586 if pat_data is not None:
587 self.pattern.import_data(pat_data)
589 self.canvas = DrumCanvas(128, self.pattern)
590 sw = Gtk.ScrolledWindow()
591 sw.set_size_request(640, 400)
592 sw.add_with_viewport(self.canvas)
593 self.vbox.pack_start(sw, True, True, 0)
594 self.vbox.pack_start(self.canvas.toolbox, False, False, 0)
595 self.add(self.vbox)
597 if __name__ == "__main__":
598 w = DrumSeqWindow()
599 w.set_title("Drum pattern editor")
600 w.show_all()
601 w.connect('destroy', lambda w: Gtk.main_quit())
603 Gtk.main()