Soundscape - Atmosphere Creator

Rain, fireplace, nature, ocean, night sounds while browsing – interaction‑aware atmosphere

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Soundscape - Atmosphere Creator
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Rain, fireplace, nature, ocean, night sounds while browsing – interaction‑aware atmosphere
// @author       Mustafa Hakan
// @match        *://*/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    const AC = window.AudioContext || window.webkitAudioContext;
    let ctx, master, playing = false, currentProfile = 'rain', volume = 0.4;
    let oscillators = [], gainNodes = [], noiseNodes = [];
    let rainNodes = [], fireNodes = [], windNodes = [], birdNodes = [];

    const PROFILES = {
        rain: {
            name: '🌧️ Rainstorm',
            desc: 'Gentle rain and distant thunder',
            color: '#4a90d9',
            icon: '🌧️'
        },
        fireplace: {
            name: '🔥 Fireplace',
            desc: 'Crackling logs and warm crackle',
            color: '#d94a1a',
            icon: '🔥'
        },
        forest: {
            name: '🌲 Deep Forest',
            desc: 'Birdsong, wind and rustling leaves',
            color: '#3d7a3d',
            icon: '🌲'
        },
        ocean: {
            name: '🌊 Ocean Waves',
            desc: 'Waves on the shore and seagulls',
            color: '#1a6b8a',
            icon: '🌊'
        },
        night: {
            name: '🦗 Night Symphony',
            desc: 'Crickets, owl and distant wind',
            color: '#2d1b69',
            icon: '🦗'
        },
        thunder: {
            name: '⛈️ Thunderstorm',
            desc: 'Heavy rain and close lightning',
            color: '#5a5a8a',
            icon: '⛈️'
        },
        cafe: {
            name: '☕ Café Ambience',
            desc: 'Distant chatter, clinking cups, soft music',
            color: '#8b6914',
            icon: '☕'
        }
    };

    function createNoise(duration) {
        const size = ctx.sampleRate * duration;
        const buffer = ctx.createBuffer(1, size, ctx.sampleRate);
        const data = buffer.getChannelData(0);
        for (let i = 0; i < size; i++) {
            data[i] = Math.random() * 2 - 1;
        }
        return buffer;
    }

    function createRain() {
        const nodes = [];
        for (let i = 0; i < 15; i++) {
            const noise = createNoise(0.1);
            const source = ctx.createBufferSource();
            source.buffer = noise;
            source.loop = true;

            const filter = ctx.createBiquadFilter();
            filter.type = 'bandpass';
            filter.frequency.value = 2000 + Math.random() * 5000;
            filter.Q.value = 0.5;

            const gain = ctx.createGain();
            gain.gain.value = 0.01 + Math.random() * 0.03;

            source.connect(filter);
            filter.connect(gain);
            gain.connect(master);
            source.start();
            nodes.push({ source, filter, gain });
        }
        return nodes;
    }

    function createFire() {
        const nodes = [];
        for (let i = 0; i < 10; i++) {
            const noise = createNoise(0.05);
            const source = ctx.createBufferSource();
            source.buffer = noise;
            source.loop = true;

            const filter = ctx.createBiquadFilter();
            filter.type = 'lowpass';
            filter.frequency.value = 200 + Math.random() * 400;
            filter.Q.value = 1;

            const gain = ctx.createGain();
            gain.gain.value = 0.02 + Math.random() * 0.04;
            gain.gain.setValueAtTime(gain.gain.value, ctx.currentTime);
            gain.gain.exponentialRampToValueAtTime(gain.gain.value * (0.3 + Math.random() * 0.7), ctx.currentTime + 0.1 + Math.random() * 0.3);

            source.connect(filter);
            filter.connect(gain);
            gain.connect(master);
            source.start();
            nodes.push({ source, filter, gain });
        }
        return nodes;
    }

    function createWind() {
        const nodes = [];
        for (let i = 0; i < 3; i++) {
            const osc = ctx.createOscillator();
            osc.type = 'sine';
            osc.frequency.value = 100 + Math.random() * 300;

            const gain = ctx.createGain();
            gain.gain.value = 0.02;
            gain.gain.setValueAtTime(0.01, ctx.currentTime);
            gain.gain.linearRampToValueAtTime(0.04, ctx.currentTime + 2 + Math.random() * 3);

            osc.connect(gain);
            gain.connect(master);
            osc.start();
            nodes.push({ osc, gain });
        }
        return nodes;
    }

    function createBirds() {
        const nodes = [];
        for (let i = 0; i < 5; i++) {
            const osc = ctx.createOscillator();
            osc.type = 'sine';
            osc.frequency.value = 2000 + Math.random() * 4000;

            const gain = ctx.createGain();
            gain.gain.value = 0;
            const schedule = () => {
                const now = ctx.currentTime;
                const start = now + Math.random() * 5;
                const dur = 0.05 + Math.random() * 0.2;
                gain.gain.setValueAtTime(0, start);
                gain.gain.linearRampToValueAtTime(0.015, start + dur * 0.3);
                gain.gain.linearRampToValueAtTime(0, start + dur);
                setTimeout(schedule, (start - now + dur) * 1000 + Math.random() * 3000);
            };
            schedule();

            osc.connect(gain);
            gain.connect(master);
            osc.start();
            nodes.push({ osc, gain });
        }
        return nodes;
    }

    function createThunder() {
        const nodes = [];
        const schedule = () => {
            const now = ctx.currentTime;
            const delay = 3 + Math.random() * 12;
            const start = now + delay;

            const noise = createNoise(1.5);
            const source = ctx.createBufferSource();
            source.buffer = noise;

            const filter = ctx.createBiquadFilter();
            filter.type = 'lowpass';
            filter.frequency.setValueAtTime(80, start);
            filter.frequency.linearRampToValueAtTime(3000, start + 0.1);
            filter.frequency.linearRampToValueAtTime(60, start + 1.5);

            const gain = ctx.createGain();
            gain.gain.setValueAtTime(0, start);
            gain.gain.linearRampToValueAtTime(0.15, start + 0.1);
            gain.gain.exponentialRampToValueAtTime(0.001, start + 2);

            source.connect(filter);
            filter.connect(gain);
            gain.connect(master);
            source.start(start);
            source.stop(start + 2);

            setTimeout(schedule, delay * 1000 + 2000);
        };
        schedule();
        return nodes;
    }

    function createCafe() {
        const nodes = [];
        const murmur = createNoise(2);
        for (let i = 0; i < 3; i++) {
            const source = ctx.createBufferSource();
            source.buffer = murmur;
            source.loop = true;

            const filter = ctx.createBiquadFilter();
            filter.type = 'bandpass';
            filter.frequency.value = 300 + Math.random() * 700;
            filter.Q.value = 0.3;

            const gain = ctx.createGain();
            gain.gain.value = 0.01;

            source.connect(filter);
            filter.connect(gain);
            gain.connect(master);
            source.start();
            nodes.push({ source, filter, gain });
        }
        return nodes;
    }

    function clearAll() {
        const allNodes = [...rainNodes, ...fireNodes, ...windNodes, ...birdNodes, ...noiseNodes];
        allNodes.forEach(n => {
            try { n.source?.stop(); } catch(e) {}
            try { n.osc?.stop(); } catch(e) {}
            try { n.gain?.disconnect(); } catch(e) {}
        });
        rainNodes = []; fireNodes = []; windNodes = []; birdNodes = []; noiseNodes = [];
        oscillators.forEach(o => { try { o.stop(); } catch(e) {} });
        oscillators = [];
        gainNodes.forEach(g => { try { g.disconnect(); } catch(e) {} });
        gainNodes = [];
    }

    function startProfile(profile) {
        clearAll();
        currentProfile = profile;
        switch(profile) {
            case 'rain':
                rainNodes = createRain();
                windNodes = createWind();
                noiseNodes = createThunder();
                break;
            case 'fireplace':
                fireNodes = createFire();
                windNodes = createWind();
                break;
            case 'forest':
                birdNodes = createBirds();
                windNodes = createWind();
                break;
            case 'ocean':
                noiseNodes = createThunder();
                windNodes = createWind();
                break;
            case 'night':
                birdNodes = createBirds();
                windNodes = createWind();
                break;
            case 'thunder':
                rainNodes = createRain();
                noiseNodes = createThunder();
                windNodes = createWind();
                break;
            case 'cafe':
                noiseNodes = createCafe();
                break;
        }
    }

    function initAudio() {
        if (ctx) return;
        ctx = new AC();
        master = ctx.createGain();
        master.gain.value = volume;
        master.connect(ctx.destination);
        startProfile(currentProfile);
        playing = true;
    }

    function setVolume(v) {
        volume = v;
        if (master) {
            master.gain.setValueAtTime(v, ctx.currentTime);
            master.gain.linearRampToValueAtTime(v, ctx.currentTime + 0.1);
        }
    }

    // Scroll sound
    let lastScroll = 0;
    window.addEventListener('scroll', () => {
        if (!playing) return;
        const now = Date.now();
        if (now - lastScroll < 50) return;
        lastScroll = now;

        const osc = ctx.createOscillator();
        const gain = ctx.createGain();
        osc.type = 'sine';
        osc.frequency.value = 200 + Math.random() * 600;
        gain.gain.setValueAtTime(0.003, ctx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.15);
        osc.connect(gain);
        gain.connect(master);
        osc.start();
        osc.stop(ctx.currentTime + 0.15);
    });

    // Click sound
    document.addEventListener('click', (e) => {
        if (!playing || e.target.closest('#soundscape-panel')) return;
        const osc = ctx.createOscillator();
        const gain = ctx.createGain();
        osc.type = 'sine';
        osc.frequency.value = 800 + Math.random() * 400;
        gain.gain.setValueAtTime(0.01, ctx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.05);
        osc.connect(gain);
        gain.connect(master);
        osc.start();
        osc.stop(ctx.currentTime + 0.05);
    });

    // Typing sound
    document.addEventListener('keydown', () => {
        if (!playing) return;
        const osc = ctx.createOscillator();
        const gain = ctx.createGain();
        osc.type = 'sine';
        osc.frequency.value = 400 + Math.random() * 300;
        gain.gain.setValueAtTime(0.005, ctx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.03);
        osc.connect(gain);
        gain.connect(master);
        osc.start();
        osc.stop(ctx.currentTime + 0.03);
    });

    function createPanel() {
        if (document.getElementById('soundscape-panel')) return;

        const panel = document.createElement('div');
        panel.id = 'soundscape-panel';
        panel.style.cssText = `
            position:fixed;bottom:30px;left:30px;background:rgba(15,15,15,0.95);
            border-radius:20px;padding:20px;z-index:2147483646;min-width:260px;
            border:1px solid rgba(255,255,255,0.1);font-family:system-ui;
            box-shadow:0 20px 60px rgba(0,0,0,0.8);backdrop-filter:blur(20px);
            transition:all 0.3s;animation:soundscapeIn 0.5s ease-out;
        `;

        panel.innerHTML = `
            <style>
                @keyframes soundscapeIn { from{opacity:0;transform:translateY(30px)} to{opacity:1;transform:translateY(0)} }
                @keyframes soundscapePulse { 0%,100%{box-shadow:0 0 10px currentColor} 50%{box-shadow:0 0 25px currentColor} }
            </style>
            <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
                <div style="color:#fff;font-weight:700;font-size:15px;">🎧 Soundscape</div>
                <div style="display:flex;gap:5px;">
                    <button id="ss-min" style="background:none;border:none;color:#888;font-size:18px;cursor:pointer;">─</button>
                    <button id="ss-close" style="background:none;border:none;color:#888;font-size:18px;cursor:pointer;">✕</button>
                </div>
            </div>
            <div id="ss-profiles" style="display:flex;flex-direction:column;gap:6px;"></div>
            <div style="margin-top:12px;display:flex;align-items:center;gap:10px;">
                <span style="color:#888;font-size:12px;">🔈</span>
                <input type="range" id="ss-volume" min="0" max="100" value="${volume*100}" style="flex:1;accent-color:#4a90d9;">
                <span style="color:#888;font-size:12px;">🔊</span>
            </div>
            <div id="ss-status" style="text-align:center;color:#666;font-size:10px;margin-top:8px;"></div>
        `;

        document.body.appendChild(panel);

        const profileContainer = document.getElementById('ss-profiles');
        Object.entries(PROFILES).forEach(([key, prof]) => {
            const btn = document.createElement('div');
            btn.style.cssText = `
                padding:12px;border-radius:12px;cursor:pointer;display:flex;align-items:center;gap:10px;
                background:${key===currentProfile?prof.color+'33':'rgba(255,255,255,0.03)'};
                border:1px solid ${key===currentProfile?prof.color:'transparent'};
                transition:all 0.2s;color:#ddd;
            `;
            btn.innerHTML = `
                <span style="font-size:22px;">${prof.icon}</span>
                <div>
                    <div style="font-weight:600;font-size:13px;">${prof.name}</div>
                    <div style="font-size:10px;color:#888;">${prof.desc}</div>
                </div>
            `;
            btn.onmouseover = () => {
                if (key !== currentProfile) btn.style.background = 'rgba(255,255,255,0.06)';
            };
            btn.onmouseout = () => {
                if (key !== currentProfile) btn.style.background = 'rgba(255,255,255,0.03)';
            };
            btn.onclick = () => {
                if (!ctx) initAudio();
                startProfile(key);
                document.querySelectorAll('#ss-profiles > div').forEach(b => {
                    b.style.background = 'rgba(255,255,255,0.03)';
                    b.style.borderColor = 'transparent';
                });
                btn.style.background = prof.color+'33';
                btn.style.borderColor = prof.color;
                document.getElementById('ss-status').textContent = '▶ ' + prof.name;
            };
            profileContainer.appendChild(btn);
        });

        document.getElementById('ss-volume').oninput = function() {
            setVolume(this.value / 100);
        };

        document.getElementById('ss-close').onclick = () => {
            panel.remove();
            if (ctx) { ctx.close(); ctx = null; playing = false; clearAll(); }
        };

        document.getElementById('ss-min').onclick = () => {
            const content = panel.querySelector('#ss-profiles');
            const vol = panel.querySelector('div:last-child');
            const status = document.getElementById('ss-status');
            if (content.style.display === 'none') {
                content.style.display = 'flex';
                vol.style.display = 'flex';
                status.style.display = 'block';
                panel.style.minWidth = '260px';
            } else {
                content.style.display = 'none';
                vol.style.display = 'none';
                status.style.display = 'none';
                panel.style.minWidth = 'auto';
                panel.style.padding = '12px';
            }
        };

        document.getElementById('ss-status').textContent = PROFILES[currentProfile] ? '▶ ' + PROFILES[currentProfile].name : '';
    }

    function addTrigger() {
        const btn = document.createElement('div');
        btn.id = 'soundscape-trigger';
        btn.style.cssText = `
            position:fixed;bottom:30px;left:30px;width:50px;height:50px;
            background:rgba(15,15,15,0.9);border-radius:50%;display:flex;
            align-items:center;justify-content:center;font-size:24px;
            cursor:pointer;z-index:2147483645;border:1px solid rgba(255,255,255,0.2);
            transition:all 0.3s;box-shadow:0 10px 30px rgba(0,0,0,0.5);
        `;
        btn.textContent = '🎧';
        btn.title = 'Soundscape';
        btn.onmouseover = () => { btn.style.transform = 'scale(1.1)'; btn.style.boxShadow = '0 15px 40px rgba(0,0,0,0.7)'; };
        btn.onmouseout = () => { btn.style.transform = 'scale(1)'; btn.style.boxShadow = '0 10px 30px rgba(0,0,0,0.5)'; };
        btn.onclick = () => {
            if (document.getElementById('soundscape-panel')) {
                document.getElementById('soundscape-panel').remove();
            } else {
                createPanel();
                if (!ctx) initAudio();
            }
        };
        document.body.appendChild(btn);
    }

    setTimeout(addTrigger, 1500);
    console.log('🎧 Soundscape ready | Click the button at bottom left');
})();