Click here to Skip to main content
15,888,579 members
Articles / Desktop Programming / WPF

Duplicate songs detector via audio fingerprinting

Rate me:
Please Sign up or sign in to vote.
4.96/5 (337 votes)
23 Jun 2020MIT44 min read 1.3M   20.4K   533  
Explains sound fingerprinting algorithm, with a practical example of detecting duplicate files on the user's local drive.
The aim of this article is to show an efficient algorithm of signal processing which will allow one to have a competent system of sound fingerprinting and signal recognition. I'll try to come with some explanations of the article's algorithm, and also speak about how it can be implemented using the C# programming language. Additionally, I'll try to cover topics of digital signal processing that are used in the algorithm, thus you'll be able to get a clearer image of the entire system. And as a proof of concept, I'll show you how to develop a simple WPF MVVM application.
// Sound Fingerprinting framework
// https://code.google.com/p/soundfingerprinting/
// Code license: GNU General Public License v2
// ciumac.sergiu@gmail.com

using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using System.Xml.Serialization;
using SoundfingerprintingLib.AudioProxies;
using SoundfingerprintingLib.AudioProxies.Strides;
using SoundfingerprintingLib.Fingerprinting;
using SoundfingerprintingLib.Hashing;
using SoundTools.Properties;

namespace SoundTools.Misc
{
    /// <summary>
    /// Miscellaneous empirical tests
    /// </summary>
    public partial class WinMisc : Form
    {
        /// <summary>
        /// Constructor
        /// </summary>
        public WinMisc()
        {
            InitializeComponent();
            Icon = Resources.Sound;
        }

        /// <summary>
        /// Path to *.mp3 file was selected
        /// </summary>
        private void TbPathToFileMouseDoubleClick(object sender, MouseEventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog {Filter = Resources.MusicFilter, FileName = "music.mp3"};
            if (ofd.ShowDialog() == DialogResult.OK)
            {
                _tbPathToFile.Text = ofd.FileName;
            }
        }


        /// <summary>
        /// Path to output file was chosen
        /// </summary>
        private void TbOutputPathMouseDoubleClick(object sender, MouseEventArgs e)
        {
            SaveFileDialog ofd = new SaveFileDialog { Filter = Resources.ExportFilter, FileName = "results.txt" };
            if (ofd.ShowDialog() == DialogResult.OK)
            {
                 _tbOutputPath.Text = ofd.FileName;
            }
        }

