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