packages feed

midi-0.2.2: test/Main.hs

{-
ToDo:

Check parsing and serialization of single MIDI messages.
-}
module Main where

import qualified Example
import qualified Parser

import qualified Test.QuickCheck as QC
import Common (check, )

import qualified Sound.MIDI.File      as MidiFile
import qualified Sound.MIDI.File.Load as Load
import qualified Sound.MIDI.File.Save as Save
import qualified Sound.MIDI.File.Event as Event

import qualified Sound.MIDI.Message.Channel.Voice as VoiceMsg
import qualified Sound.MIDI.Parser.Report as Report

import qualified Data.EventList.Relative.TimeBody as EventList

import qualified Data.ByteString.Lazy as B
import qualified Data.List as List
import qualified Data.List.HT as ListHT
import qualified Data.List.Match as Match
import Data.Int (Int64, )
import Data.Ord.HT (comparing, )

import Control.Monad (liftM, when, )



testMidiName :: FilePath
testMidiName = "quickcheck-test.mid"

runExample :: MidiFile.T -> IO ()
runExample example =
   let bin    = Save.toByteString example
       struct = Load.maybeFromByteString bin
       report = Report.Cons [] (Right example)
   in  B.writeFile testMidiName bin >>
       print (struct == report) >>
       when (struct/=report)
          (print struct >> print report)

-- provoke a test failure in order to see some examples of Arbitrary MIDI files
checkArbitrary :: MidiFile.T -> Bool
checkArbitrary (MidiFile.Cons _typ _division tracks) =
   all ((< 10) . length . EventList.toPairList) tracks


{- |
Increase the probability of similar adjacent messages
that can be compressed using the running status.
-}
newtype SortedFile = SortedFile {getSortedFile :: MidiFile.T}
   deriving Show

sortedFile :: MidiFile.T -> SortedFile
sortedFile =
   SortedFile .
   MidiFile.mapTrack
      (EventList.fromPairList .
       List.sortBy (comparing snd) . EventList.toPairList)

instance QC.Arbitrary SortedFile where
   arbitrary = liftM sortedFile QC.arbitrary
   shrink = map sortedFile . QC.shrink . getSortedFile


saveLoadByteString :: MidiFile.T -> Bool
saveLoadByteString midi =
   let bin    = Save.toByteString midi
       struct = Load.maybeFromByteString bin
       report = Report.Cons [] (Right midi)
   in  struct == report

saveLoadCompressedByteString :: MidiFile.T -> Bool
saveLoadCompressedByteString midi =
   let bin    = Save.toCompressedByteString midi
       struct = Load.maybeFromByteString bin
       report = Report.Cons [] (Right (MidiFile.implicitNoteOff midi))
   in  struct == report

compressionShortens :: MidiFile.T -> Bool
compressionShortens midi =
   B.length (Save.toByteString midi)
   >=
   B.length (Save.toCompressedByteString midi)


{-
This does not cover all cases of possible running status compression,
but the most common ones.
-}
equalStatus :: Event.T -> Event.T -> Bool
equalStatus x y =
   case (Event.maybeVoice x, Event.maybeVoice y) of
      (Just (ch0, ev0), Just (ch1, ev1)) ->
         ch0 == ch1
         &&
         case (ev0, ev1) of
            (VoiceMsg.Control _ _, VoiceMsg.Control _ _) -> True
            _ ->
               VoiceMsg.isNoteOn ev0 && VoiceMsg.isNoteOn ev1
               ||
               VoiceMsg.isNoteOff ev0 && VoiceMsg.isNoteOff ev1
      _ -> False

{-
You may test manually with Example.status
which is definitely compressible.
-}
compressible :: MidiFile.T -> Bool
compressible =
   any (or . ListHT.mapAdjacent equalStatus . EventList.getBodies) .
   MidiFile.getTracks

compressionStrictlyShortens :: MidiFile.T -> QC.Property
compressionStrictlyShortens midi =
   compressible midi
   QC.==>
   B.length (Save.toByteString midi)
   >
   B.length (Save.toCompressedByteString midi)

saveLoadMaybeByteList :: MidiFile.T -> Bool
saveLoadMaybeByteList midi =
   let bin    = Save.toByteList midi
       struct = Load.maybeFromByteList bin
       report = Report.Cons [] (Right midi)
   in  struct == report

saveLoadByteList :: MidiFile.T -> Bool
saveLoadByteList midi =
   midi == Load.fromByteList (Save.toByteList midi)


saveLoadFile :: MidiFile.T -> IO Bool
saveLoadFile midi =
   do Save.toSeekableFile testMidiName midi
      struct <- Load.fromFile testMidiName
      return $ struct == midi


loadSaveByteString :: MidiFile.T -> Bool
loadSaveByteString midi0 =
   let bin0 = Save.toByteString midi0
   in  case Load.maybeFromByteString bin0 of
          Report.Cons [] (Right midi1) ->
               bin0 == Save.toByteString midi1
          _ -> False

loadSaveCompressedByteString :: MidiFile.T -> Bool
loadSaveCompressedByteString midi0 =
   let bin0 = Save.toCompressedByteString midi0
   in  case Load.maybeFromByteString bin0 of
          Report.Cons [] (Right midi1) ->
               bin0 == Save.toCompressedByteString midi1
          _ -> False

