B7) From BP3 to SuperCollider
Anatomy of the Transpiler
You now know what an AST is, how BP3 grammars derive musical sequences, and how SuperCollider orchestrates patterns. But how do you actually get from one to the other?
Where does this article fit in?
This article is the convergence point of the previous series. It brings together the ASTs (Abstract Syntax Tree, L4) from the L series, SuperCollider patterns (I3) from the I series, and all the BP3 formalism detailed in B1 to B6. This is where theory meets implementation: we dissect the bp2sc transpiler, which transforms BP3 code into playable SuperCollider code.
Sidebar: Transpiler vs. Compiler
A compiler translates a source language into a lower-level language (C to assembly, for example). A transpiler (or source-to-source compiler) translates between two languages of the same level of abstraction. Here, BP3 and SuperCollider are both high-level languages dedicated to music. We are therefore translating from a generative formalism to a pattern formalism — this is indeed a transpilation.
Why is this important?
Understanding the transpiler means understanding the bridge between two worlds: that of formal grammar (BP3) and that of sound synthesis (SuperCollider). Without this bridge, a BP3 grammar remains inert text — rules on paper. With it, each rule transforms into a stream of musical events.
The most telling analogy: imagine a simultaneous translator at a conference. The speaker speaks in BP3 (grammar, weights, modes) and the translator must render exactly the same speech in SuperCollider (patterns, events, routines). The fidelity of the translation is crucial: an error and the music no longer matches the composer’s intention.
But this translator doesn’t work word for word. They first break down the speech into units of meaning (syntactic analysis), build an intermediate representation (the AST), and then reformulate it in the target language. This is exactly what our transpiler does in five steps.
The idea in one sentence
The bp2sc transpiler transforms BP3 code into SuperCollider code in five distinct phases — lexer (lexical analyzer), line classifier, parser (syntactic analyzer), validation, emitter (code generator) — each with a unique responsibility.
Let’s explain step by step
1. The complete pipeline
The transpiler follows a five-phase pipeline:
BP3 Code → [Lexer/Classifier] → [Parser] → AST → [Validation] → [Emitter] → SC Code
Why five phases and not just one? For the same reason you don’t build a house in a single step: each phase has a precise responsibility, and this separation allows each step to be tested and debugged independently.
Sidebar: Separation of Concerns
In software engineering, the principle of separation of concerns states that each module should do one thing and do it well. A five-phase pipeline means five independently testable modules:
- The lexer/classifier reads the raw text and identifies the type of each line (comment, header, mode, preamble, rule)
- The parser analyzes each line according to its type and builds the corresponding AST nodes
- The AST serves as an intermediate representation — a structured tree that is linked neither to BP3 nor to SC
- Validation checks the consistency of the AST (normalized weights, defined symbols, etc.)
- The emitter traverses the AST and generates the target SuperCollider code
If the parser has a bug, we fix it without touching the emitter. If we want to add a new SC pattern, only the emitter changes. This is the power of modularity.
Sidebar: The Pipeline and Chomsky’s Hierarchy
This separation into phases is not arbitrary — it reflects the levels of Chomsky’s hierarchy (L1):
Phase Chomsky Level Computation Model Complexity Lexer/classifier Type 3 (regular) Finite automaton / Regex O(n) Parser Type 2 (context-free) Pushdown automaton / Earley O(n³) Validation Type 1 (context-sensitive) Visitors on the AST Variable Emitter Transformation Tree traversal O(n) >
By first processing the simple aspects (regex for tokens), we avoid paying the cost of higher levels on the entire text. Each level enriches the representation: characters → tokens → tree → verified tree → target code.
Let’s detail each phase.
Phase 1-2: Lexer and Line Classifier
BP3 is a line-oriented format: each line has a specific role (unlike a language like Python where an instruction can span multiple lines). The classifier uses regular expressions (regex — compact notation for describing text patterns, cf. L9) to identify the type of each line:
| Regex Pattern | Line Type | Example |
|---|---|---|
^\s*//(.*)$ |
Comment | // Indian raga composition |
^-(\w+)\.(.+)$ |
File Reference | -se.Mohanam |
^\s*INIT:\s*(.+)$ |
INIT Directive | INIT: MIDI program 110 |
^\s*(ORD|RND|LIN|SUB1|SUB)(\[...\])? |
Mode Line | ORD[1] |
^\s*(_\w+\(...\))+ |
Preamble | _mm(120) _striated |
^\s*gram#(\d+)\[(\d+)\] |
Rule | gram#1[1] S --> A B |
This is a two-phase parser:
- Pass 1: classification by type (which regex matches)
- Pass 2: detailed analysis of the content (weights, flags, LHS, arrow, RHS)
For rule lines, Pass 2 uses a recursive descent parser (a technique where each grammar rule is implemented by a function that recursively calls itself for sub-rules) which extracts, in order: the optional weight <N> or <N-D>, the conditional flags /flag/ (cf. B4), the Left-Hand Side (LHS) symbol, the --> arrow, and the Right-Hand Side (RHS) sequence of symbols.
Phase 3: The AST (Abstract Syntax Tree)
The parser produces an AST (L4) whose root is a BPFile node. Here is its structure:
@dataclass
class BPFile:
headers: list[Header] # Comment, FileRef, InitDirective
grammars: list[GrammarBlock] # one or more grammar blocks
@dataclass
class GrammarBlock:
mode: str # "ORD", "RND", "LIN", "SUB", "SUB1"
index: int | None # sub-grammar number [N]
label: str | None # optional label
preamble: list[SpecialFn] # functions before the rules
rules: list[Rule] # production rules
@dataclass
class Rule:
grammar_num: int # gram#N
rule_num: int # [M]
weight: Weight | None # <3> or <50-12>
flags: list[Flag] # /Ideas/, /NumR>5/
lhs: list[RHSElement] # left side (S, A, etc.)
rhs: list[RHSElement] # right side (notes, symbols, etc.)
This structure is independent of the textual syntax of BP3 and the SC output format. But beware: its nodes (BPFile, GrammarBlock, Rule, SpecialFn) remain concepts specific to BP3. It is an intermediate representation (IR) specific to BP3, not a generic musical IR. A generic IR would have nodes like Section, Voice, Note, Tempo — independent of any notation. The transpiler could emit LilyPond, MIDI (Musical Instrument Digital Interface — standard communication protocol between digital instruments, cf. M1) or any other format from this AST, but one would still need to know the BP3 semantics to interpret it.
Phase 4-5: Validation and Emission
Validation checks that the AST is well-formed: is every symbol referenced in an RHS defined as an LHS somewhere? Are the weights consistent?
The emitter (SCEmitter) then traverses the AST node by node and generates the SuperCollider code. This is the core of the transpiler, and we will detail it in the following sections.
2. Structure of a BP3 file
Before translating, we need to understand what we are translating. A BP3 file has a precise structure:
-se.Tabla ← FileRef (settings)
-al.Tabla ← FileRef (alphabet of bols)
// Tabla composition - tintal ← Comment
ORD[1] ← Start of grammar block (mode, index)
_mm(160) ← Preamble (tempo in BPM)
gram#1[1] S --> A B ← Production rule
gram#1[2] A --> dha dhin dhin dha ← Resonant bols (1st vibhāg)
gram#1[3] B --> dha tin tin ta ← Dry bols (3rd vibhāg, khali)
The name “Bol Processor” comes directly from these bols — the mnemonic syllables of the Indian tabla. The alphabet (-al.Tabla) defines the bols as grammar terminals, exactly as Kippen and Bel designed it in the 1980s to model the Lucknow tabla [KippenBel1989].
The headers declare external resources:
| AST Node | BP3 Syntax | Role |
|---|---|---|
FileRef(prefix="se", name="Mohanam") |
-se.Mohanam |
Settings file |
FileRef(prefix="al", name="Mohanam") |
-al.Mohanam |
Alphabet file |
FileRef(prefix="ho", name="Mohanam") |
-ho.Mohanam |
Homomorphisms file |
FileRef(prefix="cs", name="Mohanam") |
-cs.Mohanam |
Sound-objects file (csound) |
Comment(text="...") |
// text |
Comment |
InitDirective(text="...") |
INIT: text |
Initialization directive |
Each GrammarBlock declares a derivation mode (ORD, RND, LIN, SUB, SUB1 — cf. B3) and contains an optional preamble (special functions applied to the entire block) followed by production rules.
3. From BP3 to SC: Translating Modes
This is the core of the correspondence. Each BP3 derivation mode (cf. B3) translates into a specific SC pattern (SuperCollider pattern — an object that generates a sequence of musical events). BP3 non-terminals become Pdef (Pattern Definition — a named and reusable container in SuperCollider):
| BP3 Mode | Conditions | SC Pattern | Behavior |
|---|---|---|---|
| ORD | — | Pseq([...], 1) |
Sequential, deterministic |
| RND | Without weights | Prand([...], 1) |
Uniform random |
| RND | With weights <N> |
Pwrand([...], [w1,w2,...].normalizeSum, 1) |
Weighted random |
| RND | With decrements <N-D> |
Prout({ |ev| ... }) |
Routine with mutable weights |
| LIN | — | Prand([...], 1) |
Identical to RND on SC side |
| SUB | — | Like ORD or RND | Global identity managed at derivation |
| SUB1 | — | Pseq([...], 1) |
Sequential, like ORD |
| (flags) | /flag/ present |
Prout({ |ev| ... if/else ... }) |
Dynamic conditions |
Sidebar: Why LIN ≈ RND in SuperCollider?
The LIN mode (linear cyclic) means: “when a non-terminal has multiple rules, apply them by cycling in order — rule 1 the first time, rule 2 the second, etc.”. This is stateful behavior (with memory): it requires knowing how many times the non-terminal has already been expanded.
The transpiler does not perform the derivation — it translates the structure of the grammar. It cannot track the cycle counter. The faithful translation would be a
Proutwith a counter:Pdef(\X, Prout({ |ev| var rules = [...]; var idx = 0; inf.do { rules[idx % rules.size].embedInStream(ev); idx = idx + 1; } }));
For simplicity, the transpiler uses
Prandas an approximation: it captures the “multiple possible alternatives” aspect of LIN, but not the cyclic order. This is a known limitation of the static transpiler — true LIN mode requires a stateful derivation engine.
Example of ORD translation — tintāl theka:
BP3:
ORD[1]
gram#1[1] S --> Vibhag1 Vibhag2
gram#1[2] Vibhag1 --> dha dhin dhin dha
gram#1[3] Vibhag2 --> dha tin tin ta
SC generated:
Pdef(\S,
Pseq([Pdef(\Vibhag1), Pdef(\Vibhag2)], 1)
);
Pdef(\Vibhag1,
Pbind(\midinote, Pseq([dha, dhin, dhin, dha], 1), \dur, 0.25)
);
Pdef(\Vibhag2,
Pbind(\midinote, Pseq([dha, tin, tin, ta], 1), \dur, 0.25)
);
Each vibhāg (section of the tāla) becomes a named Pdef. The bols — dha, dhin, tin, ta — are the terminals of the tabla alphabet, mapped to MIDI numbers or percussion SynthDefs by the -al.Tabla file.
Example of RND translation with weights — tabla improvisation:
BP3:
RND[2]
gram#2[1] <3> Motif --> dha tirakita dhin dhin
gram#2[2] <1> Motif --> tin ta dha ge na
SC generated:
Pdef(\Motif,
Pwrand([
Pbind(\midinote, Pseq([dha, tirakita, dhin, dhin], 1), \dur, 0.25),
Pbind(\midinote, Pseq([tin, ta, dha, ge, na], 1), \dur, 0.25)
], [3, 1].normalizeSum, 1)
);
The resonant motifs (dha tirakita dhin dhin, thali section of the tāla) are three times more probable than the dry motifs (tin ta dha ge na, khali section) — a faithful reflection of tabla practice, where resonant sections dominate. SuperCollider’s .normalizeSum automatically normalizes weights into probabilities: [3, 1].normalizeSum gives [0.75, 0.25]. This is equivalent to the normalization seen in B1.
Example of decremental weights (Prout):
BP3:
RND[1]
gram#1[1] <50-12> S --> A S
gram#1[2] <1> S --> fin
SC generated:
Pdef(\S, Prout({ |ev|
var w0 = 50; // decrement: 12
var w1 = 1;
inf.do {
var total = w0 + w1;
var r = total.rand;
if(r < w0) {
Pseq([Pdef(\A), Pdef(\S)], 1).embedInStream(ev);
w0 = (w0 - 12).max(0);
} {
Pdef(\fin).embedInStream(ev);
}
}
}));
The variables w0 and w1 are mutable (modifiable during execution): after each use of the first rule, w0 decreases by 12. The .embedInStream(ev) method inserts events from a pattern into the Prout routine’s stream. When it reaches 0, only the second rule (termination) remains possible. This is the dynamic weight mechanism described in B1, translated into a SuperCollider routine.
4. The 42+ Special Functions (SpecialFn — AST node for built-in functions)
BP3 has a rich set of special functions that modify musical behavior. In the AST, they are represented by the SpecialFn(name, args) node. The transpiler translates them into SuperCollider key-value pairs.
The functions fall into three categories:
- Preamble functions: placed before the rules, they apply to the entire block (
_mm,_striated) - Modification functions: in the RHS, they modify subsequent notes (
_transpose,_vel,_staccato) - Informational functions: comments or metadata with no direct sonic effect (
_part,_velcont)
Pitch
| BP3 | SC | Description |
|---|---|---|
_transpose(N) |
\ctranspose, N |
Chromatic transposition by N semitones |
_scale(name, root) |
\scale, Scale.xxx, \root, N |
Scale selection |
_pitchbend(N) |
\detune, N |
Micro-detuning in cents |
Example: _transpose(5) before do4 ré4 produces:
Pbindf(Pbind(\midinote, Pseq([60, 62], 1), \dur, 0.25), \ctranspose, 5)
Pbindf is the SC pattern that “adds” keys to an existing pattern. Here, it adds \ctranspose, 5 to all notes of the internal Pbind. do4 (MIDI 60) will therefore play as MIDI 65, and ré4 (MIDI 62) as MIDI 67.
Velocity (Dynamics)
| BP3 | SC | Description |
|---|---|---|
_vel(N) |
\amp, N/127 |
Fixed velocity (MIDI 0-127 to amplitude 0-1) |
_volume(N) |
\amp, N/127 |
Synonym for _vel |
_rndvel(N) |
\amp, Pwhite(lo, hi) |
Random velocity within a +-N interval |
Example: _vel(100) followed by _rndvel(20) produces:
\amp, Pwhite(0.63, 0.945) // (100-20)/127 to (100+20)/127
Pwhite generates uniformly distributed values between lo and hi — an elegant way to translate the natural variability of performance.
Articulation
| BP3 | SC | Description |
|---|---|---|
_staccato(N) |
\legato, N/100 |
Ratio of played duration / theoretical duration |
_legato(N) |
\legato, N/100 |
Same (both use the same calculation) |
In SuperCollider, \legato controls the fraction of the duration during which the note sounds: 0.5 = staccato (short, detached notes), 1.0 = full, 1.5 = legato (tied notes, overlapping the next).
Timing and Tempo
| BP3 | SC | Description |
|---|---|---|
_mm(BPM) |
TempoClock.default.tempo = BPM/60; |
Global tempo in BPM (Beats Per Minute), in preamble |
_tempo(N) |
\stretch, 1/N |
Relative speed factor |
_rndtime(N) |
\dur, Pwhite(lo, hi) |
Random timing variation (+-N%) |
Sidebar: _mm vs _tempo
_mm(120)defines the absolute tempo (120 beats per minute). This is a global setting, emitted once in the SC file’s preamble.
_tempo(2)is a relative factor: “play 2 times faster”. In SC, this translates to\stretch, 0.5(each note lasts half its normal duration). This is a local modifier, affecting only the notes that follow it.
Instruments and MIDI Programs
| BP3 | SC | Description |
|---|---|---|
_ins(name) |
\instrument, \name |
Instrument selection (SynthDef — Synth Definition, synthesizer definition in SC) |
_script(MIDI program N) |
\program, N |
MIDI program |
_chan(N) |
\chan, N |
MIDI channel |
Pedals (MusicXML import)
| BP3 | SC | Description |
|---|---|---|
_sustainstart() |
\sustain, 1 |
Sustain pedal pressed |
_sustainstop() |
\sustain, 0 |
Pedal released |
_sostenutostart() |
\sostenuto, 1 |
Sostenuto pedal (holds only notes already pressed) |
_softstart() |
\softPedal, 1 |
Una corda (soft pedal — attenuates sound) |
Structure and Repetition
| BP3 | SC | Description |
|---|---|---|
_repeat(N) |
Pn(next_element, N) |
Repeat the next element N times |
_retro() |
List inversion | Retrograde (mirror) |
_rotate(N) |
List rotation | Rotate by N positions |
_retro and _rotate operate within polymetric expressions (between curly braces {}). The transpiler detects them, applies the transformation to the list of elements, then generates the resulting pattern.
5. Polymetry, Ties, and Advanced Constructs
Derivation modes and special functions do not cover all BP3 constructs. The transpiler must also handle polymetry (cf. B5), ties, and homomorphisms (cf. B6).
Monophonic Polymetry: Compression and Dilation
A monophonic polymetric expression {M, elements} compresses or dilates a sequence so that it fits into M time units. The transpiler uses Pbindf with the \stretch key:
BP3:
{3, dha dhin dhin dha}
SC generated:
Pbindf(
Pbind(\midinote, Pseq([36, 42, 42, 36], 1), \dur, 0.25),
\stretch, 3/4
)
The formula is \stretch = M / N where M is the target duration and N is the number of elements. Here, 3/4 = 0.75: each bol lasts 75% of its normal duration, compressing 4 bols into the space of 3 beats.
Sidebar:
\stretchvs\durIn SuperCollider,
\dursets the absolute duration of an event (in beats).\stretchis a multiplier applied to\dur: the effective duration isdur × stretch. The transpiler uses\stretchrather than recalculating\durto preserve internal duration ratios — if one note is twice as long as another, it remains so after compression.
Polyphonic Polymetry: Parallel Superposition
A polyphonic expression {voice1, voice2} superimposes two sequences in parallel. The transpiler uses Ppar (Parallel Pattern — a pattern that plays multiple streams simultaneously):
BP3:
{dha dhin dhin dha, Sa Re Ga Ma Pa}
SC generated:
Ppar([
Pbindf(
Pbind(\midinote, Pseq([36, 42, 42, 36], 1), \dur, 0.25),
\stretch, 1/1
),
Pbindf(
Pbind(\midinote, Pseq([60, 62, 64, 65, 67], 1), \dur, 0.25),
\stretch, 4/5
)
])
The first voice (tabla, 4 bols) defines the reference duration. The second (sargam, 5 notes) is compressed with \stretch = 4/5 to fit into the same time — a 4 against 5 polymetry, typical of cross-textures between percussion and melody.
Ties
Ties extend a note by merging two consecutive notes of the same pitch. The transpiler translates them into two complementary mechanisms:
| Position | SC | Effect |
|---|---|---|
| Start of tie | \legato, 2.0 |
The note sustains beyond its nominal duration |
Continuation (~) |
Event.silent(dur) |
Silent event — the previous note resonates |
Event.silent(dur) is a SuperCollider event that emits no sound for the duration dur. By placing it where the tie continuation should be, we “fill” the time without interrupting the tied note.
The transpiler maintains an internal tracker (_pending_tie_midi): it memorizes the MIDI number of the starting note and checks that each continuation has the same pitch. If the pitches differ, a warning is issued — a tie between two different notes is likely a composer’s error.
Time Signatures
Signatures like 4/4 or 7/8 have no native equivalent in SuperCollider (which thinks in beats, not measures). The transpiler currently emits them as informative comments:
// Time signature: 7/8 (tala rupak)
These comments do not affect playback. However, signatures could influence the generated structure — for example, a 7/8 in tāla rupak (additive scheme 3+2+2) could translate into note groupings via Pn or automatic accentuation of strong beats. This is a current simplification of the transpiler, not a technical impossibility. A more complete transpiler could leverage signatures to reflect metric structure, especially for Indian tālas with complex additive schemes (cf. B5).
Variables and Symbols
BP3 variables (|x| — cf. B6) are translated as Pdefs, identically to non-terminals:
BP3:
S --> |x| - |x| - |x|
SC generated:
Pdef(\S,
Pseq([Pdef(\x), Rest(0.25), Pdef(\x), Rest(0.25), Pdef(\x)], 1)
);
The identity constraint of the tihāī (cf. B5) — the three |x| must be the same motif — is naturally ensured: all occurrences point to the same Pdef(\x).
Quoted symbols ("name") are also translated to Pdef(\name), with a warning if the symbol is not defined anywhere in the grammar.
Homomorphisms (MASTER / SLAVE)
Homomorphisms (cf. B6) define systematic substitution tables. The transpiler handles the three types of the HomoApply node:
| Type | SC Translation | Role |
|---|---|---|
| MASTER | Sequence with applied SLAVE substitutions | The transformed music |
| SLAVE | (integrated into MASTER) | The correspondence table |
| REF | None (skip) | Label without sonic content |
The transpiler resolves the homomorphism at generation time: it takes the MASTER sequence, applies the SLAVE substitutions, and directly emits the resulting pattern. The homomorphism is “compiled” — its correspondence table disappears from the generated SC code.
Example — graha bheda: if the MASTER contains Sa Re Ga Pa and the SLAVE substitutes each note one tone higher (Sa→Re, Re→Ga, Ga→Ma, Pa→Dha), the emitted SC code will directly contain the transposed MIDI numbers (62, 64, 65, 69), with no trace of the original homomorphism table.
6. Non-Emitted Elements
Some AST nodes have no direct translation into SuperCollider (these advanced constructs are detailed in B6). The transpiler flags them with warnings and emits them as comments:
| AST Node | Reason for Omission | SC Emission |
|---|---|---|
ContextMarker |
Requires a context-sensitive derivation engine | None (skip) |
GotoDirective |
Requires a derivation engine with jumps | None (skip) |
Wildcard |
Requires a pattern-matching system | Rest() |
HomoApply(kind=REF) |
Homomorphism label, not a sound | None (skip) |
_rotate(N) in RHS |
Only applies in polymetric context | SC comment |
_failed(...) |
BP3 engine error handling | SC comment |
Lambda |
Internal node without musical content | Event.silent(0) |
Annotation |
Textual metadata (title, structural comment) | None (skip) |
Sidebar: Why not translate everything?
The transpiler translates the static structure of a BP3 grammar. But BP3 is also a derivation engine: it applies rules, evaluates flags, manages contexts. Elements like
ContextMarkerorGotoDirectiveonly make sense during derivation — they control the process, not the result.Translating these elements would require embedding a complete derivation engine in SuperCollider, which is beyond the scope of the current transpiler. The transpiler generates the “potential score”; the derivation engine is a separate feature.
7. SuperCollider Patterns Used
Here is a summary of the SC patterns generated by the transpiler, with their role:
| SC Pattern | Signature | Role |
|---|---|---|
Pdef(\name, pattern) |
Pdef(\name, pattern) |
Named definition — allows real-time reactivity |
Pbind(\key, val, ...) |
Pbind(\key, val, ...) |
Event stream with key-value pairs |
Pseq([...], N) |
Pseq([items], repeats) |
Play elements in sequence, N times |
Prand([...], N) |
Prand([items], repeats) |
Choose an element randomly, N times |
Pwrand([...], weights, N) |
Pwrand([items], [weights], repeats) |
Weighted choice |
Ppar([...]) |
Ppar([patterns]) |
Play multiple patterns in parallel |
Pbindf(pattern, \key, val) |
Pbindf(pat, \key, val) |
Add keys to an existing pattern |
Pn(pattern, N) |
Pn(pattern, repeats) |
Repeat a pattern N times |
Prout({...}) |
Prout({ |ev| ... }) |
Routine (for flags and mutable weights) |
Event.silent(dur) |
Event.silent(dur) |
Silent event (ties, placeholders) |
Sidebar: Why Pdef and not a variable?
Pdef(Pattern Definition) is a named and reactive container in SuperCollider. Unlike a simple variable (~myPattern = Pseq(...)):
- It can be modified in real-time during playback (live coding)
- It is globally accessible by its symbolic name (
\S,\A, etc.)- It automatically manages transitions between old and new versions
This is exactly what is needed to represent BP3 non-terminals: each symbol (
S,A,B) becomes aPdefthat can be referenced, modified, and combined.
The fundamental correspondence:
BP3 Non-terminal → Pdef(\name, ...)
BP3 Rule → SC Pattern (Pseq, Prand, Pwrand, Prout)
BP3 Note → MIDI Number in Pbind
BP3 Silence (-) → Rest() or Event.silent(0.25)
Special Function → \key, value pair in Pbindf
Polymetry {M, …} → Pbindf(..., \stretch, M/N)
Parallel Voices → Ppar([...])
Tie (~) → \legato, 2.0 + Event.silent
Variable |x| → Pdef(\x)
Homomorphism → Inline substitution (resolved at compilation)
8. Concrete Case: Complete BP3 Derivation to SC Code
Let’s take a complete example and follow it through the entire pipeline. True to the spirit of the Bol Processor, we use a tabla grammar — the instrument for which BP was created by Kippen and Bel [BelKippen1992a].
Source BP3 file — Kayda in tintāl:
-se.Tabla
-al.Tabla
// Kayda - tabla composition in tintal
ORD[1]
_mm(160)
gram#1[1] S --> Theka Impro
RND[2]
gram#2[1] <3> Impro --> dha tirakita dhin dhin
gram#2[2] <1> Impro --> tin ta dha ge na
gram#2[3] Theka --> dha dhin dhin dha
This file encodes a kayda (tabla composition with a fixed theme and improvisations): the theka (basic tintāl motif) is deterministic (ORD), while the improvisation motifs are probabilistic (RND), with resonant motifs being three times more frequent than dry motifs.
Step 1: Line Classification
The classifier identifies each line:
| Line | Type | AST Node |
|---|---|---|
-se.Tabla |
FileRef | FileRef(prefix="se", name="Tabla") |
-al.Tabla |
FileRef | FileRef(prefix="al", name="Tabla") |
// Kayda - composition... |
Comment | Comment(text="Kayda - tabla composition in tintal") |
ORD[1] |
Mode | GrammarBlock(mode="ORD", index=1) |
_mm(160) |
Preamble | SpecialFn(name="mm", args=["160"]) |
gram#1[1] S --> Theka Impro |
Rule | Rule(grammar_num=1, rule_num=1, ...) |
RND[2] |
Mode | GrammarBlock(mode="RND", index=2) |
gram#2[1] <3> Impro --> ... |
Rule + weight | Rule(..., weight=Weight(value=3), ...) |
| etc. |
Step 2: AST Construction
The resulting AST:
BPFile(
headers=[
FileRef(prefix="se", name="Tabla"),
FileRef(prefix="al", name="Tabla"),
Comment(text="Kayda - tabla composition in tintal"),
],
grammars=[
GrammarBlock(
mode="ORD", index=1,
preamble=[SpecialFn(name="mm", args=["160"])],
rules=[
Rule(gram=1, rule=1, lhs=[NonTerminal("S")],
rhs=[NonTerminal("Theka"), NonTerminal("Impro")])
]
),
GrammarBlock(
mode="RND", index=2,
preamble=[],
rules=[
Rule(gram=2, rule=1, weight=Weight(3),
lhs=[NonTerminal("Impro")],
rhs=[Terminal("dha"), Terminal("tirakita"),
Terminal("dhin"), Terminal("dhin")]),
Rule(gram=2, rule=2, weight=Weight(1),
lhs=[NonTerminal("Impro")],
rhs=[Terminal("tin"), Terminal("ta"),
Terminal("dha"), Terminal("ge"), Terminal("na")]),
Rule(gram=2, rule=3, weight=None,
lhs=[NonTerminal("Theka")],
rhs=[Terminal("dha"), Terminal("dhin"),
Terminal("dhin"), Terminal("dha")]),
]
),
]
)
Step 3: Analysis by the Emitter
The emitter (SCEmitter) analyzes the AST:
- Collection of rules by LHS symbol:
– S is defined in block 1 (ORD) with 1 rule
– Impro is defined in block 2 (RND) with 2 rules (weights 3 and 1)
– Theka is defined in block 2 (RND) with 1 rule (no weight)
- Multi-block detection:
Sis defined only in block 1,ImproandThekain block 2 — no name conflict.
- Tempo extraction:
_mm(160)in the preamble of block 1 sets the tempo to 160 BPM — a fast tempo typical of drut laya (fast tempo) in tabla.
- Terminal resolution: The bols (
dha,dhin,tin,ta,tirakita,ge,na) are resolved via the-al.Tablafile which maps each bol to a MIDI number or a percussive SynthDef identifier. For example:dha→ MIDI 36 (bass drum),tin→ MIDI 38 (snare drum), etc.
Step 4: SuperCollider Code Generation
Here is the complete generated SC code:
// BP3 Grammar: Tabla
// Generated by bp2sc from: Tabla Kayda
// Bol Processor BP3 → SuperCollider Pattern transpiler
//
// Usage:
// 1. Boot the server: s.boot;
// 2. Execute this file: Ctrl+Enter (select all)
// 3. To stop: Cmd+. or Ctrl+.
// 4. Morph live: re-evaluate individual Pdef blocks
(
// Default SynthDef for testing (replace with your own)
SynthDef(\bp2sc_default, {
|out=0, freq=440, amp=0.1, gate=1, pan=0|
var sig, env;
env = EnvGen.kr(Env.adsr(0.01, 0.1, 0.6, 0.3), gate, doneAction: 2);
sig = SinOsc.ar(freq) + Pulse.ar(freq * 1.001, 0.3, 0.3);
sig = LPF.ar(sig, freq * 4);
Out.ar(out, Pan2.ar(sig * env * amp, pan));
}).add;
TempoClock.default.tempo = 160.0 / 60;
// BP3 reference: -se.Tabla, -al.Tabla
// Kayda - tabla composition in tintal
// --- Subgrammar 1 (ORD) ---
Pdef(\S,
Pseq([Pdef(\Theka), Pdef(\Impro)], 1)
);
// --- Subgrammar 2 (RND) ---
Pdef(\Impro,
Pwrand([
Pbind(\midinote, Pseq([36, 42, 38, 38], 1), \dur, 0.25),
Pbind(\midinote, Pseq([38, 37, 36, 43, 40], 1), \dur, 0.25)
], [3, 1].normalizeSum, 1)
);
Pdef(\Theka,
Pbind(\midinote, Pseq([36, 38, 38, 36], 1), \dur, 0.25)
);
// --- Play ---
Pdef(\S).play;
)
Let’s dissect each generated element:
Line 1-9: The Header
A standard SC comment with the source file name and usage instructions. Generated by sc_header().
Line 11: The Opening (
In SuperCollider, the enclosing parentheses ( ... ) create an execution block: all code between ( and ) is evaluated at once when Ctrl+Enter is pressed.
Lines 13-21: The SynthDef
A minimal synthesizer (SinOsc + Pulse low-pass filtered) provided by default. The composer will replace it with their own SynthDefs — ideally a tabla synthesizer with realistic timbres for each bol. Generated by sc_synthdef_default().
Line 23: The TempoTempoClock.default.tempo = 160.0 / 60; sets the tempo to ~2.67 beats per second (160 BPM, a fast tempo typical of tabla). Translated from _mm(160) in the preamble. Generated by sc_tempo(160.0).
Lines 25-26: The References
The BP3 headers (-se.Tabla, -al.Tabla, comment) become SC comments. The -al.Tabla file (alphabet of bols) is crucial — it contains the mapping between bol names and MIDI numbers.
Line 30-32: Pdef for SS --> Theka Impro in ORD mode translates to Pseq([Pdef(\Theka), Pdef(\Impro)], 1). The structure is clear: first the theka (basic motif), then the improvisation.
Lines 36-41: Pdef for ImproImpro has two rules with weights 3 and 1 in RND mode. Pwrand gives a 75% chance to the resonant motif (dha tirakita dhin dhin) and 25% to the dry motif (tin ta dha ge na) — a faithful reflection of tabla practice, where thali (resonant) sections dominate.
Lines 43-45: Pdef for ThekaTheka has a single rule without weight. A simple Pbind with the four bols of the first vibhāg of the tintāl in sequence.
Line 48: The Play CommandPdef(\S).play; triggers playback. SuperCollider will:
- Evaluate
Pdef(\S), which launchesPseq([Pdef(\Theka), Pdef(\Impro)]) - Play the theka:
dha dhin dhin dha - Evaluate
Pdef(\Impro), which randomly chooses one of the two motifs - Play the selected improvisation motif
The result will be, for example: dha dhin dhin dha dha tirakita dhin dhin (75% of cases) or dha dhin dhin dha tin ta dha ge na (25% of cases) — exactly like a tablist who first plays theka then improvises.
Sidebar: The Power of Pdef for Live Coding
Once the code is running, you can modify an individual
Pdefand re-evaluate it without stopping the music. For example, changingPdef(\B)to use other notes will update the pattern in real-time. This is the live morphing mentioned in the header of the generated file.
Generated Code Invariants
The transpiler respects three structural invariants, documented in the source code:
INV-1: No comments in arrays. SC comments (//) must never appear inside Pseq([...]), Prand([...]), or Pwrand([...]), as SC interprets them as syntax errors. Comments are emitted before or after expressions.
INV-2: No bare MIDI integers in Pseq. Each MIDI number must be wrapped in a Pbind. A bare Pseq([60, 62, 64]) does not produce musical events — it requires Pbind(\midinote, Pseq([60, 62, 64], 1)).
INV-3: Balanced delimiters. All parentheses, brackets, and braces are checked to be correctly matched.
Key Takeaways
- The bp2sc transpiler works in five phases: lexer, classifier, parser, validation, emitter. Each phase has a unique responsibility.
- The AST serves as a neutral intermediate representation between BP3 and SuperCollider. Its root is a
BPFilenode containing headers and grammar blocks. - Derivation modes translate into specific SC patterns:
Pseqfor ORD,Prand/Pwrandfor RND,Proutfor flags and decremental weights. - The 42+ special functions cover pitch, velocity, articulation, timing, instruments, and pedals. Each translates into an SC
\key, valuepair. - Advanced constructs — polymetry, ties, variables, homomorphisms — are translated into adapted SC patterns:
Pbindfwith\stretchfor temporal compression,Pparfor parallel voices,Pdeffor variables. - Some BP3 elements (ContextMarker, GotoDirective, Wildcard, Lambda, Annotation) are not emitted because they require a derivation engine or have no sonic content.
- Each BP3 non-terminal becomes a SuperCollider Pdef, allowing live coding and real-time morphing.
To Go Further
- Source code: the bp2sc transpiler is available on GitHub, with 198 tests covering each AST node
- SC documentation: SuperCollider Pattern Guide
- BP3 documentation: Bol Processor – Pattern Grammars
- Bel, B. & Kippen, J. (1992). “Modelling Music with Grammars: Formal Language Representation in the Bol Processor” — the foundational article describing the complete grammar → derivation → musical output pipeline for tabla.
- Bel, B. (1998). “Migrating Musical Concepts — An Overview of the Bol Processor”, Computer Music Journal 22(3) — overview of the BP2 architecture, the predecessor of BP3.
- Bel, B. (2001). “Rationalizing Musical Time: Syntactic and Symbolic-Numeric Approaches” — formalization of musical time in BP, direct influence on TidalCycles.
- Kippen, J. & Bel, B. (2016). “Computers, Composition and New Music in Modern India” — review of 30 years of collaboration on BP and tabla.
- Book: Aho, Lam, Sethi & Ullman, Compilers: Principles, Techniques, and Tools (the “Dragon Book”), chapters 1-2 for compilation pipelines
Glossary
- AST (Abstract Syntax Tree): A tree-like, structured representation of source code, independent of textual syntax. The BP3 AST has a BPFile node as its root.
- BPFile: The root node of the BP3 AST, containing headers (file references, comments) and grammar blocks.
- Line Classifier: The first pass of the parser that identifies the type of each line (comment, header, mode, preamble, rule) via regular expressions.
- Recursive Descent: A parsing technique where each grammar rule corresponds to a function that recursively calls itself for sub-rules.
- Emitter: The module that traverses the AST and generates the target code (here, SuperCollider). Also called code generator or backend.
- embedInStream: SuperCollider method that inserts events from a pattern into a routine’s (
Prout) stream, allowing dynamic flow control. - GrammarBlock: AST node representing a grammar block with a mode (ORD, RND, LIN, SUB, SUB1), a preamble, and rules.
- Invariant: A property of the generated code that must always be true (e.g., no comments in an SC array).
- Lexer: A module that transforms raw text into lexical units (tokens). Here, combined with the line classifier.
- Live coding: The practice of modifying code in real-time during execution, facilitated by Pdef in SuperCollider.
- Morphing: Gradual transition between two versions of a pattern, made possible by re-evaluating individual Pdefs.
- normalizeSum: SC method that divides each element of an array by the total sum, converting weights into probabilities.
- Parser: The module that analyzes the syntactic structure of the source code and builds the AST. Here, a two-pass parser (classifier + recursive descent).
- Pbind: SC pattern that creates a stream of musical events from key-value pairs (instrument, note, duration, etc.).
- Pbindf: A variant of Pbind that adds or replaces keys in an existing pattern, without structurally modifying it.
- Pdef: A named and reactive container for an SC pattern. Allows live coding and real-time morphing.
- Pipeline: A chain of transformations where the output of one stage is the input of the next. Here: BP3 text → tokens → AST → SC code.
- Prand: SC pattern that chooses an element randomly (uniform distribution) from an array, N times.
- Prout: SC pattern based on a routine, allowing programmatic control of the flow (conditions, loops, mutable variables).
- Pseq: SC pattern that plays elements of an array in sequence, N times.
- Pwrand: SC pattern that chooses an element according to weights (weighted distribution), N times.
- Rule: AST node representing a BP3 production rule with grammar number, rule number, weight, flags, LHS, and RHS.
- SpecialFn: AST node representing a BP3 special function (
_transpose,_vel,_mm, etc.) with name and arguments. - Transpiler: A source-to-source translator between two languages of the same level of abstraction (here, BP3 to SuperCollider).
- Bol: Mnemonic syllable of the tabla (e.g., dha, dhin, tin, ta). The “Bol” in “Bol Processor”. Mapped to a MIDI number or a SynthDef via the
-al.Tablafile. - Kayda: Tabla composition with a fixed theme (theka) and systematic variations. A structure naturally modelable by a BP3 grammar (ORD for the theme, RND for variations).
- Theka: The basic pattern of a tāla, played by the tabla as an ostinato. In BP3, typically a deterministic ORD rule.
- Tintāl: The most common tāla in Hindustani music (16 beats in 4 vibhāgs of 4 beats). Its theka:
dha dhin dhin dha | dha dhin dhin dha | dha tin tin ta | ta dhin dhin dha. - Event.silent(dur): SuperCollider event that emits no sound for the duration
dur. Used for tie continuations and Lambda nodes. - HomoApply: AST node representing the application of a homomorphism. Three variants: MASTER (transformed source sequence), SLAVE (substitution table, integrated into MASTER), REF (label, not emitted).
- \stretch: SuperCollider key that multiplies the duration of each event by a factor.
\stretch, 0.75compresses time by 25%. Used for translating polymetric expressions (\stretch = M/N). - Tie: BP3 construct that merges two consecutive notes of the same pitch into a single prolonged note. Translated by
\legato, 2.0(start) andEvent.silent(continuation). - Variable: Named placeholder
|x|whose value is fixed at the first occurrence. Translated toPdef(\x), ensuring identity between all occurrences (crucial for the tihāī).
Prerequisites: L4 — AST, I3 — SuperCollider, B1 — PCFG to B6 — Homomorphisms
Reading time: 20 min
Tags: #transpiler #pipeline #SuperCollider #patterns #BP3 #AST #translation