Chord Detection — User Guide

Complete chord detection with peak limiting: automatic chord transcription from audio using spectral analysis, peak detection, and chord matching.

Author: Shai Cohen Affiliation: Department of Music, Bar-Ilan University, Israel Version: 1.0 (2025) License: MIT License Repo: https://github.com/ShaiCohen-ops/Praat-plugin_AudioTools
Contents:

What this does

This script implements automatic chord detection — analyzing audio to identify musical chords over time. The algorithm extracts spectral peaks, converts frequencies to notes, filters harmonics, matches chord patterns, and outputs a TextGrid with chord labels.

Key Features:

Chord detection pipeline: (1) Windowing: Audio divided into overlapping windows. (2) Spectrum: Each window → FFT spectrum. (3) Peak detection: Find local maxima above adaptive threshold. (4) Peak limiting: Keep only N strongest peaks (default 4). (5) Frequency→MIDI: Convert peaks to MIDI note numbers. (6) Harmonic removal: Filter out peaks that are harmonics of stronger fundamentals. (7) Pitch class set: Convert to pitch classes (0–11). (8) Chord matching: Match pitch class set against chord dictionary. (9) Temporal smoothing: Apply minimum duration threshold. (10) Output: TextGrid with chord labels + individual note labels.

Technical Implementation: The script uses Praat's FFT for spectral analysis. Peak detection uses a relative threshold (25 dB below maximum in window). Peak limiting keeps only the strongest 4 peaks by default. Harmonic filtering removes peaks within 75 cents of expected harmonic positions (2×, 3×, 4×, 5×, 6× fundamental). Chord matching tests all 12 possible roots, comparing interval patterns against a comprehensive dictionary. Output is a two-tier TextGrid: tier 1 = chord names, tier 2 = individual note names.

Quick start

  1. In Praat, select exactly one Sound object.
  2. Run script…chord_detection.praat.
  3. Set Window size (100 ms default) — larger = better frequency resolution.
  4. Set Time step (50 ms default) — smaller = more temporal resolution.
  5. Set frequency range: Min frequency (80 Hz) and Max frequency (2000 Hz).
  6. Adjust peak detection: Min peak separation (40 Hz), Harmonic tolerance (75 cents).
  7. Set Max peaks to keep (4) — reduces analysis to strongest peaks.
  8. Set Minimum chord duration (200 ms) — prevents flickering.
  9. Choose whether to Show all detections or only final segments.
  10. Click OK — analysis runs, TextGrid created, info window shows results.
Quick tip: For monophonic/piano music: set Max peaks to keep = 3–4. For complex polyphonic music: increase to 5–6. Minimum chord duration should match musical context: 200 ms for fast changes, 500 ms+ for slower progressions. Enable Show all detections to see every frame's chord guess before smoothing. Use Window size = 100–200 ms for balance between frequency and time resolution. Harmonic tolerance = 50–100 cents works for most tempered instruments.
Important limitations: POLYPHONY LIMITED — Works best for 2–4 simultaneous notes. Complex orchestral textures may confuse algorithm. TEMPERED INSTRUMENTS — Assumes equal temperament; microtonal music won't match chord dictionary. NO BASS SEPARATION — Doesn't separate bass notes for slash chords. VOICING INSENSITIVE — C Major in root position vs. 1st inversion both labeled "C Major". SPECTRAL LEAKAGE — FFT windowing causes frequency smearing; use larger windows for cleaner peaks. HARMONIC FILTERING may remove valid chord tones if they coincide with harmonics of stronger fundamentals.

Chord Detection Theory

Spectral Analysis Basics

FFT Window Selection

Trade-off: Time resolution vs. frequency resolution.

Window size (W) in seconds determines frequency resolution: Δf = 1 / W (Hz) Example: W = 0.1s (100 ms) → Δf = 10 Hz Good for musical notes (semitone at A4 = 440 Hz is ~26 Hz difference) Time step (T) determines temporal resolution: Analysis frames at times: 0, T, 2T, 3T, ... Overlap = (W - T) / W Example: W=100 ms, T=50 ms → 50% overlap Rectangular window used: simple, no tapering

