-
Notifications
You must be signed in to change notification settings - Fork 1
/
PitchAndTonality.Rmd
430 lines (257 loc) · 11.1 KB
/
PitchAndTonality.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
---
title: Pitch and tonality in humdrumR
author: "Nathaniel Condit-Schultz"
date: "`r Sys.Date()`"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Pitch and tonality in humdrumR}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r, include = FALSE, message=FALSE, echo = FALSE }
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:
```{r}
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"?:
```{r}
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:
```{r}
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:
```{r}
kern('Cb6')
pitch("eee-")
pitch(-4) # semits
kern('-4')
solfa('A#6')
semits('Ab3')
```
----
The complete list of basic pitch functions is:
```{r results = 'asis', echo = FALSE}
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](https://en.wikipedia.org/wiki/Degree_(music)), 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).
```{r}
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](RPrimer.html) 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:
```{r}
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](DataFields.html#humdrum-style "HumdrumR data fields article, humdrum-style section"), so using `semits()` is very easy:
```{r}
chorales |>
semits()
```
We could then, for instance, make a histogram of the semitone values:
```{r}
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:
```{r}
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:
```{r}
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:
```{r}
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:
+ For representations like `kern()`, `pitch()` and `lilypond()`, no accidentals are included in generic output;
+ For `intervals()`, the interval quality is removed;
+ For `solfa`, scale degree four will be "fa", never "fi" (sharp four).
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:
```{r}
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:
+ For `pitch()` and `degree()`, the octave number is removed.
+ For `lilypond()` and `helmholtz()`, the octave marks `'` and `,` are removed.
+ For `solfa()` and `deg()`, the contour indicators (explanation below) `v` and `^` are removed.
+ For `interval()`, only steps between 1--7 and returned (i.e., no 10ths).
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:
```{r}
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:
```{r}
chorales <- readHumdrum(humdrumRroot, 'HumdrumData/BachChorales/.*krn')
chorales |>
kern(simple = TRUE) |>
count() |>
draw()
```
```{r}
chorales |>
kern(generic = TRUE) |>
count() |>
draw()
```
```{r}
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:
```{r}
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:
```{r}
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:
```{r}
chorales <- readHumdrum(humdrumRroot, 'HumdrumData/BachChorales/.*krn')
chorales |>
kern(simple = TRUE, transposeArgs = list(to = 'C:')) |>
count() |>
draw()
```
This looks exactly like calling `solfa()`:
```{r}
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:
```{r}
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()`):
```{r}
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`:
```{r}
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:
```{r}
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:
```{r}
chorales |>
hint(lag = Spine == 1)
```
Or, harmonic intervals from the soprano like so:
```{r}
chorales |>
hint(lag = Spine == 4)
```
Check out the "Logical lags" section of the `?hint` manual for details.