Jaman's picture
Upload __init__.py
6b55c2e verified
"""Main functions"""
import os
from shutil import copy
import pkg_resources
import tf_keras as k3
import numpy as np
from scipy.signal import resample
import gdown
import librosa
import vamp
import lazycats.np as catnp
from tensorflow import keras
_CHROMA_VAMP_LIB = pkg_resources.resource_filename('autochord', 'res/nnls-chroma.so')
_CHROMA_VAMP_KEY = 'nnls-chroma:nnls-chroma'
_CHORD_MODEL_URL = 'https://drive.google.com/uc?id=1XBn7FyYjF8Ff6EuC7PjwwPzFBLRXGP7n'
_EXT_RES_DIR = os.path.join(os.path.expanduser('~'), '.autochord')
_CHORD_MODEL_DIR = "/content/chroma-seq-bilstm-crf-v1"
_CHORD_MODEL = None
_SAMPLE_RATE = 44100 # operating sample rate for all audio
_SEQ_LEN = 128 # LSTM model sequence length
_BATCH_SIZE = 128 # arbitrary inference batch size
_STEP_SIZE = 2048/_SAMPLE_RATE # chroma vectors step size
_CHROMA_NOTES = ['C','Db','D','Eb','E','F','Gb','G','Ab','A','Bb','B']
_NO_CHORD = 'N'
_MAJMIN_CLASSES = [_NO_CHORD, *[f'{note}:maj' for note in _CHROMA_NOTES],
*[f'{note}:min' for note in _CHROMA_NOTES]]
##############
# Intializers
##############
def _setup_chroma_vamp():
# pylint: disable=c-extension-no-member
vamp_paths = vamp.vampyhost.get_plugin_path()
vamp_lib_fn = os.path.basename(_CHROMA_VAMP_LIB)
for path in vamp_paths:
try:
if not os.path.exists(os.path.join(path, vamp_lib_fn)):
os.makedirs(path, exist_ok=True)
copy(_CHROMA_VAMP_LIB, path)
# try to load to confirm if configured correctly
vamp.vampyhost.load_plugin(_CHROMA_VAMP_KEY, _SAMPLE_RATE,
vamp.vampyhost.ADAPT_NONE)
print(f'autochord: Using NNLS-Chroma VAMP plugin in {path}')
return
except Exception as e:
continue
print(f'autochord WARNING: NNLS-Chroma VAMP plugin not setup properly. '
f'Try copying `{_CHROMA_VAMP_LIB}` in any of following directories: {vamp_paths}')
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)
model_files = gdown.extractall(model_zip)
model_files.sort()
os.remove(model_zip)
print(f'autochord: Chord model downloaded in {model_files[0]}')
return model_files[0]
def _load_model():
global _CHORD_MODEL_DIR, _CHORD_MODEL
try:
if not os.path.exists(_CHORD_MODEL_DIR):
_CHORD_MODEL_DIR = _download_model()
_CHORD_MODEL = k3.models.load_model(_CHORD_MODEL_DIR)
print(f'autochord: Loaded model from {_CHORD_MODEL_DIR}')
except Exception as e:
raise Exception(f'autochord: Error in loading model: {e}')
def _init_module():
print('autochord: Initializing...')
_setup_chroma_vamp()
_load_model()
_init_module()
#################
# Core Functions
#################
def generate_chroma(audio_fn, rollon=1.0):
""" Generate chroma from raw audio using NNLS-chroma VAMP plugin """
samples, fs = librosa.load(audio_fn, sr=None, mono=True)
if fs != _SAMPLE_RATE:
samples = resample(samples, num=int(len(samples)*_SAMPLE_RATE/fs))
out = vamp.collect(samples, _SAMPLE_RATE, 'nnls-chroma:nnls-chroma',
output='bothchroma', parameters={'rollon': rollon})
chroma = out['matrix'][1]
return chroma
def predict_chord_labels(chroma_vectors):
""" Predict (numeric) chord labels from sequence of chroma vectors """
chordseq_vectors = catnp.divide_to_subsequences(chroma_vectors, sub_len=_SEQ_LEN)
pred_labels, _, _, _ = _CHORD_MODEL.predict(chordseq_vectors, batch_size=_BATCH_SIZE)
pred_labels = pred_labels.flatten()
if len(chroma_vectors) < len(pred_labels): # remove pad
pad_st = len(pred_labels)-_SEQ_LEN
pad_ed = pad_st+len(pred_labels)-len(chroma_vectors)
pred_labels = np.append(pred_labels[:pad_st], pred_labels[pad_ed:])
assert len(pred_labels)==len(chroma_vectors)
return pred_labels
def recognize(audio_fn, lab_fn=None):
"""
Perform chord recognition on provided audio file. Optionally,
you may dump the labels on a LAB file (MIREX format) through `lab_fn`.
"""
chroma_vectors = generate_chroma(audio_fn)
pred_labels = predict_chord_labels(chroma_vectors)
chord_labels = catnp.squash_consecutive_duplicates(pred_labels)
chord_lengths = [0] + list(catnp.contiguous_lengths(pred_labels))
chord_timestamps = np.cumsum(chord_lengths)
chord_labels = [_MAJMIN_CLASSES[label] for label in chord_labels]
out_labels = [(_STEP_SIZE*st, _STEP_SIZE*ed, chord_name)
for st, ed, chord_name in
zip(chord_timestamps[:-1], chord_timestamps[1:], chord_labels)]
if lab_fn: # dump labels to file
str_labels = [f'{st}\t{ed}\t{chord_name}'
for st, ed, chord_name in out_labels]
with open(lab_fn, 'w') as f:
for line in str_labels:
f.write("%s\n" % line)
return out_labels