BASS.NET API for the Un4seen BASS Audio Library

Interoperating with Unmanaged Code

BASS.NET API for the Un4seen BASS Audio Library
Managed vs. Unmanaged Code

Applications written in .NET (e.g. with C# or VB.Net) are managed code. BASS.NET is written in C# and hence is managed code.

BASS and all add-ons are written in C/C++, hence all native BASS/Add-On dll's are unmanaged or native code (some add-ons might be written in Delphi, but that does make any difference).

In the managed runtime environment, the garbage-collected heap manages all class objects. This heap monitors the lifetime of objects and frees them only when no part of the program references the objects. This ensures that objects never leak memory and that references to objects are always valid. This has some advantages, like overflow inspection, type safety checking etc. and some disadvantages, since it is not you managing the memory, it is the Garbage Collector (GC). The GC allocates and frees memory automatically and it might also move the location of already created objects to a different location in the heap!

You typically don't have to take care when memory is allocated and when it should be freed - also the location in the heap (and the fact that the location might change at any unpredictable time) is normally not of much interest.

BUT! when you want to exchange data between a managed .NET application and an unmanaged, native dynamic-linked library (like BASS) - then all this matters a lot! When you access or call BASS from .NET - like when using the BASS.NET API - you have to deal with some aspects of communication between managed and unmanaged code. You have to be aware of this and things can get a bit more complicated when using native dll's like BASS.

The common language runtime provides Platform Invocation Services, or PInvoke, that enables managed code to call C-style functions in native dynamic-linked libraries (DLLs).

PInvoke
When platform invoke calls an unmanaged function, it performs the following sequence of actions:
  1. Locates the DLL containing the function.
  2. Loads the DLL into memory (if not already loaded).
  3. Locates the address of the function in memory and pushes its arguments onto the stack, marshaling data as required.

    Note: Locating and loading the DLL, and locating the address of the function in memory occur only on the first call to the function.

  4. Transfers control to the unmanaged function.
BASS.NET tries to shield as much details about Platform Invoke, Marshaling etc. as possible from you. However, it is a good idea to understand the details of the next chapters...

Garbage Collection

This section contains the following subsections:

The .NET Framework's garbage collector manages the allocation and release of memory for your application. Each time you use the new operator to create an object, the runtime allocates memory for the object from the managed heap. As long as address space is available in the managed heap, the runtime continues to allocate space for new objects. However, memory is not infinite. Eventually the garbage collector must perform a collection in order to free some memory. The garbage collector's optimizing engine determines the best time to perform a collection, based upon the allocations being made. When the garbage collector performs a collection, it checks for objects in the managed heap that are no longer being used by the application and performs the necessary operations to reclaim their memory. During garbage collection it might be possible, that the GC engine decides to move your objects to a different place in the heap to prevent segmentation!

Be aware of all this when you hand over objects to unmanage code (since unmanaged BASS has no idea of all this):

  1. Objects might be disposed (collected) during garbage collection.
  2. Objects might be re-located during garbage collection.
So whenever you call an unmanaged BASS fuction (which is obviously almost all the time the case when you call any BASS.NET method) you might pass a reference to a managed object. Note, that in such cases the garbage collector would have no idea, if that unmanaged function is still in need of that reference! By calling an unmanaged function and passing an object reference no reference counter will be increased, nor will any other reference be made to give the garbage collector a hint to prevent this object from being removed from the heap.

So it is unluckily your responsibility to make sure, that no object is being garbage collected while it might still be in use by any native BASS function!

To achieve this is pretty easy: Simply keep a reference to the object in question locally in your managed code. Forgetting this aspect is the most common mistake which results in a "memory access violation" error - causing your application to crash.

Luckily all this only applies to reference types (e.g. a class instance, a callback delegate etc.). Value type parameters (Int32, Int64, Float, Boolean etc.) are not affected by this.

Lifetime and Pinning

The other aspect to take care about is the fact, that the GC might move the location of any heap object to a different place (re-location). This applies to all objects created with the new (New in Visial Basic) operator. Those objects must be pinned before they are handed over to the unmanaged function!

When the .NET runtime marshaler sees that your code is passing a reference to a managed reference object to native code, it automatically pins the object. This means that the object is put in a special state where the garbage collector will neither move the object in memory nor remove the object from memory. Pinned objects hurt the performance of the garbage collector, but they assure that the memory remains intact for the life of the native call; this is critical to the proper functioning of the native code. When the native function returns, the marshaled object is automatically unpinned. Or in other words: when BASS.NET performs a PInvoke to an unmanaged DLL (by calling the native function) all parameters to such function are automatically marshaled. And whenever the .NET Framework common language runtime marshals an object, the object is automatically pinned for the duration of the platform invoke call to ensure that the garbage collector does not free or re-locate the object. Automatic pinning is very convenient, but it raises another question.

What happens if an unmanaged function caches the pointer for later use?

When an unmanaged function returns, won't the garbage collector be free to move the object again? The answer is yes, and the solution for your code in such a situation is to manually pin the object using the System.Runtime.InteropServicesGCHandle type for as long as the unmanaged function might use such reference. The following lines of code use the GCHandle type in order to pin and unpin a managed string:

C#: Using GCHandle to pin a managed object
string s = "BASS.NET";
GCHandle hGC = GCHandle.Alloc(s, GCHandleType.Pinned);
// use the pointer to the string with an unmanaged function
// via "hGC.AddrOfPinnedObject()"

// further code might run here
  ...

// when the pointer is not needed anymore by the unmanaged function
// make sure to call
hGC.Free();
VB.Net: Using GCHandle to pin a managed object
Dim s As String = "BASS.NET"
Dim hGC As GCHandle = GCHandle.Alloc(s, GCHandleType.Pinned)
' use the pointer to the string with an unmanaged function
' via "hGC.AddrOfPinnedObject()"

' further code might run here
  ...

' when the pointer is not needed anymore by the unmanaged function
' make sure to call
hGC.Free()
As you can see manually pinning an object is not difficult. The tricky part is knowing when you should. Generally, PInvoke will either copy your data to fixed memory outside the GC heap, or it will pin memory in the GC heap and expose those bytes directly to unmanaged code. In either case, you don't need to explicitly pin - as long as access to these bytes is scoped to within the duration of the PInvoke call. This means, that in most cases there is no need to worry about pinning objects. However, you need to know the behavior of the function you are calling well enough to decide whether automatic pinning is sufficient, or whether you will need to manually pin your managed object. There are a few functions, where BASS (or an add-on) might hold a pointer to a managed object for longer than the duration of the platform invoke call. These are amongst others: In such case you might need to manually pin the respective object using the GCHandle structure. Make sure to call GCHandleFree on the GCHandle once it is not needed anymore to avoid memory leaks.

Using BASS_SetConfigPtr

This BASS method requires pinning, since BASS caches the configuration pointer for later use. Beside pinning, this method adds another complexity, as it expects a pointer to an ANSI string - but in the .NET world all Strings are Unicode. So before pinning an object you first need to convert the Unicode string to an ANSI string. This can be done in two ways:

  1. Use an System.TextEncoder to convert the Unicode string to a byte array and pin the array.
  2. Directly allocate an ANSI string in unmanaged memory using the System.Runtime.InteropServicesMarshal class.
Here are the two examples:
C#: Using GCHandle to pin a managed object
private GCHandle _userAgentGCH;
...
string userAgent = "BASS.NET";
byte[] userAgentBytes = Encoding.ASCII.GetBytes(userAgent);
// create a pinned handle to our managed object
_userAgentGCH = GCHandle.Alloc(userAgentBytes, GCHandleType.Pinned);
// use the pointer to the string
Bass.BASS_SetConfigPtr(BASSConfig.BASS_CONFIG_NET_AGENT, _userAgentGCH.AddrOfPinnedObject());
...
// make sure to free the handle when you don't need it anymore!
// e.g. when you dispose your class or application
_userAgentGCH.Free();
VB.Net: Using GCHandle to pin a managed object
Private _userAgentGCH As GCHandle
...
Dim userAgent As String = "BASS.NET"
Dim userAgentBytes As Byte() = Encoding.ASCII.GetBytes(userAgent)
' create a pinned handle to our managed object
_userAgentGCH = GCHandle.Alloc(userAgentBytes, GCHandleType.Pinned)
' use the pointer to the string
Bass.BASS_SetConfigPtr(BASSConfig.BASS_CONFIG_NET_AGENT, _userAgentGCH.AddrOfPinnedObject())
...
' make sure to free the handle when you don't need it anymore!
' e.g. when you dispose your class or application
_userAgentGCH.Free()
C#: Using Marshal to convert and pin an ANSI string pointer
private IntPtr _myUserAgentPtr;
...
string userAgent = "radio42";
// create an unmanaged pointer containing a copy of the string
_myUserAgentPtr = Marshal.StringToHGlobalAnsi(userAgent);
Bass.BASS_SetConfigPtr(BASSConfig.BASS_CONFIG_NET_AGENT, _myUserAgentPtr);
...
// make sure to free the myUserAgentPtr!!! 
// e.g. when you dispose your class or application
Marshal.FreeHGlobal(_myUserAgentPtr);
VB.Net: Using Marshal to convert and pin an ANSI string pointer
Private _myUserAgentPtr As IntPtr
...
Dim userAgent As String = "radio42"
' create an unmanaged pointer containing a copy of the string
_myUserAgentPtr = Marshal.StringToHGlobalAnsi(userAgent)
Bass.BASS_SetConfigPtr(BASSConfig.BASS_CONFIG_NET_AGENT, _myUserAgentPtr)
...
' make sure to free the myUserAgentPtr!!! 
' e.g. when you dispose your class or application
Marshal.FreeHGlobal(_myUserAgentPtr)

Streaming from Memory with BASS_StreamCreateFile

The data to stream is provided as a pointer to a memory location and might be used after the method returns (i.e. during playback). So the memory object needs to be pinned to prevent re-location. Here is an example:

C#: Streaming from Memory
private GCHandle _hGCFile;
...
// open a file
FileStream fs = File.OpenRead("test.mp3");
// get the legth of the file
int length = (int)fs.Length;
// create the buffer which will keep the file in memory
byte[] buffer = new byte[length];
// read the file into the buffer
fs.Read(buffer, 0, length);
// buffer is filled, file can be closed
fs.Close();

// now create a pinned handle, so that the GC will not move this object
_hGCFile = GCHandle.Alloc(buffer, GCHandleType.Pinned);
// create the stream (AddrOfPinnedObject delivers the necessary IntPtr)
int stream = Bass.BASS_StreamCreateFile(_hGCFile.AddrOfPinnedObject(), 0L,
               length, BASSFlag.BASS_SAMPLE_SOFTWARE | BASSFlag.BASS_SAMPLE_FLOAT);

if (stream != 0 && Bass.BASS_ChannelPlay(stream, false) )
{
  // playing...
}
else
{
  Console.WriteLine("Error = {0}", Bass.BASS_ErrorGetCode());
}
...
// when playback has ended and the pinned object is not needed anymore, 
// we need to free the handle!
// Note: calling this method to early will crash the application, 
// since the buffer would be stolen from BASS while still playing!
_hGCFile.Free();
VB.Net: Streaming from Memory
Private _hGCFile As GCHandle
...
' open a file
Dim fs As FileStream = File.OpenRead("test.mp3")
' get the legth of the file
Dim length As Integer = CInt(fs.Length)
' create the buffer which will keep the file in memory
Dim buffer(length) As Byte
' read the file into the buffer
fs.Read(buffer, 0, length)
' buffer is filled, file can be closed
fs.Close()

' now create a pinned handle, so that the Garbage Collector will not move this object
_hGCFile = GCHandle.Alloc(buffer, GCHandleType.Pinned)
' create the stream (AddrOfPinnedObject delivers the necessary IntPtr)
Dim stream As Integer = Bass.BASS_StreamCreateFile(_hGCFile.AddrOfPinnedObject(), 0L,
                          length, BASSFlag.BASS_SAMPLE_SOFTWARE Or BASSFlag.BASS_SAMPLE_FLOAT)

If stream <> 0 And Bass.BASS_ChannelPlay(stream, False) Then
  ' playing...
Else
  Console.WriteLine("Error = {0}", Bass.BASS_ErrorGetCode())
End If
...
' when playback has ended and the pinned object is not needed anymore, 
' we need to free the handle!
' Note: calling this method to early will crash the application, 
' since the buffer would be stolen from BASS while still playing!
_hGCFile.Free()

Using Callbacks with a 'user' Parameter

In previous versions of BASS you could only pass an Int32 value as a 'user' parameter to a callback delegate. With BASS 2.4 pointers have been introduced. This allows you to pass any value (e.g. an instance of an arbitrary class) to a callback. This means pinning would be required, if you pass a reference to an object! This might be a rather rare case (as in most scenarios you would still only pass an Int32 value as a 'user' parameter and as this is a value type no extra pinning is needed at all) - but let's look to both cases:

C#: Passing an Int32 user Parameter to a Callback
private DSPPROC _myDSPProc;
...
int myValue = 123;
_myDSPProc = new DSPPROC(MyDSP);
Bass.BASS_ChannelSetDSP(_stream, _myDSPProc, new IntPtr(myValue), 0);
...
private void MyDSP(int handle, int channel, IntPtr buffer, int length, IntPtr user)
{
  int userValue = user.ToInt32();
  // userValue now contains 123 again
  ...
}
VB.Net: Passing an Int32 user Parameter to a Callback
Private _myDSPProc As DSPPROC
...
Dim myValue As Integer =  123 
_myDSPProc = New DSPPROC(AddressOf MyDSP)
Bass.BASS_ChannelSetDSP(_stream, _myDSPProc, New IntPtr(myValue), 0)
...
Private Sub MyDSP(ByVal handle As Integer, ByVal channel As Integer, 
                  ByVal buffer As IntPtr, ByVal length As Integer, ByVal user As IntPtr)
  Dim userValue As Integer =  user.ToInt32() 
  ' userValue now contains 123 again
  ...
End Sub
As you see you can simply create an IntPtr from your Int32 value which in such case doesn't need to be pinned, as the pointer value will not be changed by the garbage collector at any time.

But when you want to pass a pointer to an instance of an arbitrary class to a callback - again you will need to pin the object by using the GCHandle structure. The following example assumes, that a class 'MyClass' exists somewhere in your code:

C#: Passing an Object user Parameter to a Callback
private DSPPROC _myDSPProc;
private int _hdsp;
private GCHandle _hGCClass;
...
_myDSPProc = new DSPPROC(MyDSP);
MyClass myValue = new MyClass("demo");
_hGCClass = GCHandle.Alloc(myValue, GCHandleType.Pinned);
_hdsp = Bass.BASS_ChannelSetDSP(_stream, _myDSPProc, _hGCClass.AddrOfPinnedObject(), 0);
...
private void MyDSP(int handle, int channel, IntPtr buffer, int length, IntPtr user)
{
  // use the following to convert the user parameter back to it's origin
  GCHandle gch = GCHandle.FromIntPtr(user);
  MyClass userValue = (MyClass)gch.Target;
  ...
}
...
// make sure to free the GCHandle when it is really not needed anymore
Bass.BASS_ChannelRemoveDSP(_stream, _hdsp);
_hGCClass.Free();
VB.Net: Passing an Object user Parameter to a Callback
Private _myDSPProc As DSPPROC
Private _hdsp As Integer
Private _hGCClass As GCHandle
...
_myDSPProc = New DSPPROC(AddressOf MyDSP)
Dim myValue As New [MyClass]("demo")
_hGCClass = GCHandle.Alloc(myValue, GCHandleType.Pinned)
_hdsp = Bass.BASS_ChannelSetDSP(_stream, _myDSPProc, _hGCClass.AddrOfPinnedObject(), 0)
...
Private Sub MyDSP(handle As Integer, channel As Integer, 
                  buffer As IntPtr, length As Integer, user As IntPtr)
  ' use the following to convert the user parameter back to it's origin
  Dim gch As GCHandle = GCHandle.FromIntPtr(user)
  Dim userValue As [MyClass] = CType(gch.Target, [MyClass])
  ...
End Sub
...
' make sure to free the GCHandle when it is really not needed anymore
Bass.BASS_ChannelRemoveDSP(_stream, _hdsp)
_hGCClass.Free()
However, don't get too worried for now, as for all the rest BASS.NET normally marshals everything for you.

Internal Marshaling

When marshaling data, the interop marshaler can copy or pin the data being marshaled. Copying the data places a copy of data from one memory location in another memory location. The following illustration shows the differences between copying a value type and copying a type passed by reference from managed to unmanaged memory:

Pin or Copy
Method arguments passed by value are marshaled to unmanaged code as values on the stack. The copying process is direct. Arguments passed by reference are passed as pointers on the stack. Reference types are also passed by value and by reference. As the following illustration shows, reference types passed by value are either copied or pinned:
Pin or Copy 2
As you can see in the above picture the second case (in the middle) is by far the best (fastest) scenario, as here no extra memory needs to be allocated/copied, this for example is being used when you call BASS_ChannelGetData or BASS_StreamPutData.

Pinning temporarily locks the data in its current memory location, thus keeping it from being relocated by the common language runtime's garbage collector. The marshaler pins data to reduce the overhead of copying and enhance performance. The type of the data determines whether it is copied or pinned during the marshaling process. Just keep in mind, that during marshaling data might be copied and such it wouldn't make sense to modify the managed object as long as the platform invoke hasn't returned or as long as the unmanaged code still keeps a reference to the pinned data.

I hope that's clear now ;-)

Callbacks and Delegates

Callbacks are widely used within BASS. They are used to signal the occurrence of an action or event. Callbacks are function pointers and the .NET Framework defines a special type (see Delegate) that provides the functionality of a function pointer. A delegate is a class that can hold a reference to a method. But as a delegate is a class the same rules for garbage collection applies as for any other object - meaning delegates might a) be disposed (collected) or b) be re-located by the garbage collector!

