Skip to content

Commit

Permalink
feat: add equalizer function (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
dimadeveatii authored Apr 27, 2022
1 parent eef3be2 commit 2b3180f
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 31 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Prettier
run: npm run prettier

- name: Lint
run: npm run lint

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Prettier
run: npm run prettier

- name: Lint
run: npm run lint

Expand Down
60 changes: 43 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ npm install octave-bands
```

```ts
import { octaves, bandwidth } from 'octave-bands';
import { octaves, bandwidth, equalizer } from 'octave-bands';
```

## Usage
Expand Down Expand Up @@ -99,25 +99,27 @@ console.table(bands);
*/
```

Get _one-half-octave_ bands in a custom audio spectrum range:
Get the frequency limits for a 21-band equalizer covering an audio spectrum
from 20Hz to 20kHz

```ts
import { octaves } from 'octave-bands';
import { equalizer } from 'octave-bands';

// Include only the bands with center frequencies
// in spectrum range [100, 16000]
const bands = octaves(1 / 2, { spectrum: [100, 16000] });
const bands = equalizer(21, { spectrum: [20, 20000] });
console.table(bands);

/*
┌─────────┬──────────┬───────────┬───────────┐
│ (index) │ 0 │ 1 │ 2 │
├─────────┼──────────┼───────────┼───────────┤
│ 0 │ 105.112 │ 125 │ 148.651 │
│ 1 │ 148.651 │ 176.777 │ 210.224 │
..............................................
│ 13 │ 9513.657 │ 11313.708 │ 13454.343 │
└─────────┴──────────┴───────────┴───────────┘
┌─────────┬───────────┬───────────┬───────────┐
│ (index) │ 0 │ 1 │ 2 │
├─────────┼───────────┼───────────┼───────────┤
│ 0 │ 16.828 │ 20 │ 23.77 │
│ 1 │ 23.77 │ 28.251 │ 33.576 │
│ 2 │ 33.576 │ 39.905 │ 47.427 │
│ 3 │ 47.427 │ 56.368 │ 66.993 │
...............................................
│ 19 │ 11913.243 │ 14158.916 │ 16827.903 │
│ 20 │ 16827.903 │ 20000 │ 23770.045 │
└─────────┴───────────┴───────────┴───────────┘
*/
```

Expand All @@ -133,7 +135,7 @@ console.log('1/3 octave bandwidth', bandwidth(1 / 3)); // ~0.232

## API

> `octaves(fraction: number, options: Options): [number, number, number][]`
> `octaves(fraction: number = 1, options?: Options): [number, number, number][]`
Calculates the frequencies for fractional octave bands.

Expand All @@ -144,8 +146,8 @@ Arguments:

```ts
type Options: {
center: number,
spectrum: [number, number]
center?: number,
spectrum?: [number, number]
}
```

Expand All @@ -156,6 +158,30 @@ Returns:

- `[number, number, number][]` returns an array where each element represents the frequency bounds of a band `[low, center, high]`

> `equalizer(bands: number, options?: Options): [number, number, number][]`
Calculates the frequency bounds for a given number of bands that should cover an audio spectrum.
This is similar to `octaves` function, but instead of using octave's _fraction_ to calculate the frequencies, it uses the
number of bands to output.

Arguments:

- `bands` - the number of bands to output

- `options` - additional options:

```ts
type Options: {
spectrum?: [number, number]
}
```

- `spectrum` - a two-numbers array representing the _min_ and _max_ values to include for center frequencies (defaults to `[15, 21000]`)

Returns:

- `[number, number, number][]` returns an array where each element represents the frequency bounds of a band `[low, center, high]`

> `bandwidth(fraction: number): number`
Calculates the constant bandwidth per 1/N-octave.
Expand Down
78 changes: 77 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type Options = {
*/
export type Band = [number, number, number];

const DEFAULT_OPTIONS = { spectrum: [15, 21000], center: 1000 };
const DEFAULT_OPTIONS: Options = { spectrum: [15, 21000], center: 1000 };

/**
* Calculates and returns the frequency limits for octave bands.
Expand All @@ -34,6 +34,9 @@ export const octaves = (fraction = 1, options?: Options) => {
...options,
};

validateSpectrum([min, max]);
validateCenter(center, [min, max]);

const factor = Math.pow(2, fraction);
const factor2 = Math.pow(Math.SQRT2, fraction);

Expand All @@ -60,8 +63,81 @@ export const bandwidth = (fraction = 1) => {
return (Math.pow(2, fraction) - 1) / Math.pow(2, fraction / 2);
};

/**
* Calculates the frequency bounds for a given spectrum and number of bands
* @param bands number of bands
* @param spectrum the audio spectrum for bands' center frequencies (default `[15, 21000]`)
* @returns an array of bands, where each band is a tuple of `[lower, center, higher]` frequencies
*/
export const equalizer = (
bands: number,
options?: Pick<Options, 'spectrum'>
) => {
validateBands(bands);

const { spectrum } = { ...DEFAULT_OPTIONS, ...options };
validateSpectrum(spectrum);

const fraction = Math.log2(spectrum[1] / spectrum[0]) / (bands - 1);

const factor = Math.pow(2, fraction);
const factor2 = Math.sqrt(factor);
const result: Band[] = [
[spectrum[0] / factor2, spectrum[0], spectrum[0] * factor2],
];

while (result[result.length - 1][2] < spectrum[1]) {
const center = result[result.length - 1][1] * factor;
result.push([center / factor2, center, center * factor2]);
}

return result;
};

const validateFraction = (fraction: number) => {
if (typeof fraction !== 'number') {
throw new Error('fraction not a number');
}

if (fraction <= 0) {
throw new Error('fraction should be positive');
}
};

const validateBands = (bands: number) => {
if (bands <= 0) {
throw new Error('bands should be positive');
}

if (Math.floor(bands) !== bands) {
throw new Error('bands should be integer');
}
};

const validateSpectrum = (spectrum: [number, number]) => {
if (typeof spectrum[0] !== 'number') {
throw new Error('spectrum min not a number');
}

if (typeof spectrum[1] !== 'number') {
throw new Error('spectrum max not a number');
}

if (spectrum[0] <= 0) {
throw new Error('spectrum min should be positive');
}

if (spectrum[0] >= spectrum[1]) {
throw new Error('spectrum range invalid');
}
};

const validateCenter = (value: number, spectrum: [number, number]) => {
if (typeof value !== 'number') {
throw new Error('center not a number');
}

if (value < spectrum[0] || value > spectrum[1]) {
throw new Error('center out of spectrum range');
}
};
99 changes: 86 additions & 13 deletions tests/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { octaves, bandwidth, Band } from '../src';
import { octaves, bandwidth, equalizer, Band } from '../src';

describe('octave-bands', () => {
const expectBand = (received: Band, [low, center, high]: Band) => {
Expand All @@ -8,12 +8,33 @@ describe('octave-bands', () => {
};

describe('octaves', () => {
it.each([Number.NEGATIVE_INFINITY, -1, -Number.EPSILON, 0])(
'validate fraction for %f',
(fraction) => {
expect(() => octaves(fraction)).toThrow(/fraction/i);
}
);
describe('validations', () => {
it.each([null, Number.NEGATIVE_INFINITY, -1, -Number.EPSILON, 0])(
'when fraction %f should throw',
(fraction) => {
expect(() => octaves(fraction)).toThrow(/fraction/i);
}
);

it.each([
[null, 10],
[10, null],
[0, 1000],
[200, 100],
[100, 100],
])('when spectrum [%p, %p] should throw', (min, max) => {
expect(() => octaves(1, { spectrum: [min, max] })).toThrow(/spectrum/i);
});

it.each([null, 0, 9.99, 100.01])(
'when center %p should throw',
(center) => {
expect(() => octaves(1, { center, spectrum: [10, 100] })).toThrow(
/center/i
);
}
);
});

it.each([
['default', '1', undefined],
Expand Down Expand Up @@ -53,12 +74,14 @@ describe('octave-bands', () => {
});

describe('bandwidth', () => {
it.each([Number.NEGATIVE_INFINITY, -1, -Number.EPSILON, 0])(
'validate fraction for %f',
(fraction) => {
expect(() => bandwidth(fraction)).toThrow(/fraction/i);
}
);
describe('validations', () => {
it.each([null, Number.NEGATIVE_INFINITY, -1, -Number.EPSILON, 0])(
'when faction %p should throw',
(fraction) => {
expect(() => bandwidth(fraction)).toThrow(/fraction/i);
}
);
});

it.each([
['default', undefined, 0.707],
Expand All @@ -76,6 +99,56 @@ describe('octave-bands', () => {
expect(bandwidth(1 / 10)).toBeCloseTo(bw, 6);
});
});

describe('equalizer', () => {
describe('validations', () => {
it.each([null, 0, 10.1])('when bands %p should throw', (bands) => {
expect(() => equalizer(bands)).toThrow(/bands/i);
});

it.each([
[null, 10],
[10, null],
[0, 1000],
[200, 100],
[100, 100],
])('when spectrum [%p, %p] should throw', (min, max) => {
expect(() => equalizer(11, { spectrum: [min, max] })).toThrow(
/spectrum/i
);
});
});

it.each([1, 7, 11, 21, 32, 99, 200])(
'when bands %p should verify bands count',
(bands) => {
const actual = equalizer(bands);
expect(actual).toHaveLength(bands);
}
);

it.each([2, 7, 11, 21, 32, 99, 200])(
'when bands %p should verify bounds',
(bands) => {
const spectrum: [number, number] = [20, 20000];
const actual = equalizer(bands, { spectrum });

expect(actual.at(0)[1]).toBeCloseTo(spectrum[0], 3);
expect(actual.at(-1)[1]).toBeCloseTo(spectrum[1], 3);
}
);

it.each([2, 7, 11, 21, 32, 99, 200])(
'when bands %p should verify bandwidth',
(bands) => {
const spectrum: [number, number] = [20, 20000];
const [first, ...others] = equalizer(bands, { spectrum });
const bw = ([low, center, high]: Band) => (high - low) / center;

others.forEach((b) => expect(bw(first)).toBeCloseTo(bw(b), 5));
}
);
});
});

// Source:
Expand Down

0 comments on commit 2b3180f

Please sign in to comment.