fix: make mp3 export resilient to async gesture loss

This commit is contained in:
b1rdmania
2026-03-03 13:23:36 +00:00
parent 5dc9d026bc
commit bbaee43c74
3 changed files with 75 additions and 33 deletions
-1
View File
@@ -1300,7 +1300,6 @@
</main> </main>
</div> </div>
</div> </div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>
-1
View File
@@ -5,4 +5,3 @@ declare module 'lamejs' {
const _default: any; const _default: any;
export default _default; export default _default;
} }
+75 -31
View File
@@ -833,35 +833,50 @@ class MotifApp {
return out; return out;
} }
private downloadBlob(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob);
try {
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.rel = 'noopener';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
} finally {
window.setTimeout(() => {
try {
URL.revokeObjectURL(url);
} catch {
// ignore
}
}, 2000);
}
}
private async handleDownloadMp3(): Promise<void> { private async handleDownloadMp3(): Promise<void> {
if (!this.hasGenerated || !this.currentMIDI) return; 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 { try {
this.downloadMp3Btn.disabled = true; this.downloadMp3Btn.disabled = true;
this.downloadMp3Btn.textContent = 'Rendering…'; this.downloadMp3Btn.textContent = 'Rendering…';
this.updateStatus('Rendering audio…'); 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 = '<p style="font-family:sans-serif;padding:16px">Preparing MP3…</p>';
} catch {
// ignore cross-window access issues
}
}
}
// Stop any current playback // Stop any current playback
this.handleMotifStop(); this.handleMotifStop();
@@ -878,10 +893,9 @@ class MotifApp {
const Mp3Encoder = (lamejs as any)?.Mp3Encoder; const Mp3Encoder = (lamejs as any)?.Mp3Encoder;
if (!Mp3Encoder) throw new Error('MP3 encoder not loaded'); if (!Mp3Encoder) throw new Error('MP3 encoder not loaded');
const channels = audioBuffer.numberOfChannels; const channels = audioBuffer.numberOfChannels;
const kbps = 128; const kbps = 128;
const encoder = new Mp3Encoder(channels, sampleRate, kbps); const encoder = new (lamejs as any).Mp3Encoder(channels, sampleRate, kbps);
const mp3Chunks: BlobPart[] = []; const mp3Chunks: BlobPart[] = [];
// Get audio data // Get audio data
@@ -903,15 +917,45 @@ class MotifApp {
if (tail && tail.length) mp3Chunks.push(tail); if (tail && tail.length) mp3Chunks.push(tail);
const blob = new Blob(mp3Chunks, { type: 'audio/mpeg' }); const blob = new Blob(mp3Chunks, { type: 'audio/mpeg' });
const result = this.searchResults[this.selectedResultIndex]; if (fileHandle) {
const safeTitle = this.cleanSongTitle(result?.title || 'wario-synth').slice(0, 80) || 'wario-synth'; const writable = await fileHandle.createWritable();
const filename = `${safeTitle}.mp3`; await writable.write(blob);
await writable.close();
this.updateStatus('MP3 downloaded!');
setTimeout(() => this.updateStatus(''), 1500);
return;
}
this.downloadMp3Btn.textContent = 'Downloading…'; const url = URL.createObjectURL(blob);
this.updateStatus('Downloading…'); let attemptedDownload = false;
this.downloadBlob(blob, filename); 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
}
window.setTimeout(() => this.updateStatus(''), 1200); 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) { } catch (e) {
this.updateStatus(`MP3 failed: ${e instanceof Error ? e.message : 'Unknown error'}`); this.updateStatus(`MP3 failed: ${e instanceof Error ? e.message : 'Unknown error'}`);
} finally { } finally {
@@ -1062,4 +1106,4 @@ try {
status.textContent = `App error: ${err instanceof Error ? err.message : 'Unknown error'}. Please refresh.`; status.textContent = `App error: ${err instanceof Error ? err.message : 'Unknown error'}. Please refresh.`;
status.style.color = '#ff6b6b'; status.style.color = '#ff6b6b';
} }
} }