From a74690c9f5aa7722fdc56b9687b46ef97937b112 Mon Sep 17 00:00:00 2001 From: towsey Date: Mon, 6 Jul 2020 17:12:33 +1000 Subject: [PATCH] Calculation of the Spectral Centroid Issue #292 Write various methods to calculate the spectral centroid of a spectrum and accompanying unit tests of the methods. --- src/AudioAnalysisTools/SpectralCentroid.cs | 81 ++++++++++-- .../AmplitudeSpectrogram.cs | 6 +- .../SpectralCentroidTests.cs | 117 ++++++++++++++++++ 3 files changed, 193 insertions(+), 11 deletions(-) create mode 100644 tests/Acoustics.Test/AudioAnalysisTools/SpectralCentroidTests.cs diff --git a/src/AudioAnalysisTools/SpectralCentroid.cs b/src/AudioAnalysisTools/SpectralCentroid.cs index d4a68621c..57043f677 100644 --- a/src/AudioAnalysisTools/SpectralCentroid.cs +++ b/src/AudioAnalysisTools/SpectralCentroid.cs @@ -1,3 +1,4 @@ +using AudioAnalysisTools.StandardSpectrograms; using System; using System.Collections.Generic; using System.Text; @@ -6,12 +7,12 @@ namespace AudioAnalysisTools { /// - /// Calculates the spectral centroid of a signal or part thereof. + /// Calculates the spectral centroid of a spectrum, or a recording segment. /// The spectral centroid is a considred to be a reliable estimate of the brightness of a recording. /// Bright recordings contain mre high frequency content. See following for good intro: /// https://www.cs.cmu.edu/~music/icm/slides/05-algorithmic-composition.pdf /// Also Wikipedia entry: https://en.wikipedia.org/wiki/Spectral_centroid - /// The spectral centroid is derived from the values in the amplitude spectrogram. + /// The spectral centroid is derived from the values in the AMPLITUDE spectrogram. /// A single spectral centroid is calculated for each time frame. /// If a summary value is required for a longer signal, i.e. one second or one minute, then the centroid values for each frame are averaged over the time period. /// Note that the frequency value for a bin is located at the centre of the bin. For a typical bin width of 43 Hz, the centre will be at 21.5 Hz above bin minimum. @@ -36,12 +37,12 @@ public static double CalculateSpectralCentroid(double[] spectrum, int nyquist) // normalise the frequency values var length = spectrum.Length; var normalisedFrequencyValues = new double[length]; - var binWidthHz = nyquist / length; + var binWidthHz = nyquist / (double)length; var halfBinWidth = binWidthHz / 2; - for (int i = 0; i < length - 1; i++) + for (int i = 0; i < length; i++) { - normalisedFrequencyValues[i] = ((i * binWidthHz) + halfBinWidth) / nyquist; + normalisedFrequencyValues[i] = ((i * binWidthHz) + halfBinWidth) / (double)nyquist; } double spectralCentroid = DataTools.DotProduct(normalisedSpectrum, normalisedFrequencyValues); @@ -57,11 +58,10 @@ public static double CalculateSpectralCentroid(double[] spectrum, int nyquist) public static double[] CalculateSpectralCentroids(double[,] spectra, int nyquist) { var frameCount = spectra.GetLength(0); - var freqBinCount = spectra.GetLength(1); - var centroidArray = new double[frameCount]; - for (int i = 0; i < frameCount - 1; i++) + // for each row spectrum + for (int i = 0; i < frameCount; i++) { double[] spectrum = MatrixTools.GetRow(spectra, i); centroidArray[i] = CalculateSpectralCentroid(spectrum, nyquist); @@ -69,5 +69,68 @@ public static double[] CalculateSpectralCentroids(double[,] spectra, int nyquist return centroidArray; } + + /// + /// Calculates the spectral centroid for each frame of an amplitude spectrogram. + /// + /// As AmplitudeSpectrogram. + /// An array of spectral centroids. + public static double[] CalculateSpectralCentroids(AmplitudeSpectrogram spectrogram) + { + int nyquist = spectrogram.Attributes.NyquistFrequency; + var centroidArray = CalculateSpectralCentroids(spectrogram.Data, nyquist); + return centroidArray; + } + + /// + /// Calculates the spectral centroid for each one-second segment of an amplitude spectrogram. + /// + /// As AmplitudeSpectrogram. + /// An array of spectral centroids. + public static double[] CalculateSpectralCentroidsInOneSecondSegments(AmplitudeSpectrogram spectrogram) + { + int nyquist = spectrogram.Attributes.NyquistFrequency; + var centroidArray = CalculateSpectralCentroids(spectrogram.Data, nyquist); + + // Get the frames per second. + var framesPerSecond = spectrogram.Attributes.FramesPerSecond; + + var centroidsByOneSecondBlocks = AverageSpectralCentroidsInOneSecondSegments(centroidArray, framesPerSecond); + return centroidsByOneSecondBlocks; + } + + public static double[] AverageSpectralCentroidsInOneSecondSegments(double[] centroidArray, double framesPerSecond) + { + // Get the frames per second and truncate partial frame. + var completeFramesPerSecond = (int)Math.Floor(framesPerSecond); + + // calculate the number of one-second blocks. Ignore the residual block IF less than half second. + var centroidArrayLength = centroidArray.Length; + var countOfCompletedSeconds = (int)Math.Round(centroidArrayLength / framesPerSecond); + + var centroidsByOneSecondBlocks = new double[countOfCompletedSeconds]; + for (int i = 0; i < countOfCompletedSeconds; i++) + { + var startFrame = (int)Math.Floor(i * framesPerSecond); + var endFrame = startFrame + completeFramesPerSecond - 1; + if (endFrame >= centroidArrayLength) + { + endFrame = centroidArrayLength - 1; + } + + // sum the centroids + double sum = 0; + int frameCount = 0; + for (int s = startFrame; s <= endFrame; s++) + { + frameCount++; + sum += centroidArray[s]; + } + + centroidsByOneSecondBlocks[i] = sum / frameCount; + } + + return centroidsByOneSecondBlocks; + } } -} + } diff --git a/src/AudioAnalysisTools/StandardSpectrograms/AmplitudeSpectrogram.cs b/src/AudioAnalysisTools/StandardSpectrograms/AmplitudeSpectrogram.cs index c817f127a..c08259404 100644 --- a/src/AudioAnalysisTools/StandardSpectrograms/AmplitudeSpectrogram.cs +++ b/src/AudioAnalysisTools/StandardSpectrograms/AmplitudeSpectrogram.cs @@ -32,10 +32,12 @@ public AmplitudeSpectrogram(SpectrogramSettings config, WavReader wav) //set attributes for the current recording and spectrogram type this.Attributes.SampleRate = wav.SampleRate; this.Attributes.Duration = wav.Time; - this.Attributes.NyquistFrequency = wav.SampleRate / 2; - this.Attributes.Duration = wav.Time; this.Attributes.MaxAmplitude = wav.CalculateMaximumAmplitude(); + this.Attributes.NyquistFrequency = wav.SampleRate / 2; + this.Attributes.FBinWidth = wav.SampleRate / (double)config.WindowSize; + this.Attributes.FrameDuration = TimeSpan.FromSeconds(this.Configuration.WindowSize / (double)wav.SampleRate); + this.Attributes.FramesPerSecond = wav.SampleRate / (double)config.WindowStep; var recording = new AudioRecording(wav); var fftdata = DSP_Frames.ExtractEnvelopeAndFfts( diff --git a/tests/Acoustics.Test/AudioAnalysisTools/SpectralCentroidTests.cs b/tests/Acoustics.Test/AudioAnalysisTools/SpectralCentroidTests.cs new file mode 100644 index 000000000..6bffa7f19 --- /dev/null +++ b/tests/Acoustics.Test/AudioAnalysisTools/SpectralCentroidTests.cs @@ -0,0 +1,117 @@ +// +// All code in this file and all associated files are the copyright and property of the QUT Ecoacoustics Research Group (formerly MQUTeR, and formerly QUT Bioacoustics Research Group). +// + +namespace Acoustics.Test.AudioAnalysisTools +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Text; + using Acoustics.Test.TestHelpers; + using Acoustics.Tools.Wav; + using global::AudioAnalysisTools; + using global::AudioAnalysisTools.StandardSpectrograms; + using global::AudioAnalysisTools.WavTools; + using global::TowseyLibrary; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + /// + /// Tests for methods to do with spectral centroids. + /// + [TestClass] + public class SpectralCentroidTests + { + /// + /// The canonical recording used for this recognizer is a 31 second recording made by Yvonne Phillips at Gympie National Park, 2015-08-18. + /// + private static readonly FileInfo TestAsset = PathHelper.ResolveAsset("Recordings", "gympie_np_1192_331618_20150818_054959_31_0.wav"); + + [TestMethod] + public void TestCalculateSpectralCentroid() + { + // set up the spectrum and nyquist + int nyquist = 11025; + double[] spectrum1 = { 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0 }; + double[] spectrum2 = { 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 }; + double[] spectrum3 = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1.0, 1.0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + double[] spectrum4 = { 1.0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1.0 }; + + // frequency bin width = 11025 / 32 = 344.53125 + // Half bin width = 172.2656 Hz. + // A normalised freq bin has width = 0.03125 + var centroid1 = SpectralCentroid.CalculateSpectralCentroid(spectrum1, nyquist); + var centroid2 = SpectralCentroid.CalculateSpectralCentroid(spectrum2, nyquist); + var centroid3 = SpectralCentroid.CalculateSpectralCentroid(spectrum3, nyquist); + var centroid4 = SpectralCentroid.CalculateSpectralCentroid(spectrum4, nyquist); + + Assert.AreEqual(0.515625, centroid1); + Assert.AreEqual(0.5, centroid2); + Assert.AreEqual(0.5, centroid3); + Assert.AreEqual(0.5, centroid4); + } + + [TestMethod] + public void TestCalculateSpectralCentroids() + { + // set up the spectrum and nyquist + int nyquist = 11025; + double[] spectrum1 = { 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0 }; + double[] spectrum2 = { 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 }; + double[] spectrum3 = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1.0, 1.0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + double[] spectrum4 = { 1.0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1.0 }; + + var frameCount = 4; + var binCount = 32; + var matrixData = new double[frameCount, binCount]; + MatrixTools.SetRow(matrixData, 0, spectrum1); + MatrixTools.SetRow(matrixData, 1, spectrum2); + MatrixTools.SetRow(matrixData, 2, spectrum3); + MatrixTools.SetRow(matrixData, 3, spectrum4); + + var centroids = SpectralCentroid.CalculateSpectralCentroids(matrixData, nyquist); + double[] expectedArray = { 0.515625, 0.5, 0.5, 0.5 }; + + CollectionAssert.AreEqual(expectedArray, centroids); + } + + [TestMethod] + public void TestCalculateSpectralCentroidsInOneSecondBlocks() + { + // set up the spectrum and nyquist + double[] centroidArray = { 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, 0, 1.0 }; + double framesPerSecond = 4.0; + var centroids = SpectralCentroid.AverageSpectralCentroidsInOneSecondSegments(centroidArray, framesPerSecond); + double[] expectedArray1 = { 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5 }; + CollectionAssert.AreEqual(expectedArray1, centroids); + + framesPerSecond = 4.2; + centroids = SpectralCentroid.AverageSpectralCentroidsInOneSecondSegments(centroidArray, framesPerSecond); + double[] expectedArray2 = { 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.66666666666666663 }; + + CollectionAssert.AreEqual(expectedArray2, centroids); + } + + [TestMethod] + public void TestCalculateSpectralCentroidsInOneSecondBlocksOnRealRecording() + { + var recording = new WavReader(TestAsset); + var config = new SpectrogramSettings(); + var amplitudeSpectrogram = new AmplitudeSpectrogram(config, recording); + + var centroids = SpectralCentroid.CalculateSpectralCentroidsInOneSecondSegments(amplitudeSpectrogram); + var length = centroids.Length; + Assert.AreEqual(31, length); + + var centroid1 = centroids[length / 2]; + var centroid2 = centroids[length - 1]; + + var delta = TestHelper.AllowedDelta; + Assert.AreEqual(0.33138923037601808, centroids[0], delta); + Assert.AreEqual(0.32098870879909, centroid1, delta); + Assert.AreEqual(0.32775708863777775, centroid2, delta); + + //Assert.IsNull(scoreTrack); + } + } +}