Summary
An object in real time (seconds) that can be nested (container with children) or flat (leaf), quantised (time/pitch/dynamic), flattened to absolute time, and rendered to MIDI (single track or polyphony split across multiple tracks) using mido
SOURCE CODE: TemporalObject.py
TemporalObject(startTimeSec=0.0, durationSec=0.0, optional: bpm=60.0, envelopes=None, annotations=None, children=None)
CONSTRUCTION PARAMETERS
startTimeSec(float): Start time in seconds (local time if nested; absolute time after flattening).durationSec(float): Duration in seconds.bpm(float, keyword-only): Tempo used for seconds↔beats conversions and MIDI tempo export; also propagated from containers to children during flattening.envelopes(dict, keyword-only): Mapping {key: Envelope}. Key conventions: “pitchwheel” is special-cased to pitchwheel messages; any other key is treated as CC (by name lookup in MIDI_MESSAGE_LOOKUP if str, else numeric CC).annotations(list, keyword-only): Free-form annotation list; MIDI export treats entries as (timeBeatsAbs, text) pairs and writes them as lyrics meta messages.children(list[TemporalObject], keyword-only): Nested temporal objects; non-empty means this object is a container.
ATTRIBUTES
id(int): Unique per-instance id allocated monotonically.startTimeSec(float): Start time in seconds.durationSec(float): Duration in seconds.endTimeSec(float): End time in seconds (startTimeSec + durationSec).bpm(float): Tempo for conversions and MIDI tempo export.dynamicdB(float): Dynamic value in a dB-like scale used for velocity mapping; default 100.pitchMidi(float): MIDI note number; may be microtonal (e.g., 60.5 = +50 cents); used by parsePitch to generate pitchwheel envelope and a rounded 12-EDO note number.velocityMidi(int|None): Cached MIDI velocity (0..127) computed by parseVelocity; invalidated (set to None) by quantiseDynamic.children(list[TemporalObject]): Direct children; defines container-ness.flatChildren(list[TemporalObject]): Flattened list of leaf objects with absolute timing; produced by flattenTheChildren and used for rendering/track allocation.envelopes(dict): Envelope mapping; see parameters.annotations(list): Annotation list; see parameters.ticksPerBeat(int): MIDI ticks-per-beat resolution for export; default 480.midiTrackIndex(int): Track index for per-note polyphony export; set per leaf by _assign_note_tracks when perNoteTracks=True.
METHODS
_allocateId()-> int: Class method that returns the next unique id and increments the counter.init(startTimeSec, durationSec, , bpm, envelopes, annotations, children): Initializes timing fields, id, default dynamic/pitch state, container lists, envelope/annotation stores, ticksPerBeat, midiTrackIndex.isContainer()-> bool: True if there are any children. add(obj): Appends a child to children.clear(): Removes all children.sortByTime(): Sorts children by (startTimeSec, durationSec).shiftTime(deltaSec): Adds deltaSec to startTimeSec and endTimeSec.stretchTime(factor):Multiplies durationSec by factor and recomputes endTimeSec = startTimeSec + durationSec.updateEndTime(): Sets endTimeSec to the maximum endTimeSec among flatChildren (assumes flatChildren is up to date).updateDuration(): Sets durationSec = endTimeSec – startTimeSec.flatten(parentalStartTimeSec=None)-> list[TemporalObject]: Returns a list of leaf objects with absolute timing by recursively adding parent offsets; containers propagate their bpm to children during recursion; mutates leaf start/end times in-place.flattenTheChildren(): Sets flatChildren = flatten(); recomputes endTimeSec and durationSec from leaves.overlaps(other)-> bool: True if this time interval overlaps with other; non-overlap if self.end <= other.start or other.end <= self.start.resolveChildOverlaps(minDurationSec=0.0): Greedy overlap resolver for direct children after sorting; trims earlier child to end at next start when possible; otherwise clamps earlier child to minDurationSec and pushes/shortens the later child accordingly; drops later child if its duration becomes non-positive or below minDurationSec.quantiseTime(timeGrid): Requires TimeGrid.quantise(seconds)->seconds; containers recurse into children then recompute their own bounds from children; leaves quantise start and end, clamp end>=start, then recompute duration.quantisePitch(pitchGrid): Requires PitchGrid.quantise(midi)->midi; containers recurse; leaves quantise pitchMidi.quantiseDynamic(dynamicGrid): Requires DynamicGrid.quantise(dB)->dB; containers recurse; leaves quantise dynamicdB and set velocityMidi=None to invalidate cached velocity.repr()-> str: Debug representation; containers show id/start/dur/end/children count; leaves show id/start/dur/end/dynamic/pitch/bpm/track.parsePitch()-> int: Converts pitchMidi to nearest 12-EDO note (rounded) and computes pitchbend from the fractional semitone difference; stores a constant Envelope under envelopes[“pitchwheel”] spanning startTimeSec..endTimeSec; if bend non-zero, appends a cent annotation; returns the rounded note number.parseVelocity(mindB=0, maxdB=100, , force=False)-> int: Returns cached velocityMidi unless force=True; otherwise normalises dynamicdB from [mindB,maxdB] to [0,1] using normaliseIntoRange, scales to 0..127, caches in velocityMidi, and returns it.parseEnvelope(envelopeKey)-> list[(timeSec, value)]: Samples the selected Envelope by creating an xGrid at 1ms resolution across the object’s duration and a yGrid of 128 steps; returns env.mapToGrid(xGrid, yGrid) for CC/pitchwheel rendering.secondsToBeats(sec=0, bpm=None)-> float: Converts seconds to beats via secbpm/60 using provided bpm or self.bpm.beatsToticks(beats, ticksPerBeat=None)-> float: Converts beats to ticks via beatsticksPerBeat using provided ticksPerBeat or self.ticksPerBeat._assign_note_tracks()-> int: Greedy interval partitioning over flatChildren sorted by (start,end); reuses a track whose last endTimeSec <= current startTimeSec (preferring smallest such end); otherwise allocates a new track; sets child.midiTrackIndex; returns number of tracks used._note_messages_abs_beats(child)-> list[(beatAbs, Message)]: Builds note_on and note_off messages at absolute beat times using child.parseVelocity and child.parsePitch; uses channel 0; time field initially 0._pitchwheel_messages_abs_beats(child, startBeats, endBeats)-> list[(beatAbs, Message)]: Builds pitchwheel messages at start/end beats from child.envelopes[“pitchwheel”], using a constant bend extracted via env.getValue(0)._cc_messages_abs_beats(child, key)-> list[(beatAbs, Message)]: Builds control_change messages by sampling child.parseEnvelope(key); resolves CC number from MIDI_MESSAGE_LOOKUP.index(key) if key is str, else int(key); converts sampled timeSec to beats; emits channel 0 CC messages with value=int(ccVal)._envelope_messages_abs_beats(child)-> list[(beatAbs, Message)]: Builds all envelope-derived messages for a child; special-cases “pitchwheel”, treats others as CC._annotation_messages_abs_beats(child)-> list[(beatAbs, MetaMessage)]: Converts child.annotations interpreted as (timeBeatsAbs, text) into lyrics meta messages at those beat times._leaf_messages_abs_beats(child, , parseEnvelopes=True)-> list[(beatAbs, msg)]: Concatenates note messages, optionally envelope messages, and annotation messages for a leaf child.toAbsoluteNoteMessagesBeatsByTrack(, parseEnvelopes=True, perNoteTracks=False)-> list[list[(beatAbs, msg)]]: Produces per-track lists of absolute-beat events; if called on a leaf, wraps it as a single-child container; ensures flatChildren exists; if perNoteTracks=False returns one track containing all leaf events; if True assigns midiTrackIndex via _assign_note_tracks and distributes each leaf’s events to its track.beatsToMidiTicksAbsToRel(eventsAbsBeats, ticksPerBeat=None, offsetSec=0)-> list[msg]: Sorts events by absolute beat time, converts beat times to absolute ticks (rounded), turns them into delta ticks by subtracting lastTick (initialised as offsetSecticksPerBeat), clamps negative deltas to 0, sets msg.time=delta, and returns the message list.parseMidi(path=”/tmp/out.mid”, trackname=“dummy”, *, parseEnvelopes=True, perNoteTracks=False): Flattens children, renders absolute events by track, writes a MIDI file with ticks_per_beat=self.ticksPerBeat; creates one MidiTrack per rendered track; writes a track_name meta message; inserts set_tempo meta message only in track 0 using bpm2tempo(self.bpm); converts absolute-beat events to delta-tick messages via beatsToMidiTicksAbsToRel; appends messages; saves to path and prints the saved location.
Examples
CONSTRUCTION AND ATTRIBUTES
Python
from GreasyPidgin.TemporalObject import TemporalObject# construction without keywords# start=0.25s, dur=1.5s (bpm defaults to 60)TO = TemporalObject(0.25, 1.5)
Python
from GreasyPidgin.TemporalObject import TemporalObject#construction with keywords (keyword-only args after durationSec)TO = TemporalObject( 0.0, 2.0, bpm=106.0, envelopes={ # CC by *name* (resolved via MIDI_MESSAGE_LOOKUP.index) "modulationWheel": Envelope([(0.0, 0.0), (2.0, 1.0)]), # CC by *number* 74: Envelope([(0.0, 0.2), (2.0, 0.8)]), }, annotations=[(0.0, "start"), (1.0, "mid")],)
Python
from GreasyPidgin.TemporalObject import TemporalObject# nested construction with children and setting attributest1 = TemporalObject(0.0, 0.5, bpm=120.0)t1.pitchMidi = 60.0t1.dynamicdB = 80.0t2 = TemporalObject(0.25, 0.5) # relative to container startt2.pitchMidi = 64.25 # microtonalt2.dynamicdB = 70.0t3 = TemporalObject( 1.0, # container start (seconds) 0.0, # duration can be 0 initially; recomputed by flattenTheChildren() bpm=120.0, # propagated to children during flatten() children=[t1, t3]) children=[t1, t3])t3.flattenTheChildren() # computes absolute leaf times + updates container end/durationprint(t3.startTimeSec, t3.endTimeSec, t3.durationSec)print([ (c.startTimeSec, c.endTimeSec) for c in t3.flatChildren ])
Python
from GreasyPidgin.TemporalObject import TemporalObject# accessing and setting the attributesobj = TemporalObject(0.0, 1.0, bpm=90.0)# accessprint(obj.id)print(obj.startTimeSec, obj.durationSec, obj.endTimeSec)print(obj.bpm, obj.pitchMidi, obj.dynamicdB, obj.velocityMidi)# set timingobj.startTimeSec = 2.0obj.durationSec = 0.75obj.endTimeSec = obj.startTimeSec + obj.durationSec # keep consistent# set musical parametersobj.pitchMidi = 61.5 # Db4 + 50 centsobj.dynamicdB = 65.0obj.velocityMidi = None # force recompute next time parseVelocity() is called# set envelopes / annotationsobj.envelopes["modulationWheel"] = Envelope([(2.0, 0.0), (2.75, 1.0)])obj.annotations.append((obj.secondsToBeats(obj.startTimeSec), "entry"))# set children (turn into container)obj.children = [TemporalObject(0.0, 0.25), TemporalObject(0.25, 0.25)]print(obj.isContainer()) # True
METHODS
Python
from GreasyPidgin.TemporalObject import TemporalObject# shiftTime / stretchTime / overlapst1 = TemporalObject(0.0, 1.0)t2 = TemporalObject(0.5, 1.0)print(t1.overlaps(t2)) # True (0.0..1.0 overlaps 0.5..1.5)t1.shiftTime(2.0) # now 2.0..3.0print(t1.startTimeSec, t1.endTimeSec) # 2.0 3.0print(t1.overlaps(t2)) # Falset2.stretchTime(0.5) # duration 1.0 -> 0.5, end recomputedprint(t2.startTimeSec, t2.durationSec, t2.endTimeSec) # 0.5 0.5 1.0
Python
from GreasyPidgin.TimeGrid import TimeGridfrom GreasyPidgin.PitchGrid import PitchGridfrom GreasyPidgin.DynamicGrid import DynamicGridfrom GreasyPidgin.TemporalObject import TemporalObjectt1 = TemporalObject(0.13, 0.49, bpm=106.0)t1.pitchMidi = 61.23456789t1.dynamicdB = 140tg = TimeGrid().fromSubdivisions([5, 6, 7, 8], durationBeats=10, bpm=t1.bpm)pg = PitchGrid().fromScaleMaskObj("ionian", 'edo12')dg = DynamicGrid(3, 100)print("before:",t1)t1.quantiseTime(tg)t1.quantisePitch(pg)t1.quantiseDynamic(dg)print("after: ", t1)
Python
from GreasyPidgin.TemporalObject import TemporalObject# parseMidi (single track vs per-note tracks)# build a container with overlapping notest1 = TemporalObject(0.0, 1.0, bpm=120.0)t1.pitchMidi = 60.0t1.dynamicdB = 80.0t2 = TemporalObject(0.5, 1.0, bpm=120.0)t2.pitchMidi = 64.25t2.dynamicdB = 70.0t3 = TemporalObject(0.0, 0.0, bpm=120.0, children=[t1, t2])# write a single-track MIDI (channel 0)t3.parseMidi( path="/tmp/single_track.mid", trackname="phrase", parseEnvelopes=True, perNoteTracks=False)# write multi-track MIDI (polyphony split across tracks, channel 0 in each)t3.parseMidi( path="/tmp/per_note_tracks.mid", trackname="phrase", parseEnvelopes=True, perNoteTracks=True)