Clearly an unmanaged function pointer must refer to a fixed address. It would be a disaster if the garbage collector were relocating that! This leads many applications to create a pinning handle for the delegate. This is completely unnecessary. Any unmanaged function pointer actually refers to a native code stub that is dynamically generated to perform the transition and marshaling. This stub exists in fixed memory outside of the GC heap. This means the re-locating issue doen't apply to delegates!

However, your application is responsible for somehow extending the lifetime of the delegate until no more calls will occur from unmanaged code. The lifetime of the native code stub is directly related to the lifetime of the delegate. Once the delegate is collected, subsequent calls via the unmanaged function pointer will crash or otherwise corrupt the process.

Scenario: You pass a managed delegate to unmanaged code as a function pointer but don't keep a reference to that delegate in your code for the lifetime of the unmanaged function pointer.

Failure: Access violations occur when the unmanaged code attempts to call into your managed code through that function pointer. This is because the delegate from which the function pointer was created and exposed to unmanaged code was garbage collected! The failure is not consistent; sometimes the call on the function pointer succeeds and sometimes it fails. The failure might occur only under heavy load or on a random number of attempts.

Cause: The failure appears random because it depends on when garbage collection occurs. If a delegate is eligible for collection, the garbage collection can occur after the callback and the call succeeds. At other times, the garbage collection occurs before the callback, the callback generates an access violation, and the program stops.

