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

filter cutoff and Q NRPNs don't behave correctly #1473

Open
mrbumpy409 opened this issue Jan 21, 2025 · 3 comments
Open

filter cutoff and Q NRPNs don't behave correctly #1473

mrbumpy409 opened this issue Jan 21, 2025 · 3 comments
Labels

Comments

@mrbumpy409
Copy link
Contributor

mrbumpy409 commented Jan 21, 2025

FluidSynth version

2.4.2

Details

Control of filter cutoff and resonance (Q) via NRPNs was introduced with the AWE32 and continued to be supported in all later Sound Blaster cards containing a hardware SoundFont synth, as far as I am aware. Recent versions of FluidSynth now support these NRPNs, but its behavior is not correct when compared to my Sound Blaster Audigy2 ZS, and likely not accurate to the original AWE32 either.

As for defining the "correct" behavior of these NRPNs, one must decide which of Creative's sound cards to model. I believe FluidSynth should model the behavior of the later, SoundFont 2.01–2.04 devices such as the Audigy for the following reasons:

  1. FluidSynth's SoundFont implementation is already based on the 2.01 and 2.04 spec, and the Audigy's NRPN implementation is aligned with the behavioral characteristics of this spec.
  2. I have a Sound Blaster Audigy2 ZS that can be used for testing and comparing the behavior with FluidSynth, but no access to an AWE32/64 or Live!.
  3. The Audigy's NRPN behavior can be recreated easily using modulators, so no need for lookup tables, etc. of the AWE32.

Test Results

I have created a filter NRPN test. It contains a recordings folder in which you can compare recordings of the test run on FluidSynth 2.4.2 and Audigy2 ZS. Through this, we can see several differences between the two synthesizers.

I will list each test from the accompanying README, along with my notes on the results of both FluidSynth and Audigy2 ZS. The tests are arranged into pairs, with the first of each pair testing NRPN behavior and the second emulating that behavior using a modulator.

Tests #⁠1 & #⁠2

Test #⁠1: If the AWE32 FC NRPN is implemented, you should hear a filter sweep from 100 to 8000 Hz and then back down to 100 Hz.
Test #⁠2: You will hear a filter sweep that emulates the AWE32 FC NRPN filter curve via CC1 modulator.

Here is a spectrogram view of tests #⁠1 (left) and #⁠2 (right) between Audigy2 (top) and FluidSynth (bottom):
Image

  • Audigy2 result: Both the NRPN sweep (left) and the emulated NRPN sweep via modulator (right) are identical.
  • FluidSynth result: The emulated NRPN sweep (right) is correct, but the actual NRPN sweep (left) is incorrect for two reasons:
    1. The filter Q specified in the test preset is being overridden to 0, though no filter Q NRPNs have been sent (see Use of AWE32 filter cutoff NRPN overrides both cutoff and emphasis #1452).
    2. It's hard to tell with the filter Q being overridden, but the NRPN to filter cutoff curve is the wrong shape.

Tests #⁠3 & #⁠4

Test #⁠3: You should hear a sustained white noise wave while the filter switches every two seconds through the following values: 0 (100 Hz), 13 (157 Hz), 25 (237 Hz), 38 (371 Hz), 51 (581 Hz), 64 (910 Hz), 76 (1377 Hz), 89 (2156 Hz), 102 (3376 Hz), 114 (5108 Hz), 127 (8000 Hz). Each jump in the filter should sound roughly like the interval of a fifth.
Test #⁠4: The same as the above test, but with the AWE32 FC NRPN filter curve emulated via CC1 modulator.

Here is a spectrogram view of tests #⁠3 (left) and #⁠4 (right) between Audigy2 (top) and FluidSynth (bottom):
Image

  • Results: The same scenario as tests #⁠1 and #⁠2 above.

Tests #⁠5 & #⁠6

Test #⁠5: You should hear a series of filter sweeps, each with progressively higher filter Q: 0 (0 dB), 13 (2.03 dB), 25 (3.91 dB), 38 (5.94 dB), 51 (7.97 dB), 64 (10 dB), 76 (11.88 dB), 89 (13.91 dB), 102 (15.94 dB), 114 (17.81 dB), 127 (19.84 dB)
Test #⁠6: The same as the above test, but with the AWE32 filter Q NRPN emulated via CC2 modulator.

Here is a spectrogram view of test #⁠5 between Audigy2 (top) and FluidSynth (bottom):
Image

  • Audigy2 result: You can see the filter Q amount increasing with each sweep.
  • FluidSynth result: FluidSynth does not appear to be responding to the filter Q NRPN messages. Each sweep instead appears to be using the 24 dB filter Q set in the preset, but the filter Q NRPN should be overriding that value.

Here is a spectrogram view of test #⁠6 between Audigy2 (top) and FluidSynth (bottom):
Image

  • Audigy2 result: The emulated filter Q NRPN via CC1 modulator perfectly matches the actual NRPN behavior from test #⁠5 above.
  • FluidSynth result: The emulated filter Q NRPN looks correct on FluidSynth. The keen-eyed might notice a bit of extra filter Q in the Audigy 2 examples at low frequencies, even when it should be at 0. This is one of the quirks of the Sound Blaster filter, but I don't believe that FluidSynth should be trying to emulate Creative's filter quirks (which also include being fully open at 8,000 Hz, limited to 100 Hz at low end, etc.).

Solution

As you can see from the tests results above, the Audigy's filter NRPN implementation can be easily emulated using modulators. This should provide a convenient mechanism for correcting the NRPN behavior in FluidSynth.

To emulate the Audigy's filter cutoff NRPN for a voice:

  1. Override the voice's filter cutoff, setting it to: 100 Hz
  2. Set the filter cutoff NRPN to modulate filter cutoff based on the following modulator:
    • Curve: unipolar linear positive
    • Amount: 7646 (timecents)
    • Destination: filter cutoff

To emulate the Audigy's filter Q NRPN for a voice:

  1. Override the voice's filter Q, setting it to: 0 dB
  2. Set the filter Q NRPN to modulate filter Q based on the following modulator:
    • Curve: unipolar linear positive
    • Amount: 200 (20 dB)
    • Destination: filter Q

In addition to getting the curves right, the filter cutoff and filter Q NRPNs should act independently. Activating the filter cutoff NRPN should have no affect on filter Q, and vice versa, but currently FluidSynth (A) overrides filter Q whenever the filter cutoff NRPN is used, and (B) doesn't respond to the filter Q NRPN independently of the filter cutoff NRPN—at least in these tests.

@derselbst
Copy link
Member

I started looking into this. Thanks for the excellent write-up and test suite! Before I go into details:

Amount: 7646 (timecents)

Should be cents :)


I started some changes on the 1473 branch. I referenced the test in your repo in fluidsynth's testsuite.

Tests ⁠5 & ⁠6 were pretty simple to solve.

Tests 1 & ⁠2 have been solved as well - however, the shape is still different. That's because the AWE32 documentation reports the

NRPN LSB 21 (Initial Filter Cutoff)
Realtime : Yes
Range : [0, 127]
Unit : 62Hz
Filter cutoff from 100Hz to 8000Hz

I.e. the unit is in Hertz, not in cents. Hence according to the documentation, this should behave linear on the Hertz. I admit this doesn't really make sense, because you get pretty poor resolution in for low frequencies, and almost no change for high frequencies. I'm open to change this to cents scaling to make it behave logarithmic on the Hertz scale.

I am struggling with Tests 3 & 4. That's because while NRPN allows to directly specify the filter cutoff, it doesn't allow to specify the filter resonance. I.e. it only allows you to specify a "resonance coefficient" and the filter Q should then be derived through some magic with this table:

NRPN LSB 22 (Initial Filter Resonance Coefficient)
Realtime : No
Range : [0, 127]
The EMU8000 has a built in resonance coefficient table
comprising 16 entries. Values 0-7 will select the first (0)
entry, values 8-15 selects the second (1) entry and so on.

Coeff Low Fc(Hz)Low Q(dB)High Fc(kHz)High Q(dB)DC Attenuation(dB)
0 92 5 Flat Flat -0.0
1 93 6 8.5 0.5 -0.5
2 94 8 8.3 1 -1.2
3 95 10 8.2 2 -1.8
4 96 11 8.1 3 -2.5
5 97 13 8.0 4 -3.3
6 98 14 7.9 5 -4.1
7 99 16 7.8 6 -5.5
8 100 17 7.7 7 -6.0
9 100 19 7.5 9 -6.6
10 100 20 7.4 10 -7.2
11 100 22 7.3 11 -7.9
12 100 23 7.2 13 -8.5
13 100 25 7.1 15 -9.3
14 100 26 7.1 16 -10.1
15 100 28 7.0 18 -11.0

