Skip to content

Commit

Permalink
Merge pull request #445 from Renumics/feature/melscale-frequency
Browse files Browse the repository at this point in the history
Feature/melscale frequency
  • Loading branch information
neindochoh authored Mar 8, 2024
2 parents 2e867e9 + 008c5ff commit 8f00e3e
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 31 deletions.
68 changes: 68 additions & 0 deletions src/lenses/SpectrogramLens/MelScale.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as d3 from 'd3';

interface MelScale {
(value: number): number;
toMelScale(value: number): number;
fromMelScale(frequency: number): number;
domain(): number[];
domain(domain: number[]): MelScale;
range(): number[];
range(range: number[]): MelScale;
copy(): MelScale;
invert(value: number): number;
ticks(count?: number): number[];
tickFormat(count?: number, specifier?: string): (d: number) => string;
}

const melScale = (): MelScale => {
// Create the base log scale
const linearScale = d3.scaleLinear();
const logScale = d3.scaleLog();

// Our custom scale function
const scale: MelScale = ((value: number) => {
return linearScale(value);
}) as MelScale;

scale.toMelScale = (frequency: number): number => {
return 2595 * Math.log10(1 + frequency / 700);
};

scale.fromMelScale = (mel: number): number => {
return 700 * (Math.pow(10, mel / 2595) - 1);
};

// Copy methods from the log scale
// eslint-disable-next-line @typescript-eslint/no-explicit-any
scale.domain = (domain?: number[]): any => {
if (domain === undefined) {
return linearScale.domain().map((d) => scale.toMelScale(d));
}
logScale.domain(domain);
return domain ? (linearScale.domain(domain), scale) : linearScale.domain();
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
scale.range = (range?: number[]): any => {
if (range) logScale.range(range);
return range ? (linearScale.range(range), scale) : linearScale.range();
};
scale.copy = () => {
return melScale().domain(scale.domain()).range(scale.range());
};
scale.invert = (value: number): number => {
return scale.fromMelScale(linearScale.invert(value));
};

scale.ticks = (count?: number): number[] => {
const ticks = logScale.ticks(count).map((val) => {
return scale.toMelScale(val);
});
if (!count) return ticks;

return [ticks[0]].concat(ticks.slice(ticks.length - count, ticks.length));
};

return scale;
};

export default melScale;
88 changes: 57 additions & 31 deletions src/lenses/SpectrogramLens/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { ColorsState, useColors } from '../../stores/colors';
import { Lens } from '../../types';
import useSetting from '../useSetting';
import MenuBar from './MenuBar';
import { fixWindow, freqType, unitType, amplitudeToDb, hzToMel } from './Spectrogram';
import melScale from './MelScale';
import { fixWindow, freqType, unitType, amplitudeToDb } from './Spectrogram';

const Container = tw.div`flex flex-col w-full h-full items-stretch justify-center`;
const EmptyNote = styled.p`
Expand All @@ -22,7 +23,7 @@ interface WebAudio_ extends WebAudio {
buffer: AudioBuffer;
}

const LOG_DOMAIN_LOWER_LIMIT = 10;
const DOMAIN_LOWER_LIMIT = 10;
const FFT_SAMPLES = 1024;

/*
Expand All @@ -41,14 +42,16 @@ const drawScale = (
numTicks = Math.round(height / 20);
} else if (scale === 'linear') {
numTicks = Math.round(height / 30);
} else if (scale === 'mel') {
numTicks = Math.round(height / 40);
} else {
numTicks = 5;
}

let axis;

if (scale === 'logarithmic') {
const domain = [LOG_DOMAIN_LOWER_LIMIT, upperLimit];
const domain = [DOMAIN_LOWER_LIMIT, upperLimit];
const range = [height, 0];
const scale = d3.scaleLog(domain, range);

Expand All @@ -60,6 +63,23 @@ const drawScale = (
.ticks(numTicks, (x: number) => {
return `${freqType(x).toFixed(1)} ${unitType(x)}`;
});
} else if (scale === 'mel') {
const domain: [number, number] = [DOMAIN_LOWER_LIMIT, upperLimit];
const range: [number, number] = [height, 0];
const scale = melScale().domain(domain).range(range);

axis = d3
.axisRight(scale)
.scale(scale)
.tickPadding(1)
.tickSizeOuter(0)
.ticks(numTicks)
.tickFormat(
(x: number) =>
`${freqType(scale.fromMelScale(x).valueOf()).toFixed(1)} ${unitType(
scale.fromMelScale(x).valueOf()
)}`
);
} else {
const domain = [upperLimit, 0];
const range = [0, height];
Expand Down Expand Up @@ -187,22 +207,22 @@ const SpectrogramLens: Lens = ({ columns, urls, values }) => {
const upperLimit = backend.buffer?.sampleRate / 2;

// 10, ..., half-samplerate (default 22050)
const domain = [LOG_DOMAIN_LOWER_LIMIT, upperLimit];
const domain = [DOMAIN_LOWER_LIMIT, upperLimit];

// 0, ..., canvas-height
const range = [0, height];

const imageData = new ImageData(width, height);

const scaleFunc = d3.scaleLog(domain, range);

// Default to linear scale
let heightScale = d3.scaleLinear(domain, [0, upperLimit]);

if (freqScale === 'logarithmic') {
heightScale = d3.scaleLinear(domain, [0, FFT_SAMPLES / 2 - 1]);
} else if (freqScale === 'linear') {
heightScale = d3.scaleLinear(domain, [0, upperLimit]);
heightScale = d3.scaleLinear(range, [0, FFT_SAMPLES / 2]);
} else if (freqScale === 'mel') {
heightScale = d3.scaleLinear(domain, [0, FFT_SAMPLES / 2 - 1]);
}

const widthScale = d3.scaleLinear([0, width], [0, frequenciesData.length]);
Expand All @@ -221,7 +241,6 @@ const SpectrogramLens: Lens = ({ columns, urls, values }) => {
ref = maxI;
}
}
//const top_db = 80;
const amin = 1e-5;

// Convert amplitudes to decibels
Expand All @@ -241,24 +260,6 @@ const SpectrogramLens: Lens = ({ columns, urls, values }) => {
}
}

drawData[i] = col;
}
} else if (ampScale === 'mel') {
for (let i = 0; i < frequenciesData.length; i++) {
const col = [];

for (let j = 0; j < frequenciesData[i].length; j++) {
const amplitude = frequenciesData[i][j];
col[j] = hzToMel(amplitude ** 2);

if (col[j] > max) {
max = col[j];
}

if (col[j] < min) {
min = col[j];
}
}
drawData[i] = col;
}
} else {
Expand All @@ -278,13 +279,21 @@ const SpectrogramLens: Lens = ({ columns, urls, values }) => {
}
const colorScale = colorPalette.scale().domain([min, max]);

const scaleFunc = d3.scaleLog(domain, range);

for (let y = 0; y < height; y++) {
let value = 0;

if (freqScale === 'logarithmic') {
value = heightScale(scaleFunc.invert(height - y));
} else if (freqScale === 'linear') {
value = Math.abs(heightScale(height - y));
value = heightScale(height - y);
} else if (freqScale === 'mel') {
const scaleFunc = melScale()
.domain([DOMAIN_LOWER_LIMIT, melScale().toMelScale(upperLimit)])
.range(range);
heightScale = d3.scaleLinear([0, upperLimit], [0, FFT_SAMPLES / 2]);
value = heightScale(scaleFunc.invert(height - y));
}

const indexA = Math.floor(value);
Expand Down Expand Up @@ -388,7 +397,7 @@ const SpectrogramLens: Lens = ({ columns, urls, values }) => {
const coords = d3.pointer(e);

if (freqScale === 'logarithmic') {
const domain = [LOG_DOMAIN_LOWER_LIMIT, upperLimit];
const domain = [DOMAIN_LOWER_LIMIT, upperLimit];
const range = [0, height];

const scale = d3.scaleLog(domain, range);
Expand All @@ -399,6 +408,24 @@ const SpectrogramLens: Lens = ({ columns, urls, values }) => {
.attr('y', coords[1])
.attr('font-size', 10)
.attr('fill', theme`colors.white`);
} else if (freqScale === 'mel') {
let y = height - coords[1];

const domain: [number, number] = [
DOMAIN_LOWER_LIMIT,
melScale().toMelScale(upperLimit),
];
const range: [number, number] = [0, height];

const scaleFunc = melScale().domain(domain).range(range);
y = scaleFunc.invert(y);

d3.select(mouseLabel.current)
.text(`${freqType(y).toFixed(1)} ${unitType(y)}`)
.attr('x', coords[0])
.attr('y', coords[1])
.attr('font-size', 10)
.attr('fill', theme`colors.white`);
} else {
//if (freqScale === 'linear') {
const domain = [upperLimit, 0];
Expand Down Expand Up @@ -462,12 +489,11 @@ const SpectrogramLens: Lens = ({ columns, urls, values }) => {
// Add the menubar as last component, so that it is rendered on top
// We don't use a z-index for this, because it interferes with the rendering of the contained menus
}

<MenuBar
availableFreqScales={['linear', 'logarithmic']}
availableFreqScales={['linear', 'logarithmic', 'mel']}
freqScale={freqScale}
onChangeFreqScale={handleFreqScaleChange}
availableAmpScales={['decibel', 'linear', 'mel']}
availableAmpScales={['decibel', 'linear']}
ampScale={ampScale}
onChangeAmpScale={handleAmpScaleChange}
/>
Expand Down

0 comments on commit 8f00e3e

Please sign in to comment.