Repository: orchidas/Chord-Recognition
Branch: master
Commit: fa6a6471fef5
Files: 7
Total size: 17.1 KB
Directory structure:
gitextract_27_45xet/
├── README.md
├── chromagram.py
├── create_templates.py
├── data/
│ ├── chord_templates.json
│ └── test_chords/
│ └── readme.txt
├── hmm.py
└── main.py
================================================
FILE CONTENTS
================================================
================================================
FILE: README.md
================================================
# Chord-Recognition
<h2> Automatic chord recognition in Python </h2>
Chords are identified automatically from monophonic/polyphonic audio. The feature extracted is called the <i>Pitch Class Profile</i>, which is obtained
by computing the <i>Constant Q Transform</i>. Two methods are used for classification:
<ol>
<li>
Template matching - The pitch profile class is correlated with 24 major and minor chords, and the chord with highest correlation is identified.
Details given in the paper <i><a href = "https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.93.4283&rep=rep1&type=pdf">Automatic Chord Recognition from Audio Using Enhanced Pitch
Class Profile</a></i> - Kyogu Lee in Proc. of ICMC, 2006.
</li>
<li>
Hidden Markov Model - HMM is trained based on music theory according to the paper <i><A HREF = "http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.375.2151&rep=rep1&type=pdf">A Robust Mid-level Representation for Harmonic Content in Music
Signals</a></i> - Juan P. Bello, Proc. of ISMIR, 2005. Viterbi decoding is used to estimate chord sequence in multi-timral, polyphonic music.
</ol>
<h2> Usage </h2>
<p> Run main.py with an input file name from data/test_chords/ with flag -m set to the method you want to use for detection, and -p for plotting the result. The default method is template matching. Example:
```
python3 main.py -i 'Grand Piano - Fazioli - major E middle.wav' -m hmm -p True
```
For help, run `python3 main.py -h`
</p>
================================================
FILE: chromagram.py
================================================
"""
Algorithm based on the paper 'Automatic Chord Recognition from
Audio Using Enhanced Pitch Class Profile' by Kyogu Lee
This script computes 12 dimensional chromagram for chord detection
@author ORCHISAMA
"""
from __future__ import division
from scipy.signal import hamming
from scipy.fftpack import fft
import numpy as np
import matplotlib.pyplot as plt
def nearestPow2(inp):
power = np.ceil(np.log2(inp))
return 2**power
"""Function to calculcate Harmonic Power Spectrum from DFT"""
def HPS(dft, M):
hps_len = int(np.ceil(np.size(dft) / (2**M)))
hps = np.ones(hps_len)
for n in range(hps_len):
for m in range(M + 1):
hps[n] *= np.absolute(dft[(2**m) * n])
return hps
"""Function to compute CQT using sparse matrix multiplication, Brown and Puckette 1992- fast"""
def CQT_fast(x, fs, bins, fmin, fmax, M):
threshold = 0.0054 # for Hamming window
K = int(bins * np.ceil(np.log2(fmax / fmin)))
Q = 1 / (2 ** (1 / bins) - 1)
nfft = np.int32(nearestPow2(np.ceil(Q * fs / fmin)))
tempKernel = np.zeros(nfft, dtype=np.complex)
specKernel = np.zeros(nfft, dtype=np.complex)
sparKernel = []
# create sparse Kernel
for k in range(K - 1, -1, -1):
fk = (2 ** (k / bins)) * fmin
N = np.int32(np.round((Q * fs) / fk))
tempKernel[:N] = hamming(N) / N * np.exp(-2 * np.pi * 1j * Q * np.arange(N) / N)
specKernel = fft(tempKernel)
specKernel[np.where(np.abs(specKernel) <= threshold)] = 0
if k == K - 1:
sparKernel = specKernel
else:
sparKernel = np.vstack((specKernel, sparKernel))
sparKernel = np.transpose(np.conjugate(sparKernel)) / nfft
ft = fft(x, nfft)
cqt = np.dot(ft, sparKernel)
ft = fft(x, nfft * (2**M))
# calculate harmonic power spectrum
# harm_pow = HPS(ft,M)
# cqt = np.dot(harm_pow, sparKernel)
return cqt
"""Function to compute constant Q Transform, Judith Brown, 1991 - slow"""
def CQT_slow(x, fs, bins, fmin, fmax):
K = int(bins * np.ceil(np.log2(fmax / fmin)))
Q = 1 / (2 ** (1 / bins) - 1)
cqt = np.zeros(K, dtype=np.complex)
for k in range(K):
fk = (2 ** (k / bins)) * fmin
N = int(np.round(Q * fs / fk))
arr = -2 * np.pi * 1j * Q * np.arange(N) / N
cqt[k] = np.dot(x[:N], np.transpose(hamming(N) * np.exp(arr))) / N
return cqt
"""Function to compute Pitch Class Profile from constant Q transform"""
def PCP(cqt, bins, M):
CH = np.zeros(bins)
for b in range(bins):
CH[b] = np.sum(cqt[b + (np.arange(M) * bins)])
return CH
def compute_chroma(x, fs):
fmin = 96
fmax = 5250
bins = 12
M = 3
nOctave = np.int32(np.ceil(np.log2(fmax / fmin)))
CH = np.zeros(bins)
# Compute constant Q transform
cqt_fast = CQT_fast(x, fs, bins, fmin, fmax, M)
# get Pitch Class Profile
CH = PCP(np.absolute(cqt_fast), bins, nOctave)
return CH
================================================
FILE: create_templates.py
================================================
"""
Algorithm based on the paper 'Automatic Chord Recognition from
Audio Using Enhanced Pitch Class Profile' by Kyogu Lee
This script computes 12 dimensional chromagram for chord detection
@author ORCHISAMA DAS
"""
"""Create pitch profile template for 12 major and 12 minor chords and save them in a json file
Gmajor template = [1,0,0,0,1,0,0,1,0,0,0,0] - needs to be run just once"""
import json
template = dict()
major = ["G", "G#", "A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#"]
minor = ["Gm", "G#m", "Am", "A#m", "Bm", "Cm", "C#m", "Dm", "D#m", "Em", "Fm", "F#m"]
offset = 0
num_chords = len(major)
# initialise lists with zeros
for chord in range(num_chords):
template[major[chord]] = list()
template[minor[chord]] = list()
for note in range(num_chords):
template[major[chord]].append(0)
template[minor[chord]].append(0)
for chord in range(num_chords):
for note in range(num_chords):
if note == 0 or note == 7:
template[major[chord]][(note + offset) % num_chords] = 1
template[minor[chord]][(note + offset) % num_chords] = 1
elif note == 4:
template[major[chord]][(note + offset) % num_chords] = 1
elif note == 3:
template[minor[chord]][(note + offset) % num_chords] = 1
offset += 1
# debugging
for key, value in template.items():
print(key, value)
# save as JSON file
with open("chord_templates.json", "w") as fp:
json.dump(template, fp, sort_keys=False)
print("Saved succesfully to JSON file")
================================================
FILE: data/chord_templates.json
================================================
{"A#m": [0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0], "C#m": [0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0], "A#": [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0], "Dm": [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0], "C#": [0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0], "Bm": [0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1], "G#": [0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0], "Fm": [0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0], "A": [0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0], "C": [1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0], "B": [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1], "E": [0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0], "D": [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1], "G": [1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0], "F": [0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0], "G#m": [0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0], "Em": [1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0], "D#m": [0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1], "Cm": [1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0], "Am": [0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0], "D#": [1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0], "F#": [0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1], "Gm": [1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0], "F#m": [0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1]}
================================================
FILE: data/test_chords/readme.txt
================================================
piano chords downloaded from http://ibeat.org/piano-chords-free/. Give them credit under the Creative Commons License.
================================================
FILE: hmm.py
================================================
"""Automatic chord recogniton with HMM, as suggested by Juan P. Bello in
'A mid level representation for harmonic content in music signals'
@author ORCHISAMA DAS, 2016"""
from __future__ import division
from chromagram import compute_chroma
import os
import numpy as np
"""calculates multivariate gaussian matrix from mean and covariance matrices"""
def multivariate_gaussian(x, meu, cov):
det = np.linalg.det(cov)
val = np.exp(-0.5 * np.dot(np.dot((x - meu).T, np.linalg.inv(cov)), (x - meu)))
try:
val /= np.sqrt(((2 * np.pi) ** 12) * det)
except:
print("Matrix is not positive, semi-definite")
if np.isnan(val):
val = np.finfo(float).eps
return val
"""initialize the emission, transition and initialisation matrices for HMM in chord recognition
PI - initialisation matrix, #A - transition matrix, #B - observation matrix"""
def initialize(chroma, templates, chords, nested_cof):
"""initialising PI with equal probabilities"""
num_chords = len(chords)
PI = np.ones(num_chords) / num_chords
"""initialising A based on nested circle of fifths"""
eps = 0.01
A = np.empty((num_chords, num_chords))
for chord in chords:
ind = nested_cof.index(chord)
t = ind
for i in range(num_chords):
if t >= num_chords:
t = t % num_chords
A[ind][t] = (abs(num_chords // 2 - i) + eps) / (
num_chords**2 + num_chords * eps
)
t += 1
"""initialising based on tonic triads - Mean matrix; Tonic with dominant - 0.8,
tonic with mediant 0.6 and mediant-dominant 0.8, non-triad diagonal elements
with 0.2 - covariance matrix"""
nFrames = np.shape(chroma)[1]
B = np.zeros((num_chords, nFrames))
meu_mat = np.zeros((num_chords, num_chords // 2))
cov_mat = np.zeros((num_chords, num_chords // 2, num_chords // 2))
meu_mat = np.array(templates)
offset = 0
for i in range(num_chords):
if i == num_chords // 2:
offset = 0
tonic = offset
if i < num_chords // 2:
mediant = (tonic + 4) % (num_chords // 2)
else:
mediant = (tonic + 3) % (num_chords // 2)
dominant = (tonic + 7) % (num_chords // 2)
# weighted diagonal
cov_mat[i, tonic, tonic] = 0.8
cov_mat[i, mediant, mediant] = 0.6
cov_mat[i, dominant, dominant] = 0.8
# off-diagonal - matrix not positive semidefinite, hence determinant is negative
# for n in [tonic,mediant,dominant]:
# for m in [tonic, mediant, dominant]:
# if (n is tonic and m is mediant) or (n is mediant and m is tonic):
# cov_mat[i,n,m] = 0.6
# else:
# cov_mat[i,n,m] = 0.8
# filling non zero diagonals
for j in range(num_chords // 2):
if cov_mat[i, j, j] == 0:
cov_mat[i, j, j] = 0.2
offset += 1
"""observation matrix B is a multivariate Gaussian calculated from mean vector and
covariance matrix"""
for m in range(nFrames):
for n in range(num_chords):
B[n, m] = multivariate_gaussian(
chroma[:, m], meu_mat[n, :], cov_mat[n, :, :]
)
return (PI, A, B)
"""Viterbi algorithm to find Path with highest probability - dynamic programming"""
def viterbi(PI, A, B):
(nrow, ncol) = np.shape(B)
path = np.zeros((nrow, ncol))
states = np.zeros((nrow, ncol))
path[:, 0] = PI * B[:, 0]
for i in range(1, ncol):
for j in range(nrow):
s = [(path[k, i - 1] * A[k, j] * B[j, i], k) for k in range(nrow)]
(prob, state) = max(s)
path[j, i] = prob
states[j, i - 1] = state
return (path, states)
================================================
FILE: main.py
================================================
import numpy as np
import os, sys, getopt
import matplotlib.pyplot as plt
from scipy.io.wavfile import read
import json
from chromagram import compute_chroma
import hmm as hmm
def get_templates(chords):
"""read from JSON file to get chord templates"""
with open("data/chord_templates.json", "r") as fp:
templates_json = json.load(fp)
templates = []
for chord in chords:
if chord == "N":
continue
templates.append(templates_json[chord])
return templates
def get_nested_circle_of_fifths():
chords = [
"N",
"G",
"G#",
"A",
"A#",
"B",
"C",
"C#",
"D",
"D#",
"E",
"F",
"F#",
"Gm",
"G#m",
"Am",
"A#m",
"Bm",
"Cm",
"C#m",
"Dm",
"D#m",
"Em",
"Fm",
"F#m",
]
nested_cof = [
"G",
"Bm",
"D",
"F#m",
"A",
"C#m",
"E",
"G#m",
"B",
"D#m",
"F#",
"A#m",
"C#",
"Fm",
"G#",
"Cm",
"D#",
"Gm",
"A#",
"Dm",
"F",
"Am",
"C",
"Em",
]
return chords, nested_cof
def find_chords(
x: np.ndarray,
fs: int,
templates: list,
chords: list,
nested_cof: list = None,
method: str = None,
plot: bool = False,
):
"""
Given a mono audio signal x, and its sampling frequency, fs,
find chords in it using 'method'
Args:
x : mono audio signal
fs : sampling frequency (Hz)
templates: dictionary of chord templates
chords: list of chords to search over
nested_cof: nested circle of fifth chords
method: template matching or HMM
plot: if results should be plotted
"""
# framing audio, window length = 8192, hop size = 1024 and computing PCP
nfft = 8192
hop_size = 1024
nFrames = int(np.round(len(x) / (nfft - hop_size)))
# zero padding to make signal length long enough to have nFrames
x = np.append(x, np.zeros(nfft))
xFrame = np.empty((nfft, nFrames))
start = 0
num_chords = len(templates)
chroma = np.empty((num_chords // 2, nFrames))
id_chord = np.zeros(nFrames, dtype="int32")
timestamp = np.zeros(nFrames)
max_cor = np.zeros(nFrames)
# step 1. compute chromagram
for n in range(nFrames):
xFrame[:, n] = x[start : start + nfft]
start = start + nfft - hop_size
timestamp[n] = n * (nfft - hop_size) / fs
chroma[:, n] = compute_chroma(xFrame[:, n], fs)
if method == "match_template":
# correlate 12D chroma vector with each of
# 24 major and minor chords
for n in range(nFrames):
cor_vec = np.zeros(num_chords)
for ni in range(num_chords):
cor_vec[ni] = np.correlate(chroma[:, n], np.array(templates[ni]))
max_cor[n] = np.max(cor_vec)
id_chord[n] = np.argmax(cor_vec) + 1
# if max_cor[n] < threshold, then no chord is played
# might need to change threshold value
id_chord[np.where(max_cor < 0.8 * np.max(max_cor))] = 0
final_chords = [chords[cid] for cid in id_chord]
elif method == "hmm":
# get max probability path from Viterbi algorithm
(PI, A, B) = hmm.initialize(chroma, templates, chords, nested_cof)
(path, states) = hmm.viterbi(PI, A, B)
# normalize path
for i in range(nFrames):
path[:, i] /= sum(path[:, i])
# choose most likely chord - with max value in 'path'
final_chords = []
indices = np.argmax(path, axis=0)
final_states = np.zeros(nFrames)
# find no chord zone
set_zero = np.where(np.max(path, axis=0) < 0.3 * np.max(path))[0]
if np.size(set_zero) > 0:
indices[set_zero] = -1
# identify chords
for i in range(nFrames):
if indices[i] == -1:
final_chords.append("NC")
else:
final_states[i] = states[indices[i], i]
final_chords.append(chords[int(final_states[i])])
if plot:
plt.figure()
if method == "match_template":
plt.yticks(np.arange(num_chords + 1), chords)
plt.plot(timestamp, id_chord, marker="o")
else:
plt.yticks(np.arange(num_chords), chords)
plt.plot(timestamp, np.int32(final_states), marker="o")
plt.xlabel("Time in seconds")
plt.ylabel("Chords")
plt.title("Identified chords")
plt.grid(True)
plt.show()
return timestamp, final_chords
def main(argv):
input_file = ""
method = ""
plot = False
has_method = False
try:
opts, args = getopt.getopt(argv, "hi:m:p:", ["ifile=", "method=", "plot="])
except getopt.GetoptError:
print("main.py -i <inputfile> -m <method>")
sys.exit(2)
for opt, arg in opts:
if opt == "-h":
print("main.py -i <input_file> -m <method> -p <plot>")
sys.exit()
elif opt in ("-i", "--ifile"):
input_file = arg
elif opt in ("-m", "--method"):
method = arg
has_method = True
elif opt in ("-p", "--plot"):
plot = arg
if not has_method:
method = "match_template"
print("Input file is ", input_file)
print("Method is ", method)
directory = os.getcwd() + "/data/test_chords/"
# read the input file
(fs, s) = read(directory + input_file)
# convert to mono if file is stereo
x = s[:, 0] if len(s.shape) else s
# get chords and circle of fifths
chords, nested_cof = get_nested_circle_of_fifths()
# get chord templates
templates = get_templates(chords)
# find the chords
if method == "match_template":
timestamp, final_chords = find_chords(
x, fs, templates=templates, chords=chords, method=method, plot=plot
)
else:
timestamp, final_chords = find_chords(
x,
fs,
templates=templates,
chords=chords[1:],
nested_cof=nested_cof,
method=method,
plot=plot,
)
# print chords with timestamps
print("Time (s)", "Chord")
for n in range(len(timestamp)):
print("%.3f" % timestamp[n], final_chords[n])
if __name__ == "__main__":
main(sys.argv[1:])
gitextract_27_45xet/ ├── README.md ├── chromagram.py ├── create_templates.py ├── data/ │ ├── chord_templates.json │ └── test_chords/ │ └── readme.txt ├── hmm.py └── main.py
SYMBOL INDEX (13 symbols across 3 files) FILE: chromagram.py function nearestPow2 (line 15) | def nearestPow2(inp): function HPS (line 23) | def HPS(dft, M): function CQT_fast (line 36) | def CQT_fast(x, fs, bins, fmin, fmax, M): function CQT_slow (line 71) | def CQT_slow(x, fs, bins, fmin, fmax): function PCP (line 88) | def PCP(cqt, bins, M): function compute_chroma (line 95) | def compute_chroma(x, fs): FILE: hmm.py function multivariate_gaussian (line 14) | def multivariate_gaussian(x, meu, cov): function initialize (line 31) | def initialize(chroma, templates, chords, nested_cof): function viterbi (line 106) | def viterbi(PI, A, B): FILE: main.py function get_templates (line 10) | def get_templates(chords): function get_nested_circle_of_fifths (line 24) | def get_nested_circle_of_fifths(): function find_chords (line 81) | def find_chords( function main (line 185) | def main(argv):
Condensed preview — 7 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (19K chars).
[
{
"path": "README.md",
"chars": 1472,
"preview": "# Chord-Recognition\n<h2> Automatic chord recognition in Python </h2>\n\nChords are identified automatically from monophoni"
},
{
"path": "chromagram.py",
"chars": 2966,
"preview": "\"\"\"\nAlgorithm based on the paper 'Automatic Chord Recognition from\nAudio Using Enhanced Pitch Class Profile' by Kyogu Le"
},
{
"path": "create_templates.py",
"chars": 1535,
"preview": "\"\"\"\nAlgorithm based on the paper 'Automatic Chord Recognition from\nAudio Using Enhanced Pitch Class Profile' by Kyogu Le"
},
{
"path": "data/chord_templates.json",
"chars": 1054,
"preview": "{\"A#m\": [0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0], \"C#m\": [0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0], \"A#\": [0, 0, 0, 1, 0, 0, 0, 1"
},
{
"path": "data/test_chords/readme.txt",
"chars": 118,
"preview": "piano chords downloaded from http://ibeat.org/piano-chords-free/. Give them credit under the Creative Commons License."
},
{
"path": "hmm.py",
"chars": 3811,
"preview": "\"\"\"Automatic chord recogniton with HMM, as suggested by Juan P. Bello in\n'A mid level representation for harmonic conten"
},
{
"path": "main.py",
"chars": 6552,
"preview": "import numpy as np\nimport os, sys, getopt\nimport matplotlib.pyplot as plt\nfrom scipy.io.wavfile import read\nimport json\n"
}
]
About this extraction
This page contains the full source code of the orchidas/Chord-Recognition GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 7 files (17.1 KB), approximately 5.8k tokens, and a symbol index with 13 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.