feat: complete Phase 8.3 - recording settings (input gain, mono/stereo, sample rate)
Recording Settings (Phase 8.3): - Added input gain control (0.0-2.0 with dB display) - Real-time gain adjustment via GainNode during recording - Mono/Stereo recording mode selection - Sample rate matching (44.1kHz, 48kHz, 96kHz) - Mono conversion averages all channels when enabled - Recording settings panel shown when track is armed Components Created: - RecordingSettings.tsx: Settings panel with gain slider, mono/stereo toggle, sample rate buttons Components Modified: - useRecording hook: Added settings state and GainNode integration - AudioEditor: Pass recording settings to TrackList - TrackList: Forward settings to Track components - Track: Show RecordingSettings when armed for recording Technical Details: - GainNode inserted between source and analyser in recording chain - Real-time gain updates via gainNode.gain.value - AudioContext created with target sample rate - Mono conversion done post-recording by averaging channels - Settings persist during recording session Phase 8 Complete: - ✅ Phase 8.1: Audio Input - ✅ Phase 8.2: Recording Controls (punch/overdub) - ✅ Phase 8.3: Recording Settings - 📋 Phase 9: Automation (NEXT) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -47,9 +47,13 @@ export function AudioEditor() {
|
||||
// Recording hook
|
||||
const {
|
||||
state: recordingState,
|
||||
settings: recordingSettings,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
requestPermission,
|
||||
setInputGain,
|
||||
setRecordMono,
|
||||
setSampleRate,
|
||||
} = useRecording();
|
||||
|
||||
// Multi-track hooks
|
||||
@@ -689,6 +693,10 @@ export function AudioEditor() {
|
||||
recordingTrackId={recordingTrackId}
|
||||
recordingLevel={recordingState.inputLevel}
|
||||
trackLevels={trackLevels}
|
||||
recordingSettings={recordingSettings}
|
||||
onInputGainChange={setInputGain}
|
||||
onRecordMonoChange={setRecordMono}
|
||||
onSampleRateChange={setSampleRate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
106
components/recording/RecordingSettings.tsx
Normal file
106
components/recording/RecordingSettings.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Volume2, Radio } from 'lucide-react';
|
||||
import { Slider } from '@/components/ui/Slider';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import type { RecordingSettings as RecordingSettingsType } from '@/lib/hooks/useRecording';
|
||||
|
||||
export interface RecordingSettingsProps {
|
||||
settings: RecordingSettingsType;
|
||||
onInputGainChange: (gain: number) => void;
|
||||
onRecordMonoChange: (mono: boolean) => void;
|
||||
onSampleRateChange: (sampleRate: number) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SAMPLE_RATES = [44100, 48000, 96000];
|
||||
|
||||
export function RecordingSettings({
|
||||
settings,
|
||||
onInputGainChange,
|
||||
onRecordMonoChange,
|
||||
onSampleRateChange,
|
||||
className,
|
||||
}: RecordingSettingsProps) {
|
||||
return (
|
||||
<div className={cn('space-y-2 p-3 bg-muted/50 rounded border border-border', className)}>
|
||||
<div className="text-xs font-medium text-muted-foreground mb-2">Recording Settings</div>
|
||||
|
||||
{/* Input Gain */}
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-xs text-muted-foreground flex items-center gap-1 w-24 flex-shrink-0">
|
||||
<Volume2 className="h-3.5 w-3.5" />
|
||||
Input Gain
|
||||
</label>
|
||||
<div className="flex-1">
|
||||
<Slider
|
||||
value={settings.inputGain}
|
||||
onChange={onInputGainChange}
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground w-12 text-right flex-shrink-0">
|
||||
{settings.inputGain === 1 ? '0 dB' : `${(20 * Math.log10(settings.inputGain)).toFixed(1)} dB`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Mono/Stereo Toggle */}
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-xs text-muted-foreground flex items-center gap-1 w-24 flex-shrink-0">
|
||||
<Radio className="h-3.5 w-3.5" />
|
||||
Channels
|
||||
</label>
|
||||
<div className="flex gap-1 flex-1">
|
||||
<button
|
||||
onClick={() => onRecordMonoChange(true)}
|
||||
className={cn(
|
||||
'flex-1 px-2 py-1 text-xs rounded transition-colors',
|
||||
settings.recordMono
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted hover:bg-muted/80 text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
Mono
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onRecordMonoChange(false)}
|
||||
className={cn(
|
||||
'flex-1 px-2 py-1 text-xs rounded transition-colors',
|
||||
!settings.recordMono
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted hover:bg-muted/80 text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
Stereo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sample Rate Selection */}
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-xs text-muted-foreground w-24 flex-shrink-0">
|
||||
Sample Rate
|
||||
</label>
|
||||
<div className="flex gap-1 flex-1">
|
||||
{SAMPLE_RATES.map((rate) => (
|
||||
<button
|
||||
key={rate}
|
||||
onClick={() => onSampleRateChange(rate)}
|
||||
className={cn(
|
||||
'flex-1 px-2 py-1 text-xs rounded transition-colors font-mono',
|
||||
settings.sampleRate === rate
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted hover:bg-muted/80 text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{rate / 1000}k
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import { EffectBrowser } from '@/components/effects/EffectBrowser';
|
||||
import { EffectDevice } from '@/components/effects/EffectDevice';
|
||||
import { createEffect, type EffectType } from '@/lib/audio/effects/chain';
|
||||
import { InputLevelMeter } from '@/components/recording/InputLevelMeter';
|
||||
import { RecordingSettings } from '@/components/recording/RecordingSettings';
|
||||
import type { RecordingSettings as RecordingSettingsType } from '@/lib/hooks/useRecording';
|
||||
|
||||
export interface TrackProps {
|
||||
track: TrackType;
|
||||
@@ -36,6 +38,10 @@ export interface TrackProps {
|
||||
isRecording?: boolean;
|
||||
recordingLevel?: number;
|
||||
playbackLevel?: number;
|
||||
recordingSettings?: RecordingSettingsType;
|
||||
onInputGainChange?: (gain: number) => void;
|
||||
onRecordMonoChange?: (mono: boolean) => void;
|
||||
onSampleRateChange?: (sampleRate: number) => void;
|
||||
}
|
||||
|
||||
export function Track({
|
||||
@@ -63,6 +69,10 @@ export function Track({
|
||||
isRecording = false,
|
||||
recordingLevel = 0,
|
||||
playbackLevel = 0,
|
||||
recordingSettings,
|
||||
onInputGainChange,
|
||||
onRecordMonoChange,
|
||||
onSampleRateChange,
|
||||
}: TrackProps) {
|
||||
const canvasRef = React.useRef<HTMLCanvasElement>(null);
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
@@ -584,6 +594,16 @@ export function Track({
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Recording Settings - Show when track is armed */}
|
||||
{track.recordEnabled && recordingSettings && onInputGainChange && onRecordMonoChange && onSampleRateChange && (
|
||||
<RecordingSettings
|
||||
settings={recordingSettings}
|
||||
onInputGainChange={onInputGainChange}
|
||||
onRecordMonoChange={onRecordMonoChange}
|
||||
onSampleRateChange={onSampleRateChange}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Track } from './Track';
|
||||
import { ImportTrackDialog } from './ImportTrackDialog';
|
||||
import type { Track as TrackType } from '@/types/track';
|
||||
import { createEffect, type EffectType, EFFECT_NAMES } from '@/lib/audio/effects/chain';
|
||||
import type { RecordingSettings } from '@/lib/hooks/useRecording';
|
||||
|
||||
export interface TrackListProps {
|
||||
tracks: TrackType[];
|
||||
@@ -25,6 +26,10 @@ export interface TrackListProps {
|
||||
recordingTrackId?: string | null;
|
||||
recordingLevel?: number;
|
||||
trackLevels?: Record<string, number>;
|
||||
recordingSettings?: RecordingSettings;
|
||||
onInputGainChange?: (gain: number) => void;
|
||||
onRecordMonoChange?: (mono: boolean) => void;
|
||||
onSampleRateChange?: (sampleRate: number) => void;
|
||||
}
|
||||
|
||||
export function TrackList({
|
||||
@@ -44,6 +49,10 @@ export function TrackList({
|
||||
recordingTrackId,
|
||||
recordingLevel = 0,
|
||||
trackLevels = {},
|
||||
recordingSettings,
|
||||
onInputGainChange,
|
||||
onRecordMonoChange,
|
||||
onSampleRateChange,
|
||||
}: TrackListProps) {
|
||||
const [importDialogOpen, setImportDialogOpen] = React.useState(false);
|
||||
|
||||
@@ -167,6 +176,10 @@ export function TrackList({
|
||||
isRecording={recordingTrackId === track.id}
|
||||
recordingLevel={recordingTrackId === track.id ? recordingLevel : 0}
|
||||
playbackLevel={trackLevels[track.id] || 0}
|
||||
recordingSettings={recordingSettings}
|
||||
onInputGainChange={onInputGainChange}
|
||||
onRecordMonoChange={onRecordMonoChange}
|
||||
onSampleRateChange={onSampleRateChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user