commit 7c422413375cbcddaf51c3019fe52fc0e4a859e6 Author: bic Date: Mon Aug 21 09:40:48 2023 +0200 da qua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..69c37d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,156 @@ + +stage + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Ignore qmlc files +*.qmlc + +.idea + +# Do not commit VPN certs. +dome.conf + +main.pyproject.user +*.autosave +builds +hosts +.python-version +host_vars/ +.vscode + +assets/ diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..b815cb7 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,213 @@ +[[package]] +name = "audioread" +version = "2.1.9" +description = "multi-library, cross-platform audio decoding" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.12" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.0.4" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "numpy" +version = "1.22.2" +description = "NumPy is the fundamental package for array computing with Python." +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "pyacoustid" +version = "1.2.2" +description = "bindings for Chromaprint acoustic fingerprinting and the Acoustid API" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +audioread = "*" +requests = "*" + +[[package]] +name = "pygments" +version = "2.11.2" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "requests" +version = "2.27.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "rich" +version = "11.2.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.6.2,<4.0.0" + +[package.dependencies] +colorama = ">=0.4.0,<0.5.0" +commonmark = ">=0.9.0,<0.10.0" +pygments = ">=2.6.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] + +[[package]] +name = "urllib3" +version = "1.26.8" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.10" +content-hash = "8c25537d733e8f48e66f2612d9e0af3f695d256b280661d9f2db2c13ca0b8a4a" + +[metadata.files] +audioread = [ + {file = "audioread-2.1.9.tar.gz", hash = "sha256:a3480e42056c8e80a8192a54f6729a280ef66d27782ee11cbd63e9d4d1523089"}, +] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, +] +click = [ + {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, + {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +commonmark = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +numpy = [ + {file = "numpy-1.22.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:515a8b6edbb904594685da6e176ac9fbea8f73a5ebae947281de6613e27f1956"}, + {file = "numpy-1.22.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76a4f9bce0278becc2da7da3b8ef854bed41a991f4226911a24a9711baad672c"}, + {file = "numpy-1.22.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:168259b1b184aa83a514f307352c25c56af111c269ffc109d9704e81f72e764b"}, + {file = "numpy-1.22.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3556c5550de40027d3121ebbb170f61bbe19eb639c7ad0c7b482cd9b560cd23b"}, + {file = "numpy-1.22.2-cp310-cp310-win_amd64.whl", hash = "sha256:aafa46b5a39a27aca566198d3312fb3bde95ce9677085efd02c86f7ef6be4ec7"}, + {file = "numpy-1.22.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:55535c7c2f61e2b2fc817c5cbe1af7cb907c7f011e46ae0a52caa4be1f19afe2"}, + {file = "numpy-1.22.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:60cb8e5933193a3cc2912ee29ca331e9c15b2da034f76159b7abc520b3d1233a"}, + {file = "numpy-1.22.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b536b6840e84c1c6a410f3a5aa727821e6108f3454d81a5cd5900999ef04f89"}, + {file = "numpy-1.22.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2638389562bda1635b564490d76713695ff497242a83d9b684d27bb4a6cc9d7a"}, + {file = "numpy-1.22.2-cp38-cp38-win32.whl", hash = "sha256:6767ad399e9327bfdbaa40871be4254d1995f4a3ca3806127f10cec778bd9896"}, + {file = "numpy-1.22.2-cp38-cp38-win_amd64.whl", hash = "sha256:03ae5850619abb34a879d5f2d4bb4dcd025d6d8fb72f5e461dae84edccfe129f"}, + {file = "numpy-1.22.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:d76a26c5118c4d96e264acc9e3242d72e1a2b92e739807b3b69d8d47684b6677"}, + {file = "numpy-1.22.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:15efb7b93806d438e3bc590ca8ef2f953b0ce4f86f337ef4559d31ec6cf9d7dd"}, + {file = "numpy-1.22.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:badca914580eb46385e7f7e4e426fea6de0a37b9e06bec252e481ae7ec287082"}, + {file = "numpy-1.22.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94dd11d9f13ea1be17bac39c1942f527cbf7065f94953cf62dfe805653da2f8f"}, + {file = "numpy-1.22.2-cp39-cp39-win32.whl", hash = "sha256:8cf33634b60c9cef346663a222d9841d3bbbc0a2f00221d6bcfd0d993d5543f6"}, + {file = "numpy-1.22.2-cp39-cp39-win_amd64.whl", hash = "sha256:59153979d60f5bfe9e4c00e401e24dfe0469ef8da6d68247439d3278f30a180f"}, + {file = "numpy-1.22.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a176959b6e7e00b5a0d6f549a479f869829bfd8150282c590deee6d099bbb6e"}, + {file = "numpy-1.22.2.zip", hash = "sha256:076aee5a3763d41da6bef9565fdf3cb987606f567cd8b104aded2b38b7b47abf"}, +] +pyacoustid = [ + {file = "pyacoustid-1.2.2.tar.gz", hash = "sha256:c279d9c30a7f481f1420fc37db65833b5f9816cd364dc2acaa93a11c482d4141"}, +] +pygments = [ + {file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"}, + {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"}, +] +requests = [ + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, +] +rich = [ + {file = "rich-11.2.0-py3-none-any.whl", hash = "sha256:d5f49ad91fb343efcae45a2b2df04a9755e863e50413623ab8c9e74f05aee52b"}, + {file = "rich-11.2.0.tar.gz", hash = "sha256:1a6266a5738115017bb64a66c59c717e7aa047b3ae49a011ede4abdeffc6536e"}, +] +urllib3 = [ + {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, + {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5c5c3e9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[tool.poetry] +name = "vibroscopio" +version = "0.1.0" +description = "" +authors = ["gibix "] + +[tool.poetry.dependencies] +python = "^3.10" +pyacoustid = "^1.2.2" +rich = "^11.2.0" +click = "^8.0.4" +numpy = "^1.22.2" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +vibroscopio = 'vibroscopio.cli:main' diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..c5f0aaa --- /dev/null +++ b/readme.md @@ -0,0 +1,9 @@ + +http://coding-geek.com/how-shazam-works/ + +https://link.springer.com/article/10.1007/s11042-020-09912-4 +https://github.com/swesterfeld/audiowmark +https://github.com/ebu/awesome-broadcasting#network--storage-testing +https://medium.com/intrasonics/a-fingerprint-for-audio-3b337551a671 + +https://github.com/kdave/audio-compare/blob/master/correlation.py diff --git a/scripts/prober.liq b/scripts/prober.liq new file mode 100644 index 0000000..9a3d6eb --- /dev/null +++ b/scripts/prober.liq @@ -0,0 +1,44 @@ +#!/usr/bin/liquidsoap + +in = mksafe(single("/srv/assets/sample.mp3")) + +def vibroscopio(path) = + # out = process.read.lines( + # "/home/me/path/to/beet random -f '$path' #{arg}" + # ) + + # log(out) + # ) + log(path) +end + +output.file( + %wav( + mono=true, + channels=1, + duration=30. + ), + "/srv/stage/source/%H.%M.%S.wav", + reopen_when = {0s or 30s}, + reopen_delay = 20., + on_close = vibroscopio, + in +) + +output.file( + %wav( + mono=true, + channels=1, + duration=60. + ), + "/srv/stage/target/%H.%M.%S.wav", + reopen_when = {0s}, + reopen_delay = 10., + on_close = vibroscopio, + in +) + +# thread.run(every=3600., +# fun () -> (), +# process.read.lines("find /srv/stage/* -type f -mmin +2 -delete") +# ) diff --git a/spectrogram.png b/spectrogram.png new file mode 100644 index 0000000..439eb60 Binary files /dev/null and b/spectrogram.png differ diff --git a/vibroscopio/cli.py b/vibroscopio/cli.py new file mode 100644 index 0000000..4d30a73 --- /dev/null +++ b/vibroscopio/cli.py @@ -0,0 +1,35 @@ + +import click +import logging +from rich.logging import RichHandler +from vibroscopio.fingerprint import correlate, signature + +@click.group() +@click.option('--verbosity', '-v', default=2, count=True) +def cli(verbosity): + FORMAT = "%(message)s" + logging.basicConfig( + level=verbosity * 10, + format=FORMAT, + datefmt="[%X]", + handlers=[RichHandler()]) + +@click.command() +@click.argument('file') +def fingerprint(path): + s = signature(path) + print(s) + +@click.command() +@click.option('--source', '-s', help='source is expected to be bigger than target', type=click.Path(exists=True)) +@click.option('--target', '-t', help='target is supposed to be smaller than source', type=click.Path(exists=True)) +@click.option('--length', '-l', default=120, help='seconds of source length') +@click.option('--span', '-s', default=60, help='allowd span allignment in seconds') +def compare(source, target, length, span): + correlate(source, target, length, length * 7) + +cli.add_command(fingerprint) +cli.add_command(compare) + +def main(): + cli() diff --git a/vibroscopio/fingerprint.py b/vibroscopio/fingerprint.py new file mode 100644 index 0000000..472904a --- /dev/null +++ b/vibroscopio/fingerprint.py @@ -0,0 +1,142 @@ + +from typing import final +import acoustid + +def signature(path): + return acoustid.fingerprint_file(path) + +import subprocess +import numpy +import os + +# step size (in points) of cross correlation +step = 1 + +# minimum number of points that must overlap in cross correlation +# exception is raised if this cannot be met +min_overlap = 20 + +# report match when cross correlation has a peak exceeding threshold +threshold = 0.5 + +# calculate fingerprint +# Generate file.mp3.fpcalc by "fpcalc -raw -length 500 file.mp3" +def calculate_fingerprints(filename: str, length: int): + if os.path.exists(filename + '.fpcalc'): + print("Found precalculated fingerprint for %s" % (filename)) + f = open(filename + '.fpcalc', "r") + fpcalc_out = ''.join(f.readlines()) + f.close() + else: + print("Calculating fingerprint by fpcalc for %s" % (filename)) + + + fpcalc_out = str() + + try: + fpcalc_out = str(subprocess.check_output(['fpcalc', '-raw', filename])) + except subprocess.CalledProcessError as e: + fpcalc_out = str(e.output) + + fpcalc_out = fpcalc_out.strip().replace('\\n', '').replace("'", "") + + fingerprint_index = fpcalc_out.find('FINGERPRINT=') + 12 + # convert fingerprint to list of integers + fingerprints = list(map(int, fpcalc_out[fingerprint_index:].split(','))) + + return fingerprints + +# returns correlation between lists +def correlation(listx, listy): + if len(listx) == 0 or len(listy) == 0: + # Error checking in main program should prevent us from ever being + # able to get here. + raise Exception('Empty lists cannot be correlated.') + if len(listx) > len(listy): + listx = listx[:len(listy)] + elif len(listx) < len(listy): + listy = listy[:len(listx)] + + covariance = 0 + for i in range(len(listx)): + covariance += 32 - bin(listx[i] ^ listy[i]).count("1") + + covariance = covariance / float(len(listx)) + + correlation = covariance / 32 + + if correlation is None: + return + + return correlation + + +# return cross correlation, with listy offset from listx +def cross_correlation(listx, listy, offset): + if offset > 0: + listx = listx[offset:] + listy = listy[:len(listx)] + elif offset < 0: + offset = -offset + listy = listy[offset:] + listx = listx[:len(listy)] + if min(len(listx), len(listy)) < min_overlap: + # Error checking in main program should prevent us from ever being + # able to get here. + return + #raise Exception('Overlap too small: %i' % min(len(listx), len(listy))) + corr = correlation(listx, listy) + return corr + +# cross correlate listx and listy with offsets from span +def compare(listx, listy, span, step): + if span > min(len(listx), len(listy)): + # Error checking in main program should prevent us from ever being + # able to get here. + raise Exception('span >= sample size: %i >= %i\n' + % (span, min(len(listx), len(listy))) + + 'Reduce span, reduce crop or increase sample_time.') + + corr_xy = [] + + # for offset in numpy.arange(-span, 1, step): + for offset in numpy.arange(-span, span + 1, step): + tmp_corr = cross_correlation(listx, listy, offset) + corr_xy.append(tmp_corr) + + corr_xy = list(filter(None, corr_xy)) + + return corr_xy + +# return index of maximum value in list +def max_index(listx): + max_index = 0 + max_value = listx[0] + for i, value in enumerate(listx): + if value > max_value: + max_value = value + max_index = i + return max_index + +def get_max_corr(corr, source, target, span): + max_corr_index = max_index(corr) + max_corr_offset = -span + max_corr_index * step + print("max_corr_index = ", max_corr_index, "max_corr_offset = ", max_corr_offset) + # report matches + if corr[max_corr_index] > threshold: + print("File A: %s" % (source)) + print("File B: %s" % (target)) + print('Match with correlation of %.2f%% at offset %i' + % (corr[max_corr_index] * 100.0, max_corr_offset)) + + +def correlate(source, target, length, span): + fingerprint_source = calculate_fingerprints(target, length) + # print(len(fingerprint_source)) + + fingerprint_target = calculate_fingerprints(source, 59) + # print(len(fingerprint_target)) + + corr = compare(fingerprint_source, fingerprint_target, span, step) + + max_corr_offset = get_max_corr(corr, source, target, span) diff --git a/vibroscopio/stream.py b/vibroscopio/stream.py new file mode 100644 index 0000000..e69de29