Files
site/mashup.js
T

43 lines
2.3 KiB
JavaScript

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); }
}