def test_book_example(self): location = ChromaticScale.parse_notation("4:9") print(location) index = ChromaticScale.location_to_index(location) print(index) loc = ChromaticScale.index_to_location(index) print(loc)
def values(self, p_map, v_note): if v_note != self.actor_note: raise Exception('v_note {0} not in ScalarConstraint actors.'.format(v_note.note)) policy_context = p_map[self.actor_note].policy_context tones = list(policy_context.harmonic_context.tonality.annotation) tones = tones[:-1] # remove final note (same as first) if len(self.scalar_roles) != 0: tones = [tones[i] for i in self.scalar_roles] if p_map[v_note].note is not None: tone = p_map[v_note].note.diatonic_pitch.diatonic_tone return OrderedSet([self.actor_note]) if tone in tones else None pitch_range = policy_context.pitch_range start_partition = max(ChromaticScale.index_to_location(pitch_range.start_index)[0] - 1, 0) end_partition = min(ChromaticScale.index_to_location(pitch_range.end_index)[0] + 1, ChromaticScale.CHROMATIC_END[0]) valid_set = OrderedSet() for tone in tones: for j in range(start_partition, end_partition + 1): pitch = DiatonicPitch(j, tone) if pitch_range.is_pitch_inbounds(pitch): note = Note(pitch, self.actor_note.base_duration, self.actor_note.num_dots) valid_set.add(note) return valid_set
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 values(self, p_map, v_note): if v_note != self.actor_note: raise Exception("Illegal v_note {0} for constraints".format(v_note)) if p_map[v_note].note is not None: if p_map[v_note].note.diatonic_pitch.diatonic_tone == self.tone: return {p_map[v_note].note} raise Exception('Fixed Tone Policy Violated has {0} should be {1}'.format( p_map[v_note].note.diatonic_pitch.diatonic_tone, self.tone)) contextual_note = p_map[self.actor_note] policy_context = contextual_note.policy_context pitch_range = contextual_note.policy_context.pitch_range start_partition = max(ChromaticScale.index_to_location(pitch_range.start_index)[0] - 1, 0) end_partition = min(ChromaticScale.index_to_location(pitch_range.end_index)[0] + 1, ChromaticScale.CHROMATIC_END[0]) # Try to find that tone in target's tonality/scale. tone = self.tone for t_str in self.tone.enharmonics(): t = DiatonicToneCache.get_tone(t_str) for scale_tone in PitchScale(policy_context.harmonic_context.tonality, policy_context.pitch_range).tone_scale: if scale_tone == t: tone = t break valid_set = OrderedSet() for i in range(start_partition, end_partition + 1): pitch = DiatonicPitch(i, tone) if pitch_range.is_pitch_inbounds(pitch): note = Note(pitch, self.actor_note.base_duration, self.actor_note.num_dots) valid_set.add(note) return valid_set
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 test_basic_plf(self): array = [(0, 'A:0'), (Fraction(1, 2), 'C:5'), (Position(3, 4), 'G:4'), (1, 'A:5')] f = PiecewiseLinearPitchFunction(array) assert DiatonicPitch.parse('A:0').chromatic_distance == f.eval(0) assert DiatonicPitch.parse('C:5').chromatic_distance == f.eval(0.5) assert DiatonicPitch.parse('G:4').chromatic_distance == f.eval( Fraction(3, 4)) assert DiatonicPitch.parse('A:5').chromatic_distance == f.eval( Position(1)) assert DiatonicPitch.parse( 'A:0').chromatic_distance == f.eval_as_chromatic_distance(0) assert DiatonicPitch.parse( 'C:5').chromatic_distance == f.eval_as_chromatic_distance(0.5) assert DiatonicPitch.parse( 'G:4').chromatic_distance == f.eval_as_chromatic_distance( Fraction(3, 4)) assert DiatonicPitch.parse( 'A:5').chromatic_distance == f.eval_as_chromatic_distance( Position(1)) print(f.eval_as_frequency(0)) assert ChromaticScale.A0 == f.eval_as_frequency(0) print( ChromaticScale.index_to_location( DiatonicPitch.parse('C:5').chromatic_distance)) print( ChromaticScale.get_frequency( ChromaticScale.index_to_location( DiatonicPitch.parse('C:5').chromatic_distance))) print(f.eval_as_frequency(0.5)) assert math.isclose( ChromaticScale.get_frequency( ChromaticScale.index_to_location( DiatonicPitch.parse('C:5').chromatic_distance)), f.eval_as_frequency(0.5)) print(f.eval(0.25)) # 34.5 print(f.eval_as_nearest_pitch(0.25)) assert math.isclose( ChromaticScale.A0 * math.pow(ChromaticScale.SEMITONE_RATIO, f.eval(0.25) - 9), f.eval_as_frequency(0.25)) assert 'A#:2' == str(f.eval_as_nearest_pitch(0.25)) pitches = f.eval_as_pitch(0.25) assert 'A#:2' == str(pitches[0]) assert 'B:2' == str(pitches[1])
def test_scale(self): scale = ChromaticScale.get_chromatic_scale( ChromaticScale.parse_notation("0:9"), ChromaticScale.parse_notation("8:0")) start = ChromaticScale.location_to_index((0, 9)) end = ChromaticScale.location_to_index((8, 0)) + 1 for i in range(start, end): logging.info('{0}{1} {1}'.format( i, ChromaticScale.index_to_location(i), scale[i - start])) assert is_close(scale[ChromaticScale.location_to_index((4, 9)) - start], 440.0), \ "Error A:4 = {0} should be 440.0".format(scale[ChromaticScale.location_to_index((4, 9)) - start]) assert is_close(scale[ChromaticScale.location_to_index((4, 0)) - start], 261.625565301), \ "Error C:4 = {0} should be 261.625565301".format(scale[ChromaticScale.location_to_index((4, 0)) - start])
def __compute_pitch_scale(self): (tone_index, pitch_index) = self.__find_lowest_tone( ) # Determine the lowest tone in the range if tone_index == -1: return [] scale = [ DiatonicPitch( ChromaticScale.index_to_location(pitch_index)[0], self.tone_scale[tone_index].diatonic_symbol) ] # Given the first pitch, sync up with the incremental intervals on the tonality, and move forward, computing # each scale pitch until we are out of range. # Note: be sure to skip the first incremental interval which should be P:1 prior_pitch = scale[0] while True: tone_index += 1 if tone_index > len(self.tone_scale) - 1: tone_index = 1 # skip 0 as that should be P:1 incremental_interval = self.tonality.modality.incremental_intervals[ tone_index] current_pitch = incremental_interval.get_end_pitch(prior_pitch) if current_pitch.chromatic_distance > self.pitch_range.end_index: break scale.append(current_pitch) prior_pitch = current_pitch return scale
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 find_lowest_placement_in_range(self, placement): """ For a given chromatic placement (0, ..., 11) find the lowest chromatic index in the range for it. """ if placement < 0 or placement >= 12: raise Exception( 'Illegal placement value {0} must be between 0 and 11'.format( placement)) start_partition = ChromaticScale.index_to_location(self.start_index)[0] end_partition = ChromaticScale.index_to_location(self.end_index)[0] lowest_index = -1 for partition in range(start_partition, end_partition + 1): if self.is_location_inbounds((partition, placement)): lowest_index = ChromaticScale.location_to_index( (partition, placement)) break return lowest_index
def is_location_inbounds(self, location): """ Determines if given chromatic location is in bounds of range. Args: location: chromatic location Returns: boolean indicating if in bounds. """ return self.is_inbounds(ChromaticScale.location_to_index(location))
def values(self, p_map, v_note): """ Return a set of possible pitches that v_note can take that must be in p_map target's chord. :param p_map: :param v_note: :return: """ if v_note != self.actor_note: raise Exception( 'v_note {0} not in ChordalToneConstraint actors.'.format( v_note.note)) policy_context = p_map[self.actor_note].policy_context tones = policy_context.harmonic_context.chord.tones if p_map[v_note].note is not None: note = p_map[v_note].note if note.diatonic_pitch.diatonic_tone in tones: return {note} raise Exception( 'Chordal Pitch Policy Violated has {0} should be member of chord {1}' .format(p_map[v_note].note.diatonic_pitch, policy_context.harmonic_context.chord)) pitch_range = policy_context.pitch_range start_partition = max( ChromaticScale.index_to_location(pitch_range.start_index)[0] - 1, 0) end_partition = min( ChromaticScale.index_to_location(pitch_range.end_index)[0] + 1, ChromaticScale.CHROMATIC_END[0]) valid_set = OrderedSet() for tone in tones: for i in range(start_partition, end_partition + 1): pitch = DiatonicPitch(i, tone[0]) if pitch_range.is_pitch_inbounds(str(pitch)): note = Note(pitch, self.actor_note.base_duration, self.actor_note.num_dots) valid_set.add(note) return valid_set
def _build_pitch_map(self): ltrs = 'CDEFGAB' index = ltrs.index(self.domain_tonality.diatonic_tone.diatonic_letter) key_ltrs = list(ltrs[index:] + ltrs[:index]) c_index = key_ltrs.index('C') low_octave = self.cue_pitch.octave if key_ltrs.index(self.cue_pitch.diatonic_tone.diatonic_letter) < c_index \ else self.cue_pitch.octave - 1 high_octave = low_octave + 1 range_ltrs = 'CBAGFED' index = range_ltrs.index(self.range_tonality.diatonic_tone.diatonic_letter) range_key_ltrs = list(range_ltrs[index:] + range_ltrs[:index]) range_c_index = range_key_ltrs.index('C') range_low_octave = self.cue_pitch.octave if range_key_ltrs.index(self.cue_pitch.diatonic_tone.diatonic_letter) \ > range_c_index else self.cue_pitch.octave - 1 range_high_octave = range_low_octave + 1 imap = dict() for tone in self.tonal_function.domain: domain_octave = low_octave if key_ltrs.index(tone.diatonic_letter) < c_index else high_octave value = self.tonal_function[tone] range_octave = range_high_octave if range_key_ltrs.index(value.diatonic_letter) <= range_c_index \ else range_low_octave imap[DiatonicPitch(domain_octave, tone)] = DiatonicPitch(range_octave, value) full_map = dict() start = ChromaticScale.index_to_location(self.domain_pitch_range.start_index)[0] end = ChromaticScale.index_to_location(self.domain_pitch_range.end_index)[0] + 1 for octave in range(start, end): if octave < ChromaticScale.CHROMATIC_START[0] or octave > ChromaticScale.CHROMATIC_END[0]: continue octave_delta = self.cue_pitch.octave - octave for pitch in imap.keys(): rrange = imap[pitch] new_pitch = DiatonicPitch(pitch.octave - octave_delta, pitch.diatonic_tone) if self.domain_pitch_range.is_pitch_inbounds(new_pitch): full_map[new_pitch] = DiatonicPitch(rrange.octave + octave_delta, rrange.diatonic_tone) return full_map
def test_lowest_placement(self): pr = PitchRange.create('G:3', 'G:5') answers = [4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3] for i in range(0, 12): lowest = pr.find_lowest_placement_in_range(i) partition = ChromaticScale.index_to_location(lowest)[0] self.assertTrue(partition == answers[i], 'Assert failure {0} != {1}'.format(partition, answers[i])) print(i, partition) with self.assertRaises(Exception): pr.find_lowest_placement_in_range(-1) with self.assertRaises(Exception): pr.find_lowest_placement_in_range(12)
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 map_to_diatonic_scale(chromatic_index): """ Convert a chromatic index (int) to a diatonic pitch in string format. Args: chromatic_index: the chromatic index of the pitch (int) Return: all enharmonic diatonic pitches """ from tonalmodel.diatonic_pitch import DiatonicPitch location = ChromaticScale.index_to_location(chromatic_index) enharmonics = DiatonicTone.DIATONIC_OFFSET_ENHARMONIC_MAPPING[ location[1]] octave_adjustments = DiatonicFoundation.ENHARMONIC_OCTAVE_ADJUSTMENT_MAPPING[ location[1]] answers = [] for i in range(0, len(enharmonics)): enharmonic = enharmonics[i] answers.append( DiatonicPitch.parse(enharmonic + ':' + str(location[0] + octave_adjustments[i]))) return answers
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 test_location_to_index(self): for i in range(1, 4): for j in range(0, 12): index = ChromaticScale.location_to_index((i, j)) assert index == 12 * i + j
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]
def __init__(self, tonal_function, scale_origins, domain_pitch_range, reversal=False): """ Constructor. :param tonal_function: TonalFunction upon which this function is based. :param scale_origins: Duplet of origin pitches for the domain and range respectively. :param domain_pitch_range: Domain pitch range (PitchRange) :param reversal: If during construction, domain and range octaves follow together forward, or if the range follow backwards. """ self.domain_tonality = tonal_function.domain_tonality self.range_tonality = tonal_function.range_tonality if domain_pitch_range is None or not isinstance(domain_pitch_range, PitchRange): raise Exception('domain pitch range cannot be None and must be PitchRange') dlex, dcom = TonalityPitchFunction._build_lex_dcom_maps(self.domain_tonality, scale_origins[0]) rlex, rcom = TonalityPitchFunction._build_lex_dcom_maps(self.range_tonality, scale_origins[1]) # Needed for getitem() self._dlex = dlex # These define how the mapping starts on the domain, in terms of octaves octaves # d_reg: The octave for which the domain tonality is centered. # d_min: The minimal octave in the octave coverage of the domain. # d_max: The maximal octave in the octave coverage of the domain. d_reg = DiatonicPitch.parse(scale_origins[0]).octave d_min = max(ChromaticScale.CHROMATIC_START[0], ChromaticScale.index_to_location(domain_pitch_range.start_index)[0]) - 1 d_max = min(ChromaticScale.CHROMATIC_END[0], ChromaticScale.index_to_location(domain_pitch_range.start_index)[1]) + 1 # Some aspects of building pitch_map. # Goal: pitch_map: (key from largest range of (tone, octave) in domain) --> (range tone, octave) # The following help: # d_lex: maps enharmonic representations of domain tone to the domain tones. # the range is called the normalized range. # d_com: for domain pitch scale centered at scale_origin[0], maps normalized tone to octave. # r_lex and r_com are similar but for the range. # # The pitch map is based on the following dynamics: # For d_key in pitch domain, d_key --(d_lex)-->d_n_key--(d_com)-->d_octave # d_key --(tonal_function)--> r_key # r_key --(r_lex)-->r_n_key --(r_com)--> r_octave # then: # pitch_map(DP(d_octave, d_key) = DP(r_octave, r_key) # with adjustments to d_octave, r_octave based on sliding over the octave ranges. pitch_map = OrderedDict() for d_octave in range(d_min, d_max + 1): for d_tone in tonal_function.map.keys(): source_pitch = DiatonicPitch(d_octave + (dcom[dlex[d_tone]] - d_reg), d_tone) r_tone = tonal_function[d_tone] if r_tone is not None: target_pitch = DiatonicPitch(rcom[rlex[r_tone]] - (-1 if reversal else 1) * (d_reg - d_octave), r_tone) if domain_pitch_range.is_pitch_inbounds(source_pitch) and \ TonalityPitchFunction.FULL_PITCH_RANGE.is_pitch_inbounds(target_pitch): pitch_map[source_pitch] = target_pitch else: pitch_map[source_pitch] = None GeneralPitchFunction.__init__(self, pitch_map)
def test_parse_chromatic_location(self): for i in range(0, 12): s = str(4) + ':' + str(i) location = ChromaticScale.parse_notation(s) assert location[0] == 4 and location[1] == i
def test_frequencies(self): assert is_close(ChromaticScale.get_frequency((4, 9)), 440.0), \ "Error A:4 = {0} should be 440.0".format(ChromaticScale.get_frequency((4, 9))) assert is_close(ChromaticScale.get_frequency((4, 0)), 261.625565301), \ "Error C:4 = {0} should be 261.625565301".format(ChromaticScale.get_frequency((4, 0)))
def test_index_to_location(self): for i in range(12, 47): location = ChromaticScale.index_to_location(i) logging.info(location) assert location[0] == i // 12 and location[1] == i % 12