Skip to content

Epic: replace the miniaudio node graph with a Go mixer on malgo #1

Description

@KevinGruber2001

Tracking issue for the engine refactor. Individual issues are linked below and are meant to be done in order — each one is provable on its own before the next starts.

The goal in one sentence

Move the C boundary from the middle of the signal path to its edge, so audio becomes a []float32 we own.

flowchart LR
  subgraph NOW
    A1[TOML] --> B1["Go<br/>config only"] --> C1["ma_engine<br/>clips + mix + FX"] --> D1[device]
  end
  subgraph AFTER
    A2[TOML] --> B2["Go<br/>Session"] --> C2["mixBlock()<br/>clips + mix + FX"] --> R2[ring buffer] --> D2["malgo<br/>device"]
  end
  style C1 fill:#ffe0dc,stroke:#c33
  style C2 fill:#dff0e6,stroke:#2a7
Loading

Today ma_engine owns the mix and Go only configures it from outside. That is why the samples are unreachable, why the compressor has no DSP, why master.limiter = true only logs a warning, and why a plugin would need a cgo callback to run at all.

What gets deleted

Path Lines
internal/audio/miniaudio.h 95,864
internal/audio/bridge.c 14
internal/audio/{audio,sound,effect,offline}.go 672
total 96,550

Replaced by roughly 1,000 lines of Go across sample/, mix/, dsp/ and a much smaller audio/.

What does NOT change

internal/project, internal/state, internal/watcher, internal/serve, cmd/codaw, the whole vscode-extension/, and every TOML file anyone has on disk. This is an engine-internals refactor, not a product change.

Order of work

flowchart LR
  I1["#1 sample/<br/>decode WAV"] --> I2["#2 mix/<br/>mixBlock()"]
  I2 --> I3["#3 dsp/ + plugin/<br/>effects as Go funcs"]
  I3 --> I4["#4 audio/<br/>malgo, delete the C"]
  I4 -.-> I5["#5 cleanup<br/>(optional)"]
Loading

The order matters for one specific reason: #3 comes before #4 so that nothing regresses at the moment the C is deleted. If the device swap happened first, there'd be a window with no EQ and no reverb.

MVP = #1 through #4. At that point CodaW does everything it does today, with no C in the repository. #5 is cleanup that can wait indefinitely.

Two things that turned out easier than expected

  1. The store is already copy-on-write. Store.Get() returns a pointer to a project that is never mutated again — see the comment at the top of internal/project/clone.go. The "immutable snapshot" architecture is essentially already built. Nothing in this refactor needs to change it.

  2. The mixer will not run on the audio thread. With a ring buffer between the mixer and malgo's callback, the callback does nothing but copy bytes out of the ring. The mixer runs on an ordinary goroutine, where a GC pause costs latency we've already buffered for rather than a dropout. This removes most of the scary parts of writing audio code in Go.

Guiding principle

Prefer the version you can explain to someone else over the version that is 5% faster. Every file in this refactor should be readable top-to-bottom without a diagram. Where this issue set proposes something clever, it also says what the boring alternative was and why it lost.

Background reading

  • docs/architecture.md — the three mixer shapes considered, and why the flat one won
  • docs/plugins.md — plugins as Go functions
  • docs/ipc.md — the two-plane rule (files carry what you'd commit; the channel carries what you'd never commit)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions