Skip to content

Perform

MIDIConfig

Bases: dict

invertible map from MIDI channel: Notochord instrument

Source code in src/notochord/perform.py
class MIDIConfig(dict):
    """
    invertible map from MIDI channel: Notochord instrument
    """
    def __init__(self, *a, **kw):
        super().__init__(*a, **kw)
        self.invertible = len(self.channels)==len(self.insts)

    @property
    def channels(self):
        """set of channels"""
        return set(self)
    @property
    def insts(self):
        """set of instruments"""
        return set(self.values())
    def inv(self, inst):
        """map from Notochord instrument: MIDI channel"""
        if not self.invertible:
            print('WARNING: MIDIConfig is not invertible')
        for chan,inst_ in self.items():
            if inst_==inst:
                return chan
        raise KeyError(f"""
            instrument {inst} has no channel
            """)

channels property

set of channels

insts property

set of instruments

inv(inst)

map from Notochord instrument: MIDI channel

Source code in src/notochord/perform.py
def inv(self, inst):
    """map from Notochord instrument: MIDI channel"""
    if not self.invertible:
        print('WARNING: MIDIConfig is not invertible')
    for chan,inst_ in self.items():
        if inst_==inst:
            return chan
    raise KeyError(f"""
        instrument {inst} has no channel
        """)

NotoPerformance

track various quantities of a Notochord performance:

event history
  • wall time
  • nominal dt
  • pitch
  • velocity (0 for noteoff)
  • notochord instrument
query for
  • instruments present in the last N events
  • number of note_ons by instrument in last N events
  • currently playing notes with user data as {(inst, pitch): Any}
  • currently playing notes as {inst: pitches}
Source code in src/notochord/perform.py
class NotoPerformance:
    """
    track various quantities of a Notochord performance:

    event history:
        * wall time
        * nominal dt
        * pitch
        * velocity (0 for noteoff)
        * notochord instrument

    query for:
        * instruments present in the last N events
        * number of note_ons by instrument in last N events
        * currently playing notes with user data as {(inst, pitch): Any}
        * currently playing notes as {inst: pitches}
    """
    def __init__(self):
        self._notes:Dict[Note, Any] = {} 
        self.past_segments:List[pd.DataFrame] = []
        self.init()

    def init(self):
        self.events = pd.DataFrame(np.array([],dtype=[
            ('wall_time_ns',np.int64), # actual wall time played in ns
            ('time',np.float32), # nominal notochord dt in seconds
            ('inst',np.int16), # notochord instrument
            ('pitch',np.int16), # MIDI pitch
            ('vel',np.int8), # MIDI velocity
            ('channel',np.int8), # MIDI channel
            ]))
        self._notes.clear()

    def push(self):
        """push current events onto a list of past segments,
            start a fresh history
        """
        self.past_segments.append(self.events)
        self.init()

    def feed(self, held_note_data:Any=None, **event):
        """
        Args:
            held_note_data: any Python object to be attached to held notes
                (ignored for note-offs)
            ('wall_time_ns',np.int64), # actual wall time played in ns
            ('time',np.float32), # nominal notochord dt in seconds
            ('inst',np.int16), # notochord instrument
            ('pitch',np.int16), # MIDI pitch
            ('vel',np.int8), # MIDI velocity
            ('channel',np.int8), # MIDI channel
        """
        if 'wall_time_ns' not in event:
            event['wall_time_ns'] = time.time_ns()
        if 'channel' not in event:
            # use -1 for missing channel to avoid coercion to float
            event['channel'] = -1 
        cast_event = {}
        for k,v in event.items():
            if k in self.events.columns:
                cast_event[k] = self.events.dtypes[k].type(v)
        event = cast_event

        self.events.loc[len(self.events)] = event

        chan = event.get('channel', None)
        # inst, pitch, vel = event['inst'], event['pitch'], event['vel']
        # k = (chan, inst, pitch)
        vel = event['vel']
        k = Note(chan, event['inst'], event['pitch'])

        if vel > 0:
            self._notes[k] = held_note_data
        else:
            self._notes.pop(k, None)

    def inst_counts(self, n=0, insts=None):
        """instrument counts in last n (default all) note_ons"""
        df = self.events
        df = df.iloc[-min(128,n*16):] # in case of very long history
        df = df.loc[df.vel > 0]
        df = df.iloc[-n:]
        counts = df.inst.value_counts()
        if insts is not None:
            for inst in insts:
                if inst not in counts.index:
                    counts[inst] = 0
        return counts

    def held_inst_pitch_map(self, insts=None):
        """held notes as {inst:[pitch]} for given instruments"""
        note_map = defaultdict(list)
        for note in self._notes:
            if insts is None or note.inst in insts:
                note_map[note.inst].append(note.pitch)
        return note_map

    @property
    def note_pairs(self):
        """
        held notes as {(inst,pitch)}.
        returns a new `set`; safe to modify history while iterating
        """
        return {(note.inst, note.pitch) for note in self._notes}

    @property
    def note_triples(self):
        """
        held notes as {(channel,inst,pitch)}.
        returns a new `set`; safe to modify history while iterating
        """
        return {(note.chan, note.inst, note.pitch) for note in self._notes}

    @property
    def notes(self):
        """
        generic way to access notes, returns set of namedtuples 
        returns a new `set`; safe to modify history while iterating
        """
        return set(self._notes)

    @property
    def note_data(self):
        """held notes as {(chan,inst,pitch):held_note_data}.
        mutable.
        """
        # NOTE: returned dictionary should be mutable
        return self._notes

