mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 22:59:59 -07:00
Replace emojis throughout the codebase with inline SVG icons using the Icons utility. Remove decorative icons where text labels already describe the content. Add classification dot CSS for risk indicators. - Extend Icons utility with comprehensive SVG icon set - Update navigation, header stats, and action buttons - Update playback controls and volume icons - Remove decorative device type and panel header emojis - Clean up notifications and alert messages - Add CSS for classification status dots Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
282 lines
8.7 KiB
JavaScript
282 lines
8.7 KiB
JavaScript
/**
|
|
* Intercept - Audio System
|
|
* Web Audio API alerts, notifications, and sound effects
|
|
*/
|
|
|
|
// ============== AUDIO STATE ==============
|
|
|
|
let audioContext = null;
|
|
let audioMuted = localStorage.getItem('audioMuted') === 'true';
|
|
let notificationsEnabled = false;
|
|
|
|
// ============== AUDIO CONTEXT ==============
|
|
|
|
/**
|
|
* Initialize the Web Audio API context
|
|
* Must be called after user interaction due to browser autoplay policies
|
|
*/
|
|
function initAudio() {
|
|
if (!audioContext) {
|
|
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
}
|
|
return audioContext;
|
|
}
|
|
|
|
/**
|
|
* Get or create the audio context
|
|
* @returns {AudioContext}
|
|
*/
|
|
function getAudioContext() {
|
|
if (!audioContext) {
|
|
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
}
|
|
return audioContext;
|
|
}
|
|
|
|
// ============== ALERT SOUNDS ==============
|
|
|
|
/**
|
|
* Play a basic alert beep
|
|
* Used for message received notifications
|
|
*/
|
|
function playAlert() {
|
|
if (audioMuted || !audioContext) return;
|
|
|
|
try {
|
|
const oscillator = audioContext.createOscillator();
|
|
const gainNode = audioContext.createGain();
|
|
oscillator.connect(gainNode);
|
|
gainNode.connect(audioContext.destination);
|
|
oscillator.frequency.value = 880;
|
|
oscillator.type = 'sine';
|
|
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
|
|
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2);
|
|
oscillator.start(audioContext.currentTime);
|
|
oscillator.stop(audioContext.currentTime + 0.2);
|
|
} catch (e) {
|
|
console.warn('Audio alert failed:', e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Play alert sound by type
|
|
* @param {string} type - 'emergency', 'military', 'warning', 'info'
|
|
*/
|
|
function playAlertSound(type) {
|
|
if (audioMuted) return;
|
|
|
|
try {
|
|
const ctx = getAudioContext();
|
|
const oscillator = ctx.createOscillator();
|
|
const gainNode = ctx.createGain();
|
|
|
|
oscillator.connect(gainNode);
|
|
gainNode.connect(ctx.destination);
|
|
|
|
switch (type) {
|
|
case 'emergency':
|
|
// Urgent two-tone alert for emergencies
|
|
oscillator.frequency.setValueAtTime(880, ctx.currentTime);
|
|
oscillator.frequency.setValueAtTime(660, ctx.currentTime + 0.15);
|
|
oscillator.frequency.setValueAtTime(880, ctx.currentTime + 0.3);
|
|
gainNode.gain.setValueAtTime(0.3, ctx.currentTime);
|
|
gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.5);
|
|
oscillator.start(ctx.currentTime);
|
|
oscillator.stop(ctx.currentTime + 0.5);
|
|
break;
|
|
|
|
case 'military':
|
|
// Single tone for military aircraft detection
|
|
oscillator.frequency.setValueAtTime(523, ctx.currentTime);
|
|
gainNode.gain.setValueAtTime(0.2, ctx.currentTime);
|
|
gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3);
|
|
oscillator.start(ctx.currentTime);
|
|
oscillator.stop(ctx.currentTime + 0.3);
|
|
break;
|
|
|
|
case 'warning':
|
|
// Warning tone (descending)
|
|
oscillator.frequency.setValueAtTime(660, ctx.currentTime);
|
|
oscillator.frequency.exponentialRampToValueAtTime(440, ctx.currentTime + 0.3);
|
|
gainNode.gain.setValueAtTime(0.25, ctx.currentTime);
|
|
gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3);
|
|
oscillator.start(ctx.currentTime);
|
|
oscillator.stop(ctx.currentTime + 0.3);
|
|
break;
|
|
|
|
case 'info':
|
|
default:
|
|
// Simple info tone
|
|
oscillator.frequency.setValueAtTime(440, ctx.currentTime);
|
|
gainNode.gain.setValueAtTime(0.15, ctx.currentTime);
|
|
gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.15);
|
|
oscillator.start(ctx.currentTime);
|
|
oscillator.stop(ctx.currentTime + 0.15);
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
console.warn('Audio alert failed:', e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Play scanner signal detected sound
|
|
* A distinctive ascending tone for radio scanner
|
|
*/
|
|
function playSignalDetectedSound() {
|
|
if (audioMuted) return;
|
|
|
|
try {
|
|
const ctx = getAudioContext();
|
|
const oscillator = ctx.createOscillator();
|
|
const gainNode = ctx.createGain();
|
|
|
|
oscillator.connect(gainNode);
|
|
gainNode.connect(ctx.destination);
|
|
|
|
// Ascending tone
|
|
oscillator.frequency.setValueAtTime(400, ctx.currentTime);
|
|
oscillator.frequency.exponentialRampToValueAtTime(800, ctx.currentTime + 0.15);
|
|
oscillator.type = 'sine';
|
|
|
|
gainNode.gain.setValueAtTime(0.2, ctx.currentTime);
|
|
gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.2);
|
|
|
|
oscillator.start(ctx.currentTime);
|
|
oscillator.stop(ctx.currentTime + 0.2);
|
|
} catch (e) {
|
|
console.warn('Signal detected sound failed:', e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Play a click sound for UI feedback
|
|
*/
|
|
function playClickSound() {
|
|
if (audioMuted) return;
|
|
|
|
try {
|
|
const ctx = getAudioContext();
|
|
const oscillator = ctx.createOscillator();
|
|
const gainNode = ctx.createGain();
|
|
|
|
oscillator.connect(gainNode);
|
|
gainNode.connect(ctx.destination);
|
|
|
|
oscillator.frequency.value = 1000;
|
|
oscillator.type = 'square';
|
|
|
|
gainNode.gain.setValueAtTime(0.1, ctx.currentTime);
|
|
gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.05);
|
|
|
|
oscillator.start(ctx.currentTime);
|
|
oscillator.stop(ctx.currentTime + 0.05);
|
|
} catch (e) {
|
|
console.warn('Click sound failed:', e);
|
|
}
|
|
}
|
|
|
|
// ============== MUTE CONTROL ==============
|
|
|
|
/**
|
|
* Toggle mute state
|
|
*/
|
|
function toggleMute() {
|
|
audioMuted = !audioMuted;
|
|
localStorage.setItem('audioMuted', audioMuted);
|
|
updateMuteButton();
|
|
}
|
|
|
|
/**
|
|
* Set mute state
|
|
* @param {boolean} muted - Whether audio should be muted
|
|
*/
|
|
function setMuted(muted) {
|
|
audioMuted = muted;
|
|
localStorage.setItem('audioMuted', audioMuted);
|
|
updateMuteButton();
|
|
}
|
|
|
|
/**
|
|
* Get current mute state
|
|
* @returns {boolean}
|
|
*/
|
|
function isMuted() {
|
|
return audioMuted;
|
|
}
|
|
|
|
/**
|
|
* Update mute button UI
|
|
*/
|
|
function updateMuteButton() {
|
|
const btn = document.getElementById('muteBtn');
|
|
if (btn) {
|
|
btn.innerHTML = audioMuted ? Icons.volumeOff('icon--sm') + ' UNMUTE' : Icons.volumeOn('icon--sm') + ' MUTE';
|
|
btn.classList.toggle('muted', audioMuted);
|
|
}
|
|
}
|
|
|
|
// ============== DESKTOP NOTIFICATIONS ==============
|
|
|
|
/**
|
|
* Request notification permission from user
|
|
*/
|
|
function requestNotificationPermission() {
|
|
if ('Notification' in window && Notification.permission === 'default') {
|
|
Notification.requestPermission().then(permission => {
|
|
notificationsEnabled = permission === 'granted';
|
|
if (notificationsEnabled && typeof showInfo === 'function') {
|
|
showInfo('Desktop notifications enabled');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show a desktop notification
|
|
* @param {string} title - Notification title
|
|
* @param {string} body - Notification body
|
|
*/
|
|
function showNotification(title, body) {
|
|
if (notificationsEnabled && document.hidden) {
|
|
new Notification(title, {
|
|
body: body,
|
|
icon: '/favicon.ico',
|
|
tag: 'intercept-' + Date.now()
|
|
});
|
|
}
|
|
}
|
|
|
|
// ============== INITIALIZATION ==============
|
|
|
|
/**
|
|
* Initialize audio system
|
|
* Should be called on first user interaction
|
|
*/
|
|
function initAudioSystem() {
|
|
// Initialize audio context
|
|
initAudio();
|
|
|
|
// Update mute button state
|
|
updateMuteButton();
|
|
|
|
// Check notification permission
|
|
if ('Notification' in window) {
|
|
if (Notification.permission === 'granted') {
|
|
notificationsEnabled = true;
|
|
} else if (Notification.permission === 'default') {
|
|
// Will request on first interaction
|
|
document.addEventListener('click', function requestOnce() {
|
|
requestNotificationPermission();
|
|
document.removeEventListener('click', requestOnce);
|
|
}, { once: true });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize on first user interaction (required for Web Audio API)
|
|
document.addEventListener('click', function initOnInteraction() {
|
|
initAudio();
|
|
document.removeEventListener('click', initOnInteraction);
|
|
}, { once: true });
|