Skip to contents

This vignette shows how a midi file can also be read in by the tuneR package and then written to disc. However, for other midi files the code might need to be adapted a bit.

Read midi in unnested format

pyramidi

With pyramidi data can be loaded into a unnested dataframe like this:


mid_file_str <- system.file("extdata", "test_midi_file.mid", package = "pyramidi")
mido_mid_file <- mido$MidiFile(mid_file_str)
#> 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'
dfc <- miditapyr$frame_midi(mido_mid_file)
ticks_per_beat = mido_mid_file$ticks_per_beat
df <- dfc %>%
  miditapyr$unnest_midi() %>% 
  as_tibble()
df
#> # A tibble: 268 × 13
#>    i_track meta  type        name   time  note velocity channel  tempo numerator
#>      <dbl> <lgl> <chr>       <chr> <dbl> <dbl>    <dbl>   <dbl>  <dbl>     <dbl>
#>  1       0 TRUE  track_name  drum…     0   NaN      NaN     NaN    NaN       NaN
#>  2       0 FALSE note_on     NA        0    43       72       9    NaN       NaN
#>  3       0 FALSE note_on     NA        0    39       64       9    NaN       NaN
#>  4       0 FALSE note_on     NA        0    36      101       9    NaN       NaN
#>  5       0 TRUE  set_tempo   NA        0   NaN      NaN     NaN 666666       NaN
#>  6       0 TRUE  time_signa… NA        0   NaN      NaN     NaN    NaN         4
#>  7       0 FALSE note_off    NA      240    43       72       9    NaN       NaN
#>  8       0 FALSE note_off    NA        0    39       64       9    NaN       NaN
#>  9       0 FALSE note_off    NA        0    36      101       9    NaN       NaN
#> 10       0 FALSE note_on     NA      240    42      101       9    NaN       NaN
#> # ℹ 258 more rows
#> # ℹ 3 more variables: denominator <dbl>, clocks_per_click <dbl>,
#> #   notated_32nd_notes_per_beat <dbl>

tuneR

With tuneR this can be done for this file in the following way:

df_tuner <- tuneR::readMidi(mid_file_str)

(for other midi files the cleaning code would have to be adapted)

Changing format in tuneR dataframe

  • to make compatible with pyramidi
# vector according to which we'll
# rename the event types to the names given by mido, cf.:
# https://mido.readthedocs.io/en/latest/message_types.html &
# https://mido.readthedocs.io/en/latest/meta_message_types.html
rename_type_vec <-
  c(
    "Set Tempo"           = "set_tempo",
    "Time Signature"      = "time_signature",
    "Note On"             = "note_on",
    "Note Off"            = "note_off",
    "Sequence/Track Name" = "track_name",
    "End of Track"        = "end_of_track",
    "Key Signature"       = "key",
    "Controller"          = "control",
    "Program Change"      = "program",
    "Pitch Bend"          = "pitch"
  )


# transform the tuneR data.frame to the pyramidi format :
df_tuner_mod <- df_tuner %>%
  mutate(event = as.character(event)) %>%
  as_tibble() %>%
  mutate(meta = !is.na(type)) %>%
  select(-type) %>%
  rename(type     = event,
         note     = parameter1,
         velocity = parameter2,
         i_track  = track) %>%
  mutate(type  = recode(type, !!!rename_type_vec)) %>%
  mutate(tempo = ifelse(type == "set_tempo", as.integer(parameterMetaSystem), NA)) %>%
  mutate(name  = ifelse(type == "track_name", parameterMetaSystem, NA)) %>%
  mutate(temp  = ifelse(type == "time_signature", parameterMetaSystem, NA)) %>%
  separate(temp, c("numerator", 
                   "denominator", 
                   "clocks_per_click", 
                   "temp1", "temp2", 
                   "notated_32nd_notes_per_beat"),
           convert = TRUE) %>%
  select(-temp1, -temp2) %>%
  arrange(i_track, time) %>%
  group_by(i_track) %>%
  # calculate time increments instead of total time:
  mutate(time = time - lag(time) %>% {.[1] = 0; .}) %>%
  ungroup() %>%
  mutate_if(is.numeric, ~ifelse(is.na(.), NaN, .)) %>%
  mutate(name = ifelse(is.na(name), list(NULL), name)) %>%
  # small correction of transforming "NA" strings into real NA values:
  mutate(name = ifelse(name == "NA", NA, name)) %>%
  select(-parameterMetaSystem)
#> Warning: There was 1 warning in `mutate()`.
#>  In argument: `tempo = ifelse(...)`.
#> Caused by warning in `ifelse()`:
#> ! NAs introduced by coercion
#> Warning: Expected 6 pieces. Additional pieces discarded in 1 rows [6].

Translate to pyramidi format

dfm <- tab_measures(df_tuner_mod, ticks_per_beat)


dfm %>%
  split_midi_frame() %->% 
  c(df_meta, df_not_notes, df_notes_wide)

df_meta <-
  df_meta

Export midi file from tuneR data

Now we have the data in 3 objects that MidiFramer can understand. First let’s create an empty MidiFramer object:

mfr <- MidiFramer$new()

We’ll set the ticks per beat (see here)

mfr$ticks_per_beat <- ticks_per_beat

First we’ll pass the first two dataframes in the according fields of mfr:

mfr$df_meta <- df_meta
mfr$df_not_notes <- df_not_notes

We’ll add the last one with the method MidiFramer$update_notes_wide(), in order to pass the information to other fields (in particular mfr$mf$midi_frame_nested, which can be transformed back to a midi file; see vignette("package_workflow")):

mfr$update_notes_wide(df_notes_wide)
mfr$mf$write_file("round_tripped_midi_file.mid")