8c6794b6.github.io

Adhoc update of ugen parameters

Adhoc update of UGen parameters, with generic functions.

Requires ghc 7.0 or higher:

> {-# LANGUAGE DeriveDataTypeable, StandaloneDeriving #-}
> {-# LANGUAGE TypeSynonymInstances #-}
>
> module AdhocUpdate where
>
> import Control.Monad
> import Data.Data (Data, Typeable)
> import Data.Generics (everywhere, mkT)
> import Data.List (zipWith4)
> import Control.Concurrent (forkIO, threadDelay)
> import System.Random
>
> import Sound.SC3
> import Sound.SC3.ID
> import Data.Generics.Uniplate.Data (transform, transformM)

To play sound example, run:

| $ scsynth -u 57110

before invoking OSC sending actions.

We have a synth named s01, written with unit generator function from hsc3.

> s01 :: UGen
> s01 =
>   let o = control IR "out" 0
>       p = control KR "pan" 0
>       f = control KR "freq" 440
>       d = control KR "decay" 1
>       e = linen (impulse KR 0.1 0) 1e-2 1 d RemoveSynth
>       s = pan2 (sinOsc AR f 0 * e * 0.3) p 1
>   in  out o s

Actions to make sound with s01:

> prepare :: IO ()
> prepare = withSC3 reset
>
> play_s01a :: IO ()
> play_s01a = audition s01

Play it:

| *AdhocUpdate> parepare >> play_s01a

It makes 440 Hz sine tone, with 1 second decay time, positioning center. There are 4 control parameters in s01: out, pan, freq, and decay. What we want to do is to use different values for these parameters. It is possible to set different value by:

We can send s_new message with only those parameters which we want to update.

> play_s01b = withSC3 $ \fd -> do
>   async fd (d_recv $ synthdef "s01" s01)
>   send fd $ s_new "s01" (-1) AddToTail 1 [("freq",3300),("decay",0.3)]

Is there any other way to do this? We can pass parameters as function arguments:

> s01' o p f d =
>   let e = linen (impulse KR 0.1 0) 1e-2 1 d RemoveSynth
>       s = pan2 (sinOsc AR f 0 * e * 0.3) p 1
>   in  out o s

But in this manner, we need to specify all values.

> play_s01c = audition $ s01' 0 0 3300 0.3

Can we update specified values only, without sending synthdef, like:

> play_s01d = audition $ ups "freq" 3300 $ ups "decay" 0.3 s01

We can do this with functions in generic modules.

UGen data is defined as:

| data UGen
|   = Constant {constantValue :: Double}
|   | Control {controlOperatingRate :: Rate,
|              controlName :: String,
|              controlDefault :: Double,
|              controlTriggered :: Bool}
|   | Primitive {ugenRate :: Rate,
|                ugenName :: String,
|                ugenInputs :: [UGen],
|                ugenOutputs :: [Output],
|                ugenSpecial :: Special,
|                ugenId :: Int}
|   | Proxy {proxySource :: UGen, proxyIndex :: Int}
|   | MCE {mceProxies :: [UGen]}
|   | MRG {mrgLeft :: UGen, mrgRight :: UGen}

We use Data and Typeable instances for data types used inside UGen graph. In hsc3-0.9, UGen is not defined as instance of Data. Using StandaloneDeriving LANGUAGE pragma to avoid writing boiler plate codes.

> deriving instance Data UGen
> deriving instance Typeable UGen
> deriving instance Data Rate
> deriving instance Typeable Rate
> deriving instance Data Special
> deriving instance Typeable Special
> deriving instance Data UGenId
> deriving instance Typeable UGenId

We want to update UGen with Control constructor only, and only if specified parameter name has matched. This could be done with using everywhere from syb.

> ups :: String -> Double -> UGen -> UGen
> ups key value ug = everywhere (mkT f) ug where
>   f (Control r key' _ t) | key == key' = Control r key value t
>   f x                    = x

Play updated sound:

> play_s01e = audition . ups "freq" 3300 . ups "decay" 0.3 $ s01

Or, using transform from uniplate package.

> upu :: String -> Double -> UGen -> UGen
> upu key value ug = transform f ug where
>   f (Control r key' _ t) | key == key' = Control r key value t
>   f x                    = x

We can use in same manner as ups.

> play_s01e' = audition . upu "freq" 3300 . upu "decay" 0.3 $ s01

How's its performance? Since we need to traverse synth structure and find out whether control name matches or not, it should spend more time than raw messaging style.

Simple grain sound, 1ms decay time sine tone in message sending style:

> play_s01f x =
>   withSC3 . flip send . s_new "s01" (-1) AddToTail 1 $
>   [("freq",x),("decay",0.001)]

And in generic updating style:

> play_s01g x = audition $ upu "freq" x . upu "decay" 0.001 $ s01

Grain generator with 500 nano seconds interval.

> mkgrn k =
>   sequence_ $ map (k >=> const (threadDelay 500)) [10000,9931..0]

Message sending style:

> grn01 :: IO ()
> grn01 = mkgrn play_s01f

Generic update style:

> grn02 :: IO ()
> grn02 = mkgrn play_s01g

Note that, difference is not only coming from parameter update. In play_s01g, entire new synthdef need to be sent to server, this synthdef sending is not so quick to do in 500 nano seconds.

From performance perspective, message passing style is superior to generic updating style. This is same in SClang, described in NodeMessaging SC help file.

There's different use cases for generic update.

One is, sending variation of synthdefs. When we know that only fixed variation of parameters are used for certain synthdef, we can send multiple synthdef with different default values in advance. This approach resembles to variants concepts, shown in SynthDef SC help file.

> prepare_s01_variants :: IO ()
> prepare_s01_variants = withSC3 $ \fd ->
>   mapM_ (\(n,u) -> async fd $ d_recv $ synthdef n u)
>     [ ("s01_alpha",upu "pan" 1 $ upu "freq" 880 s01)
>     , ("s01_beta", upu "pan" (-1) $ upu "freq" 1320 s01)
>     , ("s01_gamma",upu "freq" 2640 s01) ]

Parameters in play_variants_01 are specified in message list, parameters in play_variants_02 are predefined in synth definition. When sending lots of s_new messages with fixed value variations, predefined approach will same the amount of data transfered to server, since it contains no parameter values in message body.

> play_variants_01 :: IO ()
> play_variants_01 = withSC3 $ \fd ->
>   let ms = [ [("freq",880),("pan",1)]
>            , [("freq", 1320),("pan",-1)]
>            , [("freq",2640)]
>            , [("freq", 1320),("pan",-1)]
>            , [("freq",2640)]
>            , [("freq",880),("pan",1)] ]
>   in  forM_ ms $ \m ->
>         send fd (s_new "s01" (-1) AddToTail 1 m) >> threadDelay (5*10^5)
>
> play_variants_02 :: IO ()
> play_variants_02 = withSC3 $ \fd ->
>   let ds = [ "s01_alpha", "s01_beta", "s01_gamma"
>            , "s01_beta", "s01_gamma", "s01_alpha" ]
>   in  forM_ ds $ \d ->
>         send fd (s_new d (-1) AddToTail 1 []) >> threadDelay (5*10^5)

Another usage of generic update approach is, UGen replacing.

There are couple oscillator functions with type 'Rate -> UGen -> UGen -> UGen':

| sinOsc :: Rate -> UGen -> UGen -> UGen
| lfCub :: Rate -> UGen -> UGen -> UGen
| lfPar :: Rate -> UGen -> UGen -> UGen
| lfSaw :: Rate -> UGen -> UGen -> UGen
| lfTri :: Rate -> UGen -> UGen -> UGen

We can write a function taking these oscillator:

> mks02 o =
>  let s = pan2 (o AR f 0 * e * 0.3) p 1
>      e = envGen KR 1 1 0 1 RemoveSynth (envPerc 1e-2 1)
>      f = control KR "freq" 440
>      p = control KR "pan" 0
>  in  out 0 s

and play it:

> play_s02_c = audition $ mks02 lfCub
> play_s02_t = audition $ mks02 lfTri
> play_s02_s = audition $ mks02 lfSaw

We can do similar thing without passing function as an argument. UGen functions sinOsc, lfCub, lfPar, lfSaw, lfTri are defined with Primitive constructor:

| *AdhocUpdate> sinOsc AR 440 0
| Primitive { ugenRate = AR
|           , ugenName = "SinOsc"
|           , ugenInputs = [Constant {constantValue = 4400.0}
|                          ,Constant {constantValue = 0.0}]
|           , ugenOutputs = [AR], ugenSpecial = Special 0, ugenId = -1}

A function to update Primitive:

> primu :: String -> String -> UGen -> UGen
> primu from to ug = transform f ug where
>   f p@(Primitive r n is os s idx) | n == from = p {ugenName = to}
>   f x = x

Using primu, we can update sinOsc used in s01 to different oscillator.

> set_osc_s01 :: String -> UGen
> set_osc_s01 = flip (primu "SinOsc") s01
>
> play_s01_cub, play_s01_tri, play_s01_saw :: IO ()
> play_s01_cub = audition $ set_osc_s01 "LFCub"
> play_s01_tri = audition $ set_osc_s01 "LFTri"
> play_s01_saw = audition $ set_osc_s01 "LFSaw"

By using String to specify UGen name, we loosed type safety. We can have chance to send a UGen graph that does not work.

> s01_ng1 = set_osc_s01 "NoSuchUnitGenerator"

Invoking audition s01_ng1 will show an error message in scsynth:

exception in GraphDef_Recv: UGen 'NoSuchUnitGenerator' not installed.

There's another chance to send malformed UGen graph, to update a ugen with different inputs. UGen function phasor has type:

| phasor :: Rate -> UGen -> UGen -> UGen -> UGen -> UGen -> UGen

Its type is different from sinOsc:

| sinOsc :: Rate -> UGen -> UGen -> UGen

Though, we can update "SinOsc" with "Phasor".

> s01_ng2 = primu "SinOsc" "Phasor" s01

Again, this likely to give us unintended result. Is there any safer way to do this?

We can pass oscillator function instead of String to get the name of UGen. "LFCub" string literal used in above play_s01_cub was picked up from this result:

| *AdhocUpdate> lfCub AR 440 0

Let us call this type as OscUG.

> type OscUG = Rate -> UGen -> UGen -> UGen

Rewrite set_osc_s01 to:

> set_osc_s01' :: OscUG -> UGen
> set_osc_s01' o = primu "SinOsc" (oscName o) s01
>
> oscName :: OscUG -> String
> oscName o = ugenName (o AR 0 0)

Note the use of dummy arguments AR, 0, and 0. Using oscName, OscUG could be shown as String. Make it an instance of Show class.

> instance Show OscUG where
>   show = oscName

We can write a function to swap all occurance of OscUG s.

> swapOsc :: OscUG -> OscUG -> UGen -> UGen
> swapOsc from to ug = primu (oscName from) (oscName to) ug

Below will swap all sinOsc to lfSaw in s01.

> play_s01_saw' :: IO ()
> play_s01_saw'= audition $ swapOsc sinOsc lfSaw s01

Example usage of swapOsc, randomly replacing sinOsc.

> oscUGs :: [OscUG]
> oscUGs = [sinOsc,fSinOsc,lfPar,lfCub,lfTri,lfSaw]
>
> randomOscUG :: RandomGen g => g -> (OscUG, g)
> randomOscUG g = case randomR (0, length oscUGs - 1) g of
>     (i,g') -> (oscUGs !! i, g')

randomOscUG will randomly return oscillators:

| *AdhocUpdate> replicateM 5 (getStdRandom randomOscUG)
| [LFPar,FSinOsc,LFSaw,FSinOsc,LFPar]

Make a synth containing 30 sinOscs.

> s03 :: Double -> UGen
> s03 ff =
>   let mko f a d = sinOsc AR (mkf f) 0 * (mke a d) * 0.01
>       mkf f = constant f
>       mke a d = envGen KR 1 1 0 1 DoNothing $ envSine a d
>       me = envGen KR 1 0 1 1 RemoveSynth $ envLinen 0 5 2.5 1
>       os = zipWith3 mko
>            [x*1.2081 | x <- [ff,ff*2..]]
>            [1.25**x  | x <- [1,2..]]
>            [2.5*x    | x <- [10,9..]]
>       sig = (mix $ mce $ take 30 os) * me
>   in  out 0 (pan2 sig 0 1)

And a function to randomly replace sinOsc with getStdRandom.

> randomizeOsc :: UGen -> IO UGen
> randomizeOsc ug = transformM k ug where
>   k (Primitive r n is os s j)
>     | n == "SinOsc" = getStdRandom randomOscUG >>= \o ->
>       return $ Primitive r (oscName o) is os s j
>   k x = return x

Takes fundamental frequency and play sum of oscillators. Oscillators used for each partial are choosed randomly.

> play_s03_rand :: Double -> IO ()
> play_s03_rand x = audition =<< randomizeOsc (s03 x)

Invoking:

| *AdhocUpdate> play_s03_rand 30

will build a UGen graph with randomly chosen oscillator UGen for each partial.

Variation without using generic update. Type signature of this synth is IO UGen, since its callling randomOscUG inside.

> s03' :: Double -> IO UGen
> s03' ff = do
>   ougs <- replicateM 30 (getStdRandom randomOscUG)
>   let mko o f a d = o AR (mkf f) 0 * (mke a d) * 0.01
>       mkf f = constant f
>       mke a d = envGen KR 1 1 0 1 DoNothing $ envSine a d
>       me = envGen KR 1 0 1 1 RemoveSynth $ envLinen 0 5 2.5 1
>       os = zipWith4 mko
>            ougs
>            [x*1.2081 | x <- [ff,ff*2..]]
>            [1.25**x  | x <- [1,2..]]
>            [2.5*x    | x <- [10,9..]]
>       sig = (mix $ mce os) * me
>   return $ out 0 (pan2 sig 0 1)

The result UGen graph is different from s03. Since s03 may replaces oscillator used for left and right of same frequency with different oscillators. s03' uses same oscillator for left and right of same frequency.

> play_s03_rand' :: Double -> IO ()
> play_s03_rand' x = audition =<< s03' x
TAGGED: haskell, supercollider, generic