packages feed

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

{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE UndecidableInstances #-}

module Dahdit.Midi.Midi
  ( Channel (..)
  , ChannelCount (..)
  , Note (..)
  , Velocity (..)
  , ControlNum (..)
  , ControlVal (..)
  , Pressure (..)
  , ProgramNum (..)
  , PitchBend (..)
  , Song (..)
  , Position (..)
  , ShortManf (..)
  , LongManf (..)
  , Manf (..)
  , QuarterTimeUnit (..)
  , QuarterTime (..)
  , ChanStatus (..)
  , RtStatus (..)
  , CommonStatus (..)
  , LiveStatus (..)
  , RecStatus (..)
  , ShortStatus (..)
  , ChanStatusType (..)
  , ChanVoiceData (..)
  , ChanModeData (..)
  , ChanData (..)
  , UnivSysEx (..)
  , ManfSysEx (..)
  , SysExData (..)
  , CommonData (..)
  , LiveMsg (..)
  , MetaString (..)
  , MetaData (..)
  , RecMsg (..)
  , ShortMsg (..)
  , msgNoteOn
  , msgNoteOff
  , Event (..)
  , Track (..)
  , MidFileType (..)
  , MidFile (..)
  , SysExDump (..)
  )
where

import Control.Monad (unless, void)
import Control.Newtype (Newtype)
import Dahdit
  ( Binary (..)
  , BinaryRep (..)
  , ByteCount (..)
  , ExactBytes (..)
  , Get
  , Put
  , PutM
  , StaticByteSized (..)
  , ViaBinaryRep (..)
  , ViaGeneric (..)
  , Word16BE (..)
  , Word32BE (..)
  , byteSizeFoldable
  , getByteString
  , getExact
  , getExpect
  , getLookAhead
  , getRemainingSeq
  , getRemainingSize
  , getSeq
  , putByteString
  , putSeq
  )
import Dahdit.Midi.Binary (BoundedBinary (..), MidiInt14 (..), MidiWord14 (..), MidiWord7 (..), VarWord (..))
import Data.Bits (Bits (..))
import Data.ByteString.Short (ShortByteString)
import Data.ByteString.Short qualified as BSS
import Data.Hashable (Hashable)
import Data.Proxy (Proxy (..))
import Data.Sequence (Seq (..))
import Data.Sequence qualified as Seq
import Data.ShortWord (Word4)
import Data.String (IsString (..))
import Data.Word (Word16, Word8)
import GHC.Generics (Generic)

newtype Channel = Channel {unChannel :: MidiWord7}
  deriving stock (Show)
  deriving newtype (Eq, Ord, Enum, Num, Real, Integral, Hashable, StaticByteSized)
  deriving (Binary) via (BoundedBinary "Channel" Channel MidiWord7)

instance Newtype Channel MidiWord7

instance Bounded Channel where
  minBound = 0
  maxBound = 15

newtype ChannelCount = ChannelCount {unChannelCount :: MidiWord7}
  deriving stock (Show)
  deriving newtype
    (Eq, Ord, Enum, Num, Real, Integral, Hashable, StaticByteSized, Binary)

newtype Note = Note {unNote :: MidiWord7}
  deriving stock (Show)
  deriving newtype
    (Eq, Ord, Enum, Num, Real, Integral, Hashable, StaticByteSized, Binary)

newtype Velocity = Velocity {unVelocity :: MidiWord7}
  deriving stock (Show)
  deriving newtype
    (Eq, Ord, Enum, Num, Real, Integral, Hashable, StaticByteSized, Binary)

newtype ControlNum = ControlNum {unControlNum :: MidiWord7}
  deriving stock (Show)
  deriving newtype
    (Eq, Ord, Enum, Num, Real, Integral, Hashable, StaticByteSized, Binary)

newtype ControlVal = ControlVal {unControlVal :: MidiWord7}
  deriving stock (Show)
  deriving newtype
    (Eq, Ord, Enum, Num, Real, Integral, Hashable, StaticByteSized, Binary)

newtype Pressure = Pressure {unPressure :: MidiWord7}
  deriving stock (Show)
  deriving newtype
    (Eq, Ord, Enum, Num, Real, Integral, Hashable, StaticByteSized, Binary)

newtype ProgramNum = ProgramNum {unProgramNum :: MidiWord7}
  deriving stock (Show)
  deriving newtype
    (Eq, Ord, Enum, Num, Real, Integral, Hashable, StaticByteSized, Binary)

newtype PitchBend = PitchBend {unPitchBend :: MidiInt14}
  deriving stock (Show)
  deriving newtype
    (Eq, Ord, Enum, Num, Real, Integral, Hashable, StaticByteSized, Binary)

newtype Song = Song {unSong :: MidiWord7}
  deriving stock (Show)
  deriving newtype
    (Eq, Ord, Enum, Num, Real, Integral, Hashable, StaticByteSized, Binary)

newtype Position = Position {unPosition :: MidiWord14}
  deriving stock (Show)
  deriving newtype
    (Eq, Ord, Enum, Num, Real, Integral, Hashable, StaticByteSized, Binary)

newtype ShortManf = ShortManf {unShortManf :: MidiWord7}
  deriving stock (Show)
  deriving newtype
    (Eq, Ord, Enum, Num, Real, Integral, Hashable, StaticByteSized, Binary)

newtype LongManf = LongManf {unLongManf :: Word16}
  deriving stock (Show)
  deriving newtype
    (Eq, Ord, Enum, Num, Real, Integral, Hashable, StaticByteSized, Binary)

data Manf = ManfShort !ShortManf | ManfLong !LongManf
  deriving stock (Eq, Ord, Show, Generic)

instance Binary Manf where
  byteSize = \case
    ManfShort _ -> 1
    ManfLong _ -> 3
  get = do
    sm <- get @ShortManf
    if sm == 0
      then fmap (ManfLong . LongManf) get
      else pure (ManfShort sm)
  put = \case
    ManfShort sm -> put sm
    ManfLong lm -> put @Word8 0 *> put lm

-- | Manf id usable for non-commercial applications
eduManf :: Manf
eduManf = ManfShort (ShortManf 0x7D)

data QuarterTimeUnit
  = QTUFramesLow
  | QTUFramesHigh
  | QTUSecondsLow
  | QTUSecondsHigh
  | QTUMinutesLow
  | QTUMinutesHigh
  | QTUHoursLow
  | QTUHoursHigh
  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)

