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
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { encodeWav } from './KGOfflineRenderer';
import { encodeWav, getOfflineTrackGain, getOfflineTrackVolumeDb } from './KGOfflineRenderer';
/**
* Create a minimal AudioBuffer-like object for testing.
@@ -145,3 +145,22 @@ describe('encodeWav', () => {
expect(view.getUint32(40, true)).toBe(0); // data size = 0
});
});
describe('offline track volume conversion', () => {
it('treats 0 dB as unity gain instead of silence', () => {
expect(getOfflineTrackVolumeDb(0, false)).toBe(0);
expect(getOfflineTrackGain(0, false)).toBe(1);
});
it('converts negative dB values to linear gain', () => {
expect(getOfflineTrackVolumeDb(-6, false)).toBe(-6);
expect(getOfflineTrackGain(-6, false)).toBeCloseTo(Math.pow(10, -6 / 20), 6);
});
it('silences muted tracks and floor-level volumes', () => {
expect(getOfflineTrackVolumeDb(0, true)).toBe(-Infinity);
expect(getOfflineTrackGain(0, true)).toBe(0);
expect(getOfflineTrackVolumeDb(-60, false)).toBe(-Infinity);
expect(getOfflineTrackGain(-60, false)).toBe(0);
});
});
+14 -4
View File
@@ -3,6 +3,7 @@ import type { KGProject } from '../KGProject';
import type { KGMidiNote } from '../midi/KGMidiNote';
import type { KGAudioRegion } from '../region/KGAudioRegion';
import { FLUIDR3_INSTRUMENT_MAP } from '../../constants/generalMidiConstants';
import { AUDIO_INTERFACE_CONSTANTS } from '../../constants/coreConstants';
import { pitchToNoteNameString } from '../../util/midiUtil';
import { KGToneBuffersPool } from './KGToneBuffersPool';
import { KGToneSamplerFactory } from './KGToneSamplerFactory';
@@ -254,9 +255,8 @@ export class KGOfflineRenderer {
});
});
// Apply volume
const volumeDb = trackInfo.volume > 0 ? 20 * Math.log10(trackInfo.volume) : -Infinity;
sampler.volume.value = volumeDb;
// Track volumes are stored in dB across the app, with 0 meaning unity gain.
sampler.volume.value = getOfflineTrackVolumeDb(trackInfo.volume, trackInfo.muted);
sampler.connect(masterGain);
// Schedule all notes for this track
@@ -288,7 +288,7 @@ export class KGOfflineRenderer {
for (const trackInfo of audioTrackData) {
if (!shouldPlay(trackInfo, hasSoloedTracks)) continue;
const trackGain = new Tone.Gain(trackInfo.volume);
const trackGain = new Tone.Gain(getOfflineTrackGain(trackInfo.volume, trackInfo.muted));
trackGain.connect(masterGain);
for (const regionInfo of trackInfo.regions) {
@@ -418,6 +418,16 @@ function shouldPlay(trackInfo: { muted: boolean; solo: boolean }, hasSoloedTrack
return true;
}
export function getOfflineTrackVolumeDb(volumeDb: number, muted: boolean): number {
const isSilent = muted || volumeDb <= AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB;
return isSilent ? -Infinity : volumeDb;
}
export function getOfflineTrackGain(volumeDb: number, muted: boolean): number {
const effectiveVolumeDb = getOfflineTrackVolumeDb(volumeDb, muted);
return Number.isFinite(effectiveVolumeDb) ? Math.pow(10, effectiveVolumeDb / 20) : 0;
}
// ===== WAV ENCODER =====
/**
+4 -2
View File
@@ -71,6 +71,7 @@ interface AppConfig {
};
editor: {
playhead_update_frequency: number;
spectrogram_height_resolution: 1 | 3 | 5;
};
chatbox: {
default_open: boolean;
@@ -243,7 +244,8 @@ export class ConfigManager {
},
},
editor: {
playhead_update_frequency: 10
playhead_update_frequency: 10,
spectrogram_height_resolution: 3
},
chatbox: {
default_open: true
@@ -607,4 +609,4 @@ export class ConfigManager {
console.error('Error disposing ConfigManager:', error);
}
}
}
}