feat: switch track volume to dB scale with Logic Pro-style fader (−∞dB ~ +12dB)
This commit is contained in:
@@ -51,7 +51,7 @@
|
|||||||
.track-info {
|
.track-info {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 120px;
|
height: 120px;
|
||||||
padding: 15px;
|
padding: 15px 5px 15px 15px;
|
||||||
background-color: #2d2d2d;
|
background-color: #2d2d2d;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -155,6 +155,34 @@
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.volume-slider .volume-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 9px;
|
||||||
|
color: #aaa;
|
||||||
|
min-width: 22px;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider .volume-label-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider .volume-label-clickable:hover {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider .volume-label-input {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
padding: 0 2px;
|
||||||
|
width: 36px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.volume-slider input[type="range"] {
|
.volume-slider input[type="range"] {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -13,6 +13,35 @@ import { DEBUG_MODE } from '../../constants/uiConstants';
|
|||||||
import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface';
|
import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface';
|
||||||
import { AUDIO_INTERFACE_CONSTANTS } from '../../constants/coreConstants';
|
import { AUDIO_INTERFACE_CONSTANTS } from '../../constants/coreConstants';
|
||||||
import { showAlert, showConfirm, showPrompt } from '../common/DialogProvider';
|
import { showAlert, showConfirm, showPrompt } from '../common/DialogProvider';
|
||||||
|
|
||||||
|
const UNITY_POS = 750;
|
||||||
|
const SLIDER_MAX = 1000;
|
||||||
|
|
||||||
|
function sliderToDb(pos: number): number {
|
||||||
|
const MIN = AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB;
|
||||||
|
const MAX = AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB;
|
||||||
|
if (pos <= 0) return MIN;
|
||||||
|
if (pos >= SLIDER_MAX) return MAX;
|
||||||
|
if (pos <= UNITY_POS) {
|
||||||
|
const t = pos / UNITY_POS;
|
||||||
|
return MIN * (1 - t * t);
|
||||||
|
}
|
||||||
|
return MAX * (pos - UNITY_POS) / (SLIDER_MAX - UNITY_POS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbToSlider(db: number): number {
|
||||||
|
const MIN = AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB;
|
||||||
|
const MAX = AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB;
|
||||||
|
if (db <= MIN) return 0;
|
||||||
|
if (db >= MAX) return SLIDER_MAX;
|
||||||
|
if (db <= 0) return Math.round(Math.sqrt(1 - db / MIN) * UNITY_POS);
|
||||||
|
return Math.round(UNITY_POS + (db / MAX) * (SLIDER_MAX - UNITY_POS));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDb(db: number): string {
|
||||||
|
if (db <= AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB) return '−∞';
|
||||||
|
return `${db >= 0 ? '+' : ''}${db.toFixed(1)}`;
|
||||||
|
}
|
||||||
interface TrackInfoItemProps {
|
interface TrackInfoItemProps {
|
||||||
track: KGTrack;
|
track: KGTrack;
|
||||||
index: number;
|
index: number;
|
||||||
@@ -56,6 +85,9 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
|
|||||||
const settingsDropdownRef = useRef<HTMLDivElement>(null);
|
const settingsDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const suppressDragRef = useRef(false);
|
const suppressDragRef = useRef(false);
|
||||||
const [volume, setVolume] = useState(track.getVolume());
|
const [volume, setVolume] = useState(track.getVolume());
|
||||||
|
const [isEditingVolume, setIsEditingVolume] = useState(false);
|
||||||
|
const [volumeInputText, setVolumeInputText] = useState('');
|
||||||
|
const volumeInputRef = useRef<HTMLInputElement>(null);
|
||||||
// Local flag to track slider interaction; not used for rendering
|
// Local flag to track slider interaction; not used for rendering
|
||||||
const isAdjustingVolumeRef = useRef(false);
|
const isAdjustingVolumeRef = useRef(false);
|
||||||
const [muted, setMuted] = useState(false);
|
const [muted, setMuted] = useState(false);
|
||||||
@@ -129,11 +161,10 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
|
|||||||
|
|
||||||
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const next = Number(e.target.value) / 100;
|
const next = sliderToDb(Number(e.target.value));
|
||||||
isAdjustingVolumeRef.current = true;
|
isAdjustingVolumeRef.current = true;
|
||||||
setVolume(next);
|
setVolume(next);
|
||||||
try {
|
try {
|
||||||
// Live preview: update audio only while sliding
|
|
||||||
KGAudioInterface.instance().setTrackVolume(track.getId().toString(), next);
|
KGAudioInterface.instance().setTrackVolume(track.getId().toString(), next);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to update live volume:', err);
|
console.error('Failed to update live volume:', err);
|
||||||
@@ -167,6 +198,43 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleVolumeLabelClick = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const displayText = volume <= AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB
|
||||||
|
? '-60'
|
||||||
|
: volume.toFixed(1);
|
||||||
|
setVolumeInputText(displayText);
|
||||||
|
setIsEditingVolume(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
volumeInputRef.current?.select();
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitVolumeLabelInput = () => {
|
||||||
|
setIsEditingVolume(false);
|
||||||
|
const parsed = parseFloat(volumeInputText);
|
||||||
|
if (isNaN(parsed)) return;
|
||||||
|
const clamped = Math.max(
|
||||||
|
AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB,
|
||||||
|
Math.min(AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB, parsed)
|
||||||
|
);
|
||||||
|
setVolume(clamped);
|
||||||
|
try {
|
||||||
|
useProjectStore.getState().updateTrackProperties(track.getId(), { volume: clamped });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to set volume from label input:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVolumeLabelKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
commitVolumeLabelInput();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
setIsEditingVolume(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleToggleMute = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleToggleMute = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const next = !muted;
|
const next = !muted;
|
||||||
@@ -302,8 +370,9 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
|
|||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="1000"
|
||||||
value={Math.round(volume * 100)}
|
step="1"
|
||||||
|
value={dbToSlider(volume)}
|
||||||
onChange={handleVolumeChange}
|
onChange={handleVolumeChange}
|
||||||
onMouseDown={(e) => { e.stopPropagation(); isAdjustingVolumeRef.current = true; }}
|
onMouseDown={(e) => { e.stopPropagation(); isAdjustingVolumeRef.current = true; }}
|
||||||
onMouseUp={(e) => { e.stopPropagation(); commitVolumeChange(); }}
|
onMouseUp={(e) => { e.stopPropagation(); commitVolumeChange(); }}
|
||||||
@@ -314,12 +383,32 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="reset-volume"
|
className="reset-volume"
|
||||||
title="Reset volume"
|
title="Reset to 0 dB"
|
||||||
aria-label="Reset volume"
|
aria-label="Reset volume to 0 dB"
|
||||||
onClick={handleResetVolume}
|
onClick={handleResetVolume}
|
||||||
>
|
>
|
||||||
↺
|
↺
|
||||||
</button>
|
</button>
|
||||||
|
{isEditingVolume ? (
|
||||||
|
<input
|
||||||
|
ref={volumeInputRef}
|
||||||
|
className="volume-label volume-label-input"
|
||||||
|
type="text"
|
||||||
|
value={volumeInputText}
|
||||||
|
onChange={(e) => setVolumeInputText(e.target.value)}
|
||||||
|
onKeyDown={handleVolumeLabelKeyDown}
|
||||||
|
onBlur={commitVolumeLabelInput}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className="volume-label volume-label-clickable"
|
||||||
|
title="Click to enter dB value"
|
||||||
|
onClick={handleVolumeLabelClick}
|
||||||
|
>
|
||||||
|
{formatDb(volume)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ export const KEY_SIGNATURE_MAP = {
|
|||||||
|
|
||||||
export const AUDIO_INTERFACE_CONSTANTS = {
|
export const AUDIO_INTERFACE_CONSTANTS = {
|
||||||
DEFAULT_MASTER_VOLUME: 0.8,
|
DEFAULT_MASTER_VOLUME: 0.8,
|
||||||
DEFAULT_TRACK_VOLUME: 0.8,
|
DEFAULT_TRACK_VOLUME: 0, // 0 dB (unity gain)
|
||||||
|
MIN_TRACK_VOLUME_DB: -60, // practical floor; displayed as −∞
|
||||||
|
MAX_TRACK_VOLUME_DB: 12,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SAMPLER_CONSTANTS = {
|
export const SAMPLER_CONSTANTS = {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export class KGProject {
|
|||||||
@WithDefault(0)
|
@WithDefault(0)
|
||||||
private projectStructureVersion: number = 0;
|
private projectStructureVersion: number = 0;
|
||||||
|
|
||||||
public static readonly CURRENT_PROJECT_STRUCTURE_VERSION: number = 6;
|
public static readonly CURRENT_PROJECT_STRUCTURE_VERSION: number = 7;
|
||||||
|
|
||||||
@Expose()
|
@Expose()
|
||||||
@Type(() => KGTrack, {
|
@Type(() => KGTrack, {
|
||||||
|
|||||||
@@ -283,9 +283,8 @@ export class KGAudioBus {
|
|||||||
*/
|
*/
|
||||||
private updateSamplerVolume(): void {
|
private updateSamplerVolume(): void {
|
||||||
try {
|
try {
|
||||||
const effectiveVolume = this.muted ? 0 : this.volume;
|
const isSilent = this.muted || this.volume <= AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB;
|
||||||
const volumeDb = effectiveVolume > 0 ? 20 * Math.log10(effectiveVolume) : -Infinity;
|
this.sampler.volume.value = isSilent ? -Infinity : this.volume;
|
||||||
this.sampler.volume.value = volumeDb;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error updating volume for ${this.instrument}:`, error);
|
console.error(`Error updating volume for ${this.instrument}:`, error);
|
||||||
}
|
}
|
||||||
@@ -297,14 +296,8 @@ export class KGAudioBus {
|
|||||||
*/
|
*/
|
||||||
public applyEffectiveVolume(hasSoloedTracks: boolean): void {
|
public applyEffectiveVolume(hasSoloedTracks: boolean): void {
|
||||||
try {
|
try {
|
||||||
let effectiveVolume = this.volume;
|
const isSilent = this.muted || (hasSoloedTracks && !this.solo) || this.volume <= AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB;
|
||||||
if (this.muted) {
|
this.sampler.volume.value = isSilent ? -Infinity : this.volume;
|
||||||
effectiveVolume = 0;
|
|
||||||
} else if (hasSoloedTracks && !this.solo) {
|
|
||||||
effectiveVolume = 0;
|
|
||||||
}
|
|
||||||
const volumeDb = effectiveVolume > 0 ? 20 * Math.log10(effectiveVolume) : -Infinity;
|
|
||||||
this.sampler.volume.value = volumeDb;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error applying effective volume for ${this.instrument}:`, error);
|
console.error(`Error applying effective volume for ${this.instrument}:`, error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,14 +200,8 @@ export class KGAudioPlayerBus {
|
|||||||
*/
|
*/
|
||||||
public applyEffectiveVolume(hasSoloedTracks: boolean): void {
|
public applyEffectiveVolume(hasSoloedTracks: boolean): void {
|
||||||
try {
|
try {
|
||||||
let effectiveVolume = this.volume;
|
const isSilent = this.muted || (hasSoloedTracks && !this.solo) || this.volume <= AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB;
|
||||||
if (this.muted) {
|
this.gainNode.gain.value = isSilent ? 0 : Math.pow(10, this.volume / 20);
|
||||||
effectiveVolume = 0;
|
|
||||||
} else if (hasSoloedTracks && !this.solo) {
|
|
||||||
effectiveVolume = 0;
|
|
||||||
}
|
|
||||||
const volumeDb = effectiveVolume > 0 ? 20 * Math.log10(effectiveVolume) : -Infinity;
|
|
||||||
this.gainNode.gain.value = Math.pow(10, volumeDb / 20);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error applying effective volume for audio player bus:', error);
|
console.error('Error applying effective volume for audio player bus:', error);
|
||||||
}
|
}
|
||||||
@@ -266,9 +260,8 @@ export class KGAudioPlayerBus {
|
|||||||
|
|
||||||
private updateGainVolume(): void {
|
private updateGainVolume(): void {
|
||||||
try {
|
try {
|
||||||
const effectiveVolume = this.muted ? 0 : this.volume;
|
const isSilent = this.muted || this.volume <= AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB;
|
||||||
// Convert linear volume to gain value
|
this.gainNode.gain.value = isSilent ? 0 : Math.pow(10, this.volume / 20);
|
||||||
this.gainNode.gain.value = effectiveVolume;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating gain volume:', error);
|
console.error('Error updating gain volume:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { upgradeToV3 } from './upgradeToV3';
|
|||||||
import { upgradeToV4 } from './upgradeToV4';
|
import { upgradeToV4 } from './upgradeToV4';
|
||||||
import { upgradeToV5 } from './upgradeToV5';
|
import { upgradeToV5 } from './upgradeToV5';
|
||||||
import { upgradeToV6 } from './upgradeToV6';
|
import { upgradeToV6 } from './upgradeToV6';
|
||||||
|
import { upgradeToV7 } from './upgradeToV7';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upgrade the given project to the latest structure version, one version at a time.
|
* Upgrade the given project to the latest structure version, one version at a time.
|
||||||
@@ -48,6 +49,10 @@ export function upgradeProjectToLatest(project: KGProject): KGProject {
|
|||||||
workingProject = upgradeToV6(workingProject);
|
workingProject = upgradeToV6(workingProject);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 7: {
|
||||||
|
workingProject = upgradeToV7(workingProject);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
// If an upgrader is missing, throw to prevent loading incompatible structures
|
// If an upgrader is missing, throw to prevent loading incompatible structures
|
||||||
throw new Error(`No upgrader found for project structure version ${nextVersion}`);
|
throw new Error(`No upgrader found for project structure version ${nextVersion}`);
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { KGProject } from '../KGProject';
|
||||||
|
import { KGMidiTrack } from '../track/KGMidiTrack';
|
||||||
|
import { upgradeToV7 } from './upgradeToV7';
|
||||||
|
import { AUDIO_INTERFACE_CONSTANTS } from '../../constants/coreConstants';
|
||||||
|
|
||||||
|
function makeProject(volumes: number[]): KGProject {
|
||||||
|
const tracks = volumes.map((v, i) => {
|
||||||
|
const track = new KGMidiTrack(`Track ${i}`, i);
|
||||||
|
// Bypass setVolume clamping to simulate old 0–1 linear values stored in JSON
|
||||||
|
(track as unknown as { volume: number }).volume = v;
|
||||||
|
return track;
|
||||||
|
});
|
||||||
|
const project = new KGProject('Test', 32, 0, 125, undefined, undefined, undefined, undefined, undefined, undefined, tracks, 6);
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('upgradeToV7', () => {
|
||||||
|
it('converts 1.0 linear to 0 dB', () => {
|
||||||
|
const project = makeProject([1.0]);
|
||||||
|
upgradeToV7(project);
|
||||||
|
expect(project.getTracks()[0].getVolume()).toBeCloseTo(0, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts 0.8 linear to ~−1.94 dB', () => {
|
||||||
|
const project = makeProject([0.8]);
|
||||||
|
upgradeToV7(project);
|
||||||
|
expect(project.getTracks()[0].getVolume()).toBeCloseTo(20 * Math.log10(0.8), 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts 0.5 linear to ~−6.02 dB', () => {
|
||||||
|
const project = makeProject([0.5]);
|
||||||
|
upgradeToV7(project);
|
||||||
|
expect(project.getTracks()[0].getVolume()).toBeCloseTo(-6.021, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts 0.0 linear to MIN_TRACK_VOLUME_DB', () => {
|
||||||
|
const project = makeProject([0.0]);
|
||||||
|
upgradeToV7(project);
|
||||||
|
expect(project.getTracks()[0].getVolume()).toBe(AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clamps values that exceed MAX_TRACK_VOLUME_DB', () => {
|
||||||
|
// linear > 1.0 would give positive dB; cap at +6
|
||||||
|
const project = makeProject([2.0]);
|
||||||
|
upgradeToV7(project);
|
||||||
|
expect(project.getTracks()[0].getVolume()).toBe(AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bumps project structure version to 7', () => {
|
||||||
|
const project = makeProject([1.0]);
|
||||||
|
upgradeToV7(project);
|
||||||
|
expect(project.getProjectStructureVersion()).toBe(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles multiple tracks independently', () => {
|
||||||
|
const project = makeProject([1.0, 0.5, 0.0]);
|
||||||
|
upgradeToV7(project);
|
||||||
|
const tracks = project.getTracks();
|
||||||
|
expect(tracks[0].getVolume()).toBeCloseTo(0, 5);
|
||||||
|
expect(tracks[1].getVolume()).toBeCloseTo(-6.021, 2);
|
||||||
|
expect(tracks[2].getVolume()).toBe(AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { KGProject } from '../KGProject';
|
||||||
|
import { AUDIO_INTERFACE_CONSTANTS } from '../../constants/coreConstants';
|
||||||
|
|
||||||
|
export function upgradeToV7(project: KGProject): KGProject {
|
||||||
|
try {
|
||||||
|
// Convert track volumes from 0–1 linear scale to dB
|
||||||
|
for (const track of project.getTracks()) {
|
||||||
|
const linear = track.getVolume();
|
||||||
|
let db: number;
|
||||||
|
if (linear <= 0) {
|
||||||
|
db = AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB;
|
||||||
|
} else {
|
||||||
|
db = 20 * Math.log10(linear);
|
||||||
|
db = Math.max(AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB,
|
||||||
|
Math.min(AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB, db));
|
||||||
|
}
|
||||||
|
track.setVolume(db);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
project.setProjectStructureVersion(7);
|
||||||
|
}
|
||||||
|
return project;
|
||||||
|
}
|
||||||
@@ -19,18 +19,18 @@ describe('KGMidiTrack', () => {
|
|||||||
expect(defaultTrack.getId()).toBe(0)
|
expect(defaultTrack.getId()).toBe(0)
|
||||||
expect(defaultTrack.getType()).toBe(TrackType.MIDI)
|
expect(defaultTrack.getType()).toBe(TrackType.MIDI)
|
||||||
expect(defaultTrack.getInstrument()).toBe('acoustic_grand_piano')
|
expect(defaultTrack.getInstrument()).toBe('acoustic_grand_piano')
|
||||||
expect(defaultTrack.getVolume()).toBe(0.8) // DEFAULT_TRACK_VOLUME
|
expect(defaultTrack.getVolume()).toBe(0) // DEFAULT_TRACK_VOLUME (0 dB)
|
||||||
expect(defaultTrack.getRegions()).toEqual([])
|
expect(defaultTrack.getRegions()).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should create track with custom parameters', () => {
|
it('should create track with custom parameters', () => {
|
||||||
const customTrack = new KGMidiTrack('My Piano Track', 5, 'electric_piano_1', 0.6)
|
const customTrack = new KGMidiTrack('My Piano Track', 5, 'electric_piano_1', -4)
|
||||||
|
|
||||||
expect(customTrack.getName()).toBe('My Piano Track')
|
expect(customTrack.getName()).toBe('My Piano Track')
|
||||||
expect(customTrack.getId()).toBe(5)
|
expect(customTrack.getId()).toBe(5)
|
||||||
expect(customTrack.getType()).toBe(TrackType.MIDI)
|
expect(customTrack.getType()).toBe(TrackType.MIDI)
|
||||||
expect(customTrack.getInstrument()).toBe('electric_piano_1')
|
expect(customTrack.getInstrument()).toBe('electric_piano_1')
|
||||||
expect(customTrack.getVolume()).toBe(0.6)
|
expect(customTrack.getVolume()).toBe(-4)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should set correct type identifier', () => {
|
it('should set correct type identifier', () => {
|
||||||
@@ -193,33 +193,33 @@ describe('KGMidiTrack', () => {
|
|||||||
|
|
||||||
describe('inheritance from KGTrack', () => {
|
describe('inheritance from KGTrack', () => {
|
||||||
it('should inherit all base track properties', () => {
|
it('should inherit all base track properties', () => {
|
||||||
const customTrack = new KGMidiTrack('Test Track', 42, 'violin', 0.9)
|
const customTrack = new KGMidiTrack('Test Track', 42, 'violin', -1)
|
||||||
|
|
||||||
expect(customTrack.getName()).toBe('Test Track')
|
expect(customTrack.getName()).toBe('Test Track')
|
||||||
expect(customTrack.getId()).toBe(42)
|
expect(customTrack.getId()).toBe(42)
|
||||||
expect(customTrack.getType()).toBe(TrackType.MIDI)
|
expect(customTrack.getType()).toBe(TrackType.MIDI)
|
||||||
expect(customTrack.getVolume()).toBe(0.9)
|
expect(customTrack.getVolume()).toBe(-1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should inherit base track setters', () => {
|
it('should inherit base track setters', () => {
|
||||||
track.setName('Updated Track')
|
track.setName('Updated Track')
|
||||||
expect(track.getName()).toBe('Updated Track')
|
expect(track.getName()).toBe('Updated Track')
|
||||||
|
|
||||||
track.setVolume(0.5)
|
track.setVolume(-6)
|
||||||
expect(track.getVolume()).toBe(0.5)
|
expect(track.getVolume()).toBe(-6)
|
||||||
|
|
||||||
track.setTrackIndex(3)
|
track.setTrackIndex(3)
|
||||||
expect(track.getTrackIndex()).toBe(3)
|
expect(track.getTrackIndex()).toBe(3)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should inherit volume controls', () => {
|
it('should inherit volume controls', () => {
|
||||||
expect(track.getVolume()).toBe(0.8) // Default volume
|
expect(track.getVolume()).toBe(0) // Default volume (0 dB)
|
||||||
|
|
||||||
track.setVolume(0.5)
|
track.setVolume(-6)
|
||||||
expect(track.getVolume()).toBe(0.5)
|
expect(track.getVolume()).toBe(-6)
|
||||||
|
|
||||||
track.setVolume(1.0)
|
track.setVolume(0)
|
||||||
expect(track.getVolume()).toBe(1.0)
|
expect(track.getVolume()).toBe(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -275,8 +275,8 @@ describe('KGMidiTrack', () => {
|
|||||||
// Setup initial state
|
// Setup initial state
|
||||||
track.setName('Piano Track')
|
track.setName('Piano Track')
|
||||||
track.setInstrument('acoustic_grand_piano')
|
track.setInstrument('acoustic_grand_piano')
|
||||||
track.setVolume(0.7)
|
track.setVolume(-3)
|
||||||
|
|
||||||
const regions = [
|
const regions = [
|
||||||
createMockMidiRegion({ id: 'r1', trackId: '0', name: 'Intro' }),
|
createMockMidiRegion({ id: 'r1', trackId: '0', name: 'Intro' }),
|
||||||
createMockMidiRegion({ id: 'r2', trackId: '0', name: 'Verse' })
|
createMockMidiRegion({ id: 'r2', trackId: '0', name: 'Verse' })
|
||||||
@@ -286,21 +286,21 @@ describe('KGMidiTrack', () => {
|
|||||||
// Verify initial state
|
// Verify initial state
|
||||||
expect(track.getName()).toBe('Piano Track')
|
expect(track.getName()).toBe('Piano Track')
|
||||||
expect(track.getInstrument()).toBe('acoustic_grand_piano')
|
expect(track.getInstrument()).toBe('acoustic_grand_piano')
|
||||||
expect(track.getVolume()).toBe(0.7)
|
expect(track.getVolume()).toBe(-3)
|
||||||
expect(track.getRegions()).toHaveLength(2)
|
expect(track.getRegions()).toHaveLength(2)
|
||||||
|
|
||||||
// Modify state
|
// Modify state
|
||||||
track.setInstrument('electric_piano_1')
|
track.setInstrument('electric_piano_1')
|
||||||
track.addRegion(createMockMidiRegion({
|
track.addRegion(createMockMidiRegion({
|
||||||
id: 'r3',
|
id: 'r3',
|
||||||
trackId: '0',
|
trackId: '0',
|
||||||
name: 'Chorus'
|
name: 'Chorus'
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Verify modified state
|
// Verify modified state
|
||||||
expect(track.getName()).toBe('Piano Track')
|
expect(track.getName()).toBe('Piano Track')
|
||||||
expect(track.getInstrument()).toBe('electric_piano_1')
|
expect(track.getInstrument()).toBe('electric_piano_1')
|
||||||
expect(track.getVolume()).toBe(0.7)
|
expect(track.getVolume()).toBe(-3)
|
||||||
expect(track.getRegions()).toHaveLength(3)
|
expect(track.getRegions()).toHaveLength(3)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -337,15 +337,21 @@ describe('KGMidiTrack', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle volume boundaries', () => {
|
it('should handle volume boundaries', () => {
|
||||||
track.setVolume(0.0)
|
track.setVolume(-60)
|
||||||
expect(track.getVolume()).toBe(0.0)
|
expect(track.getVolume()).toBe(-60)
|
||||||
|
|
||||||
track.setVolume(1.0)
|
track.setVolume(0)
|
||||||
expect(track.getVolume()).toBe(1.0)
|
expect(track.getVolume()).toBe(0)
|
||||||
|
|
||||||
// Volume outside normal range (should still work)
|
track.setVolume(6)
|
||||||
track.setVolume(1.5)
|
expect(track.getVolume()).toBe(6)
|
||||||
expect(track.getVolume()).toBe(1.5)
|
|
||||||
|
// Volume outside valid dB range is clamped
|
||||||
|
track.setVolume(10)
|
||||||
|
expect(track.getVolume()).toBe(6)
|
||||||
|
|
||||||
|
track.setVolume(-100)
|
||||||
|
expect(track.getVolume()).toBe(-60)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle large number of regions', () => {
|
it('should handle large number of regions', () => {
|
||||||
|
|||||||
@@ -104,7 +104,10 @@ export class KGTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setVolume(volume: number): void {
|
public setVolume(volume: number): void {
|
||||||
this.volume = volume;
|
this.volume = Math.max(
|
||||||
|
AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB,
|
||||||
|
Math.min(AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB, volume)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setRegions(regions: KGRegion[]): void {
|
public setRegions(regions: KGRegion[]): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user