proarrow-0.4.0.0: tools/smc-audio/SmcAudio.hs
{-# LANGUAGE QualifiedDo #-}
{-# LANGUAGE RecursiveDo #-}
-- | A feedback echo, interpreted as a Yampa signal function or a string diagram.
module SmcAudio (echoWith, echo, melody, sampleRate, duration, samples, echoPicture) where
import Control.Arrow qualified as A
import FRP.Yampa qualified as Y
import Prelude hiding ((.))
import Proarrow.Category.Monoidal (Monoidal (..), MonoidalProfunctor, SymMonoidal, Tensor)
import Proarrow.Category.Monoidal.Strength (Costrong)
import Proarrow.Core (CAT, Promonad)
import Proarrow.Monoid (CocommutativeComonoid)
import Proarrow.Profunctor.Instance.Arrow (Arr (..))
import Proarrow.Tools.Diagrams.Svg (SVG (..), W (Wire), node, render)
import Proarrow.Tools.SMC (SYN (..), proc, returnA, (-<))
import Proarrow.Tools.SMC qualified as SMC
-- | Mix the input with a delayed copy of the output. The delay must break the instantaneous
-- feedback dependency. Copying the output sends it both to the delay and to the caller.
{-# INLINE echoWith #-}
echoWith
:: forall {k} (a :: k) (p :: CAT k)
. (SymMonoidal k, Promonad p, MonoidalProfunctor p, Costrong Tensor p, CocommutativeComonoid a)
=> p (a ** a) a
-> p a a
-> p a a
echoWith mix delay = proc @(F a) @(F a) \input -> SMC.do
rec output <- mix -< (input, delay -< output)
returnA -< output
-- | Echoes repeat every @seconds@, each scaled by @gain@. Use a positive delay and
-- @abs gain < 1@ for a decaying echo.
echo :: Double -> Double -> Y.SF Double Double
echo seconds gain = unArr (echoWith (Arr (A.arr (\(dry, wet) -> dry + gain * wet))) (Arr (Y.delay seconds 0)))
-- | A short arpeggio with a decaying envelope on each note, followed by silence for the echo tail.
melody :: Y.SF () Double
melody = Y.time A.>>> A.arr tone
where
tone t
| t >= 2 = 0
| otherwise =
let note = floor (t / 0.25) :: Int
age = t - fromIntegral note * 0.25
frequency = [261.6256, 329.6276, 391.9954, 523.2511, 391.9954, 329.6276, 261.6256, 523.2511] !! note
envelope = (1 - exp (-(200 * age))) * exp (-(16 * age))
in 0.3 * envelope * sin (2 * pi * frequency * age)
sampleRate :: Int
sampleRate = 44100
duration :: Double
duration = 5
-- | Sample a closed signal function at the audio rate for the demo's duration.
samples :: Y.SF () Double -> [Double]
samples sf = take (round (duration * fromIntegral sampleRate)) (Y.embed sf ((), repeat (1 / fromIntegral sampleRate, Nothing)))
-- | The feedback network drawn with its mixer and delay as boxes.
echoPicture :: String
echoPicture = render (echoWith @(S '[Wire "audio"]) (node "mix: dry + 0.5 wet") (node "delay: 0.3 s"))