From f671aa3b57571f4d87346a3f560abd6e4aaead76 Mon Sep 17 00:00:00 2001 From: b1rdmania <102524336+b1rdmania@users.noreply.github.com> Date: Fri, 26 Dec 2025 14:33:01 +0000 Subject: [PATCH] Add /play share page and X share links. Includes a UX test page and Vercel/Vite routing for share URLs. --- play.html | 197 +++++++++++ src/core/MotifEngine.ts | 5 +- src/play.ts | 144 ++++++++ ux-test.html | 753 ++++++++++++++++++++++++++++++++++++++++ vercel.json | 8 + vite.config.ts | 4 + 6 files changed, 1110 insertions(+), 1 deletion(-) create mode 100644 play.html create mode 100644 src/play.ts create mode 100644 ux-test.html diff --git a/play.html b/play.html new file mode 100644 index 0000000..58bd726 --- /dev/null +++ b/play.html @@ -0,0 +1,197 @@ + + + + + + MOTIF - Play + + + + +
+
+
+
MOTIF
+
Shared chiptune player
+
+
/play
+
+ +
+

Loading…

+
+ + +
+
+ + +
+
+ +
+
+ + + + + diff --git a/src/core/MotifEngine.ts b/src/core/MotifEngine.ts index 357f1e2..4df9739 100644 --- a/src/core/MotifEngine.ts +++ b/src/core/MotifEngine.ts @@ -27,7 +27,10 @@ export class MotifEngine { this.roleMapper = new RoleMapper(); } - async generateFromMIDI(events: NoteEvent[], transformMode: 'passthrough' | 'procedural' = 'passthrough'): Promise { + async generateFromMIDI( + events: NoteEvent[], + transformMode: 'passthrough' | 'procedural' = 'passthrough' + ): Promise { // Initialize audio context using shared unlock (iOS compatibility) if (!this.audioContext) { this.audioContext = await unlockAudio(); diff --git a/src/play.ts b/src/play.ts new file mode 100644 index 0000000..3f9fa77 --- /dev/null +++ b/src/play.ts @@ -0,0 +1,144 @@ +import { MIDIService } from './services/MIDIService'; +import { MIDIParser } from './midi/MIDIParser'; +import { MotifEngine } from './core/MotifEngine'; +import type { NoteEvent } from './types'; + +function qs(id: string): HTMLElement { + const el = document.getElementById(id); + if (!el) throw new Error(`Missing element #${id}`); + return el; +} + +function getParam(name: string): string | null { + const url = new URL(window.location.href); + return url.searchParams.get(name); +} + +function buildMidiUrlFromParams(): { midiUrl: string | null; title: string } { + const u = getParam('u'); + const src = getParam('src'); + const id = getParam('id'); + const title = getParam('title') || 'Shared MOTIF'; + + if (u) return { midiUrl: u, title }; + + // Short-link form (no server storage): reconstruct known providers. + if (src === 'bitmidi' && id && /^\d+$/.test(id)) { + return { midiUrl: `https://bitmidi.com/uploads/${id}.mid`, title }; + } + + return { midiUrl: null, title }; +} + +async function copyToClipboard(text: string): Promise { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + return; + } + + const ta = document.createElement('textarea'); + ta.value = text; + ta.style.position = 'fixed'; + ta.style.left = '-9999px'; + ta.style.top = '0'; + document.body.appendChild(ta); + ta.focus(); + ta.select(); + document.execCommand('copy'); + document.body.removeChild(ta); +} + +async function main(): Promise { + const titleEl = qs('title'); + const statusEl = qs('status'); + const playBtn = qs('playBtn') as HTMLButtonElement; + const stopBtn = qs('stopBtn') as HTMLButtonElement; + const volumeEl = qs('volume') as HTMLInputElement; + const generateOwn = qs('generateOwn') as HTMLAnchorElement; + const shareHint = qs('shareHint'); + + const { midiUrl: u, title } = buildMidiUrlFromParams(); + + titleEl.textContent = title; + shareHint.textContent = u ? 'Ready' : 'Missing MIDI URL'; + + // Set CTA back to home, prefilling search if we have a title. + generateOwn.href = title ? `/?song=${encodeURIComponent(title)}` : '/'; + + if (!u) { + statusEl.textContent = 'Missing link data (u=...).'; + playBtn.disabled = true; + stopBtn.disabled = true; + return; + } + + const midiService = new MIDIService(); + const motifEngine = new MotifEngine(); + let events: NoteEvent[] | null = null; + let isPlaying = false; + + statusEl.textContent = 'Loading MIDI…'; + try { + const buf = await midiService.fetchMIDI(u); + if (!buf) throw new Error('Failed to fetch MIDI'); + events = MIDIParser.parseMIDI(buf); + statusEl.textContent = 'Ready. Tap Play.'; + playBtn.disabled = false; + } catch (e) { + statusEl.textContent = `Load error: ${e instanceof Error ? e.message : 'Unknown error'}`; + playBtn.disabled = true; + stopBtn.disabled = true; + return; + } + + function setUiPlaying(playing: boolean): void { + isPlaying = playing; + playBtn.disabled = playing; + stopBtn.disabled = !playing; + } + + volumeEl.addEventListener('input', () => { + const vol = Number.parseFloat(volumeEl.value); + motifEngine.setVolume(vol); + }); + + stopBtn.addEventListener('click', () => { + motifEngine.stop(); + setUiPlaying(false); + statusEl.textContent = 'Stopped.'; + }); + + playBtn.addEventListener('click', async () => { + if (!events || isPlaying) return; + try { + setUiPlaying(true); + statusEl.textContent = 'Generating…'; + + // Match main page output: passthrough → existing SynthesisEngine. + await motifEngine.generateFromMIDI(events, 'passthrough'); + motifEngine.setVolume(Number.parseFloat(volumeEl.value)); + + statusEl.textContent = 'Playing…'; + await motifEngine.play(); + } catch (e) { + setUiPlaying(false); + statusEl.textContent = `Play error: ${e instanceof Error ? e.message : 'Unknown error'}`; + } + }); + + // Small quality-of-life: let user click the header “Ready” to copy link. + shareHint.addEventListener('click', async () => { + try { + await copyToClipboard(window.location.href); + shareHint.textContent = 'Link copied'; + window.setTimeout(() => (shareHint.textContent = 'Ready'), 900); + } catch { + // ignore + } + }); + shareHint.style.cursor = 'pointer'; + shareHint.title = 'Click to copy link'; +} + +void main(); + diff --git a/ux-test.html b/ux-test.html new file mode 100644 index 0000000..3df4089 --- /dev/null +++ b/ux-test.html @@ -0,0 +1,753 @@ + + + + + + MOTIF - UX Test + + + +
+
+

🎵 MOTIF

+

Gameboy synth for any song

+
+ + + +
Search for a song to get started
+ +
+
Results
+
+
+ +
+
+
Now Playing
+
--
+
+ +
+ +
+ + +
+ +
+ + +
+ + + + +
+ Volume + +
+
+ + +
+ + + + diff --git a/vercel.json b/vercel.json index c402c1f..5e19d49 100644 --- a/vercel.json +++ b/vercel.json @@ -30,6 +30,14 @@ "src": "/models", "dest": "/models.html" }, + { + "src": "/play", + "dest": "/play.html" + }, + { + "src": "/ux-test", + "dest": "/ux-test.html" + }, { "handle": "filesystem" }, { "src": "/(.*)", diff --git a/vite.config.ts b/vite.config.ts index 2658b45..69fe956 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,8 +11,12 @@ export default defineConfig({ rollupOptions: { input: { main: resolve(__dirname, 'index.html'), + // Existing pages (keep for backward compatibility) embed: resolve(__dirname, 'embed.html'), models: resolve(__dirname, 'models.html'), + // Sharing trial pages + play: resolve(__dirname, 'play.html'), + ux_test: resolve(__dirname, 'ux-test.html'), }, }, },