Spaces:
Running
Running
Update index.html
Browse files- index.html +177 -123
index.html
CHANGED
|
@@ -149,6 +149,16 @@
|
|
| 149 |
font-size: 14px;
|
| 150 |
}
|
| 151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
.button-grid {
|
| 153 |
display: grid;
|
| 154 |
grid-template-columns: repeat(2, 1fr);
|
|
@@ -183,6 +193,25 @@
|
|
| 183 |
font-size: 13px;
|
| 184 |
margin-top: 20px;
|
| 185 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
</style>
|
| 187 |
</head>
|
| 188 |
<body>
|
|
@@ -192,11 +221,14 @@
|
|
| 192 |
<p>Active Noise Cancellation & Focus Music</p>
|
| 193 |
</div>
|
| 194 |
|
| 195 |
-
<div id="
|
| 196 |
|
| 197 |
<div class="card">
|
| 198 |
<div class="card-header">
|
| 199 |
-
<div
|
|
|
|
|
|
|
|
|
|
| 200 |
<button id="ancBtn" class="btn btn-primary">Start ANC</button>
|
| 201 |
</div>
|
| 202 |
|
|
@@ -206,13 +238,16 @@
|
|
| 206 |
</div>
|
| 207 |
|
| 208 |
<p class="info-text">
|
| 209 |
-
|
| 210 |
</p>
|
| 211 |
</div>
|
| 212 |
|
| 213 |
<div class="card">
|
| 214 |
<div class="card-header">
|
| 215 |
-
<div
|
|
|
|
|
|
|
|
|
|
| 216 |
<button id="musicBtn" class="btn btn-primary">Play</button>
|
| 217 |
</div>
|
| 218 |
|
|
@@ -222,7 +257,7 @@
|
|
| 222 |
<button class="type-btn active" data-type="brown">Brown Noise</button>
|
| 223 |
<button class="type-btn" data-type="white">White Noise</button>
|
| 224 |
<button class="type-btn" data-type="pink">Pink Noise</button>
|
| 225 |
-
<button class="type-btn" data-type="
|
| 226 |
</div>
|
| 227 |
</div>
|
| 228 |
|
|
@@ -233,17 +268,19 @@
|
|
| 233 |
</div>
|
| 234 |
|
| 235 |
<div class="tip">
|
| 236 |
-
💡 Use headphones for best results.
|
| 237 |
</div>
|
| 238 |
</div>
|
| 239 |
|
| 240 |
<script>
|
| 241 |
let audioContext = null;
|
| 242 |
let micStream = null;
|
| 243 |
-
let
|
| 244 |
-
let
|
| 245 |
-
let
|
| 246 |
-
|
|
|
|
|
|
|
| 247 |
|
| 248 |
let ancActive = false;
|
| 249 |
let musicActive = false;
|
|
@@ -255,19 +292,35 @@
|
|
| 255 |
const volumeSlider = document.getElementById('volumeSlider');
|
| 256 |
const ancValue = document.getElementById('ancValue');
|
| 257 |
const volumeValue = document.getElementById('volumeValue');
|
| 258 |
-
const
|
|
|
|
|
|
|
| 259 |
const typeButtons = document.querySelectorAll('.type-btn');
|
| 260 |
|
| 261 |
-
function
|
| 262 |
-
|
| 263 |
-
|
|
|
|
| 264 |
setTimeout(() => {
|
| 265 |
-
|
| 266 |
}, 5000);
|
| 267 |
}
|
| 268 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
async function startANC() {
|
| 270 |
try {
|
|
|
|
|
|
|
| 271 |
const stream = await navigator.mediaDevices.getUserMedia({
|
| 272 |
audio: {
|
| 273 |
echoCancellation: false,
|
|
@@ -276,162 +329,156 @@
|
|
| 276 |
}
|
| 277 |
});
|
| 278 |
|
| 279 |
-
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
| 280 |
micStream = stream;
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
const output = audioContext.createGain();
|
| 285 |
|
| 286 |
-
|
| 287 |
-
output.gain.value = ancSlider.value / 100;
|
| 288 |
|
| 289 |
-
|
| 290 |
const input = e.inputBuffer.getChannelData(0);
|
| 291 |
-
const
|
| 292 |
const strength = ancSlider.value / 100;
|
| 293 |
|
| 294 |
for (let i = 0; i < input.length; i++) {
|
| 295 |
-
|
| 296 |
}
|
| 297 |
};
|
| 298 |
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
|
| 303 |
ancActive = true;
|
| 304 |
ancBtn.textContent = 'Stop ANC';
|
| 305 |
ancBtn.className = 'btn btn-danger';
|
|
|
|
|
|
|
|
|
|
| 306 |
} catch (err) {
|
| 307 |
-
|
| 308 |
console.error(err);
|
| 309 |
}
|
| 310 |
}
|
| 311 |
|
| 312 |
function stopANC() {
|
| 313 |
-
if (
|
| 314 |
-
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
}
|
| 317 |
if (micStream) {
|
| 318 |
micStream.getTracks().forEach(track => track.stop());
|
| 319 |
micStream = null;
|
| 320 |
}
|
| 321 |
-
|
| 322 |
-
audioContext.close();
|
| 323 |
-
audioContext = null;
|
| 324 |
-
}
|
| 325 |
-
gainNode = null;
|
| 326 |
ancActive = false;
|
| 327 |
ancBtn.textContent = 'Start ANC';
|
| 328 |
ancBtn.className = 'btn btn-primary';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
}
|
| 330 |
|
| 331 |
function startMusic() {
|
| 332 |
try {
|
| 333 |
-
|
| 334 |
-
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
| 335 |
-
}
|
| 336 |
-
|
| 337 |
const vol = volumeSlider.value / 100;
|
| 338 |
|
|
|
|
|
|
|
|
|
|
| 339 |
if (musicType === 'brown' || musicType === 'white' || musicType === 'pink') {
|
| 340 |
-
const
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
for (let i = 0; i < bufferSize; i++) {
|
| 351 |
-
const white = Math.random() * 2 - 1;
|
| 352 |
-
output[i] = (lastOut + (0.02 * white)) / 1.02;
|
| 353 |
-
lastOut = output[i];
|
| 354 |
-
output[i] *= 3.5;
|
| 355 |
-
}
|
| 356 |
-
} else if (musicType === 'pink') {
|
| 357 |
-
let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0;
|
| 358 |
-
for (let i = 0; i < bufferSize; i++) {
|
| 359 |
-
const white = Math.random() * 2 - 1;
|
| 360 |
-
b0 = 0.99886 * b0 + white * 0.0555179;
|
| 361 |
-
b1 = 0.99332 * b1 + white * 0.0750759;
|
| 362 |
-
b2 = 0.96900 * b2 + white * 0.1538520;
|
| 363 |
-
b3 = 0.86650 * b3 + white * 0.3104856;
|
| 364 |
-
b4 = 0.55000 * b4 + white * 0.5329522;
|
| 365 |
-
b5 = -0.7616 * b5 - white * 0.0168980;
|
| 366 |
-
output[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362;
|
| 367 |
-
output[i] *= 0.11;
|
| 368 |
-
b6 = white * 0.115926;
|
| 369 |
-
}
|
| 370 |
-
}
|
| 371 |
-
|
| 372 |
-
const noiseSource = audioContext.createBufferSource();
|
| 373 |
-
noiseSource.buffer = noiseBuffer;
|
| 374 |
-
noiseSource.loop = true;
|
| 375 |
-
|
| 376 |
-
const gain = audioContext.createGain();
|
| 377 |
-
gain.gain.value = vol;
|
| 378 |
-
musicGain = gain;
|
| 379 |
-
|
| 380 |
-
noiseSource.connect(gain);
|
| 381 |
-
gain.connect(audioContext.destination);
|
| 382 |
-
noiseSource.start(0);
|
| 383 |
-
|
| 384 |
-
oscillator = noiseSource;
|
| 385 |
-
} else if (musicType === 'binaural') {
|
| 386 |
-
const osc1 = audioContext.createOscillator();
|
| 387 |
-
const osc2 = audioContext.createOscillator();
|
| 388 |
-
osc1.frequency.value = 200;
|
| 389 |
-
osc2.frequency.value = 210;
|
| 390 |
-
|
| 391 |
-
const merger = audioContext.createChannelMerger(2);
|
| 392 |
-
const leftGain = audioContext.createGain();
|
| 393 |
-
const rightGain = audioContext.createGain();
|
| 394 |
-
leftGain.gain.value = vol;
|
| 395 |
-
rightGain.gain.value = vol;
|
| 396 |
-
musicGain = leftGain;
|
| 397 |
-
|
| 398 |
-
osc1.connect(leftGain);
|
| 399 |
-
osc2.connect(rightGain);
|
| 400 |
-
leftGain.connect(merger, 0, 0);
|
| 401 |
-
rightGain.connect(merger, 0, 1);
|
| 402 |
-
merger.connect(audioContext.destination);
|
| 403 |
-
|
| 404 |
-
osc1.start();
|
| 405 |
-
osc2.start();
|
| 406 |
-
oscillator = [osc1, osc2];
|
| 407 |
}
|
| 408 |
|
|
|
|
|
|
|
|
|
|
| 409 |
musicActive = true;
|
| 410 |
musicBtn.textContent = 'Stop';
|
| 411 |
musicBtn.className = 'btn btn-danger';
|
|
|
|
|
|
|
|
|
|
| 412 |
} catch (err) {
|
| 413 |
-
|
| 414 |
console.error(err);
|
| 415 |
}
|
| 416 |
}
|
| 417 |
|
| 418 |
function stopMusic() {
|
| 419 |
-
if (
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
}
|
| 423 |
-
|
| 424 |
}
|
| 425 |
-
|
|
|
|
| 426 |
}
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
audioContext = null;
|
| 431 |
}
|
|
|
|
| 432 |
musicActive = false;
|
| 433 |
musicBtn.textContent = 'Play';
|
| 434 |
musicBtn.className = 'btn btn-primary';
|
|
|
|
|
|
|
| 435 |
}
|
| 436 |
|
| 437 |
ancBtn.addEventListener('click', () => {
|
|
@@ -452,15 +499,15 @@
|
|
| 452 |
|
| 453 |
ancSlider.addEventListener('input', (e) => {
|
| 454 |
ancValue.textContent = e.target.value;
|
| 455 |
-
if (
|
| 456 |
-
|
| 457 |
}
|
| 458 |
});
|
| 459 |
|
| 460 |
volumeSlider.addEventListener('input', (e) => {
|
| 461 |
volumeValue.textContent = e.target.value;
|
| 462 |
-
if (
|
| 463 |
-
|
| 464 |
}
|
| 465 |
});
|
| 466 |
|
|
@@ -476,6 +523,13 @@
|
|
| 476 |
}
|
| 477 |
});
|
| 478 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
</script>
|
| 480 |
</body>
|
| 481 |
</html>
|
|
|
|
| 149 |
font-size: 14px;
|
| 150 |
}
|
| 151 |
|
| 152 |
+
.success {
|
| 153 |
+
background: rgba(34, 197, 94, 0.2);
|
| 154 |
+
border: 1px solid #22c55e;
|
| 155 |
+
color: #bbf7d0;
|
| 156 |
+
padding: 16px;
|
| 157 |
+
border-radius: 12px;
|
| 158 |
+
margin-bottom: 20px;
|
| 159 |
+
font-size: 14px;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
.button-grid {
|
| 163 |
display: grid;
|
| 164 |
grid-template-columns: repeat(2, 1fr);
|
|
|
|
| 193 |
font-size: 13px;
|
| 194 |
margin-top: 20px;
|
| 195 |
}
|
| 196 |
+
|
| 197 |
+
.status {
|
| 198 |
+
display: inline-block;
|
| 199 |
+
padding: 4px 12px;
|
| 200 |
+
border-radius: 12px;
|
| 201 |
+
font-size: 12px;
|
| 202 |
+
font-weight: 600;
|
| 203 |
+
margin-left: 8px;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.status-active {
|
| 207 |
+
background: #22c55e;
|
| 208 |
+
color: white;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.status-inactive {
|
| 212 |
+
background: rgba(255, 255, 255, 0.1);
|
| 213 |
+
color: #e9d5ff;
|
| 214 |
+
}
|
| 215 |
</style>
|
| 216 |
</head>
|
| 217 |
<body>
|
|
|
|
| 221 |
<p>Active Noise Cancellation & Focus Music</p>
|
| 222 |
</div>
|
| 223 |
|
| 224 |
+
<div id="message" style="display: none;"></div>
|
| 225 |
|
| 226 |
<div class="card">
|
| 227 |
<div class="card-header">
|
| 228 |
+
<div>
|
| 229 |
+
<span class="card-title">🔊 Noise Cancellation</span>
|
| 230 |
+
<span id="ancStatus" class="status status-inactive">OFF</span>
|
| 231 |
+
</div>
|
| 232 |
<button id="ancBtn" class="btn btn-primary">Start ANC</button>
|
| 233 |
</div>
|
| 234 |
|
|
|
|
| 238 |
</div>
|
| 239 |
|
| 240 |
<p class="info-text">
|
| 241 |
+
⚠️ Software ANC has limitations due to processing delay. Works best combined with focus music to mask background noise.
|
| 242 |
</p>
|
| 243 |
</div>
|
| 244 |
|
| 245 |
<div class="card">
|
| 246 |
<div class="card-header">
|
| 247 |
+
<div>
|
| 248 |
+
<span class="card-title">🎵 Focus Music</span>
|
| 249 |
+
<span id="musicStatus" class="status status-inactive">OFF</span>
|
| 250 |
+
</div>
|
| 251 |
<button id="musicBtn" class="btn btn-primary">Play</button>
|
| 252 |
</div>
|
| 253 |
|
|
|
|
| 257 |
<button class="type-btn active" data-type="brown">Brown Noise</button>
|
| 258 |
<button class="type-btn" data-type="white">White Noise</button>
|
| 259 |
<button class="type-btn" data-type="pink">Pink Noise</button>
|
| 260 |
+
<button class="type-btn" data-type="sine">Sine Wave</button>
|
| 261 |
</div>
|
| 262 |
</div>
|
| 263 |
|
|
|
|
| 268 |
</div>
|
| 269 |
|
| 270 |
<div class="tip">
|
| 271 |
+
💡 Use headphones for best results. Focus music works better than ANC for masking constant sounds like fans.
|
| 272 |
</div>
|
| 273 |
</div>
|
| 274 |
|
| 275 |
<script>
|
| 276 |
let audioContext = null;
|
| 277 |
let micStream = null;
|
| 278 |
+
let sourceNode = null;
|
| 279 |
+
let scriptNode = null;
|
| 280 |
+
let ancGainNode = null;
|
| 281 |
+
|
| 282 |
+
let musicSource = null;
|
| 283 |
+
let musicGainNode = null;
|
| 284 |
|
| 285 |
let ancActive = false;
|
| 286 |
let musicActive = false;
|
|
|
|
| 292 |
const volumeSlider = document.getElementById('volumeSlider');
|
| 293 |
const ancValue = document.getElementById('ancValue');
|
| 294 |
const volumeValue = document.getElementById('volumeValue');
|
| 295 |
+
const messageDiv = document.getElementById('message');
|
| 296 |
+
const ancStatus = document.getElementById('ancStatus');
|
| 297 |
+
const musicStatus = document.getElementById('musicStatus');
|
| 298 |
const typeButtons = document.querySelectorAll('.type-btn');
|
| 299 |
|
| 300 |
+
function showMessage(message, isError = false) {
|
| 301 |
+
messageDiv.textContent = message;
|
| 302 |
+
messageDiv.className = isError ? 'error' : 'success';
|
| 303 |
+
messageDiv.style.display = 'block';
|
| 304 |
setTimeout(() => {
|
| 305 |
+
messageDiv.style.display = 'none';
|
| 306 |
}, 5000);
|
| 307 |
}
|
| 308 |
|
| 309 |
+
function initAudioContext() {
|
| 310 |
+
if (!audioContext || audioContext.state === 'closed') {
|
| 311 |
+
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
| 312 |
+
}
|
| 313 |
+
// Resume if suspended (important for mobile)
|
| 314 |
+
if (audioContext.state === 'suspended') {
|
| 315 |
+
audioContext.resume();
|
| 316 |
+
}
|
| 317 |
+
return audioContext;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
async function startANC() {
|
| 321 |
try {
|
| 322 |
+
const ctx = initAudioContext();
|
| 323 |
+
|
| 324 |
const stream = await navigator.mediaDevices.getUserMedia({
|
| 325 |
audio: {
|
| 326 |
echoCancellation: false,
|
|
|
|
| 329 |
}
|
| 330 |
});
|
| 331 |
|
|
|
|
| 332 |
micStream = stream;
|
| 333 |
+
sourceNode = ctx.createMediaStreamSource(stream);
|
| 334 |
+
scriptNode = ctx.createScriptProcessor(4096, 1, 1);
|
| 335 |
+
ancGainNode = ctx.createGain();
|
|
|
|
| 336 |
|
| 337 |
+
ancGainNode.gain.value = ancSlider.value / 100;
|
|
|
|
| 338 |
|
| 339 |
+
scriptNode.onaudioprocess = (e) => {
|
| 340 |
const input = e.inputBuffer.getChannelData(0);
|
| 341 |
+
const output = e.outputBuffer.getChannelData(0);
|
| 342 |
const strength = ancSlider.value / 100;
|
| 343 |
|
| 344 |
for (let i = 0; i < input.length; i++) {
|
| 345 |
+
output[i] = -input[i] * strength;
|
| 346 |
}
|
| 347 |
};
|
| 348 |
|
| 349 |
+
sourceNode.connect(scriptNode);
|
| 350 |
+
scriptNode.connect(ancGainNode);
|
| 351 |
+
ancGainNode.connect(ctx.destination);
|
| 352 |
|
| 353 |
ancActive = true;
|
| 354 |
ancBtn.textContent = 'Stop ANC';
|
| 355 |
ancBtn.className = 'btn btn-danger';
|
| 356 |
+
ancStatus.textContent = 'ON';
|
| 357 |
+
ancStatus.className = 'status status-active';
|
| 358 |
+
showMessage('ANC Started - Microphone active');
|
| 359 |
} catch (err) {
|
| 360 |
+
showMessage('Microphone access denied. Go to browser Settings → Site Settings → Microphone and allow access.', true);
|
| 361 |
console.error(err);
|
| 362 |
}
|
| 363 |
}
|
| 364 |
|
| 365 |
function stopANC() {
|
| 366 |
+
if (scriptNode) {
|
| 367 |
+
scriptNode.disconnect();
|
| 368 |
+
scriptNode = null;
|
| 369 |
+
}
|
| 370 |
+
if (sourceNode) {
|
| 371 |
+
sourceNode.disconnect();
|
| 372 |
+
sourceNode = null;
|
| 373 |
+
}
|
| 374 |
+
if (ancGainNode) {
|
| 375 |
+
ancGainNode.disconnect();
|
| 376 |
+
ancGainNode = null;
|
| 377 |
}
|
| 378 |
if (micStream) {
|
| 379 |
micStream.getTracks().forEach(track => track.stop());
|
| 380 |
micStream = null;
|
| 381 |
}
|
| 382 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
ancActive = false;
|
| 384 |
ancBtn.textContent = 'Start ANC';
|
| 385 |
ancBtn.className = 'btn btn-primary';
|
| 386 |
+
ancStatus.textContent = 'OFF';
|
| 387 |
+
ancStatus.className = 'status status-inactive';
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
function generateNoiseBuffer(ctx, type) {
|
| 391 |
+
const bufferSize = ctx.sampleRate * 2; // 2 seconds
|
| 392 |
+
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
|
| 393 |
+
const data = buffer.getChannelData(0);
|
| 394 |
+
|
| 395 |
+
if (type === 'white') {
|
| 396 |
+
for (let i = 0; i < bufferSize; i++) {
|
| 397 |
+
data[i] = Math.random() * 2 - 1;
|
| 398 |
+
}
|
| 399 |
+
} else if (type === 'brown') {
|
| 400 |
+
let lastOut = 0;
|
| 401 |
+
for (let i = 0; i < bufferSize; i++) {
|
| 402 |
+
const white = Math.random() * 2 - 1;
|
| 403 |
+
data[i] = (lastOut + (0.02 * white)) / 1.02;
|
| 404 |
+
lastOut = data[i];
|
| 405 |
+
data[i] *= 3.5;
|
| 406 |
+
}
|
| 407 |
+
} else if (type === 'pink') {
|
| 408 |
+
let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0;
|
| 409 |
+
for (let i = 0; i < bufferSize; i++) {
|
| 410 |
+
const white = Math.random() * 2 - 1;
|
| 411 |
+
b0 = 0.99886 * b0 + white * 0.0555179;
|
| 412 |
+
b1 = 0.99332 * b1 + white * 0.0750759;
|
| 413 |
+
b2 = 0.96900 * b2 + white * 0.1538520;
|
| 414 |
+
b3 = 0.86650 * b3 + white * 0.3104856;
|
| 415 |
+
b4 = 0.55000 * b4 + white * 0.5329522;
|
| 416 |
+
b5 = -0.7616 * b5 - white * 0.0168980;
|
| 417 |
+
data[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362;
|
| 418 |
+
data[i] *= 0.11;
|
| 419 |
+
b6 = white * 0.115926;
|
| 420 |
+
}
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
return buffer;
|
| 424 |
}
|
| 425 |
|
| 426 |
function startMusic() {
|
| 427 |
try {
|
| 428 |
+
const ctx = initAudioContext();
|
|
|
|
|
|
|
|
|
|
| 429 |
const vol = volumeSlider.value / 100;
|
| 430 |
|
| 431 |
+
musicGainNode = ctx.createGain();
|
| 432 |
+
musicGainNode.gain.value = vol;
|
| 433 |
+
|
| 434 |
if (musicType === 'brown' || musicType === 'white' || musicType === 'pink') {
|
| 435 |
+
const buffer = generateNoiseBuffer(ctx, musicType);
|
| 436 |
+
musicSource = ctx.createBufferSource();
|
| 437 |
+
musicSource.buffer = buffer;
|
| 438 |
+
musicSource.loop = true;
|
| 439 |
+
musicSource.connect(musicGainNode);
|
| 440 |
+
} else if (musicType === 'sine') {
|
| 441 |
+
musicSource = ctx.createOscillator();
|
| 442 |
+
musicSource.type = 'sine';
|
| 443 |
+
musicSource.frequency.value = 220;
|
| 444 |
+
musicSource.connect(musicGainNode);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
}
|
| 446 |
|
| 447 |
+
musicGainNode.connect(ctx.destination);
|
| 448 |
+
musicSource.start(0);
|
| 449 |
+
|
| 450 |
musicActive = true;
|
| 451 |
musicBtn.textContent = 'Stop';
|
| 452 |
musicBtn.className = 'btn btn-danger';
|
| 453 |
+
musicStatus.textContent = 'ON';
|
| 454 |
+
musicStatus.className = 'status status-active';
|
| 455 |
+
showMessage('Focus music playing');
|
| 456 |
} catch (err) {
|
| 457 |
+
showMessage('Failed to start music. Try tapping the screen first to enable audio.', true);
|
| 458 |
console.error(err);
|
| 459 |
}
|
| 460 |
}
|
| 461 |
|
| 462 |
function stopMusic() {
|
| 463 |
+
if (musicSource) {
|
| 464 |
+
try {
|
| 465 |
+
musicSource.stop();
|
| 466 |
+
} catch (e) {
|
| 467 |
+
// Already stopped
|
| 468 |
}
|
| 469 |
+
musicSource.disconnect();
|
| 470 |
+
musicSource = null;
|
| 471 |
}
|
| 472 |
+
if (musicGainNode) {
|
| 473 |
+
musicGainNode.disconnect();
|
| 474 |
+
musicGainNode = null;
|
|
|
|
| 475 |
}
|
| 476 |
+
|
| 477 |
musicActive = false;
|
| 478 |
musicBtn.textContent = 'Play';
|
| 479 |
musicBtn.className = 'btn btn-primary';
|
| 480 |
+
musicStatus.textContent = 'OFF';
|
| 481 |
+
musicStatus.className = 'status status-inactive';
|
| 482 |
}
|
| 483 |
|
| 484 |
ancBtn.addEventListener('click', () => {
|
|
|
|
| 499 |
|
| 500 |
ancSlider.addEventListener('input', (e) => {
|
| 501 |
ancValue.textContent = e.target.value;
|
| 502 |
+
if (ancGainNode) {
|
| 503 |
+
ancGainNode.gain.value = e.target.value / 100;
|
| 504 |
}
|
| 505 |
});
|
| 506 |
|
| 507 |
volumeSlider.addEventListener('input', (e) => {
|
| 508 |
volumeValue.textContent = e.target.value;
|
| 509 |
+
if (musicGainNode) {
|
| 510 |
+
musicGainNode.gain.value = e.target.value / 100;
|
| 511 |
}
|
| 512 |
});
|
| 513 |
|
|
|
|
| 523 |
}
|
| 524 |
});
|
| 525 |
});
|
| 526 |
+
|
| 527 |
+
// Enable audio on first user interaction (required for mobile)
|
| 528 |
+
document.body.addEventListener('click', () => {
|
| 529 |
+
if (audioContext && audioContext.state === 'suspended') {
|
| 530 |
+
audioContext.resume();
|
| 531 |
+
}
|
| 532 |
+
}, { once: true });
|
| 533 |
</script>
|
| 534 |
</body>
|
| 535 |
</html>
|