Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Authored by Souvik Mandal, Ph.D.

Project Leader & Instructor, Computational Behavioral Sciences, LS100, FAS, Harvard University | Linkedin ID: souvik-mandal-phd


Last updated: 2026-07-02

Open in Colab Open in GitHub Codespaces

Step 0. Setup

/Users/souvikmandal/anaconda3/envs/Analysis_Engineering/bin/python
plotly version: 6.8.0
current default renderer: 'vscode'
available renderers: ['plotly_mimetype', 'jupyterlab', 'nteract', 'vscode', 'notebook', 'notebook_connected', 'kaggle', 'azure', 'colab', 'cocalc'] ...

Using plotly renderer: notebook_connected

Step 1.0. Load one audio file and visualize the waveform

We’ll start by loading a single track; paste the location of the track below.

Now, let’s plot its waveform.

The waveform shows how amplitude (loudness) changes over time.

What are we loading - the y and sr? (click to expand)

When we load an audio file with librosa, we usually write:

y, sr = librosa.load(audio_path, sr=None) # librosa loads the track as mono by default. You can add `mono=True/ False` after sr=None.

Here’s what each variable means:

VariableMeaningIn simple wordsExample
yThe audio time seriesA long list (NumPy array) of numbers representing the waveform — how air pressure changes over time. Each value is a tiny snapshot of loudness.[-0.0021, 0.0053, 0.0089, ...]
srThe sampling rate (in Hz)How many of those numbers are recorded per second. Typical values are 44,100 Hz (CD quality) or 48,000 Hz (studio standard).44100

Additional notes:

  • If you load mono, y has shape (n_samples,).

  • If you load stereo, y has shape (2, n_samples) — one row per channel (Left, Right).

  • You can compute duration easily:

    duration_sec = len(y) / sr
    print(f"Duration: {duration_sec:.2f} seconds")
Loaded: Eagles_Hotel-California.mp3
Sample rate: 44100 Hz  |  Samples: 11438208   |  Duration: 259.37 s
<Figure size 1000x300 with 1 Axes>

If you want an interactive plot for the waveform, run the following code bock.

Step 2 — From Waveform to Frequency: The Discrete Fourier Transform (DFT)

Now that we’ve seen what the audio looks like as a waveform — amplitude changing over time — let’s explore what frequencies make up that sound.
We can take the full track or a short slice of the waveform and compute its Discrete Fourier Transform (DFT) to reveal its frequency components.

The DFT answers the question: “What notes or tones are present in this track (or at a particular moment of it)?”


2.1 The Full-Track DFT

If we take the DFT of the entire song, we get the overall frequency content — a kind of “fingerprint” of all tones and harmonics that ever occur.
For example, it might show that there’s a strong 440 Hz tone (A4) somewhere in the track.

However:

  • It tells us what frequencies are present,

  • but not when they occur.
    Everything is merged together into a single global spectrum.


Understanding the Y-Axis (in Decibels)

The vertical axis shows magnitude — the strength of each frequency component — but it’s measured in decibels (dB) instead of raw amplitude.

That’s why you see negative values on the y-axis.

ConceptExplanation
Reference (0 dB)The loudest frequency in the entire track. It’s the peak amplitude.
Negative values (e.g., –20 dB, –60 dB)Frequencies that are quieter than the loudest one. For instance, –20 dB means 10× quieter; –60 dB means 1000× quieter.
Why use dB?The decibel scale matches how human hearing perceives loudness — logarithmically — and it compresses a huge dynamic range into a readable plot.

.

In simple terms:
0 dB = loudest tone,
–80 dB = much quieter tone,
but all values are still positive in energy — “negative dB” just means “quieter than the loudest frequency.”


Next, we’ll visualize this DFT interactively and overlay typical instrument frequency bands (bass, vocals, cymbals, etc.) to understand where each part of the mix lives in the spectrum.

Track length: 11,438,208 samples  |  Duration: 259.37 s
Frequency resolution: 0.0039 Hz per bin
Loading...

2.2. The short-slice (windowed) DFT

  • Music constantly changes — new notes, instruments, and beats appear over time.

  • To capture this evolution, we take a short slice of the waveform (for example, 1 second).

  • The DFT of this slice shows the frequencies active at that specific moment.

  • Repeating this for overlapping slices across time produces a spectrogram — a visual record of how the frequency content changes through the song.

The time–frequency trade-off

Window LengthFrequency ResolutionTime ResolutionWhen to Use
Long windowPrecise frequency detailsBlurry timingFor stable, steady notes
Short windowLess precise frequenciesBetter timing accuracyFor fast-changing sounds or rhythms