Resolution: Once a delegate has been marshaled out as an unmanaged function pointer, the garbage collector cannot track its lifetime. Instead, your code must keep a reference to the delegate for the lifetime of the unmanaged function pointer!

C#: Using delegates the WRONG way
private void StartRecording()
{
  // create a callback delegate
  RECORDPROC myRecProc = new RECORDPROC(MyRecoring);
  // expose the delegate to unmanaged BASS  
  rechandle = Bass.BASS_RecordStart(44100, 2, BASSRecord.BASS_DEFAULT, myRecProc, IntPtr.Zero);
}

private bool MyRecoring(int handle, IntPtr buffer, int length, IntPtr user)
{
  // this code will be invoked by unmanaged BASS
  ...
}
VB.Net: Using delegates the WRONG way
Private Sub StartRecording()
  ' create a callback delegate
  Dim myRecProc As RECORDPROC = New RECORDPROC(AddressOf MyRecoring) 
  ' expose the delegate to unmanaged BASS  
  rechandle = Bass.BASS_RecordStart(44100, 2, BASSRecord.BASS_DEFAULT, myRecProc, IntPtr.Zero)
End Sub

Private Function MyRecoring(ByVal handle As Integer, ByVal buffer As IntPtr, 
                            ByVal length As Integer, ByVal user As IntPtr) As Boolean
  ' this code will be invoked by unmanaged BASS
  ...
