Region processing
This commit is contained in:
@ -10,6 +10,7 @@
|
|||||||
import { AudioService } from "./lib/audio/services/AudioService";
|
import { AudioService } from "./lib/audio/services/AudioService";
|
||||||
import { downloadWAV } from "./lib/audio/utils/WAVEncoder";
|
import { downloadWAV } from "./lib/audio/utils/WAVEncoder";
|
||||||
import { loadVolume, saveVolume, loadDuration, saveDuration, loadPitchLockEnabled, savePitchLockEnabled, loadPitchLockFrequency, savePitchLockFrequency } from "./lib/utils/settings";
|
import { loadVolume, saveVolume, loadDuration, saveDuration, loadPitchLockEnabled, savePitchLockEnabled, loadPitchLockFrequency, savePitchLockFrequency } from "./lib/utils/settings";
|
||||||
|
import { cropAudio, cutAudio, processSelection } from "./lib/audio/utils/AudioEdit";
|
||||||
import { generateRandomColor } from "./lib/utils/colors";
|
import { generateRandomColor } from "./lib/utils/colors";
|
||||||
import { getRandomProcessor } from "./lib/audio/processors/registry";
|
import { getRandomProcessor } from "./lib/audio/processors/registry";
|
||||||
import type { AudioProcessor } from "./lib/audio/processors/AudioProcessor";
|
import type { AudioProcessor } from "./lib/audio/processors/AudioProcessor";
|
||||||
@ -39,6 +40,8 @@
|
|||||||
let pitchLockFrequency = $state(loadPitchLockFrequency());
|
let pitchLockFrequency = $state(loadPitchLockFrequency());
|
||||||
let pitchLockInput = $state(formatFrequency(loadPitchLockFrequency()));
|
let pitchLockInput = $state(formatFrequency(loadPitchLockFrequency()));
|
||||||
let pitchLockInputValid = $state(true);
|
let pitchLockInputValid = $state(true);
|
||||||
|
let selectionStart = $state<number | null>(null);
|
||||||
|
let selectionEnd = $state<number | null>(null);
|
||||||
|
|
||||||
const showDuration = $derived(engineType !== 'sample');
|
const showDuration = $derived(engineType !== 'sample');
|
||||||
const showRandomButton = $derived(engineType === 'generative');
|
const showRandomButton = $derived(engineType === 'generative');
|
||||||
@ -47,6 +50,8 @@
|
|||||||
const showMutateButton = $derived(engineType === 'generative' && !isProcessed && currentBuffer);
|
const showMutateButton = $derived(engineType === 'generative' && !isProcessed && currentBuffer);
|
||||||
const showPitchLock = $derived(engineType === 'generative');
|
const showPitchLock = $derived(engineType === 'generative');
|
||||||
const pitchLock = $derived<PitchLock>({ enabled: pitchLockEnabled, frequency: pitchLockFrequency });
|
const pitchLock = $derived<PitchLock>({ enabled: pitchLockEnabled, frequency: pitchLockFrequency });
|
||||||
|
const hasSelection = $derived(selectionStart !== null && selectionEnd !== null && currentBuffer !== null);
|
||||||
|
const showEditButtons = $derived(hasSelection);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
audioService.setVolume(volume);
|
audioService.setVolume(volume);
|
||||||
@ -89,13 +94,20 @@
|
|||||||
onVolumeIncrease: (large) => {
|
onVolumeIncrease: (large) => {
|
||||||
volume = Math.min(1, volume + (large ? 0.2 : 0.05));
|
volume = Math.min(1, volume + (large ? 0.2 : 0.05));
|
||||||
},
|
},
|
||||||
onEscape: () => showModal && closeModal(),
|
onEscape: () => {
|
||||||
|
if (hasSelection) {
|
||||||
|
clearSelection();
|
||||||
|
} else if (showModal) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function generateRandom() {
|
function generateRandom() {
|
||||||
currentParams = engine.randomParams(pitchLock);
|
currentParams = engine.randomParams(pitchLock);
|
||||||
waveformColor = generateRandomColor();
|
waveformColor = generateRandomColor();
|
||||||
isProcessed = false;
|
isProcessed = false;
|
||||||
|
clearSelection();
|
||||||
regenerateBuffer();
|
regenerateBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,9 +166,26 @@
|
|||||||
async function applyProcessor(processor: AudioProcessor) {
|
async function applyProcessor(processor: AudioProcessor) {
|
||||||
if (!currentBuffer) return;
|
if (!currentBuffer) return;
|
||||||
|
|
||||||
|
let processedLeft: Float32Array;
|
||||||
|
let processedRight: Float32Array;
|
||||||
|
|
||||||
|
if (hasSelection) {
|
||||||
|
const start = Math.min(selectionStart!, selectionEnd!);
|
||||||
|
const end = Math.max(selectionStart!, selectionEnd!);
|
||||||
|
const sampleRate = audioService.getSampleRate();
|
||||||
|
|
||||||
|
[processedLeft, processedRight] = await processSelection(
|
||||||
|
currentBuffer,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
processor,
|
||||||
|
sampleRate
|
||||||
|
);
|
||||||
|
} else {
|
||||||
const leftChannel = currentBuffer.getChannelData(0);
|
const leftChannel = currentBuffer.getChannelData(0);
|
||||||
const rightChannel = currentBuffer.getChannelData(1);
|
const rightChannel = currentBuffer.getChannelData(1);
|
||||||
const [processedLeft, processedRight] = await processor.process(leftChannel, rightChannel);
|
[processedLeft, processedRight] = await processor.process(leftChannel, rightChannel);
|
||||||
|
}
|
||||||
|
|
||||||
currentBuffer = audioService.createAudioBuffer([processedLeft, processedRight]);
|
currentBuffer = audioService.createAudioBuffer([processedLeft, processedRight]);
|
||||||
isProcessed = true;
|
isProcessed = true;
|
||||||
@ -169,6 +198,7 @@
|
|||||||
currentBuffer = null;
|
currentBuffer = null;
|
||||||
currentParams = null;
|
currentParams = null;
|
||||||
isProcessed = false;
|
isProcessed = false;
|
||||||
|
clearSelection();
|
||||||
|
|
||||||
if (engineType === 'generative') {
|
if (engineType === 'generative') {
|
||||||
generateRandom();
|
generateRandom();
|
||||||
@ -256,6 +286,44 @@
|
|||||||
showProcessorPopup = false;
|
showProcessorPopup = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSelectionChange(start: number | null, end: number | null) {
|
||||||
|
selectionStart = start;
|
||||||
|
selectionEnd = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelection() {
|
||||||
|
selectionStart = null;
|
||||||
|
selectionEnd = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cropSelection() {
|
||||||
|
if (!currentBuffer || selectionStart === null || selectionEnd === null) return;
|
||||||
|
|
||||||
|
const start = Math.min(selectionStart, selectionEnd);
|
||||||
|
const end = Math.max(selectionStart, selectionEnd);
|
||||||
|
|
||||||
|
const sampleRate = audioService.getSampleRate();
|
||||||
|
const [newLeft, newRight] = cropAudio(currentBuffer, start, end, sampleRate);
|
||||||
|
|
||||||
|
currentBuffer = audioService.createAudioBuffer([newLeft, newRight]);
|
||||||
|
clearSelection();
|
||||||
|
audioService.play(currentBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cutSelection() {
|
||||||
|
if (!currentBuffer || selectionStart === null || selectionEnd === null) return;
|
||||||
|
|
||||||
|
const start = Math.min(selectionStart, selectionEnd);
|
||||||
|
const end = Math.max(selectionStart, selectionEnd);
|
||||||
|
|
||||||
|
const sampleRate = audioService.getSampleRate();
|
||||||
|
const [newLeft, newRight] = cutAudio(currentBuffer, start, end, sampleRate);
|
||||||
|
|
||||||
|
currentBuffer = audioService.createAudioBuffer([newLeft, newRight]);
|
||||||
|
clearSelection();
|
||||||
|
audioService.play(currentBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
async function closeModal() {
|
async function closeModal() {
|
||||||
showModal = false;
|
showModal = false;
|
||||||
await audioService.initialize();
|
await audioService.initialize();
|
||||||
@ -375,6 +443,9 @@
|
|||||||
buffer={currentBuffer}
|
buffer={currentBuffer}
|
||||||
color={waveformColor}
|
color={waveformColor}
|
||||||
{playbackPosition}
|
{playbackPosition}
|
||||||
|
{selectionStart}
|
||||||
|
{selectionEnd}
|
||||||
|
onselectionchange={handleSelectionChange}
|
||||||
onclick={replaySound}
|
onclick={replaySound}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
@ -391,6 +462,10 @@
|
|||||||
{#if showMutateButton}
|
{#if showMutateButton}
|
||||||
<button onclick={mutate}>Mutate (M)</button>
|
<button onclick={mutate}>Mutate (M)</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if showEditButtons}
|
||||||
|
<button onclick={cropSelection}>Crop</button>
|
||||||
|
<button onclick={cutSelection}>Cut</button>
|
||||||
|
{/if}
|
||||||
{#if currentBuffer}
|
{#if currentBuffer}
|
||||||
<div
|
<div
|
||||||
class="process-button-container"
|
class="process-button-container"
|
||||||
|
|||||||
193
src/lib/audio/utils/AudioEdit.ts
Normal file
193
src/lib/audio/utils/AudioEdit.ts
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import type { AudioProcessor } from "../processors/AudioProcessor";
|
||||||
|
|
||||||
|
const CROSSFADE_SAMPLES = 256;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply equal-power crossfade between two regions
|
||||||
|
*/
|
||||||
|
function applyCrossfade(
|
||||||
|
samples: Float32Array,
|
||||||
|
fadeOutStart: number,
|
||||||
|
fadeOutEnd: number,
|
||||||
|
fadeInStart: number,
|
||||||
|
fadeInEnd: number
|
||||||
|
): void {
|
||||||
|
const fadeLength = Math.min(fadeOutEnd - fadeOutStart, fadeInEnd - fadeInStart);
|
||||||
|
|
||||||
|
for (let i = 0; i < fadeLength; i++) {
|
||||||
|
const t = i / fadeLength;
|
||||||
|
const fadeOutGain = Math.cos(t * Math.PI * 0.5);
|
||||||
|
const fadeInGain = Math.sin(t * Math.PI * 0.5);
|
||||||
|
|
||||||
|
const fadeOutIdx = fadeOutStart + i;
|
||||||
|
const fadeInIdx = fadeInStart + i;
|
||||||
|
|
||||||
|
if (fadeOutIdx < samples.length && fadeInIdx < samples.length) {
|
||||||
|
const fadeOutSample = samples[fadeOutIdx] * fadeOutGain;
|
||||||
|
const fadeInSample = samples[fadeInIdx] * fadeInGain;
|
||||||
|
samples[fadeOutIdx] = fadeOutSample + fadeInSample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a portion of the audio buffer (crop to selection)
|
||||||
|
*/
|
||||||
|
export function cropAudio(
|
||||||
|
buffer: AudioBuffer,
|
||||||
|
startSample: number,
|
||||||
|
endSample: number,
|
||||||
|
sampleRate: number
|
||||||
|
): [Float32Array, Float32Array] {
|
||||||
|
const start = Math.max(0, Math.floor(startSample));
|
||||||
|
const end = Math.min(buffer.length, Math.ceil(endSample));
|
||||||
|
const length = end - start;
|
||||||
|
|
||||||
|
const leftChannel = buffer.getChannelData(0);
|
||||||
|
const rightChannel = buffer.getChannelData(1);
|
||||||
|
|
||||||
|
const newLeft = new Float32Array(length);
|
||||||
|
const newRight = new Float32Array(length);
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
newLeft[i] = leftChannel[start + i];
|
||||||
|
newRight[i] = rightChannel[start + i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [newLeft, newRight];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a portion of the audio buffer (cut with crossfade)
|
||||||
|
*/
|
||||||
|
export function cutAudio(
|
||||||
|
buffer: AudioBuffer,
|
||||||
|
startSample: number,
|
||||||
|
endSample: number,
|
||||||
|
sampleRate: number
|
||||||
|
): [Float32Array, Float32Array] {
|
||||||
|
const start = Math.max(0, Math.floor(startSample));
|
||||||
|
const end = Math.min(buffer.length, Math.ceil(endSample));
|
||||||
|
|
||||||
|
const leftChannel = buffer.getChannelData(0);
|
||||||
|
const rightChannel = buffer.getChannelData(1);
|
||||||
|
|
||||||
|
const beforeLength = start;
|
||||||
|
const afterLength = buffer.length - end;
|
||||||
|
const newLength = beforeLength + afterLength;
|
||||||
|
|
||||||
|
const newLeft = new Float32Array(newLength);
|
||||||
|
const newRight = new Float32Array(newLength);
|
||||||
|
|
||||||
|
for (let i = 0; i < beforeLength; i++) {
|
||||||
|
newLeft[i] = leftChannel[i];
|
||||||
|
newRight[i] = rightChannel[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < afterLength; i++) {
|
||||||
|
newLeft[beforeLength + i] = leftChannel[end + i];
|
||||||
|
newRight[beforeLength + i] = rightChannel[end + i];
|
||||||
|
}
|
||||||
|
|
||||||
|
const crossfadeLength = Math.min(CROSSFADE_SAMPLES, beforeLength, afterLength);
|
||||||
|
if (crossfadeLength > 0 && beforeLength > 0 && afterLength > 0) {
|
||||||
|
const crossfadeStart = beforeLength - crossfadeLength;
|
||||||
|
|
||||||
|
for (let i = 0; i < crossfadeLength; i++) {
|
||||||
|
const t = i / crossfadeLength;
|
||||||
|
const fadeOutGain = Math.cos(t * Math.PI * 0.5);
|
||||||
|
const fadeInGain = Math.sin(t * Math.PI * 0.5);
|
||||||
|
|
||||||
|
const idx = crossfadeStart + i;
|
||||||
|
const beforeSample = newLeft[idx];
|
||||||
|
const afterSample = newLeft[beforeLength + i];
|
||||||
|
newLeft[idx] = beforeSample * fadeOutGain + afterSample * fadeInGain;
|
||||||
|
|
||||||
|
const beforeSampleR = newRight[idx];
|
||||||
|
const afterSampleR = newRight[beforeLength + i];
|
||||||
|
newRight[idx] = beforeSampleR * fadeOutGain + afterSampleR * fadeInGain;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = crossfadeLength; i < afterLength; i++) {
|
||||||
|
newLeft[beforeLength - crossfadeLength + i] = newLeft[beforeLength + i];
|
||||||
|
newRight[beforeLength - crossfadeLength + i] = newRight[beforeLength + i];
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalLength = beforeLength - crossfadeLength + afterLength;
|
||||||
|
return [
|
||||||
|
newLeft.slice(0, finalLength),
|
||||||
|
newRight.slice(0, finalLength)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [newLeft, newRight];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process only a selected region of the audio with crossfades at boundaries
|
||||||
|
*/
|
||||||
|
export async function processSelection(
|
||||||
|
buffer: AudioBuffer,
|
||||||
|
startSample: number,
|
||||||
|
endSample: number,
|
||||||
|
processor: AudioProcessor,
|
||||||
|
sampleRate: number
|
||||||
|
): Promise<[Float32Array, Float32Array]> {
|
||||||
|
const start = Math.max(0, Math.floor(startSample));
|
||||||
|
const end = Math.min(buffer.length, Math.ceil(endSample));
|
||||||
|
const selectionLength = end - start;
|
||||||
|
|
||||||
|
const leftChannel = buffer.getChannelData(0);
|
||||||
|
const rightChannel = buffer.getChannelData(1);
|
||||||
|
|
||||||
|
const selectedLeft = new Float32Array(selectionLength);
|
||||||
|
const selectedRight = new Float32Array(selectionLength);
|
||||||
|
|
||||||
|
for (let i = 0; i < selectionLength; i++) {
|
||||||
|
selectedLeft[i] = leftChannel[start + i];
|
||||||
|
selectedRight[i] = rightChannel[start + i];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [processedLeft, processedRight] = await processor.process(selectedLeft, selectedRight);
|
||||||
|
|
||||||
|
const newLeft = new Float32Array(buffer.length);
|
||||||
|
const newRight = new Float32Array(buffer.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
|
newLeft[i] = leftChannel[i];
|
||||||
|
newRight[i] = rightChannel[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < processedLeft.length; i++) {
|
||||||
|
newLeft[start + i] = processedLeft[i];
|
||||||
|
newRight[start + i] = processedRight[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxCrossfade = Math.floor(selectionLength / 4);
|
||||||
|
const crossfadeLength = Math.min(CROSSFADE_SAMPLES, start, buffer.length - end, maxCrossfade);
|
||||||
|
|
||||||
|
if (crossfadeLength > 0) {
|
||||||
|
for (let i = 0; i < crossfadeLength; i++) {
|
||||||
|
const t = i / crossfadeLength;
|
||||||
|
const unprocessedGain = Math.cos(t * Math.PI * 0.5);
|
||||||
|
const processedGain = Math.sin(t * Math.PI * 0.5);
|
||||||
|
|
||||||
|
const startIdx = start + i;
|
||||||
|
newLeft[startIdx] = leftChannel[startIdx] * unprocessedGain + processedLeft[i] * processedGain;
|
||||||
|
newRight[startIdx] = rightChannel[startIdx] * unprocessedGain + processedRight[i] * processedGain;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < crossfadeLength; i++) {
|
||||||
|
const t = i / crossfadeLength;
|
||||||
|
const processedGain = Math.cos(t * Math.PI * 0.5);
|
||||||
|
const unprocessedGain = Math.sin(t * Math.PI * 0.5);
|
||||||
|
|
||||||
|
const endIdx = end - crossfadeLength + i;
|
||||||
|
const processedIdx = processedLeft.length - crossfadeLength + i;
|
||||||
|
newLeft[endIdx] = processedLeft[processedIdx] * processedGain + leftChannel[endIdx] * unprocessedGain;
|
||||||
|
newRight[endIdx] = processedRight[processedIdx] * processedGain + rightChannel[endIdx] * unprocessedGain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [newLeft, newRight];
|
||||||
|
}
|
||||||
@ -5,11 +5,26 @@
|
|||||||
buffer: AudioBuffer | null;
|
buffer: AudioBuffer | null;
|
||||||
color?: string;
|
color?: string;
|
||||||
playbackPosition?: number;
|
playbackPosition?: number;
|
||||||
|
selectionStart?: number | null;
|
||||||
|
selectionEnd?: number | null;
|
||||||
|
onselectionchange?: (start: number | null, end: number | null) => void;
|
||||||
onclick?: () => void;
|
onclick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { buffer, color = '#646cff', playbackPosition = 0, onclick }: Props = $props();
|
let {
|
||||||
|
buffer,
|
||||||
|
color = '#646cff',
|
||||||
|
playbackPosition = 0,
|
||||||
|
selectionStart = null,
|
||||||
|
selectionEnd = null,
|
||||||
|
onselectionchange,
|
||||||
|
onclick
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement;
|
let canvas: HTMLCanvasElement;
|
||||||
|
let isDragging = $state(false);
|
||||||
|
let dragStartX = $state(0);
|
||||||
|
let hasMoved = $state(false);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
@ -28,6 +43,8 @@
|
|||||||
buffer;
|
buffer;
|
||||||
color;
|
color;
|
||||||
playbackPosition;
|
playbackPosition;
|
||||||
|
selectionStart;
|
||||||
|
selectionEnd;
|
||||||
draw();
|
draw();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -37,12 +54,120 @@
|
|||||||
canvas.height = parent.clientHeight;
|
canvas.height = parent.clientHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick(event: MouseEvent) {
|
||||||
|
if (hasMoved) return;
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const x = event.clientX - rect.left;
|
||||||
|
|
||||||
|
if (buffer && selectionStart !== null && selectionEnd !== null) {
|
||||||
|
const selStart = Math.min(selectionStart, selectionEnd);
|
||||||
|
const selEnd = Math.max(selectionStart, selectionEnd);
|
||||||
|
const width = canvas.width;
|
||||||
|
const startX = (selStart / buffer.length) * width;
|
||||||
|
const endX = (selEnd / buffer.length) * width;
|
||||||
|
|
||||||
|
if (x < startX || x > endX) {
|
||||||
|
if (onselectionchange) {
|
||||||
|
onselectionchange(null, null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (onclick) {
|
if (onclick) {
|
||||||
onclick();
|
onclick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleMouseDown(event: MouseEvent) {
|
||||||
|
if (!buffer) return;
|
||||||
|
|
||||||
|
isDragging = true;
|
||||||
|
hasMoved = false;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
dragStartX = event.clientX - rect.left;
|
||||||
|
|
||||||
|
const sample = Math.floor((dragStartX / canvas.width) * buffer.length);
|
||||||
|
if (onselectionchange) {
|
||||||
|
onselectionchange(sample, sample);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseMove(event: MouseEvent) {
|
||||||
|
if (!isDragging || !buffer) return;
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const currentX = event.clientX - rect.left;
|
||||||
|
const clampedX = Math.max(0, Math.min(canvas.width, currentX));
|
||||||
|
|
||||||
|
if (Math.abs(clampedX - dragStartX) > 2) {
|
||||||
|
hasMoved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endSample = Math.floor((clampedX / canvas.width) * buffer.length);
|
||||||
|
|
||||||
|
if (onselectionchange && selectionStart !== null) {
|
||||||
|
onselectionchange(selectionStart, endSample);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp() {
|
||||||
|
if (isDragging && selectionStart !== null && selectionEnd !== null) {
|
||||||
|
if (Math.abs(selectionEnd - selectionStart) < buffer!.sampleRate * 0.01) {
|
||||||
|
if (onselectionchange) {
|
||||||
|
onselectionchange(null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isDragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchStart(event: TouchEvent) {
|
||||||
|
if (!buffer || event.touches.length !== 1) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
isDragging = true;
|
||||||
|
hasMoved = false;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
dragStartX = event.touches[0].clientX - rect.left;
|
||||||
|
|
||||||
|
const sample = Math.floor((dragStartX / canvas.width) * buffer.length);
|
||||||
|
if (onselectionchange) {
|
||||||
|
onselectionchange(sample, sample);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchMove(event: TouchEvent) {
|
||||||
|
if (!isDragging || !buffer || event.touches.length !== 1) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const currentX = event.touches[0].clientX - rect.left;
|
||||||
|
const clampedX = Math.max(0, Math.min(canvas.width, currentX));
|
||||||
|
|
||||||
|
if (Math.abs(clampedX - dragStartX) > 2) {
|
||||||
|
hasMoved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endSample = Math.floor((clampedX / canvas.width) * buffer.length);
|
||||||
|
|
||||||
|
if (onselectionchange && selectionStart !== null) {
|
||||||
|
onselectionchange(selectionStart, endSample);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchEnd() {
|
||||||
|
if (isDragging && selectionStart !== null && selectionEnd !== null) {
|
||||||
|
if (Math.abs(selectionEnd - selectionStart) < buffer!.sampleRate * 0.01) {
|
||||||
|
if (onselectionchange) {
|
||||||
|
onselectionchange(null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isDragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
|
|
||||||
@ -102,6 +227,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (buffer && selectionStart !== null && selectionEnd !== null) {
|
||||||
|
const selStart = Math.min(selectionStart, selectionEnd);
|
||||||
|
const selEnd = Math.max(selectionStart, selectionEnd);
|
||||||
|
const startX = (selStart / buffer.length) * width;
|
||||||
|
const endX = (selEnd / buffer.length) * width;
|
||||||
|
|
||||||
|
ctx.fillStyle = 'rgba(100, 108, 255, 0.2)';
|
||||||
|
ctx.fillRect(startX, 0, endX - startX, height);
|
||||||
|
|
||||||
|
ctx.strokeStyle = 'rgba(100, 108, 255, 0.8)';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(startX, 0);
|
||||||
|
ctx.lineTo(startX, height);
|
||||||
|
ctx.moveTo(endX, 0);
|
||||||
|
ctx.lineTo(endX, height);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
if (playbackPosition >= 0 && buffer) {
|
if (playbackPosition >= 0 && buffer) {
|
||||||
const duration = buffer.length / buffer.sampleRate;
|
const duration = buffer.length / buffer.sampleRate;
|
||||||
const x = (playbackPosition / duration) * width;
|
const x = (playbackPosition / duration) * width;
|
||||||
@ -116,7 +260,17 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<canvas bind:this={canvas} onclick={handleClick} style="cursor: pointer;"></canvas>
|
<svelte:window onmouseup={handleMouseUp} ontouchend={handleTouchEnd} />
|
||||||
|
|
||||||
|
<canvas
|
||||||
|
bind:this={canvas}
|
||||||
|
onclick={handleClick}
|
||||||
|
onmousedown={handleMouseDown}
|
||||||
|
onmousemove={handleMouseMove}
|
||||||
|
ontouchstart={handleTouchStart}
|
||||||
|
ontouchmove={handleTouchMove}
|
||||||
|
style="cursor: {isDragging ? 'text' : 'pointer'}; touch-action: none;"
|
||||||
|
></canvas>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
canvas {
|
canvas {
|
||||||
|
|||||||
Reference in New Issue
Block a user