-
Notifications
You must be signed in to change notification settings - Fork 3
Scores
The nscore module lets you create data structures representing musical scores.
from numula.nscore import *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.
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.
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.
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
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.
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
beforeindicating 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.
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
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.
Numula's shorthand score notation lets you express ornaments compactly.