class Voice(object): def __init__(self, id, composer, note_range=[24, 48], register=None, behaviour=None, note=None, real_note=None, note_length_grouping=sample(GROUPINGS)): # AFFILIATION self.composer = composer # store the composer # IDENTITY self.id = id self.register = (self.composer.registers[register] if register else self.composer.registers[sample(self.composer.registers.keys())]) # TECH self.track_me = False self.queue = deque([], composer.settings['track_voices_length']) # STARTUP self.pan_pos = composer.behaviour.voice_get(self.id, "default_pan_position") self.range = sorted(note_range) self.dir = 0 self.prior_note = None self.note_change = True self.generator = self.voice() self.generator.next() # set the coroutine to the yield-point self.counter = 0 self.volume = composer.behaviour.voice_get(self.id, "default_volume") self.scale = composer.scale self.do_embellish = False self.note_delta = None self.weight = MEDIUM self.note = note or int((max(self.range) - min(self.range)) / 2) + min(self.range) self.real_note = (real_note or int((max(self.range) - min(self.range)) / 2) + min(self.range)) # BEHAVIOUR if behaviour: if isinstance(behaviour, basestring): self.behaviour = behaviour else: self.behaviour = behaviour[0] self.followed_voice_id = behaviour[1] self.following_counter = 0 self.follow_limit = sample(range(5, 11)) else: self.behaviour = self.composer.behaviour["default_behaviour"] self.should_play_a_melody = self.composer.behaviour.voice_get( self.id, 'should_play_a_melody') self.playing_a_melody = False self.current_adsr = self.composer.behaviour.voice_get(self.id, 'adsr') self.duration_in_msec = 0 self.change_rhythm_after_times = 1 self.note_length_grouping = note_length_grouping self.set_rhythm_grouping(note_length_grouping) self.note_duration_steps = 1 self.pause_prob = self.composer.behaviour.voice_get(self.id, 'default_pause_prob') self.legato_prob = 0.1 # to-do: really implement it # probability to have an embellishment-ornament during the current note self.embellishment_prob = self.composer.behaviour['default_embellishment_prob'] self.movement_probs = DEFAULT_MOVEMENT_PROBS self.binaural_diff = 0 # this is not used in this module directly, but serves to track self.slide = self.composer.behaviour.voice_get(self.id, "automate_slide") self.slide_duration_prop = self.composer.behaviour.voice_get( self.id, 'slide_duration_prop') self.next_pat_length = None self.note_duration_prop = composer.behaviour['default_note_duration_prop'] # WAVETABLE - this is used for non-automated wavetables self.wavetable_generation_type = sample( composer.behaviour.voice_get(self.id, 'wavetable_specs'))[0] self.partial_pool = sample( sample(self.composer.behaviour.voice_get(self.id, 'wavetable_specs'))[1]) self.num_partials = composer.behaviour.voice_get(self.id, 'default_num_partial') self.set_note_length_groupings() self.add_setters_for_behaviour_dict() self.musical_logger = logging.getLogger('musical') if self.composer.behaviour['automate_microvolume_change']: self.new_microvolume_sine() self.microvolume_variation = self.composer.behaviour.voice_get( self.id, 'microvolume_variation') self.current_microvolume = self.update_current_microvolume() def new_microvolume_sine(self): args = [random.random() * self.composer.behaviour['microvolume_max_speed_in_hz'] for n in range(10)] self.microvolume_sine = MultiSine(args) def update_current_microvolume(self): self.current_microvolume = self.microvolume_sine.get_value_as_factor( self.microvolume_variation) def add_setters_for_behaviour_dict(self): beh = self.composer.behaviour['per_voice'][self.id] beh.real_setters["slide_duration_prop"] = [setattr, self, "slide_duration_prop"] beh.real_setters["binaural_diff"] = [setattr, self, "binaural_diff"] def set_pan_pos(self, gateway, pan_pos): self.pan_pos = pan_pos gateway.send_voice_pan(self, pan_pos) def __str__(self): return str({"note": self.note, "dir": self.dir, "id": self.id, "note_change": self.note_change}) def __repr__(self): return "{0} - {1}".format(self.__class__, self.__str__()) def voice(self): """the generator method of the Voice-class""" while True: state = (yield) meter_pos = state['cycle_pos'] # print self.on_off_pattern, " for: ", self.id self.note_change = self.on_off_pattern[meter_pos] if random.random() < self.legato_prob: self.note_change = 0 self.weight = state["weight"] if self.note_change: # calculate duration by checking for the next note # in the pattern tmp_list = self.on_off_pattern[(meter_pos + 1):] if 1 in tmp_list: self.note_duration_steps = tmp_list.index(1) + 1 else: # self.note_duration_steps = 1 self.note_duration_steps = len(self.on_off_pattern) - meter_pos self.prior_note = self.note if random.random() < self.pause_prob and not self.playing_a_melody: self.note = 0 else: self.note = self.next_note(state) self.note_delta = self.note - self.prior_note if self.track_me: self.queue.append(self.note) if random.random() < self.embellishment_prob: self.do_embellish = True def next_note(self, state): """the next note is calculated/read here""" meter_pos = state["cycle_pos"] if self.behaviour == "SLAVE": follow = self.other_voices[self.followed_voice_id] if follow.note_change: if follow.note == 0: return 0 if self.following_counter == 0: self.follow_dist = sample(FOLLOWINGS) if self.following_counter < self.follow_limit: res = follow.note + self.follow_dist self.following_counter += 1 return res else: self.reset_slave() if self.id == 1: # TODO: make more sofisticated (e.g. arpeggiator or thirds) bar_sequence = state.get('bar_sequence') if bar_sequence: chord_note_offset = sample(BASS_CHORD_DISTANCE_PROBS) sequence_note = bar_sequence[state['bar_sequence_current_position']] if self.weight in [HEAVY, MEDIUM]: return sequence_note + min(self.range) + chord_note_offset move = sample([-1, 1]) * sample(self.movement_probs) if self.dir: move = (self.dir * sample(self.movement_probs)) elif self.playing_a_melody: try: move = self.manage_melody_note(meter_pos) except StopIteration: self.musical_logger.info("melody finished") self.playing_a_melody = False else: if (self.should_play_a_melody and self.note != 0 and self.weight in [HEAVY, MEDIUM]): # if (self.melody_starts_on == (self.note % 7) and # regarding the on-off pattern we try a minimum invasive strategy # by modifying only those indexes of the pattern covered by the # current note and the start of the following note # print "searching for a suitable melody" self.melody = self.search_suitable_melody(state['speed']) if self.melody: self.musical_logger.info("starting the melody: {0}".format(self.melody)) self.melody_iterator = iter(self.melody["melody"]) move = self.manage_melody_note(meter_pos) self.playing_a_melody = True res = self.note + move exceed = self.exceeds(res) if not self.playing_a_melody: if exceed: res, self.dir = exceed # self.musical_logger.info( # "exceeding note of voice {2}: '{0}', going: \t{1}".format(res, # self.dir > 0 and 'up' or 'down', # self.id)) if self.in_the_middle(res): self.dir = 0 if self.exceeds(res): raise RuntimeError('''diabolus in musica: {0} is too low/high, dir:{1}'''.format(res, self.dir)) return res def search_suitable_melody(self, speed): candidates = [] for melody_name, melody in melodies.items(): speed_range = melody["speed_range"] right_note = self.note % 7 == melody["start_note"] right_scale = self.composer.scale == melody["scale"] right_speed = speed_range[0] < speed < speed_range[1] right_meter = self.composer.meter in melody["meters"] if right_note and right_scale and right_speed and right_meter: candidates.append({melody_name: melody}) if len(candidates) > 0: chosen = sample(candidates).items()[0] self.musical_logger.info("new melody: {0}".format(chosen[0])) return chosen[1] def manage_melody_note(self, meter_pos): """retrieves next note-delta and length belonging to the melody. sets the following 'bits' of the off-on-pattern according to the specified length of the note. returns the pitch-related move (delta) and sets eventual modifier attribute on the composer""" move, length = self.melody_iterator.next() if type(move) == str: number, modifier = melody_player.extract_modified_move(move) self.composer.modified_note_in_current_frame = (number, modifier) move = number if self.melody_iterator.__length_hint__() == 1: # TODO: communicate to director that a caesura is required pass self.musical_logger.info("melody move: {0} \tof length: {1} ".format(move, length)) oop = self.on_off_pattern oop[meter_pos] = 1 remaining = len(oop) - meter_pos if remaining < length: this_pat_length = remaining self.musical_logger.info("dbg: overhanging note") oop += [0] * (length - remaining) + [1] self.next_pat_length = length - remaining # self.apply_overhanging_notes() else: this_pat_length = length # this is for the following note, # if it is not run the next pattern will start with a note # anyway try: self.on_off_pattern[meter_pos + length] = 1 except IndexError: pass for note_unit in range(1, this_pat_length): self.on_off_pattern[meter_pos + note_unit] = 0 # Note: we set next_pat_length int if note is longer than the remaining # part of the cycle. upon next cycle the rest of the note is # applied to the new on-off-pattern. return move def apply_overhanging_notes(self): """applies overhanging notes of a registered melody to the next <on_off_pattern>""" if len(self.on_off_pattern) > self.next_pat_length: for idx in range(self.next_pat_length): self.on_off_pattern[idx] = 0 # this might do the roundtrip..... # self.on_off_pattern[idx + 1] = 1 self.next_pat_length = None else: self.next_pat_length -= len(self.on_off_pattern) def exceeds(self, note): """returns min/max limits and a bounce back direction coefficient if incoming int exceeds limits, returns False otherwise""" if self.composer.scale in ["PENTATONIC", "PENTA_MINOR"]: range = [int((x / 7.0) * 5) for x in self.range] else: range = self.range if note > range[1]: return (range[1] - sample([0, 0, 1, 1, 2]), -1) elif note < range[0]: return (range[0] + sample([0, 0, 1, 1, 2]), 1) else: False def set_rhythm_grouping(self, grouping): """setter method which creates also the on/off pattern""" if self.counter % self.change_rhythm_after_times == 0: self.note_length_grouping = grouping self.on_off_pattern = analyze_grouping(grouping) # here we check if we are playing a melody and if so if we have # remaining note parts to be applied. if self.playing_a_melody: if self.next_pat_length: self.apply_overhanging_notes() self.counter += 1 def in_the_middle(self, note): """returns true if int is in the center area of the range""" range_span = self.range[1] - self.range[0] lower_thresh = self.range[0] + (range_span * 0.333) upper_thresh = self.range[0] + (range_span * 0.666) return note > lower_thresh and note < upper_thresh def reset_slave(self, change_master=False): """resets values for slave voices. if <change_master> is an int: - it is used as the id new master-voice if <change_master> is 'True': - a new random master is chosen, """ if change_master: if type(change_master) == int: self.followed_voice_id = change_master else: self.followed_voice_id = sample(self.others.keys()) follow = self.other_voices[self.followed_voice_id] self.slide_duration_prop = follow.slide_duration_prop self.slide = follow.slide self.following_counter = 0 self.follow_limit = sample(range(5, 11)) def set_note_length_groupings(self, mapping={'BASS': 'HEAVY_GROUPINGS', 'ROCK_BASS': 'FAST_GROUPINGS', 'FLAT_MID': 'FAST_GROUPINGS', 'LOW_MID': 'DEFAULT_GROUPINGS', 'MID': 'DEFAULT_GROUPINGS', 'HIGH': 'TERNARY_GROUPINGS'}): """sets the note_length_groupings attribute on self by reading the current value from the composer""" self.note_length_groupings = getattr(self.composer, mapping[self.register['name']]) def register_other_voices(self): '''returns the other voices registered in the app''' self.other_voices = {} for k, v in self.composer.voices.items(): if v != self: self.other_voices[k] = v def reload_register(self): '''reloads the current register and reapplies its settings - in voice - in the controller''' # print "reloading register: {0}".format(name) for k, v in self.register["voice_attrs"].items(): setattr(self, k, v) for k, v in self.register["voice_composer_attrs"].items(): setattr(self, k, getattr(self.composer, v)) self.counter = 0 def make_wavetable(self): '''assembles a wavetable using the registered wavetable-related params''' fun = getattr(wavetables, self.wavetable_generation_type + '_wavetable') return fun(self.num_partials, self.partial_pool)
def new_microspeed_sine(self): args = [random() * self.behaviour['microspeed_max_speed_in_hz'] for n in range(5)] self.microspeed_sine = MultiSine(args)
def new_microvolume_sine(self): args = [random.random() * self.composer.behaviour['microvolume_max_speed_in_hz'] for n in range(10)] self.microvolume_sine = MultiSine(args)
class Director(IncomingMessagesMixin, WavetableMixin, ADSRMixin, SpeedMixin): def __init__(self, gateway, behaviour, settings): composer = globals().get(settings.get('composer', 'baroq')) if not composer: raise RuntimeError("Composer is not configured correctly") self.composer = composer.Composer(gateway, settings, behaviour) self.behaviour = behaviour self.playing = None self.stopped = False self.force_caesura = False self.settings = settings self.gateway = self.composer.gateway # keep this between 0 and MAX_SHUFFLE self.shuffle_delay = behaviour["shuffle_delay"] self.meter = self.composer.applied_meter self.metronome = metronome.Metronome(self.meter) self.automate_binaural_diffs = behaviour["automate_binaural_diffs"] self.automate_meters = behaviour["automate_meters"] self.speed_change = behaviour["speed_change"] self.MIN_SPEED = behaviour["min_speed"] self.MAX_SPEED = behaviour["max_speed"] self.MAX_SHUFFLE = behaviour["max_shuffle"] self.musical_logger = logging.getLogger('musical') self.behaviour_logger = logging.getLogger('behaviour') self.gui_logger = logging.getLogger('gui') self.add_setters() if behaviour['automate_microspeed_change']: self.new_microspeed_sine() self.state = {"comp": self.composer, "speed": behaviour["speed"]} self.speed_target = behaviour["speed_target"] self.speed = self.state["speed"] if behaviour['follow_bar_sequence']: self.state.update({ 'bar_sequence': behaviour['bar_sequence'], 'bar_sequence_current_position': 0}) for voice in self.composer.voices.values(): self.apply_voice_adsr(voice) self.has_gui = settings['gui'] self.gui_sender = self.has_gui and GuiConnect() or None self.allowed_incoming_messages = ( self.has_gui and self.behaviour.keys() + ['play', 'sys', 'scale', 'force_caesura', 'trigger_wavetable'] or None) if self.has_gui: self.incoming = deque() self.gui_sender.update_gui(self) #start the reader thread thre = threading.Thread(target=self.gui_sender.read_incoming_messages, args=(self.incoming,)) thre.daemon = True thre.start() def new_microspeed_sine(self): args = [random() * self.behaviour['microspeed_max_speed_in_hz'] for n in range(5)] self.microspeed_sine = MultiSine(args) def add_setters(self): self.behaviour.real_setters["meter"] = self.set_meter self.behaviour.real_setters["transpose"] = self.gateway.set_transpose self.behaviour.real_setters["speed"] = self.new_speed self.behaviour.real_setters["binaural_diff"] = self.composer.set_binaural_diffs self.behaviour.real_setters["slide_duration_msecs"] = self.gateway.set_slide_msecs_for_all_voices for vid in self.behaviour['per_voice'].keys(): self.behaviour['per_voice'][vid].real_setters["pan_pos"] = \ [self.composer.voices[vid].set_pan_pos, self.gateway] self.behaviour['per_voice'][vid].real_setters["slide_duration_msecs"] = \ [self.gateway.set_slide_msecs, vid] def set_meter(self, meter): self.composer.set_meter(meter) self.meter = self.composer.applied_meter self.metronome.set_meter(self.composer.offered_meters[meter]["applied"]) def _play(self, shutdown_event=None, duration=None): """this is the core of the program giving the impulse for all actions. """ self.start_time = time.time() self.playing = True self.musical_logger.info("<<<<<< start playing >>>>>>") pos = 0 while not self.stopped: if shutdown_event and shutdown_event.isSet(): self.stop() if not self.playing: self.check_incoming_messages() time.sleep(0.2) continue if duration: pos += self.speed if pos > duration: self.playing = False self.musical_logger.info("<<<<< stop playing >>>>>") cycle_pos, weight = self.metronome.beat() self.state.update({'weight': weight, 'cycle_pos': cycle_pos}) # on heavy beats a new rhythm-grouping is loaded if weight == metronome.HEAVY: self.composer.choose_rhythm() comment = self.composer.generate(self.state) if ((comment == 'caesura' and random() < self.behaviour["caesura_prob"]) or self.force_caesura): if self.force_caesura: self.force_caesura = False # take 5 + 1 times out.... time.sleep(self.speed * 4) self.shuffle_delay = random() * self.MAX_SHUFFLE logger.info("shuffle delay set to: {0}".format( self.shuffle_delay)) self.new_speed() self.state["speed"] = self.speed self.metronome.reset() self.composer.gateway.stop_all_notes() voices = self.composer.voices.values() if self.behaviour['automate_adsr']: self.new_random_adsr_for_all_voices() if self.behaviour['automate_scale']: self.composer.set_scale(choice( self.composer.offered_scales)) if self.behaviour['automate_meters']: self.new_random_meter() if self.behaviour["automate_pan"]: for v in voices: if self.behaviour.voice_get(v.id, "automate_pan"): max_pos = self.behaviour.voice_get(v.id, "automate_pan") v.pan_pos = (random() * max_pos) - max_pos / 2.0 self.gateway.send_voice_pan(v, v.pan_pos) if self.behaviour["automate_binaural_diffs"]: if self.behaviour["pan_controls_binaural_diff"]: for v in voices: if not self.behaviour.voice_get(v.id, "automate_binaural_diffs"): continue diff = (abs(v.pan_pos) * self.behaviour.voice_get(v.id, "max_binaural_diff")) self.composer.set_binaural_diffs(diff, v) else: self.composer.set_binaural_diffs() if self.behaviour["automate_note_duration_prop"]: min_, max_ = self.behaviour["automate_note_duration_min_max"] if self.behaviour["common_note_duration"]: prop = random_between(min_, max_, 0.3) #print "note duration proportion: ", prop [setattr(v, 'note_duration_prop', prop) for v in self.composer.voices.values()] else: for v in self.composer.voices.values(): min_, max_ = self.behaviour.voice_get(v.id, "automate_note_duration_min_max") prop = random_between(min_, max_, 0.3) v.note_duration_prop = prop if self.behaviour["automate_transpose"]: sample = self.behaviour["transposings"] new_transpose = choice(sample) self.gateway.transpose = new_transpose self.behaviour["transpose"] = new_transpose time.sleep(self.speed) if self.behaviour["automate_wavetables"]: self.set_wavetables(voices=voices) if self.has_gui: self.gui_sender.handle_caesura(self) self.gui_sender.send({'transpose': new_transpose}) self.musical_logger.info('caesura :: meter: {0}, speed: {1}, scale: {2}'.format( self.composer.meter, self.speed, self.composer.scale)) if self.behaviour['automate_microspeed_change']: self.new_microspeed_sine() self.check_incoming_messages() shuffle_delta = self.speed * self.shuffle_delay if weight == metronome.LIGHT: sleep_time = self.speed + shuffle_delta else: sleep_time = self.speed - shuffle_delta if cycle_pos == 0: if self.state.get('bar_sequence'): new_pos = (self.state['bar_sequence_current_position'] + 1) % len(self.state['bar_sequence']) self.state['bar_sequence_current_position'] = new_pos if self.behaviour['automate_microspeed_change']: microspeed_multiplier = self.microspeed_sine.get_value() else: microspeed_multiplier = 1 time.sleep(sleep_time * (1 + microspeed_multiplier * self.behaviour['microspeed_variation'])) def check_incoming_messages(self): '''checks if there are incoming messages in the queue''' if self.has_gui: if self.playing: self.gui_sender.send_cycle_pos(self) while len(self.incoming) > 0: msg = self.incoming.pop() self.handle_incoming_message(msg) def pause(self): if self.playing: self.playing = False self.gateway.pause() return True def unpause(self): if not self.playing: self.playing = True self.gateway.unpause() return True def stop(self): if self.playing: logger.info("<<<< stop playing = length: '{0}' >>>>".format( self.make_length())) self.playing = False self.stopped = True if self.has_gui: self.gui_sender.receive_exit_requested = True self.gateway.stop() self.metronome.reset() self.composer.notator.reset() time.sleep(1) def make_length(self): delta = int(time.time() - self.start_time) return "{0}:{1}".format(int(delta / 60), str(delta % 60).zfill(2)) def new_random_meter(self): new_meter = choice(self.composer.selected_meters) self.set_meter(new_meter) self.gateway.pd.send(["sys", "meter", str(new_meter).replace(",", " "). replace(" ", "_")])