Harmonizing Festivity with Mathematics

Crafting Tunes with R

Mathemaics
R Programming
Author

Abhirup Moitra

Published

January 2, 2025

What could be more fitting than recreating the classic melodies of “Jingle Bells” to embody the festive cheer?

Introduction

The holiday season is a cherished time filled with joy, reflection, and a sense of togetherness, making it a perfect opportunity to celebrate the spirit of Christmas and New Year through music that resonates with these sentiments. By leveraging the power of mathematics and the versatility of R programming, we can blend creativity with computational precision to craft these timeless tunes. This article explores how R can be used not only as a tool for data analysis but also as a medium for artistic expression, showcasing its ability to combine mathematical principles with musical creativity in an innovative and engaging way.

A Dive into Mathematical Sound Synthesis

Music has always been a fascinating combination of art and mathematics. In this article, we explore how we can use R, a programming language often associated with data analysis, to generate sound waves and create music. We’ll dive into the mathematical foundations of sound synthesis, the code behind it, and the beautiful melody that emerges from combining math with programming.

Installing and Loading Required Libraries

Code
if(!"dplyr" %in% installed.packages()) install.packages("dplyr")
if(!"audio" %in% installed.packages()) install.packages("audio")

library("dplyr")
library("audio")

This chunk ensures that the required libraries (dplyr for data manipulation and audio for sound playback) are installed and loaded. It ensures the code will work even if the libraries aren’t installed on the system.

Defining Notes and Corresponding MIDI Values

We define the pitch classes for the notes. Each note (A, B, C, D, E, F, G) corresponds to a number representing its position relative to the note A (the tonic) in terms of a twelve-tone equal temperament scale.

Code
notes <- c(A = 0, B = 2, C = 3, D = 5, E = 7, F = 8, G = 10)
Note

A = 0 (reference), B = 2, C = 3, etc., with intervals between them.

Our goal is to create a complete musical sequence by generating sound waves corresponding to a series of musical notes. This involves carefully synthesizing the audio signals for each note, ensuring that their pitch, duration, and dynamics are accurately represented. Once the sound waves are generated, they are combined to form a coherent musical piece that can be played back. The resulting sound captures the intended melody and rhythm, bringing the musical sequence to life and allowing it to be audibly experienced.

