Click here to Skip to main content
15,880,891 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
// git://github.com/AddictedCS/soundfingerprinting.git
// Code license: CPOL v.1.02
// ciumac.sergiu@gmail.com
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Drawing;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
using Soundfingerprinting.AudioProxies;
using Soundfingerprinting.AudioProxies.Strides;
using Soundfingerprinting.DbStorage;
using Soundfingerprinting.DbStorage.Entities;
using Soundfingerprinting.Fingerprinting;
using Soundfingerprinting.Hashing;
using Soundfingerprinting.NeuralHashing.Ensemble;
using Soundfingerprinting.SoundTools.Properties;
using Un4seen.Bass.AddOn.Tags;

namespace Soundfingerprinting.SoundTools.QueryDb
{
    /// <summary>
    ///   <c>WinQueryResult</c> form, which will show all the results related to the recognition process
    /// </summary>
    public partial class WinQueryResults : Form
    {
        /// <summary>
        ///   Minimum track length (in seconds)
        /// </summary>
        private const int MIN_TRACK_LENGTH = 20;

        /// <summary>
        ///   Maximum track length (in seconds)
        /// </summary>
        private const int MAX_TRACK_LENGTH = 60*20;

        #region DataGrid Columns

        private const string COL_SONG_NAME = "SongNameTitle";
        private const string COL_RESULT_NAME = "ResultSongNameTitle";
        private const string COL_HIT = "CounterHit";
        private const string COL_RESULT = "Result";
        private const string COL_POSITION = "Position";
        private const string COL_HAMMING_AVG = "HammingAvg";
        private const string COL_HAMMING_AVG_BY_TRACK = "HammingAvgByTrack";
        private const string COL_MIN_HAMMING = "MinHamming";
        private const string COL_SORT_VALUE = "SortValue";
        private const string COL_START_QUERY_INDEX = "StartQueryIndex";
        private const string COL_TOTAL_TABLES = "TotalTables";
        private const string COL_TOTAL_TRACK_VOTES = "TotalTrackVotes";
        private const string COL_ELAPSED_TIME = "ElapsedTime";
        private const string COL_TOTAL_FINGERPRINTS = "TotalFingerprints";
        private const string COL_MAX_PATH = "MaxPath";
        private const string COL_SIMILARITY = "Similarity";
        private readonly Random _random = new Random((int) unchecked(DateTime.Now.Ticks << 4));

        #endregion

        /// <summary>
        ///   Connection string to the underlying data source
        /// </summary>
        private readonly string _connectionString;

        /// <summary>
        ///   Data access manager, allows one to access the underlying data source
        /// </summary>
        private readonly DaoGateway _dalManager;

        /// <summary>
        ///   Network ensemble
        /// </summary>
        private readonly NNEnsemble _ensemble;

        /// <summary>
        ///   List of files [.mp3] which are going to be recognized
        /// </summary>
        private readonly List<string> _fileList;

        /// <summary>
        ///   Number of hash keys per table used in MinHash + LSH schema [normally 5]
        /// </summary>
        private readonly int _hashKeys;

        /// <summary>
        ///   Number of hash tables used in the MinHash + LSH schema [normally 20]
        /// </summary>
        private readonly int _hashTables;

        /// <summary>
        ///   Fingerprint manager
        /// </summary>
        private readonly FingerprintManager _manager;

        /// <summary>
        ///   Permutation storage
        /// </summary>
        private readonly IPermutations _permStorage;

        /// <summary>
        ///   Audio proxy used for reading the data from the .mp3 files
        /// </summary>
        private readonly BassProxy _proxy = new BassProxy();

        /// <summary>
        ///   The size of the query [E.g. 253 samples]
        /// </summary>
        private readonly IStride _queryStride;

        /// <summary>
        ///   Number of fingerprints to analyze
        /// </summary>
        private readonly int _secondsToAnalyze;

        /// <summary>
        ///   Start second
        /// </summary>
        private readonly int _startSecond;

        /// <summary>
        ///   Recognition threshold 17% ~5 tables from 25 hash functions
        /// </summary>
        private readonly int _threshold;

