1/* 2 * Chat notification tones, synthesized with the Web Audio API. 3 * 4 * Shared by the operator console and the chat widget so both sides use one definition of what 5 * "chime" means, and so neither needs an audio file deployed, cached or preloaded. 6 * 7 * Usage: ChatSound.configure({ tone: 'chime', volume: 60 }); ChatSound.play(); 8 * 9 * Browsers refuse to start audio before the user has interacted with the page, so the context is 10 * created lazily on the first play and the first attempt may be silently dropped â by then the 11 * user has almost always clicked something. 12 */ 13(function (global) { 14 "use strict"; 15 16 // note = [frequency Hz, start offset s, duration s] 17 var TONES = { 18 none: [], 19 soft: [[660, 0, 0.09]], 20 chime: [[880, 0, 0.11], [1175, 0.10, 0.16]], 21 ding: [[1320, 0, 0.22]], 22 pop: [[420, 0, 0.06], [700, 0.05, 0.07]], 23 alert: [[784, 0, 0.09], [988, 0.10, 0.09], [1319, 0.20, 0.17]] 24 }; 25 26 var ctx = null; 27 var tone = "none"; 28 var volume = 0.6; 29 30 function ensureContext() { 31 if (ctx) return ctx; 32 var AC = global.AudioContext || global.webkitAudioContext; 33 if (!AC) return null; 34 try { ctx = new AC(); } catch (e) { return null; } 35 return ctx; 36 } 37 38 function play() { 39 var notes = TONES[tone]; 40 if (!notes || !notes.length || volume <= 0) return; 41 var ac = ensureContext(); 42 if (!ac) return; 43 // Autoplay policy parks the context until a gesture; resuming is a no-op when it's running. 44 if (ac.state === "suspended" && ac.resume) { try { ac.resume(); } catch (e) {} } 45 46 var now = ac.currentTime; 47 notes.forEach(function (n) { 48 var osc = ac.createOscillator(); 49 var gain = ac.createGain(); 50 osc.type = "sine"; 51 osc.frequency.value = n[0]; 52 var start = now + n[1]; 53 var end = start + n[2]; 54 // Ramp in and out: a square-edged gain change is audible as a click. 55 gain.gain.setValueAtTime(0.0001, start); 56 gain.gain.exponentialRampToValueAtTime(Math.max(0.0001, volume), start + 0.012); 57 gain.gain.exponentialRampToValueAtTime(0.0001, end); 58 osc.connect(gain).connect(ac.destination); 59 osc.start(start); 60 osc.stop(end + 0.02); 61 }); 62 } 63 64 global.ChatSound = { 65 configure: function (opts) { 66 opts = opts || {}; 67 if (typeof opts.tone === "string" && TONES[opts.tone]) tone = opts.tone; 68 if (typeof opts.volume === "number") volume = Math.max(0, Math.min(100, opts.volume)) / 100; 69 }, 70 play: play, 71 // Lets the settings page preview a tone without changing what's configured. 72 preview: function (name, vol) { 73 var prevTone = tone, prevVol = volume; 74 tone = TONES[name] ? name : "none"; 75 if (typeof vol === "number") volume = Math.max(0, Math.min(100, vol)) / 100; 76 play(); 77 tone = prevTone; volume = prevVol; 78 }, 79 tones: Object.keys(TONES) 80 }; 81})(window);
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.