Skip to content

SoundFont 2: apply preset-level generators to defaults, fix the resonance decade and keep negative envelope amounts - #255

Merged
git-moss merged 2 commits into
git-moss:mainfrom
douglas-carmichael:fix-sf2-preset-generators
Jul 29, 2026
Merged

git-moss merged 2 commits into
git-moss:mainfrom
douglas-carmichael:fix-sf2-preset-generators

Conversation

@douglas-carmichael

@douglas-carmichael douglas-carmichael commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Three related reading/writing defects in the SoundFont 2 support.

Preset-level generators were dropped when the instrument level does not carry the same generator. GeneratorHierarchy.getSignedValue/getUnsignedValue returned the generator's default as soon as neither instrument-level map held a value, without ever consulting the preset zones - but the specification (SF2 2.04, section 9.4) defines preset generators as additive offsets which apply to the instrument value or to the default when the instrument omits the generator; FluidSynth implements exactly that. A preset whose global zone carries coarseTune=-12 over a plain instrument was read an octave too high, preset-level initialAttenuation/fineTune/envelope offsets vanished, and a preset-level initialFilterFc offset never created a filter (the untouched default 13500 is the "no filter" gate). Such generators are routine in commercial and General MIDI SoundFonts. On the unsigned lookup path the offset was additionally added without the signed conversion, so a negative offset was added as a huge positive number.

The filter resonance was read a decade too small. initialFilterQ is centibels; the writer stores dB x 10 (with a comment saying so), but the reader divided by 100 - every resonance came out ten times too small (20 dB read as 2 dB) and each SoundFont-to-SoundFont round trip divided by ten again.

Negative envelope modulation amounts were dropped on write. modEnvToPitch and modEnvToFilterFc are signed generators and the reader takes them signed, but the writer gated both on > 0 - a downward filter sweep or the pitch drop of an 808-style kick lost its whole modulation envelope.

Verified with crafted SoundFonts (a preset-level-offset zone over a bare instrument, and an instrument-level resonance case), before vs. after:

Case Before After
Preset zone: coarseTune -12, fineTune -25, attenuation 100 cB, filterFc -1200 over bare instrument no tuning, no volume, no filter tune -1225 ct, volume -10 dB, low-pass at 9956 Hz
Instrument initialFilterQ = 200 (20 dB) resonance 2 dB resonance 20 dB
SFZ with resonance=3 to SF2 and back 0.30 3.00
SFZ with fileg_depth=-1200 to SF2 generator absent modEnvToFilterFc = -1200 plus the envelope times
File with no preset-level generators - unchanged

The read side of the negative amounts and all other generator laws were already correct and are untouched. (The SFZ creator has the same > 0 gate on its own output side; that is a separate format and will come separately.)


Changelog entry (all entries of this PR batch are collected in #283, so the fix PRs themselves do not touch the changelog and merge conflict-free in any order):

* SoundFont 2
  * Fixed: Generators which only appear on the preset level were dropped entirely when the instrument level did not carry the same generator, although the specification defines them as offsets to the default value in that case. A preset whose global zone transposes a plain instrument down an octave was read an octave too high, preset-level attenuation and envelope offsets vanished, and a preset-level filter offset never created a filter. Such generators are routine in commercial and General MIDI SoundFonts.
  * Fixed: A preset-level generator offset was added without the signed conversion when the instrument level carried the generator and the value was looked up unsigned - a negative offset (e.g. lowering the filter cutoff) was added as a huge positive number.
  * Fixed: The filter resonance was read as centibels divided by 100 instead of 10, so every resonance came out ten times too small (20 dB read as 2 dB) and each SoundFont-to-SoundFont round trip divided it by ten again. Writing was already correct.
  * Fixed: Negative pitch and filter envelope modulation amounts (downward sweeps, e.g. the pitch drop of an 808-style kick) were dropped when writing; both generators are signed in the format and reading them was already correct.

…ance decade and keep negative envelope amounts

Generators which only appear on the preset level were dropped entirely
when the instrument level did not carry the same generator, although
the specification defines them as offsets to the default value in that
case. A preset whose global zone transposes a plain instrument down an
octave was read an octave too high, preset-level attenuation and
envelope offsets vanished, and a preset-level filter offset never
created a filter. The offset is now applied to the instrument value or
to the generator's default, and it is sign-converted on the unsigned
lookup path as well.

The filter resonance was read as centibels divided by 100 instead of
10, so every resonance came out ten times too small and each round
trip divided it by ten again; writing was already correct.

Negative pitch and filter envelope modulation amounts (downward
sweeps) were dropped when writing although both generators are signed
in the format and reading them was already correct.
The open fix PRs all inserted their changelog entries near the same
lines, so every merged PR turned the remaining ones into conflicts.
The entries of the whole batch are collected in PR git-moss#283 and quoted in
each PR's description; the fix branches no longer touch the changelog,
so they merge cleanly in any order.
@git-moss
git-moss merged commit a05b07c into git-moss:main Jul 29, 2026
@douglas-carmichael
douglas-carmichael deleted the fix-sf2-preset-generators branch August 4, 2026 09:48
git-moss pushed a commit that referenced this pull request Sep 25, 2026
…ance decade and keep negative envelope amounts (#255)

* SoundFont 2: apply preset-level generators to defaults, fix the resonance decade and keep negative envelope amounts

Generators which only appear on the preset level were dropped entirely
when the instrument level did not carry the same generator, although
the specification defines them as offsets to the default value in that
case. A preset whose global zone transposes a plain instrument down an
octave was read an octave too high, preset-level attenuation and
envelope offsets vanished, and a preset-level filter offset never
created a filter. The offset is now applied to the instrument value or
to the generator's default, and it is sign-converted on the unsigned
lookup path as well.

The filter resonance was read as centibels divided by 100 instead of
10, so every resonance came out ten times too small and each round
trip divided it by ten again; writing was already correct.

Negative pitch and filter envelope modulation amounts (downward
sweeps) were dropped when writing although both generators are signed
in the format and reading them was already correct.

* Move the changelog entry to the collected changelog PR #283

The open fix PRs all inserted their changelog entries near the same
lines, so every merged PR turned the remaining ones into conflicts.
The entries of the whole batch are collected in PR #283 and quoted in
each PR's description; the fix branches no longer touch the changelog,
so they merge cleanly in any order.
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

Successfully merging this pull request may close these issues.

2 participants