Algorithmic Orchestration

April 9, 2026 · View on GitHub

Scoring music for a virtual orchestra via TidalCycles live coding. This document is the operational guide for the Algorithmic Orchestration principle — the paradigm of abstracting musical elements into three concerns: harmony/contexts (the Harmonic Algorithm), form/constants (the Spectral Narrative), and interfaces/timbres (instrument functions, voice lines, articulations).

Signal Chain

Code → TidalCycles → SuperDirt → MIDI → Roland JV-1010 → orchestral music

JV-1010 Configuration (16 MIDI Channels)

ChannelInstrumentType
1FluteWind
2OboeWind
3ClarinetWind
4BassoonWind
5HornBrass
6Trombone/BassBrass
7HarpPlucked
8TimpaniPitched perc
9Bass DrumUnpitched perc
10subKickMPC sub/kick
11Tam-tamUnpitched perc
12Strings pizzString artic
13Strings spiccString artic
14Strings marcString artic
15Strings leggString artic
16Strings arcoString artic

Performance Architecture

Voice leading (cyclic DP) is expensive. With 16+ stacked arrange calls (full orchestral mode), naive per-frame recomputation causes TidalCycles "skip" messages.

The fix is a construction-time voicing cache: arrange and arrange' query kProg k once when the pattern is registered, pre-compute voicings for all unique progressions (~2–3 in a typical form), and store them in an association list. Per-frame lambdas do O(1) lookup instead of running the DP solver. Result: ~800 voice leading solves/second → 2–3 (once at construction).

Frame timespan is set to 1/30 (~33ms frames). At oLatency = 0.15 (150ms SuperDirt latency), timing resolution is dominated by output latency — 33ms frames give more than sufficient musical precision.

Two Separate Concerns

  1. (-9, 9) register — pattern index trimming. A compositional parameter passed to arrange. Controls how many scale degrees above/below the input patterns can reach. This is the composer's domain.

  2. clip — MIDI range enforcement. Filters events whose MIDI note falls outside the physical instrument range. Internal to each instrument function — invisible to the composer.

These are independent: (-9, 9) controls compositional range, clip prevents impossible notes from reaching hardware.

Parameter Convention: d Last

All orchestral blocks (and updated existing blocks) take d as the last parameter, enabling the $ d syntax:

,wind f r k       $ d 0.9
,tutti arco f r k $ d 0.9
,k909 f k         $ d 0.4

Where d = (* 1) in the launcher — a dynamics multiplier applied via $.

Unified Block Template

Every block (section or blend) has the same two-part structure:

{name} f k d = p "{name}" $ do
  let vl = voiceLines {_vl = "~"
        -- , soprano = "3"
        -- , alto    = "1"
        -- , tenor   = "2"
        -- , bass    = "0"
        }
  f
    $ stack [silence
        , instrument (ki_range) k vl voiceFunc Voice
        , ...
    ] |* vel d
  1. Voice declaration (vl): voiceLines with optional overrides
  2. Instrument stack: each line is an instrument with kinetics range, IK context, voice lines, voicing paradigm, voice assignment

Instrument Functions

Pitched (arrange + clip + octave)

Each instrument is a partial application of instrument range channel:

instrument :: (Int, Int) -> Int -> (Double, Double) -> IK -> VoiceLines -> VoiceFunction -> Voice -> ControlPattern

Pipeline: arrange# ch|+ octclip

flute    = instrument (-12, 26)  1   -- C3-D6  (MIDI 48-86)
oboe     = instrument ( -2, 33)  2   -- Bb3-A6 (MIDI 58-93)
clarinet = instrument (-22, 34)  3   -- D2-Bb6 (MIDI 38-94)
-- etc.

Unpitched (struct-based)

bassdrum pat = struct pat $ midinote 36 # ch 9 # sustain 0.05
tamtam   pat = struct pat $ midinote 31 # ch 11 # sustain 0.5

Instrument Catalogue

Pitched Instruments

