Created
September 25, 2026 04:57
-
-
Save ravengraves/309c142fe75c57bbaa867f5a9fcc75cf to your computer and use it in GitHub Desktop.
reaper n-tet midi remapper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| desc:Variable-TET Note Mapper (MPE) | |
| // Input MIDI note = TET step index. | |
| // Input note 0 maps to the selected base MIDI note. | |
| // Uses MPE member channels 2-16. | |
| // Supports only up to 16 notes at a time. | |
| // match pitch bend range with synth; usually 2.0 | |
| slider1:2<1,12,1>Pitch Bend Range (semitones). | |
| slider2:16<1,72,1>TET Divisions per Octave | |
| slider3:21<0,127,1>Base MIDI Note (21 = A0, 33=A1) | |
| @init | |
| // Memory layout | |
| ch_note = 0; // 16 entries | |
| note_ch = 16; // 128 entries | |
| active_out = 144; // 128 entries | |
| vel = 272; // 128 entries | |
| ring_act = 400; // 72 entries | |
| ring_note = 472; // 72 entries | |
| i = 0; | |
| loop(16, | |
| ch_note[i] = -1; | |
| i += 1; | |
| ); | |
| i = 0; | |
| loop(128, | |
| note_ch[i] = -1; | |
| active_out[i] = -1; | |
| vel[i] = 0; | |
| ring_act[i] = 0; | |
| ring_note[i] = 0; | |
| i += 1; | |
| ); | |
| @slider | |
| // Precompute step sizes | |
| tet_div = max(1, floor(slider2 + 0.5)); | |
| step = 12 / tet_div; | |
| pb_scale = 8192 / max(1, floor(slider1 + 0.5)); | |
| base = floor(slider3 + 0.5); | |
| @block | |
| while (midirecv(offset, msg1, msg2, msg3)) ( | |
| status = msg1 & 0xF0; | |
| // NOTE ON | |
| status == 0x90 && msg3 > 0 ? ( | |
| n = msg2; | |
| vel[n] = msg3; | |
| // Compute desired 12-TET MIDI-coordinate pitch. | |
| target = base + n * step; | |
| // Nearest MIDI note. | |
| out = floor(target + 0.5); | |
| out = max(0, min(127, out)); | |
| // Remaining fractional semitone becomes pitch bend. | |
| pb = floor( | |
| 8192 + (target - out) * pb_scale + 0.5 | |
| ); | |
| pb = max(0, min(16383, pb)); | |
| // Retrigger existing instance. | |
| old_ch = note_ch[n]; | |
| old_ch > 0 ? ( | |
| midisend(offset, 0x80 | old_ch, active_out[n], 0); | |
| ch_note[old_ch] = -1; | |
| note_ch[n] = -1; | |
| active_out[n] = -1; | |
| ); | |
| // Find free MPE channel. | |
| out_ch = -1; | |
| c = 1; | |
| count = 0; | |
| while (count < 15 && out_ch < 0) ( | |
| ch_note[c] < 0 ? out_ch = c; | |
| c += 1; | |
| c > 15 ? c = 1; | |
| count += 1; | |
| ); | |
| // Drop if all are occupied. | |
| out_ch > 0 ? ( | |
| ch_note[out_ch] = n; | |
| note_ch[n] = out_ch; | |
| active_out[n] = out; | |
| midisend( | |
| offset, | |
| 0xE0 | out_ch, | |
| pb & 127, | |
| (pb >> 7) & 127 | |
| ); | |
| midisend( | |
| offset, | |
| 0x90 | out_ch, | |
| out, | |
| msg3 | |
| ); | |
| ) : ( | |
| vel[n] = 0; | |
| ); | |
| // NOTE OFF | |
| ) : ( | |
| status == 0x80 || | |
| (status == 0x90 && msg3 == 0) | |
| ) ? ( | |
| n = msg2; | |
| vel[n] = 0; | |
| out_ch = note_ch[n]; | |
| out_ch > 0 ? ( | |
| midisend( | |
| offset, | |
| 0x80 | out_ch, | |
| active_out[n], | |
| msg3 | |
| ); | |
| ch_note[out_ch] = -1; | |
| note_ch[n] = -1; | |
| active_out[n] = -1; | |
| ); | |
| // OTHER MIDI — only the master channel (1) and system messages are | |
| // still valid after we've remapped notes onto member channels 2-16. | |
| // Forwarding member-channel PB/CC would retune whichever note we | |
| // placed on that output channel (e.g. the bass). | |
| ) : ( | |
| (status >= 0xF0 || (msg1 & 0x0F) == 0) ? midisend(offset, msg1, msg2, msg3); | |
| ); | |
| ); | |
| // RING VISUALIZATION | |
| @gfx | |
| gfx_set(0,0,0,1); gfx_rect(0,0,gfx_w,gfx_h); | |
| t = tet_div; | |
| cx = gfx_w * 0.5; cy = gfx_h * 0.5; r = min(gfx_w, gfx_h) * 0.35; | |
| gfx_set(0.25,0.25,0.25,1); gfx_circle(cx,cy,r,0); | |
| i = 0; loop(t, | |
| a = 2*$pi*i/t - $pi/2; | |
| gfx_set(0.35,0.35,0.35,1); | |
| gfx_line(cx+cos(a)*r*0.92,cy+sin(a)*r*0.92, cx+cos(a)*r,cy+sin(a)*r); | |
| i += 1; | |
| ); | |
| // Scan all 128 notes, aggregate by ring position | |
| i = 0; loop(t, ring_act[i]=0; ring_note[i]=0; i += 1;); | |
| i = 0; loop(128, | |
| vel[i] > 0 ? ( | |
| p = i % t; | |
| ring_act[p] = 1; | |
| i > ring_note[p] ? ring_note[p] = i; | |
| ); | |
| i += 1; | |
| ); | |
| // Lines between successive active ring positions (modulo wrap) | |
| first = -1; prev = -1; last = -1; | |
| i = 0; loop(t, | |
| ring_act[i] ? ( | |
| first < 0 ? first = i; | |
| prev >= 0 ? ( | |
| a1 = 2*$pi*prev/t - $pi/2; a2 = 2*$pi*i/t - $pi/2; | |
| gfx_set(0.5,0.6,0.9,0.7); | |
| gfx_line(cx+cos(a1)*r,cy+sin(a1)*r, cx+cos(a2)*r,cy+sin(a2)*r, 1); | |
| ); | |
| prev = i; last = i; | |
| ); | |
| i += 1; | |
| ); | |
| first>=0 && last>=0 && first!=last ? ( | |
| a1 = 2*$pi*last/t - $pi/2; a2 = 2*$pi*first/t - $pi/2; | |
| gfx_set(0.5,0.6,0.9,0.7); | |
| gfx_line(cx+cos(a1)*r,cy+sin(a1)*r, cx+cos(a2)*r,cy+sin(a2)*r, 1); | |
| ); | |
| // Dots (colored by octave, blue→red) and MIDI note numbers | |
| i = 0; loop(t, | |
| ring_act[i] ? ( | |
| a = 2*$pi*i/t - $pi/2; | |
| x = cx+cos(a)*r; y = cy+sin(a)*r; | |
| oct = floor(ring_note[i] / t); | |
| frac = min(1, oct / 10); | |
| gfx_set(frac, 0.15, 1-frac, 1); gfx_circle(x,y,r*0.05,1); | |
| gfx_set(1,1,1,0.5); gfx_circle(x,y,r*0.05,0); | |
| gfx_x = x + r*0.06; gfx_y = y + r*0.03; | |
| gfx_set(1,1,1,0.8); | |
| gfx_drawnumber(ring_note[i], 0); | |
| ); | |
| i += 1; | |
| ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment