def __init__(self): # jack client self.client = jack.Client("palette", no_start_server=True) # interface entities = [ Entity.KEYBOARD, Entity.SAMPLER, Entity.DRUM_MACHINE, Entity.PUSH ] self.display = Interface(entities) # metronome self.metronome = Metronome(self.display, self.client) # backend constructors = [Keyboard, Sampler, DrumMachine, Push] self.be = Backend(self.client, self.metronome, constructors) # misc self.pressed_keys = [] self.fifo = open("palette.pipe", mode="rt") self.current_inst_number = 0 # let's go self.client.activate() self.display.paint_pad(0) self.metronome.sync_transport()
def __init__(self): GObject.GObject.__init__(self) brush_file = open('../brushes/classic/charcoal.myb') brush_info = brush.BrushInfo(brush_file.read()) brush_info.set_color_rgb((0.0, 0.0, 0.0)) self.default_eraser = brush_info.get_base_value("eraser") self.default_radius = brush_info.get_base_value("radius_logarithmic") self.brush = brush.Brush(brush_info) self.button_pressed = False self.last_event = (0.0, 0.0, 0.0) # (x, y, time) self.onionskin_on = True self.onionskin_by_cels = True self.onionskin_length = 3 self.onionskin_falloff = 0.5 self.eraser_on = False self.force_add_cel = True self.surface = None self.surface_node = None self.xsheet = XSheet(24 * 60) self.xsheet.connect('frame-changed', self.xsheet_changed_cb) self.xsheet.connect('layer-changed', self.xsheet_changed_cb) self.metronome = Metronome(self.xsheet) self.update_surface() self.nodes = {} self.create_graph() self.init_ui()
class TestMetronome(unittest.TestCase): # Setup to allow the metronome to be tested def setUp(self): # A stub measure to be written to for testing purposes class MeasureStub(object): def __init__(self): self.time_signature = (4, 4) self.key_signature = ('F#', 'major') self.notes = [] self.test_measure = MeasureStub() # Tests that the metronome is only writing as a metronome. def testCompose(self): self.musician = Metronome() self.musician.compose(self.test_measure, 0, 0, None) self.assertNotEqual([], self.test_measure.notes) listing = self.musician._plans.keys() for x in listing: self.assertTrue(x >= 0) notelisting = self.musician._plans[x] for y in range(len(notelisting)): test = False for z in range(len(self.test_measure.notes)): if self.test_measure.notes[z] == notelisting[y]: test = True # Fails if there is a note played that is not on the beat self.assertFalse(self.test_measure.notes[z].start % 1) # Fails if a note is missing from the measure that should be there self.assertNotEqual(test, False)
def __init__(self, ui=None, track=None, width=None, height=None, x=0, y=0): self.ui = ui # Offset on left side, leaving space for timestamps self.offset = len("00:00.000 ") if self.ui != None: logger.log("Using ui") self.height, self.width = list(map(int, self.ui.size())) self.width -= self.offset else: logger.log("Not using ui") self.height, self.width = 10, 10 self.cursorY = self.height / 2 self.cursorX = 44 self.track = track self.bpm = self.track.beatsPerMinute self.tpb = self.track.ticksPerBeat self.seconds = 60.0 / self.tpb / self.bpm #Begin and end using real time #self.length = self.track.getLengthInSec() #self.begin = 0.0 #self.end = (self.height-1) * 60.0 / self.bpm #self.step = 60.0 / self.bpm #Begin and end using beats self.length = self.track.getLengthInBeats() self.begin = 0 self.end = self.height - 1 self.step = 1 logger.log("Track's length: %.2f beats" % self.length) self.beats = [] for i in range(int(self.track.getLengthInBeats() + 1)): self.beats.append([]) logger.log("width = %d" % self.width) ## Create display window if self.width > KEYS: displayWidth = KEYS + self.offset else: displayWidth = self.width + self.offset MIN = (KEYS - self.width) / 2 MAX = (KEYS - self.width) / 2 + self.width if self.ui != None: self.display = self.ui.newWindow(self.height, displayWidth, "display", x, y) self.ui.setWindow(self.display) ## Connect to Piano self.piano = Piano() self.player = Player() self.player.start() self.player.setPiano(self.piano) self.piano.autoconnect() self.playing = False ## Create metronome beatLength = 60.0 / self.bpm self.metronome = Metronome(self.down, beatLength)
def __init__(self): self._log = logging.getLogger('musicbox.MusicBox') # Internal attributes self._selected_stompbox = 1 # 0 = global parameters, 1-8 = actual stompboxes self._current_mode = Mode.PRESET self._last_slider_update_time = 0 # OSC inputs (footpedal) try: self._midi_to_osc = MidiToOsc( 'Arduino Micro') # works via callbacks, so not blocking except ValueError as e: self._log.error('Failed to start Midi Footpedal: ' + str(e)) # OSC server (receives inputs) self._osc_server = FootpedalOscServer(self.cb_mode, self.cb_preset, self.cb_stomp, self.cb_looper, self.cb_metronome, self.cb_slider) # mod-host LV2 host (output) self._banks_manager = BanksManager() self._banks_manager.append( Bank('Bank 1')) # TODO: load banks from stored files self._modhost = ModHost('localhost') self._modhost.connect() self._banks_manager.register(self._modhost) self._pedalboard = None self._log.info("STARTED mod-host client") # Metronome output (using klick) self._metronome = Metronome() self._log.info("STARTED Metronome") # Looper object (using sooperlooper) self._looper = Looper() self._log.info("STARTED Looper") # Notifiers self._notifier = TcpNotifier() self._log.info("STARTED TcpNotifier") # Initialize: set mode PRESET and load preset1 time.sleep(2) self._set_mode(Mode.PRESET) for preset_id in range(4): self._load_preset('preset{:02d}.yaml'.format(preset_id))
def setup(self): self._set_default_settings() self._xsheet = XSheet() self._xsheet.connect("cursor-changed", self._cursor_changed_cb) self._canvas_graph = CanvasGraph(self._xsheet) self._metronome = Metronome(self._xsheet) self._setup_icons() self._init_ui() if os.path.exists('test.zip'): self._xsheet.load('test.zip')
def __init__(self, beatsPerMeasure, numMeasures, bpm, startText, endText, bgInstrument, instrumentList): self.beatsPerMeasure = beatsPerMeasure self.numMeasures = numMeasures self.bpm = bpm self.startText = startText self.endText = endText self.bgInstrument = bgInstrument self.instrumentList = instrumentList self.activePlayer = 1 self.state = NOTE self.fail = False self.notesPlayed = { 1: [], 2: [], } self.metronome = Metronome(self.bpm, self.beatsPerMeasure)
def testCompose(self): self.musician = Metronome() self.musician.compose(self.test_measure, 0, 0, None) self.assertNotEqual([], self.test_measure.notes) listing = self.musician._plans.keys() for x in listing: self.assertTrue(x >= 0) notelisting = self.musician._plans[x] for y in range(len(notelisting)): test = False for z in range(len(self.test_measure.notes)): if self.test_measure.notes[z] == notelisting[y]: test = True # Fails if there is a note played that is not on the beat self.assertFalse(self.test_measure.notes[z].start % 1) # Fails if a note is missing from the measure that should be there self.assertNotEqual(test, False)
class Level(object): __metaclass__ = ABCMeta @abstractmethod def isValidState(tickValue): raise NotImplementedError @abstractmethod def isButtonPressValid(buttonPress,tickValue): raise NotImplementedError @abstractmethod def playBackgroundInstrument(): raise NotImplementedError def isComplete(self): if (self.metronome.getBeat() > self.numMeasures*self.beatsPerMeasure): return True def getActivePlayer(self): return self.activePlayer def start(self): pass def getState(self): return self.state def advanceState(self): if (self.state == NOTE): self.state = ACTION else: self.state = NOTE self.changeActivePlayer() self.notesPlayed[self.activePlayer] = [] print "Play a note, player " + str(self.activePlayer) def changeActivePlayer(self): if self.activePlayer == 1: self.activePlayer = 2 else: self.activePlayer = 1 def failed(self): return self.fail def __init__(self, beatsPerMeasure, numMeasures, bpm, startText, endText, bgInstrument, instrumentList): self.beatsPerMeasure = beatsPerMeasure self.numMeasures = numMeasures self.bpm = bpm self.startText = startText self.endText = endText self.bgInstrument = bgInstrument self.instrumentList = instrumentList self.activePlayer = 1 self.state = NOTE self.fail = False self.notesPlayed = { 1: [], 2: [], } self.metronome = Metronome(self.bpm, self.beatsPerMeasure) def update(self): self.metronome.update() if(self.metronome.isAtNextBeat()): click = sc.Synth( "click" ) click.amp = 0.2 self.advanceState() if(not self.isValidState()): print "player " + str(self.activePlayer) + " LOSES" self.fail = True def handleMusicInput(self, player, note): if(self.isButtonPressValid(player,note)): self.notesPlayed[self.getActivePlayer()].append(note) def playBackgroundInstrument(): pass def getOtherPlayer(self): if self.activePlayer == 2: return 1 else: return 2
clock.display_channel = midi_h.channel state.reset_buttons() clock.sequencers[midi_h.channel].show_pattern() async def knob_handler(clock): # apply knob position to chan knob_ctl_val = knob.get_knob_value() if knob_ctl_val is not None: midi_h.controller_change(state.knob_controller, knob_ctl_val) cbs = [button_handler, knob_handler] # knob interrupt handler # set tempo to 160 beats-per-minute tempo = 320 # create the metronome metronome = Metronome(tempo=tempo, callbacks=cbs) # produce the coroutine metronome_coro = metronome.start() # asyncio boilerplate to run the coroutine bounce_count = 1 loop = asyncio.get_event_loop() loop.run_until_complete(metronome_coro)
class Main: def __init__(self): # jack client self.client = jack.Client("palette", no_start_server=True) # interface entities = [ Entity.KEYBOARD, Entity.SAMPLER, Entity.DRUM_MACHINE, Entity.PUSH ] self.display = Interface(entities) # metronome self.metronome = Metronome(self.display, self.client) # backend constructors = [Keyboard, Sampler, DrumMachine, Push] self.be = Backend(self.client, self.metronome, constructors) # misc self.pressed_keys = [] self.fifo = open("palette.pipe", mode="rt") self.current_inst_number = 0 # let's go self.client.activate() self.display.paint_pad(0) self.metronome.sync_transport() def key_released(self, key): if key in pad: self.be.entities[self.current_inst_number].key_released(key) self.display.paint_key_off(key) elif key in range(84, 88): self.be.entities[self.current_inst_number].normal_mode() def key_pressed(self, key): if key in pad: self.be.entities[self.current_inst_number].key_pressed(key) self.display.paint_key_on(key) elif key in looper: self.be.entities[self.current_inst_number].loop(key - 89) elif key in looper_mode_mappings: self.be.entities[self.current_inst_number].set_looper_mode( looper_mode_mappings[key]) # numpad / elif key == 84: self.be.entities[self.current_inst_number].delete_mode() # numpad * elif key == 85: self.be.entities[self.current_inst_number].record_mode() # numpad - elif key == 86: self.be.entities[self.current_inst_number].half_mode() # numpad + elif key == 87: self.be.entities[self.current_inst_number].double_mode() # space elif key == 44: self.metronome.toggle_transport() # esc elif key == 41: self.fifo.close() self.display.shutdown() quit() # instrument selection elif key in headboard: if key - 58 < len(self.be.entities): self.current_inst_number = key - 58 self.display.paint_pad(self.current_inst_number) # left arrow, decrement bpm elif key == 80: self.metronome.decrement_bpm() # right arrow, increment bpm elif key == 79: self.metronome.increment_bpm()
class Display: def __init__(self, ui=None, track=None, width=None, height=None, x=0, y=0): self.ui = ui # Offset on left side, leaving space for timestamps self.offset = len("00:00.000 ") if self.ui != None: logger.log("Using ui") self.height, self.width = list(map(int, self.ui.size())) self.width -= self.offset else: logger.log("Not using ui") self.height, self.width = 10, 10 self.cursorY = self.height / 2 self.cursorX = 44 self.track = track self.bpm = self.track.beatsPerMinute self.tpb = self.track.ticksPerBeat self.seconds = 60.0 / self.tpb / self.bpm #Begin and end using real time #self.length = self.track.getLengthInSec() #self.begin = 0.0 #self.end = (self.height-1) * 60.0 / self.bpm #self.step = 60.0 / self.bpm #Begin and end using beats self.length = self.track.getLengthInBeats() self.begin = 0 self.end = self.height - 1 self.step = 1 logger.log("Track's length: %.2f beats" % self.length) self.beats = [] for i in range(int(self.track.getLengthInBeats() + 1)): self.beats.append([]) logger.log("width = %d" % self.width) ## Create display window if self.width > KEYS: displayWidth = KEYS + self.offset else: displayWidth = self.width + self.offset MIN = (KEYS - self.width) / 2 MAX = (KEYS - self.width) / 2 + self.width if self.ui != None: self.display = self.ui.newWindow(self.height, displayWidth, "display", x, y) self.ui.setWindow(self.display) ## Connect to Piano self.piano = Piano() self.player = Player() self.player.start() self.player.setPiano(self.piano) self.piano.autoconnect() self.playing = False ## Create metronome beatLength = 60.0 / self.bpm self.metronome = Metronome(self.down, beatLength) def createList(self): skip = [] t = 0.0 n = 0 for i in range(len(self.track.track)): t += self.track.track[i].time * self.seconds if self.track.track[i].type == "note_on": if i in skip: skip.remove(i) else: msg = self.track.track[i] note = msg.note length = 0 j = i + 1 ## Find closing msg while j != len(self.track.track): ## accumulate times length += self.track.track[j].time if self.track.track[j].type == "note_on": if self.track.track[j].note == note: ## put on list to skip skip.append(j) break j += 1 length *= self.seconds beats = length * self.bpm / 60.0 beatZero = int(round(t / 60.0 * self.bpm)) for k in range(int(beats + 1)): # Append a list [note, first beat of the note self.beats[beatZero + k].append([i, k == 0]) if self.ui == None: for beat in beats: print(list(map(beat, note2char))) ## Display notes with length def putStrXY(self, x, y, txt, colorPair=272): self.ui.setColorPair(colorPair) self.ui.putStrXY(self.offset + x, y, txt) def timeStamp(self, y, timeStamp): self.ui.setColorPair(50) self.ui.putStrXY(0, y, timeStamp) def update(self): self.putStrXY(0, self.cursorY - self.begin, " " * ((MAX - MIN) / 1)) self.putStrXY(self.cursorX, self.cursorY - self.begin, "+", 2) for beat in range(self.begin, self.end + 1): y = beat - self.begin seconds = 60.0 * beat / self.bpm minutes = int(seconds / 60) seconds = seconds % 60 milisec = int(1000 * (seconds - int(seconds))) self.timeStamp(y, "%02d:%2d.%03d" % (minutes, int(seconds), milisec)) for [idx, first] in self.beats[beat]: note = self.track.track[idx].note x = MIN if note < MIN else MAX if note > MAX else note x -= MIN if self.ui != None: color = 262 if self.cursorY == y + self.begin and self.cursorX == x: logger.log("update() - beat = %d" % beat) logger.log("update() - cursorY = %d" % self.cursorY) color = 282 if first: self.putStrXY(x, y, "O", color) else: self.putStrXY(x, y, "|", color) self.ui.refresh() def quit(self): self.player.stop() self.metronome.stop() def enter(self): self.playing = self.playing == False ## Play if self.playing: logger.log("enter() - Play") beat = self.cursorY first = False while not first: for idx, first in self.beats[beat]: if first: break beat += 1 track = self.track.subTrack(idx) self.player.setTrack(track) self.player.play() self.metronome.play() ## Stop else: logger.log("enter() - Pause") self.player.pause() self.metronome.pause() self.player.clearTrack() note = self.cursorX def up(self): self.cursorY = max(0, self.cursorY - 1) logger.log("Up. cursorY = %d" % self.cursorY) if self.cursorY < self.height / 2 + self.begin: self.begin -= self.step if self.begin < 0: self.begin += self.step else: self.end -= self.step def down(self): self.cursorY = min(self.length - 2, self.cursorY + 1) logger.log("Down. cursorY = %d" % self.cursorY) if self.cursorY > self.height / 2 + self.begin: self.end += self.step if self.end > self.length: self.end -= self.step else: self.begin += self.step def left(self): self.cursorX = max(0, self.cursorX - 1) def right(self): self.cursorX = min(87, self.cursorX + 1) def pgup(self): for i in range(10): self.up() def pgdown(self): for i in range(10): self.down()
class Application(GObject.GObject): def __init__(self): GObject.GObject.__init__(self) brush_file = open('../brushes/classic/charcoal.myb') brush_info = brush.BrushInfo(brush_file.read()) brush_info.set_color_rgb((0.0, 0.0, 0.0)) self.default_eraser = brush_info.get_base_value("eraser") self.default_radius = brush_info.get_base_value("radius_logarithmic") self.brush = brush.Brush(brush_info) self.button_pressed = False self.last_event = (0.0, 0.0, 0.0) # (x, y, time) self.onionskin_on = True self.onionskin_by_cels = True self.onionskin_length = 3 self.onionskin_falloff = 0.5 self.eraser_on = False self.force_add_cel = True self.surface = None self.surface_node = None self.xsheet = XSheet(24 * 60) self.xsheet.connect('frame-changed', self.xsheet_changed_cb) self.xsheet.connect('layer-changed', self.xsheet_changed_cb) self.metronome = Metronome(self.xsheet) self.update_surface() self.nodes = {} self.create_graph() self.init_ui() def create_graph(self): self.graph = Gegl.Node() main_over = self.graph.create_child("gegl:over") self.nodes['main_over'] = main_over layer_overs = [] for l in range(self.xsheet.layers_length): over = self.graph.create_child("gegl:over") layer_overs.append(over) self.nodes['layer_overs'] = layer_overs layer_overs[0].connect_to("output", main_over, "input") for over, next_over in zip(layer_overs, layer_overs[1:]): next_over.connect_to("output", over, "input") background_node = self.graph.create_child("gegl:rectangle") background_node.set_property('color', Gegl.Color.new("#fff")) background_node.connect_to("output", layer_overs[-1], "input") self.nodes['background'] = background_node layer_nodes = [] for l in range(self.xsheet.layers_length): nodes = {} current_cel_over = self.graph.create_child("gegl:over") current_cel_over.connect_to("output", layer_overs[l], "aux") nodes['current_cel_over'] = current_cel_over onionskin_overs = [] onionskin_opacities = [] for i in range(self.onionskin_length): over = self.graph.create_child("gegl:over") onionskin_overs.append(over) opacity = self.graph.create_child("gegl:opacity") opacity.set_property('value', 1 - self.onionskin_falloff) onionskin_opacities.append(opacity) over.connect_to("output", opacity, "input") for over, next_opacity in zip(onionskin_overs, onionskin_opacities[1:]): next_opacity.connect_to("output", over, "aux") onionskin_opacities[0].connect_to("output", current_cel_over, "aux") nodes['onionskin'] = {} nodes['onionskin']['overs'] = onionskin_overs nodes['onionskin']['opacities'] = onionskin_opacities layer_nodes.append(nodes) self.nodes['layer_nodes'] = layer_nodes self.update_graph() def update_graph(self): get_cel = None if self.onionskin_by_cels: get_cel = self.xsheet.get_cel_relative_by_cels else: get_cel = self.xsheet.get_cel_relative for layer_idx in range(self.xsheet.layers_length): layer_nodes = self.nodes['layer_nodes'][layer_idx] cur_cel = self.xsheet.get_cel(layer_idx=layer_idx) if cur_cel is not None: cur_cel.surface_node.connect_to( "output", layer_nodes['current_cel_over'], "input") else: layer_nodes['current_cel_over'].disconnect("input") if not self.onionskin_on: continue layer_diff = layer_idx - self.xsheet.layer_idx for i in range(self.onionskin_length): prev_cel = get_cel(-(i + 1), layer_diff=layer_diff) over = layer_nodes['onionskin']['overs'][i] opacity = layer_nodes['onionskin']['opacities'][i] if prev_cel is not None: prev_cel.surface_node.connect_to("output", over, "input") else: over.disconnect("input") # debug # print_connections(self.nodes['main_over']) def init_ui(self): window = Gtk.Window() window.props.title = "XSheet" window.connect("destroy", self.destroy_cb) window.connect("size-allocate", self.size_allocate_cb) window.connect("key-press-event", self.key_press_cb) window.connect("key-release-event", self.key_release_cb) window.show() top_box = Gtk.Grid() window.add(top_box) top_box.show() toolbar = Gtk.Toolbar() top_box.attach(toolbar, 0, 0, 2, 1) toolbar.show() factory = Gtk.IconFactory() icon_names = [ 'xsheet-onionskin', 'xsheet-play', 'xsheet-eraser', 'xsheet-metronome', 'xsheet-settings' ] for name in icon_names: filename = os.path.join('data', 'icons', name + '.svg') pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) iconset = Gtk.IconSet.new_from_pixbuf(pixbuf) factory.add(name, iconset) factory.add_default() play_button = Gtk.ToggleToolButton() play_button.set_stock_id("xsheet-play") play_button.connect("toggled", self.toggle_play_cb) toolbar.insert(play_button, -1) play_button.show() onionskin_button = Gtk.ToggleToolButton() onionskin_button.set_stock_id("xsheet-onionskin") onionskin_button.set_active(True) onionskin_button.connect("toggled", self.toggle_onionskin_cb) toolbar.insert(onionskin_button, -1) onionskin_button.show() eraser_button = Gtk.ToggleToolButton() eraser_button.set_stock_id("xsheet-eraser") eraser_button.connect("toggled", self.toggle_eraser_cb) toolbar.insert(eraser_button, -1) eraser_button.show() metronome_button = Gtk.ToggleToolButton() metronome_button.set_stock_id("xsheet-metronome") metronome_button.connect("toggled", self.toggle_metronome_cb) toolbar.insert(metronome_button, -1) metronome_button.show() settings_button = Gtk.ToolButton() settings_button.set_stock_id("xsheet-settings") settings_button.connect("clicked", self.settings_click_cb) toolbar.insert(settings_button, -1) settings_button.show() event_box = Gtk.EventBox() event_box.connect("motion-notify-event", self.motion_to_cb) event_box.connect("button-press-event", self.button_press_cb) event_box.connect("button-release-event", self.button_release_cb) top_box.attach(event_box, 0, 1, 1, 1) event_box.props.expand = True event_box.show() view_widget = GeglGtk.View() view_widget.set_node(self.nodes['main_over']) view_widget.set_autoscale_policy(GeglGtk.ViewAutoscale.DISABLED) view_widget.set_size_request(800, 400) event_box.add(view_widget) view_widget.show() xsheet_widget = XSheetWidget(self.xsheet) top_box.attach(xsheet_widget, 1, 1, 1, 1) xsheet_widget.show() def run(self): return Gtk.main() def destroy_cb(self, *ignored): Gtk.main_quit() def size_allocate_cb(self, widget, allocation): background_node = self.nodes['background'] self.nodes['background'].set_property("width", allocation.width) self.nodes['background'].set_property("height", allocation.height) def motion_to_cb(self, widget, event): # FIXME, better disconnect if self.surface is None: return (x, y, time) = event.x, event.y, event.time pressure = event.get_axis(Gdk.AxisUse.PRESSURE) if pressure is None: pressure = 0.5 xtilt = event.get_axis(Gdk.AxisUse.XTILT) ytilt = event.get_axis(Gdk.AxisUse.YTILT) if xtilt is None or ytilt is None: xtilt = 0 ytilt = 0 dtime = (time - self.last_event[2]) / 1000.0 if self.button_pressed: self.surface.begin_atomic() self.brush.stroke_to(self.surface.backend, x, y, pressure, xtilt, ytilt, dtime) self.surface.end_atomic() self.last_event = (x, y, time) def button_press_cb(self, widget, event): if self.force_add_cel: self.xsheet.add_cel() self.button_pressed = True def button_release_cb(self, widget, event): self.button_pressed = False self.brush.reset() def xsheet_changed_cb(self, xsheet): self.update_surface() self.update_graph() def update_surface(self): cel = self.xsheet.get_cel() if cel is not None: self.surface = cel.surface self.surface_node = cel.surface_node else: self.surface = None self.surface_node = None def toggle_play_stop(self): if self.xsheet.is_playing: self.xsheet.stop() else: self.xsheet.play() def toggle_play_cb(self, widget): self.toggle_play_stop() def toggle_onionskin(self): self.onionskin_on = not self.onionskin_on for layer_idx in range(self.xsheet.layers_length): layer_nodes = self.nodes['layer_nodes'][layer_idx] onionskin_opacities = layer_nodes['onionskin']['opacities'] current_cel_over = layer_nodes['current_cel_over'] if self.onionskin_on: onionskin_opacities[0].connect_to("output", current_cel_over, "aux") else: current_cel_over.disconnect("aux") self.update_graph() def toggle_onionskin_cb(self, widget): self.toggle_onionskin() def toggle_eraser(self): self.eraser_on = not self.eraser_on if self.eraser_on: self.brush.brushinfo.set_base_value("eraser", 1.0) self.brush.brushinfo.set_base_value("radius_logarithmic", self.default_radius * 3) else: self.brush.brushinfo.set_base_value("eraser", self.default_eraser) self.brush.brushinfo.set_base_value("radius_logarithmic", self.default_radius) def toggle_eraser_cb(self, widget): self.toggle_eraser() def toggle_metronome(self): if self.metronome.is_on(): self.metronome.activate() else: self.metronome.deactivate() def toggle_metronome_cb(self, widget): self.toggle_metronome() def settings_click_cb(self, widget): dialog = SettingsDialog(widget.get_toplevel()) dialog.show() def key_press_cb(self, widget, event): if event.keyval == Gdk.KEY_Up: self.xsheet.previous_frame() elif event.keyval == Gdk.KEY_Down: self.xsheet.next_frame() def key_release_cb(self, widget, event): if event.keyval == Gdk.KEY_c: self.xsheet.add_cel() elif event.keyval == Gdk.KEY_p: self.toggle_play_stop() elif event.keyval == Gdk.KEY_o: self.toggle_onionskin() elif event.keyval == Gdk.KEY_e: self.toggle_eraser() elif event.keyval == Gdk.KEY_BackSpace: # FIXME, needs to be done in gegl backend if self.surface is not None: self.surface.clear() elif event.keyval == Gdk.KEY_Left: self.xsheet.previous_layer() elif event.keyval == Gdk.KEY_Right: self.xsheet.next_layer()
class Application(Gtk.Application): _INSTANCE = None def __init__(self): assert Application._INSTANCE is None Gtk.Application.__init__(self) Application._INSTANCE = self self.connect("activate", self._activate_cb) def setup(self): self._set_default_settings() self._xsheet = XSheet() self._xsheet.connect("cursor-changed", self._cursor_changed_cb) self._canvas_graph = CanvasGraph(self._xsheet) self._metronome = Metronome(self._xsheet) self._setup_icons() self._init_ui() if os.path.exists('test.zip'): self._xsheet.load('test.zip') def _activate_cb(self, app): self.setup() self._main_window.present() def get_metronome(self): return self._metronome def _about_cb(self, action, state): print("About") def _quit_cb(self, action, state): self._quit() def _new_cb(self, action, state): self._xsheet.new() def _cut_cb(self, action, state): self._xsheet.cut() def _copy_cb(self, action, state): self._xsheet.copy() def _paste_cb(self, action, state): self._xsheet.paste() def _remove_clear_cb(self, action, state): self._xsheet.remove_clear() def _next_frame_cb(self, action, state): self._xsheet.next_frame() def _previous_frame_cb(self, action, state): self._xsheet.previous_frame() def _next_layer_cb(self, action, state): self._xsheet.next_layer() def _previous_layer_cb(self, action, state): self._xsheet.previous_layer() def _pan_view_up_cb(self, action, state): self._main_window.get_canvas_widget().pan_view("up") def _pan_view_down_cb(self, action, state): self._main_window.get_canvas_widget().pan_view("down") def _pan_view_left_cb(self, action, state): self._main_window.get_canvas_widget().pan_view("left") def _pan_view_right_cb(self, action, state): self._main_window.get_canvas_widget().pan_view("right") def _zoom_view_in_cb(self, action, state): self._main_window.get_canvas_widget().zoom_view(1) def _zoom_view_out_cb(self, action, state): self._main_window.get_canvas_widget().zoom_view(-1) def _activate_toggle_cb(window, action, data=None): action.change_state(GLib.Variant('b', not action.get_state())) def _change_fullscreen_cb(self, action, state): if state.unpack(): self._main_window.fullscreen() else: self._main_window.unfullscreen() action.set_state(state) def _change_timeline_cb(self, action, state): if state.unpack(): self._main_window.get_xsheet_widget().show() else: self._main_window.get_xsheet_widget().hide() action.set_state(state) def _change_play_cb(self, action, state): if state.unpack(): self._xsheet.play(_settings['play']['loop']) else: self._xsheet.stop() action.set_state(state) def _change_play_loop_cb(self, action, state): if state.unpack(): _settings['play']['loop'] = True else: _settings['play']['loop'] = False action.set_state(state) def _change_onionskin_cb(self, action, state): if state.unpack(): self._canvas_graph.set_onionskin_enabled(True) else: self._canvas_graph.set_onionskin_enabled(False) action.set_state(state) def _change_eraser_cb(self, action, state): if state.unpack(): self._set_eraser_enabled(True) else: self._set_eraser_enabled(False) action.set_state(state) def _change_metronome_cb(self, action, state): if state.unpack(): self._metronome.activate() else: self._metronome.deactivate() action.set_state(state) def _quit(self): self._xsheet.save('test.zip') Gtk.Application.quit(self) def _set_default_settings(self): brush = MyPaint.Brush() brush_def = open('../mypaint/brushes/classic/charcoal.myb').read() brush.from_string(brush_def) set_base_color(brush, (0.0, 0.0, 0.0)) self._default_eraser = get_base_value(brush, "eraser") self._default_radius = get_base_value(brush, "radius_logarithmic") _settings['brush'] = brush _settings['onionskin'] = {} _settings['onionskin']['on'] = True _settings['onionskin']['by_cels'] = True _settings['onionskin']['length'] = 3 _settings['onionskin']['falloff'] = 0.5 _settings['eraser'] = {} _settings['eraser']['on'] = False _settings['play'] = {} _settings['play']['loop'] = False def _setup_icons(self): factory = Gtk.IconFactory() icon_names = ['xsheet-onionskin', 'xsheet-play', 'xsheet-eraser', 'xsheet-clear', 'xsheet-metronome', 'xsheet-settings', 'xsheet-prev-layer', 'xsheet-next-layer'] for name in icon_names: filename = os.path.join('data', 'icons', name + '.svg') pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) iconset = Gtk.IconSet.new_from_pixbuf(pixbuf) factory.add(name, iconset) factory.add_default() def _init_ui(self): self._main_window = ApplicationWindow(self, self._xsheet, self._canvas_graph) def add_simple_actions(obj, actions): for action_name, action_cb in actions: action = Gio.SimpleAction(name=action_name) action.connect("activate", action_cb) obj.add_action(action) def add_toggle_actions(obj, actions): for action_name, change_cb, enabled in actions: action = Gio.SimpleAction.new_stateful(action_name, None, GLib.Variant('b', enabled)) action.connect("activate", self._activate_toggle_cb) action.connect("change-state", change_cb) obj.add_action(action) app_actions = ( ("about", self._about_cb), ("quit", self._quit_cb), ) add_simple_actions(self, app_actions) win_actions = ( ("new", self._new_cb), ("cut", self._cut_cb), ("copy", self._copy_cb), ("paste", self._paste_cb), ("remove_clear", self._remove_clear_cb), ("next_frame", self._next_frame_cb), ("previous_frame", self._previous_frame_cb), ("next_layer", self._next_layer_cb), ("previous_layer", self._previous_layer_cb), ("pan_view_up", self._pan_view_up_cb), ("pan_view_down", self._pan_view_down_cb), ("pan_view_left", self._pan_view_left_cb), ("pan_view_right", self._pan_view_right_cb), ("zoom_view_in", self._zoom_view_in_cb), ("zoom_view_out", self._zoom_view_out_cb), ) add_simple_actions(self._main_window, win_actions) toggle_actions = ( ("fullscreen", self._change_fullscreen_cb, False), ("timeline", self._change_timeline_cb, True), ("play", self._change_play_cb, False), ("play_loop", self._change_play_loop_cb, False), ("onionskin", self._change_onionskin_cb, True), ("eraser", self._change_eraser_cb, False), ("metronome", self._change_metronome_cb, False), ) add_toggle_actions(self._main_window, toggle_actions) non_menu_accels = ( ("o", "win.onionskin", None), ("e", "win.eraser", None), ("BackSpace", "win.remove_clear", None), ("<Control>Up", "win.previous_frame", None), ("<Control>Down", "win.next_frame", None), ("<Control>Left", "win.previous_layer", None), ("<Control>Right", "win.next_layer", None), ("<Control><Shift>Up", "win.pan_view_up", None), ("<Control><Shift>Down", "win.pan_view_down", None), ("<Control><Shift>Left", "win.pan_view_left", None), ("<Control><Shift>Right", "win.pan_view_right", None), ("comma", "win.zoom_view_out", None), ("period", "win.zoom_view_in", None), ) for accel, action_name, parameter in non_menu_accels: self.add_accelerator(accel, action_name, parameter) builder = Gtk.Builder() builder.add_from_file("menu.ui") self.set_app_menu(builder.get_object("app-menu")) self.set_menubar(builder.get_object("menubar")) self._main_window.connect("destroy", self._destroy_cb) self._main_window.create_widgets() def _destroy_cb(self, *ignored): self._quit() def _set_eraser_enabled(self, enabled): _settings['eraser']['on'] = enabled brush = _settings['brush'] if _settings['eraser']['on']: set_base_value(brush, "eraser", 1.0) set_base_value(brush, "radius_logarithmic", self._default_radius * 3) else: set_base_value(brush, "eraser", self._default_eraser) set_base_value(brush, "radius_logarithmic", self._default_radius) def _cursor_changed_cb(self, xsheet): cut_action = self._main_window.lookup_action("cut") copy_action = self._main_window.lookup_action("copy") if cut_action.props.enabled and self._xsheet.get_cel() is None: cut_action.props.enabled = False copy_action.props.enabled = False elif not cut_action.props.enabled and self._xsheet.get_cel() is not None: cut_action.props.enabled = True copy_action.props.enabled = True
class MusicBox: OSC_MODES = { 'preset': Mode.PRESET, 'stomp': Mode.STOMP, 'looper': Mode.LOOPER, 'metronome': Mode.METRONOME } def __init__(self): self._log = logging.getLogger('musicbox.MusicBox') # Internal attributes self._selected_stompbox = 1 # 0 = global parameters, 1-8 = actual stompboxes self._current_mode = Mode.PRESET self._last_slider_update_time = 0 # OSC inputs (footpedal) try: self._midi_to_osc = MidiToOsc( 'Arduino Micro') # works via callbacks, so not blocking except ValueError as e: self._log.error('Failed to start Midi Footpedal: ' + str(e)) # OSC server (receives inputs) self._osc_server = FootpedalOscServer(self.cb_mode, self.cb_preset, self.cb_stomp, self.cb_looper, self.cb_metronome, self.cb_slider) # mod-host LV2 host (output) self._banks_manager = BanksManager() self._banks_manager.append( Bank('Bank 1')) # TODO: load banks from stored files self._modhost = ModHost('localhost') self._modhost.connect() self._banks_manager.register(self._modhost) self._pedalboard = None self._log.info("STARTED mod-host client") # Metronome output (using klick) self._metronome = Metronome() self._log.info("STARTED Metronome") # Looper object (using sooperlooper) self._looper = Looper() self._log.info("STARTED Looper") # Notifiers self._notifier = TcpNotifier() self._log.info("STARTED TcpNotifier") # Initialize: set mode PRESET and load preset1 time.sleep(2) self._set_mode(Mode.PRESET) for preset_id in range(4): self._load_preset('preset{:02d}.yaml'.format(preset_id)) def run(self): try: self._osc_server.start() self._osc_server._thread.join() except KeyboardInterrupt: self._log.warn('KeyboardInterrupt: shutting down') self._osc_server.stop() self._notifier.close() def _set_mode(self, mode): midisend(1, mode.value) # Action when leaving mode if mode != Mode.LOOPER: self._looper.enable(False) if mode != Mode.METRONOME: self._metronome.enable(False) # Action based on activated mode if mode == Mode.PRESET: pass elif mode == Mode.STOMP: self._activate_preset(0) # special preset 0 = stompbox mode elif mode == Mode.LOOPER: self._looper.enable(True) elif mode == Mode.METRONOME: self._metronome.enable(True) self._current_mode = mode self._notifier.update("MODE:{:d}".format(int( self._current_mode.value))) def _create_graph_from_config(self, filename): """ Loads a YAML file (could easily support JSON as well) and creates a graph for defined plugins and connections. Also returns other settings separately. """ with open(filename, 'r') as f: data = yaml.safe_load(f) settings = { 'name': data['preset']['name'], 'author': data['preset']['author'], 'global_parameters': data['preset']['global_parameters'] } self._log.debug('yaml preset data: ' + str(data['preset'])) plugins = [ Lv2Plugin(sb['lv2'], sb['connections']) for sb in data['preset']['stompboxes'] ] pb = PedalboardGraph(plugins) # Disable stompboxes if configured for i, sb in enumerate(data['preset']['stompboxes']): if 'enabled' in sb: plugins[i].is_enabled = sb['enabled'] # Assign index to each node for p in pb.nodes: p._index = pb.get_index(p) # Add graph edges (connections between effects as index to node - mod_host module will do conversion to "effect_:in" string) for p in pb.nodes: self._log.debug('Adding edges {!s} for node {!s}'.format( p._connections, p)) pb.add_edges(p, p._connections) self._log.debug("Graph with edges:\n" + str(pb)) pb.settings = settings return pb def _activate_preset(self, preset_id): # Store current pedalboard in attribute self._pedalboard = self._banks_manager.banks[0].pedalboards[preset_id] # Load new pedalboard into mod-host self._modhost.pedalboard = self._pedalboard self._log.info('Activated pedalboard {!s}'.format(self._pedalboard)) # Notifications self._preset_info_notifier_update(preset_id) for e in self._pedalboard.effects: e.toggle() self._notifier.update("STOMPEN:{:d}:{:d}".format( e.index, int(e.active))) def _load_preset(self, yaml_file, remove_previous=False): # Create graph with effect plugin objects graph = self._create_graph_from_config(yaml_file) # Cleanup existing pedalboard in mod-host if remove_previous and self._pedalboard is not None: for e in list(self._pedalboard.effects): for c in list(e.connections): self._pedalboard.disconnect(c.output, c.input) self._pedalboard.effects.remove(e) self._banks_manager.banks[0] = [] del self._pedalboard # Create PedalPi pedalboard and add to bank and mod-host pedalboard = Pedalboard(graph.settings['name']) pedalboard.graph = graph self._banks_manager.banks[0].append(pedalboard) # Add nodes (effects) to mod-host lv2_builder = Lv2EffectBuilder() for node in graph.nodes: # loop over Plugin objects self._log.info("mod-host: add effect " + str(node)) node.effect = lv2_builder.build(node.uri) pedalboard.effects.append(node.effect) sys_effect = SystemEffect('system', ['capture_1', 'capture_2'], ['playback_1', 'playback_2']) # Connect system capture to first effect pedalboard.connect(sys_effect.outputs[0], pedalboard.effects[0].inputs[0]) # Add edges (connections) to mod-host for node in graph.nodes: # looper over Plugin objects incoming = graph.get_incoming_edges(node) outgoing = graph.get_outgoing_edges(node) self._log.info( 'mod-host: add connection {!s} -> [{:d}] "{:s}" -> {!s}'. format(incoming, node.index, node.name, outgoing)) # Go through outgoing edges (indices) for e in outgoing: # loop over edge indices # Connect current effect to effect given by index e neighbor = graph.get_node_from_index(e) if node.has_stereo_output and neighbor.has_stereo_input: # stereo to stereo pedalboard.connect(node.effect.outputs[0], neighbor.effect.inputs[0]) pedalboard.connect(node.effect.outputs[1], neighbor.effect.inputs[1]) elif not node.has_stereo_output and neighbor.has_stereo_input: # split mono to stereo pedalboard.connect(node.effect.outputs[0], neighbor.effect.inputs[0]) pedalboard.connect(node.effect.outputs[0], neighbor.effect.inputs[1]) elif node.has_stereo_output and not neighbor.has_stereo_input: # sum stereo to mono pedalboard.connect(node.effect.outputs[0], neighbor.effect.inputs[0]) pedalboard.connect(node.effect.outputs[1], neighbor.effect.inputs[0]) else: # mono to mono pedalboard.connect(node.effect.outputs[0], neighbor.effect.inputs[0]) if outgoing == []: left_output = node.effect.outputs[0] right_output = node.effect.outputs[1 if node. has_stereo_output else 0] pedalboard.connect(left_output, sys_effect.inputs[0]) pedalboard.connect(right_output, sys_effect.inputs[1]) def _handle_slider_stompbox(self, slider_id, value): stompbox = self._pedalboard.graph.nodes[ self._selected_stompbox - 1] # select by index from list of Plugin objects param_name, param_info = stompbox.get_parameter_info_by_index( slider_id - 1) if param_name is None: self._log.info('{!s} has no parameter for slider {:d}'.format( stompbox, slider_id)) return self._log.debug('{:s} param_info {!s}'.format(param_name, param_info)) # Convert slider value (0-1023) to parameters (min, max) range min_max_ratio = 1024 / (param_info['Maximum'] - param_info['Minimum']) value /= min_max_ratio value += param_info['Minimum'] self._log.info( 'Setting stomp #{:d} param #{:d} "{:s}" [{:s}] to {} (ratio {})'. format(self._selected_stompbox, slider_id, param_name, param_info['Symbol'], value, min_max_ratio)) stompbox.effect.params[slider_id - 1].value = value self._notifier.update("SLIDER:{:d}:{:f}".format(slider_id - 1, value)) def cb_mode(self, uri, msg=None): """Handle incoming /mode/... OSC message""" mode = uri.rsplit('/', 1)[-1] assert mode in self.OSC_MODES.keys() self._log.info("MODE {} -> {}".format(mode, self.OSC_MODES[mode])) self._set_mode(self.OSC_MODES[mode]) def _preset_info_notifier_update(self, preset_id): # Construct JSON payload for notifiers: # current preset, list of stompboxes with parameters notifier_data = { 'preset_id': int(preset_id), 'preset_name': self._pedalboard.graph.settings['name'], 'stompboxes': [] } # Loop over all stompboxes (nodes on pedalboard graph) for sb in self._pedalboard.graph.nodes: sb_data = {'name': sb.name, 'parameters': []} # Loop over stombox's parameters assert len(sb.parameters) == len(sb.effect.parameters) for i, p in enumerate( sb.parameters): # p is { 'NAME': { params } } assert len(p.keys()) == 1 p_name = list(p.keys())[0] param_data = { 'name': p_name, 'symbol': p[p_name]['Symbol'], 'min': p[p_name]['Minimum'], 'max': p[p_name]['Maximum'], 'value': sb.effect.parameters[i] } sb_data['parameters'].append(param_data) notifier_data['stompboxes'].append(sb_data) self._log.debug("Sending JSON: " + json.dumps(notifier_data)) self._notifier.update("PRESET:" + json.dumps(notifier_data)) def cb_preset(self, uri, msg=None): """Handle incoming /preset/<N> OSC message""" preset_id = int(uri.rsplit('/', 1)[-1]) assert 0 < preset_id < 100 self._log.info("PRESET {:d}".format(preset_id)) self._activate_preset(preset_id) def cb_stomp_enable(self, uri, msg=None): """Handle incoming /stomp/<N>/enable OSC message""" uri_splits = uri.split('/')[2:] # throw away leading "/" and "stomp" assert 2 <= len(uri_splits) <= 3, uri_splits stomp_id = int(uri_splits[0]) op = uri_splits[1] value = int(uri_splits[2]) if len(uri_splits) == 3 else None self._log.debug('cb_stomp_{}: {:d} (value {!s})'.format( op, stomp_id, value)) assert 0 < stomp_id < 10 assert op in ['enable', 'select'] if op == 'select': self._selected_stompbox = stomp_id self._notifier.update( "STOMPSEL:{:d}".format(self._selected_stompbox - 1)) elif op == 'enable': assert self._pedalboard p = self._pedalboard.graph.get_node_from_index(stomp_id - 1) if p: assert p.index == stomp_id - 1 if value is None: # no value given: toggle internal state p.is_enabled = not p.is_enabled else: p.is_enabled = bool(value) self._log.info('STOMP {} "{}" ENABLE {:d}'.format( p.index, p.name, p.is_enabled)) p.effect.active = True if not p.is_enabled else False self._notifier.update("STOMPEN:{:d}:{:d}".format( p.index, p.is_enabled)) else: self._log.warn( 'cb_stomp_enable: node with index {:d} not in pedalboard'. format(stomp_id - 1)) def cb_looper(self, uri, msg=None): """Handle incoming /looper OSC messages to be proxied to sooperlooper""" _, command = uri.rsplit('/', 1) cmd_fn = { 'undo': self._looper.undo, 'redo': self._looper.redo, 'record': lambda: self._looper.record(insert=False), 'overdub': lambda: self._looper.overdub(multiply=False), 'mute_trigger': lambda: self._looper.mute(trigger=True), 'insert': lambda: self._looper.record(insert=True), 'multiply': lambda: self._looper.overdub(multiply=True), 'pause': self._looper.pause, } if command in cmd_fn: cmd_fn[command]() self._log.info( "Sent /sl/0/hit s:{:s} to sooperlooper".format(command)) else: self._log.error( "Invalid sooperlooper command {:s}".format(command)) def cb_metronome(self, uri, msg=None): """Handle incoming /metronome/ OSC messages""" uri_splits = uri.split('/') assert uri_splits[0] == '' assert uri_splits[1] == 'metronome' command = uri_splits[2] self._log.info("METRONOME {}".format(command)) if command == 'pause': self._metronome.enable(not self._metronome.is_running) elif command == 'set_bpm': assert len(uri_splits) == 4 self._metronome.set_bpm(int(uri_splits[3])) elif command == 'inc_bpm': self._metronome.set_bpm(self._metronome.bpm + 8) elif command == 'dec_bpm': self._metronome.set_bpm(self._metronome.bpm - 8) elif command == 'tap': self._metronome.tap() bpm = self._metronome.get_bpm() midisend(2, bpm) self._notifier.update("BPM:{:d}".format(bpm)) def cb_slider(self, uri, msg=None): """Handle incoming /slider/<N> OSC message""" now = time.time() if now - self._last_slider_update_time < 0.2: return self._last_slider_update_time = now uri_splits = uri.split('/') assert uri_splits[0] == '' assert uri_splits[1] == 'slider' slider_id = int(uri_splits[2]) value = float(msg) if msg is not None else float(uri_splits[3]) self._log.info("SLIDER {:d} = {:f}".format(slider_id, value)) if self._current_mode in [Mode.PRESET, Mode.STOMP]: # Adjust currently selected stompbox (default 1) self._handle_slider_stompbox(slider_id, value) elif self._current_mode == Mode.LOOPER: # Adjust looper parameters pass elif self._current_mode == Mode.METRONOME: if slider_id == 1: self._metronome.set_bpm(value) elif slider_id == 2: self._metronome.set_volume(value)