Help us shape the future of Go! Take the 2026 Go Developer Survey

link

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package link is murmur's one-way path: no handshake, no acknowledgement, no encryption.

A handshake has a cost that is not worth paying for the payloads murmur carries. A key, a URL or an OTP that somebody is going to read off the screen and compare by eye needs no ARQ - either the text appeared on the other side or it did not, and saying it again is faster than any protocol murmur could run. The path is also the one that survives being recorded: `murmur encode key.txt | pw-play -` on one machine and `pw-record - | murmur decode` on the other is a transfer with no round trip at all, and a recording of it can be replayed at any time.

The price is stated where it is paid, not hidden here:

  • There is no acknowledgement, so a lost burst is a lost fragment. The receiver notices (the digest and the missing sequence number both catch it) and says so, but the sender has already stopped talking.
  • There is no key agreement, so the payload is in the clear. Anyone in the room can decode it, and anyone who records it can decode it later. murmur has no encrypted mode: an earlier version sealed frames and set mac.FlagEncrypted, and these receivers still say so rather than reporting a length mismatch about a burst they cannot read.
  • There is no identity, so the receiver cannot name the machine that sent it. The payload carries only what the sender called it.

None of that is a shortcoming to be fixed later. It is the trade: the plain path is the only path murmur has, and `murmur decode` with `murmur encode` is how it is used.

Index

Constants

View Source
const (
	// MinPayloadSize is the floor for one frame's application bytes. The robust
	// profile carries about 31 bytes per second of airtime, so a smaller payload
	// would spend more on framing than on data.
	MinPayloadSize = 32
	// MaxMessageBytes bounds one message. A megabyte is twenty hours of airtime at
	// the robust profile; refusing is kinder than starting.
	MaxMessageBytes = 1 << 20
)
View Source
const (
	// PlainBurstGap is the silence left between bursts on the plain path.
	//
	// Without an ARQ loop nothing waits for a room to settle, so the gap is what
	// keeps one burst's reverb tail out of the next burst's preamble. It is
	// deliberately longer than the robust profile's symbol time by two orders of
	// magnitude: seconds of airtime are already being spent, and a burst that
	// cannot be found is seconds wasted.
	PlainBurstGap = 150 * time.Millisecond

	// PlainMessageTimeout is how long a partially received message is kept.
	//
	// It is not a protocol timer - there is no protocol - but the receiver has to
	// give up eventually, or a single missing fragment would leave a half
	// reassembled payload in memory until the process ended.
	PlainMessageTimeout = 5 * time.Second
)
View Source
const (
	DefaultFECData   = fec.RSDefaultData
	DefaultFECParity = fec.RSDefaultParity
)

DefaultFECData and DefaultFECParity are the block code the shipped path uses: rate 1/2 over blocks of 64 data bytes. The simulator places the knee here. On its hostile preset, uncoded delivery is 5 frames in 50; this code lifts it to 44, and a 2/3 code only reaches 42 while saving airtime that is not scarce. The remaining loss at that point is the preamble failing to be found, which no amount of payload coding can repair, so a stronger code buys nothing.

They are aliases of the codec defaults so the profile a peer reads and the codec the link applies cannot disagree about the rate.

Variables

View Source
var (
	// ErrMessageTooLarge means a payload exceeded MaxMessageBytes.
	ErrMessageTooLarge = errors.New("link: message too large")
	// ErrTimeout means no burst arrived within the listening window. On an idle
	// acoustic channel this is the normal outcome, not a failure.
	ErrTimeout = errors.New("link: no burst received")
)
View Source
var (
	// ErrPlainIncomplete means a message could not be assembled because a
	// fragment never arrived. It is always a report about one message, never a
	// reason to stop listening: the next message is unaffected.
	ErrPlainIncomplete = errors.New("link: incomplete message")
	// ErrPlainEncrypted means the burst was sealed by a build that encrypted its
	// frames. No current build does, so the useful thing to say is that the two
	// ends are running different versions rather than to complain about a length.
	ErrPlainEncrypted = errors.New("link: the sender encrypted that frame")

	// ErrPlainStillArriving means the buffer ended inside a burst: not corruption,
	// just a burst whose remaining samples have not been captured yet. A
	// streaming receiver keeps the audio and tries again, and says nothing, because
	// this happens whenever a burst begins near the end of a capture.
	//
	// It is a separate error from ErrPlainIncomplete because the two need
	// opposite responses: an arriving burst is waited for, a missing fragment is
	// reported.
	ErrPlainStillArriving = errors.New("link: that burst is still arriving")
)

