Search code examples
linuxwindowshaskellaudio

How to play an audio file from Haskell code, cross-platform


I’m writing a Haskell command line application that runs on Linux, Windows and OS X. I now have to play audio files (.wav, .ogg and .mp3) from it. How would I go about implementing a function

playAudioFile :: FilePath -> IO ()

or even better

playAudio :: ByteString -> IO ()

that simply works on all system?

(I’m happy to invoke common command line tools and also don’t mind bundling them for the Windows distribution.)


Solution

  • This is the code I came up with, using SDL-1.2:

    module PlaySound (withSound, playSound) where
    
    import Control.Monad
    import System.IO
    import System.Directory
    import Data.Foldable
    import Control.Exception
    import qualified Data.ByteString.Lazy as B
    import Foreign.ForeignPtr
    
    import Graphics.UI.SDL as SDL
    import Graphics.UI.SDL.Mixer as Mix
    
    withSound :: IO a -> IO a
    withSound = bracket_ init cleanup
      where
        init = do
            SDL.init [SDL.InitAudio]
            getError >>= traverse_ putStrLn
            ok <- Mix.tryOpenAudio Mix.defaultFrequency Mix.AudioS16LSB 2  4096
            unless ok $
                putStrLn "Failed to open SDL audio device"
    
        cleanup = do
            Mix.closeAudio
            SDL.quit
    
    playSound :: B.ByteString -> IO ()
    playSound content = do
            dir <- getTemporaryDirectory
            (tmp, h) <- openTempFile dir "sdl-input"
            B.hPutStr h content
            hClose h
    
            mus <- Mix.loadMUS tmp
            Mix.playMusic mus 1
            wait
    
            -- This would double-free the Music, as it is also freed via a
            -- finalizer
            --Mix.freeMusic mus
            finalizeForeignPtr mus
            removeFile tmp
    
    wait :: IO ()
    wait = do
        SDL.delay 50
        stillPlaying <- Mix.playingMusic
        when stillPlaying wait
    

    The program in the end works fine, but

    • compiling the SDL bindings under Windows is tricky. I followed this nice explanation on how to do it
    • the SDL bindings for SDL-1.2 seem to be unmaintained and do not even compile with GHC-7.8 or newer. I didn’t notice at first, because my distribution (Debian) patches around such issues, but it means that my users cannot easily cabal install the dependencies any more.
    • there are bindings for SDL-2, but none for the SDL_mixer, which I need here (I believe).

    So I’ll happily read better answers.