source('vignette_header.R')

par(mar=c(1,1,1,1))
pitchFunctions <- humdrumR:::pitchFunctions

As a computational musicology toolkit, r hm's tools for analyzing and manipulating pitch data are just about the most important tools in the toolbox. For the most part, r hm's pitch tools are focused on the Western system of tonality, and tools for representing pitches in a tonal context are the focus of this vignette---we also have standard tools for looking at pitch from a Western, 12-tone atonal setting as well.

Pitches and Intervals

r Hm defines a suite of "pitch functions," like kern(), solfa(), interval(), and semits(). These functions all work in essentially the same way: they take in an input argument and output pitch information in their own particular format. For example, let's take a little bit of **kern data:

input <- c('4.c', '8d', '4e', '2.g')

Notice that this data has both rhythmic information (4., 8, 4, 2.) and pitch information (c, d, e, g). What happens if we give this data to our "pitch functions"?:

kern(input)
interval(input)
semits(input)
solfa(input)

Each function correctly reads the **kern pitch information, ignoring the rhythm information, and outputs the pitch information in a different format. This allows us to "translate" between different ways of representing pitch information.

If you want to keep the non-pitch (rhythm) information, use the inPlace argument:

kern(input, inPlace = TRUE)
interval(input, inPlace = TRUE)
semits(input, inPlace = TRUE)
solfa(input, inPlace = TRUE)

The cool thing is that each of these functions can read any of the other functions' output. So you can do things like:

kern('Cb6')

pitch("eee-")

pitch(-4) # semits

kern('-4')

solfa('A#6')

semits('Ab3')

The complete list of basic pitch functions is:

pfs <- rapply(humdrumR:::pitchFunctions, 
                \(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, ' pitch representations*'), pf), collapse = '\n'), names(top), top))

pfs <- Map(\(name, l) paste(c(paste0('+ **', name, ' pitch representations**'), unlist(l)), collapse ='\n'), names(pfs), pfs)
cat(unlist(pfs), sep = '\n')

Each one of these functions represents a different way of representing equivalent pitch information.

Documentation

The global documentation for all the pitch functions can be seen by calling ?pitchFunctions. You can also call documentation for any individual function, like ?kern.

Scale degree

Some pitch representations encode scale degree, which depends on the key. We can use the Key argument to control how our pitch functions interpret key---either translating from degrees to absolute pitches, or vice versa. Pass the Key argument a humdrum key interpretation, like c: (c minor) or A-: (a-flat major).

absoluteInput <- c('c', 'd-', 'e-', 'g', 'a-')
solfa(absoluteInput)
solfa(absoluteInput, Key = 'A-:')

degreeInput <- c('do', 're', 'mi', 'fi', 'so')

kern(degreeInput)
kern(degreeInput, Key = 'A-:')