loadSaveByteList :: MidiFile.T -> Bool
loadSaveByteList midi0 =
   let bin0 = Save.toByteList midi0
   in  case Load.maybeFromByteList bin0 of
          Report.Cons [] (Right midi1) ->
               bin0 == Save.toByteList midi1
          _ -> False


restrictionByteList :: MidiFile.T -> Bool
restrictionByteList midi =
   let bin = Save.toByteList midi
   in  Load.fromByteList bin ==
       Load.fromByteList (bin++[undefined])


lazinessByteList :: MidiFile.T -> Bool
lazinessByteList (MidiFile.Cons typ divsn tracks00) =
   let tracks0 = filter (not . EventList.null) tracks00
       bin0 = Save.toByteList (MidiFile.Cons typ divsn tracks0)
       {- remove trailing EndOfTrack and its time stamp and replace the last by 
       bin1 = take (length bin0 - 5) bin0 ++ [undefined]
       -}
       bin1 = init bin0 ++ [undefined]
       (MidiFile.Cons _ _ tracks1) = Load.fromByteList bin1
   in  case ListHT.viewR tracks0 of
          Just (initTracks0, lastTrack0) ->
             List.isPrefixOf initTracks0 tracks1 &&
               let (lastTrack1:_) = Match.drop initTracks0 tracks1
               in  List.isPrefixOf
                      (init (EventList.toPairList lastTrack0))
                      (EventList.toPairList lastTrack1)
{-
              fmap fst (EventList.viewR lastTrack0) ==
              fmap fst (EventList.viewR lastTrack1)
-}
          _ -> True


{- |
Check whether corruptions in a file are properly detected
and do not trap into errors.

Sometimes it may yield an error which is @binary@'s fault.
See 'readAfterEnd'.
-}
corruptionByteString :: Int64 -> Int -> MidiFile.T -> Bool
corruptionByteString pos replacement midi =
   let bin = Save.toByteString midi
       n = mod pos (B.length bin + 1)
       (pre, post) = B.splitAt n bin
       replaceByte = fromIntegral replacement
       corruptBin =
          B.append pre
             (if B.null post
                then B.singleton replaceByte
                else B.cons replaceByte (B.tail post))
   in  -- trace (show (B.unpack corruptBin)) $
       case Load.maybeFromByteString corruptBin of
          Report.Cons _ _ -> True

corruptionByteList :: Int -> Int -> MidiFile.T -> Bool
corruptionByteList pos replacement midi =
   let bin = Save.toByteList midi
       n = mod pos (length bin + 1)
       (pre, post) = splitAt n bin
       corruptBin =
          pre ++ fromIntegral replacement :
             if null post then [] else tail post
   in  case Load.maybeFromByteList corruptBin of
          Report.Cons _ _ -> True

{- |
This demonstrates a problem of the @binary@ package.
If the binary @Get@ parser reads past the end of the file,
then @runGet@ yields an error not a parser failure.
This example provokes the problem by setting the @MTrk@ length to 0,
which makes the following data look like an alien chunk with large size.
When skipping following data of this size, 'runGet' yields an 'error'.
-}
readAfterEnd :: IO ()
readAfterEnd = do
   let bin = Save.toByteString $ Example.readAfterEnd
       fails =
          filter
             (\pos -> not $ corruptionByteString pos 0 Example.readAfterEnd)
             [0 .. B.length bin]
   when (not $ null fails) $
      putStr $ " fails at positions " ++ show fails
   putStrLn ""


main :: IO ()
main = do
   runExample Example.empty
   runExample Example.meta
   runExample Example.status
   saveLoadFile Example.status >>= print
   check "saveLoadByteString" saveLoadByteString
   check "saveLoadCompressedByteString" saveLoadCompressedByteString
   check "saveLoadCompressedByteString sorted"
      (saveLoadCompressedByteString . getSortedFile)
   check "saveLoadMaybeByteList" saveLoadMaybeByteList
   check "saveLoadByteList" saveLoadByteList
--   check "saveLoadFile" saveLoadFile
   check "loadSaveByteString" loadSaveByteString
   check "loadSaveCompressedByteString" loadSaveCompressedByteString
   check "loadSaveCompressedByteString sorted"
      (loadSaveCompressedByteString . getSortedFile)
   check "loadSaveByteList" loadSaveByteList

   check "compressionShortens" compressionShortens
   check "compressionStrictlyShortens"
      (compressionStrictlyShortens . getSortedFile)

   check "restrictionByteList" restrictionByteList

   check "lazinessZeroOrMoreByteList" Parser.lazinessZeroOrMoreByteList
   check "lazinessByteList" lazinessByteList

   check "corruptionByteList" corruptionByteList
   check "corruptionByteString" corruptionByteString
   when False $ putStr "readAfterEnd" >> readAfterEnd

{-
laziness test:
The following expressions should return the prefix of the track before running into "undefined".
I don't know, how to formalize that.

Load.fromByteList [77,84,104,100,0,0,0,6,0,1,0,1,0,10,77,84,114,107,0,0,0,28,0,147,20,64,4,147,24,64,4,147,27,64,7,131,20,64,4,131,24,64,4,131,27,64,0,255,47,undefined]

Report.result $ StreamParser.runIncomplete Load.getTrackChunk $ StreamParser.ByteList [77,84,114,107,0,0,0,28,0,147,20,64,4,147,24,64,4,147,27,64,7,131,20,64,4,131,24,64,4,131,27,64,0,255,47,undefined]
-}