Example #1
0
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)
Example #2
0
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)
Example #3
0
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={})
Example #4
0
 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)
Example #5
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
Example #6
0
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
Example #7
0
    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
Example #8
0
 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)
Example #9
0
 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)
Example #10
0
 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)
Example #11
0
 def copy(source: 'Chord') -> 'Chord':
     return Chord(harmonic_chord=source.harmonic_chord,
                  octave=source.octave,
                  key=source.key,
                  mn=MakeNoteConfig.copy(source.mn))
Example #12
0
    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()