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: NamedTuple

A timestamped, instrument-agnostic musical event.

time is 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. kwargs is opaque to the score; it is forwarded to the instrument as instrument(**kwargs) at render time, so every key should be a valid Python identifier (e.g., avoid dashes).

Event is a NamedTuple: 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 Event explicitly: Score converts any (time, kwargs) 2-tuple into an Event automatically, so Score([(0.0, {"pitch": 60})]) and Score([Event(0.0, {"pitch": 60})]) are equivalent.

Create new instance of Event(time, kwargs)

time: float

Alias for field number 0

kwargs: Dict[str, Any]

Alias for field number 1

class pyquist.score.Metronome[source]

Bases: ABC

Converts between score-time ticks and wall-clock seconds.

A “tick” here is the abstract time unit a Score uses; different Metronome subclasses define what a tick means concretely (a beat for BasicMetronome, a MIDI PPQ tick for MIDIMetronome).

abstract tick_to_seconds(tick)[source]

Returns the wall-clock time in seconds at which tick occurs.

Parameters:

tick (float)

Return type:

float

abstract seconds_to_tick(seconds)[source]

Returns the score tick at the given wall-clock time.

Parameters:

seconds (float)

Return type:

float

class pyquist.score.BasicMetronome(*, tps=1.0, bpm=None)[source]

Bases: Metronome

A 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 — BasicMetronome just 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 whose time field 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 via tps = bpm / 60 — so BasicMetronome(bpm=120) is exactly BasicMetronome(tps=2.0). When given, bpm takes precedence over tps.

Parameters:
  • tps (Optional[float]) – Ticks per second. Defaults to 1.0 (1 tick = 1 second).

  • bpm (Optional[float]) – Beats per minute. If given, overrides tps (tps = bpm / 60).

Raises:

ValueError – if both tps and bpm are None.

tick_to_seconds(tick)[source]

Returns the wall-clock time in seconds at which tick occurs.

Parameters:

tick (float)

Return type:

float

seconds_to_tick(seconds)[source]

Returns the score tick at the given wall-clock time.

Parameters:

seconds (float)

Return type:

float

class pyquist.score.Score(initlist=None)[source]

Bases: UserList

A list of Event with music-specific helpers.

Score subclasses collections.UserList, so all the standard list operations work and preserve Score type — +, *, slicing, +=, .copy(), .append(), etc.

The time field of each event is interpreted in seconds unless you pass a Metronome to render(), in which case time is treated as ticks and converted via metronome.tick_to_seconds. The metronome is not stored on the score — keep it as a separate variable (or use the (score, metronome) tuple returned by from_midi()).

Construct from any iterable of events. Bare (time, kwargs) tuples are converted to Event automatically, 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.

append(item)[source]

S.append(value) – append value to the end of the sequence

Return type:

None

insert(index, item)[source]

S.insert(index, value) – insert value before index

Return type:

None

extend(other)[source]

S.extend(iterable) – extend sequence by appending elements from the iterable

Return type:

None

property start_time: float

The earliest event’s time. Raises ValueError if empty.

property end_time: float

The latest event’s time. Raises ValueError if 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.

property duration: float

end_time - start_time. Raises ValueError if empty.

classmethod from_midi(midi, *, as_notes=True, all_events=False)[source]

Parses a MIDI file into a (score, metronome) pair.

Every emitted Event has time set to its absolute MIDI tick and kwargs["mtype"] (message type) identifying the kind of event. The exact kwargs schema depends on the flags below.

When as_notes=True (default), each note_on/note_off pair 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"]True if 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 separate Events instead — each with kwargs equal to the underlying mido message’s attributes (with "type" renamed to "mtype") sans its delta-time field.

When all_events=True, all other MIDI messages (tempo changes, program changes, control changes, …) are also emitted with the same barebones msg.dict()-style kwargs.

Pass the returned metronome to render() 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-parsed mido.MidiFile.

  • as_notes (bool) – If True (default), collapse note_on/note_off pairs into a single "note" event. If False, emit them as separate events.

  • all_events (bool) – If True, also emit non-note MIDI messages (set_tempo, program_change, control_change, …). Defaults to False (note events only).

Return type:

Tuple[Score, MIDIMetronome]

segment(*, offset=None, duration=None, relativize=True)[source]

Returns the events whose time falls 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) – If True (default), shift every kept event’s time by -offset so the returned score begins at 0. If False, keep the original timestamps.

Return type:

Score

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 via metronome.tick_to_seconds) when a metronome is given, otherwise as seconds.

The instrument is time-agnostic: it receives only the event’s kwargs, not its time. To vary an instrument by onset, copy the time into kwargs when 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_rate and a num_channels; otherwise ValueError is raised. An empty score returns a zero-length, mono Audio.

Parameters:
Return type:

Audio

class pyquist.score.MIDIMetronome(midi)[source]

Bases: Metronome

A 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_seconds is generally non-linear across the file.

Parameters:

midi (Union[str, PathLike, IO, MidiFile]) – A path, a file-like object, or an already-parsed mido.MidiFile.

tick_to_seconds(tick)[source]

Returns the wall-clock time in seconds at which tick occurs.

Parameters:

tick (float)

Return type:

float

seconds_to_tick(seconds)[source]

Returns the score tick at the given wall-clock time.

Parameters:

seconds (float)

Return type:

float