S10) Under the Hood
Compiler, engine, and dispatcher
The complete pipeline — from .bps file to sound
A
.bpsfile goes in. Sound comes out. Between the two: a JavaScript compiler, a C engine compiled into WebAssembly, and — downstream — a dispatcher that resolves notes by actor and routes them to transports.Scope: the repository covers the compile-time chain (source → BP3 grammar) and the derivation by the WASM engine, whose output is a sequence of timed tokens. The runtime (dispatcher, resolver, transports, code sessions) is the downstream consumer of the timed tokens — its implementation lives outside the repository. It is described here as a specification of the complete system.
Where does this article fit in?
After actors (S3) and the 5 pitch layers (S9), we’re opening the hood. This article is technical — it describes how everything fits together.
The pipeline in a diagram
flowchart TD
subgraph Depot["Repository: language + transpiler + engine"]
SRC["Source .bps"]
TOK["Tokenizer"]
PAR["Parser"]
ENC["Encoder"]
BP3["BP3 WASM"]
TOKENS["Timed tokens"]
end
subgraph Data["lib/"]
ALP[("alphabets")]
OCT[("octaves")]
TUN[("tunings")]
TMP[("temperaments")]
CTR[("controls")]
ROU[("routing")]
end
subgraph Aval["Runtime (downstream — outside repository)"]
DISP["Dispatcher"]
RES["Resolver by actor"]
WAUDIO["Web Audio"]
MIDIT["MIDI"]
JSEVAL["JS inline"]
end
subgraph Prevu["Planned (downstream)"]
OSCT["OSC"]
SCL["sclang"]
PYT["Python"]
end
SRC --> TOK
ALP -.-> TOK
OCT -.-> TOK
TOK --> PAR
PAR --> ENC
ALP -.-> ENC
OCT -.-> ENC
CTR -.-> ENC
ENC --> BP3
BP3 --> TOKENS
TOKENS --> DISP
TUN -.-> RES
TMP -.-> RES
ROU -.-> DISP
DISP --> RES
RES --> WAUDIO
RES --> MIDIT
DISP --> JSEVAL
JSEVAL --> DISP
DISP -.-> OSCT
DISP -.-> SCL
DISP -.-> PYT
style SRC fill:#4a7ab5,stroke:#3a6aa5,color:#fff
style TOK fill:#4a9a8a,stroke:#3a8a7a,color:#fff
style PAR fill:#4a9a8a,stroke:#3a8a7a,color:#fff
style ENC fill:#4a9a8a,stroke:#3a8a7a,color:#fff
style BP3 fill:#c8842a,stroke:#b8742a,color:#fff
style DISP fill:#8a5ab5,stroke:#7a4aa5,color:#fff
style RES fill:#7a4a9a,stroke:#6a3a8a,color:#fff
style WAUDIO fill:#2a6a4a,stroke:#1a5a3a,color:#fff
style MIDIT fill:#2a6a4a,stroke:#1a5a3a,color:#fff
style JSEVAL fill:#4a5ab5,stroke:#3a4aa5,color:#fff
style TOKENS fill:#444,stroke:#666,color:#ddd
style ALP fill:#333,stroke:#666,color:#ccc
style OCT fill:#333,stroke:#666,color:#ccc
style TUN fill:#333,stroke:#666,color:#ccc
style TMP fill:#333,stroke:#666,color:#ccc
style CTR fill:#333,stroke:#666,color:#ccc
style ROU fill:#333,stroke:#666,color:#ccc
style OSCT fill:#555,stroke:#777,color:#999
style SCL fill:#555,stroke:#777,color:#999
style PYT fill:#555,stroke:#777,color:#999
Figure 1 — BPscript Pipeline. The “Repository” subgraph covers compilation (tokenizer → parser → encoder) and BP3/WASM derivation up to the timed tokens. The “Runtime (downstream)” subgraph is the consumer of the timed tokens, outside the repository. Solid line = implemented (Web Audio, MIDI, JS inline). Dashed line = planned (OSC, sclang, Python). JSON files (
lib/) feed the compiler and the resolver.
The BPscript compiler (3 steps)
The compiler is written in JavaScript. It transforms the .bps source into BP3 grammar in three steps.
1. Tokenizer
Breaks the source into tokens. Reads alphabets.json and octaves.json to recognize note names and register suffixes/prefixes.
@actor sitar alphabet:sargam ... → [@, actor, sitar, alphabet, :, sargam, ...]
Sa_^(vel:120) → [Sa_^, (, vel, :, 120, )]
[phase==1] S -> A → [[, phase, ==, 1, ], S, ->, A]
Backticks are tokenized as opaque blocks — the content is not analyzed.
2. Parser
Builds the AST (Abstract Syntax Tree — see S13). Checks syntax, registers @actor and declarations. No separate type checker (planned, not implemented). Textual expansion of macros (accent(x) = x(vel:120)) occurs in this phase.
3. Encoder
Translates the AST into BP3 grammar:
- Note names → BP3-safe names with
bolprefix (Sa→bolSa) []engine → BP3 instructions ([speed:2]→{2, ...},[weight:50]→<50>)()runtime →_script(CTn)with control table- Guards
[X==N]→/X=N/ - Captures
?n→ BP3 metavariables - Templates
$/&→(=X)/(:X) - Ties
~→&
The encoder also produces the terminalActorMap — a dictionary {BP3 terminal → actor name} passed to the dispatcher — as well as the controlTable, the transcriptionTable (homomorphism labels) and, in multi-scene mode, the mapTable / sceneTable / exposeTable.
The flat alphabet — a major change
The historical BP3 OCT format has been abandoned. BP3 no longer knows what a note is — it sees opaque names (bolC4, bolSa). Controls (velocity, tempo, etc.) are no longer encoded in the terminals but pass through _script(CTn) with a separate control table. BP3 handles the structure (when, for how long, in what order), while the downstream resolver handles the content (which frequency, which channel, which sound).
The prototype generator
The encoder also emits the -so. file (sound-object prototypes): NoteOn/NoteOff for each terminal, using alphabets.json and octaves.json.
Total output: BP3 grammar + flat alphabet + prototypes + settings + controlTable + terminalActorMap (+ transcriptionTable and scene tables).
The BP3 WASM engine
The BP3 C code, compiled into WebAssembly via Emscripten. It is the same code that Bernard Bel has maintained since 1981, ported to run in a browser — with extensions. It receives the grammar and produces timed tokens:
[
{ terminal: "bolSa", start: 0, duration: 1000 },
{ terminal: "_script(CT0)", start: 0, duration: 0 },
{ terminal: "bolRe", start: 1000, duration: 1000 },
...
]
The engine handles: derivation, polymetry (PolyMake — B13), indeterminate rests, flags, captures, templates, homomorphisms, weights, _script(CTn) controls.
WASM extensions: bp3_set_object_duration (custom duration for terminals), _script(CTn) (table-based controls).
The dispatcher (downstream runtime)
The richest component of the runtime — downstream from the repository. For each timed token at time T:
1. Remove the prefix
bolSa → Sa (removes the opaque prefix added by the encoder).
2. Identify the actor
Via the terminalActorMap emitted at compilation: terminalActorMap["bolSa"] → "sitar".
3. Resolve the pitch
If the actor has a tuning, its resolver translates the token into frequency via the 5 layers (S9):
"Sa_^" → parse → note="sa", register=2
→ alphabet: sa = degree 0
→ tuning: degree 0 → step 0
→ temperament: step 0 → ratio 1/1
→ octave: +1 → ratio × period_ratio
→ freq = baseHz × ratio = 480 Hz
4. Resolve controls
If the token is a _script(CTn), look up in the controlTable → {vel: 120, pan: 0.5}.
5. The CV engine
CV objects (Control Voltage — ADSR envelopes, LFOs, ramps) are a layer in their own right. Declared at the head of the scene and placed in the grammar as ordinary symbols, they are resolved by the dispatcher into audio buses (Web Audio), CC message sequences (MIDI), or interpolated parameters (OSC) depending on the destination transport.
6. Route
Two streams:
- Terminals → actor transport (
actor.transport.send({frequency, duration, controls})) - Backticks → code evaluation (
actor.evalor tag for orphans)
7. Advanced management
- Simultaneity (
!): same timestamp, multiple outputs - Loop mode: automatic re-derivation (the grammar produces variations each cycle)
- Hot swapping: hot recompilation, quantized by default (S11 (coming soon))
Transports
Implemented: Web Audio and MIDI.
Planned: OSC, DMX.
| Transport | Protocol | Usage | Status |
|---|---|---|---|
| Web Audio | Browser API | In-browser sound | Implemented |
| MIDI | Web MIDI API | DAWs, hardware, synths | Implemented |
| OSC | UDP | scsynth, Processing, TouchDesigner | Planned |
| DMX | OSC/serial | Lights, motors | Planned |
Transports are universal — no state, no session. The dispatcher converts the event into a native call and sends it, without waiting for a response.
Code evaluation
Today: inline JS (
new Function()) in the browser.Planned: external code sessions (sclang, Python, GHCi+Tidal) via stdin/TCP/WebSocket.
Code evaluation is not a simple output adapter — it is a bidirectional component:
| Direction | What happens |
|---|---|
| Dispatcher → evaluator | Code sent at time T (playback) |
| Evaluator → dispatcher | Returned value (parameter resolution) |
Three timings (S4):
- Init: orphan backticks → before derivation
- Playback: backticks in the stream → at time T
- Resolution: parameter-backticks → evaluated, value injected
| Evaluator | Language | Status |
|---|---|---|
| JS inline | JavaScript (new Function()) |
Implemented |
| sclang | SuperCollider | Planned |
| Python | Python (Pyodide or exec) | Planned |
| GHCi + Tidal | Haskell | Planned |
The three interfaces
| Interface | Between | Format | Status |
|---|---|---|---|
| Interface 1 | Compiler → BP3 | BP3 grammar + alphabet + prototypes + settings + controlTable | Exists |
| Interface 2 | BP3 → Dispatcher | JS array of timed tokens | Exists |
| Interface 3 | Dispatcher → Transports + evaluators | Timestamped messages + code | Exists (Web Audio + MIDI + JS) |
Interface 2 marks the boundary of the repository: everything that follows (interface 3 and beyond) is the downstream runtime.
Key takeaways
- 3 compilation steps: tokenizer → parser → encoder
- Flat alphabet: BP3 sees opaque names (
bolSa), the resolver handles the mapping - terminalActorMap: each terminal is associated with an actor by the compiler
- One resolver per actor: same note, different frequencies depending on the tuning
- Two streams: terminals → transports, backticks → evaluators
- Today: inline JS + Web Audio + MIDI
- Scope: the repository ends at timed tokens; the dispatcher and transports are downstream
Glossary
- Tokenizer: Step 1 — breaks the source into tokens, reads alphabets and octaves
- Parser: Step 2 — builds the AST (and expands macros)
- Encoder: Step 3 — translates the AST into BP3 grammar + emits the terminalActorMap and prototypes
- terminalActorMap: Dictionary
{BP3 terminal → actor}— allows the dispatcher to identify the actor - Flat alphabet: Convention where terminals are opaque names prefixed with
bol— BP3 does not know what they mean - Resolver: Downstream component (1 per actor) that translates token → frequency via the 5 pitch layers
- CV engine: Downstream component that resolves CV objects (ADSR, LFO, ramp) into audio buses or CC messages
- controlTable: Table
{CTn → parameters}— runtime()compiled into_script(CTn) - Transport: Output protocol for timestamped data (Web Audio, MIDI, OSC)
Links in the series
- S3 — Types, actors, and attachments — how actors are declared
- S4 — Backticks — init, playback, resolution
- S9 — The pitch system — what the resolver does
- S11 — Live — hot swapping and loop mode
- S12 — The EBNF — the grammar the parser implements
- S13 — The AST — what the parser produces
- B13 — PolyMake — the polymetric expansion algorithm
Prerequisites: S3, S9
Reading time: 14 min
Tags: #BPscript #architecture #compiler #WebAssembly #dispatcher #actors
Next article: S11 — Live: modifying while it plays