knitr::opts_chunk$set( collapse = TRUE, comment = "#>" )
pyramidi::install_miditapyr(envname = "r-reticulate") library(knitr)
We'll load some libraries in R:
library(pyramidi) library(tidyr) library(dplyr) library(purrr) library(forcats) library(details)
First, we just load our package midi file to have some scaffolding to put our notes in:
midi_file_string <- system.file("extdata", "test_midi_file.mid", package = "pyramidi") mfr <- MidiFramer$new(midi_file_string)
We will compose our data by generating a dataframe as the one on the example midi file.
```{details, details.summary = "Click here to show the format of the dataframe we need to have.", lang = 'none'} mfr$df_notes_wide %>% kable()
We will measure our time in `b_note_on` and `b_note_off` being the absolute times (in quarter notes) when the notes are played. In sf2 soundfonts midi channel 10 (which is channel 9 in pythonista world :) is usually used for drums. Let's try this out. 36 is the bass drum and 38 the snare ```r n_beats <- 16 ticks_per_beat <- 960L drum <- tibble( i_track = 0, meta = FALSE, # This is just a repetition of a classical rock beat: note = rep(c(36, 38), n_beats / 2), channel = 9, i_note = 1:n_beats, velocity_note_on = 100, velocity_note_off = 0, b_note_on = 0:(n_beats-1), b_note_off = b_note_on + 1 / 2, )
``{details, details.summary = "Show
drum` dataframe", lang = 'none'}
drum %>% kable()
We'll just define a small helper function to calculate the absolute midi ticks passed from the time measured in beats, because when we generate the format to save the data to midi we need to calculate the relative time increments in ticks. ```r beats_to_ticks <- function(notes_wide) { notes_wide %>% mutate( ticks_note_on = b_note_on * ticks_per_beat, ticks_note_off = b_note_off * ticks_per_beat ) }
Ok we are ready to pass these modified notes to our MidiFramer
object
mfr$update_notes_wide(beats_to_ticks(drum))
and listen to the drums of our first composition:
mfr$play("drum.mp3")
Now we'll add notes. Channel 1 should be grand piano. We will play major chords We define that all the 4 notes in the chord are played at the same time:
(b_note_on = (0:(n_beats-1) %/% 4) * 4)
These starting times will be used to define a notes
dataframe.
notes <- tibble( i_track = 0, meta = FALSE, note = rep(c(60, 64, 67, 72), n_beats / 4), channel = 0, i_note = 1:n_beats, velocity_note_on = 100, velocity_note_off = 0, b_note_on = b_note_on, b_note_off = b_note_on + 1 * 2, )
``{details, details.summary = "click to show
notes` frame", lang = 'none'}
notes %>% kable()
With a small helper function, we can change the notes played depending on the measure we're in: ```r # We play the tonic for 2 bars, # and the subdominant (+5) and dominant (+7) for one each: change_notes_on_measures <- function(notes) { notes %>% mutate( note = case_when( floor(b_note_on/4) %% 4 == 0 ~ note, floor(b_note_on/4) %% 4 == 1 ~ note, floor(b_note_on/4) %% 4 == 2 ~ note + 5, floor(b_note_on/4) %% 4 == 3 ~ note + 7 ) ) } notes <- notes %>% change_notes_on_measures()
When we join the drum
and notes
dataframes together:
midi_note_events_wide <- bind_rows(drum, notes) %>% beats_to_ticks()
We can apply the same as above:
mfr$update_notes_wide(midi_note_events_wide)
And play it:
mfr$play("combine.mp3")
Let's rock! 🤟🥳
Instead of defining a whole dataframe as in the section before, we'll now write a small helper function writing single notes to the needed dataframe format:
frame_notes <- function( b, dur, note, velocity = 100, channel = 0, i_track = 0, meta = FALSE, velocity_note_off = 0, ... ) { tibble( i_track = i_track, meta = meta, note = note, channel = channel, velocity_note_on = velocity, velocity_note_off = velocity_note_off, b_note_on = b, b_note_off = b + dur, ) }
It outputs one line of a dataframe for each midi in the note
vector. We'll design a
repeating bass pattern of a C major chord:
bass <- frame_notes( b = 1:n_beats - 1 + 0.5, dur = 1, note = rep(c(36, 43, 41, 48), n_beats / 4) )
```{details, details.summary = "Show output"} bass %>% kable()
In order to avoid repetitive typing we'll also define a small helper function for chords: ```r frame_chords <- function(...) { frame_notes(...) %>% unnest(note) }
Now we can pass a list of chord vectors to the function. We'll repeat the same C major chord:
chords_list <- rep(list(c(60, 64, 67, 72)), n_beats)
```{details, details.summary = "Show list of chords passed"} chords_list
frame them: ```r chords <- frame_chords( b = 1:n_beats - 1, dur = 1, velocity = 70, note = chords_list )
```{details, details.summary = "Show framed chords"} chords
Now let's write a simple rising arpeggiatator function: ```r arpeggiate <- function( b, chords_list, dur = 1, pattern = "rising", n_beat = 4, octave = 1, ... ) { times <- tibble(b) %>% rowwise() %>% summarise(c(b + seq(0, 1, length.out = n_beat + 1)[-(n_beat + 1)])) %>% pull() notes <- tibble(temp = chords_list) %>% unnest(temp) %>% pull() %>% {. + octave * 12; .} frame_notes( dur = dur/n_beat, b = times, note = notes, ... ) } arp <- arpeggiate( b = 1:n_beats - 1, velocity = 90, chords_list = chords_list )
```{details, details.summary = "Show arpeggio function output", lang = 'none'} arp %>% kable()
We can concatenate these different parts into one dataframe by also changing the chords played with our small function `change_notes_on_measures()`: ```r combination <- bind_rows( # We'll add our note variation # of a major chord to tonic, subdominant and dominant: bass %>% change_notes_on_measures(), chords %>% change_notes_on_measures(), arp %>% change_notes_on_measures(), # But not on the drum :) drum ) %>% beats_to_ticks()
```{details, details.summary = "Show dataframe of whole combination", lang = 'none'} combination %>% kable()
```r mfr$update_notes_wide(combination)
mfr$play("combination.mp3")
In vignette("pyramidi", package = "pyramidi")
there is an example with accumulate()
how you can
generate multiple midifiles while successively adding modifications to the data. Now we will also
successively add parts together, but in the same midifile.
(part_names <- c("drum", "bass", "chords", "arp") %>% accumulate(paste, sep = " + "))
To do this, let's first generate a list of midi frames, where at each step the accumulative former results are added to the new:
augmentation <- list( drum, chords %>% change_notes_on_measures(), bass %>% change_notes_on_measures(), arp %>% change_notes_on_measures() ) %>% accumulate(full_join) %>% set_names(part_names)
```{details, details.summary = "Click here to see the list of parts."} augmentation
All of these parts start at time 0. In order to make the parts start one after one another, we need to shift them in time. This is what the following code does. ```r # after augmenting, we'll add parts subtracting the instruments one after one # another (the rev()erse): composition <- c(augmentation, rev(set_names(augmentation, ~paste0(., "2")))) %>% # put them in one dataframe and modify the starting time of the notes in the parts: bind_rows(.id = "part") %>% # (we need as_factor() to avoid alphabetical ordering of the parts) group_by(part = as_factor(part)) %>% mutate(i_part = cur_group_id()) %>% ungroup() %>% mutate_at(c("b_note_on", "b_note_off"), ~ . + (i_part - 1) * n_beats) %>% # remove new columns to have the needed format for midi export: select(-part, -i_part) %>% beats_to_ticks()
Let's have a listen to my first composition in R:
mfr$update_notes_wide(composition)
mfr$play("composition.mp3")
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.