Inertia's picture

OpenAL 1.1

I've taken a closer look at the SDK, and so far it's easy to import the C functions into C#. I've done the whole Alc.h header file and wrote a little test app to query devices/strings/ints/etc. and it seems to work fine with the "SB Live! Wave Device".

My question is now: Is there anyone working on OpenTK.OpenAL already? It was just an exercise to see if I can do it, and if this is already WIP by someone more experienced with importing, i'll just put it into the archive and forget about it.
If there hasn't been any work done for this, I'd continue porting FreeAlut.h and Al.h to C# too, but the Al portion is rather large and I don't want to do this if it's already addressed by someone else. It's not so much the import that's the issue, it's the quantity of documentation for the functions.


Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
the Fiddler's picture

Noone is working on this yet, it is something I was planning to do for 0.3.16/17+. Please send what you have, every little bit helps!

There's an existing implementation in Tao.OpenAl, so (thankfully) there's no need to re-implement everything from scratch. Both Tao and OpenTK are MIT-licensed, which means code sharing is not a problem; we just need convert the Tao implementation to use the OpenTK naming conventions and add overloads wherever it makes sense.

I cannot ask you to implement something like this, but code contributions are very welcome ;) It would take at least 2-3 more months for the OpenAL binding to be realized otherwise.

On the topic of DllImports/pinvoke there are a few dos/dont's I've gathered from the OpenGL/winapi/xlib bindings, that you may find interesting:

  1. Do not return raw strings from functions (like alcGetString)! Rather, declare an internal/private DllImport that returns a pointer, and wrap that in a public function that calls Marshal.PtrToStringAnsi - otherwise memory will be corrupted.
  2. Blittable types are fast. Structs are slow. It's better to DllImport a pointer and provide a wrapper that takes a 'ref struct', rather than DllImport the 'ref struct' directly.
  3. Prefer 'unsafe' pointers over the 'safe' IntPtr. The latter is a struct, thus a little bit slower.
  4. Use the [Out] attribute for parameters that are passed from the callee to the caller. This will avoid a memory copy and may improve performance.
  5. Handle pinning manually instead of relying on the default marshaller. It's faster that way.
  6. Pin types with the 'fixed' statement wherever possible. GCHandle.Alloc is 10x times slower, and allocates memory.
  7. Using the 'fixed' statement on a struct member pins the whole struct, not just the member! May sound counter-intuitive at first, but this behaviour is defined in the C# spec and it allows us to use the 'fixed' statement in places where it wouldn't be possible otherwise.
  8. A char array with an 'out' flow direction becomes a StringBuilder. A char array with an 'in' flow direction is a string. *char[] becomes string[]. Do not try to marshal strings manually, there are too many things that can go wrong (like unicode conversions, memory allocations etc).
Inertia's picture

Alc is the only the context/device related stuff, I've used IntPtr way too much, but it works. I've seen the Tao and OpenAL.Net implementations, but this is a port of the header files from the SDK, in correct order so future OpenAL headers can be easily compared against it.

I had to create a new type though for a few function's return.

/// <summary>OpenAL 8-Bit Boolean char, can either be True or False.</summary>
        public enum Bool : byte
        {
            ///<summary>Boolean False.</summary>
            False = 0,
            ///<summary>Boolean True.</summary>
            True = 1,
        }

I'm currently uncertain if it would be best to go directly with the approach of managed OpenAL Resources for the context, device and buffers, rather than keeping this IntPtr style. In the typical user case you will only use the best device found, and more than a single context is a rare case too. Buffers are located on the device and can be shared between contexts. I have to investigate how far it makes sense to use multiple contexts when capturing audio, but basically one can think of buffers like OpenGL Texture objects.

Not sure how much people would appreciate a wrapper though, currently it's only safe bindings. OpenAL is rather simple with it's
126 Tokens and 70 Functions for Al/Alc.
I will add the www.openal.org/extensions.html at some later point, I think vanilla OpenAL should be enough for the start to hunt down all Gremlins.

Thanks for the tips, i've also used the [In] Attribute to clarify. Ofcourse there will be unsafe overloads added at some point, but currently i'm just trying to find the most elegant "safe" solution for the binding.

