@onnie81/murdoku-spor 1.2.3 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,12 @@ let master, sfxBus, musicBus, analyser;
7
7
  let enabled = { sfx: true, music: true };
8
8
  let musicOn = false;
9
9
  let schedTimer = null;
10
+ let outEl = null; // iOS: audio leaves through a media element, not ctx.destination
11
+
12
+ // iPadOS pretends to be MacIntel, hence the maxTouchPoints check
13
+ const IS_IOS = typeof navigator !== 'undefined' &&
14
+ (/iP(hone|ad|od)/.test(navigator.userAgent) ||
15
+ (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1));
10
16
 
11
17
  function ensureCtx() {
12
18
  if (ctx) return ctx;
@@ -15,7 +21,21 @@ function ensureCtx() {
15
21
  ctx = new AC();
16
22
  master = ctx.createGain();
17
23
  master.gain.value = 0.9;
18
- master.connect(ctx.destination);
24
+ if (IS_IOS) {
25
+ // Direct WebAudio output is unreliable on iOS (silent-switch category and
26
+ // sample-rate mismatches can mute it entirely while the graph keeps
27
+ // "playing"). Routing through a MediaStream + <audio> element uses the
28
+ // real media playback path: audible, and immune to the ring switch.
29
+ const dest = ctx.createMediaStreamDestination();
30
+ master.connect(dest);
31
+ outEl = document.createElement('audio');
32
+ outEl.setAttribute('playsinline', '');
33
+ outEl.autoplay = true;
34
+ outEl.srcObject = dest.stream;
35
+ document.body.appendChild(outEl);
36
+ } else {
37
+ master.connect(ctx.destination);
38
+ }
19
39
  analyser = ctx.createAnalyser();
20
40
  analyser.fftSize = 2048;
21
41
  master.connect(analyser);
@@ -40,22 +60,8 @@ function ensureCtx() {
40
60
  return ctx;
41
61
  }
42
62
 
43
- // iOS: playing a (nearly) silent media element inside the gesture switches the
44
- // audio session to "playback", so WebAudio stays audible even with the ring
45
- // switch on silent. One-time, invisible.
46
- const SILENT_WAV = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAIA+AAACABAAZGF0YQAAAAA=';
47
- let mediaUnlocked = false;
48
- function unlockMediaSession() {
49
- if (mediaUnlocked) return;
50
- mediaUnlocked = true;
51
- try {
52
- const a = document.createElement('audio');
53
- a.setAttribute('playsinline', '');
54
- a.src = SILENT_WAV;
55
- a.volume = 0.01;
56
- const p = a.play();
57
- if (p && p.catch) p.catch(() => { mediaUnlocked = false; });
58
- } catch { mediaUnlocked = false; }
63
+ function unlocked() {
64
+ return ctx && ctx.state === 'running' && (!IS_IOS || (outEl && !outEl.paused));
59
65
  }
60
66
 
61
67
  export function initAudioOnGesture() {
@@ -63,22 +69,31 @@ export function initAudioOnGesture() {
63
69
  const kick = () => {
64
70
  const c = ensureCtx();
65
71
  if (!c) return;
66
- unlockMediaSession();
72
+ // both calls must happen inside the user gesture
73
+ if (outEl && outEl.paused) {
74
+ const p = outEl.play();
75
+ if (p && p.catch) p.catch(() => {});
76
+ }
67
77
  const go = () => {
68
- if (c.state !== 'running') return; // keep listeners until truly unlocked
78
+ if (!unlocked()) return; // keep listening until truly audible
69
79
  EVENTS.forEach(ev => window.removeEventListener(ev, kick));
70
80
  if (enabled.music) startMusic();
71
81
  };
72
82
  if (c.state === 'suspended') c.resume().then(go).catch(() => {});
73
- else go();
83
+ else setTimeout(go, 50);
74
84
  };
75
85
  EVENTS.forEach(ev => window.addEventListener(ev, kick));
76
86
 
77
- // phones suspend the context when the tab goes to background — resume on return
87
+ // phones suspend audio when the tab goes to background — restore on return
78
88
  document.addEventListener('visibilitychange', () => {
79
- if (!document.hidden && ctx && ctx.state === 'suspended') {
89
+ if (document.hidden || !ctx) return;
90
+ if (ctx.state === 'suspended') {
80
91
  ctx.resume().then(() => { if (enabled.music) startMusic(); }).catch(() => {});
81
92
  }
93
+ if (outEl && outEl.paused) {
94
+ const p = outEl.play();
95
+ if (p && p.catch) p.catch(() => {});
96
+ }
82
97
  });
83
98
  }
84
99
 
@@ -144,6 +159,10 @@ export function sfx(name) {
144
159
  const c = ensureCtx();
145
160
  if (!c) return;
146
161
  if (c.state === 'suspended') c.resume();
162
+ if (outEl && outEl.paused) {
163
+ const p = outEl.play();
164
+ if (p && p.catch) p.catch(() => {});
165
+ }
147
166
  switch (name) {
148
167
  case 'place': // soft wooden tap + warm blip
149
168
  noise({ d: 0.05, peak: 0.25, freq: 900, q: 0.8, type: 'lowpass' });
@@ -302,6 +321,8 @@ if (typeof window !== 'undefined') {
302
321
  state: () => (ctx ? ctx.state : 'none'),
303
322
  musicOn: () => musicOn,
304
323
  enabled: () => ({ ...enabled }),
324
+ route: () => (IS_IOS ? (outEl ? (outEl.paused ? 'ios-media:paused' : 'ios-media:playing') : 'ios-media:missing') : 'direct'),
325
+ rate: () => (ctx ? ctx.sampleRate : 0),
305
326
  rms() {
306
327
  if (!analyser) return 0;
307
328
  const a = new Float32Array(analyser.fftSize);
@@ -87,11 +87,24 @@ export function SettingsView() {
87
87
  <div className="card">
88
88
  <div className="card-head"><h2>{t('about')}</h2></div>
89
89
  <p className="muted">{t('aboutText')}</p>
90
+ <AudioDebugLine />
90
91
  </div>
91
92
  </div>
92
93
  );
93
94
  }
94
95
 
96
+ function AudioDebugLine() {
97
+ const [, force] = useState(0);
98
+ const a = typeof window !== 'undefined' ? window.__mdkAudio : null;
99
+ if (!a) return null;
100
+ return (
101
+ <p className="muted" style={{ fontSize: '0.72rem', marginTop: 8 }}
102
+ onClick={() => force(x => x + 1)}>
103
+ audio: {a.state()} · {a.route()} · {a.rate()}Hz · music {a.musicOn() ? 'on' : 'off'} · rms {a.rms().toFixed(4)}
104
+ </p>
105
+ );
106
+ }
107
+
95
108
  function AccountPanel() {
96
109
  const app = useApp();
97
110
  const [mode, setMode] = useState('signup');
package/murdoku.bundle CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onnie81/murdoku-spor",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "type": "module",
5
5
  "description": "Murdoku — online murder-mystery logic puzzle (sudoku-style placement deduction). Deploy artifact for games.onniehex.dev.",
6
6
  "license": "MIT",