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:
2025-11-18 15:51:38 +01:00
parent 5f0017facb
commit 49dd0c2d38
6 changed files with 235 additions and 14 deletions

View File

@@ -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>

View 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>
);
}

View File

@@ -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>

View File

@@ -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>