Ejemplo n.º 1
0
    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()
Ejemplo n.º 2
0
    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()
Ejemplo n.º 3
0
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)
Ejemplo n.º 4
0
    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)
Ejemplo n.º 5
0
    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))
Ejemplo n.º 6
0
    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')
Ejemplo n.º 7
0
	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)
Ejemplo n.º 8
0
    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)
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
            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)
Ejemplo n.º 11
0
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()
Ejemplo n.º 12
0
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()
Ejemplo n.º 13
0
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()
Ejemplo n.º 14
0
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
Ejemplo n.º 15
0
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)