示例#1
0
    def test_connection_not_current_pedalboard(self):
        """Test only coverage"""
        manager = BanksManager()

        bank = Bank('Bank 1')
        manager.append(bank)

        mod_host = ModHost('localhost')
        mod_host.host = MagicMock()
        manager.register(mod_host)

        pedalboard = Pedalboard('test_set_pedalboard_1')
        pedalboard.append(
            self.builder.build('http://calf.sourceforge.net/plugins/Reverb'))

        pedalboard2 = Pedalboard('test_set_pedalboard_2')
        pedalboard2.append(
            self.builder.build('http://calf.sourceforge.net/plugins/Reverb'))
        pedalboard2.append(
            self.builder.build('http://calf.sourceforge.net/plugins/Reverb'))

        bank.append(pedalboard)
        bank.append(pedalboard2)

        mod_host.pedalboard = pedalboard

        pedalboard2.connect(pedalboard2.effects[0].outputs[0],
                            pedalboard2.effects[1].inputs[0])
示例#2
0
    def test_swap_banks(self):
        manager = BanksManager()

        bank1 = Bank('Bank 1')
        bank2 = Bank('Bank 2')
        bank3 = Bank('Bank 3')
        bank4 = Bank('Bank 4')

        manager.append(bank1)
        manager.append(bank2)
        manager.append(bank3)
        manager.append(bank4)

        observer = MagicMock()
        manager.register(observer)

        manager.banks[1], manager.banks[3] = manager.banks[3], manager.banks[1]

        calls = [
            call(bank4, UpdateType.UPDATED, index=1, origin=manager, old=bank2),
            call(bank2, UpdateType.UPDATED, index=3, origin=manager, old=bank4)
        ]

        self.assertListEqual(calls, observer.on_bank_updated.call_args_list)

        self.assertEqual(bank2.manager, manager)
        self.assertEqual(bank4.manager, manager)
示例#3
0
    def test_set_pedalboard(self):
        """Test only coverage"""
        manager = BanksManager()

        bank = Bank('Bank 1')
        manager.append(bank)

        mod_host = ModHost('localhost')
        mod_host.host = MagicMock()
        manager.register(mod_host)

        pedalboard = Pedalboard('test_set_pedalboard_1')
        bank.append(pedalboard)
        reverb = self.builder.build(
            'http://calf.sourceforge.net/plugins/Reverb')
        pedalboard.append(reverb)

        pedalboard2 = Pedalboard('test_set_pedalboard_2')
        reverb2 = self.builder.build(
            'http://calf.sourceforge.net/plugins/Reverb')
        bank.append(pedalboard)
        pedalboard2.append(reverb2)

        mod_host.pedalboard = pedalboard
        mod_host.pedalboard = pedalboard2
示例#4
0
    def test_unregister(self):
        observer = MagicMock()

        manager = BanksManager()
        manager.register(observer)

        self.assertIn(observer, manager.observers)
        manager.unregister(observer)
        self.assertNotIn(observer, manager.observers)
示例#5
0
    def test_observers(self):
        observer1 = MagicMock()
        observer2 = MagicMock()

        manager = BanksManager()

        manager.register(observer1)
        manager.register(observer2)

        self.assertListEqual([observer1, observer2], manager.observers)
