import { $, asDataUrl, state, toast } from "./core.js"; import { wavBlob } from "./render.js"; import { askMash } from "./dj.js"; import { makeJob } from "./app.js"; const MASH_MAX = 3, MASH_SR = 22050; async function mashReference(blobs, seconds) { const AC = window.OfflineAudioContext || window.webkitOfflineAudioContext; if (!AC) return null; const probe = new AC(1, 1, MASH_SR); const bufs = []; for (const b of blobs) { try { bufs.push(await probe.decodeAudioData(await b.arrayBuffer())); } catch {} } if (bufs.length < 2) return null; const per = seconds / bufs.length, fade = 2, total = seconds; const ctx = new AC(1, Math.ceil(total * MASH_SR), MASH_SR); let at = 0; bufs.forEach((buf, i) => { const src = ctx.createBufferSource(); src.buffer = buf; const g = ctx.createGain(); src.connect(g); g.connect(ctx.destination); const start = Math.max(0, at - (i ? fade : 0)), len = Math.min(buf.duration, per + fade); const from = Math.max(0, Math.min(buf.duration - len, buf.duration * 0.25)); // skip the intro, take the meat g.gain.setValueAtTime(i ? 0 : 1, start); if (i) g.gain.linearRampToValueAtTime(1, start + fade); g.gain.setValueAtTime(1, start + len - fade); g.gain.linearRampToValueAtTime(0, start + len); src.start(start, from, len); at = start + len; }); const out = await ctx.startRendering(); const ch = out.getChannelData(0), pcm = new Int16Array(ch.length); for (let i = 0; i < ch.length; i++) pcm[i] = Math.max(-1, Math.min(1, ch[i])) * 32767; return asDataUrl(wavBlob(pcm, MASH_SR)); } export async function mashup(rows) { rows = rows.filter((t) => t.blob && !t.spec.break).slice(0, MASH_MAX); if (rows.length < 2) { toast("A mashup needs at least two tracks."); return; } if (!state.stationId) { toast("Open a station first; the mash goes on it."); return; } toast(`Mashing ${rows.length} tracks — it goes next.`, 4000); try { const [spec, ref] = await Promise.all([askMash(rows), mashReference(rows.map((t) => t.blob), Math.min(120, 30 * rows.length))]); if (ref) spec.refAudio = ref; const job = makeJob(spec); if (state.nextJob && !state.nextJob.blob) state.after = state.nextJob; state.nextJob = job; $("#next").textContent = "NEXT · " + spec.title.toUpperCase() + " · MASHUP"; } catch (e) { toast(e.message, 5000); } }