.

Tip: Start with ~1 second, then try shorter/longer and watch the peaks sharpen or blur.

Slice center: 30.00s | Window: 44100 samples | N_fft: 65536 | Δf ≈ 0.67 Hz
Loading...

2.3. Spectrogram: Frequencies Changing Over Time

A spectrogram shows how sound energy is distributed across frequency as the music progresses in time.

AxisRepresentsDescription
X-axisTimeMoves left → right as the song plays
Y-axisFrequencyLow notes at the bottom, high notes at the top
Color (intensity)Amplitude (loudness)Brighter areas mean stronger energy at that frequency

.

Think of it as: DFT on many short, overlapping slices → stack the spectra → color by strength.

Explore before coding:
Try this interactive website to see how different sounds look in a spectrogram:
👉 musiclab.chromeexperiments.com/spectrogram

💡 A spectrogram is like a thermal camera for sound:

  • Horizontal bands often correspond to sustained harmonics — vocals, guitar, or synths.

  • Vertical bursts usually represent transients — drum hits or sharp attacks.


How we compute the spectrogram (and why use the Mel scale + dB) (click to expand)

To build a spectrogram, we:

  1. Slice the audio into overlapping windows.

  2. Compute a Short-Time Fourier Transform (STFT) for each window to find the active frequencies.

  3. Stack all slices together in time order — this becomes the spectrogram.

There are two common types:

  • STFT spectrogram → frequencies in Hz (raw physical values).

  • Mel-spectrogram → frequencies warped to match human hearing (dense in lows, sparse in highs).

We’ll use a Mel-spectrogram because:

  • It mirrors how the ear perceives pitch.

  • It focuses resolution where it matters most for music — bass, vocals, and midrange.

  • It converts energy to decibels (dB) using power_to_db, making quiet details visible.

ParameterEffectWhen to Adjust
n_fftLarger = better frequency resolution, blurrier timeLong sustained tones
hop_lengthSmaller = finer time resolution, more framesPercussive or fast passages
n_melsMore Mel bands = smoother detailRich harmonic music

We’ll keep hop_length consistent with earlier steps so that time axes align across plots.

Loading...

Step 3. Extract Tempo, Beats, and Rhythm Structure

Now that we can visualize how sound changes across frequencies, let’s focus on the time structure of the music —
its tempo (speed), beat pattern, and rhythmic flow.

These rhythmic features form the backbone of most musical tracks:

  • Tempo (BPM) → how fast the music is.

  • Beats → the recurring pulses you could tap your foot to.

  • Downbeats and rhythm structure → how beats group into measures (the musical “grid”).

3.1 : Tempo, Beats

In this step, we’ll use librosa to detect:

  1. The global tempo of the track (in Beats Per Minute, or BPM),

  2. The positions of beats over time,

  3. The local tempo curve — how the rhythm evolves or fluctuates across the song.


How does a computer detect rhythm — and can a song have multiple tempos? (click to expand)

Humans sense rhythm intuitively — we tap our foot or nod along.
Computers must analyze the waveform to find repeating energy patterns, then estimate the timing of those pulses.

Here’s the step-by-step idea:

  1. Onset Detection:
    The program measures sudden changes in loudness or energy.
    Each spike (called an onset) marks the beginning of a sound event — like a drum hit or chord.

  2. Beat and Tempo Estimation:
    By looking at how often onsets occur, we can estimate the tempo (beats per minute, or BPM).
    Most songs have one clear tempo, but some can contain multiple dominant tempos —
    for example, a halftime or double-time section, or a brief tempo shift during a solo.

  3. Dominant BPMs:
    By aggregating tempo energy across time, we can identify one or more dominant tempos that the song tends to revolve around.
    Each dominant BPM shows a stable rhythmic “mode” of the track.

  4. Rhythm Structure:
    Once beats are identified, we can map how tempo evolves — this reveals performance dynamics,
    such as small accelerations, slowdowns, or expressive rubato.

Key takeaway:
A steady pop song will typically have one strong dominant BPM,
while more complex or live performances can show two or more rhythmic modes.

Estimated global tempo (track-level): 147.7 BPM
Detected beats (total): 635

Dominant tempo(s) and support:
  • Bin ≈ 147.7 BPM  |  Estimated: 147.7 BPM  |  Supporting intervals: 604  |  ~Beats: 605

Outlier filtering (±2 SD) allowed in this track?  True
Filtered 1712 local tempo outliers (>2 SD from mean).
Loading...

3.2 — Rhythm Structure (Musical Meter, e.g., 4/4, 3/4, 6/8)

