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)
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)