source('vignette_header.R') par(mar=c(1,1,1,1)) rhythmFunctions <- humdrumR:::rhythmFunctions
As a computational musicology toolkit, r hm
's tools for analyzing and manipulating rhythmic (timing) information are just about the most important tools in the toolbox.
For the most part, r hm
's rhythm tools are focused on the Western notions of rhythm and meter, but the tools can easily be generalized to more diverse notions of rhythm and time.
The fundamental unit of rhythm is a "duration"---a span of time.
r Hm
defines a suite of rhythm functions, notably recip()
and duration()
.
These functions all work in essentially the same way: they take in an input argument and output rhythm (duration) information in their own particular format.
For example,
input <- c('4.c', '8d', '4e', '2.g') duration(input) recip(input) notehead(input) quarters(input)
Notice that the functions all recognize the rhythm part of these input tokens, ignoring the non-rhythm (pitch) information.
If you want to keep the non-rhythm part of your tokens, use the inPlace
argument:
input <- c('4.c', '8d', '4e', '2.g') duration(input, inPlace = TRUE) recip(input, inPlace = TRUE) notehead(input, inPlace = TRUE) quarters(input, inPlace = TRUE)
The cool thing is that each of these functions can read any of the other function's output. So you can do things like:
recip(0.375) quarters('4.') quarters(0.375) # # recip("𝅘𝅥 𝅭 ")
The complete list of basic rhythm (duration) functions is:
pfs <- rapply(humdrumR:::rhythmFunctions, \(func) paste0(' + `', ifelse(humdrumR:::.names(func) == '', func, paste0(humdrumR:::.names(func))), '`', ifelse(humdrumR:::.names(func) == '', '', paste0(' (', func, ')'))), how = 'list') pfs <- lapply(pfs, \(top) Map(\(name, pf) paste(c(paste0(' + *', name, ' duration representations*'), pf), collapse = '\n'), names(top), top)) pfs <- Map(\(name, l) paste(c(paste0('+ **', name, ' duration representations**'), unlist(l)), collapse ='\n'), names(pfs), pfs) cat(unlist(pfs), sep = '\n')
The global documentation for all the rhythm functions can be seen by calling ?rhythmFunctions
.
You can also call documentation for any individual function, like ?recip
.
The rhythm functions from the previous section have a few shared arguments that they all understand: scale
and unit
.
The scale
and unit
arguments are conceptually different, but can be used to achieve very similar transformations, so it can be easy to confuse them.
The scale
argument is the easiest to understand: it simply (re)scales the output duration of the function.
Would you like to augment durations? Perhaps doubling their length?
Simply specify scale = 2
.
recip(c('8','4','4.','2'), scale = 2)
Or maybe you'd like diminish the durations, by cutting them in thirds?
recip(c('8','4','4.','2'), scale = 1/3)
Unlike scale
, which controls the output scale, unit
controls the input scale.
This is most useful with numeric values.
What do we mean by "controls the input scale"?
Imagine we have a bunch of duration values as numbers, like c(1, 1.5, 2, 1, 0.5, 1)
.
But what unit is these numbers counting?
By default, r hm
treats them as whole notes, so 0.5
is a half note, right?
However, we might prefer to think of these numbers as units of quarter notes---a very common approach.
This is what the unit
argument is for.
Because it is an argument for the rhythm parser/interpreter, you must pass unit
through the parseArgs
argument, which must be a named list of values:
input <- c(1, 1.5, 2, 1, 0.5, 1) recip(input) recip(input, parseArgs = list(unit = '4'))
As you can see, you can achieve the same difference in output using the scale
argument (scale = 1/4
), but the unit
argument is a little bit of a different way of thinking about it.
In **kern
data, some durations correspond to rests in the music, rather than notes, indicated by an "r"
.
In many analyses, we want to ignore rests and just consider the durations (timespans) between notes, ignoring the presence of rests.
These are called inter-onset-intervals, or IOIs.
The ioi()
function can be used to convert duration data that includes rests, into IOIs.
For example:
melody <- c('8g','8f#', '8g', '8r', '8g', '8f#','8e','8d', '4d#', '8r', '8f#', '4b', '4r') ioi(melody)
What did this do?
8g
becomes a 4g
4d#
becomes 4.d#
."."
).NA
, because there is no more onsets after it to create a inter-onset-interval.The last step here makes conceptual sense, but may not be what you want!
Maybe you'd like the interval between the last onset and the end of the data?
For that, specify finalOnset = TRUE
:
ioi(melody, finalOnset = TRUE)
When used in a call to with()
or within()
, ioi()
will automatically be applied within Files/Spines/Paths.
A similar case to inter-onset-intervals are ties.
In **kern
, the [
, _
, and ]
tokens are used to indicate groups of durations that are actually played as a single, longer duration.
The sumTies()
function will automatically sum the durations of tied notes:
melody <- c('[2d', '8d]', '8d', '8f#', '8g#', '4.a', '[8b-', '2b-_', '4b-]', '4a', '2g#') sumTies(melody)
The tie tokens are removed, and tied-notes are replaced by null tokens ("."
).
Traditional musical scores, and most humdrum data, encode rhythmic information as duration, like what we worked on in the previous section.
However, we often want to think about when musical events are happening, relative to a fixed reference: usually, relative to the beginning of the piece.
We can compute a timeline from sequences of duration values, using the timeline()
function.
Let's say we have a melody like:
row <- c('4c', '4c', '6c', '12d', '4e', '6e','12d', '6e', '12f', '2g', '12cc', '12cc', '12cc', '12g', '12g', '12g', '12e', '12e', '12e', '12c', '12c', '12c', '6g','12f', '6e', '12d','2c')
Let's look at a timeline for that melody:
timeline(row)
The first note (4c
) occurs at zero (the beginning) of the timeline; the last note (2c
) lands 3.5
whole-notes later; etc.
If we combine this with a call to semits()
(check out the [Pitch and Tonality][PitchAndTonality.html] guide) we can even make a plot of the melody:
plot(x = timeline(row), y = semits(row, Exclusive = 'kern'), main = 'Row Row Row Your Boat')
The timeline()
function has extra special functionality when working on actual humdrum datasets.
It will automatically (unless told not too) calculate timelines separately within each File/Spine/Path of a dataset, ignoring multiple stops.
For example:
chorales <- readHumdrum(humdrumRroot, 'HumdrumData/BachChorales/chor.*.krn') chorales |> timeline()
So we get a separate timeline for each spine in each piece.
Note:
timeline()
ignores all multi-stops. If you have spine paths in your data, you should look into the expandPaths options.
You'll notice that our timelines all start from zero, which makes sense of course.
But in some cases, you might want to have the timeline start at a different value.
For example, "Row Row Row Your Boat" is a round, so we'd want the second entrance of the round to start at the second measure.
We can do this with the start
argument to timeline:
timeline(row, start = 2)
The pickup
argument gives us another option to control where our timeline starts.
In many datasets, the first few events in the music are an anacrusis or "pick up"; we'd generally like our timeline to start after the pickup.
To make this happen, the pickup
argument can be passed a logical
vector which is the same length as the input (x
):
Any TRUE
values at the beginning of the vector are considered a pickup;
Thus, the first FALSE
value in the pickup
vector is chosen as the start of th timeline.
For example, let's say we have the melody
melody <- c('8c', '8d','2e', '2f','1e')
but the first two notes are a pickup.
Since we can see that the pickup has a duration of .25
(quarter-note), we could use the start
argument to do this:
timeline(melody, start = -.25)
Now 0
is on the downbeat, and events before that start
are negative on the timeline!
To do this with the logical
pickup
argument, we could do this:
timeline(melody, pickup = melody != '2e')
This might seem less intuitive!
However, this approach can be very useful when working with actual humdrumR datasets.
In many humdrum datasets, pickup measures are indicated by have barlines labeled =0
or =-
.
When r hm
reads a file, it counts the barlines and creates a field called Bar
, and it numbers pickup measures as zero (or negative numbers, if there are more than one).
This means pickups will have Bar < 1
.
So, in our Bach chorales:
chorales |> timeline(pickup = Bar < 1)
Now the 0
in our timelines corresponds to the first downbeat of each spine, in each file.
The key advantage is that this will work even if different pieces in the corpus have pickups of different lengths, and even if there is no pickup.
The timestamp()
function is a special variant of timeline()
which outputs a timeline in clock-time, using the dur()
format.
In order to do this timestamp()
needs to know a tempo: by default, r hm
will pass the BPM
field from humdrum data (if there is one) to timestamp()
.
If the BPM
argument is not provided, the default is 60 beats-per-minute.
Ok, the timeline()
gives us a timeline in whole-note units, giving fractional (decimal) output.
Often in music, we want to know how many beats have elapsed at a given time, rather than the exact time position.
For this, use the timecount()
, and its unit
argument.
The default unit
is a whole-note, so timecount()
will count which whole note you are on:
row <- c('4c', '4c', '6c', '12d', '4e', '6e','12d', '6e', '12f', '2g', '12cc', '12cc', '12cc', '12g', '12g', '12g', '12e', '12e', '12e', '12c', '12c', '12c', '6g','12f', '6e', '12d','2c') timecount(row)
Let's try quarter-notes instead:
timecount(row, unit = '4')
timecount()
will even work for irregular beat patterns, which must be entered as a list.
For example, the meter 7/8 is often played as three beats, with the last beat being longer than the first two: a pattern line 4 4 4.
.
timecount()
can count these irregular beats!
seven8 <- c('4', '4', '8','8','8','4','4','4','8','4','4','8','8', '8', '4') timecount(seven8, unit = list(c('4', '4', '4.')))
This could be handy for counting subdivisions in swing time!
The counterpart to timecount()
is subpos()
.
When we count beats, some notes don't actually land on the beat, but somewhere "inside" the beat---in other words, between beats.
subpos()
will tell us how far from the beat each attack is; the unit will be whole-notes, unless
we pass a scale
argument to change the scale.
Let's look at our last few examples again, but using subpos()
:
subpos(row, scale = 12) subpos(row, unit = '4', scale = 12) subpos(seven8, unit = list(c('4', '4', '4.')), scale = 8)
The timecount()
and subpos()
commands are great if we want count in a single beat/measure unit.
To take things to the next level(s), we need to consider musical meter.
From the point of view of r hm
, "meter" is a set of multiple "beat levels" occurring at the same time (in parallel), with "lower" (faster/shorter) levels nested inside "higher" (slower/longer) levels.
R Hm
defines a number of useful tools for applying metric analyses to rhythmic data.
The first thing we might want to do, is take a sequence of rhythm durations and identify which metric level each onset lands on.
To do this, use metlev()
:
eighths <- c('8', '8', '8', '8', '8', '8', '8', '8') metlev(eighths)
By default, metlev()
is assuming a duple meter for us---basically 4/4 time---so these eight eighth-notes make one measure of 4/4.
The first onset lands on the downbeat, coinciding with the highest level in the meter (as defined by default)---this highest level beat is a whole-note, which is why the output is "1"
(**recip
notation for a whole-note).
Every odd eighth-note falls down at the eighth-note beat level, and is labeled "8"
.
4/4 beats 2 and 4 (the back beats) fall on the quarter-note level, and labeled "4"
.
Finally, beat 3 is on the half-note level ("2"
).
If you prefer to have your levels simply numbered from highest to lowest, use metlev(..., value = FALSE)
:
eighths <- c('8', '8', '8', '8', '8', '8', '8', '8') metlev(eighths, value = FALSE)
The whole-note level is 1
and (in this case) the eighth-note level is 4
.
Along with metlev()
, r hm
provides to complementary functions called metcount()
, metsubpos()
.
As you might guess, these are metric parallels of timecount()
and subpos()
.
The metcount()
function counts beats at a particular beat level (the level
argument) within the highest level in the meter.
The metsubpos()
function calculates the rhythm offset between each onset and the nearest beat in the meter.
You can control the meter used by metlev()
using the meter
argument.
The simplest thing is to pass the meter as a humdrum time-signature interpretation:
eighths <- c('8', '8', '8', '8', '8', '8', '8', '8') metlev(eighths, meter = 'M3/4')
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.