├── README.md ├── .gitignore └── poly.c /README.md: -------------------------------------------------------------------------------- 1 | # properpoly 2 | 3 | RK-002 PolyMUX firmware with better polyphony. 4 | 5 | Muxes to the lowest available channel (non round-robin), or evicts the oldest keypress. 6 | 7 | --- 8 | 9 | vs. `RKPOLYMUXALLOCMODE_ROUNDROBIN`: 10 | 11 | Steals notes in a less intrusive way (the oldest press, rather than round-robin). 12 | 13 | vs. `RKPOLYMUXALLOCMODE_FIRSTFREE`: 14 | 15 | Can steal notes. 16 | 17 | vs. both: 18 | 19 | Only uses the N lowest channels necessary, so you can record a monophonic track, or in limited polyphony, by pressing fewer keys at once. 20 | 21 | ## Flashing 22 | 23 | See https://duy.retrokits.com. 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | -------------------------------------------------------------------------------- /poly.c: -------------------------------------------------------------------------------- 1 | /* 2 | * PARAMETERS: 3 | * 4 | * PMUXCH = input MIDI play channel of the polyMUX 5 | * PMUXBASECH = output base MIDI channel of polyMUX 6 | * PMUXPOLY: 1-8, number of polyMUX polyphony channels 7 | * 8 | * STARTUP CONFIGURATION: 9 | * 10 | * e.g. Press a key on keystep, plug in RK002 and release within three seconds. 11 | * PMUXCH will be set to current Keystep MIDI channel. 12 | * PMUXPOLY will be set based on the note (C = 1, C# = 2, etc.) 13 | */ 14 | 15 | #define APP_NAME "Syntakt PolyMUX (long note compatible)" 16 | #define APP_AUTHOR "erikdesjardins" 17 | #define APP_VERSION "0.3" 18 | #define APP_GUID "783b9c09-9bf5-4ea8-8faf-f59e668768c1" 19 | 20 | RK002_DECLARE_INFO(APP_NAME, APP_AUTHOR, APP_VERSION, APP_GUID) 21 | 22 | RK002_DECLARE_PARAM(PMUXCH, 1, 1, 16, 15) 23 | RK002_DECLARE_PARAM(PMUXBASECH, 1, 1, 16, 1) 24 | RK002_DECLARE_PARAM(PMUXPOLY, 1, 1, 12, 4) 25 | 26 | byte inChannel = 0; 27 | byte outBaseChannel = 0; 28 | byte polyphony = 0; 29 | 30 | byte heartCount = 150; 31 | 32 | void updateParams() 33 | { 34 | // input channel (1-16 -> 0-15 internally) 35 | inChannel = (RK002_paramGet(PMUXCH) == 0) ? 16 : RK002_paramGet(PMUXCH) - 1; 36 | // base channel (1-16 -> 0-15 internally) 37 | outBaseChannel = (RK002_paramGet(PMUXBASECH) == 0) ? 0 : RK002_paramGet(PMUXBASECH) - 1; 38 | // polyphony 39 | polyphony = RK002_paramGet(PMUXPOLY); 40 | } 41 | 42 | void trainKey(byte channel, byte note) 43 | { 44 | // train note -> set polyphony 45 | note = note % 12; 46 | byte poly = min(12, (note + 1)); 47 | RK002_paramSet(PMUXPOLY, poly); 48 | 49 | // train index -> set polymux input channel 50 | RK002_paramSet(PMUXCH, channel + 1); 51 | heartCount = 0; 52 | updateParams(); 53 | } 54 | 55 | void RK002_onParamChange(unsigned param_nr, int param_val) 56 | { 57 | updateParams(); 58 | } 59 | 60 | // called every 100ms 61 | void RK002_onHeartBeat() 62 | { 63 | if (heartCount) 64 | heartCount--; 65 | } 66 | 67 | // Active key per polymux channel. 68 | // 0xff = no active key 69 | byte activekey[12] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; 70 | 71 | unsigned int epoch = 0; 72 | 73 | // Epoch timestamp when key was pressed. 74 | // Used to decide which key to evict when polyphony is full. 75 | unsigned int activeepoch[12] = {0}; 76 | 77 | void sendPolyMuxOutput(byte polymuxidx, byte cmd, byte d1, byte d2) 78 | { 79 | byte chn = outBaseChannel + polymuxidx; 80 | 81 | if (chn <= 15) 82 | { 83 | byte sts = cmd | chn; 84 | RK002_sendChannelMessage(sts, d1, d2); 85 | } 86 | } 87 | 88 | void inputToPolyMux(byte cmd, byte d1, byte d2) 89 | { 90 | switch (cmd) 91 | { 92 | case 0x90: // note on 93 | { 94 | if (d2 > 0) 95 | { 96 | // See if this key is already pressed. 97 | // This shouldn't be possible unless you're feeding in input from more than one source, 98 | // and it means we lose the ability to track key releases, so overwrite the existing note if one exists. 99 | for (int idx = 0; idx < polyphony; ++idx) 100 | { 101 | if (activekey[idx] == d1) 102 | { 103 | sendPolyMuxOutput(idx, 0x90, d1, d2); 104 | return; 105 | } 106 | } 107 | 108 | // Now that we know this is a new key, try to find the first unused channel. 109 | for (int idx = 0; idx < polyphony; ++idx) 110 | { 111 | if (activekey[idx] == 0xff) 112 | { 113 | activekey[idx] = d1; 114 | activeepoch[idx] = epoch++; 115 | sendPolyMuxOutput(idx, 0x90, d1, d2); 116 | return; 117 | } 118 | } 119 | 120 | // There are no unused channels, so evict the oldest key. 121 | int oldestidx = 0; 122 | unsigned int oldestage = 0; 123 | for (int idx = 0; idx < polyphony; ++idx) 124 | { 125 | unsigned int age = epoch - activeepoch[idx]; 126 | if (age > oldestage) 127 | { 128 | oldestidx = idx; 129 | oldestage = age; 130 | } 131 | } 132 | // Send note off for existing key... 133 | sendPolyMuxOutput(oldestidx, 0x80, activekey[oldestidx], 0); 134 | // ...and replace it with the new key. 135 | activekey[oldestidx] = d1; 136 | activeepoch[oldestidx] = epoch++; 137 | sendPolyMuxOutput(oldestidx, 0x90, d1, d2); 138 | return; 139 | } 140 | else 141 | { 142 | // velo == 0 -> note off 143 | // fallthrough 144 | } 145 | } 146 | case 0x80: // note off 147 | { 148 | for (int idx = 0; idx < polyphony; ++idx) 149 | { 150 | if (activekey[idx] == d1) 151 | { 152 | sendPolyMuxOutput(idx, 0x80, d1, d2); 153 | activekey[idx] = 0xff; 154 | return; 155 | } 156 | } 157 | break; 158 | } 159 | case 0xA0: // aftertouch 160 | { 161 | for (int idx = 0; idx < polyphony; ++idx) 162 | { 163 | if (activekey[idx] == d1) 164 | { 165 | // transform poly aftertouch to mono aftertouch (channel pressure) 166 | sendPolyMuxOutput(idx, 0xD0, d2, 0); 167 | return; 168 | } 169 | } 170 | break; 171 | } 172 | default: // anything else (pitch bend, etc.) 173 | { 174 | // send to all possible channels 175 | for (byte idx = 0; idx < polyphony; ++idx) 176 | { 177 | sendPolyMuxOutput(idx, cmd, d1, d2); 178 | } 179 | break; 180 | } 181 | } 182 | } 183 | 184 | bool RK002_onChannelMessage(byte sts, byte d1, byte d2) 185 | { 186 | byte cmd = sts & 0xf0; 187 | byte chn = sts & 0x0f; 188 | 189 | // Handle training on startup 190 | switch (cmd) 191 | { 192 | case 0x90: // note on 193 | if (d2 > 0) 194 | { 195 | if (heartCount) 196 | { 197 | heartCount = 0; // key was not held before startup, cancel training 198 | } 199 | break; 200 | } 201 | else 202 | { 203 | // velo == 0 -> note off 204 | // fallthrough 205 | } 206 | case 0x80: // note off 207 | if (heartCount) 208 | { 209 | heartCount = 0; // disable training after this 210 | trainKey(chn, d1); 211 | } 212 | break; 213 | } 214 | 215 | bool ischannelmessage = sts >= 0x80 && sts <= 0xEF; 216 | 217 | if (ischannelmessage && chn == inChannel) 218 | { 219 | inputToPolyMux(cmd, d1, d2); 220 | 221 | return false; // don't send through 222 | } 223 | else 224 | { 225 | return true; // send through 226 | } 227 | } 228 | 229 | void setup() 230 | { 231 | updateParams(); 232 | } 233 | 234 | void loop() { 235 | // do nothing 236 | } 237 | --------------------------------------------------------------------------------