haskellopenal

How to generate OpenAL buffers in Haskell?


I'm trying to write a program in Haskell that outputs audio. I decided to use OpenAL as the audio backend, and considering I have little experience using OpenAL, one of the first things I'd figure I'd try to do is replicate the example code in the OpenAL programmer's guide, which is written in C, in Haskell.

Here's the C code from that guide:

// Initialization
Device = alcOpenDevice(NULL); // select the "preferred device"

if (Device) {
    Context=alcCreateContext(Device,NULL);
    alcMakeContextCurrent(Context);
}

// Check for EAX 2.0 support
g_bEAX = alIsExtensionPresent("EAX2.0");

// Generate Buffers
alGetError(); // clear error code

alGenBuffers(NUM_BUFFERS, g_Buffers);
if ((error = alGetError()) != AL_NO_ERROR)
{
    DisplayALError("alGenBuffers :", error);
    return;
}

// Load test.wav
loadWAVFile("test.wav",&format,&data,&size,&freq,&loop);
if ((error = alGetError()) != AL_NO_ERROR)
{
    DisplayALError("alutLoadWAVFile test.wav : ", error);
    alDeleteBuffers(NUM_BUFFERS, g_Buffers);
    return;
}

// Copy test.wav data into AL Buffer 0
alBufferData(g_Buffers[0],format,data,size,freq);
if ((error = alGetError()) != AL_NO_ERROR)
{
    DisplayALError("alBufferData buffer 0 : ", error);
    alDeleteBuffers(NUM_BUFFERS, g_Buffers);
    return;
}

// Unload test.wav
unloadWAV(format,data,size,freq);
if ((error = alGetError()) != AL_NO_ERROR)
{
    DisplayALError("alutUnloadWAV : ", error);
    alDeleteBuffers(NUM_BUFFERS, g_Buffers);
    return;
}

// Generate Sources
alGenSources(1,source);
if ((error = alGetError()) != AL_NO_ERROR)
{
    DisplayALError("alGenSources 1 : ", error);
    return;
}

// Attach buffer 0 to source
alSourcei(source[0], AL_BUFFER, g_Buffers[0]);
if ((error = alGetError()) != AL_NO_ERROR)
{
    DisplayALError("alSourcei AL_BUFFER 0 : ", error);
}

// Exit
Context=alcGetCurrentContext();
Device=alcGetContextsDevice(Context);
alcMakeContextCurrent(NULL);
alcDestroyContext(Context);
alcCloseDevice(Device);

And this is what I have so far:

module Main where

import Data.Maybe ( fromJust )
import Data.StateVar ( ($=) )
import Sound.OpenAL ( genObjectNames )
import qualified Sound.OpenAL.AL as AL
import qualified Sound.OpenAL.ALC as ALC

main :: IO ()
main = do
    -- Initialization
    maybeDevice <- ALC.openDevice Nothing  -- select the "preferred device"
    let device = fromJust maybeDevice

    maybeContext <- ALC.createContext device streamAttributes
    ALC.currentContext $= maybeContext

    -- Generate buffers
    errors <- AL.alErrors  -- clear error code
    [g_buffers] <- genObjectNames n_buffers

    -- Exit
    deviceClosed <- ALC.closeDevice device
    return ()

    where streamAttributes = [ ALC.Frequency 44100,
                               ALC.MonoSources 1,
                               ALC.StereoSources 0 ]
          n_buffers = 3

Unfortunately, I've hit a bit of a roadblock trying to generate the buffers. Currently, when I compile the above Haskell code, I get the following error:

app/Main.hs:19:20: error:
    • No instance for (Data.ObjectName.GeneratableObjectName a0)
        arising from a use of ‘genObjectNames’
    • In a stmt of a 'do' block:
        [g_buffers] <- genObjectNames n_buffers
      In the expression:
        do maybeDevice <- ALC.openDevice Nothing
           let device = fromJust maybeDevice
           maybeContext <- ALC.createContext device streamAttributes
           ALC.currentContext $= maybeContext
           ....
      In an equation for ‘main’:
          main
            = do maybeDevice <- ALC.openDevice Nothing
                 let device = ...
                 maybeContext <- ALC.createContext device streamAttributes
                 ....
            where
                streamAttributes = [ALC.Frequency 44100, ....]
                n_buffers = 3
   |
19 |     [g_buffers] <- genObjectNames n_buffers
   |                    ^^^^^^^^^^^^^^

I'm not too experienced with Haskell, so I'm finding it difficult to pinpoint exactly what I'm doing wrong here. I'm using https://hackage.haskell.org/package/OpenAL to provide the bindings. Any help would be appreciated.


Solution

  • The quick answer is that genObjectNames is an overloaded function that can generate both buffers (via the C API alGenBuffers function) and sources (via the C API alGenSources function). There's nothing in your program that tells GHC which one you want, since you don't use g_buffers in a context that would let GHC determine the desired type.

    One fix is to add an explicit type. There are a few ways to do this. One method requires no extensions but is a little roundabout, as it requires you supply the type of the right-hand side of the <- statement, so you have to specify the full monadic type:

    g_buffers <- genObjectNames n_buffers :: IO [AL.Buffer]
    

    With extensions, you can specify the return type directly, either with:

    {-# LANGUAGE ScopedTypeVariables #-}
    ...
    g_buffers :: [AL.Buffer] <- genObjectNames n_buffers
    

    or else:

    {-# LANGUAGE TypeApplications #-}
    ...
    g_buffers <- genObjectNames @AL.Buffer n_buffers
    

    Alternatively, you can leave the type unspecified and use g_buffers in a context that will provide sufficient type information:

    g_buffers <- genObjectNames n_buffers
    let data1 = AL.bufferData (g_buffers !! 0)
    

    As a slightly more detailed explanation of the error you're receiving, if you query the type of genObjectNames from GHCi, you'll see it has type:

    λ> :t genObjectNames
    genObjectNames
      :: (GeneratableObjectName a, Control.Monad.IO.Class.MonadIO m) =>
         Int -> m [a]
    

    This means is that genObjectNames runs within a monad that supports IO and returns a list [a] for any type a having a GeneratableObjectName instance.

    This part of the error message:

    • No instance for (Data.ObjectName.GeneratableObjectName a0)
        arising from a use of ‘genObjectNames’
    

    is telling you that GHC couldn't find a GenerateableObjectName instance for some unspecified type a0. This is a hint that GHC didn't know enough about the desired type to find the GeneratableObjectName instance you actually wanted. If it had said there was no instance for GeneratableObjectName Int, then you'd know that it did have enough type information to find the instance, but because of some error in your code, the type information was actually wrong, so it was looking for an instance that couldn't possibly exist.

    You can query GHCi to determine what instances are available:

    λ> :i GeneratableObjectName
    ...
    instance [safe] GeneratableObjectName Buffer
      -- Defined in ‘OpenAL-1.7.0.5:Sound.OpenAL.AL.BufferInternal’
    instance [safe] GeneratableObjectName Source
      -- Defined in ‘Sound.OpenAL.AL.Source’
    

    which shows that both Buffer and Source have such instances and would be good candidates for the return type.