You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

603 lines
24 KiB

#from gui_tools import *
from gi.repository import GObject, Gdk, Gtk, GooCanvas, GLib
import gui_tools
def guint(x):
value = GObject.Value()
value.init(GObject.TYPE_UINT)
value.set_uint(x)
return value
PPQN = 48
def standard_filter(patterns, name):
f = Gtk.FileFilter()
for p in patterns:
f.add_pattern(p)
f.set_name(name)
return f
def hide_item(citem):
citem.visibility = GooCanvas.CanvasItemVisibility.HIDDEN
def show_item(citem):
citem.visibility = GooCanvas.CanvasItemVisibility.VISIBLE
def polygon_to_path(points):
assert len(points) % 2 == 0, "Invalid number of points (%d) in %s" % (len(points), repr(points))
path = ""
if len(points) > 0:
path += "M %s %s" % (points[0], points[1])
if len(points) > 1:
for i in range(2, len(points), 2):
path += " L %s %s" % (points[i], points[i + 1])
return path
class NoteModel(object):
def __init__(self, pos, channel, row, vel, len = 1):
self.pos = int(pos)
self.channel = int(channel)
self.row = int(row)
self.vel = int(vel)
self.len = int(len)
self.item = None
self.selected = False
def __str__(self):
return "pos=%s row=%s vel=%s len=%s" % (self.pos, self.row, self.vel, self.len)
# This is stupid and needs rewriting using a faster data structure
class DrumPatternModel(GObject.GObject):
def __init__(self, beats, bars):
GObject.GObject.__init__(self)
self.ppqn = PPQN
self.beats = beats
self.bars = bars
self.notes = []
def import_data(self, data):
self.clear()
active_notes = {}
for t in data:
cmd = t[1] & 0xF0
if len(t) == 4 and (cmd == 0x90) and (t[3] > 0):
note = NoteModel(t[0], (t[1] & 15) + 1, t[2], t[3])
active_notes[t[2]] = note
self.add_note(note)
if len(t) == 4 and ((cmd == 0x90 and t[3] == 0) or cmd == 0x80):
if t[2] in active_notes:
active_notes[t[2]].len = t[0] - active_notes[t[2]].pos
del active_notes[t[2]]
end = self.get_length()
for n in active_notes.values():
n.len = end - n.pos
def clear(self):
self.notes = []
self.changed()
def add_note(self, note, send_changed = True):
self.notes.append(note)
if send_changed:
self.changed()
def remove_note(self, pos, row, channel):
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)]
self.changed()
def set_note_vel(self, note, vel):
note.vel = int(vel)
self.changed()
def set_note_len(self, note, len):
note.len = int(len)
self.changed()
def has_note(self, pos, row, channel):
for n in self.notes:
if n.pos == pos and n.row == row and (channel is None or n.channel == channel):
return True
return False
def get_note(self, pos, row, channel):
for n in self.notes:
if n.pos == pos and n.row == row and (channel is None or n.channel == channel):
return n
return None
def items(self):
return self.notes
def get_length(self):
return int(self.beats * self.bars * self.ppqn)
def changed(self):
self.emit('changed')
def delete_selected(self):
self.notes = [note for note in self.notes if not note.selected]
self.changed()
def group_set(self, **vals):
for n in self.notes:
if not n.selected:
continue
for k, v in vals.items():
setattr(n, k, v)
self.changed()
def transpose_selected(self, amount):
for n in self.notes:
if not n.selected:
continue
if n.row + amount < 0 or n.row + amount > 127:
continue
n.row += amount
self.changed()
GObject.type_register(DrumPatternModel)
GObject.signal_new("changed", DrumPatternModel, GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, ())
channel_ls = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT)
for ch in range(1, 17):
channel_ls.append((str(ch), ch))
snap_settings_ls = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT)
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)]:
snap_settings_ls.append(row)
edit_mode_ls = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING)
for row in [("Drum", "D"), ("Melodic", "M")]:
edit_mode_ls.append(row)
class DrumEditorToolbox(Gtk.HBox):
def __init__(self, canvas):
Gtk.HBox.__init__(self, spacing = 5)
self.canvas = canvas
self.vel_adj = Gtk.Adjustment(100, 1, 127, 1, 10, 0)
self.pack_start(Gtk.Label("Channel:"), False, False, 0)
self.channel_setting = gui_tools.standard_combo(channel_ls, active_item_lookup = self.canvas.channel, lookup_column = 1)
self.channel_setting.connect('changed', lambda w: self.canvas.set_channel(w.get_model()[w.get_active()][1]))
self.pack_start(self.channel_setting, False, True, 0)
self.pack_start(Gtk.Label("Mode:"), False, False, 0)
self.mode_setting = gui_tools.standard_combo(edit_mode_ls, active_item_lookup = self.canvas.edit_mode, lookup_column = 1)
self.mode_setting.connect('changed', lambda w: self.canvas.set_edit_mode(w.get_model()[w.get_active()][1]))
self.pack_start(self.mode_setting, False, True, 0)
self.pack_start(Gtk.Label("Snap:"), False, False, 0)
self.snap_setting = gui_tools.standard_combo(snap_settings_ls, active_item_lookup = self.canvas.grid_unit, lookup_column = 1)
self.snap_setting.connect('changed', lambda w: self.canvas.set_grid_unit(w.get_model()[w.get_active()][1]))
self.pack_start(self.snap_setting, False, True, 0)
self.pack_start(Gtk.Label("Velocity:"), False, False, 0)
self.pack_start(Gtk.SpinButton(adjustment = self.vel_adj, climb_rate = 0, digits = 0), False, False, 0)
button = Gtk.Button("Load")
button.connect('clicked', self.load_pattern)
self.pack_start(button, True, True, 0)
button = Gtk.Button("Save")
button.connect('clicked', self.save_pattern)
self.pack_start(button, True, True, 0)
button = Gtk.Button("Double")
button.connect('clicked', self.double_pattern)
self.pack_start(button, True, True, 0)
self.pack_start(Gtk.Label("--"), False, False, 0)
def update_edit_mode(self):
self.mode_setting.set_active(gui_tools.ls_index(edit_mode_ls, self.canvas.edit_mode, 1))
def load_pattern(self, w):
dlg = Gtk.FileChooserDialog('Open a drum pattern', self.get_toplevel(), Gtk.FileChooserAction.OPEN,
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.APPLY))
dlg.add_filter(standard_filter(["*.cbdp"], "Drum patterns"))
dlg.add_filter(standard_filter(["*"], "All files"))
try:
if dlg.run() == Gtk.ResponseType.APPLY:
pattern = self.canvas.pattern
f = file(dlg.get_filename(), "r")
pattern.clear()
pattern.beats, pattern.bars = [int(v) for v in f.readline().strip().split(";")]
for line in f.readlines():
line = line.strip()
if not line.startswith("n:"):
pos, row, vel = line.split(";")
row = int(row) + 36
channel = 10
len = 1
else:
pos, channel, row, vel, len = line[2:].split(";")
self.canvas.pattern.add_note(NoteModel(pos, channel, row, vel, len), send_changed = False)
f.close()
self.canvas.pattern.changed()
self.canvas.update_grid()
self.canvas.update_notes()
finally:
dlg.destroy()
def save_pattern(self, w):
dlg = Gtk.FileChooserDialog('Save a drum pattern', self.get_toplevel(), Gtk.FileChooserAction.SAVE,
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.APPLY))
dlg.add_filter(standard_filter(["*.cbdp"], "Drum patterns"))
dlg.add_filter(standard_filter(["*"], "All files"))
dlg.set_current_name("pattern.cbdp")
try:
if dlg.run() == Gtk.ResponseType.APPLY:
pattern = self.canvas.pattern
f = file(dlg.get_filename(), "w")
f.write("%s;%s\n" % (pattern.beats, pattern.bars))
for i in self.canvas.pattern.items():
f.write("n:%s;%s;%s;%s;%s\n" % (i.pos, i.channel, i.row, i.vel, i.len))
f.close()
finally:
dlg.destroy()
def double_pattern(self, w):
len = self.canvas.pattern.get_length()
self.canvas.pattern.bars *= 2
new_notes = []
for note in self.canvas.pattern.items():
new_notes.append(NoteModel(note.pos + len, note.channel, note.row, note.vel, note.len))
for note in new_notes:
self.canvas.pattern.add_note(note, send_changed = False)
self.canvas.pattern.changed()
self.canvas.update_size()
self.canvas.update_grid()
self.canvas.update_notes()
class DrumCanvasCursor(object):
def __init__(self, canvas):
self.canvas = canvas
self.canvas_root = canvas.get_root_item()
self.frame = GooCanvas.CanvasRect(parent = self.canvas_root, x = -6, y = -6, width = 12, height = 12 , stroke_color = "gray", line_width = 1)
hide_item(self.frame)
self.vel = GooCanvas.CanvasText(parent = self.canvas_root, x = 0, y = 0, fill_color = "blue", stroke_color = "blue", anchor = GooCanvas.CanvasAnchorType.S)
hide_item(self.vel)
self.rubberband = False
self.rubberband_origin = None
self.rubberband_current = None
def hide(self):
hide_item(self.frame)
hide_item(self.vel)
def show(self):
show_item(self.frame)
show_item(self.vel)
def move_to_note(self, note):
self.move(note.pos, note.row, note)
def move(self, pulse, row, note):
x = self.canvas.pulse_to_screen_x(pulse)
y = self.canvas.row_to_screen_y(row) + self.canvas.row_height / 2
dx = 0
if note is not None:
dx = self.canvas.pulse_to_screen_x(pulse + note.len) - x
self.frame.set_properties(x = x - 6, width = 12 + dx, y = y - 6, height = 12)
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
if note is None:
text = ""
else:
text = "[%s] %s" % (note.channel, note.vel)
self.vel.set_properties(x = x, y = cy, text = text)
def start_rubberband(self, x, y):
self.rubberband = True
self.rubberband_origin = (x, y)
self.rubberband_current = (x, y)
self.update_rubberband_frame()
show_item(self.frame)
def update_rubberband(self, x, y):
self.rubberband_current = (x, y)
self.update_rubberband_frame()
def end_rubberband(self, x, y):
self.rubberband_current = (x, y)
self.update_rubberband_frame()
hide_item(self.frame)
self.rubberband = False
def cancel_rubberband(self):
hide_item(self.frame)
self.rubberband = False
def update_rubberband_frame(self):
self.frame.set_properties(x = self.rubberband_origin[0],
y = self.rubberband_origin[1],
width = self.rubberband_current[0] - self.rubberband_origin[0],
height = self.rubberband_current[1] - self.rubberband_origin[1])
def get_rubberband_box(self):
x1, y1 = self.rubberband_origin
x2, y2 = self.rubberband_current
return (min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2))
class DrumCanvas(GooCanvas.Canvas):
def __init__(self, rows, pattern):
GooCanvas.Canvas.__init__(self)
self.rows = rows
self.pattern = pattern
self.row_height = 24
self.grid_unit = PPQN / 4 # unit in pulses
self.zoom_in = 2
self.instr_width = 120
self.edited_note = None
self.orig_velocity = None
self.orig_length = None
self.orig_y = None
self.channel_modes = ['D' if ch == 10 else 'M' for ch in range(1, 17)]
self.update_size()
self.grid = GooCanvas.CanvasGroup(parent = self.get_root_item(), x = self.instr_width)
self.update_grid()
self.notes = GooCanvas.CanvasGroup(parent = self.get_root_item(), x = self.instr_width)
self.names = GooCanvas.CanvasGroup(parent = self.get_root_item(), x = 0)
self.update_names()
self.cursor = DrumCanvasCursor(self)
hide_item(self.cursor)
self.connect('event', self.on_grid_event)
self.channel = 10
self.edit_mode = self.channel_modes[self.channel - 1]
self.toolbox = DrumEditorToolbox(self)
self.add_events(Gdk.EventMask.POINTER_MOTION_HINT_MASK)
self.grab_focus(self.grid)
self.update_notes()
def set_edit_mode(self, mode):
self.edit_mode = mode
self.channel_modes[self.channel - 1] = mode
def calc_size(self):
return (self.instr_width + self.pattern.get_length() * self.zoom_in + 1, self.rows * self.row_height + 1)
def set_grid_unit(self, grid_unit):
self.grid_unit = grid_unit
self.update_grid()
def set_channel(self, channel):
self.channel = channel
self.set_edit_mode(self.channel_modes[self.channel - 1])
self.update_notes()
self.toolbox.update_edit_mode()
def update_size(self):
sx, sy = self.calc_size()
self.set_bounds(0, 0, sx, self.rows * self.row_height)
self.set_size_request(sx, sy)
def update_names(self):
for i in self.names.items:
i.destroy()
for i in range(0, self.rows):
#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)
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")
def update_grid(self):
for i in self.grid.items:
i.destroy()
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")
bar_fg = "blue"
beat_fg = "darkgray"
grid_fg = "lightgray"
row_grid_fg = "lightgray"
row_ext_fg = "black"
for i in range(0, self.rows + 1):
color = row_ext_fg if (i == 0 or i == self.rows) else row_grid_fg
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)
for i in range(0, self.pattern.get_length() + 1, int(self.grid_unit)):
color = grid_fg
if i % self.pattern.ppqn == 0:
color = beat_fg
if (i % (self.pattern.ppqn * self.pattern.beats)) == 0:
color = bar_fg
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)
def update_notes(self):
while self.notes.get_n_children() > 0:
self.notes.remove_child(0)
for item in self.pattern.items():
x = self.pulse_to_screen_x(item.pos) - self.instr_width
y = self.row_to_screen_y(item.row + 0.5)
if item.channel == self.channel:
fill_color = 0xC0C0C0 - int(item.vel * 1.5) * 0x000101
stroke_color = 0x808080
if item.selected:
stroke_color = 0xFF8080
else:
fill_color = 0xE0E0E0
stroke_color = 0xE0E0E0
if item.len > 1:
x2 = self.pulse_to_screen_x(item.pos + item.len) - self.pulse_to_screen_x(item.pos)
polygon = [-2, 0, 0, -5, x2 - 5, -5, x2, 0, x2 - 5, 5, 0, 5]
else:
polygon = [-5, 0, 0, -5, 5, 0, 0, 5, -5, 0]
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))
#item.item.set_property('stroke_color_rgba', guint(stroke_color))
item.item.translate(x, y)
def set_selection_from_rubberband(self):
sx, sy, ex, ey = self.cursor.get_rubberband_box()
for item in self.pattern.items():
x = self.pulse_to_screen_x(item.pos)
y = self.row_to_screen_y(item.row + 0.5)
item.selected = (x >= sx and x <= ex and y >= sy and y <= ey)
self.update_notes()
def on_grid_event(self, item, event):
if event.type == Gdk.EventType.KEY_PRESS:
return self.on_key_press(item, event)
if event.type in [Gdk.EventType.BUTTON_PRESS, Gdk.EventType._2BUTTON_PRESS, Gdk.EventType.LEAVE_NOTIFY, Gdk.EventType.MOTION_NOTIFY, Gdk.EventType.BUTTON_RELEASE]:
return self.on_mouse_event(item, event)
def on_key_press(self, item, event):
keyval, state = event.keyval, event.state
kvname = Gdk.keyval_name(keyval)
if kvname == 'Delete':
self.pattern.delete_selected()
self.update_notes()
elif kvname == 'c':
self.pattern.group_set(channel = self.channel)
self.update_notes()
elif kvname == 'v':
self.pattern.group_set(vel = int(self.toolbox.vel_adj.get_value()))
self.update_notes()
elif kvname == 'plus':
self.pattern.transpose_selected(1)
self.update_notes()
elif kvname == 'minus':
self.pattern.transpose_selected(-1)
self.update_notes()
#else:
# print kvname
def on_mouse_event(self, item, event):
ex, ey = self.convert_to_item_space(self.get_root_item(), event.x, event.y)
column = self.screen_x_to_column(ex)
row = self.screen_y_to_row(ey)
pulse = column * self.grid_unit
epulse = (ex - self.instr_width) / self.zoom_in
unit = self.grid_unit * self.zoom_in
if self.cursor.rubberband:
if event.type == Gdk.EventType.MOTION_NOTIFY:
self.cursor.update_rubberband(ex, ey)
return
button = event.get_button()[1]
if event.type == Gdk.EventType.BUTTON_RELEASE and button == 1:
self.cursor.end_rubberband(ex, ey)
self.set_selection_from_rubberband()
self.request_update()
return
return
if event.type == Gdk.EventType.BUTTON_PRESS:
button = event.get_button()[1]
self.grab_focus(self.grid)
if ((event.state & Gdk.ModifierType.SHIFT_MASK) == Gdk.ModifierType.SHIFT_MASK) and button == 1:
self.cursor.start_rubberband(ex, ey)
return
if pulse < 0 or pulse >= self.pattern.get_length():
return
note = self.pattern.get_note(pulse, row, self.channel)
if note is not None:
if button == 3:
vel = int(self.toolbox.vel_adj.get_value())
self.pattern.set_note_vel(note, vel)
self.cursor.move(pulse, row, note)
self.update_notes()
return
self.toolbox.vel_adj.set_value(note.vel)
else:
note = NoteModel(pulse, self.channel, row, int(self.toolbox.vel_adj.get_value()), self.grid_unit if self.edit_mode == 'M' else 1)
self.pattern.add_note(note)
self.edited_note = note
self.orig_length = note.len
self.orig_velocity = note.vel
self.orig_y = ey
self.grab_add()
self.cursor.move(self.edited_note.pos, self.edited_note.row, note)
self.update_notes()
return
if event.type == Gdk.EventType._2BUTTON_PRESS:
if pulse < 0 or pulse >= self.pattern.get_length():
return
if self.pattern.has_note(pulse, row, self.channel):
self.pattern.remove_note(pulse, row, self.channel)
self.cursor.move(pulse, row, None)
self.update_notes()
if self.edited_note is not None:
self.grab_remove()
self.edited_note = None
return
if event.type == Gdk.EventType.LEAVE_NOTIFY and self.edited_note is None:
hide_item(self.cursor)
return
if event.type == Gdk.EventType.MOTION_NOTIFY and self.edited_note is None:
if pulse < 0 or pulse >= self.pattern.get_length():
hide_item(self.cursor)
return
if abs(pulse - epulse) > 5:
hide_item(self.cursor)
return
note = self.pattern.get_note(column * self.grid_unit, row, self.channel)
self.cursor.move(pulse, row, note)
show_item(self.cursor)
return
if event.type == Gdk.EventType.MOTION_NOTIFY and self.edited_note is not None:
vel = int(self.orig_velocity - self.snap(ey - self.orig_y) / 2)
if vel < 1: vel = 1
if vel > 127: vel = 127
self.pattern.set_note_vel(self.edited_note, vel)
len = pulse - self.edited_note.pos
if self.edit_mode == 'D':
len = 1
elif len <= -self.grid_unit:
len = self.orig_length
elif len <= self.grid_unit:
len = self.grid_unit
else:
len = int((len + 1) / self.grid_unit) * self.grid_unit - 1
self.pattern.set_note_len(self.edited_note, len)
self.toolbox.vel_adj.set_value(vel)
self.cursor.move_to_note(self.edited_note)
self.update_notes()
self.request_update()
return
if event.type == Gdk.EventType.BUTTON_RELEASE and self.edited_note is not None:
self.edited_note = None
self.grab_remove()
return
def screen_x_to_column(self, x):
unit = self.grid_unit * self.zoom_in
return int((x - self.instr_width + unit / 2) / unit)
def screen_y_to_row(self, y):
return int((y - 1) / self.row_height)
def pulse_to_screen_x(self, pulse):
return pulse * self.zoom_in + self.instr_width
def column_to_screen_x(self, column):
unit = self.grid_unit * self.zoom_in
return column * unit + self.instr_width
def row_to_screen_y(self, row):
return row * self.row_height + 1
def snap(self, val):
if val > -10 and val < 10:
return 0
if val >= 10:
return val - 10
if val <= -10:
return val + 10
assert False
class DrumSeqWindow(Gtk.Window):
def __init__(self, length, pat_data):
Gtk.Window.__init__(self, Gtk.WindowType.TOPLEVEL)
self.vbox = Gtk.VBox(spacing = 5)
self.pattern = DrumPatternModel(4, length / (4 * PPQN))
if pat_data is not None:
self.pattern.import_data(pat_data)
self.canvas = DrumCanvas(128, self.pattern)
sw = Gtk.ScrolledWindow()
sw.set_size_request(640, 400)
sw.add_with_viewport(self.canvas)
self.vbox.pack_start(sw, True, True, 0)
self.vbox.pack_start(self.canvas.toolbox, False, False, 0)
self.add(self.vbox)
if __name__ == "__main__":
w = DrumSeqWindow()
w.set_title("Drum pattern editor")
w.show_all()
w.connect('destroy', lambda w: Gtk.main_quit())
Gtk.main()