        /// <summary>
        /// Dump information into file
        /// </summary>
        private void BtnDumpInfoClick(object sender, EventArgs e)
        {
            if (String.IsNullOrEmpty(_tbPathToFile.Text))
            {
                MessageBox.Show(Resources.ErrorNoFileToAnalyze, Resources.SelectFile, MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }
            if (String.IsNullOrEmpty(_tbOutputPath.Text))
            {
                MessageBox.Show(Resources.SelectPathToDump, Resources.SelectFile, MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }
            if (!File.Exists(Path.GetFullPath(_tbPathToFile.Text)))
            {
                MessageBox.Show(Resources.NoSuchFile, Resources.NoSuchFile, MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }
            if(_chbCompare.Checked)
            {
                if (String.IsNullOrEmpty(_tbSongToCompare.Text))
                {
                    MessageBox.Show(Resources.ErrorNoFileToAnalyze, Resources.SelectFile, MessageBoxButtons.OK, MessageBoxIcon.Information);
                    return;
                }
            }
            Action action =
                () =>
                {
                    using (BassProxy proxy = new BassProxy())
                    {
                        FadeControls(false);
                        int minFreq = (int) _nudFreq.Value;
                        int topWavelets = (int) _nudTopWavelets.Value;
                        int stride = (int) _nudStride.Value;
                        IStride objStride = (_chbStride.Checked) ? (IStride) new RandomStride(0, stride) : new StaticStride(stride);
                        FingerprintManager manager = new FingerprintManager() {MinFrequency = minFreq, TopWavelets = topWavelets};
                        DumpResults resultObj = new DumpResults();
                        string pathToInput = _tbPathToFile.Text;
                        string pathToOutput = _tbOutputPath.Text;
                        int hashTables = (int)_nudTables.Value;
                        int hashKeys = (int)_nudKeys.Value;
                        stride = (int)_nudQueryStride.Value;
                        int numFingerprints = (int)_nudNumberOfSubsequent.Value;
                        IStride queryStride = (_chbQueryStride.Checked) ? (IStride)new RandomStride(0, stride) : new StaticStride(stride);
                        queryStride = new StaticStride(5115, 5115 / 2); 
                        GetFingerprintSimilarity(manager, objStride, queryStride, numFingerprints, proxy, pathToInput, resultObj);
                        GetHashSimilarity(manager, objStride, queryStride, numFingerprints, hashTables, hashKeys, proxy, pathToInput, resultObj);

                        if (_chbCompare.Checked)
                        {
                            string pathToDifferent = _tbSongToCompare.Text;
                            GetFingerprintSimilarity(manager, objStride, proxy, pathToInput, pathToDifferent, resultObj);
                        }
                        resultObj.Info.MinFrequency = minFreq;
                        resultObj.Info.TopWavelets = topWavelets;
                        resultObj.Info.StrideSize = stride;
                        resultObj.Info.RandomStride = _chbStride.Checked;
                        resultObj.Info.Filename = pathToInput;
                        resultObj.ComparisonDone = _chbCompare.Checked;

                        XmlSerializer serializer = new XmlSerializer(typeof (DumpResults));
                        TextWriter writer = new StreamWriter(pathToOutput);
                        serializer.Serialize(writer, resultObj);
                        writer.Close();
                    }
                };
            action.BeginInvoke(
                (result) =>
                {
                    action.EndInvoke(result);
                    FadeControls(true);
                }, null);
            
        }

        /// <summary>
        /// Get fingerprint similarity between 2 different songs.
        /// </summary>
        /// <param name="manager">Fingerprint manager used in file decomposition</param>
        /// <param name="stride">Stride object parameter</param>
        /// <param name="proxy">Proxy to the audio object</param>
        /// <param name="path">Path to first file</param>
        /// <param name="differentPath">Path to different file</param>
        /// <param name="results">Results object to be filled with the corresponding data</param>
        private static void GetFingerprintSimilarity(FingerprintManager manager, IStride stride, IAudio proxy, string path, string differentPath, DumpResults results)
        {
            int startindex = 0;
            int count = 0;
            double sum = 0;

            List<bool[]> imglista = manager.CreateFingerprints (proxy, path, stride);
            List<bool[]> imglistb = manager.CreateFingerprints (proxy, differentPath, stride);


            count = imglista.Count > imglistb.Count ? imglistb.Count : imglista.Count;
            double max = double.MinValue;
            for (int i = 0; i < count; i++)
            {
                int j = i;
                double value = MinHash.CalculateSimilarity(imglista[i], imglistb[j]);
                if (value > max)
                    max = value;    
                sum += value;
            }

            results.SumJaqFingerprintSimilarityBetweenDiffertSongs = sum;
            results.AverageJaqFingerprintsSimilarityBetweenDifferentSongs = sum/count;
            results.MaxJaqFingerprintsSimilarityBetweenDifferentSongs = max;
        }

        /// <summary>
        /// Get fingerprint similarity of one song
        /// </summary>
        /// <param name="manager">Fingerprint manager used in file decomposition</param>
        /// <param name="dbstride">Database creation stride</param>
        /// <param name="queryStride">Query stride</param>
        /// <param name="numberOfItemsToCompare">Number of subsequent elements to compare with</param>
        /// <param name="proxy">Proxy</param>
        /// <param name="path">Path to first file</param>
        /// <param name="results">Results object to be filled with the corresponding data</param>
        private static void GetFingerprintSimilarity(FingerprintManager manager, IStride dbstride, IStride queryStride, int numberOfItemsToCompare, IAudio proxy, string path, DumpResults results)
        {
            int startindex = 0;
            int count = 0;
            double sum = 0;

            List<bool[]> list = manager.CreateFingerprints(proxy, path, dbstride);
            List<bool[]> listToCompare = manager.CreateFingerprints(proxy, path, queryStride);

            count = list.Count;
            int toCompare = listToCompare.Count;

            double max = double.MinValue;

            for (int i = 0; i < count; i++)
            {
                for (int j = 0; j < toCompare; j++)
                {
                    double value = MinHash.CalculateSimilarity(list[i], listToCompare[j]);
                    if (value > max)
                        max = value;
                    sum += value;
                }
            }

            results.Results.SumJaqFingerprintsSimilarity = sum;
            results.Results.AverageJaqFingerprintSimilarity = sum / (count * toCompare);
            results.Results.MaxJaqFingerprintSimilarity = max;
        }

        /// <summary>
        /// Get hash similarity of one song
        /// </summary>
        /// <param name="manager">Fingerprint manager</param>
        /// <param name="dbstride">Database stride between fingerprints</param>
        /// <param name="queryStride">Query stride between fingerprints</param>
        /// <param name="numberOfFingerprintsToAnalyze">Number of fingerprints to analyze</param>
        /// <param name="hashTables">Number of hash tables in the LSH transformation</param>
        /// <param name="hashKeys">Number of hash keys per table in the LSH transformation</param>
        /// <param name="proxy">Audio proxy</param>
        /// <param name="path">Path to analyzed file</param>
        /// <param name="results">Results object to be filled with the appropriate data</param>
        private static void GetHashSimilarity(FingerprintManager manager, IStride dbstride, IStride queryStride, int numberOfFingerprintsToAnalyze, int hashTables, int hashKeys, IAudio proxy, string path, DumpResults results)
        {
            double sum = 0;
            int hashesCount = 0;
            int startindex = 0;

            List<bool[]> listDb = manager.CreateFingerprints (proxy, path, dbstride);
            List<bool[]> listQuery = manager.CreateFingerprints (proxy, path, queryStride);
            IPermutations perms = new DbPermutations(ConfigurationManager.ConnectionStrings["FingerprintConnectionString"].ConnectionString);
            MinHash minHash = new MinHash(perms);
            List<int[]> minHashDb = listDb.Select(minHash.ComputeMinHashSignature).ToList();
            List<int[]> minHashQuery = listQuery.Select(minHash.ComputeMinHashSignature).ToList();

            /*Calculate Min Hash signature similarity by comparing 2 consecutive signatures*/
            int countDb = minHashDb.Count;
            int countQuery = minHashQuery.Count;
            int minHashSignatureLen = minHashDb[0].Length;
            int similarMinHashValues = 0;
            for (int i = 0; i < countDb; i++)
            {
                for (int j = 0; j < countQuery; j++)
                {
                    for (int k = 0; k < minHashSignatureLen; k++)
                        if (minHashDb[i][k] == minHashQuery[j][k])
                            similarMinHashValues++;
                }
            }
            results.Results.SumIdenticalMinHash = similarMinHashValues;
            results.Results.AverageIdenticalMinHash = (double)similarMinHashValues / (countDb * countQuery * minHashSignatureLen);

            /*Group min hash signatures into LSH Buckets*/
            List<Dictionary<int, long>> lshBucketsDb =
                minHashDb.Select(item => minHash.GroupMinHashToLSHBuckets(item, hashTables, hashKeys)).ToList();

            List<Dictionary<int, long>> lshBucketsQuery =
                minHashQuery.Select(item => minHash.GroupMinHashToLSHBuckets(item, hashTables, hashKeys)).ToList();

            int countSignatures = lshBucketsDb.Count;
            sum = 0;
            foreach(Dictionary<int, long> a in lshBucketsDb)
            {
                var aValues = a.Values;
                foreach (Dictionary<int, long> b in lshBucketsQuery)
                {
                    var bValues = b.Values;
                    hashesCount += aValues.Intersect(bValues).Count();
                 }
            }

            results.Results.SumJaqLSHBucketSimilarity = -1;
            results.Results.AverageJaqLSHBucketSimilarity = -1;
            results.Results.TotalIdenticalLSHBuckets = hashesCount;
        }

        /// <summary>
        /// Fade all controls
        /// </summary>
        /// <param name="isVisible">Set the parameters as visible/invisible</param>
        private void FadeControls(bool isVisible)
        {
            Invoke(new Action(
                () =>
                {
                    _tbOutputPath.Enabled = isVisible;
                    _tbPathToFile.Enabled = isVisible;
                    _nudFreq.Enabled = isVisible;
                    _nudTopWavelets.Enabled = isVisible;
                    _btnDumpInfo.Enabled = isVisible;
                    _nudStride.Enabled = isVisible;
                    _chbStride.Enabled = isVisible;
                }));
        }

        /// <summary>
        /// Check box checked
        /// </summary>
        private void ChbCompareCheckedChanged(object sender, EventArgs e)
        {
            _tbSongToCompare.Enabled = !_tbSongToCompare.Enabled;
        }

        /// <summary>
        /// Song to compare select
        /// </summary>
        private void TbSongToCompareMouseDoubleClick(object sender, MouseEventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog { Filter = Resources.MusicFilter, FileName = "music.mp3" };
            if (ofd.ShowDialog() == DialogResult.OK)
            {
                _tbSongToCompare.Text = ofd.FileName;
            }
        }

        /// <summary>
        /// On window form loading event
        /// </summary>
        private void WinMiscLoad(object sender, EventArgs e)
        {
            FingerprintManager manager = new FingerprintManager();
            _nudFreq.Value = manager.MinFrequency;
            _nudTopWavelets.Value = manager.TopWavelets;
        }

        
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Software Developer
Moldova (Republic of) Moldova (Republic of)
Interested in computer science, math, research, and everything that relates to innovation. Fan of agnostic programming, don't mind developing under any platform/framework if it explores interesting topics. In search of a better programming paradigm.

Comments and Discussions