/// <summary>
        /// ONLY Use this concatenation method when you want to concatenate the files for a fixed single day.
        /// The files to be concatenated must be somewhere in the subdirectory structure of the passed list of data directories
        /// Read them into a dictionary
        /// MOST RECENT METHOD TO CONCATENATE Spectral INDEX.CSV FILES - Early September 2015.
        /// It is designed to deal with Yvonne's case where want to concatenate files distributed over arbitrary directories.
        /// It only merges files for the passed fixed date. i.e only 24 hours
        /// </summary>
        public static void DrawSpectralIndexFiles(
            Dictionary <string, double[, ]> dictionary,
            LdSpectrogramConfig sgConfig,
            IndexGenerationData indexGenerationData,
            FileInfo indexPropertiesConfigFileInfo,
            DirectoryInfo opDir,
            SiteDescription siteDescription,
            FileInfo sunriseDataFile          = null,
            List <GapsAndJoins> segmentErrors = null)
        {
            // derive new indices such as sqrt(PMN), NCDI etc -- main reason for this is to view what their distributions look like.
            dictionary = IndexMatrices.AddDerivedIndices(dictionary);

            // Calculate the index distribution statistics and write to a json file. Also save as png image
            if (indexGenerationData.RecordingStartDate != null)
            {
                DateTimeOffset dto        = (DateTimeOffset)indexGenerationData.RecordingStartDate;
                string         dateString = $"{dto.Year}{dto.Month:D2}{dto.Day:D2}";
                string         opFileStem = $"{siteDescription.SiteName}_{dateString}";

                var indexDistributions = IndexDistributions.WriteSpectralIndexDistributionStatistics(dictionary, opDir, opFileStem);

                //SummaryIndexBase[] summaryIndices = null;
                string analysisType = "Towsey.Acoustic";

                Tuple <Image, string>[] tuple = LDSpectrogramRGB.DrawSpectrogramsFromSpectralIndices(
                    opDir, // topLevelDirectories[0], // this should not be required but it is - because things have gotten complicated !
                    opDir,
                    sgConfig,
                    indexPropertiesConfigFileInfo,
                    indexGenerationData,
                    opFileStem,
                    analysisType,
                    dictionary,
                    null, //summaryIndices,
                    indexDistributions,
                    siteDescription,
                    sunriseDataFile,
                    segmentErrors,
                    ImageChrome.With);
            }
        }
        public void SummariseResults(AnalysisSettings settings, FileSegment inputFileSegment, EventBase[] events, SummaryIndexBase[] indices, SpectralIndexBase[] spectralIndices, AnalysisResult2[] results)
        {
            var acousticIndicesConfig = (AcousticIndicesConfig)settings.AnalysisAnalyzerSpecificConfiguration;

            var  sourceAudio      = inputFileSegment.Source;
            var  resultsDirectory = AnalysisCoordinator.GetNamedDirectory(settings.AnalysisOutputDirectory, this);
            bool tileOutput       = acousticIndicesConfig.TileOutput;

            var frameWidth = acousticIndicesConfig.FrameLength;
            int sampleRate = AppConfigHelper.DefaultTargetSampleRate;

            sampleRate = acousticIndicesConfig.ResampleRate ?? sampleRate;

            // Gather settings for rendering false color spectrograms
            var ldSpectrogramConfig = acousticIndicesConfig.LdSpectrogramConfig;

            string basename = Path.GetFileNameWithoutExtension(sourceAudio.Name);

            // output to disk (so other analyzers can use the data,
            // only data - configuration settings that generated these indices
            // this data can then be used by post-process analyses

            /* NOTE: The value for FrameStep is used only when calculating a standard spectrogram
             * FrameStep is NOT used when calculating Summary and Spectral indices.
             */
            var indexConfigData = new IndexGenerationData()
            {
                RecordingExtension            = inputFileSegment.Source.Extension,
                RecordingBasename             = basename,
                RecordingStartDate            = inputFileSegment.TargetFileStartDate,
                RecordingDuration             = inputFileSegment.TargetFileDuration.Value,
                SampleRateOriginal            = inputFileSegment.TargetFileSampleRate.Value,
                SampleRateResampled           = sampleRate,
                FrameLength                   = frameWidth,
                FrameStep                     = settings.Configuration.GetIntOrNull(AnalysisKeys.FrameStep) ?? frameWidth,
                IndexCalculationDuration      = acousticIndicesConfig.IndexCalculationDurationTimeSpan,
                BgNoiseNeighbourhood          = acousticIndicesConfig.BgNoiseBuffer,
                AnalysisStartOffset           = inputFileSegment.SegmentStartOffset ?? TimeSpan.Zero,
                MaximumSegmentDuration        = settings.AnalysisMaxSegmentDuration,
                BackgroundFilterCoeff         = SpectrogramConstants.BACKGROUND_FILTER_COEFF,
                LongDurationSpectrogramConfig = ldSpectrogramConfig,
            };
            var icdPath = FilenameHelpers.AnalysisResultPath(
                resultsDirectory,
                basename,
                IndexGenerationData.FileNameFragment,
                "json");

            Json.Serialise(icdPath.ToFileInfo(), indexConfigData);

            // gather spectra to form spectrograms.  Assume same spectra in all analyzer results
            // this is the most efficient way to do this
            // gather up numbers and strings store in memory, write to disk one time
            // this method also AUTOMATICALLY SORTS because it uses array indexing
            var dictionaryOfSpectra = spectralIndices.ToTwoDimensionalArray(SpectralIndexValues.CachedSelectors, TwoDimensionalArray.Rotate90ClockWise);

            // Calculate the index distribution statistics and write to a json file. Also save as png image
            var indexDistributions = IndexDistributions.WriteSpectralIndexDistributionStatistics(dictionaryOfSpectra, resultsDirectory, basename);

            // HACK: do not render false color spectrograms unless IndexCalculationDuration = 60.0 (the normal resolution)
            if (acousticIndicesConfig.IndexCalculationDurationTimeSpan != 60.0.Seconds())
            {
                Log.Warn("False color spectrograms were not rendered");
            }
            else
            {
                FileInfo indicesPropertiesConfig = acousticIndicesConfig.IndexPropertiesConfig.ToFileInfo();

                // Actually draw false color / long duration spectrograms
                Tuple <Image <Rgb24>, string>[] images =
                    LDSpectrogramRGB.DrawSpectrogramsFromSpectralIndices(
                        inputDirectory: resultsDirectory,
                        outputDirectory: resultsDirectory,
                        ldSpectrogramConfig: ldSpectrogramConfig,
                        indexPropertiesConfigPath: indicesPropertiesConfig,
                        indexGenerationData: indexConfigData,
                        basename: basename,
                        analysisType: this.Identifier,
                        indexSpectrograms: dictionaryOfSpectra,
                        indexStatistics: indexDistributions,
                        imageChrome: (!tileOutput).ToImageChrome());

                if (tileOutput)
                {
                    Debug.Assert(images.Length == 2);

                    Log.Info("Tiling output at scale: " + acousticIndicesConfig.IndexCalculationDuration);

                    foreach (var image in images)
                    {
                        TileOutput(resultsDirectory, Path.GetFileNameWithoutExtension(sourceAudio.Name), image.Item2 + ".Tile", inputFileSegment, image.Item1);
                    }
                }
            }
        }
        public override void SummariseResults(
            AnalysisSettings analysisSettings,
            FileSegment inputFileSegment,
            EventBase[] events,
            SummaryIndexBase[] indices,
            SpectralIndexBase[] spectralIndices,
            AnalysisResult2[] results)
        {
            // below is example of how to access values in ContentDescription config file.
            //sampleRate = analysisSettings.Configuration.GetIntOrNull(AnalysisKeys.ResampleRate) ?? sampleRate;
            var cdConfiguration     = (CdConfig)analysisSettings.Configuration;
            var ldSpectrogramConfig = cdConfiguration.LdSpectrogramConfig;

            //var cdConfigFile = analysisSettings.ConfigFile;
            //var configDirectory = cdConfigFile.DirectoryName ?? throw new ArgumentNullException(nameof(cdConfigFile), "Null value");
            var    sourceAudio      = inputFileSegment.Source;
            string basename         = Path.GetFileNameWithoutExtension(sourceAudio.Name);
            var    resultsDirectory = AnalysisCoordinator.GetNamedDirectory(analysisSettings.AnalysisOutputDirectory, this);

            // check for null values - this was recommended by ReSharper!
            if (inputFileSegment.TargetFileDuration == null || inputFileSegment.TargetFileSampleRate == null)
            {
                throw new NullReferenceException();
            }

            // output config data to disk so other analyzers can use the data,
            // Should contain data only - i.e. the configuration settings that generated these indices
            // this data can then be used by later analysis processes.
            var indexConfigData = new IndexGenerationData()
            {
                RecordingExtension            = inputFileSegment.Source.Extension,
                RecordingBasename             = basename,
                RecordingStartDate            = inputFileSegment.TargetFileStartDate,
                RecordingDuration             = inputFileSegment.TargetFileDuration.Value,
                SampleRateOriginal            = inputFileSegment.TargetFileSampleRate.Value,
                SampleRateResampled           = ContentSignatures.SampleRate,
                FrameLength                   = ContentSignatures.FrameSize,
                FrameStep                     = ContentSignatures.FrameSize,
                IndexCalculationDuration      = TimeSpan.FromSeconds(ContentSignatures.IndexCalculationDurationInSeconds),
                BgNoiseNeighbourhood          = TimeSpan.FromSeconds(5), // default value for content description
                AnalysisStartOffset           = inputFileSegment.SegmentStartOffset ?? TimeSpan.Zero,
                MaximumSegmentDuration        = analysisSettings.AnalysisMaxSegmentDuration,
                BackgroundFilterCoeff         = SpectrogramConstants.BACKGROUND_FILTER_COEFF,
                LongDurationSpectrogramConfig = ldSpectrogramConfig,
            };
            var icdPath = FilenameHelpers.AnalysisResultPath(
                resultsDirectory,
                basename,
                IndexGenerationData.FileNameFragment,
                "json");

            Json.Serialise(icdPath.ToFileInfo(), indexConfigData);

            // gather spectra to form spectrograms.  Assume same spectra in all analyzer results
            var dictionaryOfSpectra = spectralIndices.ToTwoDimensionalArray(SpectralIndexValuesForContentDescription.CachedSelectors, TwoDimensionalArray.Rotate90ClockWise);

            // Calculate the index distribution statistics and write to a json file. Also save as png image
            // The following method returns var indexDistributions =, but we have no use for them.
            IndexDistributions.WriteSpectralIndexDistributionStatistics(dictionaryOfSpectra, resultsDirectory, basename);

            // Draw ldfc spectrograms and return path to 2maps image.
            string ldfcSpectrogramPath =
                DrawSpectrogramsFromSpectralIndices(
                    ldSpectrogramConfig,
                    outputDirectory: resultsDirectory,
                    indexGenerationData: indexConfigData,
                    basename: basename,
                    indexSpectrograms: dictionaryOfSpectra);

            // Gather the content description results into an array of DescriptionResult and then convert to dictionary
            var allContentDescriptionResults = results.Select(x => (DescriptionResult)x.MiscellaneousResults[nameof(DescriptionResult)]);
            var contentDictionary            = DataProcessing.ConvertResultsToDictionaryOfArrays(allContentDescriptionResults.ToList());

            // Write the results to a csv file
            var filePath = Path.Combine(resultsDirectory.FullName, "AcousticSignatures.csv");

            // TODO: fix this so it writes header and a column of content description values.
            //Csv.WriteToCsv(new FileInfo(filePath), contentDictionary);
            FileTools.WriteDictionaryAsCsvFile(contentDictionary, filePath);

            // prepare graphical plots of the acoustic signatures.
            var contentPlots = GetPlots(contentDictionary);
            var images       = GraphsAndCharts.DrawPlotDistributions(contentPlots);
            var plotsImage   = ImageTools.CombineImagesVertically(images);

            plotsImage.Save(Path.Combine(resultsDirectory.FullName, "DistributionsOfContentScores.png"));

            // Attach content description plots to LDFC spectrogram and write to file
            var ldfcSpectrogram = Image.Load <Rgb24>(ldfcSpectrogramPath);
            var image           = ContentVisualization.DrawLdfcSpectrogramWithContentScoreTracks(ldfcSpectrogram, contentPlots);
            var path3           = Path.Combine(resultsDirectory.FullName, basename + ".ContentDescription.png");

            image.Save(path3);
        }