From 7deb7bd4d933b052a3f03e8b245b6f7778cdf91d Mon Sep 17 00:00:00 2001
From: b1rdmania <102524336+b1rdmania@users.noreply.github.com>
Date: Fri, 26 Dec 2025 19:27:16 +0000
Subject: [PATCH] Move MIDI preview controls into results table.
Adds per-row Play/Stop preview toggles, moves preview volume to the results header, and removes preview controls from the selected source card.
---
index.html | 17 +++++---
src/main.ts | 114 ++++++++++++++++++++++++++++++++++++++--------------
2 files changed, 96 insertions(+), 35 deletions(-)
diff --git a/index.html b/index.html
index b83d203..f43187e 100644
--- a/index.html
+++ b/index.html
@@ -413,6 +413,14 @@
}
}
+ .results-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: calc(var(--spacing-unit) * 2);
+ flex-wrap: wrap;
+ margin: 0 0 calc(var(--spacing-unit) * 2) 0;
+ }
.results-header h3 { margin: 0; }
.confidence-bar {
@@ -944,6 +952,10 @@
@@ -965,7 +977,6 @@
Search for a song, then choose a source to use.
-
@@ -976,10 +987,6 @@
-
-
-
-
Reimagined as classic game-console music using Motif’s Wario engine.
diff --git a/src/main.ts b/src/main.ts
index cbd09b7..e6f3918 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -11,6 +11,9 @@ class MotifApp {
// Preview player (lazily created for iOS compatibility)
private soundfontPlayer: SoundfontMIDIPlayer | null = null;
+ private playingPreviewIndex: number | null = null;
+ private previewStopTimeout: number | null = null;
+ private previewButtons: HTMLButtonElement[] = [];
private searchBtn!: HTMLButtonElement;
private songInput!: HTMLInputElement;
@@ -26,7 +29,6 @@ class MotifApp {
// Preview player controls
private soundfontVolumeSlider!: HTMLInputElement;
- private previewBtn!: HTMLButtonElement;
// Motif controls
private motifBtn!: HTMLButtonElement;
@@ -101,7 +103,6 @@ class MotifApp {
// Preview player controls
this.soundfontVolumeSlider = document.getElementById('soundfontVolume') as HTMLInputElement;
- this.previewBtn = document.getElementById('previewBtn') as HTMLButtonElement;
// Motif controls
this.motifBtn = document.getElementById('motifBtn') as HTMLButtonElement;
@@ -141,8 +142,7 @@ class MotifApp {
}
});
- // Preview MIDI (verification only)
- this.previewBtn.addEventListener('click', () => void this.handlePreviewMIDI());
+ // Preview MIDI (verification only) — controlled from results table rows
this.soundfontVolumeSlider.addEventListener('input', (e) => {
const volume = parseFloat((e.target as HTMLInputElement).value);
this.soundfontPlayer?.setVolume(volume);
@@ -295,6 +295,7 @@ class MotifApp {
private displayResults(): void {
this.resultsBody.innerHTML = '';
+ this.previewButtons = [];
this.searchResults.forEach((result, index) => {
const row = document.createElement('tr');
@@ -302,9 +303,21 @@ class MotifApp {
${this.cleanSongTitle(result.title)} |
${this.formatSourceLabel(result.source)} |
${this.formatDuration(result.parsed?.durationSec)} |
- |
+
+
+
+
+
+ |
`;
+ const previewBtn = row.querySelector('button.row-preview-btn') as HTMLButtonElement;
+ previewBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ void this.handleRowPreview(index);
+ });
+ this.previewButtons[index] = previewBtn;
+
// Use explicit action button to reduce accidental selection
const useBtn = row.querySelector('button.row-use-btn') as HTMLButtonElement;
useBtn.addEventListener('click', (e) => {
@@ -322,6 +335,72 @@ class MotifApp {
// No auto-select: user should choose "Use this"
}
+ private updatePreviewButtons(): void {
+ for (let i = 0; i < this.previewButtons.length; i++) {
+ const btn = this.previewButtons[i];
+ if (!btn) continue;
+ btn.textContent = this.playingPreviewIndex === i ? 'Stop' : 'Play';
+ }
+ }
+
+ private async handleRowPreview(index: number): Promise {
+ // Toggle stop if same row
+ if (this.playingPreviewIndex === index) {
+ this.stopPreview(true);
+ return;
+ }
+
+ // Stop any existing preview first
+ this.stopPreview(false);
+
+ const result = this.searchResults[index];
+ if (!result?.midiUrl) return;
+
+ try {
+ const player = await this.ensureAudioReady();
+
+ // Lazy-load events for preview without affecting selection state.
+ let events: NoteEvent[] | null = (result as any).__previewEvents || null;
+ if (!events) {
+ const midiBuffer = await this.midiService.fetchMIDI(result.midiUrl);
+ if (!midiBuffer) throw new Error('Failed to fetch MIDI');
+ events = MIDIParser.parseMIDI(midiBuffer);
+ (result as any).__previewEvents = events;
+ }
+
+ await player.load(events);
+ player.setVolume(parseFloat(this.soundfontVolumeSlider.value));
+ await player.play();
+
+ this.playingPreviewIndex = index;
+ this.updatePreviewButtons();
+
+ // Best-effort: reset UI after playback ends (SoundfontMIDIPlayer self-stops)
+ const duration = player.getDuration();
+ if (this.previewStopTimeout) window.clearTimeout(this.previewStopTimeout);
+ this.previewStopTimeout = window.setTimeout(() => {
+ if (this.playingPreviewIndex === index) {
+ this.stopPreview(false);
+ }
+ }, Math.max(0.5, duration + 0.5) * 1000);
+ } catch {
+ this.stopPreview(false);
+ }
+ }
+
+ private stopPreview(updateStatus: boolean): void {
+ if (this.previewStopTimeout) {
+ window.clearTimeout(this.previewStopTimeout);
+ this.previewStopTimeout = null;
+ }
+ this.soundfontPlayer?.stop();
+ this.playingPreviewIndex = null;
+ this.updatePreviewButtons();
+ if (updateStatus) {
+ // No status spam in main flow.
+ }
+ }
+
public async selectResult(index: number): Promise {
if (index < 0 || index >= this.searchResults.length) return;
@@ -390,29 +469,6 @@ class MotifApp {
}
}
- private async handlePreviewMIDI(): Promise {
- if (!this.currentMIDI) return;
- try {
- this.previewBtn.disabled = true;
- const prevLabel = this.previewBtn.textContent || 'Preview MIDI';
- this.previewBtn.textContent = 'Previewing…';
-
- const player = await this.ensureAudioReady();
- await player.load(this.currentMIDI.events);
- player.setVolume(parseFloat(this.soundfontVolumeSlider.value));
- await player.play();
-
- const duration = player.getDuration();
- window.setTimeout(() => {
- this.previewBtn.disabled = false;
- this.previewBtn.textContent = prevLabel;
- }, Math.max(0.5, duration + 0.25) * 1000);
- } catch {
- this.previewBtn.disabled = false;
- this.previewBtn.textContent = 'Preview MIDI';
- }
- }
-
// Motif handlers
private async handleMotif(): Promise {
if (!this.currentMIDI) {
@@ -544,13 +600,11 @@ class MotifApp {
private enablePlayerControls(): void {
this.motifBtn.disabled = false;
- this.previewBtn.disabled = false;
this.copyLinkBtn.disabled = this.currentMIDI == null;
}
private disablePlayerControls(): void {
this.motifBtn.disabled = true;
- this.previewBtn.disabled = true;
this.motifStopBtn.disabled = true;
this.copyLinkBtn.disabled = true;
}