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:

  1. The lexer/classifier reads the raw text and identifies the type of each line (comment, header, mode, preamble, rule)
  2. The parser analyzes each line according to its type and builds the corresponding AST nodes
  3. The AST serves as an intermediate representation — a structured tree that is linked neither to BP3 nor to SC
  4. Validation checks the consistency of the AST (normalized weights, defined symbols, etc.)
  5. 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({ &#124;ev&#124; ... }) 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({ &#124;ev&#124; ... 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 Prout with 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 Prand as 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:

  1. Preamble functions: placed before the rules, they apply to the entire block (_mm, _striated)
  2. Modification functions: in the RHS, they modify subsequent notes (_transpose, _vel, _staccato)
  3. 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: \stretch vs \dur

In SuperCollider, \dur sets the absolute duration of an event (in beats). \stretch is a multiplier applied to \dur: the effective duration is dur × stretch. The transpiler uses \stretch rather than recalculating \dur to 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 ContextMarker or GotoDirective only 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({ &#124;ev&#124; ... }) 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 a Pdef that 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:

  1. 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)

  1. Multi-block detection: S is defined only in block 1, Impro and Theka in block 2 — no name conflict.
  1. 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.
  1. Terminal resolution: The bols (dha, dhin, tin, ta, tirakita, ge, na) are resolved via the -al.Tabla file 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 Tempo
TempoClock.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 S
S --> 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 Impro
Impro 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 Theka
Theka 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 Command
Pdef(\S).play; triggers playback. SuperCollider will:

  1. Evaluate Pdef(\S), which launches Pseq([Pdef(\Theka), Pdef(\Impro)])
  2. Play the theka: dha dhin dhin dha
  3. Evaluate Pdef(\Impro), which randomly chooses one of the two motifs
  4. 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 Pdef and re-evaluate it without stopping the music. For example, changing Pdef(\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 BPFile node containing headers and grammar blocks.
  • Derivation modes translate into specific SC patterns: Pseq for ORD, Prand/Pwrand for RND, Prout for flags and decremental weights.
  • The 42+ special functions cover pitch, velocity, articulation, timing, instruments, and pedals. Each translates into an SC \key, value pair.
  • Advanced constructs — polymetry, ties, variables, homomorphisms — are translated into adapted SC patterns: Pbindf with \stretch for temporal compression, Ppar for parallel voices, Pdef for 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.Tabla file.
  • 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.75 compresses 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) and Event.silent (continuation).
  • Variable: Named placeholder |x| whose value is fixed at the first occurrence. Translated to Pdef(\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