data QuarterTime = QuarterTime
  { qtUnit :: !QuarterTimeUnit
  , qtValue :: !Word4
  }
  deriving stock (Eq, Ord, Show, Generic)

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

instance Binary QuarterTime where
  byteSize _ = 1
  get = do
    w <- get @Word8
    let x = shiftR w 4
    unless (x < 8) (fail ("Invalid quarter time unit: " ++ show x))
    let unit = toEnum (fromIntegral x)
        val = fromIntegral (0x0F .&. w)
    pure (QuarterTime unit val)
  put (QuarterTime unit val) =
    put @Word8 (shiftL (fromIntegral (fromEnum unit)) 4 .|. fromIntegral val)

data ChanStatusType
  = ChanStatusNoteOff
  | ChanStatusNoteOn
  | ChanStatusKeyAftertouch
  | ChanStatusControlChange
  | ChanStatusProgramChange
  | ChanStatusChanAftertouch
  | ChanStatusPitchBend
  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)

data CommonStatus
  = CommonStatusTimeFrame
  | CommonStatusSongPointer
  | CommonStatusSongSelect
  | CommonStatusTuneRequest
  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)

data RtStatus
  = RtStatusTimingClock
  | RtStatusStart
  | RtStatusContinue
  | RtStatusStop
  | RtStatusActiveSensing
  | RtStatusSystemReset
  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)

data ChanStatus = ChanStatus !Channel !ChanStatusType
  deriving stock (Eq, Ord, Show, Generic)

-- private
data StatusPeek
  = StatusPeekYes
  | StatusPeekNo !Word8
  deriving stock (Eq, Show)

-- private
peekStatus :: Get StatusPeek
peekStatus = getLookAhead $ do
  b <- get @Word8
  pure $!
    if b .&. 0x80 == 0
      then StatusPeekNo b
      else StatusPeekYes

class HasChanStatus s where
  statusIsChan :: s -> Bool
  statusAsChan :: s -> Maybe ChanStatus
  statusFromChan :: ChanStatus -> s

data LiveStatus
  = LiveStatusChan !ChanStatus
  | LiveStatusSysEx
  | LiveStatusSysCommon !CommonStatus
  | LiveStatusSysRt !RtStatus
  deriving stock (Eq, Ord, Show, Generic)

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

instance HasChanStatus LiveStatus where
  statusIsChan = \case
    LiveStatusChan _ -> True
    _ -> False

  statusAsChan = \case
    LiveStatusChan cs -> Just cs
    _ -> Nothing

  statusFromChan = LiveStatusChan

instance Binary LiveStatus where
  byteSize _ = 1
  get = do
    b <- get @Word8
    let x = b .&. 0xF0
    if
      | x < 0x80 -> fail ("Live status byte with high bit clear: " ++ show b)
      | x == 0xF0 ->
          case b of
            0xF0 -> pure LiveStatusSysEx
            0xF1 -> pure (LiveStatusSysCommon CommonStatusTimeFrame)
            0xF2 -> pure (LiveStatusSysCommon CommonStatusSongPointer)
            0xF3 -> pure (LiveStatusSysCommon CommonStatusSongSelect)
            0xF6 -> pure (LiveStatusSysCommon CommonStatusTuneRequest)
            0xF8 -> pure (LiveStatusSysRt RtStatusTimingClock)
            0xFA -> pure (LiveStatusSysRt RtStatusStart)
            0xFB -> pure (LiveStatusSysRt RtStatusContinue)
            0xFC -> pure (LiveStatusSysRt RtStatusStop)
            0xFE -> pure (LiveStatusSysRt RtStatusActiveSensing)
            0xFF -> pure (LiveStatusSysRt RtStatusSystemReset)
            _ -> fail ("Unknown system status byte: " ++ show b)
      | otherwise -> do
          let c = Channel (fromIntegral (b .&. 0x0F))
          pure $ LiveStatusChan $ ChanStatus c $ case x of
            0x80 -> ChanStatusNoteOff
            0x90 -> ChanStatusNoteOn
            0xA0 -> ChanStatusKeyAftertouch
            0xB0 -> ChanStatusControlChange
            0xC0 -> ChanStatusProgramChange
            0xD0 -> ChanStatusChanAftertouch
            0xE0 -> ChanStatusPitchBend
            _ -> error "impossible"
  put = \case
    LiveStatusChan (ChanStatus c cs) ->
      let d = fromIntegral (unChannel c)
          x = case cs of
            ChanStatusNoteOff -> 0x80
            ChanStatusNoteOn -> 0x90
            ChanStatusKeyAftertouch -> 0xA0
            ChanStatusControlChange -> 0xB0
            ChanStatusProgramChange -> 0xC0
            ChanStatusChanAftertouch -> 0xD0
            ChanStatusPitchBend -> 0xE0
      in  put @Word8 (d .|. x)
    LiveStatusSysEx -> put @Word8 0xF0
    LiveStatusSysCommon cs ->
      let x = case cs of
            CommonStatusTimeFrame -> 0x01
            CommonStatusSongPointer -> 0x02
            CommonStatusSongSelect -> 0x03
            CommonStatusTuneRequest -> 0x06
      in  put @Word8 (0xF0 .|. x)
    LiveStatusSysRt rs ->
      let !x = case rs of
            RtStatusTimingClock -> 0x00
            RtStatusStart -> 0x02
            RtStatusContinue -> 0x03
            RtStatusStop -> 0x04
            RtStatusActiveSensing -> 0x06
            RtStatusSystemReset -> 0x7
      in  put @Word8 (0xF8 .|. x)

data RecStatus
  = RecStatusChan !ChanStatus
  | RecStatusSysEx
  | RecStatusMeta
  deriving stock (Eq, Ord, Show, Generic)

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

instance HasChanStatus RecStatus where
  statusIsChan = \case
    RecStatusChan _ -> True
    _ -> False

  statusAsChan = \case
    RecStatusChan cs -> Just cs
    _ -> Nothing

  statusFromChan = RecStatusChan

instance Binary RecStatus where
  byteSize _ = 1
  get = do
    b <- get @Word8
    let x = b .&. 0xF0
    if
      | x < 0x80 -> fail ("Rec status byte with high bit clear: " ++ show b)
      | x == 0xF0 ->
          case b of
            0xF0 -> pure RecStatusSysEx
            0xFF -> pure RecStatusMeta
            _ -> fail ("Unknown rec status byte: " ++ show b)
      | otherwise -> do
          let c = Channel (fromIntegral (b .&. 0x0F))
          pure $ RecStatusChan $ ChanStatus c $ case x of
            0x80 -> ChanStatusNoteOff
            0x90 -> ChanStatusNoteOn
            0xA0 -> ChanStatusKeyAftertouch
            0xB0 -> ChanStatusControlChange
            0xC0 -> ChanStatusProgramChange
            0xD0 -> ChanStatusChanAftertouch
            0xE0 -> ChanStatusPitchBend
            _ -> error "impossible"
  put = \case
    RecStatusChan (ChanStatus c cs) ->
      let d = fromIntegral (unChannel c)
          x = case cs of
            ChanStatusNoteOff -> 0x80
            ChanStatusNoteOn -> 0x90
            ChanStatusKeyAftertouch -> 0xA0
            ChanStatusControlChange -> 0xB0
            ChanStatusProgramChange -> 0xC0
            ChanStatusChanAftertouch -> 0xD0
            ChanStatusPitchBend -> 0xE0
      in  put @Word8 (d .|. x)
    RecStatusSysEx -> put @Word8 0xF0
    RecStatusMeta -> put @Word8 0xFF

data ShortStatus
  = ShortStatusChan !ChanStatus
  | ShortStatusSysCommon !CommonStatus
  | ShortStatusSysRt !RtStatus
  deriving stock (Eq, Ord, Show, Generic)

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

instance HasChanStatus ShortStatus where
  statusIsChan = \case
    ShortStatusChan _ -> True
    _ -> False

  statusAsChan = \case
    ShortStatusChan cs -> Just cs
    _ -> Nothing

  statusFromChan = ShortStatusChan

instance Binary ShortStatus where
  byteSize _ = 1
  get = do
    b <- get @Word8
    let x = b .&. 0xF0
    if
      | x < 0x80 -> fail ("Short status byte with high bit clear: " ++ show b)
      | x == 0xF0 ->
          case b of
            0xF1 -> pure (ShortStatusSysCommon CommonStatusTimeFrame)
            0xF2 -> pure (ShortStatusSysCommon CommonStatusSongPointer)
            0xF3 -> pure (ShortStatusSysCommon CommonStatusSongSelect)
            0xF6 -> pure (ShortStatusSysCommon CommonStatusTuneRequest)
            0xF8 -> pure (ShortStatusSysRt RtStatusTimingClock)
            0xFA -> pure (ShortStatusSysRt RtStatusStart)
            0xFB -> pure (ShortStatusSysRt RtStatusContinue)
            0xFC -> pure (ShortStatusSysRt RtStatusStop)
            0xFE -> pure (ShortStatusSysRt RtStatusActiveSensing)
            0xFF -> pure (ShortStatusSysRt RtStatusSystemReset)
            _ -> fail ("Unknown system status byte: " ++ show b)
      | otherwise -> do
          let c = Channel (fromIntegral (b .&. 0x0F))
          pure $ ShortStatusChan $ ChanStatus c $ case x of
            0x80 -> ChanStatusNoteOff
            0x90 -> ChanStatusNoteOn
            0xA0 -> ChanStatusKeyAftertouch
            0xB0 -> ChanStatusControlChange
            0xC0 -> ChanStatusProgramChange
            0xD0 -> ChanStatusChanAftertouch
            0xE0 -> ChanStatusPitchBend
            _ -> error "impossible"
  put = \case
    ShortStatusChan (ChanStatus c cs) ->
      let d = fromIntegral (unChannel c)
          x = case cs of
            ChanStatusNoteOff -> 0x80
            ChanStatusNoteOn -> 0x90
            ChanStatusKeyAftertouch -> 0xA0
            ChanStatusControlChange -> 0xB0
            ChanStatusProgramChange -> 0xC0
            ChanStatusChanAftertouch -> 0xD0
            ChanStatusPitchBend -> 0xE0
      in  put @Word8 (d .|. x)
    ShortStatusSysCommon cs ->
      let x = case cs of
            CommonStatusTimeFrame -> 0x01
            CommonStatusSongPointer -> 0x02
            CommonStatusSongSelect -> 0x03
            CommonStatusTuneRequest -> 0x06
      in  put @Word8 (0xF0 .|. x)
    ShortStatusSysRt rs ->
      let !x = case rs of
            RtStatusTimingClock -> 0x00
            RtStatusStart -> 0x02
            RtStatusContinue -> 0x03
            RtStatusStop -> 0x04
            RtStatusActiveSensing -> 0x06
            RtStatusSystemReset -> 0x7
      in  put @Word8 (0xF8 .|. x)

