synthesizer-midi-0.6: src/Synthesizer/MIDI/EventList.hs
{-# LANGUAGE NoImplicitPrelude #-}
module Synthesizer.MIDI.EventList where
import qualified Sound.MIDI.Message.Class.Check as Check
import qualified Sound.MIDI.Message.Channel as ChannelMsg
import qualified Sound.MIDI.Message.Channel.Mode as Mode
import qualified Data.EventList.Relative.TimeBody as EventList
import qualified Data.EventList.Relative.MixedBody as EventListMB
import qualified Data.EventList.Relative.BodyBody as EventListBB
import Control.Monad.Trans.State
(State, state, evalState, gets, put, )
import Data.Traversable (traverse, )
import qualified Numeric.NonNegative.Class as NonNeg
import qualified Numeric.NonNegative.Wrapper as NonNegW
import qualified Numeric.NonNegative.Chunky as NonNegChunky
import Data.Array (Array, listArray, (!), bounds, inRange, )
import qualified Data.List.HT as ListHT
import Data.Tuple.HT (mapPair, mapFst, mapSnd, )
import Data.Maybe.HT (toMaybe, )
import Data.Maybe (catMaybes, isNothing, )
import Control.Monad.HT ((<=<), )
import Control.Monad (guard, msum, )
import NumericPrelude.Numeric
import NumericPrelude.Base
type StrictTime = NonNegW.Integer
{-
Maybe we could use StorableVector.Pattern.LazySize
or we could use synthesizer-core/ChunkySize.
What package should we rely on?
Which one is more portable?
We do not use this type for timing in event lists anymore.
It worked in principle but left us with a couple of memory leaks,
that I could never identify and eliminate completely.
-}
type LazyTime = NonNegChunky.T NonNegW.Integer
-- * event filters
type Filter event = State (EventList.T StrictTime [event])
{- |
We turn the strict time values into lazy ones
according to the breaks by our beat.
However for the laziness breaks we ignore the events that are filtered out.
That is we loose laziness granularity
but hopefully gain efficiency by larger blocks.
-}
getSlice ::
(event -> Maybe a) ->
Filter event (EventList.T StrictTime [a])
getSlice f =
state $
EventList.unzip .
fmap (ListHT.partitionMaybe f)
type Channel = ChannelMsg.Channel
type Controller = ChannelMsg.Controller
type Pitch = ChannelMsg.Pitch
type Velocity = ChannelMsg.Velocity
type Program = ChannelMsg.Program
getControllerEvents ::
(Check.C event) =>
Channel -> Controller ->
Filter event (EventList.T StrictTime [Int])
getControllerEvents chan ctrl =
getSlice (Check.controller chan ctrl)
{-
getControllerEvents ::
(Check.C event) =>
Channel -> Controller ->
Filter event (EventList.T StrictTime (Maybe Int))
getControllerEvents chan ctrl =
fmap (fmap (fmap snd . ListHT.viewR)) $
getSlice (Check.controller chan ctrl)
-}
data NoteBoundary a =
NoteBoundary Pitch Velocity a
| AllNotesOff
deriving (Eq, Show)
data Note = Note Program Pitch Velocity LazyTime
deriving (Eq, Show)
case_ :: Maybe a -> (a -> b) -> Maybe b
case_ = flip fmap
{-
We could also provide a function which filters for specific programs/presets.
-}
getNoteEvents ::
(Check.C event) =>
Channel ->
Filter event (EventList.T StrictTime [Either Program (NoteBoundary Bool)])
getNoteEvents chan =
getSlice $ checkNoteEvent chan
checkNoteEvent ::
(Check.C event) =>
Channel -> event ->
Maybe (Either Program (NoteBoundary Bool))
checkNoteEvent chan e = msum $
case_ (Check.noteExplicitOff chan e) (\(velocity, pitch, press) ->
Right $ NoteBoundary pitch velocity press) :
case_ (Check.program chan e) Left :
{-
We do not handle AllSoundOff here,
since this would also mean to clear reverb buffers
and this cannot be handled here.
-}
(Check.mode chan e >>= \mode -> do
guard (mode == Mode.AllNotesOff)
return (Right AllNotesOff)) :
[]
embedPrograms ::
Program ->
EventList.T StrictTime [Either Program (NoteBoundary Bool)] ->
EventList.T StrictTime [NoteBoundary (Maybe Program)]
embedPrograms initPgm =
fmap catMaybes .
flip evalState initPgm .
traverse (traverse embedProgramState)
embedProgramState ::
Either Program (NoteBoundary Bool) ->
State Program (Maybe (NoteBoundary (Maybe Program)))
embedProgramState =
-- evaluate program for every event in order to prevent a space leak
(\n -> state (\s -> (seq s n, s)))
<=<
either
(\pgm -> put pgm >> return Nothing)
(\bnd ->
gets (Just .
case bnd of
AllNotesOff -> const AllNotesOff
NoteBoundary p v press ->
NoteBoundary p v . toMaybe press))
matchNoteEvents ::
EventList.T StrictTime [NoteBoundary (Maybe Program)] ->
EventList.T StrictTime [Note]
matchNoteEvents =
matchNoteEventsCore $ \bndOn -> case bndOn of
AllNotesOff -> Nothing
NoteBoundary pitchOn velOn pressOn ->
flip fmap pressOn $ \pgm ->
(\bndOff ->
case bndOff of
AllNotesOff -> True
NoteBoundary pitchOff _velOff pressOff ->
pitchOn == pitchOff && isNothing pressOff,
Note pgm pitchOn velOn)
matchNoteEventsCore ::
(noteBnd ->
Maybe (noteBnd -> Bool, LazyTime -> Note)) ->
EventList.T StrictTime [noteBnd] ->
EventList.T StrictTime [Note]
matchNoteEventsCore methods =
let recourseEvents =
EventListMB.switchBodyL $ \evs0 xs0 -> case evs0 of
[] -> ([], xs0)
ev:evs ->
case methods ev of
Nothing ->
recourseEvents (EventListMB.consBody evs xs0)
Just (check, cons) ->
case durationRemove check (EventListMB.consBody evs xs0) of
(dur, xs1) ->
mapFst
(cons dur :)
(recourseEvents xs1)
recourse =
EventList.switchL EventList.empty $ \(t,evs0) xs0 ->
let (evs1,xs1) = recourseEvents (EventListMB.consBody evs0 xs0)
in EventList.cons t evs1 $ recourse xs1
in recourse
{-
durationRemove Char.isUpper ("a" ./ 3 /. "bf" ./ 5 /. "aCcd" ./ empty :: Data.EventList.Relative.BodyBody.T StrictTime [Char])
-}
{- |
Search for specific event,
return its time stamp and remove it.
-}
durationRemove ::
(NonNeg.C time) =>
(body -> Bool) ->
EventListBB.T time [body] ->
(NonNegChunky.T time, EventListBB.T time [body])
durationRemove p =
let errorEndOfList =
(error "no matching body element found",
error "list ended before matching element found")
recourse =
EventListMB.switchBodyL $ \evs xs0 ->
let (prefix, suffix0) = break p evs
(suffix1, rest) =
case suffix0 of
[] -> ([],
flip (EventListMB.switchTimeL errorEndOfList) xs0 $ \t xs1 ->
mapPair
(NonNegChunky.fromChunks . (t:) .
NonNegChunky.toChunks,
EventListMB.consTime t) $
recourse xs1)
_:ys -> (ys, (NonNeg.zero, xs0))
in mapSnd
(EventListMB.consBody (prefix++suffix1))
rest
in recourse
durationRemoveTB ::
(NonNeg.C time) =>
(body -> Bool) ->
EventList.T time [body] ->
(NonNegChunky.T time, EventList.T time [body])
durationRemoveTB p =
let errorEndOfList =
(error "no matching body element found",
error "list ended before matching element found")
recourse =
EventList.switchL errorEndOfList $ \(t,evs) xs ->
let (prefix, suffix0) = break p evs
(suffix1, rest) =
case suffix0 of
[] -> ([], recourse xs)
_:ys -> (ys, (NonNeg.zero, xs))
in mapPair
(NonNegChunky.fromChunks . (t:) .
NonNegChunky.toChunks,
EventList.cons t (prefix++suffix1))
rest
in recourse
-- ToDo: move to somewhere else, this has nothing todo with event lists
makeInstrumentArray :: [instr] -> Array Program instr
makeInstrumentArray instrs =
listArray
(ChannelMsg.toProgram 0, ChannelMsg.toProgram (length instrs - 1))
instrs
getInstrumentFromArray :: Array Program instr -> Program -> Program -> instr
getInstrumentFromArray bank defltPgm pgm =
bank !
if inRange (bounds bank) pgm
then pgm else defltPgm