add undo option
This commit is contained in:
@ -18,11 +18,13 @@
|
||||
import { Input } from "./lib/audio/engines/Input";
|
||||
import { createKeyboardHandler } from "./lib/utils/keyboard";
|
||||
import { parseFrequencyInput, formatFrequency } from "./lib/utils/pitch";
|
||||
import { UndoManager, type AudioState } from "./lib/utils/UndoManager";
|
||||
|
||||
let currentEngineIndex = $state(0);
|
||||
const engine = $derived(engines[currentEngineIndex]);
|
||||
const engineType = $derived(engine.getType());
|
||||
const audioService = new AudioService();
|
||||
const undoManager = new UndoManager(20);
|
||||
|
||||
let currentParams = $state<any>(null);
|
||||
let currentBuffer = $state<AudioBuffer | null>(null);
|
||||
@ -42,6 +44,7 @@
|
||||
let pitchLockInputValid = $state(true);
|
||||
let selectionStart = $state<number | null>(null);
|
||||
let selectionEnd = $state<number | null>(null);
|
||||
let canUndo = $state(false);
|
||||
|
||||
const showDuration = $derived(engineType !== 'sample');
|
||||
const showRandomButton = $derived(engineType === 'generative');
|
||||
@ -77,11 +80,50 @@
|
||||
generateRandom();
|
||||
});
|
||||
|
||||
function captureCurrentState(): AudioState | null {
|
||||
return UndoManager.captureState(
|
||||
currentBuffer,
|
||||
currentParams,
|
||||
isProcessed,
|
||||
waveformColor,
|
||||
currentEngineIndex
|
||||
);
|
||||
}
|
||||
|
||||
function pushState(): void {
|
||||
const state = captureCurrentState();
|
||||
if (state) {
|
||||
undoManager.pushState(state);
|
||||
canUndo = true;
|
||||
}
|
||||
}
|
||||
|
||||
function restoreState(state: AudioState): void {
|
||||
currentBuffer = audioService.createAudioBuffer([state.leftChannel, state.rightChannel]);
|
||||
currentParams = state.params;
|
||||
isProcessed = state.isProcessed;
|
||||
waveformColor = state.waveformColor;
|
||||
if (state.engineIndex !== currentEngineIndex) {
|
||||
currentEngineIndex = state.engineIndex;
|
||||
}
|
||||
clearSelection();
|
||||
audioService.play(currentBuffer);
|
||||
}
|
||||
|
||||
function undo(): void {
|
||||
const previousState = undoManager.undo();
|
||||
if (previousState) {
|
||||
restoreState(previousState);
|
||||
canUndo = undoManager.canUndo();
|
||||
}
|
||||
}
|
||||
|
||||
const keyboardHandler = createKeyboardHandler({
|
||||
onMutate: mutate,
|
||||
onRandom: generateRandom,
|
||||
onProcess: processSound,
|
||||
onDownload: download,
|
||||
onUndo: undo,
|
||||
onDurationDecrease: (large) => {
|
||||
duration = Math.max(0.05, duration - (large ? 1 : 0.05));
|
||||
},
|
||||
@ -104,6 +146,8 @@
|
||||
});
|
||||
|
||||
function generateRandom() {
|
||||
pushState();
|
||||
|
||||
currentParams = engine.randomParams(pitchLock);
|
||||
waveformColor = generateRandomColor();
|
||||
isProcessed = false;
|
||||
@ -116,6 +160,8 @@
|
||||
generateRandom();
|
||||
return;
|
||||
}
|
||||
pushState();
|
||||
|
||||
currentParams = engine.mutateParams(currentParams, 0.15, pitchLock);
|
||||
waveformColor = generateRandomColor();
|
||||
regenerateBuffer();
|
||||
@ -166,6 +212,8 @@
|
||||
async function applyProcessor(processor: AudioProcessor) {
|
||||
if (!currentBuffer) return;
|
||||
|
||||
pushState();
|
||||
|
||||
let processedLeft: Float32Array;
|
||||
let processedRight: Float32Array;
|
||||
|
||||
@ -194,6 +242,8 @@
|
||||
}
|
||||
|
||||
function switchEngine(index: number) {
|
||||
pushState();
|
||||
|
||||
currentEngineIndex = index;
|
||||
currentBuffer = null;
|
||||
currentParams = null;
|
||||
@ -214,6 +264,8 @@
|
||||
async function loadAudioFile(file: File) {
|
||||
if (!(engine instanceof Sample)) return;
|
||||
|
||||
pushState();
|
||||
|
||||
try {
|
||||
await engine.loadFile(file);
|
||||
currentParams = engine.randomParams(pitchLock);
|
||||
@ -229,6 +281,8 @@
|
||||
async function recordAudio() {
|
||||
if (!(engine instanceof Input) || isRecording) return;
|
||||
|
||||
pushState();
|
||||
|
||||
try {
|
||||
isRecording = true;
|
||||
await engine.record(duration);
|
||||
@ -299,6 +353,8 @@
|
||||
function cropSelection() {
|
||||
if (!currentBuffer || selectionStart === null || selectionEnd === null) return;
|
||||
|
||||
pushState();
|
||||
|
||||
const start = Math.min(selectionStart, selectionEnd);
|
||||
const end = Math.max(selectionStart, selectionEnd);
|
||||
|
||||
@ -313,6 +369,8 @@
|
||||
function cutSelection() {
|
||||
if (!currentBuffer || selectionStart === null || selectionEnd === null) return;
|
||||
|
||||
pushState();
|
||||
|
||||
const start = Math.min(selectionStart, selectionEnd);
|
||||
const end = Math.max(selectionStart, selectionEnd);
|
||||
|
||||
@ -479,6 +537,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<button onclick={download}>Download (D)</button>
|
||||
<button onclick={undo} disabled={!canUndo}>Undo (Z)</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
64
src/lib/utils/UndoManager.ts
Normal file
64
src/lib/utils/UndoManager.ts
Normal file
@ -0,0 +1,64 @@
|
||||
export interface AudioState {
|
||||
leftChannel: Float32Array;
|
||||
rightChannel: Float32Array;
|
||||
params: any;
|
||||
isProcessed: boolean;
|
||||
waveformColor: string;
|
||||
engineIndex: number;
|
||||
}
|
||||
|
||||
export class UndoManager {
|
||||
private undoStack: AudioState[] = [];
|
||||
private readonly maxHistorySize: number;
|
||||
|
||||
constructor(maxHistorySize: number = 20) {
|
||||
this.maxHistorySize = maxHistorySize;
|
||||
}
|
||||
|
||||
pushState(state: AudioState): void {
|
||||
this.undoStack.push({
|
||||
leftChannel: state.leftChannel.slice(),
|
||||
rightChannel: state.rightChannel.slice(),
|
||||
params: state.params,
|
||||
isProcessed: state.isProcessed,
|
||||
waveformColor: state.waveformColor,
|
||||
engineIndex: state.engineIndex,
|
||||
});
|
||||
|
||||
if (this.undoStack.length > this.maxHistorySize) {
|
||||
this.undoStack.shift();
|
||||
}
|
||||
}
|
||||
|
||||
undo(): AudioState | null {
|
||||
const previousState = this.undoStack.pop();
|
||||
return previousState || null;
|
||||
}
|
||||
|
||||
canUndo(): boolean {
|
||||
return this.undoStack.length > 0;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.undoStack = [];
|
||||
}
|
||||
|
||||
static captureState(
|
||||
buffer: AudioBuffer | null,
|
||||
params: any,
|
||||
isProcessed: boolean,
|
||||
waveformColor: string,
|
||||
engineIndex: number
|
||||
): AudioState | null {
|
||||
if (!buffer) return null;
|
||||
|
||||
return {
|
||||
leftChannel: buffer.getChannelData(0).slice(),
|
||||
rightChannel: buffer.getChannelData(1).slice(),
|
||||
params,
|
||||
isProcessed,
|
||||
waveformColor,
|
||||
engineIndex,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ export interface KeyboardActions {
|
||||
onVolumeDecrease?: (large: boolean) => void;
|
||||
onVolumeIncrease?: (large: boolean) => void;
|
||||
onEscape?: () => void;
|
||||
onUndo?: () => void;
|
||||
}
|
||||
|
||||
export function createKeyboardHandler(actions: KeyboardActions) {
|
||||
@ -18,6 +19,9 @@ export function createKeyboardHandler(actions: KeyboardActions) {
|
||||
const isLargeAdjustment = event.shiftKey;
|
||||
|
||||
switch (key) {
|
||||
case 'z':
|
||||
actions.onUndo?.();
|
||||
break;
|
||||
case 'm':
|
||||
actions.onMutate?.();
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user