├── requirements.txt ├── fpcalc-gen ├── compare.py ├── README.md └── correlation.py /requirements.txt: -------------------------------------------------------------------------------- 1 | correlation 2 | numpy 3 | -------------------------------------------------------------------------------- /fpcalc-gen: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | fpcalc -raw -length 500 "$1" > "$1".fpcalc 3 | -------------------------------------------------------------------------------- /compare.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # compare.py 3 | import argparse 4 | from correlation import correlate 5 | 6 | def initialize(): 7 | parser = argparse.ArgumentParser() 8 | parser.add_argument("-i ", "--source-file", help="source file") 9 | parser.add_argument("-o ", "--target-file", help="target file") 10 | args = parser.parse_args() 11 | 12 | SOURCE_FILE = args.source_file if args.source_file else None 13 | TARGET_FILE = args.target_file if args.target_file else None 14 | if not SOURCE_FILE or not TARGET_FILE: 15 | raise Exception("Source or Target files not specified.") 16 | return SOURCE_FILE, TARGET_FILE 17 | 18 | if __name__ == "__main__": 19 | SOURCE_FILE, TARGET_FILE = initialize() 20 | correlate(SOURCE_FILE, TARGET_FILE) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple tool to compare audio files 2 | 3 | NOTE: I haven't written this, merely found it on the internet and ported to 4 | python 3. 5 | 6 | * https://shivama205.medium.com/audio-signals-comparison-23e431ed2207 "Audio signals: Comparison" 7 | * https://gist.github.com/shivama205/5578f999a9c88112f5d042ebb83e54d5 scripts from the article 8 | 9 | Related projects: 10 | 11 | * https://acoustid.org/chromaprint fpcalc 12 | * numpy 13 | 14 | Usage: 15 | 16 | Sample files captured from a streaming source without exact start, duration but 17 | are the same song: 18 | 19 | $ ./compare.py -i file1.mp3 -o file2.mp3 20 | Calculating fingerprint by fpcalc for file1.mp3 21 | Calculating fingerprint by fpcalc for file2.mp3 22 | File A: file1.mp3 23 | File B: file2.mp3 24 | Match with correlation of 63.74% at offset 55 25 | 26 | $ ./compare.py -i file2.mp3 -o file1.mp3 27 | Calculating fingerprint by fpcalc for file2.mp3 28 | Calculating fingerprint by fpcalc for file1.mp3 29 | File A: file2.mp3 30 | File B: file2.mp3 31 | Match with correlation of 63.74% at offset -5 32 | 33 | For some files the swapped order may not lead to the same results due to offset 34 | or the way the `fpcalc` fingerprint is generated (see help). 35 | 36 | $ ./compare.py -i file2.mp3 -o file3.mp3 37 | Calculating fingerprint by fpcalc for file2.mp3 38 | Calculating fingerprint by fpcalc for file3.mp3 39 | File A: file2.mp3 40 | File B: file3.mp3 41 | Match with correlation of 93.01% at offset -24 42 | 43 | $ ./compare.py 44 | Calculating fingerprint by fpcalc for file1.mp3 45 | Calculating fingerprint by fpcalc for file3.mp3 46 | File A: file1.mp3 47 | File B: file3.mp3 48 | Match with correlation of 63.96% at offset 31 49 | 50 | Internally the fingerprint is generated by `fpcalc -length 500`, cached 51 | versions can be produced by `fpcalc-gen`. 52 | 53 | Changes: 54 | 55 | - port to python3 56 | - print the similary as percents 57 | - print input files on separate lines 58 | - support precalculated fingerprint in `file.mp3.fpcalc` 59 | -------------------------------------------------------------------------------- /correlation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # correlation.py 4 | import subprocess 5 | import numpy 6 | import os 7 | 8 | # seconds to sample audio file for 9 | sample_time = 500 10 | # number of points to scan cross correlation over 11 | span = 150 12 | # step size (in points) of cross correlation 13 | step = 1 14 | # minimum number of points that must overlap in cross correlation 15 | # exception is raised if this cannot be met 16 | min_overlap = 20 17 | # report match when cross correlation has a peak exceeding threshold 18 | threshold = 0.5 19 | 20 | # calculate fingerprint 21 | # Generate file.mp3.fpcalc by "fpcalc -raw -length 500 file.mp3" 22 | def calculate_fingerprints(filename): 23 | if os.path.exists(filename + '.fpcalc'): 24 | print("Found precalculated fingerprint for %s" % (filename)) 25 | f = open(filename + '.fpcalc', "r") 26 | fpcalc_out = ''.join(f.readlines()) 27 | f.close() 28 | else: 29 | print("Calculating fingerprint by fpcalc for %s" % (filename)) 30 | fpcalc_out = str(subprocess.check_output(['fpcalc', '-raw', '-length', str(sample_time), filename])).strip().replace('\\n', '').replace("'", "") 31 | 32 | fingerprint_index = fpcalc_out.find('FINGERPRINT=') + 12 33 | # convert fingerprint to list of integers 34 | fingerprints = list(map(int, fpcalc_out[fingerprint_index:].split(','))) 35 | 36 | return fingerprints 37 | 38 | # returns correlation between lists 39 | def correlation(listx, listy): 40 | if len(listx) == 0 or len(listy) == 0: 41 | # Error checking in main program should prevent us from ever being 42 | # able to get here. 43 | raise Exception('Empty lists cannot be correlated.') 44 | if len(listx) > len(listy): 45 | listx = listx[:len(listy)] 46 | elif len(listx) < len(listy): 47 | listy = listy[:len(listx)] 48 | 49 | covariance = 0 50 | for i in range(len(listx)): 51 | covariance += 32 - bin(listx[i] ^ listy[i]).count("1") 52 | covariance = covariance / float(len(listx)) 53 | 54 | return covariance/32 55 | 56 | # return cross correlation, with listy offset from listx 57 | def cross_correlation(listx, listy, offset): 58 | if offset > 0: 59 | listx = listx[offset:] 60 | listy = listy[:len(listx)] 61 | elif offset < 0: 62 | offset = -offset 63 | listy = listy[offset:] 64 | listx = listx[:len(listy)] 65 | if min(len(listx), len(listy)) < min_overlap: 66 | # Error checking in main program should prevent us from ever being 67 | # able to get here. 68 | return 69 | #raise Exception('Overlap too small: %i' % min(len(listx), len(listy))) 70 | return correlation(listx, listy) 71 | 72 | # cross correlate listx and listy with offsets from -span to span 73 | def compare(listx, listy, span, step): 74 | if span > min(len(listx), len(listy)): 75 | # Error checking in main program should prevent us from ever being 76 | # able to get here. 77 | raise Exception('span >= sample size: %i >= %i\n' 78 | % (span, min(len(listx), len(listy))) 79 | + 'Reduce span, reduce crop or increase sample_time.') 80 | corr_xy = [] 81 | for offset in numpy.arange(-span, span + 1, step): 82 | corr_xy.append(cross_correlation(listx, listy, offset)) 83 | return corr_xy 84 | 85 | # return index of maximum value in list 86 | def max_index(listx): 87 | max_index = 0 88 | max_value = listx[0] 89 | for i, value in enumerate(listx): 90 | if value > max_value: 91 | max_value = value 92 | max_index = i 93 | return max_index 94 | 95 | def get_max_corr(corr, source, target): 96 | max_corr_index = max_index(corr) 97 | max_corr_offset = -span + max_corr_index * step 98 | #print("max_corr_index = ", max_corr_index, "max_corr_offset = ", max_corr_offset) 99 | # report matches 100 | if corr[max_corr_index] > threshold: 101 | print("File A: %s" % (source)) 102 | print("File B: %s" % (target)) 103 | print('Match with correlation of %.2f%% at offset %i' 104 | % (corr[max_corr_index] * 100.0, max_corr_offset)) 105 | 106 | def correlate(source, target): 107 | fingerprint_source = calculate_fingerprints(source) 108 | fingerprint_target = calculate_fingerprints(target) 109 | 110 | corr = compare(fingerprint_source, fingerprint_target, span, step) 111 | max_corr_offset = get_max_corr(corr, source, target) 112 | --------------------------------------------------------------------------------