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 whereWe 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.IDAs 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 + bSample 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 . unSoundWith 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 + bWe 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 mkSoundThe 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 timeUnitTime interval used for pausing.
> timeUnit :: Int
> timeUnit = 12800Synthdefs 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 17In 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)) 0Action 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 mkScoreFunction 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