packages feed

RtMidi 0.3.0.0 → 0.4.0.0

raw patch · 11 files changed

+280/−118 lines, 11 filesdep +deepseqdep +pretty-simpledep +vectornew-component:exe:rtmidi-reportPVP ok

version bump matches the API change (PVP)

Dependencies added: deepseq, pretty-simple, vector

API changes (from Hackage documentation)

- Sound.RtMidi: KernelStreamingApi :: Api
- Sound.RtMidi: instance GHC.Classes.Eq Sound.RtMidi.Api
- Sound.RtMidi: instance GHC.Enum.Bounded Sound.RtMidi.Api
- Sound.RtMidi: instance GHC.Enum.Enum Sound.RtMidi.Api
- Sound.RtMidi: instance GHC.Show.Show Sound.RtMidi.Api
+ Sound.RtMidi: [unError] :: Error -> String
+ Sound.RtMidi: apiDisplayName :: MonadIO m => Api -> m String
+ Sound.RtMidi: apiName :: MonadIO m => Api -> m String
+ Sound.RtMidi: compiledApiByName :: MonadIO m => String -> m Api
+ Sound.RtMidi: getMessageMutable :: MonadIO m => InputDevice -> IOVector Word8 -> m (Double, Int)
+ Sound.RtMidi: instance Control.DeepSeq.NFData Sound.RtMidi.DeviceType
+ Sound.RtMidi: instance Control.DeepSeq.NFData Sound.RtMidi.Error
+ Sound.RtMidi: instance GHC.Classes.Ord Sound.RtMidi.DeviceType
+ Sound.RtMidi: instance GHC.Enum.Bounded Sound.RtMidi.DeviceType
+ Sound.RtMidi: instance GHC.Enum.Enum Sound.RtMidi.DeviceType
+ Sound.RtMidi: instance GHC.Generics.Generic Sound.RtMidi.DeviceType
+ Sound.RtMidi: instance GHC.Generics.Generic Sound.RtMidi.Error
+ Sound.RtMidi: setUnsafeCallback :: MonadIO m => InputDevice -> (Double -> Ptr Word8 -> Int -> IO ()) -> m ()
+ Sound.RtMidi.Report: ApiReport :: !Api -> !String -> !String -> ![(Int, String)] -> ![(Int, String)] -> ApiReport
+ Sound.RtMidi.Report: Report :: !Api -> !Api -> ![ApiReport] -> Report
+ Sound.RtMidi.Report: [apiInPorts] :: ApiReport -> ![(Int, String)]
+ Sound.RtMidi.Report: [apiOutPorts] :: ApiReport -> ![(Int, String)]
+ Sound.RtMidi.Report: [apiRepApi] :: ApiReport -> !Api
+ Sound.RtMidi.Report: [apiRepDisplayName] :: ApiReport -> !String
+ Sound.RtMidi.Report: [apiRepName] :: ApiReport -> !String
+ Sound.RtMidi.Report: [apiReports] :: Report -> ![ApiReport]
+ Sound.RtMidi.Report: [defaultInApi] :: Report -> !Api
+ Sound.RtMidi.Report: [defaultOutApi] :: Report -> !Api
+ Sound.RtMidi.Report: buildApiReport :: MonadIO m => Api -> m ApiReport
+ Sound.RtMidi.Report: buildCustomReport :: MonadIO m => Bool -> m Report
+ Sound.RtMidi.Report: buildReport :: MonadIO m => m Report
+ Sound.RtMidi.Report: data ApiReport
+ Sound.RtMidi.Report: data Report
+ Sound.RtMidi.Report: instance Control.DeepSeq.NFData Sound.RtMidi.Report.ApiReport
+ Sound.RtMidi.Report: instance Control.DeepSeq.NFData Sound.RtMidi.Report.Report
+ Sound.RtMidi.Report: instance GHC.Classes.Eq Sound.RtMidi.Report.ApiReport
+ Sound.RtMidi.Report: instance GHC.Classes.Eq Sound.RtMidi.Report.Report
+ Sound.RtMidi.Report: instance GHC.Generics.Generic Sound.RtMidi.Report.ApiReport
+ Sound.RtMidi.Report: instance GHC.Generics.Generic Sound.RtMidi.Report.Report
+ Sound.RtMidi.Report: instance GHC.Show.Show Sound.RtMidi.Report.ApiReport
+ Sound.RtMidi.Report: instance GHC.Show.Show Sound.RtMidi.Report.Report
- Sound.RtMidi: getMessage :: MonadIO m => InputDevice -> m (Double, [Word8])
+ Sound.RtMidi: getMessage :: MonadIO m => InputDevice -> m (Double, Vector Word8)
- Sound.RtMidi: getMessageSized :: MonadIO m => InputDevice -> Int -> m (Double, [Word8])
+ Sound.RtMidi: getMessageSized :: MonadIO m => InputDevice -> Int -> m (Double, Vector Word8)
- Sound.RtMidi: sendMessage :: MonadIO m => OutputDevice -> [Word8] -> m ()
+ Sound.RtMidi: sendMessage :: MonadIO m => OutputDevice -> Vector Word8 -> m ()
- Sound.RtMidi: setCallback :: MonadUnliftIO m => InputDevice -> (Double -> [Word8] -> m ()) -> m ()
+ Sound.RtMidi: setCallback :: MonadUnliftIO m => InputDevice -> (Double -> Vector Word8 -> m ()) -> m ()

