Soundscape - Atmosphere Creator

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

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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