Skip to content

Add CC1 modulation wheel (vibrato) support - #289

Merged
psi29a merged 3 commits into
masterfrom
modulation
Jul 26, 2026
Merged

Add CC1 modulation wheel (vibrato) support#289
psi29a merged 3 commits into
masterfrom
modulation

Conversation

@psi29a

@psi29a psi29a commented Jul 26, 2026

Copy link
Copy Markdown
Member

CC1 was parsed then discarded, and the GUS engine had no LFO at all. Adds a 5Hz triangle vibrato LFO (TiMidity++ style) for GUS patches, wires CC1 to vibLfoToPitch for SF2 (50 cents at full wheel, per SF2.01 8.4.4 as FluidSynth does), and adds an LFO to the MA-FM synth. RPN 5 rescales depth; CC121 and the resets clear it. Also fixes -o MIDI export, which would otherwise drop the new event type.

Files without CC1 render bit-for-bit identical to before.

Closes #143

Test files
modulation_testing.zip

CC1 was parsed then discarded, and the GUS engine had no LFO at all.
Adds a 5Hz triangle vibrato LFO (TiMidity++ style) for GUS patches, wires
CC1 to vibLfoToPitch for SF2 (50 cents at full wheel, per SF2.01 8.4.4 as
FluidSynth does), and adds an LFO to the MA-FM synth.  RPN 5 rescales
depth; CC121 and the resets clear it.  Also fixes -o MIDI export, which
would otherwise drop the new event type.

Files without CC1 render bit-for-bit identical to before.

Fixes #143
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@psi29a, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ab92b2ee-2dcc-472c-bce7-24d22ade7ac6

📥 Commits

Reviewing files that changed from the base of the PR and between 1fc6db4 and eeccfa1.

📒 Files selected for processing (3)
  • src/internal_midi.c
  • src/mafm.c
  • src/tsf/tsf.h
📝 Walkthrough

Walkthrough

Adds CC1 modulation-wheel support and RPN 5 vibrato-depth handling. Vibrato state and block-based LFO updates are integrated into MIDI playback, GUS mixing, SF2 voices, and the MA FM synthesizer.

Changes

Modulation wheel vibrato

Layer / File(s) Summary
MIDI vibrato state and control flow
include/internal_midi.h, src/internal_midi.c, src/f_midi.c
Adds channel and note vibrato state, detuned pitch increments, CC1/RPN 5 handling, reset and pitch-bend integration, and MIDI event serialization.
GUS output vibrato updates
src/wildmidi_lib.c
Updates linear and Gauss mixing loops to advance vibrato at fixed output-block intervals.
SF2 modulation integration
src/sf2.c, src/tsf/tsf.h
Maps modulation events to CC1 and combines modulation-derived depth with SF2 voice vibrato.
MA FM modulation rendering
src/mafm.c, README.md
Adds block-based triangle-LFO modulation for active FM voices and documents CC1 vibrato support.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MIDI as MIDI input
  participant WildMIDI as WildMIDI channel state
  participant Synth as GUS, SF2, or MA FM voice
  participant Audio as Audio renderer
  MIDI->>WildMIDI: Send CC1 modulation or RPN 5 depth
  WildMIDI->>Synth: Apply vibrato depth to active voices
  Audio->>Synth: Advance LFO during rendering
  Synth-->>Audio: Render pitch-modulated audio
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding CC1 modulation wheel vibrato support.
Description check ✅ Passed The description is directly related to the patch and describes the CC1 vibrato changes accurately.
Linked Issues check ✅ Passed The changes implement the requested modulation-controller vibrato behavior described in #143.
Out of Scope Changes check ✅ Passed The modified files all support CC1 vibrato handling or related export/docs changes, with no clear unrelated scope.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@psi29a

psi29a commented Jul 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai summary

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
✅ Action performed

Summary regeneration triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/internal_midi.c (1)

1283-1317: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

GM reset doesn't clear vibrato on already-sounding notes.

_WM_do_sysex_gm_reset resets channel[i].modulation/mod_depth_range to defaults but never calls set_channel_vibrato(mdi, i), unlike _WM_do_control_channel_controllers_off (Line 1161) which does. Any note already vibrato-ing when an in-song GM reset sysex arrives keeps its stale vib_depth/vib_phase/vib_inc (those live on struct _note, untouched by this loop), continuing to vibrato after a reset that's supposed to clear it.

🐛 Proposed fix
         mdi->channel[i].modulation = 0;
         mdi->channel[i].mod_depth_range = VIB_DEPTH_DEFAULT;
