|
Hektor
Posts: 10
|
 |
« on: 29 May '11 - 23:03 » |
Quote
|
Hi
I have a .net app which is currently based on XAudio2 and DirectSound and I'm trying to replace it with ASIO to reduce the overall latency. The application plays an audio file, mixes it with an audio input, process the mixed data and plays the result. All is done with small data chunks. The target is to have minimum latency from audio in to audio out. All audio data is PCM 24bit @ 48KHz.
Here is part of the code I'm using: _myAsioInProc = New ASIOPROC(AddressOf AsioInCallback)
_myAsioOutProc = New ASIOPROC(AddressOf AsioOutCallback)
' not playing anything via BASS, so don't need an update thread
Bass.BASS_SetConfig(BASSConfig.BASS_CONFIG_UPDATEPERIOD, 0)
' setup BASS - "no sound" device but 48000 (default for ASIO)
Bass.BASS_Init(1, 48000, BASSInit.BASS_DEVICE_DEFAULT + BASSInit.BASS_DEVICE_MONO, IntPtr.Zero)
BassAsio.BASS_ASIO_Init(1)
_asioin = New BassAsioHandler(True, 1, 0, 1, BASSASIOFormat.BASS_ASIO_FORMAT_24BIT, 48000)
_asioout = New BassAsioHandler(False, 1, 0, 1, BASSASIOFormat.BASS_ASIO_FORMAT_24BIT, 48000)
BassAsio.BASS_ASIO_ChannelEnable(True, 0, _myAsioInProc, IntPtr.Zero)
BassAsio.BASS_ASIO_ChannelEnable(False, 0, _myAsioOutProc, IntPtr.Zero)
_asioin.Start(0)
_asioout.Start(0)
I added two callbacks, one for Audio In and one for Audo Out. In the In callback I copy the content of "buffer" to mix and process it. This works fine. In the Out callback I write the next data chunk to the "buffer".
The problem is: there is no sound. Nothing is played. I'm most probably missing something here and appreciate any help.
Tnx
|
|
|
|
|
Logged
|
|
|
|
|
MB_SOFT
Posts: 246
|
 |
« Reply #1 on: 29 May '11 - 23:32 » |
Quote
|
In the AsioOutCallback are you returning the number of bytes that you wrote to the buffer?
|
|
|
|
|
Logged
|
|
|
|
|
radio42
Posts: 4012
|
 |
« Reply #2 on: 29 May '11 - 23:33 » |
Quote
|
As you are calling "BASS_ASIO_ChannelEnable" manually you are kind of 'stealing' the ASIOPROC from the "BassAsioHandler" - as such the BassAsioHandler wouldn't work properly anymore. So don't so that. As I assume you are using a BASSmix mixer channel to mix the input with the audio file you want to play. Let's assume you have that, here is what you can do: a) create a decoding mixer stream and use it as the source for an ASIO output handler: _mixerStream = BassMix.BASS_Mixer_StreamCreate(48000, 1, BASSFlag.BASS_STREAM_DECODE | BASSFlag.BASS_SAMPLE_FLOAT); _asioout = new BassAsioHandler(1, 0, _mixerStream);
// now you can add sources to that mixer...to be played int stream = Bass.BASS_StreamCreateFile(fileName, 0, 0, BASSFlag.BASS_STREAM_DECODE | BASSFlag.BASS_SAMPLE_FLOAT); BassMix.BASS_Mixer_StreamAddChannel(_mixerStream, stream, BASSFlag.BASS_MIXER_DOWNMIX | BASSFlag.BASS_MIXER_FILTER);
Note: the ASIO format, samplerate and number of channels are assumed from the BASS stream (_mixerStream) for an Asio output handler. b) in order to create a decoding recording stream as a source to a mixer channel you need to do the following: Call "_asioin.SetFullDuplex" to create a full-duplex decoding BASS stream (output). The "_asioin.OutputChannel" can now be used as a source to a mixer channel. Internally a decoding PUSH stream is created. The internal ASIOPROC already pushed any data received from the input to the _asioin.OutputChannel. _asioin.BypassFullDuplex = true; // bypass any output processing _asioin.SetFullDuplex(0, BASSFlag.BASS_STREAM_DECODE, false); _fullDuplexStream = _asioin.OutputChannel; BassMix.BASS_Mixer_StreamAddChannel(_mixerStream, _fullDuplexStream, BASSFlag.BASS_MIXER_DOWNMIX | BASSFlag.BASS_MIXER_FILTER); _asioin.BypassFullDuplex = false; // enable output processing
That's it.
|
|
|
|
|
Logged
|
|
|
|
|
Hektor
Posts: 10
|
 |
