8c6794b6.github.io

Making sound with side effects

This post is a scratchy idea for making sound with side effects. Sample recording created with side effects from fibonacci sequence sounds like this ogg file.

During debug, we print out intermediate data, as side effects. Like this, can we make sounds from those intermediate data, as side effects?

> {-# LANGUAGE GeneralizedNewtypeDeriving #-}
> module SideEffectSound where

We will use scsynth as sound synthesis engine.

> import Control.Concurrent
> import Control.Monad
> import Control.Monad.Reader
> import Control.Monad.State
> import Control.Monad.Writer
> import Data.Word
> import Sound.OpenSoundControl
> import Sound.SC3
> import Sound.SC3.ID

As an action to get side effect, using fibonacci sequence here. We will not use memoizatoin nor any other method to achive efficiency, since our concern resides in those side effects.

> fib_visible :: Int -> IO Int
> fib_visible n
>   | n == 0    = putStr "0" >> return 1
>   | n == 1    = putStr "1" >> return 1
>   | otherwise = do
>       putStr (show n)
>       a <- fib_visible (n-1)
>       b <- fib_visible (n-2)
>       return $ a + b

Sample output:

| ghci> fib_visible 5
| 5432101210321018

Like above, we will make sound from side effect with fibonacci sequence. Using a newtype wrapper for sending OSC message to default scsynth, running in UDP port 57110 of localhost (assuming that scsynth is running with this default settings).

> newtype Sound a = Sound {unSound :: ReaderT UDP IO a}
>   deriving (Functor, Monad, MonadReader UDP, MonadIO)
>
> runSound :: Sound a -> IO a
> runSound = withSC3 . runReaderT . unSound

With using Sound wrapper, we can write audible fibonacci sequence with side effect pretty much resembling to its visible variant. The only difference we see in the body of code is use of mkSound instead of putStr.

> fib_audible :: Int -> Sound Int
> fib_audible n
>   | n == 0    = mkSound 0 >> return 1
>   | n == 1    = mkSound 1 >> return 1
>   | otherwise = do
>       mkSound n
>       a <- fib_audible (n-1)
>       b <- fib_audible (n-2)
>       return $ a + b

We could write fib_audible and fib_visible with using mkFib.

> mkFib :: Monad m => (Int -> m ()) -> Int -> m Int
> mkFib act n
>   | n == 0    = act 0 >> return 1
>   | n == 1    = act 1 >> return 1
>   | otherwise = do
>       act n
>       a <- mkFib act (n-1)
>       b <- mkFib act (n-2)
>       return $ a + b
>
> fib_visible' = mkFib (putStr . show)
> fib_audible' = mkFib mkSound

The sound maker. Sends OSC message to scsynth and pause. Will not make sound when n is 0.

> mkSound :: Int -> Sound ()
> mkSound n
>   | n == 0    = liftIO $ threadDelay timeUnit
>   | otherwise = do
>     fd <- ask
>     let n' = fromIntegral n
>     liftIO $ do
>       send fd $ s_new "fibdef" (-1) AddToHead 1
>         [ ("freq", n' * 220)
>         , ("amp", 0.05 + (0.01 * n'))
>         , ("pan", (-1) + (2/3) * fromIntegral (n `mod` 4))
>         , ("decay", 0.1 + (n' * 0.175)) ]
>       threadDelay timeUnit

Time interval used for pausing.

> timeUnit :: Int
> timeUnit = 12800

Synthdefs used in this piece. Simple synthdef taking freq, pan and amp parameter.

> fibdef :: UGen
> fibdef =
>   let sig = sinOsc AR frq 0 * e * amp
>       frq = control KR "freq" 440
>       amp = control KR "amp" 0.1
>       e   = envGen KR 1 1 0 1 RemoveSynth shp
>       shp = envPerc 1e-4 dcy
>       pan = control KR "pan" 0
>       dcy = control KR "decay" 0.5
>   in  out 0 (pan2 sig pan 1)

Send the synthdef to scsynth.

> setup_fib :: Transport t => t -> IO OSC
> setup_fib fd = do
>   async fd $ bundle immediately
>     [ d_recv $ synthdef "fibdef" fibdef ]

Now its ready to hear the sound.

> play_fib_sound :: IO Int
> play_fib_sound = do
>   withSC3 $ \fd -> reset fd >> setup_fib fd
>   runSound $ fib_audible 17

In some case, Instead of listening sound in real time, we might want to write score and render the result in batch command. Below newtype Score will do this job. Score is a combination of Reader, Writer, and State monad, it takes delta time as Reader monad's environment value, used to increment current time in score file. Writer accumulate the OSC list written to score file, and State monad hold the current time in the score. Alternatively, it could be written with RWS monad, or single State monad. The use of newtype wrapper with GeneralizedNewtypeDeriving here is, I suppose, just a matter of taste.

> newtype Score a =
>   Score {unScore :: ReaderT Word64 (WriterT [OSC] (State Word64)) a} deriving
>   (Functor, Monad, MonadState Word64, MonadWriter [OSC], MonadReader Word64)
>
> runScore :: Score a -> Word64 -> [OSC]
> runScore m delta =
>   evalState (execWriterT (runReaderT (unScore m) delta)) 0

Action to make score from each data passed in fibonacci, and fibonacci score writer monad. We can use mkFib to make a new action to write score from side effects generated from fibonacci action.

> mkScore :: Int -> Score ()
> mkScore n = do
>   delta <- ask
>   t0 <- get
>   let n' = fromIntegral n
>   tell $ [ bundle (NTPi t0)
>     [ s_new "fibdef" (-1) AddToTail 1
>       [ ("freq", n' * 220)
>       , ("amp",  0.05 + (n' * 0.02))
>       , ("pan", (-1) + (2/3) * fromIntegral (n `mod` 4))
>       , ("decay", 0.1 + (n' * 0.25)) ]]]
>   put (t0 + delta)
>
> fib_score :: Int -> Score Int
> fib_score = mkFib mkScore

Function to write score file, with initial OSC message to send synthdef and add default group to scsynth.

> write_fib_score :: FilePath -> Score a -> IO ()
> write_fib_score path m = do
>   let iosc = bundle (NTPi 0)
>         [ g_new [(1,AddToTail,0)]
>         , d_recv $ synthdef "fibdef" fibdef ]
>   writeNRT path (iosc : runScore m (fromIntegral timeUnit * 10000))

Test it.

> test_fib_score :: IO ()
> test_fib_score = write_fib_score "/tmp/fib.osc" (fib_score 12)

Attached recording in the beginning of this post was created from fib.osc, fed to scsynth in non-realtime mode.

| $ scsynth -o 2 -N /tmp/fib.osc _ out.wav 48000 WAVE float
TAGGED: haskell, supercollider, sound