InstrumentChTidal RangeMIDI RangePitch RangeSection
Flute1(-12, 26)48–86C3–D6Wind
Oboe2(-2, 33)58–93Bb3–A6Wind
Clarinet3(-22, 34)38–94D2–Bb6Wind
Bassoon4(-28, 15)32–75Bb1–Eb5Wind
Horn5(-29, 17)31–77B1–F5Brass
Trombone6(-28, 17)32–77Bb1–F5Brass
Bass Trombone6(-39, -5)21–55A0–G3Brass
Harp7(-29, 42)31–102B1–F#7Plucked
Timpani8(-22, 0)38–60D2–C4Pitched perc
Violin 116(-5, 45)55–105G3–A7Strings (arco)
Violin 216(-5, 45)55–105G3–A7Strings (arco)
Viola16(-12, 28)48–88C3–E6Strings (arco)
Cello16(-24, 24)36–84C2–C6Strings (arco)
Contrabass16(-36, 0)24–60C1–C4Strings (arco)

Tidal note 0 = MIDI 60 = middle C. Ranges are enforced by clip inside each instrument function.

Unpitched Percussion

InstrumentChMIDI NoteUsage
Bass Drum936 (C2)bassdrum pat
Tam-tam1131 (G1)tamtam pat

subKick (separate signal chain — "thru" device, not JV-1010)

PartChMIDINotes
Sub1036–47C2–B2 (pitch class + 36, mapped from harmonic root)
Kick1048C3 (fixed)
Silence1035B1 (no sample)

String Articulations

Strings default to arco (ch 16). Override with #:

ArticulationChAliasUsage
Pizzicato12pizz# pizz or tutti pizz f k $ d 0.7
Spiccato13spicc# spicc
Marcato14marc# marc
Legato15legg# legg
Arco16arcoDefault (same as string channel)

JV-1010 Pan Positions

ChInstrumentPan
1Flute-10
2Oboe10
3Clarinet-15
4Bassoon15
5Horns-22
6Trombones18
7Harp-25
8Timpani-18
9Bass drum-6
10subKick/MPC0
11Tam-tam22
12–16Strings0

Target Ensemble

2 flutes (doubling piccolo), 2 oboes (doubling cor anglais), 2 clarinets, 2 bassoons, 2 horns, 2 trombones (doubling bass trombone), 1 timpanist, 1 percussionist, 1 harp, 8 vn1, 6 vn2, 4 va, 3 vc, 2 cb

For interactive range tests, see live/ORCHESTRAL_CATALOGUE.tidal.

Voice Line System

SATB defaults

voiceLines = VoiceLines
  { _vl  = "~"     -- structural placeholder
  , soprano = "3"     -- soprano: root 8va
  , alto    = "1"     -- alto: 2nd degree
  , tenor   = "2"     -- tenor: 3rd degree
  , bass    = "0"     -- bass: root
  }

Overriding voices

let vl = voiceLines {_vl = "~"
      , soprano = "[3 2]/4"
      -- , alto    = "1"      -- commented = use default
      -- , tenor   = "2"
      , bass    = "[0 1]/4"
      }

The _vl field enables comma-leading syntax (same principle as stack [silence, ...]).

Voice Octave Variants

Standard Italian musical terminology for register shifts:

SuffixOffsetMeaning
(none)0Loco (as written)
8va+1Octave up
15va+2Two octaves up
8vb-1Octave down
15vb-2Two octaves down

Applied to any voice: Soprano8va, Tenor8vb, Bass15vb, etc.

The octave shift happens inside the instrument function (via |+ oct) before clip filters. No |- oct 1 noise in templates.

String Articulations

Channel routing with default arco:

pizz  = ch 12    -- pizzicato
spicc = ch 13    -- spiccato
marc  = ch 14    -- marcato
legg  = ch 15    -- legato
arco  = ch 16    -- arco (default)

Override with #:

, violin1 (0,1) flow Soprano r k vl # pizz

