+
+ );
+}
+
+/** Uses the authored clip-gain scale; meter readings have a separate scale. */
+function Fader({
+ label,
+ title,
+ volume,
+ maxPosition,
+ onLive,
+ onCommit,
+}: {
+ label: string;
+ title: string;
+ volume: number;
+ maxPosition: number;
+ onLive: (v: number) => void;
+ onCommit: (v: number) => void;
+}) {
+ const trackRef = useRef
(null);
+ const draggingRef = useRef(null);
+ const position = audioGainToFaderPosition(volume);
+ const span = maxPosition - AUDIO_GAIN_FADER_MIN;
+ const fraction = (position - AUDIO_GAIN_FADER_MIN) / span;
+ const readout = audioGainToText(volume);
+
+ const positionAt = useCallback(
+ (clientY: number): number => {
+ const rect = trackRef.current?.getBoundingClientRect();
+ if (!rect || rect.height === 0) return position;
+ return (
+ AUDIO_GAIN_FADER_MIN + clampNumber(1 - (clientY - rect.top) / rect.height, 0, 1) * span
+ );
+ },
+ [position, span],
+ );
+
+ const moveTo = (clientY: number) => {
+ const gain = audioFaderPositionToGain(positionAt(clientY));
+ draggingRef.current = gain;
+ onLive(gain);
+ };
+ const finishDrag = () => {
+ if (draggingRef.current === null) return;
+ const gain = draggingRef.current;
+ draggingRef.current = null;
+ onCommit(gain);
+ };
+ const keyPositions = new Map([
+ ["ArrowUp", position + span * 0.02],
+ ["ArrowDown", position - span * 0.02],
+ ["PageUp", position + span * 0.1],
+ ["PageDown", position - span * 0.1],
+ ["Home", AUDIO_GAIN_FADER_MIN],
+ ["End", maxPosition],
+ ]);
+
+ return (
+ ) => {
+ e.currentTarget.setPointerCapture(e.pointerId);
+ moveTo(e.clientY);
+ }}
+ onPointerMove={(e: ReactPointerEvent
) => {
+ if (draggingRef.current === null) return;
+ moveTo(e.clientY);
+ }}
+ onPointerUp={finishDrag}
+ onPointerCancel={finishDrag}
+ onLostPointerCapture={finishDrag}
+ onKeyDown={(e) => {
+ const next = keyPositions.get(e.key);
+ if (next === undefined) return;
+ onCommit(audioFaderPositionToGain(clampNumber(next, AUDIO_GAIN_FADER_MIN, maxPosition)));
+ e.preventDefault();
+ }}
+ className="relative h-full w-2 shrink-0 cursor-ns-resize touch-none rounded-full bg-neutral-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-studio-accent"
+ >
+
);
@@ -139,9 +335,13 @@ function Bar({ fillRef, peakRef }: { fillRef: Ref; peakRef: Ref<
function MeterStrip({
strip,
register,
+ onLive,
+ onCommit,
}: {
strip: Strip;
register: (id: string | null, bars: StripBars | null) => void;
+ onLive: (id: string | null, volume: number) => void;
+ onCommit: (id: string | null, volume: number) => void;
}) {
const refs = [
useRef(null),
@@ -151,22 +351,28 @@ function MeterStrip({
] as const;
useEffect(() => {
register(strip.id, [
- { fill: refs[0].current, peak: refs[1].current },
- { fill: refs[2].current, peak: refs[3].current },
+ { mask: refs[0].current, peak: refs[1].current },
+ { mask: refs[2].current, peak: refs[3].current },
]);
return () => register(strip.id, null);
// refs are stable objects
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [strip.id, register]);
return (
-
+
-
-
-
+
onLive(strip.id, v)}
+ onCommit={(v) => onCommit(strip.id, v)}
+ />
+
{METER_DB_MARKS.map((db) => (
))}
+
+
+
+
{strip.label}
@@ -199,6 +409,7 @@ function MeterStripBody() {
if (b) bars.current.set(id, b);
else bars.current.delete(id);
}).current;
+ const { onLive, onCommit } = useVolumeHandlers();
useMeterLoop(strips, bars);
return (
{strips.map((strip) => (
-
+
))}
);
diff --git a/packages/studio/src/utils/audioMeterVisibility.ts b/packages/studio/src/utils/audioMeterVisibility.ts
index 3aa6002dff..215eff6c3d 100644
--- a/packages/studio/src/utils/audioMeterVisibility.ts
+++ b/packages/studio/src/utils/audioMeterVisibility.ts
@@ -1,12 +1,12 @@
import { create } from "zustand";
import { readStudioUiPreferences, writeStudioUiPreferences } from "./studioUiPreferences";
-/** Whether the audio meter strip is shown; persisted, on unless the user hid it. */
+/** Whether the audio meter strip is shown; persisted, off unless the user showed it. */
export const useAudioMetersVisible = create<{
visible: boolean;
setVisible: (visible: boolean) => void;
}>((set) => ({
- visible: readStudioUiPreferences().audioMetersVisible ?? true,
+ visible: readStudioUiPreferences().audioMetersVisible ?? false,
setVisible: (visible) => {
writeStudioUiPreferences({ audioMetersVisible: visible });
set({ visible });
diff --git a/packages/studio/src/utils/studioUiPreferences.ts b/packages/studio/src/utils/studioUiPreferences.ts
index 041df9c08e..ef384f21a0 100644
--- a/packages/studio/src/utils/studioUiPreferences.ts
+++ b/packages/studio/src/utils/studioUiPreferences.ts
@@ -25,7 +25,7 @@ export interface StudioUiPreferences {
snapToGrid?: boolean;
/** Timeline magnet: snap clip drags/trims/drops to playhead, clip edges, and beats. */
timelineSnapEnabled?: boolean;
- /** Audio level meters at the timeline's right edge; shown unless hidden here. */
+ /** Audio level meters at the timeline's right edge; hidden unless enabled here. */
audioMetersVisible?: boolean;
/** Keeps the main track gapless: deleting a clip closes the gap. Distinct
* from `timelineSnapEnabled` ("Magnet", drag/trim snapping). */