{-# LANGUAGE DeriveGeneric #-}
{-# OPTIONS_GHC -funbox-strict-fields #-}
module Sound.MIDI.Types
(
    -- * MIDI messages
    MidiMessage (..),

    -- * Basic MIDI types
    ChannelVoice (..),
    ChannelMode (..),
    SystemCommon (..),
    SystemRealTime (..),
    SystemExclusive (..),

    VendorId (..),

    -- * Numeric MIDI data
    --
    -- | Only use the direct constructors when you can assure that the values
    -- fit into 7 bits! In general you should prefer the smart constructors. For
    -- values outside of the 7 bit range, the numbers should generally wrap
    -- around, but no guarantees are made!
    Channel (..),
    mkChannel,
    Pitch (..),
    mkPitch,
    middleC,
    Patch (..),
    mkPatch,
    Velocity (..),
    mkVelocity,
    Touch (..),
    mkTouch,
    Controller (..),
    mkController,
    PositionPointer (..),
    mkPositionPointer,
    toClocks,

    -- * Helper functions
    to4Bit,
    to7Bit,
    to14Bit
)
where

import Data.Word
import Data.ByteString (ByteString)
import Data.Bits
import GHC.Generics

-- | A data type representing midi messages. Messages can be categorized into 5
-- subcategories
--
--     1. __Channel Voice Messages__. Start, stop, or alter sounds being played.
--     2. __Channel Mode Messages__. Control messages affecting the entire
--     channel.
--     3. __System Real-Time Messages__. Used by sequencers to regulate and
--     synchronize timing.
--     4. __System Common Messages__. Used for song selection, position
--     pointers, etc.
--     5. __System Exclusive Messages__. Used for device-specific extensions to
--     the MIDI protocol.
--
data MidiMessage
    = ChannelVoice ChannelVoice
    | ChannelMode ChannelMode
    | SystemCommon SystemCommon
    | SystemRealTime SystemRealTime
    | SystemExclusive SystemExclusive
    deriving (Eq, Show, Ord, Read, Generic)

-- | Type holding channel voice messages. Channel Voice messages transmit
-- real-time performance data over a single channel. Examples include "note-on"
-- messages which contain a MIDI note number that specifies the note's pitch, a
-- velocity value that indicates how forcefully the note was played, and the
-- channel number; "note-off" messages that end a note; program change messages
-- that change a device's patch; and control changes that allow adjustment of an
-- instrument's parameters.
data ChannelVoice
    = NoteOff !Channel !Pitch !Velocity
    | NoteOn !Channel !Pitch !Velocity
    | Aftertouch !Channel !Pitch !Touch
    | ControlChange !Channel !Controller !Word8
    | PatchChange !Channel !Patch
    | ChannelPressure !Channel !Touch
    | PitchBend !Channel !Word16
    deriving (Eq, Show, Ord, Read, Generic)

-- | A type for channel mode messages. Mode messages determine how an instrument
-- will receive all subsequent voice messages. This includes whether the
-- receiver will play notes monophonically or polyphonically and whether it will
-- respond only to data sent on one specific voice channel or all of them.
data ChannelMode
    = AllSoundOff !Channel
    | ResetAllControllers !Channel
    | LocalControl !Channel !Bool
    | AllNotesOff !Channel
    | OmniOff !Channel
    | OmniOn !Channel
    | MonoOn !Channel !Word8
    | PolyOn !Channel
    deriving (Eq, Show, Ord, Read, Generic)

-- | A type for system common messages. System common messages are intended for
-- all receivers in the system.
data SystemCommon
    = MTCQuarter !Word8
    | SongPosition !PositionPointer
    | SongSelect !Word8
    | TuneRequest
    | EOX
    deriving (Eq, Show, Ord, Read, Generic)

-- | System real time messages. The MIDI System Real Time messages are used to
-- synchronize all of the MIDI clock-based equipment within a system, such as
-- sequencers and drum machines. Most of the System Real Time messages are
-- normally ignored by keyboard instruments and synthesizers. To help ensure
-- accurate timing, System Real Time messages are given priority over other
-- messages, and these single-byte messages may occur anywhere in the data
-- stream (a Real Time message may appear between the status byte and data byte
-- of some other MIDI message).
data SystemRealTime
    = TimingClock
    | Start
    | Continue
    | Stop
    | ActiveSensing
    | SystemReset
    deriving (Eq, Show, Ord, Read, Generic)

-- | System exclusive messages. System Exclusive messages may be used to send
-- data such as patch parameters or sample data between MIDI devices.
-- Manufacturers of MIDI equipment may define their own formats for System
-- Exclusive data. Manufacturers are granted unique identification (ID) numbers
-- by the MMA or the JMSC, and the manufacturer ID number is included as part of
-- the System Exclusive message. See 'VendorId'.
--
-- The representation used here is deliberately generic. Special sets of system
-- exclusive messages can be implemented on top of this type.
data SystemExclusive
    = Exclusive !VendorId ByteString
    deriving (Eq, Show, Ord, Read, Generic)

-- | Data type encapsulating vendor ID numbers as used in 'SystemExclusive'.
-- They have one of two possible formats:
--
-- 1. A one byte ID (represented by 'VendorIdShort')
-- 2. A three byte ID, which must begin with @0x00@. ('VendorIdLong')
data VendorId
    = VendorIdShort !Word8
    | VendorIdLong  !Word8 !Word8
    deriving (Eq, Show, Ord, Read, Generic)

to7Bit :: Integral a => a -> Word8
to7Bit = (.&. 0x7F) . fromIntegral

to4Bit :: Integral a => a -> Word8
to4Bit = (.&. 0x0F) . fromIntegral

newtype Channel = Channel { getChannel :: Word8 }
    deriving (Eq, Show, Ord, Read)

mkChannel :: Integral a => a -> Channel
mkChannel = Channel . to4Bit

newtype Pitch = Pitch { getPitch :: Word8 }
    deriving (Eq, Show, Ord, Read)

mkPitch :: Integral a => a -> Pitch 
mkPitch = Pitch . to7Bit

-- | The middle C on a piano as defined by the MIDI specification. This can
-- serve as a reference value for working with pitches.
middleC :: Pitch
middleC = Pitch 60

newtype Velocity = Velocity { getVelocity :: Word8 }
    deriving (Eq, Show, Ord, Read)

mkVelocity :: Integral a => a -> Velocity 
mkVelocity = Velocity . to7Bit

newtype Touch = Touch { getTouch :: Word8 }
    deriving (Eq, Show, Ord, Read)

mkTouch :: Integral a => a -> Touch 
mkTouch = Touch . to7Bit

newtype Controller = Controller { getController :: Word8 }
    deriving (Eq, Show, Ord, Read)

mkController :: Integral a => a -> Controller 
mkController = Controller . to7Bit

newtype Patch = Patch { getPatch :: Word8 }
    deriving (Eq, Show, Ord, Read)

mkPatch :: Integral a => a -> Patch 
mkPatch = Patch . to7Bit

newtype PositionPointer = PositionPointer { getPositionPointer :: Word16 }
    deriving (Eq, Show, Ord, Read)

to14Bit :: Integral a => a -> Word16
to14Bit = (.&. 0x3FFF) . fromIntegral

mkPositionPointer :: Integral a => a -> PositionPointer
mkPositionPointer = PositionPointer . to14Bit

-- | Convert a 'PositionPointer', used to indicate song position, to the MIDI
-- clock. Song Position Pointer is always multiplied by 6 times the MIDI clocks
-- (F8H). Thus the smallest Song Position change is 6 MIDI clocks, or 1/16 note.
toClocks :: PositionPointer -> Int
toClocks = (* 6) . fromIntegral . getPositionPointer
{-# INLINEABLE toClocks #-}