note_data property

held notes as {(chan,inst,pitch):held_note_data}. mutable.

note_pairs property

held notes as {(inst,pitch)}. returns a new set; safe to modify history while iterating

note_triples property

held notes as {(channel,inst,pitch)}. returns a new set; safe to modify history while iterating

notes property

generic way to access notes, returns set of namedtuples returns a new set; safe to modify history while iterating

feed(held_note_data=None, **event)

Parameters:

Name Type Description Default
held_note_data Any

any Python object to be attached to held notes (ignored for note-offs)

None
Source code in src/notochord/perform.py
def feed(self, held_note_data:Any=None, **event):
    """
    Args:
        held_note_data: any Python object to be attached to held notes
            (ignored for note-offs)
        ('wall_time_ns',np.int64), # actual wall time played in ns
        ('time',np.float32), # nominal notochord dt in seconds
        ('inst',np.int16), # notochord instrument
        ('pitch',np.int16), # MIDI pitch
        ('vel',np.int8), # MIDI velocity
        ('channel',np.int8), # MIDI channel
    """
    if 'wall_time_ns' not in event:
        event['wall_time_ns'] = time.time_ns()
    if 'channel' not in event:
        # use -1 for missing channel to avoid coercion to float
        event['channel'] = -1 
    cast_event = {}
    for k,v in event.items():
        if k in self.events.columns:
            cast_event[k] = self.events.dtypes[k].type(v)
    event = cast_event

    self.events.loc[len(self.events)] = event

    chan = event.get('channel', None)
    # inst, pitch, vel = event['inst'], event['pitch'], event['vel']
    # k = (chan, inst, pitch)
    vel = event['vel']
    k = Note(chan, event['inst'], event['pitch'])

    if vel > 0:
        self._notes[k] = held_note_data
    else:
        self._notes.pop(k, None)

held_inst_pitch_map(insts=None)

held notes as {inst:[pitch]} for given instruments

Source code in src/notochord/perform.py
def held_inst_pitch_map(self, insts=None):
    """held notes as {inst:[pitch]} for given instruments"""
    note_map = defaultdict(list)
    for note in self._notes:
        if insts is None or note.inst in insts:
            note_map[note.inst].append(note.pitch)
    return note_map

inst_counts(n=0, insts=None)

instrument counts in last n (default all) note_ons

Source code in src/notochord/perform.py
def inst_counts(self, n=0, insts=None):
    """instrument counts in last n (default all) note_ons"""
    df = self.events
    df = df.iloc[-min(128,n*16):] # in case of very long history
    df = df.loc[df.vel > 0]
    df = df.iloc[-n:]
    counts = df.inst.value_counts()
    if insts is not None:
        for inst in insts:
            if inst not in counts.index:
                counts[inst] = 0
    return counts

push()

push current events onto a list of past segments, start a fresh history

Source code in src/notochord/perform.py
def push(self):
    """push current events onto a list of past segments,
        start a fresh history
    """
    self.past_segments.append(self.events)
    self.init()