Chord Detection — User Guide
Complete chord detection with peak limiting: automatic chord transcription from audio using spectral analysis, peak detection, and chord matching.
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:
- Spectral Analysis — FFT-based frequency analysis
- Peak Detection — Adaptive thresholding with peak limiting
- Harmonic Filtering — Removes harmonic duplicates (e.g., 2×, 3× fundamental)
- Chord Matching — Comprehensive chord dictionary (triads, 7ths, 9ths, etc.)
- TextGrid Output — Two-tier annotation (chords + individual notes)
- Peak Limiting — Keeps only strongest peaks for cleaner detection
- Temporal Smoothing — Minimum chord duration prevents flickering
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
- In Praat, select exactly one Sound object.
- Run script… →
chord_detection.praat.
- Set Window size (100 ms default) — larger = better frequency resolution.
- Set Time step (50 ms default) — smaller = more temporal resolution.
- Set frequency range: Min frequency (80 Hz) and Max frequency (2000 Hz).
- Adjust peak detection: Min peak separation (40 Hz), Harmonic tolerance (75 cents).
- Set Max peaks to keep (4) — reduces analysis to strongest peaks.
- Set Minimum chord duration (200 ms) — prevents flickering.
- Choose whether to Show all detections or only final segments.
- 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:
- Too small (<50 ms): Poor frequency resolution → can't separate close frequencies
- Too large (>200 ms): Poor time resolution → misses quick chord changes
- 100 ms: Good compromise for most music (4–10 Hz resolution)
Time step considerations:
- Too large (>100 ms): May miss brief chords
- Too small (<25 ms): Computationally expensive, similar results
- 50 ms: Typical for chord detection (20 frames/second)
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
| Parameter | Type | Default | Description |
| Window_size_(ms) | positive | 100 | Analysis window length (milliseconds) |
| Time_step_(ms) | positive | 50 | Time between analysis frames |
| Skip_initial_transient_(ms) | positive | 10 | Skip beginning to avoid attack artifacts |
Frequency Analysis Parameters
| Parameter | Type | Default | Description |
| Min_frequency_(Hz) | positive | 80 | Minimum frequency to analyze |
| Max_frequency_(Hz) | positive | 2000 | Maximum frequency to analyze |
Peak Detection Parameters
| Parameter | Type | Default | Description |
| Min_peak_separation_(Hz) | positive | 40 | Minimum separation between peaks |
| Harmonic_tolerance_(cents) | positive | 75 | Tolerance for harmonic identification |
| Remove_harmonic_duplicates | boolean | 1 (yes) | Remove peaks that are harmonics of stronger peaks |
| Max_peaks_to_keep | positive | 4 | Maximum number of peaks to retain (strongest N) |
Output Parameters
| Parameter | Type | Default | Description |
| Diagnostic_frames_to_analyze | natural | 10 | Number of frames for detailed debugging |
| Minimum_chord_duration_(ms) | positive | 200 | Minimum duration for chord segment |
| Show_all_detections | boolean | 0 (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:
- Run chord detection on song
- Export TextGrid to text file
- Format as chord chart with timings
- Verify/correct manually if needed
Best for: Pop, rock, jazz standards with clear harmony
Music Education
Use case: Ear training and chord identification practice
Technique:
- Student plays chord
- Script identifies chord
- Immediate feedback on accuracy
- Visualize spectrum and peaks
Music Information Retrieval
Use case: Chord-based music similarity search
Technique:
- Extract chord sequence from audio
- Create chord progression fingerprint
- Compare with database of songs
- Find songs with similar harmonic structure
Composition Analysis
Use case: Analyze harmonic structure of compositions
Technique:
- Detect chords throughout piece
- Calculate chord frequency distribution
- Identify common progressions
- Compare different sections (verse, chorus)
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