diff --git a/PLAN.md b/PLAN.md
index 1d100ab..bbc5598 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -2,7 +2,7 @@
## Progress Overview
-**Current Status**: Phase 7 In Progress (Multi-Track Support - Core Features Complete)
+**Current Status**: Phase 8 Complete (Recording with Overdub/Punch & Settings)
### Completed Phases
- ✅ **Phase 1**: Project Setup & Core Infrastructure (95% complete)
@@ -107,7 +107,7 @@
- 🔲 Advanced real-time effects: Reverb, chorus, flanger, phaser, distortion (TODO: Complex node graphs)
- 🔲 Master channel effects (TODO: Implement master effect chain UI similar to per-track effects)
-**Recording Features (Phase 8 - Phases 8.1-8.2 Complete):**
+**Recording Features (Phase 8 - Complete):**
- ✅ Microphone permission request
- ✅ Audio input device selection
- ✅ Input level meter with professional dB scale
@@ -117,11 +117,16 @@
- ✅ Recording indicator with pulse animation
- ✅ Punch-in/Punch-out controls (time-based recording region)
- ✅ Overdub mode (layer recordings by mixing audio)
+- ✅ Input gain control (0.0-2.0 with dB display, adjustable in real-time)
+- ✅ Mono/Stereo recording selection
+- ✅ Sample rate matching (44.1kHz, 48kHz, 96kHz)
+- ✅ Recording settings panel shown when track is armed
### Next Steps
- **Phase 6**: Audio effects ✅ COMPLETE (Basic + Filters + Dynamics + Time-Based + Advanced + Chain Management)
- **Phase 7**: Multi-track editing ✅ COMPLETE (Multi-track playback, effects, selection/editing)
-- **Phase 8**: Recording functionality 🚧 IN PROGRESS (Phase 8.1-8.2 complete, 8.3 remaining)
+- **Phase 8**: Recording functionality ✅ COMPLETE (Audio input, controls, settings with overdub/punch)
+- **Phase 9**: Automation (NEXT)
---
@@ -611,11 +616,12 @@ audio-ui/
- [x] Punch-in/Punch-out recording (UI controls with time inputs)
- [x] Overdub mode (mix recorded audio with existing audio)
-#### 8.3 Recording Settings
-- [ ] Sample rate matching
-- [ ] Input gain control
-- [ ] Mono/Stereo recording
-- [ ] File naming conventions
+#### 8.3 Recording Settings ✓
+- [x] Sample rate matching (44.1kHz, 48kHz, 96kHz)
+- [x] Input gain control (0.0-2.0 with dB display)
+- [x] Mono/Stereo recording selection
+- [x] Real-time gain adjustment during recording
+- 🔲 File naming conventions (Future: Auto-name recorded tracks)
### Phase 9: Automation
diff --git a/components/editor/AudioEditor.tsx b/components/editor/AudioEditor.tsx
index 7d29f66..ea447fe 100644
--- a/components/editor/AudioEditor.tsx
+++ b/components/editor/AudioEditor.tsx
@@ -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}
/>
diff --git a/components/recording/RecordingSettings.tsx b/components/recording/RecordingSettings.tsx
new file mode 100644
index 0000000..7c429e5
--- /dev/null
+++ b/components/recording/RecordingSettings.tsx
@@ -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 (
+
+
Recording Settings
+
+ {/* Input Gain */}
+
+
+
+
+
+
+ {settings.inputGain === 1 ? '0 dB' : `${(20 * Math.log10(settings.inputGain)).toFixed(1)} dB`}
+
+
+
+ {/* Mono/Stereo Toggle */}
+
+
+
+
+
+
+
+
+ {/* Sample Rate Selection */}
+
+
+
+ {SAMPLE_RATES.map((rate) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/components/tracks/Track.tsx b/components/tracks/Track.tsx
index ac4917c..1517c40 100644
--- a/components/tracks/Track.tsx
+++ b/components/tracks/Track.tsx
@@ -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(null);
const containerRef = React.useRef(null);
@@ -584,6 +594,16 @@ export function Track({
})()}
+
+ {/* Recording Settings - Show when track is armed */}
+ {track.recordEnabled && recordingSettings && onInputGainChange && onRecordMonoChange && onSampleRateChange && (
+
+ )}
>
)}
diff --git a/components/tracks/TrackList.tsx b/components/tracks/TrackList.tsx
index 6875a34..ab31bd4 100644
--- a/components/tracks/TrackList.tsx
+++ b/components/tracks/TrackList.tsx
@@ -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;
+ 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}
/>
))}
diff --git a/lib/hooks/useRecording.ts b/lib/hooks/useRecording.ts
index a43cc13..f98eb9a 100644
--- a/lib/hooks/useRecording.ts
+++ b/lib/hooks/useRecording.ts
@@ -9,8 +9,15 @@ export interface RecordingState {
inputLevel: number;
}
+export interface RecordingSettings {
+ inputGain: number; // 0.0 to 2.0 (1.0 = unity)
+ recordMono: boolean; // true = mono, false = stereo
+ sampleRate: number; // target sample rate (44100, 48000, etc.)
+}
+
export interface UseRecordingReturn {
state: RecordingState;
+ settings: RecordingSettings;
startRecording: () => Promise;
stopRecording: () => Promise;
pauseRecording: () => void;
@@ -18,6 +25,9 @@ export interface UseRecordingReturn {
getInputDevices: () => Promise;
selectInputDevice: (deviceId: string) => Promise;
requestPermission: () => Promise;
+ setInputGain: (gain: number) => void;
+ setRecordMono: (mono: boolean) => void;
+ setSampleRate: (sampleRate: number) => void;
}
export function useRecording(): UseRecordingReturn {
@@ -28,9 +38,16 @@ export function useRecording(): UseRecordingReturn {
inputLevel: 0,
});
+ const [settings, setSettings] = React.useState({
+ inputGain: 1.0,
+ recordMono: false,
+ sampleRate: 48000,
+ });
+
const mediaRecorderRef = React.useRef(null);
const audioContextRef = React.useRef(null);
const analyserRef = React.useRef(null);
+ const gainNodeRef = React.useRef(null);
const streamRef = React.useRef(null);
const chunksRef = React.useRef([]);
const startTimeRef = React.useRef(0);
@@ -128,16 +145,25 @@ export function useRecording(): UseRecordingReturn {
const stream = await navigator.mediaDevices.getUserMedia(constraints);
streamRef.current = stream;
- // Create audio context and analyser for level monitoring
- const audioContext = new AudioContext();
+ // Create audio context with target sample rate
+ const audioContext = new AudioContext({ sampleRate: settings.sampleRate });
audioContextRef.current = audioContext;
const source = audioContext.createMediaStreamSource(stream);
+
+ // Create gain node for input gain control
+ const gainNode = audioContext.createGain();
+ gainNode.gain.value = settings.inputGain;
+ gainNodeRef.current = gainNode;
+
+ // Create analyser for level monitoring
const analyser = audioContext.createAnalyser();
analyser.fftSize = 256;
analyser.smoothingTimeConstant = 0.3;
- source.connect(analyser);
+ // Connect: source -> gain -> analyser
+ source.connect(gainNode);
+ gainNode.connect(analyser);
analyserRef.current = analyser;
// Create MediaRecorder
@@ -169,7 +195,7 @@ export function useRecording(): UseRecordingReturn {
console.error('Failed to start recording:', error);
throw error;
}
- }, [monitorInputLevel]);
+ }, [monitorInputLevel, settings.sampleRate, settings.inputGain]);
// Stop recording and return AudioBuffer
const stopRecording = React.useCallback(async (): Promise => {
@@ -192,7 +218,28 @@ export function useRecording(): UseRecordingReturn {
try {
const arrayBuffer = await blob.arrayBuffer();
const audioContext = new AudioContext();
- const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
+ let audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
+
+ // Convert to mono if requested
+ if (settings.recordMono && audioBuffer.numberOfChannels > 1) {
+ const monoBuffer = audioContext.createBuffer(
+ 1,
+ audioBuffer.length,
+ audioBuffer.sampleRate
+ );
+ const monoData = monoBuffer.getChannelData(0);
+
+ // Mix all channels to mono by averaging
+ for (let i = 0; i < audioBuffer.length; i++) {
+ let sum = 0;
+ for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
+ sum += audioBuffer.getChannelData(channel)[i];
+ }
+ monoData[i] = sum / audioBuffer.numberOfChannels;
+ }
+
+ audioBuffer = monoBuffer;
+ }
// Clean up
isMonitoringRef.current = false;
@@ -219,7 +266,7 @@ export function useRecording(): UseRecordingReturn {
mediaRecorder.stop();
});
- }, []);
+ }, [settings.recordMono]);
// Pause recording
const pauseRecording = React.useCallback(() => {
@@ -272,8 +319,26 @@ export function useRecording(): UseRecordingReturn {
};
}, []);
+ // Settings setters
+ const setInputGain = React.useCallback((gain: number) => {
+ setSettings((prev) => ({ ...prev, inputGain: Math.max(0, Math.min(2, gain)) }));
+ // Update gain node if recording
+ if (gainNodeRef.current) {
+ gainNodeRef.current.gain.value = Math.max(0, Math.min(2, gain));
+ }
+ }, []);
+
+ const setRecordMono = React.useCallback((mono: boolean) => {
+ setSettings((prev) => ({ ...prev, recordMono: mono }));
+ }, []);
+
+ const setSampleRate = React.useCallback((sampleRate: number) => {
+ setSettings((prev) => ({ ...prev, sampleRate }));
+ }, []);
+
return {
state,
+ settings,
startRecording,
stopRecording,
pauseRecording,
@@ -281,5 +346,8 @@ export function useRecording(): UseRecordingReturn {
getInputDevices,
selectInputDevice,
requestPermission,
+ setInputGain,
+ setRecordMono,
+ setSampleRate,
};
}