Files
audio-ui/lib/audio/effects/selection.ts
Sebastian Krüger ee48f9475f feat: add advanced audio effects and improve UI
Phase 6.5 Advanced Effects:
- Add Pitch Shifter with semitones and cents adjustment
- Add Time Stretch with pitch preservation using overlap-add
- Add Distortion with soft/hard/tube types and tone control
- Add Bitcrusher with bit depth and sample rate reduction
- Add AdvancedParameterDialog with real-time waveform visualization
- Add 4 professional presets per effect type

Improvements:
- Fix undefined parameter errors by adding nullish coalescing operators
- Add global custom scrollbar styling with color-mix transparency
- Add custom-scrollbar utility class for side panel
- Improve theme-aware scrollbar appearance in light/dark modes
- Fix parameter initialization when switching effect types

Integration:
- All advanced effects support undo/redo via EffectCommand
- Effects accessible via command palette and side panel
- Selection-based processing support
- Toast notifications for all effects

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 20:03:40 +01:00

129 lines
3.6 KiB
TypeScript

/**
* Utilities for applying effects to audio selections
*/
import type { Selection } from '@/types/selection';
import { getAudioContext } from '../context';
/**
* Extract a region from an audio buffer
*/
export function extractRegion(
buffer: AudioBuffer,
startTime: number,
endTime: number
): AudioBuffer {
const audioContext = getAudioContext();
const sampleRate = buffer.sampleRate;
const numberOfChannels = buffer.numberOfChannels;
const startSample = Math.floor(startTime * sampleRate);
const endSample = Math.floor(endTime * sampleRate);
const length = endSample - startSample;
const regionBuffer = audioContext.createBuffer(
numberOfChannels,
length,
sampleRate
);
for (let channel = 0; channel < numberOfChannels; channel++) {
const sourceData = buffer.getChannelData(channel);
const targetData = regionBuffer.getChannelData(channel);
for (let i = 0; i < length; i++) {
targetData[i] = sourceData[startSample + i];
}
}
return regionBuffer;
}
/**
* Replace a region in an audio buffer with processed audio
*/
export function replaceRegion(
originalBuffer: AudioBuffer,
processedRegion: AudioBuffer,
startTime: number
): AudioBuffer {
const audioContext = getAudioContext();
const sampleRate = originalBuffer.sampleRate;
const numberOfChannels = originalBuffer.numberOfChannels;
// Create new buffer with same length as original
const newBuffer = audioContext.createBuffer(
numberOfChannels,
originalBuffer.length,
sampleRate
);
const startSample = Math.floor(startTime * sampleRate);
for (let channel = 0; channel < numberOfChannels; channel++) {
const originalData = originalBuffer.getChannelData(channel);
const processedData = processedRegion.getChannelData(channel);
const newData = newBuffer.getChannelData(channel);
// Copy everything from original
for (let i = 0; i < originalBuffer.length; i++) {
newData[i] = originalData[i];
}
// Replace the selected region with processed data
for (let i = 0; i < processedRegion.length; i++) {
if (startSample + i < newBuffer.length) {
newData[startSample + i] = processedData[i];
}
}
}
return newBuffer;
}
/**
* Apply an effect function to a selection, or entire buffer if no selection
*/
export function applyEffectToSelection(
buffer: AudioBuffer,
selection: Selection | null,
effectFn: (buffer: AudioBuffer) => AudioBuffer
): AudioBuffer {
if (!selection || selection.start === selection.end) {
// No selection, apply to entire buffer
return effectFn(buffer);
}
// Extract the selected region
const region = extractRegion(buffer, selection.start, selection.end);
// Apply effect to the region
const processedRegion = effectFn(region);
// Replace the region in the original buffer
return replaceRegion(buffer, processedRegion, selection.start);
}
/**
* Apply an async effect function to a selection, or entire buffer if no selection
*/
export async function applyAsyncEffectToSelection(
buffer: AudioBuffer,
selection: Selection | null,
effectFn: (buffer: AudioBuffer) => Promise<AudioBuffer>
): Promise<AudioBuffer> {
if (!selection || selection.start === selection.end) {
// No selection, apply to entire buffer
return await effectFn(buffer);
}
// Extract the selected region
const region = extractRegion(buffer, selection.start, selection.end);
// Apply effect to the region
const processedRegion = await effectFn(region);
// Replace the region in the original buffer
return replaceRegion(buffer, processedRegion, selection.start);
}