-- | A byte string prefixed by a single-byte length
newtype MetaString = MetaString {unMetaString :: ShortByteString}
  deriving stock (Show)
  deriving newtype (Eq, Ord, IsString)

instance Binary MetaString where
  byteSize = succ . fromIntegral . BSS.length . unMetaString
  get = do
    len <- get @Word8
    s <- getByteString (fromIntegral len)
    pure (MetaString s)
  put (MetaString s) = do
    let len = BSS.length s
    if len > 255
      then do
        put @Word8 255
        putByteString (BSS.take 255 s)
      else do
        put @Word8 (fromIntegral len)
        putByteString s

data MetaData = MetaData
  { mdType :: !Word8
  , mdBody :: !MetaString
  }
  deriving stock (Eq, Ord, Show, Generic)
  deriving (Binary) via (ViaGeneric MetaData)

-- newtype Tempo = Tempo {unTempo :: Word24BE}
--   deriving stock (Show)
--   deriving newtype (Eq, Ord, ByteSized, StaticByteSized, Binary)

-- data MetaData
--   = MDSeqNum !Word16
--   | MDText !MetaString
--   | MDCopyright !MetaString
--   | MDSeqName !MetaString
--   | MDInstName !MetaString
--   | MDLyrics !MetaString
--   | MDMarker !MetaString
--   | MDCuePoint !MetaString
--   | MDChanPrefix !Channel
--   | MDEndTrack
--   | MDSetTempo !Tempo
--   | MDSmpteOffset !Word8 !Word8 !Word8 !Word8 !Word8
--   | MDTimeSig !Word8 !Word8 !Word8 !Word8
--   | MDKeySig !Word8 !Word8
--   | MDSeqSpecific !SysExString
--   deriving stock (Eq, Ord, Show)

-- instance Arb I MetaData where
--   arb = genMD where
--     genMD = genSum $ NE.fromList
--       [ MDSeqNum <$> arb
--       , MDText <$> genS
--       , MDCopyright <$> genS
--       , MDSeqName <$> genS
--       , MDInstName <$> genS
--       , MDLyrics <$> genS
--       , MDMarker <$> genS
--       , MDCuePoint <$> genS
--       , MDChanPrefix <$> arb
--       , pure MDEndTrack
--       -- TODO fill in the rest
--       ]
--     genS = MetaString <$> arbSBS 0 3

-- instance Binary MetaData where
--   byteSize = succ . \case
--     MDSeqNum _ -> 2
--     MDText t -> byteSize t
--     MDCopyright t -> byteSize t
--     MDSeqName t -> byteSize t
--     MDInstName t -> byteSize t
--     MDLyrics t -> byteSize t
--     MDMarker t -> byteSize t
--     MDCuePoint t -> byteSize t
--     MDChanPrefix _ -> 1
--     MDEndTrack -> 0
--     MDSetTempo _ -> 3
--     MDSmpteOffset {} -> 5
--     MDTimeSig {} -> 4
--     MDKeySig _ _ -> 2
--     MDSeqSpecific ss -> byteSize ss
--   get = do
--     m <- get @Word8
--     case m of
--       0x00 -> fmap (MDSeqNum . unWord16BE) get
--       0x01 -> fmap MDText get
--       0x02 -> fmap MDCopyright get
--       0x03 -> fmap MDSeqName get
--       0x04 -> fmap MDInstName get
--       0x05 -> fmap MDLyrics get
--       0x06 -> fmap MDMarker get
--       0x07 -> fmap MDCuePoint get
--       0x20 -> fmap MDChanPrefix get
--       0x2F -> pure MDEndTrack
--       0x51 -> fmap MDSetTempo get
--       0x54 -> MDSmpteOffset <$> get <*> get <*> get <*> get <*> get
--       0x58 -> MDTimeSig <$> get <*> get <*> get <*> get
--       0x59 -> MDKeySig <$> get <*> get
--       0x7F -> fmap MDSeqSpecific get
--       _ -> fail ("Unknown metadata type: " ++ show m)
--   put = \case
--     MDSeqNum w -> put @Word8 0x00 *> put (Word16BE w)
--     MDText s -> put @Word8 0x01 *> put s
--     MDCopyright s -> put @Word8 0x02 *> put s
--     MDSeqName s -> put @Word8 0x03 *> put s
--     MDInstName s -> put @Word8 0x04 *> put s
--     MDLyrics s -> put @Word8 0x05 *> put s
--     MDMarker s -> put @Word8 0x06 *> put s
--     MDCuePoint s -> put @Word8 0x07 *> put s
--     MDChanPrefix c -> put @Word8 0x20 *> put c
--     MDEndTrack -> put @Word8 0x2F
--     MDSetTempo t -> put @Word8 0x51 *> put t
--     MDSmpteOffset {} -> put @Word8 0x54 *> error "TODO"
--     MDTimeSig {} -> put @Word8 0x58 *> error "TODO"
--     MDKeySig {} -> put @Word8 0x59 *> error "TODO"
--     MDSeqSpecific ss -> put @Word8 0x7F *> put ss

