def test_csound_note_to_str(start, duration, amplitude, pitch): func_table = 100 # Add multiple aliased property names for note attributes attr_name_idx_map = { 'instrument': 0, 'start': 1, 'duration': 2, 'amplitude': 3, 'pitch': 4, 'func_table': 5 } # Test using a custom cast function for an attribute, a custom attribute attr_val_cast_map = {'func_table': int} # Test assigning default values to each note created in the underlying NoteSequence attr_val_default_map = { 'instrument': float(INSTRUMENT), 'start': start, 'duration': duration, 'amplitude': amplitude, 'pitch': pitch, 'func_table': float(func_table), } mn = MakeNoteConfig(cls_name=csound_note.CLASS_NAME, num_attributes=len(attr_val_default_map), make_note=csound_note.make_note, pitch_for_key=csound_note.pitch_for_key, attr_name_idx_map=attr_name_idx_map, attr_val_default_map=attr_val_default_map, attr_val_cast_map=attr_val_cast_map) note = _note(mn) # Have to manually add the string formatter for additional custom note attributes note.set_attr_str_formatter('func_table', lambda x: str(x)) assert f'i {INSTRUMENT} {start:.5f} {duration:.5f} {round(amplitude, 2)} {round(pitch, 2)} {func_table}' == \ str(note)
def make_note_config(): # Don't pass attr_vals_default_map to Sequencer, because it constructs all its notes from patterns return MakeNoteConfig(cls_name=csound_note.CLASS_NAME, num_attributes=NUM_ATTRIBUTES, make_note=csound_note.make_note, pitch_for_key=csound_note.pitch_for_key, attr_name_idx_map=ATTR_NAME_IDX_MAP, attr_val_cast_map=ATTR_VAL_CAST_MAP)
def make_note_config(): return MakeNoteConfig(cls_name=csound_note.CLASS_NAME, num_attributes=NUM_ATTRIBUTES, make_note=csound_note.make_note, pitch_for_key=csound_note.pitch_for_key, attr_name_idx_map=ATTR_NAME_IDX_MAP, attr_val_default_map=ATTR_VAL_DEFAULT_MAP, attr_val_cast_map={})
def new_note(mn: MakeNoteConfig = None) -> Any: """Factory method to construct a single note with underlying storage so it can be appended to another NoteSequence like a Measure. Returns a NoteSequence of length 1 and a reference to the Note in that sequence, so that there is reference to the underlying NoteSequence with the storage to the note in the calling scope. If we didn't do that the Note reference would be invalid.""" seq = NoteSequence(num_notes=1, mn=MakeNoteConfig.copy(mn)) return seq.note(0)
def test_csound_note_attrs_fluent(start, duration, amplitude, pitch): # Add an additional non-core dynamically added attribute to verify correct ordering of attrs and str() func_table = 100 # Add multiple aliased property names for note attributes attr_name_idx_map = { 'i': 0, 'instrument': 0, 's': 1, 'start': 1, 'd': 2, 'dur': 2, 'duration': 2, 'a': 3, 'amp': 3, 'amplitude': 3, 'p': 4, 'pitch': 4, 'func_table': 5 } # Test using a custom cast function for an attribute, a custom attribute attr_val_cast_map = {'func_table': int} # Set the note value to not equal the values passed in to the test attr_val_default_map = { 'instrument': float(INSTRUMENT + 1), 'start': 0.0, 'duration': 0.0, 'amplitude': 0.0, 'pitch': 0.0, 'func_table': float(func_table), } # Don't pass in attr_val_default_map, so not creating a Note with the values passed in to each test mn = MakeNoteConfig(cls_name=csound_note.CLASS_NAME, num_attributes=len(attr_val_default_map), make_note=csound_note.make_note, pitch_for_key=csound_note.pitch_for_key, attr_name_idx_map=attr_name_idx_map, attr_val_default_map=attr_val_default_map, attr_val_cast_map=attr_val_cast_map) note = _note(mn) # Assert the note does not have the expected attr values assert note.start == note.s != start assert note.duration == note.dur == note.d != duration assert note.amplitude == note.amp == note.a != amplitude assert note.pitch == note.p != pitch # Then use the fluent accessors with chained syntax to assign the values passed in to this test note.I(INSTRUMENT).S(start).D(duration).A(amplitude).P(pitch) # Assert the note now has the expected attr values assert note.start == note.s == start assert note.duration == note.dur == note.d == duration assert note.amplitude == note.amp == note.a == amplitude assert note.pitch == note.p == pitch
def test_csound_note_attrs(start, duration, amplitude, pitch): # Add an additional non-core dynamically added attribute to verify correct ordering of attrs and str() func_table = 100 # Add multiple aliased property names for note attributes attr_name_idx_map = { 'i': 0, 'instrument': 0, 's': 1, 'start': 1, 'd': 2, 'duration': 2, 'a': 3, 'amplitude': 3, 'p': 4, 'pitch': 4, 'func_table': 5 } # Test using a custom cast function for an attribute, a custom attribute attr_val_cast_map = {'func_table': int} # Test assigning default values to each note created in the underlying NoteSequence attr_val_default_map = { 'instrument': float(INSTRUMENT), 'start': start, 'duration': duration, 'amplitude': amplitude, 'pitch': pitch, 'func_table': float(func_table), } mn = MakeNoteConfig(cls_name=csound_note.CLASS_NAME, num_attributes=len(attr_val_default_map), make_note=csound_note.make_note, pitch_for_key=csound_note.pitch_for_key, attr_name_idx_map=attr_name_idx_map, attr_val_default_map=attr_val_default_map, attr_val_cast_map=attr_val_cast_map) note = _note(mn=mn) assert note.instrument == note.i == int(INSTRUMENT) assert type(note.instrument) == int assert note.start == note.s == start assert note.duration == note.d == duration assert note.amplitude == note.a == amplitude assert note.pitch == note.p == pitch # Assert that non-core dynamically added attribute (which in real use would only be added by a Generator # and never directly by an end user) has the expected data type assert note.func_table == func_table assert type(note.func_table) == type(func_table) == int
def copy(source: 'Measure') -> 'Measure': new_measure = Measure(meter=source.meter, swing=source.swing, num_notes=source.num_notes, mn=MakeNoteConfig.copy(source.mn), performance_attrs=source.performance_attrs) # Copy the underlying np array from source note before constructing a Measure (and parent class NoteSequence) # from the source. This is because both of those __init__()s construct new storage and notes from the # measure's MakeNoteConfig. If that has attr_vals_default_map set it will use that to construct the notes. # But we want copy ctor semantics, not ctor semantics. So we have to repeat the same logic as is found # in NoteSequence.copy() and copy the underlying note storage from source to target. new_measure.note_attr_vals = np_copy(source.note_attr_vals) new_measure.beat = source.beat new_measure.next_note_start = source.next_note_start return new_measure
def __init__(self, name: Optional[str] = None, num_measures: int = None, meter: Optional[Meter] = None, swing: Optional[Swing] = None, player: Optional[Union[CSoundCSDPlayer, CSoundInteractivePlayer]] = None, mn: MakeNoteConfig = None): if not mn: mn = MakeNoteConfig(cls_name=CLASS_NAME, num_attributes=NUM_ATTRIBUTES, make_note=make_note, pitch_for_key=pitch_for_key, attr_name_idx_map=ATTR_NAME_IDX_MAP, attr_val_cast_map=ATTR_VAL_CAST_MAP) super(CSoundSequencer, self).__init__(name=name, num_measures=num_measures, meter=meter, swing=swing, player=player, mn=mn)
def __init__(self, name: Optional[str] = None, num_measures: int = None, meter: Optional[Meter] = None, swing: Optional[Swing] = None, midi_file_path: Path = None, mn: MakeNoteConfig = None): if not mn: mn = MakeNoteConfig(cls_name=CLASS_NAME, num_attributes=NUM_ATTRIBUTES, make_note=make_note, pitch_for_key=pitch_for_key, attr_name_idx_map=ATTR_NAME_IDX_MAP, attr_val_cast_map=ATTR_VAL_CAST_MAP) super(MidiWriterSequencer, self).__init__( name=name, num_measures=num_measures, meter=meter, swing=swing, player=MidiWriter( append_mode=MidiPlayerAppendMode.AppendAfterPreviousNote, midi_file_path=midi_file_path), mn=mn)
def __init__(self, name: Optional[str] = None, num_measures: int = None, meter: Optional[Meter] = None, swing: Optional[Swing] = None, arpeggiator_chord: Optional[Chord] = None, mn: MakeNoteConfig = None): if not mn: mn = MakeNoteConfig(cls_name=CLASS_NAME, num_attributes=NUM_ATTRIBUTES, make_note=make_note, pitch_for_key=pitch_for_key, attr_name_idx_map=ATTR_NAME_IDX_MAP, attr_val_cast_map=ATTR_VAL_CAST_MAP) super(MidiMultitrackSequencer, self).__init__( name=name, num_measures=num_measures, meter=meter, swing=swing, arpeggiator_chord=arpeggiator_chord, player=MidiInteractiveMultitrackPlayer( append_mode=MidiPlayerAppendMode.AppendAfterPreviousNote), mn=mn)
def copy(source: 'Chord') -> 'Chord': return Chord(harmonic_chord=source.harmonic_chord, octave=source.octave, key=source.key, mn=MakeNoteConfig.copy(source.mn))
def _parse_pattern_to_section(self, pattern: str = None, instrument: Union[float, int] = None, swing: Swing = None, arpeggiate: bool = False, arpeggiator_chord: Optional[HarmonicChord] = None) -> Section: section = Section([]) swing = swing or self.swing def _make_note_vals(_instrument, _start, _duration, _amplitude, _pitch): _note_vals = NoteValues(self.mn.attr_name_idx_map.keys()) _note_vals.instrument = _instrument _note_vals.start = _start _note_vals.duration = _duration _note_vals.amplitude = _amplitude _note_vals.pitch = _pitch return _note_vals measure_tokens = [t.strip() for t in pattern.split(Sequencer.MEASURE_TOKEN_DELIMITER)] for measure_token in measure_tokens: note_tokens = [t.strip() for t in measure_token.split()] next_start = 0.0 duration = self.default_note_duration # Sum up the duration of all note positions to validate that the notes fit in the measure. We look at # "note positions" because for chords we only count the duration of all the notes in the chord once, # because they sound simultaneously so that duration only contributes to the total duration once. measure_duration = 0.0 note_vals_lst = [] for i, note_token in enumerate(note_tokens): start = self.mn.attr_val_cast_map['start'](next_start) # It's a rest note if note_token == Sequencer.REST_TOKEN: # Dummy values amplitude = self.mn.attr_val_cast_map['amplitude'](0) pitch = self.mn.attr_val_cast_map['pitch'](1) note_vals = _make_note_vals(instrument, start, duration, amplitude, pitch) note_vals_lst.append(note_vals) measure_duration += duration # It's a sounding note or chord, parse the pattern and collect the note/chord parameters else: key, octave, chord, amplitude, duration = note_token.split(Sequencer.NOTE_TOKEN_DELIMITER) # Only major or minor notes supported key = MAJOR_KEY_DICT.get(key) or MINOR_KEY_DICT.get(key) if not key: raise InvalidPatternException(f'Pattern \'{pattern}\' has invalid key {key} token') octave = int(octave) amplitude = self.mn.attr_val_cast_map['amplitude'](amplitude) # If no duration provided we already assigned default note duration (quarter note) if duration: duration = float(duration) # It's a chord. `arpeggiate=True` is ignored. if chord: # Chord can be empty, but if there is a token it must be valid harmonic_chord = HARMONIC_CHORD_DICT.get(chord) if not harmonic_chord: raise InvalidPatternException(f'Pattern \'{pattern}\' has invalid chord {chord} token') chord_sequence = Chord(harmonic_chord=harmonic_chord, octave=octave, key=key, mn=self.mn) for note in chord_sequence: note_vals_lst.append(_make_note_vals(instrument, start, duration, amplitude, note.pitch)) # Only count duration of the chord once in the total for the measure measure_duration += duration # It's a single sounding note. Converted into arpeggiated chords if `arpeggiate=True`. else: pitch = self.mn.pitch_for_key(key, octave) if not arpeggiate: note_vals = _make_note_vals(instrument, start, duration, amplitude, pitch) note_vals_lst.append(note_vals) else: harmonic_chord = arpeggiator_chord or self.arpeggiator_chord chord_sequence = Chord(harmonic_chord=harmonic_chord, octave=octave, key=key, mn=self.mn) arpeggiation_offset = duration / len(chord_sequence) chord_sequence.mod_ostinato(init_start_time=start, start_time_interval=arpeggiation_offset) # Duration of arpeggiated notes are fit into the duration of the notated note arpeggiated_note_duration = duration / len(chord_sequence) for note in chord_sequence: note_vals_lst.append(_make_note_vals( instrument, note.start, arpeggiated_note_duration, amplitude, note.pitch)) measure_duration += duration next_start += duration measure = Measure(num_notes=len(note_vals_lst), meter=self.meter, swing=swing, mn=MakeNoteConfig.copy(self.mn)) # TODO BETTER RULE THAN THIS FOR ARPEGGIATION # TODO WE SHOULD NOT NEED THIS ANYMORE BUT WE STILL DO OR TESTS FAIL ON MEASURE DURATION # Don't validate measure duration if we are arpeggiating, because arpeggiating on the last note will # push offset start times beyond the end of the measure if not arpeggiate and measure_duration != \ pytest.approx(self.meter.beats_per_measure * self.meter.beat_note_dur.value): raise InvalidPatternException((f'Measure duration {measure_duration} != ' f'self.meter.beats_per_measure {self.meter.beats_per_measure} * ' f'self.meter_beat_note_dur {self.meter.beat_note_dur.value}')) for i, note_vals in enumerate(note_vals_lst): set_attr_vals_from_note_values(measure.note(i), note_vals) section.append(measure) return section
BEAT_DUR_VAL: float = BEAT_DUR.value BPM = 240 METER = Meter(beats_per_measure=BEATS_PER_MEASURE, beat_note_dur=BEAT_DUR, tempo=BPM, quantizing=True) KEY = MajorKey.C OCTAVE = 4 HARMONIC_SCALE = HarmonicScale.Major HARMONIC_CHORD = HarmonicChord.MajorTriad NUM_NOTES_IN_CHORD = 3 NOTE_CONFIG = MakeNoteConfig(cls_name=CLASS_NAME, num_attributes=NUM_ATTRIBUTES, make_note=make_note, pitch_for_key=pitch_for_key, attr_name_idx_map=ATTR_NAME_IDX_MAP, attr_val_cast_map=ATTR_VAL_CAST_MAP) SCALE = Scale(key=KEY, octave=OCTAVE, harmonic_scale=HARMONIC_SCALE, mn=NOTE_CONFIG) NUM_NOTES_IN_SCALE = 7 NUM_MEASURES = 4 BASE_VELOCITY = 100 VELOCITY_FACTOR = 2 if __name__ == '__main__': performance_attrs = PerformanceAttrs()