Mathematical Prerequisites

  1. Pitch and Frequency Calculation:

    • The notes vector defines the pitch classes of musical notes (A, B, C, D, E, F, G) in terms of MIDI note numbers.

    • Each note has a corresponding frequency determined by the formula:

      \[ f = 2^{\frac{{\text{{note}} - 60}}{{12}}} \times 440 \]

    • This equation calculates the frequency f of a note in standard tuning (with A4 = \(440\; \text{Hz}\) ) based on its position in the 12-tone equal temperament scale.

  2. Octave and Notes Calculation:

    • The code extracts the octave information from the pitch (e.g., ‘A3’, ‘G4’) and calculates the note’s absolute pitch value, including whether it is sharp (#) or flat (b).

    • The calculation of note adjusts for sharp or flat notes and scales based on the octave, ensuring each note’s position within the 12-tone scale is correctly translated into a frequency.

  3. Waveform Generation:

    • The sine wave for each note is generated using the sin() function:

      \[ \text{wave} = \sin\left( \frac{{2 \pi \times \text{freq} \times t}}{\text{sample rate}} \right) \]

      where t is the time vector determined by the duration of the note and the sample rate \(44,100\; \text{Hz}\). This generates the sound wave based on the calculated frequency.

  4. Envelope (Fade-in and Fade-out):

    • The sine wave is enveloped with a fade effect (a smooth transition into and out of the sound) by applying a fade function. The fade makes the sound smoother and avoids sharp beginnings and endings, mathematically implemented by multiplying the wave by a linear fade-in and fade-out sequence.
  5. Time and Tempo:

    • The tempo ( \(250\) beats per minute) influences the length of the note duration. Each note’s duration is adjusted relative to the tempo, providing the rhythm of the music.

Defining the Musical Sequence (pitch)

Code
pitch <- paste("E E E",
               "E E E",
               "E G C D",
               "E",
               "F F F F",
               "F E E E",
               "E D D E",
               "D G",
               "E E E",
               "E E E",
               "E G C D",
               "E",
               "F F F F",
               "F E E E E",
               "G G F D",
               "C",
               "G3 E D C",
               "G3",
               "G3 G3 G3 E D C",
               "A3",
               "A3 F E D",
               "B3",
               "G G F D",
               "E",
               "G3 E D C",
               "G3",
               "G3 E D C",
               "A3 A3",
               "A3 F E D",
               "G G G G A G F D",
               "C C5 B A G F G",
               "E E E G C D",
               "E E E G C D",
               "E F G A C E D F",
               "E C D E F G A G",
               "F F F F F F",
               "F E E E E E",
               "E D D D D E",
               "D D E F G F E D",
               "E E E G C D",
               "E E E G C D",
               "E F G A C E D F",
               "E C D E F G A G",
               "F F F F F F",
               "F E E E E E",
               "G C5 B A G F E D",
               "C C E G C5")

The melody of the music piece is an essential and defining component, serving as the sequence of musical notes that form the core identity and character of the composition. It is represented here as a string of notes, each symbolizing specific pitches and duration that, when played in order, create the recognizable tune of the piece. This string captures the melodic contour and progression, embodying the essence of the musical narrative.

Important

The sequence is a mix of different notes and octaves (e.g., A3, G3, C5), which will be converted into frequencies later. The repetition of certain notes (e.g., “E E E”) gives the melody structure.

The duration array plays a critical role in shaping the rhythm and flow of the melody by defining the length of each note within the sequence. This array assigns specific time values to individual notes, ensuring that their durations are accurately represented and contributing to the overall rhythmic structure of the music.

Code
duration <- c(1, 1, 2,
              1, 1, 2,
              1, 1, 1.5, 0.5,
              4,
              1, 1, 1, 1,
              1, 1, 1, 1,
              1, 1, 1, 1,
              2, 2,
              1, 1, 2,
              1, 1, 2,
              1, 1, 1.5, 0.5,
              4,
              1, 1, 1, 1,
              1, 1, 1, 0.5, 0.5,
              1, 1, 1, 1,
              4,
              1, 1, 1, 1,
              3, .5, .5,
              1, 1, 1, 1,
              4,
              1, 1, 1, 1,
              4,
              1, 1, 1, 1,
              4,
              1, 1, 1, 1,
              3, 1,
              1, 1, 1, 1,
              1, 1, 1, 1,
              1, 1, 1, 1,
              1, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
              1, 1, 0.5, 0.5, 0.5, 0.5,
              1, 1, 0.5, 0.5, 0.5, 0.5,
              0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
              0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
              1, 0.5, 0.5, 1, 0.5, 0.5,
              1, 0.5, 0.5, 1, 0.5, 0.5,
              1, 0.5, 0.5, 0.5, 0.5, 1,
              1, 0.33, 0.33, 0.33, 1, 0.33, 0.33, 0.33,
              1, 1, 0.5, 0.5, 0.5, 0.5,
              1, 1, 0.5, 0.5, 0.5, 0.5,
              0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
              0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
              1, 0.5, 0.5, 1, 0.5, 0.5,
              1, 0.5, 0.5, 1, 0.5, 0.5,
              0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
              1, 0.33, 0.33, 0.33, 2)

By enabling variations in note lengths, the duration array allows for the creation of diverse rhythms and supports the use of different time signatures, adding depth and complexity to the musical composition.

Note

The numbers represent the duration in beats (1 = quarter note, 0.5 = eighth note, 2 = half note, etc.).

The code is generating a musical sequence based on a predefined melody (pitch and duration), which corresponds to a simple tune. The pitch array contains the note sequence, and the duration array specifies how long each note lasts. These sequences produce a recognizable melody, and the code can be modified to play different musical pieces by changing the note and duration arrays.

Creating the Data Frame for Notes

Code
# Check the lengths of pitch and duration
#length(pitch)  # 209
#length(duration)  # 204

# Adjust the duration length to match pitch length, for example:
# Recycling the duration to match the length of pitch
duration <- rep(duration, length.out = length(pitch))

# Now create the data frame
jbells <- tibble::tibble(pitch = strsplit(pitch, " ")[[1]], duration = duration)

In this code:

  • rep(duration, length.out = length(pitch)) ensures that the duration vector is recycled to match the length of the pitch vector. The length.out argument specifies the desired length of the vector.

  • After ensuring the lengths are consistent, the data frame creation will work without errors.

Now, the jbells tibble should be created successfully.

Manipulating Data to Extract Octave, Note, and Frequency:

Code
jbells <- jbells %>%
  mutate(octave = substring(pitch, nchar(pitch)) %>%
           {suppressWarnings(as.numeric(.))} %>%
           ifelse(is.na(.), 4, .),
         note = notes[substr(pitch, 1, 1)],
         note = note + grepl("#", pitch) -
                grepl("b", pitch) + octave * 12 +
                12 * (note < 3),
         freq = 2 ^ ((note - 60) / 12) * 440)

The process begins with extracting the octave from the pitch string and assigning a default value of octave \(4\) if the octave information is not explicitly provided.

Important

The note value is adjusted by accounting for sharps (#) and flats (b), ensuring it is appropriately scaled with the specified or default octave.

Finally, the frequency of each note is calculated using the standard MIDI-to-frequency conversion formula, enabling precise determination of the pitch’s corresponding frequency.

Statistical Insights

  1. Data Processing with dplyr:

    • The code uses dplyr functions such as mutate() and ifelse() to manipulate and process the note data, including extracting and converting pitch and octave information into frequencies and ensuring consistency in handling missing values (e.g., assigning default octave 4).

    • The mapply() function applies the make_sine function to each frequency and duration pair, generating a corresponding waveform for each note.

  2. Vectorized Operations:

    • The operations involving notes, octave, and freq are vectorized, allowing the code to efficiently compute values for all notes in a single operation, which is more efficient than looping through the data manually.

Setting Tempo and Sample Rate:

Code
tempo <- 250
sample_rate <- 44100

The tempo of the music is established at \(250\) beats per minute, setting a lively and energetic pace for the composition. This tempo dictates the speed at which the notes are played, shaping the overall rhythm and mood of the piece. Additionally, the sample rate is configured to \(44,100\ \text{ Hz}\), a standard value commonly used for high-quality audio playback. This ensures that the sound waves are captured and reproduced with precision, maintaining clarity and fidelity in the resulting music. Together, these parameters play a crucial role in defining both the temporal and auditory characteristics of the musical piece.

Sine Wave Generation Function (make_sine)

A sine wave is generated based on the specified frequency (freq) and duration (duration), creating a smooth and continuous waveform that represents the fundamental tone of the note.

Code
make_sine <- function(freq, duration) {
  wave <- sin(seq(0, duration / tempo * 60, 1 / sample_rate) *
              freq * 2 * pi)
  fade <- seq(0, 1, 50 / sample_rate)
  wave * c(fade, rep(1, length(wave) - 2 * length(fade)), rev(fade))
}

This wave serves as the foundation for producing the desired musical pitch with accuracy and clarity. To enhance the auditory experience, a fade effect is applied to the waveform, which gradually increases the amplitude at the beginning and decreases it at the end of each note. This smoothing effect eliminates abrupt starts and stops, ensuring a seamless transition between notes and contributing to a more polished and natural sound in the overall musical sequence.

Implementing Sine Wave Generation Across All Notes:

The sine wave generation process is extended to cover the entire sequence of musical notes, ensuring that each note is accurately synthesized based on its unique frequency and duration. By systematically generating sine waves for all notes in the melody, we create the individual sound components that form the foundation of the musical piece.

Code
jbells_wave <- mapply(make_sine, jbells$freq, jbells$duration) %>% do.call("c", .)

This step ensures consistency in tonal quality and provides the building blocks for combining these waves into a cohesive and harmonious auditory experience.

  • Important

    mapply() applies the make_sine function for each pair of frequency and duration values, generating a list of sine waves for all notes in the sequence.

  • Important

    do.call("c", .) combines the sine waves into a single vector.

Through this approach, the mathematical precision of sine wave synthesis is seamlessly integrated into the creative process of music production.

The sound synthesis aspect is fun because it creates music directly from code by generating sine waves at the appropriate frequencies for each note. This is a mathematical approach to creating sound, which can be thought of as a fun intersection of programming and music theory.

Bringing the Melody to Life

The final step in the process involves playing the generated sound, transforming the programmatically created sequence of notes into an audible melody. This crucial line of code bridges the gap between computational synthesis and human perception, allowing users to experience the music they have meticulously crafted.

Code
play(jbells_wave)

By rendering the sound waves into a playable format, it enables real-time feedback and showcases the harmony of mathematical precision and creative expression in a tangible and engaging way.

Tip

As real-time playback of the generated musical tune may not always be feasible, we recommend saving the melody as an audio file for later use. By exporting the synthesized sound waves to a compatible format, such as WAV or MP3, users can preserve their creation and play it on any device. This not only ensures the accessibility of the melody but also allows for easy sharing and further refinement. Simply follow the provided steps to save your composition and enjoy your programmatically crafted music at your convenience.

With the use of the play() function, the generated sound waves are played in real-time, which provides a dynamic, interactive experience of musical creation.

Save The Tune

Code
# Scale wave data to integer range (-32767 to 32767 for 16-bit audio)
jbells_wave_scaled <- as.integer(jbells_wave / max(abs(jbells_wave)) * 32767)

# Create Wave object
jbells_wave_object <- Wave(left = jbells_wave_scaled, 
                           samp.rate = as.integer(sample_rate), 
                           bit = 16)

# Save the wave file
writeWave(jbells_wave_object, "jingle_bells.mp4")

#cat("The file 'jingle_bells.wav' has been saved successfully in your working directory.\n")

See the Complete Code for Testing in Your R Script


if(!"dplyr" %in% installed.packages()) install.packages("dplyr")
if(!"audio" %in% installed.packages()) install.packages("audio")

library("dplyr")
library("audio")

notes <- c(A = 0, B = 2, C = 3, D = 5, E = 7, F = 8, G = 10)

pitch <- paste("E E E",
               "E E E",
               "E G C D",
               "E",
               "F F F F",
               "F E E E",
               "E D D E",
               "D G",
               "E E E",
               "E E E",
               "E G C D",
               "E",
               "F F F F",
               "F E E E E",
               "G G F D",
               "C",
               "G3 E D C",
               "G3",
               "G3 G3 G3 E D C",
               "A3",
               "A3 F E D",
               "B3",
               "G G F D",
               "E",
               "G3 E D C",
               "G3",
               "G3 E D C",
               "A3 A3",
               "A3 F E D",
               "G G G G A G F D",
               "C C5 B A G F G",
               "E E E G C D",
               "E E E G C D",
               "E F G A C E D F",
               "E C D E F G A G",
               "F F F F F F",
               "F E E E E E",
               "E D D D D E",
               "D D E F G F E D",
               "E E E G C D",
               "E E E G C D",
               "E F G A C E D F",
               "E C D E F G A G",
               "F F F F F F",
               "F E E E E E",
               "G C5 B A G F E D",
               "C C E G C5")

duration <- c(1, 1, 2,
              1, 1, 2,
              1, 1, 1.5, 0.5,
              4,
              1, 1, 1, 1,
              1, 1, 1, 1,
              1, 1, 1, 1,
              2, 2,
              1, 1, 2,
              1, 1, 2,
              1, 1, 1.5, 0.5,
              4,
              1, 1, 1, 1,
              1, 1, 1, 0.5, 0.5,
              1, 1, 1, 1,
              4,
              1, 1, 1, 1,
              3, .5, .5,
              1, 1, 1, 1,
              4,
              1, 1, 1, 1,
              4,
              1, 1, 1, 1,
              4,
              1, 1, 1, 1,
              4,
              1, 1, 1, 1,
              3, 1,
              1, 1, 1, 1,
              1, 1, 1, 1,
              1, 1, 1, 1,
              1, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
              1, 1, 0.5, 0.5, 0.5, 0.5,
              1, 1, 0.5, 0.5, 0.5, 0.5,
              0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
              0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
              1, 0.5, 0.5, 1, 0.5, 0.5,
              1, 0.5, 0.5, 1, 0.5, 0.5,
              1, 0.5, 0.5, 0.5, 0.5, 1,
              1, 0.33, 0.33, 0.33, 1, 0.33, 0.33, 0.33,
              1, 1, 0.5, 0.5, 0.5, 0.5,
              1, 1, 0.5, 0.5, 0.5, 0.5,
              0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
              0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
              1, 0.5, 0.5, 1, 0.5, 0.5,
              1, 0.5, 0.5, 1, 0.5, 0.5,
              0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
              1, 0.33, 0.33, 0.33, 2)

jbells <- data_frame(pitch = strsplit(pitch, " ")[[1]],
                     duration = duration)

jbells <- jbells %>%
  mutate(octave = substring(pitch, nchar(pitch)) %>%
           {suppressWarnings(as.numeric(.))} %>%
           ifelse(is.na(.), 4, .),
         note = notes[substr(pitch, 1, 1)],
         note = note + grepl("#", pitch) -
           grepl("b", pitch) + octave * 12 +
           12 * (note < 3),
         freq = 2 ^ ((note - 60) / 12) * 440)

tempo <- 250

sample_rate <- 44100

make_sine <- function(freq, duration) {
  wave <- sin(seq(0, duration / tempo * 60, 1 / sample_rate) *
                freq * 2 * pi)
  fade <- seq(0, 1, 50 / sample_rate)
  wave * c(fade, rep(1, length(wave) - 2 * length(fade)), rev(fade))
}

jbells_wave <- mapply(make_sine, jbells$freq, jbells$duration) %>%
  do.call("c", .)

play(jbells_wave)

# Scale wave data to integer range (-32767 to 32767 for 16-bit audio)
jbells_wave_scaled <- as.integer(jbells_wave / max(abs(jbells_wave)) * 32767)

# Create Wave object
jbells_wave_object <- Wave(left = jbells_wave_scaled, 
                           samp.rate = as.integer(sample_rate), 
                           bit = 16)

# Save the wave file
writeWave(jbells_wave_object, "jingle_bells.mp4")
Click to listen

Now we have successfully crafted and saved the Jingle Bells tune using R. It’s a great way to showcase the power of programming and mathematics in a festive context. By combining mathematics and programming, we’ve created a simple yet beautiful melody. This approach not only demonstrates the power of R in sound synthesis but also highlights the fascinating relationship between math and art. Whether you’re a musician, a programmer, or someone curious about computational creativity, this example shows how math can bring music to life in the most unexpected ways.

🎉Happy Holidays and a Happy New Year! 🎄✨