diff --git a/play.html b/play.html
index ead06e5..15a44d8 100644
--- a/play.html
+++ b/play.html
@@ -294,6 +294,28 @@
color: var(--gb-lightest);
}
+ /* iOS audio unlock banner */
+ .ios-audio-banner {
+ margin-top: calc(var(--spacing-unit) * 2);
+ padding: calc(var(--spacing-unit) * 2);
+ border-top: 2px solid var(--gb-light);
+ display: none; /* toggled by JS */
+ text-align: center;
+ }
+ .ios-audio-banner p {
+ font-size: 8px;
+ color: var(--gb-light);
+ margin: 0 0 calc(var(--spacing-unit)) 0;
+ line-height: 2;
+ }
+ .ios-audio-banner button {
+ background: var(--gb-lightest);
+ color: var(--gb-darkest);
+ border: var(--pixel-border) solid var(--gb-light);
+ padding: 12px 20px;
+ font-size: 10px;
+ }
+
/* Footer */
.footer {
margin-top: calc(var(--spacing-unit) * 4);
@@ -338,6 +360,12 @@
+
+
+
Tap to enable audio on iOS
+
+
+
Generate your own
diff --git a/src/play.ts b/src/play.ts
index 6c3ba87..62c1104 100644
--- a/src/play.ts
+++ b/src/play.ts
@@ -1,7 +1,7 @@
import { MIDIService } from './services/MIDIService';
import { MIDIParser } from './midi/MIDIParser';
import { MotifEngine } from './core/MotifEngine';
-import { unlockAudio } from './utils/audioUnlock';
+import { unlockAudio, isAudioReady } from './utils/audioUnlock';
import type { NoteEvent } from './types';
function qs(id: string): HTMLElement {
@@ -140,6 +140,33 @@ async function main(): Promise
{
const currentTimeEl = qs('playCurrentTime') as HTMLElement;
const durationEl = qs('playDuration') as HTMLElement;
const timeRow = qs('playTimeRow') as HTMLElement;
+ const iosAudioBanner = qs('iosAudioBanner') as HTMLElement;
+ const enableAudioBtn = qs('enableAudioBtn') as HTMLButtonElement;
+
+ // Detect iOS/Safari for audio unlock banner
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
+ (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
+
+ function updateIOSBanner(): void {
+ if (isIOS && !isAudioReady()) {
+ iosAudioBanner.style.display = 'block';
+ } else {
+ iosAudioBanner.style.display = 'none';
+ }
+ }
+
+ // Show banner on iOS if needed
+ updateIOSBanner();
+
+ // Handle enable audio button
+ enableAudioBtn.addEventListener('click', async () => {
+ try {
+ await unlockAudio();
+ updateIOSBanner();
+ } catch {
+ // ignore
+ }
+ });
const { midiUrl: u, title } = buildMidiUrlFromParams();
@@ -290,6 +317,7 @@ async function main(): Promise {
try {
// Wait for unlock to complete
await unlockPromise;
+ updateIOSBanner(); // Hide banner once audio is unlocked
// First play generates the artifact (deterministically from MIDI structure).
if (!isGenerated) {