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>
201 lines
4.3 KiB
TypeScript
201 lines
4.3 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|