module Sound.Tidal.MIDI.Stream (midiStream, midiBackend, midiState, midiSetters, midiDevices,send) where import Control.Monad.Trans.Maybe -- generics import qualified Data.Map as Map import Data.List (sortBy) import Data.Maybe import Data.Ord (comparing) import Data.Time (getCurrentTime, UTCTime, diffUTCTime) import Data.Time.Clock.POSIX import Control.Concurrent import Control.Concurrent.MVar import Data.Bits import Foreign.C import Control.Applicative import Numeric -- Tidal specific import Sound.Tidal.Tempo (Tempo, cps, clockedTick) import Sound.Tidal.Stream as S import Sound.Tidal.Utils import Sound.Tidal.Time import Sound.Tidal.Transition (transition) -- MIDI specific import Sound.Tidal.MIDI.Device import Sound.Tidal.MIDI.Control import qualified Sound.PortMidi as PM type ConnectionCount = Int type TickedConnectionCount = Int type OutputOnline = Bool type OutputState = (TickedConnectionCount, ConnectionCount, OutputOnline) type MIDITime = (Tempo, Int, Double, Double) type MIDIEncoder = CULong -> PM.PMEvent type MIDIEvent = (MIDITime, MIDIEncoder) data Output = Output { conn :: PM.PMStream, buffer :: MVar [MIDIEvent], bufferstate :: MVar OutputState } type MidiMap = Map.Map S.Param (Maybe Int) type MidiDeviceMap = Map.Map String Output toMidiEvent :: ControllerShape -> S.Param -> Value -> Maybe Int toMidiEvent s p (VF x) = ($) <$> mscale <*> mrange <*> pure x where mrange = fmap range mcc mscale = fmap scalef mcc mcc = paramN s p toMidiEvent s p (VI x) = Just x toMidiEvent s p (VS x) = Nothing -- ignore strings for now, we might 'read' them later toMidiMap :: ControllerShape -> S.ParamMap -> MidiMap toMidiMap s m = Map.mapWithKey (toMidiEvent s) (Map.mapMaybe (id) m) send s ch cshape shape change tick o ctrls (tdur:tnote:trest) = midi where midi = sendmidi s cshape ch' (note, vel, dur) (change, tick, o, offset) ctrls note = fromIntegral $ ivalue $ snd tnote dur = realToFrac $ fvalue $ snd tdur (vel, nudge) = case length trest of 2 -> (mkMidi $ trest !! 1, fvalue $ snd $ trest !! 0) 1 -> (mkMidi $ trest !! 0, 0) ch' = fromIntegral ch mkMidi = fromIntegral . floor . (*127) . fvalue . snd offset = ((Sound.Tidal.MIDI.Control.latency cshape) + nudge) mkSend cshape channel s = return $ (\ shape change tick (o,m) -> do let defaulted = (S.applyShape' shape m) -- split ParamMap into Properties and Controls mpartition = fmap (Map.partitionWithKey (\k _ -> (name k) `elem` ["dur", "n", "velocity", "nudge"])) defaulted props = fmap fst mpartition ctrls = fmap snd mpartition props' = fmap (Map.toAscList) $ fmap (Map.mapMaybe (id)) props -- only send explicitly set Control values ctrls' = fmap (Map.filterWithKey (\k v -> v /= (defaultValue k))) ctrls ctrls'' = fmap (toMidiMap cshape) ctrls' send' = fmap (send s channel cshape shape change tick o) ctrls'' ($) <$> send' <*> props' ) connected cshape channel name s = do putStrLn ("Successfully initialized Device '" ++ name ++ "'") changeState goOnline s mkSend cshape channel s failed di err = do error (show err ++ ": " ++ show di) notfound name = do putStrLn "List of Available Device Names" putStrLn =<< displayOutputDevices error ("Device '" ++ show name ++ "' not found") readState f o = do s <- readMVar $ bufferstate o let fs = f s (ticked, conns, online) = s return fs isCycling (0, conns, True) = True isCycling _ = False displayState (ticked, conns, online) = show ticked ++ "/" ++ show conns ++ "[" ++ show online ++ "]" changeState f o = do bs <- takeMVar stateM let fs = f bs (ticked, conns, online) = fs putMVar stateM $ fs where stateM = bufferstate o goOnline (ticked, conns, online) = (ticked, conns, True) addConnection (ticked, conns, online) = (ticked, conns + 1, online) tickConnections (ticked, conns, online) = ((ticked + 1) `mod` conns, conns, online) useOutput outsM name lat = do outs <- readMVar outsM -- blocks let outM = Map.lookup name outs -- maybe -- if we have a valid output by now, return case outM of Just o -> do putStrLn "Cached Device Output" changeState addConnection o -- blocks return $ Just o Nothing -> do -- otherwise open a new output and store the result in the mvar devidM <- (>>= maybe (failed name "Failed opening MIDI Output Device ID") return) (getIDForDeviceName name) econn <- outputDevice devidM lat -- either case econn of Left o -> do changeState addConnection o swapMVar outsM $ Map.insert name o outs return $ Just o Right _ -> return Nothing makeConnection :: MVar (MidiDeviceMap) -> String -> Int -> ControllerShape -> IO ((S.ToMessageFunc), Output) makeConnection devicesM deviceName channel cshape = do moutput <- useOutput devicesM deviceName 1 case moutput of Just o -> do s <- connected cshape channel deviceName o return (s, o) Nothing -> --failed o error "Failed" showLate :: (CULong, Double, PM.PMEvent, CULong, UTCTime) -> String showLate (o, t, e, m, n) = unwords ["late", (show $ (\x -> [PM.status x, PM.data1 x, PM.data2 x]) $ PM.decodeMsg $ PM.message e), "midi now ", show m, " midi onset: ", show o, "onset (relative): ", show $ showFFloat (Just 3) (t - (realToFrac $ utcTimeToPOSIXSeconds n)) "", ", sched: ", show $ PM.timestamp e] -- should only send out events if all connections have ticked flushBackend :: Output -> S.Shape -> Tempo -> Int -> IO () flushBackend o shape change ticks = do changeState tickConnections o cycling <- readState isCycling o case cycling of True -> do late <- sendevents o shape change ticks let len = length late case len of 0 -> return () _ -> do putStrLn $ showLate $ head late putStrLn $ "and " ++ show (len - 1) ++ " more" False -> do s <- readState displayState o return () midiDevices :: IO (MVar (MidiDeviceMap)) midiDevices = do newMVar $ Map.fromList [] midiBackend d n c cs = do (s, o) <- makeConnection d n c cs return $ Backend s (flushBackend o) midiStream d n c s = do backend <- midiBackend d n c s stream backend (toShape s) midiState d n c s = do backend <- midiBackend d n c s S.state backend (toShape s) midiSetters :: MVar (MidiDeviceMap) -> String -> Int -> ControllerShape -> IO Time -> IO (ParamPattern -> IO (), (Time -> [ParamPattern] -> ParamPattern) -> ParamPattern -> IO ()) midiSetters d n c s getNow = do ds <- midiState d n c s return (setter ds, transition getNow ds) toDescriptor midiTime now (o,m,t,e) = (o,t,e, midiTime, now) calcOnsets (a@(tempo, tick, onset, offset), e) = (a, logicalOnset' tempo tick onset offset, e) showEvent :: PM.PMEvent -> String showEvent e = show t ++ " " ++ show msg where msg = PM.decodeMsg $ PM.message e t = PM.timestamp e showRawEvent :: (CULong, MIDITime, Double, PM.PMEvent) -> String showRawEvent (midionset, (tempo,tick,onset,offset), logicalOnset, e) = show onset ++ " " ++ " / " ++ show logicalOnset ++ " " ++ showEvent e sendevents :: Output -> S.Shape -> Tempo -> Int -> IO ([(CULong, Double, PM.PMEvent, CULong, UTCTime)]) sendevents stream shape change ticks = do let buf = buffer stream output = conn stream buf' <- tryTakeMVar buf case buf' of Nothing -> do return [] Just [] -> do -- make sure we put back an empty buffer putMVar buf [] return [] (Just evts@(x:xs)) -> do midiTime <- PM.time now <- getCurrentTime let offset = S.latency shape nextTick = logicalOnset' change (ticks+1) 0 offset onsets = map calcOnsets evts -- split into events sent now and later (e.g. a noteOff that would otherwise cut off noteOn's in the next tick) (evts', later) = span ((< nextTick).(\(_,o,_) -> o)) $ sortBy (comparing (\(_,o,_) -> o)) onsets -- calculate MIDI time to schedule events, putting time into fn to create PM.PMEvents evts'' = map (\(t, o, e) -> let midionset = scheduleTime midiTime now o in (midionset, t, o, e midionset)) evts' later' = map (\(t,o,e) -> (t,e)) later evts''' = map (\(_,_,_,e) -> e) evts'' -- filter events that are too late late = map (toDescriptor midiTime now) $ filter (\(_,_,t,_) -> t < (realToFrac $ utcTimeToPOSIXSeconds now)) $ evts'' -- write events for this tick to stream err <- PM.writeEvents output evts''' -- store later events for nextTick putMVar buf later' case err of PM.NoError -> do -- return events for logging in outer scope return late e -> do putStrLn "sending failed" return [] sendctrls :: Output -> ControllerShape -> CLong -> MIDITime -> MidiMap -> IO () sendctrls stream shape ch t ctrls = do let ctrls' = filter ((>=0) . snd) $ Map.toList $ Map.mapMaybe (id) ctrls sequence_ $ map (\(param, ctrl) -> makeCtrl stream ch (fromJust $ paramN shape param) (fromIntegral ctrl) t) ctrls' -- FIXME: we should be sure param has ControlChange return () sendnote :: Output -> t -> CLong -> (CLong, CLong, Double) -> MIDITime -> IO () sendnote stream shape ch (note,vel, dur) (tempo,tick,onset,offset) = do noteOn stream ch midinote vel (tempo,tick,onset,offset) noteOff stream ch midinote (tempo, tick, onset, offset + dur) return () where midinote = note + 60 scheduleTime :: CULong -> UTCTime -> Double -> CULong scheduleTime mnow' now' logicalOnset = t where now = realToFrac $ utcTimeToPOSIXSeconds $ now' mnow = fromIntegral mnow' t = floor $ mnow + (1000 * (logicalOnset - now)) -- 1 second are 1000 microseconds as is the unit of timestamps in PortMidi sendmidi :: Output -> ControllerShape -> CLong -> (CLong, CLong, Double) -> MIDITime -> MidiMap -> IO () sendmidi stream shape ch n t ctrls = do sendmidi' stream shape ch n t ctrls return () sendmidi' stream shape ch (128,vel,dur) t ctrls = do sendctrls stream shape ch t ctrls return () sendmidi' stream shape ch (note,vel,dur) t ctrls = do sendnote stream shape ch (note,vel,dur) t sendctrls stream shape ch t ctrls return () -- MIDI Utils encodeChannel :: (Bits a, Num a) => a -> a -> a encodeChannel ch cc = (((-) ch 1) .|. cc) -- MIDI Messages noteOn :: Output -> CLong -> CLong -> CLong -> MIDITime -> IO (Maybe a) noteOn o ch val vel t = do let evt = (t, \t' -> makeEvent 0x90 val ch vel t') sendEvent o evt noteOff :: Output -> CLong -> CLong -> MIDITime -> IO (Maybe a) noteOff o ch val t = do let evt = (t, \t' -> makeEvent 0x80 val ch 60 t') sendEvent o evt makeCtrl :: Output -> CLong -> ControlChange -> CLong -> MIDITime -> IO (Maybe a) makeCtrl o ch (CC {midi=midi, range=range}) n t = makeCC o ch (fromIntegral midi) n t makeCtrl o ch (NRPN {midi=midi, range=range}) n t = makeNRPN o ch (fromIntegral midi) n t -- This is sending CC makeCC :: Output -> CLong -> CLong -> CLong -> MIDITime -> IO (Maybe a) makeCC o ch c n t = do let evt = (t, \t' -> makeEvent 0xB0 c ch n t') sendEvent o evt -- This is sending NRPN makeNRPN :: Output -> CLong -> CLong -> CLong -> MIDITime -> IO (Maybe a) makeNRPN o ch c n t = do let nrpn = makeEvent 0xB0 evts = [(t, (\t' -> nrpn 0x63 ch (shift (c .&. 0x3F80) (-7)) t')), (t, (\t' -> nrpn 0x62 ch (c .&. 0x7F) t')), (t, (\t' -> nrpn 0x06 ch (shift (n .&. 0x3F80) (-7)) t')), (t, (\t' -> nrpn 0x26 ch (n .&. 0x7F) t')) ] mapM (sendEvent o) evts return Nothing -- Port Midi Wrapper outputDevice :: PM.DeviceID -> Int -> IO (Either Output PM.PMError) outputDevice deviceID latency = do PM.initialize now <- getCurrentTime result <- PM.openOutput deviceID latency bs <- newMVar (0, 0, False) case result of Left dev -> do info <- PM.getDeviceInfo deviceID putStrLn ("Opened: " ++ show (PM.interface info) ++ ": " ++ show (PM.name info)) buffer <- newMVar [] return (Left Output { conn=dev, buffer=buffer, bufferstate=bs }) Right err -> return (Right err) makeEvent :: CLong -> CLong -> CLong -> CLong -> CULong -> PM.PMEvent makeEvent st n ch v t = PM.PMEvent msg (t) where msg = PM.encodeMsg $ PM.PMMsg (encodeChannel ch st) (n) (v) sendEvent :: Output -> (MIDITime, (CULong -> PM.PMEvent)) -> IO (Maybe a) sendEvent o evt = do let buf = buffer o cbuf <- takeMVar buf putMVar buf (cbuf ++ [evt]) return Nothing