Soundscape - Atmosphere Creator

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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');
})();