packages feed

lambdasound-1.1: src/LambdaSound/SaveAndLoad/RawSamples.hs

{-# LANGUAGE AllowAmbiguousTypes #-}

module LambdaSound.SaveAndLoad.RawSamples
  ( saveWav,
    saveRaw,
    saveRawCompressed,
    loadWav,
    loadRaw,
    loadRawCompressed,
  )
where

import Codec.Audio.Wave
import Codec.Compression.GZip (compress, decompress)
import Control.Monad.IO.Class (liftIO)
import Data.ByteString qualified as B
import Data.ByteString.Lazy qualified as BL
import Data.Functor ((<&>))
import Data.Int (Int16, Int32, Int64)
import Data.List.NonEmpty
import Data.Massiv.Array qualified as M
import Data.Semigroup (Max (..))
import Data.Vector.Storable.ByteString (byteStringToVector, vectorToByteString)
import Data.Word (Word8)
import LambdaSound.Sound (Hz, Pulse)

-- | Save sound samples to a wave file with the given sampling frequency
saveWav :: FilePath -> Hz -> M.Vector M.S Pulse -> IO ()
saveWav filepath sampleRate floats = do
  let floatsLength = M.unSz $ M.size floats
      wave =
        Wave
          { waveFileFormat = WaveVanilla,
            waveSampleRate = round sampleRate,
            waveSampleFormat = SampleFormatIeeeFloat32Bit,
            waveChannelMask = speakerMono,
            waveDataOffset = 0,
            waveDataSize = fromIntegral floatsLength * 4,
            waveSamplesTotal = fromIntegral floatsLength,
            waveOtherChunks = []
          }
  writeWaveFile filepath wave $ \handle ->
    B.hPut handle $ vectorToByteString $ M.toStorableVector floats

-- | Load a wave file to get the sampling frequencies and the sound samples for the channels.
loadWav :: FilePath -> IO (Hz, NonEmpty (M.Vector M.S Pulse))
loadWav filePath = do
  wave <- readWaveFile filePath
  file <- B.readFile filePath
  let sourceVector = readSource wave $ B.drop (fromIntegral $ waveDataOffset wave) file
  pure (fromIntegral $ waveSampleRate wave, splitInChannels wave sourceVector)
  where
    splitInChannels :: Wave -> M.Vector M.D Pulse -> NonEmpty (M.Vector M.S Pulse)
    splitInChannels wave sourceVector =
      let channels = fromIntegral $ waveChannels wave
       in fromList $
            if channels == 1
              then [M.compute sourceVector]
              else
                [0 .. pred channels] <&> \channelOffset ->
                  M.compute $ M.downsample (M.Stride channels) $ M.drop (M.Sz1 channelOffset) sourceVector
    readSource :: Wave -> B.ByteString -> M.Vector M.D Pulse
    readSource wave sampleData =
      case waveSampleFormat wave of
        SampleFormatIeeeFloat32Bit -> mapAndLoad @Float
        SampleFormatIeeeFloat64Bit -> mapAndLoad @Double
        SampleFormatPcmInt 8 -> M.map ((+ (-1)) . (* 2)) $ mapAndLoad @Word8
        SampleFormatPcmInt 16 -> mapAndLoad @Int16
        SampleFormatPcmInt 32 -> mapAndLoad @Int32
        SampleFormatPcmInt 64 -> mapAndLoad @Int64
        _ -> error $ "The sample format \"" <> show (waveSampleFormat wave) <> "\" is not supported"
      where
        mapAndLoad :: forall a. (Real a, Num a, M.Storable a) => M.Vector M.D Pulse
        mapAndLoad =
          let rawArray =
                M.fromStorableVector @a M.Seq $
                  byteStringToVector sampleData
              (Max maxSample) = realToFrac <$> M.foldSemi (Max . abs) (Max 0) rawArray
           in M.map ((/ maxSample) . realToFrac) rawArray
        {-# INLINE mapAndLoad #-}

-- | Save the sound samples as raw floats
saveRaw :: FilePath -> M.Vector M.S Pulse -> IO ()
saveRaw filePath floats =
  B.writeFile filePath $ vectorToByteString $ M.toStorableVector floats

-- | Save the sound samples as raw floats compressed with gzip
saveRawCompressed :: FilePath -> M.Vector M.S Pulse -> IO ()
saveRawCompressed filePath floats = do
  let bytes = compress $ BL.fromStrict $ vectorToByteString $ M.toStorableVector floats
  BL.writeFile filePath bytes

-- Load the gzip compressed raw sound samples
loadRawCompressed :: FilePath -> IO (M.Vector M.S Pulse)
loadRawCompressed filePath = do
  file <- liftIO $ BL.readFile filePath
  pure $ M.fromStorableVector M.Seq $ byteStringToVector $ BL.toStrict $ decompress file

-- Load the raw sound samples
loadRaw :: FilePath -> IO (M.Vector M.S Pulse)
loadRaw filePath = do
  file <- liftIO $ B.readFile filePath
  pure $ M.fromStorableVector M.Seq $ byteStringToVector file