Patterns
pattern is an array of positions. Each position is a JSON object that must
be satisfied by one onset of the driving
feature before the search moves on
to the next position and the next onset. A 3-position pattern only matches where
3 consecutive onsets each satisfy their own position, in order.
"pattern": [
{ "deg": "3" },
{ "deg": "2" },
{ "deg": ["1", "3"] }
]
Values
A key's value can be a single string, or an array of strings. An array means "any of these". The position is satisfied if the actual value equals any entry in the list:
{ "deg": ["1", "3"] }
matches wherever the scale degree is 1 or 3.
A plain JSON boolean is accepted too and coerced to the strings
"true"/"false", so { "fermata": true } and { "fermata": "true" } are
equivalent.
Wildcards
The literal value "*", anywhere in a key's value/list, means "match anything".
On the driving feature it's how you require an onset to exist without
constraining what it is:
"pattern": [
{ "fermata": "true" },
{ "kern": "*" }
]
finds every fermata-marked note followed immediately by any next note, i.e. every place a fermata isn't the very last note of a voice.
On another spine of the position, "*" asks for a value: { "hint-14": "*" } matches only
where the soprano sounds, { "!hint-14": "*" } only where it rests.
Checking several features at once
A position isn't limited to one key. Every key present must match (an implicit AND across keys), and a key doesn't have to be the driving feature. It can reference any feature, looked up on the same line the driving feature's onset landed on:
{
"feature": "deg",
"pattern": [
{ "deg": "3", "duration": "4" },
{ "deg": "2", "duration": "4" },
{ "deg": ["1", "3"], "duration": "*", "fermata": "true" }
]
}
This is driven by deg (so it walks scale-degree onsets), but each position
also constrains duration (Humdrum recip notation: "4" = quarter note, "8"
= eighth, etc.) and, on the last position, fermata. Mixing features like this
is how you combine a melodic/harmonic shape with rhythmic or expressive
constraints in a single query.
A duration normally has to be the sounding duration of the single onset its
position landed on, and every position needs an onset of its own. If a note
written out as repeated shorter notes should count as well, see
durationAllowSplitNotes; for the
converse (several positions answered by one longer note) see
durationAllowMergedNotes. And if the
score decorates the line with notes your pattern doesn't mention at all, see
metweightSkipUnclassified.
Negating a feature
Prefix any key with ! to require that it does not match. The negation
applies to the whole value list at once, not to each entry on its own. An
OR-list under ! becomes "none of these", not "not all of these":
{ "!deg": ["3", "5"] }
matches whenever the scale degree is neither 3 nor 5. Negation works
uniformly on any key in a pattern: the driving feature, a cross-referenced
feature, duration, fermata, or the quality-aware mint/fb
matching described on the feature pages.
A practical use, catching deceptive cadences: scale degree 5 followed by a fermata-marked note that is not the first scale degree:
{
"feature": "deg",
"voices": "bass",
"pattern": [
{ "deg": "5" },
{ "!deg": "1", "fermata": "true" }
]
}
Comparisons
A value can also compare the score with a threshold, instead of matching it for
equality. It is written as an object with an operator and a value, and is one
entry of the value list like any string:
{ "duration": { "operator": "gt", "value": "4" } }
matches every note that is longer than a quarter note.
operator | Means |
|---|---|
"greaterThan", "gt" or ">" | greater than |
"greaterThanOrEqual", "gte" or ">=" | greater than or equal |
"lessThan", "lt" or "<" | less than |
"lessThanOrEqual", "lte" or "<=" | less than or equal |
What "greater" means depends on the key. Only these keys can be compared, and
value is always a single string:
| Key | Greater is | value is |
|---|---|---|
kern | a higher note | a pitch: "g", "f#", "BB-" |
duration | a longer note | a recip duration: "4", "4.", "8" |
mint | a wider interval | an interval size: "3", "m6", or with a direction: "+3", "-m6" |
hint-<pair>, hint-<voice> | a wider interval | an interval size: "10", "M10" |
The other keys (deg, fb, metweight, fermata, phrase) have no order to
compare in, and a query that tries is rejected. See the pages of
kern,
mint and
hint for the details of each.
Everything else about a value stays as it is. A comparison can sit in an OR-list
next to plain values, and a ! in front of the key negates the whole list:
{ "mint": [{ "operator": "gt", "value": "+3" }, "P1"] }
matches an upward leap of more than a third, or a repeated note, and
{ "!mint": { "operator": "gt", "value": "3" } }
matches an interval of no more than a third, either way, and, because it negates the whole position, also the first note of a voice, which has no interval at all.
Ranges
A list is an OR, so it cannot narrow a position down. All keys of a position have to hold, though, and a key may appear once as itself and once negated. That makes a range: the lower end under the key, and what lies beyond the upper end under its negation. An upward leap of a fourth or a fifth:
{
"feature": "mint",
"voices": "soprano",
"pattern": [
{
"mint": { "operator": "gte", "value": "+4" },
"!mint": { "operator": "gt", "value": "+5" }
}
]
}
Referring to another voice
For kern and duration, a value can name another voice instead of stating a pitch or
a length: "@2" stands for what voice 2 sounds on the same line. It can be a plain
value, which asks for equality, or the value of a comparison.
With a comparison, the note of the voice being searched is held against the note of the other voice. This finds the alto singing higher than the soprano:
{
"feature": "kern",
"voices": "alto",
"pattern": [{ "kern": { "operator": "gt", "value": "@4" } }]
}
A reference is only checked where a note of the searched voice begins, so this query does not find the places where the soprano moves down below a held note of the alto. Finding all of them takes a second query that searches the soprano, and the two are run together as one array of queries (see What is compared with what below).
As a plain value, a reference asks for equality. This finds the tenor singing the very same note as the soprano, in the same register, which means a unison:
{
"feature": "kern",
"voices": "tenor",
"pattern": [{ "kern": "@4" }]
}
With kernIgnoreOctave the register no longer counts, and
the same query finds the tenor doubling the soprano's pitch class, a unison or an octave
or two below, at a fermata:
{
"feature": "kern",
"voices": "tenor",
"pattern": [{ "kern": "@4", "fermata": true }],
"kernIgnoreOctave": true
}
The same works for the length of a note. This finds the tenor notes that are as long as the note the soprano sings with them, followed by one with a fermata:
{
"feature": "kern",
"voices": "tenor",
"pattern": [{ "duration": "@4" }, { "duration": "@4", "fermata": true }]
}
| Reference | Stands for |
|---|---|
"@1" … "@4" | that voice (1 is the bass, 4 the soprano) |
"@below", "@above" | the neighbour voice below or above the one searched |
"@anyBelow", "@anyAbove" | any of the voices below or above: one of them is enough |
"@allBelow", "@allAbove" | all of the voices below or above |
"@anyOther", "@allOther" | any, or all, of the other three voices |
So a voice that sings below any of the voices under it, a voice crossing, is found in all voices at once:
{
"feature": "kern",
"voices": "all",
"pattern": [{ "kern": { "operator": "lt", "value": "@anyBelow" } }]
}
- A reference is looked up on the line of the note being matched. A voice that holds a note there counts with that note.
- For
kernit is the pitch of that note. A plain"@2"is the same note in the same register, or withkernIgnoreOctavethe same pitch class. Fordurationit is how long that note sounds, tied notes counted whole. - A rest has no pitch, so nothing is higher, lower or equal to it. A negated comparison
(
!kern) therefore also matches wherever one of the two voices rests. - A voice is never its own reference, so
"@below"of the bass and the other empty groups match nothing. Several references in a list are an OR. For all of them, use the@all…form. - Only
kernanddurationtake a reference. A negateddurationreference cannot be combined withdurationAllowSplitNotesordurationAllowMergedNotes.
What is compared with what
A reference is looked up when a note of the voice being searched begins, and the note is held
against whatever the other voice sounds at that moment, including a note it is still
holding. Only the starts of notes in the voices named in voices are ever looked at. So it
matters which voice is searched and which voice is the reference, even when both queries
sound like the same question. Compare these two, which both look for a voice below the bass:
{
"feature": "kern",
"voices": "234",
"pattern": [{ "kern": { "operator": "lt", "value": "@1" } }]
}
{
"feature": "kern",
"voices": "1",
"pattern": [{ "kern": { "operator": "gt", "value": "@anyAbove" } }]
}
- The first searches the tenor, alto and soprano. It finds the notes of these voices that begin below the note the bass sounds at that moment, so the upper voice has to be the one that moves. The result is a note of voice 2, 3 or 4.
- The second searches the bass. It finds the bass notes that begin above the note of at least one voice over it, so the bass has to be the one that moves. The result is always a bass note, and it does not say which voice it crossed, only that there is one.
In chor005 a voice sounds below the bass at three moments, and each query sees only some of them:
| Where | What happens | First query | Second query |
|---|---|---|---|
| measure 8, last eighth | the bass moves up to A over the held G of the tenor | ✔ | |
| measure 9, first beat | the bass moves to B, the tenor to G, at the same time | ✔ | ✔ |
| measure 9, second half of beat 3 | the tenor moves to B below the held C of the bass | ✔ |
The queries are written as one array to find all three places:
[
{
"feature": "kern",
"voices": "234",
"pattern": [{ "kern": { "operator": "lt", "value": "@1" } }]
},
{
"feature": "kern",
"voices": "1",
"pattern": [{ "kern": { "operator": "gt", "value": "@anyAbove" } }]
}
]
Putting it together
A position, in full generality, is:
{
"<feature or !feature>": "<value>" | <comparison> | "*" | ["<value>" | <comparison>, …],
"<feature or !feature>": "…"
}
where a <comparison> is { "operator": "<operator>", "value": "<value>" }, and a
<value> of kern or duration may also be a voice reference. A position like
this is repeated once per position in the pattern array. See
Features for what a "value" looks like for each specific
feature, and Example Queries for complete, runnable queries
built this way.