Skip to content
David Anderson edited this page Jul 26, 2026 · 24 revisions

The nscore module lets you create data structures representing musical scores.

from numula.nscore import *

class Note

Represents a note, with a specified start time, duration, pitch, and volume.

Note(
    time: float,
    dur: float,
    pitch: int,
    vol: float,
    tags: list[str] = []
)

Creates a Note object. Parameters:

  • time: The note's start time in "score units" in which 1 is the duration of a 4/4 measure; i.e. a 'beat' is 0.25 score units. The default tempo is 60 beats per minute.
  • dur: The note's duration (i.e. the amount of time it sounds) in score units.
  • pitch: The note's pitch in the MIDI scale (0..127, 60 = middle C).
  • vol: The note's volume, 0..1
  • tags: An optional list of text "tags"; these and other note attributes can be used used by nuance functions to select groups of notes.

If you're entering a score, you typically don't create Note objects directly. Instead, use a shorthand notation that creates them for you.

class Score

Represents a set of notes, pedal applications, and measures.

Score(tempo: float = 60)

Creates a Score. tempo is in beats per minute. To vary the tempo within the piece, use the timing control functions.

Score.insert_note(note: Note)

Add the given note to the score.

Score.write_midi(filename: str)

Generate a MIDI file for the score.

We can now create some music:

import random

ns = Score()
for i in range(200):
    time = random.uniform(0, 10)
    dur = random.uniform(.1, 1)
    pitch = random.randrange(40, 80)
    vol = random.uniform(.1, .8)
    ns.insert_note(Note(time, dur, pitch, vol))
ns.write_midi("random.midi")

This plays 200 notes randomly spaced over 40 seconds (10 4-beat measures at quarter=60), with random duration, pitch, and volume.

Listen

Other members of Score:

Score.append_note(note: Note)

A Score has a "current time", initially zero. append_note() adds a note starting at that time. It doesn't change the current time.

Score.advance_time(dt: float)

Add dt to the current time. dt may be positive or negative.

Score.insert_score(
    score: Score,
    t: float = 0,
    tag: str = ''
)

Merge another Score into this one, starting at time t. The other score's notes, pedals, and measure boundaries are added to this Score, with t added to their start times. If tag is specified, tag the added notes.

Score.append_score(
    score: Score,
    tag = ''
)

Append the given Score to this one. If tag is specified, tag the added notes.

Score.append_scores(
    scores: list[Score],
    tag = ''
)

Insert the given scores into this Score, starting at the current time. Then advance the current time by the duration of the longest Score.

Score.tag(tag: str)

Add the given tag to all notes in the score.

Score.trim(
    t0: float,
    t1: float
)

Trim the score to the time range [t0...t1].

print(ns)

prints the score's notes,pedals, and measure boundaries in human-readable form.

class Measure

Represents a measure (i.e. a metric unit).

Measure(
    t: float,
    dur: float,
    type: str
)
  • t: start time of the measure
  • dur: duration of the measure
  • type: a string of your choosing describing the metric structure of the measure.

For example:

Measure(0., '2+2+3/8')

could represent a measure of 2+2+3 eighth notes, starting at time 0.

Score.insert_measure(m: Measure)

Add a Measure object to a Score.

Score.append_measure(
    duration: float,
    type: str
)

Append a measure of the given duration and type to a Score.

Measures are optional. They provide a framework for nuance based on position in the measure. For example, if you've added Measures to a score you can do things like:

Score.vol_adjust(
    0.9,
    lambda n: n.measure_type=='4/4' and n.measure_offset in [1, 2, 3]
)
Score.vol_adjust(
    0.8,
    lambda n: n.measure_type=='4/4' and n.measure_offset not in [0, 1, 2, 3]
)

attenuates the volume of notes in 4/4 measures based on their position:

  • The downbeat is not attenuated
  • The 2nd, 3rd, and 4th beats are attenuated by .9
  • Other notes are attenuated by .8

Ornaments

You could express ornaments (trills, mordents, turns, etc.) directly, as lists of notes. However, it's useful to have an 'ornament' construct that

  • does the timing calculations for you
  • generates tags so that you can add nuance (timing, volume, articulation) to specific ornaments, or to ornaments of a particular type.

Initial ornaments

Numula supports a class of ornaments called 'initial ornaments'. Such ornaments are described by

  • a list of pitches P1...Pn
  • a start time T
  • a 'total duration' A
  • an 'ornament duration B <= A
  • a flag before indicating whether the ornament is before or after the start time.

The timing semantics are as follows:

If before is True, B must be strictly less than A. Notes with pitches P1...Pn-1 are played, uniformly spaced, in the interval from T-B to T. A note with pitch Pn and duration A is played at T.

If before is False and B < A, notes with pitches P1...Pn-1 are played, uniformly spaced, in the interval from T to T+B. A note with pitch Pn is played at T+B, with duration A-B.

If before is False and B = A, notes with pitches P1...Pn are played, uniformly spaced, in the interval from T to T+B.

In the first two cases, the first n-1 notes are called 'ornament notes' but the last note is not. In the last case, all notes are ornament notes.

An ornament is not initial if, for example,

  • the rhythm changes, e.g. a triplet at the end of a trill;
  • the ornament (e.g. a turn) is in the middle of the main note.

Python API

To add an ornament to a Score:

score.append_ornament(
    pitches:list[int], before:bool,
    orn_dur:float, total_dur:float, tags:list[str]
)

pitches: the pitches in time order.

before: whether the ornament is before the beat.

orn_dur: the duration of the ornament notes.

total_dur: the duration of the ornament notes plus the main note.

tags: a list of tags associated with the ornament. This might include a unique ID for the ornament and/or a name for a group of ornaments.

append_ornament() adds notes to the score. Each note is tagged with the ornament's tags. It also makes an 'ornament object' and adds it to a list in the score. The object r contains:

r.t0: the start time
r.orn_dur: the duration of the ornament notes
r.total_dur: the total duration
r.notes: the ornament notes
r.tags

Adding nuance to ornaments

You can add nuance (timing, volume) to ornaments. Suppose, for example, that in all ornaments tagged 'foo', you want ornament notes to speed up by a factor of two during the ornament, without changing its duration. You could do this as follows:

for orn in filter(lambda x: 'foo' in x.tags, ns.ornaments):
    pft = sh_tempo('40 1/1 80')
    pft.scale_dur(orn.orn_dur)
    ns.tempo_adjust_pft(
        pft,
        orn.t0,
        lambda n: n in orn.notes,
        normalize = True
    )

Note that:

  • the duration of the PFT (1 measure) is scaled to the duration of each ornament;
  • the tempo adjustment is normalized so that the duration of the ornament is not changed.

Shorthand notation of ornaments

Numula's shorthand score notation lets you express ornaments compactly.

Clone this wiki locally