From f138d0245adfd0993153bd4e88352b5415aadfa0 Mon Sep 17 00:00:00 2001 From: Guerric Chupin <guerric.chupin@ensta-paristech.fr> Date: Thu, 19 May 2016 14:56:52 +0100 Subject: [PATCH] =?utf8?q?Reactogon=20acting=20as=20a=20simple=20=E2=80=9C?= =?utf8?q?pass-through=E2=80=9D=20MIDI=20device.?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- Reactogon/Auxiliary.hs | 9 +++- Reactogon/ClientState.hs | 8 +++ Reactogon/MIDI.hs | 83 ++++++++++++++++++++++++------ Reactogon/Note.hs | 14 +++--- Reactogon/Reactimation.hs | 33 ++++++++++++ Reactogon/Reactogon.hs | 103 ++++++++++++++++++++++---------------- Reactogon/Shared.hs | 30 +++++++++++ 7 files changed, 213 insertions(+), 67 deletions(-) create mode 100644 Reactogon/ClientState.hs create mode 100644 Reactogon/Reactimation.hs create mode 100644 Reactogon/Shared.hs diff --git a/Reactogon/Auxiliary.hs b/Reactogon/Auxiliary.hs index 72ba4e5..c440774 100644 --- a/Reactogon/Auxiliary.hs +++ b/Reactogon/Auxiliary.hs @@ -1,7 +1,14 @@ -module Auxiliary ( dupl +module Auxiliary ( breakMap )where import Control.Arrow +import Data.Map (Map) +import qualified Data.Map as M dupl :: (Arrow a) => a b c -> a (b,b) (c,c) dupl f = f *** f + +breakMap :: (Ord k) => k -> Map k a -> (Map k a, Map k a) +breakMap k m = (smaller, larger') + where (smaller, maybeValue, larger) = M.splitLookup k m + larger' = maybe larger (\v -> M.insert k v larger) maybeValue diff --git a/Reactogon/ClientState.hs b/Reactogon/ClientState.hs new file mode 100644 index 0000000..a38e740 --- /dev/null +++ b/Reactogon/ClientState.hs @@ -0,0 +1,8 @@ +module ClientState where + +import Sound.JACK ( NFrames + ) + +data ClientState = ClientState { rate :: Int + , buffSize :: NFrames + } diff --git a/Reactogon/MIDI.hs b/Reactogon/MIDI.hs index e9d2f94..6e15d76 100644 --- a/Reactogon/MIDI.hs +++ b/Reactogon/MIDI.hs @@ -1,52 +1,105 @@ -module MIDI ( SampleRate +module MIDI ( EventQueue + , SampleRate , Pitch , toPitch , fromPitch , fromVelocity , toVelocity , Velocity - , Voice ( fromVoice - , toVoice - ) - , Note(..) + , Message ( NoteOn + , NoteOff + , Control + ) + , fromRawMessage + , toRawMessage , ControllerIdx , ControllerValue - , Control ) where +import qualified Sound.MIDI.Message as Message + import Sound.MIDI.Message.Channel.Voice ( fromPitch , toPitch , fromVelocity , toVelocity ) +import qualified Sound.MIDI.Message.Channel as Channel import qualified Sound.MIDI.Message.Channel.Voice as Voice +import Data.Map (Map) +import FRP.Yampa + +type EventQueue = Map Time Message type SampleRate = Int +type RawMessage = Message.T + +{- +class Message a where + fromMessage :: RawMessage -> Maybe a + toMessage :: a -> RawMessage +-} + +type MidiVoice = Voice.T + +type Channel = Channel.Channel type Pitch = Voice.Pitch type Velocity = Voice.Velocity -class Voice a where - fromVoice :: Voice.T -> Maybe a - toVoice :: a -> Voice.T +{- +class (Message a) => Voice a where + fromVoice :: MidiVoice -> Maybe a + toVoice :: a -> MidiVoice +-} + +type ControllerIdx = Voice.Controller +type ControllerValue = Int -data Note = NoteOn Pitch Velocity - | NoteOff Pitch Velocity +data Message = NoteOn Channel Pitch Velocity + | NoteOff Channel Pitch Velocity + | Control Channel ControllerIdx ControllerValue deriving(Show) +fromRawMessage :: RawMessage -> Maybe Message +fromRawMessage (Message.Channel (Channel.Cons c + (Channel.Voice (Voice.NoteOn p v)))) = Just $ NoteOn c p v +fromRawMessage (Message.Channel (Channel.Cons c + (Channel.Voice (Voice.NoteOff p v)))) = Just $ NoteOff c p v +fromRawMessage _ = Nothing + +toRawMessage :: Message -> RawMessage +toRawMessage (NoteOn c p v) = (Message.Channel $ Channel.Cons c + (Channel.Voice $ Voice.NoteOn p v)) +toRawMessage (NoteOff c p v) = (Message.Channel $ Channel.Cons c + (Channel.Voice $ Voice.NoteOff p v)) + +{- +instance Message Note where + fromMessage (Message.Channel (Channel.Cons c + (Channel.Voice (Voice.NoteOn p v)))) = Just $ NoteOn c p v + fromMessage (Message.Channel (Channel.Cons c + (Channel.Voice (Voice.NoteOff p v)))) = Just $ NoteOff c p v + fromMessage _ = Nothing + toMessage (NoteOn c p v) = (Message.Channel $ Channel.Cons c + (Channel.Voice $ Voice.NoteOn p v)) + toMessage (NoteOff c p v) = (Message.Channel $ Channel.Cons c + (Channel.Voice $ Voice.NoteOff p v)) +{- instance Voice Note where fromVoice (Voice.NoteOn p v) = Just $ NoteOn p v fromVoice (Voice.NoteOff p v) = Just $ NoteOff p v fromVoice _ = Nothing toVoice (NoteOn p v) = Voice.NoteOn p v toVoice (NoteOff p v) = Voice.NoteOff p v - -type ControllerIdx = Voice.Controller -type ControllerValue = Int +-} +-} +{- data Control = Control ControllerIdx ControllerValue - +-} +{- instance Voice Control where fromVoice (Voice.Control i v) = Just $ Control i v fromVoice _ = Nothing toVoice (Control i v) = Voice.Control i v +-} diff --git a/Reactogon/Note.hs b/Reactogon/Note.hs index 9617a6a..d5b271d 100644 --- a/Reactogon/Note.hs +++ b/Reactogon/Note.hs @@ -3,23 +3,23 @@ module Note where import MIDI isOn :: Note -> Bool -isOn (NoteOn _ _) = True +isOn (NoteOn _ _ _) = True isOn _ = False isOff :: Note -> Bool isOff = not . isOn changePitch :: (Pitch -> Pitch) -> Note -> Note -changePitch f (NoteOn p v) = NoteOn (f p) v -changePitch f (NoteOff p v) = NoteOff (f p) v +changePitch f (NoteOn c p v) = NoteOn c (f p) v +changePitch f (NoteOff c p v) = NoteOff c (f p) v changeVelocity :: (Velocity -> Velocity) -> Note -> Note -changeVelocity f (NoteOn p v) = NoteOn p (f v) -changeVelocity f (NoteOff p v) = NoteOff p (f v) +changeVelocity f (NoteOn c p v) = NoteOn c p (f v) +changeVelocity f (NoteOff c p v) = NoteOff c p (f v) switchOnOff :: Note -> Note -switchOnOff (NoteOn p v) = NoteOff p v -switchOnOff (NoteOff p v) = NoteOn p v +switchOnOff (NoteOn c p v) = NoteOff c p v +switchOnOff (NoteOff c p v) = NoteOn c p v perfectFifth :: Note -> Note perfectFifth = changePitch (toPitch . (+7) . fromPitch) diff --git a/Reactogon/Reactimation.hs b/Reactogon/Reactimation.hs new file mode 100644 index 0000000..fbe875d --- /dev/null +++ b/Reactogon/Reactimation.hs @@ -0,0 +1,33 @@ +{-# LANGUAGE Arrows #-} + +module Reactimation where + +import Data.Map (Map) +import qualified Data.Map as M +import FRP.Yampa +import Control.Concurrent.MVar + +import MIDI +import Arpeggiated + +mainReact :: IO () +mainReact = reactimate (initialize inRef) (sensing synthRef) actuation mainSF + +initialize :: MVar EventQueue -> IO EventQueue +initialize = readMVar + +sensing :: MVar (SynthState) + -> MVar (Map Time a) + -> Bool + -> IO (DTime, Maybe (Map Time a)) +sensing synthRef inRef _ = do + input <- readMVar inref + synth <- readMVar synthRef + let dt = (fromIntegral $ rate synth)/(fromIntegral $ outBuffSize synth) + return (dt, Just input) + + +actuation = undefined + +mainSF :: (Message a) => SF (Map Time a) (Map Time a) +mainSF = identity diff --git a/Reactogon/Reactogon.hs b/Reactogon/Reactogon.hs index 2656ba6..6f66009 100644 --- a/Reactogon/Reactogon.hs +++ b/Reactogon/Reactogon.hs @@ -1,33 +1,31 @@ module Main where -import qualified MIDI as React +import Auxiliary +import MIDI +import ClientState +--import Reactimation import qualified Sound.JACK as Jack -import qualified Sound.MIDI.Message as MIDI import qualified Sound.JACK.MIDI as JMIDI -{- -import Data.IORef ( IORef - , newIORef - , readIORef - , writeIORef - ) --} -import Control.Concurrent -import qualified Foreign.C.Error as E -import qualified Data.EventList.Relative.TimeBody as EventList -import qualified Data.EventList.Absolute.TimeBody as EventListAbs -import qualified Data.EventList.Relative.TimeMixed as EventListTM -import qualified Control.Monad.Exception.Synchronous as Sync -import qualified Control.Monad.Trans.Class as Trans - +import qualified Sound.MIDI.Message as MIDI import qualified Sound.MIDI.Message.Channel as Channel import qualified Sound.MIDI.Message.Channel.Voice as Voice import qualified Sound.MIDI.Message.Class.Construct as MidiCons +import Control.Concurrent +import Control.Monad +import qualified Control.Monad.Exception.Synchronous as Sync +import qualified Control.Monad.Trans.Class as Trans +import qualified Data.EventList.Absolute.TimeBody as EventListAbs +import qualified Data.EventList.Relative.TimeBody as EventList +import qualified Data.EventList.Relative.TimeMixed as EventListTM +import qualified Foreign.C.Error as E + +import qualified Data.Map as M import FRP.Yampa import Debug.Trace - +{- -- | List of absolute times (at which events should occur) and events. -- We assume that the list is sorted. outLoop :: [(Time,MIDI.T)] @@ -40,6 +38,7 @@ outLoop = concat [[(t,MIDI.Channel $ Channel.Cons , Channel.messageBody = Channel.Voice $ Voice.NoteOff (Voice.toPitch 60) (Voice.toVelocity 100) })] | t <- [0,2..]] +-} reactogonName :: String reactogonName = "Reactogon" @@ -54,48 +53,64 @@ fsPortName :: String fsPortName = "fluidsynth:midi" main = do - stateRef <- newMVar outLoop + inState <- newMVar M.empty + outState <- newMVar M.empty Jack.handleExceptions $ Jack.withClientDefault reactogonName $ \client -> - Jack.withPort client outPortName $ \output -> do - Jack.withProcess client (process client stateRef output) $ + Jack.withPort client outPortName $ \output -> + Jack.withPort client inPortName $ \input -> do + clientState <- Trans.lift $ newEmptyMVar + Jack.withProcess client + (jackLoop client clientState outState input output) $ Jack.withActivation client $ do + --frpid <- Trans.lift $ forkIO mainReact Jack.connect client (reactogonName ++ ":" ++ outPortName) fsPortName Trans.lift $ putStrLn $ "Started " ++ reactogonName Trans.lift $ Jack.waitForBreak -process :: - Jack.Client -> - MVar [(Time,MIDI.T)] -> - JMIDI.Port Jack.Output -> - Jack.NFrames -> - Sync.ExceptionalT E.Errno IO () -process client stateRef output nframes@(Jack.NFrames nframesInt) = - do +jackLoop :: Jack.Client + -> MVar ClientState -- ^ MVar containing the client state (rate and buff size) + -> MVar EventQueue -- ^ MVar containing exiting events + -> JMIDI.Port Jack.Input -- ^ Jack input port + -> JMIDI.Port Jack.Output -- ^ Jack output port + -> Jack.NFrames -- ^ Buffer size for the ports + -> Sync.ExceptionalT E.Errno IO () +jackLoop client clientState outRef input output nframes@(Jack.NFrames nframesInt) = do rate <- Trans.lift $ Jack.getSampleRate client - events <- Trans.lift $ takeMVar stateRef + isEmptyState <- Trans.lift $ isEmptyMVar clientState + let updateClient c v = if isEmptyState then putMVar c v else void $ swapMVar c v + Trans.lift $ updateClient clientState $ ClientState { rate = rate + , buffSize = nframes + } + outEvents <- Trans.lift $ takeMVar outRef lframe <- Trans.lift $ Jack.lastFrameTime client + inEventsT <- JMIDI.readEventsFromPort input nframes let rateD = fromIntegral rate (Jack.NFrames lframeInt) = lframe currentTime = fromIntegral lframeInt / rateD - playableEvents = filter - (\(t,_) -> t - currentTime > - fromIntegral nframesInt / rateD) events - (processableEvents, futureEvents) = break ((> currentTime) . fst) $ - playableEvents + inEvents :: EventQueue + inEvents = M.mapMaybe fromRawMessage $ + M.fromList $ map (\(Jack.NFrames n,e) -> (currentTime + fromIntegral n/rateD, e)) $ + EventListAbs.toPairList inEventsT + playableEvents = M.filterWithKey + (\t _ -> t - currentTime > - fromIntegral nframesInt / rateD) $ + M.union inEvents outEvents + (processableEvents, futureEvents) = breakMap currentTime playableEvents + processableEvents' = M.toList processableEvents Trans.lift $ print currentTime - Trans.lift $ putMVar stateRef futureEvents - if null processableEvents + Trans.lift $ putMVar outRef futureEvents + {-if null processableEvents then Trans.lift $ putStrLn "No events in queue." - else Trans.lift $ putStrLn "Event!" - let firstEventTime = fst $ head processableEvents - Trans.lift $ print $ map ((* rateD) . smartSub currentTime . fst) processableEvents + else Trans.lift $ putStrLn "Event!"-} + let smartSub x y = if x < y then y - x else x - y + (firstTime,_) = head processableEvents' + Trans.lift $ print $ + map ((* rateD) . smartSub firstTime . fst) processableEvents' JMIDI.writeEventsToPort output nframes $ EventListAbs.fromPairList $ - map (\(t,e) -> (Jack.NFrames $ floor $ rateD * smartSub t currentTime, e)) - processableEvents - - -smartSub x y = if x < y then y - x else x - y + map (\(t,e) -> (Jack.NFrames $ floor $ rateD * smartSub t currentTime + , toRawMessage e)) $ + M.toList processableEvents {- else JMIDI.writeEventsToPort output nframes $ diff --git a/Reactogon/Shared.hs b/Reactogon/Shared.hs new file mode 100644 index 0000000..b2f5309 --- /dev/null +++ b/Reactogon/Shared.hs @@ -0,0 +1,30 @@ +module Shared ( inRef + , outRef + , clientRef + ) where + +import ClientState +import MIDI + +import Control.Concurrent.MVar +import Data.Map ( Map + , empty + ) +import FRP.Yampa +import Sound.JACK ( NFrames + ) + +-- | MVar containing all the events given by the input port. +inRef :: IO (MVar EventQueue) +inRef = newMVar empty + +-- | MVar containing all the events to be given to the output port. +outRef :: IO (MVar EventQueue) +outRef = newMVar empty + +-- | MVar containing the state of the machine (JACK client and ports). +clientRef :: Int -> NFrames -> NFrames -> IO (MVar ClientState) +clientRef rate outSize inSize = newMVar $ ClientState { rate = rate + , outSize = outSize + , inSize = inSize + } -- 2.47.2