Author Topic: BASS ASIO threading and pausing  (Read 568 times)

3delite

  • Posts: 904
BASS ASIO threading and pausing
« on: 22 Jan '17 - 19:55 »
Hi!

I have 2 questions about BASS ASIO, I would like to ask.

The current setup, example with a 8x2 channel sound card:

  • 8x2 ASIO inputs into a 16 channel push stream
  • 1x2 channel splitter from the push stream FX'ed
  • the splitter plugged into a 8x2 channel mixer stream with matrix mixing to specify output
  • 8x2 ASIO output getting data from the previous mixer stream handle

1. When I'd like to add a stereo pair, I add a splitter for the input push stream and plug it into the out mixer. My question is, that is that a good idea to pause the ASIO inputs and outputs while creating the splitter and plugging it into the out mixer. I'd like to have the splitters in sync with each other:

Code: [Select]
           BASS_ASIO_ChannelPause(True, 0);
           BASS_ASIO_ChannelPause(False, 0);

           Create splitter...
           Plug splitter to out mixer...

           BASS_ASIO_ChannelReset(True, 0, BASS_ASIO_RESET_PAUSE);
           BASS_ASIO_ChannelReset(False, 0, BASS_ASIO_RESET_PAUSE);

2. If I have this 2x8 input and 2x8 output ASIO setup, then is the ASIOPROC called in 1 single thread? Is it so that the ASIOPROC is called first for input and then for output (sounds the way to go)?
If I add FX to the splitter streams and there are 8 splitter streams, each with FX, it could be a lot of CPU work for 1 thread. And it seems the output ASIOPROC will GetData() the out mixer from within 1 thread only.

« Last Edit: 22 Jan '17 - 20:24 by 3delite »

Ian @ un4seen

  • Administrator
  • Posts: 20426
Re: BASS ASIO threading and pausing
« Reply #1 on: 23 Jan '17 - 18:07 »
1. When I'd like to add a stereo pair, I add a splitter for the input push stream and plug it into the out mixer. My question is, that is that a good idea to pause the ASIO inputs and outputs while creating the splitter and plugging it into the out mixer. I'd like to have the splitters in sync with each other:

Code: [Select]
           BASS_ASIO_ChannelPause(True, 0);
           BASS_ASIO_ChannelPause(False, 0);

           Create splitter...
           Plug splitter to out mixer...

           BASS_ASIO_ChannelReset(True, 0, BASS_ASIO_RESET_PAUSE);
           BASS_ASIO_ChannelReset(False, 0, BASS_ASIO_RESET_PAUSE);

Rather than pausing the ASIO channels, it may be better to just quickly lock the mixer (via BASS_ChannelLock) to add the new splitter:

Code: [Select]
           BASS_ChannelLock(mixer, true); // lock mixer

           Create splitter...
           Plug splitter to out mixer...

           BASS_ChannelLock(mixer, false); // unlock mixer

2. If I have this 2x8 input and 2x8 output ASIO setup, then is the ASIOPROC called in 1 single thread? Is it so that the ASIOPROC is called first for input and then for output (sounds the way to go)?
If I add FX to the splitter streams and there are 8 splitter streams, each with FX, it could be a lot of CPU work for 1 thread. And it seems the output ASIOPROC will GetData() the out mixer from within 1 thread only.

Yes, if you have combined the inputs into a single block and combined the outputs into a single block, then parallel processing won't be possible; the inputs will always be processed before the outputs (never at the same time) to allow the input data to be forwarded to the outputs with minimal latency. To allow parallel processing, you would need to split the input and/or output channel blocks.

3delite

  • Posts: 904
Re: BASS ASIO threading and pausing
« Reply #2 on: 23 Jan '17 - 19:15 »
I'm not sure locking the output mixer is enough - the input push stream's splitter can be plugged into a different ASIO device's output mixer stream as well. It's a mixer/router app., the whole purpose is to route various inputs to various outputs.

