PageSourceSearch

https://fortune-cookie.tokyo/assets/js/se-effects.js?v=1.0.610

js fortune-cookie.tokyo collected 2026-10-07 17:57:58 UTC 9,372 bytes, 213 lines download raw bytes

1/**
2 * SE (Sound Effect) foundation — provides window.SE.play(name).
3 * See docs/features/auction-bid-se/spec.md for design.
4 *
5 * API contract: all methods silent-catch every internal exception and never
6 * throw to the caller (callers still guard with typeof + try/catch for depth).
7 */
8(function () {
9    'use strict';
10
11    var LS_MUTE_KEY = 'se_muted';
12
13    var audioContext = null;
14    // 世代番号: AudioContext を再生成するたびに ++。in-flight fetch の Promise は
15    // 開始時の世代を保持し、resolve 時に世代が進んでいたら結果を破棄する(closed race 対策)
16    var contextGeneration = 0;
17    var seBuffers = {};
18    var loadingPromises = {};
19
20    function readConfig() {
21        var cfg = (typeof window !== 'undefined' && window.SE_CONFIG) || {};
22        return {
23            files: cfg.files || {},
24            volume: (typeof cfg.volume === 'number') ? cfg.volume : 1.0,
25        };
26    }
27
28    function warnIfDebug(msg, err) {
29        try {
30            if (window.SE && window.SE.debug === true) {
31                console.warn('[SE]', msg, err || '');
32            }
33        } catch (_) { /* silent */ }
34    }
35
36    function isMuted() {
37        try {
38            return typeof localStorage !== 'undefined' && localStorage.getItem(LS_MUTE_KEY) === '1';
39        } catch (_) { return false; }
40    }
41
42    function mute() {
43        try { localStorage.setItem(LS_MUTE_KEY, '1'); } catch (_) { /* silent */ }
44    }
45
46    function unmute() {
47        try { localStorage.removeItem(LS_MUTE_KEY); } catch (_) { /* silent */ }
48    }
49
50    function ensureContext() {
51        var AC = window.AudioContext || window.webkitAudioContext;
52        if (!AC) return null;
53
54        if (audioContext && audioContext.state === 'closed') {
55            audioContext = null;
56            // closed 時は AudioBuffer キャッシュも破棄 + 世代を進める
57            // (cross-context 再生の InvalidStateError 回避、in-flight fetch の resolve 破棄用)
58            seBuffers = {};
59            loadingPromises = {};
60            contextGeneration++;
61        }
62        if (!audioContext) {
63            try { audioContext = new AC(); } catch (e) { warnIfDebug('AudioContext init failed', e); return null; }
64        }
65        if (audioContext.state === 'suspended') {
66            try { audioContext.resume().catch(function (e) { warnIfDebug('resume rejected', e); }); }
67            catch (e) { warnIfDebug('resume throw', e); }
68        }
69        return audioContext;
70    }
71
72    function loadAndPlay(url, volume) {
73        var ctx = ensureContext();
74        if (!ctx) return;
75
76        if (seBuffers[url]) {
77            playBuffer(ctx, seBuffers[url], volume);
78            return;
79        }
80
81        // 同一 URL の並列 fetch は 1 本に束ねる。開始時の世代を Promise に埋め込む
82        if (!loadingPromises[url]) {
83            var startGen = contextGeneration;
84            loadingPromises[url] = fetch(url)
85                .then(function (res) {
86                    if (!res.ok) throw new Error('fetch failed: ' + res.status);
87                    return res.arrayBuffer();
88                })
89                .then(function (buf) { return ctx.decodeAudioData(buf); })
90                .then(function (decoded) {
91                    // 世代が進んでいたら旧 ctx の decode 結果なのでキャッシュに戻さない
92                    if (startGen === contextGeneration) seBuffers[url] = decoded;
93                    return decoded;
94                })
95                .catch(function (e) { warnIfDebug('load failed: ' + url, e); return null; })
96                // 成功・失敗いずれの場合も loadingPromises から除去して、次回 play 時に再試行できるようにする
97                .then(function (result) { delete loadingPromises[url]; return result; },
98                      function () { delete loadingPromises[url]; return null; });
99        }
100
101        loadingPromises[url].then(function (decoded) {
102            // fetch 完了時点で context が既に closed になっていることがある。再取得。
103            var currentCtx = ensureContext();
104            if (currentCtx && decoded) playBuffer(currentCtx, decoded, volume);
105        }).catch(function (e) { warnIfDebug('play chain failed', e); });
106    }
107
108    // 音源を fetch + decode して seBuffers に載せるだけ (再生しない).
109    // Promise を返し、呼び出し側で await 可能。既ロード or 進行中なら既存 Promise を返す.
110    function preload(url) {
111        var ctx = ensureContext();
112        if (!ctx || !url) return Promise.resolve(null);
113        if (seBuffers[url]) return Promise.resolve(seBuffers[url]);
114        if (loadingPromises[url]) return loadingPromises[url];
115        var startGen = contextGeneration;
116        loadingPromises[url] = fetch(url)
117            .then(function (res) {
118                if (!res.ok) throw new Error('fetch failed: ' + res.status);
119                return res.arrayBuffer();
120            })
121            .then(function (buf) { return ctx.decodeAudioData(buf); })
122            .then(function (decoded) {
123                if (startGen === contextGeneration) seBuffers[url] = decoded;
124                return decoded;
125            })
126            .catch(function (e) { warnIfDebug('preload failed: ' + url, e); return null; })
127            .then(function (result) { delete loadingPromises[url]; return result; },
128                  function () { delete loadingPromises[url]; return null; });
129        return loadingPromises[url];
130    }
131
132    // 名前指定で preload する(ensureLoaded の実体).
133    // SE_CONFIG.files[name] が無い / mute 中 は即 resolve(null).
134    function ensureLoaded(name) {
135        try {
136            if (!name || isMuted()) return Promise.resolve(null);
137            var cfg = readConfig();
138            var url = cfg.files[name];
139            if (!url) return Promise.resolve(null);
140            return preload(url);
141        } catch (e) {
142            warnIfDebug('ensureLoaded failed', e);
143            return Promise.resolve(null);
144        }
145    }
146
147    // AudioContext を作成 + resume するだけ. user gesture 中に呼ぶことで iOS でも unlock される.
148    // 音源 preload は行わない(必要になったタイミングで ensureLoaded を呼ぶ).
149    function unlock() {
150        try { ensureContext(); } catch (_) { /* silent */ }
151    }
152
153    function playBuffer(ctx, buffer, volume) {
154        try {
155            var source = ctx.createBufferSource();
156            var gain = ctx.createGain();
157            source.buffer = buffer;
158            gain.gain.value = (typeof volume === 'number' ? volume : 1.0);
159            source.connect(gain).connect(ctx.destination);
160            source.start(0);
161        } catch (e) { warnIfDebug('play failed', e); }
162    }
163
164    function play(name, volumeOverride) {
165        try {
166            if (!name) return;
167            if (isMuted()) return;
168            var cfg = readConfig();
169            var url = cfg.files[name];
170            if (!url) return;
171            var vol = (typeof volumeOverride === 'number') ? volumeOverride : cfg.volume;
172            loadAndPlay(url, vol);
173        } catch (e) { warnIfDebug('play top-level failed', e); }
174    }
175
176    // Public API — all methods contractually never throw
177    window.SE = {
178        play: play,
179        mute: mute,
180        unmute: unmute,
181        isMuted: isMuted,
182        unlock: unlock,
183        ensureLoaded: ensureLoaded,
184        // debug: true を DevTools で立てると console.warn が有効化される
185    };
186
187    // iOS AudioContext unlock: 初回 click(capture, once)で AudioContext を生成・resume する.
188    // 音源 preload は行わず、必要になったタイミング (SE.ensureLoaded / SE.play) で fetch する.
189    try {
190        var unlockHandler = function () {
191            unlock();
192            // SE unlock と同時に動画演出の音も自動 ON. 動画側は sessionStorage フラグで判定する
193            // (auction-room.js:1746 wantSound = sessionStorage.getItem('auction:reveal-unmuted') === '1')
194            // 注: iOS Safari は src 変更時に gesture 履歴をリセットするため video の音付き autoplay は
195            // 実質不可 (実機で priming/直接 play() いずれも効果なし確認済み). Android / Desktop では効く.
196            try { sessionStorage.setItem('auction:reveal-unmuted', '1'); } catch (_) { /* silent */ }
197        };
198        document.addEventListener('click', unlockHandler, { once: true, capture: true });
199    } catch (_) { /* silent — old browsers without addEventListener options object */ }
200
201    // タブ復帰時に AudioContext を resume する.
202    // バックグラウンド中に suspended になった AudioContext を、次の SE.play より前に走らせる.
203    // 未初期化状態 (unlock 前) では触らない (user gesture 前の new AudioContext は警告になる).
204    try {
205        document.addEventListener('visibilitychange', function () {
206            if (document.visibilityState !== 'visible') return;
207            if (!audioContext) return;  // 未初期化なら何もしない
208            if (audioContext.state === 'suspended') {
209                try { audioContext.resume().catch(function () {}); } catch (_) {}
210            }
211        });
212    } catch (_) { /* silent */ }
213})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.