|
| 1 | +<img |
| 2 | + align="center" |
| 3 | + src="https://github.com/kylestetz/slang/logo.png" |
| 4 | + width="468" |
| 5 | +/> |
| 6 | + |
| 7 | +# Slang — An audio programming language built in JS |
| 8 | + |
| 9 | +[Play with Slang](http://slang.kylestetz.com) |
| 10 | + |
| 11 | +Slang was created to explore implementing a programming language entirely in the browser. Parsing is handled by [Ohm.js](https://github.com/harc/ohm) using a [custom grammar](./slang-grammar.js), the editor uses CodeMirror with a simple syntax definition, and the runtime itself is written in JS using the Web Audio API. |
| 12 | + |
| 13 | +### Goals of this project |
| 14 | + |
| 15 | +I have always wanted to write a programming language from scratch, but as someone who didn't study computer science I find it incredibly intimidating. Discovering [Ohm.js](https://github.com/harc/ohm) changed my mind; its incredible editor and approachable JS API make it possible to experiment quickly with a lot of feedback. This project is my first pass at build a language and runtime environment from start to finish. |
| 16 | + |
| 17 | +This is not meant to be a great or comprehensive language itself, but I do hope this project can serve as a roadmap if you'd like to build your own! |
| 18 | + |
| 19 | +You'll notice a distinct lack of in-context error handling, inline docs, helpful UI, etc. Creating a great editor experience was not a goal of this project and it would take a lot of work to get there. I did my best to make it pleasant to use. |
| 20 | + |
| 21 | +# How to write Slang |
| 22 | + |
| 23 | +Slang consists of **sound lines** and **play lines**. Sound lines build up a synthesizer (or drum machine), then play lines tell those synthesizers or drum machines what to play. |
| 24 | + |
| 25 | +It turns out that explaining your own programming language is ridiculously hard, so I suggest skipping to the **Examples** section below and trying those out before reading all of these docs. |
| 26 | + |
| 27 | +## Sound Lines |
| 28 | + |
| 29 | +A sound establishes a variable (which always starts with `@`) that contains a **chain of sounds**. |
| 30 | + |
| 31 | +## Play Lines |
| 32 | + |
| 33 | +A play line starts with the word `play`, followed by the variable you want to play, and then declares a rhythm and notes to use. You can have multiple play lines referencing a single synth and they will all play independently (e.g. if you want to play polyphonic melodies). |
| 34 | + |
| 35 | +`rhythm` accepts a list of rhythm values or a function that returns rhythm values, while `notes` accepts a list of notes or a function that returns notes. Let's look at a simple example and then see how we can take advantage of the more advanced functions. |
| 36 | + |
| 37 | +A simple synth: |
| 38 | +``` |
| 39 | +@synth (adsr (osc sine) 64n 8n 0 8n) |
| 40 | +play @synth (rhythm [8n]) (notes [e3 e4 e5]) |
| 41 | +``` |
| 42 | + |
| 43 | +Now let's make a synth that plays a scale using the `(chord)` function. Chord takes a type as its second argument (e.g. `major`, `chromatic`, `phrygian`, etc.) and a root note as its third argument. |
| 44 | +``` |
| 45 | +@synth (adsr (osc tri) 64n 8n 0 8n) |
| 46 | +play @synth (rhythm [8n]) (notes (chord lydian e3)) |
| 47 | +``` |
| 48 | + |
| 49 | +Taking it one step further, let's put that `chord` function call within the `random` function, which will randomly pick one of the notes from the chord each time it's called. |
| 50 | +``` |
| 51 | +@synth (adsr (osc tri) 64n 8n 0 8n) |
| 52 | +play @synth |
| 53 | + (rhythm [8n]) |
| 54 | + (notes (random (chord lydian e3))) |
| 55 | +``` |
| 56 | + |
| 57 | +The `flatten` and `repeat` functions, when used inside of `notes`, are a powerful way to create repeating phrases. Since `notes` only takes a single list we use the `flatten` function to take a few different calls and flatten them down. The `repeat` function will take the list we give it and repeat it a number of times, saving us some copying & pasting. |
| 58 | +``` |
| 59 | +@synth (adsr (osc sine) 64n 8n 0 8n) |
| 60 | +play @synth |
| 61 | + (rhythm [8n]) |
| 62 | + (notes (flatten [ |
| 63 | + (repeat 3 (chord lydian e4 4)) |
| 64 | + (chord lydian d4 4) |
| 65 | + ])) |
| 66 | +``` |
| 67 | + |
| 68 | +## Syntax |
| 69 | + |
| 70 | +Functions are contained within parentheses, much like in Clojure. The first keyword in a function is the **functio name**, which is followed by all of its arguments. Any argument can be a primitive value or a list (neat!); if it's a list, Slang will take one value at a time and loop back to the beginning when it reaches the end. Check out the Reference section for lots of usage examples. |
| 71 | + |
| 72 | +# Reference |
| 73 | + |
| 74 | +In Slang every argument can be either a static value (such as `8n`, `e3`, `1`, etc.) or a list of values. If you provide a list as an argument to a function it will take the next value in the list every time it is called, looping back around when it reaches the end. As an example, the oscillator can accept a list of types: `(osc [sine tri saw])`. Every time a note is hit, it will use the next type in the list. |
| 75 | + |
| 76 | +## Sound Functions |
| 77 | + |
| 78 | +### `(osc <type: sine> <pitchOffset: 0>)` |
| 79 | + |
| 80 | +Creates an oscillator with an optional pitchOffset in semitones. Filters and effects can be chained off of the oscillator using the `+` sign. |
| 81 | + |
| 82 | +`type`: |
| 83 | +- `sine` |
| 84 | +- `saw` or `sawtooth` |
| 85 | +- `tri` or `triangle` |
| 86 | +- `square` |
| 87 | + |
| 88 | +`pitchOffset`: how many semitones to shift the pitch. |
| 89 | + |
| 90 | +Usage: |
| 91 | +``` |
| 92 | +# Creates a synth with two sine oscillators, one pitched 7 semitones above the root note |
| 93 | +@synth (osc sine) |
| 94 | +@synth (osc sine 7) |
| 95 | +
|
| 96 | +# Creates a synth that chooses a random oscillator for each note that is hit. |
| 97 | +@melody (osc (random [sine saw tri square])) |
| 98 | +``` |
| 99 | + |
| 100 | +### `(drums)` |
| 101 | + |
| 102 | +Creates a drum machine. It does not accept any arguments. |
| 103 | + |
| 104 | +When writing a play line, the notes 0 - 11 represent the 12 drum sounds. |
| 105 | + |
| 106 | +_Pro tip_: Any number above 11 will wrap around using modulus, so for example 25 will trigger sound 1 since `25 % 12 == 1`. This allows you to pass in note values (e.g. `e3`) as well since they correspond to number values. |
| 107 | + |
| 108 | +### `(adsr <osc> <attack: 0.05> <decay: 0> <sustain: 1> <release: 0.05>)` |
| 109 | + |
| 110 | +Creates an amp envelope which contains an oscillator followed by ADSR values. The attack, decay, and release arguments can be numbers or rhythm values (e.g. `8n`, `8t`, `4n`, etc.). Sustain is a number from 0 - 1. |
| 111 | + |
| 112 | +Usage: |
| 113 | +``` |
| 114 | +# Creates a sine wave oscillator with an amp envelope. |
| 115 | +@synth (adsr (osc sine) 8n 8n 0.5 4n) |
| 116 | +``` |
| 117 | + |
| 118 | +### `+ (filter <type: lp> <frequency: 100> <resonance: 1>)` |
| 119 | + |
| 120 | +Creates a filter. This should be chained off of a oscillator or envelope. |
| 121 | + |
| 122 | +`type`: |
| 123 | +- `lp` (lowpass) |
| 124 | +- `hp` (highpass) |
| 125 | +- `bp` (bandpass) |
| 126 | +- `n` (notch) |
| 127 | + |
| 128 | +`frequency`: A value from 0 - 127 representing the frequencies 0 - 11,025. |
| 129 | + |
| 130 | +`resonance`: A number from 0 - 100 representing the amount of resonance (Q) to apply. |
| 131 | + |
| 132 | +Usage: |
| 133 | +``` |
| 134 | +@synth (osc sine) + (filter lp 20) |
| 135 | +``` |
| 136 | +``` |
| 137 | +# Make a lowpass filter that loops through the |
| 138 | +# numbers 10 to 50 one at a time. |
| 139 | +@melody (osc saw) + (filter lp [10..50]) |
| 140 | +``` |
| 141 | + |
| 142 | +### `+ (gain <value>)` |
| 143 | + |
| 144 | +Creates a gain (volume). This should be part of a sound chain. |
| 145 | + |
| 146 | +`value`: A number from 0 - 1. |
| 147 | + |
| 148 | +Usage: |
| 149 | +``` |
| 150 | +@synth (osc sine) + (gain 0.5) |
| 151 | +@melody (osc sine) + (gain [0 0.25 0.5 0.75 1]) |
| 152 | +``` |
| 153 | + |
| 154 | +### `+ (pan <value>)` |
| 155 | + |
| 156 | +Creates a stereo panner. This should be part of a sound chain. |
| 157 | + |
| 158 | +`value`: A number from -1 (left) to 0 (center) to 1 (right). |
| 159 | + |
| 160 | +Usage: |
| 161 | +``` |
| 162 | +@synth (osc sine) + (pan -1) |
| 163 | +@synth (osc sine 12) + (pan 1) |
| 164 | +``` |
| 165 | + |
| 166 | +### `+ (delay <time: 8n> <feedback: 0.1> <wet: 0.5> <dry: 0.5> <cutoff: 11025>)` |
| 167 | + |
| 168 | +Creates a delay effect. This should be part of a sound chain. |
| 169 | + |
| 170 | +`time`: A rhythm value or a number in seconds. |
| 171 | + |
| 172 | +`feedback`: A number from 0 - 1 representing the amount of feedback to apply. |
| 173 | + |
| 174 | +`wet`: A number from 0 - 1 representing the wet level. |
| 175 | + |
| 176 | +`dry`: A number from 0 - 1 representing the dry level. |
| 177 | + |
| 178 | +`cutoff`: A number from 0 - 11025 representing the frequency of a cutoff filter on the delay. |
| 179 | + |
| 180 | +Usage: |
| 181 | +``` |
| 182 | +@synth (adsr (osc saw) 64n 8n 0 8n) + (delay 8t 0.4 1 1) |
| 183 | +``` |
| 184 | + |
| 185 | +Creates |
| 186 | + |
| 187 | +## Utility Functions |
| 188 | + |
| 189 | +### `(chord <type> <root> <length>)` |
| 190 | + |
| 191 | +Returns a list of notes belonging to a chord. |
| 192 | + |
| 193 | +`type`: A text value representing a chord type, e.g. `major`, `bebop`, `phrygian`. The list of possible chords is taken from [this library](https://github.com/danigb/tonal/blob/master/packages/dictionary/data/scales.json), but with spaces and `#` symbols removed (e.g. `minor #7M pentatonic` becomes `minor7Mpentatonic` in Slang). |
| 194 | + |
| 195 | +`root`: A note, e.g. `e3`. |
| 196 | + |
| 197 | +`length` (optional): a number representing exactly how many notes to return in the list. If unspecified, the length of the list will vary from chord to chord. |
| 198 | + |
| 199 | +Usage: |
| 200 | +``` |
| 201 | +@synth (adsr (osc sine) 64n) |
| 202 | +play @synth (notes (chord phrygian e3)) |
| 203 | +``` |
| 204 | + |
| 205 | +### `(random <list>)` |
| 206 | + |
| 207 | +Selects a random item from the list each time it is called. The list can be a range such as `[1..10]` or the output of any other utility function, such as `chord` or `flatten`. |
| 208 | + |
| 209 | +Usage: |
| 210 | +``` |
| 211 | +@synth (adsr (osc (random [saw tri])) 64n) |
| 212 | + + (filter lp [10..50]) |
| 213 | +play @synth |
| 214 | + (rhythm (random [8n 8t 4n])) |
| 215 | + (notes (random (chord phrygian e3))) |
| 216 | +``` |
| 217 | + |
| 218 | +### `(flatten <list>)` |
| 219 | + |
| 220 | +Takes a list of lists and flattens it. |
| 221 | + |
| 222 | +Usage: |
| 223 | +``` |
| 224 | +@synth (adsr (osc sine) 64n) |
| 225 | +play @synth (notes (flatten [[e3..e4] [d#4..e#3]])) |
| 226 | +``` |
| 227 | +``` |
| 228 | +@synth (adsr (osc sine) 64n) |
| 229 | +play @synth (notes (flatten [ |
| 230 | + (repeat 2 [e3 e4 e5]) |
| 231 | + (repeat 2 [d3 d4 d5]) |
| 232 | + (repeat 2 [a3 a4 a5]) |
| 233 | + [g3 g4 g5] |
| 234 | + [f3 f4 f5] |
| 235 | + ])) |
| 236 | +``` |
| 237 | + |
| 238 | +### `(repeat <amount> <list>)` |
| 239 | + |
| 240 | +Takes a list and repeats it `amount` times. Useful when used inside of `flatten`. |
| 241 | + |
| 242 | +Usage: |
| 243 | +``` |
| 244 | +@perc (drums) |
| 245 | +play @perc (notes (flatten [ |
| 246 | + (repeat 2 [0 6 3 6]) |
| 247 | + (repeat 2 [6 0 3 6]) |
| 248 | + ])) |
| 249 | +``` |
| 250 | + |
| 251 | +### `(reverse <list>)` |
| 252 | + |
| 253 | +Reverses the list. |
| 254 | + |
| 255 | +Usage: |
| 256 | +``` |
| 257 | +@synth (adsr (osc sine) 64n) + (gain 0.5) |
| 258 | +play @synth (notes (reverse (chord lydian e4))) |
| 259 | +play @synth (notes (chord lydian e5)) |
| 260 | +``` |
| 261 | + |
| 262 | +--- |
| 263 | + |
| 264 | +Primite values: |
| 265 | +- **numbers** - integers and floats (`0`, `0.25`, `10000`, etc.) |
| 266 | +- **lists** (space-separated) - `[0 1 2 3 4 5 6]` |
| 267 | +- **notes** - `e3`, `d#4`, `f2`, etc. |
| 268 | +- **rhythm** - `32t`, `32n`, `16t`, `16n`, `8t`, `8n`, `4t`, `4n`, `2n`, and `1n` |
| 269 | +- **rests** - `r32t`, `r32n`, `r16t`, `r16n`, `r8t`, `r8n`, `r4t`, `r4n`, `r2n`, and `r1n` |
| 270 | +- **special strings** - some functions take string arguments, such as `filter` and `osc` |
| 271 | + |
| 272 | +--- |
| 273 | + |
| 274 | +# Examples |
| 275 | + |
| 276 | +A simple synthesizer |
| 277 | +``` |
| 278 | +# This is a sound line that establishes a synthesizer called @melody |
| 279 | +@melody (adsr (osc sine) 64n 8n 0) |
| 280 | +# This is a play line that plays @melody using a rhythm and a list of notes |
| 281 | +play @melody (rhythm [8n]) (notes [e3 d3 g3 f3]) |
| 282 | +``` |
| 283 | + |
| 284 | +A drum machine |
| 285 | +``` |
| 286 | +# Drums don't accept any arguments (at the moment!) |
| 287 | +@percussion (drums) |
| 288 | +
|
| 289 | +# Hi-hats |
| 290 | +play @percussion (rhythm [16n r16n 16n 16n]) (notes [6 7 8]) |
| 291 | +# Kick and snare |
| 292 | +play @percussion (rhythm [8n]) (notes [0 3 11 0 3 0 3 11]) |
| 293 | +``` |
| 294 | + |
| 295 | +A randomized synth & bassline with drums |
| 296 | +``` |
| 297 | +@synth (adsr (osc saw) 64n) |
| 298 | + + (filter lp (random [5..30])) |
| 299 | + + (delay 8n 0.7 0.5 1) |
| 300 | + + (gain 0.25) |
| 301 | +
|
| 302 | +@bass (adsr (osc tri) 64n 2n 0.4 4n) |
| 303 | +
|
| 304 | +@drums (drums) + (gain 2) |
| 305 | +
|
| 306 | +play @synth |
| 307 | + (rhythm [8t]) |
| 308 | + (notes (random (chord phrygian e5))) |
| 309 | +
|
| 310 | +play @bass |
| 311 | + (rhythm [1n]) |
| 312 | + (notes (random (chord phrygian e2))) |
| 313 | +
|
| 314 | +play @drums |
| 315 | + (rhythm [8t r8t 8t 8t 8t r8t]) |
| 316 | + (notes [6]) |
| 317 | +play @drums |
| 318 | + (rhythm [4n 4n 4n r8t 8t 8t]) |
| 319 | + (notes [0 3 0 11 11]) |
| 320 | +``` |
0 commit comments