Example #1
0
class SoundBlockHandler(object):
    """
    Handles user interaction and drawing of graphics before generating a SoundBlock.
    Also stores and updates all currently active SoundBlocks.
    """
    def __init__(self, norm, sandbox, mixer, client, client_id):
        self.norm = norm
        self.module_name = 'SoundBlock'
        self.sandbox = sandbox

        self.mixer = mixer
        self.tempo_map = SimpleTempoMap(bpm=60)
        self.sched = AudioScheduler(self.tempo_map)
        self.synth = Synth("data/FluidR3_GM.sf2")
        self.sched.set_generator(self.synth)
        self.mixer.add(self.sched)
        self.cmd = {}

        self.client = client
        self.cid = client_id
        self.instruments = {
            'piano': 1,
            'violin': 41,
            'trumpet': 57,
            'ocarina': 80,
            'choir': 53
        }
        self.inst_list = ['piano', 'violin', 'trumpet', 'ocarina', 'choir']
        self.drumkit = {
            'snare': 38,
            'crash': 49,
            'bass': 35,
            'hihat': 46,
            'triangle': 81
        }
        self.drum_list = ['snare', 'crash', 'bass', 'hihat', 'triangle']
        self.channel = 0
        self.drum_channel = len(self.inst_list)

        # set up the correct sound (program: bank and preset)
        # each instrument is on a different channel
        for index, inst in enumerate(self.inst_list):
            self.synth.program(index, 0, self.instruments[inst])

        for index, drum in enumerate(self.drum_list):
            self.synth.program(index + len(self.instruments), 128, 0)

        # many variables here are dicts because a user's module handler needs to keep track of
        # not just its own variables, but other users' variables as well! so we use dictionaries
        # with client ids as the keys.
        self.hold_point = {}
        self.hold_shape = {}

        # this variable is needed for when a user clicks on a soundblock in a touch_down event,
        # so that the corresponding touch_up event is skipped
        self.skip = {}

        self.color_dict = {
            'red': (201 / 255, 108 / 255, 130 / 255),
            'orange': (214 / 255, 152 / 255, 142 / 255),
            'yellow': (238 / 255, 234 / 255, 202 / 255),
            'green': (170 / 255, 220 / 255, 206 / 255),
            'teal': (159 / 255, 187 / 255, 208 / 255),
            'blue': (44 / 255, 85 / 255, 123 / 255),
            'turquoise': (50 / 255, 147 / 255, 140 / 255),
            'indigo': (46 / 255, 40 / 255, 90 / 255),
            'violet': (147 / 255, 127 / 255, 159 / 255),
            'pink': (199 / 255, 150 / 255, 170 / 255),
            'peach': (238 / 255, 142 / 255, 154 / 255),
            'magenta': (172 / 255, 69 / 255, 133 / 255),
            'grey': (140 / 255, 143 / 255, 148 / 255),
            'white': (239 / 255, 226 / 255, 222 / 255)
        }

        self.default_color = self.color_dict['violet']
        self.default_pitch = 60
        self.default_timbre = 'sine'
        self.default_instrument = 'piano'
        self.default_drum = 'snare'

        self.color = {}
        self.pitch = {}
        self.timbre = {}
        self.instrument = {}
        self.drum = {}

        self.display = False

        self.blocks = AnimGroup()
        self.sandbox.add(self.blocks)

        self.gui = BlockGUI(self.norm,
                            pos=self.norm.nt((50, 100)),
                            is_drum=False,
                            pitch_callback=self.update_pitch,
                            instrument_callback=self.update_instrument,
                            drum_callback=self.update_drum)

        self.delete_mode = {}

    def on_touch_down(self, cid, pos):
        if cid == self.cid:
            self.gui.on_touch_down(pos)

        if not self.sandbox.in_bounds(pos):
            return

        # when a block is clicked, flash and play a sound
        for block in self.blocks.objects:
            if in_bounds(pos, block.pos, block.size):
                if self.delete_mode[cid]:
                    self.blocks.objects.remove(block)
                    self.blocks.remove(block)
                    return

                block.flash()
                self.skip[cid] = True
                return  # don't start drawing a SoundBlock

        if self.delete_mode[cid]:
            return

        self.hold_point[cid] = pos
        self.hold_shape[cid] = Rectangle(pos=pos, size=(0, 0))

        self.sandbox.add(Color(1, 1, 1))
        self.sandbox.add(self.hold_shape[cid])

    def on_touch_move(self, cid, pos):
        if not self.sandbox.in_bounds(pos):
            return

        #determine which direction rectangle is being created in
        hold_point = self.hold_point[cid]
        size = self.calculate_size(hold_point, pos)
        bottom_left = pos

        #moving northeast
        if pos[0] > hold_point[0] and pos[1] > hold_point[1]:
            bottom_left = hold_point

        #moving southeast
        elif pos[0] > hold_point[0] and pos[1] < hold_point[1]:
            bottom_left = (hold_point[0], pos[1])

        #moving southwest
        elif pos[0] < hold_point[0] and pos[1] < hold_point[1]:
            bottom_left = pos

        #moving northwest
        elif pos[0] < hold_point[0] and pos[1] > hold_point[1]:
            bottom_left = (pos[0], hold_point[1])

        self.hold_shape[cid].pos = bottom_left
        self.hold_shape[cid].size = size

    def on_touch_up(self, cid, pos):
        if self.skip.get(cid):
            self.skip[cid] = False
            return

        if self.delete_mode[cid]:
            return

        if self.hold_shape.get(cid) not in self.sandbox:
            if not self.sandbox.in_bounds(pos):
                return

        bottom_left = self.hold_shape[cid].pos
        size = self.hold_shape[cid].size

        if size[0] <= 10 or size[1] <= 10:
            self.sandbox.remove(self.hold_shape[cid])
            return

        self.sandbox.remove(self.hold_shape[cid])

        pitch = self.pitch[cid]
        color = self.color[cid]
        instrument = self.instrument[cid]
        self.channel = self.inst_list.index(instrument)
        drum = self.drum[cid]
        self.drum_channel = self.drum_list.index(drum) + len(self.inst_list)

        if self.gui.is_drum:
            block = SoundBlock(self.norm, self.sandbox, bottom_left, size,
                               self.drum_channel, self.drumkit[drum], color,
                               self, self.sound)
        else:
            block = SoundBlock(self.norm, self.sandbox, bottom_left, size,
                               self.channel, pitch, color, self, self.sound)
        self.blocks.add(block)

    def on_key_down(self, cid, key):
        index = lookup(key, 'q2w3er5t6y7ui', range(13))
        if index is not None:
            if self.cid == cid:
                self.gui.ps.select(index)
        if key == '[':
            if cid == self.cid:
                self.gui.ps.left_press()
        if key == ']':
            if cid == self.cid:
                self.gui.ps.right_press()

        if key == 'v' and cid == self.cid:
            self.delete_mode[cid] = not self.delete_mode[cid]
            self.update_server_state(post=True)

        if key == 'up':
            if not self.gui.is_drum:
                self.gui.switch_module()

        if key == 'down':
            if self.gui.is_drum:
                self.gui.switch_module()

        if self.gui.is_drum:
            drum = lookup(key, 'asdfg',
                          ['snare', 'crash', 'bass', 'hihat', 'triangle'])
            if drum is not None:
                self.drum[cid] = drum
                if self.cid == cid:
                    self.drum_channel = self.drum_list.index(drum) + len(
                        self.inst_list)
                    self.gui.ds.select(drum)
        else:
            instrument = lookup(
                key, 'asdfg',
                ['piano', 'violin', 'trumpet', 'ocarina', 'choir'])
            if instrument is not None:
                self.instrument[cid] = instrument
                if self.cid == cid:
                    self.channel = self.inst_list.index(instrument)
                    self.gui.ints.select(
                        instrument)  # have the GUI update as well

    def display_controls(self):
        info = 'delete mode: {}\n'.format(self.delete_mode[self.cid])
        if self.gui.is_drum:
            info += 'drum: {}\n'.format(self.drum_list[self.drum_channel -
                                                       len(self.inst_list)])
        else:
            info += 'instrument: {}\n'.format(self.inst_list[self.channel])
        return info

    def on_update(self):
        self.blocks.on_update()
        self.gui.on_update(Window.mouse_pos)

    def update_server_state(self, post=False):
        """
        Update server state. If post is True, relay this updated state to all clients.
        """
        state = {
            'color': self.color,
            'pitch': self.pitch,
            'timbre': self.timbre,
            'instrument': self.instrument,
            'drum': self.drum,
            'delete_mode': self.delete_mode
        }
        data = {
            'module': self.module_name,
            'cid': self.cid,
            'state': state,
            'post': post
        }
        self.client.emit('update_state', data)

    def update_client_state(self, cid, state):
        """
        Update this handler's state.
        """
        if cid != self.cid:  # this client already updated its own state
            self.color = state['color']
            self.pitch = state['pitch']
            self.timbre = state['timbre']
            self.instrument = state['instrument']
            self.drum = state['drum']
            self.delete_mode = state['delete_mode']

    def sync_state(self, state):
        """
        Initial sync with the server's copy of module state.
        We don't sync with hold_shape, hold_point, and hold_line because those objects are not
        json-serializable and are short-term values anyway.
        """
        self.color = state['color']
        self.pitch = state['pitch']
        self.timbre = state['timbre']
        self.instrument = state['instrument']
        self.drum = state['drum']
        self.delete_mode = state['delete_mode']

        # after initial sync, add default values for this client
        self.color[self.cid] = self.default_color
        self.pitch[self.cid] = self.default_pitch
        self.timbre[self.cid] = self.default_timbre
        self.instrument[self.cid] = self.default_instrument
        self.drum[self.cid] = self.default_drum
        self.delete_mode[self.cid] = False

        self.skip[self.cid] = False

        # now that default values are set, we can display this module's info
        self.display = True

        # update server with these default values
        # post=True here because we want all other clients' states to update with this client's
        # default values.
        self.update_server_state(post=True)

    def sound(self, channel, pitch):
        """
        Play a sound with a given pitch on the given channel.
        """
        if self.cmd.get((channel, pitch)):
            self.sched.cancel(self.cmd[(channel, pitch)])
        self.synth.noteon(channel, pitch, 100)
        now = self.sched.get_tick()
        self.cmd[(channel,
                  pitch)] = self.sched.post_at_tick(self._noteoff, now + 240,
                                                    (channel, pitch))

    def update_pitch(self, color, pitch):
        """Update this client's color and pitch due to PitchSelect."""

        self.color[self.cid] = self.color_dict[color]
        self.pitch[self.cid] = pitch
        self.update_server_state(post=True)

    def update_instrument(self, instrument):
        """Update this client's instrument due to InstrumentSelect."""
        self.instrument[self.cid] = instrument
        self.channel = self.inst_list.index(instrument)
        self.update_server_state(post=True)

    def update_drum(self, drum):
        self.drum[self.cid] = drum
        self.drum_channel = self.drum_list.index(drum) + len(self.inst_list)
        self.update_server_state(post=True)

    def _noteoff(self, tick, args):
        channel, pitch = args
        self.synth.noteoff(channel, pitch)

    def calculate_size(self, corner, pos):
        x = abs(pos[0] - corner[0])
        y = abs(pos[1] - corner[1])
        return (x, y)

    def calculate_center(self, corner, size):
        c_x = corner[0] + (size[0] / 2)
        c_y = corner[1] + (size[1] / 2)
        return (c_x, c_y)