End Function
In the above example the garbage collector might dispose the local instance of myRecProc once the method StartRecording is left, since after that it is eligible for collection! This might happen at any time, as you don't know when garbage collection is performed. So it might be the case, that the MyRecoring callback will invoked several times and suddenly an access violation is raised, which might simply be shown as a 'unhandled null reference exception' as the delegate was disposed and doesn't exist anymore.

So here is the correct way when passing callback delegates:

C#: Using delegates the RIGHT way
// declare it as a member, to keep a reference
private RECORDPROC _myRecProc;

private void StartRecording()
{
  // create a callback delegate
  _myRecProc = new RECORDPROC(MyRecoring);
  // expose the delegate to unmanaged BASS  
  rechandle = Bass.BASS_RecordStart(44100, 2, BASSRecord.BASS_DEFAULT, _myRecProc, IntPtr.Zero);
}

private bool MyRecoring(int handle, IntPtr buffer, int length, IntPtr user)
{
  // this code will be invoked by unmanaged BASS
  ...
}
VB.Net: Using delegates the RIGHT way
' declare it as a member, to keep a reference
Private _myRecProc As RECORDPROC

Private  Sub StartRecording()
  ' create a callback delegate
  _myRecProc = New RECORDPROC(AddressOf MyRecoring)
  ' expose the delegate to unmanaged BASS  
  rechandle = Bass.BASS_RecordStart(44100, 2, BASSRecord.BASS_DEFAULT, _myRecProc, IntPtr.Zero)
