hamid-0.9: src/System/Midi/Win32.hs
-- |A lowest common denominator interface to the Win32 and MacOSX MIDI bindings, Win32 part.
module System.MIDI.Win32
( module System.MIDI.Base
, Source
, Destination
, Connection
, enumerateSources
, enumerateDestinations
, MIDIHasName
, getName
, getModel
, getManufacturer
, openSource
, openDestination
, close
, send
, sendSysEx
, start
, stop
, getNextEvent
, getEvents
, currentTime
) where
import Control.Monad
import Control.Concurrent.MVar
import Control.Concurrent.Chan
import Data.List
import Foreign
import Foreign.StablePtr
import System.IO.Unsafe
import System.Win32.Types
import System.Win32.MIDI
import System.MIDI.Base
-- |Gets all the events from the buffer.
getEvents :: Connection -> IO [MidiEvent]
getEvents conn = do
m <- getNextEvent conn
case m of
Nothing -> return []
Just ev -> do
evs <- getEvents conn
return (ev:evs)
-- |Gets the next event from a buffered connection.
getNextEvent :: Connection -> IO (Maybe MidiEvent)
getNextEvent conn = case cn_fifo_cb conn of
Right _ -> fail "this is not a buffered connection"
Left chan -> do
b <- isEmptyChan chan
if b
then return Nothing
else do
x <- readChan chan
return (Just x)
waitFor :: IO Bool -> IO ()
waitFor check = do
b <- check
unless b $ waitFor check
-- |The opaque data type representing a MIDI connection.
data Connection = Connection
{ cn_isInput :: Bool
, cn_handle :: HMIDI
, cn_time :: MVar Word32 -- measured in milisecs
, cn_fifo_cb :: Either (Chan MidiEvent) ClientCallback
, cn_midiproc :: FunPtr (MIDIINPROC ())
, cn_mydata :: StablePtr (MVar Connection)
, cn_inbuf :: MVar (Ptr MIDIHDR)
, cn_sysex :: Chan Word8 -- channel for temporarily storing sysex messages (they can be arbritrary long, but the buffer has fixed size)
, cn_alive :: MVar Bool
}
-- |Returns the time elapsed since the last `start` call, in milisecs.
currentTime :: Connection -> IO Word32
currentTime conn = do
t <- timeGetTime
t0 <- readMVar (cn_time conn)
return (t-t0)
myMidiCallback :: HMIDIIN -> UINT -> Ptr () -> DWORD -> DWORD -> IO ()
myMidiCallback hmidi msg' myptr param1 param2 = do
let stabptr = castPtrToStablePtr myptr :: StablePtr (MVar Connection)
mv <- deRefStablePtr stabptr :: IO (MVar Connection)
mconn <- tryTakeMVar mv -- we are also "blocking" (handling) further callbacks this way
case mconn of
Nothing -> return ()
Just conn -> do
let msg = mim msg'
case msg of
MIM_DATA -> do
let tmsg = translateShortMessage $ decodeShortMessage param1
let event = MidiEvent param2 tmsg
case (cn_fifo_cb conn) of
Left chan -> writeChan chan event
Right call -> call event
MIM_LONGDATA -> do
let ptr = wordPtrToPtr (fromIntegral param1) :: Ptr MIDIHDR
q <- peek (castPtr ptr ) :: IO (Ptr Word8)
n <- peek (castPtr ptr `plusPtr` 8) :: IO DWORD
dat <- peekArray (fromIntegral n) q
sysexs <- processSysEx (cn_sysex conn) dat
forM_ sysexs $ \dat -> do
let event = MidiEvent param2 (SysEx dat)
case (cn_fifo_cb conn) of
Left chan -> writeChan chan event
Right call -> call event
-- reportedly we are not supposed to call midiInAddBuffer and the like from here,
-- but well, this is the simplest solution and it seems to work pretty well...
-- (may not work on ancient versions of Windows)
b <- isEmptyMVar (cn_time conn) -- not really elegant hack, but the emptyness of this is also used for syncing purposes
unless b $ do -- this is here because midiInReset can block if we want to free them before it returned...
freeMidiInHeader hmidi ptr
new <- midiInAddBuffer hmidi midiInBufferSize
swapMVar (cn_inbuf conn) new
return ()
MIM_CLOSE -> do
swapMVar (cn_alive conn) False
return ()
_ -> return ()
when (msg /= MIM_CLOSE) $ putMVar mv conn -- do not forget !!!
-- I'm not sure, but getChanContents seems to be too lazy for our purposes
readChanList :: Chan a -> IO [a]
readChanList chan = do
b <- isEmptyChan chan
if b
then return []
else do
x <- readChan chan
xs <- readChanList chan
return (x:xs)
-- the Win32 SysEx support is somewhat brain-dead...
processSysEx :: Chan Word8 -> [Word8] -> IO [[Word8]]
processSysEx _ [] = return []
processSysEx chan dat = do
case (findIndex (==0xf7) dat) of
Nothing -> do
writeList2Chan chan dat
return []
Just k -> do
xs <- readChanList chan
let (aa,bb) = splitAt k dat
ys = xs ++ aa
ev = if (head ys == 0xf0) then tail ys else ys
evs <- processSysEx chan (tail bb)
return (ev:evs)
midiInBufferSize = 64
-- |Opens a MIDI source.
-- There are two possibilites to receive MIDI messages. The user can either support a callback function,
-- or get the messages from an asynchronous buffer. However, mixing the two approaches is not allowed.
openSource :: Source -> Maybe ClientCallback -> IO Connection
openSource src mcallback = do
myData <- newEmptyMVar :: IO (MVar Connection)
sp <- newStablePtr myData
the_callback <- mkMIDIPROC myMidiCallback
alive <- newMVar True
fifo_cb <- case mcallback of
Just cb -> return $ Right cb
Nothing -> liftM Left $ newChan
time <- newEmptyMVar
handle <- midiInOpen src the_callback (castStablePtrToPtr sp) (CALLBACK_FUNCTION False)
bufmv <- newEmptyMVar
sysex <- newChan -- channel for temporarily storing sysex messages (they can be arbritrary long, but the buffer has fixed size)
let conn = Connection True handle time fifo_cb the_callback sp bufmv sysex alive
putMVar myData conn
return conn
-- |Opens a MIDI destination.
openDestination :: Destination -> IO Connection
openDestination dst = do
alive <- newMVar True
handle <- midiOutOpen dst nullFunPtr nullPtr CALLBACK_NULL
time <- newEmptyMVar
let conn = Connection False handle time undefined undefined undefined undefined undefined alive
return conn
sendShortMessage :: Connection -> ShortMessage -> IO ()
sendShortMessage conn msg = case (cn_isInput conn) of
True -> fail "sending short messages to midi sources is not supported"
False -> midiOutSend (cn_handle conn) msg
-- |Sends a short message. The connection must be a `Destination`.
send :: Connection -> MidiMessage -> IO ()
send conn msg = sendShortMessage conn (untranslateShortMessage msg)
-- |Sends a System Exclusive message. You shouldn't include the starting/trailing bytes 0xf0 and 0xf7 in the data.
sendSysEx :: Connection -> [Word8] -> IO ()
sendSysEx conn msg = do
let handle = cn_handle conn
case cn_isInput conn of
True -> fail "sending SysEx messages to midi sources is not supported under Win32"
False -> midiOutSendSysEx handle msg
-- |Starts a connection. This is required for receiving MIDI messages, and also for starting the clock.
start :: Connection -> IO ()
start conn = do
let handle = cn_handle conn
b <- isEmptyMVar (cn_time conn)
if b
then do
time <- timeGetTime
putMVar (cn_time conn) time
case cn_isInput conn of
True -> do
buf <- midiInAddBuffer handle midiInBufferSize
putMVar (cn_inbuf conn) buf
midiInStart handle
False -> return ()
else putStrLn "warning: you shouldn't call start twice"
-- |Starts a connection. This is required for receiving MIDI messages, and also for starting the clock.
stop :: Connection -> IO ()
stop conn = do
let handle = cn_handle conn
b <- isEmptyMVar (cn_time conn)
if not b
then do
takeMVar (cn_time conn) -- also used for syncing!
case cn_isInput conn of
True -> do
midiInReset handle
hdr <- takeMVar (cn_inbuf conn)
freeMidiInHeader handle hdr
midiInStop handle
False -> return ()
return ()
else putStrLn "warning: you shouldn't call stop twice"
-- |Resets a `Connection`.
reset :: Connection -> IO ()
reset conn = do
let handle = cn_handle conn
case cn_isInput conn of
True -> midiInReset handle
False -> midiOutReset handle
-- |Closes a `Connection`
close :: Connection -> IO ()
close conn = do
let handle = cn_handle conn
case cn_isInput conn of
True -> midiInClose handle
False -> midiOutClose handle
-- called by "close"
cleanup :: Connection -> IO ()
cleanup conn = case (cn_isInput conn) of
True -> do
waitFor (liftM not $ readMVar $ cn_alive conn)
freeHaskellFunPtr (cn_midiproc conn)
freeStablePtr (cn_mydata conn)
False -> return ()