# Welcome!
This is the documentation for klank.dev. If you're already familiar with PureScript or Haskell, the recommendation is to check out examples from this article, copy and paste them into klank.dev, compile them and play them to hear how they sound. Try to use Firefox for all playback, as it has the best audio quality. If you'd like to share what you're making with the PureScript community, ask any questions or report any issues in the underlying libraries, please visit the PureScript discourse.
This introductory page is divided into three sections. The first gives general workflow recommendations. The second explains the API for klank.dev. The third gives some helpful links on how to code in PureScript and how to work with Functional Reactive Programming.
For a tour of all available audio units (ie sinOsc
, playBuf
etc) please visit Audio Units.
# Workflow
The basic cycle for klank.dev is to tweak some code, then compile, then play. To do this, you'll want to be comfortable with purescript-audio-behaviors
. The README has a gentle introduction that builds up a scene item by item. All of the scenes in the README can be copied into klank.dev. purescript-audio-behaviors
exposes most of the audio units available in the WebAudio API (ie highpass
, convolver
, dynamicsCompressor
etc), and the relevant functions are in Audio.purs. Many of them are used in this example as part of a regression test suite.
For larger projects, a good workflow is to use an industrial editor like VSCode and then copy-and-paste into the browser. As an example, PureScript has amazing language support for VSCode. If you'd like to use this flow for editing klanks, you can clone this github repo, which has a spago.dhall
and packages.dhall
file that is up-to-date with all the modules klank can access. If you need more PS modules in that package set, please file an issue on this discourse.
# Klank API
At a minimum, every klank must have a main
record with type Klank
or Klank' accumulator
. The default klank that ships with klank.dev provides an example of this. There are plenty of example klanks on this discourse, but if you want a minimum-viable-klank to see this in action, paste the following into klank.dev, compile it, and play it 🎧
module Klank.Dev where
import Prelude
import Data.Typelevel.Num (D1)
import FRP.Behavior (Behavior)
import FRP.Behavior.Audio (AudioUnit, gain', runInBrowser, sinOsc, speaker')
import Type.Klank.Dev (Klank, klank)
scene :: Number -> Behavior (AudioUnit D1)
scene _ = pure (speaker' (gain' 0.2 (sinOsc 440.0)))
main :: Klank
main =
klank
{ run = runInBrowser scene
}
Here is a slightly more involved klank. While it's playing, try clicking the 🐭
module Klank.Dev where
import Prelude
import Data.Set (isEmpty)
import Data.Typelevel.Num (D1)
import FRP.Behavior (Behavior)
import FRP.Behavior.Audio (AudioUnit, gain', runInBrowser_, sinOsc, speaker')
import FRP.Behavior.Mouse (buttons)
import FRP.Event.Mouse (Mouse, getMouse)
import Type.Klank.Dev (Klank, klank)
scene :: Mouse -> Number -> Behavior (AudioUnit D1)
scene mouse = const $ f <$> click
where
f cl = speaker' (gain' 0.1 $ sinOsc (if cl then 440.0 else 330.0))
click :: Behavior Boolean
click = map (not <<< isEmpty) $ buttons mouse
main :: Klank
main =
klank
{ run =
runInBrowser_
( do
mouse <- getMouse
pure $ scene mouse
)
}
If you're using any other browser than Firefox or Edge, you're probably hearing crackling right now. That's because klank pounds the WebAudio API pretty hard. So try to use Firefox or Edge!
# Command line
The bottom third of the screen contains the klank command line. Type h
to get a list of currently available commands. To compile the current scene, press k
then ENTER. To play the current scene, press p
then ENTER. To stop the current scene, press s
then ENTER. There are additional commands to reveal the canvas, reveal a split view editor/canvas, and create a link to your klank.
# Enabling the microphone
If you need the microphone, enable the microphone by setting enableMicrophone
to true
. In this example, we'll use speaker
, which takes a non-empty list of audio units, instead of speaker'
, because the syntax highlighting is prettier 🤗
module Klank.Dev where
import Prelude
import Data.List (List(..))
import Data.NonEmpty ((:|))
import Data.Typelevel.Num (D1)
import FRP.Behavior (Behavior)
import FRP.Behavior.Audio (AudioUnit, microphone, runInBrowser, speaker)
import Type.Klank.Dev (Klank, klank)
scene :: Number -> Behavior (AudioUnit D1)
scene = const $ pure (speaker (microphone :| Nil))
main :: Klank
main =
klank
{ enableMicrophone = true
, run = runInBrowser scene
}
# Playing an audio track
Playing back audio can be done by creating a tracks
object with the signature (Object BrowserAudioTrack -> Effect Unit) -> (Error -> Effect Unit) -> Effect Unit
. The first function acts like resolve
in a JS Promise and should be called if everything goes well, otherwise call the error function.
Make sure that the tracks you're streaming have CORS enabled! Everything on freesound.org does, so let's use something from there in this example. We'll add a "sine wave siren" on top of some killer beats.
module Klank.Dev where
import Prelude
import Data.List ((:), List(..))
import Data.NonEmpty ((:|))
import Data.Typelevel.Num (D1)
import Effect.Class (liftEffect)
import FRP.Behavior (Behavior)
import FRP.Behavior.Audio (AudioUnit, gain', makeAudioTrack, play, runInBrowser, sinOsc, speaker)
import Foreign.Object as O
import Math (pi, sin)
import Type.Klank.Dev (Klank, Tracks, affable, klank)
scene :: Number -> Behavior (AudioUnit D1)
scene time =
let
rad = pi * time
in
pure
$ speaker
( (gain' 0.1 $ sinOsc (440.0 + (10.0 * sin (2.3 * rad))))
:| (gain' 0.25 $ sinOsc (235.0 + (10.0 * sin (1.7 * rad))))
: (gain' 0.2 $ sinOsc (337.0 + (10.0 * sin rad)))
: (gain' 0.1 $ sinOsc (530.0 + (19.0 * (5.0 * sin rad))))
: (play "forest")
: Nil
)
tracks :: Tracks
tracks _ =
affable do
forest <- liftEffect $ makeAudioTrack "https://freesound.org/data/previews/458/458087_8462944-lq.mp3"
pure $ O.singleton "forest" forest
main :: Klank
main =
klank
{ tracks = tracks
, run = runInBrowser scene
}
# Making a periodic wave
WebAudio exposes a periodic wave interface for building complex oscillators. You can roll your own periodic waves in klank.dev using the periodicWave
toplevel assignment with the signature of AudioContext -> Aff (Object BrowserPeriodicWave)
. As with audio tracks, periodic waves are referred to by name.
module Klank.Dev where
import Prelude
import Data.Typelevel.Num (D1)
import Data.Vec ((+>), empty)
import Effect.Class (liftEffect)
import FRP.Behavior (Behavior)
import FRP.Behavior.Audio (AudioUnit, gain', makePeriodicWave, periodicOsc, runInBrowser, speaker')
import Foreign.Object as O
import Math (pi, sin)
import Type.Klank.Dev (Klank, PeriodicWaves, affable, klank)
scene :: Number -> Behavior (AudioUnit D1)
scene time =
let
rad = pi * time
in
pure $ speaker' (gain' 0.2 (periodicOsc "smooth" (124.0 + (2.0 * sin (0.2 * rad)))))
periodicWaves :: PeriodicWaves
periodicWaves ctx _ =
affable do
pw <-
liftEffect
$ makePeriodicWave ctx
(0.5 +> 0.25 +> 0.1 +> empty)
(0.2 +> 0.1 +> 0.01 +> empty)
pure $ O.singleton "smooth" pw
main :: Klank
main =
klank
{ periodicWaves = periodicWaves
, run = runInBrowser scene
}
# Using buffers
Here's an example of how to loop a vinyl-scratching sound from freesound.org using loopBuf
.
module Klank.Dev where
import Prelude
import Control.Promise (toAffE)
import Data.Traversable (sequence)
import Data.Typelevel.Num (D1)
import FRP.Behavior (Behavior)
import FRP.Behavior.Audio (AudioUnit, decodeAudioDataFromUri, gain', loopBuf, runInBrowser, speaker')
import Foreign.Object as O
import Math (pi, sin)
import Type.Klank.Dev (Klank, Buffers, affable, klank)
scene :: Number -> Behavior (AudioUnit D1)
scene time =
let
rad = pi * time
in
pure $ speaker' (gain' 0.2 (loopBuf "scratch" (2.0 + sin rad) 0.0 0.0))
buffers :: Buffers
buffers ctx _ =
affable
$ sequence
( O.singleton "scratch"
$ toAffE (decodeAudioDataFromUri ctx "https://freesound.org/data/previews/71/71853_1062668-lq.mp3")
)
main :: Klank
main =
klank
{ run = runInBrowser scene
, buffers = buffers
}
# Making float arrays
If your node needs a float array (ie the waveShaper
node), you can use the floatArray
toplevel object.
module Klank.Dev where
import Prelude
import Data.Array (range)
import Data.Int (toNumber)
import Data.Typelevel.Num (D1)
import Effect.Class (liftEffect)
import FRP.Behavior (Behavior)
import FRP.Behavior.Audio (AudioUnit, Oversample(..), gain', makeAudioTrack, makeFloatArray, play, runInBrowser, speaker', waveShaper)
import Foreign.Object as O
import Math (pi, abs)
import Type.Klank.Dev (Klank, affable, klank)
makeDistortionCurve :: Number -> Array Number
makeDistortionCurve k =
map
( \i ->
let
x = (toNumber i * 2.0 / toNumber n_samples) - 1.0
in
(3.0 + k) * x * 20.0 * deg / (pi + (k * abs x))
)
(range 0 $ n_samples - 1)
where
n_samples = 44100
deg = pi / 180.0
scene :: Number -> Behavior (AudioUnit D1)
scene = const $ pure (speaker' (gain' 0.2 $ waveShaper "wicked" FourX (play "forest")))
main :: Klank
main =
klank
{ floatArrays =
const
$ affable
( do
wicked <- liftEffect $ makeFloatArray (makeDistortionCurve 400.0)
pure $ O.singleton "wicked" wicked
)
, tracks =
const
$ affable
( do
forest <- liftEffect $ makeAudioTrack "https://freesound.org/data/previews/458/458087_8462944-lq.mp3"
pure $ O.singleton "forest" forest
)
, run = runInBrowser scene
}
# Accumulator
For scenes that take an accumulator, you need to provide an initial value via the accumulator
property. Sometimes the klank will play without an initial value if it is nullish, but that's not a documented feature and may disappear in later versions.
Here's an example (try clicking the mouse):
module Klank.Dev where
import Prelude
import Color (rgb)
import Data.List (List(..))
import Data.Maybe (Maybe(..), maybe)
import Data.NonEmpty ((:|))
import Data.Set (isEmpty)
import Data.Tuple (Tuple(..))
import Data.Typelevel.Num (D2)
import Data.Vec ((+>), empty)
import FRP.Behavior (Behavior)
import FRP.Behavior.Audio (AV(..), CanvasInfo(..), dup1, gain', merger, panner, runInBrowser_, sinOsc, speaker)
import FRP.Behavior.Mouse (buttons)
import FRP.Event.Mouse (Mouse, getMouse)
import Graphics.Drawing (circle, fillColor, filled)
import Math (pi, sin)
import Type.Klank.Dev (Klank', affable, klank)
scene ::
Mouse ->
{ onset :: Maybe Number } ->
CanvasInfo ->
Number ->
Behavior (AV D2 { onset :: Maybe Number })
scene mouse acc@{ onset } (CanvasInfo { w, h }) time = f time <$> click
where
f s cl =
AV
( Just
$ dup1
( (gain' 0.1 $ sinOsc (110.0 + (3.0 * sin (0.5 * rad))))
+ ( gain' 0.1
$ sinOsc
( 220.0
+ ( if cl then
( 50.0
+ maybe 0.0
(\t -> 10.0 * (s - t))
stTime
)
else
0.0
)
)
)
) \mono ->
speaker
$ ( (panner (-0.5) (merger (mono +> mono +> empty)))
:| Nil
)
)
( Just
$ filled
(fillColor (rgb 0 0 0))
(circle (w / 2.0) (h / 2.0) (if cl then 25.0 else 5.0))
)
(acc { onset = stTime })
where
rad = pi * s
stTime = case Tuple onset cl of
(Tuple Nothing true) -> Just s
(Tuple (Just y) true) -> Just y
(Tuple _ false) -> Nothing
click :: Behavior Boolean
click = map (not <<< isEmpty) $ buttons mouse
main :: Klank' { onset :: Maybe Number }
main =
klank
{ accumulator = affable $ pure { onset: Nothing }
, run =
runInBrowser_
( do
mouse <- getMouse
pure (scene mouse)
)
}
# Loading a custom audio worklet
klank.dev can load arbitrary audio units for use with audioWorkletGenerator
and audioWorkletProcessor
from purescript-audio-behaviors
. A small-but-growing library of custom audio worklets lives at https://klank.dev/w/v0/:
- https://klank.dev/w/v0/white-noise.js
(so yeah, pretty small, but growing!)
When using your own audio processor, make sure that the JS file in question has CORS enabled!
module Klank.Dev where
import Prelude
import Data.Typelevel.Num (D1)
import FRP.Behavior (Behavior)
import FRP.Behavior.Audio (AudioUnit, audioWorkletGenerator, runInBrowser, speaker')
import Foreign.Object as O
import Math (pi, sin)
import Type.Klank.Dev (Klank, affable, klank)
scene :: Number -> Behavior (AudioUnit D1)
scene time =
pure
$ speaker'
( audioWorkletGenerator
"klank-white-noise"
(O.singleton "gain" $ 0.05 + (0.05 * sin (0.1 * rad)))
)
where
rad = time * pi
main :: Klank
main =
klank
{ worklets = const $ affable (pure [ "https://klank.dev/w/v0/white-noise.js" ])
, run = runInBrowser scene
}
# Caching
By default, klank does not cache any objects created using buffers
, periodicWaves
etc. That means that, every time you press play, these objects are created anew. For small sessions, this doesn't take much time, but for larger sessions, it can take a few seconds.
To avoid this, you can use the cache parameter that contains the previous retrieved values. For example, below, we serve back cached buffers if they exist, otherwise we fetch them.
module Klank.Dev where
import Prelude
import Control.Promise (toAffE)
import Data.Traversable (sequence)
import Data.Typelevel.Num (D1)
import FRP.Behavior (Behavior)
import FRP.Behavior.Audio (AudioUnit, decodeAudioDataFromUri, gain', loopBuf, runInBrowser, speaker')
import Foreign.Object as O
import Math (pi, sin)
import Type.Klank.Dev (Buffers, Klank, affable, klank)
scene :: Number -> Behavior (AudioUnit D1)
scene time =
let
rad = pi * time
in
pure $ speaker' (gain' 0.2 (loopBuf "scratch" (2.0 + sin rad) 0.0 0.0))
buffers :: Buffers
buffers ctx prev =
affable
( if O.size prev == 1 then
pure prev
else
sequence
( O.singleton "scratch"
$ toAffE (decodeAudioDataFromUri ctx "https://freesound.org/data/previews/71/71853_1062668-lq.mp3")
)
)
main :: Klank
main =
klank
{ buffers = buffers
, run = runInBrowser scene
}
# Dynamic configuration
The klank.dev URL supports certain query parameters for dynamic configuration.
k
: compile on loadc
: canvas modeec
: split editor/canvas modenoterm
: no terminal mode. Hide the terminal and opens a full-canvas view. Useful for interactive instruments that need to "just work" out of the box.acc={"foo":1.0,"bar":-3.1416}
: An initial value for the accumulator. ONLY works for accumulators that are pure JSON. This is useful in conjunction withnoterm
to initialize the instrument in a particular state.url=<path>
: Load a document in the editor from a url.b64=<string>
Load a document in the editor from a base-64 encoded string.noterm
Load a document in no-terminal mode. This hides the terminal, precompiles the klank, and has only a play button. This means that any stop action needs to be part of the FRP!
For example, https://klank.dev/?ec&k will open up in spit editor/canvas mode and compile on load.
# Type safety 💪
As a general rule of thumb, klanks should use the Klank
type to enforce type safety of main
. If main does not have a type annotation, you're making a bet with the compiler that your program will run, and that's a risky bet!
To add type safety to your klank, you can use the Type.Klank.Dev
package. It provides these features:
- The ability to get rid of that pesky
res rej
syntax and instead just use affs via theaffable
and, for records,affableRec
functions. - The ability to use an environment to enforce type safety for
play
(usetPlay
),playBuf
(usetPlayBuf
),waveShaper
(usetWaveShaper
) and any otherAudioUnit
that takes a string pointing to an opaque object like an audio track.
Here's an example of how to use it.
module Klank.Dev where
import Prelude
import Data.Array (range)
import Data.Int (toNumber)
import Data.Symbol (SProxy(..))
import Data.Typelevel.Num (D1)
import Effect.Aff (Aff)
import Effect.Class (liftEffect)
import FRP.Behavior (Behavior)
import FRP.Behavior.Audio (AudioUnit, BrowserAudioTrack, BrowserFloatArray, Oversample(..), gain', makeAudioTrack, makeFloatArray, runInBrowser, speaker')
import Math (pi, abs)
import Type.Data.Row (RProxy(..))
import Type.Klank.Dev (FloatArrays, Klank, PlaySignature, Tracks, WaveShaperSignature, affableRec, klank, tPlay, tWaveShaper)
makeDistortionCurve :: Number -> Array Number
makeDistortionCurve k =
map
( \i ->
let
x = (toNumber i * 2.0 / toNumber n_samples) - 1.0
in
(3.0 + k) * x * 20.0 * deg / (pi + (k * abs x))
)
(range 0 $ n_samples - 1)
where
n_samples = 44100
deg = pi / 180.0
myWaveShaper :: WaveShaperSignature MyFloatArrays
myWaveShaper = tWaveShaper (RProxy :: RProxy MyFloatArrays)
myPlay :: PlaySignature MyTracks
myPlay = tPlay (RProxy :: RProxy MyTracks)
scene :: Number -> Behavior (AudioUnit D1)
scene =
const
$ pure
( speaker'
( gain' 0.2
$ myWaveShaper
(SProxy :: SProxy "wicked")
FourX
( myPlay
(SProxy :: SProxy "forest")
)
)
)
type MyFloatArrays
= ( wicked :: BrowserFloatArray
)
type MyTracks
= ( forest :: BrowserAudioTrack
)
type MyEnv
= ( floatArrays :: Aff (Record MyFloatArrays)
, tracks :: Aff (Record MyTracks)
)
env =
{ floatArrays:
do
wicked <- liftEffect $ makeFloatArray (makeDistortionCurve 400.0)
pure { wicked }
, tracks:
do
forest <- liftEffect $ makeAudioTrack "https://freesound.org/data/previews/458/458087_8462944-lq.mp3"
pure { forest }
} ::
(Record MyEnv)
floatArrays :: FloatArrays
floatArrays _ = affableRec env.floatArrays
tracks :: Tracks
tracks _ = affableRec env.tracks
main :: Klank
main =
klank
{ floatArrays = floatArrays
, tracks = tracks
, run = runInBrowser scene
}
# PureScript and FRP
PureScript is a purely functional language that is inspired by and resembles Haskell. Here are some great resources to get up and running with PureScript.
- PureScript by Example
- Pursuit - Documentation for PureScript modules
- Learn PureScript in Y minutes
- The PureScript Discourse Instance
You can also ask questions in this forum about PureScript.
Functional Reactive Programming (FRP for short) is a pattern for treating streams of values. klank.dev makes heavy use of two FRP libraries:
The documentation for PureScript Audio Behaviors has a short introduction to the concept of a scene
.