anycoder-c0ef4ef5 / index.html
DiamanteAmarelo's picture
Upload folder using huggingface_hub
6d682bf verified
Raw
History Blame
44.8 kB
<!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>
// ==================== SERVICE WORKER ====================
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;
});
})
);
});
`;
// ==================== INDEXEDDB ====================
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);
});
}
// ==================== CACHE API ====================
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');
}
}
// ==================== AUDIO ENGINE ====================
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;
// Auxiliary source pool for Stage 4+
let auxiliarySources = [];
let auxiliaryPool = [];
const POOL_SIZE = 3;
// LFO oscillators
let lfos = [];
let noiseSource = null;
let noiseGain = null;
let droneOscillator = null;
let droneGain = null;
let ghostNotesInterval = null;
// State
let currentStage = 0;
let isPlaying = false;
let startTime = 0;
let pauseTime = 0;
let chaosLevel = 0;
let lastStageTime = 0;
let stageTransitionDuration = 3;
// Stage descriptions
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;
}
// Create impulse response for reverb
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;
}
// Create waveshaper curve for distortion
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;
}
// Create pink noise
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;
// Main source
sourceNode = ctx.createBufferSource();
sourceNode.buffer = audioBuffer;
sourceNode.loop = true;
// Main processing chain
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';
// Analyser
analyserNode = ctx.createAnalyser();
analyserNode.fftSize = 2048;
// Master gain
masterGain = ctx.createGain();
masterGain.gain.value = 1;
// Connect main chain
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);
// Initialize auxiliary source pool
for (let i = 0; i < POOL_SIZE; i++) {
auxiliaryPool.push({
source: null,
inUse: false
});
}
// Setup noise for stages 5-6
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();
// Setup drone for stage 6
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();
}
// LFO management
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);
// Set initial value
targetParam.setValueAtTime((min + max) / 2, audioContext.currentTime);
lfo.start();
return { lfo, lfoGain, targetParam, min, max };
}
function updateLFOsForStage(stage, chaos) {
// Clear existing LFOs
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:
// Pure audio - minimal processing
break;
case 1:
// Stage 1: Subtle warmth, slight blur
gainNode.gain.setTargetAtTime(0.95, audioContext.currentTime, 1);
filterNode.frequency.setTargetAtTime(8000, audioContext.currentTime, 2);
convolverNode.buffer = createImpulseResponse(1.5, 3);
break;
case 2:
// Stage 2: Fragmentation begins
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);
// Add subtle LFOs
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:
// Stage 3: Desperate lucidity
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:
// Stage 4: Collapse of linearity - chaos injection
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);
// Activate auxiliary sources for overlapping fragments
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:
// Stage 5: Agony, horror, disorientation
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);
// More auxiliary sources
activateAuxiliarySources(2, chaosFactor);
// Increase noise
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:
// Stage 6: Dissolution - drone, pink noise, ghost notes
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); // -6dB
// Drone
droneGain.gain.setTargetAtTime(0.15, audioContext.currentTime, 3);
droneOscillator.frequency.setTargetAtTime(55 * (1 - chaosFactor * 0.1), audioContext.currentTime, 1);
// Pink noise as floor
noiseGain.gain.setTargetAtTime(0.15, audioContext.currentTime, 2);
// Slow playback for main source
if (sourceNode.playbackRate) {
sourceNode.playbackRate.setTargetAtTime(0.12, audioContext.currentTime, 3);
}
// Ghost notes - sporadic triggering
clearInterval(ghostNotesInterval);
ghostNotesInterval = setInterval(() => {
if (Math.random() < 0.1) {
triggerGhostNote(chaosFactor);
}
}, 2000);
// Reset auxiliary
deactivateAuxiliarySources();
// Schedule final fade
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;
// Random start position
const startOffset = Math.random() * audioBuffer.duration;
auxSource.start(0, startOffset);
// Modulated playback rate
const pitchMod = 0.65 + Math.random() * 0.7; // 0.65 to 1.35
auxSource.playbackRate.value = pitchMod;
// Individual gain
const auxGain = audioContext.createGain();
auxGain.gain.value = 0.3 + Math.random() * 0.2;
// Connect through filter
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
};
// Store in active array
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);
// Extremely slow fade in/out
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;
// 6 minutes fade
const fadeDuration = 360; // seconds
const now = audioContext.currentTime;
masterGain.gain.setValueAtTime(masterGain.gain.value, now);
masterGain.gain.linearRampToValueAtTime(0, now + fadeDuration);
// After fade, 1 minute silence then restart
setTimeout(() => {
// Stop everything
try { sourceNode.stop(); } catch(e) {}
deactivateAuxiliarySources();
clearInterval(ghostNotesInterval);
// 1 minute silence
setTimeout(() => {
// Soft restart
stage6EndScheduled = false;
if (audioBuffer) {
playAudio(0);
changeStage(0);
}
}, 60000);
}, fadeDuration * 1000);
}
function changeStage(stage) {
currentStage = stage;
lastStageTime = Date.now();
// Update UI
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);
});
// Update audio processing
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);
// Save to IndexedDB
await saveAudioToDB('current-audio', arrayBuffer, file.name);
// Cache it
await cacheAudioFile(file);
document.getElementById('status').textContent = `Carregado: ${file.name}`;
document.getElementById('status').className = 'status success';
document.getElementById('duration').textContent = formatTime(audioBuffer.duration);
// Enable controls
document.getElementById('playBtn').disabled = false;
document.getElementById('pauseBtn').disabled = false;
document.getElementById('restartBtn').disabled = false;
// Create audio graph
createAudioGraph();
// Reset stage
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();
}
// Stop existing source
if (sourceNode) {
try { sourceNode.stop(); } catch(e) {}
}
// Create new source
sourceNode = ctx.createBufferSource();
sourceNode.buffer = audioBuffer;
sourceNode.loop = true;
// Reconnect
sourceNode.connect(gainNode);
// Start from offset
sourceNode.start(0, offset);
startTime = ctx.currentTime - offset;
isPlaying = true;
// Update processing
updateLFOsForStage(currentStage, chaosLevel);
// Start visualization
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);
}
// ==================== VISUALIZATION ====================
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;
// Clear with background
ctx.fillStyle = '#111111';
ctx.fillRect(0, 0, width, height);
// Draw waveform
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();
// Update progress
if (isPlaying && audioBuffer) {
updateProgress(getCurrentTime());
}
}
// ==================== EVENT HANDLERS ====================
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);
});
});
// ==================== OFFLINE DETECTION ====================
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();
// ==================== INITIALIZATION ====================
async function init() {
try {
// Open database
await openDatabase();
// Try to load cached audio
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('