- Notifications
You must be signed in to change notification settings - Fork31
🎤 a simple audio programming language implemented in JS
License
kylestetz/slang
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Slang was created to explore implementing a programming language entirely in the browser. Parsing is handled byOhm.js using acustom grammar, the editor uses CodeMirror with a simple syntax definition, and the runtime itself is written in JS using the Web Audio API.
I've always wanted to write a programming language from scratch, but as someone who didn't study computer science I find it incredibly intimidating. DiscoveringOhm.js 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 building a language and runtime environment from start to finish.
This is not meant to be a great or comprehensive language, but I do hope this project can serve as a roadmap if you'd like to build your own!
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.
Slang consists ofsound lines andplay lines. Sound lines build up a synthesizer (or drum machine), then play lines tell those synthesizers or drum machines what to play.
@synth (adsr (osc tri) 64n 8n 0.5 8n)play @synth(rhythm [8n])(notes [c3 d3 e3 f3 g3 a3 b3 c4])It turns out that explaining your own programming language is ridiculously hard, so I suggest skipping to theExamples section below and trying those out before reading all of these docs.
A sound line establishes a variable (which always starts with@) that contains achain of sounds. Sounds always start with either an(osc) or(drums) and can chain tools likefilter,pan, andgain together using the+ operator.
Here we have a sine oscillator which gets piped into a lowpass filter and then gain.
@synth (osc sine)+ (filter lp 100)+ (gain 0.5)You can add multiple sound lines for the same variable; when a note is played all of its corresponding chains of sound will trigger.
Here's a sound that has two oscillators, the second one pitched up an octave. When the sound plays you'll hear both oscillators firing for each note.
@synth (osc sine)@synth (osc sine 12)play @synth (notes [e3])💡 Try making multiple chains for your synth and panning them left and right to create stereo synths.
A play line starts with the wordplay, 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).
rhythm accepts a list of rhythm values or a function that returns rhythm values, whilenotes 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.
A simple synth:
@synth (adsr (osc sine) 64n 8n 0 8n)play @synth (rhythm [8n]) (notes [e3 e4 e5])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.
@synth (adsr (osc tri) 64n 8n 0 8n)play @synth (rhythm [8n]) (notes (chord lydian e3))Taking it one step further, let's put thatchord function call within therandom function, which will randomly pick one of the notes from the chord each time it's called.
@synth (adsr (osc tri) 64n 8n 0 8n)play @synth(rhythm [8n])(notes (random (chord lydian e3)))Theflatten andrepeat functions, when used inside ofnotes, are a powerful way to create repeating phrases. Sincenotes only takes a single list we use theflatten function to take a few different calls and flatten them down. Therepeat function will take the list we give it and repeat it a number of times, saving us some copying & pasting.
@synth (adsr (osc sine) 64n 8n 0 8n)play @synth(rhythm [8n])(notes (flatten [(repeat 3 (chord lydian e4 4))(chord lydian d4 4)]))💡 Try making multiple play lines for the same sound to make polyphonic melodies and drum beats.
Musical notes and rhythm values are a core concept of Slang, so let's look at them in a bit more detail.
Note values may look familiar to you if you've ever learned an instrument. They contain both a note (likec,f#, ora) and a number that represents theoctave. If you imagine a large piano,c4 is the C key right smack in the middle of it.c3 will be the C key one octave down from that,c5 will be one octave up, etc. If you're unfamiliar with how the notes map to a keyboard,here is a handy image.
Note values can also be expressed as numbers. If you've worked with synthesizers or electronic instruments before you might be familiar with the MIDI protocol, which among other things represents all notes on a keyboard from0 -127.c4 is equivalent to the MIDI number64, and to move up or down an octave you can add or subtract12.
Here are the notes in the fourth octave with their MIDI number equivalents:
Notes: c4 c#4 d4 d#4 e4 f4 f#4 g4 g#4 a4 a#4 b4 c5Numbers: 64 65 66 67 68 69 70 71 72 73 74 75 76Rhythm values describe thelength of the note as a fraction of ameasure. The longest possible note in Slang is1n, which in music notation would be referred to as awhole note.
Here are all of the values and how they line up, from slowest to fastest:
1n- whole note (the longest note)2n- half note (half of a whole note)2t- half note triplet (3 of these is equal to1n)4n- quarter note (a quarter of a whole note)4t- quarter note triplet (3 of these is equal to2n)8n- eighth note (1/8 of a whole note)8t- eighth note triplet (3 of these is equal to4n)16n- sixteenth note (1/16 of a whole note)16t- sixteenth triplet (3 of these is equal to8n)32n- thirty-second note (1/32 of a whole note)32t- thirty-second triplet (3 of these is equal to16n)64n- sixty-fourth note (1/64 of a whole note)64t- sixty-fourth triplet (3 of these is equal to32n)
When creating a rhythm in Slang you can freely mix and match these values. A good rule of thumb is that you should aim for all of your rhythm values to add up to1n or multiples of1n; for example4n 4n 4n 4n,4n 8n 8n 2n, and4n 16t 16t 16t 8n 4t 4t 4t all add up to1n.
Sometimes you'll want to pause for a beat without playing a note. This is called arest in music terminology. Addingr in front of any rhythm value will turn it into a rest (and it will appear lighter in color within the Slang editor); for example4n 4n 4n r4n will play three quarter notes and then rest for the length of one quarter note.
In addition to the rhythm notation you can also usenumber values, which correspond toseconds. Slang runs at a tempo of 120 beats per minute, which means that a whole note —1n — is exactly2 seconds long. Writing(rhythm [2]) and(rhythm [1n]) produce exactly the same rhythm. This is useful in other functions like(adsr) where the attack, decay, and release all accept a rhythm value.
Functions are contained within parentheses, much like in Clojure. The first keyword in a function is thefunction 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.
In Slang every argument can be either a static value (such as8n,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.
Creates an oscillator with an optional pitchOffset in semitones. Filters and effects can be chained off of the oscillator using the+ sign.
type:
sinesaworsawtoothtriortrianglesquare
pitchOffset: how many semitones to shift the pitch.
Usage:
# Creates a synth with two sine oscillators, one pitched 7 semitones above the root note@synth (osc sine)@synth (osc sine 7)# Creates a synth that chooses a random oscillator for each note that is hit.@melody (osc (random [sine saw tri square]))Creates a drum machine. It does not accept any arguments.
When writing a play line, the notes 0 - 11 represent the 12 drum sounds.
Pro tip: Any number above 11 will wrap around using modulus, so for example 25 will trigger sound 1 since25 % 12 == 1. This allows you to pass in note values (e.g.e3) as well since they correspond to number values.
Creates an amp envelope which contains an oscillator followed by ADSR values. If you're unfamiliar with the concept of an amp envelope, check out theTypical Stages section ofthis tutorial. Amp envelopes control thevolume of a sound over the course of a single note.
Since amp envelopes contain oscillators they can kick off a chain of sound.
osc: An oscillator function, e.g.(osc tri)
attack: A rhythm value or number in seconds corresponding to how long the sound takes to fade in
decay: A rhythm value or number in seconds corresponding to how long the sound takes to descend to the sustain value
sustain: A value from0 -1 describing how loud the note should be while it is sustained.
release: A rhythm value or number in seconds corresponding to how long the sound takes to fade from its sustain value down to0.
Usage:
# Try each of these envelopes one at a time to get a feel for what ADSR does.@synth (adsr (osc sine) 8n 8n 1 4n)# @synth (adsr (osc sine) 0 0 1 0)# @synth (adsr (osc sine) 4n 0 1 2n)# @synth (adsr (osc sine) 16n 16n 0.2 8n)play @synth (rhythm [4n]) (notes [c4 d4 e4 f4 g4 a4 b4 c5])Creates a filter. This should be part of a sound chain.
type:
lp(lowpass)hp(highpass)bp(bandpass)n(notch)
frequency: A value from 0 - 127 representing the frequencies 0 - 11,025.
resonance: A number from 0 - 100 representing the amount of resonance (Q) to apply.
Usage:
@synth (osc sine) + (filter lp 20)# Make a lowpass filter that loops through the# numbers 10 to 50 one at a time.@melody (osc saw) + (filter lp [10..50])Creates a gain (volume). This should be part of a sound chain.
value: A number from 0 - 1.
Usage:
@synth (osc sine) + (gain 0.5)@melody (osc sine) + (gain [0 0.25 0.5 0.75 1])Creates a stereo panner. This should be part of a sound chain.
value: A number from -1 (left) to 0 (center) to 1 (right).
Usage:
@synth (osc sine) + (pan -1)@synth (osc sine 12) + (pan 1)Creates a delay effect. This should be part of a sound chain.
Warning: delay doesn't work very well right now and might cause some weird audio artifacts!
time: A rhythm value or a number in seconds.
feedback: A number from 0 - 1 representing the amount of feedback to apply.
wet: A number from 0 - 1 representing the wet level.
dry: A number from 0 - 1 representing the dry level.
cutoff: A number from 0 - 11025 representing the frequency of a cutoff filter on the delay.
Usage:
@synth (adsr (osc saw) 64n 8n 0 8n) + (delay 8t 0.4 1 1)Creates
Returns a list of notes belonging to a chord.
type: A text value representing a chord type, e.g.major,bebop,phrygian. The list of possible chords is taken fromthis library, but with spaces and# symbols removed (e.g.minor #7M pentatonic becomesminor7Mpentatonic in Slang).
root: A note, e.g.e3.
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.
Usage:
@synth (adsr (osc sine) 64n)play @synth (notes (chord phrygian e3))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 aschord orflatten.
To get repeating random values, check outshuffle below.
Usage:
@synth (adsr (osc (random [saw tri])) 64n)+ (filter lp [10..50])play @synth(rhythm (random [8n 8t 4n]))(notes (random (chord phrygian e3)))Takes a list of lists and flattens it.
Usage:
@synth (adsr (osc sine) 64n)play @synth (notes (flatten [[e3..e4] [d#4..e#3]]))@synth (adsr (osc sine) 64n)play @synth (notes (flatten [(repeat 2 [e3 e4 e5])(repeat 2 [d3 d4 d5])(repeat 2 [a3 a4 a5])[g3 g4 g5][f3 f4 f5]]))Takes a list and repeats itamount times. Useful when used inside offlatten.
Usage:
@perc (drums)play @perc (notes (flatten [(repeat 2 [0 6 3 6])(repeat 2 [6 0 3 6])]))Reverses the list.
Usage:
@synth (adsr (osc sine) 64n) + (gain 0.5)play @synth (notes (reverse (chord lydian e4)))play @synth (notes (chord lydian e5))Does a one-time random shuffle of the list. Use this if you want a random but repeating sequence, and userandom if you want a random value each time the function is triggered.
Usage:
@bass (adsr (osc tri) 64n)play @bass (notes (shuffle (chord phrygian e3)))Transpose a list of numbers or notes by an amount.
amount: number
Usage
@synth (adsr (osc tri) 64n)play @synth (notes (flatten [(chord phrygian e3)(transpose 2 (chord phrygian e3))]))Generate a list that interpolates from the start to the end value over a number of steps. Useful for creating values that transition slowly over time, especially for tools likepan andgain.
start: number
end: number
steps: number
Usage:
@synth (adsr (osc tri) 64n)+ (pan (lerp -1 1 16))play @synth (notes (chord major d4 16))Primitive values:
- numbers - integers and floats (
0,0.25,10000, etc.) - lists (space-separated) -
[0 1 2 3 4 5 6] - notes -
e3,d#4,f2, etc. - rhythm -
32t,32n,16t,16n,8t,8n,4t,4n,2n,2t, and1n - rests -
r32t,r32n,r16t,r16n,r8t,r8n,r4t,r4n,r2n, andr1n - special strings - some functions take string arguments, such as
filterandosc
A simple synthesizer
# This is a sound line that establishes a synthesizer called @melody@melody (adsr (osc sine) 64n 8n 0)# This is a play line that plays @melody using a rhythm and a list of notesplay @melody (rhythm [8n]) (notes [e3 d3 g3 f3])A drum machine
# Drums don't accept any arguments (at the moment!)@percussion (drums)# Hi-hatsplay @percussion (rhythm [16n r16n 16n 16n]) (notes [6 7 8])# Kick and snareplay @percussion (rhythm [8n]) (notes [0 3 11 0 3 0 3 11])A randomized synth & bassline with drums
@synth (adsr (osc saw) 64n)+ (filter lp (random [5..30]))+ (gain 0.2)+ (pan -0.75)@synth (adsr (osc square 12) 64n)+ (filter lp 15)+ (gain 0.15)+ (pan 0.75)@bass (adsr (osc tri) 64n 2n 0.4 4n)@drums (drums) + (gain 2)play @synth(rhythm [8t])(notes (random (chord phrygian e5)))play @bass(rhythm [1n])(notes (random (chord phrygian e2)))play @drums(rhythm [8t r8t 8t 8t 8t r8t])(notes [6])play @drums(rhythm [4n 4n 4n r8t 8t 8t])(notes [0 3 0 11 11])Weird and complex little scene (I think this is in 18/8 + 17/8 ??)
@synth (adsr (osc tri) 0.01 8n 0.2 1n)+ (filter lp (flatten [[5..30] [29..5]]) 10)@synth (adsr (osc square 12) 0.01 8t 0.2 4n)+ (filter lp (flatten [[0..25] [24..0]]))@synth (adsr (osc square 7) 0 8n 0 0)+ (filter lp (random [10..25]))+ (gain 0.2)@pad (adsr (osc tri) 4n 4n 0.5 1n)+ (filter lp 10)+ (gain 0.5)@pad (adsr (osc square [7 5]) 4n 4n 0.5 1n)+ (filter lp 5)+ (gain 0.5)@pad (adsr (osc saw [7 5 7 9]) 1n 4n 0.5 1n)+ (filter hp 10)+ (filter lp 100)+ (gain 0.1)@drums (drums)play @synth(rhythm [8t r8n 4n r8n 8t r8n 8t r8n])(notes (flatten [[[e2 g2] [d2 f2]](repeat 3 (chord locrian e4 3))(reverse (chord egyptian e4 3))]))play @pad(notes [d4])(rhythm [1n r1n r1n r4n r4n])play @drums(rhythm [8t 8n 4n 8n 8t 8n 8t 8n])(notes [7 6])play @drums(rhythm [8t 8n 4n r4n r8n r8t r8t])(notes [4 3 1])About
🎤 a simple audio programming language implemented in JS
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Releases
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors4
Uh oh!
There was an error while loading.Please reload this page.

