Reputation: 3170
I'm trying to use hsndfile (the Haskell binding for libsndfile) to generate a .wav file, and I've reached yet another hump I can't get past. The following code throws the error "Bad format." (as written in openWavHandle). I've tried every combination of endianness with HeaderFormatWav and SampleFormatPcm16 that I think exists, to no avail. Does anyone know how to fix this?
import qualified Sound.File.Sndfile as Snd
import qualified Graphics.UI.SDL.Mixer.Channels as SDLC
import qualified Graphics.UI.SDL.Mixer.General as SDLG
import qualified Graphics.UI.SDL.Mixer.Samples as SDLS
import Control.Applicative
import Foreign.Marshal.Array
import Data.List.Split (splitOn)
import Data.Word (Word16)
import System.IO (hGetContents, Handle, openFile, IOMode(..))
a4 :: Double
a4 = 440.0
frameRate :: Int
frameRate = 16000
noteLength :: Double
noteLength = 5.0
volume = maxBound `div` 2 :: Word16
noteToFreq :: (String, Int) -> Double
noteToFreq (note, octave) =
if octave >= -1 && octave < 10 && n /= 12.0
then a4 * 2 ** ((o - 4.0) + ((n - 9.0) / 12.0))
else undefined
where o = fromIntegral octave :: Double
n = case note of
"B#" -> 0.0
"C" -> 0.0
"C#" -> 1.0
"Db" -> 1.0
"D" -> 2.0
"D#" -> 3.0
"Eb" -> 3.0
"E" -> 4.0
"Fb" -> 4.0
"E#" -> 5.0
"F" -> 5.0
"F#" -> 6.0
"Gb" -> 6.0
"G" -> 7.0
"G#" -> 8.0
"Ab" -> 8.0
"A" -> 9.0
"A#" -> 10.0
"Bb" -> 10.0
"B" -> 11.0
"Cb" -> 11.0
_ -> 12.0
notesToFreqs :: [(String, Int)] -> [Double]
notesToFreqs = map noteToFreq
noteToSample :: Double -> [Word16]
noteToSample freq =
take (round $ noteLength * fromIntegral frameRate) $
map ((round . (* fromIntegral volume)) . sin)
[0.0, (freq * 2 * pi / fromIntegral frameRate)..]
notesToSamples :: [Double] -> [Word16]
notesToSamples = concatMap noteToSample
getFileName :: IO FilePath
getFileName = putStr "Enter the name of the file: " >> getLine
openMFile :: FilePath -> IO Handle
openMFile fileName = openFile fileName ReadMode
getNotesAndOctaves :: IO String
getNotesAndOctaves = getFileName >>= openMFile >>= hGetContents
noteValuePairs :: String -> [(String, Int)]
noteValuePairs = pair . splitOn " "
where pair (x:y:ys) = (x, read y) : pair ys
pair [] = []
getWavSamples :: IO [Word16]
getWavSamples = (notesToSamples . notesToFreqs . noteValuePairs) <$>
getNotesAndOctaves
extendNotes :: [Word16] -> [Word16]
extendNotes = concatMap (replicate 1000)
format :: Snd.Format
format = Snd.Format Snd.HeaderFormatWav Snd.SampleFormatPcm16 Snd.EndianBig
openWavHandle :: [Word16] -> IO Snd.Handle
openWavHandle frames =
let info = Snd.Info (length frames) frameRate 1 format 1 False
in if Snd.checkFormat info
then Snd.openFile "temp.wav" Snd.WriteMode info
else error "Bad format."
writeWav :: [Word16] -> IO Snd.Count
writeWav frames = openWavHandle frames >>= \h ->
newArray frames >>= \ptr ->
Snd.hPutBuf h ptr (length frames) >>= \c ->
return c
makeWavFile :: IO ()
makeWavFile = getWavSamples >>= \s ->
writeWav s >>= \c ->
putStrLn $ "Frames written: " ++ show c
Upvotes: 2
Views: 666
Reputation: 11
Thanks to Erik this bug is fixed in version 0.5.1 on Hackage.
Because of a missing include in sndfile.h
on Linux, the Haskell bindings generator couldn't figure out that the size of a sample count sf_count_t
should be 64 bit and as a consequence the Info
struct was garbled when converted to its C representation.
Please direct follow-ups regarding this issue to the hsndfile tracker.
Upvotes: 1
Reputation: 806
Andrew,
I'm the main author of libsndfile and also a bit of a Haskell hacker. I've had a look at this and as far as I am concerned the following minimal example code should work.
import qualified Sound.File.Sndfile as Snd
import Control.Applicative
import Foreign.Marshal.Array
import Data.Word (Word16)
import System.IO (hGetContents, Handle, openFile, IOMode(..))
format :: Snd.Format
format = Snd.Format Snd.HeaderFormatWav Snd.SampleFormatPcm16 Snd.EndianFile
openWavHandle :: [Word16] -> IO Snd.Handle
openWavHandle frames =
let info = Snd.Info (length frames) 441000 1 format 1 False
in Snd.openFile "temp.wav" Snd.WriteMode info
writeWav :: [Word16] -> IO Snd.Count
writeWav frames = openWavHandle frames >>= \h ->
newArray frames >>= \ptr ->
Snd.hPutBuf h ptr (length frames) >>= \c ->
return c
makeWavFile :: IO ()
makeWavFile = writeWav [1..256] >>= \c ->
putStrLn $ "Frames written: " ++ show c
main :: IO ()
main = makeWavFile
The fact that it doesn't suggested a problem in hsndfile. To prove the, I hacked the C sources to libsndfile to print out the values of the SF_INFO struct (which hsndfile calls Info) and go this:
samplerate : 1
channels : 65538
format : 0x1
which is obviously wrong.
I've had a look at hsndfile's Interface.hsc code. The value for the format field actually ends in the channels field and the channels field ends up in the samplerate field.
I've messed with this code, but I'm not getting anywhere. I'll ping the upstream hsndfile maintainer.
Upvotes: 1