win-stream-audio
Version:
🎧 Stream Windows system audio to Android devices over WiFi with professional audio controls, EQ, pitch shifting, and effects
551 lines (462 loc) • 22.6 kB
JavaScript
/**
* AudioPilot - UI Controls Module
* Handles all user interface interactions and controls
*/
class UIControls {
constructor(audioProcessor, connectionManager, logger) {
this.audioProcessor = audioProcessor;
this.connectionManager = connectionManager;
this.logger = logger;
// Get all UI elements
this.elements = this.getUIElements();
// Control states
this.screenLockActive = false;
this.lowLatencyMode = false;
this.internetMode = false;
this.debugMode = false;
// Screen wake system
this.noSleep = null;
this.wakeLock = null;
this.initializeControls();
}
getUIElements() {
return {
// Connection controls
connectBtn: document.getElementById('connectBtn'),
disconnectBtn: document.getElementById('disconnectBtn'),
reconnectBtn: document.getElementById('reconnectBtn'),
testBtn: document.getElementById('testBtn'),
// System controls
screenLockBtn: document.getElementById('screenLockBtn'),
lowLatencyBtn: document.getElementById('lowLatencyBtn'),
internetModeBtn: document.getElementById('internetModeBtn'),
audioFilterBtn: document.getElementById('audioFilterBtn'),
// Debug controls
debugToggle: document.getElementById('debugToggle'),
clearDebugBtn: document.getElementById('clearDebugBtn'),
// Status indicators
connectionLight: document.getElementById('connectionLight'),
audioLight: document.getElementById('audioLight'),
screenLight: document.getElementById('screenLight'),
bufferLevel: document.getElementById('bufferLevel'),
latencyDisplay: document.getElementById('latencyDisplay'),
// Volume control
volumeKnob: document.getElementById('volumeKnob'),
volumeValue: document.getElementById('volumeValue'),
// EQ controls
bassSlider: document.getElementById('bassSlider'),
midSlider: document.getElementById('midSlider'),
trebleSlider: document.getElementById('trebleSlider'),
bassValue: document.getElementById('bassValue'),
midValue: document.getElementById('midValue'),
trebleValue: document.getElementById('trebleValue'),
// Pitch controls
pitchKnob: document.getElementById('pitchKnob'),
pitchValue: document.getElementById('pitchValue'),
pitchReset: document.getElementById('pitchReset'),
pitchMale: document.getElementById('pitchMale'),
pitchFemale: document.getElementById('pitchFemale'),
pitchChild: document.getElementById('pitchChild'),
pitchRobot: document.getElementById('pitchRobot'),
// Effect controls
reverbBtn: document.getElementById('reverbBtn'),
echoBtn: document.getElementById('echoBtn'),
distortionBtn: document.getElementById('distortionBtn'),
chorusBtn: document.getElementById('chorusBtn'),
// Screen wake
screenWakeVideo: document.getElementById('screenWakeVideo')
};
}
initializeControls() {
this.setupConnectionControls();
this.setupSystemControls();
this.setupVolumeControl();
this.setupEQControls();
this.setupPitchControls();
this.setupEffectControls();
this.setupDebugControls();
this.setupScreenWake();
this.setupConnectionCallbacks();
// Set initial states
this.elements.audioFilterBtn.classList.add('active');
this.elements.audioFilterBtn.textContent = '🎛️ Filter: ON';
this.logger.log('🎛️ UI Controls Initialized');
}
setupConnectionControls() {
this.elements.connectBtn.onclick = () => {
this.connectionManager.connectToMissionControl()
.then(() => {
this.elements.connectBtn.disabled = true;
this.elements.disconnectBtn.disabled = false;
this.elements.reconnectBtn.disabled = true;
})
.catch((error) => {
this.logger.log('❌ Connection failed: ' + error.message);
});
};
this.elements.disconnectBtn.onclick = () => {
this.connectionManager.disconnectFromMissionControl();
// Stop screen lock
if (this.screenLockActive) {
this.toggleScreenLock();
}
this.elements.connectBtn.disabled = false;
this.elements.disconnectBtn.disabled = true;
this.elements.reconnectBtn.disabled = false;
};
this.elements.reconnectBtn.onclick = () => {
this.connectionManager.reconnectToMissionControl()
.then(() => {
this.elements.connectBtn.disabled = true;
this.elements.disconnectBtn.disabled = false;
this.elements.reconnectBtn.disabled = true;
})
.catch((error) => {
this.logger.log('❌ Reconnection failed: ' + error.message);
});
};
this.elements.testBtn.onclick = () => {
if (this.audioProcessor.playTestTone()) {
this.logger.log('✅ Audio Test Complete - 440Hz Tone');
}
};
}
setupSystemControls() {
this.elements.screenLockBtn.onclick = () => this.toggleScreenLock();
this.elements.lowLatencyBtn.onclick = () => this.toggleLowLatency();
this.elements.internetModeBtn.onclick = () => this.toggleInternetMode();
this.elements.audioFilterBtn.onclick = () => this.toggleAudioFilter();
}
setupVolumeControl() {
const knob = this.elements.volumeKnob;
let isDragging = false;
let startAngle = 0;
const updateKnobRotation = () => {
const rotation = (this.audioProcessor.masterVolume * 270) - 135;
knob.style.transform = `rotate(${rotation}deg)`;
this.elements.volumeValue.textContent = `${Math.round(this.audioProcessor.masterVolume * 100)}%`;
};
knob.addEventListener('mousedown', startDrag);
knob.addEventListener('touchstart', startDrag);
function startDrag(e) {
isDragging = true;
const rect = knob.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const clientX = e.clientX || e.touches[0].clientX;
const clientY = e.clientY || e.touches[0].clientY;
startAngle = Math.atan2(clientY - centerY, clientX - centerX);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', stopDrag);
document.addEventListener('touchmove', drag);
document.addEventListener('touchend', stopDrag);
}
const drag = (e) => {
if (!isDragging) return;
const rect = knob.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const clientX = e.clientX || e.touches[0].clientX;
const clientY = e.clientY || e.touches[0].clientY;
const currentAngle = Math.atan2(clientY - centerY, clientX - centerX);
const deltaAngle = currentAngle - startAngle;
const newVolume = Math.max(0, Math.min(1, this.audioProcessor.masterVolume + (deltaAngle / (Math.PI * 1.5))));
this.audioProcessor.masterVolume = newVolume;
updateKnobRotation();
startAngle = currentAngle;
};
function stopDrag() {
isDragging = false;
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', stopDrag);
document.removeEventListener('touchmove', drag);
document.removeEventListener('touchend', stopDrag);
}
updateKnobRotation();
}
setupEQControls() {
this.elements.bassSlider.oninput = () => {
this.audioProcessor.eqSettings.bass = parseFloat(this.elements.bassSlider.value);
this.elements.bassValue.textContent = `${this.audioProcessor.eqSettings.bass > 0 ? '+' : ''}${this.audioProcessor.eqSettings.bass}dB`;
this.logger.log(`🎛️ Bass: ${this.audioProcessor.eqSettings.bass}dB`);
};
this.elements.midSlider.oninput = () => {
this.audioProcessor.eqSettings.mid = parseFloat(this.elements.midSlider.value);
this.elements.midValue.textContent = `${this.audioProcessor.eqSettings.mid > 0 ? '+' : ''}${this.audioProcessor.eqSettings.mid}dB`;
this.logger.log(`🎛️ Mid: ${this.audioProcessor.eqSettings.mid}dB`);
};
this.elements.trebleSlider.oninput = () => {
this.audioProcessor.eqSettings.treble = parseFloat(this.elements.trebleSlider.value);
this.elements.trebleValue.textContent = `${this.audioProcessor.eqSettings.treble > 0 ? '+' : ''}${this.audioProcessor.eqSettings.treble}dB`;
this.logger.log(`🎛️ Treble: ${this.audioProcessor.eqSettings.treble}dB`);
};
}
setupPitchControls() {
const pitchKnob = this.elements.pitchKnob;
let isDragging = false;
let startAngle = 0;
const updatePitchKnobRotation = () => {
const rotation = (this.audioProcessor.pitchShift / 1200) * 270;
pitchKnob.style.transform = `rotate(${rotation}deg)`;
this.elements.pitchValue.textContent = `${this.audioProcessor.pitchShift > 0 ? '+' : ''}${this.audioProcessor.pitchShift} cents`;
};
pitchKnob.addEventListener('mousedown', startDrag);
pitchKnob.addEventListener('touchstart', startDrag);
function startDrag(e) {
isDragging = true;
const rect = pitchKnob.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const clientX = e.clientX || e.touches[0].clientX;
const clientY = e.clientY || e.touches[0].clientY;
startAngle = Math.atan2(clientY - centerY, clientX - centerX);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', stopDrag);
document.addEventListener('touchmove', drag);
document.addEventListener('touchend', stopDrag);
}
const drag = (e) => {
if (!isDragging) return;
const rect = pitchKnob.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const clientX = e.clientX || e.touches[0].clientX;
const clientY = e.clientY || e.touches[0].clientY;
const currentAngle = Math.atan2(clientY - centerY, clientX - centerX);
const deltaAngle = currentAngle - startAngle;
const newPitch = Math.max(-1200, Math.min(1200, this.audioProcessor.pitchShift + (deltaAngle * 200)));
this.audioProcessor.pitchShift = Math.round(newPitch);
updatePitchKnobRotation();
startAngle = currentAngle;
this.logger.log(`🎵 Pitch: ${this.audioProcessor.pitchShift} cents`);
};
function stopDrag() {
isDragging = false;
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', stopDrag);
document.removeEventListener('touchmove', drag);
document.removeEventListener('touchend', stopDrag);
}
// Pitch preset buttons
this.elements.pitchReset.onclick = () => this.setPitchPreset(0, 'Reset');
this.elements.pitchMale.onclick = () => this.setPitchPreset(-200, 'Male');
this.elements.pitchFemale.onclick = () => this.setPitchPreset(300, 'Female');
this.elements.pitchChild.onclick = () => this.setPitchPreset(600, 'Child');
this.elements.pitchRobot.onclick = () => this.setPitchPreset(-800, 'Robot');
updatePitchKnobRotation();
}
setPitchPreset(cents, name) {
this.audioProcessor.pitchShift = cents;
const rotation = (cents / 1200) * 270;
this.elements.pitchKnob.style.transform = `rotate(${rotation}deg)`;
this.elements.pitchValue.textContent = `${cents > 0 ? '+' : ''}${cents} cents`;
// Update button states
document.querySelectorAll('.preset-btn').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
this.logger.log(`🎵 Pitch Preset: ${name} (${cents} cents)`);
}
setupEffectControls() {
this.elements.reverbBtn.onclick = () => this.toggleEffect('reverb', '🏛️ Reverb');
this.elements.echoBtn.onclick = () => this.toggleEffect('echo', '📢 Echo');
this.elements.distortionBtn.onclick = () => this.toggleEffect('distortion', '⚡ Distortion');
this.elements.chorusBtn.onclick = () => this.toggleEffect('chorus', '🌊 Chorus');
}
toggleEffect(effectName, displayName) {
this.audioProcessor.activeEffects[effectName] = !this.audioProcessor.activeEffects[effectName];
const button = event.target;
if (this.audioProcessor.activeEffects[effectName]) {
button.classList.add('active');
this.logger.log(`🎭 ${displayName} Enabled`);
} else {
button.classList.remove('active');
this.logger.log(`🎭 ${displayName} Disabled`);
}
}
setupDebugControls() {
this.elements.debugToggle.onclick = () => {
this.debugMode = !this.debugMode;
if (this.debugMode) {
this.elements.debugToggle.classList.add('active');
this.elements.debugToggle.textContent = '📋 Debug: ON';
this.logger.enableHistory();
this.logger.log('🔍 Debug Mode Activated');
} else {
this.elements.debugToggle.classList.remove('active');
this.elements.debugToggle.textContent = '📋 Debug Log';
this.logger.disableHistory();
this.logger.log('🔍 Debug Mode Deactivated');
}
};
this.elements.clearDebugBtn.onclick = () => {
this.logger.clear();
this.logger.log('🗑️ Mission Log Cleared');
};
}
setupScreenWake() {
this.createTransparentVideo();
if (typeof NoSleep !== 'undefined') {
this.noSleep = new NoSleep();
this.logger.log('😴 NoSleep.js System Ready');
}
}
createTransparentVideo() {
const video = this.elements.screenWakeVideo;
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const ctx = canvas.getContext('2d');
let frame = 0;
function animateTransparent() {
ctx.clearRect(0, 0, 1, 1);
ctx.fillStyle = `rgba(0, 0, 0, ${0.01 + (Math.sin(frame * 0.1) * 0.005)})`;
ctx.fillRect(0, 0, 1, 1);
frame++;
requestAnimationFrame(animateTransparent);
}
const stream = canvas.captureStream(30);
video.srcObject = stream;
animateTransparent();
this.logger.log('📹 Transparent Video System Ready');
}
setupConnectionCallbacks() {
this.connectionManager.onConnectionChange = (connected) => {
if (connected) {
this.elements.connectionLight.classList.add('connected');
} else {
this.elements.connectionLight.classList.remove('connected');
this.elements.audioLight.classList.remove('connected');
}
};
this.connectionManager.onAudioStateChange = (active) => {
if (active) {
this.elements.audioLight.classList.add('connected');
} else {
this.elements.audioLight.classList.remove('connected');
}
};
this.connectionManager.onBufferUpdate = (current, max) => {
this.elements.bufferLevel.textContent = `${current}/${max}`;
};
}
// System control methods
toggleScreenLock() {
this.screenLockActive = !this.screenLockActive;
if (this.screenLockActive) {
this.elements.screenLockBtn.classList.add('active');
this.elements.screenLockBtn.textContent = '📱 Screen: LOCKED';
this.elements.screenLight.classList.add('connected');
this.activateScreenLock();
this.logger.log('🔒 Screen Lock Activated');
} else {
this.elements.screenLockBtn.classList.remove('active');
this.elements.screenLockBtn.textContent = '📱 Screen Lock';
this.elements.screenLight.classList.remove('connected');
this.deactivateScreenLock();
this.logger.log('🔓 Screen Lock Deactivated');
}
}
activateScreenLock() {
// NoSleep.js
if (this.noSleep) {
try {
this.noSleep.enable();
this.logger.log('😴 NoSleep System Engaged');
} catch (error) {
this.logger.log('❌ NoSleep Failed: ' + error.message);
}
}
// Wake Lock API
if ('wakeLock' in navigator) {
navigator.wakeLock.request('screen').then(lock => {
this.wakeLock = lock;
this.logger.log('🔒 Wake Lock Acquired');
lock.addEventListener('release', () => {
this.logger.log('🔓 Wake Lock Released - Reacquiring...');
if (this.screenLockActive) {
setTimeout(() => {
if (this.screenLockActive) this.activateScreenLock();
}, 1000);
}
});
}).catch(error => {
this.logger.log('❌ Wake Lock Failed: ' + error.message);
});
}
// Play transparent video
this.elements.screenWakeVideo.play().catch(error => {
this.logger.log('❌ Screen Wake Video Failed: ' + error.message);
});
// Periodic interaction simulation
window.screenKeepAlive = setInterval(() => {
document.dispatchEvent(new Event('touchstart'));
document.dispatchEvent(new Event('touchend'));
}, 15000);
}
deactivateScreenLock() {
if (this.noSleep) {
try {
this.noSleep.disable();
} catch (error) {
this.logger.log('❌ NoSleep Disable Failed: ' + error.message);
}
}
if (this.wakeLock) {
this.wakeLock.release();
this.wakeLock = null;
}
this.elements.screenWakeVideo.pause();
if (window.screenKeepAlive) {
clearInterval(window.screenKeepAlive);
window.screenKeepAlive = null;
}
}
toggleLowLatency() {
if (this.internetMode) {
this.logger.log('❌ Cannot Enable Low Latency in Internet Mode');
return;
}
this.lowLatencyMode = !this.lowLatencyMode;
if (this.lowLatencyMode) {
this.elements.lowLatencyBtn.classList.add('active');
this.elements.lowLatencyBtn.textContent = '⚡ Low Latency: ON';
this.connectionManager.setBufferSize(2);
this.logger.log('⚡ Low Latency Mode Engaged - Buffer: 2');
} else {
this.elements.lowLatencyBtn.classList.remove('active');
this.elements.lowLatencyBtn.textContent = '⚡ Low Latency';
this.connectionManager.setBufferSize(this.internetMode ? 10 : 5);
this.logger.log('🔄 Normal Latency Mode - Buffer: ' + (this.internetMode ? 10 : 5));
}
}
toggleInternetMode() {
this.internetMode = !this.internetMode;
if (this.internetMode) {
this.elements.internetModeBtn.classList.add('active');
this.elements.internetModeBtn.textContent = '🌐 Internet: ON';
this.connectionManager.setBufferSize(10);
this.lowLatencyMode = false;
this.elements.lowLatencyBtn.classList.remove('active');
this.elements.lowLatencyBtn.textContent = '⚡ Low Latency';
this.logger.log('🌐 Internet Mode Engaged - Buffer: 10');
} else {
this.elements.internetModeBtn.classList.remove('active');
this.elements.internetModeBtn.textContent = '🌐 Internet Mode';
this.connectionManager.setBufferSize(5);
this.logger.log('🏠 Local Network Mode - Buffer: 5');
}
}
toggleAudioFilter() {
this.audioProcessor.audioFilterMode = !this.audioProcessor.audioFilterMode;
if (this.audioProcessor.audioFilterMode) {
this.elements.audioFilterBtn.classList.add('active');
this.elements.audioFilterBtn.textContent = '🎛️ Filter: ON';
this.logger.log('🎛️ Audio Filter Enabled - Noise Reduction Active');
} else {
this.elements.audioFilterBtn.classList.remove('active');
this.elements.audioFilterBtn.textContent = '🎛️ Audio Filter';
this.logger.log('🎛️ Audio Filter Disabled - Raw Audio Mode');
}
}
}
// Export for use in other modules
window.UIControls = UIControls;