        /// <summary>
        ///   Number of top wavelets involved
        /// </summary>
        private readonly int _topWavelets;

        /// <summary>
        ///   Running thread allows one to Abort forcibly the recognition
        /// </summary>
        private Thread _runningThread;

        /// <summary>
        ///   Flag which signalizes stop of the recognition process
        /// </summary>
        private bool _stopQuerying;

        /// <summary>
        ///   Protected constructor of WinQueryResults class
        /// </summary>
        /// <param name = "connectionString">Connection string used for the underlying data source</param>
        /// <param name = "secondsToAnalyze">Number of consequent fingerprints to analyze</param>
        /// <param name = "startSecond">Starting seconds</param>
        /// <param name = "stride">Stride used in the query</param>
        /// <param name = "topWavelets">Number of top wavelets to analyze</param>
        /// <param name = "fileList">List of all files to be recognized</param>
        protected WinQueryResults(string connectionString, int secondsToAnalyze, int startSecond,
                                  IStride stride, int topWavelets, List<string> fileList)
        {
            InitializeComponent(); /*Initialize Designer Components*/
            Icon = Resources.Sound;
            _connectionString = connectionString;
            _topWavelets = topWavelets;
            _dalManager = new DaoGateway(ConfigurationManager.ConnectionStrings["FingerprintConnectionString"].ConnectionString);
            _permStorage = new DbPermutations(ConfigurationManager.ConnectionStrings["FingerprintConnectionString"].ConnectionString);
            _manager = new FingerprintManager {TopWavelets = topWavelets};
            _dalManager.SetConnectionString(_connectionString); /*Set connection string for DAL manager*/
            _secondsToAnalyze = secondsToAnalyze; /*Number of fingerprints to analyze from each song*/
            _startSecond = startSecond;
            _fileList = fileList; /*List of files to analyze*/
            _dgvResults.Columns.Add(COL_SONG_NAME, "Initial Song");
            _dgvResults.Columns[COL_SONG_NAME].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            _dgvResults.Columns.Add(COL_RESULT_NAME, "Result Song");
            _dgvResults.Columns[COL_RESULT_NAME].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            _dgvResults.Columns.Add(COL_POSITION, "Position");
            _dgvResults.Columns[COL_POSITION].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            _dgvResults.Columns.Add(COL_RESULT, "Result");
            _dgvResults.Columns[COL_RESULT].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            _dgvResults.Columns.Add(COL_HAMMING_AVG, "Hamming Avg.");
            _dgvResults.Columns[COL_HAMMING_AVG].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            _queryStride = stride;
        }

