Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Software ANC & Focus Music</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| min-height: 100vh; | |
| background: linear-gradient(135deg, #1e293b 0%, #7e22ce 50%, #1e293b 100%); | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 600px; | |
| margin: 0 auto; | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| color: white; | |
| } | |
| .header h1 { | |
| font-size: 32px; | |
| margin-bottom: 8px; | |
| } | |
| .header p { | |
| color: #e9d5ff; | |
| font-size: 14px; | |
| } | |
| .card { | |
| background: rgba(255, 255, 255, 0.1); | |
| backdrop-filter: blur(10px); | |
| border-radius: 16px; | |
| padding: 24px; | |
| margin-bottom: 20px; | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| } | |
| .card-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| flex-wrap: wrap; | |
| gap: 12px; | |
| } | |
| .card-title { | |
| font-size: 24px; | |
| color: white; | |
| font-weight: 600; | |
| } | |
| .btn { | |
| padding: 12px 24px; | |
| border-radius: 12px; | |
| border: none; | |
| font-weight: 600; | |
| font-size: 14px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| } | |
| .btn-primary { | |
| background: #a855f7; | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| background: #9333ea; | |
| } | |
| .btn-danger { | |
| background: #ef4444; | |
| color: white; | |
| } | |
| .btn-danger:hover { | |
| background: #dc2626; | |
| } | |
| .slider-group { | |
| margin-bottom: 20px; | |
| } | |
| .slider-label { | |
| display: block; | |
| color: #e9d5ff; | |
| font-size: 14px; | |
| font-weight: 500; | |
| margin-bottom: 8px; | |
| } | |
| .slider { | |
| width: 100%; | |
| height: 8px; | |
| border-radius: 4px; | |
| background: rgba(216, 180, 254, 0.3); | |
| outline: none; | |
| -webkit-appearance: none; | |
| appearance: none; | |
| } | |
| .slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #a855f7; | |
| cursor: pointer; | |
| } | |
| .slider::-moz-range-thumb { | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #a855f7; | |
| cursor: pointer; | |
| border: none; | |
| } | |
| .info-text { | |
| color: #e9d5ff; | |
| font-size: 13px; | |
| line-height: 1.5; | |
| margin-top: 12px; | |
| } | |
| .error { | |
| background: rgba(239, 68, 68, 0.2); | |
| border: 1px solid #ef4444; | |
| color: #fecaca; | |
| padding: 16px; | |
| border-radius: 12px; | |
| margin-bottom: 20px; | |
| font-size: 14px; | |
| } | |
| .success { | |
| background: rgba(34, 197, 94, 0.2); | |
| border: 1px solid #22c55e; | |
| color: #bbf7d0; | |
| padding: 16px; | |
| border-radius: 12px; | |
| margin-bottom: 20px; | |
| font-size: 14px; | |
| } | |
| .button-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 12px; | |
| margin-bottom: 20px; | |
| } | |
| .type-btn { | |
| padding: 12px; | |
| border-radius: 8px; | |
| border: none; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| background: rgba(255, 255, 255, 0.05); | |
| color: #e9d5ff; | |
| text-transform: capitalize; | |
| } | |
| .type-btn:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| } | |
| .type-btn.active { | |
| background: #a855f7; | |
| color: white; | |
| } | |
| .tip { | |
| text-align: center; | |
| color: #d8b4fe; | |
| font-size: 13px; | |
| margin-top: 20px; | |
| } | |
| .status { | |
| display: inline-block; | |
| padding: 4px 12px; | |
| border-radius: 12px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| margin-left: 8px; | |
| } | |
| .status-active { | |
| background: #22c55e; | |
| color: white; | |
| } | |
| .status-inactive { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: #e9d5ff; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>🎧 Software ANC</h1> | |
| <p>Active Noise Cancellation & Focus Music</p> | |
| </div> | |
| <div id="message" style="display: none;"></div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <div> | |
| <span class="card-title">🔊 Noise Cancellation</span> | |
| <span id="ancStatus" class="status status-inactive">OFF</span> | |
| </div> | |
| <button id="ancBtn" class="btn btn-primary">Start ANC</button> | |
| </div> | |
| <div class="slider-group"> | |
| <label class="slider-label">ANC Strength: <span id="ancValue">70</span>%</label> | |
| <input type="range" id="ancSlider" class="slider" min="0" max="100" value="70"> | |
| </div> | |
| <p class="info-text"> | |
| ⚠️ Software ANC has limitations due to processing delay. Works best combined with focus music to mask background noise. | |
| </p> | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <div> | |
| <span class="card-title">🎵 Focus Music</span> | |
| <span id="musicStatus" class="status status-inactive">OFF</span> | |
| </div> | |
| <button id="musicBtn" class="btn btn-primary">Play</button> | |
| </div> | |
| <div class="slider-group"> | |
| <label class="slider-label">Sound Type</label> | |
| <div class="button-grid"> | |
| <button class="type-btn active" data-type="brown">Brown Noise</button> | |
| <button class="type-btn" data-type="white">White Noise</button> | |
| <button class="type-btn" data-type="pink">Pink Noise</button> | |
| <button class="type-btn" data-type="sine">Sine Wave</button> | |
| </div> | |
| </div> | |
| <div class="slider-group"> | |
| <label class="slider-label">Volume: <span id="volumeValue">50</span>%</label> | |
| <input type="range" id="volumeSlider" class="slider" min="0" max="100" value="50"> | |
| </div> | |
| </div> | |
| <div class="tip"> | |
| 💡 Use headphones for best results. Focus music works better than ANC for masking constant sounds like fans. | |
| </div> | |
| </div> | |
| <script> | |
| let audioContext = null; | |
| let micStream = null; | |
| let sourceNode = null; | |
| let scriptNode = null; | |
| let ancGainNode = null; | |
| let musicSource = null; | |
| let musicGainNode = null; | |
| let ancActive = false; | |
| let musicActive = false; | |
| let musicType = 'brown'; | |
| const ancBtn = document.getElementById('ancBtn'); | |
| const musicBtn = document.getElementById('musicBtn'); | |
| const ancSlider = document.getElementById('ancSlider'); | |
| const volumeSlider = document.getElementById('volumeSlider'); | |
| const ancValue = document.getElementById('ancValue'); | |
| const volumeValue = document.getElementById('volumeValue'); | |
| const messageDiv = document.getElementById('message'); | |
| const ancStatus = document.getElementById('ancStatus'); | |
| const musicStatus = document.getElementById('musicStatus'); | |
| const typeButtons = document.querySelectorAll('.type-btn'); | |
| function showMessage(message, isError = false) { | |
| messageDiv.textContent = message; | |
| messageDiv.className = isError ? 'error' : 'success'; | |
| messageDiv.style.display = 'block'; | |
| setTimeout(() => { | |
| messageDiv.style.display = 'none'; | |
| }, 5000); | |
| } | |
| function initAudioContext() { | |
| if (!audioContext || audioContext.state === 'closed') { | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| } | |
| // Resume if suspended (important for mobile) | |
| if (audioContext.state === 'suspended') { | |
| audioContext.resume(); | |
| } | |
| return audioContext; | |
| } | |
| async function startANC() { | |
| try { | |
| const ctx = initAudioContext(); | |
| const stream = await navigator.mediaDevices.getUserMedia({ | |
| audio: { | |
| echoCancellation: false, | |
| noiseSuppression: false, | |
| autoGainControl: false | |
| } | |
| }); | |
| micStream = stream; | |
| sourceNode = ctx.createMediaStreamSource(stream); | |
| scriptNode = ctx.createScriptProcessor(4096, 1, 1); | |
| ancGainNode = ctx.createGain(); | |
| ancGainNode.gain.value = ancSlider.value / 100; | |
| scriptNode.onaudioprocess = (e) => { | |
| const input = e.inputBuffer.getChannelData(0); | |
| const output = e.outputBuffer.getChannelData(0); | |
| const strength = ancSlider.value / 100; | |
| for (let i = 0; i < input.length; i++) { | |
| output[i] = -input[i] * strength; | |
| } | |
| }; | |
| sourceNode.connect(scriptNode); | |
| scriptNode.connect(ancGainNode); | |
| ancGainNode.connect(ctx.destination); | |
| ancActive = true; | |
| ancBtn.textContent = 'Stop ANC'; | |
| ancBtn.className = 'btn btn-danger'; | |
| ancStatus.textContent = 'ON'; | |
| ancStatus.className = 'status status-active'; | |
| showMessage('ANC Started - Microphone active'); | |
| } catch (err) { | |
| showMessage('Microphone access denied. Go to browser Settings → Site Settings → Microphone and allow access.', true); | |
| console.error(err); | |
| } | |
| } | |
| function stopANC() { | |
| if (scriptNode) { | |
| scriptNode.disconnect(); | |
| scriptNode = null; | |
| } | |
| if (sourceNode) { | |
| sourceNode.disconnect(); | |
| sourceNode = null; | |
| } | |
| if (ancGainNode) { | |
| ancGainNode.disconnect(); | |
| ancGainNode = null; | |
| } | |
| if (micStream) { | |
| micStream.getTracks().forEach(track => track.stop()); | |
| micStream = null; | |
| } | |
| ancActive = false; | |
| ancBtn.textContent = 'Start ANC'; | |
| ancBtn.className = 'btn btn-primary'; | |
| ancStatus.textContent = 'OFF'; | |
| ancStatus.className = 'status status-inactive'; | |
| } | |
| function generateNoiseBuffer(ctx, type) { | |
| const bufferSize = ctx.sampleRate * 2; // 2 seconds | |
| const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); | |
| const data = buffer.getChannelData(0); | |
| if (type === 'white') { | |
| for (let i = 0; i < bufferSize; i++) { | |
| data[i] = Math.random() * 2 - 1; | |
| } | |
| } else if (type === 'brown') { | |
| let lastOut = 0; | |
| for (let i = 0; i < bufferSize; i++) { | |
| const white = Math.random() * 2 - 1; | |
| data[i] = (lastOut + (0.02 * white)) / 1.02; | |
| lastOut = data[i]; | |
| data[i] *= 3.5; | |
| } | |
| } else if (type === 'pink') { | |
| let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0; | |
| for (let i = 0; i < bufferSize; i++) { | |
| const white = Math.random() * 2 - 1; | |
| b0 = 0.99886 * b0 + white * 0.0555179; | |
| b1 = 0.99332 * b1 + white * 0.0750759; | |
| b2 = 0.96900 * b2 + white * 0.1538520; | |
| b3 = 0.86650 * b3 + white * 0.3104856; | |
| b4 = 0.55000 * b4 + white * 0.5329522; | |
| b5 = -0.7616 * b5 - white * 0.0168980; | |
| data[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362; | |
| data[i] *= 0.11; | |
| b6 = white * 0.115926; | |
| } | |
| } | |
| return buffer; | |
| } | |
| function startMusic() { | |
| try { | |
| const ctx = initAudioContext(); | |
| const vol = volumeSlider.value / 100; | |
| musicGainNode = ctx.createGain(); | |
| musicGainNode.gain.value = vol; | |
| if (musicType === 'brown' || musicType === 'white' || musicType === 'pink') { | |
| const buffer = generateNoiseBuffer(ctx, musicType); | |
| musicSource = ctx.createBufferSource(); | |
| musicSource.buffer = buffer; | |
| musicSource.loop = true; | |
| musicSource.connect(musicGainNode); | |
| } else if (musicType === 'sine') { | |
| musicSource = ctx.createOscillator(); | |
| musicSource.type = 'sine'; | |
| musicSource.frequency.value = 220; | |
| musicSource.connect(musicGainNode); | |
| } | |
| musicGainNode.connect(ctx.destination); | |
| musicSource.start(0); | |
| musicActive = true; | |
| musicBtn.textContent = 'Stop'; | |
| musicBtn.className = 'btn btn-danger'; | |
| musicStatus.textContent = 'ON'; | |
| musicStatus.className = 'status status-active'; | |
| showMessage('Focus music playing'); | |
| } catch (err) { | |
| showMessage('Failed to start music. Try tapping the screen first to enable audio.', true); | |
| console.error(err); | |
| } | |
| } | |
| function stopMusic() { | |
| if (musicSource) { | |
| try { | |
| musicSource.stop(); | |
| } catch (e) { | |
| // Already stopped | |
| } | |
| musicSource.disconnect(); | |
| musicSource = null; | |
| } | |
| if (musicGainNode) { | |
| musicGainNode.disconnect(); | |
| musicGainNode = null; | |
| } | |
| musicActive = false; | |
| musicBtn.textContent = 'Play'; | |
| musicBtn.className = 'btn btn-primary'; | |
| musicStatus.textContent = 'OFF'; | |
| musicStatus.className = 'status status-inactive'; | |
| } | |
| ancBtn.addEventListener('click', () => { | |
| if (ancActive) { | |
| stopANC(); | |
| } else { | |
| startANC(); | |
| } | |
| }); | |
| musicBtn.addEventListener('click', () => { | |
| if (musicActive) { | |
| stopMusic(); | |
| } else { | |
| startMusic(); | |
| } | |
| }); | |
| ancSlider.addEventListener('input', (e) => { | |
| ancValue.textContent = e.target.value; | |
| if (ancGainNode) { | |
| ancGainNode.gain.value = e.target.value / 100; | |
| } | |
| }); | |
| volumeSlider.addEventListener('input', (e) => { | |
| volumeValue.textContent = e.target.value; | |
| if (musicGainNode) { | |
| musicGainNode.gain.value = e.target.value / 100; | |
| } | |
| }); | |
| typeButtons.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| typeButtons.forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| musicType = btn.dataset.type; | |
| if (musicActive) { | |
| stopMusic(); | |
| setTimeout(() => startMusic(), 100); | |
| } | |
| }); | |
| }); | |
| // Enable audio on first user interaction (required for mobile) | |
| document.body.addEventListener('click', () => { | |
| if (audioContext && audioContext.state === 'suspended') { | |
| audioContext.resume(); | |
| } | |
| }, { once: true }); | |
| </script> | |
| </body> | |
| </html> |