End Sub

Private Function MyRecoring(ByVal handle As Integer, ByVal buffer As IntPtr, 
                            ByVal length As Integer, ByVal user As IntPtr) As Boolean
  ' this code will be invoked by unmanaged BASS
  ...
End Function

Cross Thread Communication

Multi-Threading is another complex subject to understand. BASS itself is already multi-threaded (even if this is not directly visible to you). For example BASS creates various threads by itself - as there is for example an update thread for each device being initialized, which is responsible for things like keeping the output buffers up-to-date. But there might be even more threads being used by BASS in various other scenarios.

When it comes to develop a nice User Interface (UI) it is often also necessary to use threads. This because multi-threading allows your application to run more efficiently (in most cases), and it affords you the opportunity to run resource intensive processes in the background, allowing your user interface to remain usable. This is especially the case with BASS. If your UI and BASS would run in a single thread, you would notice at times that your user interface would, for all intents and purposes, be unusable, until the current audio processing completed.

Though there are numerous advantages to creating multi-threaded applications, there are also safety concerns. The main concern when creating multi-threaded applications (and note! here you have no choice - an application using BASS is always multi-threaded) is the possibility of multiple threads manipulating the same data, or space in memory, at the same time, this is known as a 'race condition'. But we are not to discuss this subject here. A more common issue with creating a user interface and dealing with multi-threading is the fact, that you can not access or modify a UI control from any other thread. Or in other words:

You can only update/modify a UI control from the thread which created the control - the so called UI thread. Otherwise you will get a 'cross thread operation not valid' exception.

Scenario: You are using BASS_ChannelSetSync and a SYNCPROC callback to get notified by BASS whenever a certain event occures and would like to update a UI control to reflect this event.

Failure: It is a common mistake to believe, that only because you have called BASS_ChannelSetSync from your UI thread, that the callback delegate will also be executed in the UI thread. No: As BASS is multi-threading, the callback delegate will be executed on another thread created by BASS (actually the update thread). When you now try to update certain UI controls directly from within you callback code you will get a 'cross thread operation not valid' exception. This is because you try to update a UI control from a thread which is not the one which created the control.

