split 4/4: player.js (the state machine and the one <audio>, moved as-is), ui/player.js, ui/library.js, ui/sheet.js, ui/taste.js, mashup.js and app.js (boot, window.g, brain.js globals); index.html is markup + CSS + two script tags

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
This commit is contained in:
Claude Fable 5.1 committed 2026-10-21 22:31:34 +00:00
1 parent cc6c971423
commit 479e110f5e
9 files changed
+1297 -1264

No files matched your search

+42
View File
@@ -0,0 +1,42 @@
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); }
}