Skip to content

Commit

Permalink
Calculation of the Spectral Centroid
Browse files Browse the repository at this point in the history
Issue #292 Write various methods to calculate the spectral centroid of a spectrum and accompanying unit tests of the methods.
  • Loading branch information
towsey committed Jul 6, 2020
1 parent 4a85b57 commit 44d91cf
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 11 deletions.
81 changes: 72 additions & 9 deletions src/AudioAnalysisTools/SpectralCentroid.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using AudioAnalysisTools.StandardSpectrograms;
using System;
using System.Collections.Generic;
using System.Text;
Expand All @@ -6,12 +7,12 @@
namespace AudioAnalysisTools
{
/// <summary>
/// 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.
Expand All @@ -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);
Expand All @@ -57,17 +58,79 @@ 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);
}

return centroidArray;
}

/// <summary>
/// Calculates the spectral centroid for each frame of an amplitude spectrogram.
/// </summary>
/// <param name="spectrogram">As AmplitudeSpectrogram.</param>
/// <returns>An array of spectral centroids.</returns>
public static double[] CalculateSpectralCentroids(AmplitudeSpectrogram spectrogram)
{
int nyquist = spectrogram.Attributes.NyquistFrequency;
var centroidArray = CalculateSpectralCentroids(spectrogram.Data, nyquist);
return centroidArray;
}

/// <summary>
/// Calculates the spectral centroid for each one-second segment of an amplitude spectrogram.
/// </summary>
/// <param name="spectrogram">As AmplitudeSpectrogram.</param>
/// <returns>An array of spectral centroids.</returns>
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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
117 changes: 117 additions & 0 deletions tests/Acoustics.Test/AudioAnalysisTools/SpectralCentroidTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// <copyright file="SpectralCentroidTests.cs" company="QutEcoacoustics">
// 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).
// </copyright>

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;

/// <summary>
/// Tests for methods to do with spectral centroids.
/// </summary>
[TestClass]
public class SpectralCentroidTests
{
/// <summary>
/// The canonical recording used for this recognizer is a 31 second recording made by Yvonne Phillips at Gympie National Park, 2015-08-18.
/// </summary>
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);
}
}
}

0 comments on commit 44d91cf

Please sign in to comment.