Resolution: You will have to use a delegate and invoke the thread which the UI control was created from. Therefore each UI control has an Invoke method in order to do so (which executes a delegate on the thread that owns the control's underlying window handle). There are several ways to implement the logic to ensure, that a UI control is only updated from it's own UI thread. Here are some:

C#: Using a delegate and Invoke a UI control update
// Process metadata received from an internet stream:
private SYNCPROC _mySync;
...
int stream = Bass.BASS_StreamCreateURL(url, 0, BASSFlag.BASS_STREAM_STATUS, null, 0);
// set a sync to get notified on stream title updates
_mySync = new SYNCPROC(MetaSync);
Bass.BASS_ChannelSetSync(stream, BASSSync.BASS_SYNC_META, 0, _mySync, IntPtr.Zero);
Bass.BASS_ChannelPlay(stream, false);
...
private void MetaSync(int handle, int channel, int data, IntPtr user)
{
  // BASS_SYNC_META is triggered
  string[] tags = Bass.BASS_ChannelGetTagsMETA(channel);

  // this would raise an error, 
  // since 'MetaSync' will be executed on a non UI thread!
  this.textBox1.Text = tags[0];

  // so instead create and invoke a delegate on the UI thread
  // assuming, that 'this' references a Windows.Forms.Form
  this.Invoke(new UpdateUIDelegate(UpdateUI), new object[] {tags[0]});
}

private delegate void UpdateUIDelegate(string tagText);

private void UpdateUI(string tagText)
{
  // this code must run on the UI thread
  this.textBox1.Text = tagText;
}
VB.Net: Using a delegate and Invoke a UI control update
' Process metadata received from an internet stream:
Private _mySync As SYNCPROC
...
Dim stream As Integer = Bass.BASS_StreamCreateURL(url, 0, BASSFlag.BASS_STREAM_STATUS, Nothing, 0) 
' set a sync to get notified on stream title updates
_mySync = New SYNCPROC(AddressOf MetaSync)
Bass.BASS_ChannelSetSync(stream, BASSSync.BASS_SYNC_META, 0, _mySync, IntPtr.Zero)
Bass.BASS_ChannelPlay(stream, False)
...
Private Sub MetaSync(ByVal handle As Integer, ByVal channel As Integer, 
                     ByVal data As Integer, ByVal user As IntPtr)
  ' BASS_SYNC_META is triggered
  Dim tags As String() = Bass.BASS_ChannelGetTagsMETA(channel)

  ' this would raise an error, 
  ' since 'MetaSync' will be executed on a non UI thread!
  Me.textBox1.Text = tags(0)

  ' so instead create and invoke a delegate on the UI thread
  ' assuming, that 'Me' references a Windows.Forms.Form
  Me.Invoke(New UpdateUIDelegate(AddressOf UpdateUI), New Object() {tags(0)})
End Sub

Private Delegate Sub UpdateUIDelegate(ByVal tagText As String) 

Private Sub UpdateUI(ByVal tagText As String) 
    ' this code must run on the UI thread 
    Me.textBox1.Text = tagText
End Sub
Another way is to use anonymous delegates, which only exist in C# (but be aware, that BASS callbacks should be as fast as possible, so do not introduce unnecessary code):
C#: Using anonymous delegates
private void MetaSync(int handle, int channel, int data, IntPtr user)
{
  // BASS_SYNC_META is triggered
  string[] tags = Bass.BASS_ChannelGetTagsMETA(channel);

  BeginInvoke( (MethodInvoker)delegate()    
  {        
    // this code runs on the UI thread!
    this.textBox1.Text = tags[0];
  } );
}
And finally another appropriate way to handle cross UI thread calls is to just call a method and let the method decide whether it's on the correct thread or not:
C#: Let the method decide if on a cross UI threads
private void MetaSync(int handle, int channel, int data, IntPtr user)
{
  // BASS_SYNC_META is triggered
  string[] tags = Bass.BASS_ChannelGetTagsMETA(channel);

  UpdateUI(tags[0]);
}

private delegate void UpdateUIDelegate(string tagText);

public void UpdateUI(string tagText)
{
  if (this.InvokeRequired)
  {
    // this is not the UI thread
    this.Invoke(new UpdateUIDelegate(UpdateUI), new object[] {tagText});
  }
  else 
  {
    // this is the UI thread
    this.textBox1.Text = tagText; 
  }
}
VB.Net: Let the method decide if on a cross UI threads
Private Sub MetaSync(ByVal handle As Integer, ByVal channel As Integer, 
                     ByVal data As Integer, ByVal user As IntPtr)
  ' BASS_SYNC_META is triggered
  Dim tags As String() = Bass.BASS_ChannelGetTagsMETA(channel) 

  UpdateUI(tags(0))
End Sub

Private Delegate Sub UpdateUIDelegate(ByVal tagText As String)

Private Sub UpdateUI(ByVal tagText As String)
  If Me.textBox1.InvokeRequired Then
    ' this is not the UI thread
    Me.Invoke(New UpdateUIDelegate(AddressOf UpdateUI), New Object() {tagText})
  Else
    ' this is the UI thread
    Me.textBox1.Text = tagText
  End If
End Sub

Marshaling and Memory Access

This section contains the following subsections:

There are several ways to deal with managed libraries within .NET and there are even ways to access unmanaged memory directly - which is as fast a native C/C++ code. Here is a quick overview about the different techniques when it comes to marshalling and memory access.

When calling unmanaged functions or when sharing data between managed and unmanaged code, this is called marshaling. Typically data will be copied between managed and unmanaged code (when using value types) or pinned (when using reference types) - see above for details. This to ensure, that the .NET runtime environment can still ensure that all objects are type safe and that any objects can be freed by the garbage collector. The downside is that when data is marshaled (as it might be copied) more memory might be needed and that this additional memory needs to be allocated - which takes some time. Often this is not critical and even not a performance issue at all, e.g. if for a short string a copy is created you would not even notice that. However, when it comes to more data and to methods where performance is critical, e.g. when implementing DSP routines, then this might be an issue. But marshaling is often more than simply creating a copy of some memory. Strings for example can automatically be converted from ANSI (single-byte characters, 8-bit) to Unicode (double-byte characters, 16-bit) vice versa.

In the BASS.NET API nearly all methods have several overloads. Most overloads already take care about marshaling in the best (fastest) way automatically. So for 99% of the methods you don't have to care about marshaling at all - all is done for you already by a proper external function declaration within the BASS.NET implementation. All methods returning or taking an IntPtr as a parameter admittedly are to be considered as raw method calls where you would have to deal with marshalling.

In most cases these IntPtr methods are dealing with sample data buffers. Sometimes you have to provide sample data in a buffer to BASS, but most of the time BASS is actually providing you with an already created buffer.

An IntPtr is a managed pointer to a block of memory. However you can access the data at the address specified by the IntPtr in various ways. Normally be using the System.Runtime.InteropServicesMarshal class. But an IntPtr can also be created for any blittable .NET object instance (using the System.Runtime.InteropServicesGCHandle structure). Plus (if you are into C#) an IntPtr can also be converted to a real pointer behaving like in C/C++, which allows direct memory access (using unsafe code blocks and avoiding the need to copy data between managed and unmanaged code)! Especially the last option (using native pointer access) makes C# an absolute favourite when it comes to deciding which managed language to choose for your application (since in VB.Net you unluckily don't have the possibility to use direct pointer access with unsafe code blocks).

Using Data in .NET which has been allocated by BASS

A typical example are all DSP routines, i.e. when you declare your own callback method using the delegate DSPPROC. Here BASS has already allocated memory and filled the data buffer. The actual sample data provided by BASS can be found at the address specified by the 'IntPtr buffer' parameter. You just need to access the memory at this address.

C#: Buffer Access using Marshal.Copy
private float _gainAmplification = 1f;
private float[] _sampleData = null;
...
// a DSP callback - which will apply a gain factor to the volume
private void MyGain(int handle, int channel, IntPtr buffer, int length, IntPtr user)
{
  if (_gainAmplification == 1f || length == 0 || buffer == IntPtr.Zero)
    return;

  // length is in bytes, so the number of floats to process is length/4 
  // byte = 8-bit, float = 32-bit
  int l4 = length / 4;

  // increase the data buffer as needed
  if (_sampleData == null || _sampleData.Length < l4)
    _sampleData = new float[l4];

  // copy from unmanaged to managed memory
  Marshal.Copy(buffer, _sampleData, 0, l4);

  // apply the gain, assuming 32-bit floats (no clipping)
  for (int a=0; a<l4; a++)
    _sampleData[a] = _sampleData[a] * _gainAmplification;

  // copy back from unmanaged to managed memory
  Marshal.Copy(_sampleData, 0, buffer, l4);
}
VB.Net: Buffer Access using Marshal.Copy
Private _gainAmplification As Single = 1f
Private _sampleData As Single() = Nothing

' a DSP callback - which will apply a gain factor to the volume
Private Sub MyGain(handle As Integer, channel As Integer, 
                   buffer As IntPtr, length As Integer, user As IntPtr)
  If _gainAmplification = 1f Or length = 0 Or buffer = IntPtr.Zero Then
      Return
   End If 

  ' length is in bytes, so the number of floats to process is length/4 
  ' byte = 8-bit, float = 32-bit
  Dim l4 As Integer = length / 4

  ' increase the data buffer as needed
  If _sampleData Is Nothing Or _sampleData.Length < l4 Then
    _sampleData = New Single(l4) {}
  End If 

  ' copy from unmanaged to managed memory
  Marshal.Copy(buffer, _sampleData, 0, l4)

  ' apply the gain, assuming 32-bit floats (no clipping)
  Dim a As Integer
  For a = 0 To l4 - 1
    _sampleData(a) = _sampleData(a) * _gainAmplification
  Next a 

  ' copy back from unmanaged to managed memory
  Marshal.Copy(_sampleData, 0, buffer, l4)
End Sub
This example is type safe but 'slow', since two copy operations are involved. One from unmanaged BASS to managed .NET and when processing has been done locally from .NET back to BASS. However, for VB.Net users this is almost the only way to do it.

C# user can be a little more lucky, since C# supports unsafe code blocks and native pointer access - which will be shown in the following example.

C#: Buffer Access using Pointers in an Unsafe Code Block
// a DSP callback - which will apply a gain factor to the volume
private unsafe void MyGain(int handle, int channel, IntPtr buffer, int length, IntPtr user)
{
  if (_gainAmplification == 1f || length == 0 || buffer == IntPtr.Zero)
    return;

  // length is in bytes, so the number of floats to process is length/4 
  // byte = 8-bit, float = 32-bit
  int l4 = length / 4;

  // simply cast the given IntPtr to a native pointer to float values
  float *data = (float*)buffer;
  for (int a=0; a<l4; a++)
  {
    data[a] = data[a] * _gainAmplification;
    // alternatively you can also use:
    // *data = *data * _gainAmplification;
    // data++;
  }
}
In this example we directly access the memory as given by BASS. When using unsafe code blocks all happens pretty much like as if you would handle things in C/C++. Unsafe here just means no overflow and no type safety checking is performed - so we are just as 'unsafe' as a C/C++ application. Note, that here BASS has allocated the memory, so there is no reason to pin the pointers with the 'fixed' statement, as all memory allocated by unmanaged BASS is of course not subject to garbage collection. The major advantage of unsafe code is, that you don't need to copy or allocate addition memory. So accessing the memory with native pointers is by far the fastest way. And performance can be a critical thing when dealing with BASS callbacks such as a DSP.

For compiling unsafe code use the /unsafe option in your compiler settings. In VS.NET go to the project property page and in 'configuration properties - build' set 'Allow Unsafe Code Blocks' to 'True'.

C#: Declaring Pointers
float *ptrToAFloatValue;  // pointer to a float value
short *ptrToAShortValue;  // pointer to an Int16 value
byte  *ptrToAByteValue;   // pointer to a byte value
int   var1 = 7;           // regular variable
int   *ptr1 = &var1;      // ptr1 points to the address of var1
*ptr1 = 20;               // assign a value of 20 to the content ptr1 is pointing to
The operator & means AddressOf, as such int *ptr1 = &var1; will hold the address of var1, where ptr1 is the address of the object in memory. Using a * before a pointer name means 'the content of the address', e.g. *ptr1 = 20; means assigning a value of 20 - var1 will now hold this value. But remember, if you are passing data allocated in .NET to unmanaged BASS you'll need to pin those objects in order to prevent garbage collection and re-location.

Allocating Data in .NET which should be used by BASS

A typical example is BASS_ChannelGetData. After calling this method BASS will provide the requested data at the address specified by the IntPtr buffer. Note: BASS will not allocate memory for you. Instead you just tell BASS the address of the memory location where BASS should put the data. BASS will assume, that the memory is already allocated and just puts the requested data in there.

C#: Buffer Provisioning By Reference
// a 30ms window in bytes to be filled with sample data
int length = (int)Bass.BASS_ChannelSeconds2Bytes(channel, 0.03);

// first we need a mananged object where the sample data should be placed
// length is in bytes, so the number of floats to process is length/4 
float[] data = new float[length/4];

// get the sample data
length = Bass.BASS_ChannelGetData(channel, data, length);
VB.Net: Buffer Provisioning By Reference
' a 30ms window in bytes to be filled with sample data
Dim length As Integer = CInt(Bass.BASS_ChannelSeconds2Bytes(channel, 0.03))

' first we need a mananged object where the sample data should be placed
' length is in bytes, so the number of floats to process is length/4 
Dim data(length / 4) As Single

' get the sample data
length = Bass.BASS_ChannelGetData(channel, data, length)
This example is by far the most easiest way to get the data filled to your buffer (and it is also the fastest, as we are making a PInvoke with a reference type which will just be pinned automatically). .NET resp. the declaration in BASS.NET takes care internally of marshaling and converting the address of the managed object to an appropriate unmanaged address pointer. So this is the recommended way of providing an address space to BASS. Note, that here you also don't need to pin the data object, since the object is automatically pinned during the internal marshaling process for the duration of the platform invoke call. And since in this example BASS doesn't use the provided address pointer after the method call returns there is definitly no need for further pinning it.

The next possibility is to manually allocate memory directly on the heap and provide that pointer to BASS:

C#: Buffer Provisioning using Marshal.Copy
// a 30ms window in bytes to be filled with sample data
int length = (int)Bass.BASS_ChannelSeconds2Bytes(channel, 0.03);

// first we need a mananged object where the sample data should be placed
// length is in bytes, so the number of floats to process is length/4 
float[] data = new float[length/4];

// allocate a buffer of that size for unmanaged code
IntPtr buffer = Marshal.AllocCoTaskMem( length );

// get the data
length = Bass.BASS_ChannelGetData(channel, buffer, length);

// and copy the data from unmanaged BASS to our local managed application
Marshal.Copy( buffer, data, 0, length/4);

// and free the allocated unmanaged memory
Marshal.FreeCoTaskMem( buffer );
VB.Net: Buffer Provisioning using Marshal.Copy
' a 30ms window in bytes to be filled with sample data
Dim length As Integer = CInt(Bass.BASS_ChannelSeconds2Bytes(channel, 0.03))

' first we need a mananged object where the sample data should be placed
' length is in bytes, so the number of floats to process is length/4 
Dim data(length / 4) As Single

' allocate a buffer of that size for unmanaged code
Dim buffer As IntPtr = Marshal.AllocCoTaskMem(length)

' get the data
length = Bass.BASS_ChannelGetData(channel, buffer, length)

' and copy the data from unmanaged BASS to our local managed application
Marshal.Copy(buffer, data, 0, length / 4)

' and free the allocated unmanaged memory
Marshal.FreeCoTaskMem(buffer)
As you can see there are three steps involved: Allocating memory, copying the data received to our .NET array and freeing the memory. This technique works perfectly and is 100% type safe, but in terms of performance not really optimal, but it shows you what is possible.

In the next example BASS will write the data directly to a managed object, since we create a handle pointing to a float array and tell BASS to write it's data at the address referenced by the handle. In terms of performance this is also a good way to do it, since no additional memory allocation is required and no copy operation is involved - and it is type safe as well - but the GCHandle operation requires some extra time.

C#: Buffer Provisioning using GCHandle
// a 30ms window in bytes to be filled with sample data
int length = (int)Bass.BASS_ChannelSeconds2Bytes(channel, 0.03);

// first we need a mananged object where the sample data should be held
// only length / 4 elements needed, since length is in byte and a float rep. 4 bytes
float[] data = new float[length/4];

// create a pinned handle to a managed object
GCHandle hGC = GCHandle.Alloc(data, GCHandleType.Pinned);

// get the data
length = Bass.BASS_ChannelGetData(channel, hGC.AddrOfPinnedObject(), length);

// free the pinned handle
hGC.Free();
VB.Net: Buffer Provisioning using GCHandle
' a 30ms window in bytes to be filled with sample data
Dim length As Integer = CInt(Bass.BASS_ChannelSeconds2Bytes(channel, 0.03))

' first we need a mananged object where the sample data should be held
' only length / 4 elements needed, since length is in byte and a float rep. 4 bytes
Dim data(length / 4) As Single

' create a pinned handle to a managed object
Dim hGC As GCHandle = GCHandle.Alloc(data, GCHandleType.Pinned)

' get the data
length = Bass.BASS_ChannelGetData(channel, hGC.AddrOfPinnedObject(), length)

' free the pinned handle
hGC.Free()
And finally a way to provide an address pointer to BASS (which is as fast as the first example) and again is only possible on C#, as it again uses an unsafe code block and allows BASS to directly write it's data to our managed object (using a direct pointer). Performance is perfect, but no type safety is checked at all. Note: You need to compile your application with the /unsafe option in order to allow unsafe code. In this example you need to pin the object using the 'fixed statement', as here no marshaling is involved at all and you need to prevent garbage collection and re-location.
C#: Buffer Provisioning using Pointers
// a 30ms window in bytes to be filled with sample data
int length = (int)Bass.BASS_ChannelSeconds2Bytes(channel, 0.03);

// first we need a mananged object where the sample data should be held
// only length / 4 elements needed, since length is in byte and a float rep. 4 bytes
float[] data = new float[length/4];

// start an unsafe code block allowing you to use native pointers
unsafe
{
    // pointers to managed objects need to be fixed
    fixed (float* p = data)    // equivalent to p = &data[0]
    {
        length = Bass.BASS_ChannelGetData(channel, (IntPtr)p, length);
    }
}
If you are into VB.Net the following example almost does the same and is as quick as the above:
VB.Net: Buffer Provisioning using Pointers
' a 30ms window in bytes to be filled with sample data
int length = (int)Bass.BASS_ChannelSeconds2Bytes(channel, 0.03);

' allocate the buffer of that size directly in unmanaged code
Dim buffer As IntPtr = Marshal.AllocCoTaskMem(length)
Try
    length = Bass.BASS_ChannelGetData(stream, buffer, length)
Catch
Finally
   ' free the allocated unmanaged memory
   If buffer <> IntPtr.Zero Then
      Marshal.FreeCoTaskMem(buffer)
   End If
End Try
The only downside with the above VB.Net code is, that you are almost unable to access the buffer content itself. However, there are various scenarios, where you only want to call BASS_ChannelGetData without accessing the data, e.g. if you need to decode an entire channel in a loop to feed a DSPPROC. In such scenarios the above makes complete sense, since the single unmanaged buffer pointer avoids the unnecessary copy operations which will be carried out automatically by the .Net Runtime during marshaling if you where using managed array buffers.

See Also

Other Resources