Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to: accurately emulate 2D panning #194

Closed
dbregman opened this issue May 28, 2018 · 4 comments
Closed

How to: accurately emulate 2D panning #194

dbregman opened this issue May 28, 2018 · 4 comments

Comments

@dbregman
Copy link

dbregman commented May 28, 2018

Hello,

I realize this question is not directly related to openal-soft, but since this question has been asked multiple times on stackoverflow and other sites without any satisfactory answers, I figured this is a place where a true expert reply might be found :)

I am porting an old game from DirectSound to OpenAL and I have not been able to figure out how to emulate IDirectSoundBuffer_SetPan(buffer, pan).

basically the pan parameter goes from -1 to 1, which should be interpreted as follows:
-1 => left channel is full volume, right channel is attenuated -100dB
-0.5 => left channel is full volume, right channel is attenuated -50dB
0 => both channels full volume
+0.5 => right channel is full volume, left channel is attenuated -50dB
+1 => right channel is full volume, left channel is attenuated -100dB
etc, etc. for other values.

Stuff I've seen on the net suggests something like:
// create a panning effect by moving the source in an arc around the listener
alDistanceModel(AL_NONE)
alSource(source, AL_SOURCE_RELATIVE, TRUE)
alSource(source, AL_POSITION, {pan, 0, sqrt(1-pan*pan)})

This pretty much works for -1 and +1, but for a lot of the inbetween values it is quite different (which is not that surprising, after all why would it be exactly the same). How does OpenAL calculate the left/right volume based on the relative position? If I knew that maybe I could invert it to find the position based on the desired volumes. Alternatively, is there another method of accurately emulating the panning functionality?

Thanks,
David

@kcat
Copy link
Owner

kcat commented May 28, 2018

Hi.

There's currently no direct way to emulate 2D panning. But depending on the purpose of panning, you can do something similar which may be good enough. Like what you posted:

// create a panning effect by moving the source in an arc around the listener
alSourcef(source, AL_ROLLOFF_FACTOR, 0.0f);
alSourcei(source, AL_SOURCE_RELATIVE, TRUE);
alSource3f(source, AL_POSITION, {pan, 0, -sqrtf(1.0f - pan*pan)});

Except pan should range between -0.5f (left) and +0.5f (right). This equates to 30 degrees left and right, which is where stereo speakers are expected to be placed and thus the response you'd get in such systems.

Given that OpenAL is a 3D audio API which simulates a 3D soundfield, you're moving sounds in 3D space rather than between discrete speaker feeds. There's no guarantee about what the output volume will be for the individual speakers. For plain stereo output, this should result in something like you expect (pan = -0.5f should have 0 dB on the left speaker and -inf dB on the right, pan = 0.5f reverses that, and pan = 0.0f should have about -4.5 dB equally on the left and right[1]).

For surround sound, UHJ, and HRTF, there is no guarantee at all about speaker levels. But it is guaranteed that OpenAL will do the best it can to focus the sound where it's placed (so regardless, with pan = 0.5f it will sound like it's 30 degrees right of center, where the right speaker would be in a stereo setup).

[1] The -4.5 reduction is to keep the apparent volume equal as it moves. If it was 0dB on each like "real" panning, it would be louder moving through the center compared to the sides, and -6dB would make it sound quieter as it moved through the center.

It is possible to further widen the panning. If you use -1 and +1 then a full left pan will be to the immediate left of the listener, and a full right pan will be to the immediate right of the listener (+/-90 degrees instead of 30). This is fine also, but that may not be a wholly desirable result; for plain stereo output, going from 30 degrees to 90 degrees has no apparent change, while surround sound or HRTF may not have as noticeable of a change beyond a certain point.

@dbregman
Copy link
Author

Thank you very much for that answer kcat. Your suggestion of moving -/+30 degrees is a big improvement compared to the original code. Actually though it is still much too aggressive in reducing the volume compared to the original panning. I've been using audacity to record the output and comparing to the original. I found that if I use only 10 degrees it seems much closer.

I still wish there were a way to be more direct in getting the result I'm looking for. Based on your post I now understand that it's going to depend on the speaker setup how OpenAL renders it, however isn't there a standard formula that's used for the stereo case (as you say, "plain stereo output") which can be inverted to get the desired volumes?

@kcat
Copy link
Owner

kcat commented May 29, 2018

Roughly speaking, plain stereo output follows a sine/cosine amplitude response. A sound on the left speaker (-30 degrees) maps to 0 radians, center is pi/4, and right (+30 degrees) is pi/2, where the left output gain is cos(r) and right output gain is sin(r). This is also similar to the square root response, where X goes from 0 (left) to 1 (right), and the gains are left = sqrt(1 - X) and right = sqrt(X). Note that these are linear gains. To get the dB response, you need to convert:

dB = log10f(gain) * 20.0f`;
gain = powf(10.0f, dB / 20.0f);

OpenAL Soft's plain stereo output may not be exactly that, but it should be fairly close. Current master is also going through some changes, but ultimately it should fall in line with that before release.

@dbregman
Copy link
Author

dbregman commented May 29, 2018

I tried one based on the sine/cosine response that you described. Inverting both responses and averaging yields this formula:

pan = (acosf(left_gain) + asinf(right_gain)) / ((float)M_PI); // average angle in [0,1]
pan = 2 * pan - 1; // convert to [-1, 1]
pan = pan * 0.5f; // 0.5 = sin(30') for a +/- 30 degree arc
alSourcei(source, AL_SOURCE_RELATIVE, TRUE);
alSource3f(source, AL_POSITION, {pan, 0, -sqrtf(1.0f - pan*pan)});

It's not perfect but this seems to work pretty well. I plotted the gain function compared to the ideal one and the main difference is it's quieter in the middle. I think it's probably possible to get even closer but I'm happy with this for now.

Thanks again for the help - would not have been able to do it without you. Hopefully someone else who searches this problem will now find this thread as well.

K4thos added a commit to K4thos/Ikemen_GO that referenced this issue Jun 29, 2021
support for missing mugen features: PlaySnd and SndPan sctrl pan / abspan parameters as well as auto sound panning based on how far character is from center of screen. Can be disabled via StereoEffects and adjusted with PanningRange (works similarly to mugen PanningWidth but operates at easier to understand range: 0-100). Enabled by default (as in mugen).

After implementing most of the code I've found following article that can be likely used for better implementation than manually adjusting channels volume: kcat/openal-soft#194 If someone would like to work on it feel free to change any of this new code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants