/// <summary> /// A one-frame track sounds like a click. /// A click is a sharp onset broadband sound of brief duration. Geometrically it is similar to a vertical whistle. /// THis method averages dB log values incorrectly but it is faster than doing many log conversions. /// This method is used to find acoustic events and is accurate enough for the purpose. /// </summary> public static (List <EventCommon> Events, double[] Intensity) GetOneFrameTracks( SpectrogramStandard sonogram, OneframeTrackParameters parameters, TimeSpan segmentStartOffset) { var sonogramData = sonogram.Data; int frameCount = sonogramData.GetLength(0); int binCount = sonogramData.GetLength(1); var frameStep = sonogram.FrameStep; int nyquist = sonogram.NyquistFrequency; double binWidth = nyquist / (double)binCount; int minBin = (int)Math.Round(parameters.MinHertz.Value / binWidth); int maxBin = (int)Math.Round(parameters.MaxHertz.Value / binWidth); var decibelThreshold = parameters.DecibelThreshold.Value; var minBandwidthHertz = parameters.MinBandwidthHertz.Value; var maxBandwidthHertz = parameters.MaxBandwidthHertz.Value; var converter = new UnitConverters( segmentStartOffset: segmentStartOffset.TotalSeconds, sampleRate: sonogram.SampleRate, frameSize: sonogram.Configuration.WindowSize, frameOverlap: sonogram.Configuration.WindowOverlap); // Find all frame peaks and place in peaks matrix // avoid row edge effects. var peaks = new double[frameCount, binCount]; // for all time frames except 1st and last allowing for edge effects. for (int t = 1; t < frameCount - 1; t++) { // buffer zone around click is one frame wide. // for all frequency bins except top and bottom in this time frame for (int bin = minBin; bin < maxBin; bin++) { if (sonogramData[t, bin] < decibelThreshold) { continue; } // THis is where the profile of a click is defined // A click requires sudden onset, with maximum amplitude followed by decay. bool isClickPeak = sonogramData[t - 1, bin] < decibelThreshold && sonogramData[t, bin] > sonogramData[t + 1, bin]; if (isClickPeak) { peaks[t, bin] = sonogramData[t, bin]; } } } //NOTE: the Peaks matrix is same size as the sonogram. var tracks = GetOneFrameTracks(peaks, minBin, maxBin, minBandwidthHertz, maxBandwidthHertz, decibelThreshold, converter); // initialise tracks as events and get the combined intensity array. var events = new List <EventCommon>(); var temporalIntensityArray = new double[frameCount]; var maxScore = decibelThreshold * 5; foreach (var track in tracks) { var ae = new ClickEvent(track, maxScore) { SegmentStartSeconds = segmentStartOffset.TotalSeconds, SegmentDurationSeconds = frameCount * converter.SecondsPerFrameStep, Name = "noName", }; events.Add(ae); // fill the intensity array var startRow = converter.FrameFromStartTime(track.StartTimeSeconds); var amplitudeTrack = track.GetAmplitudeOverTimeFrames(); for (int i = 0; i < amplitudeTrack.Length; i++) { temporalIntensityArray[startRow + i] += amplitudeTrack[i]; } } // MAY NOT WANT TO Do THIS FOR ONE-FRAME tracks // combine proximal events that occupy similar frequency band //if (combineProximalSimilarEvents) //{ // TimeSpan startDifference = TimeSpan.FromSeconds(0.5); // int hertzDifference = 500; // //######################################################################## TODO TODO TODOD // //events = AcousticEvent.CombineSimilarProximalEvents(events, startDifference, hertzDifference); //} return(events, temporalIntensityArray); }
/// <summary> /// This method averages dB log values incorrectly but it is faster than doing many log conversions. /// This method is used to find acoustic events and is accurate enough for the purpose. /// </summary> public static (List <EventCommon> ListOfevents, double[] CombinedIntensityArray) GetOnebinTracks( SpectrogramStandard sonogram, OnebinTrackParameters parameters, TimeSpan segmentStartOffset) { var sonogramData = sonogram.Data; int frameCount = sonogramData.GetLength(0); int binCount = sonogramData.GetLength(1); int nyquist = sonogram.NyquistFrequency; double binWidth = nyquist / (double)binCount; int minBin = (int)Math.Round(parameters.MinHertz.Value / binWidth); int maxBin = (int)Math.Round(parameters.MaxHertz.Value / binWidth); double decibelThreshold = parameters.DecibelThreshold.Value; double minDuration = parameters.MinDuration.Value; double maxDuration = parameters.MaxDuration.Value; var converter = new UnitConverters( segmentStartOffset: segmentStartOffset.TotalSeconds, sampleRate: sonogram.SampleRate, frameSize: sonogram.Configuration.WindowSize, frameOverlap: sonogram.Configuration.WindowOverlap); //Find all bin peaks and place in peaks matrix var peaks = new double[frameCount, binCount]; for (int tf = 0; tf < frameCount; tf++) { for (int bin = minBin + 1; bin < maxBin - 1; bin++) { if (sonogramData[tf, bin] < decibelThreshold) { continue; } // here we define the amplitude profile of a whistle. The buffer zone around whistle is five bins wide. var bandIntensity = ((sonogramData[tf, bin - 1] * 0.5) + sonogramData[tf, bin] + (sonogramData[tf, bin + 1] * 0.5)) / 2.0; var topSidebandIntensity = (sonogramData[tf, bin + 3] + sonogramData[tf, bin + 4] + sonogramData[tf, bin + 5]) / 3.0; var netAmplitude = 0.0; if (bin < 4) { netAmplitude = bandIntensity - topSidebandIntensity; } else { var bottomSideBandIntensity = (sonogramData[tf, bin - 3] + sonogramData[tf, bin - 4] + sonogramData[tf, bin - 5]) / 3.0; netAmplitude = bandIntensity - ((topSidebandIntensity + bottomSideBandIntensity) / 2.0); } if (netAmplitude >= decibelThreshold) { peaks[tf, bin] = sonogramData[tf, bin]; } } } var tracks = GetOnebinTracks(peaks, minDuration, maxDuration, decibelThreshold, converter); // Initialise tracks as events and get the combined intensity array. var events = new List <WhistleEvent>(); var combinedIntensityArray = new double[frameCount]; var scoreRange = new Interval <double>(0, decibelThreshold * 5); foreach (var track in tracks) { var ae = new WhistleEvent(track, scoreRange) { SegmentStartSeconds = segmentStartOffset.TotalSeconds, SegmentDurationSeconds = frameCount * converter.SecondsPerFrameStep, Name = "Whistle", }; events.Add(ae); // fill the intensity array var startRow = converter.FrameFromStartTime(track.StartTimeSeconds); var amplitudeTrack = track.GetAmplitudeOverTimeFrames(); for (int i = 0; i < amplitudeTrack.Length; i++) { combinedIntensityArray[startRow + i] = Math.Max(combinedIntensityArray[startRow + i], amplitudeTrack[i]); } } // This algorithm tends to produce temporally overlapped whistle events in adjacent channels. // Combine overlapping whistle events var hertzDifference = 4 * binWidth; var whistleEvents = WhistleEvent.CombineAdjacentWhistleEvents(events, hertzDifference); // Finally filter the whistles for presense of excess noise in buffer band just above the whistle. // Excess noise would suggest this is not a whistle event. var bufferHertz = 300; var bufferBins = (int)Math.Round(bufferHertz / binWidth); var filteredEvents = new List <EventCommon>(); foreach (var ev in whistleEvents) { var avNhAmplitude = GetAverageAmplitudeInNeighbourhood((SpectralEvent)ev, sonogramData, bufferBins, converter); Console.WriteLine($"###################################Buffer Average decibels = {avNhAmplitude}"); if (avNhAmplitude < decibelThreshold) { // There is little acoustic activity in the buffer zone above the whistle. It is likely to be a whistle. filteredEvents.Add(ev); } } return(filteredEvents, combinedIntensityArray); }