Click here to Skip to main content
15,886,518 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.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Threading;
using DuplicateTracks.Model;
using DuplicateTracks.Services;

namespace DuplicateTracks.ViewModel
{
    /// <summary>
    ///   Path List VM
    /// </summary>
    public class PathListViewModel : ViewModelBase
    {
        #region Constants

        private const string STEP_SELECT_ROOT_FOLDER = "Step 1/5 - Selecting root folders";
        private const string STEP_READING_TAGS = "Step 2/5 - Reading tags from files";
        private const string STEP_HASHING_MUSIC_FILES = "Step 3/5 - Fingerprinting music files";
        private const string STEP_FIND_DUPLICATES = "Step 4/5 - Finding duplicates";

        /// <summary>
        ///   Music file filters
        /// </summary>
        private readonly string[] _musicFileFilters = new[] {"*.mp3", "*.ogg", "*.flac", "*.wav"};

        #endregion

        /// <summary>
        /// Locking object
        /// </summary>
        private static readonly object LockObject = new object();

        /// <summary>
        /// Repository gateway
        /// </summary>
        private readonly RepositoryGateway _gate;

        /// <summary>
        ///   Add single music file command
        /// </summary>
        private RelayCommand _addFileCommand;

        /// <summary>
        ///   Add more folders command
        /// </summary>
        private RelayCommand _addMoreCommand;

        private int _currentProgress;

        /// <summary>
        ///   Paths to folders to be processed
        /// </summary>
        private ObservableCollection<Item> _paths;

        /// <summary>
        ///   Processed music files
        /// </summary>
        private int _processedMusicItems;

        private HashSet<Item> _processedPaths;

        /// <summary>
        ///   Flag for processing is possible
        /// </summary>
        private bool _processing;

        /// <summary>
        ///   Processing step
        /// </summary>
        private string _processingStep;

        /// <summary>
        ///   Start processing command
        /// </summary>
        private RelayCommand _startCommand;

        /// <summary>
        ///   Stop processing command
        /// </summary>
        private RelayCommand _stopCommand;

        /// <summary>
        ///   Total music items to process
        /// </summary>
        private int _totalMusicItems;

        /// <summary>
        ///   Parameter less constructor
        /// </summary>
        public PathListViewModel()
        {
            _paths = new ObservableCollection<Item>();
            _paths.CollectionChanged += CollectionChanged; /*Subscribe to the event when the collection gets changed*/
            _processingStep = STEP_SELECT_ROOT_FOLDER;
            UIDispatcher = Dispatcher.CurrentDispatcher;
            try
            {
                _gate = new RepositoryGateway();
            }
            catch (Exception ex)
            {
                Debug.Fail(ex.Message);
                IMessageBoxService msb = GetService<IMessageBoxService>(); //Show dialog if an exception occurred during processing
                if (msb != null)
                    msb.Show(ex.Message, "Exception occurred during processing!",
                             MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }


        /// <summary>
        ///   Paths to folders which contain music files
        /// </summary>
        public ObservableCollection<Item> Paths
        {
            get { return _paths ?? (_paths = new ObservableCollection<Item>()); }
            private set
            {
                _paths = value;
                OnPropertyChanged("Paths");
            }
        }

        /// <summary>
        ///   Processing step (1 out of 4)
        /// </summary>
        public string ProcessingStep
        {
            get { return _processingStep; }
            set
            {
                if (_processingStep != value)
                {
                    _processingStep = value;
                    OnPropertyChanged("ProcessingStep");
                }
            }
        }

        /// <summary>
        ///   Check if system is currently busy in processing the files
        /// </summary>
        public bool IsProcessing
        {
            get { return _processing; }
            set
            {
                if (_processing != value)
                {
                    _processing = value;
                    OnPropertyChanged("IsProcessing");
                }
            }
        }

        /// <summary>
        ///   Current progress on the progress bar
        /// </summary>
        public int CurrentProgress
        {
            get { return _currentProgress; }
            private set
            {
                if (_currentProgress != value)
                {
                    _currentProgress = value;
                    OnPropertyChanged("CurrentProgress");
                }
            }
        }


        /// <summary>
        ///   Current UI dispatcher to be invoked
        /// </summary>
        public Dispatcher UIDispatcher { get; private set; }

        /// <summary>
        ///   Get the command to be executed when AddMore button on the View is clicked
        /// </summary>
        public ICommand AddMoreCommand
        {
            get
            {
                return _addMoreCommand ?? (_addMoreCommand = new RelayCommand(
                                                                 AddMoreFolders, CanAddMoreFolders));
            }
        }

        /// <summary>
        ///   Start processing the songs
        /// </summary>
        public ICommand StartCommand
        {
            get
            {
                return _startCommand ?? (_startCommand = new RelayCommand(
                                                             StartProcessing, CanStartProcessing));
            }
        }

        /// <summary>
        ///   Abort processing
        /// </summary>
        public ICommand StopCommand
        {
            get
            {
                return _stopCommand ?? (_stopCommand = new RelayCommand(
                                                           StopProcessing, CanStopProcessing));
            }
        }

        /// <summary>
        ///   Add single music file command
        /// </summary>
        public ICommand AddFileCommand
        {
            get
            {
                return _addFileCommand ?? (_addFileCommand = new RelayCommand(
                                                                 AddFile, CanAddFile));
            }
        }

        /// <summary>
        ///   The collection gets changed
        /// </summary>
        private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                foreach (object item in e.NewItems)
                {
                    int c = ((Item) item).Count;
                    if (c > 0)
                        _totalMusicItems += c;
                }
            }
            else if (e.Action == NotifyCollectionChangedAction.Remove)
            {
                foreach (object item in e.OldItems)
                {
                    int c = ((Item) item).Count;
                    if (c > 0)
                        _totalMusicItems -= c;
                }
            }
            OnPropertyChanged("Paths"); /*Signalize that the collection got changed*/
        }

