Playing the same file through multiple devices/channels

Started by xfx,

xfx

Let me explain my setup:

  • I init and select an audio device (BASS_Init / BASS_SetDevice)
  • I create a mixer stream for the selected device (BASS_Mixer_StreamCreate)
  • I create a sample stream (BASS_StreamCreateFile) from a file
  • I create a resampling stream (BASS_FX_TempoCreate) from the sample stream in step 3
  • Finally, I add the resampling stream from step 4 to the mixer stream from step 2

Now, I need to be able to support multiple devices so that I can play the same sample stream (from step 3) on multiple devices with different speaker configurations.

The problem is that when I try to add the resampling stream to multiple mixer streams I get a BASS_ERROR_ALREADY.

The idea is to be able to send the same audio to multiple audio devices while only having to handle one single stream reference for manipulating the stream (play, pause, stop, get/set position, etc...) otherwise, I will need to create as many sample streams as devices have been assigned and keep track of all them separately; I think this would make things very complicated and there's no guarantee that all audio devices will be as-in-sync as possible.

-----------

Not that it helps much but, I have already done this on a different project but I cannot use that implementation here because that other project doesn't use a mixer stream. So I guess my problem is that I can only add one unique sample/resampling stream to a mixer stream.

-----------

Here's the UI to select multiple audio devices with multiple speaker configurations.
Hopefully this helps illustrate what I'm trying to do:



Ian @ un4seen

It looks like the BASSmix add-on's "splitter" feature may be what you want. You would create a splitter with BASS_Split_StreamCreate on the "sample stream" (from step 3) for each mixer that you want to feed, and pass the splitter to BASS_FX_TempoCreate or perhaps BASS_Mixer_StreamAddChannel - are you sure you need the BASS_FX_TempoCreate call? Mixers have built-in resampling support, so pre-resampling shouldn't be necessary.

Please see the BASS_Split_StreamCreate documentation for details on it.

xfx

Thanks for the prompt response.

Yes, this seems to work, however it looks like I will be creating as many split streams as devices have been assigned, right?

Here's how the code looks like right now:
List<int> splitterStreams = [];
private bool CreateStreams() {
    BASSError bassErrorCode = BASSError.BASS_OK;

    streamHandle = Bass.BASS_StreamCreateFile(file?.Filename, 0, 0, BASSFlag.BASS_STREAM_DECODE
                                                                    | BASSFlag.BASS_STREAM_PRESCAN
                                                                    | BASSFlag.BASS_ASYNCFILE
                                                                    | BASSFlag.BASS_SAMPLE_FLOAT);
    bassErrorCode = Bass.BASS_ErrorGetCode();
    if(streamHandle == 0 || bassErrorCode != BASSError.BASS_OK) {
        // TODO: Do something so the user knows the file cannot be played
        return false;
    }

    fxHandle = BassFx.BASS_FX_TempoCreate(streamHandle, BASSFlag.BASS_FX_TEMPO_ALGO_SHANNON
                                                        | BASSFlag.BASS_STREAM_DECODE
                                                        | BASSFlag.BASS_FX_FREESOURCE);
    bassErrorCode = Bass.BASS_ErrorGetCode();
    if(fxHandle == 0 || bassErrorCode != BASSError.BASS_OK) {
        // TODO: Do something so the user knows the file cannot be played
        return false;
    }

    foreach(var mixHandle in Program.BassMixHandles) {
        int splitterHandle = BassMix.BASS_Split_StreamCreate(fxHandle, BASSFlag.BASS_STREAM_DECODE, null);

        bassErrorCode = Bass.BASS_ErrorGetCode();
        if(splitterHandle == 0 || bassErrorCode != BASSError.BASS_OK) {
            // TODO: Do something so the user knows the file cannot be played
            return false;
        }
        splitterStreams.Add(splitterHandle);

        BASSFlag speakerConfig = GetBassSpeakerFlags(mixHandle);
        bool r = BassMix.BASS_Mixer_StreamAddChannel(mixHandle.Handle, splitterHandle, BASSFlag.BASS_MIXER_CHAN_PAUSE
                                                                                       | BASSFlag.BASS_MIXER_CHAN_NORAMPIN
                                                                                       | BASSFlag.BASS_MIXER_CHAN_BUFFER
                                                                                       | BASSFlag.BASS_STREAM_AUTOFREE
                                                                                       | speakerConfig);
        // | BASSFlag.BASS_SPEAKER_FRONTLEFT | BASSFlag.BASS_MUSIC_MONO | BASSFlag.BASS_MIXER_CHAN_DOWNMIX);
        bassErrorCode = Bass.BASS_ErrorGetCode();
        if(!r || bassErrorCode != BASSError.BASS_OK) {
            // TODO: Do something so the user knows the file cannot be played
            return false;
        }
    }

    return true;
}


So if I want to start playing the stream I will need to start all splitters, like this:

splitterHandles.ForEach(h => BassMix.BASS_Mixer_ChannelPlay(h));
Where splitterHandles are all the handles returned by calling BASS_Split_StreamCreate.
Is this the correct approach? because I was hoping I could control all those streams from a single handle and avoid having to run for-loops whenever I need to change something about the stream.

Ian @ un4seen

Yes, that all looks basically correct. You would have a splitter for each mixer that you want to feed the stream to, and they will need to be added/started individually (eg. in a "for" loop). If you'll be repeatedly stopping and starting a source then it may be a good idea to reset the splitters via BASS_Split_StreamReset before each start to make sure they're still in sync. Note you can use the source handle in that call to reset all of its splitters.

I'm not sure about the "speakerConfig" stuff in your BASS_Mixer_StreamAddChannel calls. Is that replicating the SPEAKER flags in the BASS_Mixer_StreamCreate calls? If so, that isn't necessary. A mixer's SPEAKER flags determine which speakers are used on the device, while a source's SPEAKER flags determine what channels it's on in the mix. You would use SPEAKER flags in one or the other, not both. For example, you might create an 8 channel mixer (BASS_Mixer_StreamCreate with chans=8 and no SPEAKER flags) and put a source on the rear speakers (BASS_Mixer_StreamAddChannel with BASS_SPEAKER_REAR), or you might create a stereo mixer on the rear speakers (BASS_Mixer_StreamCreate with chans=2 and BASS_SPEAKER_REAR flag) and a source would end up on the rear speakers without any SPEAKER flags. Matrix mixing (see BASS_MIXER_CHAN_MATRIX) allows more elaborate arrangements, eg. putting a source on any combination of channels in the mix.

xfx

#4
Thank you Ian! I think I got it working.

I was using the speakers' flags since that's what I used on a different project, but BASS_Mixer_speakersetMatrix appears to be a much better option.

One last thing if I may, am I setting up the matrices correctly?

private float[,] GetSpeakersMatrix(int deviceIndex) {
    float[,] matrix;
    var audioDevices = Program.Settings.Audio.MainOutputDevice.Concat(Program.Settings.Audio.MonitorDevice).ToArray();
    var speakers = audioDevices[deviceIndex].Speakers;
    int index = Program.GetDeviceIndexByName(audioDevices[deviceIndex].Name);
    Bass.BASS_SetDevice(index);
    switch(Bass.BASS_GetInfo().speakers) {
        case 2:
            matrix = new float[,] {
                    {
                        (speakers.Contains(AudioDevice.DeviceSpeakers.FrontLeft) || speakers.Contains(AudioDevice.DeviceSpeakers.FrontStereo)) ? 1 : 0,
                        0
                    },
                    {
                        0,
                        (speakers.Contains(AudioDevice.DeviceSpeakers.FrontRight) || speakers.Contains(AudioDevice.DeviceSpeakers.FrontStereo)) ? 1 : 0
                    }
            };
            break;

        case 4:
            matrix = new float[,] {
                    {
                        (speakers.Contains(AudioDevice.DeviceSpeakers.FrontLeft) || speakers.Contains(AudioDevice.DeviceSpeakers.FrontStereo)) ? 1 : 0,
                        0
                    },
                    {
                        0,
                        (speakers.Contains(AudioDevice.DeviceSpeakers.FrontRight) || speakers.Contains(AudioDevice.DeviceSpeakers.FrontStereo)) ? 1 : 0
                    },
                    {
                        (speakers.Contains(AudioDevice.DeviceSpeakers.SideLeft) || speakers.Contains(AudioDevice.DeviceSpeakers.SideStereo)) ? 1 : 0,
                        0
                    },
                    {
                        0,
                        (speakers.Contains(AudioDevice.DeviceSpeakers.SideRight) || speakers.Contains(AudioDevice.DeviceSpeakers.SideStereo)) ? 1 : 0
                    }
            };
            break;

        case 6:
            matrix = new float[,] {
                    {
                        (speakers.Contains(AudioDevice.DeviceSpeakers.FrontLeft) || speakers.Contains(AudioDevice.DeviceSpeakers.FrontStereo)) ? 1 : 0,
                        0
                    },
                    {
                        0,
                        (speakers.Contains(AudioDevice.DeviceSpeakers.FrontRight) || speakers.Contains(AudioDevice.DeviceSpeakers.FrontStereo)) ? 1 : 0
                    },
                    {
                        (speakers.Contains(AudioDevice.DeviceSpeakers.Center) || speakers.Contains(AudioDevice.DeviceSpeakers.CenterAndLFE)) ? 1 : 0,
                        0
                    },
                    {
                        0,
                        (speakers.Contains(AudioDevice.DeviceSpeakers.LFE) || speakers.Contains(AudioDevice.DeviceSpeakers.CenterAndLFE)) ? 1 : 0
                    },
                    {
                        (speakers.Contains(AudioDevice.DeviceSpeakers.SideLeft) || speakers.Contains(AudioDevice.DeviceSpeakers.SideStereo)) ? 1 : 0,
                        0
                    },
                    {
                        0,
                        (speakers.Contains(AudioDevice.DeviceSpeakers.SideRight) || speakers.Contains(AudioDevice.DeviceSpeakers.SideStereo)) ? 1 : 0
                    }
            };
            break;

        case 8:
            matrix = new float[,] {
                    {
                        (speakers.Contains(AudioDevice.DeviceSpeakers.FrontLeft) || speakers.Contains(AudioDevice.DeviceSpeakers.FrontStereo)) ? 1 : 0,
                        0
                    },
                    {
                        0,
                        (speakers.Contains(AudioDevice.DeviceSpeakers.FrontRight) || speakers.Contains(AudioDevice.DeviceSpeakers.FrontStereo)) ? 1 : 0
                    },
                    {
                        (speakers.Contains(AudioDevice.DeviceSpeakers.Center) || speakers.Contains(AudioDevice.DeviceSpeakers.CenterAndLFE)) ? 1 : 0,
                        0
                    },
                    {
                        0,
                        (speakers.Contains(AudioDevice.DeviceSpeakers.LFE) || speakers.Contains(AudioDevice.DeviceSpeakers.CenterAndLFE)) ? 1 : 0
                    },
                    {
                        (speakers.Contains(AudioDevice.DeviceSpeakers.SideLeft) || speakers.Contains(AudioDevice.DeviceSpeakers.SideStereo)) ? 1 : 0,
                        0
                    },
                    {
                        0,
                        (speakers.Contains(AudioDevice.DeviceSpeakers.SideRight) || speakers.Contains(AudioDevice.DeviceSpeakers.SideStereo)) ? 1 : 0
                    },
                    {
                        (speakers.Contains(AudioDevice.DeviceSpeakers.RearLeft) || speakers.Contains(AudioDevice.DeviceSpeakers.RearStereo)) ? 1 : 0,
                        0
                    },
                    {
                        0,
                        (speakers.Contains(AudioDevice.DeviceSpeakers.RearRight) || speakers.Contains(AudioDevice.DeviceSpeakers.RearStereo)) ? 1 : 0
                    }
            };
            break;

        default:
            matrix = new float[,] {
                    {
                        0,
                        0
                    }
            };
            break;
    }

    return matrix;
}


Ian @ un4seen

Is the source always stereo? If not, the matrix array will need adjusting for that, as it should have output (mixer) x input (source) channel elements. For example:

// out=stereo, in=stereo
matrix = new float[,] {
    {1, 0}, // left out = left in
    {0, 1} // right out = right in
};

// out=stereo, in=mono
matrix = new float[,] {
    {1}, // left out = mono in
    {1} // right out = mono in
};

Let me know if you have trouble getting the matrixes working as wanted.

xfx

I'd guess is very unlikely that some DJ would play a multi-channel track ;)

So far, everything is working perfectly.
Thanks for everything Ian!