1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308 | <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>odeon — p2p concert hall</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%23120b0e'/%3E%3Ccircle cx='16' cy='16' r='10' fill='none' stroke='%23ffb03a' stroke-width='2.2'/%3E%3Ccircle cx='16' cy='16' r='3.2' fill='%23ffb03a'/%3E%3C/svg%3E">
<style>
:root { --bg:#120b0e; --gold:#ffb03a; --gold-dim:#b98336; --cream:#f4e6cf; --line:#3a2430; --panel:#1c1218; }
* { margin:0; padding:0; box-sizing:border-box; }
body {
background:
radial-gradient(90% 60% at 50% 0%, #251219 0%, transparent 70%),
var(--bg);
color: var(--cream);
font: 15px/1.75 ui-monospace, "SF Mono", Menlo, monospace;
max-width: 780px;
margin: 0 auto;
padding: 56px 22px 90px;
}
h1 {
font-family: Didot, "Bodoni MT", "Playfair Display", Georgia, serif;
font-size: 54px;
letter-spacing: .32em;
color: var(--gold);
text-shadow: 0 0 40px rgba(255,176,58,.4);
font-weight: 400;
}
.tag { color:#c9a17a; margin: 6px 0 40px; }
h2 {
font-size: 15px; letter-spacing:.25em; text-transform: uppercase;
color: var(--gold); margin: 44px 0 12px;
}
p { margin: 12px 0; color:#d8c4a8; }
a { color: var(--gold); }
code, pre {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 7px;
}
code { padding: 1px 7px; }
pre { padding: 14px 18px; overflow-x: auto; margin: 14px 0; }
pre code { background:none; border:none; padding:0; }
.cta {
display:inline-block; background: var(--gold); color:#1a0d12;
padding: 12px 26px; border-radius: 9px; text-decoration: none;
font-weight: 700; letter-spacing:.06em; margin: 8px 12px 8px 0;
}
.cta.ghost { background:none; color:var(--gold); border:1px solid var(--gold-dim); }
ul { margin: 12px 0 12px 22px; color:#d8c4a8; }
li { margin: 7px 0; }
.band { display:flex; gap:14px; flex-wrap:wrap; margin: 18px 0; }
.inst {
flex:1; min-width: 150px;
background: var(--panel); border:1px solid var(--line); border-radius: 10px;
padding: 14px 16px; font-size: 13px; color:#c9a17a;
}
.inst b { display:block; color: var(--cream); font-size: 15px; margin-bottom: 4px; }
.foot { margin-top: 60px; font-size: 12px; color:#8a6a4d; border-top:1px solid var(--line); padding-top: 18px; }
.langs {
position: fixed; top: 12px; right: 14px; color:#8a6a4d; font-size: 13px;
letter-spacing: .06em; user-select: none;
}
.langs button {
font: inherit; color: inherit; background: none; border: 0; padding: 3px 4px;
cursor: pointer; opacity: .55;
}
.langs button.on { color: var(--gold); opacity: 1; }
</style>
</head>
<body>
<nav class="langs" id="langs"></nav>
<main data-lang-block lang="en">
<h1>ODEON</h1>
<div class="tag">a serverless p2p concert hall — walk on stage, play together as a band</div>
<a class="cta" href="https://game6.ardegazu.ro">open the hall ↗</a>
<a class="cta ghost" href="browse/index.html">browse the code</a>
<a class="cta ghost" href="https://git.ardegazu.ro/game6.git">clone the repo</a>
<h2>the band</h2>
<div class="band">
<div class="inst"><b>🎹 grand</b>layered-harmonic piano, velocity by touch, glissando keys</div>
<div class="inst"><b>🥁 pads</b>16 synthesized drum voices on an MPC grid</div>
<div class="inst"><b>👋 theremin</b>an XY surface — continuous pitch, pentatonic snap</div>
<div class="inst"><b>🎸 bass</b>fat saw + sub sine with a plucky filter</div>
</div>
<p>Up to 8 on stage; everyone else takes the best seat in the house —
spectators hear the full mix live and bring the applause. Share the invite
link and your room is your band.</p>
<h2>how a band stays tight over the internet</h2>
<p>No audio is streamed. What crosses the wire is <em>notes</em> — a note-on is
~40 bytes — and every browser synthesizes the same sound in WebAudio. Live
notes play the moment they arrive. The glue is the <b>loop station</b>: record
a 1/2/4-bar riff and it's quantized to 16ths, broadcast as data, and scheduled
by every peer sample-accurately against a shared bar clock — one peer conducts
(BPM + bar origin), everyone else syncs to its clock by ping/pong offset
estimation. Latency shifts <em>when you hear a live flourish</em>, never
<em>where the groove sits</em>.</p>
<h2>how it works</h2>
<ul>
<li><b>No servers, no accounts.</b> A static Vite + TypeScript app; browsers are
js-libp2p peers that meet through a circuit-relay-v2 node, discover each other
via gossipsub and upgrade to direct WebRTC.</li>
<li><b>The invite link is the room key.</b> The secret lives in the URL fragment
and never leaves your browser; peers prove membership pairwise with a sealed
hello (AES-GCM bound to the room id and both authenticated peer ids).</li>
<li><b>End-to-end encrypted.</b> Every frame rides libp2p's noise layer, even
across the relay fallback.</li>
<li><b>The conductor is replaceable.</b> If the host leaves, the lowest peer id
inherits the clock with the groove's phase intact; late joiners get a snapshot
of the transport and every loop.</li>
<li><b>Hosted from IPFS.</b> The app and this very page (repo included) are
static files on IPFS behind IPNS + DNSLink — this page is also the git remote.</li>
</ul>
<pre><code>git clone https://git.ardegazu.ro/game6.git</code></pre>
<h2>honest limits</h2>
<ul>
<li>Live (unlooped) notes arrive with network latency — tens of ms on good
paths. Play loose, record tight.</li>
<li>Full-mesh design: comfortable up to ~8 players; beyond that, take a seat
in the house.</li>
<li>If two peers claim the stage after a network split, the senior claim wins
and near-ties go to the lowest peer id — a fresh boot can never steal an
established band's clock.</li>
</ul>
<div class="foot">MIT licensed · built in the ardegazu.ro house style ·
<a href="https://ardegazu.ro">the whole catalog</a> ·
<a href="https://git.ardegazu.ro/">all source</a></div>
</main>
<main data-lang-block lang="ro" hidden>
<h1>ODEON</h1>
<div class="tag">o sală de concert p2p fără servere — urcă pe scenă, cântați împreună ca o trupă</div>
<a class="cta" href="https://game6.ardegazu.ro">deschide sala ↗</a>
<a class="cta ghost" href="browse/index.html">răsfoiește codul</a>
<a class="cta ghost" href="https://git.ardegazu.ro/game6.git">clonează repo-ul</a>
<h2>trupa</h2>
<div class="band">
<div class="inst"><b>🎹 pian</b>pian cu armonice suprapuse, velocitate din atingere, clape cu glissando</div>
<div class="inst"><b>🥁 pads</b>16 voci de tobe sintetizate pe o grilă MPC</div>
<div class="inst"><b>👋 theremin</b>o suprafață XY — înălțime continuă, snap pentatonic</div>
<div class="inst"><b>🎸 bas</b>saw gras + sinus sub, cu un filtru percutant</div>
</div>
<p>Cel mult 8 pe scenă; toți ceilalți primesc cel mai bun loc din sală —
spectatorii aud mixul întreg live și aduc aplauzele. Trimite linkul de
invitație și camera ta e trupa ta.</p>
<h2>cum rămâne o trupă strânsă peste internet</h2>
<p>Nu se transmite audio. Pe fir circulă <em>note</em> — un note-on are
~40 de octeți — și fiecare browser sintetizează același sunet în WebAudio.
Notele live sună în clipa în care sosesc. Liantul e <b>stația de bucle</b>:
înregistrezi un rif de 1/2/4 măsuri, e cuantizat la șaisprezecimi, difuzat ca
date și programat de fiecare peer cu precizie de eșantion pe un ceas de măsură
comun — un peer dirijează (BPM + originea măsurii), restul se sincronizează cu
ceasul lui prin estimarea decalajului cu ping/pong. Latența mută <em>momentul
în care auzi o înfloritură live</em>, niciodată <em>locul unde stă
grooveul</em>.</p>
<h2>cum funcționează</h2>
<ul>
<li><b>Fără servere, fără conturi.</b> O aplicație statică Vite + TypeScript;
browserele sunt peers js-libp2p care se întâlnesc printr-un nod
circuit-relay-v2, se descoperă prin gossipsub și trec pe WebRTC direct.</li>
<li><b>Linkul de invitație e cheia camerei.</b> Secretul stă în fragmentul
URL-ului și nu pleacă niciodată din browser; peers își dovedesc apartenența
pereche cu pereche printr-un hello sigilat (AES-GCM legat de id-ul camerei și
de ambele peer id-uri autentificate).</li>
<li><b>Criptat cap-la-cap.</b> Fiecare cadru merge pe stratul noise al lui
libp2p, chiar și prin releul de rezervă.</li>
<li><b>Dirijorul e înlocuibil.</b> Dacă gazda pleacă, cel mai mic peer id
moștenește ceasul cu faza grooveului intactă; cine intră târziu primește un
snapshot al transportului și al fiecărei bucle.</li>
<li><b>Găzduit de pe IPFS.</b> Aplicația și chiar pagina asta (cu repo cu tot)
sunt fișiere statice pe IPFS în spatele IPNS + DNSLink — pagina asta e și
remote-ul git.</li>
</ul>
<pre><code>git clone https://git.ardegazu.ro/game6.git</code></pre>
<h2>limite, pe bune</h2>
<ul>
<li>Notele live (nebuclate) sosesc cu latența rețelei — zeci de ms pe căi
bune. Cântă lejer, înregistrează strâns.</li>
<li>Design full-mesh: confortabil până la ~8 jucători; peste, ia un loc în
sală.</li>
<li>Dacă doi peers revendică scena după o ruptură de rețea, câștigă
revendicarea mai veche, iar aproape-egalitățile merg la cel mai mic peer id —
un boot proaspăt nu poate fura niciodată ceasul unei trupe așezate.</li>
</ul>
<div class="foot">Licență MIT · construit în stilul casei ardegazu.ro ·
<a href="https://ardegazu.ro">tot catalogul</a> ·
<a href="https://git.ardegazu.ro/">toată sursa</a></div>
</main>
<main data-lang-block lang="hu" hidden>
<h1>ODEON</h1>
<div class="tag">szerver nélküli p2p koncertterem — lépj színpadra, játsszatok együtt zenekarként</div>
<a class="cta" href="https://game6.ardegazu.ro">nyisd meg a termet ↗</a>
<a class="cta ghost" href="browse/index.html">böngészd a kódot</a>
<a class="cta ghost" href="https://git.ardegazu.ro/game6.git">repó klónozása</a>
<h2>a zenekar</h2>
<div class="band">
<div class="inst"><b>🎹 zongora</b>rétegzett felhangú zongora, ütéserősség érintésből, glissando billentyűk</div>
<div class="inst"><b>🥁 pads</b>16 szintetizált dobhang egy MPC-rácson</div>
<div class="inst"><b>👋 theremin</b>egy XY felület — folytonos hangmagasság, pentaton snap</div>
<div class="inst"><b>🎸 basszus</b>kövér fűrészjel + szub szinusz, pengetős szűrővel</div>
</div>
<p>Legfeljebb 8 fő a színpadon; mindenki más a terem legjobb helyét kapja —
a nézők élőben hallják a teljes mixet, és ők hozzák a tapsot. Küldd tovább a
meghívólinket, és a szobád a zenekarod.</p>
<h2>hogyan marad feszes egy zenekar az interneten át</h2>
<p>Hang nem streamel. A dróton <em>hangjegyek</em> mennek — egy note-on
~40 bájt — és minden böngésző ugyanazt a hangot szintetizálja WebAudióban. Az
élő hangok érkezésük pillanatában szólalnak meg. A ragasztó a
<b>loopállomás</b>: felveszel egy 1/2/4 ütemes riffet, tizenhatodokra
kvantálódik, adatként megy szét, és minden peer minta-pontosan ütemezi egy
közös ütemórához — egy peer vezényel (BPM + ütemkezdet), a többiek ping/pong
eltolás-becsléssel igazodnak az órájához. A késleltetés azt tolja el,
<em>mikor hallasz egy élő futamot</em>, sosem azt, <em>hol ül a groove</em>.</p>
<h2>hogyan működik</h2>
<ul>
<li><b>Se szerver, se fiók.</b> Statikus Vite + TypeScript app; a böngészők
js-libp2p peerek, amelyek egy circuit-relay-v2 csomóponton át találkoznak,
gossipsubbal fedezik fel egymást, és közvetlen WebRTC-re váltanak.</li>
<li><b>A meghívólink a szoba kulcsa.</b> A titok az URL fragmentben él, és
soha nem hagyja el a böngésződ; a peerek páronként egy lepecsételt hellóval
bizonyítják a tagságot (AES-GCM, a szoba id-jához és mindkét hitelesített
peer id-hoz kötve).</li>
<li><b>Végponttól végpontig titkosítva.</b> Minden keret a libp2p
noise-rétegén megy, a tartalék relén át is.</li>
<li><b>A karmester pótolható.</b> Ha a hoszt elmegy, a legkisebb peer id
örökli az órát a groove fázisával együtt; a későn érkezők pillanatképet
kapnak a transzportról és minden loopról.</li>
<li><b>IPFS-ről hosztolva.</b> Az app és maga ez az oldal (a repóval együtt)
statikus fájlok IPFS-en, IPNS + DNSLink mögött — ez az oldal egyben a git
remote is.</li>
</ul>
<pre><code>git clone https://git.ardegazu.ro/game6.git</code></pre>
<h2>korlátok, őszintén</h2>
<ul>
<li>Az élő (nem loopolt) hangok hálózati késéssel érkeznek — jó útvonalon
néhány tíz ms. Játssz lazán, vegyél fel feszesen.</li>
<li>Full-mesh felépítés: ~8 játékosig kényelmes; afölött ülj be a
nézőtérre.</li>
<li>Ha hálózati szakadás után két peer követeli a színpadot, a régebbi
követelés nyer, a majdnem-döntetlenek a legkisebb peer id-hoz kerülnek — egy
friss boot sosem lophatja el egy összeszokott zenekar óráját.</li>
</ul>
<div class="foot">MIT licenc · az ardegazu.ro házi stílusában épült ·
<a href="https://ardegazu.ro">a teljes katalógus</a> ·
<a href="https://git.ardegazu.ro/">minden forrás</a></div>
</main>
<script>
(function () {
var LANGS = ["en", "ro", "hu"];
var KEY = "ardegazu-site:lang";
var lang = null;
try { lang = localStorage.getItem(KEY); } catch (e) {}
if (LANGS.indexOf(lang) < 0) {
var cands = navigator.languages || [navigator.language];
for (var i = 0; i < cands.length; i++) {
var p = (cands[i] || "").slice(0, 2).toLowerCase();
if (LANGS.indexOf(p) >= 0) { lang = p; break; }
}
}
if (LANGS.indexOf(lang) < 0) lang = "en";
var nav = document.getElementById("langs");
function apply(l) {
lang = l;
document.documentElement.lang = l;
try { localStorage.setItem(KEY, l); } catch (e) {}
var blocks = document.querySelectorAll("[data-lang-block]");
for (var i = 0; i < blocks.length; i++) blocks[i].hidden = blocks[i].getAttribute("lang") !== l;
var btns = nav.querySelectorAll("button");
for (var j = 0; j < btns.length; j++) btns[j].className = btns[j].textContent === l ? "on" : "";
}
LANGS.forEach(function (l, i) {
if (i) nav.appendChild(document.createTextNode(" · "));
var b = document.createElement("button");
b.type = "button";
b.textContent = l;
b.addEventListener("click", function () { apply(l); });
nav.appendChild(b);
});
apply(lang);
})();
</script>
</body>
</html>
|