Add iOS 'Enable Audio' CTA for Motif playback.
This commit is contained in:
@@ -221,3 +221,4 @@
|
|||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+57
@@ -429,6 +429,53 @@
|
|||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
min-width: 55px;
|
min-width: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* iOS audio unlock (Motif) */
|
||||||
|
.ios-audio-banner {
|
||||||
|
margin-top: calc(var(--spacing-unit) * 2);
|
||||||
|
padding: calc(var(--spacing-unit) * 2);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: 1px solid rgba(255, 170, 68, 0.22);
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
display: none; /* toggled by JS */
|
||||||
|
}
|
||||||
|
.ios-audio-banner .row {
|
||||||
|
display: flex;
|
||||||
|
gap: calc(var(--spacing-unit) * 1.5);
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.ios-audio-banner .copy {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
line-height: 1.35;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
.ios-audio-banner .copy strong {
|
||||||
|
color: var(--color-text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.ios-audio-banner .state {
|
||||||
|
margin-top: calc(var(--spacing-unit) * 1);
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 170, 68, 0.85);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.ios-audio-banner button {
|
||||||
|
padding: calc(var(--spacing-unit) * 1.25) calc(var(--spacing-unit) * 2);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: 1px solid rgba(255, 170, 68, 0.35);
|
||||||
|
background: rgba(255, 170, 68, 0.12);
|
||||||
|
color: var(--color-text);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.ios-audio-banner button:hover:not(:disabled) {
|
||||||
|
border-color: rgba(255, 170, 68, 0.55);
|
||||||
|
box-shadow: var(--glow-secondary), var(--shadow-md);
|
||||||
|
}
|
||||||
.motif-kicker {
|
.motif-kicker {
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
color: #b6b6b6;
|
color: #b6b6b6;
|
||||||
@@ -687,6 +734,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="iosAudioBanner" class="ios-audio-banner" aria-live="polite">
|
||||||
|
<div class="row">
|
||||||
|
<div class="copy">
|
||||||
|
<strong>iOS Safari:</strong> tap once to enable audio, then press <strong>Generate & Play</strong>.
|
||||||
|
</div>
|
||||||
|
<button id="enableAudioBtn" type="button">Enable Audio</button>
|
||||||
|
</div>
|
||||||
|
<div id="iosAudioState" class="state">Audio: …</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="motif-progress-container" id="motifProgressContainer" style="display: none;">
|
<div class="motif-progress-container" id="motifProgressContainer" style="display: none;">
|
||||||
<div class="progress-time">
|
<div class="progress-time">
|
||||||
<span id="motifCurrentTime">0:00</span>
|
<span id="motifCurrentTime">0:00</span>
|
||||||
|
|||||||
+23
-10
@@ -2,6 +2,7 @@ import { MIDIService } from './services/MIDIService';
|
|||||||
import { MIDIParser } from './midi/MIDIParser';
|
import { MIDIParser } from './midi/MIDIParser';
|
||||||
import { SoundfontMIDIPlayer } from './synthesis/SoundfontMIDIPlayer';
|
import { SoundfontMIDIPlayer } from './synthesis/SoundfontMIDIPlayer';
|
||||||
import { MotifEngine } from './core/MotifEngine';
|
import { MotifEngine } from './core/MotifEngine';
|
||||||
|
import { unlockAudio } from './utils/audioUnlock';
|
||||||
import type { NoteEvent } from './types';
|
import type { NoteEvent } from './types';
|
||||||
|
|
||||||
type SearchResult = {
|
type SearchResult = {
|
||||||
@@ -42,10 +43,19 @@ async function main(): Promise<void> {
|
|||||||
const motifVol = qs<HTMLInputElement>('motifVol');
|
const motifVol = qs<HTMLInputElement>('motifVol');
|
||||||
|
|
||||||
const midiService = new MIDIService();
|
const midiService = new MIDIService();
|
||||||
const audioContext = new AudioContext();
|
|
||||||
const previewPlayer = new SoundfontMIDIPlayer(audioContext);
|
|
||||||
const motifEngine = new MotifEngine();
|
const motifEngine = new MotifEngine();
|
||||||
|
|
||||||
|
// Lazily created for iOS compatibility
|
||||||
|
let previewPlayer: SoundfontMIDIPlayer | null = null;
|
||||||
|
|
||||||
|
async function ensurePlayer(): Promise<SoundfontMIDIPlayer> {
|
||||||
|
if (!previewPlayer) {
|
||||||
|
const audioContext = await unlockAudio();
|
||||||
|
previewPlayer = new SoundfontMIDIPlayer(audioContext);
|
||||||
|
}
|
||||||
|
return previewPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
let results: SearchResult[] = [];
|
let results: SearchResult[] = [];
|
||||||
let selectedIndex = 0;
|
let selectedIndex = 0;
|
||||||
let currentEvents: NoteEvent[] | null = null;
|
let currentEvents: NoteEvent[] | null = null;
|
||||||
@@ -60,7 +70,7 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stopEverything(): void {
|
function stopEverything(): void {
|
||||||
previewPlayer.stop();
|
previewPlayer?.stop();
|
||||||
motifEngine.stop();
|
motifEngine.stop();
|
||||||
previewPlayBtn.disabled = currentEvents === null;
|
previewPlayBtn.disabled = currentEvents === null;
|
||||||
previewStopBtn.disabled = true;
|
previewStopBtn.disabled = true;
|
||||||
@@ -83,8 +93,9 @@ async function main(): Promise<void> {
|
|||||||
const events = MIDIParser.parseMIDI(midiBuffer);
|
const events = MIDIParser.parseMIDI(midiBuffer);
|
||||||
currentEvents = events;
|
currentEvents = events;
|
||||||
|
|
||||||
await previewPlayer.load(events);
|
const player = await ensurePlayer();
|
||||||
previewPlayer.setVolume(clamp01(parseFloat(previewVol.value)));
|
await player.load(events);
|
||||||
|
player.setVolume(clamp01(parseFloat(previewVol.value)));
|
||||||
|
|
||||||
setStatus(`Ready: ${r.title}`);
|
setStatus(`Ready: ${r.title}`);
|
||||||
previewPlayBtn.disabled = false;
|
previewPlayBtn.disabled = false;
|
||||||
@@ -143,7 +154,7 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
previewVol.addEventListener('input', (e) => {
|
previewVol.addEventListener('input', (e) => {
|
||||||
const v = clamp01(parseFloat((e.target as HTMLInputElement).value));
|
const v = clamp01(parseFloat((e.target as HTMLInputElement).value));
|
||||||
previewPlayer.setVolume(v);
|
previewPlayer?.setVolume(v);
|
||||||
});
|
});
|
||||||
|
|
||||||
motifVol.addEventListener('input', (e) => {
|
motifVol.addEventListener('input', (e) => {
|
||||||
@@ -156,7 +167,8 @@ async function main(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
motifEngine.stop();
|
motifEngine.stop();
|
||||||
isMotifPlaying = false;
|
isMotifPlaying = false;
|
||||||
await previewPlayer.play();
|
const player = await ensurePlayer();
|
||||||
|
await player.play();
|
||||||
previewPlayBtn.disabled = true;
|
previewPlayBtn.disabled = true;
|
||||||
previewStopBtn.disabled = false;
|
previewStopBtn.disabled = false;
|
||||||
motifStopBtn.disabled = true;
|
motifStopBtn.disabled = true;
|
||||||
@@ -167,7 +179,7 @@ async function main(): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
previewStopBtn.addEventListener('click', () => {
|
previewStopBtn.addEventListener('click', () => {
|
||||||
previewPlayer.stop();
|
previewPlayer?.stop();
|
||||||
previewPlayBtn.disabled = currentEvents === null;
|
previewPlayBtn.disabled = currentEvents === null;
|
||||||
previewStopBtn.disabled = true;
|
previewStopBtn.disabled = true;
|
||||||
setStatus('Preview stopped.');
|
setStatus('Preview stopped.');
|
||||||
@@ -177,7 +189,7 @@ async function main(): Promise<void> {
|
|||||||
if (!currentEvents) return;
|
if (!currentEvents) return;
|
||||||
try {
|
try {
|
||||||
// Stop preview and generate variation
|
// Stop preview and generate variation
|
||||||
previewPlayer.stop();
|
previewPlayer?.stop();
|
||||||
previewPlayBtn.disabled = false;
|
previewPlayBtn.disabled = false;
|
||||||
previewStopBtn.disabled = true;
|
previewStopBtn.disabled = true;
|
||||||
|
|
||||||
@@ -212,7 +224,7 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
if (volume) {
|
if (volume) {
|
||||||
previewVol.value = String(clamp01(parseFloat(volume)));
|
previewVol.value = String(clamp01(parseFloat(volume)));
|
||||||
previewPlayer.setVolume(parseFloat(previewVol.value));
|
// Volume will be applied when player is created in loadIndex
|
||||||
}
|
}
|
||||||
if (motifVolume) {
|
if (motifVolume) {
|
||||||
motifVol.value = String(clamp01(parseFloat(motifVolume)));
|
motifVol.value = String(clamp01(parseFloat(motifVolume)));
|
||||||
@@ -240,3 +252,4 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
void main();
|
void main();
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+105
-13
@@ -2,15 +2,15 @@ import { MotifEngine } from './core/MotifEngine';
|
|||||||
import { MIDIService } from './services/MIDIService';
|
import { MIDIService } from './services/MIDIService';
|
||||||
import { MIDIParser } from './midi/MIDIParser';
|
import { MIDIParser } from './midi/MIDIParser';
|
||||||
import { SoundfontMIDIPlayer } from './synthesis/SoundfontMIDIPlayer';
|
import { SoundfontMIDIPlayer } from './synthesis/SoundfontMIDIPlayer';
|
||||||
|
import { getAudioContext, isAudioReady, unlockAudio } from './utils/audioUnlock';
|
||||||
import type { NoteEvent } from './types';
|
import type { NoteEvent } from './types';
|
||||||
|
|
||||||
class MotifApp {
|
class MotifApp {
|
||||||
private motifEngine: MotifEngine;
|
private motifEngine: MotifEngine;
|
||||||
private midiService: MIDIService;
|
private midiService: MIDIService;
|
||||||
private audioContext: AudioContext;
|
|
||||||
|
|
||||||
// Preview player
|
// Preview player (lazily created for iOS compatibility)
|
||||||
private soundfontPlayer: SoundfontMIDIPlayer;
|
private soundfontPlayer: SoundfontMIDIPlayer | null = null;
|
||||||
|
|
||||||
private searchBtn!: HTMLButtonElement;
|
private searchBtn!: HTMLButtonElement;
|
||||||
private songInput!: HTMLInputElement;
|
private songInput!: HTMLInputElement;
|
||||||
@@ -39,6 +39,11 @@ class MotifApp {
|
|||||||
private motifDuration!: HTMLElement;
|
private motifDuration!: HTMLElement;
|
||||||
private motifProgressInterval: number | null = null;
|
private motifProgressInterval: number | null = null;
|
||||||
|
|
||||||
|
// iOS audio unlock UI (Motif only)
|
||||||
|
private iosAudioBanner!: HTMLElement;
|
||||||
|
private enableAudioBtn!: HTMLButtonElement;
|
||||||
|
private iosAudioState!: HTMLElement;
|
||||||
|
|
||||||
private nextResultBtn!: HTMLButtonElement;
|
private nextResultBtn!: HTMLButtonElement;
|
||||||
|
|
||||||
// Embed snippet UI
|
// Embed snippet UI
|
||||||
@@ -52,18 +57,26 @@ class MotifApp {
|
|||||||
private currentMIDI: { events: NoteEvent[], metadata: any } | null = null;
|
private currentMIDI: { events: NoteEvent[], metadata: any } | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Create AudioContext lazily on first use for iOS compatibility
|
|
||||||
this.audioContext = new AudioContext();
|
|
||||||
this.motifEngine = new MotifEngine();
|
this.motifEngine = new MotifEngine();
|
||||||
this.midiService = new MIDIService();
|
this.midiService = new MIDIService();
|
||||||
|
// soundfontPlayer created lazily on first play for iOS compatibility
|
||||||
// Initialize preview player
|
|
||||||
this.soundfontPlayer = new SoundfontMIDIPlayer(this.audioContext);
|
|
||||||
|
|
||||||
this.initializeUI();
|
this.initializeUI();
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure audio is unlocked and soundfontPlayer is ready.
|
||||||
|
* Must be called from a user gesture context.
|
||||||
|
*/
|
||||||
|
private async ensureAudioReady(): Promise<SoundfontMIDIPlayer> {
|
||||||
|
const audioContext = await unlockAudio();
|
||||||
|
if (!this.soundfontPlayer) {
|
||||||
|
this.soundfontPlayer = new SoundfontMIDIPlayer(audioContext);
|
||||||
|
}
|
||||||
|
return this.soundfontPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
private initializeUI(): void {
|
private initializeUI(): void {
|
||||||
this.searchBtn = document.getElementById('searchBtn') as HTMLButtonElement;
|
this.searchBtn = document.getElementById('searchBtn') as HTMLButtonElement;
|
||||||
this.songInput = document.getElementById('songInput') as HTMLInputElement;
|
this.songInput = document.getElementById('songInput') as HTMLInputElement;
|
||||||
@@ -93,6 +106,11 @@ class MotifApp {
|
|||||||
|
|
||||||
this.nextResultBtn = document.getElementById('nextResultBtn') as HTMLButtonElement;
|
this.nextResultBtn = document.getElementById('nextResultBtn') as HTMLButtonElement;
|
||||||
|
|
||||||
|
// iOS audio unlock UI (Motif)
|
||||||
|
this.iosAudioBanner = document.getElementById('iosAudioBanner')!;
|
||||||
|
this.enableAudioBtn = document.getElementById('enableAudioBtn') as HTMLButtonElement;
|
||||||
|
this.iosAudioState = document.getElementById('iosAudioState')!;
|
||||||
|
|
||||||
// Optional embed UI (only present on main page)
|
// Optional embed UI (only present on main page)
|
||||||
this.embedSection = document.getElementById('embedSection');
|
this.embedSection = document.getElementById('embedSection');
|
||||||
this.embedCodeEl = document.getElementById('embedCode');
|
this.embedCodeEl = document.getElementById('embedCode');
|
||||||
@@ -114,7 +132,7 @@ class MotifApp {
|
|||||||
this.soundfontStopBtn.addEventListener('click', () => this.handleSoundfontStop());
|
this.soundfontStopBtn.addEventListener('click', () => this.handleSoundfontStop());
|
||||||
this.soundfontVolumeSlider.addEventListener('input', (e) => {
|
this.soundfontVolumeSlider.addEventListener('input', (e) => {
|
||||||
const volume = parseFloat((e.target as HTMLInputElement).value);
|
const volume = parseFloat((e.target as HTMLInputElement).value);
|
||||||
this.soundfontPlayer.setVolume(volume);
|
this.soundfontPlayer?.setVolume(volume);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Motif
|
// Motif
|
||||||
@@ -136,6 +154,70 @@ class MotifApp {
|
|||||||
|
|
||||||
// Embed snippet copy (may be disabled / not-live)
|
// Embed snippet copy (may be disabled / not-live)
|
||||||
this.copyEmbedBtn?.addEventListener('click', () => void this.copyEmbedSnippet());
|
this.copyEmbedBtn?.addEventListener('click', () => void this.copyEmbedSnippet());
|
||||||
|
|
||||||
|
// iOS audio unlock CTA — must be a user gesture
|
||||||
|
const enable = () => void this.handleEnableAudio();
|
||||||
|
this.enableAudioBtn.addEventListener('click', enable);
|
||||||
|
this.enableAudioBtn.addEventListener('touchend', enable, { passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
private isIOSLike(): boolean {
|
||||||
|
const ua = navigator.userAgent || '';
|
||||||
|
const iOS = /iPad|iPhone|iPod/.test(ua);
|
||||||
|
const iPadOS13Plus = /Macintosh/.test(ua) && (navigator as any).maxTouchPoints > 1;
|
||||||
|
return iOS || iPadOS13Plus;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateIOSAudioBanner(): void {
|
||||||
|
// Only show this UX on iOS-like browsers, and only until audio is running.
|
||||||
|
if (!this.isIOSLike()) {
|
||||||
|
this.iosAudioBanner.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ready = isAudioReady();
|
||||||
|
this.iosAudioBanner.style.display = ready ? 'none' : 'block';
|
||||||
|
|
||||||
|
// Optional tiny state readout (helps support debugging)
|
||||||
|
const ctx = (() => {
|
||||||
|
try { return getAudioContext(); } catch { return null; }
|
||||||
|
})();
|
||||||
|
if (!ready && ctx) {
|
||||||
|
this.iosAudioState.style.display = 'block';
|
||||||
|
this.iosAudioState.textContent = `Audio: ${ctx.state} @ ${ctx.sampleRate}Hz`;
|
||||||
|
} else {
|
||||||
|
this.iosAudioState.style.display = 'none';
|
||||||
|
this.iosAudioState.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleEnableAudio(): Promise<void> {
|
||||||
|
// Must run in a user gesture context.
|
||||||
|
try {
|
||||||
|
this.enableAudioBtn.disabled = true;
|
||||||
|
this.iosAudioState.style.display = 'block';
|
||||||
|
this.iosAudioState.textContent = 'Audio: enabling…';
|
||||||
|
|
||||||
|
await unlockAudio();
|
||||||
|
|
||||||
|
// Update banner state
|
||||||
|
const ctx = getAudioContext();
|
||||||
|
if (ctx.state !== 'running') {
|
||||||
|
this.enableAudioBtn.disabled = false;
|
||||||
|
this.iosAudioState.textContent = 'Audio still locked. Tap Enable Audio again.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.iosAudioState.textContent = `Audio: running @ ${ctx.sampleRate}Hz`;
|
||||||
|
// Hide after a short beat to reduce flicker
|
||||||
|
window.setTimeout(() => this.updateIOSAudioBanner(), 250);
|
||||||
|
} catch {
|
||||||
|
this.enableAudioBtn.disabled = false;
|
||||||
|
this.iosAudioState.style.display = 'block';
|
||||||
|
this.iosAudioState.textContent = 'Audio enable failed. Tap again, or disable Silent Mode.';
|
||||||
|
} finally {
|
||||||
|
this.enableAudioBtn.disabled = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleSearch(): Promise<void> {
|
private async handleSearch(): Promise<void> {
|
||||||
@@ -171,6 +253,7 @@ class MotifApp {
|
|||||||
|
|
||||||
this.displayResults();
|
this.displayResults();
|
||||||
this.updateStatus(`Found ${results.length} MIDI files. Select one to play.`);
|
this.updateStatus(`Found ${results.length} MIDI files. Select one to play.`);
|
||||||
|
this.updateIOSAudioBanner();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.updateStatus(`Search error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
this.updateStatus(`Search error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
@@ -245,8 +328,9 @@ class MotifApp {
|
|||||||
|
|
||||||
this.currentMIDI = { events, metadata: { ...metadata, duration: actualDuration } };
|
this.currentMIDI = { events, metadata: { ...metadata, duration: actualDuration } };
|
||||||
|
|
||||||
// Load into preview player
|
// Load into preview player (ensure audio unlocked on iOS)
|
||||||
await this.soundfontPlayer.load(events);
|
const player = await this.ensureAudioReady();
|
||||||
|
await player.load(events);
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
this.selectedTitle.textContent = result.title;
|
this.selectedTitle.textContent = result.title;
|
||||||
@@ -259,6 +343,7 @@ class MotifApp {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
this.updateEmbedSnippet(result.title);
|
this.updateEmbedSnippet(result.title);
|
||||||
|
this.updateIOSAudioBanner();
|
||||||
|
|
||||||
this.playerSection.classList.add('visible');
|
this.playerSection.classList.add('visible');
|
||||||
this.enablePlayerControls();
|
this.enablePlayerControls();
|
||||||
@@ -273,17 +358,20 @@ class MotifApp {
|
|||||||
private async handleSoundfontPlay(): Promise<void> {
|
private async handleSoundfontPlay(): Promise<void> {
|
||||||
if (!this.currentMIDI) return;
|
if (!this.currentMIDI) return;
|
||||||
try {
|
try {
|
||||||
await this.soundfontPlayer.play();
|
// Ensure audio is unlocked (user gesture context)
|
||||||
|
const player = await this.ensureAudioReady();
|
||||||
|
await player.play();
|
||||||
this.soundfontPlayBtn.disabled = true;
|
this.soundfontPlayBtn.disabled = true;
|
||||||
this.soundfontStopBtn.disabled = false;
|
this.soundfontStopBtn.disabled = false;
|
||||||
this.updateStatus('Previewing MIDI...');
|
this.updateStatus('Previewing MIDI...');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.updateStatus(`Preview error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
this.updateStatus(`Preview error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
|
this.updateIOSAudioBanner();
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleSoundfontStop(): void {
|
private handleSoundfontStop(): void {
|
||||||
this.soundfontPlayer.stop();
|
this.soundfontPlayer?.stop();
|
||||||
this.soundfontPlayBtn.disabled = false;
|
this.soundfontPlayBtn.disabled = false;
|
||||||
this.soundfontStopBtn.disabled = true;
|
this.soundfontStopBtn.disabled = true;
|
||||||
this.updateStatus('Preview stopped.');
|
this.updateStatus('Preview stopped.');
|
||||||
@@ -298,6 +386,10 @@ class MotifApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Best-effort: ensure iOS audio is unlocked from this user gesture.
|
||||||
|
await unlockAudio();
|
||||||
|
this.updateIOSAudioBanner();
|
||||||
|
|
||||||
this.updateStatus('Generating Motif synthesis...');
|
this.updateStatus('Generating Motif synthesis...');
|
||||||
this.motifBtn.disabled = true;
|
this.motifBtn.disabled = true;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user