Why These Parameters?

Window size considerations:

Time step considerations:

Peak Detection Algorithm

Adaptive Threshold

Relative to maximum in window:

Steps: 1. Find maximum power in frequency range: P_max (dB) 2. Set threshold: P_thresh = P_max - 25 dB 3. Detect peaks: local maxima where P > P_thresh Example in window: P_max = -12 dB (strongest component) P_thresh = -12 - 25 = -37 dB Any peak > -37 dB considered Why relative threshold? - Adapts to changing loudness - Ignores quiet noise floor - Consistent across different dynamics Min peak separation: Peaks within 40 Hz merged (keep stronger) Prevents multiple peaks on same partial

Peak Limiting

Keep only strongest N peaks:

After detecting all peaks: 1. Sort peaks by power (descending) 2. Keep only top N (N = max_peaks_to_keep) 3. Discard weaker peaks Example: 8 peaks detected, N=4 Keep 4 strongest, discard 4 weakest Why peak limiting? 1. Reduces noise/irrelevant peaks 2. Focuses on chord tones 3. Matches typical polyphony (3–4 notes) 4. Faster chord matching Default N=4: suitable for triads + melody note Increase for complex jazz chords Decrease for simple triads

Harmonic Filtering

Removing Harmonic Duplicates

Problem: Strong fundamental creates harmonics that look like additional notes.

Algorithm: For each peak pair (i, j): If P_j > P_i (j stronger than i) Check if f_i ≈ n × f_j for n=2,3,4,5,6 "≈" means within harmonic_tolerance cents If f_i is harmonic of f_j: Remove peak i (weaker harmonic) Cent calculation: cents_diff = 1200 × |ln(f_i / (n×f_j)) / ln(2)| Example: f_j = 220 Hz (A3), f_i = 440 Hz (A4) Ratio = 440/220 = 2.0 (octave) If harmonic_tolerance = 75 cents: Remove 440 Hz if within 75 cents of 2×220 Hz Why important? - Guitar/piano have strong harmonics - Without filtering: C chord might detect C3, C4, E4, G4, C5 - With filtering: keeps C3, E4, G4 (chord tones)

📐 Harmonic Series Example

Fundamental 100 Hz:

Harmonics: 200, 300, 400, 500, 600 Hz...

Equal temperament comparison:

100 Hz ≈ G2, 200 Hz ≈ G3, 300 Hz ≈ D4, 400 Hz ≈ G4, 500 Hz ≈ B4, 600 Hz ≈ D5

Without filtering: Detects G, D, B, D (looks like G Major 7th)

With filtering: Keeps only G2 (true fundamental)

Frequency to Note Conversion

MIDI Note Calculation

From frequency to MIDI note number:

MIDI note formula: n = 69 + 12 × log₂(f / 440) Where: 69 = MIDI note number for A4 (440 Hz) 440 = reference frequency (A4) log₂ = base-2 logarithm Alternative form: n = 69 + 12 × ln(f/440) / ln(2) Example: f = 261.63 Hz (C4) n = 69 + 12 × ln(261.63/440) / ln(2) = 69 + 12 × (-0.514) / 0.693 = 69 - 8.9 ≈ 60 (C4) Round to nearest integer: midi_rounded = round(n) Pitch class: pitch_class = midi_rounded mod 12 0=C, 1=C#, 2=D, 3=D#, 4=E, 5=F, 6=F#, 7=G, 8=G#, 9=A, 10=A#, 11=B

Chord Matching Algorithm

Pitch Class Set Matching

Try all 12 roots:

For each candidate root R (0-11): 1. Transpose pitch classes relative to R: interval_i = (PC_i - R + 12) mod 12 2. Sort intervals ascending 3. Create pattern string (e.g., "0,4,7") 4. Look up pattern in chord dictionary 5. If match found: chord = root_name + chord_type Example: pitch classes {0,4,7} (C, E, G) Try R=0: intervals {0,4,7} → "0,4,7" → "C Major" Try R=4: intervals {8,0,3} → "0,3,8" → no match Try R=7: intervals {5,9,0} → "0,5,9" → no match Result: "C Major" Pattern matching is invariant to: - Octave (pitch classes only) - Voicing (inversions give same pitch classes) - Doubling (duplicate pitch classes removed) If no match: list individual notes e.g., "C+E+F#" for {0,4,6}

Comprehensive Chord Dictionary

Triads: 0,4,7 → Major 0,3,7 → Minor 0,3,6 → Diminished 0,4,8 → Augmented 0,5,7 → Sus4 0,2,7 → Sus2 7th chords: 0,4,7,10 → Dominant 7th 0,4,7,11 → Major 7th 0,3,7,10 → Minor 7th 0,3,6,10 → Half-Diminished 7th 0,3,6,9 → Diminished 7th 0,4,8,10 → Augmented 7th 0,3,7,11 → Minor-Major 7th Extended chords: 0,2,4,7,10 → 9th 0,2,4,7,11 → Major 9th 0,2,3,7,10 → Minor 9th 0,4,7,9 → Major 6th 0,3,7,9 → Minor 6th Sus variations: 0,5,7,10 → 7sus4 0,2,7,10 → 7sus2 Add chords: 0,2,4,7 → Add9 0,2,3,7 → Minor Add9 0,4,5,7 → Add11 Dyads: 0,7 → Power chord (5th) 0,5 → 4th 0,3 → Minor 3rd 0,4 → Major 3rd

Temporal Smoothing

Minimum Chord Duration

Prevent flickering:

Problem: Adjacent frames may give different chords Frame 1: C Major Frame 2: C Major Frame 3: A Minor (brief misdetection) Frame 4: C Major Without smoothing: C → C → Am → C (flicker) With smoothing (min_duration = 200 ms): C chord continues through brief Am detection Algorithm: 1. Track current chord and start time 2. When chord changes: a. Calculate duration = current_time - start_time b. If duration ≥ min_chord_duration: Output chord segment c. Start new chord 3. At end: output final chord if long enough Default: min_chord_duration = 200 ms Matches typical chord change timing Filters brief misdetections

Complete Processing Pipeline

SETUP: Select Sound object Parse parameters (window, step, frequency range, etc.) FRAME PROCESSING (for each window): 1. Extract window (rectangular, no tapering) 2. Compute FFT spectrum 3. Find maximum power in frequency range 4. Set threshold = max_power - 25 dB PEAK DETECTION: 5. Find local maxima > threshold 6. Enforce min_peak_separation (merge close peaks) 7. Sort peaks by power, keep top N (peak limiting) HARMONIC FILTERING: 8. For each peak pair, check if weaker is harmonic of stronger 9. Remove harmonic duplicates NOTE CONVERSION: 10. Convert frequencies to MIDI notes 11. Extract pitch classes (0-11) 12. Remove duplicate pitch classes CHORD MATCHING: 13. Try all 12 roots 14. Match interval pattern against chord dictionary 15. If match: chord name; else: list notes TEMPORAL TRACKING: 16. Compare to previous chord 17. If different and previous chord duration ≥ min_duration: Output chord segment 18. Update current chord OUTPUT: 19. Create TextGrid with two tiers: Tier 1: Chord names (smoothed) Tier 2: Individual note names (per frame) 20. Display results in info window 21. Open TextGrid with sound for verification

Parameters

Time Analysis Parameters

ParameterTypeDefaultDescription
Window_size_(ms)positive100Analysis window length (milliseconds)
Time_step_(ms)positive50Time between analysis frames
Skip_initial_transient_(ms)positive10Skip beginning to avoid attack artifacts

Frequency Analysis Parameters

ParameterTypeDefaultDescription
Min_frequency_(Hz)positive80Minimum frequency to analyze
Max_frequency_(Hz)positive2000Maximum frequency to analyze

