packages feed

lambdasound-1.1: src/LambdaSound/Sound.hs

-- |
-- This module exports all needed datatypes and all the combinators needed to manipulate them.
module LambdaSound.Sound
  ( -- * Sound types
    Sound (..),
    SoundDuration (..),
    Pulse (..),
    Duration (..),
    Progress (..),
    Percentage (..),
    SamplingInfo (..),
    Hz (..),
    Time (..),
    DetermineDuration,

    -- * Make new sounds

    -- Also take a look at @LambdaSound.Create@!
    makeSound,
    makeSoundVector,
    fillWholeSound,
    fillWholeSoundST,
    computeOnce,

    -- * Sounds in sequence
    timedSequentially,
    (>>>),
    sequentially,
    infiniteSequentially,

    -- * Sounds in parallel
    parallel2,
    parallel,

    -- * Volume
    amplify,
    reduce,

    -- * Pitch
    raise,
    diminish,

    -- * Duration
    setDuration,
    (|->),
    getDuration,
    scaleDuration,
    dropDuration,
    adoptDuration,

    -- * Sample order
    reverseSound,
    dropSound,
    takeSound,

    -- * Zipping
    zipSoundWith,
    zipSound,

    -- * Change play behavior of a sound
    changeTempo,
    changeTempoM,

    -- * Modify the samples of a sound
    modifyWholeSound,
    modifyWholeSoundST,

    -- * Access the samples of a sound
    withSamplingInfo,
    withSampledSound,
    withSampledSoundPulse,

    -- * Embed IO
    embedIO,
    embedIOLazily,
  )
where

