def __init__(self, anchor_pitch=DiatonicPitch.parse('A:0'), anchor_value=9, pitch_unit=1): """ Constructor, """ self.__anchor_pitch = anchor_pitch if not isinstance(self.anchor_pitch, DiatonicPitch): raise Exception('Anchor is not a DiatonicPitch') self.__anchor_value = anchor_value self.__pitch_unit = pitch_unit anchor_index = self.anchor_pitch.chromatic_distance base_value = anchor_value - anchor_index * pitch_unit self.value_to_pitch = OrderedMap() self.pitch_to_value = dict() for i in range(ChromaticScale.chromatic_start_index(), ChromaticScale.chromatic_end_index() + 1): pitch = DiatonicFoundation.map_to_diatonic_scale(i)[0] value = base_value + i * pitch_unit self.value_to_pitch.insert(value, pitch) self.pitch_to_value[pitch] = value PitchRangeInterpreter.__init__(self)
def _setup(self, transition_points): self.__transition_points = sorted(transition_points, key=lambda x: x[0]) self.__domain_start = self.__transition_points[0][0] self.__domain_end = self.__transition_points[ len(self.__transition_points) - 1][0] self.ordered_map = OrderedMap(self.transition_points)
def test_remove(self): ordered_list = [(10, 100), (5, 20), (7, 70), (2, 50)] om = OrderedMap(ordered_list) assert 5 in om assert om.get(5) == 20 om.remove_key(5) assert 5 not in om
def test_floor(self): ordered_list = [(10, 100), (5, 20), (7, 70), (2, 50)] om = OrderedMap(ordered_list) answers = [2, 2, 2, 5, 5, 7, 7, 7, 10, 10] for i in range(2, 12): key = om.floor(i) self.assertTrue(key == answers[i - 2]) mapto = om[key] print('find {0} is {1} --> {2}'.format(i, key, mapto))
def _reset_hc_list(self, hc_list): p = Position(0) new_ordered_map = OrderedMap() for hc in hc_list: hc.position = p new_ordered_map.insert(p, hc) p += hc.duration self.ordered_map = new_ordered_map self._wnt_duration = Duration(p.position)
def test_ceil(self): ordered_list = [(10, 100), (5, 20), (7, 70), (2, 50)] om = OrderedMap(ordered_list) answers = [2, 5, 5, 5, 7, 7, 10, 10, 10, None, None] for i in range(1, 12): key = om.ceil(i) self.assertTrue(key == answers[i - 1]) mapto = None if key is None else om[key] print('find {0} is {1} --> {2}'.format(i, key, mapto))
def test_insert(self): ordered_list = [(10, 100), (5, 20), (7, 70), (2, 50)] om = OrderedMap(ordered_list) om.insert(6, 60) keys = om.keys() last_key = -1 for k in keys: print(k, om[k]) self.assertTrue(last_key < k) last_key = k
def _setup(self, transition_points): self.__transition_points = sorted(transition_points, key=lambda x: x[0]) self.__domain_start = self.__transition_points[0][0] self.__domain_end = self.__transition_points[len(self.__transition_points) - 1][0] lin_segs = [] for i in range(0, len(self.transition_points) - 1): lin_segs.append((self.transition_points[i][0], LinearSegment(self.transition_points[i], self.transition_points[i + 1]))) self.ordered_map = OrderedMap(lin_segs)
def __init__(self, event_list=None): """ Constructor. Args: event_list: Any of None, a single Event, or a list of Events. """ self.ordered_map = OrderedMap() self._successor = {} self._predecessor = {} self.__first = None self.__last = None if event_list: self.add(event_list)
def _compute_cue_tone(old_cue_tone, tonality): from misc.ordered_map import OrderedMap tones = tonality.annotation[:-1] s = OrderedMap({v.placement: tones.index(v) for v in tones}) least_index = s[min(pl for pl in s.keys())] nearest_pl = s.floor(old_cue_tone.placement) if nearest_pl is None: return tones[-1 if least_index == 0 else least_index - 1], tones[least_index] if nearest_pl == old_cue_tone.placement: return tones[s[nearest_pl]], None nearest_idx = s[nearest_pl] return tones[nearest_idx], tones[nearest_idx + 1 if nearest_idx != len(tones) - 1 else 0]
def _compute_neighbor_tones(cue_tone, tone_list): # Find from tone_list, a tone1, tone2 with tone1<=tone<=tone2 with nearest proximity/ from misc.ordered_map import OrderedMap # s : placement -> index over all tones in tone_list s = OrderedMap({v.placement: tone_list.index(v) for v in tone_list}) # least_index == index of tone in tone_list mapped by least placement. least_index = s[min(pl for pl in s.keys())] # nearest_pl = s's placement key that is just below cue_tones placement nearest_pl = s.floor(cue_tone.placement) if nearest_pl is None: return tone_list[-1 if least_index == 0 else least_index - 1], tone_list[least_index] if nearest_pl == cue_tone.placement: return tone_list[s[nearest_pl]], None nearest_idx = s[nearest_pl] return tone_list[nearest_idx], tone_list[nearest_idx + 1 if nearest_idx != len(tone_list) - 1 else 0]
def __init__(self, tonality, anchor_pitch=None, anchor_value=None, pitch_unit=1): """ Constructor. :param tonality: The tonality being mapped to. :param anchor_pitch: A DiatonicPitch, in combo with anchor_value is a sample of the mapping. :param anchor_value: A numeric value that maps to anchor_pitch. :param pitch_unit: In the linear map of value to pitches, pitch_unit is the distance between mapping values. """ self.__tonality = tonality self.__pitch_scale = PitchScale(self.tonality, PitchRange.create('A:0', 'C:8')).pitch_scale self.anchor_pitch = self.pitch_scale[0] if anchor_pitch is None else \ DiatonicPitch.parse(anchor_pitch) if isinstance(anchor_pitch, str) else anchor_pitch anchor_index = self.pitch_scale.index(self.anchor_pitch) if anchor_index == -1: raise Exception( 'Anchor pitch \'{0}\' not found in pitch scale for tonality \'{1}\'' .format(self.anchor_pitch, self.tonality)) self.__pitch_unit = pitch_unit self.anchor_value = anchor_value if anchor_value is not None else anchor_index * self.pitch_unit # base value should map to beginning of pitch scale! # recall that pitch unit maps to each pitch, making the scalar scale linear in value! base_value = anchor_value - anchor_index * pitch_unit self.value_to_pitch = OrderedMap() self.pitch_to_value = dict() for i in range(0, len(self.pitch_scale)): pitch = self.pitch_scale[i] value = base_value + i * pitch_unit self.value_to_pitch.insert(value, pitch) self.pitch_to_value[pitch] = value PitchRangeInterpreter.__init__(self)
def _build_search_trees(self): ts_mt_list = [] ts_time_list = [] tempo_mt_list = [] tempo_time_list = [] for element in self.element_list: if element.is_tempo: tempo_mt_list.append((element.position, element.element)) tempo_time_list.append((element.position_time, element.element)) else: ts_mt_list.append((element.position, element.element)) ts_time_list.append((element.position_time, element.element)) self.ts_mt_map = OrderedMap(ts_mt_list) # whole note time --> TimeSignature self.ts_time_map = OrderedMap(ts_time_list) # actual time --> TimeSignature self.tempo_mt_map = OrderedMap(tempo_mt_list) # whole note time to Tempo self.tempo_time_map = OrderedMap(tempo_time_list) # actual time to Tempo # Build an ordered map, mapping BeatPosition --> time signature. ts_bp_list = [] (position, ts) = ts_mt_list[0] prior_pickup = 0 measure_tally = 0 if self.pickup.duration > 0: num_beats = self.pickup.duration / ts.beat_duration.duration ts_bp_list.append((BeatPosition(0, ts.beats_per_measure - num_beats), ts)) prior_pickup = num_beats else: ts_bp_list.append((BeatPosition(0, Fraction(0, 1)), ts)) for i in range(1, len(ts_mt_list)): (position, ts) = ts_mt_list[i] (prior_position, prior_ts) = ts_mt_list[i - 1] num_beats = (position - prior_position).duration / prior_ts.beat_duration.duration - prior_pickup num_measures = int(num_beats / prior_ts.beats_per_measure) + (1 if prior_pickup > 0 else 0) measure_tally += num_measures prior_pickup = 0 ts_bp_list.append((BeatPosition(measure_tally, 0), ts)) self.ts_bp_map = OrderedMap(ts_bp_list) # beat position --> TimeSignature
def class_init(): if DynamicsHelper.NAME_MAP is not None: return DynamicsHelper.NAME_MAP = { Dynamics.PPPP: 'pianissississimo', Dynamics.PPP: 'pianississimo0', Dynamics.PP: 'pianissimo', Dynamics.P: 'piano', Dynamics.MP: 'mezzo piano', Dynamics.MF: 'messo forte', Dynamics.F: 'forte', Dynamics.FF: 'fortissimo', Dynamics.FFF: 'fortississimo', Dynamics.FFFF: 'fortissississimo', } DynamicsHelper.DYNAMICS_VALUE_MAP = { Dynamics.PPPP: 16, Dynamics.PPP: 24, Dynamics.PP: 33, Dynamics.P: 49, Dynamics.MP: 64, Dynamics.MF: 80, Dynamics.F: 96, Dynamics.FF: 112, Dynamics.FFF: 120, Dynamics.FFFF: 127, } DynamicsHelper.DYNAMICS_LIST = [ Dynamics.PPPP, Dynamics.PPP, Dynamics.PP, Dynamics.P, Dynamics.MP, Dynamics.MF, Dynamics.F, Dynamics.FF, Dynamics.FFF, Dynamics.FFFF, ] DynamicsHelper.DEFAULT_DYNAMICS = Dynamics.MP DynamicsHelper.DEFAULT_DYNAMICS_VELOCITY = DynamicsHelper.DYNAMICS_VALUE_MAP[ DynamicsHelper.DEFAULT_DYNAMICS] DynamicsHelper.REVERSE_DYNAMICS_VELOCITY_MAP = OrderedMap({ value: key for (key, value) in DynamicsHelper.DYNAMICS_VALUE_MAP.items() })
def test_get(self): ordered_list = [(10, 100), (5, 20), (7, 70), (2, 50)] om = OrderedMap(ordered_list) a = om.get(5) assert a == 20 from structure.time_signature import TimeSignature from timemodel.time_signature_event import TimeSignatureEvent tse = TimeSignatureEvent(TimeSignature(3, Duration(1, 4)), Position(0)) d = OrderedMap([(tse.time, tse)]) x = d.get(0) assert x == tse
class PiecewiseLinearFunction(UnivariateFunction): """ Piecewise linear function, steps defined by a set of transition points, where values between the ordinates of adjacent transitions points, are based on a linear interpolation of the transition points' values. For example, (3, 5), (7, 10), (10, 14), (12, 2) has the following steps: (-3, 5 (3-7, 5), (7-10, 10), (10-12, 14), (12-, 2) if restrict_domain is specified (True), evaluation points must be within domain bounds. """ def __init__(self, transition_points=list(), restrict_domain=False): """ Constructor. Args: transition_points: non-empty list of ordered pairs (x, y), x is the domain, y the range. restrict_domain: boolean indicating if evaluation points must be in defined domain of transition points. default is False. """ if transition_points is None or not isinstance(transition_points, list): assert Exception('Illegal argument to SetwiseLinearFunction {0}'.format(transition_points)) self.__restrict_domain = restrict_domain self._setup(transition_points) def _setup(self, transition_points): self.__transition_points = sorted(transition_points, key=lambda x: x[0]) self.__domain_start = self.__transition_points[0][0] self.__domain_end = self.__transition_points[len(self.__transition_points) - 1][0] lin_segs = [] for i in range(0, len(self.transition_points) - 1): lin_segs.append((self.transition_points[i][0], LinearSegment(self.transition_points[i], self.transition_points[i + 1]))) self.ordered_map = OrderedMap(lin_segs) @property def transition_points(self): return self.__transition_points @property def restrict_domain(self): return self.__restrict_domain @property def domain_start(self): return self.__domain_start @property def domain_end(self): return self.__domain_end def __call__(self, x): return self.eval(x) def eval(self, x): if len(self.transition_points) == 0: raise Exception("The function is undefined due to lack of transition points.") if self.restrict_domain: if x < self.domain_start or x > self.domain_end: raise Exception('Input {0} out of range [{1}, {2}]'.format(x, self.domain_start, self.domain_end)) if x <= self.domain_start: return self.transition_points[0][1] if x >= self.domain_end: return self.transition_points[len(self.transition_points) - 1][1] key = self.ordered_map.floor(x) lin_seg = self.ordered_map.get(key) return lin_seg.eval(x) def add(self, transition_point): """ Add a transition point to the piecewise function. Args: transition_point: Pair (x, y) x, y are numerics. """ new_points = list(self.transition_points) new_points.append(transition_point) self._setup(new_points) def add_and_clear_forward(self, transition_point): """ Add a transition point to the piecewise function AND clear out higher (domain value) transition points. Args: transition_point: Pair (x, y) x, y are numerics. """ new_points = [] elimination_value = transition_point[0] for p in self.transition_points: if p[0] < elimination_value: new_points.append(p) new_points.append(transition_point) self._setup(new_points)
def __init__(self): """ Constructor. """ self.ordered_map = OrderedMap() self._wnt_duration = Duration(0)
class HarmonicContextTrack(object): def __init__(self): """ Constructor. """ self.ordered_map = OrderedMap() self._wnt_duration = Duration(0) def __getitem__(self, position): """ Get the harmonic context based on the floor of the given position. :param position: :return: """ floor_position = self.ordered_map.floor(position) return None if floor_position is None else self.ordered_map[ floor_position] def __len__(self): return len(self.ordered_map) @property def duration(self): return self._wnt_duration def hc_list(self): return [self.ordered_map[hc] for hc in self.ordered_map.keys()] def reset(self): self._reset_hc_list(self.ordered_map.value_items()) def append(self, harmonic_context): """ Append a harmonic context to the end of the track. It is recommended to use this call to build the track, as the key data members are not rebuilt. :param harmonic_context: :return: """ last_hc = None if self.ordered_map.is_empty( ) else self.ordered_map.value_items()[-1] harmonic_context.position = Position( 0) if last_hc is None else last_hc.position + last_hc.duration self.ordered_map.insert(harmonic_context.position, harmonic_context) self._wnt_duration += harmonic_context.duration.duration def append_first(self, harmonic_context): """ Append a harmonic context to the beginning of the track, and shove right all existing HC's by the duration of the added HC. :param harmonic_context: :return: """ hc_list = self.ordered_map.value_items() hc_list.insert(0, harmonic_context) self._reset_hc_list(hc_list) def insert(self, position_key, harmonic_context): """ Insert a harmonic context at a given position. The insertion happens after the HC that is floor(position_key). :param position_key: :param harmonic_context: :return: """ hc_list = self.ordered_map.value_items() hc_target_index = self.ordered_map.floor(position_key) index = 0 if hc_target_index is None else hc_list.index( self.ordered_map[hc_target_index]) hc_list.insert(index, harmonic_context) harmonic_context.position = position_key self._reset_hc_list(hc_list) def get_hc_by_position(self, position): return self.ordered_map[self.ordered_map.floor(position)] def replace(self, position_key, harmonic_context): if position_key not in self.ordered_map: raise Exception( 'Attempt to replace a harmonic context with invalid key') harmonic_context.position = position_key self.ordered_map.insert(position_key, harmonic_context) self._reset_hc_list(self.ordered_map.value_items()) def remove(self, harmonic_context): """ Remove the given harmonic content. Error if it does not exist. :param harmonic_context: :return: """ if harmonic_context.position not in self.ordered_map or \ self.ordered_map[harmonic_context.position] != harmonic_context: raise Exception( 'Attempt of remove harmonic context that is not in list') self.ordered_map.remove_key(harmonic_context.position) self._reset_hc_list(self.ordered_map.value_items()) def _reset_hc_list(self, hc_list): p = Position(0) new_ordered_map = OrderedMap() for hc in hc_list: hc.position = p new_ordered_map.insert(p, hc) p += hc.duration self.ordered_map = new_ordered_map self._wnt_duration = Duration(p.position) def clear(self): self.ordered_map = OrderedMap() self._wnt_duration = Duration(0) def __str__(self): l = self.hc_list() return '\n'.join('[{0}] {1}'.format(i, str(l[i])) for i in range(0, len(self))) def sub_hct(self, sub_track_interval=None): """ Take a sub_track of this hct. :param sub_track_interval: NmericInterval. If none, the entire hct. :return: """ sub_track_interval = NumericInterval(Fraction(0), self.duration.duration) if sub_track_interval is None else \ sub_track_interval new_track = HarmonicContextTrack() for hc in self.hc_list(): hc_interval = NumericInterval( hc.position.position, hc.position.position + hc.duration.duration) hc_intersect = hc_interval.intersection(sub_track_interval) if hc_intersect is not None: new_hc = HarmonicContext(hc.tonality, hc.chord, Duration(hc_intersect.length()), Position(hc_intersect.lower)) new_track.append(new_hc) return new_track def reverse(self): new_track = HarmonicContextTrack() offset = Position(0) for hc in reversed(self.hc_list()): hs_prime = HarmonicContext(hc.tonality, hc.chord, hc.duration, offset) new_track.append(hs_prime) offset = offset + hc.duration return new_track
def clear(self): self.ordered_map = OrderedMap() self._wnt_duration = Duration(0)
class TimeConversion(object): """ Time conversion algorithms. 1) Whole Time --> actual time 2) actual time --> Wholec Time """ def __init__(self, tempo_line, ts_line, max_position, pickup=Duration(0, 1)): """ Constructor. Args: tempo_line: (EventSequence) of TempoEvent's ts_line: (EventSequence) of TimeSignatureEvent's max_position: Position of end of whole note time pickup: whole note time for a partial initial measure Assumption: tempo_line and ts_line cover position 0 Exceptions: If pickup exceeds whole note time of the first time signature. """ self.tempo_line = tempo_line self.ts_line = ts_line self.__max_position = max_position self.__pickup = pickup if not isinstance(max_position, Position): raise Exception('max_position argument must be Position not \'{0}\'.'.format(type(max_position))) # check if the pickup exceeds the first TS if self.ts_line is None or self.ts_line.is_empty or self.tempo_line is None or self.tempo_line.is_empty: raise Exception('Time Signature and Tempo sequences must be non-empty for time conversions.') if pickup.duration >= self.ts_line.event(0).object.beats_per_measure * \ self.ts_line.event(0).object.beat_duration.duration: raise Exception( 'pickup exceeds timing based on first time signature {0}'.format(self.ts_line.event(0).object)) self._build_uniform_element_list() self._build_lines() self._build_search_trees() self.__max_time = self.position_to_actual_time(self.max_position) @property def max_position(self): return self.__max_position @property def pickup(self): return self.__pickup @property def max_time(self): return self.__max_time def _build_uniform_element_list(self): self.element_list = [Element(x.object, x.time) for x in self.tempo_line.sequence_list] + \ [Element(x.object, x.time) for x in self.ts_line.sequence_list] self.element_list.sort(key=lambda p: p.position) def _build_lines(self): """ Compute the actual time for the tempo and time signature elements. """ current_ts = None current_tempo = None current_at = 0 last_position = None for element in self.element_list: if current_ts and current_tempo: translated_tempo = current_tempo.effective_tempo(current_ts.beat_duration) current_at += (element.position - last_position).duration / \ (current_ts.beat_duration.duration * translated_tempo) * 60 * 1000 element.position_time = current_at if element.is_tempo: current_tempo = element.element else: current_ts = element.element last_position = element.position def _build_search_trees(self): ts_mt_list = [] ts_time_list = [] tempo_mt_list = [] tempo_time_list = [] for element in self.element_list: if element.is_tempo: tempo_mt_list.append((element.position, element.element)) tempo_time_list.append((element.position_time, element.element)) else: ts_mt_list.append((element.position, element.element)) ts_time_list.append((element.position_time, element.element)) self.ts_mt_map = OrderedMap(ts_mt_list) # whole note time --> TimeSignature self.ts_time_map = OrderedMap(ts_time_list) # actual time --> TimeSignature self.tempo_mt_map = OrderedMap(tempo_mt_list) # whole note time to Tempo self.tempo_time_map = OrderedMap(tempo_time_list) # actual time to Tempo # Build an ordered map, mapping BeatPosition --> time signature. ts_bp_list = [] (position, ts) = ts_mt_list[0] prior_pickup = 0 measure_tally = 0 if self.pickup.duration > 0: num_beats = self.pickup.duration / ts.beat_duration.duration ts_bp_list.append((BeatPosition(0, ts.beats_per_measure - num_beats), ts)) prior_pickup = num_beats else: ts_bp_list.append((BeatPosition(0, Fraction(0, 1)), ts)) for i in range(1, len(ts_mt_list)): (position, ts) = ts_mt_list[i] (prior_position, prior_ts) = ts_mt_list[i - 1] num_beats = (position - prior_position).duration / prior_ts.beat_duration.duration - prior_pickup num_measures = int(num_beats / prior_ts.beats_per_measure) + (1 if prior_pickup > 0 else 0) measure_tally += num_measures prior_pickup = 0 ts_bp_list.append((BeatPosition(measure_tally, 0), ts)) self.ts_bp_map = OrderedMap(ts_bp_list) # beat position --> TimeSignature def position_to_actual_time(self, position): """ Convert a whole time position to it's actual time (in ms) from the beginning. Args: position: a Position in whole time. Returns: The actual time in ms for the position relative to the beginning. Note: if the position exceeds max_position, we use max_position """ (tempo_mt_floor, tempo_element) = self.tempo_mt_map.floor_entry(position) tempo_time = self.tempo_time_map.reverse_get(tempo_element) (ts_mt_floor, ts_element) = self.ts_mt_map.floor_entry(position) ts_time = self.ts_time_map.reverse_get(ts_element) start_mt = max(tempo_mt_floor, ts_mt_floor) start_time = max(tempo_time, ts_time) # at this point, we have: # start_mt: a whole time to start from # start_time: the actual time to start from # tempo_element: the current Tempo # ts_element: the current TimeSignature delta_mt = min(position, self.max_position) - start_mt translated_tempo = tempo_element.effective_tempo(ts_element.beat_duration) # time = music_time / (beat_duration * tempo) delta_time = (delta_mt.duration / (ts_element.beat_duration.duration * translated_tempo) if delta_mt > 0 else 0) * 60 * 1000 return start_time + delta_time def actual_time_to_position(self, actual_time): """ Convert from an actual time (ms) position to a whole time Position Args: actual_time: the actual time (ms) of a position in the music Returns: the Position corresponding to the actual time. Note: if actual_time exceeds max_time, we use max_time. """ (tempo_time_floor, tempo_element) = self.tempo_time_map.floor_entry(actual_time) tempo_mt = self.tempo_mt_map.reverse_get(tempo_element) (ts_time_floor, ts_element) = self.ts_time_map.floor_entry(actual_time) ts_mt = self.ts_mt_map.reverse_get(ts_element) start_mt = max(tempo_mt, ts_mt) start_time = max(tempo_time_floor, ts_time_floor) # at this point, we have: # start_mt: a whole note time to start from # start_time: the actual time to measure from # tempo_element: the current Tempo # ts_element: the current TimeSignature delta_time = min(actual_time, self.max_time) - start_time if not isinstance(delta_time, Fraction): delta_time = Fraction.from_float(delta_time) # musicTime = time * tempo * beat_duration # Translate tempo using the time signature beat. translated_tempo = tempo_element.effective_tempo(ts_element.beat_duration) delta_mt = (delta_time * translated_tempo * ts_element.beat_duration.duration / (60 * 1000)) \ if delta_time > 0 else 0 return start_mt + delta_mt def bp_to_position(self, beat_position): """ Method to convert a beat position to a whole note time position. Args: beat_position: BeatPosition object given measure, beat number Returns: the whole note time position for beat_position. Exceptions: for improper beat_position values """ (beginning_bp, ts_element) = self.ts_bp_map.floor_entry(beat_position) if beat_position.beat_number >= ts_element.beats_per_measure: raise Exception( 'Illegal beat asked for {0}, ts has 0-{1} beats per measure.'.format(beat_position.beat_number, ts_element.beats_per_measure - 1)) ts_mt_floor = self.ts_mt_map.reverse_get(ts_element) delta_mt = ((beat_position.measure_number - beginning_bp.measure_number) * ts_element.beats_per_measure + beat_position.beat_number - beginning_bp.beat_number) * ts_element.beat_duration.duration return Position(ts_mt_floor.position + delta_mt) def position_to_bp(self, position): """ Method to convert a whole note time position to a beat position Args: position: the whole note time position Returns: the BeatPosition corresponding to the given position """ (ts_mt_floor, ts_element) = self.ts_mt_map.floor_entry(position) ts_bp = self.ts_bp_map.reverse_get(ts_element) num_beats = (position - ts_mt_floor).duration / ts_element.beat_duration.duration # - prior_pickup.duration num_measures = int(num_beats / ts_element.beats_per_measure) # + (1 if prior_pickup.duration > 0 else 0) residual_beats = num_beats - num_measures * ts_element.beats_per_measure # add the measures and beats to ts_bp beats = ts_bp.beat_number + residual_beats measures = ts_bp.measure_number + num_measures if beats >= ts_element.beats_per_measure: beats -= ts_element.beats_per_measure measures += 1 return BeatPosition(measures, beats)
class ChromaticRangeInterpreter(PitchRangeInterpreter): """ Class that interprets a number as being in the chromatic range A:0-C:8, and computes pitches that relate to that range. """ def __init__(self, anchor_pitch=DiatonicPitch.parse('A:0'), anchor_value=9, pitch_unit=1): """ Constructor, """ self.__anchor_pitch = anchor_pitch if not isinstance(self.anchor_pitch, DiatonicPitch): raise Exception('Anchor is not a DiatonicPitch') self.__anchor_value = anchor_value self.__pitch_unit = pitch_unit anchor_index = self.anchor_pitch.chromatic_distance base_value = anchor_value - anchor_index * pitch_unit self.value_to_pitch = OrderedMap() self.pitch_to_value = dict() for i in range(ChromaticScale.chromatic_start_index(), ChromaticScale.chromatic_end_index() + 1): pitch = DiatonicFoundation.map_to_diatonic_scale(i)[0] value = base_value + i * pitch_unit self.value_to_pitch.insert(value, pitch) self.pitch_to_value[pitch] = value PitchRangeInterpreter.__init__(self) @property def anchor_pitch(self): return self.__anchor_pitch @property def anchor_value(self): return self.__anchor_value @property def pitch_unit(self): return self.__pitch_unit def eval_as_nearest_pitch(self, v): candidates = self.eval_as_pitch(v) if len(candidates) == 1: return candidates[0] v1 = self.pitch_to_value[candidates[0]] v2 = self.pitch_to_value[candidates[1]] if v <= (v1 + v2) / 2: return candidates[0] return candidates[1] def value_for(self, diatonic_pitch): if isinstance(diatonic_pitch, str): diatonic_pitch = DiatonicPitch.parse(diatonic_pitch) if diatonic_pitch is None: return None if diatonic_pitch not in self.pitch_to_value: enharmonics = diatonic_pitch.enharmonics() found = False for p in enharmonics: if p in self.pitch_to_value: diatonic_pitch = p found = True break if not found: return None return self.pitch_to_value[ diatonic_pitch] if diatonic_pitch in self.pitch_to_value else None def eval_as_pitch(self, v): floor_value = self.value_to_pitch.floor(v) ceil_value = self.value_to_pitch.ceil(v) if floor_value is None or ceil_value is None: raise ChromaticRangeInterpreterException( 'Illegal chromatic pitch range paramger value {0}.'.format(v)) if math.isclose(v, self.value_for(self.value_to_pitch[floor_value])): return [self.value_to_pitch[floor_value]] else: p1 = self.value_to_pitch[floor_value] p2 = self.value_to_pitch[ceil_value] return [p1, p2] def eval_as_accurate_chromatic_distance(self, v): floor_value = self.value_to_pitch.floor(v) if floor_value is None: raise ChromaticRangeInterpreterException( 'Illegal chromatic pitch range paramger value {0}.'.format(v)) low_pitch = self.value_to_pitch[floor_value] index = low_pitch.chromatic_distance if index >= ChromaticScale.chromatic_end_index() or math.isclose( v, floor_value): return low_pitch.chromatic_distance high_pitch = DiatonicFoundation.map_to_diatonic_scale(index + 1)[0] return low_pitch.chromatic_distance + \ ((v - floor_value) / self.pitch_unit) * \ (high_pitch.chromatic_distance - low_pitch.chromatic_distance)
class EventSequence(object): """ A class to collect a sequence of Event's ordered (increasing) by the Event's time value. The class contains the following event accounting structures: 1) OrderedMap: ordering the events by time in a map that provides a floor() function. 2) successor: a dict that maps events to successors. 3) predecessor: a dict that maps events to predecessors. 4) first: first event in the event sequence. 5) last: last event in the event sequence. """ def __init__(self, event_list=None): """ Constructor. Args: event_list: Any of None, a single Event, or a list of Events. """ self.ordered_map = OrderedMap() self._successor = {} self._predecessor = {} self.__first = None self.__last = None if event_list: self.add(event_list) @property def sequence_list(self): return list(self.ordered_map.get(x) for x in self.ordered_map.keys()) @property def is_empty(self): return self.ordered_map.is_empty() def floor(self, time): return self.ordered_map.floor(time) def event(self, index): return self.ordered_map.get(index) def floor_event(self, time): floor_position = self.floor(time) return self.event(floor_position) if floor_position else None @property def first(self): return self.__first @property def last(self): return self.__last def add(self, new_members): """ Add any of a single Event or a list of Events. Args: new_members: Any of a single Event or a list of events """ if isinstance(new_members, list): mem_set = new_members inputt = [(e.time, e) for e in new_members] else: mem_set = [new_members] inputt = [(new_members.time, new_members)] for m in mem_set: if self.ordered_map.has_reverse(m): raise Exception('{0} already a member of sequence.'.format(m)) if not isinstance(m, Event): raise Exception('{0} is not an event.'.format(m)) for i in inputt: if i[1].time not in self.ordered_map: self._add_successor_predecessor_maps(i[1]) else: self._update_successor_predecessor_maps(i[1]) self.ordered_map.insert(i[0], i[1]) def remove(self, members): """ Remove any of a single Event or a list of Events already in the sequence. Args: members: Any of a single Event or a list of Events already in the sequence. """ if isinstance(members, list): for member in members: self.remove(member) else: if not self.ordered_map.has_reverse(members): raise Exception('{0} not a member of sequence'.format(members)) self._remove_successor_predecessor_maps(members) self.ordered_map.remove_key(self.ordered_map.reverse_get(members)) def move_event(self, event, new_time): """ Method to move event in sequence to a new time. Args: event: (Event) to move new_time: the new time setting for the event """ if self.event(event.time) != event: raise Exception('Given event at time {0} not in sequence'.format(event.time)) self.remove(event) event.time = new_time self.add(event) def _add_successor_predecessor_maps(self, event): fl_key = self.floor(event.time) if fl_key: a = self.event(fl_key) b = self._successor[a] # could be None event is between a and b self._successor[a] = event self._successor[event] = b self._predecessor[event] = a if b: self._predecessor[b] = event else: self.__last = event else: # this event has to come first if self.__first: self._successor[event] = self.__first self._predecessor[self.__first] = event self._predecessor[event] = None self.__first = event else: self.__first = self.__last = event self._successor[event] = None self._predecessor[event] = None def _update_successor_predecessor_maps(self, event): e = self.event(event.time) self.remove(e) self._add_successor_predecessor_maps(event) pass def _remove_successor_predecessor_maps(self, event): a = self._predecessor[event] b = self._successor[event] del self._successor[event] del self._predecessor[event] if a: self._successor[a] = b else: self.__first = b if b: self._predecessor[b] = a else: self.__last = a def clear(self): self.ordered_map.clear() self._successor.clear() self._predecessor.clear() def successor(self, event): return self._successor[event] if event in self._successor else None def predecessor(self, event): return self._predecessor[event] if event in self._predecessor else None def __str__(self): return ', '.join(str(x) for x in self.sequence_list) def print_maps(self): print('---------') if self.__first: print('first={0}'.format(self.__first)) else: print('first=None') if self.__first: print('last={0}'.format(self.__last)) else: print('last=None') print('Successor:') for i in self._successor.items(): print(' {0} --> {1}'.format(i[0].object if i[0] else 'None', i[1].object if i[1] else 'None')) print('Predecessor:') for i in self._predecessor.items(): print(' {0} --> {1}'.format(i[0].object if i[0] else 'None', i[1].object if i[1] else 'None'))
def test_order(self): ordered_list = [(10, 100), (5, 20), (7, 70), (2, 50)] om = OrderedMap(ordered_list) keys = om.keys() last_key = -1 for k in keys: print(k, om[k]) self.assertTrue(last_key < k) last_key = k ordered_list = [(Fraction(2, 3), 200), (Fraction(1, 10), 10), (Fraction(1, 2), 50), (Fraction(1, 8), 20)] om = OrderedMap(ordered_list) keys = om.keys() last_key = -1 for k in keys: print(k, om[k]) self.assertTrue(last_key < k) last_key = k ordered_list = [(Position(2, 3), 200), (Position(1, 10), 10), (Position(1, 2), 50), (Position(1, 8), 20)] om = OrderedMap(ordered_list) keys = om.keys() last_key = -Fraction(1, 1) for k in keys: print(k, om[k]) self.assertTrue(last_key < k.position) last_key = k.position ordered_list = [(Duration(2, 3), 200), (Duration(1, 10), 10), (Duration(1, 2), 50), (Duration(1, 8), 20)] om = OrderedMap(ordered_list) keys = om.keys() last_key = -Fraction(1, 1) for k in keys: print(k, om[k]) self.assertTrue(last_key < k.duration) last_key = k.duration
class ScalarRangeInterpreter(PitchRangeInterpreter): """ ScalarRangeInterpreter maps numeric values to pitches, exposed as a PitchRangeInterpreter. The mapping is linear in v. """ def __init__(self, tonality, anchor_pitch=None, anchor_value=None, pitch_unit=1): """ Constructor. :param tonality: The tonality being mapped to. :param anchor_pitch: A DiatonicPitch, in combo with anchor_value is a sample of the mapping. :param anchor_value: A numeric value that maps to anchor_pitch. :param pitch_unit: In the linear map of value to pitches, pitch_unit is the distance between mapping values. """ self.__tonality = tonality self.__pitch_scale = PitchScale(self.tonality, PitchRange.create('A:0', 'C:8')).pitch_scale self.anchor_pitch = self.pitch_scale[0] if anchor_pitch is None else \ DiatonicPitch.parse(anchor_pitch) if isinstance(anchor_pitch, str) else anchor_pitch anchor_index = self.pitch_scale.index(self.anchor_pitch) if anchor_index == -1: raise Exception( 'Anchor pitch \'{0}\' not found in pitch scale for tonality \'{1}\'' .format(self.anchor_pitch, self.tonality)) self.__pitch_unit = pitch_unit self.anchor_value = anchor_value if anchor_value is not None else anchor_index * self.pitch_unit # base value should map to beginning of pitch scale! # recall that pitch unit maps to each pitch, making the scalar scale linear in value! base_value = anchor_value - anchor_index * pitch_unit self.value_to_pitch = OrderedMap() self.pitch_to_value = dict() for i in range(0, len(self.pitch_scale)): pitch = self.pitch_scale[i] value = base_value + i * pitch_unit self.value_to_pitch.insert(value, pitch) self.pitch_to_value[pitch] = value PitchRangeInterpreter.__init__(self) @property def tonality(self): return self.__tonality @property def pitch_scale(self): return self.__pitch_scale @property def pitch_unit(self): return self.__pitch_unit def eval_as_nearest_pitch(self, v): candidates = self.eval_as_pitch(v) if len(candidates) == 1: return candidates[0] v1 = self.pitch_to_value[candidates[0]] v2 = self.pitch_to_value[candidates[1]] if v <= (v1 + v2) / 2: return candidates[0] return candidates[1] def value_for(self, diatonic_pitch): if isinstance(diatonic_pitch, str): diatonic_pitch = DiatonicPitch.parse(diatonic_pitch) if diatonic_pitch is None: return None return self.pitch_to_value[ diatonic_pitch] if diatonic_pitch in self.pitch_to_value else None def eval_as_pitch(self, v): floor_value = self.value_to_pitch.floor(v) low_pitch = self.value_to_pitch[floor_value] index = self.pitch_scale.index(low_pitch) if index >= len(self.pitch_scale) - 1 or math.isclose(v, floor_value): return [low_pitch] return [low_pitch, self.pitch_scale[index + 1]] def eval_as_accurate_chromatic_distance(self, v): floor_value = self.value_to_pitch.floor(v) low_pitch = self.value_to_pitch[floor_value] index = self.pitch_scale.index(low_pitch) if index >= len(self.pitch_scale) - 1 or math.isclose(v, floor_value): return low_pitch.chromatic_distance high_pitch = self.pitch_scale[index + 1] return low_pitch.chromatic_distance + \ ((v - floor_value) / (self.pitch_unit)) * \ (high_pitch.chromatic_distance - low_pitch.chromatic_distance)
def test_merge(self): ordered_list = [(10, 100), (5, 20), (7, 70), (2, 50)] om = OrderedMap(ordered_list) sup = [(3, 60), (15, 2)] om.merge(sup) self.assertTrue(3 in om.keys()) self.assertTrue(om.get(3) == 60) self.assertTrue(15 in om.keys()) self.assertTrue(om.get(15) == 2) self.assertTrue(5 in om.keys()) sup = {3: 60, 15: 2} om.merge(sup) self.assertTrue(3 in om.keys()) self.assertTrue(om.get(3) == 60) self.assertTrue(15 in om.keys()) self.assertTrue(om.get(15) == 2) self.assertTrue(5 in om.keys()) sup = OrderedMap({3: 60, 15: 2}) om.merge(sup) self.assertTrue(3 in om.keys()) self.assertTrue(om.get(3) == 60) self.assertTrue(15 in om.keys()) self.assertTrue(om.get(15) == 2) self.assertTrue(5 in om.keys())
class StepwiseFunction(UnivariateFunction): """ Stepwise function, steps defined by a set of transition points For example, (5, 1), (7, 3), (10, 6), (12, 8) has the following linear segments: (5, 1) to (7, 3) (7, 3) to (10, 6) (10, 6) to 12, 8) if restrict_domain is specified (True), evaluation points must be within domain bounds. """ def __init__(self, transition_points=None, restrict_domain=False): """ Constructor. Args: transition_points: non-empty list of ordered pairs (x, y) restrict_domain: boolean indicating if evaluation points must be in defined domain of transition points. default is False. """ if transition_points is None: transition_points = list() if transition_points is None or not isinstance(transition_points, list): assert Exception( 'Illegal argument to SetwiseLinearFunction {0}'.format( transition_points)) self.__restrict_domain = restrict_domain self._setup(transition_points) def _setup(self, transition_points): self.__transition_points = sorted(transition_points, key=lambda x: x[0]) self.__domain_start = self.__transition_points[0][0] self.__domain_end = self.__transition_points[ len(self.__transition_points) - 1][0] self.ordered_map = OrderedMap(self.transition_points) @property def transition_points(self): return self.__transition_points @property def domain_start(self): return self.__domain_start @property def domain_end(self): return self.__domain_end @property def restrict_domain(self): return self.__restrict_domain def eval(self, x): if len(self.transition_points) == 0: raise Exception( "The function is undefined due to lack of transition points.") if self.restrict_domain: if x < self.domain_start or x > self.domain_end: raise Exception('Input {0} out of range [{1}, {2}]'.format( x, self.domain_start, self.domain_end)) key = self.ordered_map.floor(x) if key is None: return self.ordered_map.get(self.domain_start) if key == self.domain_end: return self.ordered_map.get(self.domain_end) else: return self.ordered_map.get(key) def add(self, transition_point): """ Add a transition point to the stepwise function. Args: transition_point: Pair (x, y) x, y are numerics. """ new_points = list(self.transition_points) new_points.append(transition_point) self._setup(new_points) def add_and_clear_forward(self, transition_point): """ Add a transition point to the stepwise function AND clear out higher (domain value) transition points. Args: transition_point: Pair (x, y) x, y are numerics. """ new_points = [] elimination_value = transition_point[0] for p in self.transition_points: if p[0] < elimination_value: new_points.append(p) new_points.append(transition_point) self._setup(new_points)