reactions land in time: each tap keeps where in the track it hit (and which lyric section), shows as a tick on the bar, and the DJ hears "🔥 at 0:42 (chorus)"
shots / shots (push) Successful in 2m23s
shots / shots (push) Successful in 2m23s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
This commit is contained in:
1 parent
25d97db0ab
commit
de1537050e
1 file changed
+22
-3
+22
-3
@@ -144,6 +144,7 @@ input,textarea{font:inherit;color:inherit}
|
||||
#bar::before{content:"";position:absolute;left:0;right:0;top:-14px;bottom:-14px} /* a fat finger target around a thin line */
|
||||
#bar i{display:block;height:100%;width:0;background:var(--acc);border-radius:2px;pointer-events:none}
|
||||
#bar:hover,#bar.drag{height:4px;margin:-1px 0}
|
||||
#bar em{position:absolute;top:-16px;transform:translateX(-50%);font-size:11px;font-style:normal;line-height:1;pointer-events:none;opacity:.9}
|
||||
#bar b{position:absolute;top:-28px;transform:translateX(-50%);font:11px var(--font);letter-spacing:.1em;color:var(--dim);background:var(--card);border:1px solid var(--line);border-radius:6px;padding:2px 6px;pointer-events:none;display:none;white-space:nowrap}
|
||||
#bar.drag b{display:block}
|
||||
#controls{display:flex;align-items:center;gap:18px}
|
||||
@@ -746,7 +747,7 @@ function remixText() {
|
||||
return `\nTHIS IS A REMIX REQUEST. Rework this exact track rather than writing a new one:\n- title: ${s.title}\n- tags: ${s.tags}\n- bpm ${s.bpm}, key ${s.key}, energy ${s.energy}\n- lyrics:\n${s.lyrics || "(instrumental)"}\nKeep its bones (hook, mood, most of the lyrics) and apply the listener's changes: ${likes.length ? "more " + likes.join(", ") + "; " : ""}${avoids.length ? "less or no " + avoids.join(", ") + "; " : ""}${words.length ? "in their words: " + words.join(" / ") : ""}${!likes.length && !avoids.length && !words.length ? "a fresh take on the same song (new arrangement, same heart)" : ""}. Title it as a remix of the original (e.g. "${s.title} (remix)" or a named version).\n`;
|
||||
}
|
||||
function djPrompt() {
|
||||
const prev = state.history.slice(-4).map((t, i) => `- ${t.title} [${t.tags}] ${t.bpm}bpm ${t.key} ${t.meter || "4/4"} energy ${t.energy} ${t.seconds || 120}s${t.reaction ? " ← listener: " + t.reaction : ""}`).join("\n");
|
||||
const prev = state.history.slice(-4).map((t, i) => `- ${t.title} [${t.tags}] ${t.bpm}bpm ${t.key} ${t.meter || "4/4"} energy ${t.energy} ${t.seconds || 120}s${t.reaction ? " ← listener: " + t.reaction : ""}${t.hits && t.hits.length ? " ← when: " + t.hits.map((h) => `${h.e} at ${mmss(h.t)}${h.s ? " (" + h.s + ")" : ""}`).join(", ") : ""}`).join("\n");
|
||||
return `You are the DJ of a one-listener radio station${state.djName ? ` (your name: ${state.djName})` : ""}. The listener typed a seed and the station grows from it.${remixText()}
|
||||
Seed: "${state.seed}"
|
||||
${state.station ? `Station name: ${state.station}\n` : ""}Listener asked for more of: ${state.taste.like.join("; ") || "(nothing yet)"}
|
||||
@@ -873,7 +874,7 @@ const db = {
|
||||
/* ---------- stations: a seed, its taste, and what it has played; resumable from the seed screen ---------- */
|
||||
async function saveStation() {
|
||||
if (!state.stationId) return;
|
||||
const specs = state.history.slice(-8).map(({ id, title, tags, bpm, key, energy, reaction, made, seconds, meter }) => ({ id, title, tags, bpm, key, energy, reaction, made, seconds, meter }));
|
||||
const specs = state.history.slice(-8).map(({ id, title, tags, bpm, key, energy, reaction, made, seconds, meter, hits }) => ({ id, title, tags, bpm, key, energy, reaction, made, seconds, meter, hits }));
|
||||
await db.put({ id: state.stationId, seed: state.seed, name: state.station, taste: state.taste, history: specs, made: +state.stationId.split("-")[1] || Date.now(), last: Date.now(), played: (state.played || 0), look: state.look || "rings", palette: state.palette || "duo", djName: state.djName || "", djVoice: state.djVoice || "", welcomed: !!state.welcomed }, "stations");
|
||||
}
|
||||
async function listStations() {
|
||||
@@ -1103,6 +1104,8 @@ async function playJob(job) {
|
||||
state.current = job;
|
||||
if (!job.stepping) { state.history.push(spec); state.played = (state.played || 0) + 1; spec.stationId ||= state.stationId; }
|
||||
keepLabel();
|
||||
for (const t of $("#bar").querySelectorAll("em")) t.remove();
|
||||
if (spec.hits) { const dur = spec.seconds || 120; for (const h of spec.hits) { const tick = document.createElement("em"); tick.textContent = h.e; tick.style.left = Math.min(100, h.t / dur * 100) + "%"; tick.title = `${h.e} at ${mmss(h.t)}`; $("#bar").append(tick); } }
|
||||
$("#title").textContent = spec.break ? "✱ " + spec.title : spec.title; $("#tags").textContent = `${spec.bpm} bpm · ${spec.key}${spec.break ? " · " + (spec.sponsored ? "sponsored" : "intermission") : spec.remixOf ? " · remix" : ""}`;
|
||||
$("#lyric").textContent = spec.line || "";
|
||||
showTags(spec);
|
||||
@@ -1279,10 +1282,26 @@ const EMOJI_MEANING = {
|
||||
"⚡": { like: "faster and louder", energy: +0.3 }, "🎸": { like: "more guitar" }, "🎹": { like: "more piano and keys" },
|
||||
"🚫": { avoid: "anything like the current track" },
|
||||
};
|
||||
/* where in the track a reaction landed: the second, and the lyric section playing then (sections assumed evenly spread) */
|
||||
function sectionAt(spec, t) {
|
||||
const marks = [...String(spec.lyrics || "").matchAll(/\[([a-z -]+)\]/gi)].map((m) => m[1].toLowerCase());
|
||||
if (!marks.length) return "";
|
||||
const dur = state.audio.duration || spec.seconds || 120;
|
||||
return marks[Math.min(marks.length - 1, Math.floor(t / dur * marks.length))];
|
||||
}
|
||||
const mmss = (s) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
|
||||
function markReaction(spec, emoji, t) {
|
||||
const dur = state.audio.duration || spec.seconds || 120;
|
||||
spec.hits = (spec.hits || []).concat([{ e: emoji, t: Math.round(t), s: sectionAt(spec, t) }]).slice(-24);
|
||||
const bar = $("#bar"); if (!bar || state.current.spec !== spec) return;
|
||||
const tick = document.createElement("em"); tick.textContent = emoji; tick.style.left = Math.min(100, t / dur * 100) + "%"; tick.title = `${emoji} at ${mmss(t)}`; bar.append(tick);
|
||||
}
|
||||
async function react(emoji, text) {
|
||||
const cur = state.current && state.current.spec; if (!cur) return;
|
||||
const m = EMOJI_MEANING[emoji] || (emoji ? { notes: emoji } : null);
|
||||
const who = `${cur.title} (${cur.tags.split(",").slice(0, 3).join(",")})`;
|
||||
const at = state.audio.currentTime || 0, sec = sectionAt(cur, at), when = ` at ${mmss(at)}${sec ? " (" + sec + ")" : ""}`;
|
||||
const who = `${cur.title} (${cur.tags.split(",").slice(0, 3).join(",")})${when}`;
|
||||
if (emoji) markReaction(cur, emoji, at);
|
||||
if (m) {
|
||||
if (m.like) state.taste.like.push(`${m.like} — re ${who}`);
|
||||
if (m.avoid) state.taste.avoid.push(`${m.avoid} — re ${who}`);
|
||||
|
||||
Reference in new issue
Block a user