Source code for chiptunesak.mchirp

import copy
from chiptunesak.base import *
from chiptunesak import chirp
import more_itertools as moreit

""" Definition and methods for mchirp.MChirpSong representation """


[docs]class Measure: @staticmethod def _sort_order(c): """ Sort function for measure contents. Items are sorted by time and then, for equal times, in this order: Time Signature Key Signature Tempo Other MIDI message(s) Notes and rests """ if isinstance(c, chirp.Note): return (c.start_time, 10) elif isinstance(c, Triplet): return (c.start_time, 10) elif isinstance(c, Rest): return (c.start_time, 10) elif isinstance(c, MeasureMarker): return (c.start_time, 0) elif isinstance(c, TimeSignatureEvent): return (c.start_time, 1) elif isinstance(c, KeySignatureEvent): return (c.start_time, 2) elif isinstance(c, TempoEvent): return (c.start_time, 3) elif isinstance(c, ProgramEvent): return (c.start_time, 4) else: return (c.start_time, 5) def __init__(self, start_time, duration): """ Creation for Measure object. Populating the measure with events is a separate method populate() :param start_time: Start time of the measure, in MIDI ticks :param duration: Duration of the measure, in MIDI ticks """ self.start_time = start_time self.duration = duration self.events = []
[docs] def process_triplets(self, measure_notes, ppq): """ Processes and accounts for all triplets in the measure :param measure_notes: list of notes in the measure :type measure_notes: list of notes/triplets :param ppq: pulses per quarter from song :type ppq: int :return: new measure contents :rtype: list of notes/triplet """ triplets = [n for n in measure_notes if is_triplet(n, ppq)] while len(triplets) > 0: shortest_triplet = sorted(triplets, key=lambda t: (t.duration, t.start_time))[0] t_start = shortest_triplet.start_time - self.start_time beat_type = start_beat_type(t_start, ppq) if beat_type % 3 == 0: # This happens when the triplet does NOT start on a beat beat_division = beat_type // 3 # Get the beat size from the offset from the triplet start # The triplet start time is the previous beat of the required size triplet_start_time = (shortest_triplet.start_time * beat_division // ppq) * ppq // beat_division # Deduce the triplet length from the position of the note; it is on sub-beat 2 or 3 min_duration = min(shortest_triplet.duration, shortest_triplet.start_time - triplet_start_time) triplet_duration = 3 * min_duration while triplet_start_time + triplet_duration <= shortest_triplet.start_time: triplet_start_time += triplet_duration else: # Note is on the beat so triplet starts on the beat triplet_start_time = shortest_triplet.start_time # Assume the note is a triplet (remember it is the shortest) unless proven otherwise triplet_duration = 3 * shortest_triplet.duration # Triplet cannot cross measure boundaries if triplet_start_time + triplet_duration > self.start_time + self.duration: triplet_duration //= 2 # All notes inside the triplet have to be triplets themselves if any(not is_triplet(n, ppq) for n in measure_notes if triplet_start_time <= n.start_time < triplet_start_time + triplet_duration): triplet_duration //= 2 # Make a new triplet with the right start time and duration new_triplet = Triplet(triplet_start_time, triplet_duration) # Now take notes and fill in the triplet measure_notes = self.populate_triplet(new_triplet, measure_notes) # Check for any remaining triplets in the measure. Interstingly, the triplet object is not a triplet-note! triplets = [n for n in measure_notes if is_triplet(n, ppq)] # Sort the measure notes before returning return sorted(measure_notes, key=lambda n: n.start_time)
[docs] def populate_triplet(self, triplet, measure_notes): """ Given a triplet, populate it from the ntoes in the measure, splitting them if required :param triplet: triplet to be populated :type triplet: Triplet :param measure_notes: notes in the measure :type measure_notes: list of notes :return: measure notes now including triplet :rtype: list of notes/triplets """ triplet_end = triplet.start_time + triplet.duration # We will make a new list of notes to return new_measure_notes = [] for n in measure_notes: note_end = n.start_time + n.duration # Notes that start before the triplet and end after the triplet has started if n.start_time < triplet.start_time and note_end > triplet.start_time: assert note_end <= triplet_end, "Error in triplet processing!" new_notes = n.split(triplet.start_time) new_measure_notes.append(new_notes[0]) triplet.content.append(new_notes[-1]) # Notes that start inside the triplet elif triplet.start_time <= n.start_time < triplet_end: if note_end > triplet_end: new_notes = n.split(triplet_end) triplet.content.append(new_notes[0]) new_measure_notes.append(new_notes[-1]) else: triplet.content.append(n) # Notes not involved in the triplet else: new_measure_notes.append(n) # Add rests inside the triplet triplet.content.sort(key=lambda n: n.start_time) triplet_rests = [] current_position = triplet.start_time for n in triplet.content: if n.start_time > current_position: triplet_rests.append(Rest(current_position, n.start_time - current_position)) current_position = n.start_time + n.duration if current_position < triplet_end: triplet_rests.append(Rest(current_position, triplet_end - current_position)) triplet.content.extend(triplet_rests) triplet.content.sort(key=lambda n: n.start_time) assert sum(c.duration for c in triplet.content) == triplet.duration, "Triplet content does not sum to length!" # Add the triplet to the measure events new_measure_notes.append(triplet) return sorted(new_measure_notes, key=lambda n: n.start_time)
[docs] def add_rests(self, measure_notes): """ Add rests to a measure content :param measure_notes: notes in the measure :type measure_notes: list of notes :return: new list of events including rests :rtype: list of events in measure """ rests = [] measure_notes.sort(key=lambda n: n.start_time) current_time = self.start_time for n in measure_notes: if n.start_time > current_time: rests.append(Rest(current_time, n.start_time - current_time)) current_time = n.start_time + n.duration if current_time < self.start_time + self.duration: rests.append(Rest(current_time, self.start_time + self.duration - current_time)) measure_notes.extend(rests) return sorted(measure_notes, key=lambda n: n.start_time)
[docs] def populate(self, track, carry=None): """ Populates a single measure with notes, rests, and other events. :param track: Track from which events are to be imported :param carry: If last note in previous measure is continued in this measure, the note with remaining time :return: Carry note, if last note is to be carried into the next measure. """ ppq = track.chirp_song.metadata.ppq end = self.start_time + self.duration # Measure number is obtained from the song. measure_number = track.chirp_song.get_measure_beat(self.start_time).measure self.events.append(MeasureMarker(self.start_time, measure_number)) # Find all the notes that start in this measure; not the fastest but it works measure_notes = [copy.copy(n) for n in track.notes if self.start_time <= n.start_time < end] # Add in carry from previous measure if carry is not None: measure_notes.insert(0, copy.copy(carry)) carry = None # Process any notes carried out of the measure for n in measure_notes[::-1][:1]: note_end = n.start_time + n.duration if note_end > end: n, carry = tuple(n.split(end)) break # only one note can possible go past the end measure_notes = self.process_triplets(measure_notes, ppq) measure_notes = self.add_rests(measure_notes) self.events.extend(copy.deepcopy(measure_notes)) # Add program changes to measure: for pc in track.program_changes: if self.start_time <= pc.start_time < end: # Leave the time of these messages alone self.events.append(pc) # Add any additional track-specific messages to the measure: for m in track.other: if self.start_time <= m.start_time < end: # Leave the time of these messages alone self.events.append(m) # Now add all the song-specific events to the measure. for ks in track.chirp_song.key_signature_changes: if self.start_time <= ks.start_time < end: # Key signature changes must occur at the start of the measure self.events.append(KeySignatureEvent(self.start_time, ks.key)) for ts in track.chirp_song.time_signature_changes: if self.start_time <= ts.start_time < end: # Time signature changes must occur at the start of the measure self.events.append(TimeSignatureEvent(self.start_time, ts.num, ts.denom)) for tm in track.chirp_song.tempo_changes: if self.start_time <= tm.start_time < end: # Tempo changes can happen anywhere in the measure self.events.append(TempoEvent(tm.start_time, tm.qpm)) self.events = sorted(self.events, key=self._sort_order) return carry
def count_notes(self): return sum(1 for e in self.events if isinstance(e, chirp.Note)) def get_notes(self): return [e for e in self.events if isinstance(e, chirp.Note)] def get_rests(self): return [e for e in self.events if isinstance(e, Rest)]
[docs]class MChirpTrack: def __init__(self, mchirp_song, chirp_track=None): self.measures = [] #: List of measures in the track self.name = '' #: Track name self.channel = 0 #: Midi channel number self.mchirp_song = mchirp_song #: parent MChirpSong self.qticks_notes = mchirp_song.qticks_notes #: Inherit quantization from song self.qticks_durations = mchirp_song.qticks_durations #: Inherit quantization from song if chirp_track is not None: if not isinstance(chirp_track, chirp.ChirpTrack): raise ChiptuneSAKTypeError("MChirpTrack init can only import ChirpTrack objects.") else: self.import_chirp_track(chirp_track)
[docs] def import_chirp_track(self, chirp_track): """ Converts a track into measures, each of which is a sorted list of notes and other events :param chirp_track: A ctsSongTrack that has been quantized and had polyphony removed :type chirp_track: ChirpTrack :return: List of Measure objects corresponding to the measures """ if not chirp_track.is_quantized(): raise ChiptuneSAKQuantizationError("Track must be quantized to populate measures.") if chirp_track.is_polyphonic(): raise ChiptuneSAKPolyphonyError("Track must be non-polyphonic to populate measures.") self.qticks_notes = chirp_track.qticks_notes self.qticks_durations = chirp_track.qticks_durations measures_list = [] measure_starts = chirp_track.chirp_song.measure_starts() # Artificially add an extra measure on the end to finish processing the notes in the last measure. measure_starts.append(2 * measure_starts[-1] - measure_starts[-2]) # First add in the notes to the measure carry = None for start, end in moreit.pairwise(measure_starts): current_measure = Measure(start, end - start) carry = current_measure.populate(chirp_track, carry) measures_list.append(current_measure) self.measures = measures_list self.name = chirp_track.name self.channel = chirp_track.channel
[docs]class MChirpSong(ChiptuneSAKBase): @classmethod def cts_type(cls): return 'MChirp' def __init__(self, chirp_song=None): ChiptuneSAKBase.__init__(self) self.tracks = [] self.metadata = SongMetadata() #: Metadata self.qticks_notes = self.metadata.ppq #: Quantization for note starts, in ticks self.qticks_durations = self.metadata.ppq #: Quantization for note durations, in ticks self.other = [] #: Other MIDI events not used in measures if chirp_song is not None: if chirp_song.cts_type() != 'Chirp': raise ChiptuneSAKTypeError("MChirpSong init can only import ChirpSong objects") else: self.import_chirp_song(chirp_song) def to_chirp(self, **kwargs): self.set_options(**kwargs) return chirp.ChirpSong(self)
[docs] def import_chirp_song(self, chirp_song): """ Gets all the measures from all the tracks in a song, and removes any empty (note-free) measures from the end. :param chirp_song: A chirp.ChirpSong song :type chirp_song: ChirpSong """ if not chirp_song.is_quantized(): raise ChiptuneSAKQuantizationError("ChirpSong must be quantized before populating measures.") if chirp_song.is_polyphonic(): raise ChiptuneSAKPolyphonyError("ChirpSong must not be polyphonic to populate measures.") for t in chirp_song.tracks: self.tracks.append(MChirpTrack(self, t)) self.metadata = copy.deepcopy(chirp_song.metadata) self.qticks_notes, self.qticks_durations = chirp_song.qticks_notes, chirp_song.qticks_durations self.other = copy.deepcopy(chirp_song.other) self.trim() if chirp_song.get_option('trim_partial', False): self.trim_partial_measures()
[docs] def trim(self): """ Trims all note-free measures from the end of the song. """ if len(self.tracks) == 0: raise ChiptuneSAKContentError("No tracks in song") while all(t.measures[-1].count_notes() == 0 for t in self.tracks): for t in self.tracks: t.measures.pop() if len(t.measures) == 0: raise ChiptuneSAKContentError("No measures left in track %s" % t.name)
[docs] def trim_partial_measures(self): """ Trims any partial measures from the end of the file """ if all(isinstance(t.measures[-1].events[-1], Rest) for t in self.tracks): for t in self.tracks: t.measures.pop() if len(t.measures) == 0: raise ChiptuneSAKContentError("No measures left in track %s" % t.name)
[docs] def get_time_signature(self, time_in_ticks): """ Finds the active key signature at a given time in the song :param time_in_ticks: :return: The last time signature change event before the given time. """ current_time_signature = TimeSignatureEvent(0, 4, 4) for m in self.tracks[0].measures: if m.start_time > time_in_ticks: break else: ts = [e for e in m.events if isinstance(e, TimeSignatureEvent)] current_time_signature = ts[-1] if len(ts) > 0 else current_time_signature return current_time_signature
[docs] def get_key_signature(self, time_in_ticks): """ Finds the active key signature at a given time in the song :param time_in_ticks: :return: The last key signature change event before the given time. """ current_key_signature = KeySignatureEvent(0, 'C') for m in self.tracks[0].measures: if m.start_time > time_in_ticks: break else: ks = [e for e in m.events if isinstance(e, KeySignatureEvent)] current_key_signature = ks[-1] if len(ks) > 0 else current_key_signature return current_key_signature