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>
This commit is contained in:
128
lib/audio/effects/selection.ts
Normal file
128
lib/audio/effects/selection.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
Reference in New Issue
Block a user