diff --git a/index.html b/index.html
index 174ab6d..dd3a2ba 100644
--- a/index.html
+++ b/index.html
@@ -1222,7 +1222,6 @@
Copy link
Share to X
- Download MP3
Sorry, copy doesn't work on your device. Here's the link:
@@ -1247,9 +1246,6 @@
How it works: The Wario Synthesis Engine analyses MIDI files to identify melody, bass, and chord tracks. It then resynthesises each part using Web Audio oscillators tuned to mimic the Game Boy's 4-channel sound chip: two pulse wave channels, one wave channel, and one noise channel. All processing runs client-side in your browser - no server-side audio generation. Built with TypeScript, Vite, and Claude Code.
-
- v1.5 - Download MP3 button, offline rendering for faster exports
-
v1.4 - 16-bit to 8-bit audio fixes, improved synthesis quality
diff --git a/src/lamejs.d.ts b/src/lamejs.d.ts
deleted file mode 100644
index f022cc2..0000000
--- a/src/lamejs.d.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-declare module 'lamejs' {
- // `lamejs` ships without TypeScript types.
- // We only need `Mp3Encoder` for client-side MP3 export.
- export const Mp3Encoder: any;
- const _default: any;
- export default _default;
-}
diff --git a/src/main.ts b/src/main.ts
index 0bfc992..dba4621 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -4,7 +4,6 @@ import { MIDIParser } from './midi/MIDIParser';
import { SoundfontMIDIPlayer } from './synthesis/SoundfontMIDIPlayer';
import { getAudioContext, isAudioReady, peekAudioContext, unlockAudio } from './utils/audioUnlock';
import type { NoteEvent } from './types';
-import * as lamejs from 'lamejs';
class MotifApp {
private motifEngine: MotifEngine;
@@ -56,12 +55,9 @@ class MotifApp {
private copyLinkBtn!: HTMLButtonElement;
private shareToXBtn!: HTMLButtonElement;
- private downloadMp3Btn!: HTMLButtonElement;
private shareFallback!: HTMLElement;
private shareFallbackInput!: HTMLInputElement;
- private downloadMp3BtnLabel = 'Download MP3';
-
// Embed snippet UI
private embedSection: HTMLElement | null = null;
private embedCodeEl: HTMLElement | null = null;
@@ -133,8 +129,6 @@ class MotifApp {
this.copyLinkBtn = document.getElementById('copyLinkBtn') as HTMLButtonElement;
this.shareToXBtn = document.getElementById('shareToXBtn') as HTMLButtonElement;
- this.downloadMp3Btn = document.getElementById('downloadMp3Btn') as HTMLButtonElement;
- this.downloadMp3BtnLabel = (this.downloadMp3Btn.textContent || '').trim() || 'Download MP3';
this.shareFallback = document.getElementById('shareFallback')!;
this.shareFallbackInput = document.getElementById('shareFallbackInput') as HTMLInputElement;
@@ -195,7 +189,6 @@ class MotifApp {
this.copyLinkBtn.addEventListener('click', () => void this.handleCopyLink());
this.shareToXBtn.addEventListener('click', () => void this.handleShareToX());
- this.downloadMp3Btn.addEventListener('click', () => void this.handleDownloadMp3());
// Embed snippet copy (may be disabled / not-live)
this.copyEmbedBtn?.addEventListener('click', () => void this.copyEmbedSnippet());
@@ -824,146 +817,6 @@ class MotifApp {
}
}
- private floatToInt16(input: Float32Array): Int16Array {
- const out = new Int16Array(input.length);
- for (let i = 0; i < input.length; i++) {
- const s = Math.max(-1, Math.min(1, input[i]));
- out[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
- }
- return out;
- }
-
- private async handleDownloadMp3(): Promise {
- if (!this.hasGenerated || !this.currentMIDI) return;
-
- const result = this.searchResults[this.selectedResultIndex];
- const safeTitle = this.cleanSongTitle(result?.title || 'wario-synth').slice(0, 80) || 'wario-synth';
- const fileName = `${safeTitle}.mp3`;
- let fileHandle: any = null;
- let popup: Window | null = null;
-
- try {
- this.downloadMp3Btn.disabled = true;
- this.downloadMp3Btn.textContent = 'Rendering…';
- this.updateStatus('Rendering audio…');
-
- // Capture user activation up-front when possible.
- const maybeShowSaveFilePicker = (window as any).showSaveFilePicker;
- if (typeof maybeShowSaveFilePicker === 'function') {
- try {
- fileHandle = await maybeShowSaveFilePicker({
- suggestedName: fileName,
- types: [{
- description: 'MP3 audio',
- accept: { 'audio/mpeg': ['.mp3'] },
- }],
- });
- } catch (e: any) {
- if (e?.name === 'AbortError') {
- this.updateStatus('Download cancelled.');
- return;
- }
- }
- } else {
- // Fallback for browsers without file picker: pre-open a window from the click gesture.
- popup = window.open('', '_blank');
- if (popup && !popup.closed) {
- try {
- popup.document.title = 'Preparing MP3';
- popup.document.body.innerHTML = 'Preparing MP3…
';
- } catch {
- // ignore cross-window access issues
- }
- }
- }
-
- // Stop any current playback
- this.handleMotifStop();
-
- // Render offline (full song)
- const sampleRate = 44100;
- const audioBuffer = await this.motifEngine.renderOffline(
- this.currentMIDI.events,
- 'procedural',
- sampleRate
- );
-
- this.downloadMp3Btn.textContent = 'Encoding…';
- this.updateStatus('Encoding MP3…');
-
- const Mp3Encoder = (lamejs as any)?.Mp3Encoder;
- if (!Mp3Encoder) throw new Error('MP3 encoder not loaded');
- const channels = audioBuffer.numberOfChannels;
- const kbps = 128;
- const encoder = new (lamejs as any).Mp3Encoder(channels, sampleRate, kbps);
- const mp3Chunks: BlobPart[] = [];
-
- // Get audio data
- const leftData = audioBuffer.getChannelData(0);
- const rightData = channels > 1 ? audioBuffer.getChannelData(1) : leftData;
- const left = this.floatToInt16(leftData);
- const right = this.floatToInt16(rightData);
-
- // Encode in 1152-sample frames
- const frameSize = 1152;
- for (let i = 0; i < left.length; i += frameSize) {
- const l = left.subarray(i, i + frameSize);
- const r = right.subarray(i, i + frameSize);
- const buf = encoder.encodeBuffer(l, r);
- if (buf && buf.length) mp3Chunks.push(buf);
- }
-
- const tail = encoder.flush();
- if (tail && tail.length) mp3Chunks.push(tail);
-
- const blob = new Blob(mp3Chunks, { type: 'audio/mpeg' });
- if (fileHandle) {
- const writable = await fileHandle.createWritable();
- await writable.write(blob);
- await writable.close();
- this.updateStatus('MP3 downloaded!');
- setTimeout(() => this.updateStatus(''), 1500);
- return;
- }
-
- const url = URL.createObjectURL(blob);
- let attemptedDownload = false;
- try {
- const a = document.createElement('a');
- a.href = url;
- a.download = fileName;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- attemptedDownload = true;
- } catch {
- // fallback below
- }
-
- if (popup && !popup.closed) {
- popup.location.href = url;
- if (attemptedDownload) {
- popup.close();
- }
- } else if (!attemptedDownload) {
- window.open(url, '_blank');
- }
-
- if (attemptedDownload) {
- this.updateStatus('MP3 downloaded!');
- setTimeout(() => this.updateStatus(''), 1500);
- } else {
- this.updateStatus('MP3 opened in a new tab. Save it from there.');
- }
- setTimeout(() => URL.revokeObjectURL(url), 60_000);
- } catch (e) {
- this.updateStatus(`MP3 failed: ${e instanceof Error ? e.message : 'Unknown error'}`);
- } finally {
- this.downloadMp3Btn.disabled = false;
- this.downloadMp3Btn.textContent = this.downloadMp3BtnLabel;
- }
- }
-
private setState(state: 'idle' | 'results' | 'selected' | 'generated'): void {
// results - keep visible in all states except idle so user can pick a different source
const hasResults = state === 'results' || state === 'selected' || state === 'generated';
@@ -979,8 +832,6 @@ class MotifApp {
this.copyLinkBtn.disabled = !(state === 'generated' && this.hasGenerated);
this.shareToXBtn.style.display = state === 'generated' ? 'inline-block' : 'none';
this.shareToXBtn.disabled = !(state === 'generated' && this.hasGenerated);
- this.downloadMp3Btn.style.display = state === 'generated' ? 'inline-block' : 'none';
- this.downloadMp3Btn.disabled = !(state === 'generated' && this.hasGenerated);
// Hide share fallback when state changes
this.shareFallback.style.display = 'none';
diff --git a/ux-test.html b/ux-test.html
index e756404..f50b2f2 100644
--- a/ux-test.html
+++ b/ux-test.html
@@ -512,9 +512,6 @@
Copy link
Share on X
-
- Download MP3
-
@@ -562,7 +559,6 @@
const stopBtn = document.getElementById('stopBtn');
const copyLinkBtn = document.getElementById('copyLinkBtn');
const shareXBtn = document.getElementById('shareXBtn');
- const downloadMp3Btn = document.getElementById('downloadMp3Btn');
const shareHint = document.getElementById('shareHint');
const searchBtn = document.getElementById('searchBtn');
const songInput = document.getElementById('songInput');
@@ -718,7 +714,6 @@
playBtn.disabled = false;
copyLinkBtn.disabled = false;
shareXBtn.disabled = false;
- downloadMp3Btn.disabled = false;
setStatus('Ready to play');
shareHint.textContent = '';
@@ -761,83 +756,6 @@
stopVisualizer();
}
- function downloadBlob(blob, filename) {
- const a = document.createElement('a');
- const url = URL.createObjectURL(blob);
- a.href = url;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- setTimeout(() => URL.revokeObjectURL(url), 1000);
- }
-
- function floatToInt16(input) {
- const out = new Int16Array(input.length);
- for (let i = 0; i < input.length; i++) {
- const s = Math.max(-1, Math.min(1, input[i]));
- out[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
- }
- return out;
- }
-
- async function downloadMp3() {
- if (!currentEvents) return;
- try {
- downloadMp3Btn.disabled = true;
- shareHint.textContent = 'Rendering audio…';
-
- // Stop any current playback first
- stopPlayback();
-
- // Ensure Motif engine exists
- if (!motifEngine) motifEngine = new MotifEngine();
-
- // Render offline (faster than real-time!)
- const sampleRate = 44100;
- const audioBuffer = await motifEngine.renderOffline(currentEvents, 'procedural', sampleRate);
-
- shareHint.textContent = 'Encoding MP3…';
-
- // Use lamejs from CDN (global)
- const Mp3Encoder = window.lamejs?.Mp3Encoder;
- if (!Mp3Encoder) throw new Error('MP3 encoder not available - lamejs not loaded');
-
- const channels = audioBuffer.numberOfChannels;
- const kbps = 128;
- const encoder = new Mp3Encoder(channels, sampleRate, kbps);
- const mp3Chunks = [];
-
- // Get audio data from buffer
- const leftData = audioBuffer.getChannelData(0);
- const rightData = channels > 1 ? audioBuffer.getChannelData(1) : leftData;
- const left = floatToInt16(leftData);
- const right = floatToInt16(rightData);
-
- // Encode in 1152-sample frames
- const frameSize = 1152;
- for (let i = 0; i < left.length; i += frameSize) {
- const l = left.subarray(i, i + frameSize);
- const r = right.subarray(i, i + frameSize);
- const buf = encoder.encodeBuffer(l, r);
- if (buf && buf.length) mp3Chunks.push(buf);
- }
-
- const tail = encoder.flush();
- if (tail && tail.length) mp3Chunks.push(tail);
-
- const blob = new Blob(mp3Chunks, { type: 'audio/mpeg' });
- const safeTitle = cleanTitleForShare(currentTitle || 'motif').slice(0, 80) || 'motif';
- downloadBlob(blob, `${safeTitle}.mp3`);
- shareHint.textContent = 'MP3 downloaded.';
- window.setTimeout(() => (shareHint.textContent = ''), 1400);
- } catch (e) {
- shareHint.textContent = `MP3 failed: ${e && e.message ? e.message : 'Unknown error'}`;
- console.error('MP3 download error:', e);
- } finally {
- downloadMp3Btn.disabled = false;
- }
- }
async function copyShareLink() {
if (!currentMidiUrl) return;
@@ -958,7 +876,6 @@
stopBtn.addEventListener('click', stopPlayback);
copyLinkBtn.addEventListener('click', () => void copyShareLink());
shareXBtn.addEventListener('click', shareOnX);
- downloadMp3Btn.addEventListener('click', () => downloadMp3());
// Init
createBars();