+        set_channel_vibrato(mdi, i);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/internal_midi.c` around lines 1283 - 1317, Update _WM_do_sysex_gm_reset
to call set_channel_vibrato(mdi, i) for each channel after resetting modulation
and mod_depth_range, matching _WM_do_control_channel_controllers_off. Ensure
this updates already-sounding notes so their vibrato state is cleared while
preserving the existing GM reset initialization.
🧹 Nitpick comments (1)
src/wildmidi_lib.c (1)

1030-1043: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate vibrato-tick logic across the two mixers. The block counter + VIB_BLOCK gate + active-note walk calling _WM_update_note_vibrato is copy-pasted verbatim between the Linear and Gauss output paths; the shared root cause is the lack of a single helper for this logic.

  • src/wildmidi_lib.c#L1030-L1043: extract this block into a shared helper (e.g. _WM_tick_vibrato(struct _mdi *mdi) in internal_midi.c) and call it here.
  • src/wildmidi_lib.c#L1383-L1396: replace this identical block with a call to the same shared helper.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/wildmidi_lib.c` around lines 1030 - 1043, Extract the duplicated vibrato
tick logic into a shared _WM_tick_vibrato(struct _mdi *mdi) helper in
internal_midi.c, preserving the VIB_BLOCK counter gate, reset, active-note
traversal, and _WM_update_note_vibrato calls. Replace the inline blocks at
src/wildmidi_lib.c lines 1030-1043 and 1383-1396 with calls to this helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/mafm.c`:
- Around line 591-605: Update _WM_MAFM_Event and _WM_MAFM_Reset so
controller-off and applicable reset events clear s->chan_modulation for every
channel, matching the existing CC modulation-zero behavior. Ensure reset
processing also clears the channel modulation state rather than leaving vibrato
values active, or explicitly preserve the state only if that is the intended
contract.

In `@src/tsf/tsf.h`:
- Around line 2114-2118: Cap the RPN 5 calculation in the midiRPN == 5 branch
before assigning c->modDepthRange, limiting it to the existing VIB_DEPTH_MAX
ceiling (600 cents). Preserve the current modulation-depth conversion and
subsequent tsf_channel_apply_modulation call.
- Around line 1286-1288: Update vibrato LFO setup in tsf_voice_lfo_setup() to
use the SF2 default frequency of 240 when the region does not provide
freqVibLFO, ensuring v->viblfo.delta is nonzero before updateVibLFO checks CC1
modulation. Preserve explicitly configured region frequencies and the existing
modulation behavior.

---

Outside diff comments:
In `@src/internal_midi.c`:
- Around line 1283-1317: Update _WM_do_sysex_gm_reset to call
set_channel_vibrato(mdi, i) for each channel after resetting modulation and
mod_depth_range, matching _WM_do_control_channel_controllers_off. Ensure this
updates already-sounding notes so their vibrato state is cleared while
preserving the existing GM reset initialization.

---

Nitpick comments:
In `@src/wildmidi_lib.c`:
- Around line 1030-1043: Extract the duplicated vibrato tick logic into a shared
_WM_tick_vibrato(struct _mdi *mdi) helper in internal_midi.c, preserving the
VIB_BLOCK counter gate, reset, active-note traversal, and
_WM_update_note_vibrato calls. Replace the inline blocks at src/wildmidi_lib.c
lines 1030-1043 and 1383-1396 with calls to this helper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d8f9e9d-9c95-42e7-8ca6-74cec9cc37d7

📥 Commits

Reviewing files that changed from the base of the PR and between 50cd02f and 1fc6db4.

📒 Files selected for processing (8)
  • README.md
  • include/internal_midi.h
  • src/f_midi.c
  • src/internal_midi.c
  • src/mafm.c
  • src/sf2.c
  • src/tsf/tsf.h
  • src/wildmidi_lib.c

Comment thread src/mafm.c Outdated
Comment thread src/tsf/tsf.h
Comment thread src/tsf/tsf.h
@psi29a
psi29a requested a review from sezero July 26, 2026 15:59

@sezero sezero left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OK by me.

I'll add tsf.h changes to que I submitted to mainstream (with you as the author)

@psi29a

psi29a commented Jul 26, 2026

Copy link
Copy Markdown
Member Author

OK by me.

I'll add tsf.h changes to que I submitted to mainstream (with you as the author)

Thank you 🙏🏼

I was wondering about that.

@psi29a
psi29a merged commit e36ff65 into master Jul 26, 2026
23 checks passed
@sezero
sezero deleted the modulation branch July 26, 2026 18:38
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.

Modulation controller doesn't seem to be working.

2 participants