| <!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; |
| --border: #1a1a1a; |
| --text: #e0e0e0; |
| --text-dim: #666666; |
| --accent: #888888; |
| --accent-dim: #444444; |
| } |
| |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: 'Courier New', monospace; |
| background: var(--bg); |
| color: var(--text); |
| min-height: 100vh; |
| display: flex; |
| flex-direction: column; |
| overflow-x: hidden; |
| } |
| |
| header { |
| padding: 1rem; |
| text-align: center; |
| border-bottom: 1px solid var(--border); |
| } |
| |
| header h1 { |
| font-size: 0.9rem; |
| font-weight: 300; |
| letter-spacing: 0.3em; |
| text-transform: uppercase; |
| color: var(--text-dim); |
| } |
| |
| header a { |
| color: var(--text-dim); |
| text-decoration: none; |
| font-size: 0.7rem; |
| letter-spacing: 0.1em; |
| transition: color 0.3s; |
| } |
| |
| header a:hover { |
| color: var(--accent); |
| } |
| |
| .stage-title { |
| text-align: center; |
| padding: 1.5rem 1rem; |
| min-height: 80px; |
| } |
| |
| .stage-title h2 { |
| font-size: 1rem; |
| font-weight: 400; |
| letter-spacing: 0.2em; |
| transition: opacity 0.5s; |
| } |
| |
| .stage-title p { |
| font-size: 0.7rem; |
| color: var(--text-dim); |
| margin-top: 0.5rem; |
| max-width: 300px; |
| margin-left: auto; |
| margin-right: auto; |
| line-height: 1.6; |
| } |
| |
| main { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| padding: 1rem; |
| gap: 1.5rem; |
| } |
| |
| .visualizer-container { |
| width: 100%; |
| height: 100px; |
| background: var(--surface); |
| border: 1px solid var(--border); |
| position: relative; |
| overflow: hidden; |
| } |
| |
| #visualizer { |
| width: 100%; |
| height: 100%; |
| display: block; |
| } |
| |
| .controls { |
| display: flex; |
| flex-direction: column; |
| gap: 1rem; |
| } |
| |
| .file-input-container { |
| position: relative; |
| overflow: hidden; |
| display: inline-block; |
| width: 100%; |
| } |
| |
| .file-input-container input[type="file"] { |
| position: absolute; |
| left: 0; |
| top: 0; |
| opacity: 0; |
| width: 100%; |
| height: 100%; |
| cursor: pointer; |
| } |
| |
| .btn { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| color: var(--text); |
| padding: 1rem; |
| font-family: inherit; |
| font-size: 0.8rem; |
| letter-spacing: 0.15em; |
| text-transform: uppercase; |
| cursor: pointer; |
| transition: all 0.3s; |
| width: 100%; |
| text-align: center; |
| } |
| |
| .btn:hover:not(:disabled) { |
| border-color: var(--accent); |
| background: var(--border); |
| } |
| |
| .btn:disabled { |
| opacity: 0.3; |
| cursor: not-allowed; |
| } |
| |
| .btn.active { |
| background: var(--accent-dim); |
| border-color: var(--accent); |
| } |
| |
| .playback-controls { |
| display: flex; |
| gap: 0.5rem; |
| } |
| |
| .playback-controls .btn { |
| flex: 1; |
| } |
| |
| .progress-container { |
| width: 100%; |
| } |
| |
| .progress-bar { |
| width: 100%; |
| height: 4px; |
| background: var(--border); |
| cursor: pointer; |
| position: relative; |
| } |
| |
| .progress-fill { |
| height: 100%; |
| background: var(--accent); |
| width: 0%; |
| transition: width 0.1s linear; |
| } |
| |
| .time-display { |
| display: flex; |
| justify-content: space-between; |
| font-size: 0.65rem; |
| color: var(--text-dim); |
| margin-top: 0.5rem; |
| font-variant-numeric: tabular-nums; |
| } |
| |
| .stages-container { |
| display: grid; |
| grid-template-columns: repeat(7, 1fr); |
| gap: 0.25rem; |
| } |
| |
| .stage-btn { |
| padding: 0.75rem 0.25rem; |
| font-size: 0.65rem; |
| background: var(--surface); |
| border: 1px solid var(--border); |
| color: var(--text-dim); |
| cursor: pointer; |
| transition: all 0.3s; |
| text-align: center; |
| } |
| |
| .stage-btn:hover { |
| border-color: var(--accent-dim); |
| } |
| |
| .stage-btn.active { |
| background: var(--accent-dim); |
| border-color: var(--accent); |
| color: var(--text); |
| } |
| |
| .stage-btn[data-stage="0"] { border-left: 2px solid var(--accent); } |
| .stage-btn[data-stage="6"] { border-right: 2px solid var(--accent); } |
| |
| .chaos-control { |
| display: flex; |
| align-items: center; |
| gap: 1rem; |
| padding: 0.5rem; |
| background: var(--surface); |
| border: 1px solid var(--border); |
| } |
| |
| .chaos-control label { |
| font-size: 0.7rem; |
| color: var(--text-dim); |
| white-space: nowrap; |
| } |
| |
| .chaos-control input[type="range"] { |
| flex: 1; |
| -webkit-appearance: none; |
| background: var(--border); |
| height: 2px; |
| outline: none; |
| } |
| |
| .chaos-control input[type="range"]::-webkit-slider-thumb { |
| -webkit-appearance: none; |
| width: 12px; |
| height: 12px; |
| background: var(--accent); |
| cursor: pointer; |
| } |
| |
| .status { |
| text-align: center; |
| font-size: 0.65rem; |
| color: var(--text-dim); |
| padding: 0.5rem; |
| } |
| |
| .status.error { |
| color: #884444; |
| } |
| |
| .status.success { |
| color: #448844; |
| } |
| |
| .offline-badge { |
| position: fixed; |
| bottom: 1rem; |
| right: 1rem; |
| background: var(--surface); |
| border: 1px solid var(--border); |
| padding: 0.5rem; |
| font-size: 0.6rem; |
| color: var(--text-dim); |
| opacity: 0; |
| transition: opacity 0.3s; |
| } |
| |
| .offline-badge.visible { |
| opacity: 1; |
| } |
| |
| @media (max-width: 400px) { |
| .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; |
| } |
| } |
| </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-title"> |
| <h2 id="stageName">Estágio 0</h2> |
| <p id="stageDesc">Áudio puro</p> |
| </div> |
|
|
| <main> |
| <div class="visualizer-container"> |
| <canvas id="visualizer"></canvas> |
| </div> |
|
|
| <div class="controls"> |
| <div class="file-input-container"> |
| <button class="btn" id="loadBtn">Carregar Áudio</button> |
| <input type="file" id="fileInput" accept=".flac,.mp3,.wav,.ogg,.m4a,.aac"> |
| </div> |
|
|
| <div class="playback-controls"> |
| <button class="btn" id="playBtn" disabled>Play</button> |
| <button class="btn" id="pauseBtn" disabled>Pausa</button> |
| <button class="btn" id="restartBtn" disabled>Reiniciar</button> |
| </div> |
|
|
| <div class="progress-container"> |
| <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>Entrada/Saída</label> |
| <input type="range" id="chaosSlider" min="0" max="100" value="0"> |
| </div> |
|
|
| <div class="stages-container"> |
| <button class="stage-btn active" data-stage="0">0</button> |
| <button class="stage-btn" data-stage="1">1</button> |
| <button class="stage-btn" data-stage="2">2</button> |
| <button class="stage-btn" data-stage="3">3</button> |
| <button class="stage-btn" data-stage="4">4</button> |
| <button class="stage-btn" data-stage="5">5</button> |
| <button class="stage-btn" data-stage="6">6</button> |
| </div> |
| </div> |
|
|
| <div class="status" id="status">Aguardando arquivo de áudio...</div> |
| </main> |
|
|
| <div class="offline-badge" id="offlineBadge">Modo Offline</div> |
|
|
| <script> |
| |
| const swCode = ` |
| const CACHE_NAME = 'everywhere-v1'; |
| 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 = 1; |
| 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 cacheAudioFile(file) { |
| try { |
| const cache = await caches.open('audio-cache-v1'); |
| const response = new Response(file, { |
| headers: { 'Content-Type': file.type } |
| }); |
| await cache.put('/cached-audio', response); |
| } catch (e) { |
| console.log('Cache API not available'); |
| } |
| } |
| |
| |
| 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 = 3; |
| |
| |
| 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 stageTransitionDuration = 3; |
| |
| |
| const stageData = [ |
| { name: 'Estágio 0', desc: 'Áudio puro' }, |
| { 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.' }, |
| { 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.' }, |
| { 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.' }, |
| { name: 'Estágio 4', desc: 'A ruptura final da consciência, transformando memórias em ruídos caóticos e desoladores.' }, |
| { 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.' }, |
| { name: 'Estágio 6', desc: 'A dissolução completa do self em silêncio e caos ambientais, marcando o fim definitivo da memória.' } |
| ]; |
| |
| function initAudioContext() { |
| if (!audioContext) { |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
| } |
| return audioContext; |
| } |
| |
| |
| function createImpulseResponse(duration, decay) { |
| const sampleRate = audioContext.sampleRate; |
| const length = sampleRate * duration; |
| const impulse = audioContext.createBuffer(2, length, sampleRate); |
| |
| for (let channel = 0; channel < 2; 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 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 = 1; |
| |
| 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; |
| |
| |
| 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, |
| inUse: false |
| }); |
| } |
| |
| |
| const noiseBuffer = createPinkNoise(); |
| noiseSource = ctx.createBufferSource(); |
| noiseSource.buffer = noiseBuffer; |
| noiseSource.loop = true; |
| noiseGain = ctx.createGain(); |
| noiseGain.gain.value = 0; |
| noiseSource.connect(noiseGain); |
| noiseGain.connect(masterGain); |
| noiseSource.start(); |
| |
| |
| droneOscillator = ctx.createOscillator(); |
| droneOscillator.type = 'sine'; |
| droneOscillator.frequency.value = 55; |
| droneGain = ctx.createGain(); |
| droneGain.gain.value = 0; |
| droneOscillator.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; |
| |
| lfo.connect(lfoGain); |
| lfoGain.connect(targetParam); |
| |
| |
| targetParam.setValueAtTime((min + max) / 2, audioContext.currentTime); |
| |
| lfo.start(); |
| |
| return { lfo, lfoGain, targetParam, min, max }; |
| } |
| |
| function updateLFOsForStage(stage, chaos) { |
| |
| lfos.forEach(lfo => { |
| try { lfo.lfo.stop(); } catch(e) {} |
| }); |
| lfos = []; |
| |
| if (!audioBuffer) return; |
| |
| const bufferDuration = audioBuffer.duration; |
| const chaosFactor = chaos / 100; |
| |
| switch(stage) { |
| case 0: |
| |
| break; |
| |
| case 1: |
| |
| gainNode.gain.setTargetAtTime(0.95, audioContext.currentTime, 1); |
| filterNode.frequency.setTargetAtTime(8000, audioContext.currentTime, 2); |
| convolverNode.buffer = createImpulseResponse(1.5, 3); |
| break; |
| |
| case 2: |
| |
| gainNode.gain.setTargetAtTime(0.9, audioContext.currentTime, 1); |
| filterNode.frequency.setTargetAtTime(4000 + Math.random() * 2000, audioContext.currentTime, 1); |
| filterNode.Q.setTargetAtTime(2, audioContext.currentTime, 1); |
| delayNode.delayTime.setTargetAtTime(0.4 + Math.random() * 0.2, audioContext.currentTime, 1); |
| feedbackGain.gain.setTargetAtTime(0.4, audioContext.currentTime, 1); |
| |
| |
| lfos.push(createLFO(gainNode.gain, 0.1 + Math.random() * 0.1, 0.85, 0.95, 'sine')); |
| lfos.push(createLFO(filterNode.frequency, 0.05, 3000, 6000, 'triangle')); |
| break; |
| |
| case 3: |
| |
| gainNode.gain.setTargetAtTime(0.85, audioContext.currentTime, 0.5); |
| filterNode.frequency.setTargetAtTime(2500 + Math.random() * 1500, audioContext.currentTime, 0.5); |
| filterNode.Q.setTargetAtTime(4, audioContext.currentTime, 0.5); |
| waveShaperNode.curve = makeDistortionCurve(10 + chaosFactor * 20); |
| convolverNode.buffer = createImpulseResponse(2.5, 1.5); |
| |
| lfos.push(createLFO(gainNode.gain, 0.2, 0.7, 0.9, 'sine')); |
| lfos.push(createLFO(filterNode.frequency, 0.15, 2000, 4000, 'sawtooth')); |
| lfos.push(createLFO(waveShaperNode.curve ? waveShaperNode : { setValueAtTime: () => {} }, 0.08, 5, 30, 'square')); |
| break; |
| |
| case 4: |
| |
| gainNode.gain.setTargetAtTime(0.7 - chaosFactor * 0.3, audioContext.currentTime, 0.3); |
| filterNode.frequency.setTargetAtTime(1500 * (1 - chaosFactor * 0.5), audioContext.currentTime, 0.3); |
| filterNode.Q.setTargetAtTime(6 + chaosFactor * 4, audioContext.currentTime, 0.3); |
| waveShaperNode.curve = makeDistortionCurve(30 + chaosFactor * 50); |
| delayNode.delayTime.setTargetAtTime(0.5 + chaosFactor * 0.3, audioContext.currentTime, 0.2); |
| feedbackGain.gain.setTargetAtTime(0.5 + chaosFactor * 0.3, audioContext.currentTime, 0.2); |
| |
| |
| activateAuxiliarySources(1 + Math.floor(chaosFactor * 2), chaosFactor); |
| |
| lfos.push(createLFO(gainNode.gain, 0.3 + Math.random() * 0.3, 0.5, 0.8, 'sine')); |
| lfos.push(createLFO(filterNode.frequency, 0.2, 800, 2000, 'triangle')); |
| break; |
| |
| case 5: |
| |
| gainNode.gain.setTargetAtTime(0.5 - chaosFactor * 0.2, audioContext.currentTime, 0.2); |
| filterNode.frequency.setTargetAtTime(800 * (1 - chaosFactor * 0.3), audioContext.currentTime, 0.2); |
| filterNode.Q.setTargetAtTime(8, audioContext.currentTime, 0.2); |
| waveShaperNode.curve = makeDistortionCurve(80 + chaosFactor * 70); |
| delayNode.delayTime.setTargetAtTime(0.6 + chaosFactor * 0.4, audioContext.currentTime, 0.15); |
| feedbackGain.gain.setTargetAtTime(0.7, audioContext.currentTime, 0.15); |
| |
| |
| activateAuxiliarySources(2, chaosFactor); |
| |
| |
| noiseGain.gain.setTargetAtTime(0.05 + chaosFactor * 0.1, audioContext.currentTime, 0.5); |
| |
| lfos.push(createLFO(gainNode.gain, 0.5, 0.3, 0.6, 'sawtooth')); |
| lfos.push(createLFO(filterNode.frequency, 0.3, 400, 1200, 'square')); |
| break; |
| |
| case 6: |
| |
| gainNode.gain.setTargetAtTime(0.3 - chaosFactor * 0.2, audioContext.currentTime, 1); |
| filterNode.frequency.setTargetAtTime(110, audioContext.currentTime, 2); |
| filterNode.Q.setTargetAtTime(10, audioContext.currentTime, 2); |
| waveShaperNode.curve = makeDistortionCurve(150); |
| masterGain.gain.setTargetAtTime(0.5, audioContext.currentTime, 2); |
| |
| |
| droneGain.gain.setTargetAtTime(0.15, audioContext.currentTime, 3); |
| droneOscillator.frequency.setTargetAtTime(55 * (1 - chaosFactor * 0.1), audioContext.currentTime, 1); |
| |
| |
| noiseGain.gain.setTargetAtTime(0.15, audioContext.currentTime, 2); |
| |
| |
| if (sourceNode.playbackRate) { |
| sourceNode.playbackRate.setTargetAtTime(0.12, audioContext.currentTime, 3); |
| } |
| |
| |
| clearInterval(ghostNotesInterval); |
| ghostNotesInterval = setInterval(() => { |
| if (Math.random() < 0.1) { |
| triggerGhostNote(chaosFactor); |
| } |
| }, 2000); |
| |
| |
| deactivateAuxiliarySources(); |
| |
| |
| scheduleStage6End(); |
| break; |
| } |
| } |
| |
| function activateAuxiliarySources(count, chaos) { |
| for (let i = 0; i < count && i < auxiliaryPool.length; i++) { |
| if (!auxiliaryPool[i].inUse && audioBuffer) { |
| const auxSource = audioContext.createBufferSource(); |
| auxSource.buffer = audioBuffer; |
| |
| |
| const startOffset = Math.random() * audioBuffer.duration; |
| auxSource.start(0, startOffset); |
| |
| |
| const pitchMod = 0.65 + Math.random() * 0.7; |
| auxSource.playbackRate.value = pitchMod; |
| |
| |
| const auxGain = audioContext.createGain(); |
| auxGain.gain.value = 0.3 + Math.random() * 0.2; |
| |
| |
| const auxFilter = audioContext.createBiquadFilter(); |
| auxFilter.type = 'lowpass'; |
| auxFilter.frequency.value = 2000 + Math.random() * 3000; |
| |
| auxSource.connect(auxGain); |
| auxGain.connect(auxFilter); |
| auxFilter.connect(analyserNode); |
| |
| auxiliaryPool[i] = { |
| source: auxSource, |
| gain: auxGain, |
| filter: auxFilter, |
| inUse: true |
| }; |
| |
| |
| auxiliarySources.push(auxiliaryPool[i]); |
| } |
| } |
| } |
| |
| function deactivateAuxiliarySources() { |
| auxiliarySources.forEach(aux => { |
| if (aux && aux.gain) { |
| aux.gain.gain.setTargetAtTime(0, audioContext.currentTime, 0.5); |
| setTimeout(() => { |
| try { aux.source.stop(); } catch(e) {} |
| }, 2000); |
| } |
| }); |
| auxiliarySources = []; |
| |
| auxiliaryPool.forEach(poolItem => { |
| poolItem.inUse = false; |
| }); |
| } |
| |
| function triggerGhostNote(chaos) { |
| if (!audioBuffer) return; |
| |
| const ghostSource = audioContext.createBufferSource(); |
| ghostSource.buffer = audioBuffer; |
| |
| const ghostGain = audioContext.createGain(); |
| ghostGain.gain.value = 0; |
| |
| const ghostFilter = audioContext.createBiquadFilter(); |
| ghostFilter.type = 'bandpass'; |
| ghostFilter.frequency.value = 300 + Math.random() * 500; |
| ghostFilter.Q.value = 10; |
| |
| ghostSource.connect(ghostGain); |
| ghostGain.connect(ghostFilter); |
| ghostFilter.connect(masterGain); |
| |
| const startTime = Math.random() * (audioBuffer.duration - 2); |
| ghostSource.start(0, startTime, 4); |
| |
| |
| const now = audioContext.currentTime; |
| ghostGain.gain.setValueAtTime(0, now); |
| ghostGain.gain.linearRampToValueAtTime(0.2, now + 2); |
| ghostGain.gain.linearRampToValueAtTime(0, now + 4); |
| |
| setTimeout(() => { |
| try { ghostSource.stop(); } catch(e) {} |
| }, 4500); |
| } |
| |
| let stage6EndScheduled = false; |
| |
| function scheduleStage6End() { |
| if (stage6EndScheduled) return; |
| stage6EndScheduled = true; |
| |
| |
| const fadeDuration = 360; |
| const now = audioContext.currentTime; |
| |
| masterGain.gain.setValueAtTime(masterGain.gain.value, now); |
| masterGain.gain.linearRampToValueAtTime(0, now + fadeDuration); |
| |
| |
| setTimeout(() => { |
| |
| try { sourceNode.stop(); } catch(e) {} |
| deactivateAuxiliarySources(); |
| clearInterval(ghostNotesInterval); |
| |
| |
| setTimeout(() => { |
| |
| stage6EndScheduled = false; |
| if (audioBuffer) { |
| playAudio(0); |
| changeStage(0); |
| } |
| }, 60000); |
| |
| }, fadeDuration * 1000); |
| } |
| |
| function changeStage(stage) { |
| currentStage = stage; |
| lastStageTime = Date.now(); |
| |
| |
| document.getElementById('stageName').textContent = stageData[stage].name; |
| document.getElementById('stageDesc').textContent = stageData[stage].desc; |
| |
| document.querySelectorAll('.stage-btn').forEach(btn => { |
| btn.classList.toggle('active', parseInt(btn.dataset.stage) === stage); |
| }); |
| |
| |
| if (audioContext && audioBuffer) { |
| updateLFOsForStage(stage, chaosLevel); |
| } |
| } |
| |
| async function loadAudioFile(file) { |
| const ctx = initAudioContext(); |
| |
| try { |
| document.getElementById('status').textContent = 'Carregando áudio...'; |
| document.getElementById('status').className = 'status'; |
| |
| const arrayBuffer = await file.arrayBuffer(); |
| audioBuffer = await ctx.decodeAudioData(arrayBuffer); |
| |
| |
| await saveAudioToDB('current-audio', arrayBuffer, file.name); |
| |
| |
| await cacheAudioFile(file); |
| |
| document.getElementById('status').textContent = `Carregado: ${file.name}`; |
| document.getElementById('status').className = 'status success'; |
| |
| document.getElementById('duration').textContent = formatTime(audioBuffer.duration); |
| |
| |
| document.getElementById('playBtn').disabled = false; |
| document.getElementById('pauseBtn').disabled = false; |
| document.getElementById('restartBtn').disabled = false; |
| |
| |
| createAudioGraph(); |
| |
| |
| changeStage(0); |
| |
| } catch (error) { |
| console.error('Error loading audio:', error); |
| document.getElementById('status').textContent = 'Erro ao carregar áudio: ' + error.message; |
| document.getElementById('status').className = 'status error'; |
| } |
| } |
| |
| function playAudio(offset = 0) { |
| if (!audioBuffer) return; |
| |
| const ctx = audioContext; |
| |
| if (ctx.state === 'suspended') { |
| ctx.resume(); |
| } |
| |
| |
| if (sourceNode) { |
| try { sourceNode.stop(); } catch(e) {} |
| } |
| |
| |
| sourceNode = ctx.createBufferSource(); |
| sourceNode.buffer = audioBuffer; |
| sourceNode.loop = true; |
| |
| |
| sourceNode.connect(gainNode); |
| |
| |
| sourceNode.start(0, offset); |
| startTime = ctx.currentTime - offset; |
| isPlaying = true; |
| |
| |
| updateLFOsForStage(currentStage, chaosLevel); |
| |
| |
| drawVisualization(); |
| } |
| |
| function pauseAudio() { |
| if (sourceNode && isPlaying) { |
| try { sourceNode.stop(); } catch(e) {} |
| pauseTime = audioContext.currentTime - startTime; |
| isPlaying = false; |
| } |
| } |
| |
| function stopAudio() { |
| if (sourceNode) { |
| try { sourceNode.stop(); } catch(e) {} |
| } |
| deactivateAuxiliarySources(); |
| isPlaying = false; |
| pauseTime = 0; |
| updateProgress(0); |
| } |
| |
| function getCurrentTime() { |
| if (!audioContext || !isPlaying) return pauseTime; |
| return audioContext.currentTime - startTime; |
| } |
| |
| function formatTime(seconds) { |
| const mins = Math.floor(seconds / 60); |
| const secs = Math.floor(seconds % 60); |
| return `${mins}:${secs.toString().padStart(2, '0')}`; |
| } |
| |
| function updateProgress(time) { |
| if (!audioBuffer) return; |
| |
| const percent = (time / audioBuffer.duration) * 100; |
| document.getElementById('progressFill').style.width = `${percent}%`; |
| document.getElementById('currentTime').textContent = formatTime(time); |
| } |
| |
| |
| const canvas = document.getElementById('visualizer'); |
| const ctx = canvas.getContext('2d'); |
| |
| function resizeCanvas() { |
| const rect = canvas.parentElement.getBoundingClientRect(); |
| canvas.width = rect.width * window.devicePixelRatio; |
| canvas.height = rect.height * window.devicePixelRatio; |
| ctx.scale(window.devicePixelRatio, window.devicePixelRatio); |
| } |
| |
| resizeCanvas(); |
| window.addEventListener('resize', resizeCanvas); |
| |
| function drawVisualization() { |
| if (!analyserNode) return; |
| |
| requestAnimationFrame(drawVisualization); |
| |
| const bufferLength = analyserNode.frequencyBinCount; |
| const dataArray = new Uint8Array(bufferLength); |
| analyserNode.getByteTimeDomainData(dataArray); |
| |
| const width = canvas.width / window.devicePixelRatio; |
| const height = canvas.height / window.devicePixelRatio; |
| |
| |
| ctx.fillStyle = '#111111'; |
| ctx.fillRect(0, 0, width, height); |
| |
| |
| ctx.lineWidth = 1.5; |
| ctx.strokeStyle = '#888888'; |
| ctx.beginPath(); |
| |
| const sliceWidth = width / bufferLength; |
| let x = 0; |
| |
| for (let i = 0; i < bufferLength; i++) { |
| const v = dataArray[i] / 128.0; |
| const y = (v * height) / 2; |
| |
| if (i === 0) { |
| ctx.moveTo(x, y); |
| } else { |
| ctx.lineTo(x, y); |
| } |
| |
| x += sliceWidth; |
| } |
| |
| ctx.lineTo(width, height / 2); |
| ctx.stroke(); |
| |
| |
| if (isPlaying && audioBuffer) { |
| updateProgress(getCurrentTime()); |
| } |
| } |
| |
| |
| document.getElementById('fileInput').addEventListener('change', async (e) => { |
| const file = e.target.files[0]; |
| if (file) { |
| await loadAudioFile(file); |
| } |
| }); |
| |
| document.getElementById('playBtn').addEventListener('click', () => { |
| if (!audioContext) initAudioContext(); |
| playAudio(pauseTime); |
| }); |
| |
| document.getElementById('pauseBtn').addEventListener('click', pauseAudio); |
| |
| document.getElementById('restartBtn').addEventListener('click', () => { |
| stopAudio(); |
| if (audioBuffer) playAudio(0); |
| changeStage(0); |
| }); |
| |
| document.getElementById('progressBar').addEventListener('click', (e) => { |
| if (!audioBuffer) return; |
| |
| const rect = e.target.getBoundingClientRect(); |
| const percent = (e.clientX - rect.left) / rect.width; |
| const newTime = percent * audioBuffer.duration; |
| |
| if (isPlaying) { |
| pauseTime = newTime; |
| playAudio(newTime); |
| } else { |
| pauseTime = newTime; |
| updateProgress(newTime); |
| } |
| }); |
| |
| document.getElementById('chaosSlider').addEventListener('input', (e) => { |
| chaosLevel = parseInt(e.target.value); |
| if (audioBuffer && isPlaying) { |
| updateLFOsForStage(currentStage, chaosLevel); |
| } |
| }); |
| |
| document.querySelectorAll('.stage-btn').forEach(btn => { |
| btn.addEventListener('click', () => { |
| const stage = parseInt(btn.dataset.stage); |
| changeStage(stage); |
| }); |
| }); |
| |
| |
| function updateOnlineStatus() { |
| const badge = document.getElementById('offlineBadge'); |
| if (!navigator.onLine) { |
| badge.classList.add('visible'); |
| } else { |
| badge.classList.remove('visible'); |
| } |
| } |
| |
| window.addEventListener('online', updateOnlineStatus); |
| window.addEventListener('offline', updateOnlineStatus); |
| updateOnlineStatus(); |
| |
| |
| async function init() { |
| try { |
| |
| await openDatabase(); |
| |
| |
| const cached = await getAudioFromDB('current-audio'); |
| if (cached && cached.data) { |
| document.getElementById('status').textContent = 'Carregando áudio salvo...'; |
| document.getElementById('status').className = 'status'; |
| |
| const ctx = initAudioContext(); |
| audioBuffer = await ctx.decodeAudioData(cached.data); |
| |
| document.getElementById('status').textContent = `Carregado: ${cached.name}`; |
| document.getElementById('status').className = 'status success'; |
| document.getElementById('duration').textContent = formatTime(audioBuffer.duration); |
| |
| document.getElementById('playBtn').disabled = false; |
| document.getElementById('pauseBtn').disabled = false; |
| document.getElementById(' |