For tutti, the articulation is a parameter:

tutti arco f r k $ d 0.9
tutti pizz f r k $ d 0.7

Sections

NameInstruments
windflute, oboe, clarinet, bassoon
brsshorn, trombone, basstrom
strgviolin1, violin2, viola, cello, contrabass
perctimpani, harp, bassdrum, tamtam

Blends

NameCharacterInstruments
chalumeauDark warmthclarinet, bassoon, horn
pastoraleMid-register colourflute, oboe, clarinet
brillanteBright top (flute 8va at high k)flute 8va, flute, oboe, clarinet
maestosoFull winds + brass (climactic)wind + horn, trombone
tuttiFull orchestrastrings + winds + brass + timpani

Groove — subKick

subKick is a separate signal chain — it does not use the JV-1010. It routes via the "thru" device on MIDI channel 10 to an MPC (or equivalent sampler).

Note Mapping

PartMIDI NoteRange
Sub36–47C2–B2 (pitch class → MIDI, mapped from harmonic root)
Kick48 (fixed)C3
Silence (no sample)35B1

Sub pitches are normalised from the progression's harmonic root pitch class: pitch_class + 36. This places the sub register below all orchestral instruments, leaving MIDI 48+ free for anything else sharing the channel.

CC64 Sustain Mechanism

The sub voice uses MIDI CC64 (sustain pedal) to hold notes:

  • sustain 0.01 triggers a brief note-on (the sampler latches it)
  • segment 16 CC64=127 background keeps the sustain held (~1 event/frame at 30fps)
  • CC64=0 at maxDur or subOffPat boundaries releases the note

This mechanism is intentional and must not be changed.

Voicing Cache

Voicings are pre-computed once at construction time (not per TidalCycles frame). All unique progressions in kProg k are resolved upfront; the per-frame lambda does a lookup instead of running the voice function. This eliminates hundreds of redundant computations per second in full orchestral mode.

Usage

subk f k d = p "subKick"
  $ f
    $ subKick d k root (maxDur, subOnStr, subOffStr, kickStr)
  • root or fund — always returns the harmonic root regardless of inversion
  • Sub group gates at (0.1, 1), kick at (0.2, 1) via ki
  • maxDur < 1 triggers auto-off; maxDur >= 1 means manual-off only

Kinetics Layering

Per-instrument kinetics ranges create crescendo ordering:

tutti art f k d =
  ...
    -- Strings foundation (always)
    , violin1    (0, 1)   k vl flow Soprano    # art
    -- Winds enter at 0.2
    , flute      (0.2, 1) k vl flow Soprano
    -- Brass at 0.5
    , horn       (0.5, 1) k vl flow Soprano
    -- Percussion at peak
    , timpani    (0.8, 1) k vl grid Bass8vb

As kinetics rises from 0→1, instruments enter progressively.

Examples

Basic: wind section

,wind f k $ d 0.9

Custom voice lines

wind' f k d = p "wind" $ do
  let vl = voiceLines {_vl = "~"
        , soprano = "[3 2]/4"
        , bass    = "[0 1]/4"
        }
  f
    $ stack [silence
        , flute    (0,1) k vl flow Soprano
        , oboe     (0,1) k vl flow Alto
        , clarinet (0,1) k vl flow Tenor8vb
        , bassoon  (0,1) k vl flow Bass8vb
    ] |* vel d

Full orchestra with articulation switching

,tutti arco f k $ d 0.9    -- arco strings
,tutti pizz f k $ d 0.7    -- pizzicato strings

Combined launcher

do
  let
    f = (swingBy 0.04 2)
    d = (* 1)
    k = iK tempo form (warp "[1 2 3 4]/8")
  mapM_ id [hush, setbpm tempo
    ,wind f k       $ d 0.9
    ,strg f k       $ d 0.8
    ,brss f k       $ d 0.9
    ,perc f k       $ d 0.7
    ,subk f k       $ d 1
   ]