feat: added an option to let user choose the vertical resolution of the spectrogram, also fixed a bug that peak of pitch in the spectrogram has been mapped to the start of the note bin instead of the center

This commit is contained in:
Xiaohan-Tian
2026-05-04 20:14:04 -07:00
parent d089456676
commit 8a6e904ab4
12 changed files with 302 additions and 41 deletions
+36
View File
@@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest';
import {
getSpectrogramPitchBinCount,
getSpectrogramVisibleBinRange,
mapMidiPitchToSpectrogramPosition,
normalizeSpectrogramHeightResolution,
} from './spectrogramUtil';
describe('spectrogramUtil', () => {
it('normalizes unsupported resolutions to the default 3x', () => {
expect(normalizeSpectrogramHeightResolution(1)).toBe(1);
expect(normalizeSpectrogramHeightResolution(3)).toBe(3);
expect(normalizeSpectrogramHeightResolution(5)).toBe(5);
expect(normalizeSpectrogramHeightResolution(2)).toBe(3);
expect(normalizeSpectrogramHeightResolution(undefined)).toBe(3);
});
it('derives pitch bin counts from the full MIDI range', () => {
expect(getSpectrogramPitchBinCount(1)).toBe(128);
expect(getSpectrogramPitchBinCount(3)).toBe(384);
expect(getSpectrogramPitchBinCount(5)).toBe(640);
});
it('maps MIDI pitches into full-range spectrogram positions', () => {
expect(mapMidiPitchToSpectrogramPosition(0, 1)).toBe(0);
expect(mapMidiPitchToSpectrogramPosition(12.5, 3)).toBe(38.5);
expect(mapMidiPitchToSpectrogramPosition(127, 5)).toBe(637);
expect(mapMidiPitchToSpectrogramPosition(-1, 3)).toBeNull();
expect(mapMidiPitchToSpectrogramPosition(128, 3)).toBeNull();
});
it('derives the visible C0-B7 subrange inside the full-resolution buffer', () => {
expect(getSpectrogramVisibleBinRange(1)).toEqual({ start: 12, end: 108 });
expect(getSpectrogramVisibleBinRange(3)).toEqual({ start: 36, end: 324 });
});
});
+50
View File
@@ -0,0 +1,50 @@
export const SPECTROGRAM_FULL_MIN_MIDI_PITCH = 0;
export const SPECTROGRAM_FULL_MAX_MIDI_PITCH = 127;
export const SPECTROGRAM_MIN_MIDI_PITCH = 12; // C0
export const SPECTROGRAM_MAX_MIDI_PITCH = 107; // B7
export const SPECTROGRAM_FULL_SEMITONES =
SPECTROGRAM_FULL_MAX_MIDI_PITCH - SPECTROGRAM_FULL_MIN_MIDI_PITCH + 1;
export const SPECTROGRAM_VISIBLE_SEMITONES =
SPECTROGRAM_MAX_MIDI_PITCH - SPECTROGRAM_MIN_MIDI_PITCH + 1;
export type SpectrogramHeightResolution = 1 | 3 | 5;
export const SPECTROGRAM_HEIGHT_RESOLUTION_OPTIONS: SpectrogramHeightResolution[] = [1, 3, 5];
export function normalizeSpectrogramHeightResolution(value: unknown): SpectrogramHeightResolution {
if (value === 1 || value === 3 || value === 5) {
return value;
}
return 3;
}
export function getSpectrogramPitchBinCount(
resolution: SpectrogramHeightResolution,
): number {
return SPECTROGRAM_FULL_SEMITONES * resolution;
}
export function getSpectrogramVisibleBinRange(
resolution: SpectrogramHeightResolution,
): { start: number; end: number } {
return {
start: (SPECTROGRAM_MIN_MIDI_PITCH - SPECTROGRAM_FULL_MIN_MIDI_PITCH) * resolution,
end: (SPECTROGRAM_MAX_MIDI_PITCH - SPECTROGRAM_FULL_MIN_MIDI_PITCH + 1) * resolution,
};
}
export function mapMidiPitchToSpectrogramPosition(
midiPitch: number,
resolution: SpectrogramHeightResolution,
): number | null {
const pitchOffset = midiPitch - SPECTROGRAM_FULL_MIN_MIDI_PITCH;
if (pitchOffset < 0 || pitchOffset > SPECTROGRAM_FULL_MAX_MIDI_PITCH) {
return null;
}
// Anchor each MIDI pitch to the center of its semitone band so the
// rendered ridge lines up with the piano-roll key row rather than a boundary.
const scaled = pitchOffset * resolution + (resolution - 1) / 2;
const maxBin = getSpectrogramPitchBinCount(resolution) - 1;
return Math.max(0, Math.min(maxBin, scaled));
}