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.
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.