-
Notifications
You must be signed in to change notification settings - Fork 7
feat: add integrated mixer panel in session view #992
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,76 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useCallback } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useProjectStore } from '../../store/projectStore'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { SessionMixerStrip } from './SessionMixerStrip'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface SessionMixerProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| visible: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onToggle: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function SessionMixer({ visible, onToggle }: SessionMixerProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const project = useProjectStore((s) => s.project); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const updateTrack = useProjectStore((s) => s.updateTrack); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleVolumeChange = useCallback((trackId: string, volume: number) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| updateTrack(trackId, { volume }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [updateTrack]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handlePanChange = useCallback((trackId: string, pan: number) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // updateTrackMixer handles pan updates | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useProjectStore.getState().updateTrackMixer(trackId, { pan }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleMuteToggle = useCallback((trackId: string, currentMuted: boolean) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| updateTrack(trackId, { muted: !currentMuted }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [updateTrack]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleSoloToggle = useCallback((trackId: string, currentSoloed: boolean) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| updateTrack(trackId, { soloed: !currentSoloed }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [updateTrack]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+23
to
+29
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!project) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tracks = [...project.tracks].sort((a, b) => a.order - b.order); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div data-testid="session-mixer"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Toggle button */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center justify-center border-t border-[#333]"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={onToggle} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="w-full py-1 text-[10px] uppercase tracking-[0.16em] text-zinc-500 hover:text-zinc-300 hover:bg-[#252525] transition-colors" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-label={visible ? 'Hide session mixer' : 'Show session mixer'} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title={visible ? 'Hide Mixer' : 'Show Mixer'} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {visible ? 'Hide Mixer' : 'Show Mixer'} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Mixer strips container */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="overflow-hidden transition-[height,opacity] duration-150 ease-out" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| height: visible ? `${tracks.length * 40}px` : '0px', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| opacity: visible ? 1 : 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {tracks.map((track) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <SessionMixerStrip | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key={track.id} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackId={track.id} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackName={track.displayName} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackColor={track.color} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| volume={track.volume} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pan={track.pan ?? 0} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| muted={track.muted} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| soloed={track.soloed} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onVolumeChange={(v) => handleVolumeChange(track.id, v)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onPanChange={(v) => handlePanChange(track.id, v)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onMuteToggle={() => handleMuteToggle(track.id, track.muted)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSoloToggle={() => handleSoloToggle(track.id, track.soloed)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+57
to
+72
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {tracks.map((track) => ( | |
| <SessionMixerStrip | |
| key={track.id} | |
| trackId={track.id} | |
| trackName={track.displayName} | |
| trackColor={track.color} | |
| volume={track.volume} | |
| pan={track.pan ?? 0} | |
| muted={track.muted} | |
| soloed={track.soloed} | |
| onVolumeChange={(v) => handleVolumeChange(track.id, v)} | |
| onPanChange={(v) => handlePanChange(track.id, v)} | |
| onMuteToggle={() => handleMuteToggle(track.id, track.muted)} | |
| onSoloToggle={() => handleSoloToggle(track.id, track.soloed)} | |
| /> | |
| ))} | |
| {visible && | |
| tracks.map((track) => ( | |
| <SessionMixerStrip | |
| key={track.id} | |
| trackId={track.id} | |
| trackName={track.displayName} | |
| trackColor={track.color} | |
| volume={track.volume} | |
| pan={track.pan ?? 0} | |
| muted={track.muted} | |
| soloed={track.soloed} | |
| onVolumeChange={(v) => handleVolumeChange(track.id, v)} | |
| onPanChange={(v) => handlePanChange(track.id, v)} | |
| onMuteToggle={() => handleMuteToggle(track.id, track.muted)} | |
| onSoloToggle={() => handleSoloToggle(track.id, track.soloed)} | |
| /> | |
| ))} |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,196 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useCallback, useEffect, useRef, useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getAudioEngine } from '../../hooks/useAudioEngine'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Knob } from '../ui/Knob'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Convert linear level (0..1+) to a 0..1 fill fraction mapping -60dB..0dB */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function levelToFill(linear: number): number { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (linear <= 0) return 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const db = 20 * Math.log10(linear); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Math.max(0, Math.min(1, (db + 60) / 60)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface SessionMixerStripProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackId: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackName: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackColor: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| volume: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pan: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| muted: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| soloed: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onVolumeChange?: (volume: number) => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onPanChange?: (pan: number) => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onMuteToggle?: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSoloToggle?: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function SessionMixerStrip({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackColor, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| volume, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pan, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| muted, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| soloed, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onVolumeChange, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onPanChange, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onMuteToggle, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSoloToggle, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: SessionMixerStripProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rafRef = useRef<number>(0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [leftFill, setLeftFill] = useState(0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [rightFill, setRightFill] = useState(0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const containerRef = useRef<HTMLDivElement>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dragging = useRef(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Animate meter levels | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const engine = getAudioEngine(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tick = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const meter = engine.getTrackMeter(trackId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setLeftFill(levelToFill(meter.leftLevel)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setRightFill(levelToFill(meter.rightLevel)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rafRef.current = requestAnimationFrame(tick); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rafRef.current = requestAnimationFrame(tick); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => cancelAnimationFrame(rafRef.current); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [trackId]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Fader drag logic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const getVolumeFromX = useCallback((clientX: number) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const el = containerRef.current; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!el) return volume; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rect = el.getBoundingClientRect(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ratio = (clientX - rect.left) / rect.width; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Math.max(0, Math.min(1, ratio)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [volume]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const onPointerDown = useCallback((e: React.PointerEvent) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dragging.current = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onVolumeChange?.(getVolumeFromX(e.clientX)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [getVolumeFromX, onVolumeChange]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const onPointerMove = useCallback((e: React.PointerEvent) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!dragging.current) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onVolumeChange?.(getVolumeFromX(e.clientX)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [getVolumeFromX, onVolumeChange]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const onPointerUp = useCallback(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dragging.current = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const onDoubleClick = useCallback(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onVolumeChange?.(0.8); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [onVolumeChange]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const faderPct = volume * 100; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="flex items-center gap-2 h-10 px-2 bg-[#1b1b1b] border-b border-[#2e2e2e]" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data-testid={`session-mixer-strip-${trackId}`} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Track color accent */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="w-1 h-6 rounded-sm shrink-0" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ backgroundColor: trackColor }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data-testid="track-color-accent" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Track name (truncated) */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-16 truncate text-[10px] text-zinc-400 shrink-0" title={trackName}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {trackName} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Volume fader with meter */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref={containerRef} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="relative flex-1 min-w-[80px] max-w-[160px] cursor-ew-resize select-none" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ height: '14px' }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onPointerDown={onPointerDown} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onPointerMove={onPointerMove} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onPointerUp={onPointerUp} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onDoubleClick={onDoubleClick} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title={`Volume: ${Math.round(volume * 100)}%`} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-label={`${trackName} volume`} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| role="slider" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-valuemin={0} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-valuemax={100} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-valuenow={Math.round(volume * 100)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-valuenow={Math.round(volume * 100)} | |
| aria-valuenow={Math.round(volume * 100)} | |
| aria-orientation="horizontal" | |
| tabIndex={0} | |
| onKeyDown={(e) => { | |
| const step = 0.05; | |
| const largeStep = 0.1; | |
| let newVolume: number | null = null; | |
| switch (e.key) { | |
| case 'ArrowLeft': | |
| case 'ArrowDown': | |
| newVolume = Math.max(0, volume - step); | |
| break; | |
| case 'ArrowRight': | |
| case 'ArrowUp': | |
| newVolume = Math.min(1, volume + step); | |
| break; | |
| case 'PageDown': | |
| newVolume = Math.max(0, volume - largeStep); | |
| break; | |
| case 'PageUp': | |
| newVolume = Math.min(1, volume + largeStep); | |
| break; | |
| case 'Home': | |
| newVolume = 0; | |
| break; | |
| case 'End': | |
| newVolume = 1; | |
| break; | |
| default: | |
| break; | |
| } | |
| if (newVolume !== null) { | |
| e.preventDefault(); | |
| // Prefer prop callback if available; otherwise fall back to direct state update if present. | |
| // eslint-disable-next-line @typescript-eslint/ban-ts-comment | |
| // @ts-ignore onVolumeChange is expected to be provided via props. | |
| onVolumeChange?.(newVolume); | |
| } | |
| }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handlePanChangereaches into the store viauseProjectStore.getState()and uses an empty dependency array. For consistency with the rest of the codebase (and to avoid bypassing React hooks patterns), selectupdateTrackMixerviauseProjectStore((s) => s.updateTrackMixer)and include it in the callback dependencies.