dahdit-midi-0.5.4: src/Dahdit/Midi/Osc.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE UndecidableInstances #-}
module Dahdit.Midi.Osc where
import Control.Monad (replicateM_)
import Dahdit
( Binary (..)
, ByteCount (..)
, DoubleBE (..)
, FloatBE (..)
, Get
, Int32BE (..)
, Int64BE (..)
, Put
, StaticByteSized (..)
, TermBytes8 (..)
, Word32BE
, Word64BE (..)
, byteSizeFoldable
, byteSizeViaStatic
, getByteString
, getExact
, getExpect
, getLookAhead
, getRemainingSeq
, getRemainingSize
, putByteString
)
import Dahdit.Midi.Binary (getTermText, putTermText)
import Dahdit.Midi.Midi (ShortMsg)
import Dahdit.Midi.OscAddr (RawAddrPat (..))
import Dahdit.Midi.Pad (byteSizePad32, getPad32, pad32, putPad32)
import Data.ByteString.Internal (c2w, w2c)
import Data.ByteString.Short (ShortByteString)
import Data.ByteString.Short qualified as BSS
import Data.Coerce (coerce)
import Data.Foldable (foldMap', for_)
import Data.Int (Int32, Int64)
import Data.Monoid (Sum (..))
import Data.Sequence (Seq (..))
import Data.Sequence qualified as Seq
import Data.Text (Text)
import Data.Text qualified as T
import Data.Word (Word8)
import GHC.Generics (Generic)
import Nanotime (NtpTime (..))
data DatumType
= DatumTypeInt32
| DatumTypeInt64
| DatumTypeFloat
| DatumTypeDouble
| DatumTypeString
| DatumTypeBlob
| DatumTypeTime
| DatumTypeMidi
deriving stock (Eq, Ord, Show, Enum, Bounded)
datumTypeRep :: DatumType -> Char
datumTypeRep = \case
DatumTypeInt32 -> 'i'
DatumTypeInt64 -> 'h'
DatumTypeFloat -> 'f'
DatumTypeDouble -> 'd'
DatumTypeString -> 's'
DatumTypeBlob -> 'b'
DatumTypeTime -> 't'
DatumTypeMidi -> 'm'
datumTypeUnRep :: Char -> Maybe DatumType
datumTypeUnRep = \case
'i' -> Just DatumTypeInt32
'h' -> Just DatumTypeInt64
'f' -> Just DatumTypeFloat
'd' -> Just DatumTypeDouble
's' -> Just DatumTypeString
'b' -> Just DatumTypeBlob
't' -> Just DatumTypeTime
'm' -> Just DatumTypeMidi
_ -> Nothing
newtype Port = Port {unPort :: Word8}
deriving stock (Show)
deriving newtype (Eq, Ord, Binary, StaticByteSized)
data PortMsg = PortMsg !Port !ShortMsg
deriving stock (Eq, Ord, Show, Generic)
instance StaticByteSized PortMsg where
type StaticSize PortMsg = 4
staticByteSize _ = 4
instance Binary PortMsg where
byteSize = byteSizeViaStatic
get = do
p <- get
m <- get
replicateM_ (3 - unByteCount (byteSize m)) (getExpect "port msg pad" (get @Word8) 0)
pure (PortMsg p m)
put (PortMsg p m) = do
put p
put m
replicateM_ (3 - unByteCount (byteSize m)) (put @Word8 0)
data Datum
= DatumInt32 !Int32
| DatumInt64 !Int64
| DatumFloat !Float
| DatumDouble !Double
| DatumString !Text
| DatumBlob !ShortByteString
| DatumTime !NtpTime
| DatumMidi !PortMsg
deriving stock (Eq, Ord, Show)
datumSizer :: Datum -> ByteCount
datumSizer = \case
DatumInt32 _ -> 4
DatumInt64 _ -> 8
DatumFloat _ -> 4
DatumDouble _ -> 8
DatumString x -> ByteCount (1 + T.length x)
DatumBlob x -> ByteCount (4 + BSS.length x)
DatumTime _ -> 8
DatumMidi _ -> 4
datumGetter :: DatumType -> Get Datum
datumGetter = \case
DatumTypeInt32 -> DatumInt32 . unInt32BE <$> get
DatumTypeInt64 -> DatumInt64 . unInt64BE <$> get
DatumTypeFloat -> DatumFloat . unFloatBE <$> get
DatumTypeDouble -> DatumDouble . unDoubleBE <$> get
DatumTypeString -> DatumString <$> getPad32 getTermText
DatumTypeBlob -> fmap DatumBlob $ getPad32 $ do
w <- get @Word32BE
getByteString (fromIntegral w)
DatumTypeTime -> DatumTime . NtpTime . unWord64BE <$> get
DatumTypeMidi -> DatumMidi <$> get
datumPutter :: Datum -> Put
datumPutter = putPad32 datumSizer $ \case
DatumInt32 x -> put (Int32BE x)
DatumInt64 x -> put (Int64BE x)
DatumFloat x -> put (FloatBE x)
DatumDouble x -> put (DoubleBE x)
DatumString x -> putTermText x
DatumBlob x -> do
put @Word32BE (fromIntegral (BSS.length x))
putByteString x
DatumTime x -> put (Word64BE (unNtpTime x))
DatumMidi x -> put x
datumType :: Datum -> DatumType
datumType = \case
DatumInt32 _ -> DatumTypeInt32
DatumInt64 _ -> DatumTypeInt64
DatumFloat _ -> DatumTypeFloat
DatumDouble _ -> DatumTypeDouble
DatumString _ -> DatumTypeString
DatumBlob _ -> DatumTypeBlob
DatumTime _ -> DatumTypeTime
DatumMidi _ -> DatumTypeMidi
class IsDatum a where
toDatum :: a -> Datum
instance IsDatum Int32 where
toDatum = DatumInt32
instance IsDatum Int64 where
toDatum = DatumInt64
instance IsDatum Float where
toDatum = DatumFloat
instance IsDatum Double where
toDatum = DatumDouble
instance IsDatum Text where
toDatum = DatumString
instance IsDatum ShortByteString where
toDatum = DatumBlob
instance IsDatum NtpTime where
toDatum = DatumTime
instance IsDatum PortMsg where
toDatum = DatumMidi
newtype Sig = Sig {unSig :: Seq DatumType}
deriving stock (Show)
deriving newtype (Eq, Ord)
commaByte :: Word8
commaByte = c2w ','
hashByte :: Word8
hashByte = c2w '#'
getNextNonPad :: Get (Maybe Word8)
getNextNonPad = do
sz <- getRemainingSize
if sz == 0
then pure Nothing
else do
w <- getLookAhead (get @Word8)
if w == 0
then pure Nothing
else fmap Just (get @Word8)
sigSizer :: Sig -> ByteCount
sigSizer (Sig dts) = ByteCount (2 + Seq.length dts)
instance Binary Sig where
byteSize = byteSizePad32 sigSizer
get = getPad32 (getExpect "comma" get commaByte *> fmap Sig (go Empty))
where
go !acc = do
mnext <- getNextNonPad
case mnext of
Just w -> do
case datumTypeUnRep (w2c w) of
Nothing -> fail ("Unknown data type rep: " ++ show w)
Just dt -> go (acc :|> dt)
Nothing -> acc <$ getExpect "pad" (get @Word8) 0
put = putPad32 sigSizer $ \(Sig dts) -> do
put commaByte
for_ dts (put . c2w . datumTypeRep)
put @Word8 0
data Msg = Msg !RawAddrPat !(Seq Datum)
deriving stock (Eq, Ord, Show)
instance Binary Msg where
byteSize (Msg r ds) =
byteSize r
+ pad32 (ByteCount (2 + Seq.length ds))
+ getSum (foldMap' (Sum . pad32 . datumSizer) ds)
get = do
r <- get @RawAddrPat
s <- get @Sig
ds <- traverse datumGetter (unSig s)
pure (Msg r ds)
put (Msg r ds) = do
put r
put (Sig (fmap datumType ds))
for_ ds datumPutter
data Bundle = Bundle !NtpTime !(Seq Packet)
deriving stock (Eq, Ord, Show)
bundleTag :: TermBytes8
bundleTag = TermBytes8 "#bundle"
instance Binary Bundle where
byteSize (Bundle _ packs) = 16 + ByteCount (4 * Seq.length packs) + byteSizeFoldable packs
get = do
getExpect "bundle tag" (get @TermBytes8) bundleTag
t <- fmap (NtpTime . coerce) (get @Word64BE)
packs <- getRemainingSeq $ do
sz <- fmap (ByteCount . fromIntegral) (get @Word32BE)
getExact sz get
pure (Bundle t packs)
put (Bundle (NtpTime k) packs) = do
put bundleTag
put @Word64BE (coerce k)
for_ packs $ \pack -> do
put @Word32BE (fromIntegral (byteSize pack))
put pack
data Packet
= PacketMsg !Msg
| PacketBundle !Bundle
deriving stock (Eq, Ord, Show, Generic)
instance Binary Packet where
byteSize = \case
PacketMsg msg -> byteSize msg
PacketBundle bun -> byteSize bun
get = do
w <- getLookAhead (get @Word8)
if w == hashByte
then fmap PacketBundle get
else fmap PacketMsg get
put = \case
PacketMsg msg -> put msg
PacketBundle bun -> put bun
immediately :: NtpTime
immediately = NtpTime 1