[
  {
    "path": ".gitignore",
    "content": ".vscode/\nbuild/\ndist/\nenv/\nhtmlcov/\n.coverage\n*.DS_Store\n*.pyc\n*.bak\n*.mp3\n*.m4a\n*.tmp\n*.wav\n"
  },
  {
    "path": "README.md",
    "content": "BPM Detector in Python\n=======================\nImplementation of a Beats Per Minute (BPM) detection algorithm, as presented in the paper of G. Tzanetakis, G. Essl and P. Cook titled: \"Audio Analysis using the Discrete Wavelet Transform\".\n\nYou can find it here: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.63.5712\n\nBased on the work done in the MATLAB code located at github.com/panagiop/the-BPM-detector-python.\n\nProcess .wav file to determine the Beats Per Minute.\n\n## Requirements\nTested with Python 3.10.  Key Dependencies: scipy, numpy, pywavelets, matplotlib.  See requirements.txt\n"
  },
  {
    "path": "bpm_detection/bpm_detection.py",
    "content": "# Copyright 2012 Free Software Foundation, Inc.\n#\n# This file is part of The BPM Detector Python\n#\n# The BPM Detector Python is free software; you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation; either version 3, or (at your option)\n# any later version.\n#\n# The BPM Detector Python is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with The BPM Detector Python; see the file COPYING.  If not, write to\n# the Free Software Foundation, Inc., 51 Franklin Street,\n# Boston, MA 02110-1301, USA.\n\nimport argparse\nimport array\nimport math\nimport wave\n\nimport matplotlib.pyplot as plt\nimport numpy\nimport pywt\nfrom scipy import signal\n\n\ndef read_wav(filename):\n    # open file, get metadata for audio\n    try:\n        wf = wave.open(filename, \"rb\")\n    except IOError as e:\n        print(e)\n        return\n\n    # typ = choose_type( wf.getsampwidth() ) # TODO: implement choose_type\n    nsamps = wf.getnframes()\n    assert nsamps > 0\n\n    fs = wf.getframerate()\n    assert fs > 0\n\n    # Read entire file and make into an array\n    samps = list(array.array(\"i\", wf.readframes(nsamps)))\n\n    try:\n        assert nsamps == len(samps)\n    except AssertionError:\n        print(nsamps, \"not equal to\", len(samps))\n\n    return samps, fs\n\n\n# print an error when no data can be found\ndef no_audio_data():\n    print(\"No audio data for sample, skipping...\")\n    return None, None\n\n\n# simple peak detection\ndef peak_detect(data):\n    max_val = numpy.amax(abs(data))\n    peak_ndx = numpy.where(data == max_val)\n    if len(peak_ndx[0]) == 0:  # if nothing found then the max must be negative\n        peak_ndx = numpy.where(data == -max_val)\n    return peak_ndx\n\n\ndef bpm_detector(data, fs):\n    cA = []\n    cD = []\n    correl = []\n    cD_sum = []\n    levels = 4\n    max_decimation = 2 ** (levels - 1)\n    min_ndx = math.floor(60.0 / 220 * (fs / max_decimation))\n    max_ndx = math.floor(60.0 / 40 * (fs / max_decimation))\n\n    for loop in range(0, levels):\n        cD = []\n        # 1) DWT\n        if loop == 0:\n            [cA, cD] = pywt.dwt(data, \"db4\")\n            cD_minlen = len(cD) / max_decimation + 1\n            cD_sum = numpy.zeros(math.floor(cD_minlen))\n        else:\n            [cA, cD] = pywt.dwt(cA, \"db4\")\n\n        # 2) Filter\n        cD = signal.lfilter([0.01], [1 - 0.99], cD)\n\n        # 4) Subtract out the mean.\n\n        # 5) Decimate for reconstruction later.\n        cD = abs(cD[:: (2 ** (levels - loop - 1))])\n        cD = cD - numpy.mean(cD)\n\n        # 6) Recombine the signal before ACF\n        #    Essentially, each level the detail coefs (i.e. the HPF values) are concatenated to the beginning of the array\n        cD_sum = cD[0 : math.floor(cD_minlen)] + cD_sum\n\n    if [b for b in cA if b != 0.0] == []:\n        return no_audio_data()\n\n    # Adding in the approximate data as well...\n    cA = signal.lfilter([0.01], [1 - 0.99], cA)\n    cA = abs(cA)\n    cA = cA - numpy.mean(cA)\n    cD_sum = cA[0 : math.floor(cD_minlen)] + cD_sum\n\n    # ACF\n    correl = numpy.correlate(cD_sum, cD_sum, \"full\")\n\n    midpoint = math.floor(len(correl) / 2)\n    correl_midpoint_tmp = correl[midpoint:]\n    peak_ndx = peak_detect(correl_midpoint_tmp[min_ndx:max_ndx])\n    if len(peak_ndx) > 1:\n        return no_audio_data()\n\n    peak_ndx_adjusted = peak_ndx[0] + min_ndx\n    bpm = 60.0 / peak_ndx_adjusted * (fs / max_decimation)\n    print(bpm)\n    return bpm, correl\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(description=\"Process .wav file to determine the Beats Per Minute.\")\n    parser.add_argument(\"--filename\", required=True, help=\".wav file for processing\")\n    parser.add_argument(\n        \"--window\",\n        type=float,\n        default=3,\n        help=\"Size of the the window (seconds) that will be scanned to determine the bpm. Typically less than 10 seconds. [3]\",\n    )\n\n    args = parser.parse_args()\n    samps, fs = read_wav(args.filename)\n    data = []\n    correl = []\n    bpm = 0\n    n = 0\n    nsamps = len(samps)\n    window_samps = int(args.window * fs)\n    samps_ndx = 0  # First sample in window_ndx\n    max_window_ndx = math.floor(nsamps / window_samps)\n    bpms = numpy.zeros(max_window_ndx)\n\n    # Iterate through all windows\n    for window_ndx in range(0, max_window_ndx):\n\n        # Get a new set of samples\n        # print(n,\":\",len(bpms),\":\",max_window_ndx_int,\":\",fs,\":\",nsamps,\":\",samps_ndx)\n        data = samps[samps_ndx : samps_ndx + window_samps]\n        if not ((len(data) % window_samps) == 0):\n            raise AssertionError(str(len(data)))\n\n        bpm, correl_temp = bpm_detector(data, fs)\n        if bpm is None:\n            continue\n        bpms[window_ndx] = bpm\n        correl = correl_temp\n\n        # Iterate at the end of the loop\n        samps_ndx = samps_ndx + window_samps\n\n        # Counter for debug...\n        n = n + 1\n\n    bpm = numpy.median(bpms)\n    print(\"Completed!  Estimated Beats Per Minute:\", bpm)\n\n    n = range(0, len(correl))\n    plt.plot(n, abs(correl))\n    plt.show(block=True)\n"
  },
  {
    "path": "requirements.txt",
    "content": "certifi @ file:///private/var/folders/sy/f16zz6x50xz3113nwtb9bvq00000gp/T/abs_477u68wvzm/croot/certifi_1671487773341/work/certifi\ncontourpy==1.0.7\ncycler==0.11.0\nfonttools==4.38.0\nkiwisolver==1.4.4\nmatplotlib==3.6.3\nnumpy==1.24.2\npackaging==23.0\nPillow==9.4.0\npyparsing==3.0.9\npython-dateutil==2.8.2\nPyWavelets==1.4.1\nscipy==1.10.0\nsix==1.16.0\n"
  }
]