Added comprehensive mobile support for Phase 15 (Polish & Optimization): **Mobile Layout Enhancements:** - Track controls now collapsible on mobile with two states: - Collapsed: minimal controls with expand chevron, R/M/S buttons, horizontal level meter - Expanded: full height fader, pan control, all buttons - Track collapse buttons added to mobile view (left chevron for track collapse, right chevron for control collapse) - Master controls collapse button hidden on desktop (lg:hidden) - Automation and effects bars now available on mobile layout - Both bars collapsible with eye/eye-off icons, horizontally scrollable when zoomed - Mobile vertical stacking: controls → waveform → automation → effects per track **Bug Fixes:** - Fixed track controls and waveform container height matching on desktop - Fixed Modal component prop: isOpen → open in all dialog components - Fixed TypeScript null check for audioBuffer.duration - Fixed keyboard shortcut category: 'help' → 'view' **Technical Improvements:** - Consistent height calculation using trackHeight variable - Proper responsive breakpoints with Tailwind (sm:640px, lg:1024px) - Progressive disclosure pattern for mobile controls 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
150 lines
3.2 KiB
TypeScript
150 lines
3.2 KiB
TypeScript
/**
|
|
* Audio cleanup utilities to prevent memory leaks
|
|
*/
|
|
|
|
/**
|
|
* Safely disconnect and cleanup an AudioNode
|
|
*/
|
|
export function cleanupAudioNode(node: AudioNode | null | undefined): void {
|
|
if (!node) return;
|
|
|
|
try {
|
|
node.disconnect();
|
|
} catch (error) {
|
|
// Node may already be disconnected, ignore error
|
|
console.debug('AudioNode cleanup error (expected):', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleanup multiple audio nodes
|
|
*/
|
|
export function cleanupAudioNodes(nodes: Array<AudioNode | null | undefined>): void {
|
|
nodes.forEach(cleanupAudioNode);
|
|
}
|
|
|
|
/**
|
|
* Safely stop and cleanup an AudioBufferSourceNode
|
|
*/
|
|
export function cleanupAudioSource(source: AudioBufferSourceNode | null | undefined): void {
|
|
if (!source) return;
|
|
|
|
try {
|
|
source.stop();
|
|
} catch (error) {
|
|
// Source may already be stopped, ignore error
|
|
console.debug('AudioSource stop error (expected):', error);
|
|
}
|
|
|
|
cleanupAudioNode(source);
|
|
}
|
|
|
|
/**
|
|
* Cleanup canvas and release resources
|
|
*/
|
|
export function cleanupCanvas(canvas: HTMLCanvasElement | null | undefined): void {
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
if (ctx) {
|
|
// Clear the canvas
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
// Reset transform
|
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
}
|
|
|
|
// Release context (helps with memory)
|
|
canvas.width = 0;
|
|
canvas.height = 0;
|
|
}
|
|
|
|
/**
|
|
* Cancel animation frame safely
|
|
*/
|
|
export function cleanupAnimationFrame(frameId: number | null | undefined): void {
|
|
if (frameId !== null && frameId !== undefined) {
|
|
cancelAnimationFrame(frameId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleanup media stream tracks
|
|
*/
|
|
export function cleanupMediaStream(stream: MediaStream | null | undefined): void {
|
|
if (!stream) return;
|
|
|
|
stream.getTracks().forEach(track => {
|
|
track.stop();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a cleanup registry for managing multiple cleanup tasks
|
|
*/
|
|
export class CleanupRegistry {
|
|
private cleanupTasks: Array<() => void> = [];
|
|
|
|
/**
|
|
* Register a cleanup task
|
|
*/
|
|
register(cleanup: () => void): void {
|
|
this.cleanupTasks.push(cleanup);
|
|
}
|
|
|
|
/**
|
|
* Register an audio node for cleanup
|
|
*/
|
|
registerAudioNode(node: AudioNode): void {
|
|
this.register(() => cleanupAudioNode(node));
|
|
}
|
|
|
|
/**
|
|
* Register an audio source for cleanup
|
|
*/
|
|
registerAudioSource(source: AudioBufferSourceNode): void {
|
|
this.register(() => cleanupAudioSource(source));
|
|
}
|
|
|
|
/**
|
|
* Register a canvas for cleanup
|
|
*/
|
|
registerCanvas(canvas: HTMLCanvasElement): void {
|
|
this.register(() => cleanupCanvas(canvas));
|
|
}
|
|
|
|
/**
|
|
* Register an animation frame for cleanup
|
|
*/
|
|
registerAnimationFrame(frameId: number): void {
|
|
this.register(() => cleanupAnimationFrame(frameId));
|
|
}
|
|
|
|
/**
|
|
* Register a media stream for cleanup
|
|
*/
|
|
registerMediaStream(stream: MediaStream): void {
|
|
this.register(() => cleanupMediaStream(stream));
|
|
}
|
|
|
|
/**
|
|
* Execute all cleanup tasks and clear the registry
|
|
*/
|
|
cleanup(): void {
|
|
this.cleanupTasks.forEach(task => {
|
|
try {
|
|
task();
|
|
} catch (error) {
|
|
console.error('Cleanup task failed:', error);
|
|
}
|
|
});
|
|
this.cleanupTasks = [];
|
|
}
|
|
|
|
/**
|
|
* Get the number of registered cleanup tasks
|
|
*/
|
|
get size(): number {
|
|
return this.cleanupTasks.length;
|
|
}
|
|
}
|