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