        /// <summary>
        ///   Add more folders to the list
        /// </summary>
        public void AddMoreFolders(object param)
        {
            IFolderBrowserDialogService fbg = GetService<IFolderBrowserDialogService>();
            if (fbg != null)
            {
                if (DialogResult.OK == fbg.Show())
                {
                    string selectedPath = fbg.SelectedPath;
                    //Check if such directory exists, and check if such directory wasn't already introduced
                    if (!String.IsNullOrEmpty(selectedPath) && Paths.All(folder => selectedPath != folder.Path))
                    {
                        if (Directory.Exists(selectedPath))
                        {
                            Paths.Add(new Item {Path = selectedPath, Count = -1, IsFolder = true});
                        }
                        else if (File.Exists(selectedPath))
                        {
                            Paths.Add(new Item {Path = selectedPath, Count = 1, IsFolder = false});
                            return;
                        }
                        else
                        {
                            return;
                        }
                        //count the number of available music files asynchronously
                        Func<string, string[], bool, int> countFiles = Helper.CountNumberOfMusicFiles;
                        countFiles.BeginInvoke(selectedPath, _musicFileFilters, true,
                                               (result) =>
                                               {
                                                   int count = countFiles.EndInvoke(result);
                                                   lock (LockObject)
                                                   {
                                                       int index = -1;
                                                       foreach (Item path in Paths)
                                                       {
                                                           index++;
                                                           if (path.Path == selectedPath)
                                                               break;
                                                       }
                                                       if (Paths != null && Paths.Count >= index && index >= 0)
                                                       {
                                                           _totalMusicItems += count;
                                                           Paths[index].Count = count;
                                                           if (Dispatcher.CurrentDispatcher != UIDispatcher) /*Invalidate the UI as the property is changed from another thread*/
                                                               if (UIDispatcher != null) UIDispatcher.Invoke(new Action(CommandManager.InvalidateRequerySuggested));
                                                       }
                                                   }
                                               }, countFiles);
                    }
                }
            }
        }

        /// <summary>
        ///   Check if adding folders is allowed
        /// </summary>
        /// <param name = "param">Any parameter</param>
        /// <returns>True if allowed, otherwise false</returns>
        /// <remarks>
        ///   Can execute is called before Execute() method, thus if the method returns false, the button will be disabled
        /// </remarks>
        public bool CanAddMoreFolders(object param)
        {
            return !IsProcessing;
        }

