PortMidi-simple-0.1.0.0: src/Sound/PortMidi/Simple.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StrictData #-}
module Sound.PortMidi.Simple where
import Control.Concurrent (threadDelay)
import Data.Word (Word8, Word64)
import Data.Foldable (find)
import Data.Bits (Bits(..), (.&.), shiftR)
import Data.List (isPrefixOf)
import Data.Traversable (for)
import Control.Exception (Exception(..), bracket, bracket_, throwIO)
import qualified Sound.PortMidi as Midi
import qualified Foreign
-- * Exceptional wrappers
data MidiError
= MidiError Midi.PMError
| SysExTruncated
deriving (Show)
instance Exception MidiError
midi :: IO (Either Midi.PMError b) -> IO b
midi action = action >>= \case
Left err ->
throwIO $ MidiError err
Right res ->
pure res
midi_ :: IO (Either Midi.PMError Midi.PMSuccess) -> IO ()
midi_ action = () <$ midi action
-- * Global library wrapper
withMidi :: IO a -> IO a
withMidi = bracket_ midiInitialize midiTerminate
-- TODO: make reenterable?
midiInitialize :: IO ()
midiInitialize = midi_ Midi.initialize
midiTerminate :: IO ()
midiTerminate = midi_ Midi.terminate
-- * Devices
-- ** Enumeration
getDevices :: IO [(Midi.DeviceID, Midi.DeviceInfo)]
getDevices = do
n <- Midi.countDevices
for [0 .. n-1] \device -> do
info <- Midi.getDeviceInfo device
pure (device, info)
findDevice :: ((Midi.DeviceID, Midi.DeviceInfo) -> Bool) -> IO (Maybe (Midi.DeviceID, Midi.DeviceInfo))
findDevice pred = do
devices <- getDevices
pure $ find pred devices
findInputNamed :: [Char] -> IO (Maybe Midi.DeviceID)
findInputNamed name =
findDevice pred >>= \case
Nothing ->
pure Nothing
Just (device, _info) ->
pure $ Just device
where
pred (_device, Midi.DeviceInfo{input, name=devicName}) =
input && isPrefixOf name devicName
findOutputNamed :: [Char] -> IO (Maybe Midi.DeviceID)
findOutputNamed name =
findDevice pred >>= \case
Nothing ->
pure Nothing
Just (device, _info) ->
pure $ Just device
where
pred (_device, Midi.DeviceInfo{output, name=devicName}) =
output && isPrefixOf name devicName
-- ** Input
withInput :: Midi.DeviceID -> (Midi.PMStream -> IO c) -> IO c
withInput device action = bracket (midiOpenInput device) midiClose action
midiOpenInput :: Midi.DeviceID -> IO Midi.PMStream
midiOpenInput device = midi $ Midi.openInput device
-- ** Output
withOutput :: Midi.DeviceID -> Int -> (Midi.PMStream -> IO c) -> IO c
withOutput device latency action = bracket (midiOpenOutput device latency) midiClose action
midiOpenOutput :: Midi.DeviceID -> Int -> IO Midi.PMStream
midiOpenOutput device latency = midi $ Midi.openOutput device latency
-- ** Common
midiClose :: Midi.PMStream -> IO ()
midiClose stream = midi_ $ Midi.close stream
-- * Streams
-- ** Input
type Timestamp = Integer
data Message
= Channel Int ChannelMessage
| Raw Midi.PMEvent -- ^ Unparsed message data
| SysEx [Word8]
| Realtime Word8
deriving (Eq, Show)
type ChannelReader r = IO [(Timestamp, Message)] -> IO r
withReadMessages :: Midi.PMStream -> Int -> ChannelReader r -> IO r
withReadMessages stream bufSize action =
withReadEvents stream bufSize $
action . mkReadMessages
mkReadMessages :: IO [Midi.PMEvent] -> IO [(Timestamp, Message)]
mkReadMessages readEvents = do
{- XXX: events may contain realtime byte-sized messages and can be truncated.
> When receiving sysex messages, the sysex message is terminated by either
> an EOX status byte (anywhere in the 4 byte messages) or by a non-real-time
> status byte in the low order byte of the message.
> If you get a non-real-time status byte but there was no EOX byte, it means
> the sysex message was somehow truncated.
> This is not considered an error; e.g., a missing EOX can result from
> the user disconnecting a MIDI cable during sysex transmission.
http://portmedia.sourceforge.net/portmidi/doxygen/structPmEvent.html
-}
events <- readEvents
readMsg [] events
where
readMsg acc = \case
[] ->
pure $ reverse acc
buf@(event@Midi.PMEvent{timestamp, message} : next) ->
if message .&. 0xFF == 0xF0 then
readSysEx0 acc [] buf
else
let
msg =
case channelMessage (Midi.decodeMsg message) of
Nothing ->
Raw event
Just msg ->
msg
in
readMsg ((toInteger timestamp, msg) : acc) next
readSysEx0 msgs acc = \case
[] ->
throwIO SysExTruncated
cur : next -> do
let
quad = fromIntegral (Midi.message cur) :: Word64
chunk = map fromIntegral -- XXX: goes via Integer
[ 0xFF .&. quad
, 0xFF .&. quad `shiftR` 8
, 0xFF .&. quad `shiftR` 16
, 0xFF .&. quad `shiftR` 24
]
-- TODO: extract stray realtimes
if elem 0xF7 chunk then
let
msg =
( fromIntegral $ Midi.timestamp cur
, SysEx . concat $ reverse (chunk : acc)
)
in
readMsg (msg : msgs) next
else
readSysEx0 msgs (chunk : acc) next
withReadEvents :: Midi.PMStream -> Int -> (IO [Midi.PMEvent] -> IO r) -> IO r
withReadEvents stream bufSize action =
Foreign.allocaArray bufSize \buf ->
let
readEvents = do
Midi.PMEventCount count <- midi $
Midi.readEventsToBuffer stream buf (fromIntegral bufSize)
Foreign.peekArray (fromIntegral count) buf
-- pure do
-- Midi.PMEvent{timestamp, message} <- events
-- pure (fromIntegral timestamp, Midi.decodeMsg message)
in
action readEvents
waitInput :: Int -> Midi.PMStream -> IO ()
waitInput rate inStream =
midi (Midi.poll inStream) >>= \case
Midi.GotData ->
pure ()
Midi.NoError'NoData -> do
threadDelay rate
waitInput rate inStream
-- * Message
{- XXX: stripped-down Codec.Midi.Message
Copyright: George Giorgidze
-}
data ChannelMessage
= NoteOff
{ key :: !Int
, velocity :: !Int
}
| NoteOn
{ key :: !Int
, velocity :: !Int
}
| KeyPressure
{ key :: !Int
, pressure :: !Int
}
| ControlChange
{ controllerNumber :: !Int
, controllerValue :: !Int
}
| ProgramChange
{ preset :: !Int
}
| ChannelPressure
{ pressure :: !Int
}
| PitchWheel
{ pitchWheel :: !Int
}
deriving (Show,Eq)
{- XXX: adapted Euterpea.IO.MIDI.MidiIO.msgToMidi
Copyright (c) 2008-2015 Euterpea authors
-}
-- -- type Channel = Int
channelMessage :: Midi.PMMsg -> Maybe Message
channelMessage (Midi.PMMsg m d1 d2) =
case (m .&. 0xF0) `shiftR` 4 of
0x8 -> chan $ NoteOff (fromIntegral d1) (fromIntegral d2)
0x9 -> chan $ NoteOn (fromIntegral d1) (fromIntegral d2)
0xA -> chan $ KeyPressure (fromIntegral d1) (fromIntegral d2)
0xB -> chan $ ControlChange (fromIntegral d1) (fromIntegral d2)
0xC -> chan $ ProgramChange (fromIntegral d1)
0xD -> chan $ ChannelPressure (fromIntegral d1)
0xE -> chan $ PitchWheel (fromIntegral $ d1 + d2 `shiftL` 8)
_ -> Nothing
where
chan = Just . Channel (fromIntegral $ m .&. 0x0F)