# Welcome!

This is the documentation for klank.dev(opens new window) . If you're already familiar with PureScript(opens new window) or Haskell(opens new window) , the recommendation is to check out examples from this article(opens new window) , copy and paste them into klank.dev(opens new window) , 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(opens new window) .

This introductory page is divided into three sections. The first gives general workflow recommendations. The second explains the API for klank.dev(opens new window) . 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(opens new window) is to tweak some code, then compile, then play. To do this, you'll want to be comfortable with purescript-audio-behaviors(opens new window) . The README(opens new window) 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(opens new window) . purescript-audio-behaviors(opens new window) exposes most of the audio units available in the WebAudio API (ie highpass, convolver, dynamicsCompressor etc), and the relevant functions are in Audio.purs(opens new window) . Many of them are used in this example(opens new window) 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(opens new window) for VSCode. If you'd like to use this flow for editing klanks, you can clone this github repo(opens new window) , 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(opens new window) 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(opens new window) 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 load
  • c: canvas mode
  • ec: split editor/canvas mode
  • noterm: 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 with noterm 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(opens new window) 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(opens new window) package. It provides these features:

  • The ability to get rid of that pesky res rej syntax and instead just use affs via the affable and, for records, affableRec functions.
  • The ability to use an environment to enforce type safety for play (use tPlay), playBuf (use tPlayBuf), waveShaper (use tWaveShaper) and any other AudioUnit 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.

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(opens new window) makes heavy use of two FRP libraries:

The documentation for PureScript Audio Behaviors(opens new window) has a short introduction to the concept of a scene.