Skip to content

Add support for balance control (CC 8) #317

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

Merged
merged 6 commits into from
Feb 11, 2018
Merged

Add support for balance control (CC 8) #317

merged 6 commits into from
Feb 11, 2018

Conversation

mawe42
Copy link
Member

@mawe42 mawe42 commented Jan 4, 2018

This pull-request adds support for the balance control on CC 8, as discussed on the mailing list:
http://lists.nongnu.org/archive/html/fluid-dev/2018-01/msg00006.html

It also makes a slight change to the range of the panning control so that voices panned to the center are exactly the same value on both channels. It does this by reducing the range to 1 - 127, treating 0 and 1 as "hard left". This treatment is specified in the MIDI Recommended Practice (RP-036).

This special treatment is also used for the balance control, as it has the same problem with the non-representable center value on a 0 - 127 range.

@@ -138,6 +138,7 @@ static fluid_mod_t default_expr_mod; /* SF2.01 section 8.4.7 */
static fluid_mod_t default_reverb_mod; /* SF2.01 section 8.4.8 */
static fluid_mod_t default_chorus_mod; /* SF2.01 section 8.4.9 */
static fluid_mod_t default_pitch_bend_mod; /* SF2.01 section 8.4.10 */
static fluid_mod_t default_balance_mod; /* Non-standard modulator */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be called custom_balance_mod

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good idea. Will change it.

);
fluid_mod_set_source2(&default_balance_mod, 0, 0);
fluid_mod_set_dest(&default_balance_mod, GEN_BALANCE); /* Destination: stereo balance */
fluid_mod_set_amount(&default_balance_mod, 1000.0); /* Amount: 1000 tens of a percent */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you not using the same amount as GEN_PAN does?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Soundfont specifies that generator ranges should be in real-world units. -500;500 makes perfect sense for pan, because it is defined as the position on a 180 degree half circle. So -500 is 50% of 180 left.

Balance is not expressed as position, but as volume. So 100% left makes much more sense to say "right channel is silent" than 50%.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm... Thinking about it, maybe balance volume should be expressed in centibels, like volume and expression. That would make the range -960;960, the modulator would get a concave shape and we would use fluid_cb2amp for conversion. Actually, that would also make volume changes much smoother in the extreme regions. What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just thought that giving it a pure linear scale wouldnt be "smooth" enough. I thought using a sinus would be still somewhat linear but in a smoother manner. This is where we would end up in fluid_pan() again.

fluid_cb2amp() could work as well. What I dont like about that [centi|deci]bel scale is that it never turns off. Yes, the volume gets very silent, probably unhearable, but if I set CC8=127 I would expect the left chan to be completely off.

But sure when looking at volume and expression, perhaps we should really use fluid_cb2amp() to be somewhat "consistent"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sinus in pan is actually there to linearise the perceived volume, using equal power distribution. In listening tests I checked the perceived volume changes on a mono sample with pan and balance. With the current implementation, they are very similar, if not the same.

Using centibels, balance would be much smoother. Oh, and we could simply force -960 and 960 to be 0 (or 1 on the opposite channel)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright then fluid_cb2amp().

Oh, and we could simply force -960 and 960 to be 0 (or 1 on the opposite channel)

No special casing pls. 10^(960/-200)=0.000016 and that's how things should be then :)

Copy link
Member Author

@mawe42 mawe42 Jan 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No special casing pls. 10^(960/-200)=0.000016 and that's how things should be then :)

In that case I would strongly prefer the current solution. A balance control that can't turn one channel off completely would be flawed, imho.

What's so bad about forcing the extreme values to 0?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A balance control that can't turn be channel off completely would be flawed, imho.

Same like a main volume control that cant turn off a channel completely, which currently seems to be the case (at least I experienced that).

What's so bad about forcing the extreme values to 0?

I dont like branches in math calculations. They cause hearable discontinuity and branch prediction failures. They just dont feel right. Where would you place these if clauses? Probably after modulator processing, a place where you would need to do a floating point comparison for x==960.0. How likely is that to evaluate to true?

In that case I would strongly prefer the current solution.

Or we go with fluid_pan(), i.e. sin().

@mawe42
Copy link
Member Author

mawe42 commented Jan 4, 2018

I dont like branches in math calculations. They cause hearable discontinuity and branch prediction failures. They just dont feel right. Where would you place these if clauses? Probably after modulator processing, a place where you would need to do a floating point comparison for x==960.0. How likely is that to evaluate to true?