Regarding the threading, I was thinking it works this way. But thinking about this, the problem is I would like to have 1 output mixer that can be FX'ed and recorded (the recorder is just pushing data to a push stream - so it should be quick) and I'd like to have the path that all stages of the stream should be FX'ed (EQ and VST DSP) and recorded. The input (with all channels), the stereo ASIO split pairs and the output mixer stage too. And what maybe could be done, if a mixer has multiple inputs would it be possible somehow for the mixer, to spawn multiple threads (+1 when a new source is plugged in - and no thread when there is only 1 source), and GetData() the mixer sources in these threads in parallel, then something like WaitForMultipleObjects() all the threads and then continue? :) Would this be faster or would at all work?
Maybe with a flag, so when it's explicitly needed, to write a code like:

Code: [Select]
    if BASS_Mixer_SourceCount(Channel) > 0 then begin
        BASS_Mixer_StreamAddChannel(...BASS_MIXER_SOURCE_THREAD...);
    end else begin
        BASS_Mixer_StreamAddChannel(...same as before...);
    end;

Thank you!
« Last Edit: 23 Jan '17 - 23:53 by 3delite »

Ian @ un4seen

  • Administrator
  • Posts: 20426
Re: BASS ASIO threading and pausing
« Reply #3 on: 24 Jan '17 - 17:44 »
I'm not sure locking the output mixer is enough - the input push stream's splitter can be plugged into a different ASIO device's output mixer stream as well. It's a mixer/router app., the whole purpose is to route various inputs to various outputs.

OK. In that case, it would probably be best to pause the input channels. To help with that, here is the latest BASSASIO build, which includes the option of pausing all input channels simultaneously:

   www.un4seen.com/stuff/bassasio.zip

Set the BASS_ASIO_ChannelPause "channel" parameter to -1 to pause all input (or output) channels.

Regarding the threading, I was thinking it works this way. But thinking about this, the problem is I would like to have 1 output mixer that can be FX'ed and recorded (the recorder is just pushing data to a push stream - so it should be quick) and I'd like to have the path that all stages of the stream should be FX'ed (EQ and VST DSP) and recorded. The input (with all channels), the stereo ASIO split pairs and the output mixer stage too. And what maybe could be done, if a mixer has multiple inputs would it be possible somehow for the mixer, to spawn multiple threads (+1 when a new source is plugged in - and no thread when there is only 1 source), and GetData() the mixer sources in these threads in parallel, then something like WaitForMultipleObjects() all the threads and then continue? :) Would this be faster or would at all work?
Maybe with a flag, so when it's explicitly needed, to write a code like:

Code: [Select]
    if BASS_Mixer_SourceCount(Channel) > 0 then begin
        BASS_Mixer_StreamAddChannel(...BASS_MIXER_SOURCE_THREAD...);
    end else begin
        BASS_Mixer_StreamAddChannel(...same as before...);
    end;

The latest BASSmix build does have an async buffering option for splitters, which means splitters can request/buffer data from their sources asynchronously so that it's ready to go when needed (eg. when requested by a mixer). But I'm not sure it would help in this case because the input data is immediately requested by the output, ie. there's no opportunity for asynchronous buffering?

If you would like to try it anyway, the latest BASSmix build is here:

   www.un4seen.com/stuff/bassmix.zip