The Key argument is vectorized (see the R primer if you don't know what that means!); that means that you can actually read information with different/changing keys.

Working with pitch representations

Let's have a look at using pitch functions on real data. Let's load r hm's built-in Bach chorales:

chorales <- readHumdrum(humdrumRroot, 'HumdrumData/BachChorales/.*krn')

These chorales are full of **kern data in the (default) Token field, which can easily be parsed/translated. Maybe we'd like to convert the **kern to semitones. r Hm's pitch functions can all be used "humdrum-style, so using semits() is very easy:

chorales |>
  semits()

We could then, for instance, make a histogram of the semitone values:

chorales |>
  semits() |>
  pull() |>
  hist(main = 'Histogram of semits() in 10 chorales')

Keys

The chorale dataset has key information in its built-in Key field. If we use a pitch function in humdrum- or -tidy style, the Key field is automatically passed as a Key argument to the function! That means if we apply solfa(), we will get the correct scale degrees, given the keys in the data:

chorales |>
  solfa(Token)

Rests

**kern data, like in these chorales, includes rests, indicated by r in place of a pitch. What happens when a pitch function sees a rest in our data? Let's see:

solfa(c('8gg', '8ff', '4ee', '8dd', '8r', '2cc'))

The rest token, is output as a null (.) value---this null . is actually an R NA value. In fact, any token in the input which fails to parse as pitch will return as NA/.. For example:

kern(c('C4', 'X', 'E4'))

Pitch arguments

Since all the "pitch functions" do the same sort of task, they share a number of common arguments. The complete list of these pitch arguments can be found in the ?pitchParsing and ?pitchDeparsing man pages, but the most important ones are described here:

Generic vs Specific pitch

All pitch functions accept logical (TRUE or FALSE) generic and specific arguments. If generic == TRUE, only generic pitch information is returned. This affects different pitch representations in different ways:

The specific argument is simply the opposite of generic, so you have the choice of indicating genric = TRUE or specific = FALSE---they are equivalent.


In code:

input <- c('E', 'G#', 'A', 'B-', 'F#', 'G')

pitch(input, generic = FALSE)
pitch(input, generic = TRUE)

interval(input, specific = TRUE)
interval(input, generic = TRUE)

solfa(input, specific = TRUE)
solfa(input, specific = FALSE)

Simple vs Compound pitch

All pitch functions accept logical (TRUE or FALSE) simple and compound arguments. If simple == TRUE, octave information is stripped from the output, leaving only "simple" pitch information. This affects different pitch representations in different ways:

The compound argument is simply the opposite of simple, so you have the choice of indicating simple = TRUE or compound = FALSE---they are equivalent.


In code:

input <- c('c', 'a', 'dd#', 'ee', 'G', 'GG', 'GG-', 'CCC')

degree(input, simple = FALSE)
degree(input, simple = TRUE)


lilypond(input, compound = TRUE)
lilypond(input, simple = TRUE)

semits(input, compound = TRUE) 
semits(input, compound = FALSE)

solfa(input, compound = TRUE)
solfa(input, compound = FALSE)

interval(input, simple = FALSE)
interval(input, simple = TRUE)

Working with pitch arguments

The generic and simple arguments are useful if you want to tabulate pitch classes, for example:

chorales <- readHumdrum(humdrumRroot, 'HumdrumData/BachChorales/.*krn')


chorales |>
  kern(simple = TRUE) |>
  count() |>
  draw()
chorales |>
  kern(generic = TRUE) |>
  count() |>
  draw()
chorales |>
  kern(simple = TRUE, generic = TRUE) |>
  count() |>
  draw()

Transposition

All of r hm's pitch functions have built-in transposition functionality. You can use this functionality by passing a named list of arguments to the transpose() function, as the transposeArgs argument to the pitch function call, like in this example using the by argument:

kern(c('c', 'd', 'e', 'f', 'f#', 'g'), transposeArgs = list(by = 'M2'))

Another option is to transpose by key. You can the to argument through to tranpose(). Let's say we have music in A major that we'd like to transpose to C major:

Amelody <- c('8A', '8B', '4.c#', '16d', '8c#','8f#', '8e', '8d#', '4e')

kern(Amelody, Key = 'A:', transposeArgs = list(to = 'C:'))

So the Key argument to kern() indicates the key the music is coming from, and the to argument in tranposeArgs indicates where to transpose it to. Since humdrum/tidy functions automatically use Key information from a data set, this makes it very easy to, say, transpose all **kern tokens in your data to C major:

chorales <- readHumdrum(humdrumRroot, 'HumdrumData/BachChorales/.*krn')

chorales |>
  kern(simple = TRUE, transposeArgs = list(to = 'C:')) |>
  count() |>
  draw()

This looks exactly like calling solfa():

chorales |>
  solfa(simple = TRUE) |>
  count() |>
  draw()

Melody and Harmony

Melodic Intervals

What if we want to calculate melodic intervals in each part of a score? This the purpose of the mint() (melodic intervals) command. We can use mint() on any pitch data:

input <- c('d', 'B-', 'A', 'G', 'D', 'd', 'B-', 'G#', 'A', 'f', 'e', 'd')

mint(input)

If you use mint() in humdrum- or tidy- style on a r hm dataobject, mint() will automatically be called within each spine/path in each file (this is accomplished using the groupby argument to mint()):

chorales <- readHumdrum(humdrumRroot, 'HumdrumData/BachChorales/.*krn')

chorales |> 
  mint()

mint() has a number of special options, which you can read about in the manual by checking out ?mint. For now, we'll just show you the classify argument, which can be used to classify output intervals as either Unison, Step, Skip, or Leap:

chorales |>
  mint(classify = TRUE)

Harmonic intervals

In parallel to mint() is hint() (harmonic intervals) command. hint() works just like mint() except it calculates intervals from left to right in each record of a file:

chorales |>
  hint()

A special trick is to use the lag argument to calculate harmonic intervals all in relation to the same spine. For example, you can calculate harmonic intervals between each voice and the bass voice like so:

chorales |>
  hint(lag = Spine == 1)

Or, harmonic intervals from the soprano like so:

chorales |>
  hint(lag = Spine == 4)

Check out the "Logical lags" section of the ?hint manual for details.



Computational-Cognitive-Musicology-Lab/humdrumR documentation built on Oct. 22, 2024, 9:28 a.m.