data ChanVoiceData
  = ChanVoiceDataNoteOff !Note !Velocity
  | ChanVoiceDataNoteOn !Note !Velocity
  | ChanVoiceKeyAftertouch !Note !Pressure
  | ChanVoiceControlChange !ControlNum !ControlVal
  | ChanVoiceProgramChange !ProgramNum
  | ChanVoiceChanAftertouch !Pressure
  | ChanVoicePitchBend !PitchBend
  deriving stock (Eq, Ord, Show)

byteSizeChanVoiceData :: ChanVoiceData -> ByteCount
byteSizeChanVoiceData = \case
  ChanVoiceDataNoteOff _ _ -> 2
  ChanVoiceDataNoteOn _ _ -> 2
  ChanVoiceKeyAftertouch _ _ -> 2
  ChanVoiceControlChange _ _ -> 2
  ChanVoiceProgramChange _ -> 1
  ChanVoiceChanAftertouch _ -> 1
  ChanVoicePitchBend _ -> 2

-- private
putChanVoiceData :: ChanVoiceData -> Put
putChanVoiceData = \case
  ChanVoiceDataNoteOff n v -> put n *> put v
  ChanVoiceDataNoteOn n v -> put n *> put v
  ChanVoiceKeyAftertouch n p -> put n *> put p
  ChanVoiceControlChange cn cv -> put cn *> put cv
  ChanVoiceProgramChange pn -> put pn
  ChanVoiceChanAftertouch p -> put p
  ChanVoicePitchBend pb -> put pb

data ChanModeData
  = ChanModeAllSoundOff
  | ChanModeResetAllControllers
  | ChanModeLocalControlOff
  | ChanModeLocalControlOn
  | ChanModeAllNotesOff
  | ChanModeOmniOff
  | ChanModeOmniOn
  | ChanModeMonoOn !ChannelCount
  | ChanModeMonoOff
  deriving stock (Eq, Ord, Show, Generic)

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

-- private
putChanModeData :: ChanModeData -> Put
putChanModeData = \case
  ChanModeAllSoundOff -> do
    put @Word8 0x78
    put @Word8 0
  ChanModeResetAllControllers -> do
    put @Word8 0x79
    put @Word8 0
  ChanModeLocalControlOff -> do
    put @Word8 0x7A
    put @Word8 0
  ChanModeLocalControlOn -> do
    put @Word8 0x7A
    put @Word8 0x7F
  ChanModeAllNotesOff -> do
    put @Word8 0x7B
    put @Word8 0
  ChanModeOmniOff -> do
    put @Word8 0x7C
    put @Word8 0
  ChanModeOmniOn -> do
    put @Word8 0x7D
    put @Word8 0
  ChanModeMonoOn cc -> do
    put @Word8 0x7E
    put cc
  ChanModeMonoOff -> do
    put @Word8 0x7F
    put @Word8 0

data ChanData
  = ChanDataVoice !ChanVoiceData
  | ChanDataMode !ChanModeData
  deriving stock (Eq, Ord, Show, Generic)

byteSizeChanData :: ChanData -> ByteCount
byteSizeChanData = \case
  ChanDataVoice cvd -> byteSizeChanVoiceData cvd
  ChanDataMode _ -> staticByteSize (Proxy @ChanModeData)

chanDataType :: ChanData -> ChanStatusType
chanDataType = \case
  ChanDataVoice cvd -> case cvd of
    ChanVoiceDataNoteOff _ _ -> ChanStatusNoteOff
    ChanVoiceDataNoteOn _ _ -> ChanStatusNoteOn
    ChanVoiceKeyAftertouch _ _ -> ChanStatusKeyAftertouch
    ChanVoiceControlChange _ _ -> ChanStatusControlChange
    ChanVoiceProgramChange _ -> ChanStatusProgramChange
    ChanVoiceChanAftertouch _ -> ChanStatusChanAftertouch
    ChanVoicePitchBend _ -> ChanStatusPitchBend
  ChanDataMode _ -> ChanStatusControlChange

-- private
getChanData :: ChanStatus -> Get ChanData
getChanData (ChanStatus _ ty) = case ty of
  ChanStatusNoteOff -> do
    n <- get @Note
    v <- get @Velocity
    pure (ChanDataVoice (ChanVoiceDataNoteOff n v))
  ChanStatusNoteOn -> do
    n <- get @Note
    v <- get @Velocity
    pure (ChanDataVoice (ChanVoiceDataNoteOn n v))
  ChanStatusKeyAftertouch -> do
    n <- get @Note
    p <- get @Pressure
    pure (ChanDataVoice (ChanVoiceKeyAftertouch n p))
  ChanStatusControlChange -> do
    cn <- get @ControlNum
    cv <- get @ControlVal
    case unControlNum cn of
      0x78 -> do
        unless (cv == 0) (fail "Chan mode all sound off must have value 0")
        pure (ChanDataMode ChanModeAllSoundOff)
      0x79 -> do
        unless (cv == 0) (fail "Chan mode reset all controllers must have value 0")
        pure (ChanDataMode ChanModeResetAllControllers)
      0x7A -> do
        case unControlVal cv of
          0 -> pure (ChanDataMode ChanModeLocalControlOff)
          0x7F -> pure (ChanDataMode ChanModeLocalControlOn)
          _ -> fail "Chan mode local control must be 0 or 127"
      0x7B -> do
        unless (cv == 0) (fail "Chan mode all notes off must have value 0")
        pure (ChanDataMode ChanModeAllNotesOff)
      0x7C -> do
        unless (cv == 0) (fail "Chan mode omni off must have value 0")
        pure (ChanDataMode ChanModeOmniOff)
      0x7D -> do
        unless (cv == 0) (fail "Chan mode omni on must have value 0")
        pure (ChanDataMode ChanModeOmniOn)
      0x7E ->
        pure (ChanDataMode (ChanModeMonoOn (ChannelCount (unControlVal cv))))
      0x7F -> do
        unless (cv == 0) (fail "Chan mode mono off must have value 0")
        pure (ChanDataMode ChanModeMonoOff)
      _ -> pure (ChanDataVoice (ChanVoiceControlChange cn cv))
  ChanStatusProgramChange -> do
    pn <- get @ProgramNum
    pure (ChanDataVoice (ChanVoiceProgramChange pn))
  ChanStatusChanAftertouch -> do
    p <- get @Pressure
    pure (ChanDataVoice (ChanVoiceChanAftertouch p))
  ChanStatusPitchBend -> do
    pb <- get @PitchBend
    pure (ChanDataVoice (ChanVoicePitchBend pb))