Functions

func PlainPayloadSize

func PlainPayloadSize(scheme modulation.Scheme, sampleRate int) int

PlainPayloadSize is how many application bytes one frame carries.

It is derived from the profile's one-second capacity minus the frame overhead, so the sender and the receiver agree without anything about it travelling on the wire.

Types

type BurstSink

type BurstSink func(pcm []float32) error

BurstSink receives one burst of PCM. Returning an error stops the encoder, which is how a pipe applies backpressure: a writer that cannot keep up must be able to say so rather than have the whole message buffered in memory first.

type BurstTransport

type BurstTransport interface {
	// SendBurst transmits one burst and returns once it has left the speaker.
	SendBurst(ctx context.Context, pcm []float32) error
	// RecvBurst listens for one burst, returning ErrTimeout if none arrives
	// before ctx expires.
	RecvBurst(ctx context.Context, maxSamples int) ([]float32, error)
	// Settle waits out the transmit path and discards anything captured during
	// the transmission. Skipping it is the classic self-interference bug: the
	// receiver decodes its own burst and the link appears to work while actually
	// talking to itself.
	Settle(ctx context.Context) error
	// Latency reports the backend's buffering in each direction.
	Latency() audio.Latency
}

BurstTransport is the seam between a receiver and the acoustic channel. It exists so the one-way path can be driven by a real audio device, by a cross-wired loopback pair, or by the channel simulator, with no branch inside the codec.

func NewDeviceTransport

func NewDeviceTransport(dev audio.Device, settle time.Duration) BurstTransport

NewDeviceTransport adapts an audio.Device to BurstTransport.

It is small, but its correctness matters more than its size: every timing assumption in the link is realised here. In particular Settle must both drain the transmit path and discard the capture backlog. Doing only the first leaves the receiver decoding its own reverb tail; doing only the second leaves the sound server's playback buffer holding a burst that will be heard, and captured, later.

settle is how long to wait out the room before listening. Zero means a caller has not measured anything better.

type PlainDecoder

type PlainDecoder struct {
	// contains filtered or unexported fields
}

PlainDecoder reassembles messages from whatever it hears.

It is a state machine over bursts rather than over time: every method is a pure function of the frames handed to it, so a test can feed it a captured recording. The only clock in it is LastBurst, which exists so that a caller with a timeout can tell an idle channel from an abandoned message.

func NewPlainDecoder

func NewPlainDecoder(scheme modulation.Scheme, sampleRate int, opts ...PlainOption) (*PlainDecoder, error)

NewPlainDecoder builds a decoder for one profile.

func (*PlainDecoder) Consumed

func (d *PlainDecoder) Consumed() int

Consumed is how many samples of the buffer just handed to DecodeBurst belonged to the burst it decoded: the preamble's offset plus the burst's own length.

It exists for the streaming receiver, which is the only kind this path has. A capture buffer is a fixed size and a burst is not, so the boundary between two captures falls in the middle of a burst sooner or later, and the half after the boundary has lost its preamble: undecodable on its own, and lost forever if the receiver does not keep it. A receiver that advances its buffer by Consumed and keeps the remainder will find that burst whole on the next look, and will never decode the same burst twice.

Zero means the buffer held no usable burst, so nothing may be discarded from it.

func (*PlainDecoder) DecodeBurst

func (d *PlainDecoder) DecodeBurst(pcm []float32) ([]byte, []error)

DecodeBurst decodes one captured buffer and returns whatever it produced: the message if this burst completed one, and any reports about messages that could not be assembled.