The new async stuff is the BASS_ATTRIB_SPLIT_ASYNCBUFFER and BASS_ATTRIB_SPLIT_ASYNCPERIOD options. The former sets how much data to asynchronously buffer from the source, and the latter sets how much data to request from the source each time. Both values are in seconds, and can be adjusted (via BASS_ChannelSetAttribute) at any time. Setting BASS_ATTRIB_SPLIT_ASYNCBUFFER to 0 disables asynchronous buffering (that's the default).

3delite

  • Posts: 904
Re: BASS ASIO threading and pausing
« Reply #4 on: 25 Jan '17 - 01:30 »
Ok, thank you very much for your help!

As far as I understand the problem is that the ASIOPROC will run in a single thread. It's simple:

Code: [Select]
function ASIOProc(Input: BOOL; Channel: DWORD; Buffer: Pointer; Length: DWORD; User: Pointer): DWORD; stdcall;
var
    Sender: TASIODeviceUsage;
    GotData: Cardinal;
    PutData: Cardinal;
begin
    Result := 0;
    Sender := TASIODeviceUsage(User);
    if Input then begin
        if Sender.InChannel <> 0 then begin
            PutData := BASS_StreamPutData(Sender.InChannel, Buffer, Length);
            if PutData = DW_ERROR then begin
                PutData := 0; // an error, no data
            end;
            Result := PutData;
        end;
    end else begin
        if Sender.OutMixer <> 0 then begin
            GotData := BASS_ChannelGetData(Sender.OutMixer, Buffer, Length);
            if GotData = DW_ERROR then begin
                GotData := 0; // an error, no data
            end;
            Result := GotData;
        end;
    end;
end;

It's not clear to me whether it would help, please think about this mixer threading, it could be a useful feature if it would work.

Thank you!

3delite

  • Posts: 904
Re: BASS ASIO threading and pausing
« Reply #5 on: 27 Jan '17 - 15:22 »
One question then. So if I want to separate the ASIO output pairs into separate threads, I use BASS_ASIO_ChannelEnable() + BASS_ASIO_ChannelJoin(). Then this pair will be invoked from a separete thread, if I call these again with another params, they will be called in another thread again?
And what happens when I'd like to assign another pair, do I need to call BASS_ASIO_ChannelPause() or is it enough just to call BASS_ASIO_ChannelEnable() + BASS_ASIO_ChannelJoin() again (with different params each time)?

Ian @ un4seen

  • Administrator
  • Posts: 20426
Re: BASS ASIO threading and pausing
« Reply #6 on: 27 Jan '17 - 16:18 »
Each enabled channel/pair doesn't have its own thread. Rather the device/driver can have a pool of threads assigned to process the channels, as specified in the BASS_ASIO_Start call. Each channel's ASIOPROC may be called in any of those threads, not necessarily the same thread every time.

It isn't possible to enable/disable channels once the ASIO device has been started (via BASS_ASIO_Start); it will need to be stopped first (via BASS_ASIO_Stop). But you can pause/resume channels then. So you should enable all channels that you will need up front, and then pause/resume them as needed.

3delite

  • Posts: 904
Re: BASS ASIO threading and pausing
« Reply #7 on: 28 Jan '17 - 14:45 »
Thank you very much, it's clear now.
I modified the code to have 3x2 output channels and put a breakpoint inside the ASIOPROC and Delphi displayed that both the input and 3 output calls were all in the same thread.
I am using a Sound Blaster Z card with 2x2 inputs and 3x2 outputs. I'm not sure if all this worths the trouble. :)
So it looks like if that mixer threading would work it would help in this case too. Just a suggestion, it would be a powerful feature.

Thank you for helping!

EDIT: I created a little test app. to measure how much it takes to wake a worker-thread. My ASIO default buffer size, that I'm testing with, is 10 ms. The test app. displayed amazingly small values, from 3 to 60 microseconds! Doing this check 100 times. So this mixing threading should work even with ASIO. Hope it helps!

Thank you!
« Last Edit: 28 Jan '17 - 21:36 by 3delite »

3delite

  • Posts: 904
Re: BASS ASIO threading and pausing
« Reply #8 on: 13 Feb '17 - 10:43 »
I started my own implementation of a threaded mixer functionality and I got stuck.

I create a stream with a STREAMPROC and have the following code:

Code: [Select]
procedure TForm1.Button1Click(Sender: TObject);
var
    BASSThreadedMixer: TBASSThreadedMixer;
    SourceChannel: HStream;
    Buffer: Pointer;
    BufferSize: Integer;
    PerfFreq, AfterTicks, BeforeTicks: Int64;
    TookTime: Double;
    GotData: Cardinal;
begin
    //* Create a threaded mixer channel
    BASSThreadedMixer := TBASSThreadedMixer.Create(44100, 2, BASS_STREAM_DECODE OR BASS_SAMPLE_FLOAT);
    Channel := BASSThreadedMixer.Channel;
    //* Crreate and add a source 1
    SourceChannel := BASS_StreamCreateFile(False, PChar('Nick Cave - Where the wild roses grow.mp3'), 0, 0, BASS_UNICODE OR BASS_STREAM_DECODE OR BASS_SAMPLE_FLOAT);
    BASS_ChannelSetDSP(SourceChannel, @DSPPROC, nil, 0);
    BASSThreadedMixer.AddSource(SourceChannel, nil);
    //* Crreate and add a source 2
    SourceChannel := BASS_StreamCreateFile(False, PChar('Nick Cave - Where the wild roses grow.mp3'), 0, 0, BASS_UNICODE OR BASS_STREAM_DECODE OR BASS_SAMPLE_FLOAT);
    BASS_ChannelSetDSP(SourceChannel, @DSPPROC, nil, 0);
    BASSThreadedMixer.AddSource(SourceChannel, nil);
    //* Crreate and add a source 3
    SourceChannel := BASS_StreamCreateFile(False, PChar('Nick Cave - Where the wild roses grow.mp3'), 0, 0, BASS_UNICODE OR BASS_STREAM_DECODE OR BASS_SAMPLE_FLOAT);
    BASS_ChannelSetDSP(SourceChannel, @DSPPROC, nil, 0);
    BASSThreadedMixer.AddSource(SourceChannel, nil);
    //* Do processing
    BufferSize := 1024 * 2 * 4;
    GetMem(Buffer, BufferSize);
    try
        QueryPerformanceFrequency(PerfFreq);
        while BASS_ChannelIsActive(Channel) <> BASS_ACTIVE_STOPPED do begin
            QueryPerformanceCounter(BeforeTicks);
            GotData := BASS_ChannelGetData(Channel, Buffer, BufferSize);
            QueryPerformanceCounter(AfterTicks);
            TookTime := (AfterTicks - BeforeTicks) / PerfFreq * 1000;
            Memo1.Lines.Append(FloatToStrF(TookTime, ffFixed, 2, 2) + ' ms (' + IntToStr(GotData) + ')');
            Application.ProcessMessages;
        end;
    finally
        FreeMem(Buffer);
    end;
end;

When requesting 8192 bytes (in the code above) my STREAMPROC is called with length = 8 over and over again. Is this expected?
I return 8 bytes and then in the code above I get GotData = 0.

The STREAMPROC:

Code: [Select]
function STREAMPROC(Handle: HSTREAM; Buffer: Pointer; RequestLength: DWORD; user: Pointer): DWORD; stdcall;
begin
    ...processing here, fill 8 bytes into the buffer...
    Result := 8;
end;

If I set the stream to 8 channels then again a strange thing happens: the BASS_SYNC_FREE sync is called and after that the STREAMPROC. In the free sync I free my class and the STREAMPROC crashes because my class is already freed. Can the STREAMPROC be called after getting a BASS_SYNC_FREE sync?

Thank you!

Ian @ un4seen

  • Administrator
  • Posts: 20426
Re: BASS ASIO threading and pausing
« Reply #9 on: 13 Feb '17 - 16:57 »
When requesting 8192 bytes (in the code above) my STREAMPROC is called with length = 8 over and over again. Is this expected?

That sounds strange. Are you still using BASS_Mixer_StreamCreate/etc internally, or are you doing the mixing yourself? If the former, is "Channel" the BASS_StreamCreate or BASS_Mixer_StreamCreate stream? Please also confirm what flags you're using in the BASS_StreamCreate, BASS_Mixer_StreamCreate and BASS_Mixer_StreamAddChannel calls.

The STREAMPROC:

Code: [Select]
function STREAMPROC(Handle: HSTREAM; Buffer: Pointer; RequestLength: DWORD; user: Pointer): DWORD; stdcall;
begin
    ...processing here, fill 8 bytes into the buffer...
    Result := 8;
end;

Is the STREAMPROC always returning 8 regardless of the "RequestLength" value, or are you just saying that "RequestLength" is always 8 here? :)

If I set the stream to 8 channels then again a strange thing happens: the BASS_SYNC_FREE sync is called and after that the STREAMPROC. In the free sync I free my class and the STREAMPROC crashes because my class is already freed. Can the STREAMPROC be called after getting a BASS_SYNC_FREE sync?

What handle is the BASS_SYNC_FREE sync set on? A STREAMPROC won't/shouldn't be called on that handle afterwards, but perhaps it's being called on another one? Also make sure you're not calling BASS_StreamFree within the STREAMPROC (or a mixtime SYNCPROC).

3delite

  • Posts: 904
Re: BASS ASIO threading and pausing
« Reply #10 on: 13 Feb '17 - 18:48 »
I don't want to publish my code here, sent you an email, thank you!