putChanData :: ChanData -> Put
putChanData = \case
  ChanDataVoice cvd -> putChanVoiceData cvd
  ChanDataMode cmd -> putChanModeData cmd

-- Gets bytestring until delimiter (consuming delimiter but not including in string)
getPayload :: Get ShortByteString
getPayload = go
 where
  go = do
    len <- getLookAhead (goFind 0)
    s <- getByteString len
    _ <- get @Word8
    pure s
  goFind !i = do
    w <- get @Word8
    if w == 0xF7
      then pure i
      else goFind (i + 1)

data UnivSysEx = UnivSysEx
  { useSubId :: !Word8
  , usePayload :: !ShortByteString
  }
  deriving stock (Eq, Ord, Show)

instance Binary UnivSysEx where
  byteSize (UnivSysEx _ p) = 2 + ByteCount (BSS.length p)
  get = do
    i <- get @Word8
    unless (i == 0x7E || i == 0x7F) (fail ("Expected universal sys ex id: " ++ show i))
    fmap (UnivSysEx i) getPayload
  put (UnivSysEx i s) = do
    put i
    putByteString s
    put @Word8 0xF7

data ManfSysEx = ManfSysEx
  { mseManf :: !Manf
  , msePayload :: !ShortByteString
  }
  deriving stock (Eq, Ord, Show)

instance Binary ManfSysEx where
  byteSize (ManfSysEx m p) = 1 + byteSize m + ByteCount (BSS.length p)
  get = do
    m <- get
    fmap (ManfSysEx m) getPayload
  put (ManfSysEx m s) = do
    put m
    putByteString s
    put @Word8 0xF7

data SysExData
  = SysExDataUniv !UnivSysEx
  | SysExDataManf !ManfSysEx
  deriving stock (Eq, Ord, Show, Generic)

instance Binary SysExData where
  byteSize = \case
    SysExDataUniv x -> byteSize x
    SysExDataManf y -> byteSize y
  get = do
    peek <- getLookAhead (get @Word8)
    if peek == 0x7E || peek == 0x7F
      then fmap SysExDataUniv get
      else fmap SysExDataManf get
  put = \case
    SysExDataUniv x -> put x
    SysExDataManf y -> put y

data CommonData
  = CommonDataTimeFrame !QuarterTime
  | CommonDataSongPointer !Position
  | CommonDataSongSelect !Song
  | CommonDataTuneRequest
  deriving stock (Eq, Ord, Show, Generic)

byteSizeCommonData :: CommonData -> ByteCount
byteSizeCommonData = \case
  CommonDataTimeFrame _ -> 1
  CommonDataSongPointer _ -> 2
  CommonDataSongSelect _ -> 1
  CommonDataTuneRequest -> 0

getCommonData :: CommonStatus -> Get CommonData
getCommonData = \case
  CommonStatusTimeFrame -> fmap CommonDataTimeFrame get
  CommonStatusSongPointer -> fmap CommonDataSongPointer get
  CommonStatusSongSelect -> fmap CommonDataSongSelect get
  CommonStatusTuneRequest -> pure CommonDataTuneRequest

putCommonData :: CommonData -> Put
putCommonData = \case
  CommonDataTimeFrame qt -> put qt
  CommonDataSongPointer po -> put po
  CommonDataSongSelect so -> put so
  CommonDataTuneRequest -> pure ()

class (HasChanStatus s) => HasChanData s c | c -> s where
  extractStatus :: c -> s
  embedChanData :: Channel -> ChanData -> c

data LiveMsg
  = LiveMsgChan !Channel !ChanData
  | LiveMsgSysEx !SysExData
  | LiveMsgSysCommon !CommonData
  | LiveMsgSysRt !RtStatus
  deriving stock (Eq, Ord, Show, Generic)

instance HasChanData LiveStatus LiveMsg where
  extractStatus = liveMsgStatus
  embedChanData = LiveMsgChan

instance Binary LiveMsg where
  byteSize =
    succ . \case
      LiveMsgChan _ cd -> byteSizeChanData cd
      LiveMsgSysEx sed -> byteSize sed
      LiveMsgSysCommon cd -> byteSizeCommonData cd
      LiveMsgSysRt _ -> 0
  get = get @LiveStatus >>= getLiveMsgWithStatus
  put = void . putMsgRunning putLiveMsgData Nothing

-- private
getLiveMsgWithStatus :: LiveStatus -> Get LiveMsg
getLiveMsgWithStatus = \case
  LiveStatusChan cs@(ChanStatus chan _) -> fmap (LiveMsgChan chan) (getChanData cs)
  LiveStatusSysEx -> fmap LiveMsgSysEx get
  LiveStatusSysCommon cs -> fmap LiveMsgSysCommon (getCommonData cs)
  LiveStatusSysRt rs -> pure (LiveMsgSysRt rs)

