Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions .machine_readable/6a2/STATE.a2ml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ phase = "hardening"
maturity = "pre-production"
rationale = "Foundations + SFU solid. LLM service, PTP hardware read, Avow attestation, Opus server NIF are stubs contradicting earlier 'done' claims. Migration in progress: V-lang removed, ReScript -> AffineScript pending."

[dogfooding-status]
# Populated 2026-04-19 against real on-disk integrations. Each entry must
# resolve to a grep-hit in the cited path.
vext-protocol-implementation = "2026-04-19 — vext protocol implemented in burble server, not in the vext repo (531 LOC: vext.ex 257 + vext_groove.ex 155 + vext_test.exs 119). Paths: server/lib/burble/verification/vext.ex, server/lib/burble/verification/vext_groove.ex, server/test/burble/verification/vext_test.exs"

[route-to-mvp]
milestones = [
{ name = "v0.1.0 to v0.4.0 — Foundation & Transport", completion = 100 },
Expand All @@ -45,8 +40,10 @@ signaling-relay = { status = "consolidated", canonical = "signaling/relay.js", r
doc-reality-drift = [
"ROADMAP.adoc claims LLM Service DONE — is a stub (provider missing, parse_frame broken)",
"ROADMAP.adoc claims Formal Proofs DONE — Avow attestation is data-type-only, no dependent-type enforcement",
"README.adoc PTP claim sub-microsecond assumes hardware — code falls back to system clock without NIF",
"ffi/zig coprocessor nif_audio_encode/decode are not real Opus (intentional SFU-opaque, but misleadingly named)"
"README.adoc PTP claim sub-microsecond assumes hardware — code falls back to system clock without NIF"
]
resolved-2026-04-16 = [
"Opus naming/contract drift: Backend.audio_encode/4 + audio_decode/3 docstrings rewritten to state explicitly that they are PCM frame pack/unpack, NOT Opus. Added explicit Backend.opus_transcode/4 callback returning {:error, :not_implemented} on every backend (ElixirBackend, ZigBackend, SmartBackend, SNIFBackend). Added opus_available?/0 callback (always false). Pinned by opus_contract_test.exs."
]

[critical-next-actions]
Expand Down
10 changes: 9 additions & 1 deletion ffi/zig/src/coprocessor/audio.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@
// Burble Coprocessor — Audio kernel (Zig SIMD implementation).
//
// SIMD-accelerated audio processing operations:
// - PCM encode/decode (16-bit LE ↔ float, with SIMD clamping)
// - PCM frame pack/unpack (16-bit LE ↔ float, with SIMD clamping)
// NOTE: This is *framing*, not Opus compression. Real Opus transcoding
// requires linking libopus and is deferred — see STATE.a2ml [migration].
// Burble is an E2EE-opaque SFU: clients Opus-encode in the browser's
// WebRTC stack; the server forwards ciphertext without decoding. These
// pack/unpack helpers are used only for recording, archive, and
// self-test loopback paths. The Elixir Backend exposes an explicit
// `opus_transcode/4` callback that returns {:error, :not_implemented}
// so callers intending real Opus fail loudly.
// - Noise gate (vectorised threshold comparison)
// - Echo cancellation (NLMS adaptive filter, SIMD dot product)
//
Expand Down
67 changes: 58 additions & 9 deletions server/lib/burble/coprocessor/backend.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,21 @@
# └── SmartBackend — dispatcher routing per-operation
#
# Kernel domains:
# Audio — Opus encode/decode, noise suppression, echo cancellation
# Audio — PCM frame pack/unpack (NOT Opus transcoding — see note below),
# noise suppression, echo cancellation
# Crypto — AES-GCM frame encryption, Avow hash chains
# IO — jitter buffer, packet loss concealment, adaptive bitrate
# DSP — FFT, convolution, mixing matrix
# Neural — ML-based noise suppression (keyboard/fan/dog removal)
#
# Opus transcoding is NOT performed server-side. Burble is an E2EE-opaque
# SFU: clients encode Opus in the browser's WebRTC stack; the server forwards
# ciphertext frames without decoding. The audio_encode/audio_decode callbacks
# below pack raw PCM into length-prefixed frames — they are used for
# recording/archive/benchmark paths only, not for transcoding live RTP. An
# explicit `opus_transcode/4` callback returns {:error, :not_implemented} to
# make this contract enforceable. Real Opus would require linking libopus;
# see STATE.a2ml [migration] for the deferred decision.

defmodule Burble.Coprocessor.Backend do
@moduledoc """
Expand Down Expand Up @@ -59,19 +69,30 @@ defmodule Burble.Coprocessor.Backend do
@callback available?() :: boolean()

# ---------------------------------------------------------------------------
# Audio kernel — Opus codec, noise gate, echo cancellation
# Audio kernel — PCM frame pack/unpack, noise gate, echo cancellation
# ---------------------------------------------------------------------------

@doc """
Encode a raw PCM audio frame to Opus.
Pack raw PCM samples into a length-prefixed binary frame.

**This is NOT Opus encoding.** Clients perform Opus encoding in the
browser's WebRTC stack; the server does not transcode live RTP. This
callback is used for recording, archive, and self-test loopback paths
where raw PCM framing is needed.

The `bitrate` parameter is accepted for API stability but is currently
ignored — no compression is performed. Call `opus_transcode/4` explicitly
if you need real Opus (it will return `{:error, :not_implemented}` until
libopus is linked).

## Parameters
* `pcm` — Raw PCM samples as a list of floats (normalised -1.0..1.0)
* `sample_rate` — Sample rate in Hz (typically 48000)
* `sample_rate` — Sample rate in Hz (typically 48000); informational
* `channels` — Channel count (1 = mono, 2 = stereo)
* `bitrate` — Target bitrate in bits/sec (e.g. 32000)
* `bitrate` — Currently ignored; retained for API compatibility

Returns `{:ok, opus_binary}` or `{:error, reason}`.
Returns `{:ok, frame_binary}` or `{:error, reason}`. The binary is
round-trippable through `audio_decode/3`.
"""
@callback audio_encode(
pcm :: [float()],
Expand All @@ -81,16 +102,44 @@ defmodule Burble.Coprocessor.Backend do
) :: {:ok, binary()} | {:error, term()}

@doc """
Decode an Opus frame to raw PCM samples.
Unpack a length-prefixed PCM frame (produced by `audio_encode/4`)
back into normalised float samples.

**This is NOT Opus decoding.** See `audio_encode/4` docs for the
SFU-opaque rationale.

Returns `{:ok, pcm_floats}` or `{:error, reason}`.
Returns `{:ok, pcm_floats}` or `{:error, :invalid_frame}`.
"""
@callback audio_decode(
opus_frame :: binary(),
pcm_frame :: binary(),
sample_rate :: pos_integer(),
channels :: 1 | 2
) :: {:ok, [float()]} | {:error, term()}

@doc """
Transcode raw PCM to a real Opus frame (or real Opus to PCM if `pcm` is a
binary starting with the Opus TOC).

**Currently returns `{:error, :not_implemented}` on all backends.**
This callback exists so that callers intending real Opus transcoding fail
loudly rather than silently round-tripping raw PCM through
`audio_encode/4`. Implementing this requires linking libopus; the decision
is tracked in STATE.a2ml [migration].
"""
@callback opus_transcode(
pcm_or_opus :: [float()] | binary(),
sample_rate :: pos_integer(),
channels :: 1 | 2,
bitrate :: pos_integer()
) :: {:error, :not_implemented}

@doc """
Whether this backend can perform real Opus transcoding.

Returns `false` on every backend until libopus is linked.
"""
@callback opus_available?() :: boolean()

@doc """
Apply noise gate to PCM samples.

Expand Down
26 changes: 21 additions & 5 deletions server/lib/burble/coprocessor/elixir_backend.ex
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@ defmodule Burble.Coprocessor.ElixirBackend do

@impl true
def audio_encode(pcm, _sample_rate, channels, _bitrate) do
# Reference: pack PCM as 16-bit LE integers in a raw frame.
# Real Opus encoding requires the opus NIF or external library.
# This produces a PCM frame that can round-trip through audio_decode.
# PCM frame pack: clamp to [-1.0, 1.0], scale to i16 LE, length-prefix.
# NOT Opus compression — this round-trips raw PCM through audio_decode/3
# for recording, archive, and self-test paths. Real Opus lives in the
# browser's WebRTC encoder; server-side Opus requires linking libopus and
# is gated behind opus_transcode/4 which returns {:error, :not_implemented}.
samples =
pcm
|> Enum.map(fn sample ->
Expand All @@ -66,8 +68,9 @@ defmodule Burble.Coprocessor.ElixirBackend do
end

@impl true
def audio_decode(opus_frame, _sample_rate, _channels) do
case opus_frame do
def audio_decode(pcm_frame, _sample_rate, _channels) do
# PCM frame unpack — inverse of audio_encode/4. NOT Opus decode.
case pcm_frame do
<<_ch::8, len::32-little, data::binary-size(len), _rest::binary>> ->
samples =
for <<sample::little-signed-16 <- data>> do
Expand All @@ -81,6 +84,19 @@ defmodule Burble.Coprocessor.ElixirBackend do
end
end

@impl true
def opus_transcode(_pcm_or_opus, _sample_rate, _channels, _bitrate) do
# Real Opus transcoding is not implemented server-side by design
# (SFU-opaque E2EE model). Linking libopus is a deferred decision
# tracked in STATE.a2ml [migration]. Callers wanting real Opus must
# either (a) rely on the browser's WebRTC Opus encoder/decoder, or
# (b) request libopus integration to be added to this backend.
{:error, :not_implemented}
end

@impl true
def opus_available?, do: false

@impl true
def audio_noise_gate(pcm, threshold_db) do
# Convert dB threshold to linear amplitude.
Expand Down
15 changes: 13 additions & 2 deletions server/lib/burble/coprocessor/smart_backend.ex
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,25 @@ defmodule Burble.Coprocessor.SmartBackend do

@impl true
def audio_encode(pcm, sample_rate, channels, bitrate) do
# PCM frame pack (NOT Opus transcoding — see Backend behaviour docs).
zig_or_elixir().audio_encode(pcm, sample_rate, channels, bitrate)
end

@impl true
def audio_decode(opus_frame, sample_rate, channels) do
zig_or_elixir().audio_decode(opus_frame, sample_rate, channels)
def audio_decode(pcm_frame, sample_rate, channels) do
# PCM frame unpack (NOT Opus decoding).
zig_or_elixir().audio_decode(pcm_frame, sample_rate, channels)
end

@impl true
def opus_transcode(pcm_or_opus, sample_rate, channels, bitrate) do
# Always returns {:error, :not_implemented} — neither backend links libopus.
zig_or_elixir().opus_transcode(pcm_or_opus, sample_rate, channels, bitrate)
end

@impl true
def opus_available?, do: false

@impl true
def audio_noise_gate(pcm, threshold_db) do
# 1.2x Zig advantage — marginal but consistent.
Expand Down
13 changes: 10 additions & 3 deletions server/lib/burble/coprocessor/snif_backend.ex
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,18 @@ defmodule Burble.Coprocessor.SNIFBackend do
do: ZigBackend.audio_encode(pcm, sample_rate, channels, bitrate)

@impl true
def audio_decode(opus_frame, sample_rate, channels),
do: ZigBackend.audio_decode(opus_frame, sample_rate, channels)
def audio_decode(pcm_frame, sample_rate, channels),
do: ZigBackend.audio_decode(pcm_frame, sample_rate, channels)

@impl true
def audio_noise_gate(pcm, threshold_db),
def opus_transcode(pcm_or_opus, sample_rate, channels, bitrate),
do: ZigBackend.opus_transcode(pcm_or_opus, sample_rate, channels, bitrate)

@impl true
def opus_available?, do: false

@impl true
def audio_noise_gate(pcm, threshold_db),
do: ZigBackend.audio_noise_gate(pcm, threshold_db)

@impl true
Expand Down
11 changes: 11 additions & 0 deletions server/lib/burble/coprocessor/zig_backend.ex
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ defmodule Burble.Coprocessor.ZigBackend do
end
end

@impl true
def opus_transcode(_pcm_or_opus, _sample_rate, _channels, _bitrate) do
# Real Opus transcoding is not implemented in the Zig coprocessor either.
# The audio.zig kernel only frames PCM; no libopus is linked. Returning
# :not_implemented explicitly prevents silent round-trip-as-opus bugs.
{:error, :not_implemented}
end

@impl true
def opus_available?, do: false

# ---------------------------------------------------------------------------
# Crypto kernel — always Elixir (Erlang :crypto is native C already)
# ---------------------------------------------------------------------------
Expand Down
89 changes: 89 additions & 0 deletions server/test/burble/coprocessor/opus_contract_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# SPDX-License-Identifier: PMPL-1.0-or-later
#
# Opus contract regression test.
#
# Burble is an E2EE-opaque SFU. Clients perform Opus encoding in the
# browser's WebRTC stack; the server never transcodes live audio. The
# Backend.audio_encode/4 and Backend.audio_decode/3 callbacks pack raw PCM
# into a length-prefixed frame — they do NOT perform Opus compression.
#
# The explicit Backend.opus_transcode/4 callback exists so callers that
# *do* want real Opus transcoding fail loudly with {:error, :not_implemented}
# rather than silently receiving a round-tripped PCM frame.
#
# These tests pin that contract so a future change that silently adds real
# Opus to audio_encode (or removes opus_transcode) will break the suite.

defmodule Burble.Coprocessor.OpusContractTest do
use ExUnit.Case, async: true

alias Burble.Coprocessor.{ElixirBackend, SmartBackend, ZigBackend}

describe "opus_transcode/4 contract" do
test "ElixirBackend returns {:error, :not_implemented}" do
pcm = [0.0, 0.5, -0.5, 0.25]
assert {:error, :not_implemented} =
ElixirBackend.opus_transcode(pcm, 48_000, 1, 32_000)
end

test "ZigBackend returns {:error, :not_implemented}" do
pcm = [0.0, 0.5, -0.5, 0.25]
assert {:error, :not_implemented} =
ZigBackend.opus_transcode(pcm, 48_000, 1, 32_000)
end

test "SmartBackend returns {:error, :not_implemented}" do
pcm = [0.0, 0.5, -0.5, 0.25]
assert {:error, :not_implemented} =
SmartBackend.opus_transcode(pcm, 48_000, 1, 32_000)
end

test "opus_available?/0 is false on every backend" do
refute ElixirBackend.opus_available?()
refute ZigBackend.opus_available?()
refute SmartBackend.opus_available?()
end
end

describe "audio_encode/4 is PCM framing, NOT Opus" do
test "round-trips raw PCM through audio_decode/3 (ElixirBackend)" do
pcm = [0.0, 0.5, -0.5, 0.25, -0.25]
{:ok, frame} = ElixirBackend.audio_encode(pcm, 48_000, 1, 32_000)
{:ok, decoded} = ElixirBackend.audio_decode(frame, 48_000, 1)

# Exact round-trip within quantisation error confirms no Opus
# compression is being applied — real Opus is lossy and would lose
# precision well below the 16-bit quantisation floor we see here.
assert length(decoded) == length(pcm)

Enum.zip(pcm, decoded)
|> Enum.each(fn {orig, dec} ->
assert_in_delta orig, dec, 1.0e-4
end)
end

test "bitrate parameter is ignored (ElixirBackend)" do
pcm = [0.0, 0.5, -0.5]

{:ok, frame_low} = ElixirBackend.audio_encode(pcm, 48_000, 1, 8_000)
{:ok, frame_high} = ElixirBackend.audio_encode(pcm, 48_000, 1, 320_000)

# If bitrate controlled a real codec, low-bitrate frames would be
# shorter than high-bitrate frames. Since this is PCM framing, the
# two outputs are byte-identical regardless of "bitrate".
assert frame_low == frame_high
end

test "frame format is a stable 1-byte channel + 4-byte LE length + i16 LE PCM" do
# Two 16-bit samples × 1 channel, plus 1-byte channels header +
# 4-byte length field = 9 bytes total.
pcm = [0.5, -0.5]
{:ok, frame} = ElixirBackend.audio_encode(pcm, 48_000, 1, 32_000)

assert byte_size(frame) == 1 + 4 + 2 * 2
<<channels::8, len::32-little, _data::binary-size(len), _rest::binary>> = frame
assert channels == 1
assert len == 4
end
end
end
Loading