HCodecs 0.5.1 → 0.5.2
raw patch · 11 files changed
+3216/−3209 lines, 11 filesdep +faildep ~basesetup-changedPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: fail
Dependency ranges changed: base
API changes (from Hackage documentation)
- Codec.ByteString.Builder: instance Data.Semigroup.Semigroup Codec.ByteString.Builder.Builder
+ Codec.ByteString.Builder: instance GHC.Base.Semigroup Codec.ByteString.Builder.Builder
+ Codec.ByteString.Parser: instance Control.Monad.Fail.MonadFail Codec.ByteString.Parser.Parser
- Codec.Midi: fromAbsTime :: (Num a) => Track a -> Track a
+ Codec.Midi: fromAbsTime :: Num a => Track a -> Track a
- Codec.Midi: toAbsTime :: (Num a) => Track a -> Track a
+ Codec.Midi: toAbsTime :: Num a => Track a -> Track a
- Codec.SoundFont: buildInfos :: (Array Word Info) -> Builder
+ Codec.SoundFont: buildInfos :: Array Word Info -> Builder
- Data.Audio: buildSampleData :: (IArray UArray a) => (a -> Builder) -> SampleData a -> Builder
+ Data.Audio: buildSampleData :: IArray UArray a => (a -> Builder) -> SampleData a -> Builder
- Data.Audio: sampleNumber :: (IArray UArray a) => SampleData a -> Int
+ Data.Audio: sampleNumber :: IArray UArray a => SampleData a -> Int
- Data.Audio: sampleType :: (IArray UArray a) => SampleData a -> a
+ Data.Audio: sampleType :: IArray UArray a => SampleData a -> a
Files
- HCodecs.cabal +64/−62
- LICENSE +29/−29
- Setup.hs +2/−2
- src/Codec/ByteString/Builder.hs +395/−395
- src/Codec/ByteString/Parser.hs +600/−595
- src/Codec/Internal/Arbitrary.hs +52/−52
- src/Codec/Internal/TestSuite.hs +117/−117
- src/Codec/Midi.hs +717/−717
- src/Codec/SoundFont.hs +920/−920
- src/Codec/Wav.hs +175/−175
- src/Data/Audio.hs +145/−145
HCodecs.cabal view
@@ -1,62 +1,64 @@-name: HCodecs-version: 0.5.1-cabal-Version: >= 1.8-license: BSD3-license-file: LICENSE-copyright: (c) 2007-2014 George Giorgidze-author: George Giorgidze-maintainer: George Giorgidze (giorgidze@gmail.com)- , Pavel Krajcevski (krajcevski@gmail.com)-homepage: http://www-db.informatik.uni-tuebingen.de/team/giorgidze-category: Codec-stability: experimental-synopsis: A library to read, write and manipulate MIDI, WAVE, and SoundFont2 files.-description:- The library provides functions to read, write and manipulate MIDI, WAVE and- SoundFont2 multimedia files. It is written entirely in Haskell (without any- FFI). It uses efficient parsing and building combinators for binary data- stored in ByteStrings (based on the one in 'binary' package).- .- Correctness of significant parts of the library has been validated with- QuickCheck and Haskell Program Coverage (HPC) tool-kits.--build-type: Simple--source-repository head- type: git- location: https://github.com/Mokosha/HCodecs.git--library- hs-source-dirs: src- ghc-options: -O3 -Wall- build-depends:- base < 5- , bytestring- , random- , semigroups- , array >= 0.4- , QuickCheck >= 2.0-- exposed-modules:- Codec.Midi- Codec.Wav- Codec.SoundFont- Codec.ByteString.Parser- Codec.ByteString.Builder- Data.Audio- other-modules:- Codec.Internal.Arbitrary--test-suite tests- type: exitcode-stdio-1.0- hs-source-dirs: src- ghc-options: -O3 -Wall- build-depends:- base < 5- , bytestring- , random- , semigroups- , array >= 0.4- , QuickCheck >= 2.0-- main-is: Codec/Internal/TestSuite.hs+name: HCodecs +version: 0.5.2 +cabal-Version: >= 1.8 +license: BSD3 +license-file: LICENSE +copyright: (c) 2007-2014 George Giorgidze +author: George Giorgidze +maintainer: George Giorgidze (giorgidze@gmail.com) + , Pavel Krajcevski (krajcevski@gmail.com) +homepage: http://www-db.informatik.uni-tuebingen.de/team/giorgidze +category: Codec +stability: experimental +synopsis: A library to read, write and manipulate MIDI, WAVE, and SoundFont2 files. +description: + The library provides functions to read, write and manipulate MIDI, WAVE and + SoundFont2 multimedia files. It is written entirely in Haskell (without any + FFI). It uses efficient parsing and building combinators for binary data + stored in ByteStrings (based on the one in 'binary' package). + . + Correctness of significant parts of the library has been validated with + QuickCheck and Haskell Program Coverage (HPC) tool-kits. + +build-type: Simple + +source-repository head + type: git + location: https://github.com/Mokosha/HCodecs.git + +library + hs-source-dirs: src + ghc-options: -O3 -Wall + build-depends: + base < 5 + , bytestring + , random + , semigroups + , array >= 0.4 + , QuickCheck >= 2.0 + , fail + + exposed-modules: + Codec.Midi + Codec.Wav + Codec.SoundFont + Codec.ByteString.Parser + Codec.ByteString.Builder + Data.Audio + other-modules: + Codec.Internal.Arbitrary + +test-suite tests + type: exitcode-stdio-1.0 + hs-source-dirs: src + ghc-options: -O3 -Wall + build-depends: + base < 5 + , bytestring + , random + , semigroups + , array >= 0.4 + , QuickCheck >= 2.0 + , fail + + main-is: Codec/Internal/TestSuite.hs
LICENSE view
@@ -1,29 +1,29 @@-Copyright (c) 2007-2014 George Giorgidze--All rights reserved.--Redistribution and use in source and binary forms, with or without-modification, are permitted provided that the following conditions are met:--- Redistributions of source code must retain the above copyright notice,- this list of conditions and the following disclaimer.--- Redistributions in binary form must reproduce the above copyright notice,- this list of conditions and the following disclaimer in the documentation- and/or other materials provided with the distribution.--- Neither the name of the copyright holder nor the names of his contributors- may be used to endorse or promote products derived from this software- without specific prior written permission.--THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR-CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,-EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,-PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR-PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF-LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING-NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.+Copyright (c) 2007-2014 George Giorgidze + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +- Neither the name of the copyright holder nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Setup.hs view
@@ -1,2 +1,2 @@-import Distribution.Simple-main = defaultMain+import Distribution.Simple +main = defaultMain
src/Codec/ByteString/Builder.hs view
@@ -1,395 +1,395 @@--------------------------------------------------------------------------------- |--- Module : Data.ByteString.Builder--- Copyright : Lennart Kolmodin, Ross Paterson, George Giorgidze--- License : BSD3------ Maintainer : George Giorgidze <http://cs.nott.ac.uk/~ggg/>--- Stability : experimental--- Portability : Portable------ Efficient construction of lazy bytestrings.----------------------------------------------------------------------------------module Codec.ByteString.Builder (-- -- * The Builder type- Builder- , toLazyByteString-- -- * Constructing Builders- , empty- , singleton- , putWord8- , putInt8- , append- , fromByteString -- :: S.ByteString -> Builder- , fromLazyByteString -- :: L.ByteString -> Builder- , putString-- -- * Flushing the buffer state- , flush-- -- * Derived Builders- -- ** Big-endian writes- , putWord16be -- :: Word16 -> Builder- , putWord24be -- :: Word32 -> Builder- , putWord32be -- :: Word32 -> Builder- , putWord64be -- :: Word64 -> Builder-- , putInt16be -- :: Int16 -> Builder- , putInt32be -- :: Int32 -> Builder- , putInt64be -- :: Int64 -> Builder-- -- ** Little-endian writes- , putWord16le -- :: Word16 -> Builder- , putWord24le -- :: Word32 -> Builder- , putWord32le -- :: Word32 -> Builder- , putWord64le -- :: Word64 -> Builder-- , putInt16le -- :: Int16 -> Builder- , putInt32le -- :: Int32 -> Builder- , putInt64le -- :: Int64 -> Builder-- -- ** Host-endian, unaligned writes- , putWordHost -- :: Word -> Builder- , putWord16host -- :: Word16 -> Builder- , putWord32host -- :: Word32 -> Builder- , putWord64host -- :: Word64 -> Builder- -- Variable length numbers- , putVarLenBe- , putVarLenLe-- ) where---import qualified Data.ByteString.Internal as S-import qualified Data.ByteString as S-import qualified Data.ByteString.Lazy as L--import Foreign.Storable (Storable, poke, sizeOf)-import Foreign.Ptr (Ptr, plusPtr)-import Foreign.ForeignPtr (ForeignPtr, withForeignPtr)-import System.IO.Unsafe (unsafePerformIO)-import Data.ByteString.Internal (c2w)--import Data.Bits-import Data.Word-import Data.Int-import Data.Semigroup------------------------------------------------------------------------------ | A 'Builder' is an efficient way to build lazy 'L.ByteString's.--- There are several functions for constructing 'Builder's, but only one--- to inspect them: to extract any data, you have to turn them into lazy--- 'L.ByteString's using 'toLazyByteString'.------ Internally, a 'Builder' constructs a lazy 'L.Bytestring' by filling byte--- arrays piece by piece. As each buffer is filled, it is \'popped\'--- off, to become a new chunk of the resulting lazy 'L.ByteString'.--- All this is hidden from the user of the 'Builder'.--newtype Builder = Builder {- -- Invariant (from Data.ByteString.Lazy):- -- The lists include no null ByteStrings.- runBuilder :: (Buffer -> [S.ByteString]) -> Buffer -> [S.ByteString]- }--instance Semigroup Builder where- (<>) = append--instance Monoid Builder where- mempty = empty- mappend = (<>)------------------------------------------------------------------------------ | /O(1)./ The empty Builder, satisfying------ * @'toLazyByteString' 'empty' = 'L.empty'@----empty :: Builder-empty = Builder id---- | /O(1)./ A Builder taking a single byte, satisfying------ * @'toLazyByteString' ('singleton' b) = 'L.singleton' b@----singleton :: Word8 -> Builder-singleton = writeN 1 . flip poke--putWord8 :: Word8 -> Builder-putWord8 = singleton----------------------------------------------------------------------------- | /O(1)./ The concatenation of two Builders, an associative operation--- with identity 'empty', satisfying------ * @'toLazyByteString' ('append' x y) = 'L.append' ('toLazyByteString' x) ('toLazyByteString' y)@----append :: Builder -> Builder -> Builder-append (Builder f) (Builder g) = Builder (f . g)---- | /O(1)./ A Builder taking a 'S.ByteString', satisfying------ * @'toLazyByteString' ('fromByteString' bs) = 'L.fromChunks' [bs]@----fromByteString :: S.ByteString -> Builder-fromByteString bs- | S.null bs = empty- | otherwise = flush `append` mapBuilder (bs :)---- | /O(1)./ A Builder taking a lazy 'L.ByteString', satisfying------ * @'toLazyByteString' ('fromLazyByteString' bs) = bs@----fromLazyByteString :: L.ByteString -> Builder-fromLazyByteString bss = flush `append` mapBuilder (L.toChunks bss ++)--putString :: String -> Builder-putString = fromLazyByteString . L.pack . map c2w------------------------------------------------------------------------------ Our internal buffer type-data Buffer = Buffer {-# UNPACK #-} !(ForeignPtr Word8)- {-# UNPACK #-} !Int -- offset- {-# UNPACK #-} !Int -- used bytes- {-# UNPACK #-} !Int -- length left------------------------------------------------------------------------------ | /O(n)./ Extract a lazy 'L.ByteString' from a 'Builder'.--- The construction work takes place if and when the relevant part of--- the lazy 'L.ByteString' is demanded.----toLazyByteString :: Builder -> L.ByteString-toLazyByteString m = L.fromChunks $ unsafePerformIO $ do- buf <- newBuffer defaultSize- return (runBuilder (m `append` flush) (const []) buf)---- | /O(1)./ Pop the 'S.ByteString' we have constructed so far, if any,--- yielding a new chunk in the result lazy 'L.ByteString'.-flush :: Builder-flush = Builder $ \ k buf@(Buffer p o u l) ->- if u == 0- then k buf- else S.PS p o u : k (Buffer p (o+u) 0 l)--------------------------------------------------------------------------------- copied from Data.ByteString.Lazy----defaultSize :: Int-defaultSize = 32 * k - overhead- where k = 1024- overhead = 2 * sizeOf (undefined :: Int)------------------------------------------------------------------------------ | Sequence an IO operation on the buffer-unsafeLiftIO :: (Buffer -> IO Buffer) -> Builder-unsafeLiftIO f = Builder $ \ k buf -> unsafePerformIO $ do- buf' <- f buf- return (k buf')---- | Get the size of the buffer-withSize :: (Int -> Builder) -> Builder-withSize f = Builder $ \ k buf@(Buffer _ _ _ l) ->- runBuilder (f l) k buf---- | Map the resulting list of bytestrings.-mapBuilder :: ([S.ByteString] -> [S.ByteString]) -> Builder-mapBuilder f = Builder (f .)------------------------------------------------------------------------------ | Ensure that there are at least @n@ many bytes available.-ensureFree :: Int -> Builder-ensureFree n = n `seq` withSize $ \ l ->- if n <= l then empty else- flush `append` unsafeLiftIO (const (newBuffer (max n defaultSize)))---- | Ensure that @n@ many bytes are available, and then use @f@ to write some--- bytes into the memory.-writeN :: Int -> (Ptr Word8 -> IO ()) -> Builder-writeN n f = ensureFree n `append` unsafeLiftIO (writeNBuffer n f)--writeNBuffer :: Int -> (Ptr Word8 -> IO ()) -> Buffer -> IO Buffer-writeNBuffer n f (Buffer fp o u l) = do- withForeignPtr fp (\p -> f (p `plusPtr` (o+u)))- return (Buffer fp o (u+n) (l-n))--newBuffer :: Int -> IO Buffer-newBuffer size = do- fp <- S.mallocByteString size- return $! Buffer fp 0 0 size----------------------------------------------------------------------------- Aligned, host order writes of storable values---- | Ensure that @n@ many bytes are available, and then use @f@ to write some--- storable values into the memory.-writeNbytes :: Storable a => Int -> (Ptr a -> IO ()) -> Builder-writeNbytes n f = ensureFree n `append` unsafeLiftIO (writeNBufferBytes n f)--writeNBufferBytes :: Storable a => Int -> (Ptr a -> IO ()) -> Buffer -> IO Buffer-writeNBufferBytes n f (Buffer fp o u l) = do- withForeignPtr fp (\p -> f (p `plusPtr` (o+u)))- return (Buffer fp o (u+n) (l-n))--------------------------------------------------------------------------------- We rely on the fromIntegral to do the right masking for us.--- The inlining here is critical, and can be worth 4x performance------- | Write a Word16 in big endian format-putWord16be :: Word16 -> Builder-putWord16be w = writeN 2 $ \p -> do- poke p (fromIntegral (shiftR w 8) :: Word8)- poke (p `plusPtr` 1) (fromIntegral (w) :: Word8)---- | Write a Word16 in little endian format-putWord16le :: Word16 -> Builder-putWord16le w = writeN 2 $ \p -> do- poke p (fromIntegral (w) :: Word8)- poke (p `plusPtr` 1) (fromIntegral (shiftR w 8) :: Word8)---- putWord16le w16 = writeN 2 (\p -> poke (castPtr p) w16)---- | Write a 24 bit number in big endian format represented as Word32-putWord24be :: Word32 -> Builder-putWord24be w = writeN 3 $ \p -> do- poke p (fromIntegral (shiftR w 16) :: Word8)- poke (p `plusPtr` 1) (fromIntegral (shiftR w 8) :: Word8)- poke (p `plusPtr` 2) (fromIntegral (w) :: Word8)---- | Write a 24 bit number in little endian format represented as Word32-putWord24le :: Word32 -> Builder-putWord24le w = writeN 3 $ \p -> do- poke p (fromIntegral (w) :: Word8)- poke (p `plusPtr` 1) (fromIntegral (shiftR w 8) :: Word8)- poke (p `plusPtr` 2) (fromIntegral (shiftR w 16) :: Word8)---- | Write a Word32 in big endian format-putWord32be :: Word32 -> Builder-putWord32be w = writeN 4 $ \p -> do- poke p (fromIntegral (shiftR w 24) :: Word8)- poke (p `plusPtr` 1) (fromIntegral (shiftR w 16) :: Word8)- poke (p `plusPtr` 2) (fromIntegral (shiftR w 8) :: Word8)- poke (p `plusPtr` 3) (fromIntegral (w) :: Word8)------- a data type to tag Put/Check. writes construct these which are then--- inlined and flattened. matching Checks will be more robust with rules.------- | Write a Word32 in little endian format-putWord32le :: Word32 -> Builder-putWord32le w = writeN 4 $ \p -> do- poke p (fromIntegral (w) :: Word8)- poke (p `plusPtr` 1) (fromIntegral (shiftR w 8) :: Word8)- poke (p `plusPtr` 2) (fromIntegral (shiftR w 16) :: Word8)- poke (p `plusPtr` 3) (fromIntegral (shiftR w 24) :: Word8)---- on a little endian machine:--- putWord32le w32 = writeN 4 (\p -> poke (castPtr p) w32)---- | Write a Word64 in big endian format-putWord64be :: Word64 -> Builder-putWord64be w = writeN 8 $ \p -> do- poke p (fromIntegral (shiftR w 56) :: Word8)- poke (p `plusPtr` 1) (fromIntegral (shiftR w 48) :: Word8)- poke (p `plusPtr` 2) (fromIntegral (shiftR w 40) :: Word8)- poke (p `plusPtr` 3) (fromIntegral (shiftR w 32) :: Word8)- poke (p `plusPtr` 4) (fromIntegral (shiftR w 24) :: Word8)- poke (p `plusPtr` 5) (fromIntegral (shiftR w 16) :: Word8)- poke (p `plusPtr` 6) (fromIntegral (shiftR w 8) :: Word8)- poke (p `plusPtr` 7) (fromIntegral (w) :: Word8)---- | Write a Word64 in little endian format-putWord64le :: Word64 -> Builder-putWord64le w = writeN 8 $ \p -> do- poke p (fromIntegral (w) :: Word8)- poke (p `plusPtr` 1) (fromIntegral (shiftR w 8) :: Word8)- poke (p `plusPtr` 2) (fromIntegral (shiftR w 16) :: Word8)- poke (p `plusPtr` 3) (fromIntegral (shiftR w 24) :: Word8)- poke (p `plusPtr` 4) (fromIntegral (shiftR w 32) :: Word8)- poke (p `plusPtr` 5) (fromIntegral (shiftR w 40) :: Word8)- poke (p `plusPtr` 6) (fromIntegral (shiftR w 48) :: Word8)- poke (p `plusPtr` 7) (fromIntegral (shiftR w 56) :: Word8)---- on a little endian machine:--- putWord64le w64 = writeN 8 (\p -> poke (castPtr p) w64)---------------------------------------------------------------------------putInt8 :: Int8 -> Builder-putInt8 = putWord8 . fromIntegral--putInt16le :: Int16 -> Builder-putInt16le = putWord16le . fromIntegral--putInt16be :: Int16 -> Builder-putInt16be = putWord16be . fromIntegral--putInt32le :: Int32 -> Builder-putInt32le = putWord32le . fromIntegral--putInt32be :: Int32 -> Builder-putInt32be = putWord32be . fromIntegral--putInt64le :: Int64 -> Builder-putInt64le = putWord64le . fromIntegral--putInt64be :: Int64 -> Builder-putInt64be = putWord64be . fromIntegral----------------------------------------------------------------------------- Unaligned, word size ops---- | /O(1)./ A Builder taking a single native machine word. The word is--- written in host order, host endian form, for the machine you're on.--- On a 64 bit machine the Word is an 8 byte value, on a 32 bit machine,--- 4 bytes. Values written this way are not portable to--- different endian or word sized machines, without conversion.----putWordHost :: Word -> Builder-putWordHost w = writeNbytes (sizeOf (undefined :: Word)) (\p -> poke p w)---- | Write a Word16 in native host order and host endianness.--- 2 bytes will be written, unaligned.-putWord16host :: Word16 -> Builder-putWord16host w16 = writeNbytes (sizeOf (undefined :: Word16)) (\p -> poke p w16)---- | Write a Word32 in native host order and host endianness.--- 4 bytes will be written, unaligned.-putWord32host :: Word32 -> Builder-putWord32host w32 = writeNbytes (sizeOf (undefined :: Word32)) (\p -> poke p w32)---- | Write a Word64 in native host order.--- On a 32 bit machine we write two host order Word32s, in big endian form.--- 8 bytes will be written, unaligned.-putWord64host :: Word64 -> Builder-putWord64host w = writeNbytes (sizeOf (undefined :: Word64)) (\p -> poke p w)----------------------------------------------------------------------------putVarLenBe :: Word64 -> Builder-putVarLenBe w = varLenAux2 $ reverse $ varLenAux1 w- -putVarLenLe :: Word64 -> Builder-putVarLenLe w = varLenAux2 $ varLenAux1 w- -varLenAux1 :: Word64 -> [Word8]-varLenAux1 0 = []-varLenAux1 w = (fromIntegral $ w .&. 0x7F) : (varLenAux1 $ shiftR w 7)--varLenAux2 :: [Word8] -> Builder-varLenAux2 [] = putWord8 0-varLenAux2 [w] = putWord8 w-varLenAux2 (w : ws) = putWord8 (setBit w 7) `append` varLenAux2 ws+----------------------------------------------------------------------------- +-- | +-- Module : Data.ByteString.Builder +-- Copyright : Lennart Kolmodin, Ross Paterson, George Giorgidze +-- License : BSD3 +-- +-- Maintainer : George Giorgidze <http://cs.nott.ac.uk/~ggg/> +-- Stability : experimental +-- Portability : Portable +-- +-- Efficient construction of lazy bytestrings. +-- +----------------------------------------------------------------------------- +module Codec.ByteString.Builder ( + + -- * The Builder type + Builder + , toLazyByteString + + -- * Constructing Builders + , empty + , singleton + , putWord8 + , putInt8 + , append + , fromByteString -- :: S.ByteString -> Builder + , fromLazyByteString -- :: L.ByteString -> Builder + , putString + + -- * Flushing the buffer state + , flush + + -- * Derived Builders + -- ** Big-endian writes + , putWord16be -- :: Word16 -> Builder + , putWord24be -- :: Word32 -> Builder + , putWord32be -- :: Word32 -> Builder + , putWord64be -- :: Word64 -> Builder + + , putInt16be -- :: Int16 -> Builder + , putInt32be -- :: Int32 -> Builder + , putInt64be -- :: Int64 -> Builder + + -- ** Little-endian writes + , putWord16le -- :: Word16 -> Builder + , putWord24le -- :: Word32 -> Builder + , putWord32le -- :: Word32 -> Builder + , putWord64le -- :: Word64 -> Builder + + , putInt16le -- :: Int16 -> Builder + , putInt32le -- :: Int32 -> Builder + , putInt64le -- :: Int64 -> Builder + + -- ** Host-endian, unaligned writes + , putWordHost -- :: Word -> Builder + , putWord16host -- :: Word16 -> Builder + , putWord32host -- :: Word32 -> Builder + , putWord64host -- :: Word64 -> Builder + -- Variable length numbers + , putVarLenBe + , putVarLenLe + + ) where + + +import qualified Data.ByteString.Internal as S +import qualified Data.ByteString as S +import qualified Data.ByteString.Lazy as L + +import Foreign.Storable (Storable, poke, sizeOf) +import Foreign.Ptr (Ptr, plusPtr) +import Foreign.ForeignPtr (ForeignPtr, withForeignPtr) +import System.IO.Unsafe (unsafePerformIO) +import Data.ByteString.Internal (c2w) + +import Data.Bits +import Data.Word +import Data.Int +import Data.Semigroup + +------------------------------------------------------------------------ + +-- | A 'Builder' is an efficient way to build lazy 'L.ByteString's. +-- There are several functions for constructing 'Builder's, but only one +-- to inspect them: to extract any data, you have to turn them into lazy +-- 'L.ByteString's using 'toLazyByteString'. +-- +-- Internally, a 'Builder' constructs a lazy 'L.Bytestring' by filling byte +-- arrays piece by piece. As each buffer is filled, it is \'popped\' +-- off, to become a new chunk of the resulting lazy 'L.ByteString'. +-- All this is hidden from the user of the 'Builder'. + +newtype Builder = Builder { + -- Invariant (from Data.ByteString.Lazy): + -- The lists include no null ByteStrings. + runBuilder :: (Buffer -> [S.ByteString]) -> Buffer -> [S.ByteString] + } + +instance Semigroup Builder where + (<>) = append + +instance Monoid Builder where + mempty = empty + mappend = (<>) + +------------------------------------------------------------------------ + +-- | /O(1)./ The empty Builder, satisfying +-- +-- * @'toLazyByteString' 'empty' = 'L.empty'@ +-- +empty :: Builder +empty = Builder id + +-- | /O(1)./ A Builder taking a single byte, satisfying +-- +-- * @'toLazyByteString' ('singleton' b) = 'L.singleton' b@ +-- +singleton :: Word8 -> Builder +singleton = writeN 1 . flip poke + +putWord8 :: Word8 -> Builder +putWord8 = singleton +------------------------------------------------------------------------ + +-- | /O(1)./ The concatenation of two Builders, an associative operation +-- with identity 'empty', satisfying +-- +-- * @'toLazyByteString' ('append' x y) = 'L.append' ('toLazyByteString' x) ('toLazyByteString' y)@ +-- +append :: Builder -> Builder -> Builder +append (Builder f) (Builder g) = Builder (f . g) + +-- | /O(1)./ A Builder taking a 'S.ByteString', satisfying +-- +-- * @'toLazyByteString' ('fromByteString' bs) = 'L.fromChunks' [bs]@ +-- +fromByteString :: S.ByteString -> Builder +fromByteString bs + | S.null bs = empty + | otherwise = flush `append` mapBuilder (bs :) + +-- | /O(1)./ A Builder taking a lazy 'L.ByteString', satisfying +-- +-- * @'toLazyByteString' ('fromLazyByteString' bs) = bs@ +-- +fromLazyByteString :: L.ByteString -> Builder +fromLazyByteString bss = flush `append` mapBuilder (L.toChunks bss ++) + +putString :: String -> Builder +putString = fromLazyByteString . L.pack . map c2w + +------------------------------------------------------------------------ + +-- Our internal buffer type +data Buffer = Buffer {-# UNPACK #-} !(ForeignPtr Word8) + {-# UNPACK #-} !Int -- offset + {-# UNPACK #-} !Int -- used bytes + {-# UNPACK #-} !Int -- length left + +------------------------------------------------------------------------ + +-- | /O(n)./ Extract a lazy 'L.ByteString' from a 'Builder'. +-- The construction work takes place if and when the relevant part of +-- the lazy 'L.ByteString' is demanded. +-- +toLazyByteString :: Builder -> L.ByteString +toLazyByteString m = L.fromChunks $ unsafePerformIO $ do + buf <- newBuffer defaultSize + return (runBuilder (m `append` flush) (const []) buf) + +-- | /O(1)./ Pop the 'S.ByteString' we have constructed so far, if any, +-- yielding a new chunk in the result lazy 'L.ByteString'. +flush :: Builder +flush = Builder $ \ k buf@(Buffer p o u l) -> + if u == 0 + then k buf + else S.PS p o u : k (Buffer p (o+u) 0 l) + +------------------------------------------------------------------------ + +-- +-- copied from Data.ByteString.Lazy +-- +defaultSize :: Int +defaultSize = 32 * k - overhead + where k = 1024 + overhead = 2 * sizeOf (undefined :: Int) + +------------------------------------------------------------------------ + +-- | Sequence an IO operation on the buffer +unsafeLiftIO :: (Buffer -> IO Buffer) -> Builder +unsafeLiftIO f = Builder $ \ k buf -> unsafePerformIO $ do + buf' <- f buf + return (k buf') + +-- | Get the size of the buffer +withSize :: (Int -> Builder) -> Builder +withSize f = Builder $ \ k buf@(Buffer _ _ _ l) -> + runBuilder (f l) k buf + +-- | Map the resulting list of bytestrings. +mapBuilder :: ([S.ByteString] -> [S.ByteString]) -> Builder +mapBuilder f = Builder (f .) + +------------------------------------------------------------------------ + +-- | Ensure that there are at least @n@ many bytes available. +ensureFree :: Int -> Builder +ensureFree n = n `seq` withSize $ \ l -> + if n <= l then empty else + flush `append` unsafeLiftIO (const (newBuffer (max n defaultSize))) + +-- | Ensure that @n@ many bytes are available, and then use @f@ to write some +-- bytes into the memory. +writeN :: Int -> (Ptr Word8 -> IO ()) -> Builder +writeN n f = ensureFree n `append` unsafeLiftIO (writeNBuffer n f) + +writeNBuffer :: Int -> (Ptr Word8 -> IO ()) -> Buffer -> IO Buffer +writeNBuffer n f (Buffer fp o u l) = do + withForeignPtr fp (\p -> f (p `plusPtr` (o+u))) + return (Buffer fp o (u+n) (l-n)) + +newBuffer :: Int -> IO Buffer +newBuffer size = do + fp <- S.mallocByteString size + return $! Buffer fp 0 0 size + +------------------------------------------------------------------------ +-- Aligned, host order writes of storable values + +-- | Ensure that @n@ many bytes are available, and then use @f@ to write some +-- storable values into the memory. +writeNbytes :: Storable a => Int -> (Ptr a -> IO ()) -> Builder +writeNbytes n f = ensureFree n `append` unsafeLiftIO (writeNBufferBytes n f) + +writeNBufferBytes :: Storable a => Int -> (Ptr a -> IO ()) -> Buffer -> IO Buffer +writeNBufferBytes n f (Buffer fp o u l) = do + withForeignPtr fp (\p -> f (p `plusPtr` (o+u))) + return (Buffer fp o (u+n) (l-n)) + +------------------------------------------------------------------------ + +-- +-- We rely on the fromIntegral to do the right masking for us. +-- The inlining here is critical, and can be worth 4x performance +-- + +-- | Write a Word16 in big endian format +putWord16be :: Word16 -> Builder +putWord16be w = writeN 2 $ \p -> do + poke p (fromIntegral (shiftR w 8) :: Word8) + poke (p `plusPtr` 1) (fromIntegral (w) :: Word8) + +-- | Write a Word16 in little endian format +putWord16le :: Word16 -> Builder +putWord16le w = writeN 2 $ \p -> do + poke p (fromIntegral (w) :: Word8) + poke (p `plusPtr` 1) (fromIntegral (shiftR w 8) :: Word8) + +-- putWord16le w16 = writeN 2 (\p -> poke (castPtr p) w16) + +-- | Write a 24 bit number in big endian format represented as Word32 +putWord24be :: Word32 -> Builder +putWord24be w = writeN 3 $ \p -> do + poke p (fromIntegral (shiftR w 16) :: Word8) + poke (p `plusPtr` 1) (fromIntegral (shiftR w 8) :: Word8) + poke (p `plusPtr` 2) (fromIntegral (w) :: Word8) + +-- | Write a 24 bit number in little endian format represented as Word32 +putWord24le :: Word32 -> Builder +putWord24le w = writeN 3 $ \p -> do + poke p (fromIntegral (w) :: Word8) + poke (p `plusPtr` 1) (fromIntegral (shiftR w 8) :: Word8) + poke (p `plusPtr` 2) (fromIntegral (shiftR w 16) :: Word8) + +-- | Write a Word32 in big endian format +putWord32be :: Word32 -> Builder +putWord32be w = writeN 4 $ \p -> do + poke p (fromIntegral (shiftR w 24) :: Word8) + poke (p `plusPtr` 1) (fromIntegral (shiftR w 16) :: Word8) + poke (p `plusPtr` 2) (fromIntegral (shiftR w 8) :: Word8) + poke (p `plusPtr` 3) (fromIntegral (w) :: Word8) + +-- +-- a data type to tag Put/Check. writes construct these which are then +-- inlined and flattened. matching Checks will be more robust with rules. +-- + +-- | Write a Word32 in little endian format +putWord32le :: Word32 -> Builder +putWord32le w = writeN 4 $ \p -> do + poke p (fromIntegral (w) :: Word8) + poke (p `plusPtr` 1) (fromIntegral (shiftR w 8) :: Word8) + poke (p `plusPtr` 2) (fromIntegral (shiftR w 16) :: Word8) + poke (p `plusPtr` 3) (fromIntegral (shiftR w 24) :: Word8) + +-- on a little endian machine: +-- putWord32le w32 = writeN 4 (\p -> poke (castPtr p) w32) + +-- | Write a Word64 in big endian format +putWord64be :: Word64 -> Builder +putWord64be w = writeN 8 $ \p -> do + poke p (fromIntegral (shiftR w 56) :: Word8) + poke (p `plusPtr` 1) (fromIntegral (shiftR w 48) :: Word8) + poke (p `plusPtr` 2) (fromIntegral (shiftR w 40) :: Word8) + poke (p `plusPtr` 3) (fromIntegral (shiftR w 32) :: Word8) + poke (p `plusPtr` 4) (fromIntegral (shiftR w 24) :: Word8) + poke (p `plusPtr` 5) (fromIntegral (shiftR w 16) :: Word8) + poke (p `plusPtr` 6) (fromIntegral (shiftR w 8) :: Word8) + poke (p `plusPtr` 7) (fromIntegral (w) :: Word8) + +-- | Write a Word64 in little endian format +putWord64le :: Word64 -> Builder +putWord64le w = writeN 8 $ \p -> do + poke p (fromIntegral (w) :: Word8) + poke (p `plusPtr` 1) (fromIntegral (shiftR w 8) :: Word8) + poke (p `plusPtr` 2) (fromIntegral (shiftR w 16) :: Word8) + poke (p `plusPtr` 3) (fromIntegral (shiftR w 24) :: Word8) + poke (p `plusPtr` 4) (fromIntegral (shiftR w 32) :: Word8) + poke (p `plusPtr` 5) (fromIntegral (shiftR w 40) :: Word8) + poke (p `plusPtr` 6) (fromIntegral (shiftR w 48) :: Word8) + poke (p `plusPtr` 7) (fromIntegral (shiftR w 56) :: Word8) + +-- on a little endian machine: +-- putWord64le w64 = writeN 8 (\p -> poke (castPtr p) w64) + +----------------------------------------------------------------------- + +putInt8 :: Int8 -> Builder +putInt8 = putWord8 . fromIntegral + +putInt16le :: Int16 -> Builder +putInt16le = putWord16le . fromIntegral + +putInt16be :: Int16 -> Builder +putInt16be = putWord16be . fromIntegral + +putInt32le :: Int32 -> Builder +putInt32le = putWord32le . fromIntegral + +putInt32be :: Int32 -> Builder +putInt32be = putWord32be . fromIntegral + +putInt64le :: Int64 -> Builder +putInt64le = putWord64le . fromIntegral + +putInt64be :: Int64 -> Builder +putInt64be = putWord64be . fromIntegral + +------------------------------------------------------------------------ +-- Unaligned, word size ops + +-- | /O(1)./ A Builder taking a single native machine word. The word is +-- written in host order, host endian form, for the machine you're on. +-- On a 64 bit machine the Word is an 8 byte value, on a 32 bit machine, +-- 4 bytes. Values written this way are not portable to +-- different endian or word sized machines, without conversion. +-- +putWordHost :: Word -> Builder +putWordHost w = writeNbytes (sizeOf (undefined :: Word)) (\p -> poke p w) + +-- | Write a Word16 in native host order and host endianness. +-- 2 bytes will be written, unaligned. +putWord16host :: Word16 -> Builder +putWord16host w16 = writeNbytes (sizeOf (undefined :: Word16)) (\p -> poke p w16) + +-- | Write a Word32 in native host order and host endianness. +-- 4 bytes will be written, unaligned. +putWord32host :: Word32 -> Builder +putWord32host w32 = writeNbytes (sizeOf (undefined :: Word32)) (\p -> poke p w32) + +-- | Write a Word64 in native host order. +-- On a 32 bit machine we write two host order Word32s, in big endian form. +-- 8 bytes will be written, unaligned. +putWord64host :: Word64 -> Builder +putWord64host w = writeNbytes (sizeOf (undefined :: Word64)) (\p -> poke p w) + +------------------------------------------------------------------------ + +putVarLenBe :: Word64 -> Builder +putVarLenBe w = varLenAux2 $ reverse $ varLenAux1 w + +putVarLenLe :: Word64 -> Builder +putVarLenLe w = varLenAux2 $ varLenAux1 w + +varLenAux1 :: Word64 -> [Word8] +varLenAux1 0 = [] +varLenAux1 w = (fromIntegral $ w .&. 0x7F) : (varLenAux1 $ shiftR w 7) + +varLenAux2 :: [Word8] -> Builder +varLenAux2 [] = putWord8 0 +varLenAux2 [w] = putWord8 w +varLenAux2 (w : ws) = putWord8 (setBit w 7) `append` varLenAux2 ws
src/Codec/ByteString/Parser.hs view
@@ -1,595 +1,600 @@--------------------------------------------------------------------------------- |--- Module : Data.ByteString.Parser--- Copyright : Lennart Kolmodin, George Giorgidze--- License : BSD3------ Maintainer : George Giorgidze <http://cs.nott.ac.uk/~ggg/>--- Stability : experimental--- Portability : Portable------ A monad for efficiently building structures from--- encoded lazy ByteStrings.-----------------------------------------------------------------------------------module Codec.ByteString.Parser (-- -- * The Parser type- Parser- , runParser- , runParserState-- -- * Parsing- , choice- , expect- , skip- , lookAhead- , lookAheadM- , lookAheadE-- -- * Utility- , bytesRead- , getBytes- , remaining- , isEmpty-- -- * Parsing particular types- , satisfy- , getString- , getStringNul- , string- , getWord8- , getInt8- , word8- , int8-- -- ** ByteStrings- , getByteString- , getLazyByteString- , getLazyByteStringNul- , getRemainingLazyByteString-- -- ** Big-endian reads- , getWord16be- , word16be- , getWord24be- , word24be- , getWord32be- , word32be- , getWord64be- , word64be-- , getInt16be- , int16be- , getInt32be- , int32be- , getInt64be- , int64be-- -- ** Little-endian reads- , getWord16le- , word16le- , getWord24le- , word24le- , getWord32le- , word32le- , getWord64le- , word64le-- , getInt16le- , int16le- , getInt32le- , int32le- , getInt64le- , int64le-- -- ** Host-endian, unaligned reads- , getWordHost- , wordHost- , getWord16host- , word16host- , getWord32host- , word32host- , getWord64host- , word64host-- -- Variable length reads- , getVarLenBe- , varLenBe- , getVarLenLe- , varLenLe- ) where--import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as L-import qualified Data.ByteString.Internal as B-import qualified Data.ByteString.Lazy.Internal as L--import Foreign.Storable (Storable, peek, sizeOf)-import Foreign.Ptr (plusPtr, castPtr)-import Foreign.ForeignPtr (withForeignPtr)-import Control.Monad.ST (runST)-import Control.Monad.ST.Unsafe (unsafeInterleaveST)--import Control.Monad-import Control.Applicative-import Data.STRef-import Data.Word-import Data.Int-import Data.Bits-import Data.Maybe--import System.IO.Unsafe (unsafePerformIO)---- | The parse state-data S = S {-# UNPACK #-} !B.ByteString -- current chunk- L.ByteString -- the rest of the input- {-# UNPACK #-} !Int64 -- bytes read---- | The Get monad is just a State monad carrying around the input ByteString-newtype Parser a = Parser { unParser :: S -> Either String (a, S) }--instance Functor Parser where- fmap f m = Parser $ \s -> case unParser m s of- Left e -> Left e- Right (a, s') -> Right (f a, s')- -instance Monad Parser where- return a = Parser (\s -> Right (a, s))- m >>= k = Parser $ \s -> case (unParser m) s of- Left e -> Left e- Right (a, s') -> (unParser (k a)) s'- fail err = Parser $ \(S _ _ bytes) ->- Left (err ++ ". Failed reading at byte position " ++ show bytes)-instance MonadPlus Parser where- mzero = Parser $ \_ -> Left []- mplus p1 p2 = Parser $ \s -> case (unParser p1 s) of- Left e1 -> case (unParser p2 s) of- Left e2 -> Left (e1 ++ "\n" ++ e2)- ok -> ok- ok -> ok--instance Applicative Parser where- pure = return- (<*>) = ap- -instance Alternative Parser where- empty = mzero- (<|>) = mplus----------------------------------------------------------------------------get :: Parser S-get = Parser $ \s -> Right (s, s)--put :: S -> Parser ()-put s = Parser $ \_ -> Right ((), s)----------------------------------------------------------------------------initState :: L.ByteString -> S-initState xs = mkState xs 0--mkState :: L.ByteString -> Int64 -> S-mkState l = case l of- L.Empty -> S B.empty L.empty- L.Chunk x xs -> S x xs---- | Run the Get monad applies a 'get'-based parser on the input ByteString-runParser :: Parser a -> L.ByteString -> Either String a-runParser m str = case unParser m (initState str) of- Left e -> Left e- Right (a, _) -> Right a---- | Run the Get monad applies a 'get'-based parser on the input--- ByteString. Additional to the result of get it returns the number of--- consumed bytes and the rest of the input.-runParserState :: Parser a -> L.ByteString -> Int64 -> Either String (a, L.ByteString, Int64)-runParserState m str off =- case unParser m (mkState str off) of- Left e -> Left e- Right (a, ~(S s ss newOff)) -> Right (a, s `bsJoin` ss, newOff)----------------------------------------------------------------------------choice :: [Parser a] -> Parser a-choice = foldl (<|>) mzero---- | Skip ahead @n@ bytes. Fails if fewer than @n@ bytes are available.-skip :: Word64 -> Parser ()-skip n = readN (fromIntegral n) (const ())---- | Run @ga@, but return without consuming its input.--- Fails if @ga@ fails.-lookAhead :: Parser a -> Parser a-lookAhead ga = do- s <- get- a <- ga- put s- return a---- | Like 'lookAhead', but consume the input if @gma@ returns 'Just _'.--- Fails if @gma@ fails.-lookAheadM :: Parser (Maybe a) -> Parser (Maybe a)-lookAheadM gma = do- s <- get- ma <- gma- when (isNothing ma) $ put s- return ma---- | Like 'lookAhead', but consume the input if @gea@ returns 'Right _'.--- Fails if @gea@ fails.-lookAheadE :: Parser (Either a b) -> Parser (Either a b)-lookAheadE gea = do- s <- get- ea <- gea- case ea of- Left _ -> put s- _ -> return ()- return ea--expect :: (Show a, Eq a) => (a -> Bool) -> Parser a -> Parser a-expect f p = do- v <- p- when (not $ f v) $ fail $ show v ++ " was not expected."- return v--getString :: Int -> Parser String-getString l = do- bs <- getLazyByteString (fromIntegral l)- return $! map B.w2c (L.unpack bs)--getStringNul :: Parser String-getStringNul = do- bs <- getLazyByteStringNul- return $! map B.w2c (L.unpack bs)--string :: String -> Parser String-string s = expect (s ==) (getString $ length s)---- Utility---- | Get the total number of bytes read to this point.-bytesRead :: Parser Int64-bytesRead = do- S _ _ b <- get- return b---- | Get the number of remaining unparsed bytes.--- Useful for checking whether all input has been consumed.--- Note that this forces the rest of the input.-remaining :: Parser Int64-remaining = do- S s ss _ <- get- return $! (fromIntegral (B.length s) + L.length ss)---- | Test whether all input has been consumed,--- i.e. there are no remaining unparsed bytes.-isEmpty :: Parser Bool-isEmpty = do- S s ss _ <- get- return $! (B.null s && L.null ss)----------------------------------------------------------------------------- Utility with ByteStrings---- | An efficient 'get' method for strict ByteStrings. Fails if fewer--- than @n@ bytes are left in the input.-getByteString :: Int -> Parser B.ByteString-getByteString n = readN n id---- | An efficient 'get' method for lazy ByteStrings. Does not fail if fewer than--- @n@ bytes are left in the input.-getLazyByteString :: Int64 -> Parser L.ByteString-getLazyByteString n = do- S s ss bytes <- get- let big = s `bsJoin` ss- case splitAtST n big of- (consume, rest) -> do put $ mkState rest (bytes + n)- return consume---- | Get a lazy ByteString that is terminated with a NUL byte. Fails--- if it reaches the end of input without hitting a NUL.-getLazyByteStringNul :: Parser L.ByteString-getLazyByteStringNul = do- S s ss bytes <- get- let big = s `bsJoin` ss- (consume, t) = L.break (== 0) big- (h, rest) = L.splitAt 1 t- when (L.null h) $ fail "too few bytes"- put $ mkState rest (bytes + L.length consume + 1)- return consume---- | Get the remaining bytes as a lazy ByteString-getRemainingLazyByteString :: Parser L.ByteString-getRemainingLazyByteString = do- S s ss _ <- get- return $! (s `bsJoin` ss)----------------------------------------------------------------------------- Helpers---- | Pull @n@ bytes from the input, as a strict ByteString.-getBytes :: Int -> Parser B.ByteString-getBytes n = do- S s ss bytes <- get- if n <= B.length s- then do let (consume,rest) = B.splitAt n s- put $! S rest ss (bytes + fromIntegral n)- return $! consume- else- case L.splitAt (fromIntegral n) (s `bsJoin` ss) of- (consuming, rest) ->- do let now = B.concat . L.toChunks $ consuming- put $! mkState rest (bytes + fromIntegral n)- -- forces the next chunk before this one is returned- when (B.length now < n) $ fail "too few bytes"- return now--bsJoin :: B.ByteString -> L.ByteString -> L.ByteString-bsJoin bb lb- | B.null bb = lb- | otherwise = L.Chunk bb lb---- | Split a ByteString. If the first result is consumed before the ----- second, this runs in constant heap space.------ You must force the returned tuple for that to work, e.g.------ > case splitAtST n xs of--- > (ys,zs) -> consume ys ... consume zs----splitAtST :: Int64 -> L.ByteString -> (L.ByteString, L.ByteString)-splitAtST i ps | i <= 0 = (L.empty, ps)-splitAtST i ps = runST (- do r <- newSTRef undefined- xs <- first r i ps- ys <- unsafeInterleaveST (readSTRef r)- return (xs, ys))-- where- first r 0 xs@(L.Chunk _ _) = writeSTRef r xs >> return L.Empty- first r _ L.Empty = writeSTRef r L.Empty >> return L.Empty-- first r n (L.Chunk x xs)- | n < l = do writeSTRef r (L.Chunk (B.drop (fromIntegral n) x) xs)- return $! L.Chunk (B.take (fromIntegral n) x) L.Empty- | otherwise = do writeSTRef r (L.drop (n - l) xs)- liftM (L.Chunk x) $ unsafeInterleaveST (first r (n - l) xs)-- where l = fromIntegral (B.length x)---- Pull n bytes from the input, and apply a parser to those bytes,--- yielding a value. If less than @n@ bytes are available, fail with an--- error. This wraps @getBytes@.-readN :: Int -> (B.ByteString -> a) -> Parser a-readN n f = fmap f $ getBytes n------------------------------------------------------------------------------ Primtives---- helper, get a raw Ptr onto a strict ByteString copied out of the--- underlying lazy byteString. So many indirections from the raw parser--- state that my head hurts...--getPtr :: Storable a => Int -> Parser a-getPtr n = do- (fp,o,_) <- readN n B.toForeignPtr- return . unsafePerformIO $ withForeignPtr fp $ \p -> peek (castPtr $ p `plusPtr` o)----------------------------------------------------------------------------satisfy :: (Word8 -> Bool) -> Parser Word8-satisfy f = do- w <- getWord8- guard (f w)- return w---- | Read a Word8 from the monad state-getWord8 :: Parser Word8-getWord8 = getPtr (sizeOf (undefined :: Word8))--word8 :: Word8 -> Parser Word8-word8 w = expect (w ==) getWord8---- | Read a Word16 in big endian format-getWord16be :: Parser Word16-getWord16be = do- s <- readN 2 id- return $! (fromIntegral (s `B.index` 0) `shiftL` 8) .|.- (fromIntegral (s `B.index` 1))--word16be :: Word16 -> Parser Word16-word16be w = expect (w ==) getWord16be---- | Read a Word16 in little endian format-getWord16le :: Parser Word16-getWord16le = do- s <- readN 2 id- return $! (fromIntegral (s `B.index` 1) `shiftL` 8) .|.- (fromIntegral (s `B.index` 0) )--word16le :: Word16 -> Parser Word16-word16le w = expect (w ==) getWord16le---- | Read a 24 bit word into Word32 in big endian format-getWord24be :: Parser Word32-getWord24be = do- s <- readN 3 id- return $! (fromIntegral (s `B.index` 0) `shiftL` 16) .|.- (fromIntegral (s `B.index` 1) `shiftL` 8) .|.- (fromIntegral (s `B.index` 2) )--word24be :: Word32 -> Parser Word32-word24be w = expect (w ==) getWord24be--getWord24le :: Parser Word32-getWord24le = do- s <- readN 3 id- return $! (fromIntegral (s `B.index` 2) `shiftL` 16) .|.- (fromIntegral (s `B.index` 1) `shiftL` 8) .|.- (fromIntegral (s `B.index` 0) )--word24le :: Word32 -> Parser Word32-word24le w = expect (w ==) getWord24le---- | Read a Word32 in big endian format-getWord32be :: Parser Word32-getWord32be = do- s <- readN 4 id- return $! (fromIntegral (s `B.index` 0) `shiftL` 24) .|.- (fromIntegral (s `B.index` 1) `shiftL` 16) .|.- (fromIntegral (s `B.index` 2) `shiftL` 8) .|.- (fromIntegral (s `B.index` 3) )--word32be :: Word32 -> Parser Word32-word32be w = expect (w ==) getWord32be---- | Read a Word32 in little endian format-getWord32le :: Parser Word32-getWord32le = do- s <- readN 4 id- return $! (fromIntegral (s `B.index` 3) `shiftL` 24) .|.- (fromIntegral (s `B.index` 2) `shiftL` 16) .|.- (fromIntegral (s `B.index` 1) `shiftL` 8) .|.- (fromIntegral (s `B.index` 0) )--word32le :: Word32 -> Parser Word32-word32le w = expect (w ==) getWord32le----- | Read a Word64 in big endian format-getWord64be :: Parser Word64-getWord64be = do- s <- readN 8 id- return $! (fromIntegral (s `B.index` 0) `shiftL` 56) .|.- (fromIntegral (s `B.index` 1) `shiftL` 48) .|.- (fromIntegral (s `B.index` 2) `shiftL` 40) .|.- (fromIntegral (s `B.index` 3) `shiftL` 32) .|.- (fromIntegral (s `B.index` 4) `shiftL` 24) .|.- (fromIntegral (s `B.index` 5) `shiftL` 16) .|.- (fromIntegral (s `B.index` 6) `shiftL` 8) .|.- (fromIntegral (s `B.index` 7) )--word64be :: Word64 -> Parser Word64-word64be w = expect (w ==) getWord64be---- | Read a Word64 in little endian format-getWord64le :: Parser Word64-getWord64le = do- s <- readN 8 id- return $! (fromIntegral (s `B.index` 7) `shiftL` 56) .|.- (fromIntegral (s `B.index` 6) `shiftL` 48) .|.- (fromIntegral (s `B.index` 5) `shiftL` 40) .|.- (fromIntegral (s `B.index` 4) `shiftL` 32) .|.- (fromIntegral (s `B.index` 3) `shiftL` 24) .|.- (fromIntegral (s `B.index` 2) `shiftL` 16) .|.- (fromIntegral (s `B.index` 1) `shiftL` 8) .|.- (fromIntegral (s `B.index` 0) )--word64le :: Word64 -> Parser Word64-word64le w = expect (w ==) getWord64le--------------------------------------------------------------------------getInt8 :: Parser Int8-getInt8 = getWord8 >>= return . fromIntegral--int8 :: Int8 -> Parser Int8-int8 i = expect (i ==) getInt8--getInt16le :: Parser Int16-getInt16le = getWord16le >>= return . fromIntegral--int16le :: Int16 -> Parser Int16-int16le i = expect (i ==) getInt16le--getInt16be :: Parser Int16-getInt16be = getWord16be >>= return . fromIntegral--int16be :: Int16 -> Parser Int16-int16be i = expect (i ==) getInt16be--getInt32le :: Parser Int32-getInt32le = getWord32le >>= return . fromIntegral--int32le :: Int32 -> Parser Int32-int32le i = expect (i ==) getInt32le--getInt32be :: Parser Int32-getInt32be = getWord32be >>= return . fromIntegral--int32be :: Int32 -> Parser Int32-int32be i = expect (i ==) getInt32be--getInt64le :: Parser Int64-getInt64le = getWord64le >>= return . fromIntegral--int64le :: Int64 -> Parser Int64-int64le i = expect (i ==) getInt64le--getInt64be :: Parser Int64-getInt64be = getWord64be >>= return . fromIntegral--int64be :: Int64 -> Parser Int64-int64be i = expect (i ==) getInt64be----------------------------------------------------------------------------- Host-endian reads---- | /O(1)./ Read a single native machine word. The word is read in--- host order, host endian form, for the machine you're on. On a 64 bit--- machine the Word is an 8 byte value, on a 32 bit machine, 4 bytes.-getWordHost :: Parser Word-getWordHost = getPtr (sizeOf (undefined :: Word))--wordHost :: Word -> Parser Word-wordHost w = expect (w ==) getWordHost---- | /O(1)./ Read a 2 byte Word16 in native host order and host endianness.-getWord16host :: Parser Word16-getWord16host = getPtr (sizeOf (undefined :: Word16))--word16host :: Word16 -> Parser Word16-word16host w = expect (w ==) getWord16host---- | /O(1)./ Read a Word32 in native host order and host endianness.-getWord32host :: Parser Word32-getWord32host = getPtr (sizeOf (undefined :: Word32))--word32host :: Word32 -> Parser Word32-word32host w = expect (w ==) getWord32host---- | /O(1)./ Read a Word64 in native host order and host endianess.-getWord64host :: Parser Word64-getWord64host = getPtr (sizeOf (undefined :: Word64))--word64host :: Word64 -> Parser Word64-word64host w = expect (w ==) getWord64host---- Variable length numbers--getVarLenBe :: Parser Word64-getVarLenBe = f 0- where- f :: Word64 -> Parser Word64- f acc = do- w <- getWord8 >>= return . fromIntegral- if testBit w 7- then f $! (shiftL acc 7) .|. (clearBit w 7)- else return $! (shiftL acc 7) .|. w--varLenBe :: Word64 -> Parser Word64-varLenBe a = expect (a ==) getVarLenBe--getVarLenLe :: Parser Word64-getVarLenLe = do- w <- getWord8 >>= return . fromIntegral- if testBit w 7- then do- w' <- getVarLenLe- return $! (clearBit w 7) .|. (shiftL w' 7)- else return $! w--varLenLe :: Word64 -> Parser Word64-varLenLe a = expect (a ==) getVarLenLe+----------------------------------------------------------------------------- +-- | +-- Module : Data.ByteString.Parser +-- Copyright : Lennart Kolmodin, George Giorgidze +-- License : BSD3 +-- +-- Maintainer : George Giorgidze <http://cs.nott.ac.uk/~ggg/> +-- Stability : experimental +-- Portability : Portable +-- +-- A monad for efficiently building structures from +-- encoded lazy ByteStrings. +-- +----------------------------------------------------------------------------- + +module Codec.ByteString.Parser ( + + -- * The Parser type + Parser + , runParser + , runParserState + + -- * Parsing + , choice + , expect + , skip + , lookAhead + , lookAheadM + , lookAheadE + + -- * Utility + , bytesRead + , getBytes + , remaining + , isEmpty + + -- * Parsing particular types + , satisfy + , getString + , getStringNul + , string + , getWord8 + , getInt8 + , word8 + , int8 + + -- ** ByteStrings + , getByteString + , getLazyByteString + , getLazyByteStringNul + , getRemainingLazyByteString + + -- ** Big-endian reads + , getWord16be + , word16be + , getWord24be + , word24be + , getWord32be + , word32be + , getWord64be + , word64be + + , getInt16be + , int16be + , getInt32be + , int32be + , getInt64be + , int64be + + -- ** Little-endian reads + , getWord16le + , word16le + , getWord24le + , word24le + , getWord32le + , word32le + , getWord64le + , word64le + + , getInt16le + , int16le + , getInt32le + , int32le + , getInt64le + , int64le + + -- ** Host-endian, unaligned reads + , getWordHost + , wordHost + , getWord16host + , word16host + , getWord32host + , word32host + , getWord64host + , word64host + + -- Variable length reads + , getVarLenBe + , varLenBe + , getVarLenLe + , varLenLe + ) where + +import qualified Data.ByteString as B +import qualified Data.ByteString.Lazy as L +import qualified Data.ByteString.Internal as B +import qualified Data.ByteString.Lazy.Internal as L + +import Prelude hiding (fail) + +import Foreign.Storable (Storable, peek, sizeOf) +import Foreign.Ptr (plusPtr, castPtr) +import Foreign.ForeignPtr (withForeignPtr) +import Control.Monad.ST (runST) +import Control.Monad.ST.Unsafe (unsafeInterleaveST) +import Control.Monad.Fail + +import Control.Monad hiding (fail) +import Control.Applicative +import Data.STRef +import Data.Word +import Data.Int +import Data.Bits +import Data.Maybe + +import System.IO.Unsafe (unsafePerformIO) + +-- | The parse state +data S = S {-# UNPACK #-} !B.ByteString -- current chunk + L.ByteString -- the rest of the input + {-# UNPACK #-} !Int64 -- bytes read + +-- | The Get monad is just a State monad carrying around the input ByteString +newtype Parser a = Parser { unParser :: S -> Either String (a, S) } + +instance Functor Parser where + fmap f m = Parser $ \s -> case unParser m s of + Left e -> Left e + Right (a, s') -> Right (f a, s') + +instance Monad Parser where + return a = Parser (\s -> Right (a, s)) + m >>= k = Parser $ \s -> case (unParser m) s of + Left e -> Left e + Right (a, s') -> (unParser (k a)) s' +instance MonadPlus Parser where + mzero = Parser $ \_ -> Left [] + mplus p1 p2 = Parser $ \s -> case (unParser p1 s) of + Left e1 -> case (unParser p2 s) of + Left e2 -> Left (e1 ++ "\n" ++ e2) + ok -> ok + ok -> ok + +instance MonadFail Parser where + fail err = Parser $ \(S _ _ bytes) -> + Left (err ++ ". Failed reading at byte position " ++ show bytes) + +instance Applicative Parser where + pure = return + (<*>) = ap + +instance Alternative Parser where + empty = mzero + (<|>) = mplus + +------------------------------------------------------------------------ + +get :: Parser S +get = Parser $ \s -> Right (s, s) + +put :: S -> Parser () +put s = Parser $ \_ -> Right ((), s) + +------------------------------------------------------------------------ + +initState :: L.ByteString -> S +initState xs = mkState xs 0 + +mkState :: L.ByteString -> Int64 -> S +mkState l = case l of + L.Empty -> S B.empty L.empty + L.Chunk x xs -> S x xs + +-- | Run the Get monad applies a 'get'-based parser on the input ByteString +runParser :: Parser a -> L.ByteString -> Either String a +runParser m str = case unParser m (initState str) of + Left e -> Left e + Right (a, _) -> Right a + +-- | Run the Get monad applies a 'get'-based parser on the input +-- ByteString. Additional to the result of get it returns the number of +-- consumed bytes and the rest of the input. +runParserState :: Parser a -> L.ByteString -> Int64 -> Either String (a, L.ByteString, Int64) +runParserState m str off = + case unParser m (mkState str off) of + Left e -> Left e + Right (a, ~(S s ss newOff)) -> Right (a, s `bsJoin` ss, newOff) + +------------------------------------------------------------------------ + +choice :: [Parser a] -> Parser a +choice = foldl (<|>) mzero + +-- | Skip ahead @n@ bytes. Fails if fewer than @n@ bytes are available. +skip :: Word64 -> Parser () +skip n = readN (fromIntegral n) (const ()) + +-- | Run @ga@, but return without consuming its input. +-- Fails if @ga@ fails. +lookAhead :: Parser a -> Parser a +lookAhead ga = do + s <- get + a <- ga + put s + return a + +-- | Like 'lookAhead', but consume the input if @gma@ returns 'Just _'. +-- Fails if @gma@ fails. +lookAheadM :: Parser (Maybe a) -> Parser (Maybe a) +lookAheadM gma = do + s <- get + ma <- gma + when (isNothing ma) $ put s + return ma + +-- | Like 'lookAhead', but consume the input if @gea@ returns 'Right _'. +-- Fails if @gea@ fails. +lookAheadE :: Parser (Either a b) -> Parser (Either a b) +lookAheadE gea = do + s <- get + ea <- gea + case ea of + Left _ -> put s + _ -> return () + return ea + +expect :: (Show a, Eq a) => (a -> Bool) -> Parser a -> Parser a +expect f p = do + v <- p + when (not $ f v) $ fail $ show v ++ " was not expected." + return v + +getString :: Int -> Parser String +getString l = do + bs <- getLazyByteString (fromIntegral l) + return $! map B.w2c (L.unpack bs) + +getStringNul :: Parser String +getStringNul = do + bs <- getLazyByteStringNul + return $! map B.w2c (L.unpack bs) + +string :: String -> Parser String +string s = expect (s ==) (getString $ length s) + +-- Utility + +-- | Get the total number of bytes read to this point. +bytesRead :: Parser Int64 +bytesRead = do + S _ _ b <- get + return b + +-- | Get the number of remaining unparsed bytes. +-- Useful for checking whether all input has been consumed. +-- Note that this forces the rest of the input. +remaining :: Parser Int64 +remaining = do + S s ss _ <- get + return $! (fromIntegral (B.length s) + L.length ss) + +-- | Test whether all input has been consumed, +-- i.e. there are no remaining unparsed bytes. +isEmpty :: Parser Bool +isEmpty = do + S s ss _ <- get + return $! (B.null s && L.null ss) + +------------------------------------------------------------------------ +-- Utility with ByteStrings + +-- | An efficient 'get' method for strict ByteStrings. Fails if fewer +-- than @n@ bytes are left in the input. +getByteString :: Int -> Parser B.ByteString +getByteString n = readN n id + +-- | An efficient 'get' method for lazy ByteStrings. Does not fail if fewer than +-- @n@ bytes are left in the input. +getLazyByteString :: Int64 -> Parser L.ByteString +getLazyByteString n = do + S s ss bytes <- get + let big = s `bsJoin` ss + case splitAtST n big of + (consume, rest) -> do put $ mkState rest (bytes + n) + return consume + +-- | Get a lazy ByteString that is terminated with a NUL byte. Fails +-- if it reaches the end of input without hitting a NUL. +getLazyByteStringNul :: Parser L.ByteString +getLazyByteStringNul = do + S s ss bytes <- get + let big = s `bsJoin` ss + (consume, t) = L.break (== 0) big + (h, rest) = L.splitAt 1 t + when (L.null h) $ fail "too few bytes" + put $ mkState rest (bytes + L.length consume + 1) + return consume + +-- | Get the remaining bytes as a lazy ByteString +getRemainingLazyByteString :: Parser L.ByteString +getRemainingLazyByteString = do + S s ss _ <- get + return $! (s `bsJoin` ss) + +------------------------------------------------------------------------ +-- Helpers + +-- | Pull @n@ bytes from the input, as a strict ByteString. +getBytes :: Int -> Parser B.ByteString +getBytes n = do + S s ss bytes <- get + if n <= B.length s + then do let (consume,rest) = B.splitAt n s + put $! S rest ss (bytes + fromIntegral n) + return $! consume + else + case L.splitAt (fromIntegral n) (s `bsJoin` ss) of + (consuming, rest) -> + do let now = B.concat . L.toChunks $ consuming + put $! mkState rest (bytes + fromIntegral n) + -- forces the next chunk before this one is returned + when (B.length now < n) $ fail "too few bytes" + return now + +bsJoin :: B.ByteString -> L.ByteString -> L.ByteString +bsJoin bb lb + | B.null bb = lb + | otherwise = L.Chunk bb lb + +-- | Split a ByteString. If the first result is consumed before the -- +-- second, this runs in constant heap space. +-- +-- You must force the returned tuple for that to work, e.g. +-- +-- > case splitAtST n xs of +-- > (ys,zs) -> consume ys ... consume zs +-- +splitAtST :: Int64 -> L.ByteString -> (L.ByteString, L.ByteString) +splitAtST i ps | i <= 0 = (L.empty, ps) +splitAtST i ps = runST ( + do r <- newSTRef undefined + xs <- first r i ps + ys <- unsafeInterleaveST (readSTRef r) + return (xs, ys)) + + where + first r 0 xs@(L.Chunk _ _) = writeSTRef r xs >> return L.Empty + first r _ L.Empty = writeSTRef r L.Empty >> return L.Empty + + first r n (L.Chunk x xs) + | n < l = do writeSTRef r (L.Chunk (B.drop (fromIntegral n) x) xs) + return $! L.Chunk (B.take (fromIntegral n) x) L.Empty + | otherwise = do writeSTRef r (L.drop (n - l) xs) + liftM (L.Chunk x) $ unsafeInterleaveST (first r (n - l) xs) + + where l = fromIntegral (B.length x) + +-- Pull n bytes from the input, and apply a parser to those bytes, +-- yielding a value. If less than @n@ bytes are available, fail with an +-- error. This wraps @getBytes@. +readN :: Int -> (B.ByteString -> a) -> Parser a +readN n f = fmap f $ getBytes n + + +------------------------------------------------------------------------ +-- Primtives + +-- helper, get a raw Ptr onto a strict ByteString copied out of the +-- underlying lazy byteString. So many indirections from the raw parser +-- state that my head hurts... + +getPtr :: Storable a => Int -> Parser a +getPtr n = do + (fp,o,_) <- readN n B.toForeignPtr + return . unsafePerformIO $ withForeignPtr fp $ \p -> peek (castPtr $ p `plusPtr` o) + +------------------------------------------------------------------------ + +satisfy :: (Word8 -> Bool) -> Parser Word8 +satisfy f = do + w <- getWord8 + guard (f w) + return w + +-- | Read a Word8 from the monad state +getWord8 :: Parser Word8 +getWord8 = getPtr (sizeOf (undefined :: Word8)) + +word8 :: Word8 -> Parser Word8 +word8 w = expect (w ==) getWord8 + +-- | Read a Word16 in big endian format +getWord16be :: Parser Word16 +getWord16be = do + s <- readN 2 id + return $! (fromIntegral (s `B.index` 0) `shiftL` 8) .|. + (fromIntegral (s `B.index` 1)) + +word16be :: Word16 -> Parser Word16 +word16be w = expect (w ==) getWord16be + +-- | Read a Word16 in little endian format +getWord16le :: Parser Word16 +getWord16le = do + s <- readN 2 id + return $! (fromIntegral (s `B.index` 1) `shiftL` 8) .|. + (fromIntegral (s `B.index` 0) ) + +word16le :: Word16 -> Parser Word16 +word16le w = expect (w ==) getWord16le + +-- | Read a 24 bit word into Word32 in big endian format +getWord24be :: Parser Word32 +getWord24be = do + s <- readN 3 id + return $! (fromIntegral (s `B.index` 0) `shiftL` 16) .|. + (fromIntegral (s `B.index` 1) `shiftL` 8) .|. + (fromIntegral (s `B.index` 2) ) + +word24be :: Word32 -> Parser Word32 +word24be w = expect (w ==) getWord24be + +getWord24le :: Parser Word32 +getWord24le = do + s <- readN 3 id + return $! (fromIntegral (s `B.index` 2) `shiftL` 16) .|. + (fromIntegral (s `B.index` 1) `shiftL` 8) .|. + (fromIntegral (s `B.index` 0) ) + +word24le :: Word32 -> Parser Word32 +word24le w = expect (w ==) getWord24le + +-- | Read a Word32 in big endian format +getWord32be :: Parser Word32 +getWord32be = do + s <- readN 4 id + return $! (fromIntegral (s `B.index` 0) `shiftL` 24) .|. + (fromIntegral (s `B.index` 1) `shiftL` 16) .|. + (fromIntegral (s `B.index` 2) `shiftL` 8) .|. + (fromIntegral (s `B.index` 3) ) + +word32be :: Word32 -> Parser Word32 +word32be w = expect (w ==) getWord32be + +-- | Read a Word32 in little endian format +getWord32le :: Parser Word32 +getWord32le = do + s <- readN 4 id + return $! (fromIntegral (s `B.index` 3) `shiftL` 24) .|. + (fromIntegral (s `B.index` 2) `shiftL` 16) .|. + (fromIntegral (s `B.index` 1) `shiftL` 8) .|. + (fromIntegral (s `B.index` 0) ) + +word32le :: Word32 -> Parser Word32 +word32le w = expect (w ==) getWord32le + + +-- | Read a Word64 in big endian format +getWord64be :: Parser Word64 +getWord64be = do + s <- readN 8 id + return $! (fromIntegral (s `B.index` 0) `shiftL` 56) .|. + (fromIntegral (s `B.index` 1) `shiftL` 48) .|. + (fromIntegral (s `B.index` 2) `shiftL` 40) .|. + (fromIntegral (s `B.index` 3) `shiftL` 32) .|. + (fromIntegral (s `B.index` 4) `shiftL` 24) .|. + (fromIntegral (s `B.index` 5) `shiftL` 16) .|. + (fromIntegral (s `B.index` 6) `shiftL` 8) .|. + (fromIntegral (s `B.index` 7) ) + +word64be :: Word64 -> Parser Word64 +word64be w = expect (w ==) getWord64be + +-- | Read a Word64 in little endian format +getWord64le :: Parser Word64 +getWord64le = do + s <- readN 8 id + return $! (fromIntegral (s `B.index` 7) `shiftL` 56) .|. + (fromIntegral (s `B.index` 6) `shiftL` 48) .|. + (fromIntegral (s `B.index` 5) `shiftL` 40) .|. + (fromIntegral (s `B.index` 4) `shiftL` 32) .|. + (fromIntegral (s `B.index` 3) `shiftL` 24) .|. + (fromIntegral (s `B.index` 2) `shiftL` 16) .|. + (fromIntegral (s `B.index` 1) `shiftL` 8) .|. + (fromIntegral (s `B.index` 0) ) + +word64le :: Word64 -> Parser Word64 +word64le w = expect (w ==) getWord64le +------------------------------------------------------------------------ +getInt8 :: Parser Int8 +getInt8 = getWord8 >>= return . fromIntegral + +int8 :: Int8 -> Parser Int8 +int8 i = expect (i ==) getInt8 + +getInt16le :: Parser Int16 +getInt16le = getWord16le >>= return . fromIntegral + +int16le :: Int16 -> Parser Int16 +int16le i = expect (i ==) getInt16le + +getInt16be :: Parser Int16 +getInt16be = getWord16be >>= return . fromIntegral + +int16be :: Int16 -> Parser Int16 +int16be i = expect (i ==) getInt16be + +getInt32le :: Parser Int32 +getInt32le = getWord32le >>= return . fromIntegral + +int32le :: Int32 -> Parser Int32 +int32le i = expect (i ==) getInt32le + +getInt32be :: Parser Int32 +getInt32be = getWord32be >>= return . fromIntegral + +int32be :: Int32 -> Parser Int32 +int32be i = expect (i ==) getInt32be + +getInt64le :: Parser Int64 +getInt64le = getWord64le >>= return . fromIntegral + +int64le :: Int64 -> Parser Int64 +int64le i = expect (i ==) getInt64le + +getInt64be :: Parser Int64 +getInt64be = getWord64be >>= return . fromIntegral + +int64be :: Int64 -> Parser Int64 +int64be i = expect (i ==) getInt64be + +------------------------------------------------------------------------ +-- Host-endian reads + +-- | /O(1)./ Read a single native machine word. The word is read in +-- host order, host endian form, for the machine you're on. On a 64 bit +-- machine the Word is an 8 byte value, on a 32 bit machine, 4 bytes. +getWordHost :: Parser Word +getWordHost = getPtr (sizeOf (undefined :: Word)) + +wordHost :: Word -> Parser Word +wordHost w = expect (w ==) getWordHost + +-- | /O(1)./ Read a 2 byte Word16 in native host order and host endianness. +getWord16host :: Parser Word16 +getWord16host = getPtr (sizeOf (undefined :: Word16)) + +word16host :: Word16 -> Parser Word16 +word16host w = expect (w ==) getWord16host + +-- | /O(1)./ Read a Word32 in native host order and host endianness. +getWord32host :: Parser Word32 +getWord32host = getPtr (sizeOf (undefined :: Word32)) + +word32host :: Word32 -> Parser Word32 +word32host w = expect (w ==) getWord32host + +-- | /O(1)./ Read a Word64 in native host order and host endianess. +getWord64host :: Parser Word64 +getWord64host = getPtr (sizeOf (undefined :: Word64)) + +word64host :: Word64 -> Parser Word64 +word64host w = expect (w ==) getWord64host + +-- Variable length numbers + +getVarLenBe :: Parser Word64 +getVarLenBe = f 0 + where + f :: Word64 -> Parser Word64 + f acc = do + w <- getWord8 >>= return . fromIntegral + if testBit w 7 + then f $! (shiftL acc 7) .|. (clearBit w 7) + else return $! (shiftL acc 7) .|. w + +varLenBe :: Word64 -> Parser Word64 +varLenBe a = expect (a ==) getVarLenBe + +getVarLenLe :: Parser Word64 +getVarLenLe = do + w <- getWord8 >>= return . fromIntegral + if testBit w 7 + then do + w' <- getVarLenLe + return $! (clearBit w 7) .|. (shiftL w' 7) + else return $! w + +varLenLe :: Word64 -> Parser Word64 +varLenLe a = expect (a ==) getVarLenLe
src/Codec/Internal/Arbitrary.hs view
@@ -1,52 +1,52 @@--------------------------------------------------------------------------------- |--- Module : Data.Arbitrary--- Copyright : George Giorgidze--- License : BSD3------ Maintainer : George Giorgidze <http://cs.nott.ac.uk/~ggg/>--- Stability : Experimental--- Portability : Portable------ Additional instances of the 'Arbitrary' type class defined in 'Test.QuickCheck'.--- Some portions of code were copied from the test suite of 'binary' package.-----------------------------------------------------------------------------------{-# LANGUAGE FlexibleContexts #-}-{-# OPTIONS_GHC -fno-warn-orphans #-}--module Codec.Internal.Arbitrary (- arrayGen- , stringNulGen- ) where--import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as L--import Test.QuickCheck-import Data.Array.IArray-import Data.List-import Data.Word-import Data.Char--instance Arbitrary L.ByteString where- arbitrary = arbitrary >>= return . L.fromChunks . filter (not. B.null)--instance Arbitrary B.ByteString where- arbitrary = B.pack `fmap` arbitrary- -instance (Arbitrary e, Num i, IArray Array e, Ix i) => Arbitrary (Array i e) where- arbitrary = do- n <- choose (1, 128)- arrayGen n--arrayGen :: (Arbitrary e, Num i, IArray a e, Ix i) => Word -> Gen (a i e)-arrayGen 0 = error "Array with 0 elements can not be defined"-arrayGen n = do- es <- vector (fromIntegral n)- return $! listArray (0 , fromIntegral $ n - 1) es- -stringNulGen :: Word -> Gen String-stringNulGen n = do- sequence $ genericReplicate n $ choose (1,255) >>= return . chr+----------------------------------------------------------------------------- +-- | +-- Module : Data.Arbitrary +-- Copyright : George Giorgidze +-- License : BSD3 +-- +-- Maintainer : George Giorgidze <http://cs.nott.ac.uk/~ggg/> +-- Stability : Experimental +-- Portability : Portable +-- +-- Additional instances of the 'Arbitrary' type class defined in 'Test.QuickCheck'. +-- Some portions of code were copied from the test suite of 'binary' package. +-- +----------------------------------------------------------------------------- + +{-# LANGUAGE FlexibleContexts #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Codec.Internal.Arbitrary ( + arrayGen + , stringNulGen + ) where + +import qualified Data.ByteString as B +import qualified Data.ByteString.Lazy as L + +import Test.QuickCheck +import Data.Array.IArray +import Data.List +import Data.Word +import Data.Char + +instance Arbitrary L.ByteString where + arbitrary = arbitrary >>= return . L.fromChunks . filter (not. B.null) + +instance Arbitrary B.ByteString where + arbitrary = B.pack `fmap` arbitrary + +instance (Arbitrary e, Num i, IArray Array e, Ix i) => Arbitrary (Array i e) where + arbitrary = do + n <- choose (1, 128) + arrayGen n + +arrayGen :: (Arbitrary e, Num i, IArray a e, Ix i) => Word -> Gen (a i e) +arrayGen 0 = error "Array with 0 elements can not be defined" +arrayGen n = do + es <- vector (fromIntegral n) + return $! listArray (0 , fromIntegral $ n - 1) es + +stringNulGen :: Word -> Gen String +stringNulGen n = do + sequence $ genericReplicate n $ choose (1,255) >>= return . chr
src/Codec/Internal/TestSuite.hs view
@@ -1,117 +1,117 @@-module Main (main) where--import qualified Codec.Wav as Wav-import qualified Codec.SoundFont as SF-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as L--import Test.QuickCheck (quickCheck, (==>))--import Data.Audio-import Codec.Midi-import Codec.ByteString.Parser-import Codec.ByteString.Builder--import Data.Int-import Data.Word-import Data.Bits-import Data.Monoid-import Debug.Trace--roundTrip :: (Eq a, Show a) => (a -> Builder) -> Parser a -> a -> Bool-roundTrip b p a = if Right a == ea'- then True- else trace (unlines $ [show ea']) $ False- where ea' = runParser p bs- bs = toLazyByteString $ b a--testAudio :: IO ()-testAudio = do- putStrLn "TESTING Inctances of Audible"- quickCheck (prop_audible :: Word8 -> Bool)- quickCheck (prop_audible :: Word16 -> Bool)- quickCheck (prop_audible :: Word32 -> Bool)- -- quickCheck (prop_audible :: Word64 -> Bool)- quickCheck (prop_audible :: Int8 -> Bool)- quickCheck (prop_audible :: Int16 -> Bool)- quickCheck (prop_audible :: Int32 -> Bool)- -- quickCheck (prop_audible :: Int64 -> Bool)-- -- These two tests are commented, because they fail- -- Reason for that is the fact that Double is not abble to accomodate- -- 64 bit numbers in full precision- where- prop_audible :: (Eq a, Audible a) => a -> Bool- prop_audible a = (a == fromSample s) && (s >= -1.0) && (s <= 1.0)- where s = toSample a--testMidi :: IO ()-testMidi = do- putStrLn "TESTING PARSING AND BUILDING of Midi"-- quickCheck $ roundTrip buildMessage (parseMessage Nothing)- quickCheck $ roundTrip buildMidi parseMidi- quickCheck $ \trk -> trk == fromAbsTime (toAbsTime trk :: Track Ticks)- quickCheck $ \trk td -> trk == fromRealTime td (toRealTime td trk)-- quickCheck $ \m -> (not $ null $ tracks m) ==>- let (Midi SingleTrack _ trks) = toSingleTrack m- in length (concat $ tracks m) - length (concat trks) == length (tracks m) - 1--testWav :: IO ()-testWav = do- putStrLn "TESTING PARSING AND BUILDING of Wav"- quickCheck $ roundTrip (Wav.buildWav :: Audio Word8 -> Builder) Wav.parseWav- quickCheck $ roundTrip (Wav.buildWav :: Audio Int16 -> Builder) Wav.parseWav- quickCheck $ roundTrip (Wav.buildWav :: Audio Int32 -> Builder) Wav.parseWav--testSoundFont :: IO ()-testSoundFont = do- putStrLn "TESTING PARSING AND BUILDING of SoundFont"- quickCheck $ roundTrip SF.buildSoundFont SF.parseSoundFont--testParserBuilder :: IO ()-testParserBuilder = do- putStrLn "TESTING PARSING AND BUILDING OF NUMERICAL TYPES"-- quickCheck $ roundTrip putWord8 getWord8- quickCheck $ roundTrip putWord16be getWord16be- quickCheck $ roundTrip putWord16le getWord16le- quickCheck $ \w -> roundTrip putWord24be getWord24be (w .&. 0xFFFFFF)- quickCheck $ \w -> roundTrip putWord24le getWord24le (w .&. 0xFFFFFF)- quickCheck $ roundTrip putWord32be getWord32be- quickCheck $ roundTrip putWord32le getWord32le- quickCheck $ roundTrip putWord64be getWord64be- quickCheck $ roundTrip putWord64le getWord64le-- quickCheck $ roundTrip putInt8 getInt8- quickCheck $ roundTrip putInt16be getInt16be- quickCheck $ roundTrip putInt16le getInt16le- quickCheck $ roundTrip putInt32be getInt32be- quickCheck $ roundTrip putInt32le getInt32le- quickCheck $ roundTrip putInt64be getInt64be- quickCheck $ roundTrip putInt64le getInt64le-- quickCheck $ roundTrip putWordHost getWordHost- quickCheck $ roundTrip putWord16host getWord16host- quickCheck $ roundTrip putWord32host getWord32host- quickCheck $ roundTrip putWord64host getWord64host-- quickCheck $ roundTrip putVarLenBe getVarLenBe- quickCheck $ roundTrip putVarLenLe getVarLenLe-- putStrLn "TESTING PARSING AND BUILDING OF String and ByteString"-- quickCheck $ \s1 s2 -> roundTrip (\s -> putString s `mappend` putString s2) (getString $ length s1) s1- quickCheck $ \s1 s2 -> roundTrip (\s -> fromByteString s `mappend` fromByteString s2) (getByteString $ B.length s1) s1- quickCheck $ \s1 s2 -> roundTrip (\s -> fromLazyByteString s `mappend` fromLazyByteString s2) (getLazyByteString $ L.length s1) s1- quickCheck $ \bs -> roundTrip fromLazyByteString getRemainingLazyByteString bs---main :: IO ()-main = do- testAudio- testParserBuilder- testMidi- testWav- testSoundFont+module Main (main) where + +import qualified Codec.Wav as Wav +import qualified Codec.SoundFont as SF +import qualified Data.ByteString as B +import qualified Data.ByteString.Lazy as L + +import Test.QuickCheck (quickCheck, (==>)) + +import Data.Audio +import Codec.Midi +import Codec.ByteString.Parser +import Codec.ByteString.Builder + +import Data.Int +import Data.Word +import Data.Bits +import Data.Monoid +import Debug.Trace + +roundTrip :: (Eq a, Show a) => (a -> Builder) -> Parser a -> a -> Bool +roundTrip b p a = if Right a == ea' + then True + else trace (unlines $ [show ea']) $ False + where ea' = runParser p bs + bs = toLazyByteString $ b a + +testAudio :: IO () +testAudio = do + putStrLn "TESTING Inctances of Audible" + quickCheck (prop_audible :: Word8 -> Bool) + quickCheck (prop_audible :: Word16 -> Bool) + quickCheck (prop_audible :: Word32 -> Bool) + -- quickCheck (prop_audible :: Word64 -> Bool) + quickCheck (prop_audible :: Int8 -> Bool) + quickCheck (prop_audible :: Int16 -> Bool) + quickCheck (prop_audible :: Int32 -> Bool) + -- quickCheck (prop_audible :: Int64 -> Bool) + + -- These two tests are commented, because they fail + -- Reason for that is the fact that Double is not abble to accomodate + -- 64 bit numbers in full precision + where + prop_audible :: (Eq a, Audible a) => a -> Bool + prop_audible a = (a == fromSample s) && (s >= -1.0) && (s <= 1.0) + where s = toSample a + +testMidi :: IO () +testMidi = do + putStrLn "TESTING PARSING AND BUILDING of Midi" + + quickCheck $ roundTrip buildMessage (parseMessage Nothing) + quickCheck $ roundTrip buildMidi parseMidi + quickCheck $ \trk -> trk == fromAbsTime (toAbsTime trk :: Track Ticks) + quickCheck $ \trk td -> trk == fromRealTime td (toRealTime td trk) + + quickCheck $ \m -> (not $ null $ tracks m) ==> + let (Midi SingleTrack _ trks) = toSingleTrack m + in length (concat $ tracks m) - length (concat trks) == length (tracks m) - 1 + +testWav :: IO () +testWav = do + putStrLn "TESTING PARSING AND BUILDING of Wav" + quickCheck $ roundTrip (Wav.buildWav :: Audio Word8 -> Builder) Wav.parseWav + quickCheck $ roundTrip (Wav.buildWav :: Audio Int16 -> Builder) Wav.parseWav + quickCheck $ roundTrip (Wav.buildWav :: Audio Int32 -> Builder) Wav.parseWav + +testSoundFont :: IO () +testSoundFont = do + putStrLn "TESTING PARSING AND BUILDING of SoundFont" + quickCheck $ roundTrip SF.buildSoundFont SF.parseSoundFont + +testParserBuilder :: IO () +testParserBuilder = do + putStrLn "TESTING PARSING AND BUILDING OF NUMERICAL TYPES" + + quickCheck $ roundTrip putWord8 getWord8 + quickCheck $ roundTrip putWord16be getWord16be + quickCheck $ roundTrip putWord16le getWord16le + quickCheck $ \w -> roundTrip putWord24be getWord24be (w .&. 0xFFFFFF) + quickCheck $ \w -> roundTrip putWord24le getWord24le (w .&. 0xFFFFFF) + quickCheck $ roundTrip putWord32be getWord32be + quickCheck $ roundTrip putWord32le getWord32le + quickCheck $ roundTrip putWord64be getWord64be + quickCheck $ roundTrip putWord64le getWord64le + + quickCheck $ roundTrip putInt8 getInt8 + quickCheck $ roundTrip putInt16be getInt16be + quickCheck $ roundTrip putInt16le getInt16le + quickCheck $ roundTrip putInt32be getInt32be + quickCheck $ roundTrip putInt32le getInt32le + quickCheck $ roundTrip putInt64be getInt64be + quickCheck $ roundTrip putInt64le getInt64le + + quickCheck $ roundTrip putWordHost getWordHost + quickCheck $ roundTrip putWord16host getWord16host + quickCheck $ roundTrip putWord32host getWord32host + quickCheck $ roundTrip putWord64host getWord64host + + quickCheck $ roundTrip putVarLenBe getVarLenBe + quickCheck $ roundTrip putVarLenLe getVarLenLe + + putStrLn "TESTING PARSING AND BUILDING OF String and ByteString" + + quickCheck $ \s1 s2 -> roundTrip (\s -> putString s `mappend` putString s2) (getString $ length s1) s1 + quickCheck $ \s1 s2 -> roundTrip (\s -> fromByteString s `mappend` fromByteString s2) (getByteString $ B.length s1) s1 + quickCheck $ \s1 s2 -> roundTrip (\s -> fromLazyByteString s `mappend` fromLazyByteString s2) (getLazyByteString $ L.length s1) s1 + quickCheck $ \bs -> roundTrip fromLazyByteString getRemainingLazyByteString bs + + +main :: IO () +main = do + testAudio + testParserBuilder + testMidi + testWav + testSoundFont
src/Codec/Midi.hs view
@@ -1,717 +1,717 @@--------------------------------------------------------------------------------- |--- Module : Codec.Midi--- Copyright : George Giorgidze--- License : BSD3------ Maintainer : George Giorgidze <http://cs.nott.ac.uk/~ggg/>--- Stability : Experimental--- Portability : Portable------ Reading, writing and maniplating of standard MIDI files-----------------------------------------------------------------------------------module Codec.Midi- (- Midi (..)- , FileType (..)- , Track- , TimeDiv (..)- , Message (..)- - , Ticks- , Time- , Channel- , Key- , Velocity- , Pressure- , Preset- , Bank- , PitchWheel- , Tempo- - , isNoteOff- , isNoteOn- , isKeyPressure- , isControlChange- , isProgramChange- , isChannelPressure- , isPitchWheel- , isChannelMessage- , isMetaMessage- , isSysexMessage- , isTrackEnd- - , removeTrackEnds- , toSingleTrack- , merge- , fromAbsTime- , toAbsTime- , toRealTime- , fromRealTime- - , importFile- , exportFile- , parseMidi- , buildMidi- , parseTrack- , buildTrack- , parseMessage- , buildMessage- )- where--import qualified Data.ByteString.Lazy as L--import Test.QuickCheck (Arbitrary, arbitrary, choose, oneof)--import Codec.ByteString.Parser-import Codec.ByteString.Builder-import Codec.Internal.Arbitrary ()--import Data.Word-import Data.Bits-import Data.Maybe-import Data.List-import Data.Monoid (mempty, mconcat, mappend)-import Control.Applicative-import Control.Monad--data Midi = Midi {- fileType :: FileType- , timeDiv :: TimeDiv- , tracks :: [Track Ticks]- } deriving (Eq, Show)--instance Arbitrary Midi where- arbitrary = do- ft <- arbitrary- td <- arbitrary- if ft == SingleTrack- then do- trk <- arbitrary >>= return . fAux- return $! Midi ft td [trk]- else do- trks <- arbitrary >>= return . map fAux- return $! Midi ft td trks- where- fAux = (++ [(0,TrackEnd)]) . map (\(dt,m) -> (abs dt,m)) . removeTrackEnds--data FileType = SingleTrack | MultiTrack | MultiPattern- deriving (Eq, Show)--instance Arbitrary FileType where- arbitrary = oneof [return SingleTrack , return MultiTrack , return MultiPattern]- -type Track a = [(a,Message)]--data TimeDiv =- TicksPerBeat Int | -- 1 -- (2^15 - 1)- TicksPerSecond Int Int -- 1 - 127- -- FramesPerSecond TicksPerFrame- deriving (Show,Eq)--instance Arbitrary TimeDiv where- arbitrary = oneof [- choose (1,2 ^ (15 :: Int) - 1) >>= return . TicksPerBeat- , two (choose (1,127)) >>= \(w1,w2) -> return $! TicksPerSecond w1 w2]--type Ticks = Int -- 0 - (2^28 - 1)-type Time = Double--type Channel = Int -- 0 - 15-type Key = Int -- 0 - 127-type Velocity = Int -- 0 - 127-type Pressure = Int -- 0 - 127-type Preset = Int -- 0 - 127-type Bank = Int-type PitchWheel = Int -- 0 - (2^14 - 1)-type Tempo = Int -- microseconds per beat 1 - (2^24 - 1)--data Message =--- Channel Messages- NoteOff { channel :: !Channel, key :: !Key, velocity :: !Velocity } |- NoteOn { channel :: !Channel, key :: !Key, velocity :: !Velocity } |- KeyPressure { channel :: !Channel, key :: !Key, pressure :: !Pressure} |- ControlChange { channel :: !Channel, controllerNumber :: !Int, controllerValue :: !Int } |- ProgramChange { channel :: !Channel, preset :: !Preset } |- ChannelPressure { channel :: !Channel, pressure :: !Pressure } |- PitchWheel { channel :: !Channel, pitchWheel :: !PitchWheel } |--- Meta Messages- SequenceNumber !Int | -- 0 - (2^16 - 1)- Text !String |- Copyright !String |- TrackName !String |- InstrumentName !String |- Lyrics !String |- Marker !String |- CuePoint !String |- ChannelPrefix !Channel |- ProgramName !String |- DeviceName !String |- TrackEnd |- TempoChange !Tempo |- SMPTEOffset !Int !Int !Int !Int !Int | -- 0-23 0-59 0-59 0-30 0-99- TimeSignature !Int !Int !Int !Int | -- 0-255 0-255 0-255 1-255- KeySignature !Int !Int | -- -7 - 7 0 - 1- Reserved !Int !L.ByteString |- -- System Exclusive Messages- Sysex !Int !L.ByteString -- 0xF0 or 0xF7- deriving (Show,Eq)--instance Arbitrary Message where- arbitrary = do- -- Channel Messages- c <- choose (0,15)- oneof [- two (choose (0,127)) >>= \(w2,w3) -> return $! NoteOff c w2 w3- , two (choose (0,127)) >>= \(w2,w3) -> return $! NoteOn c w2 w3- , two (choose (0,127)) >>= \(w2,w3) -> return $! KeyPressure c w2 w3- , two (choose (0,127)) >>= \(w2,w3) -> return $! ControlChange c w2 w3- , choose (0,127) >>= \w2 -> return $! ProgramChange c w2- , choose (0,127) >>= \w2 -> return $! ChannelPressure c w2- , do p <- choose (0,2 ^ (14 :: Int) - 1)- return $! PitchWheel c p- -- Meta Messages- , choose (0,2 ^ (16 :: Int) - 1) >>= return . SequenceNumber- , arbitrary >>= return . Text- , arbitrary >>= return . Copyright- , arbitrary >>= return . TrackName- , arbitrary >>= return . InstrumentName- , arbitrary >>= return . Lyrics- , arbitrary >>= return . Marker- , arbitrary >>= return . CuePoint- , return $! ChannelPrefix c- , arbitrary >>= return . ProgramName- , arbitrary >>= return . DeviceName- , choose (0,2 ^ (14 :: Int) - 1) >>= return . TempoChange- , do w1 <- choose (0,23)- w2 <- choose (0,59)- w3 <- choose (0,59)- w4 <- choose (0,30)- w5 <- choose (0,99)- return $! SMPTEOffset w1 w2 w3 w4 w5- , do w1 <- choose (0,255)- w2 <- choose (0,255)- w3 <- choose (0,255)- w4 <- choose (1,255)- return $! TimeSignature w1 w2 w3 w4- , do w1 <- choose (-7,7)- w2 <- choose (0,1)- return $! KeySignature w1 w2- , arbitrary >>= \bs -> return $! Reserved 0x60 bs- -- System Exclusive Messages- , do w <- oneof [return 0xF0, return 0xF7]- bs <- arbitrary- return $! Sysex w bs]--isNoteOff :: Message -> Bool-isNoteOff (NoteOff {}) = True-isNoteOff _ = False--isNoteOn :: Message -> Bool-isNoteOn (NoteOn {}) = True-isNoteOn _ = False--isKeyPressure :: Message -> Bool-isKeyPressure (KeyPressure {}) = True-isKeyPressure _ = False--isControlChange :: Message -> Bool-isControlChange (ControlChange {}) = True-isControlChange _ = False--isProgramChange :: Message -> Bool-isProgramChange (ProgramChange {}) = True-isProgramChange _ = False--isChannelPressure :: Message -> Bool-isChannelPressure (ChannelPressure {}) = True-isChannelPressure _ = False--isPitchWheel :: Message -> Bool-isPitchWheel (PitchWheel {}) = True-isPitchWheel _ = False--isChannelMessage :: Message -> Bool-isChannelMessage msg = (not $ isMetaMessage msg) && (not $ isSysexMessage msg)--isSysexMessage :: Message -> Bool-isSysexMessage (Sysex _ _) = True-isSysexMessage _ = False--isMetaMessage :: Message -> Bool-isMetaMessage msg = case msg of- SequenceNumber _ -> True- Text _ -> True- Copyright _ -> True- TrackName _ -> True- InstrumentName _ -> True- Lyrics _ -> True- Marker _ -> True- CuePoint _ -> True- ChannelPrefix _ -> True- ProgramName _ -> True- DeviceName _ -> True- TrackEnd -> True- TempoChange _ -> True- SMPTEOffset _ _ _ _ _ -> True- TimeSignature _ _ _ _ -> True- KeySignature _ _ -> True- Reserved _ _ -> True- _ -> False--isTrackEnd :: Message -> Bool-isTrackEnd TrackEnd = True-isTrackEnd _ = False--removeTrackEnds :: Track a -> Track a-removeTrackEnds [] = []-removeTrackEnds trk = filter (not. isTrackEnd . snd) trk--toSingleTrack :: Midi -> Midi-toSingleTrack m@(Midi SingleTrack _ _) = m-toSingleTrack (Midi MultiTrack td trks) = Midi SingleTrack td [trk']- where trk' = foldl' merge [] trks-toSingleTrack (Midi MultiPattern td trks) = Midi SingleTrack td [trk']- where trk' = (concat $ map removeTrackEnds trks) ++ [(0,TrackEnd)]--merge :: (Num a, Ord a) => Track a -> Track a -> Track a-merge track1 track2 = (fromAbsTime $ f trk1' trk2') ++ [(0,TrackEnd)]- where- trk1' = toAbsTime $ removeTrackEnds track1- trk2' = toAbsTime $ removeTrackEnds track2- f trk [] = trk- f [] trk = trk- f ((dt1,m1) : trk1) ((dt2,m2) : trk2) = if dt1 <= dt2- then (dt1,m1) : (f trk1 ((dt2,m2) : trk2))- else (dt2,m2) : (f ((dt1,m1) : trk1) trk2)--toAbsTime :: (Num a) => Track a -> Track a-toAbsTime trk = zip ts' ms- where- (ts,ms) = unzip trk- (_,ts') = mapAccumL (\acc t -> let t' = acc + t in (t',t')) 0 ts- -fromAbsTime :: (Num a) => Track a -> Track a-fromAbsTime trk = zip ts' ms- where- (ts,ms) = unzip trk- (_,ts') = mapAccumL (\acc t -> (t,t - acc)) 0 ts--toRealTime :: TimeDiv -> Track Ticks -> Track Time-toRealTime (TicksPerBeat tpb) trk = trk'- where- (_,trk') = mapAccumL f (div 60000000 120) trk -- default tempo 120 beats per minute- formula dt tempo =- (fromIntegral dt / fromIntegral tpb) * (fromIntegral tempo) * (1.0E-6)- f :: Tempo -> (Ticks,Message) -> (Tempo, (Time,Message))- f _ (dt, TempoChange tempo) = (tempo, (formula dt tempo, TempoChange tempo))- f tempo (dt,msg) = (tempo, (formula dt tempo,msg))-toRealTime (TicksPerSecond fps tpf) trk = map f trk- where- f (dt,msg) = (fromIntegral dt / (fromIntegral fps * fromIntegral tpf), msg)--fromRealTime :: TimeDiv -> Track Time -> Track Ticks-fromRealTime (TicksPerBeat tpb) trk = trk'- where- (_,trk') = mapAccumL f (div 60000000 120) trk -- default tempo 120 beats per minute- formula dt tempo = round $- (dt * fromIntegral tpb) / (fromIntegral tempo * 1.0E-6)- f :: Tempo -> (Time,Message) -> (Tempo, (Ticks,Message))- f _ (dt, TempoChange tempo) = (tempo, (formula dt tempo, TempoChange tempo))- f tempo (dt,msg) = (tempo, (formula dt tempo,msg))-fromRealTime (TicksPerSecond fps tpf) trk = map f trk- where- f (dt,msg) = (round $ dt * fromIntegral fps * fromIntegral tpf, msg)---- MIDI import-importFile :: FilePath -> IO (Either String Midi)-importFile f = do- bs <- L.readFile f- return $! runParser parseMidi bs--exportFile :: FilePath -> Midi -> IO ()-exportFile f m = do- let bs = toLazyByteString $ buildMidi m- L.writeFile f bs---- All numeric values are stored in big-endian format--parseMidi :: Parser Midi-parseMidi = do- _ <- string "MThd"- _ <- word32be 6- formatType' <- getWord16be- trackNumber' <- getWord16be- timeDivision' <- getWord16be- let timeDivision = if testBit timeDivision' 15- then TicksPerSecond- (fromIntegral $ (flip shiftR) 9 $ shiftL timeDivision' 1)- (fromIntegral $ (flip shiftR) 8 $ shiftL timeDivision' 8)- else TicksPerBeat (fromIntegral timeDivision')- case (formatType',trackNumber') of- (0,1) -> do- track' <- parseTrack- return $! Midi SingleTrack timeDivision [track']- (1,n) -> do- tracks' <- sequence $ replicate (fromIntegral n) parseTrack- return $! Midi MultiTrack timeDivision tracks'- (2,n) -> do- tracks' <- sequence $ replicate (fromIntegral n) parseTrack- return $! Midi MultiPattern timeDivision tracks'- _ -> fail "Invalid Midi file format"--buildMidi :: Midi -> Builder-buildMidi m = mconcat [- putString "MThd"- , putWord32be 6- , case fileType m of- SingleTrack -> putWord16be 0- MultiTrack -> putWord16be 1- MultiPattern -> putWord16be 2- , putWord16be (fromIntegral $ length $ tracks m)- , case timeDiv m of- TicksPerBeat i -> putWord16be (fromIntegral i)- TicksPerSecond i1 i2 -> mconcat [- putWord8 (setBit (fromIntegral i1) 7)- , putWord8 (fromIntegral i2)]- , mconcat (map buildTrack $ tracks m)]- -parseTrack :: Parser (Track Ticks)-parseTrack = do- _ <- string "MTrk"- _ <- getWord32be -- trackSize- track' <- parseMessages Nothing- return track'- -buildTrack :: Track Ticks -> Builder-buildTrack trk = mconcat [- putString "MTrk"- , putWord32be $ fromIntegral $ L.length bs- , fromLazyByteString bs]- where- f (dt,msg) = (putVarLenBe $ fromIntegral dt) `append` buildMessage msg- bs = toLazyByteString $ mconcat (map f trk)--parseMessages :: Maybe Message -> Parser (Track Ticks)-parseMessages mPreMsg = do- dt <- getVarLenBe >>= return . fromIntegral- msg <- parseMessage mPreMsg- if (isTrackEnd msg)- then return [(dt,msg)]- else do- let mMsg = if isChannelMessage msg then (Just msg) else mPreMsg- msgs <- parseMessages mMsg- return $! (dt,msg) : msgs--parseMessage :: Maybe Message -> Parser Message-parseMessage mPreMsg = choice [- parseChannelMessage mPreMsg- , parseMetaMessage- , parseSysexMessage]- -buildMessage :: Message -> Builder-buildMessage msg | isChannelMessage msg = buildChannelMessage msg-buildMessage msg | isMetaMessage msg = buildMetaMessage msg-buildMessage msg | isSysexMessage msg = buildSysexMessage msg-buildMessage _ = mempty- -parseChannelMessage :: Maybe Message -> Parser Message-parseChannelMessage mPreMsg = choice $ map (\f -> f mPreMsg) [- parseNoteOff- , parseNoteOn- , parseKeyPressure- , parseControlChange- , parseProgramChange- , parseChannelPressure- , parsePitchWheel- ]--parseChannel :: Maybe Message -> (Message -> Bool) -> Word8 -> Parser Channel-parseChannel mPreMsg isNeededMsg msgCode = p1 <|> p2- where- p1 = do- _ <- lookAhead (satisfy ( < 0x80))- guard $ (isJust mPreMsg) && (isNeededMsg $ fromJust mPreMsg)- return $! channel (fromJust mPreMsg)- p2 = do- w8 <- getWord8- guard (msgCode == shiftR w8 4)- return $! fromIntegral $ w8 .&. (0x0F :: Word8)--parseNoteOff :: Maybe Message -> Parser Message-parseNoteOff mPreMsg = do- ch <- parseChannel mPreMsg isNoteOff 0x08- p1 <- getWord8- p2 <- getWord8- return $! NoteOff ch (fromIntegral p1) (fromIntegral p2)--parseNoteOn :: Maybe Message -> Parser Message-parseNoteOn mPreMsg = do- ch <- parseChannel mPreMsg isNoteOn 0x09- p1 <- getWord8- p2 <- getWord8- return $! NoteOn ch (fromIntegral p1) (fromIntegral p2)--parseKeyPressure :: Maybe Message -> Parser Message-parseKeyPressure mPreMsg = do- ch <- parseChannel mPreMsg isKeyPressure 0x0A- p1 <- getWord8- p2 <- getWord8- return $! KeyPressure ch (fromIntegral p1) (fromIntegral p2)--parseControlChange :: Maybe Message -> Parser Message-parseControlChange mPreMsg = do- ch <- parseChannel mPreMsg isControlChange 0x0B- p1 <- getWord8- p2 <- getWord8- return $! ControlChange ch (fromIntegral p1) (fromIntegral p2)--parseProgramChange :: Maybe Message -> Parser Message-parseProgramChange mPreMsg = do- ch <- parseChannel mPreMsg isProgramChange 0x0C- p1 <- getWord8- return $! ProgramChange ch (fromIntegral p1)--parseChannelPressure :: Maybe Message -> Parser Message-parseChannelPressure mPreMsg = do- ch <- parseChannel mPreMsg isChannelPressure 0x0D- p1 <- getWord8- return $! ChannelPressure ch (fromIntegral p1)- -parsePitchWheel :: Maybe Message -> Parser Message-parsePitchWheel mPreMsg = do- ch <- parseChannel mPreMsg isPitchWheel 0x0E- p1 <- getWord8- p2 <- getWord8- return $! PitchWheel ch $ (shiftL (fromIntegral p2) 7) .|. (fromIntegral p1)--buildChannelMessage :: Message -> Builder-buildChannelMessage msg = case msg of- NoteOff _ p1 p2 -> mconcat- [f 0x08, putWord8 $ fromIntegral $ p1, putWord8 $ fromIntegral $ p2]- NoteOn _ p1 p2 -> mconcat- [f 0x09, putWord8 $ fromIntegral $ p1, putWord8 $ fromIntegral $ p2]- KeyPressure _ p1 p2 -> mconcat- [f 0x0A, putWord8 $ fromIntegral $ p1, putWord8 $ fromIntegral $ p2]- ControlChange _ p1 p2 -> mconcat- [f 0x0B, putWord8 $ fromIntegral $ p1, putWord8 $ fromIntegral $ p2]- ProgramChange _ p1 -> mconcat [f 0x0C, putWord8 $ fromIntegral $ p1]- ChannelPressure _ p1 -> mconcat [f 0x0D, putWord8 $ fromIntegral $ p1]- PitchWheel _ p1 -> mconcat [ f 0x0E- , putWord8 (fromIntegral $ p1 .&. 0x7F)- , putWord8 (fromIntegral $ shiftR p1 7)]- _ -> mempty- where- f :: Int -> Builder- f w8 = putWord8 $ fromIntegral $ (shiftL w8 4) .|. (channel msg)- -parseMetaMessage :: Parser Message-parseMetaMessage = do- _ <- word8 0xFF- choice [- parseSequenceNumber- , parseText- , parseCopyright- , parseTrackName- , parseInstrumentName- , parseLyrics- , parseMarker- , parseCuePoint- , parseChannelPrefix- , parseProgramName- , parseDeviceName- , parseTrackEnd- , parseTempoChange- , parseSMPTEOffset- , parseTimeSignature- , parseKeySignature- , parseReserved- ]--buildMetaMessage :: Message -> Builder-buildMetaMessage msg = putWord8 0xFF `mappend`- case msg of- SequenceNumber i -> mconcat- [putWord8 0x00, putVarLenBe 2, putWord16be $ fromIntegral $ i]- Text s -> mconcat- [putWord8 0x01, putVarLenBe (fromIntegral $ length s), putString s]- Copyright s -> mconcat- [putWord8 0x02, putVarLenBe (fromIntegral $ length s), putString s]- TrackName s -> mconcat- [putWord8 0x03, putVarLenBe (fromIntegral $ length s), putString s]- InstrumentName s -> mconcat- [putWord8 0x04, putVarLenBe (fromIntegral $ length s), putString s]- Lyrics s -> mconcat- [putWord8 0x05, putVarLenBe (fromIntegral $ length s), putString s]- Marker s -> mconcat- [putWord8 0x06, putVarLenBe (fromIntegral $ length s), putString s]- CuePoint s -> mconcat- [putWord8 0x07, putVarLenBe (fromIntegral $ length s), putString s]- ProgramName s -> mconcat- [putWord8 0x08, putVarLenBe (fromIntegral $ length s), putString s]- DeviceName s -> mconcat- [putWord8 0x09, putVarLenBe (fromIntegral $ length s), putString s]- ChannelPrefix i -> mconcat- [putWord8 0x20, putVarLenBe 1, putWord8 $ fromIntegral $ i]- TrackEnd -> putWord8 0x2F `mappend` putVarLenBe 0- TempoChange i -> mconcat- [putWord8 0x51, putVarLenBe 3, putWord24be $ fromIntegral $ i]- SMPTEOffset i1 i2 i3 i4 i5 -> mconcat [- putWord8 0x54- , putVarLenBe 5- , mconcat $ map (putWord8 . fromIntegral) [i1,i2,i3,i4,i5]]- TimeSignature i1 i2 i3 i4 -> mconcat [- putWord8 0x58- , putVarLenBe 4- , mconcat $ map (putWord8 . fromIntegral) [i1,i2,i3,i4]]- KeySignature i1 i2 -> mconcat [- putWord8 0x59- , putVarLenBe 2- , putInt8 $ fromIntegral $ i1- , putWord8 $ fromIntegral $ i2]- Reserved w bs -> mconcat [- putWord8 (fromIntegral w)- , putVarLenBe (fromIntegral $ L.length bs)- , fromLazyByteString bs]- _ -> mempty--parseSequenceNumber :: Parser Message-parseSequenceNumber = do- _ <- word8 0x00- _ <- varLenBe 2- n <- getWord16be- return $! SequenceNumber (fromIntegral n)--parseText :: Parser Message-parseText = do- _ <- word8 0x01- l <- getVarLenBe- s <- getString (fromIntegral l)- return $! Text s--parseCopyright :: Parser Message-parseCopyright = do- _ <- word8 0x02- l <- getVarLenBe- s <- getString (fromIntegral l)- return $! Copyright s--parseTrackName :: Parser Message-parseTrackName = do- _ <- word8 0x03- l <- getVarLenBe- s <- getString (fromIntegral l)- return $! TrackName s--parseInstrumentName :: Parser Message-parseInstrumentName = do- _ <- word8 0x04- l <- getVarLenBe- s <- getString (fromIntegral l)- return $! InstrumentName s--parseLyrics :: Parser Message-parseLyrics = do- _ <- word8 0x05- l <- getVarLenBe- s <- getString (fromIntegral l)- return $! Lyrics s--parseMarker :: Parser Message-parseMarker = do- _ <- word8 0x06- l <- getVarLenBe- s <- getString (fromIntegral l)- return $! Marker s--parseCuePoint :: Parser Message-parseCuePoint = do- _ <- word8 0x07- l <- getVarLenBe- s <- getString (fromIntegral l)- return $! CuePoint s--parseProgramName :: Parser Message-parseProgramName = do- _ <- word8 0x08- l <- getVarLenBe- s <- getString (fromIntegral l)- return $! ProgramName s--parseDeviceName :: Parser Message-parseDeviceName = do- _ <- word8 0x09- l <- getVarLenBe- s <- getString (fromIntegral l)- return $! DeviceName s--parseChannelPrefix :: Parser Message-parseChannelPrefix = do- _ <- word8 0x20- _ <- varLenBe 1- p <- getWord8- return $! ChannelPrefix (fromIntegral p)--parseTrackEnd :: Parser Message-parseTrackEnd = do- _ <- word8 0x2F- _ <- varLenBe 0- return $! TrackEnd--parseTempoChange :: Parser Message-parseTempoChange = do- _ <- word8 0x51- _ <- varLenBe 3- t <- getWord24be- return $! TempoChange (fromIntegral t)--parseSMPTEOffset :: Parser Message-parseSMPTEOffset = do- _ <- word8 0x54- _ <- varLenBe 5- bs <- getLazyByteString 5- let [n1,n2,n3,n4,n5] = map fromIntegral (L.unpack bs)- return $! SMPTEOffset n1 n2 n3 n4 n5--parseTimeSignature :: Parser Message-parseTimeSignature = do- _ <- word8 0x58- _ <- varLenBe 4- bs <- getLazyByteString 4- let [n1,n2,n3,n4] = map fromIntegral (L.unpack bs)- return $! TimeSignature n1 n2 n3 n4--parseKeySignature :: Parser Message-parseKeySignature = do- _ <- word8 0x59- _ <- varLenBe 2- n1 <- getInt8- n2 <- getWord8- return $! KeySignature (fromIntegral n1) (fromIntegral n2)--parseReserved :: Parser Message-parseReserved = do- t <- getWord8- l <- getVarLenBe- bs <- getLazyByteString (fromIntegral l)- return $! Reserved (fromIntegral t) bs--parseSysexMessage :: Parser Message-parseSysexMessage = do- w <- (word8 0xF0) <|> (word8 0xF7)- l <- getVarLenBe- d <- getLazyByteString (fromIntegral l)- return $! Sysex (fromIntegral w) d--buildSysexMessage :: Message -> Builder-buildSysexMessage (Sysex i bs) =- mconcat [ putWord8 $ fromIntegral $ i- , putVarLenBe $ fromIntegral $ L.length bs- , fromLazyByteString bs]-buildSysexMessage _ = mempty--two :: Applicative f => f a -> f (a,a)-two a = pure ((,)) <*> a <*> a+----------------------------------------------------------------------------- +-- | +-- Module : Codec.Midi +-- Copyright : George Giorgidze +-- License : BSD3 +-- +-- Maintainer : George Giorgidze <http://cs.nott.ac.uk/~ggg/> +-- Stability : Experimental +-- Portability : Portable +-- +-- Reading, writing and maniplating of standard MIDI files +-- +----------------------------------------------------------------------------- + +module Codec.Midi + ( + Midi (..) + , FileType (..) + , Track + , TimeDiv (..) + , Message (..) + + , Ticks + , Time + , Channel + , Key + , Velocity + , Pressure + , Preset + , Bank + , PitchWheel + , Tempo + + , isNoteOff + , isNoteOn + , isKeyPressure + , isControlChange + , isProgramChange + , isChannelPressure + , isPitchWheel + , isChannelMessage + , isMetaMessage + , isSysexMessage + , isTrackEnd + + , removeTrackEnds + , toSingleTrack + , merge + , fromAbsTime + , toAbsTime + , toRealTime + , fromRealTime + + , importFile + , exportFile + , parseMidi + , buildMidi + , parseTrack + , buildTrack + , parseMessage + , buildMessage + ) + where + +import qualified Data.ByteString.Lazy as L + +import Test.QuickCheck (Arbitrary, arbitrary, choose, oneof) + +import Codec.ByteString.Parser +import Codec.ByteString.Builder +import Codec.Internal.Arbitrary () + +import Data.Word +import Data.Bits +import Data.Maybe +import Data.List +import Data.Monoid (mempty, mconcat, mappend) +import Control.Applicative +import Control.Monad + +data Midi = Midi { + fileType :: FileType + , timeDiv :: TimeDiv + , tracks :: [Track Ticks] + } deriving (Eq, Show) + +instance Arbitrary Midi where + arbitrary = do + ft <- arbitrary + td <- arbitrary + if ft == SingleTrack + then do + trk <- arbitrary >>= return . fAux + return $! Midi ft td [trk] + else do + trks <- arbitrary >>= return . map fAux + return $! Midi ft td trks + where + fAux = (++ [(0,TrackEnd)]) . map (\(dt,m) -> (abs dt,m)) . removeTrackEnds + +data FileType = SingleTrack | MultiTrack | MultiPattern + deriving (Eq, Show) + +instance Arbitrary FileType where + arbitrary = oneof [return SingleTrack , return MultiTrack , return MultiPattern] + +type Track a = [(a,Message)] + +data TimeDiv = + TicksPerBeat Int | -- 1 -- (2^15 - 1) + TicksPerSecond Int Int -- 1 - 127 + -- FramesPerSecond TicksPerFrame + deriving (Show,Eq) + +instance Arbitrary TimeDiv where + arbitrary = oneof [ + choose (1,2 ^ (15 :: Int) - 1) >>= return . TicksPerBeat + , two (choose (1,127)) >>= \(w1,w2) -> return $! TicksPerSecond w1 w2] + +type Ticks = Int -- 0 - (2^28 - 1) +type Time = Double + +type Channel = Int -- 0 - 15 +type Key = Int -- 0 - 127 +type Velocity = Int -- 0 - 127 +type Pressure = Int -- 0 - 127 +type Preset = Int -- 0 - 127 +type Bank = Int +type PitchWheel = Int -- 0 - (2^14 - 1) +type Tempo = Int -- microseconds per beat 1 - (2^24 - 1) + +data Message = +-- Channel Messages + NoteOff { channel :: !Channel, key :: !Key, velocity :: !Velocity } | + NoteOn { channel :: !Channel, key :: !Key, velocity :: !Velocity } | + KeyPressure { channel :: !Channel, key :: !Key, pressure :: !Pressure} | + ControlChange { channel :: !Channel, controllerNumber :: !Int, controllerValue :: !Int } | + ProgramChange { channel :: !Channel, preset :: !Preset } | + ChannelPressure { channel :: !Channel, pressure :: !Pressure } | + PitchWheel { channel :: !Channel, pitchWheel :: !PitchWheel } | +-- Meta Messages + SequenceNumber !Int | -- 0 - (2^16 - 1) + Text !String | + Copyright !String | + TrackName !String | + InstrumentName !String | + Lyrics !String | + Marker !String | + CuePoint !String | + ChannelPrefix !Channel | + ProgramName !String | + DeviceName !String | + TrackEnd | + TempoChange !Tempo | + SMPTEOffset !Int !Int !Int !Int !Int | -- 0-23 0-59 0-59 0-30 0-99 + TimeSignature !Int !Int !Int !Int | -- 0-255 0-255 0-255 1-255 + KeySignature !Int !Int | -- -7 - 7 0 - 1 + Reserved !Int !L.ByteString | + -- System Exclusive Messages + Sysex !Int !L.ByteString -- 0xF0 or 0xF7 + deriving (Show,Eq) + +instance Arbitrary Message where + arbitrary = do + -- Channel Messages + c <- choose (0,15) + oneof [ + two (choose (0,127)) >>= \(w2,w3) -> return $! NoteOff c w2 w3 + , two (choose (0,127)) >>= \(w2,w3) -> return $! NoteOn c w2 w3 + , two (choose (0,127)) >>= \(w2,w3) -> return $! KeyPressure c w2 w3 + , two (choose (0,127)) >>= \(w2,w3) -> return $! ControlChange c w2 w3 + , choose (0,127) >>= \w2 -> return $! ProgramChange c w2 + , choose (0,127) >>= \w2 -> return $! ChannelPressure c w2 + , do p <- choose (0,2 ^ (14 :: Int) - 1) + return $! PitchWheel c p + -- Meta Messages + , choose (0,2 ^ (16 :: Int) - 1) >>= return . SequenceNumber + , arbitrary >>= return . Text + , arbitrary >>= return . Copyright + , arbitrary >>= return . TrackName + , arbitrary >>= return . InstrumentName + , arbitrary >>= return . Lyrics + , arbitrary >>= return . Marker + , arbitrary >>= return . CuePoint + , return $! ChannelPrefix c + , arbitrary >>= return . ProgramName + , arbitrary >>= return . DeviceName + , choose (0,2 ^ (14 :: Int) - 1) >>= return . TempoChange + , do w1 <- choose (0,23) + w2 <- choose (0,59) + w3 <- choose (0,59) + w4 <- choose (0,30) + w5 <- choose (0,99) + return $! SMPTEOffset w1 w2 w3 w4 w5 + , do w1 <- choose (0,255) + w2 <- choose (0,255) + w3 <- choose (0,255) + w4 <- choose (1,255) + return $! TimeSignature w1 w2 w3 w4 + , do w1 <- choose (-7,7) + w2 <- choose (0,1) + return $! KeySignature w1 w2 + , arbitrary >>= \bs -> return $! Reserved 0x60 bs + -- System Exclusive Messages + , do w <- oneof [return 0xF0, return 0xF7] + bs <- arbitrary + return $! Sysex w bs] + +isNoteOff :: Message -> Bool +isNoteOff (NoteOff {}) = True +isNoteOff _ = False + +isNoteOn :: Message -> Bool +isNoteOn (NoteOn {}) = True +isNoteOn _ = False + +isKeyPressure :: Message -> Bool +isKeyPressure (KeyPressure {}) = True +isKeyPressure _ = False + +isControlChange :: Message -> Bool +isControlChange (ControlChange {}) = True +isControlChange _ = False + +isProgramChange :: Message -> Bool +isProgramChange (ProgramChange {}) = True +isProgramChange _ = False + +isChannelPressure :: Message -> Bool +isChannelPressure (ChannelPressure {}) = True +isChannelPressure _ = False + +isPitchWheel :: Message -> Bool +isPitchWheel (PitchWheel {}) = True +isPitchWheel _ = False + +isChannelMessage :: Message -> Bool +isChannelMessage msg = (not $ isMetaMessage msg) && (not $ isSysexMessage msg) + +isSysexMessage :: Message -> Bool +isSysexMessage (Sysex _ _) = True +isSysexMessage _ = False + +isMetaMessage :: Message -> Bool +isMetaMessage msg = case msg of + SequenceNumber _ -> True + Text _ -> True + Copyright _ -> True + TrackName _ -> True + InstrumentName _ -> True + Lyrics _ -> True + Marker _ -> True + CuePoint _ -> True + ChannelPrefix _ -> True + ProgramName _ -> True + DeviceName _ -> True + TrackEnd -> True + TempoChange _ -> True + SMPTEOffset _ _ _ _ _ -> True + TimeSignature _ _ _ _ -> True + KeySignature _ _ -> True + Reserved _ _ -> True + _ -> False + +isTrackEnd :: Message -> Bool +isTrackEnd TrackEnd = True +isTrackEnd _ = False + +removeTrackEnds :: Track a -> Track a +removeTrackEnds [] = [] +removeTrackEnds trk = filter (not. isTrackEnd . snd) trk + +toSingleTrack :: Midi -> Midi +toSingleTrack m@(Midi SingleTrack _ _) = m +toSingleTrack (Midi MultiTrack td trks) = Midi SingleTrack td [trk'] + where trk' = foldl' merge [] trks +toSingleTrack (Midi MultiPattern td trks) = Midi SingleTrack td [trk'] + where trk' = (concat $ map removeTrackEnds trks) ++ [(0,TrackEnd)] + +merge :: (Num a, Ord a) => Track a -> Track a -> Track a +merge track1 track2 = (fromAbsTime $ f trk1' trk2') ++ [(0,TrackEnd)] + where + trk1' = toAbsTime $ removeTrackEnds track1 + trk2' = toAbsTime $ removeTrackEnds track2 + f trk [] = trk + f [] trk = trk + f ((dt1,m1) : trk1) ((dt2,m2) : trk2) = if dt1 <= dt2 + then (dt1,m1) : (f trk1 ((dt2,m2) : trk2)) + else (dt2,m2) : (f ((dt1,m1) : trk1) trk2) + +toAbsTime :: (Num a) => Track a -> Track a +toAbsTime trk = zip ts' ms + where + (ts,ms) = unzip trk + (_,ts') = mapAccumL (\acc t -> let t' = acc + t in (t',t')) 0 ts + +fromAbsTime :: (Num a) => Track a -> Track a +fromAbsTime trk = zip ts' ms + where + (ts,ms) = unzip trk + (_,ts') = mapAccumL (\acc t -> (t,t - acc)) 0 ts + +toRealTime :: TimeDiv -> Track Ticks -> Track Time +toRealTime (TicksPerBeat tpb) trk = trk' + where + (_,trk') = mapAccumL f (div 60000000 120) trk -- default tempo 120 beats per minute + formula dt tempo = + (fromIntegral dt / fromIntegral tpb) * (fromIntegral tempo) * (1.0E-6) + f :: Tempo -> (Ticks,Message) -> (Tempo, (Time,Message)) + f _ (dt, TempoChange tempo) = (tempo, (formula dt tempo, TempoChange tempo)) + f tempo (dt,msg) = (tempo, (formula dt tempo,msg)) +toRealTime (TicksPerSecond fps tpf) trk = map f trk + where + f (dt,msg) = (fromIntegral dt / (fromIntegral fps * fromIntegral tpf), msg) + +fromRealTime :: TimeDiv -> Track Time -> Track Ticks +fromRealTime (TicksPerBeat tpb) trk = trk' + where + (_,trk') = mapAccumL f (div 60000000 120) trk -- default tempo 120 beats per minute + formula dt tempo = round $ + (dt * fromIntegral tpb) / (fromIntegral tempo * 1.0E-6) + f :: Tempo -> (Time,Message) -> (Tempo, (Ticks,Message)) + f _ (dt, TempoChange tempo) = (tempo, (formula dt tempo, TempoChange tempo)) + f tempo (dt,msg) = (tempo, (formula dt tempo,msg)) +fromRealTime (TicksPerSecond fps tpf) trk = map f trk + where + f (dt,msg) = (round $ dt * fromIntegral fps * fromIntegral tpf, msg) + +-- MIDI import +importFile :: FilePath -> IO (Either String Midi) +importFile f = do + bs <- L.readFile f + return $! runParser parseMidi bs + +exportFile :: FilePath -> Midi -> IO () +exportFile f m = do + let bs = toLazyByteString $ buildMidi m + L.writeFile f bs + +-- All numeric values are stored in big-endian format + +parseMidi :: Parser Midi +parseMidi = do + _ <- string "MThd" + _ <- word32be 6 + formatType' <- getWord16be + trackNumber' <- getWord16be + timeDivision' <- getWord16be + let timeDivision = if testBit timeDivision' 15 + then TicksPerSecond + (fromIntegral $ (flip shiftR) 9 $ shiftL timeDivision' 1) + (fromIntegral $ (flip shiftR) 8 $ shiftL timeDivision' 8) + else TicksPerBeat (fromIntegral timeDivision') + case (formatType',trackNumber') of + (0,1) -> do + track' <- parseTrack + return $! Midi SingleTrack timeDivision [track'] + (1,n) -> do + tracks' <- sequence $ replicate (fromIntegral n) parseTrack + return $! Midi MultiTrack timeDivision tracks' + (2,n) -> do + tracks' <- sequence $ replicate (fromIntegral n) parseTrack + return $! Midi MultiPattern timeDivision tracks' + _ -> fail "Invalid Midi file format" + +buildMidi :: Midi -> Builder +buildMidi m = mconcat [ + putString "MThd" + , putWord32be 6 + , case fileType m of + SingleTrack -> putWord16be 0 + MultiTrack -> putWord16be 1 + MultiPattern -> putWord16be 2 + , putWord16be (fromIntegral $ length $ tracks m) + , case timeDiv m of + TicksPerBeat i -> putWord16be (fromIntegral i) + TicksPerSecond i1 i2 -> mconcat [ + putWord8 (setBit (fromIntegral i1) 7) + , putWord8 (fromIntegral i2)] + , mconcat (map buildTrack $ tracks m)] + +parseTrack :: Parser (Track Ticks) +parseTrack = do + _ <- string "MTrk" + _ <- getWord32be -- trackSize + track' <- parseMessages Nothing + return track' + +buildTrack :: Track Ticks -> Builder +buildTrack trk = mconcat [ + putString "MTrk" + , putWord32be $ fromIntegral $ L.length bs + , fromLazyByteString bs] + where + f (dt,msg) = (putVarLenBe $ fromIntegral dt) `append` buildMessage msg + bs = toLazyByteString $ mconcat (map f trk) + +parseMessages :: Maybe Message -> Parser (Track Ticks) +parseMessages mPreMsg = do + dt <- getVarLenBe >>= return . fromIntegral + msg <- parseMessage mPreMsg + if (isTrackEnd msg) + then return [(dt,msg)] + else do + let mMsg = if isChannelMessage msg then (Just msg) else mPreMsg + msgs <- parseMessages mMsg + return $! (dt,msg) : msgs + +parseMessage :: Maybe Message -> Parser Message +parseMessage mPreMsg = choice [ + parseChannelMessage mPreMsg + , parseMetaMessage + , parseSysexMessage] + +buildMessage :: Message -> Builder +buildMessage msg | isChannelMessage msg = buildChannelMessage msg +buildMessage msg | isMetaMessage msg = buildMetaMessage msg +buildMessage msg | isSysexMessage msg = buildSysexMessage msg +buildMessage _ = mempty + +parseChannelMessage :: Maybe Message -> Parser Message +parseChannelMessage mPreMsg = choice $ map (\f -> f mPreMsg) [ + parseNoteOff + , parseNoteOn + , parseKeyPressure + , parseControlChange + , parseProgramChange + , parseChannelPressure + , parsePitchWheel + ] + +parseChannel :: Maybe Message -> (Message -> Bool) -> Word8 -> Parser Channel +parseChannel mPreMsg isNeededMsg msgCode = p1 <|> p2 + where + p1 = do + _ <- lookAhead (satisfy ( < 0x80)) + guard $ (isJust mPreMsg) && (isNeededMsg $ fromJust mPreMsg) + return $! channel (fromJust mPreMsg) + p2 = do + w8 <- getWord8 + guard (msgCode == shiftR w8 4) + return $! fromIntegral $ w8 .&. (0x0F :: Word8) + +parseNoteOff :: Maybe Message -> Parser Message +parseNoteOff mPreMsg = do + ch <- parseChannel mPreMsg isNoteOff 0x08 + p1 <- getWord8 + p2 <- getWord8 + return $! NoteOff ch (fromIntegral p1) (fromIntegral p2) + +parseNoteOn :: Maybe Message -> Parser Message +parseNoteOn mPreMsg = do + ch <- parseChannel mPreMsg isNoteOn 0x09 + p1 <- getWord8 + p2 <- getWord8 + return $! NoteOn ch (fromIntegral p1) (fromIntegral p2) + +parseKeyPressure :: Maybe Message -> Parser Message +parseKeyPressure mPreMsg = do + ch <- parseChannel mPreMsg isKeyPressure 0x0A + p1 <- getWord8 + p2 <- getWord8 + return $! KeyPressure ch (fromIntegral p1) (fromIntegral p2) + +parseControlChange :: Maybe Message -> Parser Message +parseControlChange mPreMsg = do + ch <- parseChannel mPreMsg isControlChange 0x0B + p1 <- getWord8 + p2 <- getWord8 + return $! ControlChange ch (fromIntegral p1) (fromIntegral p2) + +parseProgramChange :: Maybe Message -> Parser Message +parseProgramChange mPreMsg = do + ch <- parseChannel mPreMsg isProgramChange 0x0C + p1 <- getWord8 + return $! ProgramChange ch (fromIntegral p1) + +parseChannelPressure :: Maybe Message -> Parser Message +parseChannelPressure mPreMsg = do + ch <- parseChannel mPreMsg isChannelPressure 0x0D + p1 <- getWord8 + return $! ChannelPressure ch (fromIntegral p1) + +parsePitchWheel :: Maybe Message -> Parser Message +parsePitchWheel mPreMsg = do + ch <- parseChannel mPreMsg isPitchWheel 0x0E + p1 <- getWord8 + p2 <- getWord8 + return $! PitchWheel ch $ (shiftL (fromIntegral p2) 7) .|. (fromIntegral p1) + +buildChannelMessage :: Message -> Builder +buildChannelMessage msg = case msg of + NoteOff _ p1 p2 -> mconcat + [f 0x08, putWord8 $ fromIntegral $ p1, putWord8 $ fromIntegral $ p2] + NoteOn _ p1 p2 -> mconcat + [f 0x09, putWord8 $ fromIntegral $ p1, putWord8 $ fromIntegral $ p2] + KeyPressure _ p1 p2 -> mconcat + [f 0x0A, putWord8 $ fromIntegral $ p1, putWord8 $ fromIntegral $ p2] + ControlChange _ p1 p2 -> mconcat + [f 0x0B, putWord8 $ fromIntegral $ p1, putWord8 $ fromIntegral $ p2] + ProgramChange _ p1 -> mconcat [f 0x0C, putWord8 $ fromIntegral $ p1] + ChannelPressure _ p1 -> mconcat [f 0x0D, putWord8 $ fromIntegral $ p1] + PitchWheel _ p1 -> mconcat [ f 0x0E + , putWord8 (fromIntegral $ p1 .&. 0x7F) + , putWord8 (fromIntegral $ shiftR p1 7)] + _ -> mempty + where + f :: Int -> Builder + f w8 = putWord8 $ fromIntegral $ (shiftL w8 4) .|. (channel msg) + +parseMetaMessage :: Parser Message +parseMetaMessage = do + _ <- word8 0xFF + choice [ + parseSequenceNumber + , parseText + , parseCopyright + , parseTrackName + , parseInstrumentName + , parseLyrics + , parseMarker + , parseCuePoint + , parseChannelPrefix + , parseProgramName + , parseDeviceName + , parseTrackEnd + , parseTempoChange + , parseSMPTEOffset + , parseTimeSignature + , parseKeySignature + , parseReserved + ] + +buildMetaMessage :: Message -> Builder +buildMetaMessage msg = putWord8 0xFF `mappend` + case msg of + SequenceNumber i -> mconcat + [putWord8 0x00, putVarLenBe 2, putWord16be $ fromIntegral $ i] + Text s -> mconcat + [putWord8 0x01, putVarLenBe (fromIntegral $ length s), putString s] + Copyright s -> mconcat + [putWord8 0x02, putVarLenBe (fromIntegral $ length s), putString s] + TrackName s -> mconcat + [putWord8 0x03, putVarLenBe (fromIntegral $ length s), putString s] + InstrumentName s -> mconcat + [putWord8 0x04, putVarLenBe (fromIntegral $ length s), putString s] + Lyrics s -> mconcat + [putWord8 0x05, putVarLenBe (fromIntegral $ length s), putString s] + Marker s -> mconcat + [putWord8 0x06, putVarLenBe (fromIntegral $ length s), putString s] + CuePoint s -> mconcat + [putWord8 0x07, putVarLenBe (fromIntegral $ length s), putString s] + ProgramName s -> mconcat + [putWord8 0x08, putVarLenBe (fromIntegral $ length s), putString s] + DeviceName s -> mconcat + [putWord8 0x09, putVarLenBe (fromIntegral $ length s), putString s] + ChannelPrefix i -> mconcat + [putWord8 0x20, putVarLenBe 1, putWord8 $ fromIntegral $ i] + TrackEnd -> putWord8 0x2F `mappend` putVarLenBe 0 + TempoChange i -> mconcat + [putWord8 0x51, putVarLenBe 3, putWord24be $ fromIntegral $ i] + SMPTEOffset i1 i2 i3 i4 i5 -> mconcat [ + putWord8 0x54 + , putVarLenBe 5 + , mconcat $ map (putWord8 . fromIntegral) [i1,i2,i3,i4,i5]] + TimeSignature i1 i2 i3 i4 -> mconcat [ + putWord8 0x58 + , putVarLenBe 4 + , mconcat $ map (putWord8 . fromIntegral) [i1,i2,i3,i4]] + KeySignature i1 i2 -> mconcat [ + putWord8 0x59 + , putVarLenBe 2 + , putInt8 $ fromIntegral $ i1 + , putWord8 $ fromIntegral $ i2] + Reserved w bs -> mconcat [ + putWord8 (fromIntegral w) + , putVarLenBe (fromIntegral $ L.length bs) + , fromLazyByteString bs] + _ -> mempty + +parseSequenceNumber :: Parser Message +parseSequenceNumber = do + _ <- word8 0x00 + _ <- varLenBe 2 + n <- getWord16be + return $! SequenceNumber (fromIntegral n) + +parseText :: Parser Message +parseText = do + _ <- word8 0x01 + l <- getVarLenBe + s <- getString (fromIntegral l) + return $! Text s + +parseCopyright :: Parser Message +parseCopyright = do + _ <- word8 0x02 + l <- getVarLenBe + s <- getString (fromIntegral l) + return $! Copyright s + +parseTrackName :: Parser Message +parseTrackName = do + _ <- word8 0x03 + l <- getVarLenBe + s <- getString (fromIntegral l) + return $! TrackName s + +parseInstrumentName :: Parser Message +parseInstrumentName = do + _ <- word8 0x04 + l <- getVarLenBe + s <- getString (fromIntegral l) + return $! InstrumentName s + +parseLyrics :: Parser Message +parseLyrics = do + _ <- word8 0x05 + l <- getVarLenBe + s <- getString (fromIntegral l) + return $! Lyrics s + +parseMarker :: Parser Message +parseMarker = do + _ <- word8 0x06 + l <- getVarLenBe + s <- getString (fromIntegral l) + return $! Marker s + +parseCuePoint :: Parser Message +parseCuePoint = do + _ <- word8 0x07 + l <- getVarLenBe + s <- getString (fromIntegral l) + return $! CuePoint s + +parseProgramName :: Parser Message +parseProgramName = do + _ <- word8 0x08 + l <- getVarLenBe + s <- getString (fromIntegral l) + return $! ProgramName s + +parseDeviceName :: Parser Message +parseDeviceName = do + _ <- word8 0x09 + l <- getVarLenBe + s <- getString (fromIntegral l) + return $! DeviceName s + +parseChannelPrefix :: Parser Message +parseChannelPrefix = do + _ <- word8 0x20 + _ <- varLenBe 1 + p <- getWord8 + return $! ChannelPrefix (fromIntegral p) + +parseTrackEnd :: Parser Message +parseTrackEnd = do + _ <- word8 0x2F + _ <- varLenBe 0 + return $! TrackEnd + +parseTempoChange :: Parser Message +parseTempoChange = do + _ <- word8 0x51 + _ <- varLenBe 3 + t <- getWord24be + return $! TempoChange (fromIntegral t) + +parseSMPTEOffset :: Parser Message +parseSMPTEOffset = do + _ <- word8 0x54 + _ <- varLenBe 5 + bs <- getLazyByteString 5 + let [n1,n2,n3,n4,n5] = map fromIntegral (L.unpack bs) + return $! SMPTEOffset n1 n2 n3 n4 n5 + +parseTimeSignature :: Parser Message +parseTimeSignature = do + _ <- word8 0x58 + _ <- varLenBe 4 + bs <- getLazyByteString 4 + let [n1,n2,n3,n4] = map fromIntegral (L.unpack bs) + return $! TimeSignature n1 n2 n3 n4 + +parseKeySignature :: Parser Message +parseKeySignature = do + _ <- word8 0x59 + _ <- varLenBe 2 + n1 <- getInt8 + n2 <- getWord8 + return $! KeySignature (fromIntegral n1) (fromIntegral n2) + +parseReserved :: Parser Message +parseReserved = do + t <- getWord8 + l <- getVarLenBe + bs <- getLazyByteString (fromIntegral l) + return $! Reserved (fromIntegral t) bs + +parseSysexMessage :: Parser Message +parseSysexMessage = do + w <- (word8 0xF0) <|> (word8 0xF7) + l <- getVarLenBe + d <- getLazyByteString (fromIntegral l) + return $! Sysex (fromIntegral w) d + +buildSysexMessage :: Message -> Builder +buildSysexMessage (Sysex i bs) = + mconcat [ putWord8 $ fromIntegral $ i + , putVarLenBe $ fromIntegral $ L.length bs + , fromLazyByteString bs] +buildSysexMessage _ = mempty + +two :: Applicative f => f a -> f (a,a) +two a = pure ((,)) <*> a <*> a
src/Codec/SoundFont.hs view
@@ -1,920 +1,920 @@--------------------------------------------------------------------------------- |--- Module : Codec.SoundFont--- Copyright : George Giorgidze--- License : BSD3--- --- Maintainer : George Giorgidze <http://cs.nott.ac.uk/~ggg/>--- Stability : Experimental--- Portability : Portable------ Module for reading and writting of SoundFont instrument description files.-----------------------------------------------------------------------------------module Codec.SoundFont (- SoundFont (..)- , Info (..)- , Sdta (..)- , Pdta (..)- , Phdr (..)- , Bag (..)- , Mod (..)- , Generator (..)- , isSampleIndex- , isInstIndex- , Inst (..)- , Shdr (..)- , importFile- , exportFile- , parseSoundFont- , buildSoundFont- , parseInfos- , buildInfos- , parseSdta- , buildSdta- , parsePdta- , buildPdta- ) where--import Codec.ByteString.Parser-import Codec.ByteString.Builder-import Codec.Internal.Arbitrary-import qualified Data.Audio as Audio--import Data.Word-import Data.Int-import qualified Data.ByteString.Lazy as L-import Data.ByteString.Internal (w2c)-import Data.Array.IArray-import Data.List--import Test.QuickCheck-import Data.Monoid (mconcat)-import Control.Applicative-import Control.Monad--data SoundFont = SoundFont {- infos :: Array Word Info -- Suplemental information- , sdta :: Sdta -- The Sample Binary Data- , pdta :: Pdta - -- articulation :: Articulation -- The Preset, Instrument, and Sample Header data- } deriving (Eq, Show)--instance Arbitrary SoundFont where- arbitrary = do- f1 <- arbitrary; f2 <- arbitrary; f3 <- arbitrary;- return $! SoundFont f1 f2 f3---- instance Show SoundFont where--- show sf = (show $ length $ elems $ sampleData sf) ++ "\n" ++ (show $ length $ show $ articulation sf) ++ "\n"---- type SamplePointIndex = Word32--- type SampleData = UArray SamplePointIndex Audio.Sample--data Info = - Version Word Word -- Refers to file version of SounfFont RIFF file- | TargetSoundEngine String -- Refers to target Sound Engine- | BankName String -- Refers to SoundFont Bank Name- | RomName String -- Refers to Sound ROM Name- | RomVersion Word Word -- Refers to Sound ROM Version- | CreationDate String -- Refers to Date of Creation of the Bank- | Authors String -- Sound Designers and Engineers of the Bank- | IntendedProduct String -- Product fot which the bank was intended- | CopyrightMessage String -- Contains any copyright message- | Comments String -- Contains any comemnts on the Bank- | UsedTools String -- The SoundFont tools used to create and alter the bank- | ReservedInfo String Word L.ByteString- deriving (Eq,Show)--instance Arbitrary Info where- arbitrary = oneof [- do w1 <- choose (minBound :: Word16, maxBound)- w2 <- choose (minBound :: Word16, maxBound)- return $ Version (fromIntegral w1) (fromIntegral w2);- , do l <- choose (0,255); s <- genStringNul l; return $ TargetSoundEngine s- , do l <- choose (0,255); s <- genStringNul l; return $ BankName s- , do l <- choose (0,255); s <- genStringNul l; return $ RomName s- , do w1 <- choose (minBound :: Word16, maxBound)- w2 <- choose (minBound :: Word16, maxBound)- return $ RomVersion (fromIntegral w1) (fromIntegral w2)- , do l <- choose (0,255); s <- genStringNul l; return $ CreationDate s- , do l <- choose (0,255); s <- genStringNul l; return $ Authors s- , do l <- choose (0,255); s <- genStringNul l; return $ IntendedProduct s- , do l <- choose (0,255); s <- genStringNul l; return $ CopyrightMessage s - , do l <- choose (0,255); s <- genStringNul l; return $ Comments s- , do l <- choose (0,255); s <- genStringNul l; return $ UsedTools s- , do l <- choose (0,255); s <- vector (fromIntegral l);- return $ ReservedInfo "RSRV" l (L.pack s)]- where- genStringNul :: Int -> Gen String - genStringNul l = sequence $ replicate l $ fmap w2c $ choose (1,255)--data Sdta = Sdta {- smpl :: Audio.SampleData Int16- , sm24 :: Maybe (Audio.SampleData Int8)- } deriving (Eq, Show)--instance Arbitrary Sdta where- arbitrary = do- sn <- choose (1,1024)- smpl1 <- arrayGen sn- oneof [- return $! Sdta smpl1 Nothing- , do sm24' <- arrayGen sn- return $! Sdta smpl1 (Just sm24')- ]--data Pdta = Pdta {- phdrs :: Array Word Phdr- , pbags :: Array Word Bag- , pmods :: Array Word Mod- , pgens :: Array Word Generator- , insts :: Array Word Inst- , ibags :: Array Word Bag- , imods :: Array Word Mod- , igens :: Array Word Generator- , shdrs :: Array Word Shdr- } deriving (Eq, Show)- -instance Arbitrary Pdta where- arbitrary = do- f1 <- arbitrary; f2 <- arbitrary; f3 <- arbitrary; f4 <- arbitrary;- f5 <- arbitrary; f6 <- arbitrary; f7 <- arbitrary; f8 <- arbitrary;- f9 <- arbitrary;- return $! Pdta f1 f2 f3 f4 f5 f6 f7 f8 f9--data Phdr = Phdr {- presetName :: String- , preset :: Word- , bank :: Word- , presetBagNdx :: Word- , library :: Word- , genre :: Word- , morphology :: Word- } deriving (Eq, Show)--instance Arbitrary Phdr where- arbitrary = do- n <- choose (0,20)- presetName' <- stringNulGen n- preset' <- choose (minBound :: Word16, maxBound)- bank' <- choose (minBound :: Word16, maxBound)- presetBagNdx' <- choose (minBound :: Word16, maxBound)- library' <- choose (minBound :: Word32, maxBound)- genre' <- choose (minBound :: Word32, maxBound)- morphology' <- choose (minBound :: Word32, maxBound)- return $ Phdr {- presetName = presetName'- , preset = fromIntegral $ preset'- , bank = fromIntegral $ bank'- , presetBagNdx = fromIntegral $ presetBagNdx'- , library = fromIntegral $ library'- , genre = fromIntegral $ genre'- , morphology = fromIntegral $ morphology'- }--data Bag = Bag {- genNdx :: Word- , modNdx :: Word- } deriving (Eq, Show)- -instance Arbitrary Bag where- arbitrary = do- genNdx' <- choose (minBound :: Word16, maxBound)- modNdx' <- choose (minBound :: Word16, maxBound) - return $! Bag {- genNdx = fromIntegral genNdx'- , modNdx = fromIntegral modNdx'}--data Mod = Mod {- srcOper :: Word- , destOper :: Word- , amount :: Int- , amtSrcOper :: Word- , transOper :: Word- } deriving (Eq, Show)- -instance Arbitrary Mod where- arbitrary = do- srcOper' <- choose (minBound :: Word16, maxBound)- destOper' <- choose (minBound :: Word16, maxBound) - amount' <- choose (minBound :: Int16, maxBound)- amtSrcOper' <- choose (minBound :: Word16, maxBound) - transOper' <- choose (minBound :: Word16, maxBound) - return $! Mod {- srcOper = fromIntegral srcOper'- , destOper = fromIntegral destOper'- , amount = fromIntegral amount'- , amtSrcOper = fromIntegral amtSrcOper'- , transOper = fromIntegral transOper'- }--data Generator =- -- Oscillator- StartAddressOffset Int | -- 0 sample start fine offset- EndAddressOffset Int | -- 1 sample end fine offset- LoopStartAddressOffset Int | -- 2 sample start loop fine offset- LoopEndAddressOffset Int | -- 3 sample end loop file offset- StartAddressCoarseOffset Int | -- 4 sample start coarse offset- ModLfoToPitch Int | -- 5 main fm: modLfo-> pitch- VibLfoToPitch Int | -- 6 aux fm: vibLfo-> pitch- ModEnvToPitch Int | -- 7 pitch env: modEnv(aux)-> pitch-- -- Filter- InitFc Int | -- 8 initial filter cutoff- InitQ Int | -- 9 filter Q- ModLfoToFc Int | -- 10 filter modulation: lfo1 -> filter cutoff- ModEnvToFc Int | -- 11 filter env: modEnv(aux)-> filter cutoff-- - EndAddressCoarseOffset Int | -- 12 initial volume- - -- Amplifier- ModLfoToVol Int | -- 13 tremolo: lfo1-> volume- -- 14 unused-- -- Effects- Chorus Int | -- 15 chorus- Reverb Int | -- 16 reverb- Pan Int | -- 17 pan- -- 18 unused- -- 19 unused- -- 20 unused-- -- Modulation LFO- DelayModLfo Int | -- 21 delay - FreqModLfo Int | -- 22 frequency-- -- Vibrato LFO- DelayVibLfo Int | -- 23 delay - FreqVibLfo Int | -- 24 frequency-- -- Modulation Envelope- DelayModEnv Int | -- 25 delay - AttackModEnv Int | -- 26 attack- HoldModEnv Int | -- 27 hold- DecayModEnv Int | -- 28 decay- SustainModEnv Int | -- 29 sustain- ReleaseModEnv Int | -- 30 release- KeyToModEnvHold Int | -- 31 key scaling coefficient- KeyToModEnvDecay Int | -- 32 key scaling coefficient-- -- Volume Envelope (ampl/vol)- DelayVolEnv Int | -- 33 delay - AttackVolEnv Int | -- 34 attack- HoldVolEnv Int | -- 35 hold- DecayVolEnv Int | -- 36 decay- SustainVolEnv Int | -- 37 sustain- ReleaseVolEnv Int | -- 38 release- KeyToVolEnvHold Int | -- 39 key scaling coefficient- KeyToVolEnvDecay Int | -- 40 key scaling coefficient-- -- Preset- InstIndex Word | -- 41- -- 42- KeyRange Word Word | -- 43- VelRange Word Word | -- 44- LoopStartAddressCoarseOffset Int | -- 45 - Key Word | -- 46- Vel Word | -- 47- InitAtten Int | -- 48- -- 49 unused- LoopEndAddressCoarseOffset Int | -- 50-- CoarseTune Int | -- 51- FineTune Int | -- 52- SampleIndex Word | -- 53- SampleMode Audio.SampleMode | -- 54- -- 55 unused- ScaleTuning Int | -- 56- ExclusiveClass Int | -- 57- - RootKey Word | -- 58- -- 59 unused- ReservedGen Int Int -- 60 single constructor for all unused generators- deriving (Eq, Show)--instance Arbitrary Generator where- arbitrary = do- i <- choose (minBound :: Int16, maxBound) >>= return . fromIntegral- w <- choose (minBound :: Word16, maxBound) >>= return . fromIntegral- i' <- choose (60 :: Int16, maxBound) >>= return . fromIntegral- r1 <- choose (0,127)- r2 <- choose (0,127)- smplMode' <- arbitrary- oneof $ map return [- StartAddressOffset i- , EndAddressOffset i - , LoopStartAddressOffset i- , LoopEndAddressOffset i- , StartAddressCoarseOffset i- , ModLfoToPitch i- , VibLfoToPitch i- , ModEnvToPitch i- - , InitFc i- , InitQ i- , ModLfoToFc i- , ModEnvToFc i- - , EndAddressCoarseOffset i- , ModLfoToVol i- - , Chorus i- , Reverb i- , Pan i- , DelayModLfo i- , FreqModLfo i- - , DelayVibLfo i- , FreqVibLfo i- - , DelayModEnv i- , AttackModEnv i- , HoldModEnv i- , DecayModEnv i- , SustainModEnv i- , ReleaseModEnv i- , KeyToModEnvHold i- , KeyToModEnvDecay i- - , DelayVolEnv i- , AttackVolEnv i- , HoldVolEnv i- , DecayVolEnv i- , SustainVolEnv i- , ReleaseVolEnv i- , KeyToVolEnvHold i- , KeyToVolEnvDecay i- - , InstIndex w- - , KeyRange r1 r2- , VelRange r2 r2- , LoopStartAddressCoarseOffset i- , Key w- , Vel w- , InitAtten i- - , LoopEndAddressCoarseOffset i- - , CoarseTune i- , FineTune i- , SampleIndex w- , SampleMode smplMode'- - , ScaleTuning i- , ExclusiveClass i- - , RootKey w- - , ReservedGen i' i]--isSampleIndex :: Generator -> Bool-isSampleIndex g = case g of- SampleIndex _ -> True- _ -> False --isInstIndex :: Generator -> Bool-isInstIndex g = case g of- InstIndex _ -> True- _ -> False--data Inst = Inst {- instName :: String- , instBagNdx :: Word- } deriving (Eq, Show)--instance Arbitrary Inst where- arbitrary = do- n <- choose (0,20)- instName' <- stringNulGen n- instBagNdx' <- choose (maxBound :: Word16, minBound)- return $! Inst {- instName = instName'- , instBagNdx = fromIntegral $ instBagNdx'}--data Shdr = Shdr {- sampleName :: String- , start :: Word- , end :: Word- , startLoop :: Word- , endLoop :: Word- , sampleRate :: Word- , originalPitch :: Word- , pitchCorrection :: Int- , sampleLink :: Word- , sampleType :: Word- } deriving (Eq, Show)--instance Arbitrary Shdr where- arbitrary = do- n <- choose (0,20)- sampleName' <- stringNulGen n- start' <- choose (minBound :: Word32, maxBound)- end' <- choose (minBound :: Word32, maxBound)- startLoop' <- choose (minBound :: Word32, maxBound)- endLoop' <- choose (minBound :: Word32, maxBound)- sampleRate' <- choose (minBound :: Word32, maxBound)- originalPitch' <- choose (minBound :: Word8, maxBound)- pitchCorrection' <- choose (minBound :: Int8, maxBound)- sampleLink' <- choose (minBound :: Word16, maxBound)- sampleType' <- choose (minBound :: Word16, maxBound) - return $ Shdr {- sampleName = sampleName'- , start = fromIntegral start'- , end = fromIntegral end'- , startLoop = fromIntegral startLoop'- , endLoop = fromIntegral endLoop'- , sampleRate = fromIntegral sampleRate'- , originalPitch = fromIntegral originalPitch'- , pitchCorrection = fromIntegral pitchCorrection'- , sampleLink = fromIntegral sampleLink'- , sampleType = fromIntegral sampleType'- }------ SoundFont import--importFile :: FilePath -> IO (Either String SoundFont)-importFile n = do- bs <- L.readFile n- return $! runParser parseSoundFont bs--exportFile :: FilePath -> SoundFont -> IO ()-exportFile f sf = do- let bs = toLazyByteString $ buildSoundFont sf- L.writeFile f bs --parseSoundFont :: Parser SoundFont-parseSoundFont = do- _ <- string "RIFF"- _ <- getWord32le -- chunkSize- _ <- string "sfbk"- infos' <- parseInfos- sdta' <- parseSdta- pdta' <- parsePdta- return $! SoundFont {- infos = infos'- , sdta = sdta'- , pdta = pdta'- }--buildSoundFont :: SoundFont -> Builder-buildSoundFont sf = mconcat [- putString "RIFF"- , putWord32le $ fromIntegral chunkSize- , fromLazyByteString bs]- where- chunkSize = L.length bs- bs = toLazyByteString $ mconcat [- putString "sfbk"- , buildInfos (infos sf)- , buildSdta (sdta sf)- , buildPdta (pdta sf)]----buildSoundFont :: SoundFont -> Builder---buildSoundFont sf = mconcat [--parseInfos :: Parser (Array Word Info)-parseInfos = do- _ <- string "LIST"- _ <- getWord32le -- chunkSize- _ <- string "INFO"- infos' <- many p- return $! listArray (0, genericLength infos' - 1) infos'- where- p = choice [- do n <- getString 4; _ <- word32le 4; w1 <- getWord16le; w2 <- getWord16le;- case n of- "ifil" -> return $! Version (fromIntegral w1) (fromIntegral w2)- "iver" -> return $! RomVersion (fromIntegral w1) (fromIntegral w2)- _ -> fail []- , do n <- getString 4; l <- expect (<= 256) getWord32le; s <- getStringNul; - skip (fromIntegral l - genericLength s - 1);- case n of- "isng" -> return $! TargetSoundEngine s- "INAM" -> return $! BankName s- "irom" -> return $! RomName s- "ICRD" -> return $! CreationDate s- "IENG" -> return $! Authors s- "IPRD" -> return $! IntendedProduct s- "ICOP" -> return $! CopyrightMessage s- "ICMT" -> return $! Comments s- "ISFT" -> return $! UsedTools s- _ -> fail []- , do n <- expect ( /= "LIST") (getString 4)- l <- getWord32le - bs <- getLazyByteString (fromIntegral l)- return $! ReservedInfo n (fromIntegral l) bs]--buildInfos :: (Array Word Info) -> Builder-buildInfos infos' = mconcat [- putString "LIST"- , putWord32le $ (fromIntegral $ L.length bs) + 4- , putString "INFO"- , fromLazyByteString bs]- where- bs = toLazyByteString $ mconcat $ map buildInfo $ elems infos'--{--Specification says that 'ifil' 'isgn' and 'INAM' fields are mandatory-but I am not checking it during parsing--}---buildInfo :: Info -> Builder-buildInfo (Version w1 w2) = mconcat- [putString "ifil", putWord32le 4,- putWord16le (fromIntegral w1), putWord16le (fromIntegral w2)]-buildInfo (RomVersion w1 w2) = mconcat- [putString "iver", putWord32le 4,- putWord16le (fromIntegral w1), putWord16le (fromIntegral w2)]-buildInfo (TargetSoundEngine s) = mconcat [putString "isng", buildInfoString s]-buildInfo (BankName s) = mconcat [putString "INAM", buildInfoString s]-buildInfo (RomName s) = mconcat [putString "irom", buildInfoString s]-buildInfo (CreationDate s) = mconcat [putString "ICRD", buildInfoString s]-buildInfo (Authors s) = mconcat [putString "IENG", buildInfoString s]-buildInfo (IntendedProduct s) = mconcat [putString "IPRD", buildInfoString s]-buildInfo (CopyrightMessage s) = mconcat [putString "ICOP", buildInfoString s]-buildInfo (Comments s) = mconcat [putString "ICMT", buildInfoString s]-buildInfo (UsedTools s) = mconcat [putString "ISFT", buildInfoString s]-buildInfo (ReservedInfo n l bs) = mconcat- [putString n, putWord32le (fromIntegral l), fromLazyByteString bs]- -buildInfoString :: String -> Builder-buildInfoString s = if (mod l 2 == 0)- then mconcat [putWord32le (l + 2), putString s, putWord8 0, putWord8 0]- else mconcat [putWord32le (l + 1), putString s, putWord8 0]- where- l = fromIntegral $ length s- -parseSdta :: Parser Sdta-parseSdta = do- _ <- string "LIST"- sdtaSize <- getWord32le >>= return .fromIntegral- _ <- string "sdta"- _ <- string "smpl"- smplSize <- getWord32le >>= return .fromIntegral- when (odd smplSize) $ fail "'smplSize' must not be odd number"- let sn = div smplSize 2- smpl' <- Audio.parseSampleData sn getInt16le- choice [- do guard (smplSize == (sdtaSize - 12))- return $! Sdta {smpl = smpl', sm24 = Nothing}- , do _ <- string "sm24"- let sm24Size = if odd sn then sn + 1 else sn - _ <- word32le (fromIntegral sm24Size)- sm24' <- Audio.parseSampleData sn getInt8- skip (fromIntegral $ sm24Size - sn)- return $! Sdta{ smpl = smpl', sm24 = Just sm24'}- ]--buildSdta :: Sdta -> Builder-buildSdta (Sdta smpl1 Nothing) = mconcat [- putString "LIST"- , putWord32le $ fromIntegral $ sdtaSize- , putString "sdta"- , putString "smpl"- , putWord32le $ fromIntegral $ smplSize- , Audio.buildSampleData putInt16le smpl1]- where smplSize = (Audio.sampleNumber smpl1) * 2- sdtaSize = 4 + 4 + 4 + smplSize-buildSdta (Sdta smpl1 (Just sd8)) = mconcat [- putString "LIST"- , putWord32le $ fromIntegral $ sdtaSize- , putString "sdta"- , putString "smpl"- , putWord32le $ fromIntegral $ smplSize- , Audio.buildSampleData putInt16le smpl1- , putString "sm24"- , putWord32le $ fromIntegral $ sm24Size- , Audio.buildSampleData putInt8 sd8- , mconcat $ genericReplicate (sm24Size - sn) $ putWord8 0]- where sn = Audio.sampleNumber smpl1- smplSize = sn * 2- sm24Size = if odd sn then sn + 1 else sn- sdtaSize = 4 + 4 + 4 + smplSize + 4 + 4 + sm24Size--parsePdta :: Parser Pdta-parsePdta = do- _ <- string "LIST"- _ <- getWord32le -- pdtaSize- _ <- string "pdta"- phdrs' <- parseSubchunk "phdr" 38 parsePhdr- pbags' <- parseSubchunk "pbag" 4 parseBag- pmods' <- parseSubchunk "pmod" 10 parseMod- pgens' <- parseSubchunk "pgen" 4 parseGen- insts' <- parseSubchunk "inst" 22 parseInst - ibags' <- parseSubchunk "ibag" 4 parseBag- imods' <- parseSubchunk "imod" 10 parseMod- igens' <- parseSubchunk "igen" 4 parseGen- shdrs' <- parseSubchunk "shdr" 46 parseShdr- return $! Pdta phdrs' pbags' pmods' pgens' insts' ibags' imods' igens' shdrs'--buildPdta :: Pdta -> Builder-buildPdta pdta1 = mconcat [- putString "LIST"- , putWord32le $ fromIntegral chunkSize- , fromLazyByteString bs]- where- chunkSize = L.length bs- bs = toLazyByteString $ mconcat [- putString "pdta"- , buildSubchunk "phdr" 38 buildPhdr (phdrs pdta1)- , buildSubchunk "pbag" 4 buildBag (pbags pdta1)- , buildSubchunk "pmod" 10 buildMod (pmods pdta1)- , buildSubchunk "pgen" 4 buildGen (pgens pdta1)- , buildSubchunk "inst" 22 buildInst (insts pdta1)- , buildSubchunk "ibag" 4 buildBag (ibags pdta1)- , buildSubchunk "imod" 10 buildMod (imods pdta1)- , buildSubchunk "igen" 4 buildGen (igens pdta1) - , buildSubchunk "shdr" 46 buildShdr (shdrs pdta1) - ]---- For some subchunks minimal number of records is two--- but this check can be done later I am skiping it here-parseSubchunk :: String -> Word -> (Parser a) -> Parser (Array Word a)-parseSubchunk s size p = do- _ <- string s- chunkSize <- expect (\w -> mod w size == 0) (getWord32le >>= return . fromIntegral)- let n = div chunkSize size- cs <- sequence (genericReplicate n p)- return $! listArray (0, n - 1) cs--buildSubchunk :: String -> Word -> (a -> Builder) -> (Array Word a) -> Builder-buildSubchunk s size b a = mconcat [- putString s- , putWord32le $ fromIntegral $ (1 + (snd $ bounds a)) * size- , mconcat $ map b $ elems a]- -parsePhdr :: Parser Phdr -parsePhdr = do- presetName' <- getLazyByteString 20- >>= return . map w2c . L.unpack . L.takeWhile ( /= 0)- preset' <- getWord16le >>= return . fromIntegral- bank' <- getWord16le >>= return . fromIntegral- presetBagNdx' <- getWord16le >>= return . fromIntegral- library' <- getWord32le >>= return . fromIntegral- genre' <- getWord32le >>= return . fromIntegral- morphology' <- getWord32le >>= return . fromIntegral- return $ Phdr {- presetName = presetName'- , preset = preset'- , bank = bank'- , presetBagNdx = presetBagNdx'- , library = library'- , genre = genre'- , morphology = morphology'- }--buildPhdr :: Phdr -> Builder -buildPhdr phdr = mconcat [- putString $ presetName phdr- , mconcat $ replicate (20 - length (presetName phdr)) (putWord8 0) - , putWord16le $ fromIntegral $ preset phdr- , putWord16le $ fromIntegral $ bank phdr- , putWord16le $ fromIntegral $ presetBagNdx phdr- , putWord32le $ fromIntegral $ library phdr- , putWord32le $ fromIntegral $ genre phdr- , putWord32le $ fromIntegral $ morphology phdr - ]--parseBag :: Parser Bag-parseBag = do- genNdx' <- getWord16le- modNdx' <- getWord16le- return $! Bag {- genNdx = fromIntegral genNdx'- , modNdx = fromIntegral modNdx'}- -buildBag :: Bag -> Builder -buildBag bag = mconcat [- putWord16le $ fromIntegral $ genNdx bag- , putWord16le $ fromIntegral $ modNdx bag]--parseMod :: Parser Mod-parseMod = do- srcOper' <- getWord16le- destOper' <- getWord16le- amount' <- getInt16le- amtSrcOper' <- getWord16le- transOper' <- getWord16le- return $! Mod {- srcOper = fromIntegral srcOper'- , destOper = fromIntegral destOper'- , amount = fromIntegral amount'- , amtSrcOper = fromIntegral amtSrcOper'- , transOper = fromIntegral transOper'- }- -buildMod :: Mod -> Builder-buildMod m = mconcat [- putWord16le $ fromIntegral $ srcOper m- , putWord16le $ fromIntegral $ destOper m- , putWord16le $ fromIntegral $ amount m- , putWord16le $ fromIntegral $ amtSrcOper m- , putWord16le $ fromIntegral $ transOper m- ]--parseGen :: Parser Generator-parseGen = choice [- int16le 0 >> getInt16le >>= return . StartAddressOffset . fromIntegral- , int16le 1 >> getInt16le >>= return . EndAddressOffset . fromIntegral- , int16le 2 >> getInt16le >>= return . LoopStartAddressOffset . fromIntegral- , int16le 3 >> getInt16le >>= return . LoopEndAddressOffset . fromIntegral- , int16le 4 >> getInt16le >>= return . StartAddressCoarseOffset . fromIntegral- , int16le 5 >> getInt16le >>= return . ModLfoToPitch . fromIntegral- , int16le 6 >> getInt16le >>= return . VibLfoToPitch . fromIntegral- , int16le 7 >> getInt16le >>= return . ModEnvToPitch . fromIntegral-- , int16le 8 >> getInt16le >>= return . InitFc . fromIntegral- , int16le 9 >> getInt16le >>= return . InitQ . fromIntegral- , int16le 10 >> getInt16le >>= return . ModLfoToFc . fromIntegral- , int16le 11 >> getInt16le >>= return . ModEnvToFc . fromIntegral- - , int16le 12 >> getInt16le >>= return . EndAddressCoarseOffset . fromIntegral- , int16le 13 >> getInt16le >>= return . ModLfoToVol . fromIntegral- - , int16le 15 >> getInt16le >>= return . Chorus . fromIntegral- , int16le 16 >> getInt16le >>= return . Reverb . fromIntegral- , int16le 17 >> getInt16le >>= return . Pan . fromIntegral- - , int16le 21 >> getInt16le >>= return . DelayModLfo . fromIntegral- , int16le 22 >> getInt16le >>= return . FreqModLfo . fromIntegral- - , int16le 23 >> getInt16le >>= return . DelayVibLfo . fromIntegral- , int16le 24 >> getInt16le >>= return . FreqVibLfo . fromIntegral- - , int16le 25 >> getInt16le >>= return . DelayModEnv . fromIntegral- , int16le 26 >> getInt16le >>= return . AttackModEnv . fromIntegral- , int16le 27 >> getInt16le >>= return . HoldModEnv . fromIntegral- , int16le 28 >> getInt16le >>= return . DecayModEnv . fromIntegral- , int16le 29 >> getInt16le >>= return . SustainModEnv . fromIntegral- , int16le 30 >> getInt16le >>= return . ReleaseModEnv . fromIntegral- , int16le 31 >> getInt16le >>= return . KeyToModEnvHold . fromIntegral- , int16le 32 >> getInt16le >>= return . KeyToModEnvDecay . fromIntegral- - , int16le 33 >> getInt16le >>= return . DelayVolEnv . fromIntegral- , int16le 34 >> getInt16le >>= return . AttackVolEnv . fromIntegral- , int16le 35 >> getInt16le >>= return . HoldVolEnv . fromIntegral- , int16le 36 >> getInt16le >>= return . DecayVolEnv . fromIntegral- , int16le 37 >> getInt16le >>= return . SustainVolEnv . fromIntegral- , int16le 38 >> getInt16le >>= return . ReleaseVolEnv . fromIntegral- , int16le 39 >> getInt16le >>= return . KeyToVolEnvHold . fromIntegral- , int16le 40 >> getInt16le >>= return . KeyToVolEnvDecay . fromIntegral- - , int16le 41 >> getWord16le >>= return . InstIndex . fromIntegral- , do _ <- int16le 43; a <- getWord8 >>= return . fromIntegral;- b <- getWord8 >>= return . fromIntegral; return $ KeyRange a b;- , do _ <- int16le 44; a <- getWord8 >>= return . fromIntegral;- b <- getWord8 >>= return . fromIntegral; return $ VelRange a b;- - , int16le 45 >> getInt16le >>= return . LoopStartAddressCoarseOffset . fromIntegral- , int16le 46 >> getWord16le >>= return . Key . fromIntegral- , int16le 47 >> getWord16le >>= return . Vel . fromIntegral- , int16le 48 >> getInt16le >>= return . InitAtten . fromIntegral- , int16le 50 >> getInt16le >>= return . LoopEndAddressCoarseOffset . fromIntegral- - , int16le 51 >> getInt16le >>= return . CoarseTune . fromIntegral- , int16le 52 >> getInt16le >>= return . FineTune . fromIntegral- , int16le 53 >> getWord16le >>= return . SampleIndex . fromIntegral- , do _ <- int16le 54; a <- getInt16le;- case a of- 1 -> return $ SampleMode Audio.ContLoop- 3 -> return $ SampleMode Audio.PressLoop- _ -> return $ SampleMode Audio.NoLoop- , int16le 56 >> getInt16le >>= return . ScaleTuning . fromIntegral- , int16le 57 >> getInt16le >>= return . ExclusiveClass . fromIntegral- - , int16le 58 >> getWord16le >>= return . RootKey . fromIntegral- , do p1 <- getInt16le; p2 <- getInt16le;- return $ ReservedGen (fromIntegral p1) (fromIntegral p2)]---buildGen :: Generator -> Builder-buildGen g = mconcat $ case g of- StartAddressOffset i -> [putInt16le 0, putInt16le $ fromIntegral i]- EndAddressOffset i -> [putInt16le 1, putInt16le $ fromIntegral i]- LoopStartAddressOffset i -> [putInt16le 2, putInt16le $ fromIntegral i]- LoopEndAddressOffset i -> [putInt16le 3, putInt16le $ fromIntegral i]- StartAddressCoarseOffset i -> [putInt16le 4, putInt16le $ fromIntegral i]- ModLfoToPitch i -> [putInt16le 5, putInt16le $ fromIntegral i]- VibLfoToPitch i -> [putInt16le 6, putInt16le $ fromIntegral i]- ModEnvToPitch i -> [putInt16le 7, putInt16le $ fromIntegral i]-- InitFc i -> [putInt16le 8, putInt16le $ fromIntegral i]- InitQ i -> [putInt16le 9, putInt16le $ fromIntegral i]- ModLfoToFc i -> [putInt16le 10, putInt16le $ fromIntegral i]- ModEnvToFc i -> [putInt16le 11, putInt16le $ fromIntegral i]- - EndAddressCoarseOffset i -> [putInt16le 12, putInt16le $ fromIntegral i]- ModLfoToVol i -> [putInt16le 13, putInt16le $ fromIntegral i]- - Chorus i -> [putInt16le 15, putInt16le $ fromIntegral i]- Reverb i -> [putInt16le 16, putInt16le $ fromIntegral i]- Pan i -> [putInt16le 17, putInt16le $ fromIntegral i]- - DelayModLfo i -> [putInt16le 21, putInt16le $ fromIntegral i]- FreqModLfo i -> [putInt16le 22, putInt16le $ fromIntegral i]- - DelayVibLfo i -> [putInt16le 23, putInt16le $ fromIntegral i]- FreqVibLfo i -> [putInt16le 24, putInt16le $ fromIntegral i]- - DelayModEnv i -> [putInt16le 25, putInt16le $ fromIntegral i]- AttackModEnv i -> [putInt16le 26, putInt16le $ fromIntegral i]- HoldModEnv i -> [putInt16le 27, putInt16le $ fromIntegral i]- DecayModEnv i -> [putInt16le 28, putInt16le $ fromIntegral i]- SustainModEnv i -> [putInt16le 29, putInt16le $ fromIntegral i]- ReleaseModEnv i -> [putInt16le 30, putInt16le $ fromIntegral i]- KeyToModEnvHold i -> [putInt16le 31, putInt16le $ fromIntegral i]- KeyToModEnvDecay i -> [putInt16le 32, putInt16le $ fromIntegral i]- - DelayVolEnv i -> [putInt16le 33, putInt16le $ fromIntegral i]- AttackVolEnv i -> [putInt16le 34, putInt16le $ fromIntegral i]- HoldVolEnv i -> [putInt16le 35, putInt16le $ fromIntegral i]- DecayVolEnv i -> [putInt16le 36, putInt16le $ fromIntegral i]- SustainVolEnv i -> [putInt16le 37, putInt16le $ fromIntegral i]- ReleaseVolEnv i -> [putInt16le 38, putInt16le $ fromIntegral i]- KeyToVolEnvHold i -> [putInt16le 39, putInt16le $ fromIntegral i]- KeyToVolEnvDecay i -> [putInt16le 40, putInt16le $ fromIntegral i]- - InstIndex i -> [putInt16le 41, putWord16le $ fromIntegral i]- KeyRange a b -> [putInt16le 43, putWord8 $ fromIntegral a, putWord8 $ fromIntegral b]- VelRange a b -> [putInt16le 44, putWord8 $ fromIntegral a, putWord8 $ fromIntegral b]-- LoopStartAddressCoarseOffset i -> [putInt16le 45, putInt16le $ fromIntegral i]- Key i -> [putWord16le 46, putInt16le $ fromIntegral i]- Vel i -> [putWord16le 47, putInt16le $ fromIntegral i]- InitAtten i -> [putInt16le 48, putInt16le $ fromIntegral i]- LoopEndAddressCoarseOffset i -> [putInt16le 50, putInt16le $ fromIntegral i]- - CoarseTune i -> [putInt16le 51, putInt16le $ fromIntegral i]- FineTune i -> [putInt16le 52, putInt16le $ fromIntegral i]- SampleIndex i -> [putInt16le 53, putWord16le $ fromIntegral i]- - SampleMode Audio.ContLoop -> [putInt16le 54, putInt16le 1]- SampleMode Audio.PressLoop -> [putInt16le 54, putInt16le 3]- SampleMode Audio.NoLoop -> [putInt16le 54, putInt16le 2]- --56- ScaleTuning i -> [putInt16le 56, putInt16le $ fromIntegral i]- ExclusiveClass i -> [putInt16le 57, putInt16le $ fromIntegral i]- - RootKey i -> [putInt16le 58, putWord16le $ fromIntegral i]- ReservedGen i1 i2 -> [putInt16le $ fromIntegral i1, putInt16le $ fromIntegral i2]--parseInst :: Parser Inst -parseInst = do- instName' <- getLazyByteString 20- >>= return . map w2c . L.unpack . L.takeWhile ( /= 0)- instBagNdx' <- getWord16le >>= return . fromIntegral- return $ Inst {- instName = instName'- , instBagNdx = instBagNdx'}--buildInst :: Inst -> Builder -buildInst i = mconcat [- putString $ instName i- , mconcat $ replicate (20 - length (instName i)) (putWord8 0) - , putWord16le $ fromIntegral $ instBagNdx i]--parseShdr :: Parser Shdr -parseShdr = do- sampleName' <- getLazyByteString 20- >>= return . map w2c . L.unpack . L.takeWhile ( /= 0)- start' <- getWord32le- end' <- getWord32le- startLoop' <- getWord32le- endLoop' <- getWord32le - sampleRate' <- getWord32le- originalPitch' <- getWord8- pitchCorrection' <- getInt8- sampleLink' <- getWord16le- sampleType' <- getWord16le- return $ Shdr {- sampleName = sampleName'- , start = fromIntegral start'- , end = fromIntegral end'- , startLoop = fromIntegral startLoop'- , endLoop = fromIntegral endLoop'- , sampleRate = fromIntegral sampleRate'- , originalPitch = fromIntegral originalPitch'- , pitchCorrection = fromIntegral pitchCorrection'- , sampleLink = fromIntegral sampleLink'- , sampleType = fromIntegral sampleType'- }--buildShdr :: Shdr -> Builder -buildShdr shdr = mconcat [- putString $ sampleName shdr- , mconcat $ replicate (20 - length (sampleName shdr)) (putWord8 0) - , putWord32le $ fromIntegral $ start shdr- , putWord32le $ fromIntegral $ end shdr - , putWord32le $ fromIntegral $ startLoop shdr- , putWord32le $ fromIntegral $ endLoop shdr- , putWord32le $ fromIntegral $ sampleRate shdr- , putWord8 $ fromIntegral $ originalPitch shdr- , putInt8 $ fromIntegral $ pitchCorrection shdr - , putWord16le $ fromIntegral $ sampleLink shdr- , putWord16le $ fromIntegral $ sampleType shdr- ]+----------------------------------------------------------------------------- +-- | +-- Module : Codec.SoundFont +-- Copyright : George Giorgidze +-- License : BSD3 +-- +-- Maintainer : George Giorgidze <http://cs.nott.ac.uk/~ggg/> +-- Stability : Experimental +-- Portability : Portable +-- +-- Module for reading and writting of SoundFont instrument description files. +-- +----------------------------------------------------------------------------- + +module Codec.SoundFont ( + SoundFont (..) + , Info (..) + , Sdta (..) + , Pdta (..) + , Phdr (..) + , Bag (..) + , Mod (..) + , Generator (..) + , isSampleIndex + , isInstIndex + , Inst (..) + , Shdr (..) + , importFile + , exportFile + , parseSoundFont + , buildSoundFont + , parseInfos + , buildInfos + , parseSdta + , buildSdta + , parsePdta + , buildPdta + ) where + +import Codec.ByteString.Parser +import Codec.ByteString.Builder +import Codec.Internal.Arbitrary +import qualified Data.Audio as Audio + +import Data.Word +import Data.Int +import qualified Data.ByteString.Lazy as L +import Data.ByteString.Internal (w2c) +import Data.Array.IArray +import Data.List + +import Test.QuickCheck +import Data.Monoid (mconcat) +import Control.Applicative +import Control.Monad + +data SoundFont = SoundFont { + infos :: Array Word Info -- Suplemental information + , sdta :: Sdta -- The Sample Binary Data + , pdta :: Pdta + -- articulation :: Articulation -- The Preset, Instrument, and Sample Header data + } deriving (Eq, Show) + +instance Arbitrary SoundFont where + arbitrary = do + f1 <- arbitrary; f2 <- arbitrary; f3 <- arbitrary; + return $! SoundFont f1 f2 f3 + +-- instance Show SoundFont where +-- show sf = (show $ length $ elems $ sampleData sf) ++ "\n" ++ (show $ length $ show $ articulation sf) ++ "\n" + +-- type SamplePointIndex = Word32 +-- type SampleData = UArray SamplePointIndex Audio.Sample + +data Info = + Version Word Word -- Refers to file version of SounfFont RIFF file + | TargetSoundEngine String -- Refers to target Sound Engine + | BankName String -- Refers to SoundFont Bank Name + | RomName String -- Refers to Sound ROM Name + | RomVersion Word Word -- Refers to Sound ROM Version + | CreationDate String -- Refers to Date of Creation of the Bank + | Authors String -- Sound Designers and Engineers of the Bank + | IntendedProduct String -- Product fot which the bank was intended + | CopyrightMessage String -- Contains any copyright message + | Comments String -- Contains any comemnts on the Bank + | UsedTools String -- The SoundFont tools used to create and alter the bank + | ReservedInfo String Word L.ByteString + deriving (Eq,Show) + +instance Arbitrary Info where + arbitrary = oneof [ + do w1 <- choose (minBound :: Word16, maxBound) + w2 <- choose (minBound :: Word16, maxBound) + return $ Version (fromIntegral w1) (fromIntegral w2); + , do l <- choose (0,255); s <- genStringNul l; return $ TargetSoundEngine s + , do l <- choose (0,255); s <- genStringNul l; return $ BankName s + , do l <- choose (0,255); s <- genStringNul l; return $ RomName s + , do w1 <- choose (minBound :: Word16, maxBound) + w2 <- choose (minBound :: Word16, maxBound) + return $ RomVersion (fromIntegral w1) (fromIntegral w2) + , do l <- choose (0,255); s <- genStringNul l; return $ CreationDate s + , do l <- choose (0,255); s <- genStringNul l; return $ Authors s + , do l <- choose (0,255); s <- genStringNul l; return $ IntendedProduct s + , do l <- choose (0,255); s <- genStringNul l; return $ CopyrightMessage s + , do l <- choose (0,255); s <- genStringNul l; return $ Comments s + , do l <- choose (0,255); s <- genStringNul l; return $ UsedTools s + , do l <- choose (0,255); s <- vector (fromIntegral l); + return $ ReservedInfo "RSRV" l (L.pack s)] + where + genStringNul :: Int -> Gen String + genStringNul l = sequence $ replicate l $ fmap w2c $ choose (1,255) + +data Sdta = Sdta { + smpl :: Audio.SampleData Int16 + , sm24 :: Maybe (Audio.SampleData Int8) + } deriving (Eq, Show) + +instance Arbitrary Sdta where + arbitrary = do + sn <- choose (1,1024) + smpl1 <- arrayGen sn + oneof [ + return $! Sdta smpl1 Nothing + , do sm24' <- arrayGen sn + return $! Sdta smpl1 (Just sm24') + ] + +data Pdta = Pdta { + phdrs :: Array Word Phdr + , pbags :: Array Word Bag + , pmods :: Array Word Mod + , pgens :: Array Word Generator + , insts :: Array Word Inst + , ibags :: Array Word Bag + , imods :: Array Word Mod + , igens :: Array Word Generator + , shdrs :: Array Word Shdr + } deriving (Eq, Show) + +instance Arbitrary Pdta where + arbitrary = do + f1 <- arbitrary; f2 <- arbitrary; f3 <- arbitrary; f4 <- arbitrary; + f5 <- arbitrary; f6 <- arbitrary; f7 <- arbitrary; f8 <- arbitrary; + f9 <- arbitrary; + return $! Pdta f1 f2 f3 f4 f5 f6 f7 f8 f9 + +data Phdr = Phdr { + presetName :: String + , preset :: Word + , bank :: Word + , presetBagNdx :: Word + , library :: Word + , genre :: Word + , morphology :: Word + } deriving (Eq, Show) + +instance Arbitrary Phdr where + arbitrary = do + n <- choose (0,20) + presetName' <- stringNulGen n + preset' <- choose (minBound :: Word16, maxBound) + bank' <- choose (minBound :: Word16, maxBound) + presetBagNdx' <- choose (minBound :: Word16, maxBound) + library' <- choose (minBound :: Word32, maxBound) + genre' <- choose (minBound :: Word32, maxBound) + morphology' <- choose (minBound :: Word32, maxBound) + return $ Phdr { + presetName = presetName' + , preset = fromIntegral $ preset' + , bank = fromIntegral $ bank' + , presetBagNdx = fromIntegral $ presetBagNdx' + , library = fromIntegral $ library' + , genre = fromIntegral $ genre' + , morphology = fromIntegral $ morphology' + } + +data Bag = Bag { + genNdx :: Word + , modNdx :: Word + } deriving (Eq, Show) + +instance Arbitrary Bag where + arbitrary = do + genNdx' <- choose (minBound :: Word16, maxBound) + modNdx' <- choose (minBound :: Word16, maxBound) + return $! Bag { + genNdx = fromIntegral genNdx' + , modNdx = fromIntegral modNdx'} + +data Mod = Mod { + srcOper :: Word + , destOper :: Word + , amount :: Int + , amtSrcOper :: Word + , transOper :: Word + } deriving (Eq, Show) + +instance Arbitrary Mod where + arbitrary = do + srcOper' <- choose (minBound :: Word16, maxBound) + destOper' <- choose (minBound :: Word16, maxBound) + amount' <- choose (minBound :: Int16, maxBound) + amtSrcOper' <- choose (minBound :: Word16, maxBound) + transOper' <- choose (minBound :: Word16, maxBound) + return $! Mod { + srcOper = fromIntegral srcOper' + , destOper = fromIntegral destOper' + , amount = fromIntegral amount' + , amtSrcOper = fromIntegral amtSrcOper' + , transOper = fromIntegral transOper' + } + +data Generator = + -- Oscillator + StartAddressOffset Int | -- 0 sample start fine offset + EndAddressOffset Int | -- 1 sample end fine offset + LoopStartAddressOffset Int | -- 2 sample start loop fine offset + LoopEndAddressOffset Int | -- 3 sample end loop file offset + StartAddressCoarseOffset Int | -- 4 sample start coarse offset + ModLfoToPitch Int | -- 5 main fm: modLfo-> pitch + VibLfoToPitch Int | -- 6 aux fm: vibLfo-> pitch + ModEnvToPitch Int | -- 7 pitch env: modEnv(aux)-> pitch + + -- Filter + InitFc Int | -- 8 initial filter cutoff + InitQ Int | -- 9 filter Q + ModLfoToFc Int | -- 10 filter modulation: lfo1 -> filter cutoff + ModEnvToFc Int | -- 11 filter env: modEnv(aux)-> filter cutoff + + + EndAddressCoarseOffset Int | -- 12 initial volume + + -- Amplifier + ModLfoToVol Int | -- 13 tremolo: lfo1-> volume + -- 14 unused + + -- Effects + Chorus Int | -- 15 chorus + Reverb Int | -- 16 reverb + Pan Int | -- 17 pan + -- 18 unused + -- 19 unused + -- 20 unused + + -- Modulation LFO + DelayModLfo Int | -- 21 delay + FreqModLfo Int | -- 22 frequency + + -- Vibrato LFO + DelayVibLfo Int | -- 23 delay + FreqVibLfo Int | -- 24 frequency + + -- Modulation Envelope + DelayModEnv Int | -- 25 delay + AttackModEnv Int | -- 26 attack + HoldModEnv Int | -- 27 hold + DecayModEnv Int | -- 28 decay + SustainModEnv Int | -- 29 sustain + ReleaseModEnv Int | -- 30 release + KeyToModEnvHold Int | -- 31 key scaling coefficient + KeyToModEnvDecay Int | -- 32 key scaling coefficient + + -- Volume Envelope (ampl/vol) + DelayVolEnv Int | -- 33 delay + AttackVolEnv Int | -- 34 attack + HoldVolEnv Int | -- 35 hold + DecayVolEnv Int | -- 36 decay + SustainVolEnv Int | -- 37 sustain + ReleaseVolEnv Int | -- 38 release + KeyToVolEnvHold Int | -- 39 key scaling coefficient + KeyToVolEnvDecay Int | -- 40 key scaling coefficient + + -- Preset + InstIndex Word | -- 41 + -- 42 + KeyRange Word Word | -- 43 + VelRange Word Word | -- 44 + LoopStartAddressCoarseOffset Int | -- 45 + Key Word | -- 46 + Vel Word | -- 47 + InitAtten Int | -- 48 + -- 49 unused + LoopEndAddressCoarseOffset Int | -- 50 + + CoarseTune Int | -- 51 + FineTune Int | -- 52 + SampleIndex Word | -- 53 + SampleMode Audio.SampleMode | -- 54 + -- 55 unused + ScaleTuning Int | -- 56 + ExclusiveClass Int | -- 57 + + RootKey Word | -- 58 + -- 59 unused + ReservedGen Int Int -- 60 single constructor for all unused generators + deriving (Eq, Show) + +instance Arbitrary Generator where + arbitrary = do + i <- choose (minBound :: Int16, maxBound) >>= return . fromIntegral + w <- choose (minBound :: Word16, maxBound) >>= return . fromIntegral + i' <- choose (60 :: Int16, maxBound) >>= return . fromIntegral + r1 <- choose (0,127) + r2 <- choose (0,127) + smplMode' <- arbitrary + oneof $ map return [ + StartAddressOffset i + , EndAddressOffset i + , LoopStartAddressOffset i + , LoopEndAddressOffset i + , StartAddressCoarseOffset i + , ModLfoToPitch i + , VibLfoToPitch i + , ModEnvToPitch i + + , InitFc i + , InitQ i + , ModLfoToFc i + , ModEnvToFc i + + , EndAddressCoarseOffset i + , ModLfoToVol i + + , Chorus i + , Reverb i + , Pan i + , DelayModLfo i + , FreqModLfo i + + , DelayVibLfo i + , FreqVibLfo i + + , DelayModEnv i + , AttackModEnv i + , HoldModEnv i + , DecayModEnv i + , SustainModEnv i + , ReleaseModEnv i + , KeyToModEnvHold i + , KeyToModEnvDecay i + + , DelayVolEnv i + , AttackVolEnv i + , HoldVolEnv i + , DecayVolEnv i + , SustainVolEnv i + , ReleaseVolEnv i + , KeyToVolEnvHold i + , KeyToVolEnvDecay i + + , InstIndex w + + , KeyRange r1 r2 + , VelRange r2 r2 + , LoopStartAddressCoarseOffset i + , Key w + , Vel w + , InitAtten i + + , LoopEndAddressCoarseOffset i + + , CoarseTune i + , FineTune i + , SampleIndex w + , SampleMode smplMode' + + , ScaleTuning i + , ExclusiveClass i + + , RootKey w + + , ReservedGen i' i] + +isSampleIndex :: Generator -> Bool +isSampleIndex g = case g of + SampleIndex _ -> True + _ -> False + +isInstIndex :: Generator -> Bool +isInstIndex g = case g of + InstIndex _ -> True + _ -> False + +data Inst = Inst { + instName :: String + , instBagNdx :: Word + } deriving (Eq, Show) + +instance Arbitrary Inst where + arbitrary = do + n <- choose (0,20) + instName' <- stringNulGen n + instBagNdx' <- choose (maxBound :: Word16, minBound) + return $! Inst { + instName = instName' + , instBagNdx = fromIntegral $ instBagNdx'} + +data Shdr = Shdr { + sampleName :: String + , start :: Word + , end :: Word + , startLoop :: Word + , endLoop :: Word + , sampleRate :: Word + , originalPitch :: Word + , pitchCorrection :: Int + , sampleLink :: Word + , sampleType :: Word + } deriving (Eq, Show) + +instance Arbitrary Shdr where + arbitrary = do + n <- choose (0,20) + sampleName' <- stringNulGen n + start' <- choose (minBound :: Word32, maxBound) + end' <- choose (minBound :: Word32, maxBound) + startLoop' <- choose (minBound :: Word32, maxBound) + endLoop' <- choose (minBound :: Word32, maxBound) + sampleRate' <- choose (minBound :: Word32, maxBound) + originalPitch' <- choose (minBound :: Word8, maxBound) + pitchCorrection' <- choose (minBound :: Int8, maxBound) + sampleLink' <- choose (minBound :: Word16, maxBound) + sampleType' <- choose (minBound :: Word16, maxBound) + return $ Shdr { + sampleName = sampleName' + , start = fromIntegral start' + , end = fromIntegral end' + , startLoop = fromIntegral startLoop' + , endLoop = fromIntegral endLoop' + , sampleRate = fromIntegral sampleRate' + , originalPitch = fromIntegral originalPitch' + , pitchCorrection = fromIntegral pitchCorrection' + , sampleLink = fromIntegral sampleLink' + , sampleType = fromIntegral sampleType' + } + +---- SoundFont import + +importFile :: FilePath -> IO (Either String SoundFont) +importFile n = do + bs <- L.readFile n + return $! runParser parseSoundFont bs + +exportFile :: FilePath -> SoundFont -> IO () +exportFile f sf = do + let bs = toLazyByteString $ buildSoundFont sf + L.writeFile f bs + +parseSoundFont :: Parser SoundFont +parseSoundFont = do + _ <- string "RIFF" + _ <- getWord32le -- chunkSize + _ <- string "sfbk" + infos' <- parseInfos + sdta' <- parseSdta + pdta' <- parsePdta + return $! SoundFont { + infos = infos' + , sdta = sdta' + , pdta = pdta' + } + +buildSoundFont :: SoundFont -> Builder +buildSoundFont sf = mconcat [ + putString "RIFF" + , putWord32le $ fromIntegral chunkSize + , fromLazyByteString bs] + where + chunkSize = L.length bs + bs = toLazyByteString $ mconcat [ + putString "sfbk" + , buildInfos (infos sf) + , buildSdta (sdta sf) + , buildPdta (pdta sf)] + +--buildSoundFont :: SoundFont -> Builder +--buildSoundFont sf = mconcat [ + +parseInfos :: Parser (Array Word Info) +parseInfos = do + _ <- string "LIST" + _ <- getWord32le -- chunkSize + _ <- string "INFO" + infos' <- many p + return $! listArray (0, genericLength infos' - 1) infos' + where + p = choice [ + do n <- getString 4; _ <- word32le 4; w1 <- getWord16le; w2 <- getWord16le; + case n of + "ifil" -> return $! Version (fromIntegral w1) (fromIntegral w2) + "iver" -> return $! RomVersion (fromIntegral w1) (fromIntegral w2) + _ -> fail [] + , do n <- getString 4; l <- expect (<= 256) getWord32le; s <- getStringNul; + skip (fromIntegral l - genericLength s - 1); + case n of + "isng" -> return $! TargetSoundEngine s + "INAM" -> return $! BankName s + "irom" -> return $! RomName s + "ICRD" -> return $! CreationDate s + "IENG" -> return $! Authors s + "IPRD" -> return $! IntendedProduct s + "ICOP" -> return $! CopyrightMessage s + "ICMT" -> return $! Comments s + "ISFT" -> return $! UsedTools s + _ -> fail [] + , do n <- expect ( /= "LIST") (getString 4) + l <- getWord32le + bs <- getLazyByteString (fromIntegral l) + return $! ReservedInfo n (fromIntegral l) bs] + +buildInfos :: (Array Word Info) -> Builder +buildInfos infos' = mconcat [ + putString "LIST" + , putWord32le $ (fromIntegral $ L.length bs) + 4 + , putString "INFO" + , fromLazyByteString bs] + where + bs = toLazyByteString $ mconcat $ map buildInfo $ elems infos' + +{- +Specification says that 'ifil' 'isgn' and 'INAM' fields are mandatory +but I am not checking it during parsing +-} + + +buildInfo :: Info -> Builder +buildInfo (Version w1 w2) = mconcat + [putString "ifil", putWord32le 4, + putWord16le (fromIntegral w1), putWord16le (fromIntegral w2)] +buildInfo (RomVersion w1 w2) = mconcat + [putString "iver", putWord32le 4, + putWord16le (fromIntegral w1), putWord16le (fromIntegral w2)] +buildInfo (TargetSoundEngine s) = mconcat [putString "isng", buildInfoString s] +buildInfo (BankName s) = mconcat [putString "INAM", buildInfoString s] +buildInfo (RomName s) = mconcat [putString "irom", buildInfoString s] +buildInfo (CreationDate s) = mconcat [putString "ICRD", buildInfoString s] +buildInfo (Authors s) = mconcat [putString "IENG", buildInfoString s] +buildInfo (IntendedProduct s) = mconcat [putString "IPRD", buildInfoString s] +buildInfo (CopyrightMessage s) = mconcat [putString "ICOP", buildInfoString s] +buildInfo (Comments s) = mconcat [putString "ICMT", buildInfoString s] +buildInfo (UsedTools s) = mconcat [putString "ISFT", buildInfoString s] +buildInfo (ReservedInfo n l bs) = mconcat + [putString n, putWord32le (fromIntegral l), fromLazyByteString bs] + +buildInfoString :: String -> Builder +buildInfoString s = if (mod l 2 == 0) + then mconcat [putWord32le (l + 2), putString s, putWord8 0, putWord8 0] + else mconcat [putWord32le (l + 1), putString s, putWord8 0] + where + l = fromIntegral $ length s + +parseSdta :: Parser Sdta +parseSdta = do + _ <- string "LIST" + sdtaSize <- getWord32le >>= return .fromIntegral + _ <- string "sdta" + _ <- string "smpl" + smplSize <- getWord32le >>= return .fromIntegral + when (odd smplSize) $ fail "'smplSize' must not be odd number" + let sn = div smplSize 2 + smpl' <- Audio.parseSampleData sn getInt16le + choice [ + do guard (smplSize == (sdtaSize - 12)) + return $! Sdta {smpl = smpl', sm24 = Nothing} + , do _ <- string "sm24" + let sm24Size = if odd sn then sn + 1 else sn + _ <- word32le (fromIntegral sm24Size) + sm24' <- Audio.parseSampleData sn getInt8 + skip (fromIntegral $ sm24Size - sn) + return $! Sdta{ smpl = smpl', sm24 = Just sm24'} + ] + +buildSdta :: Sdta -> Builder +buildSdta (Sdta smpl1 Nothing) = mconcat [ + putString "LIST" + , putWord32le $ fromIntegral $ sdtaSize + , putString "sdta" + , putString "smpl" + , putWord32le $ fromIntegral $ smplSize + , Audio.buildSampleData putInt16le smpl1] + where smplSize = (Audio.sampleNumber smpl1) * 2 + sdtaSize = 4 + 4 + 4 + smplSize +buildSdta (Sdta smpl1 (Just sd8)) = mconcat [ + putString "LIST" + , putWord32le $ fromIntegral $ sdtaSize + , putString "sdta" + , putString "smpl" + , putWord32le $ fromIntegral $ smplSize + , Audio.buildSampleData putInt16le smpl1 + , putString "sm24" + , putWord32le $ fromIntegral $ sm24Size + , Audio.buildSampleData putInt8 sd8 + , mconcat $ genericReplicate (sm24Size - sn) $ putWord8 0] + where sn = Audio.sampleNumber smpl1 + smplSize = sn * 2 + sm24Size = if odd sn then sn + 1 else sn + sdtaSize = 4 + 4 + 4 + smplSize + 4 + 4 + sm24Size + +parsePdta :: Parser Pdta +parsePdta = do + _ <- string "LIST" + _ <- getWord32le -- pdtaSize + _ <- string "pdta" + phdrs' <- parseSubchunk "phdr" 38 parsePhdr + pbags' <- parseSubchunk "pbag" 4 parseBag + pmods' <- parseSubchunk "pmod" 10 parseMod + pgens' <- parseSubchunk "pgen" 4 parseGen + insts' <- parseSubchunk "inst" 22 parseInst + ibags' <- parseSubchunk "ibag" 4 parseBag + imods' <- parseSubchunk "imod" 10 parseMod + igens' <- parseSubchunk "igen" 4 parseGen + shdrs' <- parseSubchunk "shdr" 46 parseShdr + return $! Pdta phdrs' pbags' pmods' pgens' insts' ibags' imods' igens' shdrs' + +buildPdta :: Pdta -> Builder +buildPdta pdta1 = mconcat [ + putString "LIST" + , putWord32le $ fromIntegral chunkSize + , fromLazyByteString bs] + where + chunkSize = L.length bs + bs = toLazyByteString $ mconcat [ + putString "pdta" + , buildSubchunk "phdr" 38 buildPhdr (phdrs pdta1) + , buildSubchunk "pbag" 4 buildBag (pbags pdta1) + , buildSubchunk "pmod" 10 buildMod (pmods pdta1) + , buildSubchunk "pgen" 4 buildGen (pgens pdta1) + , buildSubchunk "inst" 22 buildInst (insts pdta1) + , buildSubchunk "ibag" 4 buildBag (ibags pdta1) + , buildSubchunk "imod" 10 buildMod (imods pdta1) + , buildSubchunk "igen" 4 buildGen (igens pdta1) + , buildSubchunk "shdr" 46 buildShdr (shdrs pdta1) + ] + +-- For some subchunks minimal number of records is two +-- but this check can be done later I am skiping it here +parseSubchunk :: String -> Word -> (Parser a) -> Parser (Array Word a) +parseSubchunk s size p = do + _ <- string s + chunkSize <- expect (\w -> mod w size == 0) (getWord32le >>= return . fromIntegral) + let n = div chunkSize size + cs <- sequence (genericReplicate n p) + return $! listArray (0, n - 1) cs + +buildSubchunk :: String -> Word -> (a -> Builder) -> (Array Word a) -> Builder +buildSubchunk s size b a = mconcat [ + putString s + , putWord32le $ fromIntegral $ (1 + (snd $ bounds a)) * size + , mconcat $ map b $ elems a] + +parsePhdr :: Parser Phdr +parsePhdr = do + presetName' <- getLazyByteString 20 + >>= return . map w2c . L.unpack . L.takeWhile ( /= 0) + preset' <- getWord16le >>= return . fromIntegral + bank' <- getWord16le >>= return . fromIntegral + presetBagNdx' <- getWord16le >>= return . fromIntegral + library' <- getWord32le >>= return . fromIntegral + genre' <- getWord32le >>= return . fromIntegral + morphology' <- getWord32le >>= return . fromIntegral + return $ Phdr { + presetName = presetName' + , preset = preset' + , bank = bank' + , presetBagNdx = presetBagNdx' + , library = library' + , genre = genre' + , morphology = morphology' + } + +buildPhdr :: Phdr -> Builder +buildPhdr phdr = mconcat [ + putString $ presetName phdr + , mconcat $ replicate (20 - length (presetName phdr)) (putWord8 0) + , putWord16le $ fromIntegral $ preset phdr + , putWord16le $ fromIntegral $ bank phdr + , putWord16le $ fromIntegral $ presetBagNdx phdr + , putWord32le $ fromIntegral $ library phdr + , putWord32le $ fromIntegral $ genre phdr + , putWord32le $ fromIntegral $ morphology phdr + ] + +parseBag :: Parser Bag +parseBag = do + genNdx' <- getWord16le + modNdx' <- getWord16le + return $! Bag { + genNdx = fromIntegral genNdx' + , modNdx = fromIntegral modNdx'} + +buildBag :: Bag -> Builder +buildBag bag = mconcat [ + putWord16le $ fromIntegral $ genNdx bag + , putWord16le $ fromIntegral $ modNdx bag] + +parseMod :: Parser Mod +parseMod = do + srcOper' <- getWord16le + destOper' <- getWord16le + amount' <- getInt16le + amtSrcOper' <- getWord16le + transOper' <- getWord16le + return $! Mod { + srcOper = fromIntegral srcOper' + , destOper = fromIntegral destOper' + , amount = fromIntegral amount' + , amtSrcOper = fromIntegral amtSrcOper' + , transOper = fromIntegral transOper' + } + +buildMod :: Mod -> Builder +buildMod m = mconcat [ + putWord16le $ fromIntegral $ srcOper m + , putWord16le $ fromIntegral $ destOper m + , putWord16le $ fromIntegral $ amount m + , putWord16le $ fromIntegral $ amtSrcOper m + , putWord16le $ fromIntegral $ transOper m + ] + +parseGen :: Parser Generator +parseGen = choice [ + int16le 0 >> getInt16le >>= return . StartAddressOffset . fromIntegral + , int16le 1 >> getInt16le >>= return . EndAddressOffset . fromIntegral + , int16le 2 >> getInt16le >>= return . LoopStartAddressOffset . fromIntegral + , int16le 3 >> getInt16le >>= return . LoopEndAddressOffset . fromIntegral + , int16le 4 >> getInt16le >>= return . StartAddressCoarseOffset . fromIntegral + , int16le 5 >> getInt16le >>= return . ModLfoToPitch . fromIntegral + , int16le 6 >> getInt16le >>= return . VibLfoToPitch . fromIntegral + , int16le 7 >> getInt16le >>= return . ModEnvToPitch . fromIntegral + + , int16le 8 >> getInt16le >>= return . InitFc . fromIntegral + , int16le 9 >> getInt16le >>= return . InitQ . fromIntegral + , int16le 10 >> getInt16le >>= return . ModLfoToFc . fromIntegral + , int16le 11 >> getInt16le >>= return . ModEnvToFc . fromIntegral + + , int16le 12 >> getInt16le >>= return . EndAddressCoarseOffset . fromIntegral + , int16le 13 >> getInt16le >>= return . ModLfoToVol . fromIntegral + + , int16le 15 >> getInt16le >>= return . Chorus . fromIntegral + , int16le 16 >> getInt16le >>= return . Reverb . fromIntegral + , int16le 17 >> getInt16le >>= return . Pan . fromIntegral + + , int16le 21 >> getInt16le >>= return . DelayModLfo . fromIntegral + , int16le 22 >> getInt16le >>= return . FreqModLfo . fromIntegral + + , int16le 23 >> getInt16le >>= return . DelayVibLfo . fromIntegral + , int16le 24 >> getInt16le >>= return . FreqVibLfo . fromIntegral + + , int16le 25 >> getInt16le >>= return . DelayModEnv . fromIntegral + , int16le 26 >> getInt16le >>= return . AttackModEnv . fromIntegral + , int16le 27 >> getInt16le >>= return . HoldModEnv . fromIntegral + , int16le 28 >> getInt16le >>= return . DecayModEnv . fromIntegral + , int16le 29 >> getInt16le >>= return . SustainModEnv . fromIntegral + , int16le 30 >> getInt16le >>= return . ReleaseModEnv . fromIntegral + , int16le 31 >> getInt16le >>= return . KeyToModEnvHold . fromIntegral + , int16le 32 >> getInt16le >>= return . KeyToModEnvDecay . fromIntegral + + , int16le 33 >> getInt16le >>= return . DelayVolEnv . fromIntegral + , int16le 34 >> getInt16le >>= return . AttackVolEnv . fromIntegral + , int16le 35 >> getInt16le >>= return . HoldVolEnv . fromIntegral + , int16le 36 >> getInt16le >>= return . DecayVolEnv . fromIntegral + , int16le 37 >> getInt16le >>= return . SustainVolEnv . fromIntegral + , int16le 38 >> getInt16le >>= return . ReleaseVolEnv . fromIntegral + , int16le 39 >> getInt16le >>= return . KeyToVolEnvHold . fromIntegral + , int16le 40 >> getInt16le >>= return . KeyToVolEnvDecay . fromIntegral + + , int16le 41 >> getWord16le >>= return . InstIndex . fromIntegral + , do _ <- int16le 43; a <- getWord8 >>= return . fromIntegral; + b <- getWord8 >>= return . fromIntegral; return $ KeyRange a b; + , do _ <- int16le 44; a <- getWord8 >>= return . fromIntegral; + b <- getWord8 >>= return . fromIntegral; return $ VelRange a b; + + , int16le 45 >> getInt16le >>= return . LoopStartAddressCoarseOffset . fromIntegral + , int16le 46 >> getWord16le >>= return . Key . fromIntegral + , int16le 47 >> getWord16le >>= return . Vel . fromIntegral + , int16le 48 >> getInt16le >>= return . InitAtten . fromIntegral + , int16le 50 >> getInt16le >>= return . LoopEndAddressCoarseOffset . fromIntegral + + , int16le 51 >> getInt16le >>= return . CoarseTune . fromIntegral + , int16le 52 >> getInt16le >>= return . FineTune . fromIntegral + , int16le 53 >> getWord16le >>= return . SampleIndex . fromIntegral + , do _ <- int16le 54; a <- getInt16le; + case a of + 1 -> return $ SampleMode Audio.ContLoop + 3 -> return $ SampleMode Audio.PressLoop + _ -> return $ SampleMode Audio.NoLoop + , int16le 56 >> getInt16le >>= return . ScaleTuning . fromIntegral + , int16le 57 >> getInt16le >>= return . ExclusiveClass . fromIntegral + + , int16le 58 >> getWord16le >>= return . RootKey . fromIntegral + , do p1 <- getInt16le; p2 <- getInt16le; + return $ ReservedGen (fromIntegral p1) (fromIntegral p2)] + + +buildGen :: Generator -> Builder +buildGen g = mconcat $ case g of + StartAddressOffset i -> [putInt16le 0, putInt16le $ fromIntegral i] + EndAddressOffset i -> [putInt16le 1, putInt16le $ fromIntegral i] + LoopStartAddressOffset i -> [putInt16le 2, putInt16le $ fromIntegral i] + LoopEndAddressOffset i -> [putInt16le 3, putInt16le $ fromIntegral i] + StartAddressCoarseOffset i -> [putInt16le 4, putInt16le $ fromIntegral i] + ModLfoToPitch i -> [putInt16le 5, putInt16le $ fromIntegral i] + VibLfoToPitch i -> [putInt16le 6, putInt16le $ fromIntegral i] + ModEnvToPitch i -> [putInt16le 7, putInt16le $ fromIntegral i] + + InitFc i -> [putInt16le 8, putInt16le $ fromIntegral i] + InitQ i -> [putInt16le 9, putInt16le $ fromIntegral i] + ModLfoToFc i -> [putInt16le 10, putInt16le $ fromIntegral i] + ModEnvToFc i -> [putInt16le 11, putInt16le $ fromIntegral i] + + EndAddressCoarseOffset i -> [putInt16le 12, putInt16le $ fromIntegral i] + ModLfoToVol i -> [putInt16le 13, putInt16le $ fromIntegral i] + + Chorus i -> [putInt16le 15, putInt16le $ fromIntegral i] + Reverb i -> [putInt16le 16, putInt16le $ fromIntegral i] + Pan i -> [putInt16le 17, putInt16le $ fromIntegral i] + + DelayModLfo i -> [putInt16le 21, putInt16le $ fromIntegral i] + FreqModLfo i -> [putInt16le 22, putInt16le $ fromIntegral i] + + DelayVibLfo i -> [putInt16le 23, putInt16le $ fromIntegral i] + FreqVibLfo i -> [putInt16le 24, putInt16le $ fromIntegral i] + + DelayModEnv i -> [putInt16le 25, putInt16le $ fromIntegral i] + AttackModEnv i -> [putInt16le 26, putInt16le $ fromIntegral i] + HoldModEnv i -> [putInt16le 27, putInt16le $ fromIntegral i] + DecayModEnv i -> [putInt16le 28, putInt16le $ fromIntegral i] + SustainModEnv i -> [putInt16le 29, putInt16le $ fromIntegral i] + ReleaseModEnv i -> [putInt16le 30, putInt16le $ fromIntegral i] + KeyToModEnvHold i -> [putInt16le 31, putInt16le $ fromIntegral i] + KeyToModEnvDecay i -> [putInt16le 32, putInt16le $ fromIntegral i] + + DelayVolEnv i -> [putInt16le 33, putInt16le $ fromIntegral i] + AttackVolEnv i -> [putInt16le 34, putInt16le $ fromIntegral i] + HoldVolEnv i -> [putInt16le 35, putInt16le $ fromIntegral i] + DecayVolEnv i -> [putInt16le 36, putInt16le $ fromIntegral i] + SustainVolEnv i -> [putInt16le 37, putInt16le $ fromIntegral i] + ReleaseVolEnv i -> [putInt16le 38, putInt16le $ fromIntegral i] + KeyToVolEnvHold i -> [putInt16le 39, putInt16le $ fromIntegral i] + KeyToVolEnvDecay i -> [putInt16le 40, putInt16le $ fromIntegral i] + + InstIndex i -> [putInt16le 41, putWord16le $ fromIntegral i] + KeyRange a b -> [putInt16le 43, putWord8 $ fromIntegral a, putWord8 $ fromIntegral b] + VelRange a b -> [putInt16le 44, putWord8 $ fromIntegral a, putWord8 $ fromIntegral b] + + LoopStartAddressCoarseOffset i -> [putInt16le 45, putInt16le $ fromIntegral i] + Key i -> [putWord16le 46, putInt16le $ fromIntegral i] + Vel i -> [putWord16le 47, putInt16le $ fromIntegral i] + InitAtten i -> [putInt16le 48, putInt16le $ fromIntegral i] + LoopEndAddressCoarseOffset i -> [putInt16le 50, putInt16le $ fromIntegral i] + + CoarseTune i -> [putInt16le 51, putInt16le $ fromIntegral i] + FineTune i -> [putInt16le 52, putInt16le $ fromIntegral i] + SampleIndex i -> [putInt16le 53, putWord16le $ fromIntegral i] + + SampleMode Audio.ContLoop -> [putInt16le 54, putInt16le 1] + SampleMode Audio.PressLoop -> [putInt16le 54, putInt16le 3] + SampleMode Audio.NoLoop -> [putInt16le 54, putInt16le 2] + --56 + ScaleTuning i -> [putInt16le 56, putInt16le $ fromIntegral i] + ExclusiveClass i -> [putInt16le 57, putInt16le $ fromIntegral i] + + RootKey i -> [putInt16le 58, putWord16le $ fromIntegral i] + ReservedGen i1 i2 -> [putInt16le $ fromIntegral i1, putInt16le $ fromIntegral i2] + +parseInst :: Parser Inst +parseInst = do + instName' <- getLazyByteString 20 + >>= return . map w2c . L.unpack . L.takeWhile ( /= 0) + instBagNdx' <- getWord16le >>= return . fromIntegral + return $ Inst { + instName = instName' + , instBagNdx = instBagNdx'} + +buildInst :: Inst -> Builder +buildInst i = mconcat [ + putString $ instName i + , mconcat $ replicate (20 - length (instName i)) (putWord8 0) + , putWord16le $ fromIntegral $ instBagNdx i] + +parseShdr :: Parser Shdr +parseShdr = do + sampleName' <- getLazyByteString 20 + >>= return . map w2c . L.unpack . L.takeWhile ( /= 0) + start' <- getWord32le + end' <- getWord32le + startLoop' <- getWord32le + endLoop' <- getWord32le + sampleRate' <- getWord32le + originalPitch' <- getWord8 + pitchCorrection' <- getInt8 + sampleLink' <- getWord16le + sampleType' <- getWord16le + return $ Shdr { + sampleName = sampleName' + , start = fromIntegral start' + , end = fromIntegral end' + , startLoop = fromIntegral startLoop' + , endLoop = fromIntegral endLoop' + , sampleRate = fromIntegral sampleRate' + , originalPitch = fromIntegral originalPitch' + , pitchCorrection = fromIntegral pitchCorrection' + , sampleLink = fromIntegral sampleLink' + , sampleType = fromIntegral sampleType' + } + +buildShdr :: Shdr -> Builder +buildShdr shdr = mconcat [ + putString $ sampleName shdr + , mconcat $ replicate (20 - length (sampleName shdr)) (putWord8 0) + , putWord32le $ fromIntegral $ start shdr + , putWord32le $ fromIntegral $ end shdr + , putWord32le $ fromIntegral $ startLoop shdr + , putWord32le $ fromIntegral $ endLoop shdr + , putWord32le $ fromIntegral $ sampleRate shdr + , putWord8 $ fromIntegral $ originalPitch shdr + , putInt8 $ fromIntegral $ pitchCorrection shdr + , putWord16le $ fromIntegral $ sampleLink shdr + , putWord16le $ fromIntegral $ sampleType shdr + ]
src/Codec/Wav.hs view
@@ -1,175 +1,175 @@--------------------------------------------------------------------------------- |--- Module : Codec.Wav--- Copyright : George Giorgidze--- License : BSD3--- --- Maintainer : George Giorgidze <http://cs.nott.ac.uk/~ggg/>--- Stability : Experimental--- Portability : Portable------ Module for reading and writting of WAVE (.wav) audio files.-----------------------------------------------------------------------------------{-# LANGUAGE FlexibleContexts #-}--module Codec.Wav (- importFile- , exportFile- , parseWav- , buildWav- , AudibleInWav(..)- ) where--import Data.Audio-import Codec.ByteString.Parser-import Codec.ByteString.Builder--import Data.Word-import Data.Int-import qualified Data.ByteString.Lazy as L-import Data.Monoid (mconcat)-import Data.Array.Unboxed-import Data.Array.IO-import Data.Bits--import Control.Monad-import Control.Applicative--class AudibleInWav a where- parseSample :: Parser a- buildSample :: a -> Builder- bitsPerSample :: a -> Int--instance AudibleInWav Word8 where- parseSample = getWord8- buildSample = putWord8- bitsPerSample _ = 8--instance AudibleInWav Int16 where- parseSample = getInt16le- buildSample = putInt16le- bitsPerSample _ = 16--instance AudibleInWav Int32 where- parseSample = getInt32le- buildSample = putInt32le- bitsPerSample _ = 32--instance AudibleInWav Int64 where- parseSample = getInt64le- buildSample = putInt64le- bitsPerSample _ = 64--parserSelector :: (Audible a, AudibleInWav a) => Int -> Parser a-parserSelector 8 = (parseSample :: Parser Word8) >>= return . fromSample . toSample-parserSelector 16 = (parseSample :: Parser Int16) >>= return . fromSample . toSample-parserSelector 24 = ((getWord24le >>= return . fromIntegral . (flip shiftL) 8) :: Parser Int32) >>= return . fromSample . toSample-parserSelector 32 = (parseSample :: Parser Int32) >>= return . fromSample . toSample-parserSelector 64 = (parseSample :: Parser Int64) >>= return . fromSample . toSample-parserSelector n = fail $ show n ++ " bitsPerSample is not supported"--bytesPerSample :: (AudibleInWav a) => a -> Int-bytesPerSample a = div (bitsPerSample a) 8 --importFile :: (MArray IOUArray a IO, IArray UArray a, Audible a, AudibleInWav a) => FilePath -> IO (Either String (Audio a))-importFile n = do- bs <- L.readFile n- return $! runParser parseWav bs--exportFile :: (IArray UArray a, Audible a, AudibleInWav a) => FilePath -> Audio a -> IO ()-exportFile f w = L.writeFile f (toLazyByteString $ buildWav w)---- All numerical values are stored in little endian format----parseWav :: (MArray IOUArray a IO, IArray UArray a, Audible a, AudibleInWav a) => Parser (Audio a)-parseWav = do- _ <- string "RIFF"--- n <- remaining--- expect (\w -> fromIntegral w == n - 4) getWord32le- _ <- getWord32le -- chunkSize- _ <- string "WAVE"- _ <- many parseUnknownChunk- (sampleRate1,channelNumber1,bitsPerSample1) <- parseFmt- _ <- many parseUnknownChunk- sampleData1 <- parseData channelNumber1 bitsPerSample1- return $! (Audio sampleRate1 channelNumber1 sampleData1)--buildWav :: (IArray UArray a, Audible a, AudibleInWav a) => Audio a -> Builder-buildWav a = mconcat [- putString "RIFF"- , putWord32le $ fromIntegral chunkSize- , putString "WAVE"- , buildFmt a- , buildData a]- where- sd = sampleData a- chunkSize =- 4 -- "WAVE" - + 24 -- fmt chunk- + 8 -- data chunk header - + (fromIntegral $ sampleNumber sd) * (bytesPerSample $ sampleType sd)- -- sample data--parseFmt :: Parser (Int,Int,Int)-parseFmt = do- _ <- string "fmt "- chunkSize <- getWord32le >>= return . fromIntegral- _ <- word16le 1 -- compression code- channelNumber1 <- getWord16le >>= return . fromIntegral- sampleRate1 <- getWord32le >>= return . fromIntegral- avgBytesPerSec <- getWord32le >>= return . fromIntegral- bytesPerSampleSlice <- getWord16le >>= return . fromIntegral- when (avgBytesPerSec /= sampleRate1 * bytesPerSampleSlice) $ - fail "avgBytesPerSec /= sampleRate * bytesPerSampleSlise"- bitsPerSample1 <- expect (\w -> (mod w 8 == 0) && w <= 64) getWord16le >>= return . fromIntegral- when (bytesPerSampleSlice /= (div bitsPerSample1 8) * channelNumber1) $ - fail "bytesPerSampleSlice /= (div bitsPerSample 8) * channelNumber"- skip (chunkSize - 16) -- skip extra fromat bytes- return $! (sampleRate1,channelNumber1,bitsPerSample1)--buildFmt :: (IArray UArray a, Audible a, AudibleInWav a) => Audio a -> Builder-buildFmt a = mconcat [- putString $ "fmt "- , putWord32le $ 16 -- chunk size- , putWord16le $ 1 -- compression code- , putWord16le $ fromIntegral $ channelNumber a- , putWord32le $ fromIntegral $ sampleRate a- , putWord32le $ fromIntegral $ avgBytesPerSec- , putWord16le $ fromIntegral $ bytesPerSampleSlice- , putWord16le $ fromIntegral $ bitsPS- ]- where- sd = sampleData a- bitsPS = bitsPerSample $ sampleType sd- bytesPS = bytesPerSample $ sampleType sd- bytesPerSampleSlice = bytesPS * channelNumber a- avgBytesPerSec = sampleRate a * bytesPerSampleSlice- -parseData :: (MArray IOUArray a IO, IArray UArray a, Audible a, AudibleInWav a)- => Int -> Int -> Parser (SampleData a)-parseData cn bitsPS = do- _ <- string "data"- let bytesPS = div bitsPS 8- chunkSize <- expect (\w -> mod (fromIntegral w) bytesPS == 0) getWord32le- >>= return . fromIntegral- let sn = fromIntegral $ div chunkSize bytesPS- when (mod sn (fromIntegral cn) /= 0) $ fail "mod sampelNumber channelNumber /= 0)"- parseSampleData sn (parserSelector bitsPS) - -buildData :: (IArray UArray a, Audible a, AudibleInWav a) => Audio a -> Builder-buildData a = mconcat [- putString "data"- , putWord32le $ fromIntegral $ chunkSize- , buildSampleData buildSample sd]- where- sd = sampleData a- chunkSize = (fromIntegral $ sampleNumber sd) * (bytesPerSample $ sampleType sd)--parseUnknownChunk :: Parser ()-parseUnknownChunk = do- _ <- expect (\s -> s /= "data" && s /= "fmt ") (getString 4)- chunkSize <- getWord32le- skip(fromIntegral chunkSize)- return ()+----------------------------------------------------------------------------- +-- | +-- Module : Codec.Wav +-- Copyright : George Giorgidze +-- License : BSD3 +-- +-- Maintainer : George Giorgidze <http://cs.nott.ac.uk/~ggg/> +-- Stability : Experimental +-- Portability : Portable +-- +-- Module for reading and writting of WAVE (.wav) audio files. +-- +----------------------------------------------------------------------------- + +{-# LANGUAGE FlexibleContexts #-} + +module Codec.Wav ( + importFile + , exportFile + , parseWav + , buildWav + , AudibleInWav(..) + ) where + +import Data.Audio +import Codec.ByteString.Parser +import Codec.ByteString.Builder + +import Data.Word +import Data.Int +import qualified Data.ByteString.Lazy as L +import Data.Monoid (mconcat) +import Data.Array.Unboxed +import Data.Array.IO +import Data.Bits + +import Control.Monad +import Control.Applicative + +class AudibleInWav a where + parseSample :: Parser a + buildSample :: a -> Builder + bitsPerSample :: a -> Int + +instance AudibleInWav Word8 where + parseSample = getWord8 + buildSample = putWord8 + bitsPerSample _ = 8 + +instance AudibleInWav Int16 where + parseSample = getInt16le + buildSample = putInt16le + bitsPerSample _ = 16 + +instance AudibleInWav Int32 where + parseSample = getInt32le + buildSample = putInt32le + bitsPerSample _ = 32 + +instance AudibleInWav Int64 where + parseSample = getInt64le + buildSample = putInt64le + bitsPerSample _ = 64 + +parserSelector :: (Audible a, AudibleInWav a) => Int -> Parser a +parserSelector 8 = (parseSample :: Parser Word8) >>= return . fromSample . toSample +parserSelector 16 = (parseSample :: Parser Int16) >>= return . fromSample . toSample +parserSelector 24 = ((getWord24le >>= return . fromIntegral . (flip shiftL) 8) :: Parser Int32) >>= return . fromSample . toSample +parserSelector 32 = (parseSample :: Parser Int32) >>= return . fromSample . toSample +parserSelector 64 = (parseSample :: Parser Int64) >>= return . fromSample . toSample +parserSelector n = fail $ show n ++ " bitsPerSample is not supported" + +bytesPerSample :: (AudibleInWav a) => a -> Int +bytesPerSample a = div (bitsPerSample a) 8 + +importFile :: (MArray IOUArray a IO, IArray UArray a, Audible a, AudibleInWav a) => FilePath -> IO (Either String (Audio a)) +importFile n = do + bs <- L.readFile n + return $! runParser parseWav bs + +exportFile :: (IArray UArray a, Audible a, AudibleInWav a) => FilePath -> Audio a -> IO () +exportFile f w = L.writeFile f (toLazyByteString $ buildWav w) + +-- All numerical values are stored in little endian format +-- +parseWav :: (MArray IOUArray a IO, IArray UArray a, Audible a, AudibleInWav a) => Parser (Audio a) +parseWav = do + _ <- string "RIFF" +-- n <- remaining +-- expect (\w -> fromIntegral w == n - 4) getWord32le + _ <- getWord32le -- chunkSize + _ <- string "WAVE" + _ <- many parseUnknownChunk + (sampleRate1,channelNumber1,bitsPerSample1) <- parseFmt + _ <- many parseUnknownChunk + sampleData1 <- parseData channelNumber1 bitsPerSample1 + return $! (Audio sampleRate1 channelNumber1 sampleData1) + +buildWav :: (IArray UArray a, Audible a, AudibleInWav a) => Audio a -> Builder +buildWav a = mconcat [ + putString "RIFF" + , putWord32le $ fromIntegral chunkSize + , putString "WAVE" + , buildFmt a + , buildData a] + where + sd = sampleData a + chunkSize = + 4 -- "WAVE" + + 24 -- fmt chunk + + 8 -- data chunk header + + (fromIntegral $ sampleNumber sd) * (bytesPerSample $ sampleType sd) + -- sample data + +parseFmt :: Parser (Int,Int,Int) +parseFmt = do + _ <- string "fmt " + chunkSize <- getWord32le >>= return . fromIntegral + _ <- word16le 1 -- compression code + channelNumber1 <- getWord16le >>= return . fromIntegral + sampleRate1 <- getWord32le >>= return . fromIntegral + avgBytesPerSec <- getWord32le >>= return . fromIntegral + bytesPerSampleSlice <- getWord16le >>= return . fromIntegral + when (avgBytesPerSec /= sampleRate1 * bytesPerSampleSlice) $ + fail "avgBytesPerSec /= sampleRate * bytesPerSampleSlise" + bitsPerSample1 <- expect (\w -> (mod w 8 == 0) && w <= 64) getWord16le >>= return . fromIntegral + when (bytesPerSampleSlice /= (div bitsPerSample1 8) * channelNumber1) $ + fail "bytesPerSampleSlice /= (div bitsPerSample 8) * channelNumber" + skip (chunkSize - 16) -- skip extra fromat bytes + return $! (sampleRate1,channelNumber1,bitsPerSample1) + +buildFmt :: (IArray UArray a, Audible a, AudibleInWav a) => Audio a -> Builder +buildFmt a = mconcat [ + putString $ "fmt " + , putWord32le $ 16 -- chunk size + , putWord16le $ 1 -- compression code + , putWord16le $ fromIntegral $ channelNumber a + , putWord32le $ fromIntegral $ sampleRate a + , putWord32le $ fromIntegral $ avgBytesPerSec + , putWord16le $ fromIntegral $ bytesPerSampleSlice + , putWord16le $ fromIntegral $ bitsPS + ] + where + sd = sampleData a + bitsPS = bitsPerSample $ sampleType sd + bytesPS = bytesPerSample $ sampleType sd + bytesPerSampleSlice = bytesPS * channelNumber a + avgBytesPerSec = sampleRate a * bytesPerSampleSlice + +parseData :: (MArray IOUArray a IO, IArray UArray a, Audible a, AudibleInWav a) + => Int -> Int -> Parser (SampleData a) +parseData cn bitsPS = do + _ <- string "data" + let bytesPS = div bitsPS 8 + chunkSize <- expect (\w -> mod (fromIntegral w) bytesPS == 0) getWord32le + >>= return . fromIntegral + let sn = fromIntegral $ div chunkSize bytesPS + when (mod sn (fromIntegral cn) /= 0) $ fail "mod sampelNumber channelNumber /= 0)" + parseSampleData sn (parserSelector bitsPS) + +buildData :: (IArray UArray a, Audible a, AudibleInWav a) => Audio a -> Builder +buildData a = mconcat [ + putString "data" + , putWord32le $ fromIntegral $ chunkSize + , buildSampleData buildSample sd] + where + sd = sampleData a + chunkSize = (fromIntegral $ sampleNumber sd) * (bytesPerSample $ sampleType sd) + +parseUnknownChunk :: Parser () +parseUnknownChunk = do + _ <- expect (\s -> s /= "data" && s /= "fmt ") (getString 4) + chunkSize <- getWord32le + skip(fromIntegral chunkSize) + return ()
src/Data/Audio.hs view
@@ -1,145 +1,145 @@--------------------------------------------------------------------------------- |--- Module : Data.Audio--- Copyright : George Giorgidze--- License : BSD3------ Maintainer : George Giorgidze <http://cs.nott.ac.uk/~ggg/>--- Stability : Experimental--- Portability : Portable------ General purpose data type for representing an audio data.-----------------------------------------------------------------------------------{-# LANGUAGE FlexibleContexts, UndecidableInstances #-}-{-# OPTIONS_GHC -fno-warn-orphans #-}--module Data.Audio (- Sample- , Audio (..)- , SampleData- , SampleMode(..)- , sampleType- , sampleNumber- , convert- , parseSampleData- , buildSampleData- , Audible- , toSample- , fromSample- ) where--import Data.Array.IO (MArray, IOUArray, newArray_, writeArray)-import Data.Array.Unsafe (unsafeFreeze, unsafeThaw)--import Codec.Internal.Arbitrary-import Codec.ByteString.Parser-import Codec.ByteString.Builder--import Test.QuickCheck-import System.IO.Unsafe-import Data.Array.Unboxed-import Data.Word-import Data.Int-import Data.Monoid (mconcat)--type Sample = Double-type SampleData a = UArray Int a--class Audible a where- toSample :: a -> Sample- fromSample :: Sample -> a---- It is required that sampleNummer `mod` channelNumber == 0-data Audio a = Audio {- sampleRate :: Int- , channelNumber :: Int- , sampleData :: SampleData a- }--instance (Eq a, IArray UArray a) => Eq (Audio a) where- a1 == a2 = and [- sampleRate a1 == sampleRate a2- , channelNumber a1 == channelNumber a2- , assocs (sampleData a1) == assocs (sampleData a2)]---instance (Show a, IArray UArray a) => Show (Audio a) where- show a = "Sample Rate: " ++ (show $ sampleRate a) ++ "\n" ++- "Channel Number: " ++ (show $ channelNumber a) ++ "\n" ++- "Sample Data Array Bounds: " ++ (show $ bounds $ sampleData a)---instance (Arbitrary a, IArray UArray a) => Arbitrary (Audio a) where- arbitrary = do- sr <- choose (1, 44100 * 8)- cn <- choose (1, 8)- sn <- choose (1, 128) >>= return . (fromIntegral cn *)- sd <- arrayGen sn- return (Audio sr cn sd)--sampleNumber :: (IArray UArray a) => SampleData a -> Int-sampleNumber sd = (snd (bounds sd)) + 1--sampleType :: (IArray UArray a) => SampleData a -> a-sampleType sd = undefined `asTypeOf` (sd ! 0)--convert :: (Audible a, Audible b, IArray UArray a, IArray UArray b) => SampleData a -> SampleData b-convert sd = amap (fromSample . toSample) sd--parseSampleData :: (MArray IOUArray a IO, IArray UArray a) => Int -> Parser a -> Parser (SampleData a)-parseSampleData sn p = pAux 0 unsafeNewArray- where- pAux i acc | (i == sn) = seq i $ seq acc $ return acc- pAux i acc | otherwise = seq i $ seq acc $ do- s <- p- seq s (pAux (i + 1) (unsafeWriteArray acc i s))-- unsafeFreezeAux :: (MArray IOUArray a IO, IArray UArray a) => IOUArray Int a -> IO (SampleData a)- unsafeFreezeAux = unsafeFreeze-- unsafeNewArray = unsafePerformIO (newArray_ (0, sn - 1) >>= unsafeFreezeAux )- unsafeWriteArray a i e = unsafePerformIO (do a' <- unsafeThaw a; writeArray a' i e; unsafeFreezeAux a';)---buildSampleData :: (IArray UArray a) => (a -> Builder) -> SampleData a -> Builder-buildSampleData b = mconcat . map b . elems--instance Audible Int8 where- toSample a = (fromIntegral a) / (2 ** 7)- fromSample s = round $ s * (2 ** 7)-instance Audible Int16 where- toSample a = (fromIntegral a) / (2 ** 15)- fromSample s = round $ s * (2 ** 15)-instance Audible Int32 where- toSample a = (fromIntegral a) / (2 ** 31)- fromSample s = round $ s * (2 ** 31)-instance Audible Int64 where- toSample a = (fromIntegral a) / (2 ** 63)- fromSample s = round $ s * (2 ** 63)-instance Audible Word8 where- toSample a = (fromIntegral a) / (2 ** 7) - 1.0- fromSample s = round $ (s + 1.0) * (2 ** 7)-instance Audible Word16 where- toSample a = (fromIntegral a) / (2 ** 15) - 1.0- fromSample s = round $ (s + 1.0) * (2 ** 15)-instance Audible Word32 where- toSample a = (fromIntegral a) / (2 ** 31) - 1.0- fromSample s = round $ (s + 1.0) * (2 ** 31)-instance Audible Word64 where- toSample a = (fromIntegral a) / (2 ** 63) - 1.0- fromSample s = round $ (s + 1.0) * (2 ** 63)--instance Audible Float where- toSample = realToFrac- fromSample = realToFrac--instance Audible Double where- toSample = id- fromSample = id--data SampleMode = NoLoop | ContLoop | PressLoop deriving (Eq, Show)--instance Arbitrary SampleMode where- arbitrary = oneof [return NoLoop, return ContLoop, return PressLoop]+----------------------------------------------------------------------------- +-- | +-- Module : Data.Audio +-- Copyright : George Giorgidze +-- License : BSD3 +-- +-- Maintainer : George Giorgidze <http://cs.nott.ac.uk/~ggg/> +-- Stability : Experimental +-- Portability : Portable +-- +-- General purpose data type for representing an audio data. +-- +----------------------------------------------------------------------------- + +{-# LANGUAGE FlexibleContexts, UndecidableInstances #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Data.Audio ( + Sample + , Audio (..) + , SampleData + , SampleMode(..) + , sampleType + , sampleNumber + , convert + , parseSampleData + , buildSampleData + , Audible + , toSample + , fromSample + ) where + +import Data.Array.IO (MArray, IOUArray, newArray_, writeArray) +import Data.Array.Unsafe (unsafeFreeze, unsafeThaw) + +import Codec.Internal.Arbitrary +import Codec.ByteString.Parser +import Codec.ByteString.Builder + +import Test.QuickCheck +import System.IO.Unsafe +import Data.Array.Unboxed +import Data.Word +import Data.Int +import Data.Monoid (mconcat) + +type Sample = Double +type SampleData a = UArray Int a + +class Audible a where + toSample :: a -> Sample + fromSample :: Sample -> a + +-- It is required that sampleNummer `mod` channelNumber == 0 +data Audio a = Audio { + sampleRate :: Int + , channelNumber :: Int + , sampleData :: SampleData a + } + +instance (Eq a, IArray UArray a) => Eq (Audio a) where + a1 == a2 = and [ + sampleRate a1 == sampleRate a2 + , channelNumber a1 == channelNumber a2 + , assocs (sampleData a1) == assocs (sampleData a2)] + + +instance (Show a, IArray UArray a) => Show (Audio a) where + show a = "Sample Rate: " ++ (show $ sampleRate a) ++ "\n" ++ + "Channel Number: " ++ (show $ channelNumber a) ++ "\n" ++ + "Sample Data Array Bounds: " ++ (show $ bounds $ sampleData a) + + +instance (Arbitrary a, IArray UArray a) => Arbitrary (Audio a) where + arbitrary = do + sr <- choose (1, 44100 * 8) + cn <- choose (1, 8) + sn <- choose (1, 128) >>= return . (fromIntegral cn *) + sd <- arrayGen sn + return (Audio sr cn sd) + +sampleNumber :: (IArray UArray a) => SampleData a -> Int +sampleNumber sd = (snd (bounds sd)) + 1 + +sampleType :: (IArray UArray a) => SampleData a -> a +sampleType sd = undefined `asTypeOf` (sd ! 0) + +convert :: (Audible a, Audible b, IArray UArray a, IArray UArray b) => SampleData a -> SampleData b +convert sd = amap (fromSample . toSample) sd + +parseSampleData :: (MArray IOUArray a IO, IArray UArray a) => Int -> Parser a -> Parser (SampleData a) +parseSampleData sn p = pAux 0 unsafeNewArray + where + pAux i acc | (i == sn) = seq i $ seq acc $ return acc + pAux i acc | otherwise = seq i $ seq acc $ do + s <- p + seq s (pAux (i + 1) (unsafeWriteArray acc i s)) + + unsafeFreezeAux :: (MArray IOUArray a IO, IArray UArray a) => IOUArray Int a -> IO (SampleData a) + unsafeFreezeAux = unsafeFreeze + + unsafeNewArray = unsafePerformIO (newArray_ (0, sn - 1) >>= unsafeFreezeAux ) + unsafeWriteArray a i e = unsafePerformIO (do a' <- unsafeThaw a; writeArray a' i e; unsafeFreezeAux a';) + + +buildSampleData :: (IArray UArray a) => (a -> Builder) -> SampleData a -> Builder +buildSampleData b = mconcat . map b . elems + +instance Audible Int8 where + toSample a = (fromIntegral a) / (2 ** 7) + fromSample s = round $ s * (2 ** 7) +instance Audible Int16 where + toSample a = (fromIntegral a) / (2 ** 15) + fromSample s = round $ s * (2 ** 15) +instance Audible Int32 where + toSample a = (fromIntegral a) / (2 ** 31) + fromSample s = round $ s * (2 ** 31) +instance Audible Int64 where + toSample a = (fromIntegral a) / (2 ** 63) + fromSample s = round $ s * (2 ** 63) +instance Audible Word8 where + toSample a = (fromIntegral a) / (2 ** 7) - 1.0 + fromSample s = round $ (s + 1.0) * (2 ** 7) +instance Audible Word16 where + toSample a = (fromIntegral a) / (2 ** 15) - 1.0 + fromSample s = round $ (s + 1.0) * (2 ** 15) +instance Audible Word32 where + toSample a = (fromIntegral a) / (2 ** 31) - 1.0 + fromSample s = round $ (s + 1.0) * (2 ** 31) +instance Audible Word64 where + toSample a = (fromIntegral a) / (2 ** 63) - 1.0 + fromSample s = round $ (s + 1.0) * (2 ** 63) + +instance Audible Float where + toSample = realToFrac + fromSample = realToFrac + +instance Audible Double where + toSample = id + fromSample = id + +data SampleMode = NoLoop | ContLoop | PressLoop deriving (Eq, Show) + +instance Arbitrary SampleMode where + arbitrary = oneof [return NoLoop, return ContLoop, return PressLoop]