fix: linter errors

This commit is contained in:
Xiaohan-Tian
2026-05-24 23:47:45 -07:00
parent b80066c2a4
commit 2c84b48f63
12 changed files with 448 additions and 377 deletions
+1 -1
View File
@@ -65,7 +65,7 @@ function createFallbackDescriptor(value: string): ChordDescriptor {
} }
function normalizeDescriptor(descriptor: ChordDescriptor): ChordDescriptor { function normalizeDescriptor(descriptor: ChordDescriptor): ChordDescriptor {
let quality = descriptor.quality; const quality = descriptor.quality;
let extensions = [...descriptor.extensions]; let extensions = [...descriptor.extensions];
const dedupe = (nextExtensions: ChordExtension[]) => Array.from(new Set(nextExtensions)); const dedupe = (nextExtensions: ChordExtension[]) => Array.from(new Set(nextExtensions));
+2 -2
View File
@@ -79,11 +79,11 @@ function formatKGOneTabLabel(tab: Tab): string {
return 'Separator'; return 'Separator';
} }
export function getDefaultKGOneTab(mode: KGOneMode): Tab { function getDefaultKGOneTab(mode: KGOneMode): Tab {
return mode === 'local-separator' ? 'separator' : 'fullsong'; return mode === 'local-separator' ? 'separator' : 'fullsong';
} }
export function getKGOneMode(): KGOneMode { function getKGOneMode(): KGOneMode {
const enabled = (ConfigManager.instance().get('general.kgone.enabled') as boolean | undefined) ?? false; const enabled = (ConfigManager.instance().get('general.kgone.enabled') as boolean | undefined) ?? false;
return enabled ? 'server' : 'local-separator'; return enabled ? 'server' : 'local-separator';
} }
+1 -1
View File
@@ -1270,7 +1270,7 @@ const MainContent: React.FC<MainContentProps> = ({
} }
const sortedRegions = [...chordRegions] const sortedRegions = [...chordRegions]
.sort((left, right) => left.getStartFromBeat() - right.getStartFromBeat()) .sort((left, right) => left.getStartFromBeat() - right.getStartFromBeat());
const targetRegion = direction === 'forward' const targetRegion = direction === 'forward'
? sortedRegions.find(region => region.getStartFromBeat() > currentStartBeat && region.getStartFromBeat() <= targetBarBeat) ? sortedRegions.find(region => region.getStartFromBeat() > currentStartBeat && region.getStartFromBeat() <= targetBarBeat)
: [...sortedRegions] : [...sortedRegions]
+1 -1
View File
@@ -36,7 +36,7 @@ import {
getRegionStartScrollLeft, getRegionStartScrollLeft,
getRegionPlayheadRelation, getRegionPlayheadRelation,
getScrollLeftForViewportRequest, getScrollLeftForViewportRequest,
} from './PianoRoll'; } from './pianoRollViewport';
import type { SheetMeasureMetric } from './sheetNotationTypes'; import type { SheetMeasureMetric } from './sheetNotationTypes';
function createContainer({ clientWidth, scrollWidth }: { clientWidth: number; scrollWidth: number }): HTMLDivElement { function createContainer({ clientWidth, scrollWidth }: { clientWidth: number; scrollWidth: number }): HTMLDivElement {
+6 -261
View File
@@ -25,37 +25,12 @@ import {
import type { PianoRollAutomationType } from './pianoRollAutomation'; import type { PianoRollAutomationType } from './pianoRollAutomation';
import type { SheetMeasureMetric } from './sheetNotationTypes'; import type { SheetMeasureMetric } from './sheetNotationTypes';
import { getSheetPlayheadPixel, getSheetQuantizationOptions, parseSheetQuantization } from './sheetNotation'; import { getSheetPlayheadPixel, getSheetQuantizationOptions, parseSheetQuantization } from './sheetNotation';
import {
type RegionPlayheadRelation = 'before' | 'inside' | 'after'; createPendingModeSwitchRequest,
type ViewportSwitchAlignment = 'center' | 'region-start' | 'region-end'; getRegionStartScrollLeft,
type ViewportClampScope = 'region' | 'track'; getScrollLeftForViewportRequest,
type PendingModeSwitchRequest,
interface PendingModeSwitchRequest { } from './pianoRollViewport';
sourceSheetMusicViewEnabled: boolean;
destinationSheetMusicViewEnabled: boolean;
destinationSheetMusicTrackScopeEnabled: boolean;
alignment: ViewportSwitchAlignment;
anchorBeat: number;
clampScope: ViewportClampScope;
}
interface ModeSwitchRequestOptions {
playheadBeat: number;
regionStartBeat: number;
regionEndBeat: number;
sourceSheetMusicViewEnabled: boolean;
destinationSheetMusicViewEnabled: boolean;
destinationSheetMusicTrackScopeEnabled: boolean;
}
interface ScrollLeftForViewportRequestOptions {
request: PendingModeSwitchRequest;
container: HTMLDivElement;
sheetMeasureMetrics: SheetMeasureMetric[];
activeRegionStartBeat: number;
activeRegionEndBeat: number;
songEndBeat: number;
}
interface PianoRollProps { interface PianoRollProps {
onClose: () => void; onClose: () => void;
@@ -1389,233 +1364,3 @@ const PianoRoll: React.FC<PianoRollProps> = ({
}; };
export default PianoRoll; export default PianoRoll;
export function getRegionPlayheadRelation(
playheadBeat: number,
regionStartBeat: number,
regionEndBeat: number
): RegionPlayheadRelation {
if (playheadBeat < regionStartBeat) {
return 'before';
}
if (playheadBeat > regionEndBeat) {
return 'after';
}
return 'inside';
}
export function createPendingModeSwitchRequest({
playheadBeat,
regionStartBeat,
regionEndBeat,
sourceSheetMusicViewEnabled,
destinationSheetMusicViewEnabled,
destinationSheetMusicTrackScopeEnabled,
}: ModeSwitchRequestOptions): PendingModeSwitchRequest {
const relation = getRegionPlayheadRelation(playheadBeat, regionStartBeat, regionEndBeat);
const enteringTrackScopeSheet = (
!sourceSheetMusicViewEnabled &&
destinationSheetMusicViewEnabled &&
destinationSheetMusicTrackScopeEnabled
);
if (relation === 'inside') {
return {
sourceSheetMusicViewEnabled,
destinationSheetMusicViewEnabled,
destinationSheetMusicTrackScopeEnabled,
alignment: 'center',
anchorBeat: playheadBeat,
clampScope: destinationSheetMusicTrackScopeEnabled ? 'track' : 'region',
};
}
if (enteringTrackScopeSheet) {
return {
sourceSheetMusicViewEnabled,
destinationSheetMusicViewEnabled,
destinationSheetMusicTrackScopeEnabled,
alignment: 'center',
anchorBeat: playheadBeat,
clampScope: 'track',
};
}
return {
sourceSheetMusicViewEnabled,
destinationSheetMusicViewEnabled,
destinationSheetMusicTrackScopeEnabled,
alignment: relation === 'before' ? 'region-start' : 'region-end',
anchorBeat: relation === 'before' ? regionStartBeat : regionEndBeat,
clampScope: 'region',
};
}
function getHorizontalViewportMetrics(container: HTMLDivElement, sheetMusicViewEnabled: boolean): {
visibleWidth: number;
keysWidth: number;
} {
const keysWidth = sheetMusicViewEnabled
? 0
: (parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-width')
) || 60);
return {
visibleWidth: Math.max(0, container.clientWidth - keysWidth),
keysWidth,
};
}
function getPixelForAbsoluteBeat(
beat: number,
sheetMusicViewEnabled: boolean,
sheetMusicTrackScopeEnabled: boolean,
sheetMeasureMetrics: SheetMeasureMetric[],
activeRegionStartBeat: number
): number {
if (sheetMusicViewEnabled) {
return getSheetPlayheadPixel(
sheetMusicTrackScopeEnabled
? Math.max(0, beat)
: Math.max(0, beat - activeRegionStartBeat),
sheetMeasureMetrics
);
}
const beatWidth = parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')
) || 40;
return beat * beatWidth;
}
function getScopeBoundsInPixels(
request: PendingModeSwitchRequest,
sheetMeasureMetrics: SheetMeasureMetric[],
activeRegionStartBeat: number,
activeRegionEndBeat: number,
songEndBeat: number
): { startPx: number; endPx: number } {
if (request.destinationSheetMusicViewEnabled) {
if (request.clampScope === 'track') {
return {
startPx: getSheetPlayheadPixel(0, sheetMeasureMetrics),
endPx: getSheetPlayheadPixel(songEndBeat, sheetMeasureMetrics),
};
}
return {
startPx: getSheetPlayheadPixel(0, sheetMeasureMetrics),
endPx: getSheetPlayheadPixel(activeRegionEndBeat - activeRegionStartBeat, sheetMeasureMetrics),
};
}
const beatWidth = parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')
) || 40;
if (request.clampScope === 'track') {
return {
startPx: 0,
endPx: songEndBeat * beatWidth,
};
}
return {
startPx: activeRegionStartBeat * beatWidth,
endPx: activeRegionEndBeat * beatWidth,
};
}
function clampScrollLeftToContainer(container: HTMLDivElement, scrollLeft: number): number {
return Math.max(0, Math.min(scrollLeft, container.scrollWidth - container.clientWidth));
}
function getCenteredScrollLeft({
pixelPosition,
visibleWidth,
scopeStartPx,
scopeEndPx,
container,
}: {
pixelPosition: number;
visibleWidth: number;
scopeStartPx: number;
scopeEndPx: number;
container: HTMLDivElement;
}): number {
const unclamped = pixelPosition - visibleWidth / 2;
const maxScopeScrollLeft = Math.max(scopeStartPx, scopeEndPx - visibleWidth);
const clampedToScope = Math.max(scopeStartPx, Math.min(unclamped, maxScopeScrollLeft));
return clampScrollLeftToContainer(container, clampedToScope);
}
function getRegionEndAlignedScrollLeft({
visibleWidth,
scopeEndPx,
container,
}: {
visibleWidth: number;
scopeEndPx: number;
container: HTMLDivElement;
}): number {
return clampScrollLeftToContainer(container, Math.max(0, scopeEndPx - visibleWidth));
}
export function getScrollLeftForViewportRequest({
request,
container,
sheetMeasureMetrics,
activeRegionStartBeat,
activeRegionEndBeat,
songEndBeat,
}: ScrollLeftForViewportRequestOptions): number {
const { visibleWidth } = getHorizontalViewportMetrics(
container,
request.destinationSheetMusicViewEnabled
);
const pixelPosition = getPixelForAbsoluteBeat(
request.anchorBeat,
request.destinationSheetMusicViewEnabled,
request.destinationSheetMusicTrackScopeEnabled,
sheetMeasureMetrics,
activeRegionStartBeat
);
const { startPx, endPx } = getScopeBoundsInPixels(
request,
sheetMeasureMetrics,
activeRegionStartBeat,
activeRegionEndBeat,
songEndBeat
);
if (request.alignment === 'region-start') {
return clampScrollLeftToContainer(container, startPx);
}
if (request.alignment === 'region-end') {
return getRegionEndAlignedScrollLeft({
visibleWidth,
scopeEndPx: endPx,
container,
});
}
return getCenteredScrollLeft({
pixelPosition,
visibleWidth,
scopeStartPx: startPx,
scopeEndPx: endPx,
container,
});
}
export function getRegionStartScrollLeft(startBeat: number): number {
const beatWidth = parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')
) || TOOLBAR_CONSTANTS.BASE_BAR_WIDTH;
return Math.max(0, startBeat * beatWidth);
}
@@ -613,7 +613,7 @@ const PianoRollAutomationLane: React.FC<PianoRollAutomationLaneProps> = ({
return; return;
} }
let nextSelection = lassoShiftKeyRef.current ? new Set(selectedPointIdSet) : new Set<string>(); const nextSelection = lassoShiftKeyRef.current ? new Set(selectedPointIdSet) : new Set<string>();
renderedPointsSorted.forEach(point => { renderedPointsSorted.forEach(point => {
const isIntersecting = ( const isIntersecting = (
@@ -74,8 +74,8 @@ vi.mock('vexflow', () => {
return this; return this;
} }
addKeySignature() { addKeySignature(...args: unknown[]) {
vexflowMocks.addKeySignatureMock(...arguments); vexflowMocks.addKeySignatureMock(...args);
return this; return this;
} }
@@ -0,0 +1,264 @@
import { TOOLBAR_CONSTANTS } from '../../constants';
import type { SheetMeasureMetric } from './sheetNotationTypes';
import { getSheetPlayheadPixel } from './sheetNotation';
export type RegionPlayheadRelation = 'before' | 'inside' | 'after';
type ViewportSwitchAlignment = 'center' | 'region-start' | 'region-end';
type ViewportClampScope = 'region' | 'track';
export interface PendingModeSwitchRequest {
sourceSheetMusicViewEnabled: boolean;
destinationSheetMusicViewEnabled: boolean;
destinationSheetMusicTrackScopeEnabled: boolean;
alignment: ViewportSwitchAlignment;
anchorBeat: number;
clampScope: ViewportClampScope;
}
export interface ModeSwitchRequestOptions {
playheadBeat: number;
regionStartBeat: number;
regionEndBeat: number;
sourceSheetMusicViewEnabled: boolean;
destinationSheetMusicViewEnabled: boolean;
destinationSheetMusicTrackScopeEnabled: boolean;
}
export interface ScrollLeftForViewportRequestOptions {
request: PendingModeSwitchRequest;
container: HTMLDivElement;
sheetMeasureMetrics: SheetMeasureMetric[];
activeRegionStartBeat: number;
activeRegionEndBeat: number;
songEndBeat: number;
}
export function getRegionPlayheadRelation(
playheadBeat: number,
regionStartBeat: number,
regionEndBeat: number
): RegionPlayheadRelation {
if (playheadBeat < regionStartBeat) {
return 'before';
}
if (playheadBeat > regionEndBeat) {
return 'after';
}
return 'inside';
}
export function createPendingModeSwitchRequest({
playheadBeat,
regionStartBeat,
regionEndBeat,
sourceSheetMusicViewEnabled,
destinationSheetMusicViewEnabled,
destinationSheetMusicTrackScopeEnabled,
}: ModeSwitchRequestOptions): PendingModeSwitchRequest {
const relation = getRegionPlayheadRelation(playheadBeat, regionStartBeat, regionEndBeat);
const enteringTrackScopeSheet = (
!sourceSheetMusicViewEnabled &&
destinationSheetMusicViewEnabled &&
destinationSheetMusicTrackScopeEnabled
);
if (relation === 'inside') {
return {
sourceSheetMusicViewEnabled,
destinationSheetMusicViewEnabled,
destinationSheetMusicTrackScopeEnabled,
alignment: 'center',
anchorBeat: playheadBeat,
clampScope: destinationSheetMusicTrackScopeEnabled ? 'track' : 'region',
};
}
if (enteringTrackScopeSheet) {
return {
sourceSheetMusicViewEnabled,
destinationSheetMusicViewEnabled,
destinationSheetMusicTrackScopeEnabled,
alignment: 'center',
anchorBeat: playheadBeat,
clampScope: 'track',
};
}
return {
sourceSheetMusicViewEnabled,
destinationSheetMusicViewEnabled,
destinationSheetMusicTrackScopeEnabled,
alignment: relation === 'before' ? 'region-start' : 'region-end',
anchorBeat: relation === 'before' ? regionStartBeat : regionEndBeat,
clampScope: 'region',
};
}
function getHorizontalViewportMetrics(container: HTMLDivElement, sheetMusicViewEnabled: boolean): {
visibleWidth: number;
keysWidth: number;
} {
const keysWidth = sheetMusicViewEnabled
? 0
: (parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-width')
) || 60);
return {
visibleWidth: Math.max(0, container.clientWidth - keysWidth),
keysWidth,
};
}
function getPixelForAbsoluteBeat(
beat: number,
sheetMusicViewEnabled: boolean,
sheetMusicTrackScopeEnabled: boolean,
sheetMeasureMetrics: SheetMeasureMetric[],
activeRegionStartBeat: number
): number {
if (sheetMusicViewEnabled) {
return getSheetPlayheadPixel(
sheetMusicTrackScopeEnabled
? Math.max(0, beat)
: Math.max(0, beat - activeRegionStartBeat),
sheetMeasureMetrics
);
}
const beatWidth = parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')
) || 40;
return beat * beatWidth;
}
function getScopeBoundsInPixels(
request: PendingModeSwitchRequest,
sheetMeasureMetrics: SheetMeasureMetric[],
activeRegionStartBeat: number,
activeRegionEndBeat: number,
songEndBeat: number
): { startPx: number; endPx: number } {
if (request.destinationSheetMusicViewEnabled) {
if (request.clampScope === 'track') {
return {
startPx: getSheetPlayheadPixel(0, sheetMeasureMetrics),
endPx: getSheetPlayheadPixel(songEndBeat, sheetMeasureMetrics),
};
}
return {
startPx: getSheetPlayheadPixel(0, sheetMeasureMetrics),
endPx: getSheetPlayheadPixel(activeRegionEndBeat - activeRegionStartBeat, sheetMeasureMetrics),
};
}
const beatWidth = parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')
) || 40;
if (request.clampScope === 'track') {
return {
startPx: 0,
endPx: songEndBeat * beatWidth,
};
}
return {
startPx: activeRegionStartBeat * beatWidth,
endPx: activeRegionEndBeat * beatWidth,
};
}
function clampScrollLeftToContainer(container: HTMLDivElement, scrollLeft: number): number {
return Math.max(0, Math.min(scrollLeft, container.scrollWidth - container.clientWidth));
}
function getCenteredScrollLeft({
pixelPosition,
visibleWidth,
scopeStartPx,
scopeEndPx,
container,
}: {
pixelPosition: number;
visibleWidth: number;
scopeStartPx: number;
scopeEndPx: number;
container: HTMLDivElement;
}): number {
const unclamped = pixelPosition - visibleWidth / 2;
const maxScopeScrollLeft = Math.max(scopeStartPx, scopeEndPx - visibleWidth);
const clampedToScope = Math.max(scopeStartPx, Math.min(unclamped, maxScopeScrollLeft));
return clampScrollLeftToContainer(container, clampedToScope);
}
function getRegionEndAlignedScrollLeft({
visibleWidth,
scopeEndPx,
container,
}: {
visibleWidth: number;
scopeEndPx: number;
container: HTMLDivElement;
}): number {
return clampScrollLeftToContainer(container, Math.max(0, scopeEndPx - visibleWidth));
}
export function getScrollLeftForViewportRequest({
request,
container,
sheetMeasureMetrics,
activeRegionStartBeat,
activeRegionEndBeat,
songEndBeat,
}: ScrollLeftForViewportRequestOptions): number {
const { visibleWidth } = getHorizontalViewportMetrics(
container,
request.destinationSheetMusicViewEnabled
);
const pixelPosition = getPixelForAbsoluteBeat(
request.anchorBeat,
request.destinationSheetMusicViewEnabled,
request.destinationSheetMusicTrackScopeEnabled,
sheetMeasureMetrics,
activeRegionStartBeat
);
const { startPx, endPx } = getScopeBoundsInPixels(
request,
sheetMeasureMetrics,
activeRegionStartBeat,
activeRegionEndBeat,
songEndBeat
);
if (request.alignment === 'region-start') {
return clampScrollLeftToContainer(container, startPx);
}
if (request.alignment === 'region-end') {
return getRegionEndAlignedScrollLeft({
visibleWidth,
scopeEndPx: endPx,
container,
});
}
return getCenteredScrollLeft({
pixelPosition,
visibleWidth,
scopeStartPx: startPx,
scopeEndPx: endPx,
container,
});
}
export function getRegionStartScrollLeft(startBeat: number): number {
const beatWidth = parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')
) || TOOLBAR_CONSTANTS.BASE_BAR_WIDTH;
return Math.max(0, startBeat * beatWidth);
}
@@ -15,7 +15,7 @@ vi.mock('../../stores/projectStore', () => ({
}), }),
})); }));
const { __trackAutomationTestUtils } = await import('./TrackAutomationLane'); const __trackAutomationTestUtils = await import('./trackAutomationLaneUtils');
describe('TrackAutomationLane mapping', () => { describe('TrackAutomationLane mapping', () => {
it('maps volume 0.0dB to the visual midpoint and back', () => { it('maps volume 0.0dB to the visual midpoint and back', () => {
+9 -101
View File
@@ -11,6 +11,14 @@ import { PIANO_ROLL_CONSTANTS } from '../../constants';
import { AUDIO_INTERFACE_CONSTANTS } from '../../constants/coreConstants'; import { AUDIO_INTERFACE_CONSTANTS } from '../../constants/coreConstants';
import { isModifierKeyPressed } from '../../util/osUtil'; import { isModifierKeyPressed } from '../../util/osUtil';
import { KGMainContentState } from '../../core/state/KGMainContentState'; import { KGMainContentState } from '../../core/state/KGMainContentState';
import {
formatAutomationValue,
getLaneMetrics,
volumeToY,
yToVolume,
panToY,
yToPan,
} from './trackAutomationLaneUtils';
interface TrackAutomationLaneProps { interface TrackAutomationLaneProps {
track: KGTrack; track: KGTrack;
@@ -32,7 +40,6 @@ interface PreviewPoint {
value: number; value: number;
} }
const LANE_PADDING_Y = 12;
const POINT_RADIUS = 5; const POINT_RADIUS = 5;
const SELECTED_POINT_COLOR = '#FFFFFF'; const SELECTED_POINT_COLOR = '#FFFFFF';
const TRACK_AUTOMATION_COLORS: Record<TrackAutomationType, string> = { const TRACK_AUTOMATION_COLORS: Record<TrackAutomationType, string> = {
@@ -40,105 +47,6 @@ const TRACK_AUTOMATION_COLORS: Record<TrackAutomationType, string> = {
pan: '#90EE90', pan: '#90EE90',
}; };
function formatAutomationValue(automationType: TrackAutomationType, value: number): string {
if (automationType === 'volume') {
if (value <= AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB) {
return '−∞';
}
return `${value >= 0 ? '+' : ''}${value.toFixed(1)}dB`;
}
const logicPanValue = value <= 0
? Math.round(value * 64)
: Math.round(value * 63);
return `${logicPanValue >= 0 ? '+' : ''}${logicPanValue}`;
}
function getLaneMetrics(laneHeight: number) {
const top = LANE_PADDING_Y;
const bottom = laneHeight - LANE_PADDING_Y;
const middle = Math.round((top + bottom) / 2);
return { top, middle, bottom };
}
function volumeToY(value: number, laneHeight: number): number {
const clamped = Math.max(
AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB,
Math.min(AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB, value)
);
const { top, middle, bottom } = getLaneMetrics(laneHeight);
if (clamped >= 0) {
const normalized = clamped / AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB;
return middle - (middle - top) * normalized;
}
const normalized = clamped / AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB;
return middle + (bottom - middle) * normalized;
}
function yToVolume(y: number, laneHeight: number): number {
const { top, middle, bottom } = getLaneMetrics(laneHeight);
const clampedY = Math.min(bottom, Math.max(top, y));
if (clampedY <= middle) {
const normalized = middle === top ? 0 : (middle - clampedY) / (middle - top);
return Math.min(
AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB,
Math.max(0, normalized * AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB)
);
}
const normalized = bottom === middle ? 0 : (clampedY - middle) / (bottom - middle);
return Math.max(
AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB,
Math.min(0, normalized * AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB)
);
}
function panToY(value: number, laneHeight: number): number {
const clamped = Math.max(-1, Math.min(1, value));
const { top, middle, bottom } = getLaneMetrics(laneHeight);
if (clamped === 0) {
return middle;
}
if (clamped > 0) {
return middle - (middle - top) * clamped;
}
return middle + (bottom - middle) * Math.abs(clamped);
}
function yToPan(y: number, laneHeight: number): number {
const { top, middle, bottom } = getLaneMetrics(laneHeight);
const clampedY = Math.min(bottom, Math.max(top, y));
// Reserve a full pixel around the midpoint for exact center.
if (Math.abs(clampedY - middle) <= 0.5) {
return 0;
}
if (clampedY < middle) {
const normalized = middle === top ? 0 : (middle - clampedY) / (middle - top);
return Math.max(0, Math.min(1, normalized));
}
const normalized = bottom === middle ? 0 : (clampedY - middle) / (bottom - middle);
return Math.max(-1, Math.min(0, -normalized));
}
export const __trackAutomationTestUtils = {
formatAutomationValue,
getLaneMetrics,
volumeToY,
yToVolume,
panToY,
yToPan,
};
const TrackAutomationLane: React.FC<TrackAutomationLaneProps> = ({ const TrackAutomationLane: React.FC<TrackAutomationLaneProps> = ({
track, track,
automationType, automationType,
@@ -467,7 +375,7 @@ const TrackAutomationLane: React.FC<TrackAutomationLaneProps> = ({
return; return;
} }
let nextSelection = lassoShiftKeyRef.current ? new Set(selectedPointIdSet) : new Set<string>(); const nextSelection = lassoShiftKeyRef.current ? new Set(selectedPointIdSet) : new Set<string>();
renderedPoints.forEach(point => { renderedPoints.forEach(point => {
const isIntersecting = ( const isIntersecting = (
point.x + POINT_RADIUS >= left && point.x + POINT_RADIUS >= left &&
@@ -0,0 +1,93 @@
import { AUDIO_INTERFACE_CONSTANTS } from '../../constants/coreConstants';
import type { TrackAutomationType } from '../../core/track/KGTrackAutomationPoint';
const LANE_PADDING_Y = 12;
export function formatAutomationValue(automationType: TrackAutomationType, value: number): string {
if (automationType === 'volume') {
if (value <= AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB) {
return '−∞';
}
return `${value >= 0 ? '+' : ''}${value.toFixed(1)}dB`;
}
const logicPanValue = value <= 0
? Math.round(value * 64)
: Math.round(value * 63);
return `${logicPanValue >= 0 ? '+' : ''}${logicPanValue}`;
}
export function getLaneMetrics(laneHeight: number) {
const top = LANE_PADDING_Y;
const bottom = laneHeight - LANE_PADDING_Y;
const middle = Math.round((top + bottom) / 2);
return { top, middle, bottom };
}
export function volumeToY(value: number, laneHeight: number): number {
const clamped = Math.max(
AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB,
Math.min(AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB, value)
);
const { top, middle, bottom } = getLaneMetrics(laneHeight);
if (clamped >= 0) {
const normalized = clamped / AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB;
return middle - (middle - top) * normalized;
}
const normalized = clamped / AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB;
return middle + (bottom - middle) * normalized;
}
export function yToVolume(y: number, laneHeight: number): number {
const { top, middle, bottom } = getLaneMetrics(laneHeight);
const clampedY = Math.min(bottom, Math.max(top, y));
if (clampedY <= middle) {
const normalized = middle === top ? 0 : (middle - clampedY) / (middle - top);
return Math.min(
AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB,
Math.max(0, normalized * AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB)
);
}
const normalized = bottom === middle ? 0 : (clampedY - middle) / (bottom - middle);
return Math.max(
AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB,
Math.min(0, normalized * AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB)
);
}
export function panToY(value: number, laneHeight: number): number {
const clamped = Math.max(-1, Math.min(1, value));
const { top, middle, bottom } = getLaneMetrics(laneHeight);
if (clamped === 0) {
return middle;
}
if (clamped > 0) {
return middle - (middle - top) * clamped;
}
return middle + (bottom - middle) * Math.abs(clamped);
}
export function yToPan(y: number, laneHeight: number): number {
const { top, middle, bottom } = getLaneMetrics(laneHeight);
const clampedY = Math.min(bottom, Math.max(top, y));
if (Math.abs(clampedY - middle) <= 0.5) {
return 0;
}
if (clampedY < middle) {
const normalized = middle === top ? 0 : (middle - clampedY) / (middle - top);
return Math.max(0, Math.min(1, normalized));
}
const normalized = bottom === middle ? 0 : (clampedY - middle) / (bottom - middle);
return Math.max(-1, Math.min(0, -normalized));
}
+67 -6
View File
@@ -10,12 +10,73 @@ function localSeparatorLog(message: string, payload?: unknown): void {
console.log(`[localSeparator] ${message}`, payload); console.log(`[localSeparator] ${message}`, payload);
} }
type GPUDeviceLike = any; interface GPUBufferLike {
type GPUBufferLike = any; destroy(): void;
type GPUComputePipelineLike = any; mapAsync(mode: number): Promise<void>;
getMappedRange(): ArrayBuffer;
unmap(): void;
}
declare const GPUBufferUsage: any; interface GPUComputePassEncoderLike {
declare const GPUMapMode: any; setPipeline(pipeline: GPUComputePipelineLike): void;
setBindGroup(index: number, bindGroup: unknown): void;
dispatchWorkgroups(x: number): void;
end(): void;
}
interface GPUCommandEncoderLike {
copyBufferToBuffer(source: GPUBufferLike, sourceOffset: number, destination: GPUBufferLike, destinationOffset: number, size: number): void;
beginComputePass(): GPUComputePassEncoderLike;
finish(): unknown;
}
interface GPUComputePipelineLike {
getBindGroupLayout(index: number): unknown;
}
interface GPUDeviceLike {
queue: {
submit(commands: unknown[]): void;
writeBuffer(buffer: GPUBufferLike, bufferOffset: number, data: BufferSource): void;
};
createBuffer(descriptor: { size: number; usage: number }): GPUBufferLike;
createCommandEncoder(): GPUCommandEncoderLike;
createBindGroup(descriptor: { layout: unknown; entries: Array<{ binding: number; resource: { buffer: GPUBufferLike } }> }): unknown;
createComputePipeline(descriptor: {
layout: 'auto';
compute: {
module: unknown;
entryPoint: string;
};
}): GPUComputePipelineLike;
createShaderModule(descriptor: { code: string }): unknown;
}
interface GPUAdapterLike {
features?: {
values?(): IterableIterator<unknown>;
};
limits?: unknown;
info?: unknown;
requestDevice(): Promise<GPUDeviceLike>;
}
interface NavigatorWithGpu {
gpu?: {
requestAdapter(options: { powerPreference: string }): Promise<GPUAdapterLike | null>;
};
}
declare const GPUBufferUsage: {
COPY_DST: number;
MAP_READ: number;
STORAGE: number;
COPY_SRC: number;
UNIFORM: number;
};
declare const GPUMapMode: {
READ: number;
};
const FRAMING_SHADER = ` const FRAMING_SHADER = `
struct Params { struct Params {
@@ -85,7 +146,7 @@ export class LocalSeparatorGpuDsp {
} }
localSeparatorLog('Requesting WebGPU adapter for GPU DSP.'); localSeparatorLog('Requesting WebGPU adapter for GPU DSP.');
const adapter = await (navigator as { gpu?: { requestAdapter: (options: { powerPreference: string }) => Promise<any> } }).gpu?.requestAdapter({ const adapter = await (navigator as NavigatorWithGpu).gpu?.requestAdapter({
powerPreference: 'high-performance', powerPreference: 'high-performance',
}); });
if (!adapter) { if (!adapter) {