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
- Variables
- func PlainPayloadSize(scheme modulation.Scheme, sampleRate int) int
- type BurstSink
- type BurstTransport
- type PlainDecoder
- func (d *PlainDecoder) Consumed() int
- func (d *PlainDecoder) DecodeBurst(pcm []float32) ([]byte, []error)
- func (d *PlainDecoder) LastBurst() time.Time
- func (d *PlainDecoder) ListenSamples() int
- func (d *PlainDecoder) OverlapSamples() int
- func (d *PlainDecoder) PayloadSize() int
- func (d *PlainDecoder) Pending() int
- func (d *PlainDecoder) Reset() int
- func (d *PlainDecoder) Stats() modulation.Stats
- type PlainEncoder
- type PlainOption
Constants ¶
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 )
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 )
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 ¶
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") )
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 ¶
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.