This has two implications:

  • When choosing different coefficients via NRPN22, you would (presumably) get different ranges for the filter's cutoff frequency, e.g. a coeff==15 would give a highest filter cutoff at 7kHz, while coeff==1 would allow filter cutoff to go up to 8.5kHz.
  • The table specifies a low and high Q - what does that mean? I'm assuming, it does not mean that Q is allowed to move within those limits, because:
    • there is no direct way to specify a Q, and
    • low Q actually has more resonance than high Q according to this table.

Hence, I assume Q is derived from whatever filter cutoff frequency is currently set. This is also why I believe one cannot use modulators for this logic (one would have to dynamically adjust the amount of the filter_fc modulator, depending on which resonance coefficient has been set).

So, in order to get a clue, we would need an additional test cases.

  1. A continuous noise with NRPN21 set to 8000Hz, while NRPN22 simply goes from 0 to 127 (I want to see if there are truly only 16 different modes the filter operates at) - I would expect the cutoff to drop down to 7kHz when specifying a resonance coefficient of 15.
  2. Same as 7, but with the filter cutoff defined via SF2 logic (e.g. at instrument level or dynamically via your CC1 logic)
  3. Same as 7, but with NRPN21 set very low, e.g. 100 Hz.
  4. Same as 9, but with filter cutoff defined via SF2 logic.

I'm not sure if tests 9&10 would be required. I was hoping they would answer whether the filter's resonance really is higher for lower cutoffs. I can't quite tell it from the filter sweeps in tests 5&6.

Let me know if I'm missing something.

@mrbumpy409
Copy link
Contributor Author

I started looking into this. Thanks for the excellent write-up and test suite! Before I go into details:

Amount: 7646 (timecents)

Should be cents :)

Whoops! That will be fixed in the next update to the test documentation.

Tests 1 & ⁠2 have been solved as well - however, the shape is still different. That's because the AWE32 documentation reports the

NRPN LSB 21 (Initial Filter Cutoff)
Realtime : Yes
Range : [0, 127]
Unit : 62Hz
Filter cutoff from 100Hz to 8000Hz

I.e. the unit is in Hertz, not in cents. Hence according to the documentation, this should behave linear on the Hertz. I admit this doesn't really make sense, because you get pretty poor resolution in for low frequencies, and almost no change for high frequencies. I'm open to change this to cents scaling to make it behave logarithmic on the Hertz scale.

Without being able to test an AWE32, there will be no way to compare its behavior with the Audigy. I agree that cents scaling makes the most sense, and I would guess that Creative Labs agreed as well when they programmed the SoundFont 2.01-compliant cards to behave logarithmically. Seeing as (A) FluidSynth is aiming to be an ideal implementation of the SoundFont 2.01/2.04 spec and (B) we do not have access to an AWE32 to test (AFAIK), my vote would be to replicate what the SoundFont 2.01/2.04 compliant hardware is doing.

I am struggling with Tests 3 & 4. That's because while NRPN allows to directly specify the filter cutoff, it doesn't allow to specify the filter resonance. I.e. it only allows you to specify a "resonance coefficient" and the filter Q should then be derived through some magic with this table:

This has two implications:

  • When choosing different coefficients via NRPN22, you would (presumably) get different ranges for the filter's cutoff frequency, e.g. a coeff==15 would give a highest filter cutoff at 7kHz, while coeff==1 would allow filter cutoff to go up to 8.5kHz.

  • The table specifies a low and high Q - what does that mean? I'm assuming, it does not mean that Q is allowed to move within those limits, because:

    • there is no direct way to specify a Q, and
    • low Q actually has more resonance than high Q according to this table.

Hence, I assume Q is derived from whatever filter cutoff frequency is currently set. This is also why I believe one cannot use modulators for this logic (one would have to dynamically adjust the amount of the filter_fc modulator, depending on which resonance coefficient has been set).

My understanding is this: the coefficient table is tied to the unique design and limitations of the EMU8000 filter. Some—if not all—of these quirks are also present in later Sound Blaster cards. For example, all Sound Blaster SoundFont synths have the filter being fully open at around 8,000 Hz, as that was the original max filter cutoff on the AWE32. The first published SoundFont specification (2.01) says nothing about these quirks, instead in section 9.1.3 stating, "Because there is tremendous variation within the industry as to filter implementations, this filter is idealized rather than being specified as a particular realization."

