Skip to content

Commit

Permalink
Initial import
Browse files Browse the repository at this point in the history
  • Loading branch information
kdave committed Oct 7, 2021
0 parents commit 4f922b7
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 0 deletions.
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Simple tool to compare audio files

NOTE: I haven't written this, merely found it on the internet and ported to
python 3.

* https://shivama205.medium.com/audio-signals-comparison-23e431ed2207 "Audio signals: Comparison"
* https://gist.github.com/shivama205/5578f999a9c88112f5d042ebb83e54d5 scripts from the article

Related projects:

* https://acoustid.org/chromaprint fpcalc
* numpy

Usage:

Sample files captured from a streaming source without exact start, duration but
are the same song:

$ ./compare.py -i file1.mp3 -o file2.mp3
Calculating fingerprint by fpcalc for file1.mp3
Calculating fingerprint by fpcalc for file2.mp3
File A: file1.mp3
File B: file2.mp3
Match with correlation of 63.74% at offset 55

$ ./compare.py -i file2.mp3 -o file1.mp3
Calculating fingerprint by fpcalc for file2.mp3
Calculating fingerprint by fpcalc for file1.mp3
File A: file2.mp3
File B: file2.mp3
Match with correlation of 63.74% at offset -5

For some files the swapped order may not lead to the same results due to offset
or the way the `fpcalc` fingerprint is generated (see help).

$ ./compare.py -i file2.mp3 -o file3.mp3
Calculating fingerprint by fpcalc for file2.mp3
Calculating fingerprint by fpcalc for file3.mp3
File A: file2.mp3
File B: file3.mp3
Match with correlation of 93.01% at offset -24

$ ./compare.py
Calculating fingerprint by fpcalc for file1.mp3
Calculating fingerprint by fpcalc for file3.mp3
File A: file1.mp3
File B: file3.mp3
Match with correlation of 63.96% at offset 31

Internally the fingerprint is generated by `fpcalc -length 500`, cached
versions can be produced by `fpcalc-gen`.

Changes:

- port to python3
- print the similary as percents
- print input files on separate lines
- support precalculated fingerprint in `file.mp3.fpcalc`
20 changes: 20 additions & 0 deletions compare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/python3
# compare.py
import argparse
from correlation import correlate

def initialize():
parser = argparse.ArgumentParser()
parser.add_argument("-i ", "--source-file", help="source file")
parser.add_argument("-o ", "--target-file", help="target file")
args = parser.parse_args()

SOURCE_FILE = args.source_file if args.source_file else None
TARGET_FILE = args.target_file if args.target_file else None
if not SOURCE_FILE or not TARGET_FILE:
raise Exception("Source or Target files not specified.")
return SOURCE_FILE, TARGET_FILE

if __name__ == "__main__":
SOURCE_FILE, TARGET_FILE = initialize()
correlate(SOURCE_FILE, TARGET_FILE)
111 changes: 111 additions & 0 deletions correlation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/python3

# correlation.py
import subprocess
import numpy
import os

# seconds to sample audio file for
sample_time = 500
# number of points to scan cross correlation over
span = 150
# 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):
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(subprocess.check_output(['fpcalc', '-raw', '-length', str(sample_time), filename])).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))

return covariance/32

# 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)))
return correlation(listx, listy)

# cross correlate listx and listy with offsets from -span to 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, span + 1, step):
corr_xy.append(cross_correlation(listx, listy, offset))
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):
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):
fingerprint_source = calculate_fingerprints(source)
fingerprint_target = calculate_fingerprints(target)

corr = compare(fingerprint_source, fingerprint_target, span, step)
max_corr_offset = get_max_corr(corr, source, target)
2 changes: 2 additions & 0 deletions fpcalc-gen
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/sh
fpcalc -raw -length 500 "$1" > "$1".fpcalc

0 comments on commit 4f922b7

Please sign in to comment.