Compare commits

..

1 Commits

Author SHA1 Message Date
Timmy
93b8475405 fix(#1356): replace HTTPServer with ThreadingHTTPServer in multi-user bridge
Some checks failed
CI / test (pull_request) Failing after 2s
Review Approval Gate / verify-review (pull_request) Failing after 8s
CI / validate (pull_request) Failing after 40s
The single-threaded HTTPServer queues requests sequentially, causing
60% timeout under 10 concurrent users (30s timeout, 7.8s avg).

Fix: use ThreadingHTTPServer (already defined in multi_user_bridge.py)
for thread-per-request concurrency.

- multi_user_bridge.py: line 2883 HTTPServer -> ThreadingHTTPServer
- world/multi_user_bridge.py: add ThreadingMixIn import + class, fix server init

Refs #1356
2026-04-13 17:23:47 -04:00
8 changed files with 9 additions and 2405 deletions

View File

@@ -2880,7 +2880,7 @@ def main():
# Start world tick system
world_tick_system.start()
server = HTTPServer((BRIDGE_HOST, BRIDGE_PORT), BridgeHandler)
server = ThreadingHTTPServer((BRIDGE_HOST, BRIDGE_PORT), BridgeHandler)
server.serve_forever()

View File

@@ -1,80 +0,0 @@
# Sovereign Sound Playground
Interactive audio-visual experience — no servers, no dependencies, pure browser.
## Apps
### Playground v3 (Full Instrument)
`playground.html` — The complete experience:
- Visual piano keyboard (2 octaves)
- 6 visualization modes: Waveform, Particles, Bars, Spiral, Gravity Well, Strobe
- 5 color palettes: Cosmic, Sunset, Ocean, Forest, Neon
- Ambient beat with chord progressions
- Mouse/touch playback on visualizer
- Chord detection
- Recording to WAV
- Export as PNG
### Synesthesia
`synesthesia.html` — Paint with sound:
- Click and drag to create colors and shapes
- Each position generates a unique tone
- Particles respond to your movement
- Touch supported
### Ambient
`ambient.html` — Evolving soundscape:
- Automatic chord progressions
- Floating orbs respond to audio
- Reverb-drenched textures
- Click to enter, let it wash over you
### Interactive
`interactive.html` — 26 key-shape mappings:
- Press A-Z to play notes
- Each key has a unique shape and color
- Shapes animate and fade
- Visual keyboard at bottom
- Touch supported
### Visualizer
`visualizer.html` — WAV frequency visualization:
- Load any audio file
- 4 modes: Spectrum, Waveform, Spectrogram, Circular
- Drag and drop support
- Real-time frequency analysis
## Features
- Zero dependencies — just open in a browser
- Local-first — no network requests
- Touch support on all apps
- Keyboard support
- Recording and export
- Multiple visualization modes
- Color palettes
## Usage
Open any HTML file in a browser. That's it.
```bash
# Quick start
open playground/playground.html
# Or serve locally
python3 -m http.server 8080 --directory playground
```
## Keyboard Shortcuts (Playground v3)
- A-; (lower row): Play piano notes
- Mouse drag on visualizer: Create sound
- Click piano keys: Play notes
## Technical
- Web Audio API for sound generation
- Canvas 2D for visualization
- MediaRecorder for recording
- No build step, no framework

View File

@@ -1,243 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ambient — Evolving Soundscape</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a0f; overflow: hidden; height: 100vh; }
canvas { display: block; width: 100%; height: 100%; }
.overlay {
position: fixed; inset: 0; display: flex; align-items: center;
justify-content: center; background: rgba(0,0,0,0.7);
transition: opacity 0.5s; z-index: 10;
}
.overlay.hidden { opacity: 0; pointer-events: none; }
.start-btn {
background: rgba(100,50,150,0.5); border: 2px solid rgba(150,100,200,0.7);
color: #e0e0e0; padding: 20px 40px; font-size: 18px; border-radius: 30px;
cursor: pointer; font-family: sans-serif; letter-spacing: 2px;
transition: all 0.3s;
}
.start-btn:hover { background: rgba(150,100,200,0.5); transform: scale(1.05); }
.title {
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
color: rgba(255,255,255,0.1); font-size: 24px; font-family: sans-serif;
letter-spacing: 4px; text-transform: uppercase; pointer-events: none;
}
</style>
</head>
<body>
<div class="title">Ambient</div>
<canvas id="canvas"></canvas>
<div class="overlay" id="overlay">
<button class="start-btn" id="start-btn">Enter Soundscape</button>
</div>
<script>
class AmbientSoundscape {
constructor() {
this.canvas = document.getElementById('canvas');
this.ctx = this.canvas.getContext('2d');
this.audioCtx = null;
this.masterGain = null;
this.analyser = null;
this.isPlaying = false;
this.currentChord = 0;
this.time = 0;
this.orbs = [];
this.resize();
window.addEventListener('resize', () => this.resize());
// Create orbs
for (let i = 0; i < 15; i++) {
this.orbs.push({
x: Math.random() * this.canvas.width,
y: Math.random() * this.canvas.height,
radius: Math.random() * 50 + 20,
speed: Math.random() * 0.5 + 0.2,
angle: Math.random() * Math.PI * 2,
color: [
[167, 139, 250], [129, 140, 248], [99, 102, 241],
[139, 92, 246], [124, 58, 237]
][i % 5]
});
}
this.bindEvents();
this.animate();
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
async initAudio() {
if (this.audioCtx) return;
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
this.masterGain = this.audioCtx.createGain();
this.masterGain.gain.value = 0.2;
this.analyser = this.audioCtx.createAnalyser();
this.analyser.fftSize = 256;
this.analyser.smoothingTimeConstant = 0.95;
this.masterGain.connect(this.analyser);
this.analyser.connect(this.audioCtx.destination);
// Create reverb
const convolver = this.audioCtx.createConvolver();
const reverbTime = 3;
const sampleRate = this.audioCtx.sampleRate;
const length = sampleRate * reverbTime;
const impulse = this.audioCtx.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, 2);
}
}
convolver.buffer = impulse;
convolver.connect(this.masterGain);
this.reverb = convolver;
}
playChord() {
if (!this.audioCtx || !this.isPlaying) return;
const progressions = [
[[261.63, 329.63, 392.00], [392.00, 493.88, 587.33],
[440.00, 523.25, 659.25], [349.23, 440.00, 523.25]],
[[220.00, 277.18, 329.63], [329.63, 415.30, 493.88],
[369.99, 466.16, 554.37], [293.66, 369.99, 440.00]]
];
const prog = progressions[this.currentChord % 2];
const chord = prog[Math.floor(this.time / 4) % prog.length];
chord.forEach((freq, i) => {
const osc = this.audioCtx.createOscillator();
const gain = this.audioCtx.createGain();
const filter = this.audioCtx.createBiquadFilter();
osc.type = 'sine';
osc.frequency.value = freq * (1 + (Math.random() - 0.5) * 0.01);
filter.type = 'lowpass';
filter.frequency.value = 800 + Math.sin(this.time * 0.1) * 400;
gain.gain.setValueAtTime(0, this.audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0.15, this.audioCtx.currentTime + 0.5);
gain.gain.exponentialRampToValueAtTime(0.01, this.audioCtx.currentTime + 4);
osc.connect(filter);
filter.connect(gain);
gain.connect(this.reverb);
osc.start();
osc.stop(this.audioCtx.currentTime + 4);
});
// High texture
const highOsc = this.audioCtx.createOscillator();
const highGain = this.audioCtx.createGain();
highOsc.type = 'sine';
highOsc.frequency.value = chord[0] * 4 + Math.random() * 50;
highGain.gain.setValueAtTime(0, this.audioCtx.currentTime);
highGain.gain.linearRampToValueAtTime(0.03, this.audioCtx.currentTime + 1);
highGain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + 5);
highOsc.connect(highGain);
highGain.connect(this.reverb);
highOsc.start();
highOsc.stop(this.audioCtx.currentTime + 5);
this.time += 4;
}
start() {
this.isPlaying = true;
document.getElementById('overlay').classList.add('hidden');
const loop = () => {
if (!this.isPlaying) return;
this.playChord();
setTimeout(loop, 4000);
};
loop();
}
stop() {
this.isPlaying = false;
}
animate() {
this.ctx.fillStyle = 'rgba(10, 10, 15, 0.03)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
const freqData = this.analyser ? new Uint8Array(this.analyser.frequencyBinCount) : [];
if (this.analyser) this.analyser.getByteFrequencyData(freqData);
const energy = freqData.length > 0 ? freqData[10] / 255 : 0;
// Move and draw orbs
this.orbs.forEach((orb, i) => {
orb.angle += orb.speed * 0.01;
orb.x += Math.cos(orb.angle) * (1 + energy * 3);
orb.y += Math.sin(orb.angle * 0.7) * (1 + energy * 3);
// Wrap around
if (orb.x < -orb.radius) orb.x = this.canvas.width + orb.radius;
if (orb.x > this.canvas.width + orb.radius) orb.x = -orb.radius;
if (orb.y < -orb.radius) orb.y = this.canvas.height + orb.radius;
if (orb.y > this.canvas.height + orb.radius) orb.y = -orb.radius;
// Draw glow
const gradient = this.ctx.createRadialGradient(
orb.x, orb.y, 0,
orb.x, orb.y, orb.radius * (1 + energy * 0.5)
);
gradient.addColorStop(0, `rgba(${orb.color.join(',')}, 0.3)`);
gradient.addColorStop(1, 'rgba(0,0,0,0)');
this.ctx.beginPath();
this.ctx.arc(orb.x, orb.y, orb.radius * (1 + energy * 0.5), 0, Math.PI * 2);
this.ctx.fillStyle = gradient;
this.ctx.fill();
// Inner glow
this.ctx.beginPath();
this.ctx.arc(orb.x, orb.y, orb.radius * 0.3, 0, Math.PI * 2);
this.ctx.fillStyle = `rgba(${orb.color.join(',')}, 0.5)`;
this.ctx.fill();
});
requestAnimationFrame(() => this.animate());
}
bindEvents() {
document.getElementById('start-btn').addEventListener('click', async () => {
await this.initAudio();
this.start();
});
document.addEventListener('click', async () => {
if (!this.audioCtx) {
await this.initAudio();
}
}, { once: true });
}
}
new AmbientSoundscape();
</script>
</body>
</html>

