Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(spectralflux): use magnitude spectrum rather than real component of complex spectrum #1076

Open
wants to merge 10 commits into
base: v6
Choose a base branch
from
Open
18 changes: 1 addition & 17 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,7 @@ jobs:

strategy:
matrix:
node-version: [
# Disabled because with typescript, the 10.x build fails by running out
# of heap space. I can't figure out how to increase heap size on the build
# but since we're dropping support for 10.x soon, I think this is fine.
# Tests still run on all the other versions, and we publish from 16.x now,
# so we I think we're fine to disable it for the next few releases.
# See you in 2035.
#
# https://github.com/meyda/meyda/pull/908/checks?check_run_id=3090204396
# 10.x,
12.x,
13.x,
14.x,
15.x,
16.x,
17.x,
]
node-version: [12.x, 14.x, 15.x, 16.x, 17.x]

steps:
- uses: actions/checkout@v1
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/merge-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ on:
push:
branches:
- main
- v6

jobs:
build:
Expand Down
41 changes: 41 additions & 0 deletions __tests__/extractors/positiveFlux.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import TestData from "../TestData";

// Setup
var positiveFlux = require("../../dist/node/extractors/positiveFlux");

describe("positiveFlux", () => {
test("should return correct Positive Flux value", (done) => {
var en = positiveFlux({
ampSpectrum: TestData.VALID_AMPLITUDE_SPECTRUM.map((e) => e * 0.8),
previousAmpSpectrum: TestData.VALID_AMPLITUDE_SPECTRUM,
});

expect(en).toEqual(2.8276224855864956e-8);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be using toBeCloseTo instead? This number is supposed to be 0, but due to Javascript™️ it isn't.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and we should also be testing with a real set of different signals, rather than just the same one twice. I'll add that.


done();
});

test.skip("should throw an error when passed an empty object", (done) => {
try {
var en = positiveFlux({});
} catch (e) {
done();
}
});

test.skip("should throw an error when not passed anything", (done) => {
try {
var en = positiveFlux();
} catch (e) {
done();
}
});

test.skip("should throw an error when passed something invalid", (done) => {
try {
var en = positiveFlux({ signal: "not a signal" });
} catch (e) {
done();
}
});
});
43 changes: 43 additions & 0 deletions __tests__/extractors/spectralFlux.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import TestData from "../TestData";

// Setup
var spectralFlux = require("../../dist/node/extractors/spectralFlux");

describe("spectralFlux", () => {
test("should return correct Spectral Flux value", (done) => {
var en = spectralFlux({
ampSpectrum: TestData.VALID_AMPLITUDE_SPECTRUM,
previousAmpSpectrum: TestData.VALID_AMPLITUDE_SPECTRUM.map(
(e) => e * 0.8
),
});

expect(en).toEqual(6.8073007097613e-8);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.


done();
});

test.skip("should throw an error when passed an empty object", (done) => {
try {
var en = spectralFlux({});
} catch (e) {
done();
}
});

test.skip("should throw an error when not passed anything", (done) => {
try {
var en = spectralFlux();
} catch (e) {
done();
}
});

test.skip("should throw an error when passed something invalid", (done) => {
try {
var en = spectralFlux({ signal: "not a signal" });
} catch (e) {
done();
}
});
});
4 changes: 2 additions & 2 deletions docs/audio-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ To use RMS in applications where you expect a ceiling on each audio feature, we
`spectralFlux`

- _Description_: A measure of how quickly the spectrum of a signal is changing. It is calculated by computing the difference between the current spectrum and that of the previous frame.
- _What Is It Used For_: Often corresponds to perceptual "roughness" of a sound. Can be used for example, to determine the timbre of a sound.
- _Range_: Starts at `0.0`. This has no upper range as it depends on the input signal.
- _What Is It Used For_: Often corresponds to perceptual "roughness" of a sound. Can be used for example, to determine the timbre of a sound, and onset detection.
- _Range_: Starts at `0.0`. The upper bound is equal to the square root of the buffer size.

