feat: add loop playback functionality

Added complete loop functionality with UI controls:
- Loop state management in useMultiTrackPlayer (loopEnabled, loopStart, loopEnd)
- Automatic restart from loop start when reaching loop end during playback
- Loop toggle button in PlaybackControls with Repeat icon
- Loop points UI showing when loop is enabled (similar to punch in/out)
- Manual loop point adjustment with number inputs
- Quick set buttons to set loop points to current time
- Wired loop functionality through AudioEditor component

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-20 07:31:53 +01:00
parent aba26126cc
commit a47bf09a32
3 changed files with 181 additions and 2 deletions

View File

@@ -294,6 +294,12 @@ export function AudioEditor() {
stop,
seek,
togglePlayPause,
loopEnabled,
loopStart,
loopEnd,
toggleLoop,
setLoopPoints,
setLoopFromSelection,
} = useMultiTrackPlayer(tracks, masterVolume, handleAutomationRecording);
// Reset latch triggered state when playback stops
@@ -1972,6 +1978,11 @@ export function AudioEditor() {
onPunchOutTimeChange={setPunchOutTime}
overdubEnabled={overdubEnabled}
onOverdubEnabledChange={setOverdubEnabled}
loopEnabled={loopEnabled}
loopStart={loopStart}
loopEnd={loopEnd}
onToggleLoop={toggleLoop}
onSetLoopPoints={setLoopPoints}
/>
</div>
</div>

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { Play, Pause, Square, SkipBack, Circle, AlignVerticalJustifyStart, AlignVerticalJustifyEnd, Layers } from 'lucide-react';
import { Play, Pause, Square, SkipBack, Circle, AlignVerticalJustifyStart, AlignVerticalJustifyEnd, Layers, Repeat } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils/cn';
@@ -31,6 +31,11 @@ export interface PlaybackControlsProps {
onPunchOutTimeChange?: (time: number) => void;
overdubEnabled?: boolean;
onOverdubEnabledChange?: (enabled: boolean) => void;
loopEnabled?: boolean;
loopStart?: number;
loopEnd?: number;
onToggleLoop?: () => void;
onSetLoopPoints?: (start: number, end: number) => void;
}
export function PlaybackControls({
@@ -59,6 +64,11 @@ export function PlaybackControls({
onPunchOutTimeChange,
overdubEnabled = false,
onOverdubEnabledChange,
loopEnabled = false,
loopStart = 0,
loopEnd = 0,
onToggleLoop,
onSetLoopPoints,
}: PlaybackControlsProps) {
const handlePlayPause = () => {
if (isPlaying) {
@@ -249,8 +259,80 @@ export function PlaybackControls({
</div>
</>
)}
{/* Loop Toggle */}
{onToggleLoop && (
<div className="flex items-center gap-1 border-l border-border pl-2 ml-1">
<Button
variant="ghost"
size="icon-sm"
onClick={onToggleLoop}
title="Toggle Loop Playback"
className={cn(
loopEnabled && 'bg-primary/20 hover:bg-primary/30'
)}
>
<Repeat className={cn('h-3.5 w-3.5', loopEnabled && 'text-primary')} />
</Button>
</div>
)}
</div>
</div>
{/* Loop Points - Show when enabled */}
{loopEnabled && onSetLoopPoints && (
<div className="flex items-center gap-3 text-xs bg-muted/50 rounded px-3 py-2">
<div className="flex items-center gap-2 flex-1">
<label className="text-muted-foreground flex items-center gap-1 flex-shrink-0">
<AlignVerticalJustifyStart className="h-3 w-3" />
Loop Start
</label>
<input
type="number"
min={0}
max={loopEnd || duration}
step={0.1}
value={loopStart.toFixed(2)}
onChange={(e) => onSetLoopPoints(parseFloat(e.target.value), loopEnd)}
className="flex-1 px-2 py-1 bg-background border border-border rounded text-xs font-mono"
/>
<Button
variant="ghost"
size="sm"
onClick={() => onSetLoopPoints(currentTime, loopEnd)}
title="Set loop start to current time"
className="h-6 px-2 text-xs"
>
Set
</Button>
</div>
<div className="flex items-center gap-2 flex-1">
<label className="text-muted-foreground flex items-center gap-1 flex-shrink-0">
<AlignVerticalJustifyEnd className="h-3 w-3" />
Loop End
</label>
<input
type="number"
min={loopStart}
max={duration}
step={0.1}
value={loopEnd.toFixed(2)}
onChange={(e) => onSetLoopPoints(loopStart, parseFloat(e.target.value))}
className="flex-1 px-2 py-1 bg-background border border-border rounded text-xs font-mono"
/>
<Button
variant="ghost"
size="sm"
onClick={() => onSetLoopPoints(loopStart, currentTime)}
title="Set loop end to current time"
className="h-6 px-2 text-xs"
>
Set
</Button>
</div>
</div>
)}
</div>
);
}