| <!DOCTYPE html> |
| <html lang="pt-BR"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <meta name="theme-color" content="#0a0a0a"> |
| <meta name="description" content="Everywhere at the End of Time - Simulador de Degradação Auditiva"> |
| <title>Everywhere - Simulador de Degradação</title> |
| <link rel="manifest" href="data:application/json;base64,ewogICJuYW1lIjogIkV2ZXJ5d2hlcmUgRGVncmFkYWNhbyIsCiAgInNob3J0X25hbWUiOiAiRXZlcnl3aGVyZSIsCiAgInN0YXJ0X3VybCI6ICIuLyIsCiAgImRpc3BsYXkiOiAic3RhbmRhbG9uZSIsCiAgImJhY2tncm91bmRfY29sb3IiOiAiIzBhMGEwYSIsCiAgInRoZW1lX2NvbG9yIjogIiMwYTBhMGEiLAogICJpY29ucyI6IFsKICAgIHsKICAgICAgInNyYyI6ICIuLyIsCiAgICAgICJzaXplcyIjogWyIxMjgiLCA2NCIsIDMyLCAxNl0sCiAgICAgICJ0eXBlIjogImltYWdlL3BuZyIKICAgIH0KICBdCn0="> |
| <style> |
| :root { |
| --bg: #0a0a0a; |
| --surface: #111111; |
| --surface-elevated: #161616; |
| --border: #1a1a1a; |
| --border-hover: #2a2a2a; |
| --text: #e0e0e0; |
| --text-dim: #666666; |
| --text-dimmer: #444444; |
| --accent: #999999; |
| --accent-dim: #555555; |
| --stage-0: #888888; |
| --stage-1: #777777; |
| --stage-2: #666666; |
| --stage-3: #555555; |
| --stage-4: #444444; |
| --stage-5: #333333; |
| --stage-6: #222222; |
| } |
| |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| html { |
| scroll-behavior: smooth; |
| } |
| |
| body { |
| font-family: 'SF Mono', 'Fira Code', 'Courier New', monospace; |
| background: var(--bg); |
| color: var(--text); |
| min-height: 100vh; |
| display: flex; |
| flex-direction: column; |
| overflow-x: hidden; |
| line-height: 1.5; |
| } |
| |
| |
| body::before { |
| content: ''; |
| position: fixed; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background: |
| radial-gradient(ellipse at 20% 20%, rgba(30, 30, 30, 0.3) 0%, transparent 50%), |
| radial-gradient(ellipse at 80% 80%, rgba(20, 20, 20, 0.3) 0%, transparent 50%); |
| pointer-events: none; |
| z-index: -1; |
| } |
| |
| header { |
| padding: 1.5rem 1rem; |
| text-align: center; |
| border-bottom: 1px solid var(--border); |
| background: linear-gradient(180deg, rgba(17, 17, 17, 0.9) 0%, transparent 100%); |
| } |
| |
| header h1 { |
| font-size: 0.85rem; |
| font-weight: 400; |
| letter-spacing: 0.4em; |
| text-transform: uppercase; |
| color: var(--text-dim); |
| margin-bottom: 0.5rem; |
| } |
| |
| header a { |
| color: var(--text-dimmer); |
| text-decoration: none; |
| font-size: 0.65rem; |
| letter-spacing: 0.15em; |
| transition: color 0.3s ease; |
| display: inline-block; |
| padding: 0.25rem 0.5rem; |
| border: 1px solid transparent; |
| } |
| |
| header a:hover { |
| color: var(--accent); |
| border-color: var(--border); |
| background: var(--surface); |
| } |
| |
| .stage-display { |
| padding: 2rem 1rem; |
| text-align: center; |
| min-height: 140px; |
| display: flex; |
| flex-direction: column; |
| justify-content: center; |
| align-items: center; |
| position: relative; |
| } |
| |
| .stage-display::after { |
| content: ''; |
| position: absolute; |
| bottom: 0; |
| left: 50%; |
| transform: translateX(-50%); |
| width: 60%; |
| height: 1px; |
| background: linear-gradient(90deg, transparent, var(--border), transparent); |
| } |
| |
| .stage-number { |
| font-size: 3rem; |
| font-weight: 200; |
| color: var(--text-dim); |
| opacity: 0.3; |
| position: absolute; |
| top: 1rem; |
| left: 50%; |
| transform: translateX(-50%); |
| letter-spacing: 0.2em; |
| } |
| |
| .stage-name { |
| font-size: 1rem; |
| font-weight: 400; |
| letter-spacing: 0.25em; |
| text-transform: uppercase; |
| margin-bottom: 0.75rem; |
| transition: all 0.5s ease; |
| } |
| |
| .stage-desc { |
| font-size: 0.7rem; |
| color: var(--text-dim); |
| max-width: 320px; |
| line-height: 1.7; |
| font-style: italic; |
| } |
| |
| main { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| padding: 1rem; |
| gap: 1.5rem; |
| max-width: 600px; |
| margin: 0 auto; |
| width: 100%; |
| } |
| |
| |
| .visualizer-container { |
| width: 100%; |
| height: 120px; |
| background: var(--surface); |
| border: 1px solid var(--border); |
| position: relative; |
| overflow: hidden; |
| border-radius: 2px; |
| } |
| |
| .visualizer-container::before { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: 0; |
| right: 0; |
| height: 1px; |
| background: linear-gradient(90deg, transparent, var(--accent-dim), transparent); |
| } |
| |
| #visualizer { |
| width: 100%; |
| height: 100%; |
| display: block; |
| } |
| |
| |
| .controls { |
| display: flex; |
| flex-direction: column; |
| gap: 1rem; |
| } |
| |
| .file-input-wrapper { |
| position: relative; |
| width: 100%; |
| } |
| |
| .file-input-wrapper input[type="file"] { |
| position: absolute; |
| left: 0; |
| top: 0; |
| opacity: 0; |
| width: 100%; |
| height: 100%; |
| cursor: pointer; |
| z-index: 2; |
| } |
| |
| .btn { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| color: var(--text); |
| padding: 0.9rem 1.2rem; |
| font-family: inherit; |
| font-size: 0.7rem; |
| letter-spacing: 0.2em; |
| text-transform: uppercase; |
| cursor: pointer; |
| transition: all 0.25s ease; |
| width: 100%; |
| text-align: center; |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .btn::before { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: -100%; |
| width: 100%; |
| height: 100%; |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.05), transparent); |
| transition: left 0.5s ease; |
| } |
| |
| .btn:hover::before { |
| left: 100%; |
| } |
| |
| .btn:hover:not(:disabled) { |
| border-color: var(--accent-dim); |
| background: var(--surface-elevated); |
| transform: translateY(-1px); |
| } |
| |
| .btn:active:not(:disabled) { |
| transform: translateY(0); |
| } |
| |
| .btn:disabled { |
| opacity: 0.25; |
| cursor: not-allowed; |
| } |
| |
| .btn.active { |
| background: var(--accent-dim); |
| border-color: var(--accent); |
| color: #fff; |
| } |
| |
| |
| .playback-controls { |
| display: flex; |
| gap: 0.5rem; |
| } |
| |
| .playback-controls .btn { |
| flex: 1; |
| } |
| |
| |
| .progress-section { |
| width: 100%; |
| } |
| |
| .progress-bar { |
| width: 100%; |
| height: 3px; |
| background: var(--border); |
| cursor: pointer; |
| position: relative; |
| border-radius: 1px; |
| overflow: hidden; |
| } |
| |
| .progress-bar::after { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background: transparent; |
| transition: background 0.3s; |
| } |
| |
| .progress-bar:hover::after { |
| background: rgba(255,255,255,0.02); |
| } |
| |
| .progress-fill { |
| height: 100%; |
| background: var(--accent); |
| width: 0%; |
| transition: width 0.1s linear; |
| position: relative; |
| } |
| |
| .progress-fill::after { |
| content: ''; |
| position: absolute; |
| right: 0; |
| top: 50%; |
| transform: translateY(-50%); |
| width: 8px; |
| height: 8px; |
| background: var(--accent); |
| border-radius: 50%; |
| opacity: 0; |
| transition: opacity 0.2s; |
| } |
| |
| .progress-bar:hover .progress-fill::after { |
| opacity: 1; |
| } |
| |
| .time-display { |
| display: flex; |
| justify-content: space-between; |
| font-size: 0.6rem; |
| color: var(--text-dimmer); |
| margin-top: 0.6rem; |
| font-variant-numeric: tabular-nums; |
| letter-spacing: 0.05em; |
| } |
| |
| |
| .chaos-control { |
| display: flex; |
| align-items: center; |
| gap: 1rem; |
| padding: 0.75rem 1rem; |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: 2px; |
| } |
| |
| .chaos-control label { |
| font-size: 0.65rem; |
| color: var(--text-dim); |
| white-space: nowrap; |
| letter-spacing: 0.1em; |
| text-transform: uppercase; |
| } |
| |
| .chaos-control input[type="range"] { |
| flex: 1; |
| -webkit-appearance: none; |
| appearance: none; |
| background: var(--border); |
| height: 2px; |
| outline: none; |
| border-radius: 1px; |
| } |
| |
| .chaos-control input[type="range"]::-webkit-slider-thumb { |
| -webkit-appearance: none; |
| width: 14px; |
| height: 14px; |
| background: var(--accent); |
| cursor: pointer; |
| border-radius: 50%; |
| transition: transform 0.2s, background 0.2s; |
| } |
| |
| .chaos-control input[type="range"]::-webkit-slider-thumb:hover { |
| transform: scale(1.2); |
| background: var(--text); |
| } |
| |
| .chaos-value { |
| font-size: 0.6rem; |
| color: var(--text-dimmer); |
| min-width: 2.5em; |
| text-align: right; |
| font-variant-numeric: tabular-nums; |
| } |
| |
| |
| .stages-section { |
| margin-top: 0.5rem; |
| } |
| |
| .stages-label { |
| font-size: 0.6rem; |
| color: var(--text-dimmer); |
| letter-spacing: 0.15em; |
| text-transform: uppercase; |
| text-align: center; |
| margin-bottom: 0.75rem; |
| } |
| |
| .stages-container { |
| display: grid; |
| grid-template-columns: repeat(7, 1fr); |
| gap: 0.3rem; |
| } |
| |
| .stage-btn { |
| padding: 0.8rem 0.25rem; |
| font-size: 0.7rem; |
| background: var(--surface); |
| border: 1px solid var(--border); |
| color: var(--text-dim); |
| cursor: pointer; |
| transition: all 0.3s ease; |
| text-align: center; |
| font-family: inherit; |
| position: relative; |
| } |
| |
| .stage-btn:hover:not(.active) { |
| border-color: var(--accent-dim); |
| background: var(--surface-elevated); |
| } |
| |
| .stage-btn.active { |
| background: var(--accent-dim); |
| border-color: var(--accent); |
| color: var(--text); |
| } |
| |
| .stage-btn[data-stage="0"].active { border-left: 2px solid var(--stage-0); } |
| .stage-btn[data-stage="6"].active { border-right: 2px solid var(--stage-6); } |
| |
| .stage-btn::after { |
| content: attr(data-desc); |
| position: absolute; |
| bottom: 100%; |
| left: 50%; |
| transform: translateX(-50%); |
| background: var(--surface-elevated); |
| border: 1px solid var(--border); |
| padding: 0.4rem 0.6rem; |
| font-size: 0.55rem; |
| white-space: nowrap; |
| opacity: 0; |
| pointer-events: none; |
| transition: opacity 0.2s, transform 0.2s; |
| z-index: 10; |
| margin-bottom: 0.5rem; |
| } |
| |
| .stage-btn:hover::after { |
| opacity: 1; |
| } |
| |
| |
| .status { |
| text-align: center; |
| font-size: 0.6rem; |
| color: var(--text-dimmer); |
| padding: 0.75rem; |
| letter-spacing: 0.05em; |
| border-top: 1px solid var(--border); |
| background: var(--surface); |
| } |
| |
| .status.error { |
| color: #996666; |
| background: rgba(153, 102, 102, 0.1); |
| } |
| |
| .status.success { |
| color: #669966; |
| background: rgba(102, 153, 102, 0.1); |
| } |
| |
| .status.processing { |
| color: var(--accent); |
| } |
| |
| |
| .offline-badge { |
| position: fixed; |
| bottom: 1.5rem; |
| right: 1.5rem; |
| background: var(--surface); |
| border: 1px solid var(--border); |
| padding: 0.5rem 0.75rem; |
| font-size: 0.55rem; |
| color: var(--text-dimmer); |
| opacity: 0; |
| transition: opacity 0.4s ease; |
| letter-spacing: 0.1em; |
| } |
| |
| .offline-badge.visible { |
| opacity: 1; |
| } |
| |
| |
| .tips { |
| font-size: 0.55rem; |
| color: var(--text-dimmer); |
| text-align: center; |
| padding: 0.5rem; |
| font-style: italic; |
| } |
| |
| |
| @media (max-width: 480px) { |
| .stages-container { |
| grid-template-columns: repeat(4, 1fr); |
| } |
| |
| .stages-container .stage-btn:nth-child(5), |
| .stages-container .stage-btn:nth-child(6), |
| .stages-container .stage-btn:nth-child(7) { |
| grid-column: span 1; |
| } |
| |
| .stage-display { |
| padding: 1.5rem 1rem; |
| min-height: 120px; |
| } |
| |
| .stage-number { |
| font-size: 2.5rem; |
| } |
| |
| .stage-name { |
| font-size: 0.9rem; |
| } |
| |
| .stage-desc { |
| font-size: 0.65rem; |
| } |
| |
| .visualizer-container { |
| height: 100px; |
| } |
| } |
| |
| |
| @keyframes pulse { |
| 0%, 100% { opacity: 0.3; } |
| 50% { opacity: 0.5; } |
| } |
| |
| .status.processing::before { |
| content: '● '; |
| animation: pulse 1.5s infinite; |
| } |
| </style> |
| </head> |
| <body> |
| <header> |
| <h1>Everywhere</h1> |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">Built with anycoder</a> |
| </header> |
|
|
| <div class="stage-display"> |
| <span class="stage-number" id="stageNumber">0</span> |
| <h2 class="stage-name" id="stageName">Estágio 0</h2> |
| <p class="stage-desc" id="stageDesc">Áudio puro e inalterado</p> |
| </div> |
|
|
| <main> |
| <div class="visualizer-container"> |
| <canvas id="visualizer"></canvas> |
| </div> |
|
|
| <div class="controls"> |
| <div class="file-input-wrapper"> |
| <button class="btn" id="loadBtn"> |
| <span>Carregar Áudio</span> |
| </button> |
| <input type="file" id="fileInput" accept=".flac,.mp3,.wav,.ogg,.m4a,.aac,.wma,.aiff"> |
| </div> |
|
|
| <div class="playback-controls"> |
| <button class="btn" id="playBtn" disabled>▶ Reproduzir</button> |
| <button class="btn" id="pauseBtn" disabled>⏸ Pausar</button> |
| <button class="btn" id="restartBtn" disabled>↺ Reiniciar</button> |
| </div> |
|
|
| <div class="progress-section"> |
| <div class="progress-bar" id="progressBar"> |
| <div class="progress-fill" id="progressFill"></div> |
| </div> |
| <div class="time-display"> |
| <span id="currentTime">0:00</span> |
| <span id="duration">0:00</span> |
| </div> |
| </div> |
|
|
| <div class="chaos-control"> |
| <label>Caos</label> |
| <input type="range" id="chaosSlider" min="0" max="100" value="0"> |
| <span class="chaos-value" id="chaosValue">0%</span> |
| </div> |
|
|
| <div class="stages-section"> |
| <div class="stages-label">Selecione o estágio de degradação</div> |
| <div class="stages-container"> |
| <button class="stage-btn active" data-stage="0" data-desc="Áudio puro">0</button> |
| <button class="stage-btn" data-stage="1" data-desc="Nostalgia doce">1</button> |
| <button class="stage-btn" data-stage="2" data-desc="Fragmentação">2</button> |
| <button class="stage-btn" data-stage="3" data-desc="Última lucidez">3</button> |
| <button class="stage-btn" data-stage="4" data-desc="Colapso">4</button> |
| <button class="stage-btn" data-stage="5" data-desc="Agonia">5</button> |
| <button class="stage-btn" data-stage="6" data-desc="Dissolução">6</button> |
| </div> |
| </div> |
|
|
| <div class="tips"> |
| Dica: Use fones de ouvido para melhor experiência |
| </div> |
| </div> |
|
|
| <div class="status" id="status">Aguardando arquivo de áudio...</div> |
| </main> |
|
|
| <div class="offline-badge" id="offlineBadge">Modo Offline</div> |
|
|
| <script> |
| |
| if ('serviceWorker' in navigator) { |
| const swBlob = new Blob([`${swCode}`], { type: 'application/javascript' }); |
| const swUrl = URL.createObjectURL(swBlob); |
| |
| navigator.serviceWorker.register(swUrl) |
| .then(reg => console.log('SW registered')) |
| .catch(err => console.log('SW registration failed:', err)); |
| } |
| |
| |
| const swCode = ` |
| const CACHE_NAME = 'everywhere-v2'; |
| const urlsToCache = ['./']; |
| |
| self.addEventListener('install', event => { |
| event.waitUntil( |
| caches.open(CACHE_NAME) |
| .then(cache => cache.addAll(urlsToCache)) |
| ); |
| self.skipWaiting(); |
| }); |
| |
| self.addEventListener('activate', event => { |
| event.waitUntil( |
| caches.keys().then(cacheNames => { |
| return Promise.all( |
| cacheNames.map(cacheName => { |
| if (cacheName !== CACHE_NAME) { |
| return caches.delete(cacheName); |
| } |
| }) |
| ); |
| }) |
| ); |
| self.clients.claim(); |
| }); |
| |
| self.addEventListener('fetch', event => { |
| event.respondWith( |
| caches.match(event.request) |
| .then(response => { |
| if (response) return response; |
| return fetch(event.request).then(response => { |
| if (!response || response.status !== 200 || response.type !== 'basic') { |
| return response; |
| } |
| const responseToCache = response.clone(); |
| caches.open(CACHE_NAME) |
| .then(cache => cache.put(event.request, responseToCache)); |
| return response; |
| }); |
| }) |
| ); |
| }); |
| `; |
| |
| |
| const DB_NAME = 'EverywhereAudioDB'; |
| const DB_VERSION = 2; |
| const STORE_NAME = 'audioFiles'; |
| |
| let db = null; |
| |
| function openDatabase() { |
| return new Promise((resolve, reject) => { |
| const request = indexedDB.open(DB_NAME, DB_VERSION); |
| |
| request.onerror = () => reject(request.error); |
| |
| request.onsuccess = () => { |
| db = request.result; |
| resolve(db); |
| }; |
| |
| request.onupgradeneeded = (event) => { |
| const database = event.target.result; |
| if (!database.objectStoreNames.contains(STORE_NAME)) { |
| database.createObjectStore(STORE_NAME, { keyPath: 'id' }); |
| } |
| }; |
| }); |
| } |
| |
| async function saveAudioToDB(id, arrayBuffer, fileName) { |
| return new Promise((resolve, reject) => { |
| const transaction = db.transaction([STORE_NAME], 'readwrite'); |
| const store = transaction.objectStore(STORE_NAME); |
| const request = store.put({ id, data: arrayBuffer, name: fileName, timestamp: Date.now() }); |
| request.onsuccess = () => resolve(); |
| request.onerror = () => reject(request.error); |
| }); |
| } |
| |
| async function getAudioFromDB(id) { |
| return new Promise((resolve, reject) => { |
| const transaction = db.transaction([STORE_NAME], 'readonly'); |
| const store = transaction.objectStore(STORE_NAME); |
| const request = store.get(id); |
| request.onsuccess = () => resolve(request.result); |
| request.onerror = () => reject(request.error); |
| }); |
| } |
| |
| async function clearAudioFromDB(id) { |
| return new Promise((resolve, reject) => { |
| const transaction = db.transaction([STORE_NAME], 'readwrite'); |
| const store = transaction.objectStore(STORE_NAME); |
| const request = store.delete(id); |
| request.onsuccess = () => resolve(); |
| request.onerror = () => reject(request.error); |
| }); |
| } |
| |
| |
| let audioContext = null; |
| let audioBuffer = null; |
| let sourceNode = null; |
| let gainNode = null; |
| let filterNode = null; |
| let convolverNode = null; |
| let delayNode = null; |
| let feedbackGain = null; |
| let waveShaperNode = null; |
| let analyserNode = null; |
| let masterGain = null; |
| |
| |
| let auxiliarySources = []; |
| let auxiliaryPool = []; |
| const POOL_SIZE = 4; |
| |
| |
| let lfos = []; |
| |
| |
| let noiseSource = null; |
| let noiseGain = null; |
| let droneOscillator = null; |
| let droneGain = null; |
| let ghostNotesInterval = null; |
| |
| |
| let currentStage = 0; |
| let isPlaying = false; |
| let startTime = 0; |
| let pauseTime = 0; |
| let chaosLevel = 0; |
| let lastStageTime = 0; |
| let stage6EndScheduled = false; |
| let animationFrameId = null; |
| |
| |
| const stageData = [ |
| { name: 'Estágio 0', desc: 'Áudio puro e inalterado', number: '0' }, |
| { name: 'Estágio 1', desc: 'Uma nostalgia doce e borrada que, sob uma superfície calma e repetitiva, esconde os primeiros sinais sutis da demência.', number: '1' }, |
| { name: 'Estágio 2', desc: 'Uma melancólica recusa em aceitar o esquecimento, onde memórias claras começam a se fragmentar sob estática crescente.', number: '2' }, |
| { name: 'Estágio 3', desc: 'A última tentativa desesperada da memória em manter a lucidez antes de sucumbir totalmente ao caos e à desorientação.', number: '3' }, |
| { name: 'Estágio 4', desc: 'A ruptura final da consciência, transformando memórias em ruídos caóticos e desoladores.', number: '4' }, |
| { name: 'Estágio 5', desc: 'O estágio 5 é a agonia da mente em ruínas, onde fragmentos musicais distorcidos dão lugar a um vazio de puro horror e desorientação.', number: '5' }, |
| { name: 'Estágio 6', desc: 'A dissolução completa do self em silêncio e caos ambientais, marcando o fim definitivo da memória.', number: '6' } |
| ]; |
| |
| function initAudioContext() { |
| if (!audioContext) { |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
| } |
| return audioContext; |
| } |
| |
| |
| function createImpulseResponse(duration, decay, channels = 2) { |
| const sampleRate = audioContext.sampleRate; |
| const length = Math.floor(sampleRate * duration); |
| const impulse = audioContext.createBuffer(channels, length, sampleRate); |
| |
| for (let channel = 0; channel < channels; channel++) { |
| const channelData = impulse.getChannelData(channel); |
| for (let i = 0; i < length; i++) { |
| channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, decay); |
| } |
| } |
| return impulse; |
| } |
| |
| |
| function makeDistortionCurve(amount) { |
| const samples = 44100; |
| const curve = new Float32Array(samples); |
| const deg = Math.PI / 180; |
| |
| for (let i = 0; i < samples; i++) { |
| const x = (i * 2) / samples - 1; |
| curve[i] = ((3 + amount) * x * 20 * deg) / (Math.PI + amount * Math.abs(x)); |
| } |
| return curve; |
| } |
| |
| |
| function createPinkNoise() { |
| const bufferSize = 2 * audioContext.sampleRate; |
| const noiseBuffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate); |
| const output = noiseBuffer.getChannelData(0); |
| |
| let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0; |
| |
| for (let i = 0; i < bufferSize; i++) { |
| const white = Math.random() * 2 - 1; |
| b0 = 0.99886 * b0 + white * 0.0555179; |
| b1 = 0.99332 * b1 + white * 0.0750759; |
| b2 = 0.96900 * b2 + white * 0.1538520; |
| b3 = 0.86650 * b3 + white * 0.3104856; |
| b4 = 0.55000 * b4 + white * 0.5329522; |
| b5 = -0.7616 * b5 - white * 0.0168980; |
| output[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362; |
| output[i] *= 0.11; |
| b6 = white * 0.115926; |
| } |
| |
| return noiseBuffer; |
| } |
| |
| |
| function createBrownNoise() { |
| const bufferSize = 2 * audioContext.sampleRate; |
| const noiseBuffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate); |
| const output = noiseBuffer.getChannelData(0); |
| |
| let lastOut = 0; |
| for (let i = 0; i < bufferSize; i++) { |
| const white = Math.random() * 2 - 1; |
| output[i] = (lastOut + (0.02 * white)) / 1.02; |
| lastOut = output[i]; |
| output[i] *= 3.5; |
| } |
| |
| return noiseBuffer; |
| } |
| |
| function createAudioGraph() { |
| const ctx = audioContext; |
| |
| |
| sourceNode = ctx.createBufferSource(); |
| sourceNode.buffer = audioBuffer; |
| sourceNode.loop = true; |
| |
| |
| gainNode = ctx.createGain(); |
| filterNode = ctx.createBiquadFilter(); |
| filterNode.type = 'lowpass'; |
| filterNode.frequency.value = 20000; |
| filterNode.Q.value = 0.7; |
| |
| convolverNode = ctx.createConvolver(); |
| convolverNode.buffer = createImpulseResponse(2, 2); |
| |
| delayNode = ctx.createDelay(5); |
| delayNode.delayTime.value = 0.3; |
| |
| feedbackGain = ctx.createGain(); |
| feedbackGain.gain.value = 0.3; |
| |
| waveShaperNode = ctx.createWaveShaper(); |
| waveShaperNode.curve = makeDistortionCurve(0); |
| waveShaperNode.oversample = '4x'; |
| |
| |
| analyserNode = ctx.createAnalyser(); |
| analyserNode.fftSize = 2048; |
| analyserNode.smoothingTimeConstant = 0.8; |
| |
| |
| masterGain = ctx.createGain(); |
| masterGain.gain.value = 1; |
| |
| |
| sourceNode.connect(gainNode); |
| gainNode.connect(filterNode); |
| filterNode.connect(waveShaperNode); |
| waveShaperNode.connect(convolverNode); |
| convolverNode.connect(delayNode); |
| delayNode.connect(feedbackGain); |
| feedbackGain.connect(delayNode); |
| convolverNode.connect(analyserNode); |
| delayNode.connect(analyserNode); |
| analyserNode.connect(masterGain); |
| masterGain.connect(ctx.destination); |
| |
| |
| for (let i = 0; i < POOL_SIZE; i++) { |
| auxiliaryPool.push({ |
| source: null, |
| gain: null, |
| filter: null, |
| inUse: false |
| }); |
| } |
| |
| |
| const noiseBuffer = createPinkNoise(); |
| noiseSource = ctx.createBufferSource(); |
| noiseSource.buffer = noiseBuffer; |
| noiseSource.loop = true; |
| noiseGain = ctx.createGain(); |
| noiseGain.gain.value = 0; |
| |
| const noiseFilter = ctx.createBiquadFilter(); |
| noiseFilter.type = 'lowpass'; |
| noiseFilter.frequency.value = 1000; |
| |
| noiseSource.connect(noiseFilter); |
| noiseFilter.connect(noiseGain); |
| noiseGain.connect(masterGain); |
| noiseSource.start(); |
| |
| |
| droneOscillator = ctx.createOscillator(); |
| droneOscillator.type = 'sine'; |
| droneOscillator.frequency.value = 55; |
| droneGain = ctx.createGain(); |
| droneGain.gain.value = 0; |
| |
| const droneFilter = ctx.createBiquadFilter(); |
| droneFilter.type = 'lowpass'; |
| droneFilter.frequency.value = 200; |
| |
| droneOscillator.connect(droneFilter); |
| droneFilter.connect(droneGain); |
| droneGain.connect(masterGain); |
| droneOscillator.start(); |
| } |
| |
| |
| function createLFO(targetParam, frequency, min, max, type = 'sine') { |
| const lfo = audioContext.createOscillator(); |
| const lfoGain = audioContext.createGain(); |
| |
| lfo.type = type; |
| lfo.frequency.value = frequency; |
| |
| const range = max - min; |
| lfoGain.gain.value = range / 2; |
| |
| |
| const centerValue = (min + max) / 2; |
| |
| lfo.connect(lfoGain); |
| lfoGain.connect(targetParam); |
| |
| |
| targetParam.setValueAtTime(centerValue, audioContext.currentTime); |
| |
| lfo.start(); |
| |
| return { lfo, lfoGain, targetParam, min, max, centerValue }; |
| } |
| |
| function clearLFOs() { |
| lfos.forEach(lfoObj => { |
| try { |
| if (lfoObj.lfo) lfoObj.lfo.stop(); |
| } catch(e) {} |
| }); |
| lfos = []; |
| } |
| |
| function updateLFOsForStage(stage, chaos) { |
| clearLFOs(); |
| |
| if (!audioBuffer || !gainNode) return; |
| |
| const chaosFactor = chaos / 100; |
| const ctx = audioContext; |
| const now = ctx.currentTime; |
| |
| switch(stage) { |
| case 0: |
| |
| gainNode.gain.setTargetAtTime(1, now, 0.5); |
| filterNode.frequency.setTargetAtTime(20000, now, 0.5); |
| filterNode.Q.setTargetAtTime(0.7, now, 0.5); |
| waveShaperNode.curve = makeDistortionCurve(0); |
| convolverNode.buffer = createImpulseResponse(1, 4); |
| delayNode.delayTime.setTargetAtTime(0.3, now, 0.5); |
| feedbackGain.gain.setTargetAtTime(0.2, now, 0.5); |
| masterGain.gain.setTargetAtTime(1, now, 0.5); |
| noiseGain.gain.setTargetAtTime(0, now, 0.5); |
| droneGain.gain.setTargetAtTime(0, now, 0.5); |
| if (sourceNode && sourceNode.playbackRate) { |
| sourceNode.playbackRate.setTargetAtTime(1, now, 0.5); |
| } |
| break; |
| |
| case 1: |
| |
| gainNode.gain.setTargetAtTime(0.95, now, 1); |
| filterNode.frequency.setTargetAtTime(8000 + chaosFactor * 2000, now, 2); |
| filterNode.Q.setTargetAtTime(1, now, 1); |
| convolverNode.buffer = createImpulseResponse(1.5 + chaosFactor, 3 - chaosFactor); |
| delayNode.delayTime.setTargetAtTime(0.35, now, 1); |
| feedbackGain.gain.setTargetAtTime(0.25 + chaosFactor * 0.1, now, 1); |
| waveShaperNode.curve = makeDistortionCurve(chaosFactor * 5); |
| |
| |
| lfos.push(createLFO(gainNode.gain, 0.08, 0.92, 0.98, 'sine')); |
| break; |
| |
| case 2: |
| |
| gainNode.gain.setTargetAtTime(0.88 - chaosFactor * 0.05, now, 0.8); |
| filterNode.frequency.setTargetAtTime(5000 - chaosFactor * 1500, now, 0.8); |
| filterNode.Q.setTargetAtTime(2 + chaosFactor * 2, now, 0.8); |
| delayNode.delayTime.setTargetAtTime(0.4 + chaosFactor * 0.15, now, 0.8); |
| feedbackGain.gain.setTargetAtTime(0.35 + chaosFactor * 0.15, now, 0.8); |
| waveShaperNode.curve = makeDistortionCurve(5 + chaosFactor * 15); |
| convolverNode.buffer = createImpulseResponse(2 + chaosFactor, 2); |
| |
| |
| lfos.push(createLFO(gainNode.gain, 0.12, 0.82, 0.92, 'sine')); |
| lfos.push(createLFO(filterNode.frequency, 0.06, 3500, 6000, 'triangle')); |
| break; |
| |
| case 3: |
| |
| gainNode.gain.setTargetAtTime(0.8 - chaosFactor * 0.1, now, 0.5); |
| filterNode.frequency.setTargetAtTime(3000 - chaosFactor * 1000, now, 0.5); |
| filterNode.Q.setTargetAtTime(4 + chaosFactor * 3, now, 0.5); |
| waveShaperNode.curve = makeDistortionCurve(20 + chaosFactor * 30); |
| convolverNode.buffer = createImpulseResponse(2.5, 1.5); |
| delayNode.delayTime.setTargetAtTime(0.45 + chaosFactor * 0.15, now, 0.5); |
| feedbackGain.gain.setTargetAtTime(0.45 + chaosFactor * 0.15, now, 0.5); |
| |
| |
| lfos.push(createLFO(gainNode.gain, 0.2, 0.65, 0.85, 'sine')); |
| lfos.push(createLFO(filterNode.frequency, 0.15, 2000, 4000, 'sawtooth')); |
| break; |
| |
| case 4: |
| |
| gainNode.gain.setTargetAtTime(0.7 - chaosFactor * 0.25, now, 0.3); |
| filterNode.frequency.setTargetAtTime(1800 - chaosFactor * 800, now, 0.3); |
| filterNode.Q.setTargetAtTime(6 + chaosFactor * 4, now, 0.3); |
| waveShaperNode.curve = makeDistortionCurve(40 + chaosFactor * 40); |
| delayNode.delayTime.setTargetAtTime(0.5 + chaosFactor * 0.25, now, 0.3); |
| feedbackGain.gain.setTargetAtTime(0.5 + chaosFactor * 0.25, now, 0.3); |
| convolverNode.buffer = createImpulseResponse(3, 1); |
| |
| |
| activateAuxiliarySources(1 + Math.floor(chaosFactor * 2), chaosFactor); |
| |
| lfos.push(createLFO(gainNode.gain, 0.3, 0.45, 0.75, 'sine')); |
| lfos.push(createLFO(filterNode.frequency, 0.2, 800, 2200, 'triangle')); |
| break; |
| |
| case 5: |
| |
| gainNode.gain.setTargetAtTime(0.5 - chaosFactor * 0.15, now, 0.2); |
| filterNode.frequency.setTargetAtTime(900 - chaosFactor * 300, now, 0.2); |
| filterNode.Q.setTargetAtTime(8 + chaosFactor * 2, now, 0.2); |
| waveShaperNode.curve = makeDistortionCurve(80 + chaosFactor * 70); |
| delayNode.delayTime.setTargetAtTime(0.55 + chaosFactor * 0.35, now, 0.2); |
| feedbackGain.gain.setTargetAtTime(0.65, now, 0.2); |
| convolverNode.buffer = createImpulseResponse(3.5, 0.8); |
| |
| |
| activateAuxiliarySources(2 + Math.floor(chaosFactor), chaosFactor); |
| |
| |
| noiseGain.gain.setTargetAtTime(0.04 + chaosFactor * 0.08, now, 0.5); |
| |
| lfos.push(createLFO(gainNode.gain, 0.5, 0.3, 0.55, 'sawtooth')); |
| lfos.push(createLFO(filterNode.frequency, 0.3, 400, 1100, 'square')); |
| break; |
| |
| case 6: |
| |
| gainNode.gain.setTargetAtTime(0.3 - chaosFactor * 0.15, now, 1); |
| filterNode.frequency.setTargetAtTime(100, now, 2); |
| filterNode.Q.setTargetAtTime(10, now, 2); |
| waveShaperNode.curve = makeDistortionCurve(150); |
| masterGain.gain.setTargetAtTime(0.6 - chaosFactor * 0.2, now, 2); |
| |
| |
| droneGain.gain.setTargetAtTime(0.12 + chaosFactor * 0.08, now, 3); |
| droneOscillator.frequency.setTargetAtTime(55 * (1 - chaosFactor * 0.15), now, 1); |
| |
| |
| noiseGain.gain.setTargetAtTime(0.12, now, 2); |
| |
| |
| if (sourceNode && sourceNode.playbackRate) { |
| sourceNode.playbackRate.setTargetAtTime(0.08 + Math.random() * 0.08, now, 2); |
| } |
| |
| |
| clearInterval(ghostNotesInterval); |
| ghostNotesInterval = setInterval(() => { |
| if (Math.random() < 0.08) { |
| triggerGhostNote(chaosFactor); |
| } |
| }, 2500); |
| |
| |
| deactivateAuxiliarySources(); |
| |
| |
| scheduleStage6End(); |
| break; |
| } |
| } |
| |
| function activateAuxiliarySources(count, chaos) { |
| for (let i = 0; i < count && i < auxiliaryPool.length; i++) { |
| if (!auxiliaryPool[i].inUse && audioBuffer && audioContext) { |
| const auxSource = audioContext.createBufferSource(); |
| auxSource.buffer = audioBuffer; |
| |
| const startOffset = Math.random() * audioBuffer.duration; |
| auxSource.start(0, startOffset); |
| |
| const pitchMod = 0.5 + Math.random() * 0.8; |
| auxSource.playbackRate.value = pitchMod; |
| |
| const auxGain = audioContext.createGain(); |
| auxGain.gain.value = 0.25 + Math.random() * 0.2; |
| |
| const auxFilter = audioContext.createBiquadFilter(); |
| auxFilter.type = 'lowpass'; |
| auxFilter.frequency.value = 1500 + Math.random() * 2500; |
| auxFilter.Q.value = 2 + Math.random() * 3; |
| |
| auxSource.connect(auxGain); |
| aux |