packages feed

riff (empty) → 0.1.0.0

raw patch · 13 files changed

+721/−0 lines, 13 filesdep +basedep +binarydep +bytestringsetup-changed

Dependencies added: base, binary, bytestring, either, filepath, riff, transformers

Files

+ Data/Riff.hs view
@@ -0,0 +1,30 @@+{-|+Description : Convenience module that brings together both the parsing and assebling of Riff files.+Copyright   : (c) Robert Massaioli, 2014+License     : MIT+Maintainer  : robertmassaioli@gmail.com+Stability   : experimental++This module was made as a convinience, it's purpose is to aid the manipulation of RIFF files such +that you can both parse them and asseble them. If you wish to just parse or assemble Riff files +then you may be better off just importing Data.Riff.Parse or Data.Riff.Assemble respectively.+-}+module Data.Riff ( +   -- * RIFF File Data Representaion+   RiffFile(..),+   RiffChunkSize, +   RiffFileType(..), +   RiffChunk(..), +   RiffId, +   RiffData, +   ParseError,+   -- * Reading (parsing) RIFF Files+   withRiffFile,+   parseRiffData,+   assembleRiffFile,+   assembleRiffFileStream+   ) where++import Data.Riff.RiffData+import Data.Riff.Parse+import Data.Riff.Assemble
+ Data/Riff/Assemble.hs view
@@ -0,0 +1,105 @@+{-|+Description : The module that allows the assembly of a RIFF / RIFX file.+Copyright   : (c) Robert Massaioli, 2014+License     : MIT+Maintainer  : robertmassaioli@gmail.com+Stability   : experimental++This module allows the assembly of RIFF files in pure Haskell. You can create a RIFF file in +pure Haskell starting with a RiffFile and building it up until you are ready to write it out +to a ByteString or Disk. For example, this is how you might construct a RIFF file to be written+out:++> import Data.Riff+>+> riffFile = RiffFile RIFF "EXPL" children+> +> children = +>    [ RiffChunkChild "fst " [1..11]+>    , RiffChunkChild "snd " [11..100]+>    ]+>+> main = assembleRiffFile "example.riff" riffFile++As you can see it is a very simple API that lets you write out data into Riff Files. Have a play +around with with the examples until you can see how it works and fits together.+-}+module Data.Riff.Assemble +   ( assembleRiffFile+   , assembleRiffFileStream+   ) where++import Data.Riff.RiffData+import Data.Riff.Operations+import qualified Data.ByteString.Lazy as BL+import System.IO (withBinaryFile, IOMode(..))+import Control.Monad (when)++import Data.Binary.Put+import Data.Char (ord)++-- | Given a file path and a RiffFile representation this will allow you to safely write+-- out a RiffFile to disk. This allows you to save anything that you like in a RiffFile.+-- Just remember that the maximum file size of a RiffFile is bounded by the maximum size+-- of a 32bit integer. The behaviour of this function, should you give it too much data,+-- is undefined.+assembleRiffFile +   :: FilePath -- ^ The location on the filesystem to save the RiffFile.+   -> RiffFile -- ^ The in-memory representation of a RiffFile to be saved.+   -> IO ()    -- ^ Writing to disk is an IO operatin and we return no results.+assembleRiffFile filePath riffFile = withBinaryFile filePath WriteMode $ \h -> +   BL.hPut h (assembleRiffFileStream riffFile)++-- | Assembles a RiffFile into it's representation in a Lazy ByteString. +assembleRiffFileStream +   :: RiffFile       -- ^ The RIFF file to be written out. +   -> BL.ByteString  -- ^ The resultant stream of bytes representing the file.+assembleRiffFileStream = runPut . writeRiffFile++writeRiffFile :: RiffFile -> Put+writeRiffFile riffFile = do+   printHeader . riffFileType $ riffFile -- Do not need safeId, chosen to be correrct+   putSize context . calculateFileLength $ riffFile+   putString . safeId . riffFileFormatType $ riffFile+   sequence_ $ fmap (writeRiffChunk context) (riffFileChildren riffFile)+   where+      context = getContext . riffFileType $ riffFile+   -- TODO do we need to word align the end of a riff file?++getContext :: RiffFileType -> AssemblyContext+getContext RIFF = AssemblyContext putWord32le+getContext RIFX = AssemblyContext putWord32be++data AssemblyContext = AssemblyContext+   { putSize :: RiffChunkSize -> Put+   }++writeRiffChunk :: AssemblyContext -> RiffChunk -> Put+writeRiffChunk context chunk@(RiffChunkChild _ _) = do+   putString . safeId . riffChunkId $ chunk+   let chunkSize = calculateChunkLength chunk+   putSize context chunkSize+   sequence_ $ fmap putWord8 (riffData chunk)+   maybeFillBlank chunkSize+writeRiffChunk context chunk@(RiffChunkParent _ _) = do+   putString "LIST" -- Do not need to pass through safeId, chosen to be correct+   let chunkSize = calculateChunkLength chunk+   putSize context chunkSize+   putString . safeId . riffFormTypeInfo $ chunk+   sequence_ $ fmap (writeRiffChunk context) (riffChunkChildren chunk)+   maybeFillBlank chunkSize++maybeFillBlank :: RiffChunkSize -> Put+maybeFillBlank chunkSize = when (chunkSize `mod` 2 == 1) putBlankByte++putBlankByte = putWord8 0++printHeader :: RiffFileType -> Put+printHeader RIFF = putString "RIFF"+printHeader RIFX = putString "RIFX"++putString :: String -> Put+putString = sequence_ . fmap (putWord8 . fromIntegral . ord)++safeId :: RiffId -> RiffId+safeId input = take 4 $ input ++ repeat ' '
+ Data/Riff/InternalUtil.hs view
@@ -0,0 +1,8 @@+module Data.Riff.InternalUtil where++import Data.Riff.RiffData++padToWord :: RiffChunkSize -> RiffChunkSize+padToWord x = if x `mod` 2 == 0+   then x+   else x + 1
+ Data/Riff/Operations.hs view
@@ -0,0 +1,47 @@+{-|+Description : The module provides common operations to perform on RIFF files.+Copyright   : (c) Robert Massaioli, 2014+License     : MIT+Maintainer  : robertmassaioli@gmail.com+Stability   : experimental++This module provides some common operations that you might like to perform on RIFF files. This is +by no means an exhaustive list but you may find it useful. This is meant to be a collection of +common methods to make your development time shorter.+-}+module Data.Riff.Operations +   ( calculateFileLength+   , calculateChunkLength+   , trueFileSize+   ) where++import Data.Riff.RiffData+import Data.Riff.InternalUtil++-- | This function calculates the size of the RIFF file if it was written out to disk.+trueFileSize +   :: RiffFile -- ^ The RIFF file that needs its size calculated.+   -> Integer  -- ^ The size of the RIFF file if it was written to disk.+trueFileSize riffFile = 8 + fromIntegral (calculateFileLength riffFile)++-- | Calculates the size that should be in the ChunkSize location of the RiffFile when it+-- has been written out to disk. If you want to get the true file size of a Riff file once+-- it has been written out to disk then you need to use the trueFileSize function.+calculateFileLength +   :: RiffFile       -- The RIFF file whose data size we want to calculate.+   -> RiffChunkSize  -- The RiffChunkSize that should be placed in the initial ChunkSize slot.+calculateFileLength (RiffFile _ _ children) = +   idLength + childHeaderLength children + childrenLength children++-- | Calculates the size of this chunk such that you could place that size in the+-- ChunkSize section of that chunk in a RIFF file that you wrote to disk.+calculateChunkLength +   :: RiffChunk      -- ^ The RIFF chunk whose size should be calculated.+   -> RiffChunkSize  -- ^ The size of the data portion of that chunk no disk.+calculateChunkLength (RiffChunkChild _ chunkData) = fromIntegral $ length chunkData+calculateChunkLength (RiffChunkParent _ children) = +   idLength + childHeaderLength children + childrenLength children++idLength = 4+childHeaderLength children = fromIntegral (8 * length children)+childrenLength = sum . fmap (padToWord . calculateChunkLength)
+ Data/Riff/Parse.hs view
@@ -0,0 +1,141 @@+{-|+Description : The module that allows parsing of a RIFF / RIFX file.+Copyright   : (c) Robert Massaioli, 2014+License     : MIT+Maintainer  : robertmassaioli@gmail.com+Stability   : experimental++This module allows the parsing of RIFF files in pure Haskell. You can parse a RIFF file using the +methods provided in this module. For example, if you wanted to parse a RIFF file and print it out+then you could:++> main = withRiffFile "path/to/file.riff" print++And that will print the RIFF file, in gory details, to the screen.+-}+module Data.Riff.Parse+   ( withRiffFile+   , parseRiffData+   ) where++import Data.Riff.RiffData+import Data.Riff.InternalUtil++import Control.Monad (when, replicateM)+import Control.Monad.Trans.Either (EitherT(..), left, right)+import Control.Monad.Trans.Class+import Data.Binary.Get+import qualified Data.ByteString.Lazy as BL+import Data.Char (chr)+import Data.Word (Word8)+import GHC.IO.IOMode (IOMode(..))+import System.IO (withBinaryFile)++-- | Given a FilePath you can provide a function that will be given either a ParseError or+-- an actual RiffFile to process. It is important to note that you should do all of the+-- processing of the RiffFile in the provided action because the file will be closed at+-- the end of this function.+withRiffFile :: FilePath                              -- ^ The file that will be read.+             -> (Either ParseError RiffFile -> IO ()) -- ^ An action to perform on the potentialy +                                                      -- parsed file+             -> IO ()                                 -- ^ The resultant IO action.+withRiffFile filePath action = withBinaryFile filePath ReadMode $ \h -> do+   riffData <- fmap parseRiffData (BL.hGetContents h)+   action riffData++-- | You can parse a raw ByteString and try and convert it into a RiffFile. This will give+-- a best attempt at parsing the data and, if success is not possible, will give you a+-- ParseError.+parseRiffData :: BL.ByteString               -- A lazy bytestring for input.+              -> Either ParseError RiffFile  -- The result of our attempted parse.+parseRiffData input =+   case runGetOrFail (runEitherT getRiffStart) input of+      Left (_, offset, error) -> Left (offset, error)+      Right (_, _, result) -> result++data ParseContext = ParseContext+   { getSize :: Get RiffChunkSize+   }++getRiffStart :: EitherT ParseError Get RiffFile+getRiffStart = do+   id <- lift getIdentifier+   (context, fileType) <- case id of+      "RIFF" -> right (leContext, RIFF)+      "RIFX" -> right (beContext, RIFX)+      _ -> do+         read <- lift bytesRead +         left (read, "RIFF file not allowed to start with chunk id: '" ++ id ++ "'. Must start with either RIFF or RIFX")+   size <- lift . getSize $ context+   riffType <- lift getIdentifier+   contents <- parseChunkList context (size - 4)+   return RiffFile+      { riffFileType = fileType+      , riffFileFormatType = riffType+      , riffFileChildren = contents+      }+   where+      leContext = ParseContext getWord32le+      beContext = ParseContext getWord32be++parseChunkList :: ParseContext -> RiffChunkSize -> EitherT ParseError Get [RiffChunk]+parseChunkList _        0         = return []+parseChunkList context  totalSize = do+   (nextChunk, dataSize) <- getRiffChunk context+   -- No matter what type of chunk it is tehre will be 8 bytes taken up by the id and size+   let chunkSize = 8 + padToWord dataSize+   if totalSize <= chunkSize+      then return [nextChunk]+      else do+         following <- parseChunkList context (totalSize - chunkSize)+         return $ nextChunk : following++getRiffChunk :: ParseContext -> EitherT ParseError Get (RiffChunk, RiffChunkSize)+getRiffChunk context = do+   id <- lift getIdentifier+   size <- lift . getSize $ context+   if id == "LIST"+      then do+         guardListSize id size+         formType <- lift getIdentifier+         -- Minus 4 because of the formType before that is part of the size+         children <- parseChunkList context (size - 4)+         lift $ skipToWordBoundary size+         return (RiffChunkParent+            { riffFormTypeInfo = formType+            , riffChunkChildren = children+            }, size)+      else do+         -- TODO do we need to consider byte boundaries here?+         riffData <- lift $ getNWords (fromIntegral size)+         lift $ skipToWordBoundary size+         return (RiffChunkChild+            { riffChunkId = id+            , riffData = riffData+            }, size)+   where+      guardListSize id size = when (size < 4) $ do+         read <- lift bytesRead+         left (read, message id size)+         where+            message id size = +               "List Chunk Id '" ++ id +               ++ "' had chunk size " ++ show size +               ++ " which is less than 4 and invalid."++skipToWordBoundary :: RiffChunkSize -> Get ()+skipToWordBoundary size = do+   empty <- isEmpty+   when (not empty && size `mod` 2 == 1) $ skip 1++getNWords :: Int -> Get [Word8]+getNWords n = replicateM n getWord8++getNChars :: Int -> Get String+getNChars = fmap (fmap byteToChar) . getNWords ++-- Parsing bytes+byteToChar :: Word8 -> Char+byteToChar = chr . fromIntegral++getIdentifier = getNChars 4
+ Data/Riff/RiffData.hs view
@@ -0,0 +1,61 @@+-- | This module is responsible for all of the datatypes that are required around the codebase.+module Data.Riff.RiffData where++import Data.Word (Word8, Word32)+import Data.Binary.Get (ByteOffset)++-- | The data in a riff file is just a collection of bytes.+type RiffData = Word8++-- | A Riff file is made up exclusively of Riff Chunks and each chunk, as the second piece+-- of data in the chunk, contains it's size. The size never includes the first 8 bytes of+-- the chunk, which are the Chunk Id and the Chunk Size but, in the case of a nested+-- chunk, it does include the four bytes of the Chunk FormType Id.+--+-- According to the specification a chunk size must be represented by four bytes of data.+-- This means that the maximum number of bytes that can be present in a RIFF file is:+--+-- > 2 ^ 32 + 8 = 4294967304 bytes or ~4GB +-- +-- If your raw data is larger than that then this file format cannot support it.+type RiffChunkSize = Word32++-- | A RiffId is just a four character string (FourCC). It is usually (but by no means+-- always) chosen to be something that is human readable when converted to ASCII. +type RiffId = String++-- | Represents an error in the parsing of a Riff File. It contains the location in the+-- file that we read up to and a message of what went wrong.+type ParseError = (ByteOffset, String)++-- | This is our representation of a RIFF file. These files all have a+-- format type and are composed by one or more nestable data Chunks which we represent+-- with a RiffChunk.+data RiffFile = RiffFile +   { riffFileType :: RiffFileType      -- ^ The type of RIFF file.+   , riffFileFormatType :: RiffId      -- ^ An ID representing the type of data contained within.+   , riffFileChildren :: [RiffChunk]   -- ^ The chunks that make up the file.+   }+   deriving (Eq, Show)++-- | There are only two different types of RIFF file: RIFF and RIFX and the difference is+-- in the way that data is encoded inside them.+data RiffFileType +   = RIFF -- ^ This is the most common type of RIFF file and is in little endian format.+   | RIFX -- ^ This is a rare riff format that uses big endian encoding (otherwise known as+          -- Motorolla byte order)+   deriving(Eq, Show)++-- | A RiffFile is just an alias for a RiffChunk. A RiffFile is merely a nested collection+-- of RiffChunks where the first element must be a list of chunks with the ID RIFF or+-- RIFX. +data RiffChunk +   = RiffChunkChild+      { riffChunkId :: RiffId+      , riffData :: [RiffData]+      }+   | RiffChunkParent+      { riffFormTypeInfo :: RiffId+      , riffChunkChildren :: [RiffChunk]+      }+   deriving (Eq, Show)
+ LICENSE view
@@ -0,0 +1,21 @@+The MIT License (MIT)++Copyright (c) 2014 Robert Massaioli <robertmassaioli@gmail.com>++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+THE SOFTWARE.
+ README.markdown view
@@ -0,0 +1,49 @@+# RIFF for Haskell++[RIFF][2] has been around for a long time and I thought that now would be a+good time to write a library for it in Haskell. This library supports reading most RIFF+files. Example file formats (taken from Wikipedia and elsewhere) that use RIFF as the +container layer include:++ - WAV (Windows audio)+ - AVI (Windows audiovisual)+ - RMI (Windows "RIFF MIDIfile")+ - CDR (CorelDRAW vector graphics file)+ - ANI (Animated Windows cursors)+ - DLS (Downloadable Sounds)+ - WebP (An image format developed by Google)+ - XMA (Microsoft Xbox 360 console audio format based on WMA Pro)++And there are many more. You can even come up with your own data that can be contained+inside RIFF.++## Building the Code++To build the code:++    $ cabal sandbox init+    $ cabal install++And that is all that there is to it.++## Bundled Executables++I have written a few simple programs just so that I could test out the code that I have written:++    $ cabal run riff-structure # prints out the internal structure of a RIFF file+    $ cabal run riff-convert # lets you convert files to and from RIFF and RIFX formats+    $ cabal run riff-identity # the identity over RIFF files, useful for testing++Feel free to use any of these executables to have a play with the RIFF file format, or better yet,+write some code using this library and let me know about it.++## Example Data++You can find example data all over the internet. Any WAVE file is a RIFF file for example.+Here is a list of example data that you may be able to download:++ - [John Loomis Examples][1]+++ [1]: http://www.johnloomis.org/cpe102/asgn/asgn1/riff.zip+ [2]: http://en.wikipedia.org/wiki/Resource_Interchange_File_Format
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ riff.cabal view
@@ -0,0 +1,110 @@+-- Initial riff.cabal generated by cabal init.  For further documentation, +-- see http://haskell.org/cabal/users-guide/++-- The name of the package.+name:                riff++-- The package version.  See the Haskell package versioning policy (PVP) +-- for standards guiding when and how versions should be incremented.+-- http://www.haskell.org/haskellwiki/Package_versioning_policy+-- PVP summary:      +-+------- breaking API changes+--                   | | +----- non-breaking API additions+--                   | | | +--- code changes with no API change+version:             0.1.0.0++-- A short (one-line) description of the package.+synopsis:            RIFF parser for Haskell++-- A longer description of the package.+description:         This library provides a RIFF parser for Haskell for easy manipulation+                     of common file formats like WAVE and RIFF container AVI files.++stability:           experimental++homepage:            https://bitbucket.org/robertmassaioli/riff/overview++bug-reports:         http://bitbucket.org/robertmassaioli/riff/issues++-- The license under which the package is released.+license:             MIT++-- The file containing the license text.+license-file:        LICENSE++-- The package author(s).+author:              Robert Massaioli++-- An email address to which users can send suggestions, bug reports, and +-- patches.+maintainer:          robertmassaioli@gmail.com++-- A copyright notice.+copyright:           (c) 2014 Robert Massaioli         ++category:            Data++build-type:          Simple++-- Extra files to be distributed with the package, such as examples or a +-- README.+extra-source-files:  LICENSE, README.markdown++-- Constraint on the version of Cabal needed to build this package.+cabal-version:       >=1.10++-- The source repository that this comes from.+source-repository head+  type: git+  location: git://bitbucket.org/robertmassaioli/riff.git++library+  -- Modules exported by the library.+  exposed-modules:     Data.Riff+                       , Data.Riff.Operations+  +  -- Modules included in this library but not exported.+  other-modules:       Data.Riff.InternalUtil+                       , Data.Riff.RiffData+                       , Data.Riff.Parse+                       , Data.Riff.Assemble+  +  -- LANGUAGE extensions used by modules in this package.+  -- other-extensions:    +  +  -- Other library packages from which modules are imported.+  build-depends:       base >=4.5 && <5.0+                       , binary >=0.7 && <0.8+                       , bytestring >=0.9 && <1.0+                       , either >=4.1 && <4.2+                       , transformers >=0.3.0 && <0.3.1+  +  -- Directories containing source files.+  -- hs-source-dirs:      +  +  -- Base language which the package is written in.+  default-language:    Haskell2010++  ghc-options: -W+  ++executable riff-structure+  hs-source-dirs: src+  main-is:        riff-structure.hs+  build-depends:       base >=4.5 && <5.0+                       , riff+  default-language:    Haskell2010++executable riff-identity+  hs-source-dirs: src+  main-is:        riff-identity.hs+  build-depends:       base >=4.5 && <5.0+                       , riff+  default-language:    Haskell2010++executable riff-convert+  hs-source-dirs: src+  main-is:        riff-convert.hs+  build-depends:       base >=4.5 && <5.0+                       , filepath >=1.3 && <1.4+                       , riff+  default-language:    Haskell2010
+ src/riff-convert.hs view
@@ -0,0 +1,46 @@+module Main where++import System.Console.GetOpt+import System.Environment (getArgs)+import System.FilePath (splitExtension)+import Data.Riff++data Flag+   = Help+   | ToRifx+   deriving(Eq, Show)++options :: [OptDescr Flag]+options = +   [ Option "h" ["help"] (NoArg Help) "prints this help message"+   , Option "x" ["rifx"] (NoArg ToRifx) "converts to the RIFX format. Converts to RIFF otherwise."+   ]++main = do+   args <- getArgs+   let (flags, extras, _) = getOpt Permute options args+   handleFlags flags extras+   +handleFlags :: [Flag] -> [String] -> IO ()+handleFlags flags files = do+   if Help `elem` flags+      then putStrLn $ usageInfo "riff-convert" options+      else sequence_ $ fmap (convert toRifx) files+   where+      toRifx = ToRifx `elem` flags++convert :: Bool -> FilePath -> IO ()+convert toRifx filePath = do+   putStr $ "> Reading " ++ filePath ++ "..."+   withRiffFile filePath $ \result -> case result of+      Left (pos, error) -> putStrLn $ error ++ " (Offset: " ++ show pos ++ ")"+      Right riffFile -> do+         putStr "converting..."+         assembleRiffFile newFilename $ convertRep riffFile+         putStrLn "[DONE]"+   where+      newFilename = filename ++ ".converted" ++ ext+      (filename, ext) = splitExtension filePath++      convertRep riffFile = riffFile { riffFileType = riffFT } +      riffFT = if toRifx then RIFX else RIFF
+ src/riff-identity.hs view
@@ -0,0 +1,21 @@+module Main where++import Data.Riff++import System.Environment (getArgs)++main = do+   args <- getArgs+   case args of+      [] -> putStrLn "You need to give this a riff file!"+      xs -> do+         putStrLn "Parsing and reassembling RIFF files:"+         sequence_ $ fmap reassembleFile xs+         putStrLn "Finished reassembling RIFF files."++reassembleFile :: FilePath -> IO ()+reassembleFile filePath = do+   putStr $ "> " ++ filePath ++ " ..."+   withRiffFile filePath $ \e -> case e of+      Left (_, error) -> putStrLn "[FAILED]" >> putStrLn error+      Right riffFile -> assembleRiffFile (filePath ++ ".clone") riffFile >> putStrLn "[Done]"
+ src/riff-structure.hs view
@@ -0,0 +1,80 @@+module Main where++import Data.Riff+import Data.Riff.Operations++import System.Environment (getArgs)+import Data.List (intersperse)+import Control.Monad (when)+import Data.Char (chr)++main = do+   args <- getArgs+   case args of+      [] -> error "No filename provided to scan. Please give me a file."+      xs -> sequence_ . intersperse putNewline $ fmap processFile xs++processFile :: String -> IO ()+processFile filePath = do+   putStr "File: "+   print filePath+   withRiffFile filePath $ \potential -> case potential of+      Left (offset, error) -> putStrLn $ error ++ " (Offset: " ++ show offset ++ ")"+      Right riffFile -> printRiffFile startContext riffFile++data PrintContext = PrintContext+   { indentation :: Int+   , printValue :: Bool+   }+   deriving (Show)++startContext :: PrintContext+startContext = PrintContext 0 False++printRiffFile :: PrintContext -> RiffFile -> IO ()+printRiffFile context file@(RiffFile riffType formatType children) = do+   printWithIndent context (typeName riffType)+   putStrLn $ " (" ++ formatType ++ ") [" ++ showFileLength file ++ "]"+   mapM_ (printRiffChunk nextContext) children+   where+      nextContext = PrintContext+         { indentation = 1 + indentation context+         , printValue = False+         }++      typeName RIFF = "RIFF"+      typeName RIFX = "RIFX"++printRiffChunk :: PrintContext -> RiffChunk -> IO ()+printRiffChunk context chunk@(RiffChunkChild id value) = do+   printWithIndent context id+   putStr $ " [" ++ showLength chunk ++ "]"+   when (printValue context) $ do+      putStr ": "+      inQuotes (putStr . fmap (chr . fromIntegral) $ takeWhile (/= 0) value)+   putNewline+printRiffChunk context chunk@(RiffChunkParent typeName children) = do+   printWithIndent context "LIST"+   putStrLn $ " (" ++ typeName ++ ") [" ++ showLength chunk ++ "]"+   mapM_ (printRiffChunk nextContext) children+   where+      nextContext = PrintContext+         { indentation = 1 + indentation context+         , printValue = typeName == "INFO"+         }++showFileLength = show . calculateFileLength+showLength = show . calculateChunkLength++inQuotes :: IO () -> IO ()+inQuotes action = do+   putChar '"'+   action+   putChar '"'++printWithIndent :: PrintContext -> String -> IO ()+printWithIndent context value = do+   putStr . concat . take (indentation context) $ repeat "   "+   putStr value++putNewline = putStrLn ""