class ExerciseWidget(BaseWidget): def __init__(self): super(ExerciseWidget, self).__init__() self.anim_group = AnimGroup() self.canvas.add(self.anim_group) self.line = TriangleA() self.anim_group.add(self.line) self.info = topleft_label() self.add_widget(self.info) self.last_touch = None def on_update(self): self.anim_group.on_update() self.update_info_label() def on_touch_move(self, touch): self.line.line.points = [200, 200, touch.pos[0], touch.pos[1]] send(str(touch.pos)) def update_info_label(self): self.info.text = str(Window.mouse_pos) self.info.text += '\nfps:%d' % kivyClock.get_fps() self.info.text += '\nobjects:%d' % len(self.anim_group.objects) self.info.text += '\nreceived:' + str(received)
class Scene(BaseWidget): def __init__(self): super(Scene, self).__init__() self.background = BackgroundWidget() self.add_widget(self.background) self.foreground = ForegroundWidget() self.add_widget(self.foreground) # Flying music notes self.anim_group = AnimGroup() self.canvas.add(self.anim_group) self.anim_group.add(FadingMusicNote()) def on_layout(self, win_size): self.background.on_layout(win_size) self.foreground.on_layout(win_size) def on_update(self): self.anim_group.on_update() def add_note_sprite(self): self.anim_group.add(FadingMusicNote((320, 80)))
class MainWidget(BaseWidget): def __init__(self): super(MainWidget, self).__init__() self.audio = Audio(2, input_func=self.receive_audio, num_input_channels=1) self.mixer = Mixer() self.audio.set_generator(self.mixer) self.pitch = PitchDetector() self.recorder = VoiceAudioWriter('data') self.info = topleft_label() self.add_widget(self.info) self.anim_group = AnimGroup() self.mic_meter = MeterDisplay((50, 25), 150, (-96, 0), (.1, .9, .3)) self.mic_graph = GraphDisplay((110, 25), 150, 300, (-96, 0), (.1, .9, .3)) self.pitch_meter = MeterDisplay((50, 200), 150, (30, 90), (.9, .1, .3)) self.pitch_graph = GraphDisplay((110, 200), 150, 300, (30, 90), (.9, .1, .3)) self.canvas.add(self.mic_meter) self.canvas.add(self.mic_graph) self.canvas.add(self.pitch_meter) self.canvas.add(self.pitch_graph) # Record button self.record_button = InteractiveImage() self.record_button.source = "../data/mic.png" self.record_button.x = 400 self.record_button.y = 400 self.record_button.size = (100, 100) self.record_button.set_callback(self.init_recording) self.add_widget(self.record_button) # Play button self.play_button = InteractiveImage() self.play_button.source = "../data/play.png" self.play_button.x = 600 self.play_button.y = 400 self.play_button.size = (100, 100) self.play_button.set_callback(self.play_recording) self.add_widget(self.play_button) self.canvas.add(self.anim_group) self.onset_disp = None self.onset_x = 0 self.cur_pitch = 0 # Note Scheduler self.synth = Synth('../data/FluidR3_GM.sf2') # create TempoMap, AudioScheduler self.tempo_map = SimpleTempoMap(120) self.sched = AudioScheduler(self.tempo_map) # connect scheduler into audio system self.mixer.add(self.sched) self.sched.set_generator(self.synth) # Note Sequencers self.seq = [] # live Generator self.live_wave = None def on_update(self): self.audio.on_update() self.anim_group.on_update() self.info.text = 'fps:%d\n' % kivyClock.get_fps() self.info.text += 'load:%.2f\n' % self.audio.get_cpu_load() self.info.text += "pitch: %.1f\n" % self.cur_pitch self.info.text += 'max delta: %.3f\n' % self.onset_detector.get_max_delta( ) self.info.text += 'onset delta thresh (up/down): %.3f\n' % self.onset_detector.onset_thresh if self.recorder.active: self.info.text += 'RECORDING' def receive_audio(self, frames, num_channels): assert (num_channels == 1) # Microphone volume level, take RMS, convert to dB. # display on meter and graph rms = np.sqrt(np.mean(frames**2)) rms = np.clip(rms, 1e-10, 1) # don't want log(0) db = 20 * np.log10(rms) # convert from amplitude to decibels self.mic_meter.set(db) self.mic_graph.add_point(db) # pitch detection: get pitch and display on meter and graph self.cur_pitch = self.pitch.write(frames) self.pitch_meter.set(self.cur_pitch) self.pitch_graph.add_point(self.cur_pitch) # record audio self.recorder.add_audio(frames, num_channels) # onset detection and classification self.onset_detector.write(frames) def init_recording(self): data = self.recorder.toggle() if data: print(data) wave_gen, filename, duration_midi = data for i in range(len(duration_midi)): if duration_midi[i][0] < 0.12: duration_midi[i] = (duration_midi[i][0], 0) duration_midi = harmony.harmonize(duration_midi) self.live_wave = wave_gen print([[i[1] for i in j] for j in duration_midi]) tempo = 120 multiplier = 1 / 60 * tempo * 480 converted_midi_duration = [[(i * multiplier, j) for i, j in k] for k in duration_midi] for i in converted_midi_duration: self.seq.append( NoteSequencer(self.sched, self.synth, 1, (0, 0), i, True)) def play_recording(self): print("hello") for i in self.seq: i.start() if self.live_wave: self.mixer.add(self.live_wave) def on_key_down(self, keycode, modifiers): t = lookup(keycode[1], ['up', 'down'], [.001, -.001]) if t is not None: self.onset_detector.onset_thresh += t if keycode[1] == "w": self.init_recording() if keycode[1] == "s" and self.seq: self.play_recording()
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)
class MainWidget3(BaseWidget) : def __init__(self): super(MainWidget3, self).__init__() self.audio = Audio(2) self.synth = Synth('../data/FluidR3_GM.sf2') # create TempoMap, AudioScheduler self.tempo_map = SimpleTempoMap(104) self.sched = AudioScheduler(self.tempo_map) # connect scheduler into audio system self.audio.set_generator(self.sched) self.sched.set_generator(self.synth) # create the metronome: self.metro = Metronome(self.sched, self.synth) percNotes = [(480,35), (360,42), (120,35), (480,35), (480,42)] self.base1Notes = [(240,43), (240,43), (240,43), (120,47), (240,41), (240,41), (360,41), (120,40), (360,41), (240,41), (240,41), (120,40), (120,36), (480,-1), (120,40), (240,41), (120,43)] self.base2Notes = [(120,-1), (120,45), (240,43), (120,-1), (240,43), (120,40), (480,43), (120,-1), (120,45), (120,45), (120,48), (240,-1), (240,41), (120,-1), (240,41), (120,40), (480,41), (120,-1), (120,45), (120,45), (120,48), (240,-1), (240,45), (120,-1), (240,45), (120,45), (480,45), (240,43), (120,-1), (120,45), (240,-1), (240,45), (120,-1), (240,45), (120,45), (480,45), (120,-1), (120,45), (120,45), (120,48)] self.baseNotes = self.base2Notes #[40, 41, 43, 45 48,] #changes / pitch sutff self.changes = [ (1920, [72, 74, 76, 79, 81, 84]), (1920, [69, 72, 74, 81]), (3840, [69, 72, 74, 76, 79, 81, 84])] self.changesIndex = 0 self.curChanges = [] self.selectSize = 2 self.lastPitchIndex = None self.lastTouch = None #Note length stuff self.noteLengths = [480, 240, 120] self.articulation = 1 self.lastPulseIndex = 0 #Declare the players self.perc = NoteSequencer(self.sched, self.synth, 1, (128,0), percNotes) self.base1 = NoteSequencer(self.sched, self.synth, 2, (0,33), self.base1Notes, callback = self.graphic_callback) self.base2 = NoteSequencer(self.sched, self.synth, 2, (0,33), self.base2Notes, callback = self.graphic_callback) self.lead = Arpeggiator(self.sched, self.synth, channel = 3, program = (0,65), callback = self.graphic_callback) self.lead.set_direction('updown') #Start the non-interactive stuff now = self.sched.get_tick() next_beat = quantize_tick_up(now, 480) self.perc.toggle() self.base2.toggle() self.sched.post_at_tick(self._updateChanges, next_beat) #Update changes as music starts self.sched.post_at_tick(self._spawnCrossBar, next_beat) # and text to display our status #self.label = topleft_label() #self.add_widget(self.label) #Graphics stuff self.objects = AnimGroup() self.canvas.add(self.objects) #self.allNotes = [40, 41, 43, 45, 48, 900, 69, 72, 74, 76, 79, 81, 84] self.allNotes = [36, 40, 41, 43, 45, 47, 48, 900, 69, 72, 74, 76, 79, 81, 84] def graphic_callback(self, pitch, length): w = Window.width numBuckets = len(self.allNotes) bucket = self.allNotes.index(pitch) widthOfBucket = w/numBuckets width = widthOfBucket - 10 leftX = bucket*widthOfBucket + 5 height = length/480 * 100 shape = NoteShape((leftX,0), height, width) self.objects.add(shape) def _spawnCrossBar(self, tick, ignore): shape = CrossBar() self.objects.add(shape) self.sched.post_at_tick(self._spawnCrossBar, tick+480) def _updateChanges(self, tick, ignore): timeTillNextChange = self.changes[self.changesIndex][0] self.curChanges = self.changes[self.changesIndex][1] #print("CHANGE OCCURED: ", self.curChanges) self.changesIndex = (self.changesIndex + 1) % len(self.changes) self.sched.post_at_tick(self._updateChanges, tick+timeTillNextChange) self.lastPitchIndex = None if self.lastTouch != None: self.update_pitches(self.lastTouch) def changeBaseLine(self): self.base1.toggle() self.base2.toggle() def on_key_down(self, keycode, modifiers): obj = lookup(keycode[1], 'm', (self.metro)) if obj is not None: obj.toggle() if keycode[1] == 'q': self.changeBaseLine() def on_key_up(self, keycode): pass def on_touch_down(self, touch): p = touch.pos self.update_pitches(p) self.update_pulse(p) self.lead.start() self.lastTouch = p def on_touch_up(self, touch): self.lead.stop() def on_touch_move(self, touch): p = touch.pos self.update_pitches(p) self.update_pulse(p) self.lastTouch = p def update_pitches(self, pos=(0,0)): mouseX = pos[0] w = Window.width numBuckets = len(self.curChanges) - self.selectSize + 1 sizeOfBucket = w / numBuckets noteBucket = int(mouseX // sizeOfBucket) if noteBucket != self.lastPitchIndex: arpegNotes = self.curChanges[noteBucket:noteBucket+self.selectSize] self.lead.set_pitches(arpegNotes) self.lastPitchIndex = noteBucket def update_pulse(self, pos=(0,0)): mouseY = pos[1] h = Window.height numBuckets = len(self.noteLengths) sizeOfBucket = h / numBuckets pulseBucket = int(mouseY // sizeOfBucket) if pulseBucket < len(self.noteLengths) and pulseBucket != self.lastPulseIndex: length = self.noteLengths[pulseBucket] self.lead.set_rhythm(length, self.articulation) self.lastPulseIndex = pulseBucket def on_update(self) : self.audio.on_update() self.objects.on_update()
class PianoPuzzle(Puzzle): def __init__(self, prev_room, level=0, on_finished_puzzle=None): super().__init__() self.door_sources = { (4, 9): "./data/Door_up.png", # UP (4, -1): "./data/door_down.png", # DOWN (-1, 4): "./data/door_left.png", # LEFT (9, 4): "./data/Door_right.png", # RIGHT } self.prev_room = prev_room self.on_finished_puzzle = on_finished_puzzle self.animations = AnimGroup() self.level = level self.notes, self.actual_key = levels[level] duration = choice(durations) pitch_shift = choice(range(-3, 4)) self.user_notes = [ Note(duration, n.get_pitch() + pitch_shift) for n in self.notes ] render_user_notes = self.level < 2 self.actual_sound = PuzzleSound(self.notes) self.user_sound = PuzzleSound(self.user_notes) self.music_bar = MusicBar(self.notes, self.user_notes, render_user_notes, self.actual_sound) self.animations.add(self.music_bar) self.add(self.animations) self.user_key = "C" self.place_objects() self.key_label = CLabelRect( (Window.width // 30, 23 * Window.height // 32), f"Key: {self.user_key}", 34) self.add(Color(rgba=(1, 1, 1, 1))) self.add(self.key_label) self.create_instructions((Window.width, Window.height)) self.add(self.instructions_text_color) self.add(self.instructions_text) self.objects = {} def play(self, actual=False): if actual: print("Should be setting the cb_ons") self.actual_sound.set_cb_ons([self.music_bar.play]) self.actual_sound.toggle() else: self.user_sound.set_cb_ons([self.music_bar.play]) self.user_sound.toggle() def create_instructions(self, win_size): self.instructions_text_color = Color(rgba=(1, 1, 1, 1)) self.instructions_text = CLabelRect( (win_size[0] // 8, win_size[1] * 4 / 7), "Press + to hear your note sequence.\nPress - to hear the sequence\nyou are trying to match.", 20, ) def on_pitch_change(self, pitch_index): offset = 1 if self.character.direction == Button.RIGHT.value else -1 for note in self.user_notes: pitch = note.get_pitch() note.set_note(pitch + offset) self.user_sound.set_notes(self.user_notes) def on_duration_change(self, dur_index): for note in self.user_notes: note.set_dur(durations[dur_index]) self.user_sound.set_notes(self.user_notes) def on_key_change(self, key_index): self.user_key = key_names[key_index] self.update_key() self.user_sound.set_notes(self.user_notes) def update_key(self): key_sig = keys[self.user_key] for note in self.user_notes: base_letter = note.get_letter()[0] letter_before = names[names.index(base_letter) - 1] if base_letter not in key_sig["#"]: note.remove_sharp() if base_letter not in key_sig["b"]: note.remove_flat() if base_letter in key_sig["#"] and not (note.get_letter()[:-1] == base_letter + "#"): note.add_sharp() if base_letter in key_sig["b"] and not (note.get_letter()[:-1] == letter_before + "#"): note.add_flat() """ Mandatory Puzzle methods """ def is_game_over(self): same_key = self.user_key == self.actual_key same_dur = (self.music_bar.user_notes[0].get_dur() == self.music_bar.actual_notes[0].get_dur()) same_pitch = (self.music_bar.user_notes[0].get_pitch() == self.music_bar.actual_notes[0].get_pitch()) return same_key and same_dur and same_pitch def create_objects(self): size = (self.grid.tile_side_len, self.grid.tile_side_len) # PITCH self.objects[(4, 7)] = MovingBlock( size, self.grid.grid_to_pixel((4, 7)), ((1, 7), (8, 7)), "./data/pitch_slider.png", self.on_pitch_change, ) # RHYTHM duration = self.user_notes[0].get_dur() self.objects[(durations.index(duration) + 3, 2)] = MovingBlock( size, self.grid.grid_to_pixel((durations.index(duration) + 3, 2)), ((3, 2), (7, 2)), "./data/rhythm_slider.png", self.on_duration_change, ) # KEY self.objects[(key_names.index(self.user_key) + 1, 5)] = MovingBlock( size, self.grid.grid_to_pixel((key_names.index(self.user_key) + 1, 5)), ((1, 5), (7, 5)), "./data/key_slider.png", self.on_key_change, ) self.objects[(9, 4)] = DoorTile( size, self.grid.grid_to_pixel((9, 4)), self.prev_room, source=self.door_sources[(9, 4)], ) def place_objects(self): # rhythm_color = Color(rgba=(0, 0.5, 0.5, 1)) for i in range(len(durations)): # rhythm self.grid.get_tile( (i + 3, 2)).set_color(color=Tile.base_color, source="./data/trackv2.png") # pitch_color = Color(rgba=(0.2, 0.5, 1, 1)) for i in range(7): # rhythm self.grid.get_tile( (i + 1, 7)).set_color(color=Tile.base_color, source="./data/trackv2.png") # key_color = Color(rgba=(0.2, 0.5, 0, 1)) for i in range(len(key_names)): self.grid.get_tile( (i + 1, 5)).set_color(color=Tile.base_color, source="./data/trackv2.png") self.add(PushMatrix()) self.add(Translate(*self.grid.pos)) for pos, obj in self.objects.items(): self.add(obj) self.add(PopMatrix()) def move_block(self, new_location, x, y): obj_loc = (new_location[0] + x, new_location[1] + y) if self.is_valid_pos(obj_loc) and self.valid_block_move( obj_loc, self.objects[new_location].move_range): self.remove(self.objects[new_location]) obj = MovingBlock( self.objects[new_location].size, self.grid.grid_to_pixel(obj_loc), self.objects[new_location].move_range, self.objects[new_location].icon_source, self.objects[new_location].callback, ) del self.objects[new_location] self.add(PushMatrix()) self.add(Translate(*self.grid.pos)) self.add(obj) self.add(PopMatrix()) self.objects[obj_loc] = obj self.objects[obj_loc].on_block_placement(obj_loc) return True else: return False def valid_block_move(self, pos, move_range): return (move_range[0][0] <= pos[0] < move_range[1][0] and move_range[0][1] <= pos[1] <= move_range[1][1]) def on_player_input(self, button): player_pos = self.character.grid_pos if button in [Button.UP, Button.DOWN, Button.LEFT, Button.RIGHT]: move_possible = True x, y = button.value new_pos = (player_pos[0] + x, player_pos[1] + y) if new_pos in self.objects: if isinstance(self.objects[new_pos], DoorTile): if not isinstance(self.objects[new_pos].other_room, Puzzle): # instantiate class when we enter the door self.objects[new_pos].other_room = self.objects[ new_pos].other_room(self, self.level + 1, self.on_finished_puzzle) next_room_pos = (8 - new_pos[0] + x, 8 - new_pos[1] + y) self.objects[ new_pos].other_room.character.change_direction( button.value) self.objects[new_pos].other_room.character.move_player( next_room_pos) return self.objects[new_pos].other_room self.character.change_direction(button.value) if new_pos in self.objects: if self.objects[new_pos].moveable: move_possible = self.move_block(new_pos, x, y) if move_possible: self.character.move_player(new_pos) player_pos = self.character.grid_pos if button == Button.MINUS: self.play(actual=True) elif button == Button.PLUS: self.play(actual=False) def on_update(self): self.animations.on_update() self.actual_sound.on_update() self.user_sound.on_update() self.key_label.set_text(f"Key: {self.user_key}") if not self.game_over and self.is_game_over(): for pos, obj in self.objects.items(): if isinstance(obj, MovingBlock): obj.moveable = False if self.level == max(levels.keys()): self.on_finished_puzzle() self.on_game_over() else: if (-1, 4) not in self.objects: size = (self.grid.tile_side_len, self.grid.tile_side_len) self.objects[(-1, 4)] = DoorTile( size, self.grid.grid_to_pixel((-1, 4)), PianoPuzzle, source=self.door_sources[(-1, 4)], ) self.add(PushMatrix()) self.add(Translate(*self.grid.pos)) self.add(self.objects[(-1, 4)]) self.add(PopMatrix()) def on_layout(self, win_size): self.remove(self.character) self.remove(self.grid) self.remove(self.instructions_text_color) self.remove(self.instructions_text) self.grid.on_layout((win_size[0], 0.75 * win_size[1])) for pos, obj in self.objects.items(): self.remove(obj) self.add(self.grid) if not self.objects: self.create_objects() self.place_objects() self.character.on_layout(win_size) self.add(self.character) self.music_bar.on_layout(win_size) self.remove(self.key_label) self.key_label = CLabelRect( (win_size[0] // 30, 23 * win_size[1] // 32), f"Key: {self.user_key}", 34) self.add(Color(rgba=(1, 1, 1, 1))) self.add(self.key_label) self.create_instructions(win_size) self.add(self.instructions_text_color) self.add(self.instructions_text) self.create_game_over_text(win_size) if self.game_over: self.remove(self.game_over_window_color) self.remove(self.game_over_window) self.remove(self.game_over_text_color) self.remove(self.game_over_text) self.on_game_over()
class PhysicsBubbleHandler(object): """ Handles user interaction and drawing of graphics before generating a PhysicsBubble. Handles the PhysicsBubble GUI. Also stores and updates all currently active PhysicsBubbles. """ def __init__(self, norm, sandbox, mixer, client, client_id, block_handler): self.norm = norm self.module_name = 'PhysicsBubble' self.sandbox = sandbox self.mixer = mixer self.client = client self.cid = client_id self.block_handler = block_handler # 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_line = {} self.hold_point = {} self.hold_shape = {} self.text = {} self.text_color = Color(0, 0, 0) # this mysterious variable is needed for a race condition in which touch_up events are # sometimes registered before touch_down events when the user clicks too fast, causing # touch_down and touch_up to occur at roughly the same time. if touch_up happens first, # it returns early. when touch_down is called later, it **skips** (hence the name) # adding shapes to the sandbox, after which this is toggled to False again. basically, # nothing is drawn to the screen, which indirectly prompts the user to try again. 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.pitch_list = [60, 62, 64, 65, 67, 69, 71, 72] self.default_color = self.color_dict['red'] self.default_pitch = self.pitch_list[0] self.default_timbre = 'sine' self.default_bounces = 5 self.color = {} self.pitch = {} self.timbre = {} self.bounces = {} self.gravity = {} # flag used to only display controls when this module is synced # see on_update() and sync_state() self.display = False self.bubbles = AnimGroup() self.sandbox.add(self.bubbles) # GUI elements self.gui = BubbleGUI(self.norm, pos=self.norm.nt((50, 100)), pitch_callback=self.update_pitch, bounce_callback=self.update_bounces, gravity_callback=self.update_gravity, timbre_callback=self.update_timbre) def timbre_to_shape(self, timbre, pos): if timbre == 'sine': return CEllipse(cpos=pos, size=self.norm.nt((80, 80)), segments=20) elif timbre == 'triangle': return CEllipse(cpos=pos, size=self.norm.nt((90, 90)), segments=3) elif timbre == 'square': return CRectangle(cpos=pos, size=self.norm.nt((80, 80))) elif timbre == 'sawtooth': # square rotated 45 degrees return CEllipse(cpos=pos, size=self.norm.nt((90, 90)), segments=4) 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 # start drawing drag line and preview of the PhysicsBubble self.hold_point[cid] = pos self.hold_shape[cid] = self.timbre_to_shape(self.timbre[cid], pos) self.hold_line[cid] = Line(points=(*pos, *pos), width=3) self.text[cid] = CLabelRect(cpos=pos, text=str(self.bounces[cid])) # if self.skip.get(cid) == True: # self.skip[cid] = False # return self.sandbox.add(Color(*self.color[cid])) self.sandbox.add(self.hold_shape[cid]) self.sandbox.add(self.hold_line[cid]) self.sandbox.add(self.text_color) self.sandbox.add(self.text[cid]) def on_touch_move(self, cid, pos): if not self.sandbox.in_bounds(pos): return # update the position of the drag line and preview of the PhysicsBubble self.hold_shape[cid].set_cpos(pos) self.text[cid].set_cpos(pos) self.hold_line[cid].points = (*self.hold_point[cid], *pos) def on_touch_up(self, cid, pos): if (self.hold_shape.get(cid) not in self.sandbox) or \ (self.text.get(cid) not in self.sandbox) or \ (self.hold_line.get(cid) not in self.sandbox): # if we were currently drawing a preview shape/line but released the mouse out of # bounds, we should release the shape anyway as a QOL measure if not self.sandbox.in_bounds(pos): return # else: # self.skip[cid] = True # return self.sandbox.remove(self.hold_shape[cid]) self.sandbox.remove(self.text[cid]) self.sandbox.remove(self.hold_line[cid]) # calculate velocity hold_point = self.hold_point[cid] dx = pos[0] - hold_point[0] dy = pos[1] - hold_point[1] vel = (-dx, -dy) pitch = self.pitch[cid] timbre = self.timbre[cid] color = self.color[cid] bounces = self.bounces[cid] gravity = self.gravity[cid] # release the PhysicsBubble bubble = PhysicsBubble(self.norm, self.sandbox, pos, vel, pitch, timbre, color, bounces, self, gravity=gravity, callback=self.sound) self.bubbles.add(bubble) 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() d_bounces = lookup(key, ['right', 'left'], [1, -1]) if d_bounces is not None: self.bounces[cid] += d_bounces if cid == self.cid: self.gui.bs.update_bounces(self.bounces[cid]) timbre = lookup(key, 'asdf', ['sine', 'square', 'triangle', 'sawtooth']) if timbre is not None: self.timbre[cid] = timbre if self.cid == cid: self.gui.ts.select(timbre) # have the GUI update as well if key == 'g': # toggle gravity if cid == self.cid: self.gravity[cid] = not self.gravity[cid] self.gui.gs.toggle() # other clients should update their state to reflect this client's new selection. if self.cid == cid: # don't want every client updating server's state at the same time! self.update_server_state(post=False) def sound(self, pitch, timbre): """ Play a sound when a PhysicsBubble collides with a collidable object. """ note = NoteGenerator(pitch, 1, timbre) env = Envelope(note, 0.01, 1, 0.2, 2) self.mixer.add(env) 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_timbre(self, timbre): """Update this client's timbre due to TimbreSelect.""" self.timbre[self.cid] = timbre self.update_server_state(post=True) def update_gravity(self, gravity): """Update this client's gravity due to GravitySelect.""" self.gravity[self.cid] = gravity self.update_server_state(post=True) def update_bounces(self, bounces): """Update this client's bounces due to BounceSelect.""" self.bounces[self.cid] = bounces self.update_server_state(post=True) def display_controls(self): """Provides additional text info specific to this module to go on the top-left label.""" return 'click and drag!' def on_update(self): self.bubbles.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, 'bounces': self.bounces, 'gravity': self.gravity } 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.bounces = state['bounces'] self.gravity = state['gravity'] 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.bounces = state['bounces'] self.gravity = state['gravity'] # 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.bounces[self.cid] = self.default_bounces self.gravity[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)