autochord

repository·main·Indexed 41 days ago

https://github.com/cjbayron/autochord

A 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.

Tokens
12.3K
Snippets
36
Records
40
Agent score
50%

What's inside autochord

  1. Visualize chord transcriptions with the autochord JS app

    main

    The autochord Javascript app allows you to visualize chord labels.

    Workflow:

    1. Load a song.
    2. Upload a .lab file (generated via the Python library or otherwise) to visualize the chord labels.
    3. (Optional) Load a second .lab file to compare against (e.g., ground-truth labels or predictions from another model).
  2. Install the nnls-chroma VAMP plugin on Linux

    main

    To use the nnls-chroma VAMP plugin on a Linux system, follow these steps:

    1. Copy the .so file to /usr/local/lib/vamp. If the directory does not exist, you must create it first.
    2. Run sudo ldconfig to update the shared library cache.
    cp path/to/nnls-chroma.so /usr/local/lib/vamp
    sudo ldconfig
  3. Chord label naming convention

    main

    The 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.

  4. Implement Cross-Validation splits with get_next_cv_split

    main

    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_split
  5. Prepare data for training with SplitData and QueueData

    main

    To 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)
  6. Convert Keras model to a frozen TensorFlow graph

    main

    To export the model as a frozen graph (useful for deployment in environments without full Keras support), follow these steps:

    1. Access the base_model.
    2. Convert the model to a ConcreteFunction using tf.function.
    3. Use convert_variables_to_constants_v2 to freeze the variables.
    4. Write the graph to disk using 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)
  7. Remove duplicate songs from the dataset

    main

    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)
  8. Automate hyperparameter tuning with Hyperopt and Guild

    main

    To optimize model performance, combine hyperopt for searching the parameter space and guild for experiment tracking.

    1. Define a training function (hpset_trainloop) that accepts hyperparameters as arguments and returns a metric (e.g., validation accuracy).
    2. Define a tuning loop (tuning_loop) that uses guild.run to execute the training function for each hyperparameter set suggested by hyperopt.
    3. Use 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)
  9. Pre-generate and save chord sequence datasets

    main

    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:

    1. Load song metadata from a CSV (e.g., billboard-2.0-manychords.csv).
    2. Use dataloader.get_chord_features_and_labels to fetch raw features.
    3. Use divide_to_subsequences to chunk the data.
    4. Store in a dictionary keyed by song ID.
    5. Save using 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)
  10. Download the Chord Model

    main

    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_DIR
  11. Install dependencies for model development

    main

    To 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 guildai

    Note: You may need to restart your runtime/kernel after installation for changes to take effect.