packages feed

wave-0.2.0: tests/Codec/Audio/WaveSpec.hs

{-# LANGUAGE BangPatterns         #-}
{-# LANGUAGE OverloadedStrings    #-}
{-# LANGUAGE RecordWildCards      #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Codec.Audio.WaveSpec
  ( spec )
where

import Codec.Audio.Wave
import Data.Word
import System.IO
import System.IO.Temp (withSystemTempFile)
import Test.Hspec
import Test.QuickCheck
import qualified Data.ByteString as B
import qualified Data.Set        as E

-- The test suite has two parts. In the first part we establish that the
-- library is capable of reading various sample files. In the second part,
-- we generate random WAVE files and write them to temporary files, then
-- read the files back and compare. This way we ensure that 1) the library
-- supports reading arbitrary valid files 2) since reading is valid, we can
-- assume that writing is valid too if the read results look OK.
--
-- Should any problems be discovered in the future, it's simple to extend
-- the first part of the test suite to check for those cases.

spec :: Spec
spec = do

  describe "vanilla WAVE" $ do
    it "2 channels  8000 Hz  8 bit" $ do
      w@Wave {..} <- readWaveFile "audio-samples/2ch-8000hz-8bit.wav"
      waveFileFormat      `shouldBe` WaveVanilla
      waveSampleRate      `shouldBe` 8000
      waveSampleFormat    `shouldBe` SampleFormatPcmInt 8
      waveChannelMask     `shouldBe` speakerStereo
      waveDataOffset      `shouldBe` 44
      waveDataSize        `shouldBe` 11376
      waveSamplesTotal    `shouldBe` 5688
      waveOtherChunks     `shouldBe` []
      waveByteRate      w `shouldBe` 16000
      waveBitRate       w `shouldBe` 128
      waveBitsPerSample w `shouldBe` 8
      waveBlockAlign    w `shouldBe` 2
      waveChannels      w `shouldBe` 2
      waveDuration      w `shouldBe` 0.711

    it "2 channels 11025 Hz 24 bit" $ do
      w@Wave {..} <- readWaveFile "audio-samples/2ch-11025hz-24bit.wav"
      waveFileFormat      `shouldBe` WaveVanilla
      waveSampleRate      `shouldBe` 11025
      waveSampleFormat    `shouldBe` SampleFormatPcmInt 24
      waveChannelMask     `shouldBe` speakerStereo
      waveDataOffset      `shouldBe` 44
      waveDataSize        `shouldBe` 23274
      waveSamplesTotal    `shouldBe` 3879
      waveOtherChunks     `shouldBe` []
      waveByteRate      w `shouldBe` 66150
      waveBitRate       w `shouldBe` 529.2
      waveBitsPerSample w `shouldBe` 24
      waveBlockAlign    w `shouldBe` 6
      waveChannels      w `shouldBe` 2
      waveDuration      w `shouldBe` 0.35183673469387755

    it "1 channel  44100 Hz 16 bit" $ do
      w@Wave {..} <- readWaveFile "audio-samples/1ch-44100hz-16bit.wav"
      waveFileFormat      `shouldBe` WaveVanilla
      waveSampleRate      `shouldBe` 44100
      waveSampleFormat    `shouldBe` SampleFormatPcmInt 16
      waveChannelMask     `shouldBe` speakerMono
      waveDataOffset      `shouldBe` 44
      waveDataSize        `shouldBe` 5046
      waveSamplesTotal    `shouldBe` 2523
      waveOtherChunks     `shouldBe` []
      waveByteRate      w `shouldBe` 88200
      waveBitRate       w `shouldBe` 705.6
      waveBitsPerSample w `shouldBe` 16
      waveBlockAlign    w `shouldBe` 2
      waveChannels      w `shouldBe` 1
      waveDuration      w `shouldBe` 0.0572108843537415

    it "1 channel  48000 Hz 32 bit float" $ do
      w@Wave {..} <- readWaveFile "audio-samples/1ch-48000hz-32bit-float.wav"
      waveFileFormat      `shouldBe` WaveVanilla
      waveSampleRate      `shouldBe` 48000
      waveSampleFormat    `shouldBe` SampleFormatIeeeFloat32Bit
      waveChannelMask     `shouldBe` speakerMono
      waveDataOffset      `shouldBe` 80
      waveDataSize        `shouldBe` 48140
      waveSamplesTotal    `shouldBe` 12035
      waveOtherChunks     `shouldBe`
        [("PEAK","\SOH\NUL\NUL\NUL\139\214FX\205\204L?,\SOH\NUL\NUL")]
      waveByteRate      w `shouldBe` 192000
      waveBitRate       w `shouldBe` 1536.0
      waveBitsPerSample w `shouldBe` 32
      waveBlockAlign    w `shouldBe` 4
      waveChannels      w `shouldBe` 1
      waveDuration      w `shouldBe` 0.25072916666666667

    it "1 channel  16000 Hz 64 bit float" $ do
      w@Wave {..} <- readWaveFile "audio-samples/1ch-16000hz-64bit-float.wav"
      waveFileFormat      `shouldBe` WaveVanilla
      waveSampleRate      `shouldBe` 16000
      waveSampleFormat    `shouldBe` SampleFormatIeeeFloat64Bit
      waveChannelMask     `shouldBe` speakerMono
      waveDataOffset      `shouldBe` 80
      waveDataSize        `shouldBe` 104080
      waveSamplesTotal    `shouldBe` 13010
      waveOtherChunks     `shouldBe`
        [("PEAK","\SOH\NUL\NUL\NUL\243\215FX\205\204L?d\NUL\NUL\NUL")]
      waveByteRate      w `shouldBe` 128000
      waveBitRate       w `shouldBe` 1024.0
      waveBitsPerSample w `shouldBe` 64
      waveBlockAlign    w `shouldBe` 8
      waveChannels      w `shouldBe` 1
      waveDuration      w `shouldBe` 0.813125

  describe "vanilla WAVE with extensible fmt chunk" $ do
    it "2 channels  8000 Hz  8 bit" $ do
      w@Wave {..} <- readWaveFile "audio-samples/2ch-8000hz-8bit-x.wav"
      waveFileFormat      `shouldBe` WaveVanilla
      waveSampleRate      `shouldBe` 8000
      waveSampleFormat    `shouldBe` SampleFormatPcmInt 8
      waveChannelMask     `shouldBe` speakerStereo
      waveDataOffset      `shouldBe` 80
      waveDataSize        `shouldBe` 11376
      waveSamplesTotal    `shouldBe` 5688
      waveOtherChunks     `shouldBe` []
      waveByteRate      w `shouldBe` 16000
      waveBitRate       w `shouldBe` 128
      waveBitsPerSample w `shouldBe` 8
      waveBlockAlign    w `shouldBe` 2
      waveChannels      w `shouldBe` 2
      waveDuration      w `shouldBe` 0.711

    it "2 channels 11025 Hz 24 bit" $ do
      w@Wave {..} <- readWaveFile "audio-samples/2ch-11025hz-24bit-x.wav"
      waveFileFormat      `shouldBe` WaveVanilla
      waveSampleRate      `shouldBe` 11025
      waveSampleFormat    `shouldBe` SampleFormatPcmInt 24
      waveChannelMask     `shouldBe` speakerStereo
      waveDataOffset      `shouldBe` 80
      waveDataSize        `shouldBe` 23274
      waveSamplesTotal    `shouldBe` 3879
      waveOtherChunks     `shouldBe` []
      waveByteRate      w `shouldBe` 66150
      waveBitRate       w `shouldBe` 529.2
      waveBitsPerSample w `shouldBe` 24
      waveBlockAlign    w `shouldBe` 6
      waveChannels      w `shouldBe` 2
      waveDuration      w `shouldBe` 0.35183673469387755

    it "1 channel  44100 Hz 16 bit" $ do
      w@Wave {..} <- readWaveFile "audio-samples/1ch-44100hz-16bit-x.wav"
      waveFileFormat      `shouldBe` WaveVanilla
      waveSampleRate      `shouldBe` 44100
      waveSampleFormat    `shouldBe` SampleFormatPcmInt 16
      waveChannelMask     `shouldBe` speakerMono
      waveDataOffset      `shouldBe` 80
      waveDataSize        `shouldBe` 5046
      waveSamplesTotal    `shouldBe` 2523
      waveOtherChunks     `shouldBe` []
      waveByteRate      w `shouldBe` 88200
      waveBitRate       w `shouldBe` 705.6
      waveBitsPerSample w `shouldBe` 16
      waveBlockAlign    w `shouldBe` 2
      waveChannels      w `shouldBe` 1
      waveDuration      w `shouldBe` 0.0572108843537415

    it "1 channel  48000 Hz 32 bit float" $ do
      w@Wave {..} <- readWaveFile "audio-samples/1ch-48000hz-32bit-float-x.wav"
      waveFileFormat      `shouldBe` WaveVanilla
      waveSampleRate      `shouldBe` 48000
      waveSampleFormat    `shouldBe` SampleFormatIeeeFloat32Bit
      waveChannelMask     `shouldBe` speakerMono
      waveDataOffset      `shouldBe` 104
      waveDataSize        `shouldBe` 48140
      waveSamplesTotal    `shouldBe` 12035
      waveOtherChunks     `shouldBe`
        [("PEAK","\SOH\NUL\NUL\NUL\129\DC3GX\205\204L?,\SOH\NUL\NUL")]
      waveByteRate      w `shouldBe` 192000
      waveBitRate       w `shouldBe` 1536.0
      waveBitsPerSample w `shouldBe` 32
      waveBlockAlign    w `shouldBe` 4
      waveChannels      w `shouldBe` 1
      waveDuration      w `shouldBe` 0.25072916666666667

    it "1 channel  16000 Hz 64 bit float" $ do
      w@Wave {..} <- readWaveFile "audio-samples/1ch-16000hz-64bit-float-x.wav"
      waveFileFormat      `shouldBe` WaveVanilla
      waveSampleRate      `shouldBe` 16000
      waveSampleFormat    `shouldBe` SampleFormatIeeeFloat64Bit
      waveChannelMask     `shouldBe` speakerMono
      waveDataOffset      `shouldBe` 104
      waveDataSize        `shouldBe` 104080
      waveSamplesTotal    `shouldBe` 13010
      waveOtherChunks     `shouldBe`
        [("PEAK","\SOH\NUL\NUL\NUL\f\DC4GX\205\204L?d\NUL\NUL\NUL")]
      waveByteRate      w `shouldBe` 128000
      waveBitRate       w `shouldBe` 1024.0
      waveBitsPerSample w `shouldBe` 64
      waveBlockAlign    w `shouldBe` 8
      waveChannels      w `shouldBe` 1
      waveDuration      w `shouldBe` 0.813125

  describe "RF64 WAVE" $ do
    it "2 channels  8000 Hz  8 bit" $ do
      w@Wave {..} <- readWaveFile "audio-samples/2ch-8000hz-8bit.rf64"
      waveFileFormat      `shouldBe` WaveRF64
      waveSampleRate      `shouldBe` 8000
      waveSampleFormat    `shouldBe` SampleFormatPcmInt 8
      waveChannelMask     `shouldBe` speakerStereo
      waveDataOffset      `shouldBe` 104
      waveDataSize        `shouldBe` 11376
      waveSamplesTotal    `shouldBe` 5688
      waveOtherChunks     `shouldBe` []
      waveByteRate      w `shouldBe` 16000
      waveBitRate       w `shouldBe` 128
      waveBitsPerSample w `shouldBe` 8
      waveBlockAlign    w `shouldBe` 2
      waveChannels      w `shouldBe` 2
      waveDuration      w `shouldBe` 0.711

    it "2 channels 11025 Hz 24 bit" $ do
      w@Wave {..} <- readWaveFile "audio-samples/2ch-11025hz-24bit.rf64"
      waveFileFormat      `shouldBe` WaveRF64
      waveSampleRate      `shouldBe` 11025
      waveSampleFormat    `shouldBe` SampleFormatPcmInt 24
      waveChannelMask     `shouldBe` speakerStereo
      waveDataOffset      `shouldBe` 104
      waveDataSize        `shouldBe` 23274
      waveSamplesTotal    `shouldBe` 3879
      waveOtherChunks     `shouldBe` []
      waveByteRate      w `shouldBe` 66150
      waveBitRate       w `shouldBe` 529.2
      waveBitsPerSample w `shouldBe` 24
      waveBlockAlign    w `shouldBe` 6
      waveChannels      w `shouldBe` 2
      waveDuration      w `shouldBe` 0.35183673469387755

    it "1 channel  44100 Hz 16 bit" $ do
      w@Wave {..} <- readWaveFile "audio-samples/1ch-44100hz-16bit.rf64"
      waveFileFormat      `shouldBe` WaveRF64
      waveSampleRate      `shouldBe` 44100
      waveSampleFormat    `shouldBe` SampleFormatPcmInt 16
      waveChannelMask     `shouldBe` speakerMono
      waveDataOffset      `shouldBe` 104
      waveDataSize        `shouldBe` 5046
      waveSamplesTotal    `shouldBe` 2523
      waveOtherChunks     `shouldBe` []
      waveByteRate      w `shouldBe` 88200
      waveBitRate       w `shouldBe` 705.6
      waveBitsPerSample w `shouldBe` 16
      waveBlockAlign    w `shouldBe` 2
      waveChannels      w `shouldBe` 1
      waveDuration      w `shouldBe` 0.0572108843537415

    it "1 channel  48000 Hz 32 bit float" $ do
      w@Wave {..} <- readWaveFile "audio-samples/1ch-48000hz-32bit-float.rf64"
      waveFileFormat      `shouldBe` WaveRF64
      waveSampleRate      `shouldBe` 48000
      waveSampleFormat    `shouldBe` SampleFormatIeeeFloat32Bit
      waveChannelMask     `shouldBe` speakerMono
      waveDataOffset      `shouldBe` 104
      waveDataSize        `shouldBe` 48140
      waveSamplesTotal    `shouldBe` 12035
      waveOtherChunks     `shouldBe` []
      waveByteRate      w `shouldBe` 192000
      waveBitRate       w `shouldBe` 1536.0
      waveBitsPerSample w `shouldBe` 32
      waveBlockAlign    w `shouldBe` 4
      waveChannels      w `shouldBe` 1
      waveDuration      w `shouldBe` 0.25072916666666667

    it "1 channel  16000 Hz 64 bit float" $ do
      w@Wave {..} <- readWaveFile "audio-samples/1ch-16000hz-64bit-float.rf64"
      waveFileFormat      `shouldBe` WaveRF64
      waveSampleRate      `shouldBe` 16000
      waveSampleFormat    `shouldBe` SampleFormatIeeeFloat64Bit
      waveChannelMask     `shouldBe` speakerMono
      waveDataOffset      `shouldBe` 104
      waveDataSize        `shouldBe` 104080
      waveSamplesTotal    `shouldBe` 13010
      waveOtherChunks     `shouldBe` []
      waveByteRate      w `shouldBe` 128000
      waveBitRate       w `shouldBe` 1024.0
      waveBitsPerSample w `shouldBe` 64
      waveBlockAlign    w `shouldBe` 8
      waveChannels      w `shouldBe` 1
      waveDuration      w `shouldBe` 0.813125

  describe "writing/reading of arbitrary WAVE files" . around withSandbox $
    it "works" $ \path ->
      property $ \wave -> do
        let dataSize  = waveDataSize wave
            dataSize' =
              if odd (dataSize + totalExtraLength wave)
                then dataSize + 1
                else dataSize
            samplesTotal = pcmSamplesTotal wave { waveDataSize = dataSize' }
        writeWaveFile path wave (writeBytes dataSize)
        wave' <- readWaveFile path
        wave' `shouldBe` wave
          { waveDataOffset   = waveDataOffset wave'
          , waveDataSize     = dataSize'
          , waveSamplesTotal = samplesTotal
          , waveOtherChunks  = waveOtherChunks wave }

  describe "pre-defined speaker configurations" $ do
    let def = Wave
          { waveFileFormat   = WaveVanilla
          , waveSampleRate   = 44100
          , waveSampleFormat = SampleFormatPcmInt 16
          , waveChannelMask  = E.empty
          , waveDataOffset   = 0
          , waveDataSize     = 0
          , waveSamplesTotal = 0
          , waveOtherChunks  = []
          }
    describe "speakerMono" $
      it "has 1 channel" $
        waveChannels def { waveChannelMask = speakerMono } `shouldBe` 1
    describe "speakerStereo" $
      it "has 2 channels" $
        waveChannels def { waveChannelMask = speakerStereo } `shouldBe` 2
    describe "speakerQuad" $
      it "has 4 channels" $
        waveChannels def { waveChannelMask = speakerQuad } `shouldBe` 4
    describe "speakerSurround" $
      it "has 4 channels" $
        waveChannels def { waveChannelMask = speakerSurround } `shouldBe` 4
    describe "speaker5_1" $
      it "has 6 channels" $
        waveChannels def { waveChannelMask = speaker5_1 } `shouldBe` 6
    describe "speaker7_1" $
      it "has 8 channels" $
        waveChannels def { waveChannelMask = speaker7_1 } `shouldBe` 8
    describe "speaker5_1Surround" $
      it "has 6 channels" $
        waveChannels def { waveChannelMask = speaker5_1Surround } `shouldBe` 6
    describe "speaker7_1" $
      it "has 8 channels" $
        waveChannels def { waveChannelMask = speaker7_1Surround } `shouldBe` 8

----------------------------------------------------------------------------
-- Instances

instance Arbitrary Wave where
  arbitrary = do
    waveFileFormat <- elements [minBound..maxBound]
    waveSampleRate <- choose (0, 196000)
    waveSampleFormat <- oneof
      [ SampleFormatPcmInt <$> choose (1, 64)
      , pure SampleFormatIeeeFloat32Bit
      , pure SampleFormatIeeeFloat64Bit ]
    waveChannelMask <- arbitrary `suchThat` (not . E.null)
    let waveDataOffset = 0
    waveDataSize <- getSmall <$> arbitrary
    waveSamplesTotal <- arbitrary
    waveOtherChunks <- listOf $ do
      tag  <- B.pack <$> vectorOf 4 arbitrary
      body <- B.pack <$> arbitrary
      return (tag, body)
    return Wave {..}

instance Arbitrary SpeakerPosition where
  arbitrary = elements [minBound..maxBound]

----------------------------------------------------------------------------
-- Helpers

-- | Make a temporary copy of @audio-samples/sample.flac@ file and provide
-- the path to the file. Automatically remove the file when the test
-- finishes.

withSandbox :: ActionWith FilePath -> IO ()
withSandbox action = withSystemTempFile "sample.wav" $ \path h -> do
  hClose h
  action path

-- | Write the specified number of NULL bytes to given 'Handle'.

writeBytes :: Word64 -> Handle -> IO ()
writeBytes 0  _ = return ()
writeBytes !n h = hPutChar h '\NUL' >> writeBytes (n - 1) h

-- | Get total length of custom chunks.

totalExtraLength :: Wave -> Word64
totalExtraLength =
  fromIntegral . sum . fmap (B.length . snd) . waveOtherChunks

-- | Estimate the total number of samples for a PCM audio stream.

pcmSamplesTotal :: Wave -> Word64
pcmSamplesTotal wave =
  waveDataSize wave `quot` fromIntegral (waveBlockAlign wave)