libremidi-0.4.2: src/Libremidi/Api.hs
-- | Mid-level interface to Libremidi.
-- Operations in IO do not fail.
-- Operations in ErrM can possibly fail.
module Libremidi.Api where
import Control.Monad ((>=>))
import Control.Monad.IO.Class (liftIO)
import Data.Coerce (coerce)
import Data.Default (Default (..))
import Data.Foldable (traverse_)
import Data.Int (Int64)
import Data.Maybe (fromMaybe)
import Data.Proxy (Proxy (..))
import Data.Text (Text)
import Data.Text.Foreign qualified as TF
import Foreign.Concurrent qualified as FC
import Foreign.ForeignPtr (ForeignPtr, finalizeForeignPtr, withForeignPtr)
import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)
import Foreign.Ptr (FunPtr, Ptr, castPtrToFunPtr, nullPtr)
import Libremidi.Common
( BitEnum (..)
, Cb
, ErrM
, Field
, MallocPtr (..)
, UniquePtr
, assocM
, checkM
, consumeCb
, consumeUniquePtr
, fromCBool
, fromCLong
, pokeField
, takeM
, text0M
, textM
, toCBool
, toCLong
, toCSize
, unRunErrM
, withCb
)
import Libremidi.Foreign qualified as LMF
data TimestampMode
= TimestampModeNone
| TimestampModeRelative
| TimestampModeAbsolute
| TimestampModeSystemMono
| TimestampModeAudioFrame
| TimestampModeCustom
deriving stock (Eq, Ord, Show, Enum, Bounded)
instance BitEnum LMF.TimestampMode TimestampMode where
fromBitEnum = \case
a | a == LMF.tmNoTimestamp -> Just TimestampModeNone
a | a == LMF.tmRelative -> Just TimestampModeRelative
a | a == LMF.tmAbsolute -> Just TimestampModeAbsolute
a | a == LMF.tmSystemMonotonic -> Just TimestampModeSystemMono
a | a == LMF.tmAudioFrame -> Just TimestampModeAudioFrame
a | a == LMF.tmCustom -> Just TimestampModeCustom
_ -> Nothing
toBitEnum = \case
TimestampModeNone -> LMF.tmNoTimestamp
TimestampModeRelative -> LMF.tmRelative
TimestampModeAbsolute -> LMF.tmAbsolute
TimestampModeSystemMono -> LMF.tmSystemMonotonic
TimestampModeAudioFrame -> LMF.tmAudioFrame
TimestampModeCustom -> LMF.tmCustom
data Api
= ApiUnspecified
| ApiCoremidi
| ApiAlsaSeq
| ApiAlsaRaw
| ApiJackMidi
| ApiWindowsMm
| ApiWindowsUwp
| ApiWebmidi
| ApiPipewire
| ApiKeyboard
| ApiNetwork
| ApiAlsaRawUmp
| ApiAlsaSeqUmp
| ApiCoremidiUmp
| ApiWindowsMidiServices
| ApiKeyboardUmp
| ApiNetworkUmp
| ApiDummy
deriving stock (Eq, Ord, Show, Enum, Bounded)
instance BitEnum LMF.Api Api where
fromBitEnum = \case
a | a == LMF.apiUnspecified -> Just ApiUnspecified
a | a == LMF.apiCoremidi -> Just ApiCoremidi
a | a == LMF.apiAlsaSeq -> Just ApiAlsaSeq
a | a == LMF.apiAlsaRaw -> Just ApiAlsaRaw
a | a == LMF.apiJackMidi -> Just ApiJackMidi
a | a == LMF.apiWindowsMm -> Just ApiWindowsMm
a | a == LMF.apiWindowsUwp -> Just ApiWindowsUwp
a | a == LMF.apiWebmidi -> Just ApiWebmidi
a | a == LMF.apiPipewire -> Just ApiPipewire
a | a == LMF.apiKeyboard -> Just ApiKeyboard
a | a == LMF.apiNetwork -> Just ApiNetwork
a | a == LMF.apiAlsaRawUmp -> Just ApiAlsaRawUmp
a | a == LMF.apiAlsaSeqUmp -> Just ApiAlsaSeqUmp
a | a == LMF.apiCoremidiUmp -> Just ApiCoremidiUmp
a | a == LMF.apiWindowsMidiServices -> Just ApiWindowsMidiServices
a | a == LMF.apiKeyboardUmp -> Just ApiKeyboardUmp
a | a == LMF.apiNetworkUmp -> Just ApiNetworkUmp
a | a == LMF.apiDummy -> Just ApiDummy
_ -> Nothing
toBitEnum = \case
ApiUnspecified -> LMF.apiUnspecified
ApiCoremidi -> LMF.apiCoremidi
ApiAlsaSeq -> LMF.apiAlsaSeq
ApiAlsaRaw -> LMF.apiAlsaRaw
ApiJackMidi -> LMF.apiJackMidi
ApiWindowsMm -> LMF.apiWindowsMm
ApiWindowsUwp -> LMF.apiWindowsUwp
ApiWebmidi -> LMF.apiWebmidi
ApiPipewire -> LMF.apiPipewire
ApiKeyboard -> LMF.apiKeyboard
ApiNetwork -> LMF.apiNetwork
ApiAlsaRawUmp -> LMF.apiAlsaRawUmp
ApiAlsaSeqUmp -> LMF.apiAlsaSeqUmp
ApiCoremidiUmp -> LMF.apiCoremidiUmp
ApiWindowsMidiServices -> LMF.apiWindowsMidiServices
ApiKeyboardUmp -> LMF.apiKeyboardUmp
ApiNetworkUmp -> LMF.apiNetworkUmp
ApiDummy -> LMF.apiDummy
data ConfigType
= ConfigTypeObserver
| ConfigTypeInput
| ConfigTypeOutput
deriving stock (Eq, Ord, Show, Enum, Bounded)
instance BitEnum LMF.ConfigType ConfigType where
fromBitEnum = \case
a | a == LMF.ctObserver -> Just ConfigTypeObserver
a | a == LMF.ctInput -> Just ConfigTypeInput
a | a == LMF.ctOutput -> Just ConfigTypeOutput
_ -> Nothing
toBitEnum = \case
ConfigTypeObserver -> LMF.ctObserver
ConfigTypeInput -> LMF.ctInput
ConfigTypeOutput -> LMF.ctOutput
data Version
= VersionMidi1
| VersionMidi2
deriving stock (Eq, Ord, Show, Enum, Bounded)
instance BitEnum LMF.Version Version where
fromBitEnum = \case
a | a == LMF.verMidi1 -> Just VersionMidi1
a | a == LMF.verMidi2 -> Just VersionMidi2
_ -> Nothing
toBitEnum = \case
VersionMidi1 -> LMF.verMidi1
VersionMidi2 -> LMF.verMidi2
type Timestamp = Int64
type InPort = ForeignPtr LMF.InPort
freeInPort :: InPort -> IO ()
freeInPort = finalizeForeignPtr
cloneInPort' :: Ptr LMF.InPort -> ErrM InPort
cloneInPort' = takeM . LMF.libremidi_midi_in_port_clone
cloneInPort :: InPort -> ErrM InPort
cloneInPort = assocM cloneInPort'
inPortName' :: Ptr LMF.InPort -> ErrM Text
inPortName' = textM . LMF.libremidi_midi_in_port_name
inPortName :: InPort -> ErrM Text
inPortName = assocM inPortName'
type OutPort = ForeignPtr LMF.OutPort
freeOutPort :: OutPort -> IO ()
freeOutPort = finalizeForeignPtr
cloneOutPort' :: Ptr LMF.OutPort -> ErrM OutPort
cloneOutPort' = takeM . LMF.libremidi_midi_out_port_clone
cloneOutPort :: OutPort -> ErrM OutPort
cloneOutPort = assocM cloneOutPort'
outPortName' :: Ptr LMF.OutPort -> ErrM Text
outPortName' = textM . LMF.libremidi_midi_out_port_name
outPortName :: OutPort -> ErrM Text
outPortName = assocM outPortName'
data LogLvl = LogLvlWarn | LogLvlErr
deriving stock (Eq, Ord, Show, Enum, Bounded)
type LogFun = LogLvl -> Text -> IO ()
newLogCb :: LogFun -> LogLvl -> IO (Cb LMF.LogFun)
newLogCb f lvl = LMF.newLogCb $ \_ s l _ -> do
msg <- liftIO (TF.fromPtr (coerce s) (fromIntegral l))
f lvl msg
data ObsAct = ObsActAdd | ObsActRem
deriving stock (Eq, Ord, Show, Enum, Bounded)
type ObsFun p = ObsAct -> Ptr p -> IO ()
type ObsInFun = ObsFun LMF.InPort
type ObsOutFun = ObsFun LMF.OutPort
newObsCb :: ObsFun p -> ObsAct -> IO (Cb (LMF.ObsFun p))
newObsCb f = LMF.newObsCb . const . f
type EnumFun p = Ptr p -> IO ()
type EnumInFun = EnumFun LMF.InPort
type EnumOutFun = EnumFun LMF.OutPort
newEnumCb :: EnumFun p -> IO (Cb (LMF.ObsFun p))
newEnumCb = LMF.newObsCb . const
data ObsConfig = ObsConfig
{ ocOnErr :: !(Maybe (Cb LMF.LogFun))
, ocOnWarn :: !(Maybe (Cb LMF.LogFun))
, ocInAdd :: !(Maybe (Cb (LMF.ObsFun LMF.InPort)))
, ocInRem :: !(Maybe (Cb (LMF.ObsFun LMF.InPort)))
, ocOutAdd :: !(Maybe (Cb (LMF.ObsFun LMF.OutPort)))
, ocOutRem :: !(Maybe (Cb (LMF.ObsFun LMF.OutPort)))
, ocTrackHardware :: !Bool
, ocTrackVirtual :: !Bool
, ocTrackAny :: !Bool
, ocNotifInCon :: !Bool
}
deriving stock (Eq)
instance Default ObsConfig where
def =
ObsConfig
{ ocOnErr = Nothing
, ocOnWarn = Nothing
, ocInAdd = Nothing
, ocInRem = Nothing
, ocOutAdd = Nothing
, ocOutRem = Nothing
, ocTrackHardware = True
, ocTrackVirtual = True
, ocTrackAny = True
, ocNotifInCon = True
}
setObsLogCb :: LogFun -> ObsConfig -> IO ObsConfig
setObsLogCb f oc = do
onErr <- newLogCb f LogLvlErr
onWarn <- newLogCb f LogLvlWarn
pure $
oc
{ ocOnErr = Just onErr
, ocOnWarn = Just onWarn
}
setObsInCb :: ObsInFun -> ObsConfig -> IO ObsConfig
setObsInCb f oc = do
onAdd <- newObsCb f ObsActAdd
onRem <- newObsCb f ObsActRem
pure $
oc
{ ocInAdd = Just onAdd
, ocInRem = Just onRem
}
setObsOutCb :: ObsOutFun -> ObsConfig -> IO ObsConfig
setObsOutCb f oc = do
onAdd <- newObsCb f ObsActAdd
onRem <- newObsCb f ObsActRem
pure $
oc
{ ocOutAdd = Just onAdd
, ocOutRem = Just onRem
}
mkObsConfig :: ObsConfig -> IO (ForeignPtr LMF.ObsConfig)
mkObsConfig oc = do
f <- mallocPtr (Proxy @LMF.ObsConfig)
withForeignPtr f $ \p -> do
let
setFun :: forall x. Field LMF.ObsConfig (FunPtr x) -> Cb x -> IO ()
setFun field cb = do
q <- consumeCb cb
let w = castPtrToFunPtr (unsafeForeignPtrToPtr q)
pokeField field p w
FC.addForeignPtrFinalizer f (finalizeForeignPtr q)
traverse_ (setFun LMF.ocOnErr) (ocOnErr oc)
traverse_ (setFun LMF.ocOnWarn) (ocOnWarn oc)
traverse_ (setFun LMF.ocInAdd) (ocInAdd oc)
traverse_ (setFun LMF.ocInRem) (ocInRem oc)
traverse_ (setFun LMF.ocOutAdd) (ocOutAdd oc)
traverse_ (setFun LMF.ocOutRem) (ocOutRem oc)
pokeField LMF.ocTrackHardware p (toCBool (ocTrackHardware oc))
pokeField LMF.ocTrackVirtual p (toCBool (ocTrackVirtual oc))
pokeField LMF.ocTrackAny p (toCBool (ocTrackAny oc))
pokeField LMF.ocNotifInCon p (toCBool (ocNotifInCon oc))
pure f
data MidiPort
= MidiPortIn !(UniquePtr LMF.InPort)
| MidiPortOut !(UniquePtr LMF.OutPort)
deriving stock (Eq)
data MsgFun
= MsgFun1 (Cb (LMF.MsgFun LMF.Sym1))
| MsgFun2 (Cb (LMF.MsgFun LMF.Sym2))
deriving stock (Eq)
msgFunVersion :: MsgFun -> Version
msgFunVersion = \case
MsgFun1 _ -> VersionMidi1
MsgFun2 _ -> VersionMidi2
data MidiConfig = MidiConfig
{ mcVersion :: !(Maybe Version)
, mcPort :: !(Maybe MidiPort)
, mcOnMsg :: !(Maybe MsgFun)
, mcGetTime :: !(Maybe (Cb LMF.TimeFun))
, mcOnErr :: !(Maybe (Cb LMF.LogFun))
, mcOnWarn :: !(Maybe (Cb LMF.LogFun))
, mcVirtualPort :: !Bool
, mcIgnoreSysex :: !Bool
, mcIgnoreTiming :: !Bool
, mcIgnoreSensing :: !Bool
, mcTimestamps :: !TimestampMode
}
deriving stock (Eq)
instance Default MidiConfig where
def =
MidiConfig
{ mcVersion = Nothing
, mcPort = Nothing
, mcOnMsg = Nothing
, mcGetTime = Nothing
, mcOnErr = Nothing
, mcOnWarn = Nothing
, mcVirtualPort = False
, mcIgnoreSysex = False
, mcIgnoreTiming = False
, mcIgnoreSensing = True
, mcTimestamps = TimestampModeAbsolute
}
setMidiLogCb :: LogFun -> MidiConfig -> IO MidiConfig
setMidiLogCb f mc = do
onErr <- newLogCb f LogLvlErr
onWarn <- newLogCb f LogLvlWarn
pure $
mc
{ mcOnErr = Just onErr
, mcOnWarn = Just onWarn
}
mkMidiConfig :: MidiConfig -> IO (ForeignPtr LMF.MidiConfig)
mkMidiConfig mc = do
let ver = fromMaybe (maybe VersionMidi1 msgFunVersion (mcOnMsg mc)) (mcVersion mc)
f <- mallocPtr (Proxy @LMF.MidiConfig)
withForeignPtr f $ \p -> do
let
setFun :: forall x. Field LMF.MidiConfig (FunPtr x) -> Cb x -> IO ()
setFun field cb = do
q <- consumeCb cb
let w = castPtrToFunPtr (unsafeForeignPtrToPtr q)
pokeField field p w
FC.addForeignPtrFinalizer f (finalizeForeignPtr q)
setPtr :: forall x. Field LMF.MidiConfig (Ptr x) -> UniquePtr x -> IO ()
setPtr field u = do
q <- consumeUniquePtr u
let w = unsafeForeignPtrToPtr q
pokeField field p w
FC.addForeignPtrFinalizer f (finalizeForeignPtr q)
case mcPort mc of
Just (MidiPortIn ip) -> setPtr LMF.mcInPort ip
Just (MidiPortOut op) -> setPtr LMF.mcOutPort op
Nothing -> pure ()
case mcOnMsg mc of
Just (MsgFun1 cb1) -> setFun LMF.mcOnMsg1 cb1
Just (MsgFun2 cb2) -> setFun LMF.mcOnMsg2 cb2
Nothing -> pure ()
traverse_ (setFun LMF.mcGetTime) (mcGetTime mc)
traverse_ (setFun LMF.mcOnErr) (mcOnErr mc)
traverse_ (setFun LMF.mcOnWarn) (mcOnWarn mc)
pokeField LMF.mcVersion p (toBitEnum ver)
pokeField LMF.mcVirtualPort p (toCBool (mcVirtualPort mc))
pokeField LMF.mcIgnoreSysex p (toCBool (mcIgnoreSysex mc))
pokeField LMF.mcIgnoreTiming p (toCBool (mcIgnoreTiming mc))
pokeField LMF.mcIgnoreSensing p (toCBool (mcIgnoreSensing mc))
pokeField LMF.mcTimestamps p (toBitEnum (mcTimestamps mc))
pure f
data ApiConfig = ApiConfig
{ acApi :: !Api
, acConfigType :: !ConfigType
}
deriving stock (Eq)
instance Default ApiConfig where
def =
ApiConfig
{ acApi = ApiUnspecified
, acConfigType = ConfigTypeObserver
}
mkApiConfig :: ApiConfig -> IO (ForeignPtr LMF.ApiConfig)
mkApiConfig ac = do
f <- mallocPtr (Proxy @LMF.ApiConfig)
withForeignPtr f $ \p -> do
pokeField LMF.acApi p (toBitEnum (acApi ac))
pokeField LMF.acConfigType p (toBitEnum (acConfigType ac))
pure f
type ObsHandle = ForeignPtr LMF.ObsHandle
newObsHandle :: Api -> ObsConfig -> ErrM ObsHandle
newObsHandle api oc = do
fac <- liftIO (mkApiConfig (ApiConfig {acApi = api, acConfigType = ConfigTypeObserver}))
foc <- liftIO (mkObsConfig oc)
let pac = unsafeForeignPtrToPtr fac
poc = unsafeForeignPtrToPtr foc
fh <- takeM (LMF.libremidi_midi_observer_new poc pac)
liftIO $ do
FC.addForeignPtrFinalizer fh (finalizeForeignPtr fac)
FC.addForeignPtrFinalizer fh (finalizeForeignPtr foc)
pure fh
freeObsHandle :: ObsHandle -> IO ()
freeObsHandle = finalizeForeignPtr
enumInPorts :: ObsHandle -> EnumInFun -> ErrM ()
enumInPorts h f = unRunErrM $ do
cb <- newEnumCb f
withForeignPtr h $ \p ->
withCb cb $ \fun ->
checkM (LMF.libremidi_midi_observer_enumerate_input_ports p nullPtr fun)
enumOutPorts :: ObsHandle -> EnumOutFun -> ErrM ()
enumOutPorts h f = unRunErrM $ do
cb <- newEnumCb f
withForeignPtr h $ \p ->
withCb cb $ \fun ->
checkM (LMF.libremidi_midi_observer_enumerate_output_ports p nullPtr fun)
type InHandle = ForeignPtr LMF.InHandle
newInHandle :: Api -> MidiConfig -> ErrM InHandle
newInHandle api mc = do
fac <- liftIO (mkApiConfig (ApiConfig {acApi = api, acConfigType = ConfigTypeInput}))
fmc <- liftIO (mkMidiConfig mc)
let pac = unsafeForeignPtrToPtr fac
pmc = unsafeForeignPtrToPtr fmc
fh <- takeM (LMF.libremidi_midi_in_new pmc pac)
liftIO $ do
FC.addForeignPtrFinalizer fh (finalizeForeignPtr fac)
FC.addForeignPtrFinalizer fh (finalizeForeignPtr fmc)
pure fh
freeInHandle :: InHandle -> IO ()
freeInHandle = finalizeForeignPtr
inIsConnected :: InHandle -> IO Bool
inIsConnected fih = withForeignPtr fih (fmap fromCBool . LMF.libremidi_midi_in_is_connected)
inAbsTimestamp :: InHandle -> IO Timestamp
inAbsTimestamp fih = withForeignPtr fih (fmap fromCLong . LMF.libremidi_midi_in_absolute_timestamp)
type OutHandle = ForeignPtr LMF.OutHandle
newOutHandle :: Api -> MidiConfig -> ErrM OutHandle
newOutHandle api mc = do
fac <- liftIO (mkApiConfig (ApiConfig {acApi = api, acConfigType = ConfigTypeOutput}))
fmc <- liftIO (mkMidiConfig mc)
let pac = unsafeForeignPtrToPtr fac
pmc = unsafeForeignPtrToPtr fmc
fh <- takeM (LMF.libremidi_midi_out_new pmc pac)
liftIO $ do
FC.addForeignPtrFinalizer fh (finalizeForeignPtr fac)
FC.addForeignPtrFinalizer fh (finalizeForeignPtr fmc)
pure fh
freeOutHandle :: OutHandle -> IO ()
freeOutHandle = finalizeForeignPtr
outIsConnected :: OutHandle -> IO Bool
outIsConnected foh = withForeignPtr foh (fmap fromCBool . LMF.libremidi_midi_out_is_connected)
type Msg1 = Ptr LMF.Sym1
outSendMsg1 :: OutHandle -> Msg1 -> Int -> ErrM ()
outSendMsg1 h msg len = unRunErrM $
withForeignPtr h $ \p ->
checkM (LMF.libremidi_midi_out_send_message p msg (toCSize (fromIntegral len)))
outSchedMsg1 :: OutHandle -> Timestamp -> Msg1 -> Int -> ErrM ()
outSchedMsg1 h ts msg len = unRunErrM $
withForeignPtr h $ \p ->
checkM (LMF.libremidi_midi_out_schedule_message p (toCLong ts) msg (toCSize (fromIntegral len)))
type Msg2 = Ptr LMF.Sym2
outSendMsg2 :: OutHandle -> Msg2 -> Int -> ErrM ()
outSendMsg2 h msg len = unRunErrM $
withForeignPtr h $ \p ->
checkM (LMF.libremidi_midi_out_send_ump p msg (toCSize (fromIntegral len)))
outSchedMsg2 :: OutHandle -> Timestamp -> Msg2 -> Int -> ErrM ()
outSchedMsg2 h ts msg len = unRunErrM $
withForeignPtr h $ \p ->
checkM (LMF.libremidi_midi_out_schedule_ump p (toCLong ts) msg (toCSize (fromIntegral len)))
getVersion :: IO Text
getVersion = LMF.libremidi_get_version >>= text0M
type AvailFun = Maybe Api -> IO ()
newAvailCb :: AvailFun -> IO (Cb LMF.AvailFun)
newAvailCb f = LMF.newAvailCb (\_ -> f . fromBitEnum)
availApis1 :: AvailFun -> IO ()
availApis1 f = do
cb <- newAvailCb f
withCb cb (LMF.libremidi_midi1_available_apis nullPtr)
availApis2 :: AvailFun -> IO ()
availApis2 f = do
cb <- newAvailCb f
withCb cb (LMF.libremidi_midi2_available_apis nullPtr)
apiIdentifier :: Api -> IO Text
apiIdentifier = LMF.libremidi_api_identifier . toBitEnum >=> text0M
apiDisplayName :: Api -> IO Text
apiDisplayName = LMF.libremidi_api_display_name . toBitEnum >=> text0M
getCompiledApiByIdentifier :: Text -> IO (Maybe Api)
getCompiledApiByIdentifier n =
TF.withCString n (fmap fromBitEnum . LMF.libremidi_get_compiled_api_by_identifier)