World's simplest R music system. Part 3 - Non-sinusoidal Waveforms

Introduction

A note doesn’t have to be a sine wave.

Square, Triangle and sawtooth waves are also common and have different sound characteristics.

Utilities developed in prior posts

Click to reveal ADSR profiling and other utilities
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#' Create an ADSR profile of the given length - using only linear segments
#' 
#' @param N length of profile (integer)
#' @param a,d,r fractions of the profile devoted to attack, decay and release
#'        respectively
#' @param s the level of sustain in range [0,1].  The duration of the sustain
#'        will be the remaining fraction after attack, decay and release
#'        are accounted for
#'
#' @return numeric vector in range [0,1] of length N which defines a 
#'         volumn envelope for the note.
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
create_adsr_profile <- function(N, a, d, s, r) {
   
  dur <- 1 - a - d - r
  stopifnot(dur >= 0)
  
  profile <- c(
    seq(0, 1, length.out = a * N),
    seq(1, s, length.out = d * N),
    rep(s, length.out = dur * N),
    seq(s, 0, length.out = r * N)
  )
  
  # Ensure we have the right number of elements in case of weird rounding
  length(profile) <- N
  profile[is.na(profile)] <- 0
  
  profile
}

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Play a note
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
play_note <- function(samples) {
  audio::play(samples, rate = 44100)
}

Create simple note with different waveforms

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Create a note - sine wave
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
create_sine_wave <- function(freq, duration) {
  t <- seq(duration * 44100)
  samples <- sin((t) * 2*pi/ (44100/freq))
  samples
}

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Create a note - square wave
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
create_square_wave <- function(freq, duration) {
  samples <- create_sine_wave(freq, duration)
  samples <- ifelse(samples >=0, 1, -1)
  samples
}

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Create a note - triangular wave. 
# Create as a cumsum() of a square wave.
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
create_triangle_wave <- function(freq, duration) {
  period <- 44100/freq
  wave <- c(
    seq( 0,  1, length.out = (period/4 - 1)),
    seq( 1, -1, length.out = (period/2 - 1)),
    seq(-1,  0, length.out = (period/4 - 1))
  )
  
  rep(wave, length.out = 44100 * duration)
}

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Create a note - sawtooth wave
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
create_sawtooth_wave <- function(freq, duration) {
  period <- 44100/freq
  wave <- seq(-1,  1, length.out = period - 1)
  rep(wave, length.out = 44100 * duration)
}



note_sine <- create_sine_wave(440, 1)
plot(note_sine[1:300], type = 'l')
title('Sine')



note_square <- create_square_wave(440, 1)
plot(note_square[1:300], type = 'l')
title('Square')


note_tri <- create_triangle_wave(440, 1) 
plot(note_tri[1:300], type = 'l')
title('Triangle')


note_saw <- create_sawtooth_wave(440, 1)
plot(note_saw[1:300], type = 'l')
title('Sawtooth')

profile <- create_adsr_profile(44100, 0.2, 0.3, 0.8, 0.5)

play_note(note_sine   * profile)
play_note(note_square * profile)
play_note(note_tri    * profile)
play_note(note_saw    * profile)

Sine

Square

Triangle

Sawtooth

World’s simplest R music system - with linear ADSR profile shaping + sawtooth waveform

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Octaves/Notes/Frequencies in an equal tempered scale
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ets <- tibble(
  freq   = 440 * (2 ^ ((-57:50)/12)),
  note   = rep(c('C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'), 9),
  octave = rep(0:8, each = 12),
  mnote  = ifelse(nchar(note) == 1, paste(note, octave, sep = "-"), paste(note, octave, sep = ""))
)

head(ets)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Note-to-Freq lookup
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
freq <- setNames(ets$freq, ets$mnote)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Function: Play a note of the given frequency for the given duration
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
profile <- create_adsr_profile(0.4 * 44100, 0.1, 0.25, 0.2, 0.6)
  
play_note <- function(freq, duration) {
  samples <- create_sawtooth_wave(freq, duration)
  samples <- samples * profile
  
  
  audio::play(samples , rate = 44100)
}

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Mary had a little lamb.
# Format is "[Note]-[Octave]"
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
mhall <- c(
  'A-4', 'G-4', 'F-4', 'G-4', 'A-4', 'A-4', 'A-4',
  'G-4', 'G-4', 'G-4', 'A-4', 'C-5', 'C-5',
  'A-4', 'G-4', 'F-4', 'G-4', 'A-4', 'A-4', 'A-4',
  'A-4', 'G-4', 'G-4', 'A-4', 'G-4', 'F-4'
)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Play it
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
for (note in mhall) {
  play_note(freq[[note]], 0.4)
  Sys.sleep(0.4)
}

Different waveforms sound different

The following two live captures of the same song at the same pitch with the same ADSR note shaping - only the note waveform is different.

Live capture of “Mary had a little lamb” - with ADSR note shaping + sawtooth waveform

Live capture of “Mary had a little lamb” - with ADSR note shaping + sine waveform