tidal-midi-0.8: Sound/Tidal/MIDI/Stream.hs
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