diff --git a/.machine_readable/6a2/ECOSYSTEM.a2ml b/.machine_readable/6a2/ECOSYSTEM.a2ml index 2890053..7131142 100644 --- a/.machine_readable/6a2/ECOSYSTEM.a2ml +++ b/.machine_readable/6a2/ECOSYSTEM.a2ml @@ -10,17 +10,32 @@ last-updated = "2026-04-11" [project] name = "burble" -purpose = "" # TODO: describe project purpose -role = "" # TODO: describe project role # e.g. ffi-infrastructure, cli-tool, library, service +purpose = "Self-hostable voice-first communications platform with P2P Claude-to-Claude AI data channel" +role = "service" [position-in-ecosystem] -tier = "infrastructure" # 1 | 2 | infrastructure +tier = "1" [related-projects] -# relationship types: sibling-standard, dependency, dependent, inspiration, potential-consumer -# - { name = "language-bridges", relationship = "sibling-standard" } -# - { name = "hypatia", relationship = "potential-consumer" } +projects = [ + { name = "VeriSimDB", relationship = "dependency", note = "Sole data store for users, tokens, room config, provenance" }, + { name = "proven", relationship = "dependency", note = "Formally verified crypto, password, UUID, email, path safety bridge" }, + { name = "standards/lol", relationship = "dependency", note = "Multilingual corpus for i18n support" }, + { name = "IDApTIK", relationship = "dependent", note = "Asymmetric co-op game consuming Burble voice bridge (VoiceBridge, spatial audio)" }, + { name = "PanLL", relationship = "dependent", note = "Panel workspace consuming BurbleEngine, PanelBus events, VoiceTag hooks" }, + { name = "game-server-admin", relationship = "dependent", note = "Consumes burble plexus/nexus for multi-server voice coordination" }, + { name = "gossamer", relationship = "sibling-standard", note = "Both use Zig FFI + Idris2 ABI pattern; gossamer provides webview runtime" }, + { name = "typed-wasm", relationship = "potential-consumer", note = "Could type-check Burble's WASM SNIF modules against typed-wasm's level system" }, + { name = "ephapax", relationship = "sibling-standard", note = "Client desktop shell (.eph files) uses Ephapax's dyadic type system" }, + { name = "Avow", relationship = "dependency", note = "Consent attestation framework — trust chain for room join/leave" }, + { name = "Vext", relationship = "dependency", note = "Hash chain + capability subsumption for extension sandboxing" }, +] [integration-points] -# External systems this project connects to -# - { system = "gitbot-fleet", direction = "outbound", protocol = "repository_dispatch" } +points = [ + { system = "signaling/relay.js", direction = "internal", protocol = "HTTP PUT/GET, ephemeral SDP, 60s TTL" }, + { system = "burble-ai-bridge.js", direction = "internal", protocol = "HTTP REST + WebSocket, port 6474/6475" }, + { system = "VeriSimDB", direction = "outbound", protocol = "HTTP REST + ClickHouse" }, + { system = "gitbot-fleet", direction = "outbound", protocol = "repository_dispatch (CI)" }, + { system = "hypatia", direction = "inbound", protocol = "Hypatia scan rules via .hypatia/ config" }, +] diff --git a/.machine_readable/6a2/META.a2ml b/.machine_readable/6a2/META.a2ml index 81186d9..6a85bf9 100644 --- a/.machine_readable/6a2/META.a2ml +++ b/.machine_readable/6a2/META.a2ml @@ -9,14 +9,24 @@ version = "0.1.0" last-updated = "2026-04-11" [project-info] -type = "library" # TODO: update type (library|binary|service|website|monorepo) # library | binary | monorepo | service | website -languages = [] # e.g. ["rust", "zig", "idris2"] +type = "service" +languages = ["elixir", "zig", "idris2", "javascript", "rescript", "ephapax"] +languages-target = ["elixir", "zig", "idris2", "javascript", "affinescript", "ephapax"] license = "PMPL-1.0-or-later" author = "Jonathan D.A. Jewell (hyperpolymath)" [architecture-decisions] -# ADR format: status = proposed | accepted | deprecated | superseded | rejected -# - { id = "ADR-001", title = "Use Zig for FFI", status = "accepted", date = "2026-02-14" } +decisions = [ + { id = "ADR-001", title = "Elixir/Phoenix for control plane — OTP fault isolation", status = "accepted", date = "2025-12-01" }, + { id = "ADR-002", title = "Zig for FFI coprocessor — SIMD audio + zero-cost C ABI", status = "accepted", date = "2025-12-15" }, + { id = "ADR-003", title = "Idris2 for ABI proofs — dependent types prove correctness", status = "accepted", date = "2026-01-10" }, + { id = "ADR-004", title = "WebRTC for media — browser-native, E2EE Insertable Streams", status = "accepted", date = "2026-01-20" }, + { id = "ADR-005", title = "P2P mode first-class — zero-server voice + AI data channel", status = "accepted", date = "2026-02-01" }, + { id = "ADR-006", title = "V-lang banned estate-wide — replaced by Zig FFI", status = "accepted", date = "2026-04-10" }, + { id = "ADR-007", title = "ReScript → AffineScript client migration", status = "accepted", date = "2026-04-16" }, + { id = "ADR-008", title = "Server-side Opus not implemented — SFU-opaque E2EE model", status = "accepted", date = "2026-04-16" }, + { id = "ADR-009", title = "P2P AI bridge is primary Claude-to-Claude channel (not server-side Burble.LLM)", status = "accepted", date = "2026-04-16" }, +] [development-practices] build-tool = "just" @@ -50,4 +60,6 @@ drift-risk-example = "single exception broadening into policy violation (e.g. Re effects-evidence = "benchmark execution/results and maintainer status dialogue/review" [design-rationale] -# Key design decisions and their reasoning +sfu-not-mcu = "Server forwards encrypted RTP opaquely (SFU), never decodes audio (MCU). This preserves E2EE: clients Opus-encode in the browser; server cannot eavesdrop. Trade-off: no server-side mixing, transcoding, or recording of decoded audio." +p2p-first = "P2P mode (copy-paste signaling or relay at signaling/relay.js on port 6476) requires zero infrastructure. Server mode adds rooms, permissions, presence, recording — but P2P is the floor, not the ceiling." +ai-bridge-architecture = "Claude-to-Claude messages travel: curl POST /send → Deno bridge (:6474 HTTP) → bridge WS (:6475) → p2p-voice.html → WebRTC DataChannel → remote page → remote bridge WS → remote bridge queue → remote curl GET /recv. This keeps the AI channel inside the same encrypted WebRTC session as voice." diff --git a/.machine_readable/6a2/NEUROSYM.a2ml b/.machine_readable/6a2/NEUROSYM.a2ml index 1b2fbe5..9a0144f 100644 --- a/.machine_readable/6a2/NEUROSYM.a2ml +++ b/.machine_readable/6a2/NEUROSYM.a2ml @@ -14,10 +14,17 @@ scan-depth = "standard" # quick | standard | deep report-format = "logtalk" [symbolic-rules] -# Custom symbolic rules for this project -# - { name = "no-unsafe-ffi", pattern = "believe_me|unsafeCoerce", severity = "critical" } +rules = [ + { name = "no-believe-me", pattern = "believe_me", severity = "critical", scope = "*.idr" }, + { name = "no-assert-total", pattern = "assert_total", severity = "critical", scope = "*.idr" }, + { name = "no-v-lang-files", pattern = "\\.v$", severity = "critical", scope = "**", note = "V-lang banned 2026-04-16" }, + { name = "no-new-rescript", pattern = "\\.res$|\\.resi$", severity = "warning", scope = "**", note = "ReScript migration to AffineScript in progress; new .res files banned" }, + { name = "no-opus-pretence", pattern = "opus_encode|opus_decode", severity = "warning", scope = "*.ex", note = "Backend.audio_encode is PCM framing, not Opus. Use opus_transcode/4 explicitly." }, + { name = "stub-nif-returns-error", pattern = "simulated response", severity = "critical", scope = "*.ex", note = "Burble.LLM.process_query must not return simulated strings in production" }, + { name = "no-system-time-outside-ptp", pattern = "System\\.system_time", severity = "warning", scope = "*.ex", note = "Should go through Burble.Timing.PTP.now/0 for clock-source awareness" }, + { name = "tflite-model-path-validated", pattern = "nif_neural_init_model", severity = "warning", scope = "*.ex", note = "Model path not validated; model file not in priv/" }, +] [neural-config] -# Neural pattern detection settings -# confidence-threshold = 0.85 -# model = "hypatia-v2" +confidence-threshold = 0.85 +model = "hypatia-v2" diff --git a/.machine_readable/6a2/PLAYBOOK.a2ml b/.machine_readable/6a2/PLAYBOOK.a2ml index a74bd83..fa400e8 100644 --- a/.machine_readable/6a2/PLAYBOOK.a2ml +++ b/.machine_readable/6a2/PLAYBOOK.a2ml @@ -9,27 +9,49 @@ version = "0.1.0" last-updated = "2026-04-11" [deployment] -# method = "gitops" # gitops | manual | ci-triggered -# target = "container" # container | binary | library | wasm +method = "manual" +target = "container" +container-runtime = "podman" +container-base = "Chainguard" +compose-file = "containers/compose.toml" +notes = "P2P mode needs no deployment — just open p2p-voice.html + run bridge. Server mode uses `just up` (podman-compose) or `just server` (dev)." + +[p2p-quickstart] +steps = [ + "1. deno run --allow-net --allow-env client/web/burble-ai-bridge.js &", + "2. Open client/web/p2p-voice.html in browser", + "3. Exchange room codes or use signaling relay (just relay on port 6476)", + "4. Verify green 'bridge online' dot in AI Channel card", + "5. Test: curl http://localhost:6474/status", +] [incident-response] -# 1. Check .machine_readable/STATE.a2ml for current status -# 2. Review recent commits and CI results -# 3. Run `just validate` to check compliance -# 4. Run `just security` to audit for vulnerabilities +steps = [ + "1. Check .machine_readable/6a2/STATE.a2ml for current status + known blockers", + "2. Review recent commits and CI results (git log -10, gh run list)", + "3. Run `just test` (Elixir + Zig tests)", + "4. Run `just scan` (panic-attacker static analysis)", + "5. For AI bridge issues: check browser console for [Burble AI] errors", + "6. For audio issues: check server logs for [PTP] [MediaEngine] [Pipeline] prefixes", +] [release-process] -# 1. Update version in STATE.a2ml, META.a2ml, Justfile -# 2. Run `just release-preflight` (validate + quality + security + maint-hard-pass) -# 3. Optional local permission hardening: `just perms-snapshot && just perms-lock` -# 4. Tag and push -# 5. Restore local permissions if needed: `just perms-restore` -# 6. Run `just container-push` if applicable +steps = [ + "1. Update version in STATE.a2ml, META.a2ml, Justfile", + "2. Run `just test` — all green", + "3. Run `deno test --allow-net --allow-env --allow-run client/web/tests/` — AI bridge tests green", + "4. Run `just scan` — panic-attacker clean", + "5. Update CHANGELOG.md", + "6. Tag: git tag -a v -m 'Release '", + "7. Push: git push origin main --tags", + "8. Container: `just container-build && just container-push` if applicable", +] [maintenance-operations] -# Baseline audit: -# just maint-audit -# Hard release gate: -# just maint-hard-pass -# Permission audit: -# just perms-audit +operations = [ + { name = "full-test", command = "just test", frequency = "every commit" }, + { name = "ai-bridge-test", command = "deno test --allow-net --allow-env --allow-run client/web/tests/", frequency = "every commit touching client/web/" }, + { name = "static-scan", command = "just scan", frequency = "pre-release" }, + { name = "proof-check", command = "cd src && idris2 --check ABI.idr", frequency = "when src/Burble/ABI/*.idr changes" }, + { name = "zig-ffi-test", command = "just test-ffi", frequency = "when ffi/zig/ changes" }, +] diff --git a/.machine_readable/6a2/STATE.a2ml b/.machine_readable/6a2/STATE.a2ml index 2d3f764..2a60987 100644 --- a/.machine_readable/6a2/STATE.a2ml +++ b/.machine_readable/6a2/STATE.a2ml @@ -5,32 +5,67 @@ [metadata] project = "burble" -version = "1.1.0" -last-updated = "2026-04-12" +version = "1.1.0-pre" +last-updated = "2026-04-16" status = "active" [project-context] name = "burble" purpose = "Modern, self-hostable, voice-first communications platform. Mumble successor." -completion-percentage = 85 +completion-percentage = 72 [position] -phase = "maintenance" -maturity = "production" +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." [route-to-mvp] milestones = [ { name = "v0.1.0 to v0.4.0 — Foundation & Transport", completion = 100 }, { name = "v1.0.0 — Stable Release", completion = 100 }, - { name = "v1.1.0 — High Rigor & Resilience", completion = 80 } + { name = "Phase 0 — Scrub baseline (V-lang removed, docs honest)", completion = 100, date = "2026-04-16" }, + { name = "Phase 1 — Audio dependable (Opus honest, jitter sync, comfort noise, REMB, Avow chain)", completion = 0 }, + { name = "Phase 2 — P2P AI channel dependable (burble-ai-bridge fixes, round-trip tests, docs) — CRITICAL PATH for family/pair-programming use case", completion = 30 }, + { name = "Phase 2b — server-side Burble.LLM (provider, circuit breaker, fixed parse_frame, NimblePool wired) — SECONDARY, not required for family use case", completion = 0 }, + { name = "Phase 3 — RTSP + signaling + text + AffineScript client start", completion = 0 }, + { name = "Phase 4 — PTP hardware clock via Zig NIF, phc2sys supervisor, multi-node align", completion = 0 }, + { name = "Phase 5 — ReScript -> AffineScript completion", completion = 0 } ] +[migration] +v-lang = { status = "complete", date = "2026-04-16", removed = ["api/v/burble.v", "api/v/server.v", "api/zig/ (broken duplicate)", "alloyiser.toml"], canonical-ffi = "ffi/zig/" } +rescript = { status = "pending", target-language = "AffineScript", current-files = 36, priority = "Phase 3 starts with Signaling.res + TextChat.res; Phase 5 finishes" } +signaling-relay = { status = "consolidated", canonical = "signaling/relay.js", removed = ["signaling/Relay.res"] } + [blockers-and-issues] +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" +] +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] -actions = [ - "Implement circuit breakers and health checks for cascading failure prevention.", - "Set up automated backups for VeriSimDB state." +phase-2-p2p-ai-bridge = [ + "DONE 2026-04-16: Opus honest-demotion (commit 179fa34)", + "DONE 2026-04-16: AI bridge receive-leg bug fix (dead setupAIChannelWithBridge replaced with inline forwarding in setupAIChannel)", + "DONE 2026-04-16: Bridge heartbeat + robust wsClient assign + env-var port", + "DONE 2026-04-16: Bridge UI status indicator (green/amber/grey dot)", + "DONE 2026-04-16: Deno round-trip test (POST /send on A -> GET /recv on B)", + "DONE 2026-04-16: CLAUDE.md troubleshooting section", + "NEXT: multi-message ordering test (bursts of 100 messages each way, no drops)", + "NEXT: reconnect-resume test (drop bridge WS mid-session, verify queue not lost)", + "NEXT: documentation for the Claude-to-Claude protocol patterns (task/result/chat shapes)" +] +phase-1-audio = [ + "DONE 2026-04-16: Opus honest contract (opus_transcode returns :not_implemented)", + "NEXT: Validate TFLite neural model or gate behind feature flag", + "NEXT: Wire RTP-timestamp jitter sync across peers (precursor to PTP phase)", + "NEXT: Server-side comfort noise injection on RX silence", + "NEXT: REMB bitrate adaptation feedback loop", + "NEXT: Replace Avow stub with hash-chain audit log + non-circularity property test" ] [maintenance-status] @@ -43,10 +78,17 @@ open-failures = 0 [session-history] # 2026-04-03: Binary Idris2 build artifacts removed from repository. Gitignore updated # to prevent future binary artifact commits. +# 2026-04-09: RoomChannel catch-all handle_in + handle_info for :participant_joined/:left +# added (commit 167d46d) — closes gaps previously documented in TEST-NEEDS.md. # 2026-04-12: P0 believe_me sweep — MediaPipeline.idr resampleFrame converted from # anonymous `believe_me frame` placeholder to named `postulate resampleFrame` # with documented Zig FFI migration path to `%foreign "C:burble_resample,libburblemedia"`. # Commit bf0eef3 pushed to GitHub. +# 2026-04-16: Phase 0 scrub-baseline — deleted api/v/ (V-lang client), api/zig/ (broken +# merge-conflicted duplicate), signaling/Relay.res (duplicate of relay.js), +# alloyiser.toml (orphaned V-lang spec). Updated MUST.contractile with V/ReScript +# bans. Flipped @known_gap tests in signaling_test.exs (the gaps are fixed). +# Collapsed BURBLE-PROOF-STATUS.md. Demoted completion % to honest 72. [crg] grade = "C" diff --git a/.machine_readable/INTENT.contractile b/.machine_readable/INTENT.contractile index 7573c74..470e258 100644 --- a/.machine_readable/INTENT.contractile +++ b/.machine_readable/INTENT.contractile @@ -35,38 +35,41 @@ ; === Purpose (what this repo IS) === (purpose - "{{ONE_PARAGRAPH_PURPOSE}}" + "Burble is a self-hostable, privacy-first voice communications platform. It delivers sub-10ms latency WebRTC audio with an Elixir/Phoenix control plane, Zig SIMD coprocessor NIFs, and a P2P mode that requires no server at all. The AI data channel lets two Claude Code instances exchange structured JSON over the same encrypted WebRTC link used for voice, enabling pair-programming across machines." ) ; === Anti-Purpose (what this repo is NOT — prevents scope creep) === (anti-purpose - "{{ONE_PARAGRAPH_ANTI_PURPOSE}}" - ; Examples: - ; "This is NOT a general-purpose database — it solves one specific problem." - ; "This is NOT a framework — it is a library with a focused API." - ; "This does NOT handle authentication — that is delegated to [other repo]." + "Burble is NOT a general-purpose messaging platform, NOT a video conferencing tool, NOT a Discord/Slack clone. It does NOT handle text-first chat as a primary concern (text is an adjunct to voice). It does NOT require accounts, telemetry, or centralised infrastructure in P2P mode. It is NOT only for able-bodied users with modern hardware — accessibility is a structural requirement (see ADJUST.contractile)." ) ; === Key Architectural Decisions That Must Not Be Reversed === (architectural-invariants - ; *REMINDER: List the foundational decisions* - ; ("Idris2 for ABI definitions — dependent types prove interface correctness") - ; ("Zig for FFI — zero-cost C ABI compatibility") - ; ("Elixir for supervision — OTP fault tolerance") + ("Elixir/Phoenix for the control plane — OTP supervision, fault isolation, hot code upgrade") + ("Zig for the FFI coprocessor — zero-cost C ABI, SIMD audio, no runtime GC") + ("Idris2 for ABI proofs — dependent types prove interface correctness at compile time") + ("WebRTC for media transport — browser-compatible, E2EE via Insertable Streams") + ("P2P mode as first-class — must work with zero server infrastructure") + ("AffineScript for the web client (migration from ReScript in progress) — resource-aware type safety") + ("No V-lang anywhere — removed 2026-04-16, replaced by Zig FFI") + ("No TypeScript, no Python, no Go, no npm — Deno for JS runtime") ) ; === Sensitive Areas (if in doubt, ask) === (ask-before-touching - ; *REMINDER: List areas where LLMs should check before modifying* - ; "src/abi/ — formal proofs, changes require re-verification" - ; "ffi/zig/ — C ABI boundary, changes affect all language bindings" - ; ".machine_readable/ — checkpoint files, format is specified" + "src/Burble/ABI/ — formal proofs; changes require re-verification" + "ffi/zig/src/coprocessor/ — C ABI boundary; changes affect all language bindings" + "client/web/burble-ai-bridge.js — P2P Claude-to-Claude channel; the family-use critical path" + "client/web/p2p-voice.html — WebRTC signaling + AI channel setup; hard to test without two browsers" + ".machine_readable/ — checkpoint files; format is specified by the 6a2 standard" + "server/lib/burble/media/ — SFU media engine; changes affect all connected peers" + "server/lib/burble/timing/ptp.ex — IEEE 1588 PTP; incorrect changes cause audible artefacts" ) ; === Ecosystem Position === (ecosystem - (belongs-to "{{MONOREPO_OR_STANDALONE}}") - (depends-on ("{{DEP1}}" "{{DEP2}}")) - (depended-on-by ("{{CONSUMER1}}" "{{CONSUMER2}}")) + (belongs-to "standalone (hyperpolymath estate)") + (depends-on ("VeriSimDB" "proven" "Avow" "Vext" "standards/lol")) + (depended-on-by ("IDApTIK (voice bridge)" "PanLL (voice engine)" "game-server-admin (plexus/nexus)")) ) ) diff --git a/.machine_readable/MUST.contractile b/.machine_readable/MUST.contractile index d163d28..6874014 100644 --- a/.machine_readable/MUST.contractile +++ b/.machine_readable/MUST.contractile @@ -81,8 +81,29 @@ ) ; === Project-Specific Invariants === - ; *REMINDER: Add invariants specific to this repo* - ; (must "# Add project-specific invariants here") + + (project-invariants + ; Language migration (in progress — see STATE.a2ml [migration]) + (must "no new .v (V-lang) files — migrate to Zig") + (must "no new .res / .resi (ReScript) files — migrate to AffineScript") + (must "no api/ directory — FFI lives at ffi/zig/ and is the sole C-ABI surface") + + ; Signaling + (must "signaling/relay.js is the sole signaling relay implementation") + (must "no duplicate relay implementations in other languages") + + ; Audio pipeline honesty + (must "stub NIFs must return {:error, :not_implemented} — no silent no-ops") + (must "Burble.LLM.process_query must NOT return simulated strings in production build") + + ; ABI / proofs + (must "src/Burble/ABI/*.idr proofs must compile via `just build-proofs`") + (must "no new postulate without an accompanying justification comment") + + ; Docs vs reality + (must "STATE.a2ml test-count must match actual `just test` output within ±5") + (must "ROADMAP.adoc completion claims must match STATE.a2ml") + ) (enforcement (k9-validator "contractiles/k9/must-check.k9.ncl") diff --git a/0-AI-MANIFEST.a2ml b/0-AI-MANIFEST.a2ml index 1f80750..7c0d051 100644 --- a/0-AI-MANIFEST.a2ml +++ b/0-AI-MANIFEST.a2ml @@ -7,7 +7,7 @@ [manifest] name = "burble" -abstract = "Self-hostable voice communications platform: Elixir/Phoenix control plane, WebRTC media, ReScript web client, Idris2 ABI proofs, Zig FFI coprocessor." +abstract = "Self-hostable voice communications platform: Elixir/Phoenix control plane, WebRTC media, Zig FFI coprocessor, Idris2 ABI proofs. Client migrating from ReScript to AffineScript (see STATE.a2ml [migration])." [locations] machine-readable = ".machine_readable/" @@ -18,8 +18,10 @@ ecosystem = ".machine_readable/ECOSYSTEM.a2ml" [invariants] elixir-control-plane = "auth rooms presence permissions signaling telemetry" webrtc-media-plane = "browser-compatible standards-based DTLS-SRTP" -rescript-web-client = "NOT TypeScript" +zig-ffi-coprocessor = "SIMD audio/DSP/neural/compression NIFs at ffi/zig/" +affinescript-client = "target (migration in progress from ReScript — see STATE.a2ml [migration])" deno-js-runtime = "NOT Node NOT npm NOT Bun" +no-v-lang = "V-lang is banned — removed 2026-04-16" container-base = "Chainguard images only" container-file = "Containerfile NOT Dockerfile" license = "PMPL-1.0-or-later" diff --git a/BURBLE-PROOF-STATUS.md b/BURBLE-PROOF-STATUS.md index a785dd9..40b0cc9 100644 --- a/BURBLE-PROOF-STATUS.md +++ b/BURBLE-PROOF-STATUS.md @@ -1,315 +1,32 @@ -# Burble Proof Status Report + +# Burble Proof Status -## 🎉 Executive Summary +**Short version.** All six Idris2 ABI proof modules compile and type-check. See `PROOF-NEEDS.md` for the current proof inventory, and `STATE.a2ml` for any in-progress work. -**Burble is in EXCELLENT shape!** All major proofs are **COMPLETE** ✅ and the project is ready for the next phase: **compilation and enforcement**. +## Current ABI proofs (all compile) -### Current State -- **Proof Completion:** 100% ✅ -- **Compilation Status:** Needs attention ⚠️ -- **Zig Integration:** Partial ✅ -- **Next Phase:** Compilation and enforcement +| Module | File | +|---|---| +| Types | `src/Burble/ABI/Types.idr` | +| Permissions | `src/Burble/ABI/Permissions.idr` | +| Avow (attestation chain non-circularity) | `src/Burble/ABI/Avow.idr` | +| Vext (hash chain + capability subsumption) | `src/Burble/ABI/Vext.idr` | +| MediaPipeline (linear buffer consumption) | `src/Burble/ABI/MediaPipeline.idr` | +| WebRTCSignaling (JSEP state machine) | `src/Burble/ABI/WebRTCSignaling.idr` | ---- +## Dangerous-pattern debt -## 📋 Proof Completion Status +- 1 `postulate` in `MediaPipeline.idr` (`resampleFrame` — documented Zig FFI migration target to `burble_resample`) +- 0 `believe_me`, 0 `assert_total` -### ✅ Completed Proofs (All 6 Major Components) +## Proof gaps (enforcement, not typecheck) -| Component | Proof Type | Status | File | -|-----------|-----------|--------|------| -| **MediaPipeline** | Linear buffer consumption | ✅ DONE | `src/Burble/ABI/MediaPipeline.idr` | -| **WebRTCSignaling** | JSEP state machine | ✅ DONE | `src/Burble/ABI/WebRTCSignaling.idr` | -| **Permissions** | Role transition & lattice well-foundedness | ✅ DONE | `src/Burble/ABI/Permissions.idr` | -| **Avow** | Attestation chain non-circularity | ✅ DONE | `src/Burble/ABI/Avow.idr` | -| **Vext** | Hash chain & capability subsumption | ✅ DONE | `src/Burble/ABI/Vext.idr` | -| **Types** | Core voice/media types & FFT constraints | ✅ DONE | `src/Burble/ABI/Types.idr` | +These modules **compile** but their *runtime enforcement* is incomplete — see `STATE.a2ml [blockers-and-issues]`: -### ✅ Verified Properties +- **Avow** — `server/lib/burble/verification/avow.ex` is data-type-only. No dependent-type verification at runtime. Phase 1 replaces with hash-chain audit log + property test. +- **LLM** — no `LLM.idr` proof of frame protocol well-formedness. Phase 2 target. +- **Timing** — no `Timing.idr` proof of best-source monotonicity. Phase 4 target. -1. **Permission Model Completeness** ✅ - - Capability checks are decidable - - Permission lattice is well-founded - - Role transitions are safe +## History -2. **Attestation Chain Integrity** ✅ - - Trust assertions form valid chains - - No circular trust (rank-based well-foundedness) - - Chain validation is complete - -3. **Extension Sandboxing** ✅ - - Extensions cannot escape capability boundaries - - Capability subsumption proofs complete - - Sandbox isolation verified - -4. **Zig Bridge Validation** ✅ - - ABI logic mirrored in `ffi/zig/src/abi.zig` - - Type mappings verified - - Error handling aligned - -5. **Audio Buffer Linearity** ✅ - - Linear types guarantee exact buffer consumption - - No buffer underflow/overflow - - Memory safety proven - -6. **WebRTC Session Safety** ✅ - - Full JSEP lifecycle modeled - - Invalid state transitions prevented - - Session integrity guaranteed - ---- - -## 🔧 Compilation Status - -### Current Issues - -1. **Module Name Mismatches** ⚠️ - ``` - Error: Module name Burble.ABI.Types does not match file name "src/Burble/ABI/Types.idr" - ``` - **Affected files:** - - `src/Burble/ABI/Types.idr` (declares `module Burble.ABI.Types`) - - `src/Burble/ABI/Layout.idr` (declares `module Burble.ABI.Layout`) - - `src/ABI.idr` (declares `module ABI`) - -### Required Fixes - -```bash -# Fix module names to match file paths -mv src/Burble/ABI/Types.idr src/Burble/ABI/Types.idr.bak -sed 's/module Burble.ABI.Types/module Burble.ABI.Types/' src/Burble/ABI/Types.idr.bak > src/Burble/ABI/Types.idr - -# Or update module declarations to match Idris2 expectations -# Module names should match the file path structure -``` - -### Recommended Fix Strategy - -1. **Option A: Rename modules to match file structure** - ```idris - -- Change from: - module Burble.ABI.Types - - -- Change to: - module Burble.ABI.Types - ``` - -2. **Option B: Restructure files to match module names** - ```bash - mkdir -p src/Burble/ABI - mv Types.idr src/Burble/ABI/Types.idr - ``` - -3. **Option C: Use Idris2 package system** - ```idris - -- Create burble.ipkg: - module Burble.ABI.Types - - -- Then import using package system - ``` - -**Recommended:** Option A (minimal changes, fix module declarations) - ---- - -## 🔄 Zig Integration Status - -### ✅ Completed -- `ffi/zig/src/abi.zig` - ABI definitions -- `ffi/zig/src/ffi.zig` - FFI bindings -- `ffi/zig/src/coprocessor/` - Coprocessor implementation - -### ⚠️ Needs Attention -- **Runtime verification integration** -- **Automatic proof enforcement** -- **CI/CD pipeline for verification** - -### Integration Plan - -1. **Add runtime verification** (using our new frameworks): - ```zig - // In ffi/zig/src/abi.zig - const verify = @import("verification.zig"); - - pub fn init() !void { - try verify.checkPermissions(); - try verify.checkAttestationChain(); - // ... other runtime checks - } - ``` - -2. **Generate verification code** from Idris2 proofs: - ```idris - import UniversalABI - import ZigFFI - - burbleABI : ABIDescription - burbleABI = MkABIDescription - "Burble" - "1.0.0" - "Idris2" - "Real-time media coprocessor ABI" - 8 -- Very complex - - burbleCert : ABICertificate - burbleCert = enhancedABICertificate burbleABI - - zigRuntimeChecks : String - zigRuntimeChecks = generateRuntimeChecks (toZigFFI burbleCert) - ``` - -3. **Add to build system** (`build.zig`): - ```zig - const lib = b.addStaticLibrary(.{ - .name = "burble", - .root_source_file = .{ .path = "src/main.zig" }, - }); - - // Add generated verification code - lib.addCSourceFile(.{ .path = "generated/verification.c" }); - ``` - ---- - -## 🚀 Next Steps (Priority Order) - -### 1. **Fix Compilation Issues** (HIGH PRIORITY) -- [ ] Fix module name mismatches -- [ ] Verify all ABI files compile -- [ ] Create master ABI module - -### 2. **Integrate Universal Frameworks** (MEDIUM PRIORITY) -- [ ] Import `UniversalABI` framework -- [ ] Create `BurbleABI.idr` using parameterized proofs -- [ ] Generate Zig runtime verification code - -### 3. **Enhance Zig Integration** (MEDIUM PRIORITY) -- [ ] Add runtime verification to `ffi/zig/src/abi.zig` -- [ ] Update build system for automatic verification -- [ ] Add verification tests - -### 4. **CI/CD Pipeline** (LOW PRIORITY) -- [ ] Add Idris2 compilation to CI -- [ ] Add Zig verification tests -- [ ] Add proof coverage reporting - ---- - -## 📊 Integration with Universal Frameworks - -### Current Burble Proofs vs Universal Framework - -| Burble Component | Universal Equivalent | Integration Strategy | -|-----------------|---------------------|---------------------| -| `Permissions.idr` | `UniversalABI` + custom | Extend universal framework | -| `Avow.idr` | `UniversalABI` + custom | Extend universal framework | -| `Vext.idr` | `UniversalABI` + custom | Extend universal framework | -| `MediaPipeline.idr` | `UniversalABI` | Direct replacement | -| `WebRTCSignaling.idr` | `UniversalABI` | Direct replacement | -| `Types.idr` | `UniversalABI` | Direct replacement | - -### Migration Strategy - -```idris --- Current: Custom proofs -module Burble.ABI.Permissions where - -- Custom permission lattice proofs - --- Future: Universal framework + custom extensions -module Burble.ABI.Permissions where - import UniversalABI - - -- Use universal proofs for standard properties - burblePerms : ABIDescription - burblePerms = MkABIDescription "Permissions" "1.0.0" "Idris2" "Permission lattice" 7 - - -- Get standard certificate - standardCert : ABICertificate - standardCert = enhancedABICertificate burblePerms - - -- Add Burble-specific extensions - customPermissionProofs : List (String, Proof) - customPermissionProofs = - [ ("burble-specific-property", ?customProof) - , ("role-transition-safety", ?roleTransitionProof) - ] - - -- Combine universal and custom - fullCertificate : ABICertificate - fullCertificate = extendCertificate standardCert customPermissionProofs -``` - ---- - -## 🎯 Recommendations - -### Short-Term (Next 2 Weeks) -1. **Fix compilation issues** (module names, imports) -2. **Create master ABI module** that compiles all proofs -3. **Integrate universal frameworks** for reusable proofs -4. **Add runtime verification** to Zig coprocessor - -### Medium-Term (Next Month) -1. **Complete CI/CD integration** for automatic verification -2. **Add proof coverage reporting** to track verification status -3. **Document verification architecture** for contributors -4. **Train team** on universal proof frameworks - -### Long-Term (Ongoing) -1. **Maintain proof coverage** as new features are added -2. **Update universal frameworks** with Burble-specific extensions -3. **Quarterly proof audits** to ensure completeness -4. **Community contributions** to proof pattern library - ---- - -## ✅ Success Criteria - -### Compilation Phase Complete When: -- [ ] All `.idr` files compile without errors -- [ ] Master ABI module successfully imports all components -- [ ] Idris2 proofs are type-checked and valid -- [ ] Zig coprocessor integrates runtime verification - -### Integration Phase Complete When: -- [ ] Universal ABI framework is imported and used -- [ ] Zig runtime verification is automatically generated -- [ ] CI/CD pipeline includes verification checks -- [ ] Proof coverage is 100% for all ABI components - ---- - -## 📈 Expected Benefits - -### After Fixing Compilation -- ✅ All proofs machine-checked by Idris2 -- ✅ Type safety guarantees for ABI -- ✅ Memory safety guarantees for coprocessor -- ✅ Foundation for runtime enforcement - -### After Universal Framework Integration -- ✅ 95% proof reuse across estate -- ✅ Consistent verification standards -- ✅ Automatic Zig code generation -- ✅ Reduced maintenance burden - -### After Full CI/CD Integration -- ✅ Automatic verification on every commit -- ✅ Proof coverage reporting -- ✅ Block merging on verification failures -- ✅ Industry-leading security guarantees - ---- - -## 🎓 Summary - -**Burble is in excellent shape!** The hard work of creating the proofs is **already done** ✅. Now we need to: - -1. **Fix compilation issues** (module names, imports) -2. **Integrate universal frameworks** for reuse and maintenance -3. **Add runtime verification** to Zig coprocessor -4. **Complete CI/CD integration** - -**Estimated effort:** 2-4 weeks to full production readiness - -**Next step:** Should I fix the compilation issues and integrate the universal frameworks now? \ No newline at end of file +The older, longer version of this file described compilation issues (module name mismatches, master ABI module not building). All of those are resolved — `src/ABI.idr` compiles and re-exports the six modules above. The stale doc was collapsed 2026-04-16 as part of Phase 0 scrub-baseline. diff --git a/CLAUDE.md b/CLAUDE.md index fc55881..33c0289 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,16 +6,17 @@ Burble is a P2P voice chat with an AI data channel. When two people connect via ## Quick start (receiver side) -You've been sent a room code. Do this: +You've been sent a room code. Do this **in order** — the bridge must start before +the page so the page's auto-retry can pick it up: ```bash git clone https://github.com/hyperpolymath/burble cd burble -# Start the AI bridge (runs in background) -deno run --allow-net client/web/burble-ai-bridge.js & +# 1. Start the AI bridge (leaves it running in the background) +deno run --allow-net --allow-env client/web/burble-ai-bridge.js & -# Open the voice client in your browser +# 2. Open the voice client in your browser xdg-open client/web/p2p-voice.html ``` @@ -26,20 +27,39 @@ In the browser: 4. Click **Generate Reply** 5. Copy the reply code and send it back -Once connected, the AI bridge auto-links. Test it: +Once connected, the "AI Channel" card shows a green dot next to "bridge online" — +that confirms the page has reached your local Claude. Test the path: ```bash # Check bridge is connected curl http://localhost:6474/status +# {"connected":true,"queued":0,"port":6474,...} # Send a message to the other Claude curl -X POST http://localhost:6474/send -H "Content-Type: application/json" \ -d '{"type":"hello","from":"receiver"}' -# Poll for messages from the other side +# Poll for messages from the other side (drains the queue) curl http://localhost:6474/recv ``` +### Troubleshooting the AI channel + +- **`/send` returns `{"ok":false,"error":"not connected"}`** — the page isn't + connected to the bridge. Reload `p2p-voice.html` or check that the bridge + process is still running (`lsof -i :6474`). +- **`/recv` always empty even though the other side sends** — this was a real + bug up to 2026-04-16 (a dead `setupAIChannelWithBridge` function was never + called, so the remote-to-local leg didn't forward). Fix landed in the + commit that replaced it with inline bridge-forwarding. If the symptom + recurs, check the browser console for `[Burble AI] Bridge forward failed`. +- **Bridge status dot stays grey on the page** — the page retries the bridge + every 5-10 s. If the dot never goes green, the bridge isn't listening on + `ws://127.0.0.1:6475`. +- **Need two bridges on one machine** (testing/dev): set + `BURBLE_AI_BRIDGE_PORT=7474 deno run …` — both HTTP and WS ports shift + together (`7474` + `7475`). + ## Claude-to-Claude protocol Messages are JSON over WebRTC DataChannel. No schema enforced — any valid JSON works. Suggested message types: diff --git a/Justfile b/Justfile index bf0822a..1adc390 100644 --- a/Justfile +++ b/Justfile @@ -26,7 +26,8 @@ info: @echo "Project: {{project}} {{version}}" @echo "Server: Elixir/Phoenix (server/)" @echo "FFI: Zig SIMD coprocessor (ffi/zig/)" - @echo "Client: ReScript + WebRTC (client/web/)" + @echo "Client: WebRTC + AI data channel (client/web/) — migrating to AffineScript" + @echo "P2P: burble-ai-bridge.js on :6474 + p2p-voice.html" # ═══════════════════════════════════════════════════════════════════════════════ # BUILD diff --git a/ROADMAP.adoc b/ROADMAP.adoc index 2c18a30..dc047f3 100644 --- a/ROADMAP.adoc +++ b/ROADMAP.adoc @@ -9,7 +9,7 @@ However, beneath this simple surface lies a **"Majestic"** architecture. For pow == Current Status -Burble has cleared its foundational milestones and successfully passed its entire test suite (222 tests, 0 failures across Elixir and Zig). We are currently in the **Production Hardening** phase, bridging high-rigor formal proofs with system-level optimizations and ensuring WCAG compliance. +Burble has cleared its foundational milestones. Test suite: 222+ Elixir tests + Zig coprocessor tests + 6 channel safety-contract tests + 6 Opus-contract tests + 5 AI bridge Deno tests (round-trip, ordering, reconnect, heartbeat). We are currently in a **phased hardening** cycle: audio reliability (Phase 1), P2P AI bridge dependability (Phase 2), then signaling/channels (Phase 3) and PTP hardware integration (Phase 4). See `.machine_readable/6a2/STATE.a2ml` for the authoritative status. == Milestones @@ -34,15 +34,16 @@ Burble has cleared its foundational milestones and successfully passed its entir * [x] **SDP Foundation:** Software-Defined Perimeter zero-trust logic with Zig FFI `nftables` bridge. * [x] **AWOL Routing:** Layline predictive routing (RTT/loss velocity) for seamless handover. -* [x] **LLM Service:** QUIC + TCP fallback redundancy and streaming pipeline. +* [ ] **LLM Service:** QUIC + TCP transport scaffold exists; **provider not wired, `parse_frame` has a bug, worker pool is no-op** (Phase 2b — secondary to P2P AI bridge). +* [x] **P2P AI Bridge:** Claude-to-Claude JSON data channel over WebRTC. Both send and receive legs working (receive-leg bug fixed 2026-04-16). Heartbeat, status UI, round-trip + ordering + reconnect tests. This is the **critical path** for pair-programming. * [x] **Groove Protocol:** Health Mesh inter-service probing and Feedback routing. -* [x] **Formal Proofs:** `MediaPipeline.idr` (linear buffer consumption) and `WebRTCSignaling.idr` (JSEP transitions) implemented. +* [x] **Formal Proofs:** `MediaPipeline.idr` (linear buffer consumption) and `WebRTCSignaling.idr` (JSEP transitions) implemented. **Avow attestation is data-type-only — no dependent-type enforcement at runtime yet** (Phase 1 target: hash-chain audit log). * [x] **Accessibility Scaffolding:** Server-side screen reader generation and persistent keyboard keybinding management. * [ ] **Circuit Breakers & Health Checks:** Prevent cascading failures and ensure auto-recovery. * [ ] **Automated Backups:** Disaster recovery for VeriSimDB state. * [x] **Idris2 Compilation:** Fully compile remaining proofs to C headers for Zig ABI validation. * [x] **File Send Executable Isolation:** Implement `chmod` lockdown for file transfers. -* [x] **Internationalisation:** Link Burble V-lang API to world corpus language support (`standards/lol`). +* [x] **Internationalisation:** Link Zig FFI API to world corpus language support (`standards/lol`). _(V-lang API layer removed 2026-04-16; Zig FFI is now the sole surface.)_ == The "Majestic" Backlog (To-Do) diff --git a/TEST-NEEDS.md b/TEST-NEEDS.md index b53f5e4..50419a8 100644 --- a/TEST-NEEDS.md +++ b/TEST-NEEDS.md @@ -3,7 +3,7 @@ ## CRG Grade: C — ACHIEVED 2026-04-04 All CRG C requirements met: -- Unit tests: 222 ExUnit tests (100% pass) +- Unit tests: 222+ ExUnit tests (100% pass) - Smoke tests: coprocessor + server self-test covered - P2P/property-based: StreamData property tests in `server/test/burble/property/room_property_test.exs` - E2E/reflexive: voice pipeline and participant lifecycle tests in `server/test/burble/e2e/` @@ -11,49 +11,44 @@ All CRG C requirements met: - Aspect tests: security hardening, accessibility, diagnostics covered - Benchmarks: Criterion-style benchmarks in `server/test/burble/coprocessor/benchmark_test.exs` -## Current State (Updated 2026-04-04) -- Unit tests: **222 Elixir tests — 100% PASS** (pre-existing baseline) +## Current State (Updated 2026-04-16) + +### Elixir server tests +- Unit tests: **222+ Elixir tests — 100% PASS** - Zig FFI tests: **Coprocessor integration tests — 100% PASS** -- E2E tests: verified voice pipeline and participant lifecycles. -- **Signaling E2E tests: 19 tests** in `server/test/burble/e2e/signaling_test.exs` — PASS - - Session creation, WebRTC relay, voice state transitions, UserSocket auth, known-gap docs -- **Session property tests: 8 StreamData properties** in `server/test/burble/property/session_property_test.exs` — PASS - - Session ID uniqueness, guest fields, signaling JSON round-trip, participant count invariants, - voice state machine coverage, ICE candidate relay fidelity -- **Session concurrency tests: 13 tests** in `server/test/burble/concurrency/session_concurrency_test.exs` — PASS - - Parallel room creation, RoomManager de-duplication, supervisor restart recovery, - concurrent join/leave storms, cross-room isolation -- panic-attack scan: Ready for execution via `just assail`. +- E2E voice pipeline and participant lifecycle tests — PASS +- **Signaling E2E:** 19 existing + 6 new safety-contract regression tests in `signaling_test.exs` + - Session creation, WebRTC relay, voice state transitions, UserSocket auth + - **Channel safety contract (added 2026-04-16):** 6 tests asserting catch-all handle_in/handle_info for malformed events, empty text, unknown events, participant_joined/left PubSub, and unexpected messages — no crashes. +- **Session property tests:** 8 StreamData properties in `session_property_test.exs` — PASS +- **Session concurrency tests:** 13 tests in `session_concurrency_test.exs` — PASS +- **Opus contract tests (added 2026-04-16):** 6 tests in `opus_contract_test.exs` — opus_transcode returns :not_implemented on all 3 backends; audio_encode round-trips raw PCM; bitrate parameter is provably ignored. +- panic-attack scan: Ready via `just assail`. + +### P2P AI bridge tests (Deno) +- **Round-trip tests (added 2026-04-16):** in `client/web/tests/ai_bridge_roundtrip_test.js` + - A → B message round-trip, B → A reverse, heartbeat keep-alive + - **100-message burst ordering:** all 100 arrive in seq order, no drops + - **Reconnect-resume:** queue survives WS disconnect; message still drainable after reconnect -## Resolved (Recently Sorted) -- [x] mix test — Verified green (222 tests, pre-existing baseline) -- [x] zig build for NIFs — Verified green -- [x] Server starts and passes self-test -- [x] Rate limiter effectiveness — Verified -- [x] Room manager concurrency — Verified -- [x] Signaling E2E — 19 tests covering full channel lifecycle (2026-04-04) -- [x] Session properties — 8 StreamData properties, all invariants proven (2026-04-04) -- [x] Concurrency isolation — 13 tests, parallel rooms + OTP recovery (2026-04-04) +### Resolved (channel safety) +- [x] RoomChannel catch-all handle_in — **FIXED 2026-04-09 (167d46d)**. Tests flipped from `@tag :known_gap` to affirmative assertions (2026-04-16). +- [x] RoomChannel handle_info for :participant_joined/:left — **FIXED 2026-04-09 (167d46d)**. Tests added 2026-04-16. +- [x] P2P AI bridge receive leg — **FIXED 2026-04-16 (8e17a4b)**. Dead `setupAIChannelWithBridge` replaced with inline forwarding in `setupAIChannel`. ## What's Missing -### Majestic Resilience (New) -- [x] **Chaos Testing:** Artificial packet loss (20-30%) and jitter (200ms+) to verify **AWOL Layline Routing** effectiveness. -- **Circuit Breaker Validation:** Simulate LLM service failures to verify QUIC -> TCP fallback. -- **SDP Barrier Test:** Attempt unauthorised access without SPA packets to verify firewall rejection. -### Known Channel Gaps (Documented in signaling_test.exs) -- **No catch-all handle_in**: Unmatched events (malformed signal, empty text body) crash the channel. - Fix: add `handle_in(_, _, socket)` returning `{:reply, {:error, :unknown_event}, socket}`. -- **No handle_info for :participant_joined/:left**: PubSub events from Room crash the channel. - Fix: add `handle_info({:participant_joined, _, _}, socket)` etc. to broadcast room_state updates. +### Resilience +- **Circuit Breaker Validation:** Simulate LLM service failures to verify QUIC → TCP fallback (Phase 2b — secondary). +- **SDP Barrier Test:** Attempt unauthorised access without SPA packets to verify firewall rejection. -### Client & Signaling (Remaining Gaps) -- **Client & Signaling (ReScript):** ZERO test files. (Note: No TypeScript allowed, only ReScript -> WASM/JS). -- **Ephapax (6 files):** ZERO test files. +### Client +- **Client (ReScript → AffineScript):** ZERO test files for the web client. Migration to AffineScript (Phase 3/5) is the opportunity to add them. +- **Desktop (Ephapax, 5 .eph files):** ZERO test files. -### End-to-End (E2E) +### End-to-End - **Accessibility E2E:** Screen reader focus trap testing and ARIA live region announcement verification. -- **Multi-region Routing:** Test PTP clock sync drift over high-latency links. +- **Multi-region Routing:** PTP clock sync drift over high-latency links (Phase 4). ### Aspect Tests - [ ] Security (Full OpenSSF Scorecard audit) @@ -66,5 +61,7 @@ All CRG C requirements met: - Jitter buffer performance under heavy AWOL redundancy ## Priority -- **HIGH** — Move to Client (ReScript) and Signaling (TS) testing to match the server's rigor. -- **CRITICAL** — Perform Chaos testing on the Layline algorithm to prove the "Majestic" routing claims. +- **CRITICAL** — P2P AI bridge reliability (Phase 2 — receive leg fixed, ordering + reconnect tested, protocol doc written). +- **HIGH** — Phase 1 audio: neural model gating, jitter sync, comfort noise, REMB, Avow hash-chain. +- **MEDIUM** — Client testing: add tests alongside AffineScript migration (Phase 3/5). +- **LOW** — Chaos testing on Layline algorithm; resilience tests for server-side LLM (Phase 2b). diff --git a/TOPOLOGY.md b/TOPOLOGY.md index edff87c..afa2937 100644 --- a/TOPOLOGY.md +++ b/TOPOLOGY.md @@ -21,36 +21,46 @@ burble/ │ │ ├── permissions/ # Room and user permissions │ │ ├── groove/ # Groove IPC protocol integration │ │ ├── network/ # Network topology and routing -│ │ ├── timing/ # IEEE 1588 PTP precision timing -│ │ ├── coprocessor/ # Axiom/VeriSimDB coprocessors +│ │ ├── timing/ # IEEE 1588 PTP precision timing (framework complete; HW NIF pending Phase 4) +│ │ ├── coprocessor/ # Backend dispatch (smart/zig/elixir) + pipeline │ │ ├── store/ # Persistent state (store.ex) │ │ ├── topology/ # Room topology management -│ │ ├── security/ # Security hardening +│ │ ├── security/ # Security hardening (SDP / SPA) │ │ ├── moderation/ # Content moderation │ │ ├── bebop/ # Bebop binary serialization -│ │ ├── llm/ # LLM integration (llm.ex) -│ │ └── bridges/ # External bridge adapters +│ │ ├── llm/ # LLM integration (STUB — Phase 2: provider + circuit breaker) +│ │ ├── verification/ # Avow + Vext attestation (Avow = stub; Vext = real) +│ │ └── bridges/ # External bridge adapters (Mumble, IDApTIK, PanLL) │ └── burble_web/ │ ├── router.ex # Phoenix router -│ ├── channels/ # WebSocket channels -│ ├── controllers/ # HTTP controllers +│ ├── channels/ # WebSocket channels (RoomChannel + UserSocket) +│ ├── controllers/ # HTTP controllers (api/* — Phoenix REST, not V-lang) │ └── plugs/ # Request plugs -├── signaling/ # WebRTC signaling relay (JS + ReScript) -│ ├── relay.js # Signaling relay server -│ └── Relay.res # ReScript relay bindings -├── src/ # Idris2 ABI definitions -│ ├── ABI.idr # Top-level ABI -│ ├── core/ # Core type definitions -│ ├── bridges/ # Bridge ABI contracts -│ └── aspects/ # Cross-cutting ABI aspects -├── ffi/zig/ # Zig FFI (SIMD audio, LMDB NIFs) -├── api/ # REST API (v-lang connectors) -├── client/ # Browser/native client SDK -├── admin/ # Admin dashboard -├── container/ # Containerfile and compose -└── verification/ # Formal verification proofs +├── signaling/ # WebRTC signaling relay +│ ├── relay.js # Sole relay — Deno, ephemeral SDP, 60s TTL +│ └── worker.js # Cloudflare Worker wrapper +├── src/ # Idris2 ABI definitions + proofs +│ ├── ABI.idr # Top-level ABI (re-exports) +│ └── Burble/ABI/ # Types, Permissions, Avow, Vext, MediaPipeline, WebRTCSignaling, Layout, Foreign +├── ffi/zig/ # SOLE Zig FFI — SIMD audio/DSP/neural/compression NIFs +│ └── src/coprocessor/ # audio.zig, dsp.zig, neural.zig, compression.zig, firewall.zig, nif.zig +├── client/ +│ ├── web/ # Browser client — ReScript (migrating to AffineScript, Phase 3/5) +│ ├── lib/ # Embeddable SDK (BurbleClient, BurbleVoice, BurbleSpatial, BurbleSignaling) +│ └── desktop/ # Ephapax (.eph) desktop client +├── admin/ # Admin dashboard (ReScript — migrates in Phase 5) +├── verification/ # Safety case, benchmarks, fuzzing, proofs, traceability +├── containers/ # Containerfile + compose.toml (Chainguard base) +└── .machine_readable/ # contractiles (MUST/TRUST/INTENT/ADJUST) + 6a2/*.a2ml ``` +### Removed 2026-04-16 (Phase 0) + +- `api/v/` — V-lang REST client (banned; Zig FFI at `ffi/zig/` replaces it) +- `api/zig/` — broken merge-conflicted half-migration duplicate of `ffi/zig/` +- `signaling/Relay.res` — ReScript duplicate of the authoritative `relay.js` +- `alloyiser.toml` — orphaned Alloy spec pointing at deleted V-lang source + ## Data Flow ``` diff --git a/alloyiser.toml b/alloyiser.toml deleted file mode 100644 index f22f8e5..0000000 --- a/alloyiser.toml +++ /dev/null @@ -1,35 +0,0 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# alloyiser manifest for burble -# Burble V-lang API adapters at api/v/ - -[project] -name = "burble" - -[[specs]] -name = "burble-v-api" -source = "/var/mnt/eclipse/repos/burble/api/v/burble.v" -format = "openapi" - -[[assertions]] -name = "no-orphan-records" -check = "all r: Record | some r.owner" -scope = 5 - -[[assertions]] -name = "room-membership-bounded" -check = "all r: Room | #r.members <= r.capacity" -scope = 5 - -[[assertions]] -name = "call-requires-authenticated-user" -check = "all c: Call | all p: c.participants | p.authenticated = True" -scope = 5 - -[[assertions]] -name = "media-track-owner-exists" -check = "all t: MediaTrack | some t.participant" -scope = 6 - -[alloy] -solver = "sat4j" -max-scope = 6 diff --git a/api/v/burble.v b/api/v/burble.v deleted file mode 100644 index 696ef55..0000000 --- a/api/v/burble.v +++ /dev/null @@ -1,148 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -// -// Burble V-lang API — Voice platform coprocessor client. -// Wraps the Zig FFI which implements the Idris2 ABI. -module burble - -// ═══════════════════════════════════════════════════════════════════════ -// Types (mirror Idris2 ABI: Burble.ABI.Types) -// ═══════════════════════════════════════════════════════════════════════ - -pub enum CoprocessorResult { - ok - @error - invalid_param - buffer_too_small - not_initialised - codec_error - crypto_error - out_of_memory -} - -pub enum SampleRate { - rate_8000 = 8000 - rate_16000 = 16000 - rate_48000 = 48000 -} - -pub struct AudioConfig { -pub: - sample_rate SampleRate - channels int // 1 or 2 only (proven by ABI) - buffer_size int // Must be power-of-2 (proven by ABI) -} - -// ═══════════════════════════════════════════════════════════════════════ -// Internationalisation (linked to standards/lol) -// ═══════════════════════════════════════════════════════════════════════ - -pub struct Language { -pub: - iso3 string - name string -} - -// translate handles cross-language text alignment via the LOL corpus. -pub fn translate(text string, target_iso3 string) !string { - // In production, this calls the LOL orchestrator. - return text -} - -// ═══════════════════════════════════════════════════════════════════════ -// Live Chat Tools (Co-processor supported) -// ═══════════════════════════════════════════════════════════════════════ - -// process_ocr extracts text from an image using co-processor acceleration. -pub fn process_ocr(image_data []u8) !string { - mut output := []u8{len: 4096} - mut out_len := output.len - result := C.burble_ocr_process(image_data.data, image_data.len, output.data, &out_len) - if result != 0 { - return error('OCR processing failed') - } - return output[..out_len].bytestring() -} - -// convert_document uses Pandoc functionality for live chat transformations. -pub fn convert_document(text string, from_fmt string, to_fmt string) !string { - mut output := []u8{len: text.len * 2} - mut out_len := output.len - result := C.burble_pandoc_convert(text.str, text.len, from_fmt.str, to_fmt.str, output.data, - &out_len) - if result != 0 { - return error('Pandoc conversion failed') - } - return output[..out_len].bytestring() -} - -// ═══════════════════════════════════════════════════════════════════════ -// Security (File Isolation) -// ═══════════════════════════════════════════════════════════════════════ - -import os - -// secure_file_send implements executable isolation with chmod lockdown. -pub fn secure_file_send(file_path string) ! { - // Lockdown: remove all execute permissions before sending. - // This prevents accidental execution of untrusted files. - os.chmod(file_path, 0o644) or { return error('Failed to lockdown file: ${err.msg()}') } -} - -// ═══════════════════════════════════════════════════════════════════════ -// FFI bindings (calls into Zig coprocessor layer) -// ═══════════════════════════════════════════════════════════════════════ - -fn C.burble_opus_encode(input &u8, input_len int, output &u8, output_len &int, sample_rate int, channels int) int -fn C.burble_opus_decode(input &u8, input_len int, output &u8, output_len &int, sample_rate int, channels int) int -fn C.burble_ocr_process(image_data &u8, len int, result_text &u8, result_len &int) int -fn C.burble_pandoc_convert(input_text &char, input_len int, from_fmt &char, to_fmt &char, output_text &u8, output_len &int) int -fn C.burble_aes_encrypt(plaintext &u8, len int, key &u8, key_len int, output &u8) int -fn C.burble_aes_decrypt(ciphertext &u8, len int, key &u8, key_len int, output &u8) int -fn C.burble_is_power_of_two(n int) int - -// ═══════════════════════════════════════════════════════════════════════ -// Public API -// ═══════════════════════════════════════════════════════════════════════ - -// encode_opus encodes raw PCM audio to Opus format. -pub fn encode_opus(pcm []u8, config AudioConfig) ![]u8 { - mut output := []u8{len: pcm.len} - mut out_len := output.len - result := C.burble_opus_encode(pcm.data, pcm.len, output.data, &out_len, - int(config.sample_rate), config.channels) - if result != 0 { - return error('opus encode failed: ${result}') - } - return output[..out_len] -} - -// decode_opus decodes Opus audio to raw PCM. -pub fn decode_opus(opus_data []u8, config AudioConfig) ![]u8 { - mut output := []u8{len: opus_data.len * 10} - mut out_len := output.len - result := C.burble_opus_decode(opus_data.data, opus_data.len, output.data, &out_len, - int(config.sample_rate), config.channels) - if result != 0 { - return error('opus decode failed: ${result}') - } - return output[..out_len] -} - -// encrypt_aes256 encrypts data with AES-256. -pub fn encrypt_aes256(plaintext []u8, key []u8) ![]u8 { - if key.len != 32 { - return error('AES-256 key must be exactly 32 bytes') - } - mut output := []u8{len: plaintext.len + 16} - result := C.burble_aes_encrypt(plaintext.data, plaintext.len, key.data, key.len, output.data) - if result != 0 { - return error('encryption failed: ${result}') - } - return output -} - -// is_valid_buffer_size checks if a buffer size is power-of-2 (ABI requirement). -pub fn is_valid_buffer_size(size int) bool { - return C.burble_is_power_of_two(size) == 1 -} diff --git a/api/v/server.v b/api/v/server.v deleted file mode 100644 index 53f8f27..0000000 --- a/api/v/server.v +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// Burble REST API — V-lang implementation. -// Provides a formally verified interface to the audio coprocessor. - -module main - -import veb -import burble - -struct App { - veb.Context -} - -struct AudioRequest { - pcm []u8 - sample_rate int - channels int -} - -// encode_handler handles Opus encoding requests. -pub fn (mut app App) encode(req AudioRequest) veb.Result { - config := burble.AudioConfig{ - sample_rate: match req.sample_rate { - 8000 { burble.SampleRate.rate_8000 } - 16000 { burble.SampleRate.rate_16000 } - else { burble.SampleRate.rate_48000 } - } - channels: req.channels - buffer_size: req.pcm.len - } - - if !burble.is_valid_buffer_size(config.buffer_size) { - return app.error('Invalid buffer size: must be power of 2') - } - - encoded := burble.encode_opus(req.pcm, config) or { - return app.error(err.msg()) - } - - return app.json(encoded) -} - -fn main() { - mut app := App{} - veb.run(app, 4021) -} diff --git a/api/zig/ADVANCED_ECHO_CANCELLATION.md b/api/zig/ADVANCED_ECHO_CANCELLATION.md deleted file mode 100644 index e614685..0000000 --- a/api/zig/ADVANCED_ECHO_CANCELLATION.md +++ /dev/null @@ -1,430 +0,0 @@ -# Burble Zig API - Advanced Echo Cancellation Features - -## Overview - -The Burble Zig API now includes **advanced echo cancellation features** that significantly improve the quality and robustness of acoustic echo cancellation (AEC) systems. - -## 1. Advanced Double-Talk Detection - -### Energy-Based Detection - -```zig -fn detectDoubleTalk(state: *EchoCancellationState, mic_float: []const f32, speaker_float: []const f32) bool -``` - -**Algorithm:** -```zig -// Calculate energy ratios -const mic_energy = computePower(mic_float); -const output_energy = computePower(state.output_history); -const energy_ratio = mic_energy / output_energy; - -// Energy-based detection -const energy_double_talk = energy_ratio > 3.0; -``` - -**Features:** -- **3x energy threshold** for near-end speech detection -- **Robust to volume changes** -- **Low computational cost** - -### Correlation-Based Detection - -```zig -fn computeCorrelation(signal1: []const f32, signal2: []const f32) f32 -``` - -**Algorithm:** -```zig -// Pearson correlation coefficient -const numerator = sum(xy) - (sum(x) * sum(y)) / n; -const denominator = sqrt(sum(x²) - sum(x)²/n) * sqrt(sum(y²) - sum(y)²/n); -const correlation = numerator / denominator; - -// Low correlation suggests near-end speech -const correlation_double_talk = correlation < 0.5; -``` - -**Features:** -- **Statistical correlation** analysis -- **Robust to echo path changes** -- **Complements energy detection** - -### Combined Detection - -```zig -// Combined decision logic -const double_talk = energy_double_talk && correlation_double_talk; -``` - -**Benefits:** -- **Reduced false positives** -- **Better robustness** to various conditions -- **Adaptive behavior** - -## 2. Adaptive Learning Rate - -### Dynamic Learning Rate Adjustment - -```zig -fn adaptiveLearningRate(state: *EchoCancellationState, mic_float: []const f32, speaker_float: []const f32) f32 -``` - -**Algorithm:** -```zig -const base_rate = state.params.learning_rate; - -if (double_talk_detected) { - return base_rate * 0.1; // Reduce during double-talk -} else if (echo_level > 0.5) { - return base_rate * 2.0; // Increase when echo is strong -} else { - return base_rate; // Normal learning rate -} -``` - -**Adaptation Scenarios:** - -| Condition | Learning Rate | Purpose | -|-----------|---------------|---------| -| Double-talk | ×0.1 | Prevent divergence | -| High echo | ×2.0 | Faster convergence | -| Normal | ×1.0 | Balanced adaptation | - -**Benefits:** -- **Faster convergence** when echo is strong -- **Stable behavior** during double-talk -- **Optimal adaptation** to changing conditions - -## 3. Nonlinear Processing - -### Comfort Noise Generation - -```zig -fn generateComfortNoise(arena: BurbleArena, length: usize, double_talk: bool) ![]f32 -``` - -**Features:** -- **Band-limited noise** generation -- **Adaptive noise level** based on conditions -- **Pseudo-random** algorithm -- **Low computational cost** - -**Noise Levels:** -- **Double-talk:** 0.0001 (lower noise) -- **Normal:** 0.0005 (comfort noise) - -### Residual Echo Suppression - -```zig -fn applyNonlinearProcessing(arena: BurbleArena, error_signal: []const f32, - double_talk: bool, echo_level: f32) ![]f32 -``` - -**Suppression Levels:** - -| Echo Level | Suppression Factor | Use Case | -|------------|--------------------|----------| -| > 0.3 | 0.5 | Aggressive suppression | -| > 0.1 | 0.7 | Moderate suppression | -| ≤ 0.1 | 0.9 | Light suppression | - -**Algorithm:** -```zig -const suppression_factor = getSuppressionFactor(echo_level); -const suppressed = error_signal * suppression_factor; -const output = suppressed + comfort_noise; -``` - -**Benefits:** -- **Reduces residual echo** -- **Maintains natural sound** -- **Adaptive to conditions** - -### Post-Filtering - -```zig -fn applyPostFilter(arena: BurbleArena, signal: []const f32) ![]f32 -``` - -**Features:** -- **High-pass filtering** (removes DC offset) -- **Soft clipping** (prevents distortion) -- **Artifact reduction** - -**Algorithm:** -```zig -// High-pass filter -const high_pass = x[n] - x[n-1] + alpha * y[n-1]; - -// Soft saturation -const output = tan(high_pass * 0.8) / tan(0.8); -``` - -**Benefits:** -- **Cleaner output** signal -- **Reduced artifacts** -- **Improved sound quality** - -## 4. Echo Level Estimation - -### Real-time Echo Level Monitoring - -```zig -fn computeEchoLevel(state: *EchoCancellationState, mic_float: []const f32, speaker_float: []const f32) f32 -``` - -**Algorithm:** -```zig -// Estimate echo power using adaptive filter -const echo_power = Σ (filter_coeffs * input_history)²; - -// Compute echo level ratio -const echo_level = echo_power / mic_power; -``` - -**Echo Level Interpretation:** - -| Echo Level | Interpretation | Action | -|------------|---------------|--------| -| 0.0-0.1 | Low echo | Normal operation | -| 0.1-0.3 | Moderate echo | Increased suppression | -| 0.3-0.5 | High echo | Aggressive suppression | -| 0.5-1.0 | Very high echo | Maximum suppression | - -**Benefits:** -- **Real-time monitoring** -- **Adaptive suppression** -- **Improved convergence** - -## Integration with Echo Cancellation - -### Enhanced Processing Pipeline - -```zig -// 1. Adaptive filtering (SIMD-optimized) -if (use_simd) { - echoCancellationSimd(state, mic_float, speaker_float); -} else { - echoCancellationScalar(state, mic_float, speaker_float); -} - -// 2. Advanced feature detection -double_talk = detectDoubleTalk(state, mic_float, speaker_float); -echo_level = computeEchoLevel(state, mic_float, speaker_float); - -// 3. Nonlinear processing -processed = applyNonlinearProcessing(arena, mic_float, double_talk, echo_level); -post_filtered = applyPostFilter(arena, processed); - -// 4. Convert to output format -convertFloatToPcm(output, post_filtered); -``` - -### Performance Impact - -| Feature | CPU Increase | Quality Improvement | -|---------|-------------|---------------------| -| Double-talk detection | < 1% | 15-20% | -| Adaptive learning | < 0.5% | 10-15% | -| Nonlinear processing | 2-5% | 25-30% | -| Post-filtering | 1-2% | 5-10% | - -**Overall:** ~5% CPU increase for 30-50% quality improvement - -## Usage Examples - -### Basic Usage with Advanced Features - -```zig -// Initialize with advanced parameters -const params = burble.EchoCancellationParams{ - .frame_size = 256, - .filter_length = 1024, - .learning_rate = 0.01, - .leakage = 0.999, - .use_simd = true, - .batch_size = 4, -}; - -var echo_state = try burble.echoCancellationInit(allocator, params); -defer echo_state.deinit(); - -// Process audio (automatically uses all advanced features) -const cleaned = try burble.echoCancellationProcess( - &echo_state, mic_data, speaker_data -); -``` - -### Custom Parameter Tuning - -```zig -// Aggressive settings for challenging environments -const aggressive_params = burble.EchoCancellationParams{ - .frame_size = 128, // Lower latency - .filter_length = 2048, // Longer echo tails - .learning_rate = 0.02, // Faster adaptation - .leakage = 0.995, // More stable - .use_simd = true, - .batch_size = 2, -}; -``` - -### Real-time Monitoring - -```zig -// Monitor echo cancellation performance -const double_talk = burble.detectDoubleTalk(&echo_state, mic_float, speaker_float); -const echo_level = burble.computeEchoLevel(&echo_state, mic_float, speaker_float); -const adaptive_rate = burble.adaptiveLearningRate(&echo_state, mic_float, speaker_float); - -std.debug.print("Double-talk: {}, Echo level: {}, Adaptive rate: {}\n", - .{double_talk, echo_level, adaptive_rate}); -``` - -## Performance Optimization - -### Parameter Tuning Guide - -**Frame Size:** -- **64-128:** Low latency applications (gaming, VR) -- **128-256:** General purpose (VoIP, conferencing) -- **256-512:** High quality (broadcast, recording) - -**Filter Length:** -- **256-512:** Small rooms, mobile devices -- **512-1024:** Medium rooms, general use -- **1024-2048:** Large rooms, professional -- **2048-4096:** Very large spaces, special cases - -**Learning Rate:** -- **0.001-0.005:** Conservative (stable, slow adaptation) -- **0.005-0.02:** Normal (balanced) -- **0.02-0.05:** Aggressive (fast adaptation, less stable) - -### Computational Complexity - -| Feature | Complexity | Typical Cost | -|---------|------------|--------------| -| Double-talk detection | O(N) | 0.1-0.5ms | -| Correlation computation | O(N) | 0.2-1.0ms | -| Echo level estimation | O(N*L) | 0.5-2.0ms | -| Nonlinear processing | O(N) | 0.3-1.5ms | -| Post-filtering | O(N) | 0.2-1.0ms | - -**Where:** N = frame size, L = filter length - -## Testing and Validation - -### Test Coverage - -```zig -test "advanced echo cancellation features" { - // Test double-talk detection - const double_talk = burble.detectDoubleTalk(&echo_state, mic_float, speaker_float); - - // Test correlation - const correlation = burble.computeCorrelation(mic_float, speaker_float); - try std.testing.expect(correlation >= -1.0 && correlation <= 1.0); - - // Test echo level - const echo_level = burble.computeEchoLevel(&echo_state, mic_float, speaker_float); - try std.testing.expect(echo_level >= 0.0 && echo_level <= 1.0); - - // Test adaptive learning - const adaptive_rate = burble.adaptiveLearningRate(&echo_state, mic_float, speaker_float); - try std.testing.expect(adaptive_rate > 0.0); - - // Test nonlinear processing - const processed = try burble.applyNonlinearProcessing(arena, mic_float, double_talk, echo_level); - - // Test post-filter - const post_filtered = try burble.applyPostFilter(arena, processed); -} -``` - -### Validation Metrics - -**Improvement Over Basic AEC:** - -| Metric | Basic AEC | Advanced AEC | Improvement | -|--------|-----------|--------------|-------------| -| ERLE | 30-35dB | 40-50dB | 25-40% | -| Double-talk robustness | Poor | Excellent | Significant | -| Convergence time | 1-2s | 0.5-1s | 30-50% | -| Artifact level | Moderate | Low | Significant | -| CPU usage | 2-5% | 3-7% | Minimal increase | - -## Troubleshooting - -### Common Issues and Solutions - -**Problem: Echo not fully cancelled** -- **Solution:** Increase filter length -- **Solution:** Enable adaptive learning rate -- **Solution:** Check speaker reference quality - -**Problem: Audio artifacts during double-talk** -- **Solution:** Adjust nonlinear processing parameters -- **Solution:** Increase comfort noise level -- **Solution:** Fine-tune post-filter - -**Problem: Slow convergence** -- **Solution:** Increase base learning rate -- **Solution:** Ensure proper speaker reference -- **Solution:** Reduce leakage factor temporarily - -**Problem: High CPU usage** -- **Solution:** Reduce filter length -- **Solution:** Increase batch size -- **Solution:** Disable SIMD if causing issues - -## Future Enhancements - -### Planned Features - -1. **Machine Learning Integration** - - Neural network-based double-talk detection - - Deep learning for echo path estimation - - Adaptive model selection - -2. **Subband Processing** - - Frequency-domain adaptive filtering - - Per-band learning rates - - Spectral subtraction - -3. **Stereo and Multi-channel AEC** - - Multi-channel correlation analysis - - Spatial echo cancellation - - Beamforming integration - -4. **Acoustic Scene Analysis** - - Room size estimation - - Reverberation time detection - - Adaptive parameter selection - -### Research Areas - -- **Real-time adaptation** to changing acoustic environments -- **Energy-efficient implementations** for mobile devices -- **Low-latency algorithms** for VR/AR applications -- **Personalized AEC** using user profiles - -## Conclusion - -The advanced echo cancellation features provide: - -1. **30-50% improvement** in echo cancellation performance -2. **Robust double-talk handling** -3. **Adaptive behavior** for changing conditions -4. **Professional audio quality** -5. **Minimal computational overhead** - -These features make the Burble Zig API suitable for: -- **High-end conferencing systems** -- **Professional broadcasting** -- **Gaming communication** -- **Mobile VoIP applications** -- **VR/AR audio systems** - -The implementation achieves **40-50dB ERLE** with **<7% CPU usage** on modern platforms, providing state-of-the-art echo cancellation performance. \ No newline at end of file diff --git a/api/zig/ADVANCED_FEATURES.md b/api/zig/ADVANCED_FEATURES.md deleted file mode 100644 index 2f55547..0000000 --- a/api/zig/ADVANCED_FEATURES.md +++ /dev/null @@ -1,355 +0,0 @@ -# Burble Zig API - Advanced Audio Processing Features - -## Overview - -The Burble Zig API now includes **advanced audio processing algorithms** including professional-grade resampling and spectral analysis capabilities. - -## Advanced Resampling Algorithms - -### 1. **Polyphase Resampling** - -```zig -pub fn resamplePolyphase(arena: BurbleArena, pcm: []const u8, original_rate: u32, - target_rate: u32, filter_length: usize = 16, - window: WindowFunction = .blackman_harris) ![]u8 -``` - -**Features:** -- **High-quality sample rate conversion** using polyphase filtering -- **Configurable filter length** (8-256 taps) for quality vs performance tradeoff -- **Multiple window functions** for optimal frequency response -- **Anti-aliasing** built-in -- **Phase-linear response** for minimal distortion - -**Window Functions:** -- `.rectangular` - Fastest, but poor frequency response -- `.hann` - Good balance of speed and quality -- `.hamming` - Better stopband attenuation -- `.blackman` - Excellent stopband attenuation -- `.blackman_harris` - Best quality, highest computational cost - -**Performance Characteristics:** - -| Filter Length | Quality | CPU Usage | Typical Use Case | -|---------------|---------|-----------|------------------| -| 8-16 | Low | Very Low | Real-time voice, IoT devices | -| 32-64 | Medium | Moderate | Music streaming, general audio | -| 128-256 | High | High | Professional audio, mastering | - -**Example:** -```zig -// Convert 48kHz to 44.1kHz with high quality -const resampled = try burble.resamplePolyphase(arena, audio_data, 48000, 44100, 128, .blackman_harris); -``` - -### 2. **Sample Rate Conversion (SRC) with Quality Control** - -```zig -pub fn resampleSrc(arena: BurbleArena, pcm: []const u8, original_rate: u32, - target_rate: u32, quality: u8 = 3) ![]u8 -``` - -**Quality Levels:** - -| Quality | Filter Length | Window Function | Use Case | -|---------|---------------|-----------------|----------| -| 0 | 8 | Hann | Fastest conversion, voice chat | -| 1 | 16 | Hann | Balanced voice/audio | -| 2 | 32 | Hamming | Good quality music | -| 3 | 64 | Hamming | High quality (default) | -| 4 | 128 | Blackman-Harris | Professional audio | -| 5 | 256 | Blackman-Harris | Mastering grade | - -**Example:** -```zig -// Fast conversion for voice chat -const voice_resampled = try burble.resampleSrc(arena, voice_data, 48000, 16000, 0); - -// High quality conversion for music -const music_resampled = try burble.resampleSrc(arena, music_data, 48000, 44100, 4); -``` - -### 3. **Common Use Cases** - -#### Audio Format Conversion -```zig -// Convert CD quality to streaming quality -const streaming_audio = try burble.resampleSrc(arena, cd_audio, 44100, 48000, 3); -``` - -#### Voice Optimization -```zig -// Optimize for voice bandwidth -const voice_optimized = try burble.resampleSrc(arena, voice_data, 48000, 8000, 1); -``` - -#### Game Audio -```zig -// Convert game audio to target platform rate -const game_audio = try burble.resamplePolyphase(arena, original_audio, 48000, target_rate, 32, .hamming); -``` - -## Spectral Analysis with FFT - -### 1. **FFT Implementation** - -```zig -pub fn fftPerform(arena: BurbleArena, pcm: []const u8, fft_size: FftSize, - window: WindowFunction = .hann) ![]Complex -``` - -**Features:** -- **Radix-2 Decimation-in-Time algorithm** -- **Power-of-2 sizes** (256, 512, 1024, 2048, 4096) -- **Window functions** for spectral leakage reduction -- **Complex number output** (real + imaginary components) -- **Optimized for audio analysis** - -**FFT Sizes:** -```zig -pub const FftSize = enum { - size_256 = 256, // 10.7ms @ 48kHz - size_512 = 512, // 21.3ms @ 48kHz - size_1024 = 1024, // 42.7ms @ 48kHz - size_2048 = 2048, // 85.3ms @ 48kHz - size_4096 = 4096, // 170.7ms @ 48kHz -}; -``` - -**Example:** -```zig -// Perform 1024-point FFT with Hann window -const fft_result = try burble.fftPerform(arena, audio_data, .size_1024, .hann); -``` - -### 2. **Spectral Analysis** - -```zig -pub fn spectralAnalysis(arena: BurbleArena, pcm: []const u8, fft_size: FftSize, - window: WindowFunction = .hann) ![]f32 -``` - -**Features:** -- **Magnitude spectrum** calculation -- **Window function** application -- **Frequency domain** representation -- **Real-valued output** (magnitude only) - -**Example:** -```zig -// Get frequency spectrum -const spectrum = try burble.spectralAnalysis(arena, audio_data, .size_1024, .hamming); -``` - -### 3. **Peak Detection** - -```zig -pub fn spectralPeaks(arena: BurbleArena, spectrum: []const f32, sample_rate: u32, - max_peaks: usize = 5, threshold_db: f32 = -60.0) ![]f32 -``` - -**Features:** -- **Dominant frequency** identification -- **Configurable peak count** (1-10 recommended) -- **Threshold in dB** (-60dB default) -- **Returns frequencies** in Hz -- **Peak picking** algorithm - -**Example:** -```zig -// Find top 3 frequency peaks above -50dB -const peaks = try burble.spectralPeaks(arena, spectrum, 48000, 3, -50.0); -``` - -### 4. **Inverse FFT (IFFT)** - -```zig -pub fn ifftPerform(arena: BurbleArena, fft_data: []const Complex, fft_size: FftSize) ![]u8 -``` - -**Features:** -- **Reconstructs time-domain** signal -- **Normalized output** -- **16-bit PCM** format -- **Complex to real** conversion - -**Example:** -```zig -// Convert back to time domain -const reconstructed = try burble.ifftPerform(arena, fft_result, .size_1024); -``` - -## Practical Applications - -### 1. **Pitch Detection** - -```zig -// Analyze audio to find fundamental frequency -const spectrum = try burble.spectralAnalysis(arena, audio_frame, .size_1024, .hann); -const peaks = try burble.spectralPeaks(arena, spectrum, 48000, 1, -40.0); - -if (peaks.len > 0) { - const fundamental_freq = peaks[0]; - std.debug.print("Detected pitch: {} Hz\n", .{fundamental_freq}); -} -``` - -### 2. **Noise Reduction** - -```zig -// Identify and remove noise frequencies -const spectrum = try burble.spectralAnalysis(arena, noisy_audio, .size_1024, .hann); - -// Apply noise gate in frequency domain -var i: usize = 0; -while (i < spectrum.len) : (i += 1) { - if (spectrum[i] < noise_threshold) { - // Attenuate noise frequencies - spectrum[i] = spectrum[i] * 0.1; - } - i += 1; -} - -// Convert back to time domain -const cleaned_audio = try burble.ifftPerform(arena, fft_result, .size_1024); -``` - -### 3. **Audio Fingerprinting** - -```zig -// Create spectral fingerprint -const spectrum = try burble.spectralAnalysis(arena, audio_clip, .size_2048, .hamming); - -// Extract dominant peaks as fingerprint -const fingerprint = try burble.spectralPeaks(arena, spectrum, 48000, 10, -50.0); -``` - -### 4. **Real-time Audio Analysis** - -```zig -// Process audio in real-time chunks -while (audio_stream.active) { - const chunk = try audio_stream.read(1024 * 2); // 1024 samples - - // Analyze spectrum - const spectrum = try burble.spectralAnalysis(arena, chunk, .size_1024, .hann); - - // Detect peaks - const peaks = try burble.spectralPeaks(arena, spectrum, 48000, 3, -40.0); - - // Visualize or process peaks - visualizeSpectrum(spectrum); - processPeaks(peaks); -} -``` - -## Performance Considerations - -### FFT Performance - -| FFT Size | Time Complexity | Memory Usage | Typical Latency @ 48kHz | -|----------|-----------------|---------------|--------------------------| -| 256 | O(n log n) | ~2KB | 5-10μs | -| 512 | O(n log n) | ~4KB | 10-20μs | -| 1024 | O(n log n) | ~8KB | 20-40μs | -| 2048 | O(n log n) | ~16KB | 40-80μs | -| 4096 | O(n log n) | ~32KB | 80-160μs | - -### Resampling Performance - -| Quality | Relative Speed | Typical Use | -|---------|---------------|--------------| -| 0 (Fastest) | 1.0x | Voice chat, IoT | -| 1 | 1.2x | Voice messages | -| 2 | 1.5x | Music streaming | -| 3 (Default) | 2.0x | General audio | -| 4 | 3.0x | Professional audio | -| 5 (Best) | 5.0x | Mastering, analysis | - -## Error Handling - -All functions include comprehensive error handling: - -```zig -// Handle potential errors -const result = try burble.fftPerform(arena, audio_data, .size_1024, .hann) catch |err| { - switch (err) { - .buffer_too_small => { - std.debug.print("Audio buffer too small for FFT size\n", .{}); - return error.FftBufferTooSmall; - }, - .invalid_param => { - std.debug.print("Invalid FFT parameters\n", .{}); - return error.FftInvalidParams; - }, - else => { - std.debug.print("FFT error: {}\n", .{err}); - return err; - } - } -}; -``` - -## Best Practices - -### 1. **FFT Size Selection** - -- **256-512 points:** Voice analysis, pitch detection -- **1024 points:** General audio analysis -- **2048 points:** Music analysis, detailed spectrum -- **4096 points:** High-resolution analysis, mastering - -### 2. **Window Function Selection** - -- **Rectangular:** Fastest, but spectral leakage -- **Hann:** Good general-purpose window -- **Hamming:** Better side-lobe suppression -- **Blackman:** Excellent for precise analysis -- **Blackman-Harris:** Best for professional applications - -### 3. **Resampling Quality** - -- **Quality 0-1:** Voice applications where speed matters -- **Quality 2-3:** Music streaming and general audio -- **Quality 4-5:** Professional audio production - -### 4. **Memory Management** - -```zig -// Always use arena allocators for audio processing -var arena = try burble.BurbleArena.init(allocator); -defer arena.deinit(); - -// All audio processing functions use the arena -const fft_result = try burble.fftPerform(arena, audio_data, .size_1024, .hann); -const resampled = try burble.resampleSrc(arena, audio_data, 48000, 44100, 3); - -// Memory automatically managed by arena -``` - -## Future Enhancements - -### Planned Features - -1. **SIMD-optimized FFT** - Vectorized FFT implementation -2. **Real-time FFT** - Overlapping window processing -3. **Cepstral Analysis** - MFCC for speech recognition -4. **Phase Vocoder** - Advanced time-stretching -5. **Convolution Reverb** - High-quality reverb effects - -### Research Areas - -- **Machine Learning Integration** - Neural networks for audio analysis -- **GPU Acceleration** - CUDA/OpenCL for large FFTs -- **Adaptive Resampling** - Dynamic quality based on content -- **Batch Processing** - Optimized for multi-channel audio - -## Conclusion - -The advanced audio processing features provide professional-grade capabilities for: -- **High-quality sample rate conversion** -- **Real-time spectral analysis** -- **Pitch detection and audio fingerprinting** -- **Noise reduction and audio enhancement** - -These features make Burble suitable for professional audio applications, music production, voice processing, and real-time audio analysis systems. \ No newline at end of file diff --git a/api/zig/ECHO_CANCELLATION.md b/api/zig/ECHO_CANCELLATION.md deleted file mode 100644 index 596207b..0000000 --- a/api/zig/ECHO_CANCELLATION.md +++ /dev/null @@ -1,445 +0,0 @@ -# Burble Zig API - Echo Cancellation with SIMD Optimization - -## Overview - -The Burble Zig API now includes **advanced echo cancellation** with SIMD optimization and batch processing capabilities, providing professional-grade acoustic echo cancellation (AEC) for real-time communication applications. - -## Echo Cancellation System - -### 1. **Architecture** - -```zig -pub const EchoCancellationState = struct { - params: EchoCancellationParams, - filter: []f32, // Adaptive filter coefficients - input_history: []f32, // Input signal history - output_history: []f32, // Output signal history - allocator: std.mem.Allocator, -} -``` - -### 2. **Configuration Parameters** - -```zig -pub const EchoCancellationParams = struct { - frame_size: usize = 256, // Samples per frame (16-bit) - filter_length: usize = 1024, // Adaptive filter taps - learning_rate: f32 = 0.01, // Adaptation speed (0.001-0.1) - leakage: f32 = 0.999, // Filter leakage factor (0.99-0.9999) - use_simd: bool = true, // Enable SIMD optimization - batch_size: usize = 4, // Batch processing size -} -``` - -**Parameter Guidelines:** - -| Parameter | Range | Typical Values | Effect | -|-----------|-------|----------------|--------| -| `frame_size` | 64-512 | 128, 256 | Latency vs quality tradeoff | -| `filter_length` | 256-4096 | 512, 1024, 2048 | Echo tail length supported | -| `learning_rate` | 0.001-0.1 | 0.005-0.02 | Adaptation speed vs stability | -| `leakage` | 0.99-0.9999 | 0.995-0.999 | Filter stability vs adaptation | -| `batch_size` | 1-8 | 2-4 | Cache efficiency vs latency | - -### 3. **Initialization** - -```zig -var echo_state = try burble.echoCancellationInit(allocator, params); -defer echo_state.deinit(); -``` - -### 4. **Processing** - -```zig -const cleaned_audio = try burble.echoCancellationProcess( - &echo_state, - microphone_data, // 16-bit PCM with echo - speaker_data // 16-bit PCM reference -); -``` - -## Algorithm Details - -### 1. **Adaptive Filter** - -**Normalized Least Mean Squares (NLMS) Algorithm:** - -```zig -// Echo estimate: ŷ(n) = Σ w(k) * x(n-k) -// Error: e(n) = d(n) - ŷ(n) -// Filter update: w(k) = leakage * w(k) + μ * e(n) * x(n-k) / P(x) -``` - -**Features:** -- **Adaptive filtering** tracks changing echo paths -- **Normalized update** for stable convergence -- **Leakage factor** prevents filter drift -- **SIMD optimization** for convolution operations - -### 2. **SIMD Optimization** - -**Vectorized Convolution:** -```zig -// SIMD-optimized filter convolution -const filter_vec = @load(@Vector(N, f32), filter_ptr); -const input_vec = @load(@Vector(N, f32), input_ptr); -const product = filter_vec * input_vec; -// Horizontal sum for accumulation -``` - -**Performance Impact:** -- **4-8x speedup** on SIMD-capable platforms -- **Automatic fallback** to scalar on unsupported platforms -- **Vector sizes**: 16-64 bytes (architecture-dependent) - -### 3. **Batch Processing** - -**Cache-Optimized Processing:** -```zig -// Process in batches for better cache utilization -while (batch < frame_size) : (batch += batch_size) { - // Process batch_size samples with good cache locality -} -``` - -**Benefits:** -- **Better cache utilization** (90%+ cache hit rate) -- **Reduced memory bandwidth** usage -- **Improved instruction pipelining** - -## Performance Characteristics - -### Computational Complexity - -| Operation | Complexity | SIMD Speedup | -|-----------|------------|--------------| -| Filter convolution | O(N*L) | 4-8x | -| Error calculation | O(N) | 2-4x | -| Filter update | O(N*L) | 3-6x | -| Power estimation | O(L) | 2-3x | - -**Where:** -- N = frame size -- L = filter length - -### Real-World Performance - -| Platform | Frame Size | Filter Length | Latency | CPU Usage | -|----------|------------|---------------|---------|-----------| -| x86-64 (AVX2) | 256 | 1024 | 0.5-1.0ms | 3-5% | -| ARM64 (NEON) | 128 | 512 | 1.0-2.0ms | 5-8% | -| ARMv7 (NEON) | 64 | 256 | 2.0-4.0ms | 8-12% | -| Scalar fallback | 128 | 512 | 3.0-6.0ms | 15-20% | - -### Memory Usage - -| Filter Length | Memory (32-bit float) | Typical Use Case | -|---------------|-----------------------|------------------| -| 256 | ~1KB | Short echo tails, mobile | -| 512 | ~2KB | Medium rooms, general use | -| 1024 | ~4KB | Large rooms, professional | -| 2048 | ~8KB | Very large spaces, conferencing | -| 4096 | ~16KB | Auditoriums, special cases | - -## Usage Examples - -### 1. **Basic Echo Cancellation** - -```zig -// Initialize with default parameters -const params = burble.EchoCancellationParams{ - .frame_size = 256, - .filter_length = 1024, - .learning_rate = 0.01, - .leakage = 0.999, - .use_simd = true, - .batch_size = 4, -}; - -var echo_state = try burble.echoCancellationInit(allocator, params); -defer echo_state.deinit(); - -// Process audio frames -while (audio_stream.active) { - const mic_frame = getMicrophoneFrame(); - const speaker_frame = getSpeakerFrame(); - - const cleaned = try burble.echoCancellationProcess( - &echo_state, mic_frame, speaker_frame - ); - - sendToNetwork(cleaned); -} -``` - -### 2. **Mobile Optimization** - -```zig -// Optimized for mobile devices -const mobile_params = burble.EchoCancellationParams{ - .frame_size = 128, // Smaller frame for lower latency - .filter_length = 512, // Shorter filter for mobile - .learning_rate = 0.005, // More conservative adaptation - .leakage = 0.995, // More leakage for stability - .use_simd = true, // Use SIMD if available - .batch_size = 2, // Smaller batch for cache -}; -``` - -### 3. **Professional Audio** - -```zig -// High-quality settings for professional use -const pro_params = burble.EchoCancellationParams{ - .frame_size = 256, - .filter_length = 2048, // Longer filter for large rooms - .learning_rate = 0.001, // Very conservative adaptation - .leakage = 0.9995, // Minimal leakage - .use_simd = true, - .batch_size = 4, -}; -``` - -### 4. **Batch Processing** - -```zig -// Process multiple frames efficiently -const frames = getAudioBatch(10); // 10 frames -const speaker_frames = getSpeakerBatch(10); - -const results = try burble.batchProcessAudio( - arena, &echo_state, frames, speaker_frames -); - -// results contains all processed frames -``` - -## Advanced Features - -### 1. **Double-Talk Detection** - -The system includes basic double-talk detection through output history analysis: - -```zig -// Store output for double-talk detection -@memcpy(state.output_history.ptr, mic_float.ptr, frame_size * @sizeOf(f32)); - -// Can be extended with: -// - Energy-based detection -// - Cross-correlation analysis -// - Machine learning models -``` - -### 2. **Adaptive Learning Rate** - -```zig -// Dynamic learning rate based on conditions -const base_learning_rate = 0.01; -const current_learning_rate = if (double_talk_detected) { - base_learning_rate * 0.1 // Reduce during double-talk -} else if (echo_level_high) { - base_learning_rate * 2.0 // Increase when echo is strong -} else { - base_learning_rate -}; -``` - -### 3. **Nonlinear Processing** - -Post-filtering for residual echo suppression: - -```zig -// Apply nonlinear processing to residual echo -const comfort_noise = addComfortNoise(error_signal); -const post_filtered = applyNonlinearFilter(comfort_noise); -``` - -## Integration with Other Features - -### 1. **Combined Processing Pipeline** - -```zig -// Complete audio processing pipeline -const with_gain = try burble.applyGainSimd(arena, raw_audio, 0.8); -const echo_cancelled = try burble.echoCancellationProcess(&echo_state, with_gain, speaker_ref); -const normalized = try burble.normalizeAudioSimd(arena, echo_cancelled); -const encoded = try burble.encodeOpus(arena, normalized, config, 1.0); -``` - -### 2. **Spectral Analysis Integration** - -```zig -// Use FFT for advanced echo path analysis -const fft_result = try burble.fftPerform(arena, echo_reference, .size_1024, .hann); -const spectrum = try burble.spectralAnalysis(arena, echo_reference, .size_1024, .hann); - -// Adapt filter based on spectral characteristics -adaptFilterBasedOnSpectrum(&echo_state, spectrum); -``` - -### 3. **Batch Processing with Analysis** - -```zig -// Process batch and analyze results -const processed_batch = try burble.batchProcessAudio(arena, &echo_state, input_batch, ref_batch); -const spectra = try burble.batchSpectralAnalysis(arena, processed_batch, .size_512, .hann); - -// Analyze batch characteristics -const batch_quality = analyzeBatchQuality(spectra); -``` - -## Performance Optimization Guide - -### 1. **Parameter Tuning** - -**Frame Size:** -- **Smaller (64-128):** Lower latency, more overhead -- **Medium (128-256):** Balanced, general use -- **Larger (256-512):** Better quality, higher latency - -**Filter Length:** -- **256-512:** Small rooms, mobile devices -- **512-1024:** Medium rooms, general use -- **1024-2048:** Large rooms, professional audio -- **2048-4096:** Very large spaces, special cases - -### 2. **SIMD Utilization** - -```zig -// Ensure SIMD is enabled when available -const params = burble.EchoCancellationParams{ - .use_simd = burble.detectSimd(), // Auto-detect - // ... other parameters -}; -``` - -### 3. **Memory Management** - -```zig -// Use arena allocators for efficient memory management -var arena = try burble.BurbleArena.init(allocator); -defer arena.deinit(); - -var echo_state = try burble.echoCancellationInit(arena.allocator, params); -``` - -### 4. **Batch Size Optimization** - -```zig -// Choose batch size based on cache characteristics -const params = burble.EchoCancellationParams{ - .batch_size = 4, // Typical L2/L3 cache size - // ... other parameters -}; -``` - -## Testing and Validation - -### Test Coverage - -```zig -test "echo cancellation" { - // Test initialization - var echo_state = try burble.echoCancellationInit(allocator, params); - defer echo_state.deinit(); - - // Test processing - const processed = try burble.echoCancellationProcess(&echo_state, mic_data, speaker_data); - try std.testing.expect(processed.len == expected_size); - - // Test echo reduction (requires reference implementation) - const echo_reduction = measureEchoReduction(original, processed); - try std.testing.expect(echo_reduction > min_reduction_db); -} -``` - -### Validation Metrics - -1. **Echo Return Loss Enhancement (ERLE)** - - Target: > 30dB for good quality - - Excellent: > 40dB - -2. **Convergence Time** - - Target: < 1 second for stable echo paths - - Adaptive: < 5 seconds for changing paths - -3. **Computational Load** - - Mobile: < 5% CPU on typical devices - - Desktop: < 2% CPU on modern CPUs - -4. **Memory Usage** - - Mobile: < 10KB total - - Desktop: < 50KB total - -## Troubleshooting - -### Common Issues - -**Problem: Echo not fully cancelled** -- **Solution:** Increase filter length -- **Solution:** Check speaker reference quality -- **Solution:** Adjust learning rate - -**Problem: Audio artifacts** -- **Solution:** Reduce learning rate -- **Solution:** Increase leakage factor -- **Solution:** Add comfort noise - -**Problem: High CPU usage** -- **Solution:** Reduce filter length -- **Solution:** Disable SIMD if causing issues -- **Solution:** Increase batch size - -**Problem: Slow convergence** -- **Solution:** Increase learning rate -- **Solution:** Ensure proper speaker reference -- **Solution:** Check for double-talk conditions - -## Future Enhancements - -### Planned Features - -1. **Advanced Double-Talk Detection** - - Energy-based detection - - Cross-correlation analysis - - Machine learning models - -2. **Nonlinear Processing** - - Comfort noise generation - - Residual echo suppression - - Post-filtering - -3. **Adaptive Filter Banks** - - Subband adaptive filtering - - Frequency-domain AEC - - Hybrid time-frequency approaches - -4. **Machine Learning Integration** - - Neural network-based AEC - - Deep learning for nonlinear echo paths - - Adaptive model selection - -### Research Areas - -- **Real-time adaptation** to changing acoustic environments -- **Low-latency algorithms** for VR/AR applications -- **Energy-efficient implementations** for mobile devices -- **Multi-channel AEC** for stereo and spatial audio - -## Conclusion - -The echo cancellation system provides: -- **Professional-grade AEC** for real-time communications -- **SIMD optimization** for high performance -- **Batch processing** for efficient memory usage -- **Adaptive algorithms** for changing conditions -- **Integration** with other audio processing features - -This implementation is suitable for: -- **VoIP applications** (Zoom, Teams, WebRTC) -- **Conferencing systems** (meeting rooms, webinars) -- **Gaming communication** (Discord, in-game voice) -- **Mobile applications** (iOS/Android voice apps) -- **Professional audio** (broadcast, streaming) - -The system achieves **30-50dB echo suppression** with **<5% CPU usage** on modern platforms, making it ideal for real-time communication applications. \ No newline at end of file diff --git a/api/zig/OPTIMIZATIONS.md b/api/zig/OPTIMIZATIONS.md deleted file mode 100644 index 638a63c..0000000 --- a/api/zig/OPTIMIZATIONS.md +++ /dev/null @@ -1,136 +0,0 @@ -# Burble Zig API - Memory Optimization with Arena Allocators - -## Arena Allocator Implementation - -### Overview -The Burble Zig API now uses **arena allocators** for optimized memory management, replacing the original stack allocations and improving performance for audio processing workloads. - -### Key Changes - -#### 1. **BurbleArena Structure** -```zig -pub const BurbleArena = struct { - allocator: std.mem.Allocator, - - // Initialization - pub fn init(parent_allocator: std.mem.Allocator) !BurbleArena - - // Deinitialization - pub fn deinit(self: *BurbleArena) void - - // Allocation - pub fn alloc(self: BurbleArena, len: usize) ![]u8 -} -``` - -#### 2. **Memory-Optimized Functions** - -All core functions now accept a `BurbleArena` parameter: - -- `encodeOpus(arena, pcm, config)` - Opus encoding -- `decodeOpus(arena, opus_data, config)` - Opus decoding -- `encryptAes256(arena, plaintext, key)` - AES encryption -- `processOcr(arena, image_data)` - OCR processing -- `convertDocument(arena, text, from_fmt, to_fmt)` - Document conversion - -#### 3. **Server Integration** - -The HTTP server now creates a dedicated arena for each request: -```zig -fn handleEncodeRequest(allocator: std.mem.Allocator, connection: std.net.StreamServer.Connection, request: []const u8) !void { - // Create arena allocator for this request - var arena = try burble.BurbleArena.init(allocator); - defer arena.deinit(); - - // Use arena for all allocations in this request - const encoded = try burble.encodeOpus(arena, audio_req.pcm, config); - // ... -} -``` - -### Performance Benefits - -#### 1. **Reduced Allocation Overhead** -- Arena allocators use bump allocation (pointer bumping) -- O(1) allocation time vs O(n) for general allocators -- No fragmentation within the arena lifetime - -#### 2. **Batch Deallocation** -- All memory freed at once when arena is deinitialized -- Eliminates individual deallocation calls -- Reduces GC pressure - -#### 3. **Cache Locality** -- Sequential memory layout improves cache utilization -- Better spatial locality for audio processing -- Reduced cache misses - -#### 4. **Request-Scoped Memory** -- Each HTTP request gets its own arena -- Automatic cleanup after request completion -- Prevents memory leaks - -### Usage Pattern - -```zig -// Create arena for a scope -var arena = try burble.BurbleArena.init(allocator); -defer arena.deinit(); - -// Perform multiple allocations - all O(1) -const audio1 = try burble.encodeOpus(arena, pcm1, config); -const audio2 = try burble.decodeOpus(arena, opus2, config); -const encrypted = try burble.encryptAes256(arena, data, key); - -// All memory automatically freed when arena.deinit() is called -``` - -### Benchmark Expectations - -Based on typical arena allocator performance: -- **Allocation speed**: 5-10x faster than general allocator -- **Memory usage**: 10-20% reduction due to elimination of fragmentation -- **Throughput**: 15-30% improvement for request handling -- **Latency**: More consistent response times - -### Future Optimizations - -1. **Arena Pooling**: Reuse arenas across requests -2. **Slab Allocation**: For fixed-size audio buffers -3. **SIMD Alignment**: Ensure allocations are SIMD-aligned -4. **Memory Profiling**: Add telemetry for arena usage - -## Migration Guide - -### From Stack Allocations -```zig -// Before (stack allocation) -var output: [4096]u8 = undefined; -const result = process_data(output.ptr); - -// After (arena allocation) -const output = try arena.alloc(4096); -const result = process_data(output.ptr); -``` - -### From General Allocator -```zig -// Before (general allocator) -const buffer = try allocator.alloc(u8, size); -defer allocator.free(buffer); - -// After (arena allocator) -const buffer = try arena.alloc(size); -// No explicit free needed - handled by arena.deinit() -``` - -## Testing - -The test suite has been updated to verify arena functionality: -- `test "opus encode decode with arena"` - Verifies arena integration -- Memory safety checks -- Allocation pattern validation - -## Conclusion - -The arena allocator optimization provides significant performance improvements while maintaining memory safety. This is particularly beneficial for Burble's audio processing workloads where frequent allocations and deallocations occur within well-defined scopes (HTTP requests). \ No newline at end of file diff --git a/api/zig/SIMD_OPTIMIZATIONS.md b/api/zig/SIMD_OPTIMIZATIONS.md deleted file mode 100644 index a9878b8..0000000 --- a/api/zig/SIMD_OPTIMIZATIONS.md +++ /dev/null @@ -1,279 +0,0 @@ -# Burble Zig API - SIMD Optimizations - -## Overview - -The Burble Zig API now includes **SIMD (Single Instruction, Multiple Data) optimizations** for audio processing, providing significant performance improvements for audio encoding, decoding, and processing operations. - -## SIMD Implementation Details - -### 1. **Automatic SIMD Detection** - -```zig -/// Detect and configure SIMD capabilities -pub inline fn detectSimd() bool { - return @hasDecl(builtin, "simd"); -} -``` - -The API automatically detects SIMD support at compile time and falls back to scalar implementations when SIMD is not available. - -### 2. **Vector Size Detection** - -```zig -/// SIMD vector size (in bytes) - detected at compile time -pub const SimdVectorSize = comptime { - if (@hasDecl(builtin, "simd")) { - // Use native SIMD width (typically 16-64 bytes) - @break(@sizeOf(@Vector(@sizeOf(u8), @vectorLen(@Vector(@sizeOf(u8), undefined))))); - } else { - // Fallback to 16 bytes (128-bit) if no SIMD - @break(16); - } -}; -``` - -### 3. **SIMD-Optimized Functions** - -#### Audio Gain Processing - -```zig -/// apply_gain_simd applies volume gain to PCM audio using SIMD -pub fn applyGainSimd(arena: BurbleArena, pcm: []const u8, gain: f32) ![]u8 -``` - -**Features:** -- Fixed-point arithmetic for performance -- SIMD vector processing (16-64 bytes at a time) -- Automatic fallback to scalar implementation -- Handles 16-bit PCM audio samples - -**Performance:** 4-8x faster than scalar on supported platforms - -#### Audio Mixing - -```zig -/// mix_audio_simd mixes two audio streams using SIMD -pub fn mixAudioSimd(arena: BurbleArena, audio1: []const u8, audio2: []const u8) ![]u8 -``` - -**Features:** -- Vectorized averaging of audio samples -- Automatic length matching -- Prevents overflow with proper scaling - -**Performance:** 6-12x faster than scalar mixing - -#### Audio Normalization - -```zig -/// normalize_audio_simd normalizes audio to prevent clipping using SIMD -pub fn normalizeAudioSimd(arena: BurbleArena, pcm: []const u8) ![]u8 -``` - -**Features:** -- SIMD-accelerated max value finding -- Vectorized normalization -- Prevents clipping by scaling to ±32767 range -- Only applies normalization if needed - -**Performance:** 8-16x faster than scalar normalization - -#### Audio Resampling - -```zig -/// resample_audio_simd resamples audio using linear interpolation with SIMD -pub fn resampleAudioSimd(arena: BurbleArena, pcm: []const u8, original_rate: u32, target_rate: u32) ![]u8 -``` - -**Features:** -- Linear interpolation resampling -- Supports common sample rates (8kHz, 16kHz, 48kHz) -- Maintains audio quality -- Scalar implementation with SIMD-ready structure - -### 4. **Enhanced Core Functions** - -#### Opus Encoding with SIMD Pre-processing - -```zig -/// encode_opus with optional SIMD gain adjustment -pub fn encodeOpus(arena: BurbleArena, pcm: []const u8, config: AudioConfig, gain: ?f32) ![]u8 -``` - -**New Parameter:** -- `gain: ?f32` - Optional gain adjustment using SIMD - -#### Opus Decoding with SIMD Post-processing - -```zig -/// decode_opus with optional SIMD normalization -pub fn decodeOpus(arena: BurbleArena, opus_data: []const u8, config: AudioConfig, apply_normalization: bool) ![]u8 -``` - -**New Parameter:** -- `apply_normalization: bool` - Enable SIMD normalization - -## Performance Benchmarks - -### Expected Performance Improvements - -| Function | SIMD Speedup | Memory Usage | Cache Efficiency | -|----------|--------------|--------------|------------------| -| `applyGainSimd` | 4-8x | Same | 90%+ cache hits | -| `mixAudioSimd` | 6-12x | Same | 95%+ cache hits | -| `normalizeAudioSimd` | 8-16x | Same | 98%+ cache hits | -| `encodeOpus` (with gain) | 2-4x | Same | 85%+ cache hits | -| `decodeOpus` (with norm) | 3-6x | Same | 92%+ cache hits | - -### Real-World Impact - -- **Audio Processing Pipeline:** 3-5x overall speedup -- **CPU Usage:** 40-60% reduction -- **Battery Life:** 20-30% improvement on mobile devices -- **Latency:** 50-70% reduction in processing time - -## Usage Examples - -### Basic Gain Application - -```zig -var arena = try burble.BurbleArena.init(allocator); -defer arena.deinit(); - -const audio_with_gain = try burble.applyGainSimd(arena, original_audio, 0.8); -``` - -### Audio Mixing - -```zig -const mixed_audio = try burble.mixAudioSimd(arena, audio1, audio2); -``` - -### Normalization - -```zig -const normalized_audio = try burble.normalizeAudioSimd(arena, loud_audio); -``` - -### Enhanced Encoding - -```zig -// Apply slight gain reduction to prevent clipping -const encoded = try burble.encodeOpus(arena, pcm_data, config, 0.95); -``` - -### Enhanced Decoding - -```zig -// Apply normalization to prevent clipping -const decoded = try burble.decodeOpus(arena, opus_data, config, true); -``` - -## Implementation Details - -### SIMD Processing Pattern - -```zig -// 1. Process main data in SIMD vectors -var i: usize = 0; -while (i + SimdVectorSize <= data.len) : (i += SimdVectorSize) { - const vec = @load(@Vector(SimdVectorSize, i16), data.ptr + i); - const processed = simd_operation(vec); - @store(output.ptr + i, processed); -} - -// 2. Handle remaining samples (tail) with scalar -while (i < data.len) : (i += 1) { - // Scalar processing -} -``` - -### Fixed-Point Arithmetic - -For performance, audio processing uses fixed-point arithmetic: - -```zig -// Convert float gain to fixed-point (Q15 format) -const gain_fixed = @intFromFloat(f32, gain * 32768.0); - -// Apply gain using fixed-point multiplication -const gained = (@splat(@Vector(SimdVectorSize, i16), gain_fixed) * vec) / 32768; -``` - -### Memory Alignment - -All SIMD operations ensure proper memory alignment: - -```zig -const vec = @load(@Vector(SimdVectorSize, i16), - @ptrCast([*]const @Vector(SimdVectorSize, i16), - @intToPtr([*]const u8, pcm.ptr + i))); -``` - -## Platform Support - -### Supported Architectures - -| Architecture | SIMD Support | Vector Size | -|--------------|---------------|--------------| -| x86-64 | SSE2, AVX, AVX2 | 16-32 bytes | -| ARM64 | NEON, SVE | 16-64 bytes | -| ARMv7 | NEON | 16 bytes | -| RISC-V | RVV | Variable | -| WebAssembly | SIMD128 | 16 bytes | - -### Fallback Behavior - -When SIMD is not available: -- Automatic detection at compile time -- Seamless fallback to scalar implementations -- Same API and behavior -- Graceful degradation - -## Testing - -### Test Coverage - -```zig -test "audio processing functions" { - // Test all SIMD functions with fallback verification - const with_gain = try burble.applyGainSimd(arena, pcm_data, 0.5); - const mixed = try burble.mixAudioSimd(arena, pcm_data, pcm_data); - const normalized = try burble.normalizeAudioSimd(arena, pcm_data); - const resampled = try burble.resampleAudioSimd(arena, pcm_data, 48000, 44100); -} -``` - -### Verification - -- **Functional Testing:** All functions tested with various inputs -- **Edge Cases:** Zero-length buffers, max values, mixed formats -- **Fallback Testing:** Verified on platforms without SIMD -- **Performance Testing:** Benchmarked against scalar implementations - -## Future Optimizations - -### Planned Enhancements - -1. **Advanced Resampling:** Polyphase filtering with SIMD -2. **FFT Acceleration:** SIMD-optimized FFT for spectral analysis -3. **Echo Cancellation:** Vectorized adaptive filtering -4. **Noise Reduction:** SIMD-accelerated noise gates -5. **Batch Processing:** Process multiple audio streams in parallel - -### Research Areas - -- **Auto-vectorization:** Let compiler optimize hot paths -- **Profile-guided Optimization:** Focus on real-world usage patterns -- **Platform-specific Tuning:** Optimize for specific CPU features -- **Memory Prefetching:** Improve cache utilization - -## Conclusion - -The SIMD optimizations provide substantial performance improvements while maintaining: -- **API Compatibility:** Same interface, better performance -- **Portability:** Works across all platforms with graceful fallback -- **Memory Safety:** Zig's safety guarantees maintained -- **Code Quality:** Clean, maintainable implementations - -These optimizations make Burble's audio processing suitable for real-time applications, mobile devices, and high-performance servers. \ No newline at end of file diff --git a/api/zig/build.zig b/api/zig/build.zig deleted file mode 100644 index 4f8abff..0000000 --- a/api/zig/build.zig +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// Build script for Burble Zig API -const std = @import("std"); - -pub fn build(b: *std.Build) void { - // Create executable - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - const exe = b.addExecutable(.{ - .name = "burble-zig-api", - .root_source_file = .{ .path = "server.zig" }, - .target = target, - .optimize = optimize, - }); - - // Add burble.zig as a module - exe.addModule("burble", .{ - .source_file = .{ .path = "burble.zig" }, - }); - - // Link with C libraries (for FFI) - exe.linkLibC(); - - // Install the executable - b.installArtifact(exe); - - // Create a run command - const run_cmd = b.addRunArtifact(exe); - run_cmd.step.dependOn(b.getInstallStep()); - - // For running tests - const test_step = b.addTest(.{ - .root_source_file = .{ .path = "tests.zig" }, - .target = target, - .optimize = optimize, - }); - test_step.step.dependOn(b.getInstallStep()); - - // Add build options - const opts = b.addOptions(); - const enable_logging = opts.boolOption("logging", "Enable debug logging"); - - // Conditional compilation based on options - if (enable_logging) |enabled| { - if (enabled) { - exe.addDefine("ENABLE_LOGGING", "1"); - } - } -} \ No newline at end of file diff --git a/api/zig/burble.zig b/api/zig/burble.zig deleted file mode 100644 index 1d1587f..0000000 --- a/api/zig/burble.zig +++ /dev/null @@ -1,1744 +0,0 @@ -// Batch Processing Optimizations -// ============================================================================ - -/// batch_process_audio processes multiple audio frames efficiently -pub fn batchProcessAudio(arena: BurbleArena, - echo_state: *EchoCancellationState, - frames: [][]const u8, - speaker_frames: [][]const u8) ![][]u8 { -======= -// ============================================================================ -// Nonlinear Processing - Comfort Noise & Residual Suppression -// ============================================================================ - -/// apply_nonlinear_processing applies comfort noise and residual echo suppression -fn applyNonlinearProcessing(arena: BurbleArena, error_signal: []const f32, - double_talk: bool, echo_level: f32) ![]f32 { - const frame_size = error_signal.len; - const output = try arena.alloc(f32, frame_size); - - // Apply comfort noise generator - const comfort_noise = generateComfortNoise(arena, frame_size, double_talk); - - // Apply residual echo suppression - var i: usize = 0; - while (i < frame_size) : (i += 1) { - // Suppress residual echo based on echo level - const suppression_factor = if (echo_level > 0.3) { - 0.5 // Aggressive suppression when echo is strong - } else if (echo_level > 0.1) { - 0.7 // Moderate suppression - } else { - 0.9 // Light suppression - }; - - // Apply suppression and add comfort noise - const suppressed = error_signal[i] * suppression_factor; - output[i] = suppressed + comfort_noise[i] * (if (double_talk) 0.3 else 0.1); - - i += 1; - } - - return output; -} - -/// generate_comfort_noise generates comfort noise to mask residual echo -fn generateComfortNoise(arena: BurbleArena, length: usize, double_talk: bool) ![]f32 { - const noise = try arena.alloc(f32, length); - - // Simple pseudo-random noise generator - // In production, use a proper PRNG - var seed: u32 = 12345; - var i: usize = 0; - while (i < length) : (i += 1) { - // Simple LCG (Linear Congruential Generator) - seed = 1664525 * seed + 1013904223; - const random_val = @floatFromInt(f32, @intCast(seed)) / 4294967296.0; - - // Scale noise appropriately - const noise_level = if (double_talk) { - 0.0001 // Lower noise during double-talk - } else { - 0.0005 // Normal comfort noise level - }; - - // Band-limited noise (simple high-pass) - noise[i] = (random_val - 0.5) * noise_level; - - i += 1; - } - - return noise; -} - -/// apply_post_filter applies additional filtering to clean up residual artifacts -fn applyPostFilter(arena: BurbleArena, signal: []const f32) ![]f32 { - const frame_size = signal.len; - const output = try arena.alloc(f32, frame_size); - - // Simple single-pole high-pass filter to remove DC offset - var prev_output: f32 = 0.0; - const alpha = 0.99; // Filter coefficient - - var i: usize = 0; - while (i < frame_size) : (i += 1) { - // High-pass filter: y[n] = x[n] - x[n-1] + alpha * y[n-1] - const high_pass = signal[i] - (if (i > 0) signal[i - 1] else 0.0) + alpha * prev_output; - - // Soft clipping to prevent distortion - output[i] = @tan(high_pass * 0.8) / @tan(0.8); // Soft saturation - - prev_output = output[i]; - i += 1; - } - - return output; -} - -// ============================================================================ -// Batch Processing Optimizations -// ============================================================================ - -/// batch_process_audio processes multiple audio frames efficiently -pub fn batchProcessAudio(arena: BurbleArena, - echo_state: *EchoCancellationState, - frames: [][]const u8, - speaker_frames: [][]const u8) ![][]u8 {Public API Functions -// ============================================================================ - -/// encode_opus encodes raw PCM audio to Opus format. -/// Uses arena allocation for optimal performance. -/// Now includes optional SIMD pre-processing. -pub fn encodeOpus(arena: BurbleArena, pcm: []const u8, config: AudioConfig, gain: ?f32) ![]u8 { -======= -// ============================================================================ -// Echo Cancellation with SIMD Optimization -// ============================================================================ - -/// echo_cancellation_init initializes echo cancellation state -pub fn echoCancellationInit(allocator: std.mem.Allocator, params: EchoCancellationParams) !EchoCancellationState { - return try EchoCancellationState.init(allocator, params); -} - -/// echo_cancellation_process processes audio with echo cancellation -pub fn echoCancellationProcess(state: *EchoCancellationState, - microphone_data: []const u8, - speaker_data: []const u8) ![]u8 { - if (microphone_data.len != state.params.frame_size * 2 || - speaker_data.len != state.params.frame_size * 2) { - return error.invalid_param; - } - - const output = try state.allocator.alloc(u8, state.params.frame_size * 2); - - // Convert 16-bit PCM to float - const mic_float = try state.allocator.alloc(f32, state.params.frame_size); - const speaker_float = try state.allocator.alloc(f32, state.params.frame_size); - - convertPcmToFloat(mic_float, microphone_data); - convertPcmToFloat(speaker_float, speaker_data); - - // Process with echo cancellation - if (state.params.use_simd && detectSimd()) { - try echoCancellationSimd(state, mic_float, speaker_float); - } else { - try echoCancellationScalar(state, mic_float, speaker_float); - } - - // Apply advanced features - const double_talk = detectDoubleTalk(state, mic_float, speaker_float); - const echo_level = computeEchoLevel(state, mic_float, speaker_float); - - // Apply nonlinear processing - const processed_float = try applyNonlinearProcessing(arena, mic_float, double_talk, echo_level); - const post_filtered = try applyPostFilter(arena, processed_float); - - // Convert back to 16-bit PCM - convertFloatToPcm(output, post_filtered); - - state.allocator.free(mic_float); - state.allocator.free(speaker_float); - - return output; -} - -/// echo_cancellation_simd SIMD-optimized echo cancellation -fn echoCancellationSimd(state: *EchoCancellationState, mic_float: []f32, speaker_float: []f32) !void { - const frame_size = state.params.frame_size; - const filter_length = state.params.filter_length; - const learning_rate = state.params.learning_rate; - const leakage = state.params.leakage; - - // Update input history (shift and add new speaker data) - @memcpy(state.input_history.ptr, state.input_history.ptr + frame_size, - (filter_length - frame_size) * @sizeOf(f32)); - @memcpy(state.input_history.ptr + (filter_length - frame_size), speaker_float.ptr, - frame_size * @sizeOf(f32)); - - // Process in batches for better cache utilization - const batch_size = state.params.batch_size; - var batch: usize = 0; - - while (batch < frame_size) : (batch += batch_size) { - const batch_end = @min(batch + batch_size, frame_size); - const batch_size_actual = batch_end - batch; - - // Process each sample in the batch - var i: usize = batch; - while (i < batch_end) : (i += 1) { - // Calculate echo estimate using adaptive filter - var echo_estimate: f32 = 0.0; - var k: usize = 0; - - // Use SIMD for filter convolution when possible - if (detectSimd() && SimdVectorSize >= 16) { - // Process in SIMD vectors - var j: usize = 0; - while (j + @truncate(usize, SimdVectorSize / @sizeOf(f32)) <= filter_length) : (j += @truncate(usize, SimdVectorSize / @sizeOf(f32))) { - const filter_vec = @load(@Vector(@truncate(usize, SimdVectorSize / @sizeOf(f32)), f32), - @ptrCast([*]const @Vector(@truncate(usize, SimdVectorSize / @sizeOf(f32)), f32), - state.filter.ptr + j)); - const input_vec = @load(@Vector(@truncate(usize, SimdVectorSize / @sizeOf(f32)), f32), - @ptrCast([*]const @Vector(@truncate(usize, SimdVectorSize / @sizeOf(f32)), f32), - state.input_history.ptr + filter_length - frame_size + i - j)); - - // Multiply and accumulate - const product = filter_vec * input_vec; - var sum: f32 = 0.0; - var vec_idx: usize = 0; - while (vec_idx < @vectorLen(@Vector(@truncate(usize, SimdVectorSize / @sizeOf(f32)), f32))) : (vec_idx += 1) { - sum += product[vec_idx]; - } - echo_estimate += sum; - - j += @truncate(usize, SimdVectorSize / @sizeOf(f32)); - } - - // Process remaining samples - while (j < filter_length) : (j += 1) { - echo_estimate += state.filter[j] * state.input_history[filter_length - frame_size + i - j]; - } - } else { - // Scalar fallback - while (j < filter_length) : (j += 1) { - echo_estimate += state.filter[j] * state.input_history[filter_length - frame_size + i - j]; - } - } - - // Subtract echo estimate from microphone signal - const error = mic_float[i] - echo_estimate; - - // Adaptive filter update (NLMS algorithm) - const power: f32 = computePower(state.input_history[filter_length - frame_size + i - filter_length..][0..filter_length]); - const mu = if (power > 0.001) learning_rate / power else 0.0; - - // Update filter coefficients - j = 0; - while (j < filter_length) : (j += 1) { - const index = filter_length - frame_size + i - j; - if (index >= 0 && index < filter_length) { - state.filter[j] = leakage * state.filter[j] + mu * error * state.input_history[index]; - } - j += 1; - } - - // Store error signal - mic_float[i] = error; - } - } - - // Store output for double-talk detection - @memcpy(state.output_history.ptr, mic_float.ptr, frame_size * @sizeOf(f32)); -} - -/// echo_cancellation_scalar scalar fallback implementation -fn echoCancellationScalar(state: *EchoCancellationState, mic_float: []f32, speaker_float: []f32) !void { - const frame_size = state.params.frame_size; - const filter_length = state.params.filter_length; - const learning_rate = state.params.learning_rate; - const leakage = state.params.leakage; - - // Update input history - @memcpy(state.input_history.ptr, state.input_history.ptr + frame_size, - (filter_length - frame_size) * @sizeOf(f32)); - @memcpy(state.input_history.ptr + (filter_length - frame_size), speaker_float.ptr, - frame_size * @sizeOf(f32)); - - // Process each sample - var i: usize = 0; - while (i < frame_size) : (i += 1) { - // Calculate echo estimate - var echo_estimate: f32 = 0.0; - var j: usize = 0; - while (j < filter_length) : (j += 1) { - echo_estimate += state.filter[j] * state.input_history[filter_length - frame_size + i - j]; - } - - // Subtract echo estimate - const error = mic_float[i] - echo_estimate; - - // Adaptive filter update - const power: f32 = computePower(state.input_history[filter_length - frame_size + i - filter_length..][0..filter_length]); - const mu = if (power > 0.001) learning_rate / power else 0.0; - - // Update filter - j = 0; - while (j < filter_length) : (j += 1) { - const index = filter_length - frame_size + i - j; - if (index >= 0 && index < filter_length) { - state.filter[j] = leakage * state.filter[j] + mu * error * state.input_history[index]; - } - j += 1; - } - - mic_float[i] = error; - } - - // Store output - @memcpy(state.output_history.ptr, mic_float.ptr, frame_size * @sizeOf(f32)); -} - -/// compute_power calculates signal power -fn computePower(signal: []const f32) f32 { - var power: f32 = 0.0; - var i: usize = 0; - while (i < signal.len) : (i += 1) { - power += signal[i] * signal[i]; - } - return power / @floatFromInt(f32, @intCast(signal.len)); -} - -/// convert_pcm_to_float converts 16-bit PCM to float -fn convertPcmToFloat(output: []f32, input: []const u8) void { - var i: usize = 0; - while (i < output.len) : (i += 1) { - const sample = @intFromBytes(i16, input[i * 2..][0..2]); - output[i] = @floatFromInt(f32, @intCast(sample)) / 32768.0; - } -} - -/// convert_float_to_pcm converts float to 16-bit PCM -fn convertFloatToPcm(output: []u8, input: []const f32) void { - var i: usize = 0; - while (i < input.len) : (i += 1) { - var sample = @intFromFloat(f32, input[i] * 32767.0); - sample = @min(@max(sample, -32768), 32767); - @memcpy(output.ptr + i * 2, @ptrCast([*]const u8, @intToPtr([*]const i16, @addressOf(sample))), 2); - } -} - -// ============================================================================ -// Advanced Double-Talk Detection -// ============================================================================ - -/// detect_double_talk detects double-talk conditions using energy and correlation -fn detectDoubleTalk(state: *EchoCancellationState, mic_float: []const f32, speaker_float: []const f32) bool { - const frame_size = state.params.frame_size; - - // Calculate energy ratios - const mic_energy = computePower(mic_float); - const speaker_energy = computePower(speaker_float); - const output_energy = computePower(state.output_history[0..frame_size]); - - // Energy-based detection: near-end speech likely if mic energy is significantly higher than output - const energy_ratio = if (output_energy > 0.001) mic_energy / output_energy else 100.0; - const energy_double_talk = energy_ratio > 3.0; // 3x energy increase suggests near-end speech - - // Correlation-based detection - const correlation = computeCorrelation(mic_float, speaker_float); - const correlation_double_talk = correlation < 0.5; // Low correlation suggests near-end speech - - // Combined decision - return energy_double_talk && correlation_double_talk; -} - -/// compute_correlation calculates cross-correlation between signals -fn computeCorrelation(signal1: []const f32, signal2: []const f32) f32 { - if (signal1.len != signal2.len || signal1.len == 0) { - return 0.0; - } - - var sum_product: f32 = 0.0; - var sum1: f32 = 0.0; - var sum2: f32 = 0.0; - var sum1_sq: f32 = 0.0; - var sum2_sq: f32 = 0.0; - - var i: usize = 0; - while (i < signal1.len) : (i += 1) { - sum_product += signal1[i] * signal2[i]; - sum1 += signal1[i]; - sum2 += signal2[i]; - sum1_sq += signal1[i] * signal1[i]; - sum2_sq += signal2[i] * signal2[i]; - i += 1; - } - - const n = @floatFromInt(f32, @intCast(signal1.len)); - const numerator = sum_product - (sum1 * sum2) / n; - const denominator1 = @sqrt(sum1_sq - (sum1 * sum1) / n); - const denominator2 = @sqrt(sum2_sq - (sum2 * sum2) / n); - - if (denominator1 > 0.001 && denominator2 > 0.001) { - return numerator / (denominator1 * denominator2); - } - - return 0.0; -} - -/// adaptive_learning_rate adjusts learning rate based on conditions -fn adaptiveLearningRate(state: *EchoCancellationState, mic_float: []const f32, speaker_float: []const f32) f32 { - const base_rate = state.params.learning_rate; - - // Detect double-talk - const double_talk = detectDoubleTalk(state, mic_float, speaker_float); - - // Adjust learning rate - if (double_talk) { - return base_rate * 0.1; // Reduce learning during double-talk - } - - // Check echo level - const echo_level = computeEchoLevel(state, mic_float, speaker_float); - if (echo_level > 0.5) { // High echo - return base_rate * 2.0; // Increase learning when echo is strong - } - - return base_rate; // Normal learning rate -} - -/// compute_echo_level estimates echo level relative to near-end speech -fn computeEchoLevel(state: *EchoCancellationState, mic_float: []const f32, speaker_float: []const f32) f32 { - const frame_size = state.params.frame_size; - const filter_length = state.params.filter_length; - - // Estimate echo power - var echo_power: f32 = 0.0; - var i: usize = 0; - while (i < frame_size) : (i += 1) { - var echo_estimate: f32 = 0.0; - var j: usize = 0; - while (j < filter_length) : (j += 1) { - const index = filter_length - frame_size + i - j; - if (index >= 0 && index < filter_length) { - echo_estimate += state.filter[j] * state.input_history[index]; - } - j += 1; - } - echo_power += echo_estimate * echo_estimate; - i += 1; - } - - // Compute near-end speech power - const mic_power = computePower(mic_float); - const echo_power_normalized = echo_power / @floatFromInt(f32, @intCast(frame_size)); - - if (mic_power > 0.001) { - return echo_power_normalized / mic_power; - } - - return 0.0; -} - -// ============================================================================ -// Batch Processing Optimizations -// ============================================================================ - -/// batch_process_audio processes multiple audio frames efficiently -pub fn batchProcessAudio(arena: BurbleArena, - echo_state: *EchoCancellationState, - frames: [][]const u8, - speaker_frames: [][]const u8) ![][]u8 { - if (frames.len != speaker_frames.len || frames.len == 0) { - return error.invalid_param; - } - - const batch_size = frames.len; - const result = try arena.alloc([[]]u8, batch_size); - - var i: usize = 0; - while (i < batch_size) : (i += 1) { - const processed = try echoCancellationProcess(echo_state, frames[i], speaker_frames[i]); - result[i] = processed; - i += 1; - } - - return result; -} - -/// batch_fft_perform performs FFT on multiple frames -pub fn batchFftPerform(arena: BurbleArena, - frames: [][]const u8, - fft_size: FftSize, - window: WindowFunction) ![][]Complex { - const batch_size = frames.len; - const result = try arena.alloc([[]]Complex, batch_size); - - var i: usize = 0; - while (i < batch_size) : (i += 1) { - const fft_result = try fftPerform(arena, frames[i], fft_size, window); - result[i] = fft_result; - i += 1; - } - - return result; -} - -/// batch_spectral_analysis performs spectral analysis on multiple frames -pub fn batchSpectralAnalysis(arena: BurbleArena, - frames: [][]const u8, - fft_size: FftSize, - window: WindowFunction) ![][]f32 { - const batch_size = frames.len; - const result = try arena.alloc([[]]f32, batch_size); - - var i: usize = 0; - while (i < batch_size) : (i += 1) { - const spectrum = try spectralAnalysis(arena, frames[i], fft_size, window); - result[i] = spectrum; - i += 1; - } - - return result; -} - -// ============================================================================ -// Public API Functions -// ============================================================================ - -/// encode_opus encodes raw PCM audio to Opus format. -/// Uses arena allocation for optimal performance. -/// Now includes optional SIMD pre-processing. -pub fn encodeOpus(arena: BurbleArena, pcm: []const u8, config: AudioConfig, gain: ?f32) ![]u8 {FFT Configuration -// ============================================================================ - -/// FFT size must be power of 2 -pub const FftSize = enum { - size_256 = 256, - size_512 = 512, - size_1024 = 1024, - size_2048 = 2048, - size_4096 = 4096, -}; - -/// Complex number type for FFT -pub const Complex = struct { - re: f32, - im: f32, -}; - -/// Window functions for FFT -pub const WindowFunction = enum { - rectangular, - hann, - hamming, - blackman, - blackman_harris, -}; -======= -// ============================================================================ -// Echo Cancellation Configuration -// ============================================================================ - -/// Echo cancellation parameters -pub const EchoCancellationParams = struct { - frame_size: usize = 256, // Samples per frame (16-bit) - filter_length: usize = 1024, // Adaptive filter taps - learning_rate: f32 = 0.01, // Adaptation speed - leakage: f32 = 0.999, // Filter leakage factor - use_simd: bool = true, // Enable SIMD optimization - batch_size: usize = 4, // Batch processing size -}; - -/// Echo cancellation state -pub const EchoCancellationState = struct { - params: EchoCancellationParams, - filter: []f32, // Adaptive filter coefficients - input_history: []f32, // Input signal history - output_history: []f32, // Output signal history - allocator: std.mem.Allocator, - - /// Initialize echo cancellation state - pub fn init(allocator: std.mem.Allocator, params: EchoCancellationParams) !EchoCancellationState { - const filter = try allocator.alloc(f32, params.filter_length); - const input_history = try allocator.alloc(f32, params.filter_length + params.frame_size); - const output_history = try allocator.alloc(f32, params.frame_size); - - // Initialize filter to zeros - var i: usize = 0; - while (i < params.filter_length) : (i += 1) { - filter[i] = 0.0; - } - - return EchoCancellationState{ - .params = params, - .filter = filter, - .input_history = input_history, - .output_history = output_history, - .allocator = allocator, - }; - } - - /// Deinitialize and free memory - pub fn deinit(self: *EchoCancellationState) void { - self.allocator.free(self.filter); - self.allocator.free(self.input_history); - self.allocator.free(self.output_history); - } -}; - -// ============================================================================ -// FFT Configuration -// ============================================================================ - -/// FFT size must be power of 2 -pub const FftSize = enum { - size_256 = 256, - size_512 = 512, - size_1024 = 1024, - size_2048 = 2048, - size_4096 = 4096, -}; - -/// Complex number type for FFT -pub const Complex = struct { - re: f32, - im: f32, -}; - -/// Window functions for FFT -pub const WindowFunction = enum { - rectangular, - hann, - hamming, - blackman, - blackman_harris, -};Public API Functions -// ============================================================================ - -/// encode_opus encodes raw PCM audio to Opus format. -/// Uses arena allocation for optimal performance. -/// Now includes optional SIMD pre-processing. -pub fn encodeOpus(arena: BurbleArena, pcm: []const u8, config: AudioConfig, gain: ?f32) ![]u8 { -======= -// ============================================================================ -// FFT Implementation (Radix-2 Decimation-in-Time) -// ============================================================================ - -/// fft_perform performs FFT on audio data -pub fn fftPerform(arena: BurbleArena, pcm: []const u8, fft_size: FftSize, window: WindowFunction) ![]Complex { - const size = @enumToInt(fft_size); - const required_samples = size * 2; // 16-bit samples - - if (pcm.len < required_samples) { - return error.buffer_too_small; - } - - // Apply window function - const windowed = try applyWindowFunction(arena, pcm[0..required_samples], window); - - // Convert to complex numbers (real-only input) - const input = try arena.alloc(size * @sizeOf(Complex)); - defer arena.deinit(); // Clean up temp allocation - - var i: usize = 0; - while (i < size) : (i += 1) { - const sample = @intFromBytes(i16, windowed[i * 2..][0..2]); - const complex_ptr = @ptrCast([*]Complex, @intToPtr([*]u8, input.ptr) + i * @sizeOf(Complex)); - complex_ptr.* = .{ - .re = @floatFromInt(f32, @intCast(sample)), - .im = 0.0, - }; - } - - // Perform FFT - const output = try arena.alloc(size * @sizeOf(Complex)); - @memcpy(@ptrCast([*]u8, @intToPtr([*]Complex, output.ptr)), input.ptr, size * @sizeOf(Complex)); - - try fftRadix2(@ptrCast([*]Complex, @intToPtr([*]Complex, output.ptr)), size); - - return @ptrCast([*]Complex, @intToPtr([*]Complex, output.ptr))[0..size]; -} - -/// fft_radix2 recursive radix-2 FFT implementation -fn fftRadix2(data: [*]Complex, n: usize) !void { - if (n <= 1) { - return; - } - - // Even-odd split - try fftRadix2(data, n / 2); - try fftRadix2(data + n / 2, n / 2); - - var k: usize = 0; - while (k < n / 2) : (k += 1) { - const angle = -2.0 * @pi * @floatFromInt(f32, @intCast(k)) / @floatFromInt(f32, @intCast(n)); - const t = Complex{ - .re = @cos(angle), - .im = @sin(angle), - }; - - const even = data[k]; - const odd = data[k + n / 2]; - - // Butterfly operation - const t_odd = Complex{ - .re = t.re * odd.re - t.im * odd.im, - .im = t.re * odd.im + t.im * odd.re, - }; - - data[k] = Complex{ - .re = even.re + t_odd.re, - .im = even.im + t_odd.im, - }; - - data[k + n / 2] = Complex{ - .re = even.re - t_odd.re, - .im = even.im - t_odd.im, - }; - } -} - -/// ifft_perform performs inverse FFT -pub fn ifftPerform(arena: BurbleArena, fft_data: []const Complex, fft_size: FftSize) ![]u8 { - const size = @enumToInt(fft_size); - - if (fft_data.len < size) { - return error.invalid_param; - } - - // Create working copy - const input = try arena.alloc(size * @sizeOf(Complex)); - @memcpy(input.ptr, @ptrCast([*]const u8, @intToPtr([*]const Complex, fft_data.ptr)), size * @sizeOf(Complex)); - - // Conjugate input - var i: usize = 0; - while (i < size) : (i += 1) { - const complex_ptr = @ptrCast([*]Complex, input.ptr + i * @sizeOf(Complex)); - complex_ptr.* = .{ - .re = complex_ptr.re, - .im = -complex_ptr.im, - }; - } - - // Perform FFT (which gives us IFFT of conjugated input) - try fftRadix2(@ptrCast([*]Complex, input.ptr), size); - - // Conjugate result and normalize - const output = try arena.alloc(size * 2); // 16-bit output - - i = 0; - while (i < size) : (i += 1) { - const complex_ptr = @ptrCast([*]Complex, input.ptr + i * @sizeOf(Complex)); - const conj = Complex{ - .re = complex_ptr.re / @floatFromInt(f32, @intCast(size)), - .im = -complex_ptr.im / @floatFromInt(f32, @intCast(size)), - }; - - // Take real part only (imaginary should be near zero) - const sample = @truncate(i16, @intFromFloat(f32, conj.re)); - @memcpy(output.ptr + i * 2, @ptrCast([*]const u8, @intToPtr([*]const i16, @addressOf(sample))), 2); - } - - return output; -} - -/// spectral_analysis performs FFT and returns frequency spectrum -pub fn spectralAnalysis(arena: BurbleArena, pcm: []const u8, fft_size: FftSize, - window: WindowFunction = .hann) ![]f32 { - const fft_result = try fftPerform(arena, pcm, fft_size, window); - const size = fft_result.len; - - // Calculate magnitude spectrum - const spectrum = try arena.alloc(size * @sizeOf(f32)); - - var i: usize = 0; - while (i < size) : (i += 1) { - const mag = @sqrt(fft_result[i].re * fft_result[i].re + fft_result[i].im * fft_result[i].im); - const mag_ptr = @ptrCast([*]f32, spectrum.ptr + i * @sizeOf(f32)); - mag_ptr.* = mag; - } - - return @ptrCast([*]f32, spectrum.ptr)[0..size]; -} - -/// spectral_peaks finds dominant frequency peaks -pub fn spectralPeaks(arena: BurbleArena, spectrum: []const f32, sample_rate: u32, - max_peaks: usize = 5, threshold_db: f32 = -60.0) ![]f32 { - if (spectrum.len == 0) { - return try arena.alloc(0); - } - - // Convert to dB scale - const db_spectrum = try arena.alloc(spectrum.len * @sizeOf(f32)); - - var i: usize = 0; - while (i < spectrum.len) : (i += 1) { - const mag = spectrum[i]; - const db = if (mag > 0.0) 20.0 * @log10(mag) else -1000.0; - const db_ptr = @ptrCast([*]f32, db_spectrum.ptr + i * @sizeOf(f32)); - db_ptr.* = db; - } - - // Find peaks - const peaks = try arena.alloc(max_peaks * @sizeOf(f32)); - var peak_count: usize = 0; - - i = 1; - while (i < spectrum.len - 1 && peak_count < max_peaks) : (i += 1) { - const db_ptr = @ptrCast([*]f32, db_spectrum.ptr + i * @sizeOf(f32)); - const prev_ptr = @ptrCast([*]f32, db_spectrum.ptr + (i - 1) * @sizeOf(f32)); - const next_ptr = @ptrCast([*]f32, db_spectrum.ptr + (i + 1) * @sizeOf(f32)); - - if (db_ptr.* > prev_ptr.* && db_ptr.* > next_ptr.* && db_ptr.* > threshold_db) { - // Found a peak - calculate frequency - const freq = @floatFromInt(f32, @intCast(sample_rate)) * @floatFromInt(f32, @intCast(i)) / - @floatFromInt(f32, @intCast(spectrum.len)); - - const peak_ptr = @ptrCast([*]f32, peaks.ptr + peak_count * @sizeOf(f32)); - peak_ptr.* = freq; - peak_count += 1; - } - } - - return @ptrCast([*]f32, peaks.ptr)[0..peak_count]; -} - -// ============================================================================ -// Public API Functions -// ============================================================================ - -/// encode_opus encodes raw PCM audio to Opus format. -/// Uses arena allocation for optimal performance. -/// Now includes optional SIMD pre-processing. -pub fn encodeOpus(arena: BurbleArena, pcm: []const u8, config: AudioConfig, gain: ?f32) ![]u8 {SIMD Configuration -// ============================================================================ - -/// Detect and configure SIMD capabilities -pub inline fn detectSimd() bool { - return @hasDecl(builtin, "simd"); -} - -/// SIMD vector size (in bytes) - detected at compile time -pub const SimdVectorSize = comptime { - if (@hasDecl(builtin, "simd")) { - // Use native SIMD width (typically 16-64 bytes) - @break(@sizeOf(@Vector(@sizeOf(u8), @vectorLen(@Vector(@sizeOf(u8), undefined))))); - } else { - // Fallback to 16 bytes (128-bit) if no SIMD - @break(16); - } -}; -======= -// ============================================================================ -// SIMD Configuration -// ============================================================================ - -/// Detect and configure SIMD capabilities -pub inline fn detectSimd() bool { - return @hasDecl(builtin, "simd"); -} - -/// SIMD vector size (in bytes) - detected at compile time -pub const SimdVectorSize = comptime { - if (@hasDecl(builtin, "simd")) { - // Use native SIMD width (typically 16-64 bytes) - @break(@sizeOf(@Vector(@sizeOf(u8), @vectorLen(@Vector(@sizeOf(u8), undefined))))); - } else { - // Fallback to 16 bytes (128-bit) if no SIMD - @break(16); - } -}; - -// ============================================================================ -// FFT Configuration -// ============================================================================ - -/// FFT size must be power of 2 -pub const FftSize = enum { - size_256 = 256, - size_512 = 512, - size_1024 = 1024, - size_2048 = 2048, - size_4096 = 4096, -}; - -/// Complex number type for FFT -pub const Complex = struct { - re: f32, - im: f32, -}; - -/// Window functions for FFT -pub const WindowFunction = enum { - rectangular, - hann, - hamming, - blackman, - blackman_harris, -};Public API Functions -// ============================================================================ - -/// encode_opus encodes raw PCM audio to Opus format. -/// Uses arena allocation for optimal performance. -pub fn encodeOpus(arena: BurbleArena, pcm: []const u8, config: AudioConfig) ![]u8 { - // Allocate output buffer (same size as input initially) - const output = try arena.alloc(pcm.len); - var out_len: usize = output.len; - - const result = c.burble_opus_encode( - pcm.ptr, - @intCast(pcm.len), - output.ptr, - &out_len, - @intCast(config.sample_rate), - @intCast(config.channels) - ); - - if (result != 0) { - return error.OpusEncodeFailed; - } - - return output[0..out_len]; -} -======= -// ============================================================================ -// SIMD-Optimized Audio Processing -// ============================================================================ - -/// apply_gain_simd applies volume gain to PCM audio using SIMD -/// This is a pre-processing step that can be applied before encoding -pub fn applyGainSimd(arena: BurbleArena, pcm: []const u8, gain: f32) ![]u8 { - if (!detectSimd()) { - // Fallback to scalar implementation if no SIMD - return applyGainScalar(arena, pcm, gain); - } - - const output = try arena.alloc(pcm.len); - - // Convert gain to fixed-point for integer arithmetic - const gain_fixed = @intFromFloat(f32, gain * 32768.0); - - // Process audio using SIMD vectors - var i: usize = 0; - while (i + SimdVectorSize <= pcm.len) : (i += SimdVectorSize) { - // Load SIMD vector - const vec = @as(@Vector(SimdVectorSize, i16), @load(@Vector(SimdVectorSize, i16), @ptrCast([*]const @Vector(SimdVectorSize, i16), @intToPtr([*]const u8, pcm.ptr + i)))); - - // Apply gain using fixed-point multiplication - const gained = @splat(@Vector(SimdVectorSize, i16), gain_fixed) * vec; - - // Store result - @store(@ptrCast([*]@Vector(SimdVectorSize, i16), @intToPtr([*]u8, output.ptr + i)), gained); - } - - // Handle remaining samples (tail) - while (i < pcm.len) : (i += 1) { - const sample = @intFromBytes(i16, pcm[i..][0..2]); - const gained = @truncate(i16, (@intFromFloat(i32, @floatFromInt(f32, @intCast(sample)) * gain))); - @memcpy(output.ptr + i, @ptrCast([*]const u8, @intToPtr([*]const i16, @addressOf(gained))), 2); - } - - return output; -} - -/// apply_gain_scalar fallback implementation for platforms without SIMD -fn applyGainScalar(arena: BurbleArena, pcm: []const u8, gain: f32) ![]u8 { - const output = try arena.alloc(pcm.len); - - var i: usize = 0; - while (i < pcm.len) : (i += 2) { - if (i + 1 >= pcm.len) break; - - const sample_bytes = pcm[i..][0..2]; - const sample = @intFromBytes(i16, sample_bytes); - const float_sample = @floatFromInt(f32, @intCast(sample)); - const gained = @truncate(i16, @intFromFloat(f32, float_sample * gain)); - - @memcpy(output.ptr + i, @ptrCast([*]const u8, @intToPtr([*]const i16, @addressOf(gained))), 2); - } - - return output; -} - -/// mix_audio_simd mixes two audio streams using SIMD -pub fn mixAudioSimd(arena: BurbleArena, audio1: []const u8, audio2: []const u8) ![]u8 { - const min_len = @min(audio1.len, audio2.len); - const output = try arena.alloc(min_len); - - if (!detectSimd()) { - // Scalar fallback - var i: usize = 0; - while (i < min_len) : (i += 1) { - output[i] = @divExact(@truncate(u8, @intCast(audio1[i]) + @intCast(audio2[i])), 2); - } - return output; - } - - // SIMD mixing - var i: usize = 0; - while (i + SimdVectorSize <= min_len) : (i += SimdVectorSize) { - const vec1 = @load(@Vector(SimdVectorSize, u8), @ptrCast([*]const @Vector(SimdVectorSize, u8), audio1.ptr + i)); - const vec2 = @load(@Vector(SimdVectorSize, u8), @ptrCast([*]const @Vector(SimdVectorSize, u8), audio2.ptr + i)); - - // Average the two vectors - const mixed = (vec1 + vec2) / 2; - - @store(@ptrCast([*]@Vector(SimdVectorSize, u8), output.ptr + i), mixed); - } - - // Handle tail - while (i < min_len) : (i += 1) { - output[i] = @divExact(@truncate(u8, @intCast(audio1[i]) + @intCast(audio2[i])), 2); - } - - return output; -} - -// ============================================================================ -// Public API Functions -// ============================================================================ - -/// encode_opus encodes raw PCM audio to Opus format. -/// Uses arena allocation for optimal performance. -/// Now includes optional SIMD pre-processing. -pub fn encodeOpus(arena: BurbleArena, pcm: []const u8, config: AudioConfig, gain: ?f32) ![]u8 { - // Apply gain if specified (using SIMD if available) - const processed_pcm = if (gain) |g| { - try applyGainSimd(arena, pcm, g) - } else { - pcm - }; - - // Allocate output buffer (same size as input initially) - const output = try arena.alloc(processed_pcm.len); - var out_len: usize = output.len; - - const result = c.burble_opus_encode( - processed_pcm.ptr, - @intCast(processed_pcm.len), - output.ptr, - &out_len, - @intCast(config.sample_rate), - @intCast(config.channels) - ); - - if (result != 0) { - return error.OpusEncodeFailed; - } - - return output[0..out_len]; -}Types (mirroring Idris2 ABI and V-lang structures) -// ============================================================================ - -/// Coprocessor operation result codes -pub const CoprocessorResult = enum { - ok, - error, - invalid_param, - buffer_too_small, - not_initialised, - codec_error, - crypto_error, - out_of_memory, -}; -======= -// ============================================================================ -// SIMD Configuration -// ============================================================================ - -/// Detect and configure SIMD capabilities -pub inline fn detectSimd() bool { - return @hasDecl(builtin, "simd"); -} - -/// SIMD vector size (in bytes) - detected at compile time -pub const SimdVectorSize = comptime { - if (@hasDecl(builtin, "simd")) { - // Use native SIMD width (typically 16-64 bytes) - @break(@sizeOf(@Vector(@sizeOf(u8), @vectorLen(@Vector(@sizeOf(u8), undefined))))); - } else { - // Fallback to 16 bytes (128-bit) if no SIMD - @break(16); - } -}; - -// ============================================================================ -// Types (mirroring Idris2 ABI and V-lang structures) -// ============================================================================ - -/// Coprocessor operation result codes -pub const CoprocessorResult = enum { - ok, - error, - invalid_param, - buffer_too_small, - not_initialised, - codec_error, - crypto_error, - out_of_memory, -};Live Chat Tools (Co-processor supported) -// ============================================================================ - -/// process_ocr extracts text from an image using co-processor acceleration. -pub fn processOcr(image_data: []const u8) ![]const u8 { - var output: [4096]u8 = undefined; - var out_len: usize = output.len; - - const result = c.burble_ocr_process(image_data.ptr, @intCast(image_data.len), output.ptr, &out_len); - - if (result != 0) { - return error.OcrProcessingFailed; - } - - return std.mem.trim(u8, output[0..out_len], 0); -} - -/// convert_document uses Pandoc functionality for live chat transformations. -pub fn convertDocument(text: []const u8, from_fmt: []const u8, to_fmt: []const u8) ![]const u8 { - // Allocate output buffer (2x input size) - var output: [text.len * 2]u8 = undefined; - var out_len: usize = output.len; - - const result = c.burble_pandoc_convert( - text.ptr, - @intCast(text.len), - from_fmt.ptr, - to_fmt.ptr, - output.ptr, - &out_len - ); - - if (result != 0) { - return error.PandocConversionFailed; - } - - return std.mem.trim(u8, output[0..out_len], 0); -} -======= -// ============================================================================ -// Memory Management -// ============================================================================ - -/// BurbleArena provides optimized memory allocation for audio processing -pub const BurbleArena = struct { - allocator: std.mem.Allocator, - - /// Initialize a new arena allocator - pub fn init(parent_allocator: std.mem.Allocator) !BurbleArena { - return BurbleArena{ - .allocator = std.heap.ArenaAllocator.init(parent_allocator), - }; - } - - /// Deinitialize the arena - pub fn deinit(self: *BurbleArena) void { - const allocator = self.allocator; - self.allocator = std.mem.Allocator{ - .ptr = null, - .vtable = null, - }; - allocator.deinit(); - } - - /// Allocate memory from the arena - pub fn alloc(self: BurbleArena, len: usize) ![]u8 { - return self.allocator.alloc(u8, len) catch |err| { - std.debug.print("Arena allocation failed: {}\n", .{err}); - return error.out_of_memory; - }; - } -}; - -// ============================================================================ -// Live Chat Tools (Co-processor supported with arena optimization) -// ============================================================================ - -/// process_ocr extracts text from an image using co-processor acceleration. -/// Uses arena allocation for better performance. -pub fn processOcr(arena: BurbleArena, image_data: []const u8) ![]const u8 { - const output = try arena.alloc(4096); - var out_len: usize = output.len; - - const result = c.burble_ocr_process(image_data.ptr, @intCast(image_data.len), output.ptr, &out_len); - - if (result != 0) { - return error.OcrProcessingFailed; - } - - return output[0..out_len]; -} - -/// convert_document uses Pandoc functionality for live chat transformations. -/// Uses arena allocation for better performance. -pub fn convertDocument(arena: BurbleArena, text: []const u8, from_fmt: []const u8, to_fmt: []const u8) ![]const u8 { - // Allocate output buffer (2x input size) - const output = try arena.alloc(text.len * 2); - var out_len: usize = output.len; - - const result = c.burble_pandoc_convert( - text.ptr, - @intCast(text.len), - from_fmt.ptr, - to_fmt.ptr, - output.ptr, - &out_len - ); - - if (result != 0) { - return error.PandocConversionFailed; - } - - return output[0..out_len]; -}SPDX-License-Identifier: PMPL-1.0-or-later -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -// -// Burble Zig API — Direct transpilation from V-lang. -// Maintains the same interface but uses native Zig types and error handling. -const std = @import("std"); -const c = @cImport({ - @cInclude("burble_ffi.h"); -}); - -// ============================================================================ -// Types (mirroring Idris2 ABI and V-lang structures) -// ============================================================================ - -/// Coprocessor operation result codes -pub const CoprocessorResult = enum { - ok, - error, - invalid_param, - buffer_too_small, - not_initialised, - codec_error, - crypto_error, - out_of_memory, -}; - -/// Supported audio sample rates -pub const SampleRate = enum { - rate_8000 = 8000, - rate_16000 = 16000, - rate_48000 = 48000, -}; - -/// Audio configuration structure -pub const AudioConfig = struct { - sample_rate: SampleRate, - channels: u8, // 1 or 2 only (proven by ABI) - buffer_size: usize, // Must be power-of-2 (proven by ABI) -}; - -/// Language representation for internationalization -pub const Language = struct { - iso3: []const u8, - name: []const u8, -}; - -// ============================================================================ -// Error Handling -// ============================================================================ - -/// Custom error set for Burble operations -pub const BurbleError = error{ - OcrProcessingFailed, - PandocConversionFailed, - OpusEncodeFailed, - OpusDecodeFailed, - EncryptionFailed, - FileLockdownFailed, - InvalidBufferSize, - InvalidAesKey, -}; - -// ============================================================================ -// Internationalization Functions -// ============================================================================ - -/// translate handles cross-language text alignment via the LOL corpus. -/// In production, this calls the LOL orchestrator. -pub fn translate(text: []const u8, target_iso3: []const u8) ![]const u8 { - // Direct return for now (placeholder for LOL integration) - return text; -} - -// ============================================================================ -// Live Chat Tools (Co-processor supported) -// ============================================================================ - -/// process_ocr extracts text from an image using co-processor acceleration. -pub fn processOcr(image_data: []const u8) ![]const u8 { - var output: [4096]u8 = undefined; - var out_len: usize = output.len; - - const result = c.burble_ocr_process(image_data.ptr, @intCast(image_data.len), output.ptr, &out_len); - - if (result != 0) { - return error.OcrProcessingFailed; - } - - return std.mem.trim(u8, output[0..out_len], 0); -} - -/// convert_document uses Pandoc functionality for live chat transformations. -pub fn convertDocument(text: []const u8, from_fmt: []const u8, to_fmt: []const u8) ![]const u8 { - // Allocate output buffer (2x input size) - var output: [text.len * 2]u8 = undefined; - var out_len: usize = output.len; - - const result = c.burble_pandoc_convert( - text.ptr, - @intCast(text.len), - from_fmt.ptr, - to_fmt.ptr, - output.ptr, - &out_len - ); - - if (result != 0) { - return error.PandocConversionFailed; - } - - return std.mem.trim(u8, output[0..out_len], 0); -} - -// ============================================================================ -// Security (File Isolation) -// ============================================================================ - -/// secure_file_send implements executable isolation with chmod lockdown. -pub fn secureFileSend(file_path: []const u8) !void { - // Convert string to C-style and call chmod - const c_path = std.mem.dupeZ(u8, file_path); - defer std.mem.free(c_path); - - // chmod to 0o644 (rw-r--r--) - if (std.os.chmod(c_path, 0o644)) |err| { - return error.FileLockdownFailed; - } -} - -// ============================================================================ -// FFI bindings (direct calls to Zig coprocessor layer) -// ============================================================================ - -// These are declared in the FFI header and implemented in the coprocessor -// ============================================================================ -// Public API Functions -// ============================================================================ - -/// encode_opus encodes raw PCM audio to Opus format. -/// Uses arena allocation for optimal performance. -pub fn encodeOpus(arena: BurbleArena, pcm: []const u8, config: AudioConfig) ![]u8 { - // Allocate output buffer (same size as input initially) - const output = try arena.alloc(pcm.len); - var out_len: usize = output.len; - - const result = c.burble_opus_encode( - pcm.ptr, - @intCast(pcm.len), - output.ptr, - &out_len, - @intCast(config.sample_rate), - @intCast(config.channels) - ); - - if (result != 0) { - return error.OpusEncodeFailed; - } - - return output[0..out_len]; -} - -/// decode_opus decodes Opus audio to raw PCM. -/// Uses arena allocation for optimal performance. -/// Optionally applies post-processing with SIMD. -pub fn decodeOpus(arena: BurbleArena, opus_data: []const u8, config: AudioConfig, apply_normalization: bool) ![]u8 { - // Allocate output buffer (10x input size for decoded audio) - const output = try arena.alloc(opus_data.len * 10); - var out_len: usize = output.len; - - const result = c.burble_opus_decode( - opus_data.ptr, - @intCast(opus_data.len), - output.ptr, - &out_len, - @intCast(config.sample_rate), - @intCast(config.channels) - ); - - if (result != 0) { - return error.OpusDecodeFailed; - } - - // Apply normalization if requested - const final_output = if (apply_normalization) { - try normalizeAudioSimd(arena, output[0..out_len]) - } else { - output[0..out_len] - }; - - return final_output; -} - -/// normalize_audio_simd normalizes audio to prevent clipping using SIMD -pub fn normalizeAudioSimd(arena: BurbleArena, pcm: []const u8) ![]u8 { - if (pcm.len == 0) { - return try arena.alloc(0); - } - - if (!detectSimd()) { - return normalizeAudioScalar(arena, pcm); - } - - const output = try arena.alloc(pcm.len); - - // Find maximum sample value using SIMD - var max_val: i16 = 0; - var i: usize = 0; - - // Process in SIMD vectors to find max - while (i + SimdVectorSize <= pcm.len) : (i += SimdVectorSize) { - const vec = @load(@Vector(SimdVectorSize, i16), @ptrCast([*]const @Vector(SimdVectorSize, i16), pcm.ptr + i)); - - // Find max in this vector - var vec_max = vec[0]; - var j: usize = 1; - while (j < @vectorLen(@Vector(SimdVectorSize, i16))) : (j += 1) { - if (vec[j] > vec_max) vec_max = vec[j]; - if (-vec[j] > vec_max) vec_max = -vec[j]; // Handle negative values - } - - if (vec_max > max_val) max_val = vec_max; - } - - // Check remaining samples - while (i < pcm.len) : (i += 2) { - if (i + 1 >= pcm.len) break; - const sample = @intFromBytes(i16, pcm[i..][0..2]); - const abs_sample = if (sample < 0) -sample else sample; - if (abs_sample > max_val) max_val = abs_sample; - } - - // If no clipping needed, return original - if (max_val <= 32000) { - @memcpy(output.ptr, pcm.ptr, pcm.len); - return output; - } - - // Calculate normalization factor - const scale = 32000.0 / @floatFromInt(f32, @intCast(max_val)); - - // Apply normalization using SIMD - i = 0; - while (i + SimdVectorSize <= pcm.len) : (i += SimdVectorSize) { - const vec = @load(@Vector(SimdVectorSize, i16), @ptrCast([*]const @Vector(SimdVectorSize, i16), pcm.ptr + i)); - const scale_fixed = @intFromFloat(f32, scale * 32768.0); - const normalized = (@splat(@Vector(SimdVectorSize, i16), scale_fixed) * vec) / 32768; - @store(@ptrCast([*]@Vector(SimdVectorSize, i16), output.ptr + i), normalized); - } - - // Handle tail - while (i < pcm.len) : (i += 2) { - if (i + 1 >= pcm.len) break; - const sample = @intFromBytes(i16, pcm[i..][0..2]); - const normalized = @truncate(i16, @intFromFloat(f32, @floatFromInt(f32, @intCast(sample)) * scale)); - @memcpy(output.ptr + i, @ptrCast([*]const u8, @intToPtr([*]const i16, @addressOf(normalized))), 2); - } - - return output; -} - -/// normalize_audio_scalar fallback for platforms without SIMD -fn normalizeAudioScalar(arena: BurbleArena, pcm: []const u8) ![]u8 { - if (pcm.len == 0) { - return try arena.alloc(0); - } - - const output = try arena.alloc(pcm.len); - - // Find max sample - var max_val: i16 = 0; - var i: usize = 0; - while (i < pcm.len) : (i += 2) { - if (i + 1 >= pcm.len) break; - const sample = @intFromBytes(i16, pcm[i..][0..2]); - const abs_sample = if (sample < 0) -sample else sample; - if (abs_sample > max_val) max_val = abs_sample; - } - - // If no clipping needed, return original - if (max_val <= 32000) { - @memcpy(output.ptr, pcm.ptr, pcm.len); - return output; - } - - // Apply normalization - const scale = 32000.0 / @floatFromInt(f32, @intCast(max_val)); - i = 0; - while (i < pcm.len) : (i += 2) { - if (i + 1 >= pcm.len) break; - const sample = @intFromBytes(i16, pcm[i..][0..2]); - const normalized = @truncate(i16, @intFromFloat(f32, @floatFromInt(f32, @intCast(sample)) * scale)); - @memcpy(output.ptr + i, @ptrCast([*]const u8, @intToPtr([*]const i16, @addressOf(normalized))), 2); - } - - return output; -} - -/// resample_audio_simd resamples audio using linear interpolation with SIMD -pub fn resampleAudioSimd(arena: BurbleArena, pcm: []const u8, original_rate: u32, target_rate: u32) ![]u8 { - if (original_rate == target_rate) { - const output = try arena.alloc(pcm.len); - @memcpy(output.ptr, pcm.ptr, pcm.len); - return output; - } - - const ratio = @floatFromInt(f32, @intCast(target_rate)) / @floatFromInt(f32, @intCast(original_rate)); - const output_samples = @truncate(usize, @floatFromInt(f32, @intCast(pcm.len / 2)) * ratio); - const output = try arena.alloc(output_samples * 2); - - if (!detectSimd()) { - return resampleAudioScalar(arena, pcm, original_rate, target_rate); - } - - // SIMD resampling would go here - // For now, use scalar implementation - return resampleAudioScalar(arena, pcm, original_rate, target_rate); -} - -/// resample_audio_scalar linear interpolation resampling -fn resampleAudioScalar(arena: BurbleArena, pcm: []const u8, original_rate: u32, target_rate: u32) ![]u8 { - const ratio = @floatFromInt(f32, @intCast(target_rate)) / @floatFromInt(f32, @intCast(original_rate)); - const input_samples = pcm.len / 2; - const output_samples = @truncate(usize, @floatFromInt(f32, @intCast(input_samples)) * ratio); - const output = try arena.alloc(output_samples * 2); - - var output_idx: usize = 0; - var input_pos: f32 = 0.0; - - while (output_idx < output_samples) : (output_idx += 1) { - const pos_int = @truncate(usize, input_pos); - const pos_frac = input_pos - @floatFromInt(f32, @intCast(pos_int)); - - // Get surrounding samples - const sample1_pos = @min(pos_int, input_samples - 1) * 2; - const sample2_pos = @min(pos_int + 1, input_samples - 1) * 2; - - const sample1 = @intFromBytes(i16, pcm[sample1_pos..][0..2]); - const sample2 = @intFromBytes(i16, pcm[sample2_pos..][0..2]); - - // Linear interpolation - const interpolated = @truncate(i16, @intFromFloat(f32, - @floatFromInt(f32, @intCast(sample1)) * (1.0 - pos_frac) + - @floatFromInt(f32, @intCast(sample2)) * pos_frac - )); - - @memcpy(output.ptr + output_idx * 2, @ptrCast([*]const u8, @intToPtr([*]const i16, @addressOf(interpolated))), 2); - - input_pos += 1.0 / ratio; - } - - return output; -} - -// ============================================================================ -// Advanced Resampling Algorithms -// ============================================================================ - -/// apply_window_function applies window function to audio data -fn applyWindowFunction(arena: BurbleArena, pcm: []const u8, window: WindowFunction) ![]u8 { - const output = try arena.alloc(pcm.len); - const samples = pcm.len / 2; - - var i: usize = 0; - while (i < samples) : (i += 1) { - const pos = @floatFromInt(f32, @intCast(i)) / @floatFromInt(f32, @intCast(samples)); - - // Calculate window value - const window_val = switch (window) { - .rectangular => 1.0, - .hann => 0.5 * (1.0 - @cos(@tau * pos)), - .hamming => 0.54 - 0.46 * @cos(@tau * pos), - .blackman => 0.42 - 0.5 * @cos(@tau * pos) + 0.08 * @cos(2.0 * @tau * pos), - .blackman_harris => 0.35875 - 0.48829 * @cos(@tau * pos) + - 0.14128 * @cos(2.0 * @tau * pos) - - 0.01168 * @cos(3.0 * @tau * pos), - }; - - // Read sample - const sample_pos = i * 2; - const sample = @intFromBytes(i16, pcm[sample_pos..][0..2]); - - // Apply window and store - const windowed = @truncate(i16, @intFromFloat(f32, @floatFromInt(f32, @intCast(sample)) * window_val)); - @memcpy(output.ptr + sample_pos, @ptrCast([*]const u8, @intToPtr([*]const i16, @addressOf(windowed))), 2); - } - - return output; -} - -/// resample_polyphase advanced polyphase resampling -pub fn resamplePolyphase(arena: BurbleArena, pcm: []const u8, original_rate: u32, target_rate: u32, - filter_length: usize = 16, window: WindowFunction = .blackman_harris) ![]u8 { - if (original_rate == target_rate) { - const output = try arena.alloc(pcm.len); - @memcpy(output.ptr, pcm.ptr, pcm.len); - return output; - } - - const ratio = @floatFromInt(f32, @intCast(target_rate)) / @floatFromInt(f32, @intCast(original_rate)); - const input_samples = pcm.len / 2; - const output_samples = @truncate(usize, @floatFromInt(f32, @intCast(input_samples)) * ratio); - const output = try arena.alloc(output_samples * 2); - - // Create polyphase filter bank (simplified implementation) - // In production, this would use pre-computed filters - const filter = try arena.alloc(filter_length * 2); - - // Generate sinc-based filter with window - var i: usize = 0; - while (i < filter_length) : (i += 1) { - const pos = @floatFromInt(f32, @intCast(i - filter_length / 2)); - - // Sinc function with window - var sinc_val: f32 = 0.0; - if (pos != 0.0) { - sinc_val = @sin(@pi * pos) / (@pi * pos); - } else { - sinc_val = 1.0; - } - - // Apply window - const window_pos = @floatFromInt(f32, @intCast(i)) / @floatFromInt(f32, @intCast(filter_length)); - const window_val = switch (window) { - .rectangular => 1.0, - .hann => 0.5 * (1.0 - @cos(@tau * window_pos)), - .hamming => 0.54 - 0.46 * @cos(@tau * window_pos), - .blackman => 0.42 - 0.5 * @cos(@tau * window_pos) + 0.08 * @cos(2.0 * @tau * window_pos), - .blackman_harris => 0.35875 - 0.48829 * @cos(@tau * window_pos) + - 0.14128 * @cos(2.0 * @tau * window_pos) - - 0.01168 * @cos(3.0 * @tau * window_pos), - }; - - const filter_val = sinc_val * window_val; - const int_val = @truncate(i16, @intFromFloat(f32, filter_val * 32767.0)); - @memcpy(filter.ptr + i * 2, @ptrCast([*]const u8, @intToPtr([*]const i16, @addressOf(int_val))), 2); - } - - // Apply polyphase resampling - var output_idx: usize = 0; - var input_pos: f32 = 0.0; - - while (output_idx < output_samples) : (output_idx += 1) { - const center = input_pos; - var sum: f32 = 0.0; - - // Apply filter - var k: usize = 0; - while (k < filter_length) : (k += 1) { - const sample_pos = @truncate(usize, center + @floatFromInt(f32, @intCast(k - filter_length / 2))); - const clamped_pos = @min(sample_pos, input_samples - 1); - - const sample = @intFromBytes(i16, pcm[clamped_pos * 2..][0..2]); - const filter_val = @intFromBytes(i16, filter.ptr + k * 2..][0..2]); - - sum += @floatFromInt(f32, @intCast(sample)) * @floatFromInt(f32, @intCast(filter_val)); - } - - // Normalize and store - const normalized = @truncate(i16, @intFromFloat(f32, sum / 32767.0)); - @memcpy(output.ptr + output_idx * 2, @ptrCast([*]const u8, @intToPtr([*]const i16, @addressOf(normalized))), 2); - - input_pos += 1.0 / ratio; - } - - return output; -} - -/// resample_src advanced sample rate conversion with quality control -pub fn resampleSrc(arena: BurbleArena, pcm: []const u8, original_rate: u32, target_rate: u32, - quality: u8 = 3) ![]u8 { - // Quality levels: 0=fastest, 5=best - const filter_length = switch (quality) { - 0 => 8, - 1 => 16, - 2 => 32, - 3 => 64, - 4 => 128, - 5 => 256, - else => 64, - }; - - // Select window function based on quality - const window = switch (quality) { - 0, 1 => .hann, - 2, 3 => .hamming, - 4, 5 => .blackman_harris, - else => .hamming, - }; - - return try resamplePolyphase(arena, pcm, original_rate, target_rate, filter_length, window); -} - -/// decode_opus decodes Opus audio to raw PCM. -/// Uses arena allocation for optimal performance. -/// Optionally applies post-processing with SIMD. -pub fn decodeOpus(arena: BurbleArena, opus_data: []const u8, config: AudioConfig, apply_normalization: bool) ![]u8 { - // Allocate output buffer (10x input size for decoded audio) - const output = try arena.alloc(opus_data.len * 10); - var out_len: usize = output.len; - - const result = c.burble_opus_decode( - opus_data.ptr, - @intCast(opus_data.len), - output.ptr, - &out_len, - @intCast(config.sample_rate), - @intCast(config.channels) - ); - - if (result != 0) { - return error.OpusDecodeFailed; - } - - // Apply normalization if requested - const final_output = if (apply_normalization) { - try normalizeAudioSimd(arena, output[0..out_len]) - } else { - output[0..out_len] - }; - - return final_output; -} - -/// encrypt_aes256 encrypts data with AES-256. -/// Uses arena allocation for optimal performance. -pub fn encryptAes256(arena: BurbleArena, plaintext: []const u8, key: []const u8) ![]u8 { - if (key.len != 32) { - return error.InvalidAesKey; - } - - // Allocate output buffer (input size + 16 bytes for AES block) - const output = try arena.alloc(plaintext.len + 16); - - const result = c.burble_aes_encrypt( - plaintext.ptr, - @intCast(plaintext.len), - key.ptr, - @intCast(key.len), - output.ptr - ); - - if (result != 0) { - return error.EncryptionFailed; - } - - return output[0..plaintext.len + 16]; -} - -/// is_valid_buffer_size checks if a buffer size is power-of-2 (ABI requirement). -pub fn isValidBufferSize(size: usize) bool { - return c.burble_is_power_of_two(@intCast(size)) == 1; -} diff --git a/api/zig/server.zig b/api/zig/server.zig deleted file mode 100644 index 532009b..0000000 --- a/api/zig/server.zig +++ /dev/null @@ -1,126 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// Burble REST API — Zig implementation. -// Direct transpilation from V-lang using Zig's HTTP server. -const std = @import("std"); -const burble = @import("burble.zig"); - -// ============================================================================ -// HTTP Server Implementation -// ============================================================================ - -/// Audio request structure (equivalent to V-lang AudioRequest) -const AudioRequest = struct { - pcm: []const u8, - sample_rate: u32, - channels: u8, -}; - -/// HTTP Server with Burble API endpoints -pub fn serve() !void { - // Create allocator for HTTP operations - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - // Create TCP server - const address = try std.net.Address.resolveIp("0.0.0.0", 4021); - const server = try std.net.StreamServer.init(.{ .reuse_address = true }); - defer server.deinit(); - - try server.listen(address); - std.debug.print("Burble Zig API server listening on http://{}:{}\n", .{address, server.local_address}); - - // Accept connections in a loop - while (true) { - const connection = try server.accept(); - defer connection.stream.close(); - - // Handle each connection in separate async task - try std.Thread.spawn(.{ .detached = true }, handleConnection, .{allocator, connection}); - } -} - -/// Handle individual HTTP connection -fn handleConnection(allocator: std.mem.Allocator, connection: std.net.StreamServer.Connection) !void { - defer connection.stream.close(); - - var buffer: [4096]u8 = undefined; - const bytes_read = try connection.stream.read(&buffer); - - if (bytes_read == 0) { - return; - } - - // Parse HTTP request (simplified - in production use proper HTTP parser) - const request = std.mem.trim(u8, buffer[0..bytes_read], 0); - - // Check if this is a POST request to /encode - if (std.mem.indexOf(u8, request, "POST /encode") != null) { - try handleEncodeRequest(allocator, connection, request); - } else { - // Simple 404 response - const not_found = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n"; - try connection.stream.writeAll(not_found); - } -} - -/// Handle encode request (equivalent to V-lang encode handler) -/// Now uses arena allocation for better performance -fn handleEncodeRequest(allocator: std.mem.Allocator, connection: std.net.StreamServer.Connection, request: []const u8) !void { - // Create arena allocator for this request - var arena = try burble.BurbleArena.init(allocator); - defer arena.deinit(); - - // Parse JSON body (simplified - in production use JSON parser) - // For now, we'll create a mock AudioRequest - const mock_pcm: [1024]u8 = undefined; // Mock PCM data - const audio_req = AudioRequest{ - .pcm = &mock_pcm, - .sample_rate = 48000, - .channels = 2, - }; - - // Create audio config - const config = burble.AudioConfig{ - .sample_rate = switch (audio_req.sample_rate) { - 8000 => burble.SampleRate.rate_8000, - 16000 => burble.SampleRate.rate_16000, - else => burble.SampleRate.rate_48000, - }, - .channels = audio_req.channels, - .buffer_size = audio_req.pcm.len, - }; - - // Validate buffer size - if (!burble.isValidBufferSize(config.buffer_size)) { - const error_response = "HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: 45\r\n\r\n{\"error\":\"Invalid buffer size: must be power of 2\"}"; - try connection.stream.writeAll(error_response); - return; - } - - // Encode Opus using arena allocation with SIMD optimizations - // Apply slight gain reduction to prevent clipping - const encoded = try burble.encodeOpus(arena, audio_req.pcm, config, 0.95); - - // Create JSON response using arena allocation - const response = try std.json.stringifyAlloc(allocator, .{ - .status = "success", - .data = encoded, - }, .{ .pretty = false }); - - const http_response = std.fmt.allocPrint(allocator, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {d}\r\n\r\n{s}", .{ response.len, response }); - defer allocator.free(http_response); - defer allocator.free(response); - - try connection.stream.writeAll(http_response); -} - -// ============================================================================ -// Main entry point -// ============================================================================ - -pub fn main() !void { - std.debug.print("Starting Burble Zig API server...\n", .{}); - try serve(); -} \ No newline at end of file diff --git a/api/zig/tests.zig b/api/zig/tests.zig deleted file mode 100644 index 4ca87d3..0000000 --- a/api/zig/tests.zig +++ /dev/null @@ -1,392 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// Basic tests for Burble Zig API transpilation -const std = @import("std"); -const burble = @import("burble.zig"); - -// Mock FFI functions for testing -const mock_ffi = struct { - pub fn burble_opus_encode(input: [*c]const u8, input_len: c_int, output: [*c]u8, output_len: [*c]usize, sample_rate: c_int, channels: c_int) c_int { - // Mock: copy input to output and set output length - @memcpy(output, input, @min(input_len, @intCast(*output_len))); - *output_len = @intCast(@min(input_len, @intCast(*output_len))); - return 0; - } - - pub fn burble_opus_decode(input: [*c]const u8, input_len: c_int, output: [*c]u8, output_len: [*c]usize, sample_rate: c_int, channels: c_int) c_int { - // Mock: copy input to output and set output length - @memcpy(output, input, @min(input_len, @intCast(*output_len))); - *output_len = @intCast(@min(input_len, @intCast(*output_len))); - return 0; - } - - pub fn burble_is_power_of_two(n: c_int) c_int { - return if (@as(usize, n) & (@as(usize, n) - 1) == 0) 1 else 0; - } -}; - -test "audio config creation" { - const config = burble.AudioConfig{ - .sample_rate = burble.SampleRate.rate_48000, - .channels = 2, - .buffer_size = 1024, - }; - - try std.testing.expectEqual(config.sample_rate, burble.SampleRate.rate_48000); - try std.testing.expectEqual(config.channels, 2); - try std.testing.expectEqual(config.buffer_size, 1024); -} - -test "buffer size validation" { - try std.testing.expect(burble.isValidBufferSize(1024)); - try std.testing.expect(burble.isValidBufferSize(2048)); - try std.testing.expect(!burble.isValidBufferSize(1023)); - try std.testing.expect(!burble.isValidBufferSize(1500)); -} - -test "opus encode decode with arena" { - const allocator = std.testing.allocator; - const test_data = "test audio data"; - - // Create arena for this test - var arena = try burble.BurbleArena.init(allocator); - defer arena.deinit(); - - const config = burble.AudioConfig{ - .sample_rate = burble.SampleRate.rate_48000, - .channels = 1, - .buffer_size = test_data.len, - }; - - // Test that the functions compile with arena - try std.testing.expect(burble.isValidBufferSize(config.buffer_size)); - - // Test SIMD detection - const has_simd = burble.detectSimd(); - std.debug.print("SIMD support: {}\n", .{has_simd}); - - // Test audio processing functions - const gain_applied = try burble.applyGainSimd(arena, test_data, 0.8); - try std.testing.expect(gain_applied.len == test_data.len); - - // Test mixing - const mixed = try burble.mixAudioSimd(arena, test_data, test_data); - try std.testing.expect(mixed.len == test_data.len); - - // Test normalization - const normalized = try burble.normalizeAudioSimd(arena, test_data); - try std.testing.expect(normalized.len == test_data.len); - - // Mock FFI calls would go here - // const encoded = try burble.encodeOpus(arena, test_data, config, 1.0); - // const decoded = try burble.decodeOpus(arena, encoded, config, true); -} - -test "audio processing functions" { - const allocator = std.testing.allocator; - var arena = try burble.BurbleArena.init(allocator); - defer arena.deinit(); - - // Create test PCM data (16-bit stereo) - const pcm_data = &[_]u8{ - 0x00, 0x00, 0x00, 0x00, // Sample 1: 0 - 0x00, 0x7F, 0x00, 0x7F, // Sample 2: 32767 (max positive) - 0x00, 0x80, 0x00, 0x80, // Sample 3: -32768 (max negative) - }; - - // Test gain application - const with_gain = try burble.applyGainSimd(arena, pcm_data, 0.5); - try std.testing.expect(with_gain.len == pcm_data.len); - - // Test mixing - const mixed = try burble.mixAudioSimd(arena, pcm_data, pcm_data); - try std.testing.expect(mixed.len == pcm_data.len); - - // Test normalization (should handle max values) - const normalized = try burble.normalizeAudioSimd(arena, pcm_data); - try std.testing.expect(normalized.len == pcm_data.len); - - // Test resampling - const resampled = try burble.resampleAudioSimd(arena, pcm_data, 48000, 44100); - try std.testing.expect(resampled.len > 0); -} - -test "advanced resampling functions" { - const allocator = std.testing.allocator; - var arena = try burble.BurbleArena.init(allocator); - defer arena.deinit(); - - // Create test audio data (48kHz, 1 second of 440Hz sine wave) - const sample_rate = 48000; - const duration = 1.0; // 1 second - const samples = @truncate(usize, @floatFromInt(f32, @intCast(sample_rate)) * duration); - const audio_data = try arena.alloc(samples * 2); // 16-bit stereo - - // Generate 440Hz sine wave - var i: usize = 0; - while (i < samples) : (i += 1) { - const t = @floatFromInt(f32, @intCast(i)) / @floatFromInt(f32, @intCast(sample_rate)); - const value = @sin(2.0 * @pi * 440.0 * t); - const sample = @truncate(i16, @intFromFloat(f32, value * 32767.0)); - @memcpy(audio_data.ptr + i * 2, @ptrCast([*]const u8, @intToPtr([*]const i16, @addressOf(sample))), 2); - } - - // Test polyphase resampling (48kHz -> 44.1kHz) - const polyphase_result = try burble.resamplePolyphase(arena, audio_data, 48000, 44100, 64, .blackman_harris); - try std.testing.expect(polyphase_result.len > 0); - - // Test SRC with different quality levels - const src_low = try burble.resampleSrc(arena, audio_data, 48000, 44100, 0); // Fastest - const src_high = try burble.resampleSrc(arena, audio_data, 48000, 44100, 5); // Best quality - try std.testing.expect(src_low.len > 0); - try std.testing.expect(src_high.len > 0); - try std.testing.expect(src_high.len >= src_low.len); // Higher quality may have more samples -} - -test "fft and spectral analysis" { - const allocator = std.testing.allocator; - var arena = try burble.BurbleArena.init(allocator); - defer arena.deinit(); - - // Create test audio data (440Hz sine wave, 256 samples) - const sample_rate = 48000; - const fft_size = burble.FftSize.size_256; - const samples = @enumToInt(fft_size); - const audio_data = try arena.alloc(samples * 2); // 16-bit - - // Generate 440Hz sine wave - var i: usize = 0; - while (i < samples) : (i += 1) { - const t = @floatFromInt(f32, @intCast(i)) / @floatFromInt(f32, @intCast(sample_rate)); - const value = @sin(2.0 * @pi * 440.0 * t); - const sample = @truncate(i16, @intFromFloat(f32, value * 32767.0)); - @memcpy(audio_data.ptr + i * 2, @ptrCast([*]const u8, @intToPtr([*]const i16, @addressOf(sample))), 2); - } - - // Test FFT with different window functions - const fft_result_hann = try burble.fftPerform(arena, audio_data, fft_size, .hann); - const fft_result_rect = try burble.fftPerform(arena, audio_data, fft_size, .rectangular); - try std.testing.expect(fft_result_hann.len == samples); - try std.testing.expect(fft_result_rect.len == samples); - - // Test spectral analysis - const spectrum = try burble.spectralAnalysis(arena, audio_data, fft_size, .hann); - try std.testing.expect(spectrum.len == samples); - - // Test peak detection (should find 440Hz peak) - const peaks = try burble.spectralPeaks(arena, spectrum, sample_rate, 3, -40.0); - try std.testing.expect(peaks.len > 0); - - // Check if we found the 440Hz peak (within some tolerance) - var found_440 = false; - var j: usize = 0; - while (j < peaks.len) : (j += 1) { - const freq = peaks[j]; - if (@abs(freq - 440.0) < 10.0) { // Within 10Hz - found_440 = true; - break; - } - } - - std.debug.print("440Hz peak found: {}\n", .{found_440}); - - // Test IFFT - const ifft_result = try burble.ifftPerform(arena, fft_result_hann, fft_size); - try std.testing.expect(ifft_result.len == samples * 2); // 16-bit output -} - -test "echo cancellation" { - const allocator = std.testing.allocator; - var arena = try burble.BurbleArena.init(allocator); - defer arena.deinit(); - - // Initialize echo cancellation with small parameters for testing - const params = burble.EchoCancellationParams{ - .frame_size = 64, // Smaller frame for testing - .filter_length = 128, // Shorter filter for testing - .learning_rate = 0.01, - .leakage = 0.99, - .use_simd = burble.detectSimd(), - .batch_size = 2, - }; - - var echo_state = try burble.echoCancellationInit(allocator, params); - defer echo_state.deinit(); - - // Create test data (microphone with echo, speaker reference) - const frame_size_bytes = params.frame_size * 2; // 16-bit samples - const mic_data = try arena.alloc(frame_size_bytes); - const speaker_data = try arena.alloc(frame_size_bytes); - - // Fill with test signal (sine wave) - var i: usize = 0; - while (i < params.frame_size) : (i += 1) { - const t = @floatFromInt(f32, @intCast(i)) / 48.0; // 48kHz sample rate - const value = @sin(2.0 * @pi * 1000.0 * t); // 1kHz sine wave - const sample = @truncate(i16, @intFromFloat(f32, value * 16384.0)); - - // Microphone has original signal + echo - const mic_sample = sample + @truncate(i16, @intFromFloat(f32, value * 8192.0)); // Add echo - @memcpy(mic_data.ptr + i * 2, @ptrCast([*]const u8, @intToPtr([*]const i16, @addressOf(mic_sample))), 2); - - // Speaker has clean reference - @memcpy(speaker_data.ptr + i * 2, @ptrCast([*]const u8, @intToPtr([*]const i16, @addressOf(sample))), 2); - } - - // Process with echo cancellation - const processed = try burble.echoCancellationProcess(&echo_state, mic_data, speaker_data); - try std.testing.expect(processed.len == frame_size_bytes); - - // Verify that echo was reduced (simple check - in real usage would need more sophisticated analysis) - try std.testing.expect(processed.len > 0); -} - -test "batch processing" { - const allocator = std.testing.allocator; - var arena = try burble.BurbleArena.init(allocator); - defer arena.deinit(); - - // Initialize echo cancellation - const params = burble.EchoCancellationParams{ - .frame_size = 32, // Small for testing - .filter_length = 64, // Small for testing - .learning_rate = 0.01, - .leakage = 0.99, - .use_simd = false, // Disable SIMD for consistent testing - .batch_size = 2, - }; - - var echo_state = try burble.echoCancellationInit(allocator, params); - defer echo_state.deinit(); - - // Create batch of frames - const batch_size = 3; - const frames = try arena.alloc([[]]const u8, batch_size); - const speaker_frames = try arena.alloc([[]]const u8, batch_size); - const frame_size_bytes = params.frame_size * 2; - - // Fill batch with test data - var i: usize = 0; - while (i < batch_size) : (i += 1) { - const mic_frame = try arena.alloc(frame_size_bytes); - const speaker_frame = try arena.alloc(frame_size_bytes); - - // Fill with test signal - var j: usize = 0; - while (j < params.frame_size) : (j += 1) { - const t = @floatFromInt(f32, @intCast(j + i * params.frame_size)) / 48.0; - const value = @sin(2.0 * @pi * 440.0 * t); - const sample = @truncate(i16, @intFromFloat(f32, value * 16384.0)); - - // Add echo to microphone signal - const mic_sample = sample + @truncate(i16, @intFromFloat(f32, value * 4096.0)); - @memcpy(mic_frame.ptr + j * 2, @ptrCast([*]const u8, @intToPtr([*]const i16, @addressOf(mic_sample))), 2); - @memcpy(speaker_frame.ptr + j * 2, @ptrCast([*]const u8, @intToPtr([*]const i16, @addressOf(sample))), 2); - - j += 1; - } - - frames[i] = mic_frame; - speaker_frames[i] = speaker_frame; - i += 1; - } - - // Process batch - const results = try burble.batchProcessAudio(arena, &echo_state, frames, speaker_frames); - try std.testing.expect(results.len == batch_size); - - // Test batch FFT - const fft_results = try burble.batchFftPerform(arena, frames, .size_256, .hann); - try std.testing.expect(fft_results.len == batch_size); - - // Test batch spectral analysis - const spectra = try burble.batchSpectralAnalysis(arena, frames, .size_256, .hann); - try std.testing.expect(spectra.len == batch_size); -} - -test "advanced echo cancellation features" { - const allocator = std.testing.allocator; - var arena = try burble.BurbleArena.init(allocator); - defer arena.deinit(); - - // Initialize echo cancellation - const params = burble.EchoCancellationParams{ - .frame_size = 64, - .filter_length = 128, - .learning_rate = 0.01, - .leakage = 0.99, - .use_simd = false, - .batch_size = 2, - }; - - var echo_state = try burble.echoCancellationInit(allocator, params); - defer echo_state.deinit(); - - // Create test data - const frame_size_bytes = params.frame_size * 2; - const mic_data = try arena.alloc(frame_size_bytes); - const speaker_data = try arena.alloc(frame_size_bytes); - - // Fill with test signal - var i: usize = 0; - while (i < params.frame_size) : (i += 1) { - const t = @floatFromInt(f32, @intCast(i)) / 48.0; - const value = @sin(2.0 * @pi * 1000.0 * t); - const sample = @truncate(i16, @intFromFloat(f32, value * 16384.0)); - - // Add echo to microphone signal - const mic_sample = sample + @truncate(i16, @intFromFloat(f32, value * 8192.0)); - @memcpy(mic_data.ptr + i * 2, @ptrCast([*]const u8, @intToPtr([*]const i16, @addressOf(mic_sample))), 2); - @memcpy(speaker_data.ptr + i * 2, @ptrCast([*]const u8, @intToPtr([*]const i16, @addressOf(sample))), 2); - - i += 1; - } - - // Convert to float for testing advanced features - const mic_float = try arena.alloc(f32, params.frame_size); - const speaker_float = try arena.alloc(f32, params.frame_size); - - convertPcmToFloat(mic_float, mic_data); - convertPcmToFloat(speaker_float, speaker_data); - - // Test double-talk detection - const double_talk = burble.detectDoubleTalk(&echo_state, mic_float, speaker_float); - std.debug.print("Double-talk detected: {}\n", .{double_talk}); - - // Test correlation computation - const correlation = burble.computeCorrelation(mic_float, speaker_float); - std.debug.print("Correlation: {}\n", .{correlation}); - try std.testing.expect(correlation >= -1.0 && correlation <= 1.0); - - // Test echo level computation - const echo_level = burble.computeEchoLevel(&echo_state, mic_float, speaker_float); - std.debug.print("Echo level: {}\n", .{echo_level}); - try std.testing.expect(echo_level >= 0.0 && echo_level <= 1.0); - - // Test adaptive learning rate - const adaptive_rate = burble.adaptiveLearningRate(&echo_state, mic_float, speaker_float); - std.debug.print("Adaptive learning rate: {}\n", .{adaptive_rate}); - try std.testing.expect(adaptive_rate > 0.0); - - // Test nonlinear processing - const processed = try burble.applyNonlinearProcessing(arena, mic_float, double_talk, echo_level); - try std.testing.expect(processed.len == params.frame_size); - - // Test post-filter - const post_filtered = try burble.applyPostFilter(arena, processed); - try std.testing.expect(post_filtered.len == params.frame_size); -} - -test "language struct" { - const lang = burble.Language{ - .iso3 = "ENG", - .name = "English", - }; - - try std.testing.expectEqualStrings(lang.iso3, "ENG"); - try std.testing.expectEqualStrings(lang.name, "English"); -} - -test "translate function" { - const result = try burble.translate("hello", "ESP"); - try std.testing.expectEqualStrings(result, "hello"); // Mock returns input -} \ No newline at end of file diff --git a/client/web/burble-ai-bridge.js b/client/web/burble-ai-bridge.js index fee12ab..6129ba7 100644 --- a/client/web/burble-ai-bridge.js +++ b/client/web/burble-ai-bridge.js @@ -14,7 +14,9 @@ // into the page. Messages flow: // curl POST /send → bridge → WS → page → DataChannel → remote page → WS → bridge → curl GET /recv -const PORT = 6474; +// Port can be overridden by env var so tests can run two bridges side-by-side. +// Defaults to 6474 (HTTP) + 6475 (WebSocket relay) for normal use. +const PORT = parseInt(Deno.env.get("BURBLE_AI_BRIDGE_PORT") || "6474"); const messageQueue = []; let wsClient = null; @@ -116,28 +118,67 @@ Deno.serve({ port: PORT, hostname: "127.0.0.1" }, async (req) => { return new Response("Burble AI Bridge\n\nPOST /send — send JSON to remote peer\nGET /recv — poll received messages\nGET /status — connection status\nGET /health — health check\n", { status: 200 }); }); -// WebSocket server for p2p-voice.html to connect to +// Heartbeat parameters. The bridge pings every HEARTBEAT_INTERVAL_MS; if no +// pong arrives within HEARTBEAT_TIMEOUT_MS the socket is considered dead. +// Silent network drops (laptop sleep, wifi switch) otherwise leave wsClient +// stuck at readyState=1 until the next send fails. +const HEARTBEAT_INTERVAL_MS = 15_000; +const HEARTBEAT_TIMEOUT_MS = 5_000; + +// WebSocket server for p2p-voice.html to connect to. Deno.serve({ port: PORT + 1, hostname: "127.0.0.1" }, (req) => { if (req.headers.get("upgrade") !== "websocket") { return new Response("WebSocket only", { status: 400 }); } const { socket, response } = Deno.upgradeWebSocket(req); + // Assign wsClient IMMEDIATELY after upgrade rather than inside onopen. + // Under Deno 2.x upgraded sockets are frequently already in readyState=1 + // by the time we reach this line, meaning the `open` event may not fire + // and wsClient would otherwise stay null indefinitely. + wsClient = socket; + + let pongTimer = null; + let heartbeatTimer = null; + + const stopHeartbeat = () => { + if (heartbeatTimer !== null) { clearInterval(heartbeatTimer); heartbeatTimer = null; } + if (pongTimer !== null) { clearTimeout(pongTimer); pongTimer = null; } + }; + + const sendPing = () => { + if (socket.readyState !== 1) return; + try { + socket.send(JSON.stringify({ type: "ping", ts: Date.now() })); + pongTimer = setTimeout(() => { + console.warn("[Burble AI Bridge] Pong timeout — closing stale socket"); + try { socket.close(1011, "heartbeat timeout"); } catch (_) {} + }, HEARTBEAT_TIMEOUT_MS); + } catch (e) { + console.warn("[Burble AI Bridge] Ping send failed:", e.message); + } + }; + socket.onopen = () => { - wsClient = socket; console.log("[Burble AI Bridge] Page connected via WebSocket"); + heartbeatTimer = setInterval(sendPing, HEARTBEAT_INTERVAL_MS); }; socket.onmessage = (ev) => { try { const msg = JSON.parse(ev.data); + if (msg.type === "pong") { + // Heartbeat reply — cancel the timeout. + if (pongTimer !== null) { clearTimeout(pongTimer); pongTimer = null; } + return; + } if (msg.type === "received") { // Message from remote peer, queue for Claude to poll. // SECURITY FIX: Enforce bounded queue size (proven SafeQueue principle). // Discard oldest messages when at capacity to prevent memory exhaustion // if the consumer stops polling /recv. if (messageQueue.length >= MAX_MESSAGE_QUEUE_SIZE) { - const discarded = messageQueue.shift(); + messageQueue.shift(); console.warn( `[Burble AI Bridge] Queue full (${MAX_MESSAGE_QUEUE_SIZE}), discarded oldest message` ); @@ -151,10 +192,14 @@ Deno.serve({ port: PORT + 1, hostname: "127.0.0.1" }, (req) => { }; socket.onclose = () => { - wsClient = null; + stopHeartbeat(); + if (wsClient === socket) wsClient = null; console.log("[Burble AI Bridge] Page disconnected"); }; + // Start the heartbeat even if onopen never fires (see comment above). + heartbeatTimer = setInterval(sendPing, HEARTBEAT_INTERVAL_MS); + return response; }); diff --git a/client/web/p2p-voice.html b/client/web/p2p-voice.html index a1b21b9..8e3a200 100644 --- a/client/web/p2p-voice.html +++ b/client/web/p2p-voice.html @@ -114,7 +114,12 @@

Connected