Peak Detection Parameters

ParameterTypeDefaultDescription
Min_peak_separation_(Hz)positive40Minimum separation between peaks
Harmonic_tolerance_(cents)positive75Tolerance for harmonic identification
Remove_harmonic_duplicatesboolean1 (yes)Remove peaks that are harmonics of stronger peaks
Max_peaks_to_keeppositive4Maximum number of peaks to retain (strongest N)

Output Parameters

ParameterTypeDefaultDescription
Diagnostic_frames_to_analyzenatural10Number of frames for detailed debugging
Minimum_chord_duration_(ms)positive200Minimum duration for chord segment
Show_all_detectionsboolean0 (no)Show every frame's detection (not just segments)

Chord Dictionary

Chord Types Recognized

Triads (3 notes): Major: 0,4,7 (e.g., C E G) Minor: 0,3,7 (e.g., C Eb G) Diminished: 0,3,6 (e.g., C Eb Gb) Augmented: 0,4,8 (e.g., C E G#) Sus4: 0,5,7 (e.g., C F G) Sus2: 0,2,7 (e.g., C D G) 7th Chords (4 notes): Dominant 7th: 0,4,7,10 (e.g., C E G Bb) Major 7th: 0,4,7,11 (e.g., C E G B) Minor 7th: 0,3,7,10 (e.g., C Eb G Bb) Half-Diminished: 0,3,6,10 (e.g., C Eb Gb Bb) Diminished 7th: 0,3,6,9 (e.g., C Eb Gb A) Augmented 7th: 0,4,8,10 (e.g., C E G# Bb) Minor-Major 7th: 0,3,7,11 (e.g., C Eb G B) Extended Chords (5 notes): 9th: 0,2,4,7,10 (e.g., C D E G Bb) Major 9th: 0,2,4,7,11 (e.g., C D E G B) Minor 9th: 0,2,3,7,10 (e.g., C D Eb G Bb) 6th Chords (4 notes): Major 6th: 0,4,7,9 (e.g., C E G A) Minor 6th: 0,3,7,9 (e.g., C Eb G A) Sus7 Chords: 7sus4: 0,5,7,10 (e.g., C F G Bb) 7sus2: 0,2,7,10 (e.g., C D G Bb) Add Chords: Add9: 0,2,4,7 (e.g., C D E G) Minor Add9: 0,2,3,7 (e.g., C D Eb G) Add11: 0,4,5,7 (e.g., C E F G) Dyads (2 notes): 5th: 0,7 (e.g., C G) 4th: 0,5 (e.g., C F) Minor 3rd: 0,3 (e.g., C Eb) Major 3rd: 0,4 (e.g., C E)

Pattern Matching Logic

Invariant properties:

Chord recognition is based on pitch class sets: 1. Octave invariant: C4-E4-G4 = {0,4,7} C3-E3-G3 = {0,4,7} C5-E5-G5 = {0,4,7} All recognized as "C Major" 2. Voicing invariant: Root position: C4-E4-G4 = {0,4,7} 1st inversion: E3-G3-C4 = {0,4,7} (sorted) 2nd inversion: G3-C4-E4 = {0,4,7} All recognized as "C Major" 3. Doubling invariant: C3-E3-G3-C4 = {0,4,7} (C appears twice) Still recognized as "C Major" 4. Order invariant: {0,4,7}, {4,7,0}, {7,0,4} all sorted to {0,4,7} 5. Transposition invariant: Try all 12 roots to find best match {2,5,9} = {0,3,7} with root=2 → "D Minor"

Limitations of Dictionary

Missing chord types:
  • Slash chords: C/E recognized as C Major, not "C/E"
  • Altered chords: C7#9, C7b9 not in dictionary
  • Polychords: (C Major over F Major) not recognized
  • Cluster chords: Dense clusters may return individual notes
  • Quartal harmony: Chords built in 4ths may not match
When chords don't match: If no dictionary match found, script outputs note names separated by "+", e.g., "C+E+F#". This happens for:
  • Non-standard chord voicings
  • Incomplete chords (2 notes not in dictionary)
  • Cluster chords
  • Microtonal intervals

Applications

Automatic Music Transcription

Use case: Convert audio recordings to chord charts

Workflow:

Best for: Pop, rock, jazz standards with clear harmony

Music Education

Use case: Ear training and chord identification practice

Technique:

Music Information Retrieval

Use case: Chord-based music similarity search

Technique:

Composition Analysis

Use case: Analyze harmonic structure of compositions

Technique:

Practical Workflow Examples

🎸 Guitar Chord Recognition

Goal: Identify guitar chords in recording

Optimal settings:

  • Window size: 150 ms (guitar sustain)
  • Min frequency: 82 Hz (low E string)
  • Max peaks to keep: 6 (up to 6 strings)
  • Harmonic tolerance: 50 cents (tempered guitar)
  • Min chord duration: 300 ms (strumming rhythm)

Output: TextGrid with chord names matching strum pattern

🎹 Piano Piece Analysis

Goal: Analyze classical piano piece harmony

Optimal settings:

  • Window size: 200 ms (piano sustain)
  • Min frequency: 27.5 Hz (A0)
  • Max peaks to keep: 4 (typically 3-4 note chords)
  • Remove harmonic duplicates: Yes (piano has strong harmonics)
  • Min chord duration: 500 ms (slow harmonic rhythm)

Output: Chord progression analysis for harmonic study

🎤 Vocal Harmony Analysis

Goal: Detect chords in a cappella singing

Optimal settings:

  • Window size: 100 ms (vowel stability)
  • Min frequency: 100 Hz (low male voice)
  • Max peaks to keep: 4 (SATB = 4 voices)
  • Harmonic tolerance: 100 cents (vocal vibrato)
  • Min chord duration: 250 ms (sung chord duration)

Challenge: Formants may create false peaks; results may need manual correction

Advanced Techniques

Parameter tuning for different music:
  • Fast music (bluegrass, bebop): Smaller window (50-80 ms), smaller time step (25 ms)
  • Slow music (ballads, ambient): Larger window (150-200 ms), larger min chord duration (500 ms+)
  • Dense textures (orchestral): Increase max peaks to 6-8, but expect more "Unknown" chords
  • Sparse textures (solo instrument): Decrease max peaks to 2-3, increase harmonic tolerance
Improving accuracy:
  • Pre-filter: Apply high-pass filter to remove rumble below min_frequency
  • Normalize: Ensure consistent loudness before analysis
  • Isolate section: Analyze verse/chorus separately if harmony differs
  • Manual verification: Use TextGrid editor to correct misdetections
  • Multiple passes: Run with different parameters, compare results

Troubleshooting Common Issues

Problem: Too many "Unknown" chords
Causes: Complex harmony, non-dictionary chords, too many peaks
Solutions: Increase max peaks to keep, extend chord dictionary, accept note listing as output
Problem: Chord changes missed
Causes: Window too large, time step too large, min chord duration too long
Solutions: Decrease window size, decrease time step, decrease min chord duration
Problem: Incorrect chord identification
Causes: Harmonic filtering too aggressive, peak limiting too restrictive, frequency range wrong
Solutions: Increase harmonic tolerance, increase max peaks, adjust frequency range
Problem: TextGrid shows brief flickering chords
Causes: Min chord duration too short, threshold too sensitive
Solutions: Increase min chord duration, increase relative threshold (e.g., 30 dB instead of 25)
Problem: Bass notes mistaken for chord roots
Causes: Algorithm doesn't separate bass from chord
Solutions: Pre-process to separate bass (e.g., high-pass filter at 200 Hz), or manually interpret slash chords

Technical Deep Dive

FFT Implementation Details

Praat's FFT Parameters

Window extraction and spectrum computation:

In script: extract = Extract part: start_time, end_time, "rectangular", 1.0, "no" spectrum = To Spectrum: "no" Praat's To Spectrum: - Computes FFT of entire window - No zero-padding (FFT size = window samples) - Rectangular window (no tapering) - Returns complex spectrum (real + imaginary) Frequency bins: bin_freq = i × sampling_rate / N where N = window samples = window_size × sampling_rate Example: 44.1 kHz, 100 ms window: N = 0.1 × 44100 = 4410 samples Δf = 44100 / 4410 = 10 Hz Good for musical analysis (semitone ~ 26 Hz at A4) Power calculation: power = sqrt(real² + imag²) dB = 20 × log₁₀(power) (if power > 0)

Peak Detection Mathematics

Local Maxima Detection

Checking neighbors:

For bin i (2 ≤ i ≤ n_bins-1): Let P_i = power at bin i Let P_{i-1} = power at bin i-1 Let P_{i+1} = power at bin i+1 Condition for peak: P_i > P_{i-1} AND P_i > P_{i+1} Additional constraint: P_i > threshold (relative to window max) Why check neighbors? - Ensures local maximum - Not just above threshold - Avoids plateau detection Min peak separation: After detection, merge peaks within min_peak_separation Keep stronger of close peaks Prevents multiple detections on same partial

Peak Limiting Algorithm

Bubble sort implementation:

Given: n_peaks detected, keep top N Bubble sort (descending by power): for i from 1 to n_peaks - 1 for j from 1 to n_peaks - i if power_j < power_{j+1} swap peaks j and j+1 After sorting: peaks 1..N are strongest Set n_peaks = N (discard others) Complexity: O(n_peaks²) Acceptable since n_peaks typically < 20 Alternative: Could use selection algorithm but bubble sort simple for small n

Harmonic Filtering Mathematics

Cents Calculation

Logarithmic frequency difference:

Cents formula: cents = 1200 × log₂(f₁ / f₂) For checking if f₁ is harmonic of f₂: expected = f₂ × n (n = 2,3,4,5,6) ratio = f₁ / expected cents_diff = 1200 × |log₂(ratio)| Simplify using natural log: cents_diff = 1200 × |ln(ratio)| / ln(2) Example: f₂ = 220 Hz, f₁ = 440 Hz, n=2 expected = 440 Hz ratio = 440/440 = 1 cents_diff = 0 (perfect octave) With tolerance = 75 cents: Accept if cents_diff < 75 (about ±6.25% frequency deviation) Why 75 cents? - Covers slight detuning - Covers vibrato - Less than half semitone (100 cents)

Chord Matching Optimization

Efficient Pattern Matching

Pre-sorting intervals:

For root R, pitch classes P = {p₁, p₂, ..., pₖ} Transpose: t_i = (p_i - R) mod 12 Sort: sort t_i ascending → T = {t₁, t₂, ..., tₖ} Create pattern string: "t₁,t₂,...,tₖ" Dictionary lookup: pattern → chord name Optimization: Only need to sort k elements k ≤ max_peaks_to_keep (typically 4) Sorting trivial (bubble sort) Why sort? - Makes pattern invariant to voicing order - Standardized representation - Easier dictionary lookup Example: C Major in different voicings: Root: {0,4,7} → "0,4,7" 1st inv: {4,7,0} → sort → {0,4,7} → "0,4,7" 2nd inv: {7,0,4} → sort → {0,4,7} → "0,4,7" All match "0,4,7" in dictionary

Performance Considerations

Computational Complexity

Let: D = duration (seconds) W = window size (seconds) T = time step (seconds) N_frames = D / T Operations per frame: 1. FFT: O(N log N) where N = W × sampling_rate 2. Peak detection: O(N) (scan bins) 3. Peak limiting: O(P²) where P = peaks detected 4. Harmonic filtering: O(P²) 5. Chord matching: O(12 × P log P) Total: O(N_frames × (N log N + P²)) Typical values: D=180s, W=0.1s, T=0.05s → N_frames=3600 N=4410 (44.1 kHz, 100 ms) P≤4 Practical performance: ~1-5 minutes for 3-minute song Most time in FFT computation