示例#6
0
    def test_observers(self):
        mock = MagicMock()
        observer = Autosaver('data/test/')
        observer.save = mock
        observer.delete = mock

        manager = BanksManager()
        manager.register(observer)

        bank = Bank('Bank 1')
        manager.append(bank)
        observer.save.assert_called_with(bank)

        pedalboard = Pedalboard('Rocksmith')
        bank.append(pedalboard)
        observer.save.assert_called_with(bank)

        builder = Lv2EffectBuilder()
        reverb = builder.build('http://calf.sourceforge.net/plugins/Reverb')
        filter = builder.build('http://calf.sourceforge.net/plugins/Filter')
        reverb2 = builder.build('http://calf.sourceforge.net/plugins/Reverb')

        pedalboard.append(reverb)
        observer.save.assert_called_with(bank)
        pedalboard.append(filter)
        observer.save.assert_called_with(bank)
        pedalboard.append(reverb2)
        observer.save.assert_called_with(bank)

        reverb.outputs[0].connect(filter.inputs[0])
        observer.save.assert_called_with(bank)
        reverb.outputs[1].connect(filter.inputs[0])
        observer.save.assert_called_with(bank)
        filter.outputs[0].connect(reverb2.inputs[0])
        observer.save.assert_called_with(bank)
        reverb.outputs[0].connect(reverb2.inputs[0])
        observer.save.assert_called_with(bank)

        filter.toggle()
        observer.save.assert_called_with(bank)

        filter.params[0].value = (filter.params[0].maximum -
                                  filter.params[0].minimum) / 2
        observer.save.assert_called_with(bank)

        del bank.pedalboards[0]
        observer.save.assert_called_with(bank)

        bank2 = Bank('Bank 2')
        manager.banks[0] = bank2
        observer.delete.assert_called_with(bank2)
        observer.save.assert_called_with(bank2)

        manager.banks.remove(bank2)
        observer.delete.assert_called_with(bank2)
示例#7
0
    def test_observers_mock(self):
        """Test only coverage"""
        sys_effect = SystemEffect('system', ('capture_1', 'capture_2'),
                                  ('playback_1', 'playback_2'))
        manager = BanksManager()

        bank = Bank('Bank 1')
        manager.append(bank)

        mod_host = ModHost('localhost')
        mod_host.host = MagicMock()
        manager.register(mod_host)

        pedalboard = Pedalboard('Rocksmith')

        mod_host.pedalboard = pedalboard

        bank.append(pedalboard)

        reverb = self.builder.build(
            'http://calf.sourceforge.net/plugins/Reverb')
        filter = self.builder.build(
            'http://calf.sourceforge.net/plugins/Filter')
        reverb2 = self.builder.build(
            'http://calf.sourceforge.net/plugins/Reverb')

        pedalboard.append(reverb)
        pedalboard.append(filter)
        pedalboard.append(reverb2)

        pedalboard.connect(reverb.outputs[0], filter.inputs[0])
        pedalboard.connect(reverb.outputs[1], filter.inputs[0])
        pedalboard.connect(filter.outputs[0], reverb2.inputs[0])
        pedalboard.connect(reverb.outputs[0], reverb2.inputs[0])

        filter.toggle()
        filter.params[0].value = (filter.params[0].maximum -
                                  filter.params[0].minimum) / 2

        pedalboard.disconnect(filter.outputs[0], reverb2.inputs[0])
        filter.toggle()

        pedalboard.effects.remove(filter)

        pedalboard.connections.append(
            Connection(sys_effect.outputs[0], reverb.inputs[0]))
        pedalboard.connections.append(
            Connection(reverb2.outputs[0], sys_effect.inputs[0]))

        for connection in list(pedalboard.connections):
            pedalboard.connections.remove(connection)

        for effect in list(pedalboard.effects):
            pedalboard.effects.remove(effect)
    def test_notification(self):
        observer = MockObserver()

        manager = BanksManager()
        components_observer = ComponentsObserver(manager)

        manager.register(components_observer)
        components_observer.register(observer)

        bank = self.generate_bank('test_notification')
        manager.append(bank)

        observer.on_bank_updated.assert_called_with(bank, UpdateType.CREATED, index=bank.index, origin=manager)
    def test_notification_scope(self):
        observer = MockObserver()

        manager = BanksManager()
        components_observer = ComponentsObserver(manager)

        manager.register(components_observer)
        components_observer.register(observer)

        bank = self.generate_bank('test_notification')
        with observer:
            manager.append(bank)

        observer.on_bank_updated.assert_not_called()
    def test_notification_scope(self):
        observer = MockObserver()

        manager = BanksManager()
        components_observer = ComponentsObserver(manager)

        manager.register(components_observer)
        components_observer.register(observer)

        bank = self.generate_bank('test_notification')
        with observer:
            manager.append(bank)

        observer.on_bank_updated.assert_not_called()
    def test_notification(self):
        observer = MockObserver()

        manager = BanksManager()
        components_observer = ComponentsObserver(manager)

        manager.register(components_observer)
        components_observer.register(observer)

        bank = self.generate_bank('test_notification')
        manager.append(bank)

        observer.on_bank_updated.assert_called_with(bank,
                                                    UpdateType.CREATED,
                                                    index=bank.index,
                                                    origin=manager)
