packages feed

dahdit-midi-0.5.1: src/Dahdit/Midi/Binary.hs

{-# LANGUAGE TemplateHaskell #-}

module Dahdit.Midi.Binary
  ( getTermText
  , putTermText
  , BoundedBinary (..)
  , MidiWord7 (..)
  , MidiInt7 (..)
  , MidiWord14 (..)
  , MidiInt14 (..)
  , VarWord (..)
  , Word14
  , Int14
  )
where

import Control.Newtype (Newtype (..))
import Dahdit (Binary (..), Get, Put, StaticByteSized (..), TermBytes8 (..), Word16BE (..), putText)
import Data.Bits (Bits (..))
import Data.ByteString.Short qualified as BSS
import Data.Hashable (Hashable)
import Data.Proxy (Proxy (..))
import Data.ShortWord (Int7, Word7)
import Data.ShortWord.TH (mkShortWord)
import Data.Text (Text)
import Data.Text.Encoding qualified as TE
import Data.Word (Word16, Word32, Word8)
import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)

getTermText :: Get Text
getTermText = do
  TermBytes8 bs <- get
  case TE.decodeUtf8' (BSS.fromShort bs) of
    Left err -> fail ("Invalid UTF-8: " ++ show err)
    Right s -> pure s

putTermText :: Text -> Put
putTermText s = putText s *> put @Word8 0

newtype BoundedBinary (s :: Symbol) a b = BoundedBinary {unBoundedBinary :: a}

instance
  (KnownSymbol s, Bounded a, Ord a, Show a, Newtype a b, Binary b)
  => Binary (BoundedBinary s a b)
  where
  get = do
    w <- get
    let v = pack w
    if v < minBound || v > maxBound
      then fail (symbolVal (Proxy :: Proxy s) ++ " value out of bounds: " ++ show v)
      else pure (BoundedBinary v)
  put = put . unpack . unBoundedBinary

newtype MidiWord7 = MidiWord7 {unMidiWord7 :: Word7}
  deriving stock (Show)
  deriving newtype (Eq, Ord, Enum, Bounded, Num, Real, Integral, Hashable)

instance StaticByteSized MidiWord7 where
  type StaticSize MidiWord7 = 1
  staticByteSize _ = 1

instance Binary MidiWord7 where
  byteSize _ = 1
  get = do
    w <- get @Word8
    if w .&. 0x80 == 0
      then pure (MidiWord7 (fromIntegral w))
      else fail ("Word7 high bit set: " ++ show w)
  put v = put @Word8 (0x7F .&. fromIntegral (unMidiWord7 v))

newtype MidiInt7 = MidiInt7 {unMidiInt7 :: Int7}
  deriving stock (Show)
  deriving newtype (Eq, Ord, Enum, Bounded, Num, Real, Integral, Hashable)

instance StaticByteSized MidiInt7 where
  type StaticSize MidiInt7 = 1
  staticByteSize _ = 1

instance Binary MidiInt7 where
  byteSize _ = 1
  get = do
    w <- get @Word8
    if w .&. 0x80 == 0
      then pure (MidiInt7 (fromIntegral w))
      else fail ("Int7 high bit set: " ++ show w)
  put v = put @Word8 (0x7F .&. fromIntegral (unMidiInt7 v))

mkShortWord "Word14" "Word14" "aWord14" "Int14" "Int14" "anInt14" ''Word16 14 []

expandW14 :: Word14 -> Word16
expandW14 w =
  let x = fromIntegral w :: Word16
      xLo = x .&. 0x007F
      xHi = shiftL x 1 .&. 0x7F00
  in  xHi .|. xLo

contractW14 :: Word16 -> Word14
contractW14 v =
  let vLo = v .&. 0x007F
      vHi = v .&. 0x7F00
      x = shiftR vHi 1 .|. vLo
  in  fromIntegral x

newtype MidiWord14 = MidiWord14 {unMidiWord14 :: Word14}
  deriving stock (Show)
  deriving newtype (Eq, Ord, Enum, Bounded, Num, Real, Integral, Hashable)

instance StaticByteSized MidiWord14 where
  type StaticSize MidiWord14 = 2
  staticByteSize _ = 2

instance Binary MidiWord14 where
  byteSize _ = 2
  get = fmap (MidiWord14 . contractW14 . unWord16BE) get
  put = put . Word16BE . expandW14 . unMidiWord14

newtype MidiInt14 = MidiInt14 {unMidiInt14 :: Int14}
  deriving stock (Show)
  deriving newtype (Eq, Ord, Enum, Bounded, Num, Real, Integral, Hashable)

instance StaticByteSized MidiInt14 where
  type StaticSize MidiInt14 = 2
  staticByteSize _ = 2

instance Binary MidiInt14 where
  byteSize _ = 2
  get = fmap (MidiInt14 . fromIntegral . contractW14 . unWord16BE) get
  put = put . Word16BE . expandW14 . fromIntegral . unMidiInt14

newtype VarWord = VarWord {unVarWord :: Word32}
  deriving stock (Show)
  deriving newtype (Eq, Ord, Enum, Num, Integral, Real, Hashable)

instance Bounded VarWord where
  minBound = VarWord 0
  maxBound = VarWord 0x00FFFFFF

instance Binary VarWord where
  byteSize (VarWord w) =
    if
      | w .&. 0xFFFFFF80 == 0 -> 1
      | w .&. 0xFFFFC000 == 0 -> 2
      | w .&. 0xFFE00000 == 0 -> 3
      | otherwise -> 4
  get = go 0 0
   where
    go !off !acc = do
      w <- get @Word8
      let wLow = fromIntegral (w .&. 0x7F)
          wShift = shiftL wLow off
          accNext = acc .|. wShift
      if w .&. 0x80 == 0
        then pure (VarWord accNext)
        else go (off + 7) accNext

  put (VarWord acc) = go (0 :: Int) acc
   where
    go !off !w = do
      let wLow = fromIntegral (w .&. 0x7F)
          wShift = shiftR w 7
      if wShift == 0 || off == 3
        then put @Word8 wLow
        else put @Word8 (wLow .|. 0x80) *> go (off + 1) wShift