        /// <summary>
        ///   Public constructor for LSH + Min Hash algorithm
        /// </summary>
        /// <param name = "connectionString">Connection string</param>
        /// <param name = "secondsToAnalyze">Number of fingerprints to analyze</param>
        /// <param name = "stride">Stride</param>
        /// <param name = "fileList">File list</param>
        /// <param name = "hashTables">Min hash hash tables</param>
        /// <param name = "hashKeys">Min hash hash keys</param>
        /// <param name = "startSecond">Starting second of analysis</param>
        /// <param name = "thresholdTables">Number of threshold tables</param>
        /// <param name = "topWavelets">Number of top wavelets to consider</param>
        public WinQueryResults(string connectionString, int secondsToAnalyze, int startSecond, IStride stride, List<string> fileList, int hashTables, int hashKeys, int thresholdTables, int topWavelets)
            : this(connectionString, secondsToAnalyze, startSecond, stride, topWavelets, fileList)
        {
            _hashTables = hashTables;
            _hashKeys = hashKeys;
            _threshold = thresholdTables;
            _dgvResults.Columns.Add(COL_HAMMING_AVG_BY_TRACK, "Hamming Avg. By Track");
// ReSharper disable PossibleNullReferenceException
            _dgvResults.Columns[COL_HAMMING_AVG_BY_TRACK].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            _dgvResults.Columns.Add(COL_MIN_HAMMING, "Min. Hamming");
            _dgvResults.Columns[COL_MIN_HAMMING].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            _dgvResults.Columns.Add(COL_SORT_VALUE, "Sort Value");
            _dgvResults.Columns[COL_SORT_VALUE].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            _dgvResults.Columns.Add(COL_TOTAL_TABLES, "Total Table Votes");
            _dgvResults.Columns[COL_TOTAL_TABLES].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            _dgvResults.Columns.Add(COL_TOTAL_TRACK_VOTES, "Total Track Votes");
            _dgvResults.Columns[COL_TOTAL_TRACK_VOTES].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            _dgvResults.Columns.Add(COL_START_QUERY_INDEX, "Query Index Start");
            _dgvResults.Columns[COL_START_QUERY_INDEX].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            _dgvResults.Columns.Add(COL_TOTAL_FINGERPRINTS, "Total Fingerprints");
            _dgvResults.Columns[COL_TOTAL_FINGERPRINTS].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            _dgvResults.Columns.Add(COL_SIMILARITY, "Min Similarity");
            _dgvResults.Columns[COL_SIMILARITY].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            _dgvResults.Columns.Add(COL_ELAPSED_TIME, "Elapsed Time");
            _dgvResults.Columns[COL_ELAPSED_TIME].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
// ReSharper restore PossibleNullReferenceException
            _btnExport.Enabled = false;
            _nudTotal.Value = _fileList.Count;
            Action action = ExtractCandidatesWithMinHashAlgorithm; /*Extract candidates using MinHash + LSH algorithm*/
            action.BeginInvoke(
                (result) =>
                {
                    try
                    {
                        action.EndInvoke(result);
                    }
                    catch (ThreadAbortException)
                    {
                        /*Recognition aborted*/
                        return;
                    }
                    catch (Exception e)
                    {
                        MessageBox.Show(Resources.RecognitionEndedWithAnError + e.Message + Resources.LineFeed + e.StackTrace, Resources.Error, MessageBoxButtons.OK, MessageBoxIcon.Error);
                        return;
                    }
                    MessageBox.Show(Resources.RecognitionEnded, Resources.RecognitionEnded, MessageBoxButtons.OK, MessageBoxIcon.Information);
                    Invoke(new Action(
                        () =>
                        {
                            _btnStop.Enabled = true;
                            _btnExport.Enabled = true;
                        }));
                }, action);
        }

        /// <summary>
        ///   Public constructor for NN algorithm
        /// </summary>
        /// <param name = "connectionString">Connection string</param>
        /// <param name = "secondsToAnalyze">Number of fingerprints to analyze</param>
        /// <param name = "startSeconds">Starting seconds</param>
        /// <param name = "stride">Query stride</param>
        /// <param name = "topWavelets">Number of top wavelets</param>
        /// <param name = "fileList">File list to analyze</param>
        /// <param name = "pathToEnsemble">Path to ensemble</param>
        public WinQueryResults(string connectionString, int secondsToAnalyze, int startSeconds, IStride stride, int topWavelets, List<string> fileList, string pathToEnsemble)
            : this(connectionString, secondsToAnalyze, startSeconds, stride, topWavelets, fileList)
        {
            _ensemble = NNEnsemble.Load(pathToEnsemble);
            _dgvResults.Columns.Add(COL_HIT, "Number of hits");
            _dgvResults.Columns[COL_HIT].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            Action action = ExtractCandidatesWithNeuralHasher;
            action.BeginInvoke((result) => action.EndInvoke(result), action);
        }