« Reply #3 on: 30 May '11 - 00:04 » |
Quote
|
Thanks guys for the prompt reply. In the AsioOutCallback are you returning the number of bytes that you wrote to the buffer?
Excellent catch. I was returning zero.  Now although it is still not working properly, I start to hear something. @radio42 I apologize. I guess I should have provided a better description of my app. All the mixing and processing are handled by my app, not using BASS. I'm only using BASS as In and Out interface to ASIO. What I need is a simple and low latency way to get the PCM row samples from the Audio Input and a way to deliver the post processing PCM samples to the Audio Output. Clearly, I need the In and Out to be in Sync on the clock level to prevent underflow or overflow. To make it clear, accessing the Audio File, mixing it with the Audio In samples and processing the mixed data is all handled by the app outside BASS. I hope it's clearer now. As you are calling "BASS_ASIO_ChannelEnable" manually you are kind of 'stealing' the ASIOPROC from the "BassAsioHandler" - as such the BassAsioHandler wouldn't work properly anymore. So don't so that. I'm confused. w/o "BASS_ASIO_ChannelEnable" how do I specify the callbacks for In and Out ?
|
|
|
|
« Last Edit: 30 May '11 - 00:17 by Hektor »
|
Logged
|
|
|
|
|
radio42
Posts: 4012
|
 |
« Reply #4 on: 30 May '11 - 06:39 » |
Quote
|
w/o "BASS_ASIO_ChannelEnable" how do I specify the callbacks for In and Out ? "BASS_ASIO_ChannelEnable" is the correct way! I just say, that this wouldn't work together with the "BassAsioHandler" class. The "BassAsioHandler" class is a wrapper which typically does the jobs for you. But in your case you need to setup ASIO totally manually. Meaning just doen't use the "BassAsioHandler" class, but use the native methods yourself! Using: BASS_ASIO_Init, BASS_ASIO_SetDevice, BASS_ASIO_SetRate, BASS_ASIO_ChannelEnable, BASS_ASIO_ChannelSetFormat, BASS_ASIO_ChannelSetRate.
|
|
|
|
|
Logged
|
|
|
|
|
Hektor
Posts: 10
|
 |
« Reply #5 on: 30 May '11 - 08:11 » |
Quote
|
OK. Got it now. Thanks.
One quick question: I'm using ch 0 as Asio in and ch 0+1 as Asio stereo out. How do I now control Start/Stop separately for In and Out ?
BASS_ASIO_Start() doesn't provide control over a specific channel and direction.
|
|
|
|
|
Logged
|
|
|
|
|
Hektor
Posts: 10
|
 |
« Reply #6 on: 30 May '11 - 10:13 » |
Quote
|
Ok, discovered the Channel Reset.
Tnx
|
|
|
|
|
Logged
|
|
|
|
|
Hektor
Posts: 10
|
 |
« Reply #7 on: 7 Jun '11 - 22:39 » |
Quote
|
I was able to get the ASIO IN and OUT working, however, it is not stable. I'm using the following ASIO setup: _myAsioInProc = New ASIOPROC(AddressOf AsioInCallback) _myAsioOutProc = New ASIOPROC(AddressOf AsioOutCallback)
BASS_SetConfig(BASSConfig.BASS_CONFIG_UPDATEPERIOD, 0) BASS_Init(1, 48000, BASSInit.BASS_DEVICE_DEFAULT, IntPtr.Zero) BASS_ASIO_Init(1)
BASS_ASIO_SetDevice(1) BASS_ASIO_SetRate(48000) BASS_ASIO_ChannelSetRate(True, 0, 48000) BASS_ASIO_ChannelSetFormat(True, 0, BASSASIOFormat.BASS_ASIO_FORMAT_24BIT) BASS_ASIO_ChannelSetRate(False, 0, 48000) BASS_ASIO_ChannelSetFormat(False, 0, BASSASIOFormat.BASS_ASIO_FORMAT_24BIT) BASS_ASIO_ChannelJoin(False, 1, 0)
BASS_ASIO_ChannelEnable(True, 0, _myAsioInProc, IntPtr.Zero) BASS_ASIO_ChannelEnable(False, 0, _myAsioOutProc, IntPtr.Zero)
BASS_ASIO_ChannelPause(False, 0) BASS_ASIO_Start(512) In the two callbacks I'm handling the PCM data manually. - I've added a stopwatch to the Audio In callback and discovered that as expected for 512 samples at 48KHz, the gap between two calls is 10mS or 11mS (this is fine since the stopwatch resolution is 1mS). However, from time to time the gap between two calls is larger then that and usually the gap to the next call is shorter. For example, a gap of 18mS between calls will be usually followed by a gap of 2mS (so the average is kept). - I've also noticed that the callback thread priority is set to "Highest". - I checked other Threads running in parallel and as far as I can tell they are very light on the CPU and also have lower priority Any ideas why the gap between callbacks is not steady ? tnx.
|
|
|
|
|
Logged
|
|
|
|
|
radio42
Posts: 4012
|
 |
« Reply #8 on: 8 Jun '11 - 09:31 » |
Quote
|
This might be related to the .Net GarbageCollector running from time to time which might defer unmanaged callbacks during the time the GC is running. If you are not experiancing any audio drop outs this shouldn't be problematic?!
|
|
|
|
|
Logged
|
|
|
|
|
Hektor
Posts: 10
|
 |
« Reply #9 on: 8 Jun '11 - 10:25 » |
Quote
|
Tnx.
well, unfortunately I do experience dropouts. This is why I started investigating this. Is there anyway to solve this ?
|
|
|
|
|
Logged
|
|
|
|
|
|
|
Hektor
Posts: 10
|
 |
« Reply #11 on: 10 Jun '11 - 21:42 » |
Quote
|
Just for my undestanding, regarding what I'm expiriencing, is it related to my specific implementation ? Assuming it is has something to do with GC, will GC.keepalive() be of any help ?
Another info on the problem, I discovered that there are times that the callback is trigrred sooner then expected (not only after a delayed callback). I mean, sometimes I get several callbacks at 10mS/11mS and then a callback at 2mS (or other values less than 10mS/11mS).
|
|
|
|
|
Logged
|
|
|
|
|
radio42
Posts: 4012
|
 |
« Reply #12 on: 11 Jun '11 - 08:09 » |
Quote
|
An GC issue can not be resolved by GC.KeepAlive, since that will only work with managed objects but not with unmanaged callbacks. However, any GC should only delay a callback call, but not decrease it. If an ASIOPROC is being called 'earlier' this must be due to the ASIO driver requesting data more often.
|
|
|
|
|
Logged
|
|
|
|
|
Hektor
Posts: 10
|
 |
« Reply #13 on: 14 Jun '11 - 05:42 » |
Quote
|
Tnx.
I've done additional tests and checked also the multi-thread performance view. Like you suggested it looks like it has something to do with GC as I see that each time clr.dll thread is completed, a callback is skipped.
As for GC.Keepalive, I'm no expert, but as far as I can understand it is specifically targeting unmanaged parts as stated by MS: "The purpose of the KeepAlive method is to ensure the existence of a reference to an object that is at risk of being prematurely reclaimed by the garbage collector. A common scenario where this might happen is when there are no references to the object in managed code or data, but the object is still in use in unmanaged code such as Win32 APIs, unmanaged DLLs, or methods using COM."
I believe that what I'm trying to do is very basic: getting all the samples of a single audio input via ASIO w/o dropouts in .Net. and I understand that it can't be done with the current bass.net without going into mixed managed/unmanaged code handling myself.
- Any chances you'll be adding this handling to bass.net ? this make sense as it is a common issue for all .net ASIO implementations. - Any otherway this can be solved without me going into mixed managed/unmanaged handling ?
tnx
|
|
|
|
« Last Edit: 14 Jun '11 - 08:03 by Hektor »
|
Logged
|
|
|
|
|
radio42
Posts: 4012
|
 |
« Reply #14 on: 14 Jun '11 - 10:05 » |
Quote
|
Actually there is no chance to add that in general to Bass.Net, as this has nothing to do with the Bass.Net implementation - GC handling is a typical .Net thing and can not be changed/modified within Bass.Net alone. The GC.KeepAlive won't help either, as any GC run will anyhow 'suspend' any pending managed/unmanaged threads - which will be the case for an ASIOPROC anyhow. The GC.KeepAlive just tells the GC not to collect an object which is only logically references from unmanaged code - but the GC will still run and as such suspend any threads. So not much Bass.Net can do here! To avoid this you can either do one of the following: a) make sure you GC runs take minimum time b) increase your ASIO buffer, so that it is bigger than the largest GC run time c) use mixed managed/unmanaged assembies In case of c) you wouldn't have to take much care about a) and b) anymore. Actually ASIO processing is a 'non basic' scenario for C# as we are dealing with real-time processing in such case - and real-time coding is one of the most advanced coding topics at all! Any .Net languages are not perfect for real-time processing (due to the nature of the GC). That's why I posted the mixed managed/unmanaged code example to provide a propper solution. See here: http://www.un4seen.com/forum/?topic=4932.msg60399#msg60399Also note, that as written in the docs and in the above example almost ANY unmanaged BASS callback is a candidate which might break a real-time application! As you can guess that each user might need different callback implementations. That's why it is almost impossible to come up with a generic Bass.Net solution. However, if you take a look to the mixed mode example you'll see, that it is actually not difficult to build one. Note, that this ONLY applies to real-time audio apps (ie. using ASIO with very low latency) any other 'buffered' audio apps are typically not effected.
|
|
|
|
|
Logged
|
|
|
|
|
Hektor
Posts: 10
|
 |