Example #2
0
class TempoCursorHandler(object):
    """
    Handles the TempoCursor GUI.
    Also stores and updates all currently active TempoCursors.
    """
    def __init__(self,
                 norm,
                 sandbox,
                 mixer,
                 client,
                 client_id,
                 block_handler,
                 tempo=60):
        self.norm = norm
        self.module_name = 'TempoCursor'
        self.sandbox = sandbox
        self.mixer = mixer
        self.client = client
        self.cid = client_id
        self.block_handler = block_handler

        self.tempo = tempo
        self.clock = Clock()
        self.tempo_map = SimpleTempoMap(bpm=self.tempo)

        self.touch_points = {}

        self.cursors = AnimGroup()
        self.sandbox.add(self.cursors)

        self.gui = CursorGUI(norm,
                             pos=self.norm.nt((20, 300)),
                             beat_callback=self.update_touch_points)

        self.delete_mode = {}

    def on_touch_down(self, cid, pos):
        if cid == self.cid:
            self.gui.on_touch_down(pos)

        if not self.sandbox.in_bounds(pos):
            return

        for cursor in self.cursors.objects:
            cursor_pos = (cursor.pos[0] - cursor.size[0] / 2,
                          cursor.pos[1] - cursor.size[1] / 2)
            if in_bounds(pos, cursor_pos, cursor.size):
                if self.delete_mode[cid]:
                    self.cursors.objects.remove(cursor)
                    self.cursors.remove(cursor)
                    return

        if self.delete_mode[cid]:
            return

        touch_points = self.touch_points[cid]

        if len(touch_points) == 0:
            return
        cursor = TempoCursor(self.norm, pos, self.tempo,
                             self.clock, self.tempo_map,
                             copy.deepcopy(touch_points), self.block_handler)
        self.cursors.add(cursor)

    def on_touch_move(self, cid, pos):
        pass

    def on_touch_up(self, cid, pos):
        pass

    def on_key_down(self, cid, key):
        if key == 'p':
            self.clock.toggle()
        if key == 'v' and cid == self.cid:
            self.delete_mode[cid] = not self.delete_mode[cid]
            self.update_server_state(post=True)
        if key == 'up':
            self.tempo += 4
            self.tempo_map.set_tempo(self.tempo)
            self.update_server_state(post=True)
        if key == 'down':
            self.tempo -= 4
            self.tempo_map.set_tempo(self.tempo)
            self.update_server_state(post=True)

    def on_update(self):
        self.cursors.on_update()

    def update_touch_points(self, touch_points):
        self.touch_points[self.cid] = touch_points
        self.update_server_state(post=True)

    def display_controls(self):
        cur_time = self.clock.get_time()
        cur_tick = self.tempo_map.time_to_tick(cur_time)
        info = 'delete mode: {}\n\n'.format(self.delete_mode[self.cid])
        info += 'tempo: {}\n'.format(self.tempo)
        return info

    def update_server_state(self, post=False):
        """Update server state. If post is True, relay this updated state to all clients."""
        state = {
            'touch_points': self.touch_points,
            'delete_mode': self.delete_mode,
            'tempo': self.tempo
        }
        data = {
            'module': self.module_name,
            'cid': self.cid,
            'state': state,
            'post': post
        }
        self.client.emit('update_state', data)

    def update_client_state(self, cid, state):
        """Update this handler's state."""
        if cid != self.cid:  # this client already updated its own state
            self.touch_points = state['touch_points']
            self.delete_mode = state['delete_mode']
            self.tempo = state['tempo']

    def sync_state(self, state):
        """
        Initial sync with the server's copy of module state.
        """
        self.touch_points = state['touch_points']
        self.delete_mode = state['delete_mode']
        self.tempo = state['tempo']

        # after initial sync, add default values for this client
        self.touch_points[self.cid] = []
        self.delete_mode[self.cid] = False

        # update server with these default values
        # post=True here because we want all other clients' states to update with this client's
        # default values.
        self.update_server_state(post=True)