feat: add sound system (LS.sfx) — synthesized Web Audio API sounds for classroom, gamification, quiz
- New js/sound.js: shared LS.sfx module with 21 synthesized sounds (ADSR envelope, sequences, sweeps, noise) - Classroom: lesson_start/end, user_joined/left, hand_raise, chat_message, muted, draw_permitted - Dashboard: achievement, level_up, xp_gain, coin via SSE events - Live quiz: quiz_start, quiz_end on question launch and results - Settings panel: global enable toggle + volume slider + localStorage persistence - Replaces old _crBeep() in classroom.html Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+36
-17
@@ -3000,6 +3000,7 @@
|
||||
<script src="/js/whiteboard.js"></script>
|
||||
<script src="/js/classroom-rtc.js"></script>
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/sound.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/notifications.js"></script>
|
||||
<script>
|
||||
@@ -3073,6 +3074,11 @@
|
||||
_crSegActive('crs-vad', _prefs.vadSensitivity);
|
||||
_crCheckSync('crs-chat-sound', _prefs.chatSound);
|
||||
_crCheckSync('crs-hand-sound', _prefs.handSound);
|
||||
if (window.LS && LS.sfx) {
|
||||
_crCheckSync('crs-sfx-enabled', LS.sfx.enabled);
|
||||
const vol = document.getElementById('crs-sfx-volume');
|
||||
if (vol) vol.value = Math.round(LS.sfx.volume * 100);
|
||||
}
|
||||
_crSegActive('crs-def-tool', _prefs.defaultTool);
|
||||
_crSegActive('crs-hand', _prefs.leftHand ? 'left' : 'right');
|
||||
_crSegActive('crs-stylus', String(_prefs.stylusMultiplier));
|
||||
@@ -3139,17 +3145,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Notification sounds (AudioContext beep) */
|
||||
function _crBeep(freq = 880, dur = 120, vol = 0.18) {
|
||||
try {
|
||||
const ctx = new AudioContext();
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.connect(gain); gain.connect(ctx.destination);
|
||||
osc.frequency.value = freq; gain.gain.value = vol;
|
||||
osc.start(); osc.stop(ctx.currentTime + dur / 1000);
|
||||
osc.onended = () => ctx.close();
|
||||
} catch {}
|
||||
/* Notification sounds — delegated to LS.sfx */
|
||||
function _crSfx(name) {
|
||||
try { if (window.LS && LS.sfx) LS.sfx.play(name); } catch {}
|
||||
}
|
||||
function crSfxSetEnabled(v) {
|
||||
if (window.LS && LS.sfx) { LS.sfx.setEnabled(v); if (v) LS.sfx.play('click'); }
|
||||
}
|
||||
function crSfxSetVolume(v) {
|
||||
if (window.LS && LS.sfx) { LS.sfx.setVolume(v / 100); LS.sfx.play('click'); }
|
||||
}
|
||||
|
||||
/* ── state ── */
|
||||
@@ -3496,12 +3500,13 @@
|
||||
// when _sessionId comes from JSON (number) vs SSE data (could be string in edge cases).
|
||||
if (data.type === 'classroom_started') {
|
||||
onClassroomStarted(data);
|
||||
_crSfx('lesson_start');
|
||||
} else if (data.type === 'classroom_ended') {
|
||||
if (_sessionId == data.sessionId) onClassroomEnded(true); // teacher ended remotely → show toast
|
||||
if (_sessionId == data.sessionId) { onClassroomEnded(true); _crSfx('lesson_end'); }
|
||||
} else if (data.type === 'classroom_user_joined') {
|
||||
if (_sessionId == data.sessionId) onUserJoined(data);
|
||||
if (_sessionId == data.sessionId) { onUserJoined(data); _crSfx('user_joined'); }
|
||||
} else if (data.type === 'classroom_user_left') {
|
||||
if (_sessionId == data.sessionId) onUserLeft(data);
|
||||
if (_sessionId == data.sessionId) { onUserLeft(data); _crSfx('user_left'); }
|
||||
} else if (data.type === 'classroom_chat') {
|
||||
if (_sessionId == data.sessionId) appendChatMessage(data);
|
||||
} else if (data.type === 'classroom_strokes') {
|
||||
@@ -3568,6 +3573,7 @@
|
||||
}
|
||||
} else if (data.type === 'classroom_muted') {
|
||||
if (_sessionId == data.sessionId && _rtc) {
|
||||
_crSfx('muted');
|
||||
_rtc.forceMute();
|
||||
if (_participants[_me.id]) _participants[_me.id].micMuted = true;
|
||||
updateParticipantsList();
|
||||
@@ -3577,7 +3583,7 @@
|
||||
'Учитель выключил ваш микрофон');
|
||||
}
|
||||
} else if (data.type === 'classroom_draw_permitted') {
|
||||
if (_sessionId == data.sessionId) enableDrawPermission();
|
||||
if (_sessionId == data.sessionId) { enableDrawPermission(); _crSfx('draw_permitted'); }
|
||||
} else if (data.type === 'classroom_draw_revoked') {
|
||||
if (_sessionId == data.sessionId) disableDrawPermission();
|
||||
} else if (data.type === 'classroom_reaction') {
|
||||
@@ -5324,7 +5330,7 @@
|
||||
updateHandsList();
|
||||
updateParticipantsList();
|
||||
if (_prefs.handSound && String(userId) !== String(_me?.id))
|
||||
_crBeep(660, 150, 0.15);
|
||||
_crSfx('hand_raise');
|
||||
}
|
||||
|
||||
function onHandLowered(userId) {
|
||||
@@ -5659,7 +5665,7 @@
|
||||
_chatUnread++;
|
||||
const badge = document.getElementById('chat-unread');
|
||||
if (badge) { badge.textContent = _chatUnread; badge.style.display = 'inline'; }
|
||||
if (_prefs.chatSound) _crBeep(880, 100, 0.12);
|
||||
if (_prefs.chatSound) _crSfx('chat_message');
|
||||
}
|
||||
|
||||
const wrap = document.getElementById('cr-messages');
|
||||
@@ -7676,6 +7682,19 @@
|
||||
</div>
|
||||
<div class="cr-sp-section">
|
||||
<div class="cr-sp-section-label">Звуки</div>
|
||||
<div class="cr-sp-row">
|
||||
<div class="cr-sp-row-lbl">Звуки включены</div>
|
||||
<label class="cr-toggle">
|
||||
<input type="checkbox" id="crs-sfx-enabled" onchange="crSfxSetEnabled(this.checked)">
|
||||
<span class="cr-toggle-track"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="cr-sp-row" style="gap:10px">
|
||||
<div class="cr-sp-row-lbl">Громкость</div>
|
||||
<input type="range" id="crs-sfx-volume" min="0" max="100" value="75"
|
||||
style="flex:1;accent-color:var(--violet)"
|
||||
oninput="crSfxSetVolume(this.value)">
|
||||
</div>
|
||||
<div class="cr-sp-row">
|
||||
<div class="cr-sp-row-lbl">Звук при новом сообщении</div>
|
||||
<label class="cr-toggle">
|
||||
|
||||
@@ -1521,6 +1521,7 @@
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/sound.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/notifications.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
@@ -3626,6 +3627,15 @@
|
||||
} else if (ev.type === 'session') {
|
||||
LS.toast(ev.message, 'info');
|
||||
if (isTeacher) loadAdminSessions();
|
||||
} else if (ev.type === 'achievement') {
|
||||
if (window.LS && LS.sfx) LS.sfx.play('achievement');
|
||||
} else if (ev.type === 'xp_update') {
|
||||
if (window.LS && LS.sfx) {
|
||||
if (ev.levelUp) LS.sfx.play('level_up');
|
||||
else LS.sfx.play('xp_gain');
|
||||
}
|
||||
} else if (ev.type === 'coins') {
|
||||
if (window.LS && LS.sfx) LS.sfx.play('coin');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -449,6 +449,7 @@
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/sound.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
||||
@@ -795,6 +796,7 @@
|
||||
updateStudentCounter(0);
|
||||
renderActiveQuestion(currentQuestion);
|
||||
renderQuestionList();
|
||||
if (window.LS && LS.sfx) LS.sfx.play('quiz_start');
|
||||
} catch (e) {
|
||||
LS.toast(e.message || 'Ошибка запуска вопроса', 'error');
|
||||
}
|
||||
@@ -860,6 +862,7 @@
|
||||
try {
|
||||
const data = await LS.api('/api/live/' + activeSession.id + '/results');
|
||||
renderResults(data, resultsArea);
|
||||
if (window.LS && LS.sfx) LS.sfx.play('quiz_end');
|
||||
} catch (e) {
|
||||
resultsArea.innerHTML = `<div style="padding:20px;text-align:center;color:#8898AA;font-size:0.84rem">${esc(e.message || 'Ошибка загрузки результатов')}</div>`;
|
||||
}
|
||||
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
// LearnSpace — Sound System
|
||||
// All sounds synthesized via Web Audio API. No external files needed.
|
||||
// Usage: LS.sfx.play('achievement') LS.sfx.setVolume(0.5) LS.sfx.setEnabled(false)
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ── AudioContext (lazy, respects autoplay policy) ──────────────────────────
|
||||
let _ctx = null;
|
||||
function _getCtx() {
|
||||
if (!_ctx) {
|
||||
_ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
}
|
||||
if (_ctx.state === 'suspended') _ctx.resume();
|
||||
return _ctx;
|
||||
}
|
||||
|
||||
// ── Master volume node ─────────────────────────────────────────────────────
|
||||
let _masterGain = null;
|
||||
function _getMaster() {
|
||||
if (!_masterGain) {
|
||||
_masterGain = _getCtx().createGain();
|
||||
_masterGain.connect(_getCtx().destination);
|
||||
}
|
||||
return _masterGain;
|
||||
}
|
||||
|
||||
// ── Low-level synth: single tone with ADSR envelope ───────────────────────
|
||||
// freq — Hz
|
||||
// dur — total duration ms
|
||||
// type — OscillatorType: 'sine'|'square'|'sawtooth'|'triangle'
|
||||
// vol — 0–1 (before master)
|
||||
// attack — ms
|
||||
// release — ms
|
||||
function _tone(freq, dur, type, vol, attack, release) {
|
||||
type = type || 'sine';
|
||||
vol = vol || 0.18;
|
||||
attack = attack || 8;
|
||||
release = release || Math.min(80, dur * 0.4);
|
||||
|
||||
const ctx = _getCtx();
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(_getMaster());
|
||||
|
||||
osc.type = type;
|
||||
osc.frequency.value = freq;
|
||||
|
||||
const now = ctx.currentTime;
|
||||
const durS = dur / 1000;
|
||||
const atkS = attack / 1000;
|
||||
const relS = release / 1000;
|
||||
|
||||
gain.gain.setValueAtTime(0, now);
|
||||
gain.gain.linearRampToValueAtTime(vol, now + atkS);
|
||||
if (durS - atkS - relS > 0) {
|
||||
gain.gain.setValueAtTime(vol, now + durS - relS);
|
||||
}
|
||||
gain.gain.linearRampToValueAtTime(0, now + durS);
|
||||
|
||||
osc.start(now);
|
||||
osc.stop(now + durS + 0.02);
|
||||
}
|
||||
|
||||
// ── Sequence: play notes with delay offsets ────────────────────────────────
|
||||
// notes: [ [freq, dur, delayMs, type?, vol?], ... ]
|
||||
function _seq(notes, defaultType, defaultVol) {
|
||||
notes.forEach(function (n) {
|
||||
setTimeout(function () {
|
||||
_tone(n[0], n[1], n[2] || defaultType || 'sine', n[3] !== undefined ? n[3] : (defaultVol || 0.15));
|
||||
}, n[4] || 0);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Noise burst (for 'error' thud) ────────────────────────────────────────
|
||||
function _noise(dur, vol) {
|
||||
const ctx = _getCtx();
|
||||
const bufSize = ctx.sampleRate * (dur / 1000);
|
||||
const buf = ctx.createBuffer(1, bufSize, ctx.sampleRate);
|
||||
const data = buf.getChannelData(0);
|
||||
for (let i = 0; i < bufSize; i++) data[i] = Math.random() * 2 - 1;
|
||||
const src = ctx.createBufferSource();
|
||||
const gain = ctx.createGain();
|
||||
const filt = ctx.createBiquadFilter();
|
||||
src.buffer = buf;
|
||||
filt.type = 'lowpass';
|
||||
filt.frequency.value = 400;
|
||||
src.connect(filt); filt.connect(gain); gain.connect(_getMaster());
|
||||
const now = ctx.currentTime;
|
||||
const durS = dur / 1000;
|
||||
gain.gain.setValueAtTime(vol || 0.12, now);
|
||||
gain.gain.linearRampToValueAtTime(0, now + durS);
|
||||
src.start(now);
|
||||
src.stop(now + durS + 0.02);
|
||||
}
|
||||
|
||||
// ── Sweep (pitch glide) ────────────────────────────────────────────────────
|
||||
function _sweep(f1, f2, dur, type, vol) {
|
||||
const ctx = _getCtx();
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.connect(gain); gain.connect(_getMaster());
|
||||
osc.type = type || 'sine';
|
||||
const now = ctx.currentTime;
|
||||
const durS = dur / 1000;
|
||||
osc.frequency.setValueAtTime(f1, now);
|
||||
osc.frequency.linearRampToValueAtTime(f2, now + durS);
|
||||
gain.gain.setValueAtTime(vol || 0.14, now);
|
||||
gain.gain.linearRampToValueAtTime(0, now + durS);
|
||||
osc.start(now);
|
||||
osc.stop(now + durS + 0.02);
|
||||
}
|
||||
|
||||
// ── Sound definitions ──────────────────────────────────────────────────────
|
||||
const _sounds = {
|
||||
|
||||
// ── UI ────────────────────────────────────────────────────────────────
|
||||
click: function () {
|
||||
_tone(1200, 40, 'sine', 0.10, 2, 30);
|
||||
},
|
||||
success: function () {
|
||||
_tone(523, 100, 'sine', 0.14, 8, 60);
|
||||
setTimeout(function () { _tone(659, 120, 'sine', 0.16, 8, 70); }, 90);
|
||||
setTimeout(function () { _tone(784, 180, 'sine', 0.18, 8, 100); }, 190);
|
||||
},
|
||||
error: function () {
|
||||
_tone(200, 180, 'sawtooth', 0.14, 5, 120);
|
||||
_noise(180, 0.06);
|
||||
setTimeout(function () { _tone(150, 200, 'sawtooth', 0.12, 5, 150); }, 150);
|
||||
},
|
||||
notification: function () {
|
||||
_tone(880, 80, 'sine', 0.13, 5, 50);
|
||||
setTimeout(function () { _tone(1108, 120, 'sine', 0.15, 5, 80); }, 90);
|
||||
},
|
||||
|
||||
// ── Classroom ─────────────────────────────────────────────────────────
|
||||
hand_raise: function () {
|
||||
_tone(660, 100, 'sine', 0.14, 8, 60);
|
||||
setTimeout(function () { _tone(880, 140, 'sine', 0.17, 8, 90); }, 110);
|
||||
},
|
||||
chat_message: function () {
|
||||
_tone(880, 90, 'triangle', 0.13, 5, 60);
|
||||
},
|
||||
user_joined: function () {
|
||||
_sweep(392, 523, 140, 'sine', 0.13);
|
||||
setTimeout(function () { _tone(659, 120, 'sine', 0.12, 5, 80); }, 150);
|
||||
},
|
||||
user_left: function () {
|
||||
_sweep(523, 392, 140, 'sine', 0.11);
|
||||
setTimeout(function () { _tone(330, 120, 'sine', 0.09, 5, 80); }, 150);
|
||||
},
|
||||
muted: function () {
|
||||
_tone(300, 80, 'triangle', 0.13, 5, 60);
|
||||
_noise(100, 0.05);
|
||||
},
|
||||
draw_permitted: function () {
|
||||
_tone(784, 80, 'sine', 0.13, 5, 50);
|
||||
setTimeout(function () { _tone(1047, 80, 'sine', 0.14, 5, 50); }, 85);
|
||||
setTimeout(function () { _tone(1319, 140, 'sine', 0.16, 5, 90); }, 170);
|
||||
},
|
||||
lesson_start: function () {
|
||||
// C4-E4-G4 major chord arpeggio up
|
||||
_tone(261, 160, 'sine', 0.14, 10, 100);
|
||||
setTimeout(function () { _tone(330, 160, 'sine', 0.14, 10, 100); }, 130);
|
||||
setTimeout(function () { _tone(392, 280, 'sine', 0.16, 10, 160); }, 260);
|
||||
setTimeout(function () { _tone(523, 350, 'sine', 0.18, 10, 220); }, 390);
|
||||
},
|
||||
lesson_end: function () {
|
||||
// G4-E4-C4 descending
|
||||
_tone(392, 160, 'sine', 0.15, 10, 100);
|
||||
setTimeout(function () { _tone(330, 160, 'sine', 0.14, 10, 100); }, 140);
|
||||
setTimeout(function () { _tone(261, 350, 'sine', 0.13, 10, 220); }, 280);
|
||||
},
|
||||
|
||||
// ── Gamification ──────────────────────────────────────────────────────
|
||||
xp_gain: function () {
|
||||
_tone(880, 60, 'sine', 0.10, 3, 40);
|
||||
},
|
||||
level_up: function () {
|
||||
// Fanfare: C4-E4-G4-C5
|
||||
var notes = [
|
||||
[261, 120, 'sine', 0.15, 0],
|
||||
[330, 120, 'sine', 0.15, 110],
|
||||
[392, 120, 'sine', 0.15, 220],
|
||||
[523, 360, 'sine', 0.20, 330],
|
||||
];
|
||||
notes.forEach(function (n) {
|
||||
setTimeout(function () { _tone(n[0], n[1], n[2], n[3]); }, n[4]);
|
||||
});
|
||||
// Harmony on last note
|
||||
setTimeout(function () { _tone(659, 360, 'sine', 0.12); }, 330);
|
||||
setTimeout(function () { _tone(784, 360, 'sine', 0.10); }, 330);
|
||||
},
|
||||
achievement: function () {
|
||||
// C5-E5-G5-E5-C6 sparkle
|
||||
var notes = [
|
||||
[523, 100, 0],
|
||||
[659, 100, 95],
|
||||
[784, 100, 190],
|
||||
[659, 100, 285],
|
||||
[1047, 300, 380],
|
||||
];
|
||||
notes.forEach(function (n) {
|
||||
setTimeout(function () { _tone(n[0], n[1], 'sine', 0.16); }, n[2]);
|
||||
});
|
||||
// Shimmer
|
||||
setTimeout(function () { _tone(1319, 200, 'triangle', 0.08); }, 450);
|
||||
setTimeout(function () { _tone(1047, 300, 'triangle', 0.06); }, 550);
|
||||
},
|
||||
coin: function () {
|
||||
_tone(1319, 50, 'sine', 0.14, 2, 35);
|
||||
setTimeout(function () { _tone(1047, 80, 'triangle', 0.10, 2, 60); }, 55);
|
||||
},
|
||||
streak: function () {
|
||||
_tone(523, 80, 'sine', 0.13, 5, 55);
|
||||
setTimeout(function () { _tone(659, 80, 'sine', 0.14, 5, 55); }, 80);
|
||||
setTimeout(function () { _tone(784, 160, 'sine', 0.16, 5, 100); }, 160);
|
||||
},
|
||||
|
||||
// ── Quiz ──────────────────────────────────────────────────────────────
|
||||
quiz_start: function () {
|
||||
// 3-2-1 countdown beeps
|
||||
_tone(440, 100, 'sine', 0.15, 5, 70);
|
||||
setTimeout(function () { _tone(440, 100, 'sine', 0.15, 5, 70); }, 700);
|
||||
setTimeout(function () { _tone(880, 200, 'sine', 0.20, 5, 120); }, 1400);
|
||||
},
|
||||
quiz_correct: function () {
|
||||
_tone(659, 90, 'sine', 0.15, 5, 60);
|
||||
setTimeout(function () { _tone(880, 160, 'sine', 0.18, 5, 100); }, 95);
|
||||
},
|
||||
quiz_wrong: function () {
|
||||
_tone(220, 200, 'sawtooth', 0.13, 5, 150);
|
||||
setTimeout(function () { _tone(180, 250, 'sawtooth', 0.10, 5, 200); }, 180);
|
||||
},
|
||||
quiz_end: function () {
|
||||
// C major chord burst
|
||||
_tone(523, 400, 'sine', 0.14, 10, 250);
|
||||
_tone(659, 400, 'sine', 0.12, 10, 250);
|
||||
_tone(784, 400, 'sine', 0.10, 10, 250);
|
||||
setTimeout(function () { _tone(1047, 500, 'sine', 0.18, 10, 350); }, 350);
|
||||
},
|
||||
quiz_tick: function () {
|
||||
_tone(660, 40, 'square', 0.08, 2, 25);
|
||||
},
|
||||
};
|
||||
|
||||
// ── Persistence ───────────────────────────────────────────────────────────
|
||||
var LS_SFX_KEY = 'ls_sfx';
|
||||
|
||||
function _load() {
|
||||
try {
|
||||
var raw = localStorage.getItem(LS_SFX_KEY);
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
function _save() {
|
||||
try {
|
||||
localStorage.setItem(LS_SFX_KEY, JSON.stringify({
|
||||
enabled : sfx.enabled,
|
||||
volume : sfx.volume,
|
||||
prefs : sfx.prefs,
|
||||
}));
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ── Public API ─────────────────────────────────────────────────────────────
|
||||
var sfx = {
|
||||
enabled : true,
|
||||
volume : 0.75,
|
||||
prefs : {
|
||||
ui : true,
|
||||
classroom : true,
|
||||
gamification : true,
|
||||
quiz : true,
|
||||
},
|
||||
|
||||
// Category map — which prefs key each sound belongs to
|
||||
_cats: {
|
||||
click: 'ui', success: 'ui', error: 'ui', notification: 'ui',
|
||||
hand_raise: 'classroom', chat_message: 'classroom',
|
||||
user_joined: 'classroom', user_left: 'classroom',
|
||||
muted: 'classroom', draw_permitted: 'classroom',
|
||||
lesson_start: 'classroom', lesson_end: 'classroom',
|
||||
xp_gain: 'gamification', level_up: 'gamification',
|
||||
achievement: 'gamification', coin: 'gamification', streak: 'gamification',
|
||||
quiz_start: 'quiz', quiz_correct: 'quiz', quiz_wrong: 'quiz',
|
||||
quiz_end: 'quiz', quiz_tick: 'quiz',
|
||||
},
|
||||
|
||||
play: function (name) {
|
||||
if (!this.enabled) return;
|
||||
var cat = this._cats[name];
|
||||
if (cat && !this.prefs[cat]) return;
|
||||
if (!_sounds[name]) return;
|
||||
// Apply volume to master gain
|
||||
if (_masterGain) _masterGain.gain.value = this.volume;
|
||||
try { _sounds[name](); } catch (e) {}
|
||||
},
|
||||
|
||||
setEnabled: function (v) {
|
||||
this.enabled = !!v;
|
||||
_save();
|
||||
},
|
||||
|
||||
setVolume: function (v) {
|
||||
this.volume = Math.max(0, Math.min(1, parseFloat(v) || 0));
|
||||
if (_masterGain) _masterGain.gain.value = this.volume;
|
||||
_save();
|
||||
},
|
||||
|
||||
setPref: function (cat, v) {
|
||||
if (this.prefs.hasOwnProperty(cat)) {
|
||||
this.prefs[cat] = !!v;
|
||||
_save();
|
||||
}
|
||||
},
|
||||
|
||||
init: function () {
|
||||
var saved = _load();
|
||||
if (!saved) return;
|
||||
if (saved.enabled !== undefined) this.enabled = saved.enabled;
|
||||
if (saved.volume !== undefined) this.volume = saved.volume;
|
||||
if (saved.prefs) {
|
||||
var self = this;
|
||||
Object.keys(saved.prefs).forEach(function (k) {
|
||||
if (self.prefs.hasOwnProperty(k)) self.prefs[k] = saved.prefs[k];
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Attach to LS namespace
|
||||
window.LS = window.LS || {};
|
||||
window.LS.sfx = sfx;
|
||||
|
||||
// Auto-init on load
|
||||
sfx.init();
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user