### Spectral Slope

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"wav": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || ^13 || ^14 || ^15 || ^16 || ^17"
"node": "^12 || ^14 || ^15 || ^16 || ^17"
},
"jest": {
"preset": "ts-jest",
Expand Down
27 changes: 27 additions & 0 deletions src/extractors/positiveFlux.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { normalizeToOne } from "../utilities";

export default function ({
ampSpectrum,
previousAmpSpectrum,
}: {
ampSpectrum: Float32Array;
previousAmpSpectrum: Float32Array;
}): number {
if (!previousAmpSpectrum) {
return 0;
}

const normalizedMagnitudeSpectrum = normalizeToOne(ampSpectrum);
const previousNormalizedMagnitudeSpectrum =
normalizeToOne(previousAmpSpectrum);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this is correct. Should they be normalised between themselves? Should they be normalised at all? I am not sure normalising each one to 1 separately makes sense. Can you share the source this was taken from?

FWIW, Matlab implements it without normalisation, it seems.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure - I added normalization because wikipedia implies that only some implementations do not normalize. I think it's useful because it makes this feature purely about the spectral content, and resilient to overall volume changes, which can be measured much better with rms and loudness. But, I haven't gone looking for other implementations yet.


let sf = 0;
for (let i = 0; i < normalizedMagnitudeSpectrum.length; i++) {
let x =
Math.abs(normalizedMagnitudeSpectrum[i]) -
Math.abs(previousNormalizedMagnitudeSpectrum[i]);
sf += Math.pow(Math.max(x, 0), 2);
nevosegal marked this conversation as resolved.
Show resolved Hide resolved
}

return Math.sqrt(sf);
}
32 changes: 17 additions & 15 deletions src/extractors/spectralFlux.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
// This file isn't being typechecked at all because there are major issues with it.
// See #852 for details. Once that's merged, this file should be typechecked.
// @ts-nocheck
import { normalizeToOne } from "../utilities";

export default function ({
signal,
previousSignal,
bufferSize,
ampSpectrum,
previousAmpSpectrum,
}: {
signal: Float32Array;
previousSignal: Float32Array;
bufferSize: number;
ampSpectrum: Float32Array;
previousAmpSpectrum: Float32Array;
}): number {
if (typeof signal !== "object" || typeof previousSignal != "object") {
throw new TypeError();
if (!previousAmpSpectrum) {
return 0;
}

const normalizedMagnitudeSpectrum = normalizeToOne(ampSpectrum);
const previousNormalizedMagnitudeSpectrum =
normalizeToOne(previousAmpSpectrum);

let sf = 0;
for (let i = -(bufferSize / 2); i < signal.length / 2 - 1; i++) {
x = Math.abs(signal[i]) - Math.abs(previousSignal[i]);
sf += (x + Math.abs(x)) / 2;
for (let i = 0; i < normalizedMagnitudeSpectrum.length; i++) {
let x =
normalizedMagnitudeSpectrum[i] - previousNormalizedMagnitudeSpectrum[i];
sf += Math.pow(x, 2);
}

return sf;
return Math.sqrt(sf);
}
2 changes: 2 additions & 0 deletions src/featureExtractors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import mfcc from "./extractors/mfcc";
import chroma from "./extractors/chroma";
import powerSpectrum from "./extractors/powerSpectrum";
import spectralFlux from "./extractors/spectralFlux";
import positiveFlux from "./extractors/positiveFlux";

let buffer = function (args) {
return args.signal;
Expand Down Expand Up @@ -49,4 +50,5 @@ export {
mfcc,
chroma,
spectralFlux,
positiveFlux,
};
5 changes: 4 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ export interface MeydaFeaturesObject {
rms: number;
spectralCentroid: number;
spectralFlatness: number;
spectralFlux: number;
spectralKurtosis: number;
spectralRolloff: number;
spectralSkewness: number;
spectralSlope: number;
spectralSpread: number;
zcr: number;
positiveFlux: number;
}

export type MeydaWindowingFunction =
Expand Down Expand Up @@ -63,7 +65,8 @@ export type MeydaAudioFeature =
| "spectralSlope"
| "spectralSpread"
| "zcr"
| "buffer";
| "buffer"
| "positiveFlux";

/**
* A type representing an audio signal. In general it should be an array of
Expand Down
13 changes: 11 additions & 2 deletions src/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,15 @@ export function mean(a) {
);
}

const MEL_CONSTANT = 1127;

function _melToFreq(melValue) {
var freqValue = 700 * (Math.exp(melValue / 1125) - 1);
var freqValue = 700 * (Math.exp(melValue / MEL_CONSTANT) - 1);
return freqValue;
}

function _freqToMel(freqValue) {
var melValue = 1125 * Math.log(1 + freqValue / 700);
var melValue = MEL_CONSTANT * Math.log(1 + freqValue / 700);
return melValue;
}

Expand Down Expand Up @@ -270,3 +272,10 @@ export function frame(buffer, frameLength, hopLength) {
.fill(0)
.map((_, i) => buffer.slice(i * hopLength, i * hopLength + frameLength));
}

export function magnitudeForComplexSpectrum(complexSpectrum) {
return complexSpectrum.real.map((real, i) => {
const imag = complexSpectrum.imag[i];
return Math.sqrt(Math.pow(real, 2) + Math.pow(imag, 2));
});
}
Comment on lines +275 to +281
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we have this somewhere already? Many feature extractors already use the amplitude spectrum AFAIK.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - it's here - and even in this PR we actually use that implementation, not this one. This function is from before I remembered that magnitude spectrum and amplitude spectrum are the same.

In that part of the source, the calculation is an inline snippet in a function for preparing the spectrum, and it only calculates the lower half of the buffer (which is good). But when I swapped it out for half the result of this function, I got a different result, so I wanted to keep this unused function around and debug it later.