Content:
- Managed vs. Unmanaged Code
- Garbage Collection
- Lifetime and Pinning
- Using BASS_SetConfigPtr
- Streaming from Memory with BASS_StreamCreateFile
- Using Callbacks with a 'user' Parameter
- Internal Marshaling
- Callbacks and Delegates
- Cross Thread Communication
- Marshaling and Memory Access
- Using Data in .NET which has been allocated by BASS
- Allocating Data in .NET which should be used by BASS
- See Also
Interoperating with 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).
- Locates the DLL containing the function.
- Loads the DLL into memory (if not already loaded).
- 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.
- Transfers control to the unmanaged function.
This section contains the following subsections:
- Lifetime and Pinning
- Using BASS_SetConfigPtr
- Streaming from Memory with BASS_StreamCreateFile
- Using Callbacks with a 'user' Parameter
- Internal Marshaling
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):- Objects might be disposed (collected) during garbage collection.
- Objects might be re-located during garbage collection.
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: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();
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()
- Any call to BASS_SetConfigPtr.
- Any call to BASS_StreamCreateFile when providing a memory block to stream from.
- Any call to a function which takes a callback "user" parameter to which you want to pass a class instance, e.g.BASS_StreamCreate/STREAMPROC, BASS_StreamCreateFileUser/BASS_FILEPROCS, BASS_StreamCreateURL/DOWNLOADPROC, BASS_RecordStart/RECORDPROC, BASS_ChannelSetDSP/DSPPROC, BASS_ChannelSetSync/SYNCPROC etc.
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:
- Use an System.TextEncoder to convert the Unicode string to a byte array and pin the array.
- Directly allocate an ANSI string in unmanaged memory using the System.Runtime.InteropServicesMarshal class.
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();
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()
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);
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)
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:
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();
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()
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:
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 ... }
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
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();
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()
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:


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!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 ... }
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
// 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 ... }
' 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
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:// 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; }
' 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
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]; } ); }
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; } }
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
This section contains the following subsections:
- Using Data in .NET which has been allocated by BASS
- Allocating Data in .NET which should be used by BASS
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).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.
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); }
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
// 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++; } }
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
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.
// 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);
' 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)
// 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 );
' 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)
// 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();
' 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()
// 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); } }
' 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