Skip to content

Commit c050684

Browse files
committed
fixed: false positioning in other playbackrate, loop function on dummywaverforms
1 parent fcf70c1 commit c050684

10 files changed

Lines changed: 849 additions & 353 deletions

CODEMAP.md

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,13 @@ Re-generate: `./scripts/generate-codemap.sh > CODEMAP.md`
303303
- 29:export function useResizablePanelHeight({
304304
- **deps**: ../services/db
305305

306+
### useSectionLoop.test.ts (84 lines)
307+
- **deps**: ../stores/useLoopStore
308+
309+
### useSectionLoop.ts (46 lines)
310+
- 25:export function useSectionLoop({
311+
- **deps**: ../stores/useLoopStore,../utils/songLoop
312+
306313
### useSetlistAdvance.ts (131 lines)
307314
- 27:export function useSetlistAdvance({ onPlay }: UseSetlistAdvanceOptions): SetlistAdvanceResult
308315
- **deps**: ../stores/useModeStore,../stores/useSetlistStore,../stores/useSongStore,../stores/useTabStore
@@ -315,11 +322,19 @@ Re-generate: `./scripts/generate-codemap.sh > CODEMAP.md`
315322
- 34:export function useSyncSession({
316323
- **deps**: ../services/syncEmitter,../../shared/syncProtocol,../stores/useSetlistStore,../stores/useSongStore,../stores/useSyncStore,../stores/useTabStore,../stores/useTempoStore,../types
317324

325+
### useWaveformInteraction.test.ts (185 lines)
326+
- **deps**: ../stores/useLoopStore,../stores/useSongStore,../types
327+
328+
### useWaveformInteraction.ts (167 lines)
329+
- 9:export interface WaveformDragState
330+
- 39:export function useWaveformInteraction({
331+
- **deps**: ../stores/useLoopStore,../stores/useSongStore,../types
332+
318333
## src/components/Layout
319334

320-
### AppShell.tsx (1272 lines)
321-
- 60:export default function AppShell()
322-
- **deps**: ../Controller/RemoteControlView,../../hooks/useActiveMarkerTracker,../../hooks/useAlphaSynthPlayback,../../hooks/useAudioFile,../../hooks/useControlCommandHandler,../../hooks/useCountIn,../../hooks/useDummyPlayback,../../hooks/useGpFile,../../hooks/useKeyboardShortcuts,../../hooks/useMetronome,../../hooks/useMidiInput,../../hooks/usePlayback,../../hooks/useSetlistAdvance,../../hooks/useSyncBroadcast,../../hooks/useSyncSession,../Markers/MarkerForm,../Player/CountInIndicator,../Player/CountInToggle,../Player/DummyWaveform,../Player/Looppopoverbutton,../Player/MetronomeSplitButton,../Player/MetronomeToggle,../Player/TempoControls,../Player/TempoIndicator,../Player/TransportControls,../Player/VolumeControl,../Player/WaveformPlayer,../../services/syncEmitter,../../services/tempoMap,../../../shared/syncProtocol,../../stores/useCountInStore,../../stores/useModeStore,../../stores/useSetlistStore,../../stores/useSongStore,../../stores/useSyncStore,../../stores/useTabStore,../../stores/useTempoStore,../../stores/useToastStore,../Tabs/GpMarkerImportDialog,../Tabs/NotationPanel,../Tabs/TabEditor,../Tabs/TabViewer,../Tools/StandaloneMetronome,../../types,../../utils/gpMarkerImport,../../utils/iconSizes
335+
### AppShell.tsx (1281 lines)
336+
- 61:export default function AppShell()
337+
- **deps**: ../Controller/RemoteControlView,../../hooks/useActiveMarkerTracker,../../hooks/useAlphaSynthPlayback,../../hooks/useAudioFile,../../hooks/useControlCommandHandler,../../hooks/useCountIn,../../hooks/useDummyPlayback,../../hooks/useGpFile,../../hooks/useKeyboardShortcuts,../../hooks/useMetronome,../../hooks/useMidiInput,../../hooks/usePlayback,../../hooks/useSectionLoop,../../hooks/useSetlistAdvance,../../hooks/useSyncBroadcast,../../hooks/useSyncSession,../Markers/MarkerForm,../Player/CountInIndicator,../Player/CountInToggle,../Player/DummyWaveform,../Player/Looppopoverbutton,../Player/MetronomeSplitButton,../Player/MetronomeToggle,../Player/TempoControls,../Player/TempoIndicator,../Player/TransportControls,../Player/VolumeControl,../Player/WaveformPlayer,../../services/syncEmitter,../../services/tempoMap,../../../shared/syncProtocol,../../stores/useCountInStore,../../stores/useModeStore,../../stores/useSetlistStore,../../stores/useSongStore,../../stores/useSyncStore,../../stores/useTabStore,../../stores/useTempoStore,../../stores/useToastStore,../Tabs/GpMarkerImportDialog,../Tabs/NotationPanel,../Tabs/TabEditor,../Tabs/TabViewer,../Tools/StandaloneMetronome,../../types,../../utils/gpMarkerImport,../../utils/iconSizes
323338

324339
### CreateDummySongDialog.tsx (265 lines)
325340
- 13:export function CreateDummySongDialog({ onClose }: CreateDummySongDialogProps)
@@ -386,18 +401,26 @@ Re-generate: `./scripts/generate-codemap.sh > CODEMAP.md`
386401
- 11:export function CountInToggle({ compact = false }: CountInToggleProps = {})
387402
- **deps**: ../../services/clickSoundGenerator,../../stores/useCountInStore,../../stores/useMetronomeStore,../../stores/useSongStore
388403

389-
### DummyWaveform.tsx (145 lines)
390-
- 16:export function DummyWaveform({ duration, currentTime, height = 96, onSeek }: DummyWaveformProps)
391-
- **deps**: ../../stores/useSongStore
404+
### DummyWaveform.tsx (124 lines)
405+
- 28:export function DummyWaveform({ duration, currentTime, height = 96, onSeek }: DummyWaveformProps)
406+
- **deps**: ../../hooks/useWaveformInteraction,../../stores/useLoopStore,../../stores/useSongStore
392407

393408
### LoopControls.tsx (136 lines)
394409
- 15:export function LoopControls({ songLoop }: LoopControlsProps)
395410
- **deps**: ../../stores/useLoopStore,../../utils/formatTime,../../utils/iconSizes
396411

412+
### LoopOverlay.tsx (127 lines)
413+
- 66:export function LoopOverlay({
414+
- **deps**: ../../hooks/useWaveformInteraction,../../types
415+
397416
### Looppopoverbutton.tsx (31 lines)
398417
- 13:export function LoopPopoverButton({ songLoop }: LoopPopoverButtonProps)
399418
- **deps**: ../Common/Popover
400419

420+
### MarkerOverlay.tsx (93 lines)
421+
- 21:export function MarkerOverlay({
422+
- **deps**: ../../hooks/useWaveformInteraction,../../types
423+
401424
### MetronomeSplitButton.tsx (198 lines)
402425
- 25:export function MetronomeSplitButton({
403426
- **deps**: ../Common/Popover,../../services/clickSoundGenerator,../../stores/useMetronomeStore,../../stores/useSongStore
@@ -422,9 +445,9 @@ Re-generate: `./scripts/generate-codemap.sh > CODEMAP.md`
422445
- 6:export function VolumeControl()
423446
- **deps**: ../../stores/useSongStore,../../utils/iconSizes
424447

425-
### WaveformPlayer.tsx (416 lines)
426-
- 25:export function WaveformPlayer({
427-
- **deps**: ../../stores/useLoopStore,../../stores/useSongStore,../../stores/useTempoStore,../../utils/songLoop
448+
### WaveformPlayer.tsx (193 lines)
449+
- 20:export function WaveformPlayer({
450+
- **deps**: ../../hooks/useWaveformInteraction,../../stores/useLoopStore,../../stores/useSongStore,../../stores/useTempoStore
428451

429452
### WaveformTimeline.tsx (70 lines)
430453
- 12:export function WaveformTimeline({ duration, currentTime }: WaveformTimelineProps)

src/components/Layout/AppShell.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { useCountInStore } from '../../stores/useCountInStore';
4545
import { CountInToggle } from '../Player/CountInToggle';
4646
import { CountInIndicator } from '../Player/CountInIndicator';
4747
import { useMetronome } from '../../hooks/useMetronome';
48+
import { useSectionLoop } from '../../hooks/useSectionLoop';
4849
import { MetronomeToggle } from '../Player/MetronomeToggle';
4950
import { MetronomeSplitButton } from '../Player/MetronomeSplitButton';
5051
import type { ControlCommand, SyncRole } from '../../../shared/syncProtocol';
@@ -359,6 +360,14 @@ const controlCommandRef = useRef<((cmd: ControlCommand) => void) | null>(null);
359360
};
360361
}, [canCountIn, startCountIn, handlePlayPause]);
361362

363+
// --- Section loop (shared across wavesurfer, alphaSynth and dummy clock) ---
364+
useSectionLoop({
365+
isPlaying: _isPlaying,
366+
currentTime: _currentTime,
367+
onSeek: handleSeekTo,
368+
enabled: !isViewer,
369+
});
370+
362371
// --- Metronome (continuous click during playback) ---
363372
// alphaSynth reports score time directly, so no sync offset applies there.
364373
// Audio playback reports media time, which the song's sync offset maps to

src/components/Player/DummyWaveform.tsx

Lines changed: 58 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1-
import { useRef, useCallback, useMemo } from 'react';
1+
import { useRef, useMemo } from 'react';
22
import { useSongStore } from '../../stores/useSongStore';
3+
import { useLoopStore } from '../../stores/useLoopStore';
34
import { WaveformTimeline } from './WaveformTimeline';
5+
import { LoopOverlay } from './LoopOverlay';
6+
import { MarkerOverlay } from './MarkerOverlay';
7+
import { useWaveformInteraction } from '../../hooks/useWaveformInteraction';
8+
9+
const PLACEHOLDER_BAR_COUNT = 120;
10+
11+
/** Deterministic pseudo-random bar height for visual variety */
12+
function placeholderBarHeight(index: number): number {
13+
return 20 + ((index * 7 + 13) % 60);
14+
}
415

516
interface DummyWaveformProps {
617
duration: number;
@@ -11,93 +22,50 @@ interface DummyWaveformProps {
1122

1223
/**
1324
* Static waveform placeholder for dummy songs (no audio file).
14-
* Displays marker overlays, a clickable seek area, and a playhead cursor.
25+
* Displays marker overlays, the loop region, a clickable seek area and a
26+
* playhead cursor. Pointer interaction is shared with WaveformPlayer.
1527
*/
1628
export function DummyWaveform({ duration, currentTime, height = 96, onSeek }: DummyWaveformProps) {
1729
const containerRef = useRef<HTMLDivElement>(null);
1830
const markersBySong = useSongStore((state) => state.markersBySong);
1931
const activeSongId = useSongStore((state) => state.activeSongId);
32+
const loop = useLoopStore((state) => state.loop);
33+
const loopEnabled = useLoopStore((state) => state.loopEnabled);
34+
const abMode = useLoopStore((state) => state.abMode);
35+
const abStart = useLoopStore((state) => state.abStart);
2036

2137
const markers = useMemo(
2238
() => (activeSongId ? (markersBySong[activeSongId] ?? []) : []),
2339
[markersBySong, activeSongId],
2440
);
2541

26-
const sortedMarkers = useMemo(
27-
() => [...markers].sort((a, b) => a.startTime - b.startTime),
28-
[markers],
29-
);
30-
31-
const handleClick = useCallback((e: React.MouseEvent) => {
32-
const el = containerRef.current;
33-
if (!el || !duration || !onSeek) return;
34-
const rect = el.getBoundingClientRect();
35-
const percent = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
36-
onSeek(percent * duration);
37-
}, [duration, onSeek]);
42+
const { drag, handleClick, handleMarkerMouseDown, handleLoopHandleMouseDown } =
43+
useWaveformInteraction({
44+
containerRef,
45+
duration,
46+
markers,
47+
onSeek,
48+
});
3849

3950
const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0;
4051

41-
// Marker overlays (simplified version of WaveformPlayer's rendering)
42-
const markerOverlays = sortedMarkers.map((marker, idx) => {
43-
const leftPercent = (marker.startTime / duration) * 100;
44-
const nextStart = sortedMarkers[idx + 1]?.startTime ?? duration;
45-
const widthPercent = ((nextStart - marker.startTime) / duration) * 100;
46-
47-
return (
48-
<div key={marker.id}>
49-
{/* Section background fill */}
50-
<div
51-
className='absolute top-0 h-full pointer-events-none'
52-
style={{
53-
left: `${leftPercent}%`,
54-
width: `${Math.max(0, widthPercent)}%`,
55-
backgroundColor: marker.color,
56-
opacity: 0.25,
57-
}}
58-
/>
59-
{/* Marker line + label */}
60-
<div
61-
className='absolute top-0 h-full pointer-events-none'
62-
style={{ left: `${leftPercent}%` }}
63-
>
64-
<div
65-
className='absolute top-0 h-full w-px'
66-
style={{ backgroundColor: marker.color }}
67-
/>
68-
<span
69-
className='absolute top-1 left-2 text-xs font-mono whitespace-nowrap
70-
px-1.5 py-0.5 rounded'
71-
style={{ backgroundColor: marker.color, color: '#fff' }}
72-
>
73-
{marker.label}
74-
</span>
75-
</div>
76-
</div>
77-
);
78-
});
79-
8052
return (
8153
<div className='w-full bg-slate-800 rounded-lg p-4'>
8254
<div
8355
ref={containerRef}
84-
className='relative w-full cursor-pointer'
85-
style={{ height: `${height}px` }}
56+
className='relative w-full'
57+
style={{ height: `${height}px`, cursor: abMode ? 'crosshair' : 'pointer' }}
8658
onClick={handleClick}
8759
>
8860
{/* Static placeholder bars (unplayed = dim) */}
8961
<div className='absolute inset-0 flex items-center justify-center gap-px opacity-30'>
90-
{Array.from({ length: 120 }).map((_, i) => {
91-
// Deterministic pseudo-random heights for visual variety
92-
const h = 20 + ((i * 7 + 13) % 60);
93-
return (
94-
<div
95-
key={i}
96-
className='flex-1 bg-slate-500 rounded-sm'
97-
style={{ height: `${h}%`, minWidth: '2px' }}
98-
/>
99-
);
100-
})}
62+
{Array.from({ length: PLACEHOLDER_BAR_COUNT }).map((_, i) => (
63+
<div
64+
key={i}
65+
className='flex-1 bg-slate-500 rounded-sm'
66+
style={{ height: `${placeholderBarHeight(i)}%`, minWidth: '2px' }}
67+
/>
68+
))}
10169
</div>
10270

10371
{/* Progress overlay (played = bright, clipped to playhead position) */}
@@ -106,16 +74,13 @@ export function DummyWaveform({ duration, currentTime, height = 96, onSeek }: Du
10674
pointer-events-none'
10775
style={{ clipPath: `inset(0 ${100 - progressPercent}% 0 0)` }}
10876
>
109-
{Array.from({ length: 120 }).map((_, i) => {
110-
const h = 20 + ((i * 7 + 13) % 60);
111-
return (
112-
<div
113-
key={i}
114-
className='flex-1 bg-slate-300 rounded-sm'
115-
style={{ height: `${h}%`, minWidth: '2px' }}
116-
/>
117-
);
118-
})}
77+
{Array.from({ length: PLACEHOLDER_BAR_COUNT }).map((_, i) => (
78+
<div
79+
key={i}
80+
className='flex-1 bg-slate-300 rounded-sm'
81+
style={{ height: `${placeholderBarHeight(i)}%`, minWidth: '2px' }}
82+
/>
83+
))}
11984
</div>
12085

12186
{/* "No audio" indicator */}
@@ -128,15 +93,29 @@ export function DummyWaveform({ duration, currentTime, height = 96, onSeek }: Du
12893
{/* Playhead cursor */}
12994
<div
13095
className='absolute top-0 h-full w-px bg-white pointer-events-none'
131-
style={{ left: `${progressPercent}%`, zIndex: 4 }}
96+
style={{ left: `${progressPercent}%`, zIndex: 5 }}
13297
/>
13398

134-
{/* Marker overlays */}
99+
{/* Loop region and marker overlays */}
135100
<div
136101
className='absolute top-0 left-0 w-full h-full pointer-events-none'
137102
style={{ zIndex: 1 }}
138103
>
139-
{markerOverlays}
104+
<LoopOverlay
105+
loop={loop}
106+
loopEnabled={loopEnabled}
107+
abStart={abStart}
108+
duration={duration}
109+
drag={drag}
110+
onHandleMouseDown={handleLoopHandleMouseDown}
111+
/>
112+
<MarkerOverlay
113+
markers={markers}
114+
duration={duration}
115+
drag={drag}
116+
abMode={abMode}
117+
onMarkerMouseDown={handleMarkerMouseDown}
118+
/>
140119
</div>
141120
</div>
142121

0 commit comments

Comments
 (0)