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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user