Language Basics
Core syntax and concepts
Variables
Use let to define variables:
let bass = [C2 _ C2 _]
let melody = [E4 G4 A4 G4]Tempo
Set the tempo in BPM:
tempo 120
tempo 90Time Signatures
Set the time signature after tempo with a comma:
tempo 120 // Default 4/4
tempo 120, 7/8 // 120 BPM in 7/8 time
tempo 90, 5/4 // 90 BPM in 5/4 timeTime signature affects the default pattern duration (one bar).
Notes
Notes use scientific pitch notation: note name + octave number.
C4 // Middle C
Eb4 // E flat
F#5 // F sharpMicrotonal Pitches
Add + or - followed by a number (in cents) after a note to shift its pitch by fractions of a semitone. 100 cents = 1 semitone.
C4+50 // Quarter tone above C4
Eb3-15 // Eb3 lowered 15 cents
A4+50.5 // Fractional cents for fine tuning
D#5-50 // Quarter tone below E5This enables microtonal scales, quarter-tone melodies, and just-intonation approximations.
Frequency Literals
Use hz values directly in patterns to play an exact frequency:
[432hz 440hz 448hz] // A4 in different tunings
[267.5hz 300hz] // Arbitrary frequenciesRests
Use _ for rests (silence):
[C4 _ E4 _] // Note, rest, note, restPer-Note Probability
Add ? after a note to give it a chance of being replaced with silence:
[C4? E4 G4?] // C4 and G4 have 50% chance (default)
[C4?0.3 E4 G4?0.7] // C4 has 30%, G4 has 70% chance
[_?0.5] // Rest with 50% chance
[Cm7?0.5] // Chord with probabilityThe probability is deterministic per cycle - the same notes will play/skip on each loop.
Per-Note Velocity
Add @ followed by a value (0.0-1.0) to set note velocity:
[C4@0.8 E4@0.5 G4@1.0] // Explicit velocities
[C4@0.3 E4 G4] // E4 and G4 use default (1.0)
[Cm7@0.6] // Chord with velocityCombined Modifiers
Modifiers can be combined in any order after the note weight. Cent offsets are part of the note itself:
[C4:2?0.5@0.8 E4 G4] // Double duration, 50% chance, 80% velocity
[C4?0.5@0.8 E4@0.6?0.3 G4] // Order of ? and @ is flexible
[C4+50:2@0.8 Eb3-15?0.5] // Microtonal with modifiersUnits
Sanic supports unit literals for time and frequency values. Units are attached directly to numbers.
Time Units
0.5s // 0.5 seconds
500ms // 500 milliseconds (= 0.5s)
1b // 1 beat (tempo-relative)
1/4 // Quarter note (1 beat)
1/8 // Eighth note (0.5 beats)
3/16 // Dotted eighth (0.75 beats)Frequency Units
2hz // 2 cycles per second
0.5hz // 1 cycle every 2 secondsTempo-Aware Conversion
Beat-based units automatically convert based on the current tempo:
tempo 120
// At 120 BPM:
// - 1 beat = 0.5 seconds
// - 1/4 (quarter note) = 0.5 seconds
// - 1/8 (eighth note) = 0.25 seconds
|> delay(1/4) // 0.5 second delay at 120 BPM
|> delay(1/8) // 0.25 second delay at 120 BPM
|> lfo(rate: 1/4) // 2 Hz LFO at 120 BPM (one cycle per beat)If you change the tempo, beat-based values automatically adjust:
tempo 60 // Half as fast
|> delay(1/4) // Now 1 second delay (1 beat at 60 BPM)Patterns
Patterns are sequences of notes in square brackets:
[C4 E4 G4] // Three notes
[C4 _ E4 _ G4 _] // Notes with rests
[C4 E4 | G4 B4] // Bar lines (visual only)Pipe Operator
Chain operations with |>:
let sound = [C4 E4 G4]
|> saw
|> reverb(0.5)
|> delay(0.25)Each step flows into the next: pattern → transforms → instrument → envelope → effects.
Pipe order matters:
| Stage | What | Examples |
|---|---|---|
| 1. Pattern | Notes to play | [C4 E4 G4], Cm7 |
| 2. Transforms | Modify the pattern | reverse, transpose, every, arp |
| 3. Instrument | How it sounds | saw, sine, supersaw, fm |
| 4. Envelope | Shape over time | adsr |
| 5. Effects | Process the sound | lowpass, reverb, delay |
Comments
// This is a comment
let x = 42 // Inline commentImports
Import from other files or packages:
import "github:SanicBoom/drums"
from "./bass.sanic" import { bassline }
from "github:SanicBoom/some-lib" import { kick }GitHub libraries can expose .sanic modules — see Sample Libraries for details.
Play
Use play to output sound:
play melody
play stack(drums, bass, melody)Sections (Arrangement)
Use section blocks to sequence parts over time. Each section plays for a fixed number of bars, then the next section starts. After all sections finish, the arrangement loops.
section "intro" 8
play [C4 E4 G4] |> sine |> reverb(0.8)
section "drop" 8
play [C2 C2 C2 C2] |> saw |> lowpass(800)
play [C4 E4 G4 B4] |> supersawSections play sequentially: "intro" for 8 bars, then "drop" for 8 bars, then back to "intro".
Crossfading Sections
Add fade N after the bar count to fade in over the first N bars and fade out over the last N bars. This creates smooth crossfades between adjacent sections:
section "intro" 8 fade 2
play [C4 E4 G4] |> sine |> reverb(0.8)
section "drop" 8 fade 2
play [C2 C2 C2 C2] |> saw |> lowpass(800)The fade linearly scales note velocity from 0 to full over the fade-in region and back to 0 over the fade-out region. When two sections both have fades, their overlapping fade-out/fade-in creates a crossfade.
Mixing Sections and Free-Standing Play
play statements outside of sections loop forever, unaffected by the arrangement:
play [C2 _ C2 _] |> sine // loops forever (no section)
section "verse" 16
play [E4 G4 A4 G4] |> saw
section "chorus" 16
play [C4 E4 G4 C5] |> supersawThe bass loops continuously while verse and chorus alternate every 16 bars.
User-Defined Functions
Define your own functions with fn:
fn name(params) = bodyBasic Functions
fn octaveUp(p) = p |> transpose(12)
fn darkSaw(p) = p |> saw |> lowpass(400)
play [C4 E4 G4] |> octaveUp |> saw
play [C4 E4 G4] |> darkSawFunctions with Multiple Parameters
fn transposedArp(p, semitones) = p |> transpose(semitones) |> arp(up)
play Cm7 |> transposedArp(5) |> sawBuilding Complex Transforms
fn wobble(p) = p |> slow(2) |> every(2, fast(2))
fn dreamy(p) = p |> reverb(size: 0.8) |> delay(time: 0.3)
play [C4 E4 G4 B4] |> wobble |> saw |> dreamyFunctions as Transforms
User-defined functions work with every and probability functions:
fn myTransform(p) = p |> reverse |> transpose(12)
play [C4 D4 E4 F4] |> every(4, myTransform) |> saw
play [C4 D4 E4 F4] |> sometimes(myTransform) |> saw