diff --git a/index.html b/index.html index ae60444..78ee7ef 100644 --- a/index.html +++ b/index.html @@ -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}`);