Skip to content

Earshot

Who hears whom, and what it sounds like. Game voice comms written as a rule set rather than as conditionals scattered through netcode — plus the part nobody sells separately, which is the character the audio carries once it arrives.

In development TypeScript Go WebRTC Opus

How it's built Get in touch

A routing diagram: talkers on the left wired to listeners on the right with one pair denied, and a clean waveform passing through an audio chain to come out as radio static.

What it does

Routing is a function of state

Who can hear whom is computed from participant state by pure functions — no transport, no media server, no I/O. Diffing the old graph against the new one yields the smallest set of changes worth applying.

Character is part of the route

A per-listener processing chain and mix belong to the model rather than being bolted on afterwards, so one channel can arrive as CB static and another as a clean command net.

Partitions that hold

Isolation — one faction, one server, one room — is a property of the rules rather than of the deployment. A partition something is permitted to cross is not a partition, so nothing is allowed to cross one.

Several channels at once

Group membership is a set, so a participant can be on a squad net, a command net and a fire net together. Rules carry priority and ducking, which is what makes listening to three of them at once bearable.

Deny is not a predicate

A block written into each rule fails open the moment one rule forgets it, so blocks are evaluated separately, after the rules, over the whole pair. It is the difference between a convention and a guarantee.

A voice-only media server

Opus only, no video, ever — which is precisely what makes building one tractable. Stripping video leaves room for the things it is for: processing in the path and a genuine mix per listener.

How it's built

The core is a TypeScript package with no dependencies on anything that moves: state in, subscription graph out. It was lifted out of Channel 19, which is still its first consumer and still routes through it, and the tests came with it so the move had to prove it changed no behaviour. Anything specific to a deployment is injected rather than hardcoded — the core does not know, for instance, which participants are services publishing processed audio rather than people.

The media server is separate and written in Go: Opus forwarding, the processing chain in the path, and per-listener mixing. Processing every listener costs far more than forwarding a stream untouched, and that is the trade being made on purpose rather than a surprise waiting on a hosting bill — it is asserted in the tests so it stays a decision. Audio character needs libopus, so the server still builds and still forwards without a C toolchain; it simply cannot colour anything.

Three products are meant to consume it and one of them is Java, which is why the wire contract is not optional and why this is a project of its own rather than a folder inside a game.

Core
TypeScript — subscription graph, graph diffing, proximity, processing chain, per-listener mixing
Media server
Go — Opus forwarding, in-path DSP, mixing, ICE and TURN. An early spike
Client
A TypeScript browser client, which the desktop apps run inside a hidden web view rather than reimplementing
Rule model
A written contract, currently a version ahead of the evaluator — the lift out of Channel 19 came first, deliberately
Consumers
Channel 19 today and WARDOGCOMMS as it is built; Neon Crow Games is planned, over the wire, since it is Java
Scope
Voice only, Opus only. A second media type is a product decision, not an engineering one
Status
Early. The core is real and in use; the control, ingest and media services are being built. Not published for anyone else to use

Back to all projects

Questions about this build?

Happy to go deeper on any part of it — the rule model, the audio path, the rest.

Get in touch