示例#12
0
    def test_replace_bank(self):
        observer = Autosaver('../data/test/')

        manager = BanksManager()
        manager.register(observer)

        bank1 = Bank('Bank 1')
        pedalboard = Pedalboard('Rocksmith')
        bank1.append(pedalboard)

        manager.append(bank1)

        manager.banks[0] = Bank('Bank 2')

        self.validate_persisted(manager)

        while manager.banks:
            del manager.banks[0]
示例#13
0
    def test_swap_bank(self):
        observer = Autosaver('../data/test/')

        manager = BanksManager()
        manager.register(observer)

        bank1 = Bank('Bank 1')
        bank2 = Bank('Bank 2')

        manager.banks.append(bank1)
        manager.banks.append(bank2)

        manager.banks[0], manager.banks[1] = manager.banks[1], manager.banks[0]

        self.validate_persisted(manager)

        while manager.banks:
            del manager.banks[0]
示例#14
0
    def load(self, system_effect):
        """
        Return a :class:`.BanksManager` instance contains the banks present in
        :attr:`~pluginsmanager.observer.autosaver.autosaver.Autosaver.data_path`

        :param SystemEffect system_effect: SystemEffect used in pedalboards
        :return BanksManager: :class:`.BanksManager` with banks persisted in
                :attr:`~pluginsmanager.observer.autosaver.autosaver.Autosaver.data_path`
        """
        banks = self.banks_files.load(system_effect)
        banks_ordered = self.index_file.load(banks)

        manager = BanksManager()
        manager.register(self)

        for bank in banks_ordered:
            manager.append(bank)
            bank.manager = manager

        return manager
示例#15
0
    def test_notify(self):
        observer1 = Observer()
        observer1.on_bank_updated = MagicMock()
        observer2 = Observer()
        observer2.on_bank_updated = MagicMock()
        observer3 = Observer()
        observer3.on_bank_updated = MagicMock()

        manager = BanksManager()
        manager.register(observer1)
        manager.register(observer2)
        manager.register(observer3)

        bank = Bank('Bank 1')
        with observer1:
            manager.banks.append(bank)

        observer1.on_bank_updated.assert_not_called()
        observer2.on_bank_updated.assert_called_with(bank, UpdateType.CREATED, index=0, origin=manager)
        observer3.on_bank_updated.assert_called_with(bank, UpdateType.CREATED, index=0, origin=manager)
示例#16
0
from pluginsmanager.model.pedalboard import Pedalboard
from pluginsmanager.model.connection import Connection

from pluginsmanager.model.lv2.lv2_effect_builder import Lv2EffectBuilder

from pluginsmanager.model.system.system_effect import SystemEffect

if __name__ == '__main__':
    manager = BanksManager()

    bank = Bank('Bank 1')
    manager.append(bank)

    mod_host = ModHost('raspberrypi.local')
    mod_host.connect()
    manager.register(mod_host)

    pedalboard = Pedalboard('Rocksmith')
    bank.append(pedalboard)

    mod_host.pedalboard = pedalboard

    builder = Lv2EffectBuilder()

    reverb = builder.build('http://calf.sourceforge.net/plugins/Reverb')
    fuzz = builder.build(
        'http://guitarix.sourceforge.net/plugins/gx_fuzz_#fuzz_')
    reverb2 = builder.build('http://calf.sourceforge.net/plugins/Reverb')

    pedalboard.append(reverb)
    pedalboard.append(fuzz)
