Add ACE-Step music generation to the studio and library.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="overflow-hidden rounded-2xl border border-white/10 bg-zinc-950/80 p-3">
|
||||
<canvas
|
||||
ref="canvasEl"
|
||||
class="h-16 w-full cursor-pointer"
|
||||
height="64"
|
||||
@click="onWaveClick"
|
||||
/>
|
||||
<input
|
||||
type="range"
|
||||
class="aigen-slider mt-2 w-full"
|
||||
min="0"
|
||||
:max="Math.max(duration, 0.01)"
|
||||
step="0.01"
|
||||
:value="current"
|
||||
:disabled="!ready"
|
||||
:aria-label="label || 'Playback'"
|
||||
@input="onScrub"
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full border border-amber-300/40 bg-amber-400/10 px-3 py-1.5 text-xs font-semibold text-amber-100 disabled:opacity-50"
|
||||
:disabled="!ready"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ playing ? 'Pause' : 'Play' }}
|
||||
</button>
|
||||
<p class="min-w-[6.5rem] text-xs tabular-nums text-zinc-400">{{ clock }}</p>
|
||||
<label class="ml-auto flex items-center gap-2 text-xs text-zinc-400">
|
||||
Vol
|
||||
<input
|
||||
v-model.number="volume"
|
||||
type="range"
|
||||
class="aigen-slider w-24"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
aria-label="Volume"
|
||||
>
|
||||
</label>
|
||||
<a
|
||||
v-if="src"
|
||||
:href="src"
|
||||
:download="downloadName"
|
||||
class="rounded-full border border-white/10 px-3 py-1.5 text-xs text-zinc-300 hover:text-white"
|
||||
>Download</a>
|
||||
</div>
|
||||
<audio
|
||||
ref="audioEl"
|
||||
:src="src || undefined"
|
||||
preload="metadata"
|
||||
class="hidden"
|
||||
@loadedmetadata="onMeta"
|
||||
@timeupdate="onTime"
|
||||
@ended="playing = false"
|
||||
@error="onError"
|
||||
/>
|
||||
<p v-if="error" class="text-xs text-red-300">{{ error }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(defineProps<{
|
||||
src: string
|
||||
downloadName?: string
|
||||
label?: string
|
||||
}>(), {
|
||||
downloadName: 'track.flac',
|
||||
label: ''
|
||||
})
|
||||
|
||||
const audioEl = ref<HTMLAudioElement | null>(null)
|
||||
const canvasEl = ref<HTMLCanvasElement | null>(null)
|
||||
const current = ref(0)
|
||||
const duration = ref(0)
|
||||
const playing = ref(false)
|
||||
const ready = ref(false)
|
||||
const error = ref('')
|
||||
const volume = ref(0.9)
|
||||
const peaks = ref<number[]>([])
|
||||
|
||||
const clock = computed(() => `${fmt(current.value)} / ${fmt(duration.value)}`)
|
||||
|
||||
function fmt(seconds: number) {
|
||||
if (!Number.isFinite(seconds) || seconds < 0) return '0:00'
|
||||
const m = Math.floor(seconds / 60)
|
||||
const s = Math.floor(seconds % 60)
|
||||
return `${m}:${String(s).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
const audio = audioEl.value
|
||||
if (!audio) return
|
||||
if (playing.value) {
|
||||
audio.pause()
|
||||
playing.value = false
|
||||
return
|
||||
}
|
||||
void audio.play().then(() => { playing.value = true }).catch(() => {
|
||||
error.value = 'Could not play this file'
|
||||
})
|
||||
}
|
||||
|
||||
function onMeta() {
|
||||
const audio = audioEl.value
|
||||
if (!audio) return
|
||||
duration.value = audio.duration || 0
|
||||
ready.value = true
|
||||
error.value = ''
|
||||
void decodeWave()
|
||||
}
|
||||
|
||||
function onTime() {
|
||||
const audio = audioEl.value
|
||||
if (!audio) return
|
||||
current.value = audio.currentTime || 0
|
||||
draw()
|
||||
}
|
||||
|
||||
function onScrub(event: Event) {
|
||||
const audio = audioEl.value
|
||||
if (!audio) return
|
||||
const next = Number((event.target as HTMLInputElement).value)
|
||||
audio.currentTime = next
|
||||
current.value = next
|
||||
draw()
|
||||
}
|
||||
|
||||
function onWaveClick(event: MouseEvent) {
|
||||
const canvas = canvasEl.value
|
||||
const audio = audioEl.value
|
||||
if (!canvas || !audio || !duration.value) return
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const ratio = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width))
|
||||
audio.currentTime = ratio * duration.value
|
||||
current.value = audio.currentTime
|
||||
draw()
|
||||
}
|
||||
|
||||
function onError() {
|
||||
ready.value = false
|
||||
error.value = 'Could not load this audio file'
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const canvas = canvasEl.value
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
const width = canvas.clientWidth || 320
|
||||
const height = canvas.height
|
||||
if (canvas.width !== width) canvas.width = width
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
const samples = peaks.value.length ? peaks.value : Array.from({ length: 80 }, () => 0.12)
|
||||
const barW = Math.max(1, width / samples.length)
|
||||
const progress = duration.value > 0 ? current.value / duration.value : 0
|
||||
samples.forEach((amp, index) => {
|
||||
const x = index * barW
|
||||
const h = Math.max(2, amp * (height - 8))
|
||||
const y = (height - h) / 2
|
||||
ctx.fillStyle = (index / samples.length) <= progress ? '#fbbf24' : 'rgba(255,255,255,0.18)'
|
||||
ctx.fillRect(x, y, Math.max(1, barW - 1), h)
|
||||
})
|
||||
}
|
||||
|
||||
async function decodeWave() {
|
||||
if (!props.src || typeof window === 'undefined') return
|
||||
try {
|
||||
const res = await fetch(props.src)
|
||||
if (!res.ok) return
|
||||
const buf = await res.arrayBuffer()
|
||||
const ctx = new AudioContext()
|
||||
const decoded = await ctx.decodeAudioData(buf.slice(0))
|
||||
await ctx.close()
|
||||
const data = decoded.getChannelData(0)
|
||||
const buckets = 120
|
||||
const size = Math.floor(data.length / buckets) || 1
|
||||
const next: number[] = []
|
||||
for (let i = 0; i < buckets; i++) {
|
||||
let sum = 0
|
||||
const start = i * size
|
||||
for (let j = 0; j < size; j++) sum += Math.abs(data[start + j] || 0)
|
||||
next.push(Math.min(1, (sum / size) * 3.2))
|
||||
}
|
||||
peaks.value = next
|
||||
draw()
|
||||
} catch {
|
||||
draw()
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.src, () => {
|
||||
current.value = 0
|
||||
duration.value = 0
|
||||
playing.value = false
|
||||
ready.value = false
|
||||
error.value = ''
|
||||
peaks.value = []
|
||||
draw()
|
||||
})
|
||||
|
||||
watch(volume, (value) => {
|
||||
if (audioEl.value) audioEl.value.volume = value
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (audioEl.value) audioEl.value.volume = volume.value
|
||||
draw()
|
||||
window.addEventListener('resize', draw)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', draw)
|
||||
audioEl.value?.pause()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user