knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)

Setup

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 = "Showdrum` 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 shownotes` 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! 🤟🥳

Write functions to compose midi frames

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")

Combine parts

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")


urswilke/pyramidi documentation built on March 7, 2024, 3:48 p.m.