anvilsign in

collin/mahjong · bf80fd25

Say it from where the player is sitting

Collin Richards · 2026-08-25 08:02 UTC · bf80fd25d16d8a9fa574610a0d7110811e9baa7d · parent 0ab65f87 · browse files

modifiedREADME.md+64 −1
⋯ 147 unchanged lines
148148 五萬, 東風. A flower says 補花 and then which one. A new call cuts off one
149149 still being spoken; at table pace the newest is the only one that matters.
150150 See [the voice pack](#the-voice-pack) for where the audio comes from.
151+- **方位音 (sound by seat)** — a third switch, on by default. Every noise comes
152+ out of the edge its seat is sitting at: 上家 to your left, 下家 to your right,
153+ and 對家 across the table, where it is quieter and has the top taken off it —
154+ a metre of air and three people's arms do not carry 12 kHz. So you can tell
155+ whose 碰 that was without looking up, which is the whole point of hearing it.
156+ Tiles out in the middle are placed properly rather than by edge: the physics
157+ knows to the pixel where a tile landed, and that is where the knock comes
158+ from. Online the table is turned so your own seat is at the bottom, and the
159+ sound turns with it. See [audio, by seat](#audio-by-seat).
151160 - **Settings** — 設定 from the lobby, or from the game-over panel: player names,
152161 底 / 台 / starting chips, the house-rule switches below, and sound. Kept in
153162 `localStorage` separately from the save, so they carry over to the next game.
⋯ 31 unchanged lines
185194 tag on the nameplate to show it again for a player who lost theirs. A phone
186195 that leaves hands the seat back to the laptop. Seats never lock: a second
187196 phone scanning the same QR joins the same hand — two people playing one
188- hand, whoever acts first acts.
197+ hand, whoever acts first acts. And with sound on, the phone in your hand is
198+ what says your calls — see [audio, by seat](#audio-by-seat).
189199
190200 Design notes, in the order they bit:
191201
⋯ 220 unchanged lines
412422 House rules vary a lot; the tai table and payments are plain data/functions in
413423 `tai.ts` and `types.ts` if yours differ.
414424
425+## Audio, by seat
426+
427+A sound at a mahjong table comes from somewhere. You know whose 碰 that was
428+because it came from your left, and you know a tile has been thrown hard at the
429+far corner without watching it go. Both of those are cheap to keep, and both
430+are gone the moment everything comes out of the middle of one screen.
431+
432+**Every cue is a seat's.** `SoundCue` carries a `seat` alongside the kind and
433+the tile — the seat that threw, called, or won — and leaves it off the things
434+that belong to the table rather than to anybody: the deal, a 流局, an undo. The
435+engine says whose only because a sound has to come from somewhere; what that
436+means in the room is entirely the UI's business, as it always was.
437+
438+**A seat becomes a place.** `App.tsx` turns a seat into a *position round this
439+screen* — 0 the near edge, 1 the right, 2 across, 3 the left — because that is
440+what the ear wants and the seat number is not: online the table is turned so
441+you are always at the bottom, and the sound has to turn with it.
442+`game/sound.ts` gives each position a bus of its own: a pan, a gain, and a
443+lowpass that opens all the way for the near edge and closes to 5 kHz for the
444+far one. Distance is two things at once — quieter, and with the top taken off
445+— because between you and 對家 there is a metre of air, a wall of tiles, and
446+three people's arms, none of which carry treble. Tiles out in the middle skip
447+the edges entirely and are placed continuously, panned by where on the felt
448+they actually landed; the physics knows that to the pixel. Buses are built once
449+per spot and quantised, so a pool full of skidding tiles reuses a handful
450+rather than building one per contact. Old Safari has no `StereoPannerNode`; it
451+plays where it always did.
452+
453+**At a party table, the words come out of the player.** Five devices in one
454+room, and the split the sound should take is the one the room already has: the
455+table makes the noise, because the tiles are on the table, and the phone in
456+somebody's hand says their calls, because the calls are theirs. So the party
457+screen drops the *voice* for a seat while keeping the clack, and the phone
458+holding that seat says 碰 and names its own discards and makes no table noise
459+at all.
460+
461+That only works if the phone really is going to say it, so it is asked rather
462+than assumed. `sound.voiceLive()` is the honest answer — sound on, 報牌 on, the
463+audio hardware awake, the pack decoded — and every device reports it to the
464+host whenever it changes (`EV_VOICE`). The host works out, seat by seat, which
465+hands have a device that will speak for them and publishes that as `voices` in
466+room state; a nameplate wearing 🔊 next to its 📱 is a seat the table has gone
467+quiet for on purpose. Mute the phone, put it to sleep, hand the seat back —
468+the flag drops and the table takes the calls back mid-hand. The host's own
469+device never counts, even when it holds seats: it is the table, and a table
470+that fell silent on the grounds that it was about to speak would say nothing at
471+all.
472+
473+A browser will not start audio before the page has been touched, and a phone
474+that arrived by QR may never touch anything that asks for it by name — so the
475+first touch anywhere unlocks it, which is also the moment it can start
476+claiming the job.
477+
415478 ## The voice pack
416479
417480 A mahjong table only ever says about fifty things — 42 tile names and a handful
⋯ 301 unchanged lines