pyquist.score¶
Score representation, rendering, and MIDI ingestion.
Score representation, rendering, and MIDI ingestion.
A Score is a list of Event — onset-based musical events.
Score behaves like a regular Python list (subclass of
collections.UserList): you can iterate, index, slice, concatenate
with +, repeat with *, append, etc. — all preserving Score type.
A minimal end-to-end example — build a score, define an instrument, render it
to Audio:
import numpy as np
import pyquist as pq
from pyquist.helper import pitch_to_frequency
from pyquist.score import Score, BasicMetronome
# Each (time, kwargs) tuple is one event; Score coerces tuples to Events.
score = Score([
(0, {"pitch": 60, "duration": 0.5}),
(1, {"pitch": 64, "duration": 0.5}),
])
def sine(pitch, duration, sample_rate=44100, **kwargs):
t = np.arange(int(duration * sample_rate)) / sample_rate
freq = pitch_to_frequency(pitch)
return pq.Audio(0.3 * np.sin(2 * np.pi * freq * t), sample_rate=sample_rate)
# With a metronome, event times are beats; at 120 BPM, 1 beat = 0.5 s.
audio = score.render(sine, metronome=BasicMetronome(bpm=120))
The rest of this module fills in the details behind that flow.
Each event carries a time (in seconds if rendered without a metronome,
in ticks otherwise — a “tick” being whatever discrete time unit the
metronome maps to seconds) and an instrument-specific kwargs dict. The
metronome is kept as a separate object (a Metronome instance) and
passed explicitly to Score.render() when needed.
To turn a Score into audio, call Score.render() with an
Instrument — a callable invoked as instrument(**event.kwargs)
that returns Audio. An instrument simply declares the kwargs it
cares about and absorbs the rest with **kwargs:
def sine(pitch, duration, **kwargs):
...
return Audio(samples, sample_rate=sr)
For per-event dispatch (e.g. different sounds for drums vs. pitched notes), capture the deciding key as a named parameter and forward the rest:
def my_instrument(is_drum, **kwargs):
if is_drum:
return drum_kit(**kwargs)
return sine(**kwargs)
Because instruments are called with **event.kwargs, every kwargs key
should be a valid Python identifier.
Common “tick” units:
beats — paired with
BasicMetronome(e.g., 120 BPM).MIDI ticks — paired with
MIDIMetronome(subdivisions of a quarter note as defined by the MIDI file’s PPQ resolution and tempo map).
For MIDI ingestion, see Score.from_midi().
- class pyquist.score.Event(time: float, kwargs: Dict[str, Any])[source]¶
Bases:
NamedTupleA timestamped, instrument-agnostic musical event.
timeis in seconds when the score is rendered without a metronome, or in ticks (whatever unit the metronome maps to seconds — beats, MIDI ticks, …) when a metronome is supplied.kwargsis opaque to the score; it is forwarded to the instrument asinstrument(**kwargs)at render time, so every key should be a valid Python identifier (e.g., avoid dashes).Eventis aNamedTuple: it unpacks like a regular tuple (time, kwargs = event), can be constructed positionally (Event(0.5, {"pitch": 60})) or by keyword.You rarely need to construct an
Eventexplicitly:Scoreconverts any(time, kwargs)2-tuple into anEventautomatically, soScore([(0.0, {"pitch": 60})])andScore([Event(0.0, {"pitch": 60})])are equivalent.Create new instance of Event(time, kwargs)
- class pyquist.score.Metronome[source]¶
Bases:
ABCConverts between score-time ticks and wall-clock seconds.
A “tick” here is the abstract time unit a
Scoreuses; differentMetronomesubclasses define what a tick means concretely (a beat forBasicMetronome, a MIDI PPQ tick forMIDIMetronome).
- class pyquist.score.BasicMetronome(*, tps=1.0, bpm=None)[source]¶
Bases:
MetronomeA fixed-tempo metronome: a constant number of ticks per second.
A tick is the canonical unit of score time (see
Metronome). Depending on the score it might stand for a beat, a MIDI tick, a second, or any other unit —BasicMetronomejust maps ticks to seconds at a single constant rate,ticks_per_second.Set that rate with exactly one of two keyword arguments:
tps— ticks per second, the canonical parameter. The default,tps=1.0, makes 1 tick = 1 second: an identity mapping convenient for scores whosetimefield is already in seconds.bpm— beats per minute, a musical convenience for the common case where a tick is a beat. BPM is just that same rate expressed per minute rather than per second, converted internally viatps = bpm / 60— soBasicMetronome(bpm=120)is exactlyBasicMetronome(tps=2.0). When given,bpmtakes precedence overtps.
- Parameters:
- Raises:
ValueError – if both
tpsandbpmareNone.
- class pyquist.score.Score(initlist=None)[source]¶
Bases:
UserListA list of
Eventwith music-specific helpers.Scoresubclassescollections.UserList, so all the standard list operations work and preserveScoretype —+,*, slicing,+=,.copy(),.append(), etc.The
timefield of each event is interpreted in seconds unless you pass aMetronometorender(), in which casetimeis treated as ticks and converted viametronome.tick_to_seconds. The metronome is not stored on the score — keep it as a separate variable (or use the(score, metronome)tuple returned byfrom_midi()).Construct from any iterable of events. Bare
(time, kwargs)tuples are converted toEventautomatically, so these are equivalent:Score([Event(0.0, {"pitch": 60}), Event(1.0, {"pitch": 64})]) Score([(0.0, {"pitch": 60}), (1.0, {"pitch": 64})])
Or load from a MIDI file via
from_midi().Builds a
Score, coercing any(time, kwargs)tuples to events.- extend(other)[source]¶
S.extend(iterable) – extend sequence by appending elements from the iterable
- Return type:
- property end_time: float¶
The latest event’s
time. RaisesValueErrorif empty.Note: this is the latest onset, not the end of any sustained note. Events have no intrinsic duration at the score level — only their instrument knows.
- classmethod from_midi(midi, *, as_notes=True, all_events=False)[source]¶
Parses a MIDI file into a
(score, metronome)pair.Every emitted
Eventhastimeset to its absolute MIDI tick andkwargs["mtype"](message type) identifying the kind of event. The exact kwargs schema depends on the flags below.When
as_notes=True(default), eachnote_on/note_offpair across all tracks is collapsed into a single"note"event with:kwargs["mtype"]— the literal string"note".kwargs["duration"]— the note’s duration in seconds.kwargs["duration_ticks"]— the note’s duration in MIDI ticks.kwargs["pitch"]— MIDI pitch (0–127).kwargs["velocity"]— MIDI NOTE_ON velocity (0–127).kwargs["program"]— MIDI program (instrument) number (0–127).kwargs["is_drum"]—Trueif the note is on MIDI channel 10 (zero-indexed: 9), the conventional percussion channel.kwargs["channel"]— MIDI channel (0-15).
When
as_notes=False, raw"note_on"and"note_off"events are emitted as separateEvents instead — each withkwargsequal to the underlyingmidomessage’s attributes (with"type"renamed to"mtype") sans its delta-timefield.When
all_events=True, all other MIDI messages (tempo changes, program changes, control changes, …) are also emitted with the same barebonesmsg.dict()-style kwargs.Pass the returned
metronometorender()so ticks are converted to seconds using the file’s actual tempo map.- Parameters:
midi (
Union[str,PathLike,IO,MidiFile]) – A path, a file-like object, or an already-parsedmido.MidiFile.as_notes (
bool) – IfTrue(default), collapsenote_on/note_offpairs into a single"note"event. IfFalse, emit them as separate events.all_events (
bool) – IfTrue, also emit non-note MIDI messages (set_tempo,program_change,control_change, …). Defaults toFalse(note events only).
- Return type:
- segment(*, offset=None, duration=None, relativize=True)[source]¶
Returns the events whose
timefalls in[offset, offset+duration).The right boundary is open with a small epsilon (
offset + duration - 1e-9) to suppress floating-point off-by-ones at exact-end matches.- Parameters:
offset (
Optional[float]) – Lower bound on event time (inclusive). Defaults to the beginning of the score (0.0).duration (
Optional[float]) – Length of the window. Defaults to the rest of the score (no upper bound).relativize (
bool) – IfTrue(default), shift every kept event’stimeby-offsetso the returned score begins at 0. IfFalse, keep the original timestamps.
- Return type:
- render(instrument, metronome=None)[source]¶
Renders this score to a single mixed
Audio.Each event is rendered by calling
instrument(**event.kwargs)and mixed in at its onset time. Onsets are interpreted as ticks (and converted viametronome.tick_to_seconds) when a metronome is given, otherwise as seconds.The instrument is time-agnostic: it receives only the event’s
kwargs, not itstime. To vary an instrument by onset, copy the time intokwargswhen building the score. For per-event dispatch (e.g. different instruments for drums vs. pitched notes), branch inside the instrument.All instrument outputs must share a
sample_rateand anum_channels; otherwiseValueErroris raised. An empty score returns a zero-length, monoAudio.
- class pyquist.score.MIDIMetronome(midi)[source]¶
Bases:
MetronomeA metronome that follows a MIDI file’s PPQ resolution and tempo map.
1 tick = 1 MIDI tick (a PPQ subdivision of a quarter note). Tempo changes inside the file are honored, so
tick_to_secondsis generally non-linear across the file.- Parameters:
midi (
Union[str,PathLike,IO,MidiFile]) – A path, a file-like object, or an already-parsedmido.MidiFile.