        /// <summary>
        ///   Extract possible candidates from the data source using Neural Hasher
        /// </summary>
        private void ExtractCandidatesWithNeuralHasher()
        {
            int recognized = 0, verified = 0;

            Action<object[], Color> actionAddItems =
                (parameters, color) =>
                {
                    int index = _dgvResults.Rows.Add(parameters);
                    _dgvResults.FirstDisplayedScrollingRowIndex = index;
                    if (color != Color.Empty)
                        _dgvResults.Rows[index].DefaultCellStyle.BackColor = color;
                };

            Action<float> actionRecognition = (recognition) => _tbResults.Text = recognition.ToString();

            foreach (string pathToFile in _fileList)
            {
                //Samples to skip from each of the song
                IStride samplesToSkip = _queryStride;
                long elapsedMiliseconds = 0;

                TAG_INFO tags = _proxy.GetTagInfoFromFile(pathToFile); //Get Tags from file
                if (tags == null)
                {
                    //TAGS are null
                    Invoke(actionAddItems, new Object[] {"TAGS ARE NULL!", pathToFile}, Color.Red); //Show that the file has no tags
                    continue;
                }

                string artist = tags.artist; //Artist
                string title = tags.title; //Title
                double duration = tags.duration; //Duration

                if (duration < MIN_TRACK_LENGTH || duration > MAX_TRACK_LENGTH) //Check whether the duration is ok
                {
                    //Duration too small
                    Invoke(actionAddItems, new Object[] {"BAD DURATION!", pathToFile}, Color.Red);
                    continue;
                }

                if (String.IsNullOrEmpty(artist) || String.IsNullOrEmpty(title)) //Check whether the tags are properly defined
                {
                    Invoke(actionAddItems, new Object[] {"NO TAGS!", pathToFile}, Color.Red);
                    continue;
                }

                /*
                * Returned dictionary is sorted by Values
                * allCandidates.ElementAt(0) will return the pair with best query results
                */
                Dictionary<Int32, QueryStats> allCandidatesNotSorted = new Dictionary<int, QueryStats>(); //QueryFingerprintManager.QueryOneSongNeuralHasher(_ensemble, pathToFile, samplesToSkip, _proxy, _dalManager, _secondsToAnalyze, ref elapsedMiliseconds);

                if (allCandidatesNotSorted == null)
                {
                    Invoke(actionAddItems, new Object[] {"BAD CANDIDATES!", pathToFile}, Color.Red);
                    continue;
                }

                //Sort candidates
                IOrderedEnumerable<KeyValuePair<Int32, QueryStats>> result =
                    allCandidatesNotSorted.OrderByDescending((pair) => pair.Value.NumberOfTrackIdOccurences);
                int candidatesCount = result.Count();
                if (candidatesCount == 0)
                {
                    Invoke(actionAddItems, new Object[] {artist + "-" + title, "NO CANDIDATES!"}, Color.Yellow);
                    verified++;
                    continue;
                }

                bool found = false;
                KeyValuePair<Int32, QueryStats> item = result.ElementAt(0);
                Track track = _dalManager.ReadTrackByArtistAndTitleName(artist, title);
                if (track == null)
                {
                    Invoke(actionAddItems, new Object[] {artist + "-" + title, "No such track in the database!"}, Color.Yellow);
                    continue;
                }

                if (track.Id == item.Key) /*Compare returned track by actual*/
                {
                    recognized++;
                    found = true;
                    Invoke(actionAddItems, new Object[] {title + "-" + artist, track.Title + "-" + track.Artist, 1, found, 0, candidatesCount}, Color.Empty);
                }


                if (!found)
                {
                    var itemFound = result.Select((pair, indexAt) => new {Pair = pair, IndexAt = indexAt}).Where((a) => a.Pair.Key == track.Id);
                    if (itemFound != null && itemFound.Count() > 0)
                    {
                        int indexOfCount = itemFound.ElementAt(0).IndexAt + 1;
                        Invoke(actionAddItems, new Object[] {title + "-" + artist, title + "-" + artist, indexOfCount, true, 0, candidatesCount}, Color.Yellow);
                    }
                    else
                    {
                        Invoke(actionAddItems, new Object[] {title + "-" + artist, "Not found!", -1, false, 0, candidatesCount}, Color.Yellow);
                    }
                }
                verified++;
                //results
                Invoke(actionRecognition, (float) recognized/verified);
            }
        }

