2

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
Andy
  • 3,132
  • 4
  • 36
  • 68
  • I've not used hsndfile - but the docs for Sound.File.Sndfile.Buffer seem to indicate the sample data should be floating point values [-1.0, 1.0]. You are trying to write Word16's. – stephen tetley Apr 15 '11 at 17:23
  • I modified it to use doubles, but that didn't change anything. :/ The error is with `format`. – Andy Apr 15 '11 at 17:29
  • 2
    I'm pretty sure WAV is little endian - `format` is claiming it is big endian. – stephen tetley Apr 15 '11 at 17:36
  • @Stephen Changed that, still no dice. :( I got it to throw an error, though! `Exception {errorString = "Error : major format is 0."}` I'm not 100% sure what to make of it. – Andy Apr 15 '11 at 20:41
  • 1
    I changed this to little-endian, and from ghci ran `*Main> writeWav [1,2,3,4,3,2,1,0]`. This returned "8", and the file "temp.wav" was created properly. The error about a "major format" implies that perhaps the wave header isn't generated properly. Is any output created? – John L Apr 15 '11 at 21:04
  • @John I'm calling `makeWavFile`, which calls `writeWav` with data from a file. The file contains the sequence `C 4 D 4 E 4 F 4 G 4 A 4 B 4`. – Andy Apr 15 '11 at 21:08
  • @John `*Main> writeWav [1,2,3,4,3,2,1,0] *** Exception: Exception {errorString = "Error : major format is 0."}` – Andy Apr 15 '11 at 21:09
  • @Andrew - I can't repro this error. Maybe try reinstalling libsndfile and hsndfile? – John L Apr 15 '11 at 23:47
  • @John Oy, this makes me a sad panda. I reinstalled both to no avail. Are there any dependencies I should be on the lookout for? – Andy Apr 16 '11 at 00:27
  • Andrew, I'm the upstream author of libsndfile and also a bit of a Haskell hacker. I've tried – Erik de Castro Lopo May 07 '11 at 04:52

2 Answers2

1

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.

  • The upstream hsndfile maintainer and I have fixed the issue (weird behavior of c2hs when a header file was missing).https://github.com/kaoskorobase/hsndfile/pull/3 – Erik de Castro Lopo May 11 '11 at 11:43
1

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.