Measurements from my Audigy2 show that the filter is always more resonant at low frequencies than high, even when there is no resonance specified at all. This seems to line up with the behavior suggested by the coefficient table. You can see this added resonance at low frequencies quite clearly in the Audigy2's filter measurements analyzed in my sampler filter comparison video from a couple years ago. (Please note that since this video was published, BASSMIDI now defaults to the same filter model as FluidSynth, only activating the "quirky" filter when "Enable Sound Blaster Limits" is enabled.)

You can also see this in the spectrogram view of test 6 in my original post, where the frequency presence at the cutoff point of the sweep always appears brighter for the Audigy2 than for FluidSynth between 100 and around 2,000 Hz. Assuming this is the same frequency-dependent filter emphasis differential laid out in the coefficient table, it would appear that the quirks revealed in the coefficient table are not just used by the filter Q NRPN, but apply to any use of filter Q. Thus, filter Q values set within an instrument or preset may also be limited in resolution and behavior to this table, though the 16-step resolution would be mapped to around 0–20 dB or so for instrument/preset Q values. (It's hard to tell at which dB value the Sound Blaster's filter Q parameter becomes capped, but it seems to be around 20-24 dB before there is no further change.)

Interestingly, examples of limited resolution can be seen in some of the other SoundFont parameters when actually measured from a Sound Blaster card. The Sound Blaster's envelope hold phase has a particularly poor resolution, which I've been able to roughly measure as follows (in seconds):

Hold value SB measured
0.001–0.059 0.001
0.06–0.171 0.085
0.172–0.244 0.172?

...etc.

It's possible that all envelope phases have similar limits, though if so, attack, decay and release might have their resolution weighted (e.g., high resolution at short times, low resolution at longer times, where inaccuracy is not noticeable). It's hard to say for sure without gruelling testing. Either way, I don't think FluidSynth should be trying to emulate these hardware resolution limitations.

So, in order to get a clue, we would need an additional test cases.

  1. A continuous noise with NRPN21 set to 8000Hz, while NRPN22 simply goes from 0 to 127 (I want to see if there are truly only 16 different modes the filter operates at) - I would expect the cutoff to drop down to 7kHz when specifying a resonance coefficient of 15.

  2. Same as 7, but with the filter cutoff defined via SF2 logic (e.g. at instrument level or dynamically via your CC1 logic)

  3. Same as 7, but with NRPN21 set very low, e.g. 100 Hz.

  4. Same as 9, but with filter cutoff defined via SF2 logic.

I'm not sure if tests 9&10 would be required. I was hoping they would answer whether the filter's resonance really is higher for lower cutoffs. I can't quite tell it from the filter sweeps in tests 5&6.

Test 9 & 10 wouldn't be required, as it is already possible to see that the differential resonance levels already exist with both NRPN and non-NRPN use on the Audigy.

So, at this point, we have the following factors affecting the filter Q NRPN:

  1. Parameter resolution (16-row table lookup). –Testing needed to determine if the Audigy inherits this resolution limit, as you proposed.
  2. Different filter Q level depending on cutoff frequency. –This appears to be always present in the Audigy filter, not just NRPN.
  3. Drifting low and high cutoff caps depending on filter Q level. –Testing would be needed to determine if the Audigy matches this behavior.

How important is any of this if FluidSynth is aiming to implement the ideal second-order lowpass filter as described in the spec? To the above factors:

  1. Do we really need to limit the filter Q behavior to 16 steps? I don't think anybody would really be able to tell the difference between entry 7 and halfway between 7 and 8.
  2. This appears to be inherent to the Sound Blaster's unique filter properties, which FluidSynth does not emulate.
  3. FluidSynth's filter is not limited to the Sound Blaster's filter range (c.100–8,000 Hz), so it makes little sense to emulate this range limit drift.

I can say with great confidence that if you were to implement the NRPN behavior based on the modulators I proposed, the result would match the Audigy quite well, with the only audible differences being those that already impact the sound due to the differences in filter implementation between Sound Blaster and FluidSynth. If FluidSynth wishes to be able to emulate the EMU8000 filter, that would require the development a whole new filter model. If that were to happen, I would strongly suggest it not replace the current filter model but exist as a separate option (similar to how BASSMIDI currently does it).

@derselbst
Copy link
Member

Alright Christian, you're very convincing as usual :)

I'll ditch all the mentioned filter-implementation-quirks from the NRPN implementation and just go ahead with the modulator approach you've suggested. Doing so might also give me an opportunity to come up with a solution that would somehow fit to the request in #1420.

I guess finding a good place to override the initial filter Q and fc with the values you provided and injecting the special NPRN modulators instead will become the most tricky part.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants