feat: implement Phase 10.1 & 10.2 - frequency analyzer and spectrogram

Added real-time audio analysis visualizations to master column:

- FrequencyAnalyzer component with FFT bar display
  - Canvas-based rendering with requestAnimationFrame
  - Color gradient from cyan to green based on frequency
  - Frequency axis labels (20Hz, 1kHz, 20kHz)

- Spectrogram component with time-frequency waterfall display
  - Scrolling visualization with ImageData pixel manipulation
  - Color mapping: black → blue → cyan → green → yellow → red
  - Vertical frequency axis with labels

- Master column redesign
  - Fixed width layout (280px)
  - Toggle buttons to switch between FFT and Spectrum views
  - Integrated above master controls with 300px minimum height

- Exposed masterAnalyser from useMultiTrackPlayer hook
  - Analyser node now accessible to visualization components

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-19 01:40:04 +01:00
parent 4281c65ec1
commit 7dc0780bd2
4 changed files with 276 additions and 22 deletions

View File

@@ -4,6 +4,8 @@ import * as React from 'react';
import { Music, Plus, Upload, Trash2, Settings, Download } from 'lucide-react';
import { PlaybackControls } from './PlaybackControls';
import { MasterControls } from '@/components/controls/MasterControls';
import { FrequencyAnalyzer } from '@/components/analysis/FrequencyAnalyzer';
import { Spectrogram } from '@/components/analysis/Spectrogram';
import { ThemeToggle } from '@/components/layout/ThemeToggle';
import { CommandPalette } from '@/components/ui/CommandPalette';
import { GlobalSettingsDialog } from '@/components/settings/GlobalSettingsDialog';
@@ -47,6 +49,7 @@ export function AudioEditor() {
const [settingsDialogOpen, setSettingsDialogOpen] = React.useState(false);
const [exportDialogOpen, setExportDialogOpen] = React.useState(false);
const [isExporting, setIsExporting] = React.useState(false);
const [analyzerView, setAnalyzerView] = React.useState<'frequency' | 'spectrogram'>('frequency');
const { addToast } = useToast();
@@ -225,6 +228,7 @@ export function AudioEditor() {
masterPeakLevel,
masterRmsLevel,
masterIsClipping,
masterAnalyser,
resetClipIndicator,
play,
pause,
@@ -1036,28 +1040,64 @@ export function AudioEditor() {
</div>
</main>
{/* Right Sidebar - Master Controls */}
<aside className="flex-shrink-0 border-l border-border bg-card flex items-center justify-center p-4">
<MasterControls
volume={masterVolume}
pan={masterPan}
peakLevel={masterPeakLevel}
rmsLevel={masterRmsLevel}
isClipping={masterIsClipping}
isMuted={isMasterMuted}
onVolumeChange={setMasterVolume}
onPanChange={setMasterPan}
onMuteToggle={() => {
if (isMasterMuted) {
setMasterVolume(0.8);
setIsMasterMuted(false);
} else {
setMasterVolume(0);
setIsMasterMuted(true);
}
}}
onResetClip={resetClipIndicator}
/>
{/* Right Sidebar - Master Controls & Analyzers */}
<aside className="flex-shrink-0 border-l border-border bg-card flex flex-col p-4 gap-4 w-[280px]">
{/* Master Controls */}
<div className="flex items-center justify-center">
<MasterControls
volume={masterVolume}
pan={masterPan}
peakLevel={masterPeakLevel}
rmsLevel={masterRmsLevel}
isClipping={masterIsClipping}
isMuted={isMasterMuted}
onVolumeChange={setMasterVolume}
onPanChange={setMasterPan}
onMuteToggle={() => {
if (isMasterMuted) {
setMasterVolume(0.8);
setIsMasterMuted(false);
} else {
setMasterVolume(0);
setIsMasterMuted(true);
}
}}
onResetClip={resetClipIndicator}
/>
</div>
{/* Analyzer Toggle */}
<div className="flex gap-1 bg-muted/20 border border-border/50 rounded-md p-1">
<button
onClick={() => setAnalyzerView('frequency')}
className={`flex-1 px-2 py-1 rounded text-[10px] font-bold uppercase tracking-wider transition-all ${
analyzerView === 'frequency'
? 'bg-accent text-accent-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
FFT
</button>
<button
onClick={() => setAnalyzerView('spectrogram')}
className={`flex-1 px-2 py-1 rounded text-[10px] font-bold uppercase tracking-wider transition-all ${
analyzerView === 'spectrogram'
? 'bg-accent text-accent-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
Spectrum
</button>
</div>
{/* Analyzer Display */}
<div className="flex-1 min-h-[300px]">
{analyzerView === 'frequency' ? (
<FrequencyAnalyzer analyserNode={masterAnalyser} />
) : (
<Spectrogram analyserNode={masterAnalyser} />
)}
</div>
</aside>
</div>