Here’s what I have. It generates a 5 second Au file with a 440 Hz sine wave, inspired by this question.
-- file: tone.hs
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy.Char8 as BLC
import Data.Binary.Put
-- au format header: https://en.wikipedia.org/wiki/Au_file_format
header :: Double -> Integer -> Integer -> Put
header dur rate bps = do
putLazyByteString $ BLC.pack ".snd"
putWord32be 24
putWord32be $ fromIntegral $ floor $ fromIntegral bps * dur * fromIntegral rate
putWord32be 3
putWord32be $ fromIntegral rate
putWord32be 1
-- audio sample data
samples :: Double -> Integer -> Integer -> Double -> Double -> Put
samples dur rate bps freq vol =
foldl1 (>>) [put i | i <- [0..numSamples-1]]
where
numSamples = floor $ fromIntegral rate * dur
scale i = 2 * pi * freq / fromIntegral rate * fromIntegral i
sample i = vol * sin (scale i)
coded samp = floor $ (2 ^ (8*bps-1) - 1) * samp
put i = putWord16be $ coded $ sample i
freq = 440 :: Double -- 440 Hz sine wave
dur = 5 :: Double -- played for 5 seconds
rate = 44100 :: Integer -- at a 44.1 kHz sample rate
vol = 0.8 :: Double -- with a peak amplitude of 0.8
bps = 2 :: Integer -- at 16 bits (2 bytes) per sample
main =
BL.putStr $ runPut au
where
au = do
header dur rate bps
samples dur rate bps freq vol
If you’re running Linux, you can listen with runghc tone.hs | aplay. For other operating systems, you can probably redirect output to a .au file and play it in an audio player.
How can I make this code more idiomatic? For example:
- I wrote
fromIntegralall over the place. Could I have avoided that? - Should/could I be using a different package for outputting the binary data?
- Am I using reasonable types?
Nothing really bad here.
foldl1 (>>) [put i | i <- [0..numSamples-1]]is equivalent tomapM_ put [0 .. numSamples-1]. Rate should just be aDouble, that gets you rid of thefromIntegrals.Data.Binary.Putis really fine for binary output. One might question whether it’s good to write the samples to the monad right away (it might be more flexible to keep them as directly-accessible floating-point values in some suitable container (like chunks ofData.Vector.Storable) and onlyputthem from some generic function right in the end), but performance-wise your approach is actually very efficient. And since it’s notIOyour using, you can always get the data back in a safe, pure way.