Soundscape - Atmosphere Creator

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

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==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');
})();