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
+
+
+
+
+
+
+
+
+
+
+
+
Search for a song to get started
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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'),
},
},
},