        /// <summary>
        ///   Actual method which start processing
        /// </summary>
        /// <param name = "param">Parameter</param>
        public void StartProcessing(object param)
        {
             ProcessingStep = STEP_READING_TAGS;
            IsProcessing = true;
            int alreadyProcessedFiles = _processedMusicItems;
            HashSet<Item> pathsToBeProcessed = null;

            if (_processedPaths == null)
            {
                pathsToBeProcessed = new HashSet<Item>(Paths);
                _processedPaths = new HashSet<Item>(Paths);
            }
            else
            {
                //if there are already paths that have been processed, use only new ones
                IEnumerable<Item> except = Paths.Except(_processedPaths);
                pathsToBeProcessed = new HashSet<Item>(except);
                foreach (Item item in except)
                {
                    _processedPaths.Add(item);
                }
            }

            _gate.ProcessTracksAsync(
                pathsToBeProcessed,
                _musicFileFilters,
                (tracks, exception) => /*processing ends*/
                {
                    if (exception != null) /*Exception occurred during processing*/
                    {
                        IMessageBoxService msg = GetService<IMessageBoxService>();
                        msg.Show("Error occurred during processing!\n" + exception.Message, "Error!",
                                 MessageBoxButton.OK, MessageBoxImage.Error);
                        return;
                    }
                    if (tracks == null || tracks.Count == 0) /*Processing aborted*/
                    {
                        ProcessingStep = STEP_SELECT_ROOT_FOLDER;
                        return;
                    }
                    /*Processing ended*/
                    ProcessingStep = STEP_FIND_DUPLICATES;
                    CurrentProgress = 0;
                    HashSet<Track>[] duplicates = _gate.FindAllDuplicates();
                    if (UIDispatcher != null && Dispatcher.CurrentDispatcher != UIDispatcher)
                    {
                        UIDispatcher.Invoke(
                            new Action(() =>
                                       {
                                           ReportViewModel report = new ReportViewModel { Sets = duplicates };
                                           GenericViewModel viewModel = new GenericViewModel();
                                           viewModel.Workspaces.Add(report);
                                           IGenericViewWindow view = GetService<IGenericViewWindow>();
                                           IWindowService windowMediator = GetService<IWindowService>();
                                           windowMediator.ShowDialog(view, viewModel,
                                                                     ((o, args) => report.StopPlaying()));
                                           ProcessingStep = STEP_SELECT_ROOT_FOLDER;
                                           IsProcessing = false;

                                       }));
                    }
                },
                (track) => /*one file is processed*/
                {
                    lock (LockObject)
                    {
                        // ReSharper disable AccessToModifiedClosure
                        Interlocked.Increment(ref alreadyProcessedFiles);
                        // ReSharper restore AccessToModifiedClosure
                        CurrentProgress = (int)Math.Ceiling(((float)(alreadyProcessedFiles) / _totalMusicItems * 100));
                        if (alreadyProcessedFiles >= _totalMusicItems)
                        {
                            _processedMusicItems = alreadyProcessedFiles; /*set processed unit items*/
                            alreadyProcessedFiles = 0;
                            CurrentProgress = 0;
                        }
                    }
                },
                (file) => /*tags read from 1 file*/
                {
                    lock (LockObject)
                    {
                        Interlocked.Increment(ref alreadyProcessedFiles);
                        CurrentProgress = (int)Math.Ceiling(((float)(alreadyProcessedFiles) / _totalMusicItems * 100));
                        if (alreadyProcessedFiles >= _totalMusicItems)
                        {
                            if (UIDispatcher != null && Dispatcher.CurrentDispatcher != UIDispatcher)
                                UIDispatcher.Invoke(new Action(
                                                        () => ProcessingStep = STEP_HASHING_MUSIC_FILES));
                            else
                                ProcessingStep = STEP_HASHING_MUSIC_FILES;
                            alreadyProcessedFiles = _processedMusicItems; /*set already processed to previous processed items*/
                            CurrentProgress = 0;
                        }
                    }
                }
                );
        }

        /// <summary>
        ///   Check if the system can start processing
        /// </summary>
        /// <param name = "param">Parameter</param>
        /// <returns>True/False</returns>
        public bool CanStartProcessing(object param)
        {
            return !IsProcessing && _totalMusicItems > 1 && (_totalMusicItems - _processedMusicItems > 0); /*Processing can start if you have at least 2 files to play with*/
        }

        /// <summary>
        ///   Stop processing the songs
        /// </summary>
        /// <param name = "param">Parameter</param>
        public void StopProcessing(object param)
        {
            IsProcessing = false;
            CurrentProgress = 0;
            ProcessingStep = STEP_SELECT_ROOT_FOLDER;
            _processedMusicItems = 0;
            _processedPaths = null;
            _gate.AbortProcessing();
        }

        /// <summary>
        ///   Check if the system can stop processing the songs
        /// </summary>
        /// <param name = "param">Parameter</param>
        /// <returns>True/False</returns>
        public bool CanStopProcessing(object param)
        {
            return IsProcessing;
        }

        /// <summary>
        ///   Add a single file to the union of files to be processed
        /// </summary>
        /// <param name = "param"></param>
        public void AddFile(object param)
        {
            IOpenFileDialogService fbg = GetService<IOpenFileDialogService>();
            if (fbg != null)
            {
                if (DialogResult.OK == fbg.Show("Please choose your awesome track", "Amazing Track", Helper.GetMultipleFilter("Supported Formats", _musicFileFilters), true))
                {
                    string[] selectedPaths = fbg.SelectedPaths;
                    foreach (string selectedPath in selectedPaths)
                    {
                        //Check if such directory exists, and check if such directory wasn't already introduced
                        if (!String.IsNullOrEmpty(selectedPath) && Paths.All(folder => selectedPath != folder.Path))
                        {
                            if (Directory.Exists(selectedPath))
                            {
                                Paths.Add(new Item {Path = selectedPath, Count = -1, IsFolder = true});
                            }
                            else if (File.Exists(selectedPath))
                            {
                                Paths.Add(new Item {Path = selectedPath, Count = 1, IsFolder = false});
                            }
                        }
                    }
                }
            }
            if (param != null)
            {
                if (File.Exists(param as string))
                {
                    string extension = Path.GetExtension(param as string);
                    if (_musicFileFilters.Where(filter => extension != null).Any(filter => filter.Contains(extension)))
                    {
                        Paths.Add(new Item {Path = param as string, Count = 1, IsFolder = false});
                    }
                }
            }
        }

        /// <summary>
        ///   See if adding files is allowed
        /// </summary>
        /// <param name = "param">Parameter</param>
        /// <returns>True/False</returns>
        private bool CanAddFile(object param)
        {
            return !IsProcessing;
        }
    }
}

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