diff --git a/src/AudioAnalysisTools/DSP/FrequencyScale.cs b/src/AudioAnalysisTools/DSP/FrequencyScale.cs index 08cdb25fb..b04ad202c 100644 --- a/src/AudioAnalysisTools/DSP/FrequencyScale.cs +++ b/src/AudioAnalysisTools/DSP/FrequencyScale.cs @@ -48,6 +48,23 @@ public FrequencyScale(int nyquist, int frameSize, int hertzGridInterval) this.GridLineLocations = GetLinearGridLineLocations(nyquist, this.HertzGridInterval, this.FinalBinCount); } + /// + /// Initializes a new instance of the class. + /// CONSTRUCTOR + /// Call this constructor when want to change freq scale but keep linear. + /// + public FrequencyScale(int nyquist, int frameSize, int finalBinCount, int hertzGridInterval) + { + this.ScaleType = FreqScaleType.Linear; + this.Nyquist = nyquist; + this.WindowSize = frameSize; + this.FinalBinCount = finalBinCount; + this.HertzGridInterval = hertzGridInterval; + this.LinearBound = nyquist; + this.BinBounds = this.GetLinearBinBounds(); + this.GridLineLocations = GetLinearGridLineLocations(nyquist, this.HertzGridInterval, this.FinalBinCount); + } + /// /// Initializes a new instance of the class. /// CONSTRUCTOR @@ -259,11 +276,13 @@ public int GetBinIdInReducedSpectrogramForHerzValue(int herzValue) public int[,] GetLinearBinBounds() { double herzInterval = this.Nyquist / (double)this.FinalBinCount; + double scaleFactor = this.WindowSize / 2 / (double)this.FinalBinCount; + var binBounds = new int[this.FinalBinCount, 2]; for (int i = 0; i < this.FinalBinCount; i++) { - binBounds[i, 0] = i; + binBounds[i, 0] = (int)Math.Round(i * scaleFactor); binBounds[i, 1] = (int)Math.Round(i * herzInterval); } @@ -331,6 +350,12 @@ public static void DrawFrequencyLinesOnImage(Image bmp, int[,] gridLineLo int height = bmp.Height; int bandCount = gridLineLocations.GetLength(0); + if (gridLineLocations == null || bmp.Height < 50) + { + // there is no point placing gridlines on a narrow image. It obscures too much spectrogram. + return; + } + // draw the grid line for each frequency band for (int b = 0; b < bandCount; b++) { diff --git a/src/AudioAnalysisTools/StandardSpectrograms/BaseSonogram.cs b/src/AudioAnalysisTools/StandardSpectrograms/BaseSonogram.cs index fa07a4b02..fc2639915 100644 --- a/src/AudioAnalysisTools/StandardSpectrograms/BaseSonogram.cs +++ b/src/AudioAnalysisTools/StandardSpectrograms/BaseSonogram.cs @@ -132,6 +132,13 @@ public BaseSonogram(SonogramConfig config, FrequencyScale freqScale, WavReader w this.FreqScale = freqScale; this.InitialiseSpectrogram(wav); + + if (this.FreqScale.ScaleType == FreqScaleType.Linear && this.FreqScale.WindowSize != this.FreqScale.FinalBinCount) + { + // convert the spectrogram frequency scale + this.Data = RescaleLinearFrequencyScale(this.Data, this.FreqScale); + } + this.Make(this.Data); } @@ -200,6 +207,41 @@ private void InitialiseSpectrogram(WavReader wav) } } + public static double[,] RescaleLinearFrequencyScale(double[,] inputSpgram, FrequencyScale freqScale) + { + if (freqScale == null) + { + throw new ArgumentNullException(nameof(freqScale)); + } + + if (freqScale.ScaleType != FreqScaleType.Linear) + { + LoggedConsole.WriteLine("Require a Linear frequency scale for this method."); + throw new ArgumentNullException(nameof(freqScale)); + } + + // get the bin bounds for this scale type + var binBounds = freqScale.BinBounds; + int newBinCount = binBounds.GetLength(0); + + // set up the new spectrogram + int frameCount = inputSpgram.GetLength(0); + + double[,] opM = new double[frameCount, newBinCount]; + + for (int row = 0; row < frameCount; row++) + { + //get each frame or spectrum in turn and rescale. + var linearSpectrum = MatrixTools.GetRow(inputSpgram, row); + var rescaledSpectrum = SpectrogramStandard.RescaleSpectrumUsingFilterbank(binBounds, linearSpectrum); + + //return the spectrum to output spectrogram. + MatrixTools.SetRow(opM, row, rescaledSpectrum); + } + + return opM; + } + /// /// Calculates SNR, ENERGY PER FRAME and NORMALISED dB PER FRAME. /// @@ -258,7 +300,10 @@ public Image GetImageFullyAnnotated(Image image, string title, int throw new ArgumentNullException(nameof(image)); } - FrequencyScale.DrawFrequencyLinesOnImage(image, gridLineLocations, includeLabels: true); + if (gridLineLocations != null) + { + FrequencyScale.DrawFrequencyLinesOnImage(image, gridLineLocations, includeLabels: true); + } // collect all the images and combine. var titleBar = DrawTitleBarOfGrayScaleSpectrogram(title, image.Width, tag); @@ -665,51 +710,5 @@ public static Image DrawTitleBarOfGrayScaleSpectrogram(string title, int return bmp; } - - /* - // mark of time scale according to scale. - public static Image DrawTimeTrack(TimeSpan offsetMinute, TimeSpan xAxisPixelDuration, TimeSpan xAxisTicInterval, TimeSpan labelInterval, int trackWidth, int trackHeight, string title) - { - var bmp = new Image(trackWidth, trackHeight); - bmp.Mutate(g => - { - g.Clear(Color.Black); - - double elapsedTime = offsetMinute.TotalSeconds; - double pixelDuration = xAxisPixelDuration.TotalSeconds; - int labelSecondsInterval = (int)labelInterval.TotalSeconds; - var whitePen = new Pen(Color.White, 1); - var stringFont = Drawing.Arial8; - - // for columns, draw in second lines - double xInterval = (int)(xAxisTicInterval.TotalMilliseconds / xAxisPixelDuration.TotalMilliseconds); - - // for pixels in the line - for (int x = 1; x < trackWidth; x++) - { - elapsedTime += pixelDuration; - if (x % xInterval <= pixelDuration) - { - g.DrawLine(whitePen, x, 0, x, trackHeight); - int totalSeconds = (int)Math.Round(elapsedTime); - if (totalSeconds % labelSecondsInterval == 0) - { - int minutes = totalSeconds / 60; - int seconds = totalSeconds % 60; - string time = $"{minutes}m{seconds}s"; - g.DrawTextSafe(time, stringFont, Color.White, new PointF(x + 1, 2)); //draw time - } - } - } - - g.DrawLine(whitePen, 0, 0, trackWidth, 0); //draw upper boundary - g.DrawLine(whitePen, 0, trackHeight - 1, trackWidth, trackHeight - 1); //draw lower boundary - g.DrawLine(whitePen, trackWidth, 0, trackWidth, trackHeight - 1); //draw right end boundary - g.DrawTextSafe(title, stringFont, Color.White, new PointF(4, 3)); - }); - - return bmp; - } - */ } } \ No newline at end of file diff --git a/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramStandard.cs b/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramStandard.cs index 13225d6ec..c51562cbc 100644 --- a/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramStandard.cs +++ b/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramStandard.cs @@ -11,7 +11,7 @@ namespace AudioAnalysisTools.StandardSpectrograms public class SpectrogramStandard : BaseSonogram { - //There are five CONSTRUCTORS + //There are six CONSTRUCTORS /// /// Initializes a new instance of the class. @@ -28,6 +28,18 @@ public SpectrogramStandard(SonogramConfig config, WavReader wav) { } + /// + /// Initializes a new instance of the class. + /// Use this constructor when want to increase or decrease the linear frquency scale. + /// + /// Other info to construct the spectrogram. + /// The required new frequency scale. + /// The recording. + public SpectrogramStandard(SonogramConfig config, FrequencyScale scale, WavReader wav) + : base(config, scale, wav) + { + } + /// /// Initializes a new instance of the class. /// Use this constructor when you want to init a new Spectrogram by extracting portion of an existing sonogram. @@ -133,5 +145,88 @@ public override void Make(double[,] amplitudeM) this.SnrData.ModalNoiseProfile = tuple.Item2; // store the full bandwidth modal noise profile } } + + /// + /// Converts a single linear spectrum to octave scale spectrum. + /// + public static double[] RescaleSpectrumUsingFilterbank(int[,] transformMatrix, double[] linearSpectrum) + { + int length = transformMatrix.GetLength(0); + var rescaledSpectrum = new double[length]; + + // Fill in the first value of the rescaled spectrum + int lowIndex1 = transformMatrix[0, 0]; + int centreIndex1 = transformMatrix[0, 0]; + int highIndex1 = transformMatrix[1, 0]; + rescaledSpectrum[0] = FilterbankIntegral(linearSpectrum, lowIndex1, centreIndex1, highIndex1); + + // fill in remainder except last + for (int i = 1; i < length - 1; i++) + { + int lowIndex = transformMatrix[i - 1, 0]; + int centreIndex = transformMatrix[i, 0]; + int highIndex = transformMatrix[i + 1, 0]; + if (highIndex >= linearSpectrum.Length) + { + highIndex = linearSpectrum.Length - 1; + } + + rescaledSpectrum[i] = FilterbankIntegral(linearSpectrum, lowIndex, centreIndex, highIndex); + } + + // now fill in the last value of the rescaled spectrum + int lowIndex2 = transformMatrix[length - 2, 0]; + int centreIndex2 = transformMatrix[length - 1, 0]; + int highIndex2 = transformMatrix[length - 1, 0]; + rescaledSpectrum[length - 1] = FilterbankIntegral(linearSpectrum, lowIndex2, centreIndex2, highIndex2); + + return rescaledSpectrum; + } + + public static double FilterbankIntegral(double[] spectrum, int lowIndex, int centreIndex, int highIndex) + { + // let k = index into spectral vector. + // for all k < lowIndex, filterBank[k] = 0; + // for all k > highIndex, filterBank[k] = 0; + + // for all k in range (lowIndex <= k < centreIndex), filterBank[k] = (k-lowIndex) /(centreIndex - lowIndex) + // for all k in range (centreIndex <= k <= highIndex), filterBank[k] = (highIndex-k)/(highIndex - centreIndex) + + double area = 0.0; + double integral = 0.0; + int delta = centreIndex - lowIndex; + if (delta > 0) + { + for (int k = lowIndex; k < centreIndex; k++) + { + double weight = (k - lowIndex) / (double)delta; + integral += weight * spectrum[k]; + area += weight; + } + } + + integral += spectrum[centreIndex]; + area += 1.0; + + delta = highIndex - centreIndex; + if (delta > 0) + { + for (int k = centreIndex + 1; k <= highIndex; k++) + { + if (delta == 0) + { + continue; + } + + double weight = (highIndex - k) / (double)delta; + integral += weight * spectrum[k]; + area += weight; + } + } + + // NormaliseMatrixValues to area of the triangular filter + integral /= area; + return integral; + } } } \ No newline at end of file