« Reply #15 on: 14 Jun '11 - 16:03 » |
Quote
|
Thank you.
I'm actually using VB and not C# or C++ and this is why I'm trying to avoid the managed/unmanaged part. I'm probably missing something here but I don't really understand why it's not reasonable to add some sort of a typical mixed handling to bass.net just for receiving or sending audio data via ASIO. All other handling can be done outside the callback.
btw, the skipped callback is happening not during each run of the clr.dll thread but just after the completion of each run. (pls see the attached pic) Thinking of an alternative solution, would it be possible to use polling with real-time interrupt instead of the callback ? it's probably not the most efficient way to handle it but would it work ? is there a way to get the amount data available in the ASIO in buffer ?
Tnx
|
|
|
« Last Edit: 14 Jun '11 - 16:18 by Hektor »
|
Logged
|
|
|
|
|
radio42
Posts: 4012
|
 |
« Reply #16 on: 14 Jun '11 - 16:51 » |
Quote
|
It's just a matter of when the GC is suspending/blocking managed/unmanaged threads/calls - which is not exactly know in .Net, as this is an internal Microsoft implementation. Using any other solutions, like using 'real-time interrupts' would have the same effect: at some point unmanaged code must call intop something in the managed .Net world - as such the .Net GC bounderies would be hit again (e.g. at the time you want to process the interrupt the GC might start running).
Bass.Net implements a generic wrapper for any BASS module. So directly implementing a 'basic' mixed-mode handler for ASIO within Bass.Net would enforce each user of Bass.Net to also provide and use the BassAsio.dll - as it is impossible to dynamically link BassAsio in a mixe-mode assembly - however, many users don't even use or want to use BassAsio!
In addition (as said) the exact same thing might not only happen within an ASIOPROC, but in ANY bass callback, e.g. a DSPPROC (see the docs)! And coding a 'basic' or 'generic' DSPPROC is impossible - as any user wants to do different things in a DSPPROC! That's why I think it is not reasonable and even possible to provide any generic or basic mixed-mode handling in any way.
So that again leaves you with the options outlined above. In most cases it is sufficient to optimize your code, so that the time it takes for a GC to complete stays below the ASIO buffer. But if minimum latency is absolutly critical for you and you can not further minimize the time it takes for a GC to complete (as I guess), that you must provide your own mixed-mode assembly - that's the only way to be really independent from any .Net GC things!
When you take a look to the mixed-mode assembly example given, this is rather straight forward and easy to implement - nothing complicated at all. The needed C++ code itself does even look very very similar to C# or VB and consists only of a few lines - so I really must assume, that anyone coding a hightly critical real-time ASIO application should be able to code these things, as it's really ONLY the callback code which needs to be moved to the mixed-mode assembly, ANY other (BASS) code can still fully exist in your managed application.
|
|
|
|
|
Logged
|
|
|
|
|
Hektor
Posts: 10
|
 |
« Reply #17 on: 3 Jul '11 - 06:19 » |
Quote
|
Tnx again for the detailed answer.
I can't see how reducing GC time below the buffer time will help since as can be seen in the figure I previously attached, the callbacks are being serviced in parallel to GC. The problem is that a callback is skipped after GC is completed.
Anyway, the reason I initially tried BASS.NET was to see if the is a quick and working solution for ASIO under .NET as I prefer to invest my time in my application. For those, like myself, who are only using ASIOPROC it would be great if you can provide the required mixed-mode DLL to support the ASIO IN and ASIO OUT callbacks functions:
AsioInCallback(ByVal input As Boolean, ByVal channel As Integer, ByVal buffer As IntPtr, ByVal length As Integer, ByVal user As IntPtr) As Integer
AsioOutCallback(ByVal input As Boolean, ByVal channel As Integer, ByVal buffer As IntPtr, ByVal length As Integer, ByVal user As IntPtr) As Integer
This DLL will save some of us a lot of time and can be used only by those who need it and won't affect others. If and when there will be a solution for ASIO in .NET w/o me going into mixed mode programming, I'd be more then happy to use it. Otherwise, I'm afraid I'll have to use something else.
Tnx.
|
|
|
|
|
Logged
|
|
|
|
|