@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.
package/client/src/audio.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
87
|
+
// phones suspend audio when the tab goes to background — restore on return
|
|
78
88
|
document.addEventListener('visibilitychange', () => {
|
|
79
|
-
if (
|
|
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