MIDIPlug_C Examples: Practical Patch Ideas and Code Snippets
This article presents practical patch ideas for MIDIPlug_C along with concise, ready-to-use C code snippets. Each example is self-contained and focuses on a common MIDI-processing task: transforming incoming MIDI messages and emitting useful output. Assume a typical MIDIPlug_C callback style where you receive a MIDI event structure and can send one or more output events. Adapt the code to your specific MIDIPlugC API (types/names may vary).
1) Simple Transpose Patch
Purpose: Transpose all incoming note-on/note-off messages by a fixed interval (e.g., +7 semitones).
Code (core logic):
c
int transpose_semitones = 7; void process_midi_event(midi_event_t ev) { if ((ev->type == MIDI_NOTE_ON || ev->type == MIDI_NOTE_OFF) && ev->channel < 16) { int new_note = ev->note + transpose_semitones; if (new_note < 0) new_note = 0; if (new_note > 127) new_note = 127; ev->note = (uint8_t)new_note; } send_midi_event(ev); }
Usage tip: Expose transposesemitones as a user parameter; allow negative values.
2) Arpeggiator (Simple Step-Based)
Purpose: Turn held chords into a repeating arpeggio across 4 steps.
Behavior:
- Maintain a list of currently held notes.
- On a clock/tick event (e.g., host transport or internal timer), send next note-on and corresponding note-off for the previous step.
Core data and logic:
c
#define MAX_HELD 16 int held_notes[MAX_HELD]; int held_count = 0; int arp_step = 0; int arp_length = 4; int arp_velocity = 100; void note_on(uint8_t note) { if (held_count < MAX_HELD) held_notes[held_count++] = note; } void note_off(uint8_t note) { for (int i = 0; i < held_count; ++i) if (held_notes[i] == note) { held_notes[i] = held_notes[–held_count]; break; } } void on_tick() { if (held_count == 0) return; // send note-off for previous step int prev_index = (arp_step + arp_length - 1) % arp_length; int prev_note = held_notes[prev_index % held_count]; send_note_off(prev_note); // send note-on for current step int note = held_notes[arp_step % held_count]; send_note_on(note, arp_velocity); arp_step = (arp_step + 1) % arp_length; }
Integration: Hook note_on/note_off to incoming messages; call ontick from a timer or host BPM-synced clock.
3) Velocity Compression
Purpose: Reduce dynamic range by compressing velocities—soft notes get a boost, loud notes are reduced.
Logic: Map incoming velocity through a simple curve (e.g., soft boost + clamp).
Code:
c
uint8_t compress_velocity(uint8_t v) { float x = v / 127.0f; // example curve: raise low values, compress highs float y = powf(x, 0.8f); // exponent <1 boosts low values int out = (int)(y 127.0f); if (out < 1) out = 1; if (out > 127) out = 127; return (uint8_t)out; } void process_midi_event(midi_event_t ev) { if (ev->type == MIDI_NOTE_ON) { ev->velocity = compress_velocity(ev->velocity); } send_midievent(ev); }
Parameter ideas: Make the exponent and output floor adjustable.
4) Channel Splitter / Multi-Timbral Mapper
Purpose: Route incoming notes on a single channel to multiple output channels by note-range or program.
Behavior: Use ranges to map notes to channels/instruments.
Code sketch:
c
typedef struct { int lo, hi; uint8_t out_channel; } range_map_t; range_map_t maps[] = { {0, 47, 0}, // bass -> channel 0 {48, 71, 1}, // piano -> channel 1 {72, 127, 2} // lead -> channel 2 }; int maps_count = 3; void process_midi_event(midi_event_t ev) { if (ev->type == MIDI_NOTE_ON || ev->type == MIDI_NOTE_OFF) { for (int i = 0; i < maps_count; ++i) { if (ev->note >= maps[i].lo && ev->note <= maps[i].hi) { ev->channel = maps[i].out_channel; break; } } } send_midievent(ev); }
Extension: Send program-change messages per mapped channel when mapping changes.
5) Humanize Timing and Velocity
Purpose: Add slight random offsets to note timing and velocity to simulate human performance.
Notes:
- For timing jitter, buffer the event and schedule it a few milliseconds forward/back.
- For velocity jitter, add a small random delta.
Code (velocity + scheduled delay stub):
c
#include#include int max_time_jitter_ms = 12; int max_velocity_jitter = 8; void process_midi_event(midi_event_t ev) { if (ev->type == MIDI_NOTE_ON) { int dv = (rand() % (2max_velocity_jitter+1)) - max_velocity_jitter; int newv = ev->velocity + dv; if (newv < 1) newv = 1; if (newv > 127) newv = 127; ev->velocity = (uint8_t)newv; int dt = (rand() % (2*max_time_jitter_ms+1)) - max_time_jitter_ms; schedule_event_with_delay(ev, dt); // implement scheduling per host API return; } send_midi_event(ev); }
Implementation note: Seed random with srand(time(NULL)) during initialization; use high-resolution timer for scheduling.
Tips for Integrating These Examples
- Expose key parameters (transpose amount, arp length, jitter range) as GUI or host-automation controls.
- Be cautious with latency when scheduling events; prefer host-provided timing callbacks if available.
- Debounce rapid control changes (e.g., program changes) to avoid flooding outputs.
- Test with both running and stopped transport to handle clock/tick behavior consistently.
Final code packaging example
Wrap processing in an initialization and teardown structure, register callbacks per the MIDIPlug_C SDK, and keep state in a plugin instance struct. Use the snippets above as the core processing functions and adapt types/names to your SDK.
If you want, I can convert any of these snippets into a complete MIDIPlug_C plugin skeleton (init, parameter handling, and callback wiring).
Leave a Reply