-- private
-- Running status is for Voice and Mode messages only!
getMsgRunning :: (Binary s, HasChanStatus s) => (s -> Get c) -> Maybe ChanStatus -> Get c
getMsgRunning getter mayLastStatus = do
  peeked <- peekStatus
  case peeked of
    StatusPeekYes -> do
      status <- get
      getter status
    StatusPeekNo dat ->
      case mayLastStatus of
        Nothing -> fail ("Expected status byte (no running status): " ++ show dat)
        Just lastStatus -> getter (statusFromChan lastStatus)

-- private
putMsgRunning :: (HasChanData s c, Binary s) => (c -> Put) -> Maybe ChanStatus -> c -> PutM (Maybe ChanStatus)
putMsgRunning putter mayLastStatus msg = do
  mayCurStatus <- putMsgStatusRunning mayLastStatus msg
  putter msg
  pure mayCurStatus

-- private
putMsgStatusRunning :: (HasChanData s c, Binary s) => Maybe ChanStatus -> c -> PutM (Maybe ChanStatus)
putMsgStatusRunning mayLastStatus msg =
  let curStatus = extractStatus msg
      mayChanStatus = statusAsChan curStatus
  in  case mayLastStatus of
        Nothing -> do
          put curStatus
          pure mayChanStatus
        Just lastStatus ->
          case mayChanStatus of
            Just chanStatus ->
              if chanStatus == lastStatus
                then pure mayLastStatus
                else Just chanStatus <$ put curStatus
            _ -> Nothing <$ put curStatus

-- private
putLiveMsgData :: LiveMsg -> Put
putLiveMsgData = \case
  LiveMsgChan _ cd -> putChanData cd
  LiveMsgSysEx sed -> put sed
  LiveMsgSysCommon cd -> putCommonData cd
  LiveMsgSysRt _ -> pure ()

commonMsgStatus :: CommonData -> CommonStatus
commonMsgStatus = \case
  CommonDataTimeFrame _ -> CommonStatusTimeFrame
  CommonDataSongPointer _ -> CommonStatusSongPointer
  CommonDataSongSelect _ -> CommonStatusSongSelect
  CommonDataTuneRequest -> CommonStatusTuneRequest

liveMsgStatus :: LiveMsg -> LiveStatus
liveMsgStatus = \case
  LiveMsgChan chan cd -> LiveStatusChan (ChanStatus chan (chanDataType cd))
  LiveMsgSysEx _ -> LiveStatusSysEx
  LiveMsgSysCommon cd -> LiveStatusSysCommon (commonMsgStatus cd)
  LiveMsgSysRt rs -> LiveStatusSysRt rs

msgNoteOn :: (HasChanData s c) => Channel -> Note -> Velocity -> c
msgNoteOn c k v = embedChanData c (ChanDataVoice (ChanVoiceDataNoteOn k v))

msgNoteOff :: Channel -> Note -> LiveMsg
msgNoteOff c k = msgNoteOn c k 0

data RecMsg
  = RecMsgChan !Channel !ChanData
  | RecMsgSysEx !SysExData
  | RecMsgMeta !MetaData
  deriving stock (Eq, Ord, Show, Generic)

recMsgStatus :: RecMsg -> RecStatus
recMsgStatus = \case
  RecMsgChan chan cd -> RecStatusChan (ChanStatus chan (chanDataType cd))
  RecMsgSysEx _ -> RecStatusSysEx
  RecMsgMeta _ -> RecStatusMeta

instance HasChanData RecStatus RecMsg where
  extractStatus = recMsgStatus
  embedChanData = RecMsgChan

instance Binary RecMsg where
  byteSize =
    succ . \case
      RecMsgChan _ cd -> byteSizeChanData cd
      RecMsgSysEx sed -> byteSize sed
      RecMsgMeta md -> byteSize md
  get = get @RecStatus >>= getRecMsgWithStatus
  put = void . putMsgRunning putRecMsgData Nothing

getRecMsgWithStatus :: RecStatus -> Get RecMsg
getRecMsgWithStatus = \case
  RecStatusChan cs@(ChanStatus chan _) -> fmap (RecMsgChan chan) (getChanData cs)
  RecStatusSysEx -> fmap RecMsgSysEx get
  RecStatusMeta -> fmap RecMsgMeta get

putRecMsgData :: RecMsg -> Put
putRecMsgData = \case
  RecMsgChan _ cd -> putChanData cd
  RecMsgSysEx sed -> put sed
  RecMsgMeta md -> put md

data ShortMsg
  = ShortMsgChan !Channel !ChanData
  | ShortMsgSysCommon !CommonData
  | ShortMsgSysRt !RtStatus
  deriving stock (Eq, Ord, Show, Generic)

instance HasChanData ShortStatus ShortMsg where
  extractStatus = shortMsgStatus
  embedChanData = ShortMsgChan

instance Binary ShortMsg where
  byteSize =
    succ . \case
      ShortMsgChan _ cd -> byteSizeChanData cd
      ShortMsgSysCommon cd -> byteSizeCommonData cd
      ShortMsgSysRt _ -> 0
  get = get @ShortStatus >>= getShortMsgWithStatus
  put = void . putMsgRunning putShortMsgData Nothing

-- private
shortMsgStatus :: ShortMsg -> ShortStatus
shortMsgStatus = \case
  ShortMsgChan chan cd -> ShortStatusChan (ChanStatus chan (chanDataType cd))
  ShortMsgSysCommon cd -> ShortStatusSysCommon $ case cd of
    CommonDataTimeFrame _ -> CommonStatusTimeFrame
    CommonDataSongPointer _ -> CommonStatusSongPointer
    CommonDataSongSelect _ -> CommonStatusSongSelect
    CommonDataTuneRequest -> CommonStatusTuneRequest
  ShortMsgSysRt rs -> ShortStatusSysRt rs