        /// <summary>
        ///   Extract Candidates from the underlying data source using Min Hash + LSH Schema
        /// </summary>
        private void ExtractCandidatesWithMinHashAlgorithm()
        {
            _runningThread = Thread.CurrentThread;
            int recognized = 0, verified = 0;
            IStride samplesToSkip = _queryStride;
            Action<int> action = ((check) => _nudChecked.Value = check);
            Action<object[], Color> actionAddItems = (parameters, color) =>
                                                     {
                                                         int index = _dgvResults.Rows.Add(parameters);
                                                         _dgvResults.FirstDisplayedScrollingRowIndex = index;
                                                         if (color != Color.Empty)
                                                             _dgvResults.Rows[index].DefaultCellStyle.BackColor = color;
                                                     };
            Action<float> actionRecognition = (recognition) => _tbResults.Text = recognition.ToString();

            for (int i = 0; i < _fileList.Count; i++) /*For each song in the list, query the DATABASE*/
            {
                if (InvokeRequired)
                    Invoke(action, i);
                else
                    action(i);
                if (_stopQuerying)
                    break;

                string pathToFile = _fileList[i]; /*Path to song to recognize*/
                TAG_INFO tags = _proxy.GetTagInfoFromFile(pathToFile); //Get Tags from file

                if (tags == null)
                {
                    //TAGS are null
                    Invoke(actionAddItems, new Object[] {"TAGS ARE NULL", pathToFile}, Color.Red);
                    continue;
                }

                string artist = tags.artist; //Artist
                string title = tags.title; //Title
                double duration = tags.duration; //Duration

                if (duration < MIN_TRACK_LENGTH || duration > MAX_TRACK_LENGTH) //Check whether the duration is ok
                {
                    //Duration too small
                    Invoke(actionAddItems, new Object[] {"BAD DURATION", pathToFile}, Color.Red);
                    continue;
                }

                if (String.IsNullOrEmpty(artist) || String.IsNullOrEmpty(title)) //Check whether the tags are properly defined
                {
                    //Title or Artist tag is null
                    Invoke(actionAddItems, new Object[] {"NO TAGS", pathToFile}, Color.Red);
                    continue;
                }

                long elapsedMiliseconds = 0;

                /*Get correct track id*/
                Track actualTrack = _dalManager.ReadTrackByArtistAndTitleName(artist, title);
                if (actualTrack == null)
                {
                    Invoke(actionAddItems, new Object[] {title + "-" + artist, "No such song in the database!", -1, false, 0, -1, -1, -1, -1, -1, -1, -1, elapsedMiliseconds}, Color.Red);
                    continue;
                }
                List<bool[]> signatures = _manager.CreateFingerprints(_proxy, pathToFile, samplesToSkip, _secondsToAnalyze*1000, _startSecond*1000);
                Dictionary<Int32, QueryStats> allCandidates = QueryFingerprintManager.QueryOneSongMinHash
                    (signatures, _dalManager, _permStorage, _secondsToAnalyze, _hashTables, _hashKeys,
                        _threshold, ref elapsedMiliseconds); /*Query the database using Min Hash*/

                if (allCandidates == null) /*No candidates*/
                {
                    Invoke(actionAddItems, new Object[] {title + "-" + artist, "No candidates!", -1, false, 0, -1, -1, -1, -1, -1, -1, -1, elapsedMiliseconds}, Color.Red);
                    continue;
                }

                /*Order by Hamming Similarity*/
                OrderedParallelQuery<KeyValuePair<Int32, QueryStats>> order = allCandidates.AsParallel() /*Using PLINQ*/
                    .OrderBy((pair) =>
                             {
                                 return pair.Value.OrderingValue = pair.Value.HammingDistance/pair.Value.NumberOfTotalTableVotes
                                                                   + 0.4*pair.Value.MinHammingDistance;
                             });

                Track recognizedTrack = null;
                bool found = false;

                if (order.Count() > 0)
                {
                    KeyValuePair<Int32, QueryStats> item = order.ElementAt(0);
                    recognizedTrack = _dalManager.ReadTrackById(item.Key);
                    if (actualTrack.Id == recognizedTrack.Id)
                    {
                        recognized++;
                        found = true;
                    }
                    Invoke(actionAddItems, new Object[]
                                           {
                                               title + "-" + artist, /*Actual title and artist*/
                                               recognizedTrack.Title + "-" + recognizedTrack.Artist, /*Recognized Title Track Name*/
                                               1, /*Position in the ordered list*/
                                               actualTrack.Id == recognizedTrack.Id, /*Found?*/
                                               item.Value.HammingDistance/item.Value.NumberOfTotalTableVotes, /*Average hamming distance*/
                                               (double) item.Value.HammingDistanceByTrack/item.Value.NumberOfTrackIdOccurences,
                                               (double) item.Value.MinHammingDistance,
                                               item.Value.OrderingValue,
                                               item.Value.NumberOfTotalTableVotes,
                                               item.Value.NumberOfTrackIdOccurences,
                                               item.Value.StartQueryIndex,
                                               allCandidates.Count,
                                               item.Value.Similarity,
                                               elapsedMiliseconds
                                           }, Color.Empty);
                }

                verified++;
                if (!found)
                {
                    /*If the element is not found, search it in all candidates*/
                    var query = order.Select((pair, indexAt) => new {Pair = pair, IndexAt = indexAt}).Where((a) => a.Pair.Key == actualTrack.Id);
                    if (query != null && query.Count() > 0)
                    {
                        var anonymType = query.ElementAt(0);
                        recognizedTrack = _dalManager.ReadTrackById(anonymType.Pair.Key);
                        Invoke(actionAddItems, new Object[]
                                               {
                                                   title + "-" + artist,
                                                   recognizedTrack.Title + "-" + recognizedTrack.Artist, /*Recognized Title Track Name*/
                                                   anonymType.IndexAt + 1, /*Position in the ordered list*/
                                                   actualTrack.Id == recognizedTrack.Id, /*Found?*/
                                                   anonymType.Pair.Value.HammingDistance/anonymType.Pair.Value.NumberOfTotalTableVotes, /*Main Criterion*/
                                                   (double) anonymType.Pair.Value.HammingDistanceByTrack/anonymType.Pair.Value.NumberOfTrackIdOccurences,
                                                   (double) anonymType.Pair.Value.MinHammingDistance,
                                                   anonymType.Pair.Value.OrderingValue,
                                                   anonymType.Pair.Value.NumberOfTotalTableVotes,
                                                   anonymType.Pair.Value.NumberOfTrackIdOccurences,
                                                   anonymType.Pair.Value.StartQueryIndex,
                                                   allCandidates.Count,
                                                   anonymType.Pair.Value.Similarity,
                                                   elapsedMiliseconds
                                               }, Color.Yellow);
                    }
                    else
                        Invoke(actionAddItems, new Object[]
                                               {
                                                   title + "-" + artist, "Not Found", -1, /*Position in the ordered list*/
                                                   false, -1, -1, -1, -1, -1, -1, -1, -1, -1, elapsedMiliseconds
                                               }, Color.Yellow);
                }
                Invoke(actionRecognition, (float) recognized/verified);
            }
        }

        /// <summary>
        ///   Callback invoked when <c>Stop</c> button is pressed, meaning that the recognition is over
        /// </summary>
        private void BtnStopClick(object sender, EventArgs e)
        {
            _stopQuerying = true;
            Invoke(new Action(() => { _btnStop.Enabled = false; }));
        }

        /// <summary>
        ///   Callback invoked when the <c>Export</c> button is pressed
        /// </summary>
        private void BtnExportClick(object sender, EventArgs e)
        {
            /*Export into CSV file*/
            WinUtils.ExportInExcel(_dgvResults, "Recognition Rate", _tbResults.Text, "Date", DateTime.Now);
        }


        /// <summary>
        ///   Callback invoked when the Win Query results is closing
        /// </summary>
        private void WinQueryResultsFormClosing(object sender, FormClosingEventArgs e)
        {
            if (_runningThread != null)
                _runningThread.Abort();
        }
    }
}

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