hommage-0.0.5: Sound/Hommage/Midi.hs
module Sound.Hommage.Midi
(
-- | The action 'writeMidiFile' stores the datatype 'MidiFile' in a MIDI-File.
-- It consists of a field for the 'Ticks' per quarter and a 'MidiTrack' (or some).
-- The function 'runMidiMusic' produces such a track from a 'MidiMusic', which can
-- be created using the functions 'noteMidiMusic', 'restMidiMusic', 'appendMidiMusic' and
-- 'mergeMidiMusic'.
-- 'MidiNote' is used to create note or controller events; it consists of the Midi-Channel
-- ('Chan') and two fields with a 'MidiValue'.
-- * MidiFile
MidiFile (..)
, writeMidiFile
, Ticks
-- * MidiNote
, MidiNote (..)
, Chan
, MidiValue
-- * MidiMusic
, MidiMusic
, noteMidiMusic
, restMidiMusic
, appendMidiMusic
, mergeMidiMusic
, runMidiMusic
-- * MidiTrack
, MidiTrack
, Delta
-- * MidiEvent
, MidiEvent (..)
-- * Encoding
, encodeMidiFile
, midiHeaderSync
, midiHeaderSingle
, midiHeaderAsync
, encodeMidiTrack
, encodeDelta
, encodeMidiDeltaEvent
, encodeEvent
, lastByte
, nextByte
, toBytes
)
where
import Sound.Hommage.WavFile
import Data.Word
import Data.Bits
import Data.Ratio
---------------------------------------------------------------------------------------------------
type Delta = Int
-- | A kind of extended MidiTrack type, that allows delta-times without events
-- (will be summed up by converting to 'MidiTrack').
type MidiMusic = [(Delta, Maybe MidiEvent)]
-- | A sequence of MidiEvents with a delta-time which means the number of ticks
-- before the event happens (relative to the last event).
type MidiTrack = [(Delta, MidiEvent)]
-- | Ticks per Quarter. For some unknown reasons it does not work with any value.
-- 96 seems to be ok.
type Ticks = Int
---------------------------------------------------------------------------------------------------
-- | Converts a 'MidiMusic' to a 'MidiTrack'.
runMidiMusic :: MidiMusic -> MidiTrack
runMidiMusic mt = loop 0 mt
where
loop delta ((d, Just e) : r) = (delta + d, e) : loop 0 r
loop delta ((d, Nothing) : r) = loop (d+delta) r
loop delta [] = [(delta, MidiEndOfTrack)]
-- | Writes a 'MidiFile' to a File.
writeMidiFile :: FilePath -> MidiFile -> IO ()
writeMidiFile fp mf = writeDataFile fp $ encodeMidiFile mf
---------------------------------------------------------------------------------------------------
-- | A Midi-Channel (0-15)
type Chan = Word8
-- | A Midi-Value (0-127)
type MidiValue = Word8
---------------------------------------------------------------------------------------------------
data MidiNote = MidiNote Chan MidiValue MidiValue -- ^ A note with pitch and volume
| MidiCtrl Chan MidiValue MidiValue -- ^ Controllernumber and value
---------------------------------------------------------------------------------------------------
-- | Creates a note or controller event with given length (relative to 'Ticks').
-- The length means the delta-time between note-on and note-off resp. the time after
-- the controller event.
noteMidiMusic :: Int -> MidiNote -> MidiMusic
noteMidiMusic d (MidiNote c n v) =
[(0, Just $ MidiNoteOn c n v), (d, Just $ MidiNoteOff c n v)]
noteMidiMusic d (MidiCtrl c n v) =
[(0, Just $ MidiControl c n v), (d, Nothing)]
-- | A pause with given length.
restMidiMusic :: Int -> MidiMusic
restMidiMusic d = [(d, Nothing)]
---------------------------------------------------------------------------------------------------
-- | Parallel composition of 'MidiMusic'.
mergeMidiMusic :: MidiMusic -> MidiMusic -> MidiMusic
mergeMidiMusic ((d1,m1):r1) ((d2,m2):r2) | d1 < d2 = (d1,m1) : mergeMidiMusic r1 ((d2-d1,m2):r2)
| d1 > d2 = (d2,m2) : mergeMidiMusic ((d1-d2,m1):r1) r2
| otherwise = (d1,m1) : (0, m2) : mergeMidiMusic r1 r2
mergeMidiMusic [] r2 = r2
mergeMidiMusic r1 [] = r1
-- | Sequencial composition of 'MidiMusic'.
appendMidiMusic :: MidiMusic -> MidiMusic -> MidiMusic
appendMidiMusic = (++)
---------------------------------------------------------------------------------------------------
data MidiFile = MidiSync Ticks [MidiTrack] -- ^ Some parallel (synchronous) MidiTracks.
| MidiSingle Ticks MidiTrack -- ^ A single MidiTrack. (Seems not to work yet)
| MidiAsync Ticks [MidiTrack] -- ^ Some asynchronous MidiTracks. (Seems not to work yet)
data MidiEvent = MidiNoteOff !Chan !MidiValue !MidiValue -- note-nr and velocity
| MidiNoteOn !Chan !MidiValue !MidiValue -- note-nr and velocity
| MidiKeyAfter !Chan !MidiValue !MidiValue -- note-nr and velocity
| MidiControl !Chan !MidiValue !MidiValue -- controller-nr and value
| MidiEndOfTrack
-- | MidiProgram !Chan !MidiValue -- program-nr
-- | MidiChannelAfter !Chan !MidiValue -- channel-nr
-- | MidiPitchWheel !Chan !MidiValue !MidiValue -- bottom, top
---------------------------------------------------------------------------------------------------
encodeMidiFile :: MidiFile -> [Word8]
encodeMidiFile mf =
case mf of -- HEADER TRACKS TICKS
MidiSync tc mts -> midiHeaderSync ++ toBytes 2 (length mts) ++ toBytes 2 tc ++ (mts >>= encodeMidiTrack)
MidiSingle tc mt -> midiHeaderSingle ++ toBytes 2 1 ++ toBytes 2 tc ++ encodeMidiTrack mt
MidiAsync tc mts -> midiHeaderAsync ++ toBytes 2 (length mts) ++ toBytes 2 tc ++ (mts >>= encodeMidiTrack)
---------------------------------------------------------------------------------------------------
midiHeaderSingle :: [Word8]
midiHeaderSingle = [ 77, 84, 104, 100, 0, 0, 0, 6, 0, 0]
midiHeaderSync :: [Word8]
midiHeaderSync = [ 77, 84, 104, 100, 0, 0, 0, 6, 0, 1]
midiHeaderAsync :: [Word8]
midiHeaderAsync = [ 77, 84, 104, 100, 0, 0, 0, 6, 0, 2]
---------------------------------------------------------------------------------------------------
encodeMidiTrack :: MidiTrack -> [Word8]
encodeMidiTrack ms = [ 77, 84, 114, 107 ] ++ toBytes 4 (length t) ++ t
where
t = (ms >>= encodeMidiDeltaEvent)
---------------------------------------------------------------------------------------------------
encodeDelta :: Int -> [Word8]
encodeDelta n = loop n
where
loop n | n < 128 = [fromIntegral n]
| otherwise = fromIntegral (128 + mod n 128) : loop (div n 128)
---------------------------------------------------------------------------------------------------
encodeMidiDeltaEvent :: (Delta, MidiEvent) -> [Word8]
encodeMidiDeltaEvent (delta, me) = encodeDelta delta ++ encodeEvent me
encodeEvent :: MidiEvent -> [Word8]
encodeEvent me =
case me of
MidiNoteOff ch no va -> [ch + 128, no, va]
MidiNoteOn ch no va -> [ch + 144, no, va]
MidiKeyAfter ch no va -> [ch + 160, no, va]
MidiControl ch no va -> [ch + 176, no, va]
MidiEndOfTrack -> [255,47,0]
--------------------------------------------------------------------------------------------------
lastByte :: Int -> Word8
lastByte n = fromIntegral (mod n 256)
nextByte :: Int -> Int
nextByte n = div n 256
toBytes :: Int -> Int -> [Word8]
toBytes k n = reverse $ take k $ map lastByte $ iterate nextByte n -- reverse?
---------------------------------------------------------------------------------------------------