import Control.Monad.ST
import Data.Coerce (coerce)
import Data.Foldable (foldl')
import Data.Massiv.Array qualified as M
import Data.Massiv.Array.Unsafe qualified as MU
import LambdaSound.Sound.ComputeSound
import LambdaSound.Sound.Types
import System.IO.Unsafe (unsafePerformIO)

-- | 'Sound's may have a duration attached to them.
-- 'T'imed 'Sound's have a duration.
-- 'I'nfinite 'Sound's have no duration.
data SoundDuration = I | T

-- | Determines the duration of two sounds when they are combined
type family DetermineDuration (d1 :: SoundDuration) (d2 :: SoundDuration) where
  DetermineDuration I d = d
  DetermineDuration d I = d
  DetermineDuration T _ = T
  DetermineDuration _ T = T

data Sound (d :: SoundDuration) a where
  TimedSound ::
    !Duration ->
    ComputeSound a ->
    Sound T a
  InfiniteSound ::
    ComputeSound a ->
    Sound I a

-- data SoundType d where
--   InfiniteSoundType :: SoundType I
--   TimedSoundType :: SoundType T

-- class DetermineSoundType d where
--   determineSoundType :: SoundType d

-- instance DetermineSoundType I where
--   determineSoundType = InfiniteSoundType

-- instance DetermineSoundType T where
--   determineSoundType = TimedSoundType

getCS :: Sound d a -> ComputeSound a
getCS (InfiniteSound cs) = cs
getCS (TimedSound _ cs) = cs

mapComputation :: (ComputeSound a -> ComputeSound b) -> Sound d a -> Sound d b
mapComputation f (InfiniteSound cs) = InfiniteSound $ f cs
mapComputation f (TimedSound d cs) = TimedSound d $ f cs

instance Show (Sound d Pulse) where
  show (TimedSound d c) = showSampledCompute d c
  show (InfiniteSound c) = showSampledCompute 3 c

showSampledCompute :: Duration -> ComputeSound Pulse -> String
showSampledCompute d cs = unsafePerformIO $ do
  let si = makeSamplingInfo (coerce $ 25 / d) d
  floats <- sampleComputeSound si cs
  pure $ show $ M.toList floats

instance Semigroup (Sound d Pulse) where
  -- \| Combines two sounds in a parallel manner (see 'parallel2')
  (<>) = parallel2

instance Monoid (Sound I Pulse) where
  mempty = pure 0

instance Monoid (Sound T Pulse) where
  mempty = TimedSound 0 $ makeWithIndexFunction $ const $ const 0

instance (Num a) => Num (Sound I a) where
  (+) = zipSoundWith (+)
  (*) = zipSoundWith (*)
  (-) = zipSoundWith (-)
  abs = fmap abs
  fromInteger x = makeSound $ \_ _ -> fromInteger x
  signum = fmap signum
  negate = fmap negate

instance Functor (Sound d) where
  fmap f = mapComputation $ mapComputeSound f

instance Applicative (Sound I) where
  pure a = makeSound $ \_ _ -> a
  (<*>) = zipSoundWith ($)

-- | Append two sounds. This is only possible for sounds with a duration.
timedSequentially :: Sound T Pulse -> Sound T Pulse -> Sound T Pulse
timedSequentially (TimedSound d1 c1) (TimedSound d2 c2) =
  TimedSound (d1 + d2) $
    computeSequentially (coerce $ d1 / (d1 + d2)) c1 c2

-- | Append two infinite sounds where the 'Percentage' in the range @[0,1]@
-- specifies when the first sound ends and the next begins.
infiniteSequentially :: Percentage -> Sound I Pulse -> Sound I Pulse -> Sound I Pulse
infiniteSequentially factor' (InfiniteSound c1) (InfiniteSound c2) =
  InfiniteSound $
    computeSequentially factor c1 c2
  where
    factor = max 0 $ min 1 factor'

-- | Same as 'timedSequentially'
(>>>) :: Sound T Pulse -> Sound T Pulse -> Sound T Pulse
(>>>) = timedSequentially

infixl 5 >>>

-- | Combine a list of sounds in a sequential manner.
sequentially :: [Sound T Pulse] -> Sound T Pulse
sequentially = foldl' timedSequentially mempty

-- | Combine two sounds such that they play in parallel. If one 'Sound' is longer than the other,
-- it will be played without the shorter one for its remaining time
parallel2 :: Sound d Pulse -> Sound d Pulse -> Sound d Pulse
parallel2 (InfiniteSound c1) (InfiniteSound c2) = InfiniteSound $ computeParallel c1 1 c2
parallel2 (TimedSound d1 c1) (TimedSound d2 c2) = TimedSound newDuration $ computeParallel longerC (coerce factor) shorterC
  where
    (longerC, factor, shorterC) =
      if d1 >= d2
        then (c1, d2 / newDuration, c2)
        else (c2, d1 / newDuration, c1)
    newDuration = max d1 d2

-- | Combine a lists of sounds such that they play in parallel
parallel :: (Monoid (Sound d Pulse)) => [Sound d Pulse] -> Sound d Pulse
parallel = foldl' parallel2 mempty

-- | Zip two 'Sound's. The duration of the resulting 'Sound' is equivalent
-- to the duration of the shorter 'Sound', cutting away the excess samples from the longer one.
zipSoundWith :: (a -> b -> c) -> Sound d1 a -> Sound d2 b -> Sound (DetermineDuration d1 d2) c
zipSoundWith f sound1 sound2 =
  case (sound1, sound2) of
    (TimedSound d1 _, TimedSound d2 _) ->
      let d = min d1 d2
       in case (takeSound d sound1, takeSound d sound2) of
            (TimedSound _ c1, TimedSound _ c2) -> TimedSound d $ zipWithCompute f c1 c2
    (TimedSound d c1, InfiniteSound c2) -> TimedSound d $ zipWithCompute f c1 c2
    (InfiniteSound c1, TimedSound d c2) -> TimedSound d $ zipWithCompute f c1 c2
    (InfiniteSound c1, InfiniteSound c2) -> InfiniteSound $ zipWithCompute f c1 c2

-- | Zip two 'Sound's. The duration of the resulting 'Sound' is equivalent
-- to the duration of the shorter 'Sound', cutting away the excess samples from the longer one.
zipSound :: Sound d1 (a -> b) -> Sound d2 a -> Sound (DetermineDuration d1 d2) b
zipSound = zipSoundWith ($)

-- | Amplifies the volume of the given 'Sound'
amplify :: Float -> Sound d Pulse -> Sound d Pulse
amplify x = fmap (* coerce x)

-- | Reduces the volume of the given 'Sound'
reduce :: Float -> Sound d Pulse -> Sound d Pulse
reduce x = amplify (1 / x)

-- | Raises the frequency of the 'Sound' by the given factor.
-- Only works if the sound is based on some frequency (e.g. 'sineWave' but not 'noise')
raise :: Float -> Sound d Pulse -> Sound d Pulse
raise x = mapComputation $ \(ComputeSound compute) -> ComputeSound $ \si memo -> do
  compute (si {period = coerce x * si.period}) memo

-- | Diminishes the frequency of the 'Sound' by the given factor.
-- Only works if the sound is based on some frequency (e.g. 'pulse' but not 'noise')
diminish :: Float -> Sound d Pulse -> Sound d Pulse
diminish x = raise $ 1 / x

-- | Sets the duration of the 'Sound'.
-- The resuling sound is a 'T'imed 'Sound'.
setDuration :: Duration -> Sound d a -> Sound T a
setDuration d (TimedSound _ c) = TimedSound (max d 0) c
setDuration d (InfiniteSound c) = TimedSound (max d 0) c

-- | Same as `setDuration` but in operator form.
(|->) :: Duration -> Sound d a -> Sound 'T a
(|->) = setDuration

infix 7 |->

-- | Drop the duration associated with a 'Sound' and get an infinite sound again.
-- If you have combined timed sounds with a sequence combinator and then drop
-- their 'Duration', the sounds will keep their proportional length to each other.
-- Essentially, the percentage of their play time stays the same.
dropDuration :: Sound d a -> Sound I a
dropDuration (InfiniteSound cs) = InfiniteSound cs
dropDuration (TimedSound _ cs) = InfiniteSound cs

-- | Scales the 'Duration' of a 'Sound'.
-- The following makes a sound twice as long:
--
-- > scaleDuration 2 sound
scaleDuration :: Float -> Sound T a -> Sound T a
scaleDuration x (TimedSound d c) = TimedSound (coerce x * d) c

-- | Get the duration of a 'T'imed 'Sound'
getDuration :: Sound T a -> Duration
getDuration (TimedSound d _) = d

-- | Set the 'Duration' of a 'Sound' to the same as another one 'Sound'
adoptDuration :: Sound d a -> Sound x b -> Sound d b
adoptDuration (TimedSound duration _) = setDuration duration
adoptDuration (InfiniteSound _) = dropDuration

-- | Reverses a 'Sound' similar to 'reverse' for lists
reverseSound :: Sound d a -> Sound d a
reverseSound = mapComputation $ mapDelayedResult $ \si ->
  MU.unsafeBackpermute (M.Sz1 si.samples) (\index -> pred si.samples - index)

-- | Drop parts of a sound similar to 'drop' for lists
dropSound :: Duration -> Sound T a -> Sound T a
dropSound dropD' (TimedSound originalD cs) =
  TimedSound (originalD - dropD) $
    withSamplingInfoCS $ \oldSI ->
      changeSamplingInfo (\si -> si {samples = round $ factor * fromIntegral si.samples}) $
        mapDelayedResult
          ( \newSI ->
              MU.unsafeBackpermute (M.Sz1 oldSI.samples) $ \index ->
                index + newSI.samples - oldSI.samples
          )
          cs
  where
    dropD = max 0 $ min originalD dropD'
    droppedFactor = min 1 $ dropD / originalD
    factor =
      if droppedFactor == 1
        then 0
        else 1 / (1 - droppedFactor)

-- | Take parts of a sound similar to 'take' for lists
takeSound :: Duration -> Sound T a -> Sound T a
takeSound takeD' (TimedSound originalD cs) =
  TimedSound takeD $
    withSamplingInfoCS $ \oldSI ->
      changeSamplingInfo
        ( \si ->
            si
              { samples =
                  if takeD == 0
                    then 0
                    else round $ fromIntegral @_ @Float si.samples * (1 / coerce factor)
              }
        )
        $ mapDelayedResult
          (\_ -> M.slice' 0 $ M.Sz1 oldSI.samples)
          cs
  where
    takeD = max 0 $ min takeD' originalD
    factor = takeD / originalD

-- | Change how the 'Sound' progresses. For example, you can slow it
-- down in the beginning and speed it up at the end. However, the total
-- duration stays the same.
--
-- Negative 'Progress' is treated as '0' and 'Progress' above '1' is treated as '1'
changeTempo :: (Progress -> Progress) -> Sound d a -> Sound d a
changeTempo f = mapComputation $ mapDelayedResult $ \si ->
  MU.unsafeBackpermute (M.Sz1 si.samples) $ \index ->
    min si.samples $
      round $
        f
          (fromIntegral index / fromIntegral si.samples)
          * fromIntegral si.samples

changeTempoM :: Sound I (Progress -> Progress) -> Sound d a -> Sound d a
changeTempoM (InfiniteSound msc1) =
  mapComputation $
    mergeDelayedResult
      ( \si progressVector valueVector ->
          M.makeArray M.Seq (M.Sz1 si.samples) $ \index ->
            MU.unsafeIndex valueVector $
              min si.samples $
                round $
                  MU.unsafeIndex
                    progressVector
                    index
                    (fromIntegral index / fromIntegral si.samples)
                    * fromIntegral si.samples
      )
      msc1

-- | Compute a value once and then reuse it while computing all samples
computeOnce :: (SamplingInfo -> a) -> Sound d (a -> b) -> Sound d b
computeOnce f = mapComputation $ mapDelayedResult $ \si ->
  let a = f si
   in M.map ($ a)

-- | Fill a sound with a vector of sound samples. Keep in mind that the vector has the appropriate length!
fillWholeSound :: (M.Load r M.Ix1 Pulse) => (SamplingInfo -> M.Vector r Pulse) -> Sound I Pulse
fillWholeSound f = InfiniteSound $ fillSoundInMemoryIO $ \si dest -> do
  let vector = f si
  M.computeInto dest vector

-- | Fill a sound with a vector of sound samples in a mutable fashion.
fillWholeSoundST :: (SamplingInfo -> M.MVector M.RealWorld M.S Pulse -> ST M.RealWorld ()) -> Sound I Pulse
fillWholeSoundST f = InfiniteSound $ fillSoundInMemoryIO $ fmap stToIO . f

-- | Modify all samples of a sound so that you can look into the past and future
-- of a sound (e.g. IIR filter).
modifyWholeSound :: (M.Load r M.Ix1 Pulse) => (M.Vector M.S Pulse -> M.Vector r Pulse) -> Sound d Pulse -> Sound d Pulse
modifyWholeSound f = mapComputation $ mapSoundFromMemory f

-- | Modify all samples of a sound so that you can look into the past and future
-- of a sound (e.g. IIR filter).
modifyWholeSoundST :: (M.Vector M.S Pulse -> M.MVector M.RealWorld M.S Pulse -> ST M.RealWorld ()) -> Sound d Pulse -> Sound d Pulse
modifyWholeSoundST f = mapComputation $ mapSoundFromMemoryIO $ fmap stToIO . f

-- | Access the sample rate of an infinite sound
withSamplingInfo :: (SamplingInfo -> Sound d a) -> Sound I a
withSamplingInfo f = InfiniteSound $ withSamplingInfoCS (getCS . f)

-- | Access the samples of a sound.
--
-- The pulse version is slightly faster since you get a storable vector
withSampledSoundPulse :: Sound T Pulse -> (M.Vector M.S Pulse -> Sound I a) -> Sound I a
withSampledSoundPulse (TimedSound duration cs) = InfiniteSound . withSampledSoundPulseCS duration cs . fmap getCS

-- | Access the samples of a sound.
withSampledSound :: Sound T a -> (M.Vector M.D a -> Sound I b) -> Sound I b
withSampledSound (TimedSound duration cs) = InfiniteSound . withSampledSoundCS duration cs . fmap getCS

-- | Calculate sound samples based on their index.
-- Take a look at @LambdaSound.Create@ for other creation functions.
makeSound :: (SamplingInfo -> Int -> a) -> Sound I a
makeSound f = InfiniteSound $ makeWithIndexFunction f

-- | Calculate the samples of the sound as one vector
-- Take a look at @LambdaSound.Create@ for other creation functions.
makeSoundVector :: (SamplingInfo -> M.Vector M.D a) -> Sound I a
makeSoundVector f = InfiniteSound $ makeDelayedResult f

-- | Embed an IO calculation when generating an infinite sound.
--
-- This IO action will be run each time the sound is used.
embedIO :: IO (Sound d a) -> Sound I a
embedIO ioSound = InfiniteSound $ embedIOCS $ getCS <$> ioSound

-- | Embed an IO calculation lazily when generating an infinite sound.
--
-- This IO action will not necessarily run each time the sound is used due to memoization.
-- The IO action will run at least once and at most as often as the sound occurs.
embedIOLazily :: IO (Sound d a) -> Sound I a
embedIOLazily ioSound = InfiniteSound $ embedIOLazilyCS $ getCS <$> ioSound