// ==UserScript== // @name canvas.cdawgva - targeted pixel mute // @namespace http://tampermonkey.net/ // @version 1.18 // @description Targeted pixel notification mute + custom replacement // @match https://canvas.cdawgva.com/* // @grant none // @run-at document-start // @updateURL https://raw.githubusercontent.com/swordofbling/cdawgva-pixel-sound/main/cdawgva-pixel-sound.user.js // @downloadURL https://raw.githubusercontent.com/swordofbling/cdawgva-pixel-sound/main/cdawgva-pixel-sound.user.js // ==/UserScript== (function () { 'use strict'; // ---------- CONFIG ---------- const STORAGE_KEY = 'cdawgva_sound_settings_v9'; const DEFAULTS = { allowed: [6], volume: 100, preAllowPrev: true }; // preAllowPrev enforced const POLL_INTERVAL = 400; // debug flag (toggled by Alt+Shift+S) window.__CDGVA_SOUND_DEBUG = false; // ---------- Fingerprint seeds ---------- const TARGET_FINGERPRINTS = [ { sr: 48000, len: 51270, ch: 2, smallHash: null, note: 'pixel-notif-seed-1' }, { sr: 48000, len: 13375, ch: 1, smallHash: null, note: 'pixel-notif-seed-2' } ]; const STACK_PATTERNS = [ /canvas\.cdawgva\.com\/:328:94808/, /canvas\.cdawgva\.com\/:318:10022/, /play@https?:\/\/canvas\.cdawgva\.com/ ]; // ---------- storage helpers ---------- function loadSettings() { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return Object.assign({}, DEFAULTS); const s = Object.assign({}, DEFAULTS, JSON.parse(raw)); s.preAllowPrev = true; // enforced if (!Array.isArray(s.allowed)) s.allowed = Array.isArray(DEFAULTS.allowed) ? DEFAULTS.allowed.slice() : [6]; return s; } catch (e) { console.warn('cdawgva-sound: loadSettings error', e); return Object.assign({}, DEFAULTS); } } function saveSettings(s) { try { s = Object.assign({}, s, { preAllowPrev: true }); localStorage.setItem(STORAGE_KEY, JSON.stringify(s)); } catch (e) { console.warn('cdawgva-sound: saveSettings error', e); } } // ---------- custom replacement storage (no replaceAlways) ---------- function loadCustomSettings() { try { const raw = localStorage.getItem(STORAGE_KEY + '_custom'); if (!raw) return { mode: null, url: null, dataUrl: null, playWhenAllowed: true }; const parsed = JSON.parse(raw); parsed.playWhenAllowed = true; // enforced return parsed; } catch (e) { return { mode: null, url: null, dataUrl: null, playWhenAllowed: true }; } } function saveCustomSettings(s) { try { s = Object.assign({}, s, { playWhenAllowed: true }); localStorage.setItem(STORAGE_KEY + '_custom', JSON.stringify(s)); } catch (e) { console.warn('custom settings save error', e); } } // ---------- fingerprint helpers ---------- function captureStackString() { try { const s = (new Error()).stack || ''; return s.split('\n').slice(2).join('\n'); } catch (e) { return ''; } } function stackMatches(stack) { if (!stack) return false; try { return STACK_PATTERNS.some(rx => rx.test(stack)); } catch (e) { return false; } } function computeSmallHashFromBuffer(buffer, sampleCount = 4096) { try { if (!buffer || typeof buffer.length !== 'number') return null; const ch = buffer.numberOfChannels || 1; const len = Math.min(sampleCount, buffer.length); let acc = 2166136261 >>> 0; for (let c = 0; c < ch; c++) { const data = buffer.getChannelData(c); for (let i = 0; i < len; i += 4) { const v = Math.floor((data[i] || 0) * 32767); acc ^= (v & 0xFFFF); acc = (acc * 16777619) >>> 0; } } return acc >>> 0; } catch (e) { if (window.__CDGVA_SOUND_DEBUG) console.warn('fingerprint compute error', e); return null; } } function fingerprintFromBuffer(buffer) { if (!buffer) return null; return { sr: buffer.sampleRate || null, len: buffer.length || null, ch: buffer.numberOfChannels || null, smallHash: computeSmallHashFromBuffer(buffer) }; } function fingerprintMatches(a, b) { if (!a || !b) return false; if (a.len !== b.len) return false; if (a.ch !== b.ch) return false; if (a.sr !== b.sr) return false; if ((a.smallHash != null) && (b.smallHash != null)) return a.smallHash === b.smallHash; return true; } // persist/load fingerprints function persistFingerprints() { try { const toSave = TARGET_FINGERPRINTS.map(p => ({ sr: p.sr, len: p.len, ch: p.ch, smallHash: p.smallHash, note: p.note })); localStorage.setItem(STORAGE_KEY + '_fingerprints', JSON.stringify(toSave)); } catch (e) { if (window.__CDGVA_SOUND_DEBUG) console.warn('persist fingerprints failed', e); } } function loadPersistedFingerprints() { try { const raw = localStorage.getItem(STORAGE_KEY + '_fingerprints'); if (!raw) return; const arr = JSON.parse(raw); if (!Array.isArray(arr)) return; for (const p of arr) { const exists = TARGET_FINGERPRINTS.some(tp => tp.sr === p.sr && tp.len === p.len && tp.ch === p.ch && (tp.smallHash === p.smallHash)); if (!exists) TARGET_FINGERPRINTS.push({ sr: p.sr, len: p.len, ch: p.ch, smallHash: p.smallHash || null, note: p.note || 'persisted' }); } } catch (e) { if (window.__CDGVA_SOUND_DEBUG) console.warn('load persisted fingerprints error', e); } } loadPersistedFingerprints(); // ---------- page fraction / settings logic ---------- function parseFractionFromText(text) { if (!text) return null; let m = text.match(/\b(\d+)\s*(?:of)\s*(\d+)\b/i); if (m) return { x: parseInt(m[1], 10), y: parseInt(m[2], 10) }; m = text.match(/\b(\d+)\s*\/\s*(\d+)\b/); if (m) return { x: parseInt(m[1], 10), y: parseInt(m[2], 10) }; return null; } function queryFractionFromDOM() { try { const selectors = [ 'nav.grid-5.switcher .switcher-button.active small', 'nav.switcher .switcher-button.active small', 'nav .switcher-button.active small', 'nav.grid-5.switcher', 'nav.switcher' ]; for (const sel of selectors) { const el = document.querySelector(sel); if (!el) continue; const txt = (el.innerText || el.textContent || '').trim(); const f = parseFractionFromText(txt); if (f) return f; const span = el.querySelector('span'); if (span) { const f2 = parseFractionFromText((span.innerText || span.textContent || '').trim()); if (f2) return f2; } } if (document.title) { const ft = parseFractionFromText(document.title); if (ft) return ft; } } catch (e) {} return null; } let lastFraction = null, lastFractionTime = 0; function updateCachedFraction() { const f = queryFractionFromDOM(); if (f) { lastFraction = f; lastFractionTime = Date.now(); return true; } return false; } setInterval(updateCachedFraction, POLL_INTERVAL); function startFractionObserver() { try { const target = document.querySelector('nav.grid-5.switcher') || document.querySelector('nav.switcher') || document.documentElement; if (!target) return; const mo = new MutationObserver(() => updateCachedFraction()); mo.observe(target, { childList: true, subtree: true, characterData: true }); updateCachedFraction(); } catch (e) { setTimeout(startFractionObserver, 500); } } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', startFractionObserver, { once: true }); else startFractionObserver(); function findCurrentFractionWithCache() { const f = queryFractionFromDOM(); if (f) return f; if (lastFraction && (Date.now() - lastFractionTime) <= 1500) return lastFraction; return null; } // ---------- shouldAllowAudioNow: preAllowPrev always true ---------- function shouldAllowAudioNow(settings) { const frac = findCurrentFractionWithCache(); if (!frac) { if (window.__CDGVA_SOUND_DEBUG) console.debug('no fraction -> block by default'); return false; } const preAllowPrev = true; if (settings.allowed.includes(frac.x)) { if (window.__CDGVA_SOUND_DEBUG) console.debug('allowed by list', frac.x); return true; } if (preAllowPrev && settings.allowed.includes(frac.x + 1)) { if (window.__CDGVA_SOUND_DEBUG) console.debug('preAllowPrev allowed', frac.x); return true; } if (window.__CDGVA_SOUND_DEBUG) console.debug('blocked by list', frac.x); return false; } // ---------- custom sound management ---------- let customAudioContext = null; let customAudioBuffer = null; let customSettings = loadCustomSettings(); function ensureAudioContext() { try { if (!customAudioContext) { const AC = window.AudioContext || window.webkitAudioContext; customAudioContext = new (AC)(); } return customAudioContext; } catch (e) { return null; } } async function decodeArrayBufferToBuffer(arrayBuffer) { try { const ctx = ensureAudioContext(); if (!ctx) return null; return await ctx.decodeAudioData(arrayBuffer.slice(0)); } catch (e) { if (window.__CDGVA_SOUND_DEBUG) console.warn('decode error', e); return null; } } async function loadCustomFromUrl(url) { try { if (!url) return null; const resp = await fetch(url, {mode:'cors'}); if (!resp.ok) throw new Error('fetch failed ' + resp.status); const ab = await resp.arrayBuffer(); const buf = await decodeArrayBufferToBuffer(ab); if (buf) { customAudioBuffer = buf; customSettings = Object.assign(customSettings, { mode: 'url', url, playWhenAllowed: true }); saveCustomSettings(customSettings); console.info('cdawgva-sound: loaded custom audio from URL', url); return buf; } } catch (e) { console.warn('cdawgva-sound: loadCustomFromUrl error', e); } return null; } async function loadCustomFromDataUrl(dataUrl) { try { if (!dataUrl) return null; const comma = dataUrl.indexOf(','); if (comma < 0) return null; const b64 = dataUrl.slice(comma + 1); const binary = atob(b64); const len = binary.length; const ab = new ArrayBuffer(len); const dv = new Uint8Array(ab); for (let i = 0; i < len; i++) dv[i] = binary.charCodeAt(i); const buf = await decodeArrayBufferToBuffer(ab); if (buf) { customAudioBuffer = buf; customSettings = Object.assign(customSettings, { mode: 'data', dataUrl, playWhenAllowed: true }); saveCustomSettings(customSettings); console.info('cdawgva-sound: loaded custom audio from data URL'); return buf; } } catch (e) { console.warn('cdawgva-sound: loadCustomFromDataUrl error', e); } return null; } (function initCustomAtStart() { try { customSettings = loadCustomSettings(); if (customSettings.dataUrl) { loadCustomFromDataUrl(customSettings.dataUrl); } else if (customSettings.url) { loadCustomFromUrl(customSettings.url); } } catch (e) {} })(); function playCustomSound(volume = 1) { try { const buf = customAudioBuffer; if (!buf) return false; const ctx = ensureAudioContext(); if (!ctx) return false; const src = ctx.createBufferSource(); src.buffer = buf; const g = ctx.createGain(); g.gain.value = Math.max(0, Math.min(1, volume)); src.connect(g); g.connect(ctx.destination); try { src.start(0); } catch (e) { try { src.start(); } catch (ee) {} } const dur = (buf.duration || 1) + 0.1; setTimeout(() => { try { src.disconnect(); g.disconnect(); } catch (_) {} }, (dur * 1000) + 200); return true; } catch (e) { if (window.__CDGVA_SOUND_DEBUG) console.warn('playCustomSound error', e); return false; } } // ---------- last seen fingerprint ---------- let lastBlockedFingerprint = null; // ---------- patch AudioBufferSourceNode.start (conditional + replacement, no replaceAlways) ---------- (function patchAudioStartConditional() { if (window.__cdgva_start_patched_v118) return; window.__cdgva_start_patched_v118 = true; try { if (typeof AudioBufferSourceNode === 'undefined' || !AudioBufferSourceNode.prototype) { if (window.__CDGVA_SOUND_DEBUG) console.log('AudioBufferSourceNode not present yet'); return; } const origStart = AudioBufferSourceNode.prototype.start; AudioBufferSourceNode.prototype.start = function (...args) { try { const buffer = this.buffer; const stack = captureStackString(); if (!buffer) return origStart.apply(this, args); const fp = fingerprintFromBuffer(buffer); if (!fp) return origStart.apply(this, args); lastBlockedFingerprint = fp; const matchedTarget = TARGET_FINGERPRINTS.find(t => fingerprintMatches(t, fp)); if (stackMatches(stack) && matchedTarget) { const settings = loadSettings(); const custom = loadCustomSettings(); const allowNow = shouldAllowAudioNow(settings); // playWhenAllowed forced true by design if (allowNow) { if (customAudioBuffer) { try { this.__cdgva_blocked = true; } catch (_) {} if (window.__CDGVA_SOUND_DEBUG) console.info('cdawgva-sound: allowed pixel -> playing custom replacement', matchedTarget); playCustomSound(getVolumeFloat(settings)); return; } else { if (window.__CDGVA_SOUND_DEBUG) console.info('cdawgva-sound: allowed pixel but no custom audio -> letting original play', matchedTarget); return origStart.apply(this, args); } } else { try { this.__cdgva_blocked = true; } catch (_) {} if (window.__CDGVA_SOUND_DEBUG) console.info('cdawgva-sound: pixel not allowed -> blocking original', matchedTarget); return; } } return origStart.apply(this, args); } catch (e) { if (window.__CDGVA_SOUND_DEBUG) console.warn('cdawgva-sound: error in AudioBufferSourceNode.start patch', e); return origStart.apply(this, args); } }; if (window.__CDGVA_SOUND_DEBUG) console.log('cdawgva-sound: AudioBufferSourceNode.start patched (v1.18)'); } catch (e) { console.warn('cdawgva-sound: failed to patch AudioBufferSourceNode.start', e); } })(); // ---------- Howler fallback (same rules) ---------- (function patchHowlerFallbackConditionalV118() { if (window.__cdgva_howler_patched_v118) return; window.__cdgva_howler_patched_v118 = true; try { const Howl = window.Howl || (window.Howler && window.Howler.Howl); if (!Howl || !Howl.prototype) { if (window.__CDGVA_SOUND_DEBUG) console.log('Howler not present yet for patch'); return; } const origPlay = Howl.prototype.play; Howl.prototype.play = function (...args) { try { const stack = captureStackString(); if (!stackMatches(stack)) return origPlay.apply(this, args); const src = this._src || this._srcs || this._srcArray || ''; const spriteKeys = this._sprite ? Object.keys(this._sprite) : []; const metaBlob = (src ? JSON.stringify(src) : '') + '|' + JSON.stringify(spriteKeys); const looksLikePixel = /pixel/i.test(metaBlob) || /notification/i.test(metaBlob); const settingsNow = loadSettings(); const custom = loadCustomSettings(); const allowNow = shouldAllowAudioNow(settingsNow); if (looksLikePixel) { const id = origPlay.apply(this, args); if (allowNow) { if (customAudioBuffer) { try { if (typeof this.stop === 'function') this.stop(id); } catch (_) {} playCustomSound(getVolumeFloat(settingsNow)); console.info('cdawgva-sound: replaced Howler.play (allowed) with custom', { id }); return id; } else { if (window.__CDGVA_SOUND_DEBUG) console.info('cdawgva-sound: Howler.play allowed but no custom -> let play', { id }); return id; } } else { try { if (typeof this.stop === 'function') this.stop(id); } catch (_) {} console.info('cdawgva-sound: Howler.play blocked (not allowed)', { id }); return id; } } } catch (e) { if (window.__CDGVA_SOUND_DEBUG) console.warn('Howler.play replacement error', e); } return origPlay.apply(this, args); }; if (window.__CDGVA_SOUND_DEBUG) console.log('cdawgva-sound: Howler.play patched (v1.18)'); } catch (e) { console.warn('cdawgva-sound: failed to patch Howler.play', e); } })(); // ---------- volume helper ---------- function getVolumeFloat(settings) { const v = Math.max(0, Math.min(100, Number(settings.volume || 100))); return v / 100; } // ---------- UI: badge with allowed checkboxes restored ---------- function createBadgeUI() { if (document.getElementById('cdawg-sound-panel-root')) return; const settings = loadSettings(); customSettings = loadCustomSettings(); const root = document.createElement('div'); root.id = 'cdawg-sound-panel-root'; root.style.cssText = 'position:fixed;right:12px;bottom:12px;z-index:2147483647;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;'; const badge = document.createElement('button'); badge.id = 'cdawg-sound-badge'; badge.type = 'button'; badge.title = 'cdawgva sound settings'; badge.style.cssText = 'width:54px;height:44px;border-radius:10px;border:0;box-shadow:0 4px 12px rgba(0,0,0,0.25);background:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:16px;padding:6px;'; badge.innerHTML = '