# import packages
using DigitalMusicology
using DataFrames
using Plots
# A4 is set to 440Hz
frequency_to_pitch(f) = 69 + 12 * log2(f / 440)
pitch_to_frequency(p) = 2^((p - 69) / 12) * 440
pitch_to_frequency (generic function with 1 method)
# the frequency of C4
pitch_to_frequency(60)
261.6255653005986
# the lowest midi note has a frequency of about 8Hz,
# the highest about 13kHz
# humans typically hear between 20Hz and 20kHz
pitch_to_frequency(0), pitch_to_frequency(127)
(8.175798915643707, 12543.853951415975)
The midi file can also be read by Garageband and MuseScore!
notes = midifilenotes("Run_To_The_Hills.mid")
head(notes)
onset_ticks | offset_ticks | onset_wholes | offset_wholes | onset_secs | offset_secs | pitch | velocity | track | channel | key_sharps | key_major | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 46 | 0//1 | 23//384 | 0.0 | 0.10569841666666667 | 42 | 110 | 7 | 9 | 0 | true |
2 | 0 | 160 | 0//1 | 5//24 | 0.0 | 0.3676466666666667 | 36 | 110 | 7 | 9 | 0 | true |
3 | 48 | 94 | 1//16 | 47//384 | 0.110294 | 0.2159924166666667 | 42 | 110 | 7 | 9 | 0 | true |
4 | 96 | 142 | 1//8 | 71//384 | 0.220588 | 0.3262864166666667 | 42 | 110 | 7 | 9 | 0 | true |
5 | 144 | 190 | 3//16 | 95//384 | 0.330882 | 0.4365804166666667 | 42 | 110 | 7 | 9 | 0 | true |
6 | 192 | 238 | 1//4 | 119//384 | 0.441176 | 0.5468744166666667 | 42 | 110 | 7 | 9 | 0 | true |
key_sharps
and key_major
have default values(0,true)
like here!notes[:key_sharps] |> unique
1-element DataArrays.DataArray{Int64,1}: 0
?midifilenotes
search: midifilenotes
midifilenotes(file; warnings=false, overlaps=:queue, orphans=:skip)
Reads a midi file and returns a DataFrame
with one row per note. On- and offset times are given in ticks, whole notes, and seconds. The data frame has the following columns:
If warnings
is true
, warnings about encoding errors will be displayed. If two notes overlap on the same channel and track (e.g. two ons, then two offs for the same pitch) overlaps
provides the strategy for interpreting the sequence of on and off events:
:queue
matches ons and offs in a FIFO manner (first on to first off).:stack
matches ons and offs in a LIFO manner (first on to last off).orphans
determines what happens to on and off events without counterpart. Currently, its value is ignored and orphan events are always skipped.
# this is a small pipeline
notes[:track] |> unique |> sort
6-element DataArrays.DataArray{Int64,1}: 2 3 4 5 6 7
# it is equivalent to
sort(unique(notes[:track]))
6-element DataArrays.DataArray{Int64,1}: 2 3 4 5 6 7
track_names = ["meta", "vocals", "vocal harmony", "guitar 1", "guitar 2", "bass", "drums"]
7-element Array{String,1}: "meta" "vocals" "vocal harmony" "guitar 1" "guitar 2" "bass" "drums"
# drums in first bar
# there are 192 ticks per quarter note
notes[1:20, [:onset_wholes, :onset_ticks]]
onset_wholes | onset_ticks | |
---|---|---|
1 | 0//1 | 0 |
2 | 0//1 | 0 |
3 | 1//16 | 48 |
4 | 1//8 | 96 |
5 | 3//16 | 144 |
6 | 1//4 | 192 |
7 | 1//4 | 192 |
8 | 5//16 | 240 |
9 | 3//8 | 288 |
10 | 7//16 | 336 |
11 | 1//2 | 384 |
12 | 1//2 | 384 |
13 | 9//16 | 432 |
14 | 5//8 | 480 |
15 | 11//16 | 528 |
16 | 3//4 | 576 |
17 | 3//4 | 576 |
18 | 7//8 | 672 |
19 | 1//1 | 768 |
20 | 1//1 | 768 |
# select the the instruments other than the drums
not_percussive_notes = notes[notes[:track] .!= 7, :]
head(not_percussive_notes)
onset_ticks | offset_ticks | onset_wholes | offset_wholes | onset_secs | offset_secs | pitch | velocity | track | channel | key_sharps | key_major | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 2976 | 3214 | 31//8 | 1607//384 | 6.838228 | 7.385102416666667 | 86 | 110 | 4 | 2 | 0 | true |
2 | 2976 | 3070 | 31//8 | 1535//384 | 6.838228 | 7.054220416666666 | 62 | 110 | 5 | 3 | 0 | true |
3 | 2976 | 3070 | 31//8 | 1535//384 | 6.838228 | 7.054220416666666 | 69 | 110 | 5 | 3 | 0 | true |
4 | 2976 | 3070 | 31//8 | 1535//384 | 6.838228 | 7.054220416666666 | 74 | 110 | 5 | 3 | 0 | true |
5 | 2976 | 3070 | 31//8 | 1535//384 | 6.838228 | 7.054220416666666 | 38 | 110 | 6 | 4 | 0 | true |
6 | 3072 | 3214 | 4//1 | 1607//384 | 7.058816 | 7.385102416666667 | 64 | 110 | 5 | 3 | 0 | true |
# convert midi notes to pitch numbers
pitches = [note.pitch for note in not_percussive_notes[:pitch]]
pitches[1:10]
10-element Array{Int64,1}: 86 62 69 74 38 64 71 76 40 86
# plot pitch histogram
histogram(pitches, bins=collect(0:127))
# plot pitch class histogram
pitch_classes = [mod(p, 12) for p in pitches]
xticks = (collect(0:11) .+ 0.5, collect(0:11))
histogram(pitch_classes, bins=12, xticks=xticks)
Looks like a C major scale!
# shift the histogram so that the most prominent note is in front
shifted_xticks = (collect(0:11) .+ 0.5, [mod(pc+7, 12) for pc in 0:11])
histogram(
[mod(pc-7, 12) for pc in pitch_classes],
bins=12,
xticks=shifted_xticks
)
transform(p) = mod((p+4)*7, 12)
histogram(
transform.(pitch_classes),
bins = 12,
xticks = (transform.(collect(0:11)) .+ 0.5, (collect(0:11))),
xlim = (0,12)
)
for track in 2:6
display(
histogram(
[mod(note.pitch, 12) for note in notes[notes[:track] .== track, :pitch]],
bins = collect(0:12),
xticks = xticks,
title = track_names[track],
xlim = (0,12)
)
)
end
onsets = notes[notes[:track] .== 4, :onset_wholes]
offsets = notes[notes[:track] .== 4, :offset_wholes]
durations = offsets - onsets
1691-element DataArrays.DataArray{Rational{Int64},1}: 119//384 71//384 9//16 71//384 71//384 9//16 71//384 71//384 9//16 71//384 71//384 9//16 71//384 ⋮ 5//128 5//128 5//128 5//128 5//128 5//128 263//384 263//384 263//384 5//24 5//24 5//24
[(d.num + 1) // d.den for d in durations]
1691-element Array{Rational{Int64},1}: 5//16 3//16 5//8 3//16 3//16 5//8 3//16 3//16 5//8 3//16 3//16 5//8 3//16 ⋮ 3//64 3//64 3//64 3//64 3//64 3//64 11//16 11//16 11//16 1//4 1//4 1//4
for track in 2:6
onsets = notes[notes[:track] .== track, :onset_wholes]
offsets = notes[notes[:track] .== track, :offset_wholes]
durations = offsets - onsets
display(histogram([(d.num + 1) // d.den for d in durations], title=track_names[track]))
end
for track in 2:6
onsets = notes[notes[:track] .== track, :onset_wholes]
display(
histogram(
[o - floor(o) for o in onsets],
bins = [x*1/16 for x in 0:16],
title = track_names[track]
)
)
end