-- private
getShortMsgWithStatus :: ShortStatus -> Get ShortMsg
getShortMsgWithStatus = \case
  ShortStatusChan cs@(ChanStatus chan _) -> fmap (ShortMsgChan chan) (getChanData cs)
  ShortStatusSysCommon cs -> fmap ShortMsgSysCommon (getCommonData cs)
  ShortStatusSysRt rs -> pure (ShortMsgSysRt rs)

-- private
putShortMsgData :: ShortMsg -> Put
putShortMsgData = \case
  ShortMsgChan _ cd -> putChanData cd
  ShortMsgSysCommon cd -> putCommonData cd
  ShortMsgSysRt _ -> pure ()

-- | NOTE: Time delta is in number of ticks since previous message
data Event = Event
  { evDelta :: !VarWord
  , evMsg :: !RecMsg
  }
  deriving stock (Eq, Ord, Show, Generic)

-- private
type TrackMagic = ExactBytes 4 "MTrk"

newtype Track = Track {unTrack :: Seq Event}
  deriving stock (Show)
  deriving newtype (Eq, Ord)

-- private
byteSizeEventsLoop :: ByteCount -> Maybe ChanStatus -> Seq Event -> ByteCount
byteSizeEventsLoop !bc !mayLastStatus = \case
  Empty -> bc
  Event td msg :<| mes ->
    let !tc = byteSize td
        !mayNextStatus = statusAsChan (extractStatus msg)
        !mc = byteSize msg
        !sc = case mayNextStatus of
          Just _ | mayNextStatus == mayLastStatus -> mc - 1
          _ -> mc
    in  byteSizeEventsLoop (bc + tc + sc) mayNextStatus mes

-- private
byteSizeEvents :: Seq Event -> ByteCount
byteSizeEvents = byteSizeEventsLoop 0 Nothing

-- private
putEventsLoop :: Maybe ChanStatus -> Seq Event -> Put
putEventsLoop !mayLastStatus = \case
  Empty -> pure ()
  Event td msg :<| mes -> do
    put td
    mayNextStatus <- putMsgRunning putRecMsgData mayLastStatus msg
    putEventsLoop mayNextStatus mes

-- private
putEvents :: Seq Event -> Put
putEvents = putEventsLoop Nothing

getEventsScope :: ByteCount -> Get (Seq Event)
getEventsScope bc = getExact bc (go Empty Nothing)
 where
  go !acc !mayLastStatus = do
    sz <- getRemainingSize
    -- traceM $ "SIZE LEFT : " ++ show sz
    if sz == 0
      then pure acc
      else do
        td <- get
        msg <- getMsgRunning getRecMsgWithStatus mayLastStatus
        let !me = Event td msg
            !mayNextStatus = statusAsChan (extractStatus msg)
        go (acc :|> me) mayNextStatus

instance Binary Track where
  byteSize (Track events) = 8 + byteSizeEvents events

  get = do
    _ <- get @TrackMagic
    chunkSize <- get @Word32BE
    fmap Track (getEventsScope (fromIntegral chunkSize))

  put t@(Track events) = do
    put @TrackMagic (ExactBytes ())
    put @Word32BE (fromIntegral (byteSize t) - 8)
    putEvents events

data MidFileType
  = MidFileTypeSingle
  | MidFileTypeMultiSync
  | MidFileTypeMultiAsync
  deriving stock (Eq, Ord, Enum, Bounded, Show)
  deriving (StaticByteSized, Binary) via (ViaBinaryRep Word16BE MidFileType)

instance BinaryRep Word16BE MidFileType where
  fromBinaryRep = \case
    0 -> Right MidFileTypeSingle
    1 -> Right MidFileTypeMultiSync
    2 -> Right MidFileTypeMultiAsync
    other -> Left ("invalid midi file type: " ++ show other)
  toBinaryRep = \case
    MidFileTypeSingle -> 0
    MidFileTypeMultiSync -> 1
    MidFileTypeMultiAsync -> 2

-- private
type MidFileMagic = ExactBytes 8 "MThd\NUL\NUL\NUL\ACK"

-- | NOTE: Ticks could also be SMTPE-related, but we don't support that here
data MidFile = MidFile
  { mfType :: !MidFileType
  , mfTicks :: !Word16
  , mfTracks :: !(Seq Track)
  }
  deriving stock (Eq, Ord, Show)

instance Binary MidFile where
  byteSize (MidFile _ _ tracks) = 14 + byteSizeFoldable tracks
  get = do
    _ <- get @MidFileMagic
    ty <- get
    Word16BE numTracks <- get
    Word16BE ticks <- get
    -- traceM ("NUM TRACKS : " ++ show numTracks)
    tracks <- getSeq (fromIntegral numTracks) get
    pure (MidFile ty ticks tracks)
  put (MidFile ty ticks tracks) = do
    put @MidFileMagic (ExactBytes ())
    put ty
    put (Word16BE (fromIntegral (Seq.length tracks)))
    put (Word16BE ticks)
    putSeq put tracks

newtype SysExDump = SysExDump {unSysExDump :: Seq SysExData}
  deriving stock (Show)
  deriving newtype (Eq, Ord)

instance Binary SysExDump where
  byteSize (SysExDump ds) = fromIntegral (Seq.length ds) + byteSizeFoldable ds
  get = fmap SysExDump $ getRemainingSeq $ do
    getExpect @Word8 "sysex status byte" get 0xF0
    get
  put = putSeq (\d -> put @Word8 0xF0 *> put d) . unSysExDump