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>
161 lines
4.8 KiB
TypeScript
161 lines
4.8 KiB
TypeScript
/**
|
|
* Memory limit checking utilities for audio file handling
|
|
*/
|
|
|
|
export interface MemoryCheckResult {
|
|
allowed: boolean;
|
|
warning?: string;
|
|
estimatedMemoryMB: number;
|
|
availableMemoryMB?: number;
|
|
}
|
|
|
|
/**
|
|
* Estimate memory required for an audio buffer
|
|
* @param duration Duration in seconds
|
|
* @param sampleRate Sample rate (default: 48000 Hz)
|
|
* @param channels Number of channels (default: 2 for stereo)
|
|
* @returns Estimated memory in MB
|
|
*/
|
|
export function estimateAudioMemory(
|
|
duration: number,
|
|
sampleRate: number = 48000,
|
|
channels: number = 2
|
|
): number {
|
|
// Each sample is a 32-bit float (4 bytes)
|
|
const bytesPerSample = 4;
|
|
const totalSamples = duration * sampleRate * channels;
|
|
const bytes = totalSamples * bytesPerSample;
|
|
|
|
// Convert to MB
|
|
return bytes / (1024 * 1024);
|
|
}
|
|
|
|
/**
|
|
* Get available device memory if supported
|
|
* @returns Available memory in MB, or undefined if not supported
|
|
*/
|
|
export function getAvailableMemory(): number | undefined {
|
|
if (typeof navigator === 'undefined') return undefined;
|
|
|
|
// @ts-ignore - deviceMemory is not in TypeScript types yet
|
|
const deviceMemory = navigator.deviceMemory;
|
|
if (typeof deviceMemory === 'number') {
|
|
// deviceMemory is in GB, convert to MB
|
|
return deviceMemory * 1024;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Check if a file size is within safe memory limits
|
|
* @param fileSizeBytes File size in bytes
|
|
* @returns Memory check result
|
|
*/
|
|
export function checkFileMemoryLimit(fileSizeBytes: number): MemoryCheckResult {
|
|
// Estimate memory usage (audio files decompress to ~10x their size)
|
|
const estimatedMemoryMB = (fileSizeBytes / (1024 * 1024)) * 10;
|
|
const availableMemoryMB = getAvailableMemory();
|
|
|
|
// Conservative limits
|
|
const WARN_THRESHOLD_MB = 100; // Warn if file will use > 100MB
|
|
const MAX_RECOMMENDED_MB = 500; // Don't recommend files > 500MB
|
|
|
|
if (estimatedMemoryMB > MAX_RECOMMENDED_MB) {
|
|
return {
|
|
allowed: false,
|
|
warning: `This file may require ${Math.round(estimatedMemoryMB)}MB of memory. ` +
|
|
`Files larger than ${MAX_RECOMMENDED_MB}MB are not recommended as they may cause performance issues or crashes.`,
|
|
estimatedMemoryMB,
|
|
availableMemoryMB,
|
|
};
|
|
}
|
|
|
|
if (estimatedMemoryMB > WARN_THRESHOLD_MB) {
|
|
const warning = availableMemoryMB
|
|
? `This file will require approximately ${Math.round(estimatedMemoryMB)}MB of memory. ` +
|
|
`Your device has ${Math.round(availableMemoryMB)}MB available.`
|
|
: `This file will require approximately ${Math.round(estimatedMemoryMB)}MB of memory. ` +
|
|
`Large files may cause performance issues on devices with limited memory.`;
|
|
|
|
return {
|
|
allowed: true,
|
|
warning,
|
|
estimatedMemoryMB,
|
|
availableMemoryMB,
|
|
};
|
|
}
|
|
|
|
return {
|
|
allowed: true,
|
|
estimatedMemoryMB,
|
|
availableMemoryMB,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if an audio buffer is within safe memory limits
|
|
* @param duration Duration in seconds
|
|
* @param sampleRate Sample rate
|
|
* @param channels Number of channels
|
|
* @returns Memory check result
|
|
*/
|
|
export function checkAudioBufferMemoryLimit(
|
|
duration: number,
|
|
sampleRate: number = 48000,
|
|
channels: number = 2
|
|
): MemoryCheckResult {
|
|
const estimatedMemoryMB = estimateAudioMemory(duration, sampleRate, channels);
|
|
const availableMemoryMB = getAvailableMemory();
|
|
|
|
const WARN_THRESHOLD_MB = 100;
|
|
const MAX_RECOMMENDED_MB = 500;
|
|
|
|
if (estimatedMemoryMB > MAX_RECOMMENDED_MB) {
|
|
return {
|
|
allowed: false,
|
|
warning: `This audio (${Math.round(duration / 60)} minutes) will require ${Math.round(estimatedMemoryMB)}MB of memory. ` +
|
|
`Audio longer than ${Math.round((MAX_RECOMMENDED_MB / sampleRate / channels / 4) / 60)} minutes may cause performance issues.`,
|
|
estimatedMemoryMB,
|
|
availableMemoryMB,
|
|
};
|
|
}
|
|
|
|
if (estimatedMemoryMB > WARN_THRESHOLD_MB) {
|
|
const warning = availableMemoryMB
|
|
? `This audio will require approximately ${Math.round(estimatedMemoryMB)}MB of memory. ` +
|
|
`Your device has ${Math.round(availableMemoryMB)}MB available.`
|
|
: `This audio will require approximately ${Math.round(estimatedMemoryMB)}MB of memory.`;
|
|
|
|
return {
|
|
allowed: true,
|
|
warning,
|
|
estimatedMemoryMB,
|
|
availableMemoryMB,
|
|
};
|
|
}
|
|
|
|
return {
|
|
allowed: true,
|
|
estimatedMemoryMB,
|
|
availableMemoryMB,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Format memory size in human-readable format
|
|
* @param bytes Size in bytes
|
|
* @returns Formatted string (e.g., "1.5 MB", "250 KB")
|
|
*/
|
|
export function formatMemorySize(bytes: number): string {
|
|
if (bytes < 1024) {
|
|
return `${bytes} B`;
|
|
} else if (bytes < 1024 * 1024) {
|
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
} else if (bytes < 1024 * 1024 * 1024) {
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
} else {
|
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
}
|
|
}
|