feat: implement medium effort features - markers, web workers, and bezier automation
Implemented three major medium effort features to enhance the audio editor: **1. Region Markers System** - Add marker type definitions supporting point markers and regions - Create useMarkers hook for marker state management - Build MarkerTimeline component for visual marker display - Create MarkerDialog component for adding/editing markers - Add keyboard shortcuts: M (add marker), Shift+M (next), Shift+Ctrl+M (previous) - Support marker navigation, editing, and deletion **2. Web Worker for Computations** - Create audio worker for offloading heavy computations - Implement worker functions: generatePeaks, generateMinMaxPeaks, normalizePeaks, analyzeAudio, findPeak - Build useAudioWorker hook for easy worker integration - Integrate worker into Waveform component with peak caching - Significantly improve UI responsiveness during waveform generation **3. Bezier Curve Automation** - Enhance interpolateAutomationValue to support Bezier curves - Implement cubic Bezier interpolation with control handles - Add createSmoothHandles for auto-smooth curve generation - Add generateBezierCurvePoints for smooth curve rendering - Support bezier alongside existing linear and step curves All features are type-safe and integrate seamlessly with the existing codebase. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
200
lib/workers/audio.worker.ts
Normal file
200
lib/workers/audio.worker.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Web Worker for heavy audio computations
|
||||
* Offloads waveform generation, analysis, and normalization to background thread
|
||||
*/
|
||||
|
||||
export interface WorkerMessage {
|
||||
id: string;
|
||||
type: 'generatePeaks' | 'generateMinMaxPeaks' | 'normalizePeaks' | 'analyzeAudio' | 'findPeak';
|
||||
payload: any;
|
||||
}
|
||||
|
||||
export interface WorkerResponse {
|
||||
id: string;
|
||||
type: string;
|
||||
result?: any;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Message handler
|
||||
self.onmessage = (event: MessageEvent<WorkerMessage>) => {
|
||||
const { id, type, payload } = event.data;
|
||||
|
||||
try {
|
||||
let result: any;
|
||||
|
||||
switch (type) {
|
||||
case 'generatePeaks':
|
||||
result = generatePeaks(
|
||||
payload.channelData,
|
||||
payload.width
|
||||
);
|
||||
break;
|
||||
|
||||
case 'generateMinMaxPeaks':
|
||||
result = generateMinMaxPeaks(
|
||||
payload.channelData,
|
||||
payload.width
|
||||
);
|
||||
break;
|
||||
|
||||
case 'normalizePeaks':
|
||||
result = normalizePeaks(
|
||||
payload.peaks,
|
||||
payload.targetMax
|
||||
);
|
||||
break;
|
||||
|
||||
case 'analyzeAudio':
|
||||
result = analyzeAudio(payload.channelData);
|
||||
break;
|
||||
|
||||
case 'findPeak':
|
||||
result = findPeak(payload.channelData);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown worker message type: ${type}`);
|
||||
}
|
||||
|
||||
const response: WorkerResponse = { id, type, result };
|
||||
self.postMessage(response);
|
||||
} catch (error) {
|
||||
const response: WorkerResponse = {
|
||||
id,
|
||||
type,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
self.postMessage(response);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate waveform peaks from channel data
|
||||
*/
|
||||
function generatePeaks(channelData: Float32Array, width: number): Float32Array {
|
||||
const peaks = new Float32Array(width);
|
||||
const samplesPerPeak = Math.floor(channelData.length / width);
|
||||
|
||||
for (let i = 0; i < width; i++) {
|
||||
const start = i * samplesPerPeak;
|
||||
const end = Math.min(start + samplesPerPeak, channelData.length);
|
||||
|
||||
let max = 0;
|
||||
for (let j = start; j < end; j++) {
|
||||
const abs = Math.abs(channelData[j]);
|
||||
if (abs > max) {
|
||||
max = abs;
|
||||
}
|
||||
}
|
||||
|
||||
peaks[i] = max;
|
||||
}
|
||||
|
||||
return peaks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate min/max peaks for more detailed waveform visualization
|
||||
*/
|
||||
function generateMinMaxPeaks(
|
||||
channelData: Float32Array,
|
||||
width: number
|
||||
): { min: Float32Array; max: Float32Array } {
|
||||
const min = new Float32Array(width);
|
||||
const max = new Float32Array(width);
|
||||
const samplesPerPeak = Math.floor(channelData.length / width);
|
||||
|
||||
for (let i = 0; i < width; i++) {
|
||||
const start = i * samplesPerPeak;
|
||||
const end = Math.min(start + samplesPerPeak, channelData.length);
|
||||
|
||||
let minVal = 1;
|
||||
let maxVal = -1;
|
||||
|
||||
for (let j = start; j < end; j++) {
|
||||
const val = channelData[j];
|
||||
if (val < minVal) minVal = val;
|
||||
if (val > maxVal) maxVal = val;
|
||||
}
|
||||
|
||||
min[i] = minVal;
|
||||
max[i] = maxVal;
|
||||
}
|
||||
|
||||
return { min, max };
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize peaks to a given range
|
||||
*/
|
||||
function normalizePeaks(peaks: Float32Array, targetMax: number = 1): Float32Array {
|
||||
const normalized = new Float32Array(peaks.length);
|
||||
let max = 0;
|
||||
|
||||
// Find max value
|
||||
for (let i = 0; i < peaks.length; i++) {
|
||||
if (peaks[i] > max) {
|
||||
max = peaks[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize
|
||||
const scale = max > 0 ? targetMax / max : 1;
|
||||
for (let i = 0; i < peaks.length; i++) {
|
||||
normalized[i] = peaks[i] * scale;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze audio data for statistics
|
||||
*/
|
||||
function analyzeAudio(channelData: Float32Array): {
|
||||
peak: number;
|
||||
rms: number;
|
||||
crestFactor: number;
|
||||
dynamicRange: number;
|
||||
} {
|
||||
let peak = 0;
|
||||
let sumSquares = 0;
|
||||
let min = 1;
|
||||
let max = -1;
|
||||
|
||||
for (let i = 0; i < channelData.length; i++) {
|
||||
const val = channelData[i];
|
||||
const abs = Math.abs(val);
|
||||
|
||||
if (abs > peak) peak = abs;
|
||||
if (val < min) min = val;
|
||||
if (val > max) max = val;
|
||||
|
||||
sumSquares += val * val;
|
||||
}
|
||||
|
||||
const rms = Math.sqrt(sumSquares / channelData.length);
|
||||
const crestFactor = rms > 0 ? peak / rms : 0;
|
||||
const dynamicRange = max - min;
|
||||
|
||||
return {
|
||||
peak,
|
||||
rms,
|
||||
crestFactor,
|
||||
dynamicRange,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find peak value in channel data
|
||||
*/
|
||||
function findPeak(channelData: Float32Array): number {
|
||||
let peak = 0;
|
||||
|
||||
for (let i = 0; i < channelData.length; i++) {
|
||||
const abs = Math.abs(channelData[i]);
|
||||
if (abs > peak) peak = abs;
|
||||
}
|
||||
|
||||
return peak;
|
||||
}
|
||||
Reference in New Issue
Block a user