示例#17
0
        pass

    def on_connection_updated(self, connection, update_type, pedalboard,
                              **kwargs):
        pass


#############################
# Test

observer1 = MyAwesomeObserver("Hi! I am observer1")
observer2 = MyAwesomeObserver("Hi! I am observer2")
observer3 = MyAwesomeObserver("Hi! I am observer3")

manager = BanksManager()
manager.register(observer1)
manager.register(observer2)
manager.register(observer3)

#############################
print("Notify all observers")

bank = Bank('Bank 1')
manager.banks.append(bank)

print("Not notify observer 1")
with observer1:
    del manager.banks[0]

print("Not notify observer 2")
with observer2:
示例#18
0
    def test_observers_calls(self):
        observer = MagicMock()

        manager = BanksManager()
        manager.register(observer)

        bank = Bank('Bank 1')
        manager.append(bank)
        observer.on_bank_updated.assert_called_with(bank, UpdateType.CREATED, index=0, origin=manager)

        pedalboard = Pedalboard('Rocksmith')
        bank.append(pedalboard)
        observer.on_pedalboard_updated.assert_called_with(pedalboard, UpdateType.CREATED, index=0, origin=bank)

        builder = Lv2EffectBuilder()
        reverb = builder.build('http://calf.sourceforge.net/plugins/Reverb')
        filter = builder.build('http://calf.sourceforge.net/plugins/Filter')
        reverb2 = builder.build('http://calf.sourceforge.net/plugins/Reverb')

        pedalboard.append(reverb)
        observer.on_effect_updated.assert_called_with(reverb, UpdateType.CREATED, index=reverb.index, origin=pedalboard)
        pedalboard.append(filter)
        observer.on_effect_updated.assert_called_with(filter, UpdateType.CREATED, index=filter.index, origin=pedalboard)
        pedalboard.append(reverb2)
        observer.on_effect_updated.assert_called_with(reverb2, UpdateType.CREATED, index=reverb2.index, origin=pedalboard)

        pedalboard.connect(reverb.outputs[0], filter.inputs[0])
        observer.on_connection_updated.assert_called_with(
            Connection(reverb.outputs[0], filter.inputs[0]),
            UpdateType.CREATED,
            pedalboard=pedalboard
        )
        pedalboard.connect(reverb.outputs[1], filter.inputs[0])
        observer.on_connection_updated.assert_called_with(
            Connection(reverb.outputs[1], filter.inputs[0]),
            UpdateType.CREATED,
            pedalboard=pedalboard
        )
        pedalboard.connect(filter.outputs[0], reverb2.inputs[0])
        observer.on_connection_updated.assert_called_with(
            Connection(filter.outputs[0], reverb2.inputs[0]),
            UpdateType.CREATED,
            pedalboard=pedalboard
        )
        pedalboard.connect(reverb.outputs[0], reverb2.inputs[0])
        observer.on_connection_updated.assert_called_with(
            Connection(reverb.outputs[0], reverb2.inputs[0]),
            UpdateType.CREATED,
            pedalboard=pedalboard
        )

        filter.toggle()
        observer.on_effect_status_toggled.assert_called_with(filter)

        filter.params[0].value = (filter.params[0].maximum - filter.params[0].minimum) / 2
        observer.on_param_value_changed.assert_called_with(filter.params[0])

        del bank.pedalboards[0]
        observer.on_pedalboard_updated.assert_called_with(pedalboard, UpdateType.DELETED, index=0, origin=bank)

        bank2 = Bank('Bank 2')
        old_bank = manager.banks[0]
        manager.banks[0] = bank2
        observer.on_bank_updated.assert_called_with(bank2, UpdateType.UPDATED, index=0, origin=manager, old=old_bank)

        manager.banks.remove(bank2)
        observer.on_bank_updated.assert_called_with(bank2, UpdateType.DELETED, index=0, origin=manager)
示例#19
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)