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:
1 parent
b9f2ba9e47
commit
04f867547d
14 files changed
+2057
-42
No files matched your search
@@ -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 }
|
||||
}
|
||||
Reference in new issue
Block a user