Пример #1
0
def test_meter():
    meter = Meter(beat_note_dur=BEAT_NOTE_DUR,
                  beats_per_measure=BEATS_PER_MEASURE,
                  tempo=TEMPO_QPM,
                  quantizing=DEFAULT_IS_QUANTIZING)

    assert meter.beats_per_measure == BEATS_PER_MEASURE
    assert meter.beat_note_dur == BEAT_NOTE_DUR
    # noinspection PyTypeChecker
    beat_note_dur: float = BEAT_NOTE_DUR.value
    assert meter.meter_notation == (BEATS_PER_MEASURE, int(1 / beat_note_dur))
    assert meter.tempo_qpm == TEMPO_QPM
    assert meter.quarter_note_dur_secs == pytest.approx(Meter.SECS_PER_MINUTE /
                                                        TEMPO_QPM)
    assert meter.quarter_notes_per_beat_note == pytest.approx(
        beat_note_dur / Meter.QUARTER_NOTE_DUR)
    assert meter.beat_note_dur_secs == pytest.approx(
        meter.quarter_notes_per_beat_note * meter.quarter_note_dur_secs)
    assert meter.measure_dur_secs == pytest.approx(MEASURE_DUR)
    # 4/4
    # 4 quarter note beats per measure
    # 1 whole note beat per measure
    expected_beat_start_times_secs = [0.0, 0.25, 0.5, 0.75]
    for i, start in enumerate(meter.beat_start_times_secs):
        assert start == pytest.approx(expected_beat_start_times_secs[i])
Пример #2
0
def test_get_bpm_and_duration_from_meter_string(
):  # sourcery skip: move-assign
    valid_meter_string = '3/8'
    beats_per_measure, beat_note_duration = Meter.get_bpm_and_duration_from_meter_string(
        valid_meter_string)
    assert beats_per_measure == 3 and beat_note_duration == NoteDur.EIGHTH

    invalid_meter_string = '38'
    with pytest.raises(InvalidMeterStringException):
        _, _ = Meter.get_bpm_and_duration_from_meter_string(
            invalid_meter_string)

    invalid_meter_string = '3.8'
    with pytest.raises(InvalidMeterStringException):
        _, _ = Meter.get_bpm_and_duration_from_meter_string(
            invalid_meter_string)
Пример #3
0
def test_quantizing_on_off(meter):
    # Default is quantizing on
    assert meter.is_quantizing()
    # Can override default
    meter_2 = Meter(beat_note_dur=BEAT_NOTE_DUR,
                    beats_per_measure=BEATS_PER_MEASURE,
                    quantizing=False)
    assert not meter_2.is_quantizing()
    # Can toggle with methods
    meter_2.quantizing_on()
    assert meter_2.is_quantizing()
    meter_2.quantizing_off()
    assert not meter_2.is_quantizing()
Пример #4
0
def test_assign_meter_swing(meter, section):
    new_meter = Meter(beats_per_measure=BEATS_PER_MEASURE * 2,
                      beat_note_dur=BEAT_DUR)
    section.meter = new_meter
    assert section.meter == new_meter
    new_swing = Swing(swing_range=SWING_RANGE * 2)
    section._swing = new_swing
    assert section._swing == new_swing
Пример #5
0
def test_quantizing_on_off(section):
    # Default is quantizing on
    for measure in section.measure_list:
        assert measure.meter.is_quantizing()
    # Can override default
    meter_2 = Meter(beat_note_dur=BEAT_DUR,
                    beats_per_measure=BEATS_PER_MEASURE,
                    quantizing=False)
    assert not meter_2.is_quantizing()
    # Can toggle with methods
    meter_2.quantizing_on()
    assert meter_2.is_quantizing()
    meter_2.quantizing_off()
    assert not meter_2.is_quantizing()
Пример #6
0
INSTRUMENTS = [INSTRUMENT_1]
#
SCORE_HEADER = '''; Function 1
; GEN10 Parameters: 
;	- str1, str2, str3 ... where str is a fixed harmonic partial
;		- the value of str# is the relative strength of the partial in the final mixed timbre
;		- partials to be skipped are given value 0
;
; Func # 	Loadtm 	TblSize GEN   Parameters ...
; First partial variations
f 1		    0		    8193		10		1'''
SCORE_HEADER_LINES = [SCORE_HEADER]

if __name__ == '__main__':
    meter = Meter(beats_per_measure=BEATS_PER_MEASURE,
                  beat_note_dur=BEAT_DUR,
                  tempo=TEMPO_QPM)
    swing = Swing(swing_range=SWING_RANGE)
    measure = Measure(num_notes=NUM_NOTES,
                      meter=meter,
                      swing=swing,
                      mn=DEFAULT_NOTE_CONFIG())
    for i in range(NUM_NOTES):
        measure[i].instrument = INSTRUMENT_1_ID
        measure[i].start = (i % NUM_NOTES) * DUR
        measure[i].duration = DUR
        measure[i].amplitude = BASE_AMP
        measure[i].pitch = PITCH
    measure.apply_swing()
    track = Track(to_add=[measure],
                  name='ostinato',
Пример #7
0
def meter():
    return Meter(beat_note_dur=BEAT_DUR,
                 beats_per_measure=BEATS_PER_MEASURE,
                 tempo=TEMPO_QPM,
                 quantizing=DEFAULT_IS_QUANTIZING)
Пример #8
0
class Measure(NoteSequence):
    """Represents a musical measure in a musical Score. As such it includes a NoteSequence
       and attributes that affect the performance of all Notes in that NoteSequence.
       Additional attributes are Meter, BPM, Scale and Key.

       Also manages musical notions of time. So maintains a Meter and knows how long a beat is,
       current beat position, and can add notes on beat or at a specified time. Also manages the
       notes in the underlying note_sequence in order sorted by start_time.
    """

    DEFAULT_METER = Meter(beats_per_measure=4,
                          beat_note_dur=NoteDur.QUARTER,
                          quantizing=True)
    # This value for tempo means a 4/4 measure or four quarter notes is one second, that is that a quarter note,
    #  which has the unit-less float value of 0.25, is actually 0.25 secs duration. So this is the "unit tempo,"
    #  the tempo for which no adjustment to note start_time or duration is necessary to adjust unitless note
    #  values to their actual wall time value in seconds. This is used where we adjust start_time and duration,
    #  such as the tempo setter property.
    UNIT_TEMPO_QPM = 240

    # Measure does not take `child_sequences` arg because musical measures do not meaningfully have "child measures"
    def __init__(self,
                 meter: Meter = None,
                 swing: Swing = None,
                 num_notes: int = None,
                 mn: MakeNoteConfig = None,
                 performance_attrs: PerformanceAttrs = None):
        validate_optional_types(
            ('meter', meter, Meter), ('swing', swing, Swing),
            ('performance_attrs', performance_attrs, PerformanceAttrs))
        super(Measure, self).__init__(num_notes=num_notes, mn=mn)

        # TODO Enforce duration of meter bpm and tempo and add unit test coverage, currently the onus is on
        #  the caller to put correct duration in note_config, as that is what is used to create notes, ignoring tempo

        # Maintain the invariant that notes are sorted ascending by start
        self._sort_notes_by_start_time()

        self.meter = meter or copy(Measure.DEFAULT_METER)
        self.swing = swing
        self.num_notes = num_notes or 0
        self.performance_attrs = performance_attrs

        # Support adding notes based on Meter
        self.beat = 0
        # Support adding notes offset from end of previous note
        self.next_note_start = 0.0
        self.max_duration = self.meter.beats_per_measure * self.meter.beat_note_dur_secs

    def _sort_notes_by_start_time(self):
        # Sort notes by start time to manage adding on beat
        # The underlying NoteSequence stores the notes in a numpy array, which is a fixed-order data structure.
        # So we compute an ordered array mapping note start times to their index in the underlying array, then
        #  swap the values into the right indexes so the Notes in the underlying array are in sorted order
        # TODO NUMPY COPY AND SORT AND COPY
        sorted_notes_attr_vals = [as_list(note) for note in self]
        sorted_notes_attr_vals.sort(key=lambda x: x[START_I])
        # Now walk the dict of copied attributes, and write them back into the underlying notes at each index
        for i, sorted_note_attr_vals in enumerate(sorted_notes_attr_vals):
            note = self[i]
            for j in range(len(sorted_note_attr_vals)):
                note.note_attr_vals[j] = sorted_note_attr_vals[j]

    # Beat state management
    def reset_current_beat(self):
        self.beat = 0

    def increment_beat(self):
        self.beat = min(self.beat + 1, self.meter.beats_per_measure)

    def decrement_beat(self):
        self.beat = max(0, self.beat - 1)

    # /Beat state management

    # Adding notes in sequence on the beat
    def add_note_on_beat(self, note: Any, increment_beat=False) -> 'Measure':
        """Modifies the note_sequence in place by setting its start_time to the value of measure.beat.
        If increment_beat == True the measure_beat is also incremented, after the insertion. So this method
        is a convenience method for inserting multiple notes in sequence on the beat.
        """
        validate_type('increment_beat', increment_beat, bool)
        if len(self) + 1 > self.meter.beats_per_measure:
            raise ValueError(
                f'Attempt to add a note to a measure greater than the the number of beats per measure'
            )

        note.start = self.meter.beat_start_times_secs[self.beat]
        self.append(note)
        # Increment beat position if flag set and beat is not on last beat of the measure already
        if increment_beat:
            self.increment_beat()

        return self

    def add_notes_on_beat(self, to_add: NoteSequence) -> 'Measure':
        """Uses note as a template and makes copies of it to fill the measure. Each new note's start time is set
           to that beat start time.

           NOTE: This *replaces* all notes in the Measure with this sequence of notes on the beat
        """
        validate_types(('to_add', to_add, NoteSequence))
        if len(to_add) > self.meter.beats_per_measure:
            raise ValueError(
                f'Sequence `to_add` must have a number of notes <= to the number of beats per measure'
            )

        # Now iterate the beats per measure and assign each note in note_list to the next start time on the beat
        for i, beat_start_time in enumerate(self.meter.beat_start_times_secs):
            # There might be fewer notes being added than beats per measure
            if i == len(to_add):
                break
            to_add[i].start = beat_start_time

        self.extend(to_add)

        return self

    # /Adding notes in sequence on the beat

    # Updating Tempo and resetting note start and duration
    @property
    def tempo(self):
        return self.meter.tempo

    @tempo.setter
    def tempo(self, tempo: int):
        self.meter.tempo = tempo
        for note in self:
            note.start *= (Measure.UNIT_TEMPO_QPM / tempo)
            note.duration *= (Measure.UNIT_TEMPO_QPM / tempo)
        self._sort_notes_by_start_time()

    def _get_start_for_tempo(self, note: Any) -> float:
        # Get the ratio of the note start time to the duration of the entire measure, and then adjust for tempo
        #  to get the actual start time
        measure_duration = self.meter.beats_per_measure * \
                           self.meter.quarter_notes_per_beat_note * \
                           NoteDur.QUARTER.value
        return note.start * (measure_duration * self.meter.measure_dur_secs)

    def _get_duration_for_tempo(self, note: Any) -> float:
        return self.meter.quarter_note_dur_secs * (note.duration /
                                                   NoteDur.QUARTER.value)

    # /Updating Tempo and resetting note start and duration

    # Adding notes in sequence from the current start time, one note immediately after another
    def add_note_on_start(self, note: Any) -> 'Measure':
        """Modifies the note_sequence in place by setting its start_time to the value of measure.start.
           If increment_start == True then measure.start is also incremented, after the insertion. So this method
           is a convenience method for inserting multiple notes in sequence.
           Validates that all the durations fit in the total duration of the measure.
        """
        # The note has a specified duration, but this is derived from its NoteDur, which can be thought of
        #  as a note on a musical score, i.e a "quarter note." NoteDur maps these to float values where
        #  a whole note == 1, and so a quarter note == 0.25. To convert that unitless value into a float
        #  number of seconds, the ratio of note.duration to a quarter note is multiplied by the actual
        #  wall time of a quarter note derived from the tempo, which is the number of quarter notes per minute.
        actual_duration_secs = self._get_duration_for_tempo(note)
        if self.next_note_start + actual_duration_secs > self.meter.measure_dur_secs:
            raise ValueError((
                f'measure.next_note_start {self.next_note_start} + note.duration {note.dur} > '
                f'measure.max_duration {self.max_duration}'))

        note.duration = actual_duration_secs
        note.start = self.next_note_start
        self.next_note_start += note.duration
        super(Measure, self).append(note)
        self._sort_notes_by_start_time()

        return self

    def add_notes_on_start(self, to_add: NoteSequence) -> 'Measure':
        """Uses note as a template and makes copies of it to fill the measure. Each new note's start time is set
           to that of the previous notes start + duration.
           Validates that all the durations fit in the total duration of the measure.

           NOTE: This *replaces* all notes in the Measure with this sequence of notes on the beat
        """
        validate_types(('to_add', to_add, NoteSequence))

        # TODO DO THIS IN NUMPY NATIVE WAY
        sum_of_durations = sum(
            self._get_duration_for_tempo(note) for note in to_add)
        if self.next_note_start + sum_of_durations > self.meter.measure_dur_secs:
            raise ValueError(
                (f'measure.next_note_start {self.next_note_start} + '
                 f'sum of note.durations {sum_of_durations} > '
                 f'measure.max_duration {self.max_duration}'))

        for note in to_add:
            note.duration = self._get_duration_for_tempo(note)
            note.start = self.next_note_start
            self.next_note_start += note.duration
            super(Measure, self).append(note)
        self._sort_notes_by_start_time()

        return self

    def replace_notes_on_start(self, to_add: NoteSequence) -> 'Measure':
        self.remove((0, len(self)))
        self.next_note_start = 0.0
        for note in to_add:
            note.duration = self._get_duration_for_tempo(note)
            note.start = self.next_note_start
            self.next_note_start += note.duration
            super(Measure, self).append(note)
        self._sort_notes_by_start_time()

        return self

    # /Adding notes in sequence from the current start time, one note immediately after another

    # Quantize notes
    def quantizing_on(self):
        self.meter.quantizing_on()

    def quantizing_off(self):
        self.meter.quantizing_off()

    def is_quantizing(self):
        return self.meter.is_quantizing()

    def quantize(self):
        self.meter.quantize(self)

    def quantize_to_beat(self):
        self.meter.quantize_to_beat(self)

    # /Quantize notes

    # Apply Swing and Phrasing to notes
    def set_swing_on(self):
        if self.swing:
            self.swing.set_swing_on()
        else:
            raise MeasureSwingNotEnabledException(
                'Measure.swing_on() called but swing is None in Measure')

    def set_swing_off(self):
        if self.swing:
            self.swing.set_swing_off()
        else:
            raise MeasureSwingNotEnabledException(
                'Measure.swing_off() called but swing is None in Measure')

    def is_swing_on(self):
        if self.swing:
            return self.swing.is_swing_on()
        else:
            raise MeasureSwingNotEnabledException(
                'Measure.is_swing_on() called but swing is None in Measure')

    def apply_swing(self):
        """Moves all notes in Measure according to how self.swing is configured.
        """
        if self.swing:
            self.swing.apply_swing(self)
        else:
            raise MeasureSwingNotEnabledException(
                'Measure.apply_swing() called but swing is None in Measure')

    def apply_phrasing(self):
        """Moves the first note in Measure forward and the last back by self.swing.swing.swing_factor.
           The idea is to slightly accentuate the metric phrasing of each measure. Handles boundary condition
           of having 0 or 1 notes. If there is only 1 note no adjustment is made.

           NOTE: At this time this only supports a fixed adjustment. Swing can support a random adjustment
           within a swing_range if measure would also set swing_jitter_type to Swing.SwingJitterType.Random
        """
        if self.swing:
            if len(self) > 1:
                self[0].start += \
                    self.swing.calculate_swing_adjust(swing_direction=Swing.SwingDirection.Forward,
                                                      swing_jitter_type=Swing.SwingJitterType.Fixed)
                self[-1].start -= \
                    self.swing.calculate_swing_adjust(swing_direction=Swing.SwingDirection.Forward,
                                                      swing_jitter_type=Swing.SwingJitterType.Fixed)
        else:
            raise MeasureSwingNotEnabledException(
                'Measure.apply_phrasing() called but swing is None in Measure')

    # /Apply Swing and Phrasing to notes

    # Apply to all notes
    def transpose(self, interval: int):
        for note in self:
            note.transpose(interval)

    # Dynamic setter for an attribute over all Notes in the Measure
    def get_attr(self, name: str) -> List[Any]:
        """Return list of all values for attribute `name` from all notes in the measure, in start time order"""
        validate_type('name', name, str)
        return [getattr(note, name) for note in self]

    def set_attr(self, name: str, val: Any):
        """Apply to all notes in note_list"""
        validate_type('name', name, str)
        for note in self:
            setattr(note, name, val)

    # NoteSequence note_list management
    # Wrap all parent methods to maintain invariant that note_list is sorted by note.start_time ascending
    def append(self, note: Any, start=None) -> 'Measure':
        note.start = start or self._get_start_for_tempo(note)
        note.duration = self._get_duration_for_tempo(note)
        # This is a COPY operation so all modifications to note state must be done before
        #  append() in parent class, which will create new storage for the note and copy
        #  its values into the storage, and expose that storage through iterator/accessor interface.
        super(Measure, self).append(note)
        self._sort_notes_by_start_time()
        return self

    def extend(self, to_add: NoteSequence) -> 'Measure':
        for note in to_add:
            note.start = self._get_start_for_tempo(note)
            note.duration = self._get_duration_for_tempo(note)
        super(Measure, self).extend(to_add)
        self._sort_notes_by_start_time()
        return self

    def __add__(self, to_add: Any) -> 'Measure':
        if isinstance(to_add, NoteSequence):
            self.extend(to_add)
        else:
            self.append(to_add)
        return self

    def __lshift__(self, to_add: Any) -> 'Measure':
        return self.__add__(to_add)

    def insert(self, index: int, to_add: Any) -> 'Measure':
        if isinstance(to_add, NoteSequence):
            for note in to_add:
                to_add.start = self._get_start_for_tempo(note)
                to_add.duration = self._get_duration_for_tempo(note)
        else:
            to_add.start = self._get_start_for_tempo(to_add)
            to_add.duration = self._get_duration_for_tempo(to_add)
        super(Measure, self).insert(index, to_add)
        self._sort_notes_by_start_time()
        return self

    def remove(self, range_to_remove: Tuple[int, int]) -> 'Measure':
        super(Measure, self).remove(range_to_remove)
        self._sort_notes_by_start_time()
        return self

    # /NoteSequence note_list management

    # Iterator support
    def __eq__(self, other: 'Measure') -> bool:
        if not super(Measure, self).__eq__(other):
            return False
        return self.meter == other.meter and \
            self.swing == other.swing and \
            self.beat == other.beat and \
            self.next_note_start == pytest.approx(other.next_note_start) and \
            self.max_duration == pytest.approx(other.max_duration)

    # /Iterator support

    # TODO ALL CLASSES LIKE METER AND SWING NEED COPY AND ALL COPIES ARE DEEP COPIES
    @staticmethod
    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
Пример #9
0
class Sequencer(Song):
    """
    Pattern language:
      - Notes have 5 elements: Pitch:Octave:Chord:Amplitude:Duration
      - Chord is optional. Optional elements are simply no characters long.
      - Duration is optional, and again can be no characters to indicate no value being provided. If the pattern
        does not have a duration the note is assigned the beat duration for the meter the sequencer is constructed with.
      - Elements in a Note entry are delimited with colons
      - Rests are '.' entries
      - Measures are '|'
      - If the pattern has more measures than self.num_measures an Exception is raised
      - If the pattern has fewer measures than self.num_measures then it is repeated to generate the measures.
        - The number of measures in pattern does not need to evenly divide self.num_measures.
    C:4:MajorSeventh:100 . . .|. . E:5::110 .
    """

    DEFAULT_PATTERN_RESOLUTION = NoteDur.QUARTER
    DEFAULT_TEMPO = 120
    DEFAULT_METER = Meter(beat_note_dur=NoteDur.QUARTER, beats_per_measure=4, tempo=DEFAULT_TEMPO)
    DEFAULT_NUM_MEASURES = 4

    MEASURE_TOKEN_DELIMITER = '|'
    REST_TOKEN = '.'
    NOTE_TOKEN_DELIMITER = ':'

    DEFAULT_ARPEGGIATOR_CHORD = HarmonicChord.MajorTriad
    DEFAULT_ARPEGGIATOR_CHORD_KEY = harmonic_chord_to_str(DEFAULT_ARPEGGIATOR_CHORD)

    def __init__(self,
                 name: Optional[str] = None,
                 num_measures: int = None,
                 meter: Optional[Meter] = None,
                 swing: Optional[Swing] = None,
                 player: Optional[Union[Player, Writer]] = None,
                 arpeggiator_chord: Optional[Chord] = None,
                 mn: MakeNoteConfig = None):
        validate_types(('num_measures', num_measures, int), ('mn', mn, MakeNoteConfig))
        validate_optional_types(('name', name, str),
                                ('swing', swing, Swing),
                                ('arpeggiator_chord', arpeggiator_chord, Chord))
        validate_optional_type_choice('player', player, (Player, Writer))

        # Sequencer wraps song but starts with no Tracks. It provides an alternate API for generating and adding Tracks.
        to_add = []
        meter = meter or Sequencer.DEFAULT_METER
        super(Sequencer, self).__init__(to_add, name=name, meter=meter, swing=swing)

        self.player = player
        if self.player:
            self.player.song = self
        self.arpeggiator_chord = arpeggiator_chord or Sequencer.DEFAULT_ARPEGGIATOR_CHORD
        self.mn = mn

        self.num_measures = num_measures or Sequencer.DEFAULT_NUM_MEASURES
        self.default_note_duration: float = self.meter.beat_note_dur.value

        self.num_tracks = 0
        # Internal index to the next track to create when add_track() or add_pattern_as_track() are called
        self._next_track = 0
        self._track_name_idx_map = {}
        self._track_name_player_map = {}

    # Properties
    @property
    def tempo(self) -> int:
        return self.meter.tempo_qpm

    @tempo.setter
    def tempo(self, tempo: int) -> None:
        """
        Sets the meter.tempo_qpm to the tempo value provided. Also recursively traverses down to every measure
        in every section in every track, rebuilding the measures notes to adjust their start times to use the
        new meter with the new tempo.
        """
        validate_type('tempo', tempo, int)
        self.meter.tempo = tempo
        for track in self.track_list:
            track.tempo = tempo

    def set_tempo_for_track(self, track_name: str = None, tempo: int = None):
        validate_types(('track_name', track_name, str), ('tempo', tempo, int))
        # Make a new meter object set to the new tempo and assign it to the sequencer
        track = self.track_list[self._track_name_idx_map[track_name]]
        track.tempo = tempo

    def track(self, track_name: str):
        return self.track_list[self._track_name_idx_map[track_name]]

    def track_names(self):
        return '\n'.join(self._track_name_idx_map.keys())
    # /Properties

    # Note Modification
    def transpose(self, interval: int):
        validate_type('interval', interval, int)
        for track in self.track_list:
            for measure in track:
                measure.transpose(interval)
    # /Note Modification

    # Track and Pattern Management
    # TODO Validate instrument is int, MidiInstrument enum or a class (Foxdot)
    def add_track(self,
                  track_name: str = None,
                  instrument: Union[float, int] = None) -> Track:
        validate_type('track_name', track_name, str)
        validate_type_choice('instrument', instrument, (float, int))

        track_name = track_name or str(self._next_track)
        track = Track(meter=self.meter,
                      swing=self.swing, name=track_name,
                      instrument=instrument)
        self.append(track)
        self.num_tracks += 1
        self._track_name_idx_map[track_name] = self._next_track
        self._next_track += 1
        return track

    def set_track_pattern(self,
                          track_name: str = None,
                          pattern: str = None,
                          instrument: Optional[Union[float, int]] = None,
                          swing: Optional[Swing] = None,
                          arpeggiate: bool = False,
                          arpeggiator_chord: Optional[HarmonicChord] = None):
        """
        - Sets the pattern, a section of measures in the track named `track_name`.
        - If the track already has a pattern, it is replaced. If the track is empty, its pattern is set to `pattern`.
        - If `swing` is arg is supplied, then the track will have swing applied using it
        - If the `instrument` arg is supplied, this instrument will be bound to the track, replacing whatever
          instrument it previously was bound to
        - If the `apply_swing` arg is True and the class has `self.swing` then the class swing
          object will be used to apply swing
        """
        validate_types(('track_name', track_name, str), ('pattern', pattern, str))
        validate_optional_types(('arpeggiate', arpeggiate, bool),
                                ('arpeggiator_chord', arpeggiator_chord, HarmonicChord), ('swing', swing, Swing))
        validate_optional_type_choice('instrument', instrument, (float, int))

        # Will raise if track_name is not valid
        track = self.track_list[self._track_name_idx_map[track_name]]
        if track.measure_list:
            track.remove((0, self.num_measures))
        instrument = instrument or track.instrument
        section = self._parse_pattern_to_section(pattern=pattern, instrument=instrument,
                                                 arpeggiate=arpeggiate, arpeggiator_chord=arpeggiator_chord)
        if len(section) < self.num_measures:
            self._fill_section_to_track_length(section)
        track.extend(to_add=section)
        swing = swing or self.swing
        if swing and swing.is_swing_on():
            track.apply_swing()

    def add_pattern_as_new_track(self,
                                 track_name: Optional[str] = None,
                                 pattern: str = None,
                                 instrument: Union[float, int] = None,
                                 swing: Optional[Swing] = None,
                                 track_type: Optional[Any] = Track,
                                 arpeggiate: bool = False,
                                 arpeggiator_chord: Optional[HarmonicChord] = None) -> Track:
        """
        - Sets the pattern, a section of measures in a new track named `track_name` or if no name is provided
          in a track with a default name of its track number.
        - Track is bound to `instrument`
        - If `track_name` is not supplied, a default name of `Track N`, where N is the index of the track, is assigned
        - If `swing` is arg is supplied and `is_swing_on()` is `True`, then the track will have swing applied using it
        - If `self.swing` `is_swing_on()` is `True` then the track will have swing applied using it
        - If `track_type` is provided, this method constructs a subclass of Track. This allows callers to
          construct for example a MidiTrack, which derives from Track and adds attributes specific to MIDI.
        """
        validate_type('pattern', pattern, str)
        validate_optional_types(('track_name', track_name, str), ('swing', swing, Swing),
                                ('arpeggiate', arpeggiate, bool),
                                ('arpeggiator_chord', arpeggiator_chord, HarmonicChord))
        validate_type_choice('instrument', instrument, (float, int))
        validate_type_reference('track_type', track_type, Track)

        # Set the measures in the section to add to the track
        section = self._parse_pattern_to_section(pattern=pattern, instrument=instrument,
                                                 arpeggiate=arpeggiate, arpeggiator_chord=arpeggiator_chord)
        # If the section is shorter than num_measures, the length of all tracks, repeat it to fill the track
        if len(section) < self.num_measures:
            self._fill_section_to_track_length(section)

        # Create the track, add the section to it, apply quantize and swing according to the logic in the docstring
        track_name = track_name or str(self._next_track)
        self._track_name_idx_map[track_name] = self._next_track
        swing = swing or self.swing
        track = track_type(name=track_name, instrument=instrument, meter=self.meter, swing=swing)
        track.extend(to_add=section)
        if swing and swing.is_swing_on():
            track.apply_swing()

        # Add the track to the sequencer, update bookkeeping
        self.append(track)
        self.num_tracks += 1
        self._next_track += 1

        return track

    def _fill_section_to_track_length(self, section: Section):
        """
        Implements logic to repeat a pattern shorter than `self.num_measures` to fill out the measures in the track.
        The logic is simple `divmod()`, repeat as many times as needed and if the pattern doesn't fit evenly then
        the last repeat is partial and cuts off on whatever measure reaches `num_measures`.
        """
        # We already have a section of the length of the pattern, so subtract that
        quotient, remainder = divmod(self.num_measures - len(section), len(section))
        section_cpy = Section.copy(section)
        for _ in range(quotient):
            section.extend(Section.copy(section_cpy).measure_list)
        section.extend(Section.copy(section_cpy).measure_list[:remainder])

    # TODO MORE SOPHISTICATED PARSING IF WE EXTEND THE PATTERN LANGUAGE
    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
    # /Track and Pattern Management

    # Track and Player Management and Playback
    def add_player_for_track(self,
                             track_name: str = None,
                             player: Player = None):
        """Adds a Player specific to a track, which overrides self.player if present"""
        validate_types(('track_name', track_name, str), ('player', player, Player))
        self._track_name_player_map[track_name] = player

    async def play_track(self, track_name: str = None):
        """Plays a track with a track-mapped Player, if present, otherwise plays using self.Player"""
        validate_type('track_name', track_name, str)
        track = self._track_name_idx_map[track_name]
        # Create an anonymous song with just this track to pass to the Player, since Players play Songs
        song = Song(to_add=track, meter=self.meter, swing=self.swing)
        player = self._track_name_player_map.get(track_name) or self.player
        if not player:
            raise InvalidPlayerException(f'No track player or self.player found to play track {track_name}')
        player.song = song
        await player.play()

    def play(self):
        # noinspection PyArgumentList
        self.player.song = self
        self.player.play()

    def loop(self):
        # noinspection PyArgumentList
        self.player.song = self
        self.player.loop()
Пример #10
0
from omnisound.src.note.adapter.midi_note import MidiInstrument
from omnisound.src.container.track import MidiTrack
from omnisound.src.generator.chord_globals import HarmonicChord
from omnisound.src.generator.sequencer.midi_sequencer import (
    MidiSingleTrackSequencer, MidiMultitrackSequencer, MidiWriterSequencer)
from omnisound.src.modifier.meter import Meter, NoteDur
from omnisound.src.modifier.swing import Swing

# Meter
BEATS_PER_MEASURE = 4
BEAT_DUR = NoteDur.QUARTER
# noinspection PyTypeChecker
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)
# Swing
SWING_FACTOR = 0.005
SWING = Swing(swing_on=True,
              swing_range=SWING_FACTOR,
              swing_direction=Swing.SwingDirection.Both)
# Sequencer
SEQUENCER_NAME = 'example_midi_sequencer_song'
NUM_MEASURES = 4
MIDI_FILE_PATH = Path(
    '/Users/markweiss/Documents/projects/omnisound/omnisound/example/example_sequencer_song.mid'
)

BASE_VELOCITY = 100
VELOCITY_FACTOR = 2
Пример #11
0
                CHANNELS[int(track_idx)].append(
                        (int(note_idx),
                         event_type,
                         [int(p) for p in chord_pitches] if values[event] else (0, 0, 0))
                )

    window.close()


def _parse_args():
    parser = OptionParser()
    parser.add_option('-n', '--num-tracks', dest='num_tracks', type='int', help='Number of sequencer tracks')
    parser.add_option('-l', '--measures-per-track', dest='measures_per_track', type='int',
                      help='Number of measures per sequencer track')
    parser.add_option('-t', '--tempo', dest='tempo', type='int',
                      help='Tempo in beats per minute of all sequencer tracks')
    parser.add_option('-m', '--meter',
                      action='store', dest='meter', default='4/4', type='string',
                      help='Meter of sequencer tracks. Default is 4/4.')

    return parser.parse_args()


if __name__ == '__main__':
    options, _ = _parse_args()
    beats_per_measure, beat_note_dur = Meter.get_bpm_and_duration_from_meter_string(options.meter)
    _generate_tracks_and_layout(options.num_tracks, options.measures_per_track,
                                Meter(beats_per_measure=beats_per_measure, beat_note_dur=beat_note_dur,
                                      tempo=options.tempo))
    start(beats_per_measure, options.measures_per_track)