diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
 # RtMidi
 
+[![CircleCI](https://circleci.com/gh/riottracker/RtMidi/tree/master.svg?style=svg)](https://circleci.com/gh/riottracker/RtMidi/tree/master)
+
 Haskell wrapper for [RtMidi](http://www.music.mcgill.ca/~gary/rtmidi/), the lightweight, cross-platform MIDI I/O library.
 
 ## Development
@@ -24,7 +26,7 @@
     make docker-repl
 
     # Inside the docker image:
-    cabal update && cabal build -fjack
+    cabal update && cabal build
 
 (Note that you can't use any `RtMidi` functions in the containerized env unless you are running a Linux host, and
 even then you'd probably have to start the process with something like `docker run --device /dev/snd`.)
diff --git a/RtMidi.cabal b/RtMidi.cabal
--- a/RtMidi.cabal
+++ b/RtMidi.cabal
@@ -1,5 +1,5 @@
 name:                RtMidi
-version:             0.2.0.0
+version:             0.3.0.0
 synopsis:            Haskell wrapper for RtMidi, the lightweight, cross-platform MIDI I/O library.
 description:         Please see the README on GitHub at <https://github.com/riottracker/RtMidi#readme>
 category:            Sound
@@ -19,27 +19,15 @@
 license:             MIT
 
 Flag alsa {
-  Description:  Enable ALSA sequencer api
+  Description:  Enable ALSA sequencer api on Linux
   Default:      True
 }
 
 Flag jack {
-  Description:  Enable JACK api
+  Description:  Enable JACK api on Linux or OSX
   Default:      False
 }
 
-Flag core {
-  Description:  Enable CoreMIDI api
-  Default:      True
-}
-
--- TODO(ejconlon) Add windows support
--- Flag mm {
---   Description:  Enable Windows Multimedia Library api
---   Default:      False
--- }
-
-
 library
   exposed-modules:     Sound.RtMidi
   other-modules:       Sound.RtMidi.Foreign
@@ -50,14 +38,26 @@
   extra-libraries:     stdc++
   c-sources:           rtmidi/RtMidi.cpp
                        rtmidi/rtmidi_c.cpp
-  if flag(alsa) && os(linux)
-    cc-options:       -D__LINUX_ALSA__
-    extra-libraries:  asound pthread
-  if flag(jack) && (os(linux) || os(darwin))
-    cc-options:       -D__UNIX_JACK__
-    extra-libraries:  jack
-  if flag(core) && os(darwin)
-    cc-options:       -D__MACOSX_CORE__
+
+  -- TODO(ejconlon) Add windows support
+
+  if os(linux)
+    if flag(alsa) && flag(jack)
+      cc-options:       -D__LINUX_ALSA__ -D__UNIX_JACK__
+      extra-libraries:  asound pthread jack
+    if flag(alsa) && !flag(jack)
+      cc-options:       -D__LINUX_ALSA__
+      extra-libraries:  asound pthread
+    if !flag(alsa) && flag(jack)
+      cc-options:       -D__UNIX_JACK__
+      extra-libraries:  jack
+
+  if os(darwin)
+    if flag(jack)
+      cc-options:       -D__MACOSX_CORE__ -D__UNIX_JACK__
+      extra-libraries:  jack
+    else
+      cc-options:       -D__MACOSX_CORE__
     frameworks:       CoreMIDI CoreAudio CoreFoundation
     -- NOTE(ejconlon) This is to make the c ffi wrapper actually catch
     -- the c++ exceptions instead of simply aborting.
@@ -65,7 +65,6 @@
     -- avoid https://gitlab.haskell.org/ghc/ghc/issues/11829
     ld-options:        -Wl,-keep_dwarf_unwind
     ghc-options:       -pgmc=clang++
-  -- TODO(ejconlon) Add windows support
 
 executable rtmidi-callback
   main-is:            callback.hs
diff --git a/Sound/RtMidi.hs b/Sound/RtMidi.hs
--- a/Sound/RtMidi.hs
+++ b/Sound/RtMidi.hs
@@ -18,6 +18,7 @@
   , defaultInput
   , createInput
   , setCallback
+  , setForeignCallback
   , cancelCallback
   , ignoreTypes
   , getMessage
@@ -25,18 +26,18 @@
   , defaultOutput
   , createOutput
   , sendMessage
-  , closeDevice
   , currentApi
   ) where
 
 import Control.Exception (Exception, throwIO)
-import Control.Monad (unless)
+import Control.Monad (unless, void)
 import Control.Monad.IO.Class (MonadIO (..))
 import Control.Monad.IO.Unlift (MonadUnliftIO (..))
 import Data.Coerce (coerce)
 import Data.Word (Word8)
 import Foreign (FunPtr, Ptr, Storable (..), alloca, allocaArray, nullPtr, peekArray, with, withArrayLen)
 import Foreign.C (CDouble (..), CInt (..), CSize, CString, CUChar (..), peekCString, withCString)
+import Foreign.ForeignPtr (ForeignPtr, newForeignPtr, withForeignPtr)
 import Sound.RtMidi.Foreign
 
 -- The default message size (in bytes) expected from 'getMessage'
@@ -51,7 +52,7 @@
   | OutputDeviceType
   deriving (Eq, Show)
 
-newtype Device = Device { unDevice :: Ptr Wrapper }
+newtype Device = Device { unDevice :: ForeignPtr Wrapper }
   deriving (Eq, Show)
 
 -- | Generalizes 'InputDevice' and 'OutputDevice' for use in common functions
@@ -59,9 +60,6 @@
   toDevice :: d -> Device
   getDeviceType :: d -> DeviceType
 
-toDevicePtr :: IsDevice d => d -> Ptr Wrapper
-toDevicePtr = unDevice . toDevice
-
 -- | A handle to a device to be used for input
 newtype InputDevice = InputDevice { unInputDevice :: Device }
   deriving (Eq, Show)
@@ -70,6 +68,9 @@
   toDevice = unInputDevice
   getDeviceType _ = InputDeviceType
 
+newInputDevice :: Ptr Wrapper -> IO InputDevice
+newInputDevice = fmap (InputDevice . Device) . newForeignPtr rtmidi_in_free
+
 -- | A handle to a device to be used for input
 newtype OutputDevice = OutputDevice { unOutputDevice :: Device }
   deriving (Eq, Show)
@@ -78,6 +79,9 @@
   toDevice = unOutputDevice
   getDeviceType _ = OutputDeviceType
 
+newOutputDevice :: Ptr Wrapper -> IO OutputDevice
+newOutputDevice = fmap (OutputDevice . Device) . newForeignPtr rtmidi_out_free
+
 -- | Enum of RtMidi-supported APIs
 data Api
   = UnspecifiedApi
@@ -89,6 +93,10 @@
   | DummyApi
   deriving (Eq, Show)
 
+instance Bounded Api where
+  minBound = UnspecifiedApi
+  maxBound = DummyApi
+
 instance Enum Api where
   fromEnum UnspecifiedApi = 0
   fromEnum CoreMidiApi = 1
@@ -103,10 +111,6 @@
   toEnum 4 = MultimediaApi
   toEnum 5 = DummyApi
 
--- | Check if a device is ok
-ready :: (MonadIO m, IsDevice d) => d -> m Bool
-ready = liftIO . fmap ok . peek  . toDevicePtr
-
 -- | An internal RtMidi error
 newtype Error = Error String deriving (Eq, Show)
 instance Exception Error
@@ -119,15 +123,26 @@
     e <- peekCString (msg w)
     throwIO (Error e)
 
+-- Operate on the underlying device ptr
+withDevicePtrUnguarded :: IsDevice d => d -> (Ptr Wrapper -> IO a) -> IO a
+withDevicePtrUnguarded = withForeignPtr . unDevice . toDevice
+
+-- Operate on the underlying device ptr and guard for errors
+withDevicePtr :: IsDevice d => d -> (Ptr Wrapper -> IO a) -> IO a
+withDevicePtr d f = withDevicePtrUnguarded d (\dptr -> f dptr <* guardError dptr)
+
+-- | Check if a device is ok
+ready :: (MonadIO m, IsDevice d) => d -> m Bool
+ready d = liftIO (withDevicePtr d (fmap ok . peek))
+
 -- | A static function to determine MIDI 'Api's built in.
 compiledApis :: MonadIO m => m [Api]
 compiledApis = liftIO $ do
-  n <- fmap fromIntegral (rtmidi_get_compiled_api nullPtr)
-  as <- allocaArray n $ flip with $ \ptr -> do
-    rtmidi_get_compiled_api ptr
-    x <- peek ptr
-    peekArray n x
-  pure (map (toEnum . fromEnum) as)
+  n <- fmap fromIntegral (rtmidi_get_compiled_api nullPtr 0)
+  as <- allocaArray n $ \ptr -> do
+    rtmidi_get_compiled_api ptr (fromIntegral n)
+    peekArray n ptr
+  pure (map (toEnum . fromIntegral) as)
 
 -- | Open a MIDI connection
 openPort :: (MonadIO m, IsDevice d)
@@ -135,42 +150,32 @@
          -> Int          -- ^ port number
          -> String       -- ^ name for the application port that is used
          -> m ()
-openPort d n name = liftIO $ do
-  let dptr = toDevicePtr d
+openPort d n name = liftIO $ withDevicePtr d $ \dptr ->
   withCString name (rtmidi_open_port dptr (toEnum n))
-  guardError dptr
 
 -- | This function creates a virtual MIDI output port to which other software applications can connect.
 --
 -- This type of functionality is currently only supported by the Macintosh OS X, Linux ALSA and JACK APIs
 -- (the function does nothing with the other APIs).
 openVirtualPort :: (MonadIO m, IsDevice d) => d -> String -> m ()
-openVirtualPort d name = liftIO $ do
-  let dptr = toDevicePtr d
+openVirtualPort d name = liftIO $ withDevicePtr d $ \dptr -> do
   withCString name (rtmidi_open_virtual_port dptr)
-  guardError dptr
 
 -- | Close an open MIDI connection (if one exists).
 closePort :: (MonadIO m, IsDevice d) => d -> m ()
-closePort d = liftIO $ do
-  let dptr = toDevicePtr d
-  rtmidi_close_port dptr
-  guardError dptr
+closePort d = liftIO (withDevicePtr d rtmidi_close_port)
 
 -- | Return the number of MIDI ports available to the 'Device'.
 portCount :: (MonadIO m, IsDevice d) => d -> m Int
-portCount d = liftIO$ do
-  let dptr = toDevicePtr d
+portCount d = liftIO $ withDevicePtr d $ \dptr -> do
   x <- rtmidi_get_port_count dptr
-  guardError dptr
   pure (fromIntegral x)
 
 -- | Return a string identifier for the specified MIDI port number.
 --
 -- 'Nothing' is returned if an invalid port specifier is provided.
 portName :: (MonadIO m, IsDevice d) => d -> Int -> m (Maybe String)
-portName d n = liftIO $ do
-  let dptr = toDevicePtr d
+portName d n = liftIO $ withDevicePtrUnguarded d $ \dptr -> do
   x <- rtmidi_get_port_name dptr (toEnum n)
   guardError dptr
   s <- peekCString x
@@ -220,7 +225,7 @@
 defaultInput = liftIO $ do
   dptr <- rtmidi_in_create_default
   guardError dptr
-  pure (InputDevice (Device dptr))
+  newInputDevice dptr
 
 -- | Create a new 'Device' to use for input.
 createInput :: MonadIO m
@@ -229,9 +234,9 @@
             -> Int        -- ^ size of the MIDI input queue
             -> m InputDevice
 createInput api clientName queueSizeLimit = liftIO $ do
-  dptr <- withCString clientName (\str -> rtmidi_in_create (toEnum $ fromEnum api) str (toEnum queueSizeLimit))
+  dptr <- withCString clientName (\str -> rtmidi_in_create (fromIntegral (fromEnum api)) str (fromIntegral queueSizeLimit))
   guardError dptr
-  pure (InputDevice (Device dptr))
+  newInputDevice dptr
 
 foreign import ccall "wrapper"
   mkCallbackPointer :: (CDouble -> Ptr CUChar -> CInt -> Ptr () -> IO ()) -> IO (FunPtr (CDouble -> Ptr CUChar -> CInt -> Ptr () -> IO ()))
@@ -248,20 +253,27 @@
             => InputDevice
             -> (Double -> [Word8] -> m ())  -- ^ Function that takes a timestamp and a MIDI message as arguments
             -> m ()
-setCallback d c = withRunInIO $ \run -> do
-  let dptr = toDevicePtr d
+setCallback d c = withRunInIO $ \run -> withDevicePtr d $ \dptr -> do
   f <- mkCallbackPointer (adaptCallbackCTypes (\ts bytes -> run (c ts bytes)))
   rtmidi_in_set_callback dptr f nullPtr
-  guardError dptr
 
+-- | Set a /foreign/ callback function to be invoked for incoming MIDI messages.
+--
+-- This variant allows you to set the callback to a C function pointer so we're not forced
+-- to enter a Haskell wrapper every invocation.
+setForeignCallback :: MonadIO m
+                   => InputDevice
+                   -> FunPtr (CDouble -> Ptr CUChar -> CInt -> Ptr () -> IO ())
+                   -> Ptr () -- ^ Pointer to context that will be passed into the callback
+                   -> m ()
+setForeignCallback d f ctx = liftIO $ withDevicePtr d $ \dptr ->
+  rtmidi_in_set_callback dptr f ctx
+
 -- | Cancel use of the current callback function (if one exists).
 --
 -- Subsequent incoming MIDI messages will be written to the queue and can be retrieved with the `getMessage` function.
 cancelCallback :: MonadIO m => InputDevice -> m ()
-cancelCallback d = liftIO $ do
-  let dptr = toDevicePtr d
-  rtmidi_in_cancel_callback dptr
-  guardError dptr
+cancelCallback d = liftIO (withDevicePtr d rtmidi_in_cancel_callback)
 
 -- | Specify whether certain MIDI message types should be queued or ignored during input.
 --
@@ -274,12 +286,11 @@
             -> Bool       -- ^ Time messages
             -> Bool       -- ^ Sense messages
             -> m ()
-ignoreTypes d x y z = liftIO (rtmidi_in_ignore_types (toDevicePtr d) x y z)
+ignoreTypes d x y z = liftIO (withDevicePtrUnguarded d (\dptr -> rtmidi_in_ignore_types dptr x y z))
 
 -- | Variant of 'getMessage' that allows you to set message buffer size (typically for large sysex messages).
 getMessageSized :: MonadIO m => InputDevice -> Int -> m (Double, [Word8])
-getMessageSized d n = liftIO $ alloca $ \s -> allocaArray n $ flip with $ \m -> do
-  let dptr = toDevicePtr d
+getMessageSized d n = liftIO $ alloca $ \s -> allocaArray n $ flip with $ \m -> withDevicePtrUnguarded d $ \dptr -> do
   poke s (fromIntegral n)
   CDouble timestamp <- rtmidi_in_get_message dptr m s
   guardError dptr
@@ -306,7 +317,7 @@
 defaultOutput = liftIO $ do
   dptr <- rtmidi_out_create_default
   guardError dptr
-  pure (OutputDevice (Device dptr))
+  newOutputDevice dptr
 
 -- | Create a new 'Device' to use for output.
 createOutput :: MonadIO m
@@ -314,32 +325,20 @@
              -> String     -- ^ client name
              -> m OutputDevice
 createOutput api clientName = liftIO $ do
-  dptr <- withCString clientName (rtmidi_out_create (toEnum (fromEnum api)))
+  dptr <- withCString clientName (rtmidi_out_create (fromIntegral (fromEnum api)))
   guardError dptr
-  pure (OutputDevice (Device dptr))
+  newOutputDevice dptr
 
 -- | Immediately send a single message out an open MIDI output port.
 sendMessage :: MonadIO m => OutputDevice -> [Word8] -> m ()
-sendMessage d m = liftIO $ withArrayLen m $ \n ptr -> do
-  let dptr = toDevicePtr d
-  rtmidi_out_send_message dptr (coerce ptr) (fromIntegral n)
-  guardError dptr
-
--- | If a MIDI connection is still open, it will be closed
-closeDevice :: MonadIO m => IsDevice d => d -> m ()
-closeDevice d = liftIO $
-  let dptr = toDevicePtr d
-  in case getDeviceType d of
-    InputDeviceType -> rtmidi_in_free dptr
-    OutputDeviceType -> rtmidi_out_free dptr
+sendMessage d m = liftIO $ withArrayLen m $ \n ptr -> withDevicePtr d $ \dptr ->
+  void (rtmidi_out_send_message dptr (coerce ptr) (fromIntegral n))
 
 -- | Returns the specifier for the MIDI 'Api' in use
 currentApi :: MonadIO m => IsDevice d => d -> m Api
-currentApi d = liftIO $ do
-  let dptr = toDevicePtr d
+currentApi d = liftIO $ withDevicePtr d $ \dptr -> do
   res <-
     case getDeviceType d of
       InputDeviceType -> rtmidi_in_get_current_api dptr
       OutputDeviceType -> rtmidi_out_get_current_api dptr
-  guardError dptr
-  pure (toEnum (fromEnum res))
+  pure (toEnum (fromIntegral res))
diff --git a/Sound/RtMidi/Foreign.hsc b/Sound/RtMidi/Foreign.hsc
--- a/Sound/RtMidi/Foreign.hsc
+++ b/Sound/RtMidi/Foreign.hsc
@@ -28,7 +28,7 @@
 #include "rtmidi_c.h"
 
 import Foreign (FunPtr, Ptr, Storable (..))
-import Foreign.C (CDouble (..), CInt (..), CString, CSize, CUChar)
+import Foreign.C (CDouble (..), CInt (..), CString, CSize, CUChar, CUInt (..))
 
 data Wrapper = Wrapper
   { ptr :: !(Ptr ())
@@ -52,32 +52,35 @@
     d <- #{peek struct RtMidiWrapper, msg} ptr
     pure (Wrapper a b c d)
 
+-- A parameter we'll be de/serializing from the 'Api' enum.
+type ApiEnum = CInt
+
 foreign import ccall "rtmidi_c.h rtmidi_close_port"
   rtmidi_close_port :: Ptr Wrapper -> IO ()
 
 foreign import ccall "rtmidi_c.h rtmidi_get_compiled_api"
-  rtmidi_get_compiled_api :: Ptr (Ptr CInt) -> IO CInt
+  rtmidi_get_compiled_api :: Ptr ApiEnum -> CUInt -> IO CInt
 
 foreign import ccall "rtmidi_c.h rtmidi_get_port_count"
-  rtmidi_get_port_count :: Ptr Wrapper -> IO CInt
+  rtmidi_get_port_count :: Ptr Wrapper -> IO CUInt
 
 foreign import ccall "rtmidi_c.h rtmidi_get_port_name"
-  rtmidi_get_port_name :: Ptr Wrapper -> CInt -> IO CString
+  rtmidi_get_port_name :: Ptr Wrapper -> CUInt -> IO CString
 
 foreign import ccall "rtmidi_c.h rtmidi_in_cancel_callback"
   rtmidi_in_cancel_callback :: Ptr Wrapper -> IO ()
 
 foreign import ccall "rtmidi_c.h rtmidi_in_create"
-  rtmidi_in_create :: CInt -> CString -> CInt -> IO (Ptr Wrapper)
+  rtmidi_in_create :: ApiEnum -> CString -> CUInt -> IO (Ptr Wrapper)
 
 foreign import ccall "rtmidi_c.h rtmidi_in_create_default"
   rtmidi_in_create_default :: IO (Ptr Wrapper)
 
-foreign import ccall "rtmidi_c.h rtmidi_in_free"
-  rtmidi_in_free :: Ptr Wrapper -> IO ()
+foreign import ccall "rtmidi_c.h &rtmidi_in_free"
+  rtmidi_in_free :: FunPtr (Ptr Wrapper -> IO ())
 
 foreign import ccall "rtmidi_c.h rtmidi_in_get_current_api"
-  rtmidi_in_get_current_api :: Ptr Wrapper -> IO CInt
+  rtmidi_in_get_current_api :: Ptr Wrapper -> IO ApiEnum
 
 foreign import ccall "rtmidi_c.h rtmidi_in_get_message"
   rtmidi_in_get_message :: Ptr Wrapper -> Ptr (Ptr CUChar) -> Ptr CSize -> IO CDouble
@@ -89,22 +92,22 @@
   rtmidi_in_set_callback :: Ptr Wrapper -> FunPtr (CDouble -> Ptr CUChar -> CInt -> Ptr () -> IO ()) -> Ptr () -> IO ()
 
 foreign import ccall "rtmidi_c.h rtmidi_open_port"
-  rtmidi_open_port :: Ptr Wrapper -> CInt -> CString -> IO ()
+  rtmidi_open_port :: Ptr Wrapper -> CUInt -> CString -> IO ()
 
 foreign import ccall "rtmidi_c.h rtmidi_open_virtual_port"
   rtmidi_open_virtual_port :: Ptr Wrapper -> CString -> IO ()
 
 foreign import ccall "rtmidi_c.h rtmidi_out_create"
-  rtmidi_out_create :: CInt -> CString -> IO (Ptr Wrapper)
+  rtmidi_out_create :: ApiEnum-> CString -> IO (Ptr Wrapper)
 
 foreign import ccall "rtmidi_c.h rtmidi_out_create_default"
   rtmidi_out_create_default :: IO (Ptr Wrapper)
 
-foreign import ccall "rtmidi_c.h rtmidi_out_free"
-  rtmidi_out_free :: Ptr Wrapper -> IO ()
+foreign import ccall "rtmidi_c.h &rtmidi_out_free"
+  rtmidi_out_free :: FunPtr (Ptr Wrapper -> IO ())
 
 foreign import ccall "rtmidi_c.h rtmidi_out_get_current_api"
-  rtmidi_out_get_current_api :: Ptr Wrapper -> IO CInt
+  rtmidi_out_get_current_api :: Ptr Wrapper -> IO ApiEnum
 
 foreign import ccall "rtmidi_c.h rtmidi_out_send_message"
   rtmidi_out_send_message :: Ptr Wrapper -> Ptr CUChar -> CInt -> IO CInt
diff --git a/examples/callback.hs b/examples/callback.hs
--- a/examples/callback.hs
+++ b/examples/callback.hs
@@ -1,5 +1,5 @@
 import Data.Word (Word8)
-import Sound.RtMidi (closeDevice, closePort, defaultInput, openPort, setCallback)
+import Sound.RtMidi (closePort, defaultInput, openPort, setCallback)
 import Numeric (showHex)
 
 callback :: Double -> [Word8] -> IO ()
@@ -12,5 +12,4 @@
   setCallback i callback
   _ <- getLine
   closePort i
-  closeDevice i
   return ()
diff --git a/examples/playback.hs b/examples/playback.hs
--- a/examples/playback.hs
+++ b/examples/playback.hs
@@ -1,4 +1,4 @@
-import Sound.RtMidi (closeDevice, closePort, defaultOutput, openPort, sendMessage, portCount, portName)
+import Sound.RtMidi (closePort, defaultOutput, openPort, sendMessage, portCount, portName)
 import Control.Concurrent (threadDelay)
 
 main :: IO ()
@@ -18,4 +18,3 @@
   mapM_ (\x -> sendMessage device [0x90, x, 0x7f] >> threadDelay 120000) $ take 240 song
   putStrLn "exiting..."
   closePort device
-  closeDevice device
diff --git a/examples/poll.hs b/examples/poll.hs
--- a/examples/poll.hs
+++ b/examples/poll.hs
@@ -1,4 +1,4 @@
-import Sound.RtMidi (InputDevice, closeDevice, closePort, defaultInput, getMessage, openPort)
+import Sound.RtMidi (InputDevice, closePort, defaultInput, getMessage, openPort)
 import Control.Concurrent (forkIO, killThread)
 import Control.Monad (forever)
 
@@ -17,5 +17,4 @@
   _ <- getLine
   killThread id
   closePort i
-  closeDevice i
   return ()
diff --git a/examples/query.hs b/examples/query.hs
--- a/examples/query.hs
+++ b/examples/query.hs
@@ -1,4 +1,4 @@
-import Sound.RtMidi (closeDevice, compiledApis, createOutput, currentApi, defaultOutput, listPorts)
+import Sound.RtMidi (compiledApis, createOutput, currentApi, defaultOutput, listPorts)
 
 main :: IO ()
 main = do
@@ -9,4 +9,3 @@
   putStrLn $ "RtMidi output using " ++ (show api)
   putStrLn $ "built-in: " ++ (show builtin)
   putStrLn $ "available ports: " ++ (show portPairs)
-  closeDevice device
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1,13 +1,13 @@
 module Main (main) where
 
 import Control.Concurrent (threadDelay)
-import Control.Monad (replicateM_)
+import Control.Monad (replicateM_, unless, when)
 import Data.IORef (IORef, newIORef, readIORef, modifyIORef)
 import Data.List (isInfixOf)
 import Data.Word (Word8)
-import Sound.RtMidi (closeDevice, closePort, defaultInput, defaultOutput, findPort, sendMessage, setCallback, openPort, openVirtualPort)
+import Sound.RtMidi (Api (..), closePort, compiledApis, createInput, createOutput, currentApi, findPort, sendMessage, setCallback, openPort, openVirtualPort)
 import Test.Tasty (TestTree, defaultMain, testGroup)
-import Test.Tasty.HUnit (testCase, (@?=))
+import Test.Tasty.HUnit (assertFailure, testCase, (@?=))
 
 incIORef :: IORef Int -> IO ()
 incIORef = flip modifyIORef succ
@@ -15,21 +15,25 @@
 readerCallback :: IORef Int -> Double -> [Word8] -> IO ()
 readerCallback countRef _ msg = incIORef countRef
 
-testVirtualReadWrite :: TestTree
-testVirtualReadWrite = testCase "virtual read write" $ do
+testVirtualReadWrite :: Api -> TestTree
+testVirtualReadWrite api = testCase ("virtual read write with " <> show api) $ do
   let expectedCount = 3
       -- a simple note-on message
       message = [0x90, 0x51, 0x7f]
       portName = "rtmidi-test-port"
-      -- 0.01 second delay in us
-      delayUs = 10000
+      -- 100 ms delay in us
+      delayUs = 100000
   countRef <- newIORef 0
   -- Create reader with callback
-  inDev <- defaultInput
+  inDev <- createInput api "rtmidi-test-input" 100
+  inApi <- currentApi inDev
+  inApi @?= api
   setCallback inDev (readerCallback countRef)
   openVirtualPort inDev portName
   -- Create writer and connect to reader virtual port
-  outDev <- defaultOutput
+  outDev <- createOutput api "rtmidi-test-output"
+  outApi <- currentApi outDev
+  outApi @?= api
   maybePortNum <- findPort outDev (isInfixOf portName)
   let portNum = maybe (error "Could not find port") id maybePortNum
   openPort outDev portNum portName
@@ -39,13 +43,15 @@
   threadDelay delayUs
   -- Close writer
   closePort outDev
-  closeDevice outDev
   -- Close reader
   closePort inDev
-  closeDevice inDev
   -- Verify number of messages received
   actualCount <- readIORef countRef
   actualCount @?= expectedCount
 
 main :: IO ()
-main = defaultMain (testGroup "RtMidi" [testVirtualReadWrite])
+main = do
+  apis <- compiledApis
+  when (null apis) (assertFailure "No compiled APIs found")
+  let tests = fmap testVirtualReadWrite apis
+  defaultMain (testGroup "RtMidi" tests)
