Skip to contents

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

Load libraries

When hopefully everything is set up correctly, we’ll load some libraries.

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:

mfr$mf$midi_frame_nested$df %>% head()
#>   i_track  meta                            msg
#> 1       0 FALSE          9, 69, note_on, 72, 0
#> 2       0 FALSE          9, 60, note_on, 64, 0
#> 3       0 FALSE         9, 64, note_on, 101, 0
#> 4       0  TRUE    track_name, drum-t1-1-t1, 0
#> 5       0  TRUE           set_tempo, 666666, 0
#> 6       0  TRUE time_signature, 4, 4, 24, 8, 0

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.