Install the autochord Python library
mainInstall the autochord library using pip to perform automatic chord recognition using TensorFlow.
$ pip install autochordrepository·main·Indexed 41 days ago
https://github.com/cjbayron/autochordA toolset for automatic chord recognition featuring a Python library for audio processing via TensorFlow and a Javascript application for visualizing chord transcriptions. The Python library provides the `autochord.recognize()` function, which utilizes the NNLS-Chroma VAMP plugin and a Bi-LSTM-CRF model to recognize 25 chord classes (12 major triads, 12 minor triads, and 'N' for no-chord). It supports exporting results to MIREX-format .lab files for use in the accompanying visualization app.
Install the autochord library using pip to perform automatic chord recognition using TensorFlow.
$ pip install autochordThe autochord Javascript app allows you to visualize chord labels.
Workflow:
.lab file (generated via the Python library or otherwise) to visualize the chord labels..lab file to compare against (e.g., ground-truth labels or predictions from another model).To use the nnls-chroma VAMP plugin on a Linux system, follow these steps:
.so file to /usr/local/lib/vamp. If the directory does not exist, you must create it first.sudo ldconfig to update the shared library cache.cp path/to/nnls-chroma.so /usr/local/lib/vamp
sudo ldconfigThe library uses a specific string format for chord names. The possible labels include:
'N': No chord detected.'{note}:maj': Major chords (e.g., 'C:maj', 'Db:maj').'{note}:min': Minor chords (e.g., 'C:min', 'Db:min').The supported notes are: C, Db, D, Eb, E, F, Gb, G, Ab, A, Bb, B.
To perform cross-validation, use the get_next_cv_split generator. It takes a train_split and a list of val_splits. In each iteration, it yields a new training set composed of the original train_split plus all val_splits except the current one, and uses the current val_split as the validation set. This ensures that every validation split is used for training exactly once across the full CV loop.
def get_next_cv_split(train_split, val_splits):
num_cv = len(val_splits)
for v_ix in range(num_cv):
addl_train_splits = val_splits[0:v_ix] + val_splits[v_ix+1:]
val_split = val_splits[v_ix]
full_train_feats = np.concatenate((train_split.feats, *[split.feats for split in addl_train_splits]))
full_train_labels = np.concatenate((train_split.labels, *[split.labels for split in addl_train_splits]))
full_train_split = SplitData(feats=full_train_feats, labels=full_train_labels)
yield full_train_split, val_splitTo prepare datasets for training, use QueueData to manage data chunks and SplitData to hold feature/label pairs. The SplitData class supports concatenating new features and labels via .push(feats, labels) and provides a .shape property.
When splitting data, ensure you have enough samples per class to satisfy the requirements of your test and validation splits. The get_splits function (demonstrated in the notebook) uses a QueueData object to pull a specific number of samples per class for testing, multiple validation splits, and finally the remaining data for training.
class SplitData():
def __init__(self, feats=None, labels=None):
self.feats = feats
self.labels = labels
def push(self, feats, labels):
assert(len(feats)==len(labels))
if self.feats is None:
self.feats = feats
else:
self.feats = np.concatenate((self.feats, feats))
if self.labels is None:
self.labels = labels
else:
self.labels = np.concatenate((self.labels, labels))
def __len__(self):
return len(self.labels)
@property
def shape(self):
return (self.feats.shape, self.labels.shape)To export the model as a frozen graph (useful for deployment in environments without full Keras support), follow these steps:
base_model.ConcreteFunction using tf.function.convert_variables_to_constants_v2 to freeze the variables.tf.io.write_graph in either .pb (binary) or .pbtxt (text) format.import tensorflow as tf
from tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2
# 1. Convert to ConcreteFunction
full_model = tf.function(lambda x: model.base_model(x))
full_model = full_model.get_concrete_function(
x=tf.TensorSpec(model.base_model.inputs[0].shape, model.base_model.inputs[0].dtype))
# 2. Freeze variables
frozen_func = convert_variables_to_constants_v2(full_model)
# 3. Save to disk
tf.io.write_graph(graph_or_graph_def=frozen_func.graph,
logdir="models/graph/",
name="frozen_model.pb",
as_text=False)To ensure each song is unique based on its title and artist, use a boolean mask with duplicated(subset=['title','artist'], keep='first'). This allows you to keep only the first occurrence of any song and save the cleaned dataset to a new CSV.
# Create a filter to keep only the first occurrence of each title/artist pair
unique_filter = ~df_songs.duplicated(subset=['title','artist'], keep='first')
df_songs_unique = df_songs[unique_filter]
# Save the unique songs
df_songs_unique.to_csv(f'{base_dir}/billboard-2.0-unique.csv', index=False)To optimize model performance, combine hyperopt for searching the parameter space and guild for experiment tracking.
hpset_trainloop) that accepts hyperparameters as arguments and returns a metric (e.g., validation accuracy).tuning_loop) that uses guild.run to execute the training function for each hyperparameter set suggested by hyperopt.fmin to find the best parameters. Note that fmin minimizes the return value, so if you are maximizing accuracy, return (1 - avg_acc).Important: If a hyperparameter set performs extremely poorly on a single CV split (e.g., acc < 0.5), you can return a high penalty (e.g., 1.0) to prune that branch early.
from hyperopt import hp, tpe, fmin
import guild.ipy as guild
# 1. The training function to track
def hpset_trainloop(hd=256, dp=0.6, opt='adam', lr=0.001, bs=512, si=0):
# ... training logic ...
return best_acc
# 2. The tuning loop for hyperopt
def tuning_loop(hparams):
global _TRAIN, _VAL
avg_acc = 0.0
num_runs = 0
for cv_ix, (train, val) in enumerate(ds.get_next_cv_split()):
_TRAIN, _VAL = train, val
run, acc = guild.run(hpset_trainloop,
hd=int(hparams['base_hidden_dim']),
dp=hparams['drop_rate'],
opt=hparams['opt'],
lr=hparams['lr'],
bs=int(hparams['batch_size']),
si=cv_ix)
if acc < 0.5: return 1.0 # Early exit for bad params
avg_acc += acc
num_runs += 1
return (1 - (avg_acc / num_runs))
# 3. Define search space and run
hparams = {
'base_hidden_dim': hp.choice('base_hidden_dim', [256, 512, 1024]),
'drop_rate': hp.choice('drop_rate', [0.9, 0.7, 0.5, 0.3]),
'opt': hp.choice('opt', ['adam']),
'lr': hp.choice('lr', [1e-2, 1e-3, 3e-4, 1e-4]),
'batch_size': hp.choice('batch_size', [16, 32, 64]),
}
best = fmin(tuning_loop, hparams, algo=tpe.suggest, max_evals=1)To prepare a dataset for training, you can iterate through song IDs, extract features and labels using a dataloader, divide them into subsequences, and save the resulting dictionary to a pickle file.
Recommended workflow:
billboard-2.0-manychords.csv).dataloader.get_chord_features_and_labels to fetch raw features.divide_to_subsequences to chunk the data.pickle.dump or joblib.dump for large datasets.import os
import pickle
import dataloader
import lazycats.np as catnp
_DUMP_DIR = 'data/chordseq'
_LABEL_TYPE = 'majmin'
_SEQ_LEN = 128
chordseq_dict = {}
for _id in df_songs.index:
chroma_vectors, chord_labels = dataloader.get_chord_features_and_labels(_id, label_type=_LABEL_TYPE)
# Chunking
chordseq_vectors = catnp.divide_to_subsequences(chroma_vectors, sub_len=_SEQ_LEN, pad=[0]*24)
chordseq_labels = catnp.divide_to_subsequences(chord_labels, sub_len=_SEQ_LEN, pad=0)
chordseq_dict[_id] = {'feats': chordseq_vectors, 'labels': chordseq_labels}
# Save the dataset
os.makedirs(_DUMP_DIR, exist_ok=True)
with open(f'{_DUMP_DIR}/{_LABEL_TYPE}_{_SEQ_LEN}.pkl', 'wb') as f:
pickle.dump(chordseq_dict, f)If the model is not present locally, you can download it from Google Drive using gdown. The model is typically stored in a hidden directory ~/.autochord.
import os
import gdown
from tensorflow import keras
_CHORD_MODEL_URL = 'https://drive.google.com/uc?id=1XBn7FyYjF8Ff6EuC7PjwwPzFBLRXGP7n'
_EXT_RES_DIR = os.path.join(os.path.expanduser('~'), '.autochord')
_CHORD_MODEL_DIR = os.path.join(_EXT_RES_DIR, 'chroma-seq-bilstm-crf-v1')
def download_model():
os.makedirs(_EXT_RES_DIR, exist_ok=True)
model_zip = os.path.join(_EXT_RES_DIR, 'model.zip')
gdown.download(_CHORD_MODEL_URL, model_zip, quiet=False)
# Extract and cleanup
import zipfile
with zipfile.ZipFile(model_zip, 'r') as zip_ref:
zip_ref.extractall(_EXT_RES_DIR)
os.remove(model_zip)
return _CHORD_MODEL_DIRTo prepare your environment for training and model development, install the following Python packages:
!pip install tensorflow-addons
!pip install mir_eval
!pip install hyperopt
!pip install guildaiNote: You may need to restart your runtime/kernel after installation for changes to take effect.