Files

README.md view
@@ -4,32 +4,26 @@  Haskell wrapper for [RtMidi](http://www.music.mcgill.ca/~gary/rtmidi/), the lightweight, cross-platform MIDI I/O library. -## Development--This project is tested with Cabal (latest versions of last two compiler lines) and Stack (latest LTS).+## How to use -You can get started with development like so:+See [Hackage](https://hackage.haskell.org/package/RtMidi) for the latest released version and add `RtMidi` and `vector` to your library `build-depends`. -    # Build on OSX CoreMIDI support or Linux with ALSA support-    # You can also pass `--flag RtMidi:jack` for Jack support-    stack build+Follow the [callback](https://github.com/riottracker/RtMidi/blob/master/examples/callback.hs) example to receive messages. -    # Verify that it works:-    stack exec -- rtmidi-query+Follow the [playback](https://github.com/riottracker/RtMidi/blob/master/examples/playback.hs) example to send messages. -There is also a `Dockerfile` in the `docker` directory and some `make` targets that can help you verify Linux builds:+## Development -    # Build the image and tag it `haskell-rtmidi-dev`-    make docker-build+This project is tested with Cabal (latest versions of last two compiler lines) and Stack (latest LTS). -    # Enter the docker image-    make docker-repl+You can get started with development like so: -    # Inside the docker image:-    cabal update && cabal build+    # Build and test on OSX CoreMIDI support or Linux with ALSA support.+    # (You can also manually invoke stack and pass `--flag RtMidi:jack` for Jack support.)+    make test -(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`.)+    # Print information about all accessible MIDI devices and all compiled APIs.+    make example-report  ## TODO 
RtMidi.cabal view
@@ -1,5 +1,5 @@ name:                RtMidi-version:             0.3.0.0+version:             0.4.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@@ -14,7 +14,7 @@                      examples/callback.hs                      examples/playback.hs                      examples/poll.hs-                     examples/query.hs+                     examples/report.hs cabal-version:       >=1.10 license:             MIT @@ -30,10 +30,16 @@  library   exposed-modules:     Sound.RtMidi+                     , Sound.RtMidi.Report   other-modules:       Sound.RtMidi.Foreign   build-depends:       base >=4.9 && <4.15+                     , deepseq >= 1.4.4.0 && < 2                      , unliftio-core >= 0.1.2.0 && < 1+                     , vector >= 0.12.1.2 && < 1   default-language:    Haskell2010+  default-extensions:  DeriveGeneric+                     , DerivingStrategies+                     , GeneralizedNewtypeDeriving   include-dirs:        rtmidi   extra-libraries:     stdc++   c-sources:           rtmidi/RtMidi.cpp@@ -72,6 +78,7 @@   build-depends:       base     , RtMidi+    , vector   default-language:   Haskell2010   ghc-options: -threaded -rtsopts @@ -81,6 +88,7 @@   build-depends:       base     , RtMidi+    , vector   default-language:   Haskell2010   ghc-options: -threaded -rtsopts @@ -90,15 +98,17 @@   build-depends:       base     , RtMidi+    , vector   default-language:   Haskell2010   ghc-options: -threaded -rtsopts -executable rtmidi-query-  main-is:            query.hs+executable rtmidi-report+  main-is:            report.hs   hs-source-dirs:     examples   build-depends:       base     , RtMidi+    , pretty-simple >= 3.2.3.0 && < 4   default-language:   Haskell2010   ghc-options: -threaded -rtsopts @@ -109,6 +119,7 @@   build-depends:       base     , RtMidi+    , vector     , tasty >= 1.2.3 && < 2     , tasty-hunit >= 0.10.0.2 && < 1   default-language:   Haskell2010
Sound/RtMidi.hs view
@@ -1,3 +1,6 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE DeriveAnyClass #-}+ -- | Interface to RtMidi module Sound.RtMidi   ( InputDevice@@ -6,6 +9,9 @@   , DeviceType (..)   , Api (..)   , Error (..)+  , apiName+  , apiDisplayName+  , compiledApiByName   , ready   , compiledApis   , openPort@@ -18,26 +24,32 @@   , defaultInput   , createInput   , setCallback+  , setUnsafeCallback   , setForeignCallback   , cancelCallback   , ignoreTypes   , getMessage   , getMessageSized+  , getMessageMutable   , defaultOutput   , createOutput   , sendMessage   , currentApi   ) where +import Control.DeepSeq (NFData) import Control.Exception (Exception, throwIO) import Control.Monad (unless, void) import Control.Monad.IO.Class (MonadIO (..)) import Control.Monad.IO.Unlift (MonadUnliftIO (..)) import Data.Coerce (coerce)+import qualified Data.Vector.Storable as VS+import qualified Data.Vector.Storable.Mutable as VSM import Data.Word (Word8)-import Foreign (FunPtr, Ptr, Storable (..), alloca, allocaArray, nullPtr, peekArray, with, withArrayLen)+import Foreign (FunPtr, Ptr, Storable (..), alloca, allocaArray, nullPtr, peekArray) import Foreign.C (CDouble (..), CInt (..), CSize, CString, CUChar (..), peekCString, withCString) import Foreign.ForeignPtr (ForeignPtr, newForeignPtr, withForeignPtr)+import GHC.Generics (Generic) import Sound.RtMidi.Foreign  -- The default message size (in bytes) expected from 'getMessage'@@ -50,10 +62,11 @@ data DeviceType =     InputDeviceType   | OutputDeviceType-  deriving (Eq, Show)+  deriving stock (Eq, Show, Ord, Enum, Bounded, Generic)+  deriving anyclass (NFData)  newtype Device = Device { unDevice :: ForeignPtr Wrapper }-  deriving (Eq, Show)+  deriving stock (Eq, Show)  -- | Generalizes 'InputDevice' and 'OutputDevice' for use in common functions class IsDevice d where@@ -62,7 +75,7 @@  -- | A handle to a device to be used for input newtype InputDevice = InputDevice { unInputDevice :: Device }-  deriving (Eq, Show)+  deriving stock (Eq, Show)  instance IsDevice InputDevice where   toDevice = unInputDevice@@ -73,7 +86,7 @@  -- | A handle to a device to be used for input newtype OutputDevice = OutputDevice { unOutputDevice :: Device }-  deriving (Eq, Show)+  deriving stock (Eq, Show)  instance IsDevice OutputDevice where   toDevice = unOutputDevice@@ -82,37 +95,11 @@ newOutputDevice :: Ptr Wrapper -> IO OutputDevice newOutputDevice = fmap (OutputDevice . Device) . newForeignPtr rtmidi_out_free --- | Enum of RtMidi-supported APIs-data Api-  = UnspecifiedApi-  | CoreMidiApi-  | AlsaApi-  | JackApi-  | MultimediaApi-  | KernelStreamingApi-  | DummyApi-  deriving (Eq, Show)--instance Bounded Api where-  minBound = UnspecifiedApi-  maxBound = DummyApi--instance Enum Api where-  fromEnum UnspecifiedApi = 0-  fromEnum CoreMidiApi = 1-  fromEnum AlsaApi = 2-  fromEnum JackApi = 3-  fromEnum MultimediaApi = 4-  fromEnum DummyApi = 5-  toEnum 0 = UnspecifiedApi-  toEnum 1 = CoreMidiApi-  toEnum 2 = AlsaApi-  toEnum 3 = JackApi-  toEnum 4 = MultimediaApi-  toEnum 5 = DummyApi- -- | An internal RtMidi error-newtype Error = Error String deriving (Eq, Show)+newtype Error = Error { unError :: String }+  deriving stock (Eq, Show, Generic)+  deriving anyclass (NFData)+ instance Exception Error  -- Detects and throws internal errors@@ -131,6 +118,18 @@ withDevicePtr :: IsDevice d => d -> (Ptr Wrapper -> IO a) -> IO a withDevicePtr d f = withDevicePtrUnguarded d (\dptr -> f dptr <* guardError dptr) +-- | Get the display name for the given 'Api'.+apiDisplayName :: MonadIO m => Api -> m String+apiDisplayName api = liftIO (rtmidi_api_display_name (fromApi api) >>= peekCString)++-- | Get the internal name for the given 'Api'.+apiName :: MonadIO m => Api -> m String+apiName api = liftIO (rtmidi_api_name (fromApi api) >>= peekCString)++-- | Lookup a compiled 'Api' by name.+compiledApiByName :: MonadIO m => String -> m Api+compiledApiByName name = liftIO (fmap toApi (withCString name rtmidi_compiled_api_by_name))+ -- | Check if a device is ok ready :: (MonadIO m, IsDevice d) => d -> m Bool ready d = liftIO (withDevicePtr d (fmap ok . peek))@@ -142,7 +141,7 @@   as <- allocaArray n $ \ptr -> do     rtmidi_get_compiled_api ptr (fromIntegral n)     peekArray n ptr-  pure (map (toEnum . fromIntegral) as)+  pure (map toApi as)  -- | Open a MIDI connection openPort :: (MonadIO m, IsDevice d)@@ -234,16 +233,21 @@             -> Int        -- ^ size of the MIDI input queue             -> m InputDevice createInput api clientName queueSizeLimit = liftIO $ do-  dptr <- withCString clientName (\str -> rtmidi_in_create (fromIntegral (fromEnum api)) str (fromIntegral queueSizeLimit))+  dptr <- withCString clientName (\str -> rtmidi_in_create (fromApi api) str (fromIntegral queueSizeLimit))   guardError dptr   newInputDevice dptr  foreign import ccall "wrapper"   mkCallbackPointer :: (CDouble -> Ptr CUChar -> CInt -> Ptr () -> IO ()) -> IO (FunPtr (CDouble -> Ptr CUChar -> CInt -> Ptr () -> IO ())) -adaptCallbackCTypes :: (Double -> [Word8] -> IO ()) -> (CDouble -> Ptr CUChar -> CInt -> Ptr () -> IO ())-adaptCallbackCTypes f (CDouble t) d s _ = peekArray (fromIntegral s) d >>= \a -> f t (coerce a)+adaptCallbackCTypes :: (Double -> VS.Vector Word8 -> IO ()) -> (CDouble -> Ptr CUChar -> CInt -> Ptr () -> IO ())+adaptCallbackCTypes !f (CDouble !t) m s _ = do+  buf <- VS.generateM (fromIntegral s) (peekElemOff (coerce m))+  f t buf +adaptUnsafeCallbackCTypes :: (Double -> Ptr Word8 -> Int -> IO ()) -> (CDouble -> Ptr CUChar -> CInt -> Ptr () -> IO ())+adaptUnsafeCallbackCTypes !f (CDouble !t) m s _ = f t (coerce m) (fromIntegral s)+ -- | Set a callback function to be invoked for incoming MIDI messages. -- -- The callback function will be called whenever an incoming MIDI message is received.@@ -251,12 +255,22 @@ -- some messages in the queue. setCallback :: MonadUnliftIO m             => InputDevice-            -> (Double -> [Word8] -> m ())  -- ^ Function that takes a timestamp and a MIDI message as arguments+            -> (Double -> VS.Vector Word8 -> m ())  -- ^ Function that takes a timestamp and a MIDI message as arguments             -> m () 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 +-- | A variant of 'setCallback' that takes a raw pointer and length. It is unsafe to share or reference the pointer beyond the+-- scope of the callback, as the RtMidi-owned memory it references may have been changed or freed.+setUnsafeCallback :: MonadIO m+                  => InputDevice+                  -> (Double -> Ptr Word8 -> Int -> IO ())+                  -> m ()+setUnsafeCallback d c = liftIO $ withDevicePtr d $ \dptr -> do+  f <- mkCallbackPointer (adaptUnsafeCallbackCTypes c)+  rtmidi_in_set_callback dptr f nullPtr+ -- | 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@@ -279,7 +293,7 @@ -- -- By default, MIDI timing and active sensing messages are ignored during message input because of their -- relative high data rates. MIDI sysex messages are ignored by default as well.--- Variable values of `true` imply that the respective message type will be ignored.+-- Variable values of 'True' imply that the respective message type will be ignored. ignoreTypes :: MonadIO m             => InputDevice             -> Bool       -- ^ SysEx messages@@ -288,28 +302,30 @@             -> m () 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 -> withDevicePtrUnguarded d $ \dptr -> do-  poke s (fromIntegral n)-  CDouble timestamp <- rtmidi_in_get_message dptr m s+-- | Variant of 'getMessage' that allows you to fill a shared buffer, returning timestamp and size.+getMessageMutable :: MonadIO m => InputDevice -> VSM.IOVector Word8 -> m (Double, Int)+getMessageMutable d buf = liftIO $ alloca $ \s -> VSM.unsafeWith buf $ \m -> withDevicePtrUnguarded d $ \dptr -> do+  poke s (fromIntegral (VSM.length buf))+  CDouble !timestamp <- rtmidi_in_get_message dptr (coerce m) s   guardError dptr-  size <- peek s-  message <--    case size of-      0 -> pure []-      _ -> do-        x <- peek m-        y <- peekArray (fromIntegral size) x-        pure (coerce y)-  pure (timestamp, message)+  csize <- peek s+  let !size = fromIntegral csize+  pure (timestamp, size) +-- | Variant of 'getMessage' that allows you to set message buffer size (typically for large sysex messages).+getMessageSized :: MonadIO m => InputDevice -> Int -> m (Double, VS.Vector Word8)+getMessageSized d n = liftIO $ do+  buf <- VSM.new n+  (!timestamp, !size) <- getMessageMutable d buf+  vec <- VSM.unsafeWith buf (VS.generateM size . peekElemOff)+  pure (timestamp, vec)+ -- | Return data bytes for the next available MIDI message in the input queue and the event delta-time in seconds. -- -- This function returns immediately whether a new message is available or not. -- A valid message is indicated by whether the list contains any elements. -- Note that large sysex messages will be silently dropped! Use 'getMessageSized' or use a callback to get these safely.-getMessage :: MonadIO m => InputDevice -> m (Double, [Word8])+getMessage :: MonadIO m => InputDevice -> m (Double, VS.Vector Word8) getMessage d = liftIO (getMessageSized d defaultMessageSize)  -- | Default constructor for a 'Device' to use for output.@@ -325,14 +341,14 @@              -> String     -- ^ client name              -> m OutputDevice createOutput api clientName = liftIO $ do-  dptr <- withCString clientName (rtmidi_out_create (fromIntegral (fromEnum api)))+  dptr <- withCString clientName (rtmidi_out_create (fromApi api))   guardError 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 -> withDevicePtr d $ \dptr ->-  void (rtmidi_out_send_message dptr (coerce ptr) (fromIntegral n))+sendMessage :: MonadIO m => OutputDevice -> VS.Vector Word8 -> m ()+sendMessage d buf = liftIO $ VS.unsafeWith buf $ \m -> withDevicePtr d $ \dptr ->+  void (rtmidi_out_send_message dptr (coerce m) (fromIntegral (VS.length buf)))  -- | Returns the specifier for the MIDI 'Api' in use currentApi :: MonadIO m => IsDevice d => d -> m Api@@ -341,4 +357,4 @@     case getDeviceType d of       InputDeviceType -> rtmidi_in_get_current_api dptr       OutputDeviceType -> rtmidi_out_get_current_api dptr-  pure (toEnum (fromIntegral res))+  pure (toApi res)
Sound/RtMidi/Foreign.hsc view
@@ -1,10 +1,17 @@-{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE ForeignFunctionInterface #-}  -- | FFI defs for RtMidi module Sound.RtMidi.Foreign   ( Wrapper (..)+  , Api (..)+  , ApiInternal+  , toApi+  , fromApi+  , rtmidi_api_display_name+  , rtmidi_api_name   , rtmidi_close_port+  , rtmidi_compiled_api_by_name   , rtmidi_get_compiled_api   , rtmidi_get_port_count   , rtmidi_get_port_name@@ -27,15 +34,18 @@  #include "rtmidi_c.h" +import Control.DeepSeq (NFData) import Foreign (FunPtr, Ptr, Storable (..)) import Foreign.C (CDouble (..), CInt (..), CString, CSize, CUChar, CUInt (..))+import GHC.Generics (Generic)  data Wrapper = Wrapper   { ptr :: !(Ptr ())   , dat :: !(Ptr ())   , ok  :: !Bool   , msg :: !CString-  } deriving (Eq, Show)+  } deriving stock (Eq, Show, Generic)+    deriving anyclass (NFData)  instance Storable Wrapper where   sizeOf _ = #{size struct RtMidiWrapper}@@ -52,14 +62,40 @@     d <- #{peek struct RtMidiWrapper, msg} ptr     pure (Wrapper a b c d) +-- | Enum of RtMidi-supported APIs+data Api+  = UnspecifiedApi+  | CoreMidiApi+  | AlsaApi+  | JackApi+  | MultimediaApi+  | DummyApi+  deriving stock (Eq, Show, Ord, Enum, Bounded, Generic)+  deriving anyclass (NFData)+ -- A parameter we'll be de/serializing from the 'Api' enum.-type ApiEnum = CInt+newtype ApiInternal = ApiInternal { unApiInternal :: CInt } deriving newtype (Storable) +toApi :: ApiInternal -> Api+toApi = toEnum . fromIntegral . unApiInternal++fromApi :: Api -> ApiInternal+fromApi = ApiInternal . fromIntegral . fromEnum++foreign import ccall "rtmidi_c.h rtmidi_api_display_name"+  rtmidi_api_display_name :: ApiInternal -> IO CString++foreign import ccall "rtmidi_c.h rtmidi_api_name"+  rtmidi_api_name :: ApiInternal -> IO CString+ foreign import ccall "rtmidi_c.h rtmidi_close_port"   rtmidi_close_port :: Ptr Wrapper -> IO () +foreign import ccall "rtmidi_c.h rtmidi_compiled_api_by_name"+  rtmidi_compiled_api_by_name :: CString -> IO ApiInternal+ foreign import ccall "rtmidi_c.h rtmidi_get_compiled_api"-  rtmidi_get_compiled_api :: Ptr ApiEnum -> CUInt -> IO CInt+  rtmidi_get_compiled_api :: Ptr ApiInternal -> CUInt -> IO CInt  foreign import ccall "rtmidi_c.h rtmidi_get_port_count"   rtmidi_get_port_count :: Ptr Wrapper -> IO CUInt@@ -71,7 +107,7 @@   rtmidi_in_cancel_callback :: Ptr Wrapper -> IO ()  foreign import ccall "rtmidi_c.h rtmidi_in_create"-  rtmidi_in_create :: ApiEnum -> CString -> CUInt -> IO (Ptr Wrapper)+  rtmidi_in_create :: ApiInternal -> CString -> CUInt -> IO (Ptr Wrapper)  foreign import ccall "rtmidi_c.h rtmidi_in_create_default"   rtmidi_in_create_default :: IO (Ptr Wrapper)@@ -80,7 +116,7 @@   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 ApiEnum+  rtmidi_in_get_current_api :: Ptr Wrapper -> IO ApiInternal  foreign import ccall "rtmidi_c.h rtmidi_in_get_message"   rtmidi_in_get_message :: Ptr Wrapper -> Ptr (Ptr CUChar) -> Ptr CSize -> IO CDouble@@ -98,7 +134,7 @@   rtmidi_open_virtual_port :: Ptr Wrapper -> CString -> IO ()  foreign import ccall "rtmidi_c.h rtmidi_out_create"-  rtmidi_out_create :: ApiEnum-> CString -> IO (Ptr Wrapper)+  rtmidi_out_create :: ApiInternal -> CString -> IO (Ptr Wrapper)  foreign import ccall "rtmidi_c.h rtmidi_out_create_default"   rtmidi_out_create_default :: IO (Ptr Wrapper)@@ -107,7 +143,7 @@   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 ApiEnum+  rtmidi_out_get_current_api :: Ptr Wrapper -> IO ApiInternal  foreign import ccall "rtmidi_c.h rtmidi_out_send_message"   rtmidi_out_send_message :: Ptr Wrapper -> Ptr CUChar -> CInt -> IO CInt
+ Sound/RtMidi/Report.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE DeriveAnyClass #-}++-- | Utility code to gather MIDI system information.+module Sound.RtMidi.Report+  ( ApiReport (..)+  , Report (..)+  , buildApiReport+  , buildCustomReport+  , buildReport+  ) where++import Control.DeepSeq (NFData)+import Control.Monad.IO.Class (MonadIO)+import Data.List (nub)+import GHC.Generics (Generic)+import Sound.RtMidi++-- | MIDI system information specific to a particular API.+data ApiReport = ApiReport+  { apiRepApi :: !Api+  , apiRepName :: !String+  , apiRepDisplayName :: !String+  , apiInPorts :: ![(Int, String)]+  , apiOutPorts :: ![(Int, String)]+  } deriving stock (Eq, Show, Generic)+    deriving anyclass (NFData)++-- | MIDI system information for any number of APIs.+data Report = Report+  { defaultInApi :: !Api+  , defaultOutApi :: !Api+  , apiReports :: ![ApiReport]+  } deriving stock (Eq, Show, Generic)+    deriving anyclass (NFData)++-- | Gather information about the given 'Api', including port information.+buildApiReport :: MonadIO m => Api -> m ApiReport+buildApiReport api = do+  name <- apiName api+  displayName <- apiDisplayName api+  inDev <- createInput api ("rtmidi-report-input-" ++ name) 0+  inPorts <- listPorts inDev+  outDev <- createOutput api ("rtmidi-report-output-" ++ name)+  outPorts <- listPorts outDev+  pure (ApiReport api name displayName inPorts outPorts)++-- | Variant of 'buildReport' that allows you to restrict it to the default APIs.+buildCustomReport :: MonadIO m+                  => Bool  -- ^ True to report on default APIs, False to report on all compiled APIs.+                  -> m Report+buildCustomReport defaultOnly = do+  inDev <- defaultInput+  defInApi <- currentApi inDev+  outDev <- defaultOutput+  defOutApi <- currentApi outDev+  apis <- if defaultOnly then pure (nub [defInApi, defOutApi]) else compiledApis+  apiReps <- traverse buildApiReport apis+  pure (Report defInApi defOutApi apiReps)++-- | Gather information about all compiled APIs.+buildReport :: MonadIO m => m Report+buildReport = buildCustomReport False
examples/callback.hs view
@@ -1,9 +1,10 @@+import qualified Data.Vector.Storable as VS import Data.Word (Word8)-import Sound.RtMidi (closePort, defaultInput, openPort, setCallback) import Numeric (showHex)+import Sound.RtMidi (closePort, defaultInput, openPort, setCallback) -callback :: Double -> [Word8] -> IO ()-callback delta msg = putStrLn $ (foldr (showHex . fromEnum) "" msg) ++ " - " ++ (show delta)+callback :: Double -> VS.Vector Word8 -> IO ()+callback delta msg = putStrLn $ (VS.foldr (showHex . fromEnum) "" msg) ++ " - " ++ (show delta)  main :: IO () main = do
examples/playback.hs view
@@ -1,5 +1,6 @@-import Sound.RtMidi (closePort, defaultOutput, openPort, sendMessage, portCount, portName) import Control.Concurrent (threadDelay)+import qualified Data.Vector.Storable as VS+import Sound.RtMidi (closePort, defaultOutput, openPort, sendMessage, portCount, portName)  main :: IO () main = do@@ -15,6 +16,6 @@   let arp2 = take 12 $ cycle [0x50, 0x53, 0x58]   let song = cycle (arp0 ++ arp1 ++ arp2 ++ arp0)   putStrLn "playing..."-  mapM_ (\x -> sendMessage device [0x90, x, 0x7f] >> threadDelay 120000) $ take 240 song+  mapM_ (\x -> sendMessage device (VS.fromList [0x90, x, 0x7f]) >> threadDelay 120000) $ take 240 song   putStrLn "exiting..."   closePort device
examples/poll.hs view
@@ -1,11 +1,12 @@-import Sound.RtMidi (InputDevice, closePort, defaultInput, getMessage, openPort) import Control.Concurrent (forkIO, killThread) import Control.Monad (forever)+import qualified Data.Vector.Storable as VS+import Sound.RtMidi (InputDevice, closePort, defaultInput, getMessage, openPort)  mainLoop :: InputDevice -> IO () mainLoop d = do   m <- getMessage d-  if length (snd m) > 0 then+  if VS.length (snd m) > 0 then     putStrLn $ show m   else return () 
− examples/query.hs
@@ -1,11 +0,0 @@-import Sound.RtMidi (compiledApis, createOutput, currentApi, defaultOutput, listPorts)--main :: IO ()-main = do-  device <- defaultOutput-  api <- currentApi device-  builtin <- compiledApis-  portPairs <- listPorts device-  putStrLn $ "RtMidi output using " ++ (show api)-  putStrLn $ "built-in: " ++ (show builtin)-  putStrLn $ "available ports: " ++ (show portPairs)
+ examples/report.hs view
@@ -0,0 +1,5 @@+import Sound.RtMidi.Report (buildReport)+import Text.Pretty.Simple (pPrint)++main :: IO ()+main = buildReport >>= pPrint
test/Main.hs view
@@ -2,27 +2,67 @@  import Control.Concurrent (threadDelay) import Control.Monad (replicateM_, unless, when)+import Data.Foldable (for_) import Data.IORef (IORef, newIORef, readIORef, modifyIORef) import Data.List (isInfixOf)+import qualified Data.Vector.Storable as VS import Data.Word (Word8)-import Sound.RtMidi (Api (..), closePort, compiledApis, createInput, createOutput, currentApi, findPort, sendMessage, setCallback, openPort, openVirtualPort)+import Sound.RtMidi (Api (..), apiName, apiDisplayName, closePort, compiledApiByName, compiledApis, createInput, createOutput, currentApi, findPort, sendMessage, setCallback, openPort, openVirtualPort)+import Sound.RtMidi.Report (Report, buildReport) import Test.Tasty (TestTree, defaultMain, testGroup) import Test.Tasty.HUnit (assertFailure, testCase, (@?=))  incIORef :: IORef Int -> IO () incIORef = flip modifyIORef succ -readerCallback :: IORef Int -> Double -> [Word8] -> IO ()-readerCallback countRef _ msg = incIORef countRef+-- A simple note-on message+exampleMessage :: VS.Vector Word8+exampleMessage = VS.fromList [0x90, 0x51, 0x7f] +readerCallback :: IORef Int -> Double -> VS.Vector Word8 -> IO ()+readerCallback countRef _ msg = do+  msg @?= exampleMessage+  incIORef countRef++testApiName :: TestTree+testApiName = testCase "apiName" $ do+  actualName1 <- apiName UnspecifiedApi+  actualName1 @?= "unspecified"+  actualName2 <- apiName DummyApi+  actualName2 @?= "dummy"++testApiDisplayName :: TestTree+testApiDisplayName = testCase "apiDisplayName" $ do+  actualName1 <- apiDisplayName UnspecifiedApi+  actualName1 @?= "Unknown"+  actualName2 <- apiDisplayName DummyApi+  actualName2 @?= "Dummy"++testCompiledApiByName :: TestTree+testCompiledApiByName = testCase "compiledApiByName" $ do+  actualApi1 <- compiledApiByName "unspecified"+  actualApi1 @?= UnspecifiedApi+  actualApi2 <- compiledApiByName "dummy"+  actualApi2 @?= UnspecifiedApi+  actualApi3 <- compiledApiByName "invalid"+  actualApi3 @?= UnspecifiedApi++testBuildReport :: TestTree+testBuildReport = testCase "buildReport" $ do+  -- We could test more but we mostly just want to make sure we can run 'buildReport'.+  _ <- buildReport+  pure ()+ 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"       -- 100 ms delay in us       delayUs = 100000+  -- First a check of api name+  name <- apiName api+  actualApi <- compiledApiByName name+  actualApi @?= api   countRef <- newIORef 0   -- Create reader with callback   inDev <- createInput api "rtmidi-test-input" 100@@ -38,7 +78,7 @@   let portNum = maybe (error "Could not find port") id maybePortNum   openPort outDev portNum portName   -- Send messages-  replicateM_ expectedCount (sendMessage outDev message)+  replicateM_ expectedCount (sendMessage outDev exampleMessage)   -- Sleep a bit to ensure message delivery   threadDelay delayUs   -- Close writer@@ -52,6 +92,12 @@ main :: IO () main = do   apis <- compiledApis-  when (null apis) (assertFailure "No compiled APIs found")-  let tests = fmap testVirtualReadWrite apis-  defaultMain (testGroup "RtMidi" tests)+  let rwTests = fmap testVirtualReadWrite (filter (/= DummyApi) apis)+      rwGroup = testGroup "R/W" rwTests+  defaultMain $ testGroup "RtMidi" $+    [ testApiName+    , testApiDisplayName+    , testCompiledApiByName+    , testBuildReport+    , rwGroup+    ]