does this really result in a memory leak?

// ALC_API const ALCchar * ALC_APIENTRY alcGetString( ALCdevice *device, ALCenum param );
        [DllImport( Alc.Lib, EntryPoint = "alcGetString", ExactSpelling = true, CallingConvention = Alc.Style, CharSet = CharSet.Ansi ), SuppressUnmanagedCodeSecurity( )]
        internal static extern string GetString( [In] IntPtr device, Enums.AlcGetString param );
the Fiddler's picture
public enum Bool : byte
{
    False = 0,
    True = 1,
}

Is this really an OpenAL type? OpenGL defines 'Boolean' as an enum, taking True or False (thus, an int). In any case, using a plain bool should work, since on most platforms parameters are aligned on sizeof('native int') boundaries; even if the function expects a 1-byte char and you pass a 4-byte int it will work (I'd test that first, though). It might also be possible to define these parameters as plain bools and use the [MarshalAs(UnamangedType.U1)] attribute to control marshalling.

[wrapper vs plain bindings]
It's probably easier to build plain bindings first, then provide wrappers to make life easier. I'm not sure I understand what you mean by 'managed OpenAL Resources', could you elaborate a little?

Inertia's picture

From my Type map:

// 8-bit boolean 
typedef char ALCboolean;
 * byte
// character 
typedef char ALCchar;
 * byte
// signed 8-bit 2's complement integer 
typedef char ALCbyte;
 * byte

unless my understanding of C is leaving me, those 3 ALC types map to byte.

the Fiddler's picture

Of course. It would just be nice to map ALCboolean to a bool if possible (for usability reasons).

Inertia's picture

Summary from IRC chat:

Corrected bindings for the code snippet above

private static extern IntPtr GetString( [In] IntPtr device, Enums.AlcGetString param );
 
internal static string GetString(IntPtr device, Enums.AlcGetString param)
 {
     return Marhsal.PtrToStringAnsi(GetStringInternal(device, param));
}

[Context/Device]
TheFiddler: is context creation cross-platform?
Inertia: yeah, there's no wgl/glx it's Alc for all platforms
Inertia: that's why i started with Alc, need a context for the other stuff

Inertia: In OpenAL there are only 3 Objects: a Listener, Sources and Buffers.
Inertia: buffers are .wav files (sound data), sources are orientation/speed/effects/etc. of a buffer
Inertia: and the listener is basically just where the user stands
Inertia: that's it ...

Fiddler then tested the bindings with x64 and mono (added a dll.config file pointing towards "libopenal.so") and both seemed to compile and execute fine with minor changes.

SVN for version control has been discussed, and will be used once there are bindings for every major function. Once all Gremlins are hunted down, there will most likely be a higher level wrapper for Alc that assists with context/device allocation.

Inertia: i just had that idea with that wrapper, because it would make sense let GameWindow offer more
Inertia: basically it already offers Graphics/input to the user
TheFiddler: it should initialize audio, too
Inertia: would adding sound be a bad idea, it could be enabled trough a bool?
TheFiddler: I think ALC just begs for an AudioContext class
TheFiddler: no, GameWindow should probably handle that as well
Inertia: AL uses very little resources when no buffers are loaded
Inertia: it has no "framebuffer" in that sense, allocating lots of memory
TheFiddler: agreed
TheFiddler: GameWindow could initialize the context
Inertia: maybe it's best if you write the higher level wrapper ontop of alc and i leave it as is
TheFiddler: with sensible defaults etc
TheFiddler: yep
TheFiddler: fine by me

OpenAL 1.1 will not make it into OpenTK 3.14, but will definitely be included in 3.15 (and available for preview through SVN before that)

I think this pretty much sums up all important info.

Inertia's picture

'managed OpenAL Resources'

Basically my idea was to apply http://www.opentk.com/news/garbage_collect_opengl_resources to OpenAL Buffer Objects (which are quite similar to Texture Objects in GL). I haven't really given the details much thought yet, but it would make sense using similar hooks for OpenAL.

It should also be looked at how this could be applied to the Device (which physically stores the Buffer/sound data) itself. If the AL device knew which Buffers are loaded into it and which contexts use what, it could delete the Buffers on a Device shutdown, or respond to context destruction.

Inertia's picture

Here's an example "hello world" in OpenTK.OpenAL, which plays 2 sounds and sets a few parameters to show in what direction this is heading.

 
            Alut.Init( );
            uint[] TestBuffers = new uint[2];
            TestBuffers[0] = Alut.CreateBufferFromFile( "Media/stereo.wav" );
            GetOpenALErrors( ); // check for file I/O problems
            TestBuffers[1] = Alut.CreateBufferWaveform( AlutWaveform.Sine, 500f, 42f, 1.5f );
 
            uint[] TestSources = new uint[2];
            Al.GenSources( 2, out TestSources[0] ); // gen 2 Source Handles
 
            Al.Sourcei( TestSources[0], AlSourcei.Buffer, (int) TestBuffers[0] ); // attach the buffer to a source
 
            Al.SourcePlay( TestSources[0] ); // start playback
            Al.SourceBool( TestSources[0], AlSourceBool.Looping, true ); // source loops infinitely
 
            Al.Sourcei( TestSources[1], AlSourcei.Buffer, (int) TestBuffers[1] );
            Vector3 Position = new Vector3( 1f, 2f, 3f );
            Al.Source3f( TestSources[1], AlSource3f.Position, ref Position );
            Al.Sourcef( TestSources[1], AlSourcef.Gain, 0.85f );
            Al.SourcePlay( TestSources[1] );
 
            Alut.Sleep( 1 ); // appears not to be working
            Console.ReadLine( );
 
            Al.SourceStop( TestSources[0] ); // halt playback
            Al.SourceStop( TestSources[1] );
 
            Al.DeleteSources( TestSources.Length, TestSources ); // free Handles
            Al.DeleteBuffers( TestBuffers.Length, TestBuffers );
            Alut.Exit( );

The biggest difference to OpenTK's GL bindings is currently that Al.Source* functions use different enums.
You cannot pass a bool to AlSourcef.Gain without casting, it must be a single float.
AlSource3f.Position accepts either 3 floats, or a Math.Vector3
AlSourcei.Buffer expects an Integer parameter, and so on.
This is supposed to help the user writing error-free programs, if it's more confusing than helping to must users i will revert that change and stuff it all into a single enum, but so far it seems the right design choice.

the Fiddler's picture

Looks very simple!

I'll look into hooking the resources into the garbage collector. It can be done, and I think it would be very nice.

A couple of suggestions: according to the .Net capitalization conventions, the correct form for "Al" is "AL":


Do capitalize both characters of two-character acronyms, except the first word of a camel-cased identifier.

A property named DBRate is an example of a short acronym (DB) used as the first word of a Pascal-cased identifier. A parameter named ioChannel is an example of a short acronym (IO) used as the first word of a camel-cased identifier.

Do capitalize only the first character of acronyms with three or more characters, except the first word of a camel-cased identifier.

A class named XmlWriter is an example of a long acronym used as the first word of a camel-cased identifier. A parameter named htmlReader is an example of a long acronym used as the first word of a camel-cased identifier.

I think we should follow these guidelines for public members as closely as possible.

Also, let's overload "Source" instead of having different functions with suffixes ("Sourcei", "Sourcef", "SourceBool").

Edit: about the enums, it's much better that way, good job!

Inertia's picture

[Naming conventions]
Maybe it would be best to make this different for enums and functions, so it would look like this.

func: AL.Sourcef()
enum: AlSourcef

I consider this more of a cosmetic detail, will adjust this to make everyone happy.

Also, let's overload "Source" instead of having different functions with suffixes ("Sourcei", "Sourcef", "SourceBool").
SourceBool isn't from OpenAL, i just added this for convenience because some parameters expect a System.bool.

I think for now it's better if the functions have the explicit suffix -i -3f -fv etc. I'm a little afraid that overloading will result in options being hidden from the user (i.e. i rather type Al.Sourcef than Al.Source and find the correct overload). This is open for debate, currently i'm just making sure it runs fine and the usage is similar for the functions.