Reports come back alongside a message rather than instead of it, and that is deliberate: a receiver that dropped the payload it just decoded in order to complain about an older one would turn a report into a loss. A caller filters what it cares about - modulation.ErrNoSync means the window was simply quiet, which is the normal case - and a caller that treats any report as fatal will stop listening at the first burst of somebody else's protocol, which is exactly what a silent receiver must not do.

func (*PlainDecoder) LastBurst

func (d *PlainDecoder) LastBurst() time.Time

LastBurst is when a frame was last decoded. A zero time means none ever was.

func (*PlainDecoder) ListenSamples

func (d *PlainDecoder) ListenSamples() int

ListenSamples is how much audio to capture per look.

func (*PlainDecoder) OverlapSamples

func (d *PlainDecoder) OverlapSamples() int

OverlapSamples is how many samples a streaming receiver must keep when it discards what it has already decoded: long enough for the longest burst this profile can produce, plus the silence the encoder leaves between bursts.

func (*PlainDecoder) PayloadSize

func (d *PlainDecoder) PayloadSize() int

PayloadSize is the application bytes per frame this decoder expects.

func (*PlainDecoder) Pending

func (d *PlainDecoder) Pending() int

Pending is how many bytes of a message are currently held, 0 when nothing is being assembled.

func (*PlainDecoder) Reset

func (d *PlainDecoder) Reset() int

Reset discards a partially assembled message and reports how many bytes were thrown away, so a caller can tell the user what it gave up on rather than forgetting it in silence.

func (*PlainDecoder) Stats

func (d *PlainDecoder) Stats() modulation.Stats

Stats describes the last burst that decoded, for reporting.

type PlainEncoder

type PlainEncoder struct {
	// contains filtered or unexported fields
}

PlainEncoder renders payloads as bursts of sound.

It holds one counter, not a window: with no acknowledgements there is nothing to retransmit and nothing to track, and a fragment's sequence number exists only so the receiver can tell a duplicate from a gap.

func NewPlainEncoder

func NewPlainEncoder(scheme modulation.Scheme, sampleRate int, opts ...PlainOption) (*PlainEncoder, error)

NewPlainEncoder builds an encoder for one profile.

func (*PlainEncoder) EncodeMessage

func (e *PlainEncoder) EncodeMessage(msg []byte, emit BurstSink) error

EncodeMessage renders one packed message as bursts, in order.

msg is already framed by the transfer layer: this layer knows about bytes and fragments, not about names and digests. Every burst is followed by PlainBurstGap of silence, including the last, so that a player which stops at the end of its input does not clip the final burst's tail.

func (*PlainEncoder) FrameLen

func (e *PlainEncoder) FrameLen() int

FrameLen is the total on-air length of a full frame, header, payload, CRC and any block-code parity included.

func (*PlainEncoder) ListenSamples

func (e *PlainEncoder) ListenSamples() int

ListenSamples is how much audio a receiver should capture to be sure of catching one burst: the burst itself plus half a second of slack for a playback buffer that is not quite where it says it is.

func (*PlainEncoder) PayloadSize

func (e *PlainEncoder) PayloadSize() int

PayloadSize is the application bytes per frame.

type PlainOption

type PlainOption func(*plainConfig)

PlainOption adjusts how a plain encoder or decoder protects a frame.

func WithDefaultFEC

func WithDefaultFEC() PlainOption

WithDefaultFEC applies the shipped block code. It exists so that the command line cannot forget the codec in one of the two places it opens a link, which is exactly the kind of asymmetry that makes a receiver hear nothing. The construction cannot fail for the fixed constants, and if it somehow does the error surfaces from the constructor rather than being swallowed here.

func WithFEC

func WithFEC(codec fec.Codec) PlainOption

WithFEC protects every frame with a block code. It is applied above the MAC frame, so the frame's own header and CRC are covered too: a burst whose length field the noise damaged can be repaired along with everything else, which is what makes the code useful rather than decorative.

A nil codec, or no option at all, leaves the frame bare. That is the default so that a caller who wants to measure the raw channel still can, and so that the bench and the file-backed devices keep testing the PHY rather than the codec.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL