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])
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)
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()
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
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()
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',
def meter(): return Meter(beat_note_dur=BEAT_DUR, beats_per_measure=BEATS_PER_MEASURE, tempo=TEMPO_QPM, quantizing=DEFAULT_IS_QUANTIZING)
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
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()
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
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)