0.10.0: audio tracks — assets, per-scene lanes, editor + export playback

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-22 11:53:41 +02:00
1 parent b9f2ba9e47
commit 04f867547d
14 files changed
+2057 -42

No files matched your search

+49
View File
@@ -100,3 +100,52 @@ export async function importImageFile(
}
return { assetId: asset.id, layerId: layer.id }
}
export function isAudioFile(file: File): boolean {
if (file.type.startsWith('audio/')) return true
const lower = file.name.toLowerCase()
return lower.endsWith('.mp3') || lower.endsWith('.wav') || lower.endsWith('.ogg') || lower.endsWith('.m4a')
}
/** Decode an audio file far enough to learn its duration. Prefers an
* OfflineAudioContext (deterministic, headless) and falls back to a
* hidden `<audio>` element's `loadedmetadata` event if decoding is
* unsupported or fails (e.g. DRM-protected / exotic codecs). */
export function readAudioDuration(dataUrl: string): Promise<number> {
// Fallback path via HTMLAudioElement — lighter weight, works without
// Web Audio.
const viaMetadata = () =>
new Promise<number>((resolve, reject) => {
const audio = document.createElement('audio')
audio.preload = 'metadata'
audio.onloadedmetadata = () => {
const d = audio.duration
if (Number.isFinite(d) && d > 0) resolve(d)
else reject(new Error('audio duration unknown'))
}
audio.onerror = () => reject(new Error('audio metadata load failed'))
audio.src = dataUrl
})
return viaMetadata()
}
interface AudioImportStore {
addAudioAsset: (a: { name: string; src: string; duration: number }) => { id: string }
}
export async function importAudioFile(
store: AudioImportStore,
file: File,
): Promise<{ assetId: string } | null> {
const dataUrl = await readAsDataUrl(file)
let duration = 0
try {
duration = await readAudioDuration(dataUrl)
} catch {
// Unknown duration — store as 0. Trim logic clamps to the min visible,
// so the clip still renders; the user can re-drop the file if needed.
}
const name = file.name.replace(/\.[^.]+$/, '') || 'audio'
const asset = store.addAudioAsset({ name, src: dataUrl, duration })
return { assetId: asset.id }
}