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 compute_closest_scale_tones(tonality, pitch): """ Returns either the pitch if in tonality, or lower/upper pitches in scale to pitch. :param tonality: :param pitch: :return: an array with 1 element if exact match, otherwise closest lower and upper bound pitches in given tonality. """ from tonalmodel.pitch_range import PitchRange chromatic_index = pitch.chromatic_distance pitch_range = PitchRange( max(chromatic_index - 12, ChromaticScale.chromatic_start_index()), min(chromatic_index + 12, ChromaticScale.chromatic_end_index())) pitch_scale = PitchScale(tonality, pitch_range) for i in range(0, len(pitch_scale.pitch_scale)): p = pitch_scale.pitch_scale[i] if p.chromatic_distance < chromatic_index: continue if p.chromatic_distance == chromatic_index: return [p] if i == 0: raise Exception( 'unexpected logic issue in compute_closest_pitch_range {0}, {1]' .format(tonality, pitch)) return [pitch_scale.pitch_scale[i - 1], p] raise Exception( 'unexpected logic fail in compute_closest_pitch_range {0}, {1]'. format(tonality, pitch))
def __init__(self, start_index, end_index): """ Constructor Args: start_index: integer start chromatic pitch index end_index: integer end chromatic pitch index Exceptions: is start, end out of range of absolute chromatic range, plus those from Range. """ Range.__init__(self, start_index, end_index) if start_index < ChromaticScale.chromatic_start_index(): raise Exception( "Start index {0} lower than chromatic start {1}".format( start_index, ChromaticScale.chromatic_start_index())) if end_index > ChromaticScale.chromatic_end_index(): raise Exception( "end index {0} higher than chromatic end {1}".format( end_index, ChromaticScale.chromatic_end_index()))
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)
def _find_closest_pitch(self, pitch): """ Given a pitch, find the scale pitch closest to it in chromatic distance. :param pitch: :return: 1) closest in half-steps pitch in tonality. 2) chromatic distance measured from closest pitch to given pitch. Note: algorithm looks for tonal pitches with same letter as pitch first, otherwise nearest non-tonal pitch. """ start_octave = max(pitch.octave - 1, ChromaticScale.CHROMATIC_START[0]) end_octave = min(pitch.octave + 1, ChromaticScale.CHROMATIC_END[0]) # Compute the first and last pitches within the start/end octave range. To build a PitchScale first_pitch = None for t in self.tonality.annotation: first_pitch = DiatonicPitch(start_octave, t) if DiatonicFoundation.get_chromatic_distance(first_pitch) >= ChromaticScale.chromatic_start_index(): break last_pitch = None loop_finished = False for o in range(end_octave, end_octave - 2, -1): if loop_finished: break for t in reversed(self.tonality.annotation): last_pitch = DiatonicPitch(o, t) if DiatonicFoundation.get_chromatic_distance(last_pitch) <= ChromaticScale.chromatic_end_index(): loop_finished = True break scale = PitchScale(self.tonality, PitchRange.create(first_pitch, last_pitch)) # determine if pitch ltr is in tonality, get that tone ll = [t for t in self.tones if t.diatonic_letter == pitch.diatonic_tone.diatonic_letter] if len(ll) == 1: pp = DiatonicPitch(pitch.octave, ll[0]) return pp, pitch.chromatic_distance - pp.chromatic_distance # Do something if len(ll) > 1 elif len(ll) > 1: ll.sort(key=lambda x: abs(x.augmentation_offset - pitch.diatonic_tone.augmentation_offset)) pp = DiatonicPitch(pitch.octave, ll[0]) return pp, pitch.chromatic_distance - pp.chromatic_distance before_pitch = first_pitch after_pitch = last_pitch for p in scale.pitch_scale: if pitch.chromatic_distance <= p.chromatic_distance: after_pitch = p break else: before_pitch = p before_distance = pitch.chromatic_distance - before_pitch.chromatic_distance after_distance = pitch.chromatic_distance - after_pitch.chromatic_distance if pitch.diatonic_tone.diatonic_letter == before_pitch.diatonic_tone.diatonic_letter: closest_distance = before_distance closest_pitch = before_pitch elif pitch.diatonic_tone.diatonic_letter == after_pitch.diatonic_tone.diatonic_letter: closest_distance = after_distance closest_pitch = after_pitch else: if abs(before_distance) < abs(after_distance): closest_distance = before_distance closest_pitch = before_pitch else: closest_distance = after_distance closest_pitch = after_pitch return closest_pitch, closest_distance
def _build_pitch_map(self): imap = dict() n_tones = len(self.tones) if self.flip_type == FlipType.CenterTone: upper = lower = self._tone_index upper_octave = lower_octave = self._scale_origin_octave upper_tone = self.cue_tone lower_tone = self.tonal_function[self.cue_tone] imap[DiatonicPitch(lower_octave, lower_tone)] = DiatonicPitch(upper_octave, upper_tone) else: if self.flip_type == FlipType.LowerNeighborOfPair: lower = self._tone_index upper = (lower + 1) % n_tones lower_tone = self.tones[lower] upper_tone = self.tonal_function[lower_tone] # self.tones[upper] lower_octave = self._scale_origin_octave upper_octave = self._scale_origin_octave + 1 if DiatonicPitch.crosses_c(lower_tone, upper_tone, True)\ else self._scale_origin_octave else: upper = self._tone_index lower = (upper - 1) % n_tones lower_tone = self.tones[lower] upper_tone = self.tonal_function[lower_tone] # self.tones[upper] upper_octave = self._scale_origin_octave lower_octave = self._scale_origin_octave if not DiatonicPitch.crosses_c(lower_tone, upper_tone, True) \ else self._scale_origin_octave - 1 lower_pitch = DiatonicPitch(lower_octave, lower_tone) upper_pitch = DiatonicPitch(upper_octave, upper_tone) imap[lower_pitch] = upper_pitch imap[upper_pitch] = lower_pitch entered_range = False while True: lower = (lower - 1) % n_tones upper = (upper + 1) % n_tones if DiatonicPitch.crosses_c(lower_tone, self._tones[lower], False): lower_octave = lower_octave - 1 if DiatonicPitch.crosses_c(upper_tone, self._tones[upper], True): upper_octave = upper_octave + 1 lower_tone = self._tones[lower] upper_tone = self.tonal_function[lower_tone] lower_pitch = DiatonicPitch(lower_octave, lower_tone) upper_pitch = DiatonicPitch(upper_octave, upper_tone) if upper_pitch.chromatic_distance < ChromaticScale.chromatic_start_index() or \ upper_pitch.chromatic_distance > ChromaticScale.chromatic_end_index(): break if lower_pitch.chromatic_distance < ChromaticScale.chromatic_start_index() or \ lower_pitch.chromatic_distance > ChromaticScale.chromatic_end_index(): break if entered_range: if not self._domain_pitch_range.is_pitch_inbounds(lower_pitch) and \ not self._domain_pitch_range.is_pitch_inbounds(upper_pitch): break elif self._domain_pitch_range.is_pitch_inbounds(lower_pitch) or \ self._domain_pitch_range.is_pitch_inbounds(upper_pitch): entered_range = True else: continue imap[lower_pitch] = upper_pitch imap[upper_pitch] = lower_pitch return self._build_non_tonal_pitch_map(imap)
def _build_pitch_map1(self): pitch_map = dict() lo_reg = self.domain_pitch_range.start_index // 12 hi_reg = self.domain_pitch_range.end_index // 12 domain_start_tone = CrossTonalityShiftPitchFunction.compute_lowest_letter( self.domain_pitch_range.start_index % 12) # self.domain_tonality.annotation[0] range_start_tone = CrossTonalityShiftPitchFunction.compute_highest_letter( self.domain_pitch_range.end_index % 12) # self.range_tonality.annotation[0] # note letter template should start with letter for domain_start_tone note_letters = 'ABCDEFG' i = note_letters.index(domain_start_tone.diatonic_letter) note_letters = note_letters[i:] + note_letters[:i] domain_tones = list() for ltr in note_letters: for aug in ['bb', 'b', '', '#', '##']: domain_tones.append(DiatonicFoundation.get_tone(ltr + aug)) first_domain_pitch = None first_range_pitch = None last_domain_pitch = None last_range_pitch = None for register in range(lo_reg, hi_reg + 1): domain_reg = register target_pitch = self.root_shift_interval.get_end_pitch( DiatonicPitch(domain_reg, domain_start_tone)) range_reg = target_pitch.octave domain_reg_upped = False range_reg_upped = False for tone in domain_tones: if not domain_reg_upped and tone.diatonic_letter == 'C' and domain_start_tone.diatonic_letter != 'C': domain_reg = domain_reg + 1 domain_reg_upped = True domain_pitch = DiatonicPitch(domain_reg, tone) if domain_pitch.chromatic_distance < ChromaticScale.chromatic_start_index() or \ domain_pitch.chromatic_distance > ChromaticScale.chromatic_end_index(): continue if self.domain_pitch_range.is_pitch_inbounds(domain_pitch): range_tone = self.tonal_function[tone] if not range_reg_upped and range_tone.diatonic_letter == 'C' and \ range_start_tone.diatonic_letter != 'C': range_reg = range_reg + 1 range_reg_upped = True target_pitch = DiatonicPitch(range_reg, range_tone) if target_pitch.chromatic_distance < ChromaticScale.chromatic_start_index() or \ target_pitch.chromatic_distance > ChromaticScale.chromatic_end_index(): continue pitch_map[domain_pitch] = target_pitch if first_domain_pitch is None: first_domain_pitch = domain_pitch first_range_pitch = target_pitch last_domain_pitch = domain_pitch last_range_pitch = target_pitch if first_domain_pitch.chromatic_distance > last_domain_pitch.chromatic_distance: self.__domain_pitch_range = PitchRange.create( last_domain_pitch, first_domain_pitch) else: self.__domain_pitch_range = PitchRange.create( first_domain_pitch, last_domain_pitch) if first_range_pitch.chromatic_distance > last_range_pitch.chromatic_distance: self.__range_pitch_range = PitchRange.create( last_range_pitch, first_range_pitch) else: self.__range_pitch_range = PitchRange.create( first_range_pitch, last_range_pitch) return pitch_map
def create_default(tonality): return PitchScale( tonality, PitchRange(ChromaticScale.chromatic_start_index(), ChromaticScale.chromatic_end_index()))
def compute_tonal_pitch_range(tonality, pitch, lower_index, upper_index): """ Find all pitches within range of tonality based on an arbitrary pitch given as starting point. In all cases, look at the closest pitches (1 or 2) as origin 0, and the lower/upper as counting indices below or up from them. :param tonality: :param pitch: :param lower_index: :param upper_index: :return: """ import math from tonalmodel.pitch_range import PitchRange starting_points = PitchScale.compute_closest_scale_tones( tonality, pitch) # Determine the number of octaves that will cover the given range. up_chrom = max( 0, int( math.ceil(float(abs(upper_index)) / len(tonality.annotation)) * 12) * (-1 if upper_index < 0 else 1)) down_chrom = min( 0, int( math.ceil(float(abs(lower_index)) / len(tonality.annotation)) * 12) * (-1 if lower_index < 0 else 1)) # Compute all pitches within that range low = max(starting_points[0].chromatic_distance + down_chrom, ChromaticScale.chromatic_start_index()) high = min( (starting_points[0].chromatic_distance if len(starting_points) == 1 else starting_points[1].chromatic_distance) + up_chrom, ChromaticScale.chromatic_end_index()) pitch_range = PitchRange(low, high) pitch_scale = PitchScale(tonality, pitch_range).pitch_scale # The first starting point is either the enharmonic equivalent to pitch, or the lower scale pitch to the pitch. # lower_starting_index is the index in pitch_scale for that pitch. lower_starting_index = [ index for index in range(0, len(pitch_scale)) if pitch_scale[index].chromatic_distance == starting_points[0].chromatic_distance ][0] if len(starting_points) == 1: full_range = range( lower_starting_index + lower_index, min(lower_starting_index + upper_index + 1, len(pitch_scale))) return [pitch_scale[i] for i in full_range] else: upper_starting_index = [ index for index in range(0, len(pitch_scale)) if pitch_scale[index].chromatic_distance == starting_points[1].chromatic_distance ][0] lo = lower_index + (lower_starting_index if lower_index <= 0 else upper_starting_index) hi = upper_index + (lower_starting_index if upper_index < 0 else upper_starting_index) full_range = range(lo, hi + 1) return [pitch_scale[i] for i in full_range]