Serialist-Grammar provides a concise syntax for the expression and transformation of rows in serial music composition.
Install with npm:
npm install --save serialist-grammar
Serialist-Grammar includes a pre-compiled parser as a CommonJS module:
-
Require the parser and process input according to the syntax defined below
var SerialistGrammar = require('serialist-grammar'); var output = SerialistGrammar.parse('flat pc(0 3 1 8 7) @r');
-
Map the parser output to your target application
If you need the parser in a different module format, you can compile it with PEG.js:
- Install PEG.js
- Use PEG.js to generate a parser from
serialist-grammar.pegjs
- Use the parser to process input according to the syntax defined below
- Map the parser output to your target application
Serialist-Grammar accepts rows for pitch class, octave, dynamics, duration and arbitrary data. Row types may be placed in any order and used as many times as you want.
Pitch class rows consist of whitespace-delimited pitch classes, surrounded by parenthesis and prefixed with pc
. Pitch classes are expressed in the traditional notation — digits 0-9
, t
for 10
and e
for 11
.
pc(0 1 4 t) // parsed as [0, 1, 4, 10]
Octave rows consist of whitespace-delimited signed integer values, surrounded by parenthesis and prefixed with oct
. 0
should be mapped to an octave around the middle of the pitch range of your target application. Positive and negative values should represent octaves above and below the middle octave, respectively.
oct(0 1 -2 4) // parsed as [0, 1, -2, 4]
Dynamics rows consist of whitespace-delimited float values, surrounded by parenthesis and prefixed with dyn
. Dynamics values are limited to the range 0-1
.
dyn(0.5 0.75 1 1.5) // parsed as [0.5, 0.75, 1, 1]
Duration rows consist of whitespace-delimited float values, surrounded by parenthesis and prefixed with dur
. Duration values should be mapped to a multiple of beat/cycle duration in your target application. (1
is equal to 1 beat, 0.5
is equal to half a beat, etc.) Values must be positive — negative values will result in a parse error and 0
values will be filtered.
dur(1 0.5 0.5 2 0) // parsed as [1, 0.5, 0.5, 2]
Data rows consist of whitespace-delimited numeric values, surrounded by parenthesis and prefixed with any alphanumeric string. Data rows are indended to allow application developers to add support for arbitrary features (e.g. MIDI continuous controller messages).
cc127(32 64 96) // parsed as [32, 64, 96] with the label 'cc127'
Serial-Grammar provides a series of transformations that can be applied to all types of rows. Transformations may be placed in any order and used as many times as you want:
pc(1 4 0 t 7) @r >> 3 [1 4] @i +3 << 1 *2
The example above does the following:
pc(1 4 0 t 7)
: define a pitch class row@r
: use the retrograde form of the row>> 3
: rotate the row forward by 3 positions[1 4]
: shorten the row by slicing between indexes 1 and 4@i
: use the inverted form of the shortened row+3
: add 3 to all pitch classes in the row (transpose +3 semitones)<< 1
: rotate the row backward by 1 position+3
: multiply all pitch classes in the row by 2
Row forms are expressed as the @
symbol followed by r
and/or i
, which indicate the retrograde and inverted forms of the row:
@r
: retrograde@i
: inversion@ri
retrograde followed by inversion (may also be expressed as@r @i
)@ir
inversion followed by retrograde (may also be expressed as@i @r
)
The retrograde form is simply the row in reverse order:
pc(1 2 3) // reverses to pc(3 2 1)
Inversion behaves differently depending on the type of row. Pitch class rows are inverted within the octave, according to the equation (12 - value) % 12
:
pc(1 4 0 t 7) @i // inverts to pc(e 8 0 2 5)
Octave rows are inverted around the middle octave by negating the value:
oct(1 0 -2) @i // inverts to oct(-1 0 2)
Dynamics rows are inverted within the range 0-1
:
dyn(1 0.5 0.25 0) @i // inverts to dyn(0 0.5 0.75 1)
Duration rows are inverted according to the equation 1 / value
:
dur(1 0.5 1.5 2) @i // inverts to dur(1 2 0.6666666666666666 0.5)
Data rows are inverted using negation:
myData(1 0.5 2) @i // inverts to myData(-1 -0.5 -2)
Row rotation is expressed with the >>
operator for forward rotation and the <<
operator for backward rotation, followed by the number of positions to rotate by:
pc(1 2 3 4 5) >> 1 // rotates to pc(2 3 4 5 1)
pc(1 2 3 4 5) >> 2 // rotates to pc(3 4 5 1 2)
pc(1 2 3 4 5) << 1 // rotates to pc(5 1 2 3 4)
pc(1 2 3 4 5) << 2 // rotates to pc(4 5 1 2 3)
The rotation position will wrap within the length of the row, so values greater than the length of the row will still provide useful results:
pc(1 2 3 4 5) >> 7 // equivalent to pc(1 2 3 4 5) >> (7 % 5) or pc(1 2 3 4 5) >> 2 and rotates to pc(3 4 5 1 2)
Slicing a row results in a shortened row that contains the values between the start and end indexes of the slice. A slice is expressed as a start index and optional end index, separated by whitespace and surrounded by square brackets:
pc(1 2 3 4 5) [1 4] // slices to [2 3 4]
Omit the end index to slice from the start index to the end of the row:
pc(1 2 3 4 5) [2] // slices to pc(3 4 5)
Slicing uses Array.prototype.slice, so the value at the end index is not included in the slice.
Serialist-Grammar supports addition (+
), subtraction (-
), multiplication (*
), division (/
) and remainder (%
) operators. Math expressions are evaluated on each value in the row with the following limitations:
- In pitch class rows, the result will be wrapped within the normal pitch class range (
0-11
) and rounded to the nearest integer - In octave rows, the result will be rounded to the nearest integer
- In dynamics rows, the result will be always limited to the range
0-1
- In duration rows, the result will be limited to a minimum value of
0
and0
values are filtered - Division by
0
expressions will be ignored
Examples:
pc(1 4 5 8) + 2 // results in pc(3 6 7 t)
pc(1 4 5 8) - 2 // results in pc(e 2 3 6) after wrapping within 0-11
pc(1 4 5 8) * 2 // results in pc(2 8 t 4) after wrapping within 0-11
pc(1 4 5 8) / 2 // results in pc(1 2 3 4) after rounding to the nearest integer
pc(1 4 5 8) % 2 // results in pc(1 0 1 0)
dyn(0.5 0.25 0.75 2) - 0.5 // results in dyn(0 0 0.25 1) after limiting to 0-1
dur(1 0.5 0.5 2) - 1 // results in pc(1) after limiting to a minimum of zero and filtering zero
dur(1 0.5 0.5) / 0 // results in dur(1 0.5 0.5) because division by zero is ignored
Each sequence of rows may be given an arbitrary identifier consisting of an alphanumeric string prefixed by id:
:
id:sequence1 pc(1 2 5)
Flags may be added to the beginning of the input to control parser behaviour.
Currently, the only supported flag is flat
, which changes the output format. Without the flat
flag, the output closely matches the input. Notice that the two pitch class rows are represented by separate arrays:
id(sequence1) pc(1 2 5) pc(7 8 t) oct(0 2) dyn(0.75 0.5 0.25) dur(1 0.5 0.5)
// Output:
/*
[
[
[
"id",
"sequence1"
],
[
"pc",
[
1,
2,
5
]
],
[
"pc",
[
7,
8,
10
]
],
[
"oct",
[
0,
2
]
],
[
"dyn",
[
0.75,
0.5,
0.25
]
],
[
"dur",
[
1,
0.5,
0.5
]
]
]
]
*/
The flat
flag simplifies the output by formatting each voice as an object with one member per row type. Multiple rows of the same type will be concatenated in the order in which they appear in the input:
flat
id(sequence1) pc(1 2 5) pc(7 8 t) oct(0 2) dyn(0.75 0.5 0.25) dur(1 0.5 0.5)
// Output:
[
{
"id": "sequence1",
"pc": [
1,
2,
5,
7,
8,
10
],
"oct": [
0,
2
],
"dyn": [
0.75,
0.5,
0.25
],
"dur": [
1,
0.5,
0.5
]
}
]
Multiple voices may be defined by separating sequences of rows with a comma and newline. Each voice will appear as a separate object in the output:
flat
id(sequence1) pc(1 2 5) dur(1 0.5),
id(sequence2) pc(3 4 7) dur(0.5 0.25)
// Output:
/*
[
{
"id": "sequence1",
"pc": [
1,
2,
5
],
"oct": [],
"dyn": [],
"dur": [
1,
0.5
]
},
{
"id": "sequence2",
"pc": [
3,
4,
7
],
"oct": [],
"dyn": [],
"dur": [
0.5,
0.25
]
}
]
*/
Serialist-Grammar is very accommodating of whitespace (or lack thereof). All of the following inputs should be valid and produce identical output:
// 1.
id(sequence1) pc(1 2 5) + 2 pc(7 8 t) << 1 oct(0 2) / 2 dyn(0.75 0.5 0.25) [1] dur(1 0.5 0.5) * 2,
id(sequence2) pc(t 1 2) << 2 oct(3 5 4 2) * 3 dyn(1 0.5) - 0.25 dur(0.5 0.25 0.25)
// 2.
id(sequence1) pc(1 2 5)+2 pc(7 8 t)<<1 oct(0 2)/2 dyn(0.75 0.5 0.25)[1] dur(1 0.5 0.5)*2,
id(sequence2) pc(t 1 2)<<2 oct(3 5 4 2)*3 dyn(1 0.5)-0.25 dur(0.5 0.25 0.25)
// 3.
id(sequence1)
pc(1 2 5) +2
pc(7 8 t) <<1
oct(0 2) /2
dyn(0.75 0.5 0.25) [1]
dur(1 0.5 0.5) * 2,
id(sequence2)
pc(t 1 2) <<2
oct(3 5 4 2) *3
dyn(1 0.5) - 0.25
dur(0.5 0.25 0.25)
// 4.
id(sequence1)
pc(1 2 5) +2
pc(7 8 t) <<1
oct(0 2) /2
dyn(0.75 0.5 0.25) [1]
dur(1 0.5 0.5) * 2,
id(sequence2)
pc(t 1 2) <<2
oct(3 5 4 2) *3
dyn(1 0.5) - 0.25
dur(0.5 0.25 0.25)
Serialist-Grammar is made available under the terms of the GNU General Public License v3.0 (or greater).