View File

@@ -1,294 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive — 26 Key Shapes</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a0f; overflow: hidden; height: 100vh; }
canvas { display: block; width: 100%; height: 100%; }
.title {
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
color: rgba(255,255,255,0.1); font-size: 24px; font-family: sans-serif;
letter-spacing: 4px; text-transform: uppercase; pointer-events: none;
}
.key-hint {
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
color: rgba(255,255,255,0.3); font-size: 14px; font-family: monospace;
pointer-events: none;
}
</style>
</head>
<body>
<div class="title">Interactive</div>
<div class="key-hint">Press A-Z to play</div>
<canvas id="canvas"></canvas>
<script>
class Interactive {
constructor() {
this.canvas = document.getElementById('canvas');
this.ctx = this.canvas.getContext('2d');
this.audioCtx = null;
this.analyser = null;
this.shapes = [];
this.activeKeys = new Set();
this.resize();
window.addEventListener('resize', () => this.resize());
// 26 key mappings with unique shapes and frequencies
this.keyMap = {
'a': { freq: 261.63, shape: 'circle', color: [255,100,100] },
'b': { freq: 277.18, shape: 'square', color: [255,150,100] },
'c': { freq: 293.66, shape: 'triangle', color: [255,200,100] },
'd': { freq: 311.13, shape: 'diamond', color: [255,255,100] },
'e': { freq: 329.63, shape: 'star', color: [200,255,100] },
'f': { freq: 349.23, shape: 'hexagon', color: [150,255,100] },
'g': { freq: 369.99, shape: 'cross', color: [100,255,100] },
'h': { freq: 392.00, shape: 'circle', color: [100,255,150] },
'i': { freq: 415.30, shape: 'square', color: [100,255,200] },
'j': { freq: 440.00, shape: 'triangle', color: [100,255,255] },
'k': { freq: 466.16, shape: 'diamond', color: [100,200,255] },
'l': { freq: 493.88, shape: 'star', color: [100,150,255] },
'm': { freq: 523.25, shape: 'hexagon', color: [100,100,255] },
'n': { freq: 554.37, shape: 'cross', color: [150,100,255] },
'o': { freq: 587.33, shape: 'circle', color: [200,100,255] },
'p': { freq: 622.25, shape: 'square', color: [255,100,255] },
'q': { freq: 659.25, shape: 'triangle', color: [255,100,200] },
'r': { freq: 698.46, shape: 'diamond', color: [255,100,150] },
's': { freq: 739.99, shape: 'star', color: [255,120,120] },
't': { freq: 783.99, shape: 'hexagon', color: [255,170,120] },
'u': { freq: 830.61, shape: 'cross', color: [255,220,120] },
'v': { freq: 880.00, shape: 'circle', color: [220,255,120] },
'w': { freq: 932.33, shape: 'square', color: [170,255,120] },
'x': { freq: 987.77, shape: 'triangle', color: [120,255,120] },
'y': { freq: 1046.50, shape: 'diamond', color: [120,255,170] },
'z': { freq: 1108.73, shape: 'star', color: [120,255,220] }
};
this.bindEvents();
this.animate();
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
async initAudio() {
if (this.audioCtx) return;
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
this.analyser = this.audioCtx.createAnalyser();
this.analyser.fftSize = 256;
this.analyser.connect(this.audioCtx.destination);
}
playKey(key) {
if (!this.audioCtx || !this.keyMap[key]) return;
const { freq, shape, color } = this.keyMap[key];
// Play sound
const osc = this.audioCtx.createOscillator();
const gain = this.audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.value = freq;
gain.gain.setValueAtTime(0.3, this.audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.audioCtx.currentTime + 1);
osc.connect(gain);
gain.connect(this.analyser);
osc.start();
osc.stop(this.audioCtx.currentTime + 1);
// Create shape
const x = Math.random() * (this.canvas.width - 200) + 100;
const y = Math.random() * (this.canvas.height - 200) + 100;
this.shapes.push({
key, x, y, shape, color,
size: 50,
maxSize: 150,
life: 1,
rotation: 0
});
}
drawShape(shape) {
const { x, y, size, rotation, shape: shapeType, color, life } = shape;
this.ctx.save();
this.ctx.translate(x, y);
this.ctx.rotate(rotation);
this.ctx.globalAlpha = life;
this.ctx.strokeStyle = `rgb(${color.join(',')})`;
this.ctx.lineWidth = 3;
this.ctx.fillStyle = `rgba(${color.join(',')}, 0.2)`;
this.ctx.beginPath();
switch (shapeType) {
case 'circle':
this.ctx.arc(0, 0, size, 0, Math.PI * 2);
break;
case 'square':
this.ctx.rect(-size, -size, size * 2, size * 2);
break;
case 'triangle':
this.ctx.moveTo(0, -size);
this.ctx.lineTo(size * 0.866, size * 0.5);
this.ctx.lineTo(-size * 0.866, size * 0.5);
this.ctx.closePath();
break;
case 'diamond':
this.ctx.moveTo(0, -size);
this.ctx.lineTo(size * 0.7, 0);
this.ctx.lineTo(0, size);
this.ctx.lineTo(-size * 0.7, 0);
this.ctx.closePath();
break;
case 'star':
for (let i = 0; i < 5; i++) {
const angle = (i * 4 * Math.PI) / 5 - Math.PI / 2;
const method = i === 0 ? 'moveTo' : 'lineTo';
this.ctx[method](Math.cos(angle) * size, Math.sin(angle) * size);
}
this.ctx.closePath();
break;
case 'hexagon':
for (let i = 0; i < 6; i++) {
const angle = (i * Math.PI) / 3;
const method = i === 0 ? 'moveTo' : 'lineTo';
this.ctx[method](Math.cos(angle) * size, Math.sin(angle) * size);
}
this.ctx.closePath();
break;
case 'cross':
const w = size * 0.3;
this.ctx.moveTo(-w, -size);
this.ctx.lineTo(w, -size);
this.ctx.lineTo(w, -w);
this.ctx.lineTo(size, -w);
this.ctx.lineTo(size, w);
this.ctx.lineTo(w, w);
this.ctx.lineTo(w, size);
this.ctx.lineTo(-w, size);
this.ctx.lineTo(-w, w);
this.ctx.lineTo(-size, w);
this.ctx.lineTo(-size, -w);
this.ctx.lineTo(-w, -w);
this.ctx.closePath();
break;
}
this.ctx.fill();
this.ctx.stroke();
// Draw key label
this.ctx.fillStyle = `rgba(${color.join(',')}, ${life})`;
this.ctx.font = `${size * 0.4}px monospace`;
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.fillText(shape.key.toUpperCase(), 0, 0);
this.ctx.restore();
}
animate() {
this.ctx.fillStyle = 'rgba(10, 10, 15, 0.1)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Update and draw shapes
this.shapes = this.shapes.filter(shape => {
shape.size += (shape.maxSize - shape.size) * 0.05;
shape.rotation += 0.02;
shape.life -= 0.01;
if (shape.life <= 0) return false;
this.drawShape(shape);
return true;
});
// Draw active key indicators
const keyWidth = this.canvas.width / 13;
const keyHeight = 40;
const startY = this.canvas.height - 60;
let col = 0;
for (const key of 'abcdefghijklmnopqrstuvwxyz') {
const x = col * keyWidth + keyWidth / 2;
const isActive = this.activeKeys.has(key);
this.ctx.fillStyle = isActive
? `rgba(${this.keyMap[key].color.join(',')}, 0.5)`
: 'rgba(255,255,255,0.05)';
this.ctx.beginPath();
this.ctx.roundRect(x - 15, startY, 30, keyHeight, 5);
this.ctx.fill();
this.ctx.fillStyle = isActive ? '#fff' : 'rgba(255,255,255,0.3)';
this.ctx.font = '12px monospace';
this.ctx.textAlign = 'center';
this.ctx.fillText(key.toUpperCase(), x, startY + 25);
col++;
}
requestAnimationFrame(() => this.animate());
}
bindEvents() {
document.addEventListener('keydown', async (e) => {
if (e.repeat) return;
const key = e.key.toLowerCase();
if (this.keyMap[key] && !this.activeKeys.has(key)) {
await this.initAudio();
this.activeKeys.add(key);
this.playKey(key);
}
});
document.addEventListener('keyup', (e) => {
const key = e.key.toLowerCase();
this.activeKeys.delete(key);
});
// Touch support
this.canvas.addEventListener('touchstart', async (e) => {
e.preventDefault();
await this.initAudio();
// Map touch area to key
for (const touch of e.changedTouches) {
const keyIndex = Math.floor(touch.clientX / (this.canvas.width / 26));
const key = 'abcdefghijklmnopqrstuvwxyz'[keyIndex];
if (key) {
this.activeKeys.add(key);
this.playKey(key);
}
}
});
this.canvas.addEventListener('touchend', (e) => {
e.preventDefault();
this.activeKeys.clear();
});
}
}
new Interactive();
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,198 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Synesthesia — Paint with Sound</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a0f; overflow: hidden; height: 100vh; cursor: crosshair; }
canvas { display: block; width: 100%; height: 100%; }
.title {
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
color: rgba(255,255,255,0.15); font-size: 24px; font-family: sans-serif;
letter-spacing: 4px; text-transform: uppercase; pointer-events: none;
}
</style>
</head>
<body>
<div class="title">Synesthesia</div>
<canvas id="canvas"></canvas>
<script>
class Synesthesia {
constructor() {
this.canvas = document.getElementById('canvas');
this.ctx = this.canvas.getContext('2d');
this.audioCtx = null;
this.analyser = null;
this.particles = [];
this.trails = [];
this.mouse = { x: 0, y: 0, down: false };
this.lastNote = null;
this.colors = [
[255, 100, 150], [100, 200, 255], [150, 255, 150],
[255, 200, 100], [200, 150, 255]
];
this.resize();
window.addEventListener('resize', () => this.resize());
this.bindEvents();
this.animate();
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
async initAudio() {
if (this.audioCtx) return;
this.audioCtx = new AudioContext();
this.analyser = this.audioCtx.createAnalyser();
this.analyser.fftSize = 256;
this.analyser.connect(this.audioCtx.destination);
}
playTone(x, y) {
if (!this.audioCtx) return;
const freq = 200 + (x / this.canvas.width) * 600;
const noteId = Math.round(freq / 20);
if (noteId === this.lastNote) return;
this.lastNote = noteId;
const osc = this.audioCtx.createOscillator();
const gain = this.audioCtx.createGain();
osc.type = 'sine';
osc.frequency.value = freq;
gain.gain.setValueAtTime(0.2, this.audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.audioCtx.currentTime + 0.5);
osc.connect(gain);
gain.connect(this.analyser);
osc.start();
osc.stop(this.audioCtx.currentTime + 0.5);
// Spawn particles
const color = this.colors[Math.floor(Math.random() * this.colors.length)];
for (let i = 0; i < 8; i++) {
this.particles.push({
x, y,
vx: (Math.random() - 0.5) * 8,
vy: (Math.random() - 0.5) * 8,
life: 1,
color,
size: Math.random() * 10 + 5
});
}
// Add trail
this.trails.push({ x, y, color, size: 30, alpha: 0.5 });
}
animate() {
this.ctx.fillStyle = 'rgba(10, 10, 15, 0.05)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Draw trails
this.trails = this.trails.filter(t => {
t.alpha -= 0.005;
if (t.alpha <= 0) return false;
this.ctx.beginPath();
this.ctx.arc(t.x, t.y, t.size, 0, Math.PI * 2);
this.ctx.fillStyle = `rgba(${t.color.join(',')}, ${t.alpha})`;
this.ctx.fill();
return true;
});
// Draw particles
this.particles = this.particles.filter(p => {
p.x += p.vx;
p.y += p.vy;
p.life -= 0.02;
p.vy += 0.1;
if (p.life <= 0) return false;
this.ctx.beginPath();
this.ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
this.ctx.fillStyle = `rgba(${p.color.join(',')}, ${p.life * 0.8})`;
this.ctx.fill();
return true;
});
// Draw mouse trail
if (this.mouse.down) {
this.ctx.beginPath();
this.ctx.arc(this.mouse.x, this.mouse.y, 20, 0, Math.PI * 2);
const color = this.colors[Date.now() % 5];
this.ctx.fillStyle = `rgba(${color.join(',')}, 0.3)`;
this.ctx.fill();
}
requestAnimationFrame(() => this.animate());
}
bindEvents() {
this.canvas.addEventListener('mousedown', async (e) => {
await this.initAudio();
this.mouse.down = true;
this.mouse.x = e.clientX;
this.mouse.y = e.clientY;
this.playTone(e.clientX, e.clientY);
});
this.canvas.addEventListener('mousemove', (e) => {
this.mouse.x = e.clientX;
this.mouse.y = e.clientY;
if (this.mouse.down) {
this.playTone(e.clientX, e.clientY);
}
});
this.canvas.addEventListener('mouseup', () => {
this.mouse.down = false;
this.lastNote = null;
});
this.canvas.addEventListener('mouseleave', () => {
this.mouse.down = false;
this.lastNote = null;
});
this.canvas.addEventListener('touchstart', async (e) => {
e.preventDefault();
await this.initAudio();
this.mouse.down = true;
const touch = e.touches[0];
this.mouse.x = touch.clientX;
this.mouse.y = touch.clientY;
this.playTone(touch.clientX, touch.clientY);
});
this.canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
const touch = e.touches[0];
this.mouse.x = touch.clientX;
this.mouse.y = touch.clientY;
if (this.mouse.down) {
this.playTone(touch.clientX, touch.clientY);
}
});
this.canvas.addEventListener('touchend', () => {
this.mouse.down = false;
this.lastNote = null;
});
}
}
new Synesthesia();
</script>
</body>
</html>

