packages feed

synthesizer-alsa-0.4: src/Synthesizer/PiecewiseConstant/ALSA/MIDI.hs

{- |
Convert MIDI events of a MIDI controller to a control signal.
-}
{-# LANGUAGE NoImplicitPrelude #-}
module Synthesizer.PiecewiseConstant.ALSA.MIDI (
   T,
   duration,
   PC.zipWith,

   initWith,
   controllerLinear,
   controllerExponential,
   pitchBend,
   channelPressure,
   bendWheelPressure,
   checkBendWheelPressure,
   bendWheelPressureZip,
   ) where

import qualified Synthesizer.EventList.ALSA.MIDI as Ev
import Synthesizer.EventList.ALSA.MIDI (LazyTime, StrictTime, Filter, Channel, )

import qualified Sound.MIDI.ALSA.Check as Check
import qualified Sound.MIDI.Message.Channel.Voice as VoiceMsg
import qualified Sound.ALSA.Sequencer.Event as Event
import qualified Synthesizer.MIDIValue.BendModulation as BM
import qualified Synthesizer.MIDIValue.BendWheelPressure as BWP
import qualified Synthesizer.MIDIValue as MV

import qualified Synthesizer.PiecewiseConstant.Signal as PC

-- import qualified Data.EventList.Relative.TimeTime  as EventListTT
import qualified Data.EventList.Relative.TimeMixed as EventListTM
import qualified Data.EventList.Relative.MixedTime as EventListMT
import qualified Data.EventList.Relative.BodyTime  as EventListBT
import qualified Data.EventList.Relative.TimeBody  as EventList

import qualified Numeric.NonNegative.Class   as NonNeg
-- import qualified Numeric.NonNegative.Wrapper as NonNegW
import qualified Numeric.NonNegative.Chunky as NonNegChunky

import qualified Algebra.Transcendental as Trans
import qualified Algebra.RealRing       as RealRing
import qualified Algebra.Field          as Field

import qualified Data.Accessor.Monad.Trans.State as AccState
import Control.Monad.Trans.State (State, evalState, state, get, put, )
import Control.Monad (liftM, liftM2, msum, )
import Data.Traversable (traverse, sequence, )
import Data.Foldable (traverse_, )

import qualified Data.List.HT as ListHT
import qualified Data.List as List
import Data.Either (Either(Left, Right), )
import Data.Maybe (maybe, )
import Data.Function ((.), ($), flip, )

import NumericPrelude.Numeric
import Prelude as P (Maybe, fmap, (>>), )


type T = EventListBT.T StrictTime


duration :: T y -> LazyTime
duration =
   NonNegChunky.fromChunks . EventListBT.getTimes


{-# INLINE initWith #-}
initWith ::
   (y -> c) ->
   c -> EventList.T StrictTime [y] -> T c
initWith f initial =
{-
   EventListTM.switchBodyR EventListBT.empty
      (\xs _ -> EventListMT.consBody initial xs) .
-}
   EventListMT.consBody initial .
   flip EventListTM.snocTime NonNeg.zero .
   flip evalState initial .
   traverse
      (\ys -> traverse_ (put . f) ys >> get)


{-# INLINE controllerLinear #-}
controllerLinear ::
   (Field.C y) =>
   Channel -> Ev.Controller ->
   (y,y) -> y ->
   Filter (T y)
controllerLinear chan ctrl bnd initial =
   liftM (initWith (MV.controllerLinear bnd) initial) $
   Ev.getControllerEvents chan ctrl


{-# INLINE controllerExponential #-}
controllerExponential ::
   (Trans.C y) =>
   Channel -> Ev.Controller ->
   (y,y) -> y ->
   Filter (T y)
controllerExponential chan ctrl bnd initial =
   liftM (initWith (MV.controllerExponential bnd) initial) $
   Ev.getControllerEvents chan ctrl


{- |
@pitchBend channel range center@:
emits frequencies on an exponential scale from
@center/range@ to @center*range@.
-}
{-# INLINE pitchBend #-}
pitchBend ::
   (Trans.C y) =>
   Channel ->
   y -> y ->
   Filter (T y)
pitchBend chan range center =
   liftM (initWith (MV.pitchBend range center) center) $
   Ev.getSlice (Check.pitchBend chan)
--   getPitchBendEvents chan

{-# INLINE channelPressure #-}
channelPressure ::
   (Trans.C y) =>
   Channel ->
   y -> y ->
   Filter (T y)
channelPressure chan maxVal initVal =
   liftM (initWith (MV.controllerLinear (0,maxVal)) initVal) $
   Ev.getSlice (Check.channelPressure chan)


{-# INLINE bendWheelPressure #-}
bendWheelPressure ::
   (RealRing.C y, Trans.C y) =>
   Channel ->
   Int -> y -> y ->
   Filter (T (BM.T y))
bendWheelPressure chan
      pitchRange wheelDepth pressDepth =
   let toBM = BM.fromBendWheelPressure pitchRange wheelDepth pressDepth
   in  liftM (initWith toBM (toBM BWP.deflt)) $
       state $
       EventList.unzip .
       fmap ListHT.unzipEithers .
       flip evalState BWP.deflt .
       traverse (traverse (separateBWP chan))

separateBWP :: Channel -> Event.T -> State BWP.T (Either BWP.T Event.T)
separateBWP chan ev =
   fmap (maybe (Right ev) Left) $
   checkBendWheelPressure chan ev

{- |
I hesitate to move this to MIDIValue or BendWheelPressure module
because it depends on ALSA specific Event.
-}
checkBendWheelPressure ::
   Channel -> Event.T -> State BWP.T (Maybe BWP.T)
checkBendWheelPressure chan ev =
   sequence $
   (fmap (>> get)) $
   msum $ List.map ($ev) $
      (fmap (AccState.set BWP.bend) . Check.pitchBend chan) :
      (fmap (AccState.set BWP.wheel) . Check.controller chan VoiceMsg.modulation) :
      (fmap (AccState.set BWP.pressure) . Check.channelPressure chan) :
      []

{- |
This one is certainly not as efficient as 'bendWheelPressure'
since it first slices the event list
and then zips the slices together.
-}
{-# INLINE bendWheelPressureZip #-}
bendWheelPressureZip ::
   (RealRing.C y, Trans.C y) =>
   Channel ->
   Int -> y -> y ->
   Filter (T (BM.T y))
bendWheelPressureZip chan
     pitchRange wheelDepth pressDepth =
   liftM2 (PC.zipWith BM.Cons)
      (pitchBend chan (2^?(fromIntegral pitchRange/12)) 1)
      (liftM2 (PC.zipWith (+))
         (controllerLinear chan VoiceMsg.modulation (0,wheelDepth) 0)
         (channelPressure chan pressDepth 0))