Hearable discontinuities are there in any case. Just listen to the steps in volume between pan 127 and pan 126 on a mono sample. And if we put the special casing into the setup of the conversion table (just like it's already been done for fluid_concave_tab and fluid_convex_tab where index 0 is forced to 0.0f and index 127 is forced to 1.0f), then we don't need any new branches. So just add the following after the fluid_cb2amp_tab setup:

fluid_cb2amp_tab[0] = 0.0f;
fluid_cb2amp_tab[FLUID_CB_AMP_SIZE - 1] = 1.0f;

That would have the added benefit that the volume and expression controls can also turn off (and on) completely.

But incidentally... I wonder why FLUID_CB_AMP_SIZE is defined as 961?!

@mawe42
Copy link
Member Author

mawe42 commented Jan 4, 2018

But incidentally... I wonder why FLUID_CB_AMP_SIZE is defined as 961?!

Never mind... of course it's because the range is that long, from 0 - 960. Doh...

@derselbst
Copy link
Member

Hearable discontinuities are there in any case. Just listen to the steps in volume between pan 127 and pan 126 on a mono sample.

Well ok, that's a necessary discontinuity due to the limited 7 bit resolution.

fluid_cb2amp_tab[0] = 0.0f;

I think it should be fluid_cb2amp_tab[FLUID_CB_AMP_SIZE-1] = 0.0f. But anyway, even if it's just a minor nit-picky change, it would violate the spec.

That would have the added benefit that the volume and expression controls can also turn off (and on) completely.

Yes I would be very happy with it. Still by the spec apparently volume and expression are not meant to turn channels off completely and I do not want to support such a change just because of my personal attitude.

So we have these three options. From my personal musical perspective/preference I would go with fluid_pan(). OTOH to be consistent with volume calc. I would use fluid_cb2amp(). Difficult. Keeping it linear is not my favourite. But I would leave the decision to you, as you have more musical experience than I.

@mawe42
Copy link
Member Author

mawe42 commented Jan 4, 2018

Yes I would be very happy with it. Still by the spec apparently volume and expression are not meant to turn channels off completely and I do not want to support such a change just because of my personal attitude.

Actually, I think the current behaviour is against the spec. From the Sound Font 2.04 document:

The volume envelope operates in dB, with the attack peak providing a full scale output, appropriately scaled by the initial volume. The zero value, however, is actually zero gain. The implementation in the EMU8000 provides for 96 dB of amplitude control. When 96 dB of attenuation is reached in the final gain amplifier, an abrupt jump to zero gain (infinite dB of attenuation) occurs. In a 16-bit system, this jump is inaudible.

@derselbst
Copy link
Member

The zero value, however, is actually zero gain.

Oh, good spot. Well ok then, let's make us both happy with fluid_cb2amp_tab[FLUID_CB_AMP_SIZE-1] = 0.0f and fluid_cb2amp().

@mawe42
Copy link
Member Author

mawe42 commented Jan 4, 2018

Hm... on the other hand, that is only specified for the final gain amplifier and doesn't express anything about intermediate calculations. Oh dear... :-)

@derselbst
Copy link
Member

Argh, I intentionally over-read that ^^

Well, we could ask mrbumpy409 (S. Christian Collins), he owns some EMU hardware, perhaps a third opinion might help?

@mawe42
Copy link
Member Author

mawe42 commented Jan 4, 2018

we could ask mrbumpy409 (S. Christian Collins), he owns some EMU hardware, perhaps a third opinion might help?

That would be great, yes.

However, reading the spec again and again, I come to the conclusion that Fluidsynths current implementation is actually not correct. The output of the final gain amplifier (per sample/voice) is meant to be mixed into the output and sent to the effect units. The amount of attenuation that a voice receives should only be considered at this stage, and here we should also check if the attenuation is 96db or greater and force it to be infinite (so an amp of 0), according to spec. So the best place to check this is IMHO fluid_voice_calculate_gain_amplitude, maybe like this:

static FLUID_INLINE fluid_real_t
fluid_voice_calculate_gain_amplitude(const fluid_voice_t* voice, fluid_real_t gain)
{
    /* we use 24bit samples in fluid_rvoice_dsp. in order to normalize float
     * samples to [0.0;1.0] divide samples by the max. value of an int24 and
     * amplify them with the gain */
    const fluid_real_t INT24_MAX = (1 << (16+8-1)) * 1.0f;
    const fluid_real_t MIN_AMP = pow(10.0, (double) 960.0 / -200.0) / INT24_MAX;
    fluid_real_t amp = gain * voice->synth_gain / INT24_MAX;
    return (amp <= MIN_AMP) ? 0.0f : amp;
}

I've just tested this with s16 and s24 output and it does have the desired effect. And one more float comparison shouldn't affect performance, I think.

@mawe42
Copy link
Member Author

mawe42 commented Jan 4, 2018

Ah, no. It shouldn't take the final output gain into consideration. So the check should be on gain, not on amp:

static FLUID_INLINE fluid_real_t
fluid_voice_calculate_gain_amplitude(const fluid_voice_t* voice, fluid_real_t gain)
{
    /* we use 24bit samples in fluid_rvoice_dsp. in order to normalize float
     * samples to [0.0;1.0] divide samples by the max. value of an int24 and
     * amplify them with the gain */
    const fluid_real_t INT24_MAX = (1 << (16+8-1)) * 1.0f;
    const fluid_real_t MIN_GAIN = pow(10.0, (double) 960.0 / -200.0);

    if (gain <= MIN_GAIN) {
        return 0.0f;
    }
    else {
        return gain * voice->synth_gain / INT24_MAX;
    }
}

@mawe42
Copy link
Member Author

mawe42 commented Jan 4, 2018

Maybe I should think about this more before posting. Actually that only takes pan and balance into account, not the rest of the volume envelope. So the check should be in fluid_rvoice_calc_amp, testing if target_amp is less than pow(10.0, (double) 960.0 / -200.0).

@mawe42
Copy link
Member Author

mawe42 commented Jan 4, 2018

Ok, I think this "96 dB should be zero" problem is outside of the scope of this feature addition, as it also affects other attenuation sources like expression and volume.

What about this: we change the balance to use centibels and live with not-completely-silent-channels problem for now. In addition, we open a ticket for the attenuation problem and solve that separately.

@derselbst
Copy link
Member

we change the balance to use centibels and live with not-completely-silent-channels problem for now.

Ok.

In addition, we open a ticket for the attenuation problem and solve that separately.

Feel free to do so.

If you still need advice/review from mrbumpy409, he's on github, you could just ping him.

Copy link
Member

@derselbst derselbst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested and seems to works as expected. I would leave this open for a few days to give the mailing list community some time. You can merge this whenever you want.

@derselbst derselbst added this to the 2.0 milestone Jan 6, 2018
@mawe42
Copy link
Member Author

mawe42 commented Jan 6, 2018

Ok, thanks a lot for testing! Will wait a few days and then merge.

@jjceresa
Copy link
Collaborator

jjceresa commented Jan 29, 2018

@mawe42 I have done a quick look inside (not in depth). I only just wanted to get confirmation about the intended implementation.
Both, existing PAN modulator and new BALANCE modulator (this PR) now act simultaneously on the final voices amp (left and right) parameters. That is:

  • On CC pan change , both new CC pan value and current CC balance value update voice amp (left,right). ?

  • similarly, on CC balance change , both new CC balance value and current CC pan value update voice amp (left,right). ?

So actually the implementation seems to be done in the right place (i.e on the MIDI side task) without impacting performance of the audio rendering function (i.e on the audio task side).

  1. I have not yet tested the CC balance response (i.e the function amp = f(CC balance)). I must admit that i haven't yet understood the intended response behaviour. Probably this could be easily resumed ?.
    I guess default balance value is neutral (i.e centered) and behaves as before ?
  2. Pan behaviour seems to be closer the same as before ?.

Thanks for your work.

@mawe42
Copy link
Member Author

mawe42 commented Feb 4, 2018

@jjceresa Sorry for the late answer! Yes, pan and balance both affect the voices amp values.

I must admit that i haven't yet understood the intended response behaviour. Probably this could be easily resumed ? I guess default balance value is neutral (i.e centered) and behaves as before ?

Yes, the initial balance value is neutral, so if you don't set a balance value, the sound is unaffected. The intended response behaviour is just like the stereo balance knob on any home hifi system. So "turning the knob" to the left makes the right channel quieter but leaves the left channel untouched.

Pan behaviour seems to be closer the same as before ?

Yes, pan behaviour is unaffected by this change.

@jjceresa
Copy link
Collaborator

jjceresa commented Feb 4, 2018

So "turning the knob" to the left makes the right channel quieter.

Yes , but please what is the response curve: left volume = f (from balance center to balance left) ?.
(i.e the function f is it linear, logarithmic, or other ?)

@derselbst
Copy link
Member

derselbst commented Feb 5, 2018 via email

@mawe42
Copy link
Member Author

mawe42 commented Feb 5, 2018

(i.e the function fis it linear, logarithmic, or other ?)

As Tom said, it's using fluid_cb2amp at the moment. Which response curve to use has been subject to quite a bit of discussion already. But I would be very interested in your opinion on the subject!

@jjceresa
Copy link
Collaborator

jjceresa commented Feb 5, 2018

it's using fluid_cb2amp at the moment, ..But I would be very interested in your opinion on the subject.

amp = fb(fluid_cb2amp(balance)) seems a good choice as it is also the response of amp = fv(volume) and both are what we have on an home hifi.

@derselbst
Copy link
Member

Let's merge this as well.

@derselbst derselbst merged commit 825216f into master Feb 11, 2018
@derselbst derselbst deleted the balance branch February 11, 2018 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants