Audio & Focus

AMBIENT SYNTHESIZER

100% browser-synthesized focus soundscapes. Mix pink noise, rain, ocean waves, fireplace crackle, and binaural beats offline without loading external audio clips.

Pink Noise
40%
Brownian Deep Noise
50%
Gentle Rain
60%
Ocean Waves
40%
Fireplace Crackle
30%
Alpha Binaural Beat (10Hz)
35%
Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

let audioCtx = null;
    let masterGain = null;
    let isPlaying = false;
    let animId = null;

    const btnMaster = document.getElementById('btn-master');
    const canvas = document.getElementById('audio-visualizer');
    const ctx = canvas.getContext('2d');

    const sounds = {
      pink: { node: null, gain: null, chk: 'chk-pink', vol: 'vol-pink', card: 'card-pink', txt: 'txt-vol-pink' },
      brown: { node: null, gain: null, chk: 'chk-brown', vol: 'vol-brown', card: 'card-brown', txt: 'txt-vol-brown' },
      rain: { node: null, gain: null, chk: 'chk-rain', vol: 'vol-rain', card: 'card-rain', txt: 'txt-vol-rain' },
      waves: { node: null, gain: null, chk: 'chk-waves', vol: 'vol-waves', card: 'card-waves', txt: 'txt-vol-waves' },
      fire: { node: null, gain: null, chk: 'chk-fire', vol: 'vol-fire', card: 'card-fire', txt: 'txt-vol-fire' },
      binaural: { nodeLeft: null, nodeRight: null, gain: null, chk: 'chk-binaural', vol: 'vol-binaural', card: 'card-binaural', txt: 'txt-vol-binaural' }
    };

    function initAudio() {
      if (audioCtx) return;
      audioCtx = new (window.AudioContext || window.webkitAudioContext)();
      masterGain = audioCtx.createGain();
      masterGain.gain.value = 1.0;
      masterGain.connect(audioCtx.destination);
    }

    function createPinkNoiseNode() {
      const bufferSize = audioCtx.sampleRate * 2;
      const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
      const data = buffer.getChannelData(0);
      let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0;
      for (let i = 0; i < bufferSize; i++) {
        let 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;
      }
      const noise = audioCtx.createBufferSource();
      noise.buffer = buffer;
      noise.loop = true;
      return noise;
    }

    function createBrownNoiseNode() {
      const bufferSize = audioCtx.sampleRate * 2;
      const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
      const data = buffer.getChannelData(0);
      let lastOutput = 0.0;
      for (let i = 0; i < bufferSize; i++) {
        let white = Math.random() * 2 - 1;
        data[i] = (lastOutput + (0.02 * white)) / 1.02;
        lastOutput = data[i];
        data[i] *= 3.5;
      }
      const noise = audioCtx.createBufferSource();
      noise.buffer = buffer;
      noise.loop = true;
      return noise;
    }

    function toggleMaster() {
      initAudio();
      if (audioCtx.state === 'suspended') {
        audioCtx.resume();
      }

      isPlaying = !isPlaying;
      if (isPlaying) {
        btnMaster.innerHTML = '⏸️ Pause Soundscape';
        btnMaster.style.background = '#ff5555';
        startActiveSounds();
        drawVisualizer();
      } else {
        btnMaster.innerHTML = '▶ Play Soundscape';
        btnMaster.style.background = 'var(--accent)';
        stopAllSounds();
        cancelAnimationFrame(animId);
        clearCanvas();
      }
    }

    function startActiveSounds() {
      if (!isPlaying) return;
      Object.keys(sounds).forEach(key => {
        const s = sounds[key];
        const isChecked = document.getElementById(s.chk).checked;
        const volVal = parseInt(document.getElementById(s.vol).value) / 100;
        document.getElementById(s.card).classList.toggle('active', isChecked);

        if (isChecked) {
          if (!s.gain) {
            s.gain = audioCtx.createGain();
            s.gain.connect(masterGain);
          }
          s.gain.gain.setValueAtTime(volVal, audioCtx.currentTime);

          if (key === 'pink' && !s.node) {
            s.node = createPinkNoiseNode();
            s.node.connect(s.gain);
            s.node.start();
          } else if (key === 'brown' && !s.node) {
            s.node = createBrownNoiseNode();
            s.node.connect(s.gain);
            s.node.start();
          } else if (key === 'rain' && !s.node) {
            s.node = createPinkNoiseNode();
            const filter = audioCtx.createBiquadFilter();
            filter.type = 'lowpass';
            filter.frequency.value = 1200;
            s.node.connect(filter);
            filter.connect(s.gain);
            s.node.start();
          } else if (key === 'waves' && !s.node) {
            s.node = createBrownNoiseNode();
            const filter = audioCtx.createBiquadFilter();
            filter.type = 'lowpass';
            filter.frequency.value = 400;
            s.node.connect(filter);
            filter.connect(s.gain);
            s.node.start();
          } else if (key === 'binaural' && !s.nodeLeft) {
            s.nodeLeft = audioCtx.createOscillator();
            s.nodeRight = audioCtx.createOscillator();
            s.nodeLeft.frequency.value = 200;
            s.nodeRight.frequency.value = 210; // 10Hz Alpha difference
            const merger = audioCtx.createChannelMerger(2);
            s.nodeLeft.connect(merger, 0, 0);
            s.nodeRight.connect(merger, 0, 1);
            merger.connect(s.gain);
            s.nodeLeft.start();
            s.nodeRight.start();
          }
        } else {
          stopSoundKey(key);
        }
      });
    }

    function stopSoundKey(key) {
      const s = sounds[key];
      if (s.node) {
        try { s.node.stop(); s.node.disconnect(); } catch (e) {}
        s.node = null;
      }
      if (s.nodeLeft) {
        try { s.nodeLeft.stop(); s.nodeRight.stop(); s.nodeLeft.disconnect(); s.nodeRight.disconnect(); } catch (e) {}
        s.nodeLeft = null; s.nodeRight = null;
      }
    }

    function stopAllSounds() {
      Object.keys(sounds).forEach(key => stopSoundKey(key));
    }

    function clearCanvas() {
      canvas.width = canvas.clientWidth;
      canvas.height = canvas.clientHeight;
      ctx.fillStyle = '#08080a';
      ctx.fillRect(0, 0, canvas.width, canvas.height);
    }

    function drawVisualizer() {
      if (!isPlaying) return;
      canvas.width = canvas.clientWidth;
      canvas.height = canvas.clientHeight;
      const w = canvas.width;
      const h = canvas.height;

      ctx.fillStyle = '#08080a';
      ctx.fillRect(0, 0, w, h);

      ctx.beginPath();
      ctx.lineWidth = 2;
      ctx.strokeStyle = '#ff2200';

      const sliceWidth = w / 60;
      let x = 0;

      for (let i = 0; i < 60; i++) {
        const v = Math.sin(Date.now() * 0.005 + i * 0.3) * (h * 0.3);
        const y = (h / 2) + v;
        if (i === 0) ctx.moveTo(x, y);
        else ctx.lineTo(x, y);
        x += sliceWidth;
      }

      ctx.stroke();
      animId = requestAnimationFrame(drawVisualizer);
    }

    btnMaster.addEventListener('click', toggleMaster);

    Object.keys(sounds).forEach(key => {
      const s = sounds[key];
      const chk = document.getElementById(s.chk);
      const vol = document.getElementById(s.vol);
      const txt = document.getElementById(s.txt);

      chk.addEventListener('change', () => {
        startActiveSounds();
      });

      vol.addEventListener('input', () => {
        txt.textContent = vol.value + '%';
        if (s.gain) {
          s.gain.gain.setValueAtTime(parseInt(vol.value) / 100, audioCtx.currentTime);
        }
      });
    });

    document.querySelectorAll('.btn-preset').forEach(btn => {
      btn.addEventListener('click', () => {
        const p = btn.dataset.preset;
        Object.keys(sounds).forEach(k => document.getElementById(sounds[k].chk).checked = false);

        if (p === 'focus') {
          document.getElementById('chk-pink').checked = true;
          document.getElementById('chk-binaural').checked = true;
        } else if (p === 'storm') {
          document.getElementById('chk-rain').checked = true;
          document.getElementById('chk-brown').checked = true;
        } else if (p === 'ocean') {
          document.getElementById('chk-waves').checked = true;
          document.getElementById('chk-pink').checked = true;
        } else if (p === 'zen') {
          document.getElementById('chk-binaural').checked = true;
          document.getElementById('chk-brown').checked = true;
        }

        if (isPlaying) startActiveSounds();
      });
    });

    clearCanvas();