After installing pyramidi, you can run the helper function to install
its python dependency miditapyr in your environment (in this case
"r-reticulate"
):
pyramidi::install_miditapyr(envname = "r-reticulate")
#> Using virtual environment 'r-reticulate' ...
#> + /home/runner/.virtualenvs/r-reticulate/bin/python -m pip install --upgrade --no-user miditapyr
MidiFramer
class
This class is the main structure of the package allowing to read midi files, manipulate the data, and write it back to disk. You can also use it to generate the midi data from scratch in R.
Generate MidiFramer
object
We’ll create an R6 object of class "MidiFramer"
from a
midi_file_path
by passing it to the
constructor method MidiFramer$new()
:
midi_file_path <- system.file("extdata", "test_midi_file.mid", package = "pyramidi")
mfr <- MidiFramer$new(midi_file_path)
#> Error in python_config_impl(python) :
#> Error running '/home/runner/.virtualenvs/r-reticulate/python': No such file.
#> The Python installation used to create the virtualenv has been moved or removed:
#> '/opt/hostedtoolcache/Python/3.11.8/x64/bin'
mfr
Show print output
<MidiFramer>
Public:
clone: function (deep = FALSE)
df_meta: data.frame
df_not_notes: data.frame
df_notes_long: tbl_df, tbl, data.frame
df_notes_wide: tbl_df, tbl, data.frame
dfm: tbl_df, tbl, data.frame
initialize: function (midi_file_string = NULL)
mf: miditapyr.midi_frame.MidiFrames, python.builtin.object
midi_file_string: /tmp/RtmpcdQMVt/temp_libpath1e2a167c2fce/pyramidi/extdat ...
midi_frame_mod: tbl_df, tbl, data.frame
params: list
play: function (audiofile = tempfile("mf_out_", fileext = ".mp3"),
populate_r_fields: function ()
ticks_per_beat: active binding
update_notes_wide: function (mod)
Private:
deep_clone: function (name, value)
Fields in MidiFramer
objects
It is an R6 object that contains the following fields:
enframe(as.list(mfr))
Show print output
[38;5;246m# A tibble: 16 × 2
[39m
name value
[3m
[38;5;246m<chr>
[39m
[23m
[3m
[38;5;246m<list>
[39m
[23m
[38;5;250m 1
[39m .__enclos_env__
[38;5;246m<env>
[39m
[38;5;250m 2
[39m ticks_per_beat
[38;5;246m<int [1]>
[39m
[38;5;250m 3
[39m params
[38;5;246m<named list [1]>
[39m
[38;5;250m 4
[39m midi_frame_mod
[38;5;246m<tibble [268 × 13]>
[39m
[38;5;250m 5
[39m df_notes_wide
[38;5;246m<tibble [130 × 11]>
[39m
[38;5;250m 6
[39m df_not_notes
[38;5;246m<df [0 × 9]>
[39m
[38;5;250m 7
[39m df_meta
[38;5;246m<df [8 × 12]>
[39m
[38;5;250m 8
[39m df_notes_long
[38;5;246m<tibble [260 × 9]>
[39m
[38;5;250m 9
[39m dfm
[38;5;246m<tibble [268 × 15]>
[39m
[38;5;250m10
[39m mf
[38;5;246m<miditapyr.midi_frame.MidiFrames>
[39m
[38;5;250m11
[39m midi_file_string
[38;5;246m<chr [1]>
[39m
[38;5;250m12
[39m clone
[38;5;246m<fn>
[39m
[38;5;250m13
[39m play
[38;5;246m<fn>
[39m
[38;5;250m14
[39m populate_r_fields
[38;5;246m<fn>
[39m
[38;5;250m15
[39m update_notes_wide
[38;5;246m<fn>
[39m
[38;5;250m16
[39m initialize
[38;5;246m<fn>
[39m
Please refer to help(MidiFramer)
for more information on
this class. The field mf
is a miditapyr.MidiFrames()
object. After its first element mfr$mf$midi_file
, a mido midi
file object with the mido message data, there are 3 more
dataframes:
list(
mfr$mf$midi_frame_raw,
mfr$mf$midi_frame_unnested$df,
mfr$mf$midi_frame_nested$df
) %>%
map(head, 30) %>%
walk(\(x) print(knitr::kable(x)))
Show print output of 30 first rows of these dataframes
[[1]]
i_track meta msg
1 0 TRUE track_name, drum-t1-1-t1, 0
2 0 FALSE note_on, 0, 43, 72, 9
3 0 FALSE note_on, 0, 39, 64, 9
4 0 FALSE note_on, 0, 36, 101, 9
5 0 TRUE set_tempo, 666666, 0
6 0 TRUE time_signature, 4, 4, 24, 8, 0
7 0 FALSE note_off, 240, 43, 72, 9
8 0 FALSE note_off, 0, 39, 64, 9
9 0 FALSE note_off, 0, 36, 101, 9
10 0 FALSE note_on, 240, 42, 101, 9
11 0 FALSE note_on, 0, 38, 101, 9
12 0 FALSE note_on, 240, 43, 64, 9
13 0 FALSE note_off, 0, 42, 101, 9
14 0 FALSE note_off, 0, 38, 101, 9
15 0 FALSE note_off, 240, 43, 64, 9
16 0 FALSE note_on, 0, 36, 101, 9
17 0 FALSE note_off, 240, 36, 101, 9
18 0 FALSE note_on, 240, 43, 60, 9
19 0 FALSE note_on, 0, 42, 101, 9
20 0 FALSE note_off, 240, 43, 60, 9
21 0 FALSE note_off, 0, 42, 101, 9
22 0 FALSE note_on, 240, 43, 60, 9
23 0 FALSE note_on, 0, 39, 66, 9
24 0 FALSE note_on, 0, 36, 101, 9
25 0 FALSE note_off, 240, 43, 60, 9
26 0 FALSE note_off, 0, 39, 66, 9
27 0 FALSE note_off, 0, 36, 101, 9
28 0 FALSE note_on, 240, 42, 101, 9
29 0 FALSE note_on, 0, 38, 101, 9
30 0 FALSE note_on, 240, 43, 53, 9
[[2]]
i_track meta type name time note velocity channel tempo
1 0 TRUE track_name drum-t1-1-t1 0 NaN NaN NaN NaN
2 0 FALSE note_on <NA> 0 43 72 9 NaN
3 0 FALSE note_on <NA> 0 39 64 9 NaN
4 0 FALSE note_on <NA> 0 36 101 9 NaN
5 0 TRUE set_tempo <NA> 0 NaN NaN NaN 666666
6 0 TRUE time_signature <NA> 0 NaN NaN NaN NaN
7 0 FALSE note_off <NA> 240 43 72 9 NaN
8 0 FALSE note_off <NA> 0 39 64 9 NaN
9 0 FALSE note_off <NA> 0 36 101 9 NaN
10 0 FALSE note_on <NA> 240 42 101 9 NaN
11 0 FALSE note_on <NA> 0 38 101 9 NaN
12 0 FALSE note_on <NA> 240 43 64 9 NaN
13 0 FALSE note_off <NA> 0 42 101 9 NaN
14 0 FALSE note_off <NA> 0 38 101 9 NaN
15 0 FALSE note_off <NA> 240 43 64 9 NaN
16 0 FALSE note_on <NA> 0 36 101 9 NaN
17 0 FALSE note_off <NA> 240 36 101 9 NaN
18 0 FALSE note_on <NA> 240 43 60 9 NaN
19 0 FALSE note_on <NA> 0 42 101 9 NaN
20 0 FALSE note_off <NA> 240 43 60 9 NaN
21 0 FALSE note_off <NA> 0 42 101 9 NaN
22 0 FALSE note_on <NA> 240 43 60 9 NaN
23 0 FALSE note_on <NA> 0 39 66 9 NaN
24 0 FALSE note_on <NA> 0 36 101 9 NaN
25 0 FALSE note_off <NA> 240 43 60 9 NaN
26 0 FALSE note_off <NA> 0 39 66 9 NaN
27 0 FALSE note_off <NA> 0 36 101 9 NaN
28 0 FALSE note_on <NA> 240 42 101 9 NaN
29 0 FALSE note_on <NA> 0 38 101 9 NaN
30 0 FALSE note_on <NA> 240 43 53 9 NaN
numerator denominator clocks_per_click notated_32nd_notes_per_beat
1 NaN NaN NaN NaN
2 NaN NaN NaN NaN
3 NaN NaN NaN NaN
4 NaN NaN NaN NaN
5 NaN NaN NaN NaN
6 4 4 24 8
7 NaN NaN NaN NaN
8 NaN NaN NaN NaN
9 NaN NaN NaN NaN
10 NaN NaN NaN NaN
11 NaN NaN NaN NaN
12 NaN NaN NaN NaN
13 NaN NaN NaN NaN
14 NaN NaN NaN NaN
15 NaN NaN NaN NaN
16 NaN NaN NaN NaN
17 NaN NaN NaN NaN
18 NaN NaN NaN NaN
19 NaN NaN NaN NaN
20 NaN NaN NaN NaN
21 NaN NaN NaN NaN
22 NaN NaN NaN NaN
23 NaN NaN NaN NaN
24 NaN NaN NaN NaN
25 NaN NaN NaN NaN
26 NaN NaN NaN NaN
27 NaN NaN NaN NaN
28 NaN NaN NaN NaN
29 NaN NaN NaN NaN
30 NaN NaN NaN NaN
[[3]]
i_track meta msg
1 0 TRUE track_name, drum-t1-1-t1, 0
2 0 FALSE note_on, 0, 43, 72, 9
3 0 FALSE note_on, 0, 39, 64, 9
4 0 FALSE note_on, 0, 36, 101, 9
5 0 TRUE set_tempo, 0, 666666
6 0 TRUE time_signature, 0, 4, 4, 24, 8
7 0 FALSE note_off, 240, 43, 72, 9
8 0 FALSE note_off, 0, 39, 64, 9
9 0 FALSE note_off, 0, 36, 101, 9
10 0 FALSE note_on, 240, 42, 101, 9
11 0 FALSE note_on, 0, 38, 101, 9
12 0 FALSE note_on, 240, 43, 64, 9
13 0 FALSE note_off, 0, 42, 101, 9
14 0 FALSE note_off, 0, 38, 101, 9
15 0 FALSE note_off, 240, 43, 64, 9
16 0 FALSE note_on, 0, 36, 101, 9
17 0 FALSE note_off, 240, 36, 101, 9
18 0 FALSE note_on, 240, 43, 60, 9
19 0 FALSE note_on, 0, 42, 101, 9
20 0 FALSE note_off, 240, 43, 60, 9
21 0 FALSE note_off, 0, 42, 101, 9
22 0 FALSE note_on, 240, 43, 60, 9
23 0 FALSE note_on, 0, 39, 66, 9
24 0 FALSE note_on, 0, 36, 101, 9
25 0 FALSE note_off, 240, 43, 60, 9
26 0 FALSE note_off, 0, 39, 66, 9
27 0 FALSE note_off, 0, 36, 101, 9
28 0 FALSE note_on, 240, 42, 101, 9
29 0 FALSE note_on, 0, 38, 101, 9
30 0 FALSE note_on, 240, 43, 53, 9
When mf
is initialized midi_frame_raw
and
midi_frame_nested$df
should be the same (except the
ordering of the named fields in the msg
column might
differ).
For a detailed interactive overview with further links showing how
the various fields in the MidiFramer
class are related and
calculated see vignette("package_workflow")
.
Populating an empty MidiFramer
object
You could also achieve the same result by first creating an empty
MidiFramer
object like so:
mfr <- MidiFramer$new()
mfr
Show print output
<MidiFramer>
Public:
clone: function (deep = FALSE)
df_meta: NULL
df_not_notes: NULL
df_notes_long: NULL
df_notes_wide: NULL
dfm: NULL
initialize: function (midi_file_string = NULL)
mf: miditapyr.midi_frame.MidiFrames, python.builtin.object
midi_file_string: NULL
midi_frame_mod: NULL
params: list
play: function (audiofile = tempfile("mf_out_", fileext = ".mp3"),
populate_r_fields: function ()
ticks_per_beat: active binding
update_notes_wide: function (mod)
Private:
deep_clone: function (name, value)
In this case all the dataframe fields are initialized to
NULL
(or None
in python which reticulate also
translates to NULL
in R).
In order to load a midi file to mfr$mf
you have to use
the miditapyr method calc_attributes()
:
mfr$mf$calc_attributes(midi_file_path)
Then you can populate the fields of mfr
with the
MidiFramer
method populate_r_fields()
.
mfr$populate_r_fields()
Usage
Modifying midi data
In the MidiFramer
object, we can modify
mfr$df_notes_wide
, the notes in note-wise wide format
(note_on
& note_off
events in the same
line). Thus we don’t need to worry which midi events belong together
Let’s look at a small example. We’ll define a function to replace every note with a random midi note between 60 & 71:
mod <- function(dfn, seed) {
n_notes <- sum(!is.na(dfn$note))
dfn %>% mutate(note = ifelse(
!is.na(note),
sample(60:71, n_notes, TRUE),
note
))
}
We could modify the notes in wide format like this:
mod(mfr$df_notes_wide)
#> # A tibble: 130 × 11
#> i_track meta note channel i_note velocity_note_on velocity_note_off
#> <dbl> <lgl> <int> <dbl> <dbl> <dbl> <dbl>
#> 1 0 FALSE 61 9 1 72 72
#> 2 0 FALSE 71 9 1 64 64
#> 3 0 FALSE 71 9 1 101 101
#> 4 0 FALSE 62 9 1 101 101
#> 5 0 FALSE 61 9 1 101 101
#> 6 0 FALSE 71 9 2 64 64
#> 7 0 FALSE 67 9 2 101 101
#> 8 0 FALSE 60 9 3 60 60
#> 9 0 FALSE 63 9 2 101 101
#> 10 0 FALSE 61 9 4 60 60
#> # ℹ 120 more rows
#> # ℹ 4 more variables: ticks_note_on <dbl>, ticks_note_off <dbl>,
#> # b_note_on <dbl>, b_note_off <dbl>
Then we would have to adapt all the following elements of
mfr
that depend on mfr$df_notes_wide
.
When we call the method mfr$update_notes_wide()
, all the
depending list elements are also automatically updated.
# Apply the modification to mfr$df_notes_wide and all depending dataframes:
mfr$update_notes_wide(mod)
The data has also been changed in mfr$mf
the miditapyr
MidiFrames
object in mfr:
Writing modified midi files
Thus we can now directly save the modifications to a midi file:
midifile <- "mod_test_midi_file.mid"
mfr$mf$write_file(midifile)
For a more extensive and less arbitrary demo how to compose your own
music with pyramidi, see vignette("compose")
.
Playing audio
You need to install fluidsynth on your computer and the R package (which should be installed with pyramidi) if you want to do that on your computer.
If you want to produce audio files from the midi files you can
synthesize them with the convenience function player()
midifile |> player()
As you can hear, the sample()
function in
mod()
changed all midi notes randomly.
You can also listen to your synthesized files by embedding an audio
player for the MidiFramer
object:
mfr$play()
When using R interactively, this will directly play the generated audio in the console.
Multiple results
One of the reasons to create this package was that R might help to avoid repetitive work. See below how you can use functional programming approaches of purrr, to generate multiple midi files in one call (actually two calls for more clarity but you could also put them in one).
You can also generate a list of multiple MidiFramer
objects (in this case 2) and apply different modifications to each:
l_mfr <- 1:2 %>%
set_names(paste0("test", ., ".mp3")) %>%
map(~mfr$clone(deep = TRUE)$update_notes_wide(mod))
And if you want to successively add modifications to the same object and store all intermediate results in a list, you could do it like this:
l_mfr2 <- 1:2 %>%
set_names(paste0("test", ., ".mp3")) %>%
accumulate(~.x$clone(deep = TRUE)$update_notes_wide(mod), .init = mfr)
Please note that we made deep copies
($clone(deep = TRUE)
) of the MidiFramer
object
in the list l_mfr
. Otherwise the field of the python miditapyr.MidiFrames
object mf
is a shallow copy. This means that updates to one
of these elements in l_mfr
(for instance
l_mfr[[1]]$mf
) also change the others, because they all are
references to the same object.
This is how you can embed multiple audio files in an rmarkdown
document using purrr::imap()
:
tagList(
imap(
l_mfr,
~ div(
h4(paste("audio result", .y)),
.x$play(audiofile = .y),
# add 2 line breaks to vertically separate a bit:
br(),
br()
)
)
)
audio result test1.mp3
audio result test2.mp3
Compared to the two audio samples in Playing audio (which are the same), we now have two different results.