packages feed

hamid-0.10: src/System/Midi/MacOSX.hs

-- |A lowest common denominator interface to the Win32 and MacOSX Midi bindings, MacOSX part.

module System.Midi.MacOSX
    ( module System.Midi.Base

    , Source
    , Destination
    , Connection
 
    , enumerateSources
    , enumerateDestinations
    
    , MidiHasName(..)  

    , openSource
    , openDestination
    , close
    , send
    , sendSysEx
    , start
    , stop
    
    , getNextEvent
    , getEvents
    , currentTime
    
    ) where

import System.Midi.Base

import Control.Monad
import Control.Concurrent.MVar
import Control.Concurrent.Chan
import Data.List
import Foreign hiding (unsafePerformIO)
import Foreign.StablePtr
import System.IO.Unsafe

import System.MacOSX.CoreFoundation
import System.MacOSX.CoreAudio
import System.MacOSX.CoreMidi

-- |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)

type Client      = MIDIClientRef
type Device      = MIDIDeviceRef
type Port        = MIDIPortRef

-- |The opaque data type representing a Midi connection
data Connection = Connection
    { cn_isInput     :: Bool
    , cn_port        :: MIDIPortRef
    , cn_endpoint    :: MIDIEndpointRef
    , cn_time        :: MVar UInt64     -- measured in nanosecs
    , cn_alive       :: MVar Bool
    , cn_fifo_cb     :: Either (Chan MidiEvent) ClientCallback
    , cn_midiproc    :: FunPtr (MIDIReadProc () ())
    , cn_mydata      :: StablePtr (MVar Connection)
    }

----- automatic client creation 

client = unsafePerformIO $ newEmptyMVar :: MVar Client

{-
#ifdef __GLASGOW_HASKELL__
clientFinalizer :: IO ()
clientFinalizer = do
    c <- readMVar client
    disposeClient c
#endif
-}

getClient :: IO MIDIClientRef
getClient = do
    b <- isEmptyMVar client
    if b 
      then do
        x <- newClient "HaskellMIDI" 
        putMVar client x
{-
#ifdef __GLASGOW_HASKELL__
        addMVarFinalizer client clientFinalizer      -- uh-oh, that's not a good idea (not in the present form)
#endif      
-}
        return x
      else readMVar client

-- |Returns the time elapsed since the last `start` call, in milisecs.
currentTime :: Connection -> IO Word32
currentTime conn = do
    t  <- audioGetCurrentTimeInNanos 
    t0 <- readMVar (cn_time conn)
    return (nanoToMili $ t-t0)

nanoToMili :: UInt64 -> Word32
nanoToMili n = fromIntegral $ div n 1000000

convertShortMessage :: UInt64 -> (MIDITimeStamp,[Word8]) -> IO MidiEvent
convertShortMessage t0 (ts',bytes) = do
    ts <- audioConvertHostTimeToNanos ts'
    return $ MidiEvent (nanoToMili $ ts-t0) (translateShortMessage $ decodeShortMessage bytes) 

myMidiReadProc :: Ptr MIDIPacket -> Ptr () -> Ptr () -> IO ()
myMidiReadProc packets myptr _  = 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
        time0 <- readMVar (cn_time conn)
        list1 <- depackMIDIPacketList packets
        let (normal,sysex') = partition (\(_,bytes) -> isShortMessage bytes) list1
        sysexs <- forM sysex' $ \(ts',bytes) -> do
          ts <- audioConvertHostTimeToNanos ts'
          return $ MidiEvent (nanoToMili $ ts-time0) (SysEx $ tail bytes)
        normals <- mapM (convertShortMessage time0) normal
        let events = sysexs ++ normals
        case (cn_fifo_cb conn) of
          Left  chan -> writeList2Chan chan events 
          Right call -> mapM_ call events 
        putMVar mv conn      -- do not forget to put it back!

-- |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@(Source endpoint) mcallback = do
 
    client <- getClient
    
    myData <- newEmptyMVar :: IO (MVar Connection)
    sp <- newStablePtr myData 
    the_callback <- mkMIDIReadProc myMidiReadProc

    time  <- newEmptyMVar 
    alive <- newMVar True

    fifo_cb <- case mcallback of
      Just cb -> return $ Right cb
      Nothing -> liftM Left $ newChan 

    inport <- newInputPort client "Input Port" the_callback (castStablePtrToPtr sp) 
      
    let conn = Connection True inport endpoint time alive fifo_cb the_callback sp 
    putMVar myData conn
    return conn 

-- |Opens a Midi Destination.
openDestination :: Destination -> IO Connection 
openDestination dst@(Destination endpoint) = do

    client <- getClient 
    outport <- newOutputPort client "Output Port" 
    alive <- newMVar True
    time  <- newEmptyMVar 

    let conn = Connection False outport endpoint time alive undefined undefined undefined 
    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 -> midiSend (cn_port conn) (Destination $ cn_endpoint 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.
sendSysEx :: Connection -> [Word8] -> IO ()
sendSysEx conn dat = midiSendSysEx (cn_endpoint conn) dat 
 
-- |Starts a connection. This is required for receiving Midi messages, and also for starting the clock.
start :: Connection -> IO ()
start conn = do 
    b <- isEmptyMVar (cn_time conn)
    if b
      then do
        hosttime <- audioGetCurrentTimeInNanos
        putMVar (cn_time conn) hosttime
        case cn_isInput conn of 
          True  -> connectToSource (cn_port conn) (Source $ cn_endpoint conn) nullPtr
          False -> return ()
      else putStrLn "warning: you shouldn't call start twice"  

-- |Stops a connection.
stop :: Connection -> IO ()
stop conn = do
    b <- isEmptyMVar (cn_time conn)
    if not b
      then do
        takeMVar (cn_time conn) 
        case cn_isInput conn of 
          True  -> disconnectFromSource (cn_port conn) (Source $ cn_endpoint conn)
          False -> return ()
      else putStrLn "warning: you shouldn't call stop twice"  
    
-- |Closes a Midi Connection
close conn = do
    when (cn_isInput conn) $ do
      b <- isEmptyMVar (cn_time conn)
      when (not b) (stop conn) 
    disposePort (cn_port conn)
    cleanup conn
 
-- called by "close"; not exposed. 
cleanup :: Connection -> IO ()
cleanup conn = case (cn_isInput conn) of
    True  -> do
      freeHaskellFunPtr (cn_midiproc conn)
      freeStablePtr     (cn_mydata   conn)
    False -> return ()