_ _ __ _ _|_ (_ (_| | (_| |_ computer-aided rhythm analysis toolbox
This notebook shows how to extract microtiming patterns from a recording using carat library.
The procedure is based on the tools proposed in:
Note: At this point it is assumed that there are beat and onset annotations for the recording.
The following steps shows how to:
You can download the notebook and run it locally in your computer.
You can also run it in Google Colab by using the following link.
Run in Google Colab |
You should install the following packages by running the next two cells.
!pip install carat
import matplotlib.pyplot as plt
import os, sys
import numpy as np
import IPython.display as ipd
from carat import annotations, audio, display, microtiming, util
%matplotlib inline
This first step loads the audio file and the corresponding beat/downbeat annotations from a text file.
# use an example audio file provided
audio_path = util.example("chico_audio")
# load audio file (only 30 seconds)
y, sr = audio.load(audio_path, duration=30.0)
# time corresponding to the audio signal
time = np.arange(0, y.size)/sr
plt.figure(figsize=(12,6))
ax1 = plt.subplot(211)
display.wave_plot(y, sr, ax=ax1)
plt.tight_layout()
We can listen to the first 30 seconds of the audio file.
Note: This is a separate track from a performance comprising three drums. The track corresponds to the chico drum, which is the timekeeper of the ensemble. The performance starts by playing the clave pattern (timeline pattern). After a few rhythmic cycles the chico drum starts playing an ostinato pattern that articulates the four subdivisions of the beat.
ipd.Audio(y, rate=sr)
# use beat annotations provided for the example audio file
beat_annotations_path = util.example("chico_beats")
# load beats and beat labels
beats, beat_labs = annotations.load_beats(beat_annotations_path)
# plot waveform and beats for the first 30 seconds
plt.figure(figsize=(12,6))
ax1 = plt.subplot(211)
display.wave_plot(y, sr, ax=ax1, beats=beats)
plt.tight_layout()
This second step loads the onsets from an annotations file. Then, beats are aligned to the closest onsets.
# use onset annotations provided for the example audio file
onset_annotations_file = util.example("chico_onsets")
# load onset annotations
onsets, _ = annotations.load_onsets(onset_annotations_file)
# compute beats from onsets
beat_ons = microtiming.beats_from_onsets(beats, onsets)
# plot waveform and beats
plt.figure(figsize=(12,6))
ax1 = plt.subplot(211)
display.wave_plot(y, sr, ax=ax1, beats=beats)
# plot aligned beats
ax2 = plt.subplot(212, sharex=ax1)
display.wave_plot(y, sr, ax=ax2, beats=beat_ons)
ax1.set_xlim([26.1, 29.5])
plt.tight_layout()
Note: The alignment is applied in order to use the onsets as the reference for defining the beat starting point. Note that the first and last beats of the plot are clearly better aligned to the onsets.
Because of the variations in tempo that the recordings can exhibit, it is not possible to analyse timing data in absolute durations (e.g. in milliseconds). Therefore, it is necesary to normalize the durations, and in this case the beat duration is choosen as a reference. The onsets are then converted to their relative position with regards to the beats and are asigned to a position in an isochronous metrical grid (equally distributed subdivisions within the beat). The temporal reference for the beats that we consider is the estimation based on the closest onsets to the annotated beats, as previously computed. Anyway, in the following the two options are compared, i.e. normalizing with the annotated beats or with the beats aligned to the onsets.
# normalization of onsets to the annotated beats
ons1_norm = microtiming.normalize_onsets(beats, onsets)
# normalization of onsets to the beats aligned to the onsets
ons2_norm = microtiming.normalize_onsets(beat_ons, onsets)
# create default metrical grid (isochronous, 4 subdivisions)
metrical_grid = microtiming.define_metrical_grid()
# assign onsets to metrical grid normalized according to annotated beats
ons1_in_grid = microtiming.onsets_to_metrical_grid(ons1_norm, metrical_grid)
# assign onsets to metrical grid normalized according to the beats aligned to onsets
ons2_in_grid = microtiming.onsets_to_metrical_grid(ons2_norm, metrical_grid)
# plot the obtained microtiming patterns
plt.figure(figsize=(8,8))
ax1 = plt.subplot(211)
display.onsets_in_grid_plot(ons1_in_grid[0], ax=ax1, hist_ons=True)
ax2 = plt.subplot(212)
display.onsets_in_grid_plot(ons2_in_grid[0], ax=ax2, hist_ons=True)
plt.tight_layout()
Note: At the bottom of the figure an histogram of the onset locations is depicted. We fit a normal distribution to the onsets in each subdivision, and compute the mean and stdev values, which are shown in the figure. The mean values of the location of the subdivisions remains quite the same, but the stdev is clearly reduced when using the closest onset to the annotate beat as temporal reference. Interestingly enough, the third and fourth subdivisions are clearly ahead compared to an isochronous subdivision of the beat in four.
In the end, we compare the microtiming pattern of two timekeeper instruments from different music styles: the chico drum from a candombe recording, and the tamborim from a samba enredo recording.
# use the example audio file of a taborim provided
audio_path = util.example("tamborim_audio")
# load audio file
y, sr = audio.load(audio_path)
# time corresponding to the audio signal
time = np.arange(0, y.size)/sr
# beat annotations file
beat_annotations_path = util.example("tamborim_beats")
# load beats
beats, _ = annotations.load_beats(beat_annotations_path, delimiter=' ')
# plot waveform and beats
plt.figure(figsize=(12,6))
ax1 = plt.subplot(211)
display.wave_plot(y, sr, ax=ax1, beats=beats)
plt.tight_layout()
We can listen to the audio file.
ipd.Audio(y, rate=sr)
We repeat all the steps for the tamborim audio file.
# use onset annotations provided for the example audio file
onset_annotations_file = util.example("tamborim_onsets")
# load onset annotations
onsets, _ = annotations.load_onsets(onset_annotations_file, delimiter=' ')
# compute beats from onsets
beat_ons = microtiming.beats_from_onsets(beats, onsets)
# normalization of onsets to the annotated beats
ons3_norm = microtiming.normalize_onsets(beat_ons, onsets)
# create default metrical grid (isochronous, 4 subdivisions)
metrical_grid = microtiming.define_metrical_grid()
# assign onsets to metrical grid normalized according to the beats aligned to onsets
ons3_in_grid = microtiming.onsets_to_metrical_grid(ons3_norm, metrical_grid)
We can now compare the microtiming patterns of each instrument.
# plot the obtained microtiming patterns
plt.figure(figsize=(8,8))
ax1 = plt.subplot(211)
display.onsets_in_grid_plot(ons2_in_grid[0], ax=ax1)
ax2 = plt.subplot(212)
display.onsets_in_grid_plot(ons3_in_grid[0], ax=ax2, color='crimson')
plt.tight_layout()
Note: The microtiming pattern of the tamborim shows an even more drastic compresion of the location of onsets. Is worth noting that the fourth stroke of the instrument is closer to a division in three of the beat than to a division in four (66% compared to 75%).