Beyond tempo and beats, most music groups beats into repeating patterns called meter (or time signature).
Examples: 4/4 (four beats per bar), 3/4 (waltz feel), 6/8 (two big pulses, each subdivided into three).

We can estimate meter by looking for accent patterns across beats: some beats are stronger; those accents repeat every bar.
If the strongest repetition happens every 4 beats, we’ll call it ≈4/4; every 3 beats → ≈3/4, etc.

How we estimate meter (and what can be ambiguous) (click to expand)

Method overview

  1. Detect beats and compute an onset strength curve (how much new energy appears).

  2. Sample onset strength at each beat → a sequence of beat accents.

  3. Use autocorrelation of beat accents to find the most likely beats-per-bar (3, 4, 5, 6, 7…).

  4. Repeat in sliding windows to detect meter changes over time.

Ambiguities

  • 6/8 vs 3/4 can look similar (both involve “3”), but their grouping differs (2 groups of 3 vs 3 groups of 2). With only beat-level accents (no sub-beat info), we label ≈6/8 (compound) when a 6-beat cycle is strongest.

  • Live rubato, dense mixes, or weak percussion can reduce confidence. We include a simple confidence score and mark low-confidence spans as “uncertain”.

Global meter estimate: ≈6/8 (compound) (confidence 0.32)
Loading...

Meter segments:
     1.15s →   13.72s : ≈4/4  (conf 0.27)
     4.42s →   17.00s : ≈4/4  (conf 0.36)
     7.65s →   20.27s : ≈4/4  (conf 0.22)
    10.90s →   23.52s : ≈4/4  (conf 0.21)
    14.15s →   26.76s : ≈4/4  (conf 0.34)
    17.43s →   30.03s : ≈4/4  (conf 0.42)
    20.66s →   33.28s : ≈4/4  (conf 0.34)
    23.92s →   36.54s : ≈4/4  (conf 0.23)
    27.16s →   39.80s : ≈4/4  (conf 0.24)
    30.43s →   43.06s : ≈4/4  (conf 0.37)
    33.69s →   46.31s : ≈4/4  (conf 0.28)
    36.94s →   49.57s : ≈2/4  (conf 0.40)
    40.21s →   52.78s : ≈6/8 (compound)  (conf 0.38)
    43.46s →   56.02s : ≈2/4  (conf 0.39)
    46.71s →   59.26s : ≈2/4  (conf 0.33)
    49.96s →   62.52s : ≈2/4  (conf 0.24)
    53.17s →   65.75s : ≈6/8 (compound)  (conf 0.35)
    56.42s →   69.03s : ≈6/8 (compound)  (conf 0.29)
    59.66s →   72.29s : ≈6/8 (compound)  (conf 0.18)
    62.91s →   75.55s : uncertain  (conf 0.09)
    66.16s →   78.80s : ≈6/8 (compound)  (conf 0.17)
    69.43s →   82.06s : ≈6/8 (compound)  (conf 0.25)
    72.67s →   85.30s : ≈6/8 (compound)  (conf 0.27)
    75.95s →   88.56s : ≈6/8 (compound)  (conf 0.41)
    79.20s →   91.84s : ≈4/4  (conf 0.23)
    82.46s →   95.10s : ≈6/8 (compound)  (conf 0.21)
    85.71s →   98.37s : ≈6/8 (compound)  (conf 0.28)
    88.96s →  101.62s : uncertain  (conf 0.12)
    92.24s →  104.90s : ≈6/8 (compound)  (conf 0.19)
    95.50s →  108.16s : ≈7/4  (conf 0.30)
    98.75s →  111.43s : ≈6/8 (compound)  (conf 0.36)
   102.02s →  114.68s : ≈6/8 (compound)  (conf 0.36)
   105.29s →  117.91s : ≈2/4  (conf 0.24)
   108.56s →  121.17s : uncertain  (conf 0.09)
   111.84s →  124.46s : uncertain  (conf 0.08)
   115.09s →  127.70s : uncertain  (conf 0.08)
   118.31s →  130.94s : ≈4/4  (conf 0.34)
   121.59s →  134.22s : uncertain  (conf 0.12)
   124.85s →  137.49s : uncertain  (conf 0.06)
   128.11s →  140.75s : uncertain  (conf 0.01)
   131.36s →  144.01s : uncertain  (conf 0.08)
   134.62s →  147.27s : uncertain  (conf 0.12)
   137.89s →  150.51s : ≈7/4  (conf 0.20)
   141.15s →  153.78s : ≈2/4  (conf 0.17)
   144.42s →  157.03s : ≈2/4  (conf 0.38)
   147.68s →  160.29s : ≈4/4  (conf 0.66)
   150.92s →  163.56s : ≈4/4  (conf 0.63)
   154.18s →  166.81s : ≈4/4  (conf 0.63)
   157.43s →  170.07s : ≈4/4  (conf 0.50)
   160.70s →  173.33s : ≈2/4  (conf 0.25)
   163.96s →  176.58s : ≈2/4  (conf 0.44)
   167.20s →  179.84s : ≈2/4  (conf 0.56)
   170.48s →  183.10s : ≈2/4  (conf 0.51)
   173.74s →  186.36s : ≈2/4  (conf 0.49)
   176.98s →  189.64s : ≈2/4  (conf 0.37)
   180.25s →  192.89s : ≈2/4  (conf 0.45)
   183.50s →  196.12s : ≈2/4  (conf 0.57)
   186.78s →  199.40s : ≈2/4  (conf 0.51)
   190.04s →  202.66s : ≈4/4  (conf 0.43)
   193.28s →  205.93s : ≈4/4  (conf 0.39)
   196.53s →  209.17s : ≈2/4  (conf 0.52)
   199.81s →  212.43s : ≈2/4  (conf 0.36)
   203.07s →  215.68s : ≈2/4  (conf 0.41)
   206.32s →  218.95s : ≈2/4  (conf 0.31)
   209.58s →  222.19s : ≈4/4  (conf 0.25)
   212.84s →  225.45s : ≈4/4  (conf 0.27)
   216.08s →  228.69s : ≈4/4  (conf 0.31)
   219.35s →  231.97s : uncertain  (conf 0.09)
   222.61s →  235.22s : ≈7/4  (conf 0.17)
   225.86s →  238.47s : ≈6/8 (compound)  (conf 0.20)
   229.10s →  241.70s : ≈6/8 (compound)  (conf 0.26)
   232.38s →  244.96s : ≈6/8 (compound)  (conf 0.30)
   235.62s →  248.23s : ≈2/4  (conf 0.36)
   238.88s →  251.50s : ≈6/8 (compound)  (conf 0.41)
   242.10s →  254.75s : ≈6/8 (compound)  (conf 0.45)
   245.39s →  258.03s : ≈6/8 (compound)  (conf 0.47)
   248.65s →  259.23s : ≈6/8 (compound)  (conf 0.31)
   251.90s →  259.23s : ≈6/8 (compound)  (conf 0.40)
   255.16s →  259.23s : ≈2/4  (conf 0.26)

Multiple rhythm structures detected? Yes

Step 4. Chord Map (Time × Root, Colored by Quality)

We’ll visualize the harmony as time on the X-axis and root pitch class on the Y-axis (12 rows: C → B).
Each detected chord becomes a thick horizontal bar placed on its root’s row; the bar color encodes the chord quality:

  • Major, Minor, Sus, Aug, Dim, 7th (dominant/maj7/min7/etc.)

Why this view? It stays readable on full songs, shows where roots occur, and lets us compare qualities at a glance.

Controls in code

  • top_n_chords: show only the N chord labels with the largest total duration (reduces clutter).

  • bar_width: thickness of each row’s bar.

Slash chords (e.g., D/F#) are shown with their full label in the hover/legend, but the bar is placed on the root row (D).

How this chord map is computed (and how to read it) (click to expand)

Pipeline (recap):

  1. Compute beat-synchronous chroma (12 pitch classes per beat).

  2. Match each beat’s chroma to an expanded chord template bank (maj, min, 7, maj7, min7, sus2/4, dim, aug, dim7, m7b5).

  3. Estimate a bass pitch class from a low-octave CQT; keep slash notation in the label if bass ≠ root.

  4. Merge consecutive identical labels into time segments.

Color categories used in the plot:

  • Major (default if not minor/sus/aug/dim/7th)

  • Minor (labels starting with “m”, e.g., Am, Am7, Am7b5)

  • Sus (sus2, sus4)

  • Aug (aug)

  • Dim (dim, dim7, m7b5)

  • 7th (any label containing 7/9/11/13, including maj7 and min7)

Interpreting the plot:

  • Long bars at the same row → stable root; changing colors → changing quality over that root.

  • Rapid row changes → frequent root movement (progressions/turnarounds).

  • Use zoom/hover to examine exact start/end times and full chord labels.

Limitations / notes:

  • Dense mixes and tuning drift can confuse chroma matching; treat results as estimates.

  • top_n_chords filters the view to the most time-dominant chord labels; increase it if you need more detail.

Estimated tempo for beat-sync: 147.7 BPM
Extracted chord segments: 497
Loading...
Loading...
Top chords by total duration (shown):
         Esus2 — 26.1 s
        F#maj7 — 24.3 s
         Emaj7 — 23.3 s
         Dmaj7 — 23.1 s
         Gmaj7 — 19.9 s
          F#m7 — 13.9 s
         Fmaj7 — 13.8 s
           Em7 — 12.2 s
           F#7 — 9.0 s
         F#maj — 7.3 s
          Dmaj — 6.9 s
         Esus4 — 5.7 s

Step 5. Extracting Melody (Pitch Tracking with CREPE)

The melody is the sequence of notes or sung pitches that carry the main tune of a song.
To analyze it, we estimate the fundamental frequency (F₀) — the perceived pitch — at every moment in time.

We’ll use CREPE, a modern deep-learning–based pitch tracker.
Unlike traditional spectral methods (like autocorrelation or YIN), CREPE uses a convolutional neural network (CNN) trained on millions of labeled audio samples to recognize pitch patterns directly from the waveform.

🎶 Think of it as a “neural ear” that listens to the music and tells us the note being sung or played at each instant.

After estimating F₀ over time, we can convert it to musical notes (C, C#, D, E, …) and visualize how the melody rises and falls.


🎵 Chords vs. Melody — How This Differs from the Previous Step

AspectChord ExtractionMelody Extraction (Pitch Tracking)
GoalIdentify harmonic structure — which combination of notes sound together.Track the main pitch (usually a single note) over time — the tune or vocal line.
Data typeBeat-synchronous chroma vectors (12 pitch classes per beat) → chord templates (e.g., Cmaj7, Am, Gsus4).Frame-level fundamental frequency (F₀) estimation (every 10–20 ms).
ResolutionCoarse (1 value per beat or per bar).Fine (hundreds of values per second).
OutputDiscrete symbolic labels (“Cmaj”, “Am7”, “D/F#”, etc.).Continuous pitch curve (in Hz or semitones), convertible to note names (“A4”, “C#5”, …).
Musical functionHarmony — what notes accompany or support the melody.Melody — what note is being sung or played.

Both analyses are complementary, not redundant:

  • Chord extraction reveals the harmonic backdrop — the structure that supports the tune.

  • Melody extraction follows the lead voice — what listeners hum or sing along to.

Together, they provide a more complete picture of the music.


⚙️ About CREPE — Performance, Limitations, and Practical Notes (click to expand)

Why this step is computationally heavier:

  • Unlike previous steps (e.g., FFT or chroma-based chord extraction) that use mathematical transforms,
    CREPE performs deep neural network inference on every small frame of the audio (≈ every 20 ms).

  • Each frame runs through a convolutional model trained on millions of examples, so
    longer tracks → many more frames → higher compute cost.

  • On a typical CPU, a 4–5 minute track may take 2–4 minutes to process; on GPU or Google Colab, it can be 10–20× faster.

Best practices:

  • Use CREPE on monophonic or lightly mixed audio (vocals, lead guitar, flute).
    Polyphonic sources (multiple overlapping notes) can confuse the tracker.

  • The model outputs:

    • time — frame timestamp in seconds

    • frequency (Hz) — estimated pitch (fundamental frequency)

    • confidence — how sure the model is (0–1)

    • note_name — closest musical note after conversion

Relation to earlier steps:

  • While the chord extractor captures harmonic context (groups of notes sounding together),
    CREPE isolates the dominant note at each moment — the melodic contour that defines the tune.

  • Combining both gives a multi-layered representation:

    • Harmony (from chords) + Melody (from CREPE) = rich, human-like description of a song.

Next:
We’ll run CREPE on our track, filter out low-confidence frames, and visualize the melody curve using Plotly —
so you can see the tune unfolding across time.

---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[54], line 28
     27 try:
---> 28     import crepe
     29 except Exception as e:

ModuleNotFoundError: No module named 'crepe'

The above exception was the direct cause of the following exception:

RuntimeError                              Traceback (most recent call last)
Cell In[54], line 30
     28     import crepe
     29 except Exception as e:
---> 30     raise RuntimeError(
     31         "CREPE is not installed. Please run:\n"
     32         "  pip install crepe\n"
     33         "Then re-run this cell."
     34     ) from e
     36 # ---- 2) Mono + resample for CREPE ----
     37 # CREPE expects a mono waveform at a known sample rate; it handles others but resampling is faster/consistent.
     38 if y.ndim > 1:

RuntimeError: CREPE is not installed. Please run:
  pip install crepe
Then re-run this cell.

Where to go next

  • Instruments (tagging with musicnn)

  • Clustering (comparing multiple tracks using embeddings)

Each can be introduced in its own mini-step later.