├── LICENSE ├── README.md └── analog_minisynth_controller.ino /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 petegaggs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # analog_synth_controller 2 | arduino controller for analog mini-synth. Does MIDI-CV, envelope, LFO, noise 3 | -------------------------------------------------------------------------------- /analog_minisynth_controller.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * Analog Minisynth Contoller 3 | * by Peter Gaggs 4 | * Used for the controller part of analog mini-synth 5 | * MIDI to CV conversion (with MCP4921 SPI DAC) 6 | * Envelope (PWM) 7 | * LFO (PWM), several waveforms 8 | * Noise source 9 | */ 10 | #define NOTE_PRIORITY_LAST // supports HIGH, LOW, LAST 11 | #define TRIGGER_MULTI_RELEASE // supports SINGLE, MULTI, MULTI_RELEASE 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | MIDI_CREATE_DEFAULT_INSTANCE(); 18 | #include 19 | #include 20 | #include 21 | #include 22 | #define SLAVE_SELECT_PIN 7 //spi chip select 23 | #define NOISE_PIN 8 24 | #define TEST_PIN 6 //test critical interrupt timing 25 | #define AUTO_REPEAT_MODE_PIN 5 26 | #define LFO_PWM OCR1A 27 | #define LFO_PWM_PIN 9 28 | #define ENV_PWM OCR1B 29 | #define ENV_PWM_PIN 10 30 | #define ENV_ATTACK_PIN A0 31 | #define ENV_DECAY_PIN A1 32 | #define ENV_SUSTAIN_PIN A2 33 | #define ENV_RELEASE_PIN A3 34 | #define LFO_FREQ_PIN A4 35 | #define LFO_WAVE_PIN A5 36 | #define LFO_SYNC_MODE_PIN 2 // The lfo can be reset by note on if desired 37 | #define DAC_SCALE_PER_SEMITONE 42 38 | 39 | uint32_t lfsr = 1; //32 bit LFSR, must be non-zero to start 40 | 41 | // table of 256 sine values / one sine period / stored in flash memory 42 | const char sineTable[] PROGMEM = { 43 | 127,130,133,136,139,143,146,149,152,155,158,161,164,167,170,173,176,178,181,184,187,190,192,195,198,200,203,205,208,210,212,215,217,219,221,223,225,227,229,231,233,234,236,238,239,240, 44 | 242,243,244,245,247,248,249,249,250,251,252,252,253,253,253,254,254,254,254,254,254,254,253,253,253,252,252,251,250,249,249,248,247,245,244,243,242,240,239,238,236,234,233,231,229,227,225,223, 45 | 221,219,217,215,212,210,208,205,203,200,198,195,192,190,187,184,181,178,176,173,170,167,164,161,158,155,152,149,146,143,139,136,133,130,127,124,121,118,115,111,108,105,102,99,96,93,90,87,84,81,78, 46 | 76,73,70,67,64,62,59,56,54,51,49,46,44,42,39,37,35,33,31,29,27,25,23,21,20,18,16,15,14,12,11,10,9,7,6,5,5,4,3,2,2,1,1,1,0,0,0,0,0,0,0,1,1,1,2,2,3,4,5,5,6,7,9,10,11,12,14,15,16,18,20,21,23,25,27,29,31, 47 | 33,35,37,39,42,44,46,49,51,54,56,59,62,64,67,70,73,76,78,81,84,87,90,93,96,99,102,105,108,111,115,118,121,124 48 | }; 49 | 50 | // table of exponential rising waveform for envelope gen 51 | const char expTable[] PROGMEM = { 52 | 0,5,10,15,19,24,28,33,37,41,45,49,53,57,61,65,69,72,76,79,83,86,89,93,96,99,102,105,108,111,114,116,119,122,124,127,129,132,134,136,139,141,143,145,148,150,152,154,156,158,160,161,163,165,167,169, 53 | 170,172,174,175,177,178,180,181,183,184,185,187,188,189,191,192,193,194,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,214,215,216,217,218,218,219,220,220,221,222,222, 54 | 223,224,224,225,226,226,227,227,228,228,229,229,230,230,231,231,232,232,233,233,234,234,235,235,235,236,236,237,237,237,238,238,238,239,239,239,240,240,240,241,241,241,242,242,242,242,243,243,243, 55 | 243,244,244,244,244,245,245,245,245,245,246,246,246,246,246,247,247,247,247,247,247,248,248,248,248,248,248,249,249,249,249,249,249,249,249,250,250,250,250,250,250,250,250,251,251,251,251,251,251, 56 | 251,251,251,251,252,252,252,252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254, 57 | 254,254,254,254 58 | }; 59 | 60 | // LFO stuff 61 | uint32_t lfoPhaccu; // phase accumulator 62 | uint32_t lfoTword_m; // dds tuning word m 63 | uint8_t lfoCnt; // top 8 bits of accum is index into table 64 | 65 | float lfoControlVoltage; 66 | enum lfoWaveTypes { 67 | RAMP, 68 | SAW, 69 | TRI, 70 | SINE, 71 | SQR 72 | }; 73 | lfoWaveTypes lfoWaveform; 74 | bool lfoSyncMode = false; 75 | bool lfoReset = false; 76 | 77 | // envelope stuff 78 | uint32_t envPhaccu; // phase accumulator 79 | uint32_t envAttackTword; // dds tuning word attack stage 80 | uint32_t envReleaseTword; // dds tuning word release stage 81 | uint32_t envDecayTword; // dds tuning word decay stage 82 | uint8_t envSustainControl; // envelope sustain control 0-255 83 | uint8_t envCnt; // top 8 bits of accum is index into table 84 | uint8_t lastEnvCnt; 85 | uint8_t envCurrentLevel; // the current level of envelope 86 | uint8_t envStoredLevel; // the level that the envelope was at start of release stage 87 | uint8_t envMultFactor; // multiplication factor to account for release starting before attack complete and visa versa 88 | float envControlVoltage; 89 | 90 | bool autoRepeatMode = false; 91 | 92 | enum envStates { 93 | WAIT, 94 | START_ATTACK, 95 | ATTACK, 96 | START_DECAY, 97 | DECAY, 98 | SUSTAIN, 99 | START_RELEASE, 100 | RELEASE 101 | }; 102 | envStates envState; 103 | 104 | //MIDI variables 105 | int currentMidiNote; //the note currently being played 106 | int keysPressedArray[128] = {0}; //to keep track of which keys are pressed 107 | bool notePlaying = false; 108 | 109 | void setup() { 110 | MIDI.begin(MIDI_CHANNEL_OMNI); 111 | MIDI.setHandleNoteOn(handleNoteOn); // Put only the name of the function 112 | MIDI.setHandleNoteOff(handleNoteOff); 113 | pinMode(LFO_PWM_PIN, OUTPUT); 114 | pinMode(ENV_PWM_PIN, OUTPUT); //PWM OC2B Envelope output 115 | pinMode(NOISE_PIN, OUTPUT); 116 | pinMode(LFO_SYNC_MODE_PIN, INPUT); 117 | digitalWrite(LFO_SYNC_MODE_PIN, HIGH); //enable internal pullup 118 | pinMode(AUTO_REPEAT_MODE_PIN, INPUT); 119 | digitalWrite(AUTO_REPEAT_MODE_PIN, HIGH); //enable internal pullup 120 | //SPI stuff 121 | pinMode (SLAVE_SELECT_PIN, OUTPUT); // set the slaveSelectPin as an output: 122 | digitalWrite(SLAVE_SELECT_PIN,HIGH); //set chip select high 123 | SPI.begin(); 124 | pinMode(TEST_PIN, OUTPUT); // for testing how long ISR takes 125 | // timer 1 phase accurate PWM 8 bit, no prescaling, non inverting mode channels A & B used 126 | TCCR1A = _BV(COM1A1) | _BV(COM1B1)| _BV(WGM10); 127 | TCCR1B = _BV(CS10); 128 | // timer 1 interrupt 129 | TIMSK1 = _BV(TOIE1); 130 | // envelope stuff 131 | envState = WAIT; 132 | setNotePitch(60); // Middle C, just to get things started 133 | } 134 | 135 | void dacWrite(int value) { 136 | //write a 12 bit number to the MCP8421 DAC 137 | if ((value < 0) || (value > 4095)) { 138 | value = 0; 139 | } 140 | // take the SS pin low to select the chip: 141 | digitalWrite(SLAVE_SELECT_PIN,LOW); 142 | //send a value to the DAC 143 | SPI.transfer(0x10 | ((value >> 8) & 0x0F)); //bits 0..3 are bits 8..11 of 12 bit value, bits 4..7 are control data 144 | SPI.transfer(value & 0xFF); //bits 0..7 of 12 bit value 145 | // take the SS pin high to de-select the chip: 146 | digitalWrite(SLAVE_SELECT_PIN,HIGH); 147 | } 148 | 149 | void setNotePitch(int note) { 150 | //receive a midi note number and set the DAC voltage accordingly for the pitch CV 151 | dacWrite(note * DAC_SCALE_PER_SEMITONE); 152 | } 153 | 154 | void getLfoParams() { 155 | // read ADC to calculate the required DDS tuning word, log scale between 0.01Hz and 10Hz approx 156 | float lfoControlVoltage = analogRead(LFO_FREQ_PIN) * 11/1024; //gives 11 octaves range 0.01Hz to 10Hz 157 | lfoTword_m = 1369 * pow(2.0, lfoControlVoltage); //1369 sets the lowest frequency to 0.01Hz 158 | // read ADC to get the LFO wave type 159 | int adcVal = analogRead(LFO_WAVE_PIN); 160 | if (adcVal < 128) { 161 | lfoWaveform = RAMP; 162 | } else if (adcVal < 384) { 163 | lfoWaveform = SAW; 164 | } else if (adcVal < 640) { 165 | lfoWaveform = TRI; 166 | } else if (adcVal < 896) { 167 | lfoWaveform = SINE; 168 | } else { 169 | lfoWaveform = SQR; 170 | } 171 | lfoSyncMode = digitalRead(LFO_SYNC_MODE_PIN) == 0 ? true : false; 172 | } 173 | 174 | void getEnvParams() { 175 | float envControlVoltage; 176 | // read ADC to calculate the required DDS tuning word, log scale between 1ms and 10s approx 177 | envControlVoltage = (1023 - analogRead(ENV_ATTACK_PIN)) * 13/1024; //gives 13 octaves range 1ms to 10s 178 | envAttackTword = 13690 * pow(2.0, envControlVoltage); //13690 sets the longest rise time to 10s 179 | envControlVoltage = (1023 - analogRead(ENV_RELEASE_PIN)) * 13/1024; //gives 13 octaves range 1ms to 10s 180 | envReleaseTword = 13690 * pow(2.0, envControlVoltage); //13690 sets the longest rise time to 10s 181 | envControlVoltage = (1023 - analogRead(ENV_DECAY_PIN)) * 13/1024; //gives 13 octaves range 1ms to 10s 182 | envDecayTword = 13690 * pow(2.0, envControlVoltage); //13690 sets the longest rise time to 10s 183 | envSustainControl = analogRead(ENV_SUSTAIN_PIN) >> 2; //0 to 255 level for sustain control 184 | autoRepeatMode = digitalRead(AUTO_REPEAT_MODE_PIN) == 0 ? true : false; 185 | } 186 | 187 | void loop() { 188 | MIDI.read(); 189 | getLfoParams(); 190 | getEnvParams(); 191 | } 192 | 193 | SIGNAL(TIMER1_OVF_vect) { 194 | PORTD |= 0x40; //set D6 to test timing 195 | // handle noise signal. Set or clear noise pin PB0 (digital pin 8) 196 | unsigned lsb = lfsr & 1; 197 | if (lsb) { 198 | PORTB |= 1; 199 | } else { 200 | PORTB &= ~1; 201 | } 202 | // advance LFSR 203 | lfsr >>= 1; 204 | if (lsb) { 205 | lfsr ^= 0xA3000000u; 206 | } 207 | // handle LFO DDS 208 | if (lfoReset) { 209 | lfoPhaccu = 0; // reset the lfo 210 | lfoReset = false; 211 | } else { 212 | lfoPhaccu += lfoTword_m; // increment phase accumulator 213 | } 214 | lfoCnt = lfoPhaccu >> 24; // use upper 8 bits for phase accu as frequency information 215 | switch (lfoWaveform) { 216 | case RAMP: 217 | LFO_PWM = lfoCnt; 218 | break; 219 | case SAW: 220 | LFO_PWM = 255 - lfoCnt; 221 | break; 222 | case TRI: 223 | if (lfoCnt & 0x80) { 224 | LFO_PWM = 254 - ((lfoCnt & 0x7F) << 1); //ramp down 225 | } else { 226 | LFO_PWM = lfoCnt << 1; //ramp up 227 | } 228 | break; 229 | case SINE: 230 | // sine wave from table 231 | LFO_PWM = pgm_read_byte_near(sineTable + lfoCnt); 232 | break; 233 | case SQR: 234 | if (lfoCnt & 0x80) { 235 | LFO_PWM = 255; 236 | } else { 237 | LFO_PWM = 0; 238 | } 239 | break; 240 | default: 241 | break; 242 | } 243 | // handle Envelope DDS 244 | switch (envState) { 245 | case WAIT: 246 | envPhaccu = 0; // clear the accumulator 247 | lastEnvCnt = 0; 248 | envCurrentLevel = 0; 249 | ENV_PWM = 0; 250 | if (autoRepeatMode) { 251 | envState = START_ATTACK; 252 | } 253 | break; 254 | case START_ATTACK: 255 | envPhaccu = 0; // clear the accumulator 256 | lastEnvCnt = 0; 257 | envMultFactor = 255 - envCurrentLevel; 258 | envStoredLevel = envCurrentLevel; 259 | envState = ATTACK; 260 | break; 261 | case ATTACK: 262 | envPhaccu += envAttackTword; // increment phase accumulator 263 | envCnt = envPhaccu >> 24; // use upper 8 bits as index into table 264 | if (envCnt < lastEnvCnt) { 265 | envState = START_DECAY; // end of attack stage when counter wraps 266 | } else { 267 | envCurrentLevel = ((envMultFactor * pgm_read_byte_near(expTable + envCnt)) >> 8) + envStoredLevel; 268 | ENV_PWM = envCurrentLevel; 269 | lastEnvCnt = envCnt; 270 | } 271 | break; 272 | case START_DECAY: 273 | envPhaccu = 0; // clear the accumulator 274 | lastEnvCnt = 0; 275 | envMultFactor = 255 - envSustainControl; 276 | envState = DECAY; 277 | break; 278 | case DECAY: 279 | envPhaccu += envDecayTword; // increment phase accumulator 280 | envCnt = envPhaccu >> 24; // use upper 8 bits as index into table 281 | if (envCnt < lastEnvCnt) { 282 | envState = SUSTAIN; // end of release stage when counter wraps 283 | } else { 284 | envCurrentLevel = 255 - ((envMultFactor * pgm_read_byte_near(expTable + envCnt)) >> 8); 285 | ENV_PWM = envCurrentLevel; 286 | lastEnvCnt = envCnt; 287 | } 288 | break; 289 | case SUSTAIN: 290 | envPhaccu = 0; // clear the accumulator 291 | lastEnvCnt = 0; 292 | envCurrentLevel = envSustainControl; 293 | ENV_PWM = envCurrentLevel; 294 | if (autoRepeatMode) { 295 | envState = START_RELEASE; 296 | } 297 | break; 298 | case START_RELEASE: 299 | envPhaccu = 0; // clear the accumulator 300 | lastEnvCnt = 0; 301 | envMultFactor = envCurrentLevel; 302 | envStoredLevel = envCurrentLevel; 303 | envState = RELEASE; 304 | break; 305 | case RELEASE: 306 | envPhaccu += envReleaseTword; // increment phase accumulator 307 | envCnt = envPhaccu >> 24; // use upper 8 bits as index into table 308 | if (envCnt < lastEnvCnt) { 309 | envState = WAIT; // end of release stage when counter wraps 310 | } else { 311 | envCurrentLevel = envStoredLevel - ((envMultFactor * pgm_read_byte_near(expTable + envCnt)) >> 8); 312 | ENV_PWM = envCurrentLevel; 313 | lastEnvCnt = envCnt; 314 | } 315 | break; 316 | default: 317 | break; 318 | } 319 | PORTD &= ~0x40; //clear D6 to test timing 320 | } 321 | 322 | void handleNoteOn(byte channel, byte pitch, byte velocity) { 323 | // this function is called automatically when a note on message is received 324 | bool trigger; 325 | #ifdef TRIGGER_SINGLE 326 | if (notePlaying == false) { 327 | trigger = true; 328 | } else { 329 | trigger = false; 330 | } 331 | #endif 332 | #ifdef TRIGGER_MULTI 333 | trigger = true; 334 | #endif 335 | #ifdef TRIGGER_MULTI_RELEASE 336 | trigger = true; 337 | #endif 338 | keysPressedArray[pitch] = 1; 339 | #ifdef NOTE_PRIORITY_HIGH 340 | if (pitch == findHighestKeyPressed()) { 341 | synthNoteOn(pitch, trigger); 342 | } 343 | #endif 344 | #ifdef NOTE_PRIORITY_LOW 345 | if (pitch == findLowestKeyPressed()) { 346 | synthNoteOn(pitch, trigger); 347 | } 348 | #endif 349 | #ifdef NOTE_PRIORITY_LAST 350 | synthNoteOn(pitch, trigger); 351 | #endif 352 | } 353 | 354 | void handleNoteOff(byte channel, byte pitch, byte velocity) 355 | { 356 | int newKey; 357 | bool trigger = false; 358 | keysPressedArray[pitch] = 0; //update the array holding the keys pressed 359 | if (pitch == currentMidiNote) { 360 | //only act if the note released is the one currently playing, otherwise ignore it 361 | #ifdef NOTE_PRIORITY_HIGH 362 | newKey = findHighestKeyPressed(); //search the array to find the highest key pressed, will return -1 if no keys pressed 363 | #endif 364 | #ifdef NOTE_PRIORITY_LOW 365 | newKey = findLowestKeyPressed(); //search the array to find the lowest key pressed, will return -1 if no keys pressed 366 | #endif 367 | #ifdef NOTE_PRIORITY_LAST 368 | newKey = findLowestKeyPressed(); //this is a tricky one, treat same as lowest priority 369 | #endif 370 | #ifdef TRIGGER_MULTI_RELEASE 371 | trigger = true; 372 | #endif 373 | if (newKey != -1) { 374 | //there is another key pressed somewhere, update pitch according to note priority 375 | synthNoteOn(newKey, true); 376 | } 377 | else { 378 | //there are no other keys pressed so proper note off 379 | synthNoteOff(); 380 | } 381 | } 382 | } 383 | 384 | int findHighestKeyPressed(void) { 385 | //search the array to find the highest key pressed. Return -1 if no keys are pressed 386 | int highestKeyPressed = -1; 387 | for (int count = 0; count < 128; count++) { 388 | //go through the array holding the keys pressed to find which is the highest (highest note has priority), and to find out if no keys are pressed 389 | if (keysPressedArray[count] == 1) { 390 | highestKeyPressed = count; //find the highest one 391 | } 392 | } 393 | return(highestKeyPressed); 394 | } 395 | 396 | int findLowestKeyPressed(void) { 397 | //search the array to find the lowest key pressed. Return -1 if no keys are pressed 398 | int lowestKeyPressed = -1; 399 | for (int count = 127; count >= 0; count--) { 400 | //go through the array holding the keys pressed to find which is the lowest (lowest note has priority), and to find out if no keys are pressed 401 | if (keysPressedArray[count] == 1) { 402 | lowestKeyPressed = count; //find the lowest one 403 | } 404 | } 405 | return(lowestKeyPressed); 406 | } 407 | 408 | void synthNoteOn(int note, bool trigger) { 409 | //starts playback of a note 410 | setNotePitch(note); //set the oscillator pitch 411 | if (trigger){ 412 | if (envState != ATTACK) { 413 | envState = START_ATTACK; 414 | } 415 | if (lfoSyncMode) { 416 | lfoReset = true; 417 | } 418 | } 419 | currentMidiNote = note; //store the current note 420 | notePlaying = true; 421 | } 422 | 423 | void synthNoteOff(void) { 424 | envState = START_RELEASE; 425 | notePlaying = false; 426 | } 427 | 428 | 429 | --------------------------------------------------------------------------------