View File

@@ -1,371 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Visualizer — WAV Frequency</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a0f; overflow: hidden; height: 100vh; }
canvas { display: block; width: 100%; height: 100%; }
.title {
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
color: rgba(255,255,255,0.1); font-size: 24px; font-family: sans-serif;
letter-spacing: 4px; text-transform: uppercase; pointer-events: none;
}
.controls {
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
display: flex; gap: 10px; z-index: 10;
}
button, select {
background: rgba(30,30,40,0.9); border: 1px solid rgba(100,100,120,0.3);
color: #e0e0e0; padding: 10px 20px; border-radius: 8px; font-size: 14px;
cursor: pointer;
}
button:hover, select:hover { background: rgba(50,50,70,0.9); }
.drop-zone {
position: fixed; inset: 0; display: flex; align-items: center;
justify-content: center; pointer-events: none; z-index: 5;
}
.drop-zone.active {
background: rgba(100,50,150,0.2);
border: 3px dashed rgba(150,100,200,0.5);
}
.drop-text {
color: rgba(255,255,255,0.3); font-size: 24px; font-family: sans-serif;
}
</style>
</head>
<body>
<div class="title">Visualizer</div>
<div class="drop-zone" id="drop-zone">
<span class="drop-text">Drop WAV file here or use controls</span>
</div>
<canvas id="canvas"></canvas>
<div class="controls">
<input type="file" id="file-input" accept="audio/*" style="display: none;">
<button id="load-btn">Load Audio</button>
<button id="play-btn" disabled>Play</button>
<button id="stop-btn" disabled>Stop</button>
<select id="viz-mode">
<option value="spectrum">Spectrum</option>
<option value="waveform">Waveform</option>
<option value="spectrogram">Spectrogram</option>
<option value="circular">Circular</option>
</select>
</div>
<script>
class AudioVisualizer {
constructor() {
this.canvas = document.getElementById('canvas');
this.ctx = this.canvas.getContext('2d');
this.audioCtx = null;
this.analyser = null;
this.source = null;
this.audioBuffer = null;
this.isPlaying = false;
this.mode = 'spectrum';
this.spectrogramData = [];
this.startOffset = 0;
this.resize();
window.addEventListener('resize', () => this.resize());
this.bindEvents();
this.animate();
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
async initAudio() {
if (this.audioCtx) return;
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
this.analyser = this.audioCtx.createAnalyser();
this.analyser.fftSize = 2048;
this.analyser.smoothingTimeConstant = 0.8;
this.analyser.connect(this.audioCtx.destination);
}
async loadAudio(file) {
await this.initAudio();
const arrayBuffer = await file.arrayBuffer();
this.audioBuffer = await this.audioCtx.decodeAudioData(arrayBuffer);
document.getElementById('play-btn').disabled = false;
document.getElementById('stop-btn').disabled = false;
document.querySelector('.drop-text').textContent = file.name;
}
play() {
if (!this.audioBuffer || this.isPlaying) return;
this.source = this.audioCtx.createBufferSource();
this.source.buffer = this.audioBuffer;
this.source.connect(this.analyser);
this.source.start(0, this.startOffset);
this.isPlaying = true;
this.source.onended = () => {
this.isPlaying = false;
this.startOffset = 0;
document.getElementById('play-btn').textContent = 'Play';
};
document.getElementById('play-btn').textContent = 'Pause';
}
stop() {
if (this.source && this.isPlaying) {
this.source.stop();
this.isPlaying = false;
this.startOffset = 0;
document.getElementById('play-btn').textContent = 'Play';
}
}
animate() {
this.ctx.fillStyle = 'rgba(10, 10, 15, 0.15)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
if (!this.analyser) {
requestAnimationFrame(() => this.animate());
return;
}
const freqData = new Uint8Array(this.analyser.frequencyBinCount);
const timeData = new Uint8Array(this.analyser.frequencyBinCount);
this.analyser.getByteFrequencyData(freqData);
this.analyser.getByteTimeDomainData(timeData);
switch (this.mode) {
case 'spectrum': this.drawSpectrum(freqData); break;
case 'waveform': this.drawWaveform(timeData); break;
case 'spectrogram': this.drawSpectrogram(freqData); break;
case 'circular': this.drawCircular(freqData, timeData); break;
}
requestAnimationFrame(() => this.animate());
}
drawSpectrum(data) {
const w = this.canvas.width;
const h = this.canvas.height;
const barCount = 128;
const barWidth = w / barCount;
const step = Math.floor(data.length / barCount);
for (let i = 0; i < barCount; i++) {
const value = data[i * step] / 255;
const barHeight = value * h * 0.8;
const hue = (i / barCount) * 300;
this.ctx.fillStyle = `hsla(${hue}, 80%, 60%, 0.8)`;
this.ctx.fillRect(
i * barWidth,
h - barHeight,
barWidth - 1,
barHeight
);
// Mirror
this.ctx.fillStyle = `hsla(${hue}, 80%, 60%, 0.2)`;
this.ctx.fillRect(
i * barWidth,
0,
barWidth - 1,
barHeight * 0.3
);
}
}
drawWaveform(data) {
const w = this.canvas.width;
const h = this.canvas.height;
this.ctx.beginPath();
this.ctx.strokeStyle = '#a78bfa';
this.ctx.lineWidth = 2;
const sliceWidth = w / data.length;
let x = 0;
for (let i = 0; i < data.length; i++) {
const v = data[i] / 128.0;
const y = (v * h) / 2;
if (i === 0) {
this.ctx.moveTo(x, y);
} else {
this.ctx.lineTo(x, y);
}
x += sliceWidth;
}
this.ctx.stroke();
// Glow
this.ctx.strokeStyle = 'rgba(167,139,250,0.3)';
this.ctx.lineWidth = 6;
this.ctx.stroke();
}
drawSpectrogram(data) {
const w = this.canvas.width;
const h = this.canvas.height;
// Add new line
this.spectrogramData.push([...data]);
// Keep last N lines
const maxLines = Math.floor(w / 2);
if (this.spectrogramData.length > maxLines) {
this.spectrogramData.shift();
}
// Draw
const line_width = w / maxLines;
this.spectrogramData.forEach((line, x) => {
const step = Math.floor(line.length / h);
for (let y = 0; y < h; y++) {
const value = line[y * step] || 0;
const hue = 270 - (value / 255) * 200;
const lightness = 20 + (value / 255) * 50;
this.ctx.fillStyle = `hsl(${hue}, 80%, ${lightness}%)`;
this.ctx.fillRect(x * line_width, h - y, line_width, 1);
}
});
}
drawCircular(freqData, timeData) {
const w = this.canvas.width;
const h = this.canvas.height;
const cx = w / 2;
const cy = h / 2;
const maxRadius = Math.min(w, h) * 0.35;
// Frequency ring
const freqStep = Math.floor(freqData.length / 180);
for (let i = 0; i < 180; i++) {
const value = freqData[i * freqStep] / 255;
const angle = (i / 180) * Math.PI * 2;
const radius = maxRadius * 0.5 + value * maxRadius * 0.5;
const x = cx + Math.cos(angle) * radius;
const y = cy + Math.sin(angle) * radius;
this.ctx.beginPath();
this.ctx.arc(x, y, 3, 0, Math.PI * 2);
const hue = (i / 180) * 360;
this.ctx.fillStyle = `hsla(${hue}, 80%, 60%, 0.8)`;
this.ctx.fill();
}
// Waveform circle
this.ctx.beginPath();
this.ctx.strokeStyle = 'rgba(255,255,255,0.3)';
this.ctx.lineWidth = 1;
for (let i = 0; i < timeData.length; i++) {
const v = timeData[i] / 128.0;
const angle = (i / timeData.length) * Math.PI * 2;
const radius = maxRadius * 0.3 * v;
const x = cx + Math.cos(angle) * radius;
const y = cy + Math.sin(angle) * radius;
if (i === 0) {
this.ctx.moveTo(x, y);
} else {
this.ctx.lineTo(x, y);
}
}
this.ctx.closePath();
this.ctx.stroke();
// Center glow
const gradient = this.ctx.createRadialGradient(cx, cy, 0, cx, cy, maxRadius * 0.3);
gradient.addColorStop(0, 'rgba(167,139,250,0.3)');
gradient.addColorStop(1, 'rgba(167,139,250,0)');
this.ctx.beginPath();
this.ctx.arc(cx, cy, maxRadius * 0.3, 0, Math.PI * 2);
this.ctx.fillStyle = gradient;
this.ctx.fill();
}
bindEvents() {
document.getElementById('load-btn').addEventListener('click', () => {
document.getElementById('file-input').click();
});
document.getElementById('file-input').addEventListener('change', async (e) => {
if (e.target.files[0]) {
await this.loadAudio(e.target.files[0]);
}
});
document.getElementById('play-btn').addEventListener('click', () => {
if (this.isPlaying) {
this.stop();
} else {
this.play();
}
});
document.getElementById('stop-btn').addEventListener('click', () => {
this.stop();
});
document.getElementById('viz-mode').addEventListener('change', (e) => {
this.mode = e.target.value;
this.spectrogramData = [];
});
// Drag and drop
const dropZone = document.getElementById('drop-zone');
document.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('active');
});
document.addEventListener('dragleave', () => {
dropZone.classList.remove('active');
});
document.addEventListener('drop', async (e) => {
e.preventDefault();
dropZone.classList.remove('active');
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('audio/')) {
await this.loadAudio(file);
}
});
// Touch support
this.canvas.addEventListener('click', async () => {
if (!this.audioCtx) {
await this.initAudio();
}
if (this.audioCtx.state === 'suspended') {
this.audioCtx.resume();
}
});
}
}
new AudioVisualizer();
</script>
</body>
</html>

View File

@@ -26,11 +26,17 @@ import threading
import hashlib
import os
import sys
from http.server import HTTPServer, BaseHTTPRequestHandler
from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn
from pathlib import Path
from datetime import datetime
from typing import Optional
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
"""Thread-per-request HTTP server."""
daemon_threads = True
# ── Configuration ──────────────────────────────────────────────────────
BRIDGE_PORT = int(os.environ.get('TIMMY_BRIDGE_PORT', 4004))
@@ -274,7 +280,7 @@ def main():
print(f" POST /bridge/move — Move user to room (user_id, room)")
print()
server = HTTPServer((BRIDGE_HOST, BRIDGE_PORT), BridgeHandler)
server = ThreadingHTTPServer((BRIDGE_HOST, BRIDGE_PORT), BridgeHandler)
server.serve_forever()