Varying Modulation

This post shows how to use the varying library to create modulations. The goal is to define a value that varies over time, for example to react smoothly to an external event.

Varying Modulation Plot

This document is a literate haskell file


-- Extensions to create a newtype:
{-# LANGUAGE DerivingStrategies, GeneralizedNewtypeDeriving #-}
-- Quality of life syntaxic sugar
{-# LANGUAGE ImportQualifiedPost, NamedFieldPuns #-}

import System.Environment (withArgs)
import Control.Concurrent.STM (STM, TVar, atomically, newTVar, readTVar, writeTVar)
import Control.Monad (forM_, forever)

-- https://hackage.haskell.org/package/varying
import Control.Varying.Core qualified as Varying
import Control.Varying.Tween qualified as Varying

-- For the final plot, using https://diagrams.github.io
import Diagrams.Prelude (V2, (&~))
import Diagrams.Backend.SVG.CmdLine (mainWith, B)
-- and https://hackage.haskell.org/package/plots
import Plots (Axis, r2Axis, linePlot, key)

A varying VarT is defined by a time and value type. In this example we’ll use a new type for milli seconds:

newtype MSec = MSec Float
    deriving newtype (Num, Show, Real, Ord, Eq, Fractional, Enum)

-- Simplify the VarT definition
type VaryingVar = Varying.VarT STM MSec Float

And we’ll use this data type to represent a modulation:

data Modulation = Modulation
  { name :: String
  , source :: TVar VaryingVar
  , lastPoll :: TVar MSec
  }

So here is an example sin modulation, where the VaryingVar is implemented manually:

modSIN :: STM Modulation
modSIN = do
  lastPoll <- newTVar 0
  let sinVar = Varying.VarT $ \(MSec dt) -> do
        MSec lastDt <- readTVar lastPoll
        pure (sin ((lastDt + dt) / 1000), sinVar)
  source <- newTVar sinVar
  pure $ Modulation "sin" source lastPoll

The varying library enables defining Tween to generate intermediate samples, which is great for dynamic animation:

-- Simplify the Tween definition
type Tween = Varying.TweenT MSec Float STM Float

Tweens let us describe more complex modulation using the do notation, here is an example of an attack/decay envelop:

tweenAD :: Float -> MSec -> MSec -> Tween
tweenAD velocity attack decay = do
    -- raise to velocity value during the attack
    x <- Varying.tween Varying.easeInExpo 0 velocity attack
    -- then slowly go back to 0 during the decay
    Varying.tween Varying.easeOutCubic x 0 decay

And we use an helper function to create a ‘Modulation’ from a ‘Tween’, with a resting value of 0.

newTweenModulation :: String -> Tween -> STM Modulation
newTweenModulation name tween =
   Modulation name <$> newTVar var <*> newTVar 0
 where
   var = Varying.tweenStream tween 0

Which we can use to create the envelop modulation, here with 500ms attack and 1sec decay:

modAD :: STM Modulation
modAD = newTweenModulation "adEnv" (tweenAD 1 500 1000)

Here is another example of a periodic modulation:

modPulse :: STM Modulation
modPulse = newTweenModulation "pulse" $ forever $ do
    -- raise to 0.5 in 300ms
    Varying.tween_ Varying.easeOutExpo 0 0.5 300
    -- lower to 0 in 600ms
    Varying.tween_ Varying.easeOutExpo 0.5 0 600

Now, given a modulation and the current time, we can compute it’s value:

pollModulation :: MSec -> Modulation -> STM Float
pollModulation now Modulation{source, lastPoll} = do
  src <- readTVar source
  pre <- readTVar lastPoll
  (value, newSource) <- Varying.runVarT src (now - pre)
  writeTVar lastPoll now
  writeTVar source newSource
  pure value

Finally, the diagram rendering code:

-- Sample a modulation
runModulation :: Int -> Modulation -> STM [(Float, Float)]
runModulation count modulation = reverse <$> go 0 []
  where
    step :: MSec
    step = 2000 / fromIntegral count
    go n acc
      | n >= count = pure acc
      | otherwise = do
          let now@(MSec nowF) = fromIntegral n * step
          v <- pollModulation now modulation
          go (n + 1) ((nowF, v) : acc)

-- Create the plot
mkPlotMods :: [STM Modulation] -> STM (Axis B V2 Float)
mkPlotMods mkMods = do
  mods <- sequence mkMods
  lines <- traverse (runModulation 40) mods
  pure $ r2Axis &~ do
    forM_ (zip mods lines) $ \(mod, line) -> do
      linePlot line $ key (name mod)

main :: IO ()
main = do
  plot <- atomically (mkPlotMods [modSIN, modAD, modPulse])
  withArgs ["-o", "static/varying-modulation.svg", "-w 400"] $ mainWith plot

To evaluate the file: nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/da60f2dc9c95692804fa6575fa467e659de5031b.tar.gz -p ghcid -p “haskellPackages.ghcWithPackages (p: [p.markdown-unlit p.plots p.varying p.diagrams-lib p.diagrams-svg])” –command ’ghcid –test=:main –command “ghci -pgmL markdown-unlit” varying-modulation.lhs’

Links to this page
#haskell #blog