Shorthand notations - davidpanderson/Numula GitHub Wiki
Describing a substantial piece of music - the score and the nuance - can involve lots of information. Numula lets you represent this information as Python function calls and data structure declarations, but this can involve huge amounts of typing.
To reduce typing, Numula lets you encode the same information in several "shorthand" notations - character strings that are parsed and converted to data structures.
For example, instead of
ns = Score()
ns.append_note(Note(60, 0, .25))
ns.append_note(Note(62, .25, .25))
ns.append_note(Note(64, .5, .5))
you can write
ns = sh_score('c d 1/2 e')
These produce identical Score
objects:
quarter note C5 and D5, followed by half note E5.
Numula provides shorthand notations for:
- Scores (as in the above example)
- Volume control
- Timing control
- Pedal control
The notation is different in each case. However, all the notations share the following features:
*4 ... *
Repeats the given section 4 times (or whatever number is given).
This construct can be nested.
Python has several ways to do substitution in strings. These are handy for shorthand strings; they let you "factor out" parts of shorthand strings that occur repeatedly.
For example, consider
sh_vol('pp 2/4 mf 2/4 pp 4/4 mf 4/4 pp')
This defines a pair of swells from pp to mf and back, over 4 and 8 beats. Suppose you want to start from ppp instead of pp. You'd have to manually change the 3 occurrences of 'pp'.
Instead, you can use Python's f-string mechanism:
v0 = 'pp'
v1 = 'mf'
sh_vol(f'{v0} 2/4 {v1} 2/4 {v0} 4/4 {v0} 4/4 {v1}')
This lets you change the limits of the swells all at once.
The text within {}
is interpreted as Python code.
It can have expressions, function calls, etc. as well as variables.
You can also use substitution to reduce typing, e.g.
cmaj = '[c5 e g]'
ns = sh_score(
f'{cmaj} f e {cmaj} a b {cmaj}'
)
denotes 3 C major chords, separated by single notes.
For long shorthand strings, it's handy to include measure numbers, which are denoted
|n
For example:
ns = sh_score(' \
|1 1/8 c d e f e d c e \
|2 a g f e d e f g \
|3 ... \
')
When you want to fix or change something, this makes it easy to find it in the string.
Measure numbers can also be used to check timing correctness. Consider the following shorthand description of a volume function:
rhv = sh_vol(' meas2/4 \
|20 *2 *3 ppp 3/4 p 1/4 ppp * 2/4 p 2/4 ppp * \
|36 *2 [ pp 8/4 p * \
|44 [ pp 6/4 p 2/4 pp 4/4 pp \
|50 \
')
This involves lots of time intervals (denoted 3/4, 1/4 etc.). If one of them is wrong or missing, the function will be out of phase from that point on. This type of error can be difficult to track down.
To help locate such errors, Numula keeps track of the sum of the time intervals.
Each time it sees a measure number it checks that the sum of times
matches the number of measures.
To do this it needs to know the duration of a measure;
this is specified by the meas2/4
.
Timing errors are reported as:
Traceback (most recent call last):
...
File "C:\Users\davea\Documents\Personal\Music\Numula\notate_nuance.py", line 91, in vol
comment(t, dt, mdur)
File "C:\Users\davea\Documents\Personal\Music\Numula\notate.py", line 105, in comment
raise Exception('Inconsistent measure number: %f != %f'%(m1, m2))
Exception: Inconsistent measure number: 44.000000 != 44.250000
Measure duration can change. For example:
ns = sh_score(' meas4/4 \
|1 1/4 c d e f \
|2 meas3/4 c d e \
|3 meas4/4 c d e f \
')
This represents a score with a 3/4 measure between two 4/4 measures.
Note: each new measure duration (e.g. meas3/4
) must be immediately preceded by a |n
.
A shorthand can have items before the first measure boundary, e.g. upbeats. For example:
ns = sh_score(' meas4/4 \
1/4 b
|1 c d e f \
')
In this case duration-checking begins at the first measure boundary.
If a piece has fixed measure duration, you can indicate it with
measure_duration(4/4)
This duration will be used for subsequent shorthands
(and you can omit the meas
items from them).
To error-check time intervals in the last measure, include a final measure number, following the last time interval.
If a pipe character is followed by non-numeric text, or no measure duration is specified, it is treated as a comment and is ignored.
Each shorthand notation is like a mini programming language, and there can be syntax errors. Such an error produces output like
error in shorthand: - 1/4 + 1/8 + * 1/4 - 4/4
Traceback (most recent call last):
File "C:\Users\davea\Documents\Personal\Music\Numula\test_nuance.py", line 127, in <module>
test_ped()
File "C:\Users\davea\Documents\Personal\Music\Numula\test_nuance.py", line 123, in test_ped
ns.pedal_pft(sh_pedal('- 1/4 + 1/8 + * 1/4 - 4/4'))
File "C:\Users\davea\Documents\Personal\Music\Numula\notate_nuance.py", line 241, in pedal
items = expand_iter(items)
File "C:\Users\davea\Documents\Personal\Music\Numula\notate.py", line 52, in expand_iter
raise Exception('unmatched *')
Exception: unmatched *
The first line shows the token where the error happened (in Idle this is shown in red) and 5 tokens on either side. This is hopefully sufficient for locating it in the notation string. The stack trace shows where the error happened in the code.