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:
1 parent
cc6c971423
commit
479e110f5e
9 files changed
+1297
-1264
No files matched your search
@@ -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); }
|
||||
}
|
||||
Reference in new issue
Block a user