mirror of
https://github.com/smittix/intercept.git
synced 2026-05-30 17:53:38 -07:00
2cb62d5f34
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 });
|