├── .gitignore ├── LICENSE ├── README.md ├── clock-divider ├── 3d │ ├── plate.fcstd │ ├── plate.stl │ └── plate.svg ├── README.md ├── clock-divider-bom.csv ├── clock-divider.ino ├── lib │ ├── Button.cpp │ └── Led.cpp ├── pcb │ ├── board-easyeda.json │ ├── board-gerber.zip │ ├── panel-decor.svg │ ├── panel-easyeda.json │ ├── panel-font.ttf │ ├── panel-gerber.zip │ └── panel.svg ├── pictures │ ├── IMG_20190107_215258.jpg │ ├── IMG_20190112_155043.jpg │ ├── IMG_20190112_163027.jpg │ ├── IMG_20190112_224337.jpg │ ├── IMG_20190306_214855.jpg │ ├── IMG_20190307_225219.jpg │ ├── IMG_20210702_130542.jpg │ ├── IMG_20210702_130610.jpg │ ├── IMG_20210702_130733_M.jpg │ └── IMG_20210804_140742.jpg └── schematic.png ├── forks ├── 3d │ ├── plate.fcstd │ ├── plate.stl │ └── plate.svg ├── README.md ├── forks-bom.csv ├── forks.ino ├── lib │ ├── Button.cpp │ ├── CV.cpp │ └── Led.cpp ├── pcb │ ├── board-easyeda.json │ ├── board-gerber.zip │ ├── panel-decor.svg │ ├── panel-easyeda.json │ ├── panel-font.ttf │ ├── panel-gerber.zip │ └── panel.svg ├── pictures │ ├── IMG_20190120_210330.jpg │ ├── IMG_20190426_182714.jpg │ ├── IMG_20190427_112937.jpg │ ├── IMG_20190427_113603.jpg │ ├── IMG_20211223_133624.jpg │ ├── IMG_20211223_133624_T.jpg │ ├── IMG_20211223_135158_M.jpg │ ├── IMG_20211223_135315.jpg │ └── IMG_20211228_124830.jpg └── schematic.png ├── in-cv ├── 3d │ ├── plate.fcstd │ └── plate.stl ├── README.md ├── in-cv.ino ├── lib │ ├── Button.cpp │ ├── Led.cpp │ ├── MCP4728.cpp │ ├── MultiPointMap.cpp │ └── SR74HC595.cpp ├── patterns │ ├── .eslintrc.json │ ├── .gitignore │ ├── cli.js │ ├── cli.test.js │ ├── package.json │ ├── patterns.h │ └── patterns.txt ├── pcb │ ├── board-easyeda.json │ ├── board-gerber.zip │ ├── panel-art-copper.svg │ ├── panel-art-readme.txt │ ├── panel-art.svg │ ├── panel-easyeda.json │ ├── panel-font.ttf │ └── panel-gerber.zip ├── pictures │ ├── IMG_20190720_185259.jpg │ ├── IMG_20190726_192809.jpg │ ├── IMG_20190727_130919.jpg │ ├── IMG_20190727_131911.jpg │ ├── IMG_20210323_125702_M.png │ ├── IMG_20210323_125702_R.jpg │ ├── IMG_20210323_125702_S.jpg │ ├── IMG_20210323_125702_T.jpg │ ├── IMG_20210323_125946_R.jpg │ ├── IMG_20210323_125946_S.jpg │ └── IMG_20210328_141742.jpg └── schematic.png ├── lib ├── Button.cpp ├── CV.cpp ├── Led.cpp ├── MCP4728.cpp ├── MultiPointMap.cpp └── SR74HC595.cpp ├── midi4plus1 ├── 3d │ ├── plate.fcstd │ └── plate.stl ├── README.md ├── lib │ ├── Button.cpp │ ├── Led.cpp │ ├── MCP4728.cpp │ └── MultiPointMap.cpp ├── midi4plus1-bom.csv ├── midi4plus1.ino ├── mono.cpp ├── pcb │ ├── board-easyeda.json │ ├── board-gerber.zip │ ├── panel-easyeda.json │ ├── panel-font.ttf │ ├── panel-gerber.zip │ └── panel.svg ├── pictures │ ├── IMG_20200307_181436.jpg │ ├── IMG_20200413_181507.jpg │ ├── IMG_20210620_175313.jpg │ ├── IMG_20210620_175313_T.jpg │ ├── IMG_20210621_140800.jpg │ ├── IMG_20210621_140800_M.jpg │ ├── IMG_20210621_140800_T.jpg │ ├── IMG_20210621_140842.jpg │ ├── IMG_20210621_140903.jpg │ ├── IMG_20210621_140903_T.jpg │ └── IMG_20210901_124851.jpg ├── poly.cpp └── schematic.png └── tools └── mcp4728_addr ├── MCP4728.cpp ├── SoftI2cMaster.cpp ├── SoftI2cMaster.h ├── TwoWireBase.h └── mcp4728_addr.ino /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.fcstd1 2 | **/plate-sheet.* 3 | */pcb/update-pcbs.sh 4 | tools/test 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Arduino Eurorack projects 2 | ========================= 3 | 4 | DIY Eurorack projects with Arduino and C++ libraries. 5 | 6 | > 🛒 *Some of these modules are for sale on **[Reverb](https://reverb.com/shop/joeseggiola)** and **[Tindie](https://www.tindie.com/stores/joeseggiola/)**, as PCB and panel kits or fully assembled!* 7 | 8 | Modules 9 | ------- 10 | 11 | Each module has its own detailed README file. 12 | 13 | - [Clock divider](clock-divider/): clock divider in 4 HP. 14 | - [Forks](forks/): two Bernoulli gates, clone of Mutable Instruments Branches. 15 | - [In CV](in-cv/): virtual ensemble that plays Terry Riley's "In C" on CV/gate outputs. 16 | - [MIDI 4+1](midi4plus1/): polyphonic and monophonic MIDI to 4x CV/gate interface in 6 HP. 17 | 18 | Libraries and tools 19 | ------------------- 20 | 21 | - [Button class](lib/Button.cpp): convenient reading methods, debouncing, combined single and long-press, internal pull-up usage. 22 | - [CV class](lib/CV.cpp): analog input reader with low/high thresholds, for CV inputs and knobs. 23 | - [LED class](lib/Led.cpp): handles minimum duration to ensure visibility, implements blinking, toggle, flash. 24 | - [MCP4728 class](lib/MCP4728.cpp): extends [Hideaki Tai's lib](https://github.com/hideakitai/MCP4728) to include optional LDAC; a sketch for [setting I2C address (device ID)](tools/mcp4728_addr) is provided. 25 | - [MultiPointMap class](lib/MultiPointMap.cpp): maps values using a multi-linear scale that can be persisted in EEPROM, used to implement DACs calibration (adapted from Befaco [MIDI Thing](https://github.com/Befaco/midithing) and Emilie Gillet's [CVpal](https://github.com/pichenettes/cvpal)). 26 | - [SR74HC595 class](lib/SR74HC595.cpp): simple wrapper around `shiftOut()` to handle 74HC595 shift registers. 27 | 28 | License 29 | ------- 30 | 31 | Code: [GPL 3.0](LICENSE), hardware: [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/). 32 | -------------------------------------------------------------------------------- /clock-divider/3d/plate.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/clock-divider/3d/plate.fcstd -------------------------------------------------------------------------------- /clock-divider/3d/plate.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/clock-divider/3d/plate.stl -------------------------------------------------------------------------------- /clock-divider/README.md: -------------------------------------------------------------------------------- 1 | Clock divider 2 | ============= 3 | 4 | A DIY Arduino-powered clock divider in 4 HP. 5 | 6 | **[Arduino code][1]** | **[BOM][5]** | [🛒 **Buy PCB and panel on Tindie**][3] | **[ModularGrid][2]** | [🗨️ **Mod Wiggler**][4] 7 | 8 | [1]: clock-divider.ino 9 | [5]: clock-divider-bom.csv 10 | [2]: https://www.modulargrid.net/e/joeseggiola-clock-divider 11 | [3]: https://www.tindie.com/products/joeseggiola/clock-divider-for-eurorack-pcb-panel/ 12 | [4]: https://modwiggler.com/forum/viewtopic.php?t=214669 13 | 14 | Features 15 | -------- 16 | 17 | - Divides incoming clock signal by 2, 3, 4, 5, 6, 8, 16, 32 (configurable in code). 18 | - Reset as trigger or manual button. 19 | - Down-beat counting. 20 | - Trigger mode: duration of incoming pulses is preserved on outputs. 21 | - Gate-mode: duration of the output pulses is 50% of divided tempo, enabled by long-pressing the manual reset button. 22 | - Euclidean mode: outputs provide 8 channels of Euclidean rhythms (can be activated [in code](clock-divider.ino#L19), implemented by [Tim Richardson](https://github.com/timini/arduino-eurorack-projects/tree/master/clock-divider-euclid-mod)). 23 | 24 | Schematic 25 | --------- 26 | 27 | ![](schematic.png) 28 | 29 | Pictures 30 | -------- 31 | 32 | ### New [PCB](pcb/) build 33 | 34 | [🛒 **Buy PCB and panel on Tindie**][3] 35 | 36 | 37 | 38 | ### Old [3D-printed](3d/) build 39 | 40 | 41 | -------------------------------------------------------------------------------- /clock-divider/clock-divider-bom.csv: -------------------------------------------------------------------------------- 1 | Type,Description,Name,Value,Qty,Notes / Link 2 | IC,Arduino Nano,,,1,https://store.arduino.cc/products/arduino-nano 3 | Capacitor,"Bypass, ceramic",BYP,100nF,1,"Optional, close values should work the same" 4 | Capacitor,"Bypass, electrolytic",BYP,10µF,1,"Optional, close values should work the same" 5 | Diode,Inputs section,1N4148,,2,"Or 1N914, or anything similar" 6 | Resistor,Inputs section,,10kΩ,4,Close values should work the same 7 | Resistor,Inputs section,,220kΩ,2,Any value down to 100kΩ should work the same 8 | Resistor,Button pull-down,,10kΩ,1,Close values should work the same 9 | Resistor,For LEDs,RL0..8,1kΩ,9,Adjust value depending on desired brightness 10 | Transistor,Inputs section,BC337,,2,Any similar NPN should work the same 11 | Button,"Tactile switch, 7mm",BTN,,1,Glued to the panel and connected with wire to the PCB 12 | Connector,IDC 10 pin socket,,,1,"Eurorack power socket, or 2x5 male pin header" 13 | Connector,Pin header 1x2 female,,,10,"For button and LEDs, if you don’t want to solder them to the PCB" 14 | Jack,Clock and outputs,PJ-316A,,10,https://www.aliexpress.com/item/32837286787.html 15 | LED,"Clock LED, 3mm",L0,,1,"Green, or any colour you like" 16 | LED,"Divisions LEDs, 3mm",L1..8,,8,"Red, or any colour you like" 17 | -------------------------------------------------------------------------------- /clock-divider/clock-divider.ino: -------------------------------------------------------------------------------- 1 | 2 | // CONFIGURATION ============================================================= 3 | 4 | const bool DEBUG = false; // FALSE to disable debug messages on serial port 5 | 6 | const int CLOCK_LED = 1; // LED pin for input signal indication 7 | const int CLOCK_INPUT = 2; // Input signal pin, must be usable for interrupts 8 | const int RESET_INPUT = 3; // Reset signal pin, must be usable for interrupts 9 | const int RESET_BUTTON = 4; // Reset button pin 10 | 11 | const int DIVISIONS[] { 2, 3, 4, 5, 6, 8, 16, 32 }; // Integer divisions of the input clock (max 32 values) 12 | const int DIVISIONS_OUTPUT[] { 5, 6, 7, 8, 9, 10, 11, 12 }; // Output pins 13 | const int DIVISIONS_LEDS[] { 0, A5, A4, A3, A2, A1, A0, 13 }; // LEDs pins 14 | 15 | const unsigned long MODE_SWITCH_LONG_PRESS_DURATION_MS = 3000; // Reset button long-press duration for trig/gate mode switch 16 | const unsigned long BUTTON_DEBOUNCE_DELAY = 50; // Debounce delay for all buttons 17 | const unsigned long LED_MIN_DURATION_MS = 50; // Minimum "on" duration for all LEDs visibility 18 | 19 | const bool EUCLIDEAN = false; // TRUE to enable 8 channels of Euclidean rhythms 20 | const int EUCLIDEAN_N_STEPS = 8; 21 | const int EUCLIDEAN_RHYTHMS[][EUCLIDEAN_N_STEPS] { 22 | { 1, 0, 0, 0, 0, 0, 0, 0 }, 23 | { 1, 0, 0, 0, 1, 0, 0, 0 }, 24 | { 1, 0, 0, 1, 0, 0, 1, 0 }, 25 | { 1, 0, 1, 0, 1, 0, 1, 0 }, 26 | { 0, 1, 1, 0, 1, 1, 0, 1 }, 27 | { 0, 1, 1, 1, 0, 1, 1, 1 }, 28 | { 0, 1, 1, 1, 1, 1, 1, 1 }, 29 | { 1, 1, 1, 1, 1, 1, 1, 1 }, 30 | }; 31 | 32 | // =========================================================================== 33 | 34 | #include 35 | 36 | #include "lib/Button.cpp" 37 | #include "lib/Led.cpp" 38 | 39 | unsigned int n = 0; // Number of divisions 40 | long count = -1; // Input clock counter, -1 in order to go to 0 no the first pulse 41 | bool gateMode = false; // TRUE if gate mode is active, FALSE if standard trig mode is active 42 | 43 | Led clockLed; // Input LED 44 | Led leds[32]; // Output LEDs 45 | Button resetButton; 46 | 47 | volatile bool clock = false; // Clock signal digital reading, set in the clock ISR 48 | volatile bool clockFlag = false; // Clock signal change flag, set in the clock ISR 49 | volatile bool resetFlag = false; // Reset flag, set in the reset ISR 50 | 51 | const int MODE_EEPROM_ADDRESS = 0; 52 | 53 | void setup() { 54 | 55 | // Debugging 56 | if (DEBUG) Serial.begin(9600); 57 | 58 | // Number of divisions 59 | n = sizeof(DIVISIONS) / sizeof(DIVISIONS[0]); 60 | 61 | // Trig/gate mode from permanent storage 62 | gateMode = EEPROM.read(MODE_EEPROM_ADDRESS) == 1; 63 | 64 | // Input 65 | resetButton.init(RESET_BUTTON, BUTTON_DEBOUNCE_DELAY); 66 | 67 | // Setup outputs (divisions and LEDs) 68 | clockLed.init(CLOCK_LED, LED_MIN_DURATION_MS); 69 | for (int i = 0; i < n; i++) { 70 | leds[i].init(DIVISIONS_LEDS[i], LED_MIN_DURATION_MS); 71 | pinMode(DIVISIONS_OUTPUT[i], OUTPUT); 72 | digitalWrite(DIVISIONS_OUTPUT[i], LOW); 73 | } 74 | 75 | // Interrupts 76 | pinMode(CLOCK_INPUT, INPUT); 77 | pinMode(RESET_INPUT, INPUT); 78 | attachInterrupt(digitalPinToInterrupt(CLOCK_INPUT), isrClock, CHANGE); 79 | attachInterrupt(digitalPinToInterrupt(RESET_INPUT), isrReset, RISING); 80 | 81 | } 82 | 83 | void loop() { 84 | 85 | // Read manual reset button and set the flag 86 | if (!resetFlag) { 87 | if (resetButton.read()) { 88 | resetFlag = true; 89 | } 90 | } 91 | 92 | // Clock signal changed 93 | if (clockFlag) { 94 | clockFlag = false; 95 | 96 | if (DEBUG) { 97 | Serial.print("Clock signal changed: "); 98 | Serial.println(clock); 99 | } 100 | 101 | // Input LED 102 | clockLed.set(clock); 103 | 104 | if (clock) { 105 | 106 | // Clock rising, update counter 107 | if (resetFlag) { 108 | resetFlag = false; 109 | count = 0; 110 | } else { 111 | count++; 112 | } 113 | 114 | if (DEBUG) { 115 | Serial.print("Counter changed: "); 116 | Serial.println(count); 117 | } 118 | 119 | } 120 | 121 | // Update outputs according to current trig/gate mode 122 | if (gateMode) { 123 | processGateMode(); 124 | } else { 125 | processTriggerMode(); 126 | } 127 | 128 | } 129 | 130 | // Mode switch 131 | if (resetButton.readLongPressOnce(MODE_SWITCH_LONG_PRESS_DURATION_MS)) { 132 | gateMode = !gateMode; 133 | EEPROM.update(MODE_EEPROM_ADDRESS, gateMode ? 1 : 0); // Mode selection on permanent storage 134 | } 135 | 136 | // Update LEDs 137 | clockLed.loop(); 138 | for (int i = 0; i < n; i++) leds[i].loop(); 139 | 140 | } 141 | 142 | void processTriggerMode() { 143 | 144 | // Copy input signal on current divisions 145 | if (clock) { 146 | 147 | // Rising edge, go HIGH on current divisions (or advance Euclidean patterns) 148 | for (int i = 0; i < n; i++) { 149 | bool v; 150 | if (!EUCLIDEAN) { 151 | v = (count % DIVISIONS[i] == 0); 152 | } else { 153 | int step = count % EUCLIDEAN_N_STEPS; 154 | v = EUCLIDEAN_RHYTHMS[i][step] == 1; 155 | } 156 | digitalWrite(DIVISIONS_OUTPUT[i], v ? HIGH : LOW); 157 | leds[i].set(v); 158 | } 159 | 160 | } else { 161 | 162 | // Falling edge, go LOW on every output 163 | for (int i = 0; i < n; i++) { 164 | digitalWrite(DIVISIONS_OUTPUT[i], LOW); 165 | leds[i].off(); 166 | } 167 | 168 | } 169 | 170 | } 171 | 172 | void processGateMode() { 173 | 174 | // Keep outputs high for ~50% of divided time 175 | for (int i = 0; i < n; i++) { 176 | 177 | // Go HIGH on the rising edges that corresponds to the division (or Euclidean pattern) 178 | bool high; 179 | int modulo, step; 180 | if (!EUCLIDEAN) { 181 | modulo = (count % DIVISIONS[i]); 182 | high = clock && modulo == 0; 183 | } else { 184 | step = count % EUCLIDEAN_N_STEPS; 185 | high = clock && EUCLIDEAN_RHYTHMS[i][step] == 1; 186 | } 187 | if (high) { 188 | digitalWrite(DIVISIONS_OUTPUT[i], HIGH); 189 | leds[i].on(); 190 | } 191 | 192 | // Go LOW on rising edges for even divisions and falling edges for odd divisions, 193 | // considering the edges that corresponds to the half value of the division 194 | bool low; 195 | if (!EUCLIDEAN) { 196 | bool divisionIsOdd = (DIVISIONS[i] % 2 != 0); 197 | low = (modulo == (int)(floor(DIVISIONS[i] / 2.0))); 198 | low = low && ((clock && !divisionIsOdd) || (!clock && divisionIsOdd)); 199 | } else { 200 | low = clock && EUCLIDEAN_RHYTHMS[i][step] == 0; 201 | } 202 | if (low) { 203 | digitalWrite(DIVISIONS_OUTPUT[i], LOW); 204 | leds[i].off(); 205 | } 206 | 207 | } 208 | 209 | } 210 | 211 | void isrClock() { 212 | clock = (digitalRead(CLOCK_INPUT) == HIGH); 213 | clockFlag = true; 214 | } 215 | 216 | void isrReset() { 217 | resetFlag = true; 218 | } 219 | -------------------------------------------------------------------------------- /clock-divider/lib/Button.cpp: -------------------------------------------------------------------------------- 1 | #ifndef Button_h 2 | #define Button_h 3 | 4 | #include "Arduino.h" 5 | 6 | class Button { 7 | 8 | public: 9 | 10 | /** 11 | * Setup the button, specifying and optional debounce delay 12 | */ 13 | void init(byte pin, unsigned int debounceDelayMs = 0, bool invert = false, bool internalPullup = false) { 14 | 15 | this->pin = pin; 16 | this->debounceDelayMs = debounceDelayMs; 17 | this->invert = invert; 18 | 19 | this->lastPressedMs = 0; 20 | this->longPressStartMs = 0; 21 | this->shortOrLongPressStartMs = 0; 22 | 23 | this->readOnceFlag = false; 24 | this->readLongPressOnceFlag = false; 25 | this->readShortOrLongPressOnceFlag = false; 26 | 27 | pinMode(this->pin, internalPullup ? INPUT_PULLUP : INPUT); 28 | 29 | } 30 | 31 | /** 32 | * Get the button state, TRUE if the pin is HIGH. 33 | * Immediately reads presses, but the release can be delayed according to debouncing. 34 | */ 35 | bool read() { 36 | 37 | bool reading = digitalRead(this->pin); 38 | if (this->invert) reading = !reading; 39 | 40 | if (reading) { 41 | 42 | // Pressed: return TRUE 43 | this->lastPressedMs = millis(); // Remember time for debouncing 44 | if (this->longPressStartMs == 0) this->longPressStartMs = millis(); // Start long press detection 45 | return true; 46 | 47 | } else { 48 | 49 | // Released: wait for debouncing and return FALSE 50 | if (this->lastPressedMs > 0) { 51 | if (millis() - this->lastPressedMs >= debounceDelayMs) { 52 | this->lastPressedMs = 0; // Reset remembered time 53 | this->longPressStartMs = 0; // Stop long press detection 54 | } else { 55 | return true; // Waiting for debouncing... 56 | } 57 | } 58 | 59 | } 60 | 61 | return false; 62 | 63 | } 64 | 65 | /** 66 | * Same as read(), but returns TRUE only once, until the button is released. 67 | */ 68 | bool readOnce() { 69 | if (this->read()) { 70 | if (!this->readOnceFlag) { 71 | this->readOnceFlag = true; 72 | return true; 73 | } 74 | } else { 75 | this->readOnceFlag = false; 76 | } 77 | return false; 78 | } 79 | 80 | /** 81 | * Detect button long press, TRUE if the pin was HIGH for longer than given duration. 82 | */ 83 | bool readLongPress(unsigned long durationMs) { 84 | if (this->read()) { 85 | if (millis() - this->longPressStartMs >= durationMs) { 86 | return true; 87 | } 88 | } 89 | return false; 90 | } 91 | 92 | /** 93 | * Same as readLongPress(), but returns TRUE only once, until the button is released. 94 | */ 95 | bool readLongPressOnce(unsigned long durationMs) { 96 | if (this->readLongPress(durationMs)) { 97 | if (!this->readLongPressOnceFlag) { 98 | this->readLongPressOnceFlag = true; 99 | return true; 100 | } 101 | } else { 102 | this->readLongPressOnceFlag = false; 103 | } 104 | return false; 105 | } 106 | 107 | /* 108 | * A combined readOnce() and readLongPressOnce() for a multi-purpose button. 109 | * Returns 1 when the button is released before specified duration (short press). 110 | * Returns 2 as soon as the button has been pressed for specified duration. 111 | * Returns 0 in subsequent calls, while idle or while being pressed. 112 | */ 113 | byte readShortOrLongPressOnce(unsigned long longPressDurationMs) { 114 | byte r = 0; 115 | if (this->read()) { 116 | if (!this->readShortOrLongPressOnceFlag) { 117 | if (this->shortOrLongPressStartMs == 0) { 118 | this->shortOrLongPressStartMs = millis(); 119 | } else { 120 | if (millis() - this->shortOrLongPressStartMs >= longPressDurationMs) { 121 | this->readShortOrLongPressOnceFlag = true; 122 | r = 2; 123 | } 124 | } 125 | } 126 | } else { 127 | if (!this->readShortOrLongPressOnceFlag) { 128 | if (this->shortOrLongPressStartMs != 0) { 129 | r = 1; 130 | } 131 | } 132 | this->shortOrLongPressStartMs = 0; 133 | this->readShortOrLongPressOnceFlag = false; 134 | } 135 | return r; 136 | } 137 | 138 | private: 139 | byte pin; 140 | unsigned int debounceDelayMs; 141 | bool invert; 142 | unsigned long lastPressedMs; 143 | unsigned long longPressStartMs; 144 | unsigned long shortOrLongPressStartMs; 145 | bool readOnceFlag; 146 | bool readLongPressOnceFlag; 147 | bool readShortOrLongPressOnceFlag; 148 | 149 | }; 150 | 151 | #endif 152 | -------------------------------------------------------------------------------- /clock-divider/lib/Led.cpp: -------------------------------------------------------------------------------- 1 | #ifndef Led_h 2 | #define Led_h 3 | 4 | #include "Arduino.h" 5 | 6 | class Led { 7 | 8 | public: 9 | 10 | /** 11 | * Setup the LED, specifying and optional minimum "on" duration for user visibility 12 | */ 13 | void init(byte pin, unsigned int minDurationMs = 0) { 14 | 15 | this->pin = pin; 16 | this->minDurationMs = minDurationMs; 17 | 18 | this->state = false; 19 | this->stateHardware = false; 20 | this->blinkMs = 0; 21 | this->lastOnMs = 0; 22 | 23 | pinMode(this->pin, OUTPUT); 24 | digitalWrite(this->pin, LOW); 25 | 26 | } 27 | 28 | /** 29 | * Turn the LED on or off. 30 | * If a minimum duration was set, it could not turn off if it was 31 | * on for too little, you need to call loop() to update the state. 32 | */ 33 | void set(bool state) { 34 | 35 | this->blinkMs = 0; // Stop blinking 36 | this->state = state; 37 | 38 | if (state) { 39 | 40 | // Remember last time it was requested to be on 41 | this->lastOnMs = millis(); 42 | 43 | // Turn on the LED if necessary 44 | if (!this->stateHardware) { 45 | this->stateHardware = true; 46 | digitalWrite(this->pin, HIGH); 47 | } 48 | 49 | } else { 50 | 51 | // Turn the LED off if necessary 52 | this->loop(); 53 | 54 | } 55 | 56 | } 57 | 58 | /** 59 | * Starts blinking with given period, until any other method is called. 60 | * Use duty to specify how long the LED will be on, and invert to flip the blinking phase. 61 | * This method can also be used make it fade, using a short period and duty to adjust brightness. 62 | * Make sure to call loop() to keep the LED blinking. 63 | */ 64 | void blink(unsigned int periodMs, float duty = 0.5, bool invert = false) { 65 | this->blinkMs = periodMs; 66 | this->blinkDuty = max(0, min(periodMs, duty * periodMs)); 67 | this->blinkStartedMs = millis() - (invert ? this->blinkDuty : 0); 68 | } 69 | 70 | /** 71 | * Turn the LED off if necessary, or keep it blinking. 72 | * Call this in the main loop. 73 | */ 74 | void loop() { 75 | 76 | if (this->blinkMs > 0) { 77 | 78 | unsigned long t = ((millis() - this->blinkStartedMs) % this->blinkMs); 79 | this->stateHardware = t < this->blinkDuty; 80 | digitalWrite(this->pin, this->stateHardware); 81 | 82 | } else { 83 | 84 | // Turn the LED off if necessary 85 | if (!this->state && this->stateHardware) { 86 | if (millis() - this->lastOnMs >= this->minDurationMs) { 87 | this->stateHardware = false; 88 | digitalWrite(this->pin, LOW); 89 | } 90 | } 91 | 92 | } 93 | 94 | } 95 | 96 | void on() { 97 | this->set(true); 98 | } 99 | 100 | void off() { 101 | this->set(false); 102 | } 103 | 104 | void toggle() { 105 | this->set(!state); 106 | } 107 | 108 | /** 109 | * Turn on the LED, then turn it off immediately. 110 | * A single impulse of light will be visible if LED's minDurationMs is long enough. 111 | */ 112 | void flash() { 113 | this->set(true); 114 | this->set(false); 115 | } 116 | 117 | private: 118 | byte pin; 119 | unsigned int minDurationMs; 120 | bool state; 121 | bool stateHardware; 122 | unsigned int blinkMs; 123 | unsigned long blinkStartedMs; 124 | unsigned int blinkDuty; 125 | unsigned long lastOnMs; 126 | 127 | }; 128 | 129 | #endif -------------------------------------------------------------------------------- /clock-divider/pcb/board-gerber.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/clock-divider/pcb/board-gerber.zip -------------------------------------------------------------------------------- /clock-divider/pcb/panel-decor.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 38 | 40 | 41 | 43 | image/svg+xml 44 | 46 | 47 | 48 | 49 | 50 | 55 | 60 | 65 | 68 | 75 | 82 | 89 | 96 | 103 | 110 | 117 | 124 | 131 | 138 | 145 | 152 | 159 | 166 | 173 | 180 | 187 | 194 | 201 | 208 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /clock-divider/pcb/panel-font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/clock-divider/pcb/panel-font.ttf -------------------------------------------------------------------------------- /clock-divider/pcb/panel-gerber.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/clock-divider/pcb/panel-gerber.zip -------------------------------------------------------------------------------- /clock-divider/pcb/panel.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 38 | 40 | 41 | 43 | image/svg+xml 44 | 46 | 47 | 48 | 49 | 50 | 55 | 66 | CLK÷ 81 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /clock-divider/pictures/IMG_20190107_215258.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/clock-divider/pictures/IMG_20190107_215258.jpg -------------------------------------------------------------------------------- /clock-divider/pictures/IMG_20190112_155043.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/clock-divider/pictures/IMG_20190112_155043.jpg -------------------------------------------------------------------------------- /clock-divider/pictures/IMG_20190112_163027.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/clock-divider/pictures/IMG_20190112_163027.jpg -------------------------------------------------------------------------------- /clock-divider/pictures/IMG_20190112_224337.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/clock-divider/pictures/IMG_20190112_224337.jpg -------------------------------------------------------------------------------- /clock-divider/pictures/IMG_20190306_214855.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/clock-divider/pictures/IMG_20190306_214855.jpg -------------------------------------------------------------------------------- /clock-divider/pictures/IMG_20190307_225219.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/clock-divider/pictures/IMG_20190307_225219.jpg -------------------------------------------------------------------------------- /clock-divider/pictures/IMG_20210702_130542.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/clock-divider/pictures/IMG_20210702_130542.jpg -------------------------------------------------------------------------------- /clock-divider/pictures/IMG_20210702_130610.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/clock-divider/pictures/IMG_20210702_130610.jpg -------------------------------------------------------------------------------- /clock-divider/pictures/IMG_20210702_130733_M.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/clock-divider/pictures/IMG_20210702_130733_M.jpg -------------------------------------------------------------------------------- /clock-divider/pictures/IMG_20210804_140742.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/clock-divider/pictures/IMG_20210804_140742.jpg -------------------------------------------------------------------------------- /clock-divider/schematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/clock-divider/schematic.png -------------------------------------------------------------------------------- /forks/3d/plate.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/forks/3d/plate.fcstd -------------------------------------------------------------------------------- /forks/3d/plate.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/forks/3d/plate.stl -------------------------------------------------------------------------------- /forks/README.md: -------------------------------------------------------------------------------- 1 | Forks 2 | ===== 3 | 4 | A DIY Arduino-powered clone of [Mutable Instruments Branches][5], with a couple additional features. 5 | 6 | **[Arduino code][1]** | **[BOM][2]** | [🛒 **Buy PCB and panel on Tindie**][3] | **[ModularGrid][6]** | [🗨️ **Mod Wiggler**][4] 7 | 8 | [1]: forks.ino 9 | [2]: forks-bom.csv 10 | [3]: https://www.tindie.com/products/joeseggiola/forks-a-diy-clone-of-mi-branches-pcb-panel/ 11 | [4]: https://www.modwiggler.com/forum/viewtopic.php?t=216665 12 | [5]: https://mutable-instruments.net/modules/branches/ 13 | [6]: https://www.modulargrid.net/e/joeseggiola-forks 14 | 15 | Features 16 | -------- 17 | 18 | - Two Bernoulli gates similar to the ones found in [Mutable Instruments Branches][5]: 19 | - the **input** signal (trigger or gate) is routed to either of two outputs; 20 | - the **knob** and **CV input** control the probability of routing the signal to either outputs. 21 | - Manual input button, it can be used as a manual trigger/gate generator. 22 | - Both toggle and latch modes are enabled with two independent dedicated switches: 23 | - in **toggle** mode, probability is used to decide if sending the signal to the same output as before, or the other; 24 | - in **latch** mode, an output stays high until the other output gets activated. 25 | - The second channel input is optionally normalized to the first one, using a jumper on the back. 26 | 27 | Schematic 28 | --------- 29 | 30 | Only one Bernoulli gate is laid out, the second is an exact copy. 31 | 32 | ![](schematic.png) 33 | 34 | Pictures 35 | -------- 36 | 37 | ### New [PCB](pcb/) build 38 | 39 | [🛒 **Buy PCB and panel on Tindie**][3] 40 | 41 | 42 | 43 | ### Old [3D-printed](3d/) build 44 | 45 | 46 | -------------------------------------------------------------------------------- /forks/forks-bom.csv: -------------------------------------------------------------------------------- 1 | Type,Description / Location,Name,Value,Qty,Notes / Link 2 | IC,Arduino Nano,,,1,https://store.arduino.cc/products/arduino-nano 3 | IC,CV opamp,TL062,,1,"Or TL082, or TL072" 4 | IC,5V reference,LM4040,5V,1,LM4040-5.0 in TO-92 package 5 | Capacitor,"CV inputs, ceramic",1n,1nF,2, 6 | Capacitor,"Bypass and pots, ceramic","BYP, 100n",100nF,4,Close values should work the same 7 | Capacitor,"Bypass, electrolytic",BYP,10µF,1,"Optional, close values should work the same" 8 | Diode,Gate inputs,1N4148,,2,"Or 1N914, or anything similar" 9 | Resistor,Gate inputs,,10kΩ,4,Close values should work the same 10 | Resistor,Gate inputs,,220kΩ,2,Any value down to 100kΩ should work the same 11 | Resistor,CV inputs,,33kΩ,2, 12 | Resistor,CV inputs,,100kΩ,4, 13 | Resistor,5V reference,,5.1kΩ,1,4.7kΩ is also perfectly fine 14 | Resistor,Outputs,,1kΩ,4, 15 | Resistor,LEDs resistors,,1kΩ,2,Adjust value depending on desired brightness 16 | Potentiometer,"Probability, linear",,10kΩ,2,"Alpha 9mm RD901F-40, Tayda: A-4728, remove anti-rotation tag" 17 | Transistor,Gate inputs,BC337,,2,Any similar NPN should work the same 18 | Button,Tactile switch,"BTN1,2",,2,Mouser: 612-TL1105SP-250 or 612-TL1105SP 19 | Button,Tactile switch cap,,,2,"Mouser: 612-1RWHT, 612-1R-LGR or any colour you like" 20 | Switch,Slide switch,"TG, LT",,4,"Tayda: A-5111, or Mouser: 118-5FS1S102M2QES" 21 | Connector,IDC 10 pin socket,,,1,"Eurorack power socket, or 2x5 male pin header" 22 | Connector,Jumper,NORM,,1,Jumper to enable normalization of channel #2 input to #1 23 | Connector,Pin header 1x3 male,NORM,,1,"For the jumper, you can break a longer one" 24 | Connector,Pin header 1x7 male,,,4,You can break a longer one 25 | Connector,Pin header 1x7 female,,,4,You can break a longer one 26 | Jack,"Inputs, CVs, outputs",PJ398SM,,8,https://www.thonk.co.uk/shop/thonkiconn/ 27 | LED,"Bi-color, 3mm","LED1,2",,2,"Common cathode, red and green, Mouser: 604-WP3VEGW" 28 | -------------------------------------------------------------------------------- /forks/forks.ino: -------------------------------------------------------------------------------- 1 | 2 | // CONFIGURATION ============================================================= 3 | 4 | const bool DEBUG = false; // FALSE to disable debug messages on serial port 5 | 6 | const int INPUTS[] { 2, 3 }; // Input, one for each Forks channel 7 | const int INPUT_BUTTONS[] { 4, 5 }; // Manual input buttons 8 | const int OUTPUTS_A[] { 6, 8 }; // First outputs 9 | const int OUTPUTS_B[] { 7, 9 }; // Second outputs 10 | 11 | const int PROBABILITY_KNOBS[] { A4, A5 }; // Probability knobs pins 12 | const int PROBABILITY_CV_INPUTS[] { A6, A7 }; // Probability CV 13 | 14 | const int MODE_TOGGLE_PINS[] { A0, A1 }; // Switch for enabling toggle mode 15 | const int MODE_LATCH_PINS[] { A2, A3 }; // Switch for enabling latch mode 16 | const unsigned long MODE_POLL_EVERY_MS = 100; // Check for mode switches periodically 17 | 18 | const int LEDS_A[] { 10, 12 }; // LED indicators for first outputs (red) 19 | const int LEDS_B[] { 11, 13 }; // LED indicators for second outputs (green) 20 | 21 | const int PROBABILITY_KNOBS_THRESHOLD_LOW = 0; // Everything read under this value in the 0-1023 scale is considered the minimum value 22 | const int PROBABILITY_KNOBS_THRESHOLD_HIGH = 1023; // Everything read over this value in the 0-1023 scale is considered the maximum value 23 | const int PROBABILITY_CV_INPUTS_THRESHOLD_LOW = 6; // Everything read under this value in the 0-1023 scale is considered the minimum value 24 | const int PROBABILITY_CV_INPUTS_THRESHOLD_HIGH = 670; // Everything read over this value in the 0-1023 scale is considered the maximum value 25 | 26 | const unsigned long BUTTON_DEBOUNCE_DELAY = 50; // Debounce delay for manual input buttons 27 | const unsigned long LED_MIN_DURATION_MS = 50; // Minimum "on" duration for all LEDs visibility 28 | 29 | // =========================================================================== 30 | 31 | #include "lib/Button.cpp" 32 | #include "lib/CV.cpp" 33 | #include "lib/Led.cpp" 34 | 35 | unsigned int n = 0; // Number of channels 36 | 37 | Button buttons[8]; 38 | CV knobs[8]; 39 | CV cvs[8]; 40 | Led ledsA[8]; // LEDs for outputs A 41 | Led ledsB[8]; // LEDs for outputs B 42 | 43 | volatile bool inputs[8]; // Input signal digital reading, set in ISR 44 | bool inputsLast[8]; // TRUE if input signal was high or manual button was pressed 45 | bool outcomeLast[8]; // Last outcome for toggle mode 46 | 47 | bool modeToggle[8]; // TRUE if toggle mode is enabled for the channel 48 | bool modeLatch[8]; // TRUE if latch mode is enabled for the channel 49 | unsigned long lastModePollMs = 0; // Modes switches are polled periodically 50 | 51 | void setup() { 52 | 53 | // Debugging 54 | if (DEBUG) { 55 | Serial.begin(9600); 56 | Serial.println("FORKS - joeSeggiola"); 57 | } 58 | 59 | // Number of divisions 60 | n = sizeof(INPUTS) / sizeof(INPUTS[0]); 61 | 62 | // Initialize state 63 | for (int i = 0; i < n; i++) { 64 | inputs[i] = false; 65 | inputsLast[i] = false; 66 | outcomeLast[i] = false; 67 | modeToggle[i] = false; 68 | modeLatch[i] = false; 69 | } 70 | 71 | // Setup I/O 72 | for (int i = 0; i < n; i++) { 73 | buttons[i].init(INPUT_BUTTONS[i], BUTTON_DEBOUNCE_DELAY, true, true); 74 | knobs[i].init(PROBABILITY_KNOBS[i], PROBABILITY_KNOBS_THRESHOLD_LOW, PROBABILITY_KNOBS_THRESHOLD_HIGH); 75 | cvs[i].init(PROBABILITY_CV_INPUTS[i], PROBABILITY_CV_INPUTS_THRESHOLD_LOW, PROBABILITY_CV_INPUTS_THRESHOLD_HIGH, true); 76 | ledsA[i].init(LEDS_A[i], LED_MIN_DURATION_MS); 77 | ledsB[i].init(LEDS_B[i], LED_MIN_DURATION_MS); 78 | pinMode(OUTPUTS_A[i], OUTPUT); 79 | pinMode(OUTPUTS_B[i], OUTPUT); 80 | digitalWrite(OUTPUTS_A[i], LOW); 81 | digitalWrite(OUTPUTS_B[i], LOW); 82 | pinMode(MODE_TOGGLE_PINS[i], INPUT_PULLUP); 83 | pinMode(MODE_LATCH_PINS[i], INPUT_PULLUP); 84 | } 85 | 86 | // Interrupts 87 | for (int i = 0; i < n; i++) { 88 | pinMode(INPUTS[i], INPUT); 89 | attachInterrupt(digitalPinToInterrupt(INPUTS[i]), isrInputs, CHANGE); 90 | } 91 | 92 | } 93 | 94 | void loop() { 95 | 96 | modePolling(); 97 | 98 | // For each channel 99 | for (int i = 0; i < n; i++) { 100 | 101 | // Channel input changed? 102 | bool input = inputs[i] || buttons[i].read(); // Current input 103 | if (input != inputsLast[i]) { 104 | inputsLast[i] = input; // Remember the new input 105 | 106 | if (input) { 107 | 108 | // Calculate probability combining knob and CV 109 | float probability; 110 | float probabilityKnob = knobs[i].read() * 1.1 - 0.05; // Let the knob push more toward the edges, to compensate for CV values near zero 111 | float probabilityCV = cvs[i].read() - 0.5; // Use CV as an offset 112 | probability = constrain(probabilityKnob + probabilityCV, 0.0, 1.0); 113 | 114 | // Flip coin 115 | randomSeed(micros()); // No unconnected analog pins available, seed with millis() 116 | bool outcome = random(0, 1024) >= (probability * 1024); // TRUE if random is bigger than probability factor 117 | 118 | // Toggle mode? 119 | if (modeToggle[i]) outcome = (outcome == outcomeLast[i]); 120 | outcomeLast[i] = outcome; 121 | 122 | // Turn on output and LED 123 | if (outcome) { 124 | digitalWrite(OUTPUTS_A[i], HIGH); 125 | ledsA[i].on(); 126 | } else { 127 | digitalWrite(OUTPUTS_B[i], HIGH); 128 | ledsB[i].on(); 129 | } 130 | 131 | // If in latch mode, turn off the other output and LED 132 | if (modeLatch[i]) { 133 | if (outcome) { 134 | digitalWrite(OUTPUTS_B[i], LOW); 135 | ledsB[i].off(); 136 | } else { 137 | digitalWrite(OUTPUTS_A[i], LOW); 138 | ledsA[i].off(); 139 | } 140 | } 141 | 142 | if (DEBUG) { 143 | Serial.print("CH"); 144 | Serial.print(i); 145 | Serial.print(" -> Gate on -> P: "); 146 | Serial.print(probability, 3); 147 | Serial.print(" (knob: "); 148 | Serial.print(probabilityKnob, 3); 149 | Serial.print(", CV: "); 150 | Serial.print(probabilityCV, 3); 151 | Serial.print(") -> Outcome: "); 152 | Serial.println(outcome ? 'A' : 'B'); 153 | } 154 | 155 | } else { 156 | 157 | // If not in latch mode, turn off all outputs and LEDs 158 | if (!modeLatch[i]) { 159 | digitalWrite(OUTPUTS_A[i], LOW); 160 | digitalWrite(OUTPUTS_B[i], LOW); 161 | ledsA[i].off(); 162 | ledsB[i].off(); 163 | } 164 | 165 | if (DEBUG) { 166 | Serial.print("CH"); 167 | Serial.print(i); 168 | Serial.println(" -> Gate off"); 169 | } 170 | 171 | } 172 | 173 | } 174 | 175 | // Update LEDs 176 | ledsA[i].loop(); 177 | ledsB[i].loop(); 178 | 179 | } 180 | 181 | } 182 | 183 | void isrInputs() { 184 | 185 | // Check each channel 186 | for (int i = 0; i < n; i++) { 187 | inputs[i] = (digitalRead(INPUTS[i]) == HIGH); 188 | } 189 | 190 | } 191 | 192 | void modePolling() { 193 | 194 | // Poll modes switches 195 | unsigned long ms = millis(); 196 | if (lastModePollMs == 0 || ms >= (lastModePollMs + MODE_POLL_EVERY_MS)) { 197 | lastModePollMs = ms > 0 ? ms : 1; // Avoid to re-set zero 198 | 199 | for (int i = 0; i < n; i++) { 200 | modeToggle[i] = (digitalRead(MODE_TOGGLE_PINS[i]) == HIGH); 201 | modeLatch[i] = (digitalRead(MODE_LATCH_PINS[i]) == HIGH); 202 | } 203 | 204 | } 205 | 206 | } 207 | -------------------------------------------------------------------------------- /forks/lib/Button.cpp: -------------------------------------------------------------------------------- 1 | #ifndef Button_h 2 | #define Button_h 3 | 4 | #include "Arduino.h" 5 | 6 | class Button { 7 | 8 | public: 9 | 10 | /** 11 | * Setup the button, specifying and optional debounce delay 12 | */ 13 | void init(byte pin, unsigned int debounceDelayMs = 0, bool invert = false, bool internalPullup = false) { 14 | 15 | this->pin = pin; 16 | this->debounceDelayMs = debounceDelayMs; 17 | this->invert = invert; 18 | 19 | this->lastPressedMs = 0; 20 | this->longPressStartMs = 0; 21 | this->shortOrLongPressStartMs = 0; 22 | 23 | this->readOnceFlag = false; 24 | this->readLongPressOnceFlag = false; 25 | this->readShortOrLongPressOnceFlag = false; 26 | 27 | pinMode(this->pin, internalPullup ? INPUT_PULLUP : INPUT); 28 | 29 | } 30 | 31 | /** 32 | * Get the button state, TRUE if the pin is HIGH. 33 | * Immediately reads presses, but the release can be delayed according to debouncing. 34 | */ 35 | bool read() { 36 | 37 | bool reading = digitalRead(this->pin); 38 | if (this->invert) reading = !reading; 39 | 40 | if (reading) { 41 | 42 | // Pressed: return TRUE 43 | this->lastPressedMs = millis(); // Remember time for debouncing 44 | if (this->longPressStartMs == 0) this->longPressStartMs = millis(); // Start long press detection 45 | return true; 46 | 47 | } else { 48 | 49 | // Released: wait for debouncing and return FALSE 50 | if (this->lastPressedMs > 0) { 51 | if (millis() - this->lastPressedMs >= debounceDelayMs) { 52 | this->lastPressedMs = 0; // Reset remembered time 53 | this->longPressStartMs = 0; // Stop long press detection 54 | } else { 55 | return true; // Waiting for debouncing... 56 | } 57 | } 58 | 59 | } 60 | 61 | return false; 62 | 63 | } 64 | 65 | /** 66 | * Same as read(), but returns TRUE only once, until the button is released. 67 | */ 68 | bool readOnce() { 69 | if (this->read()) { 70 | if (!this->readOnceFlag) { 71 | this->readOnceFlag = true; 72 | return true; 73 | } 74 | } else { 75 | this->readOnceFlag = false; 76 | } 77 | return false; 78 | } 79 | 80 | /** 81 | * Detect button long press, TRUE if the pin was HIGH for longer than given duration. 82 | */ 83 | bool readLongPress(unsigned long durationMs) { 84 | if (this->read()) { 85 | if (millis() - this->longPressStartMs >= durationMs) { 86 | return true; 87 | } 88 | } 89 | return false; 90 | } 91 | 92 | /** 93 | * Same as readLongPress(), but returns TRUE only once, until the button is released. 94 | */ 95 | bool readLongPressOnce(unsigned long durationMs) { 96 | if (this->readLongPress(durationMs)) { 97 | if (!this->readLongPressOnceFlag) { 98 | this->readLongPressOnceFlag = true; 99 | return true; 100 | } 101 | } else { 102 | this->readLongPressOnceFlag = false; 103 | } 104 | return false; 105 | } 106 | 107 | /* 108 | * A combined readOnce() and readLongPressOnce() for a multi-purpose button. 109 | * Returns 1 when the button is released before specified duration (short press). 110 | * Returns 2 as soon as the button has been pressed for specified duration. 111 | * Returns 0 in subsequent calls, while idle or while being pressed. 112 | */ 113 | byte readShortOrLongPressOnce(unsigned long longPressDurationMs) { 114 | byte r = 0; 115 | if (this->read()) { 116 | if (!this->readShortOrLongPressOnceFlag) { 117 | if (this->shortOrLongPressStartMs == 0) { 118 | this->shortOrLongPressStartMs = millis(); 119 | } else { 120 | if (millis() - this->shortOrLongPressStartMs >= longPressDurationMs) { 121 | this->readShortOrLongPressOnceFlag = true; 122 | r = 2; 123 | } 124 | } 125 | } 126 | } else { 127 | if (!this->readShortOrLongPressOnceFlag) { 128 | if (this->shortOrLongPressStartMs != 0) { 129 | r = 1; 130 | } 131 | } 132 | this->shortOrLongPressStartMs = 0; 133 | this->readShortOrLongPressOnceFlag = false; 134 | } 135 | return r; 136 | } 137 | 138 | private: 139 | byte pin; 140 | unsigned int debounceDelayMs; 141 | bool invert; 142 | unsigned long lastPressedMs; 143 | unsigned long longPressStartMs; 144 | unsigned long shortOrLongPressStartMs; 145 | bool readOnceFlag; 146 | bool readLongPressOnceFlag; 147 | bool readShortOrLongPressOnceFlag; 148 | 149 | }; 150 | 151 | #endif 152 | -------------------------------------------------------------------------------- /forks/lib/CV.cpp: -------------------------------------------------------------------------------- 1 | #ifndef CV_h 2 | #define CV_h 3 | 4 | #include "Arduino.h" 5 | 6 | class CV { 7 | 8 | public: 9 | 10 | /** 11 | * Setup the analog input reader (CV input or knob), specifying optional thresholds 12 | */ 13 | void init(byte pin, int thresholdLow = 0, int thresholdHigh = 1023, bool invert = false) { 14 | 15 | this->pin = pin; 16 | 17 | this->thresholdLow = thresholdLow; 18 | this->thresholdHigh = thresholdHigh; 19 | this->invert = invert; 20 | 21 | } 22 | 23 | /** 24 | * Return the raw reading, as returned by analogRead() 25 | */ 26 | int readRaw() { 27 | return analogRead(this->pin); 28 | } 29 | 30 | /** 31 | * Return the reading as a float number between 0 and 1, included. 32 | * Optional thresholds are used to map the raw values into the returned 0..1 range. 33 | */ 34 | float read() { 35 | 36 | int r = this->readRaw(); 37 | float f; 38 | 39 | if (r <= this->thresholdLow) { 40 | f = 0.0; 41 | } else if (r >= this->thresholdHigh) { 42 | f = 1.0; 43 | } else { 44 | f = float(r - this->thresholdLow) / float(this->thresholdHigh - this->thresholdLow); 45 | } 46 | 47 | if (this->invert) { 48 | return 1.0 - f; 49 | } else { 50 | return f; 51 | } 52 | 53 | } 54 | 55 | private: 56 | byte pin; 57 | int thresholdLow; 58 | int thresholdHigh; 59 | bool invert; 60 | 61 | }; 62 | 63 | #endif -------------------------------------------------------------------------------- /forks/lib/Led.cpp: -------------------------------------------------------------------------------- 1 | #ifndef Led_h 2 | #define Led_h 3 | 4 | #include "Arduino.h" 5 | 6 | class Led { 7 | 8 | public: 9 | 10 | /** 11 | * Setup the LED, specifying and optional minimum "on" duration for user visibility 12 | */ 13 | void init(byte pin, unsigned int minDurationMs = 0) { 14 | 15 | this->pin = pin; 16 | this->minDurationMs = minDurationMs; 17 | 18 | this->state = false; 19 | this->stateHardware = false; 20 | this->blinkMs = 0; 21 | this->lastOnMs = 0; 22 | 23 | pinMode(this->pin, OUTPUT); 24 | digitalWrite(this->pin, LOW); 25 | 26 | } 27 | 28 | /** 29 | * Turn the LED on or off. 30 | * If a minimum duration was set, it could not turn off if it was 31 | * on for too little, you need to call loop() to update the state. 32 | */ 33 | void set(bool state) { 34 | 35 | this->blinkMs = 0; // Stop blinking 36 | this->state = state; 37 | 38 | if (state) { 39 | 40 | // Remember last time it was requested to be on 41 | this->lastOnMs = millis(); 42 | 43 | // Turn on the LED if necessary 44 | if (!this->stateHardware) { 45 | this->stateHardware = true; 46 | digitalWrite(this->pin, HIGH); 47 | } 48 | 49 | } else { 50 | 51 | // Turn the LED off if necessary 52 | this->loop(); 53 | 54 | } 55 | 56 | } 57 | 58 | /** 59 | * Starts blinking with given period, until any other method is called. 60 | * Use duty to specify how long the LED will be on, and invert to flip the blinking phase. 61 | * This method can also be used make it fade, using a short period and duty to adjust brightness. 62 | * Make sure to call loop() to keep the LED blinking. 63 | */ 64 | void blink(unsigned int periodMs, float duty = 0.5, bool invert = false) { 65 | this->blinkMs = periodMs; 66 | this->blinkDuty = max(0, min(periodMs, duty * periodMs)); 67 | this->blinkStartedMs = millis() - (invert ? this->blinkDuty : 0); 68 | } 69 | 70 | /** 71 | * Turn the LED off if necessary, or keep it blinking. 72 | * Call this in the main loop. 73 | */ 74 | void loop() { 75 | 76 | if (this->blinkMs > 0) { 77 | 78 | unsigned long t = ((millis() - this->blinkStartedMs) % this->blinkMs); 79 | this->stateHardware = t < this->blinkDuty; 80 | digitalWrite(this->pin, this->stateHardware); 81 | 82 | } else { 83 | 84 | // Turn the LED off if necessary 85 | if (!this->state && this->stateHardware) { 86 | if (millis() - this->lastOnMs >= this->minDurationMs) { 87 | this->stateHardware = false; 88 | digitalWrite(this->pin, LOW); 89 | } 90 | } 91 | 92 | } 93 | 94 | } 95 | 96 | void on() { 97 | this->set(true); 98 | } 99 | 100 | void off() { 101 | this->set(false); 102 | } 103 | 104 | void toggle() { 105 | this->set(!state); 106 | } 107 | 108 | /** 109 | * Turn on the LED, then turn it off immediately. 110 | * A single impulse of light will be visible if LED's minDurationMs is long enough. 111 | */ 112 | void flash() { 113 | this->set(true); 114 | this->set(false); 115 | } 116 | 117 | private: 118 | byte pin; 119 | unsigned int minDurationMs; 120 | bool state; 121 | bool stateHardware; 122 | unsigned int blinkMs; 123 | unsigned long blinkStartedMs; 124 | unsigned int blinkDuty; 125 | unsigned long lastOnMs; 126 | 127 | }; 128 | 129 | #endif -------------------------------------------------------------------------------- /forks/pcb/board-gerber.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/forks/pcb/board-gerber.zip -------------------------------------------------------------------------------- /forks/pcb/panel-font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/forks/pcb/panel-font.ttf -------------------------------------------------------------------------------- /forks/pcb/panel-gerber.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/forks/pcb/panel-gerber.zip -------------------------------------------------------------------------------- /forks/pcb/panel.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 65 | FORKS 78 | 81 | 83 | 96 | 109 | 116 | 123 | 130 | 137 | 138 | 139 | 144 | 148 | 153 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /forks/pictures/IMG_20190120_210330.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/forks/pictures/IMG_20190120_210330.jpg -------------------------------------------------------------------------------- /forks/pictures/IMG_20190426_182714.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/forks/pictures/IMG_20190426_182714.jpg -------------------------------------------------------------------------------- /forks/pictures/IMG_20190427_112937.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/forks/pictures/IMG_20190427_112937.jpg -------------------------------------------------------------------------------- /forks/pictures/IMG_20190427_113603.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/forks/pictures/IMG_20190427_113603.jpg -------------------------------------------------------------------------------- /forks/pictures/IMG_20211223_133624.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/forks/pictures/IMG_20211223_133624.jpg -------------------------------------------------------------------------------- /forks/pictures/IMG_20211223_133624_T.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/forks/pictures/IMG_20211223_133624_T.jpg -------------------------------------------------------------------------------- /forks/pictures/IMG_20211223_135158_M.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/forks/pictures/IMG_20211223_135158_M.jpg -------------------------------------------------------------------------------- /forks/pictures/IMG_20211223_135315.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/forks/pictures/IMG_20211223_135315.jpg -------------------------------------------------------------------------------- /forks/pictures/IMG_20211228_124830.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/forks/pictures/IMG_20211228_124830.jpg -------------------------------------------------------------------------------- /forks/schematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/forks/schematic.png -------------------------------------------------------------------------------- /in-cv/3d/plate.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/in-cv/3d/plate.fcstd -------------------------------------------------------------------------------- /in-cv/3d/plate.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/in-cv/3d/plate.stl -------------------------------------------------------------------------------- /in-cv/README.md: -------------------------------------------------------------------------------- 1 | In CV 2 | ===== 3 | 4 | A DIY Arduino-powered virtual ensemble that plays Terry Riley's "In C" on CV/gate outputs. 5 | 6 | **[Arduino code][1]** | [🛒 **Buy on Tindie**][6] | [🛒 **Buy on Reverb**][5] | **[ModularGrid][7]** | **[YouTube demo][3]** | [🗨️ **Mod Wiggler**][4] 7 | 8 | [1]: in-cv.ino 9 | [3]: https://youtu.be/ea2zLXFY1C4 10 | [4]: https://modwiggler.com/forum/viewtopic.php?t=227451 11 | [5]: https://reverb.com/item/39927694-in-cv-terry-riley-s-in-c-implementation-for-eurorack 12 | [6]: https://www.tindie.com/products/joeseggiola/in-cv-terry-rileys-in-c-for-eurorack/ 13 | [7]: https://www.modulargrid.net/e/joeseggiola-in-cv 14 | 15 | Features 16 | -------- 17 | 18 | * Six performers that play "In C" patterns on 1V/oct CV/gate outputs. 19 | * Each performer has a button to advance through the 53 patterns, or to pause at the end of the current loop (long-press). 20 | * LEDs show when a note is played, but will start blinking if the performer is left behind by three or more patterns. 21 | * External clock input to control playback speed. 22 | * Main button to show late performers, or to reset everything to initial state (long-press). 23 | * In the initial state every performer plays a steady C with no gate (for tuning), until the advance button is pressed and the first pattern starts being played. 24 | 25 | ### Patterns data generation 26 | 27 | Arduino code reads the definitions of the 53 patterns of "In C" from [`patterns/patterns.h`][20]. This file is not hand-written, but automatically generated using [`patterns/cli.js`][21], a Node.js command line script. This script translates music notation contained in [`patterns/patterns.txt`][22] into performant and memory-efficient data. 28 | 29 | That said, you can write your own patterns and sequences to make the module play just about anything. Edit the TXT file (one sequence per line), install [Node.js][23] and then run this in the `patterns/` folder: 30 | 31 | npm install 32 | npm start -- patterns.txt 33 | 34 | [20]: patterns/patterns.h 35 | [21]: patterns/cli.js 36 | [22]: patterns/patterns.txt 37 | [23]: https://nodejs.org/en/ 38 | 39 | ### Calibration procedure 40 | 41 | Power on your modular system with the reset button pressed: the reset LED lights up steadily to show you entered the DACs calibration procedure. Performers LEDs show which CV output is currently being calibrated. The procedure requires measuring the output voltage using a multimeter with mV precision (0.001V). 42 | 43 | The first CV output should be around 0.5V: use the first performer button to decrease the measured value, or the second one to increase it, until you get exactly 0.500V. Now press the reset button to advance to the next calibration point, that is 1.000V, and adjust the measured value again. There are 8 calibration points for each CV output, after which the calibration process is repeated for the other CV outputs, as shown by performers LEDs. 44 | 45 | At the end of the whole procedure, the module will reboot itself and all CV outputs will track 1V/oct accurately. Calibration data is stored in Arduino EEPROM memory, so it's persisted across reboots and won't be lost by uploading new patterns. 46 | 47 | Schematic 48 | --------- 49 | 50 | Both DACs are on the same I2C bus, so you need to [set their device IDs][30] to `0` and `1` to address them individually. This is not possible once the circuit is soldered on the PCB. 51 | 52 | ![](schematic.png) 53 | 54 | [30]: ../tools/mcp4728_addr/mcp4728_addr.ino 55 | 56 | Pictures 57 | -------- 58 | 59 | ### New [PCB](pcb/) build 60 | 61 | [🛒 **Buy on Tindie**][6] | [🛒 **Buy on Reverb**][5] 62 | 63 | 64 | 65 | ### Old [3D-printed](3d/) build 66 | 67 | 68 | 69 | Performance tips and tricks 70 | --------------------------- 71 | 72 | * Always **tune all your voices** before each performance. In the initial state, the module plays a steady C note on all performers (with no gate): use that make sure every oscillator you're going to use is correctly tuned to C in whatever octave you prefer - or a different note if you want to play it trasposed. 73 | 74 | * You **don't have to use all six performers:** you can simply never activate some by never pressing their buttons. They will never start playing the first pattern, and they won't be considered being late. Use this to your advantage if you have a small system with only a few voices. 75 | 76 | * Don't be afraid to often completely pause one or more performers! [In C directions](https://teropa.info/blog/2017/01/23/terry-rileys-in-c.html#a-musical-possibility-spacethe-open-architecture-of-in-c) say "it is very important that performers listen very carefully to one another and this means occasionally to **drop out** and listen". You can pause a performer by long-pressing its button: the LED will flash waiting for the end of the loop, and then the performer will rest until the button is pressed again. This means that resuming can also be used to change the "phase" of the pattern relative to the others. 77 | 78 | * Although the directions do not say it, the performers on the module can **skip patterns**! You have to quickly press the button multiple times before the loop ends. You normally press it once, and the performer advances to the next pattern. If you press it twice it'll skip one pattern (eg. when it reaches the end of #12 it'll start playing #14), three times and it'll skip two. 79 | 80 | * Often use the main button to **display late performers**. When using this function, the LEDs stop blinking notes, and light up for 2 seconds if their performer is 3 or more patterns behind. Please be aware that if you keep the main button pressed for 4 seconds, the module will reset! Directions say to "stay within 2 or 3 patterns of each other". 81 | 82 | Thanks 83 | ------ 84 | 85 | - [Tero Parviainen][10]'s [analysis and JavaScript implementation][11] of "In C" 86 | 87 | [10]: https://teropa.info/ 88 | [11]: https://teropa.info/blog/2017/01/23/terry-rileys-in-c.html 89 | -------------------------------------------------------------------------------- /in-cv/lib/Button.cpp: -------------------------------------------------------------------------------- 1 | #ifndef Button_h 2 | #define Button_h 3 | 4 | #include "Arduino.h" 5 | 6 | class Button { 7 | 8 | public: 9 | 10 | /** 11 | * Setup the button, specifying and optional debounce delay 12 | */ 13 | void init(byte pin, unsigned int debounceDelayMs = 0, bool invert = false, bool internalPullup = false) { 14 | 15 | this->pin = pin; 16 | this->debounceDelayMs = debounceDelayMs; 17 | this->invert = invert; 18 | 19 | this->lastPressedMs = 0; 20 | this->longPressStartMs = 0; 21 | this->shortOrLongPressStartMs = 0; 22 | 23 | this->readOnceFlag = false; 24 | this->readLongPressOnceFlag = false; 25 | this->readShortOrLongPressOnceFlag = false; 26 | 27 | pinMode(this->pin, internalPullup ? INPUT_PULLUP : INPUT); 28 | 29 | } 30 | 31 | /** 32 | * Get the button state, TRUE if the pin is HIGH. 33 | * Immediately reads presses, but the release can be delayed according to debouncing. 34 | */ 35 | bool read() { 36 | 37 | bool reading = digitalRead(this->pin); 38 | if (this->invert) reading = !reading; 39 | 40 | if (reading) { 41 | 42 | // Pressed: return TRUE 43 | this->lastPressedMs = millis(); // Remember time for debouncing 44 | if (this->longPressStartMs == 0) this->longPressStartMs = millis(); // Start long press detection 45 | return true; 46 | 47 | } else { 48 | 49 | // Released: wait for debouncing and return FALSE 50 | if (this->lastPressedMs > 0) { 51 | if (millis() - this->lastPressedMs >= debounceDelayMs) { 52 | this->lastPressedMs = 0; // Reset remembered time 53 | this->longPressStartMs = 0; // Stop long press detection 54 | } else { 55 | return true; // Waiting for debouncing... 56 | } 57 | } 58 | 59 | } 60 | 61 | return false; 62 | 63 | } 64 | 65 | /** 66 | * Same as read(), but returns TRUE only once, until the button is released. 67 | */ 68 | bool readOnce() { 69 | if (this->read()) { 70 | if (!this->readOnceFlag) { 71 | this->readOnceFlag = true; 72 | return true; 73 | } 74 | } else { 75 | this->readOnceFlag = false; 76 | } 77 | return false; 78 | } 79 | 80 | /** 81 | * Detect button long press, TRUE if the pin was HIGH for longer than given duration. 82 | */ 83 | bool readLongPress(unsigned long durationMs) { 84 | if (this->read()) { 85 | if (millis() - this->longPressStartMs >= durationMs) { 86 | return true; 87 | } 88 | } 89 | return false; 90 | } 91 | 92 | /** 93 | * Same as readLongPress(), but returns TRUE only once, until the button is released. 94 | */ 95 | bool readLongPressOnce(unsigned long durationMs) { 96 | if (this->readLongPress(durationMs)) { 97 | if (!this->readLongPressOnceFlag) { 98 | this->readLongPressOnceFlag = true; 99 | return true; 100 | } 101 | } else { 102 | this->readLongPressOnceFlag = false; 103 | } 104 | return false; 105 | } 106 | 107 | /* 108 | * A combined readOnce() and readLongPressOnce() for a multi-purpose button. 109 | * Returns 1 when the button is released before specified duration (short press). 110 | * Returns 2 as soon as the button has been pressed for specified duration. 111 | * Returns 0 in subsequent calls, while idle or while being pressed. 112 | */ 113 | byte readShortOrLongPressOnce(unsigned long longPressDurationMs) { 114 | byte r = 0; 115 | if (this->read()) { 116 | if (!this->readShortOrLongPressOnceFlag) { 117 | if (this->shortOrLongPressStartMs == 0) { 118 | this->shortOrLongPressStartMs = millis(); 119 | } else { 120 | if (millis() - this->shortOrLongPressStartMs >= longPressDurationMs) { 121 | this->readShortOrLongPressOnceFlag = true; 122 | r = 2; 123 | } 124 | } 125 | } 126 | } else { 127 | if (!this->readShortOrLongPressOnceFlag) { 128 | if (this->shortOrLongPressStartMs != 0) { 129 | r = 1; 130 | } 131 | } 132 | this->shortOrLongPressStartMs = 0; 133 | this->readShortOrLongPressOnceFlag = false; 134 | } 135 | return r; 136 | } 137 | 138 | private: 139 | byte pin; 140 | unsigned int debounceDelayMs; 141 | bool invert; 142 | unsigned long lastPressedMs; 143 | unsigned long longPressStartMs; 144 | unsigned long shortOrLongPressStartMs; 145 | bool readOnceFlag; 146 | bool readLongPressOnceFlag; 147 | bool readShortOrLongPressOnceFlag; 148 | 149 | }; 150 | 151 | #endif 152 | -------------------------------------------------------------------------------- /in-cv/lib/Led.cpp: -------------------------------------------------------------------------------- 1 | #ifndef Led_h 2 | #define Led_h 3 | 4 | #include "Arduino.h" 5 | 6 | class Led { 7 | 8 | public: 9 | 10 | /** 11 | * Setup the LED, specifying and optional minimum "on" duration for user visibility 12 | */ 13 | void init(byte pin, unsigned int minDurationMs = 0) { 14 | 15 | this->pin = pin; 16 | this->minDurationMs = minDurationMs; 17 | 18 | this->state = false; 19 | this->stateHardware = false; 20 | this->blinkMs = 0; 21 | this->lastOnMs = 0; 22 | 23 | pinMode(this->pin, OUTPUT); 24 | digitalWrite(this->pin, LOW); 25 | 26 | } 27 | 28 | /** 29 | * Turn the LED on or off. 30 | * If a minimum duration was set, it could not turn off if it was 31 | * on for too little, you need to call loop() to update the state. 32 | */ 33 | void set(bool state) { 34 | 35 | this->blinkMs = 0; // Stop blinking 36 | this->state = state; 37 | 38 | if (state) { 39 | 40 | // Remember last time it was requested to be on 41 | this->lastOnMs = millis(); 42 | 43 | // Turn on the LED if necessary 44 | if (!this->stateHardware) { 45 | this->stateHardware = true; 46 | digitalWrite(this->pin, HIGH); 47 | } 48 | 49 | } else { 50 | 51 | // Turn the LED off if necessary 52 | this->loop(); 53 | 54 | } 55 | 56 | } 57 | 58 | /** 59 | * Starts blinking with given period, until any other method is called. 60 | * Use duty to specify how long the LED will be on, and invert to flip the blinking phase. 61 | * This method can also be used make it fade, using a short period and duty to adjust brightness. 62 | * Make sure to call loop() to keep the LED blinking. 63 | */ 64 | void blink(unsigned int periodMs, float duty = 0.5, bool invert = false) { 65 | this->blinkMs = periodMs; 66 | this->blinkDuty = max(0, min(periodMs, duty * periodMs)); 67 | this->blinkStartedMs = millis() - (invert ? this->blinkDuty : 0); 68 | } 69 | 70 | /** 71 | * Turn the LED off if necessary, or keep it blinking. 72 | * Call this in the main loop. 73 | */ 74 | void loop() { 75 | 76 | if (this->blinkMs > 0) { 77 | 78 | unsigned long t = ((millis() - this->blinkStartedMs) % this->blinkMs); 79 | this->stateHardware = t < this->blinkDuty; 80 | digitalWrite(this->pin, this->stateHardware); 81 | 82 | } else { 83 | 84 | // Turn the LED off if necessary 85 | if (!this->state && this->stateHardware) { 86 | if (millis() - this->lastOnMs >= this->minDurationMs) { 87 | this->stateHardware = false; 88 | digitalWrite(this->pin, LOW); 89 | } 90 | } 91 | 92 | } 93 | 94 | } 95 | 96 | void on() { 97 | this->set(true); 98 | } 99 | 100 | void off() { 101 | this->set(false); 102 | } 103 | 104 | void toggle() { 105 | this->set(!state); 106 | } 107 | 108 | /** 109 | * Turn on the LED, then turn it off immediately. 110 | * A single impulse of light will be visible if LED's minDurationMs is long enough. 111 | */ 112 | void flash() { 113 | this->set(true); 114 | this->set(false); 115 | } 116 | 117 | /** 118 | * Set the optional minimum "on" duration for user visibility 119 | */ 120 | void setMinDurationMs(unsigned int minDurationMs = 0) { 121 | this->minDurationMs = minDurationMs; 122 | this->loop(); 123 | } 124 | 125 | private: 126 | byte pin; 127 | unsigned int minDurationMs; 128 | bool state; 129 | bool stateHardware; 130 | unsigned int blinkMs; 131 | unsigned long blinkStartedMs; 132 | unsigned int blinkDuty; 133 | unsigned long lastOnMs; 134 | 135 | }; 136 | 137 | #endif 138 | -------------------------------------------------------------------------------- /in-cv/lib/MCP4728.cpp: -------------------------------------------------------------------------------- 1 | 2 | // LIBRARY FOR MCP4728 3 | // Link: https://github.com/hideakitai/MCP4728 4 | // Author: Hideaki Tai 5 | // License: MIT (https://github.com/hideakitai/MCP4728/blob/master/LICENSE) 6 | // Extended by Joe Seggiola to include optional LDAC 7 | 8 | #pragma once 9 | #ifndef MCP4728_H 10 | #define MCP4728_H 11 | 12 | #include "Arduino.h" 13 | #include 14 | 15 | class MCP4728 { 16 | 17 | public: 18 | 19 | enum class CMD { 20 | FAST_WRITE = 0x00, 21 | MULTI_WRITE = 0x40, 22 | SINGLE_WRITE = 0x58, 23 | SEQ_WRITE = 0x50, 24 | SELECT_VREF = 0x80, 25 | SELECT_GAIN = 0xC0, 26 | SELECT_PWRDOWN = 0xA0 27 | }; 28 | 29 | enum class VREF { VDD, INTERNAL_2_8V }; 30 | enum class PWR_DOWN { NORMAL, GND_1KOHM, GND_100KOHM, GND_500KOHM }; 31 | enum class GAIN { X1, X2 }; 32 | 33 | void init(TwoWire& w, uint8_t addr = 0, int8_t pin = -1) { 34 | wire_ = &w; 35 | addr_ = I2C_ADDR + addr; 36 | pin_ldac_ = pin; 37 | if (pin_ldac_ > -1) { 38 | pinMode(pin_ldac_, OUTPUT); 39 | enable(false); 40 | } 41 | readRegisters(); 42 | } 43 | 44 | void enable(bool b) { 45 | if (pin_ldac_ > -1) { 46 | digitalWrite(pin_ldac_, !b); 47 | } 48 | } 49 | 50 | uint8_t analogWrite(uint8_t ch, uint16_t data, bool b_eep = false) { 51 | if (b_eep) { 52 | eep_[ch].data = data > 0xFFF ? 0xFFF : data; 53 | return singleWrite(ch); 54 | } else { 55 | reg_[ch].data = data > 0xFFF ? 0xFFF : data; 56 | return fastWrite(); 57 | } 58 | } 59 | 60 | uint8_t analogWrite(uint16_t a, uint16_t b, uint16_t c, uint16_t d, bool b_eep = false) { 61 | if (b_eep) { 62 | reg_[0].data = eep_[0].data = a > 0xFFF ? 0xFFF : a; 63 | reg_[1].data = eep_[1].data = b > 0xFFF ? 0xFFF : b; 64 | reg_[2].data = eep_[2].data = c > 0xFFF ? 0xFFF : c; 65 | reg_[3].data = eep_[3].data = d > 0xFFF ? 0xFFF : d; 66 | return seqWrite(); 67 | } else { 68 | reg_[0].data = a > 0xFFF ? 0xFFF : a; 69 | reg_[1].data = b > 0xFFF ? 0xFFF : b; 70 | reg_[2].data = c > 0xFFF ? 0xFFF : c; 71 | reg_[3].data = d > 0xFFF ? 0xFFF : d; 72 | return fastWrite(); 73 | } 74 | } 75 | 76 | uint8_t selectVref(VREF a, VREF b, VREF c, VREF d) { 77 | reg_[0].vref = a; 78 | reg_[1].vref = b; 79 | reg_[2].vref = c; 80 | reg_[3].vref = d; 81 | uint8_t data = (uint8_t)CMD::SELECT_VREF; 82 | for (uint8_t i = 0; i < 4; ++i) bitWrite(data, 3 - i, (uint8_t)reg_[i].vref); 83 | wire_->beginTransmission(addr_); 84 | wire_->write(data); 85 | return wire_->endTransmission(); 86 | } 87 | 88 | uint8_t selectPowerDown(PWR_DOWN a, PWR_DOWN b, PWR_DOWN c, PWR_DOWN d) { 89 | reg_[0].pd = a; 90 | reg_[1].pd = b; 91 | reg_[2].pd = c; 92 | reg_[3].pd = d; 93 | uint8_t h = ((uint8_t)CMD::SELECT_PWRDOWN) | ((uint8_t)a << 2) | (uint8_t)b; 94 | uint8_t l = 0 | ((uint8_t)c << 6) | ((uint8_t)d << 4); 95 | wire_->beginTransmission(addr_); 96 | wire_->write(h); 97 | wire_->write(l); 98 | return wire_->endTransmission(); 99 | } 100 | 101 | uint8_t selectGain(GAIN a, GAIN b, GAIN c, GAIN d) { 102 | reg_[0].gain = a; 103 | reg_[1].gain = b; 104 | reg_[2].gain = c; 105 | reg_[3].gain = d; 106 | uint8_t data = (uint8_t)CMD::SELECT_GAIN; 107 | for (uint8_t i = 0; i < 4; ++i) bitWrite(data, 3 - i, (uint8_t)reg_[i].gain); 108 | wire_->beginTransmission(addr_); 109 | wire_->write(data); 110 | return wire_->endTransmission(); 111 | } 112 | 113 | void readRegisters() { 114 | wire_->requestFrom((int)addr_, 24); 115 | if (wire_->available() == 24) { 116 | for (uint8_t i = 0; i < 8; ++i) { 117 | uint8_t data[3]; 118 | bool isEeprom = i % 2; 119 | for (uint8_t i = 0; i < 3; ++i) data[i] = wire_->read(); 120 | uint8_t ch = (data[0] & 0x30) >> 4; 121 | if (isEeprom) { 122 | read_eep_[ch].vref = (VREF) ((data[1] & 0b10000000) >> 7); 123 | read_eep_[ch].pd = (PWR_DOWN)((data[1] & 0b01100000) >> 5); 124 | read_eep_[ch].gain = (GAIN) ((data[1] & 0b00010000) >> 4); 125 | read_eep_[ch].data = (uint16_t)((data[1] & 0b00001111) << 8 | data[2]); 126 | } else { 127 | read_reg_[ch].vref = (VREF) ((data[1] & 0b10000000) >> 7); 128 | read_reg_[ch].pd = (PWR_DOWN)((data[1] & 0b01100000) >> 5); 129 | read_reg_[ch].gain = (GAIN) ((data[1] & 0b00010000) >> 4); 130 | read_reg_[ch].data = (uint16_t)((data[1] & 0b00001111) << 8 | data[2]); 131 | } 132 | } 133 | } 134 | } 135 | 136 | uint8_t getVref(uint8_t ch, bool b_eep = false) { 137 | return b_eep ? (uint8_t)read_eep_[ch].vref : (uint8_t)read_reg_[ch].vref; 138 | } 139 | 140 | uint8_t getGain(uint8_t ch, bool b_eep = false) { 141 | return b_eep ? (uint8_t)read_eep_[ch].gain: (uint8_t)read_reg_[ch].gain; 142 | } 143 | 144 | uint8_t getPowerDown(uint8_t ch, bool b_eep = false) { 145 | return b_eep ? (uint8_t)read_eep_[ch].pd : (uint8_t)read_reg_[ch].pd; 146 | } 147 | 148 | uint16_t getDACData(uint8_t ch, bool b_eep = false) { 149 | return b_eep ? (uint16_t)read_eep_[ch].data : (uint16_t)read_reg_[ch].data; 150 | } 151 | 152 | private: 153 | 154 | uint8_t fastWrite() { 155 | wire_->beginTransmission(addr_); 156 | for (uint8_t i = 0; i < 4; ++i) { 157 | wire_->write((uint8_t)CMD::FAST_WRITE | highByte(reg_[i].data)); 158 | wire_->write(lowByte(reg_[i].data)); 159 | } 160 | return wire_->endTransmission(); 161 | } 162 | 163 | uint8_t multiWrite() { 164 | wire_->beginTransmission(addr_); 165 | for (uint8_t i = 0; i < 4; ++i) { 166 | wire_->write((uint8_t)CMD::MULTI_WRITE | (i << 1)); 167 | wire_->write(((uint8_t)reg_[i].vref << 7) | ((uint8_t)reg_[i].pd << 5) | ((uint8_t)reg_[i].gain << 4) | highByte(reg_[i].data)); 168 | wire_->write(lowByte(reg_[i].data)); 169 | } 170 | return wire_->endTransmission(); 171 | } 172 | 173 | uint8_t seqWrite() { 174 | wire_->beginTransmission(addr_); 175 | wire_->write((uint8_t)CMD::SEQ_WRITE); 176 | for (uint8_t i = 0; i < 4; ++i) { 177 | wire_->write(((uint8_t)eep_[i].vref << 7) | ((uint8_t)eep_[i].pd << 5) | ((uint8_t)eep_[i].gain << 4) | highByte(eep_[i].data)); 178 | wire_->write(lowByte(eep_[i].data)); 179 | } 180 | return wire_->endTransmission(); 181 | } 182 | 183 | uint8_t singleWrite(uint8_t ch) { 184 | wire_->beginTransmission(addr_); 185 | wire_->write((uint8_t)CMD::SINGLE_WRITE | (ch << 1)); 186 | wire_->write(((uint8_t)eep_[ch].vref << 7) | ((uint8_t)eep_[ch].pd << 5) | ((uint8_t)eep_[ch].gain << 4) | highByte(eep_[ch].data)); 187 | wire_->write(lowByte(eep_[ch].data)); 188 | return wire_->endTransmission(); 189 | } 190 | 191 | private: 192 | 193 | struct DACInputData { 194 | VREF vref; 195 | PWR_DOWN pd; 196 | GAIN gain; 197 | uint16_t data; 198 | }; 199 | 200 | const uint8_t I2C_ADDR {0x60}; 201 | 202 | uint8_t addr_ {I2C_ADDR}; 203 | int8_t pin_ldac_; 204 | 205 | DACInputData reg_[4]; 206 | DACInputData eep_[4]; 207 | DACInputData read_reg_[4]; 208 | DACInputData read_eep_[4]; 209 | 210 | TwoWire* wire_; 211 | 212 | }; 213 | 214 | #endif 215 | -------------------------------------------------------------------------------- /in-cv/lib/MultiPointMap.cpp: -------------------------------------------------------------------------------- 1 | #ifndef MultiPointMap_h 2 | #define MultiPointMap_h 3 | 4 | #include "Arduino.h" 5 | #include 6 | 7 | class MultiPointMap { 8 | 9 | public: 10 | 11 | /** 12 | * Initialize a function that maps values using a multi-linear scale defined by equidistant 13 | * fixed points along the specified range. This is used to implement DACs calibration, and 14 | * it's adapted from Befaco MIDI Thing and Emilie Gillet's CVpal. 15 | */ 16 | void init(uint16_t range = 4000) { 17 | this->step = range / N; // Distance between two consecutive fixed points 18 | this->reset(); 19 | } 20 | 21 | /** 22 | * Map the given value to another value, interpolating between a pair of fixed points 23 | */ 24 | uint16_t map(uint16_t value) { 25 | uint8_t interval = value / this->step; // Index of the interval in which the given value falls 26 | if (interval > N - 1) interval = N - 1; 27 | int16_t a = interval == 0 ? 0 : this->points[interval - 1]; // Low interpolation point 28 | int16_t b = this->points[interval]; // High interpolation point 29 | return a + ((int32_t)(value - interval * step) * (b - a)) / step; // Linear interpolation 30 | } 31 | 32 | /** 33 | * Get the value of a fixed point 34 | */ 35 | uint16_t get(uint8_t i) { 36 | return this->points[i]; 37 | } 38 | 39 | /** 40 | * Set the value of a fixed point 41 | */ 42 | uint16_t set(uint8_t i, uint16_t value) { 43 | this->points[i] = value; 44 | } 45 | 46 | /** 47 | * Returns the distance between two consecutive points of the multi-linear scale 48 | */ 49 | uint16_t getStep() { 50 | return this->step; 51 | } 52 | 53 | /** 54 | * Return the number of points in the multi-linear scale 55 | */ 56 | uint8_t size() { 57 | return N; 58 | } 59 | 60 | /** 61 | * Load the map from the EEPROM memory starting from the given address. 62 | * If the loaded data is invalid, the points are initialized linearly. 63 | * Return the number of bytes read. 64 | */ 65 | int load(int address) { 66 | int size = 0; 67 | uint16_t checksum = 0, checksumLoaded = 0; 68 | for (uint8_t i = 0; i < N; i++) { 69 | EEPROM.get(address + size, this->points[i]); 70 | checksum += this->points[i]; 71 | size += sizeof(this->points[i]); 72 | } 73 | EEPROM.get(address + size, checksumLoaded); 74 | size += sizeof(checksumLoaded); 75 | if (checksum != checksumLoaded) { 76 | this->reset(); 77 | } 78 | return size; 79 | } 80 | 81 | /** 82 | * Write the map to the EEPROM memory starting from the given address. 83 | * Return the number of bytes written. 84 | */ 85 | int save(int address) { 86 | int size = 0; 87 | uint16_t checksum = 0; 88 | for (uint8_t i = 0; i < N; i++) { 89 | EEPROM.put(address + size, this->points[i]); 90 | checksum += this->points[i]; 91 | size += sizeof(this->points[i]); 92 | } 93 | EEPROM.put(address + size, checksum); 94 | size += sizeof(checksum); 95 | return size; 96 | } 97 | 98 | /** 99 | * Initialize the points of the multi-linear scale linearly. 100 | * The first point is assumed to be zero. 101 | */ 102 | void reset() { 103 | for (uint8_t i = 0; i < N; i++) { 104 | this->points[i] = (i + 1) * this->getStep(); 105 | } 106 | } 107 | 108 | private: 109 | 110 | static const uint8_t N = 8; 111 | 112 | uint16_t step; 113 | uint16_t points[N]; 114 | 115 | }; 116 | 117 | #endif -------------------------------------------------------------------------------- /in-cv/lib/SR74HC595.cpp: -------------------------------------------------------------------------------- 1 | #ifndef SR74HC595_h 2 | #define SR74HC595_h 3 | 4 | #include "Arduino.h" 5 | 6 | class SR74HC595 { 7 | 8 | public: 9 | 10 | /** 11 | * Setup the shift register interface. The "clock pin" is for the shift register 12 | * clock (SCK), the "latch pin" is for the storage register clock (RCK). 13 | * https://www.arduino.cc/en/Tutorial/ShiftOut 14 | */ 15 | void init(byte dataPin, byte clockPin, byte latchPin) { 16 | 17 | this->dataPin = dataPin; 18 | this->clockPin = clockPin; 19 | this->latchPin = latchPin; 20 | 21 | pinMode(this->dataPin, OUTPUT); 22 | pinMode(this->clockPin, OUTPUT); 23 | pinMode(this->latchPin, OUTPUT); 24 | 25 | } 26 | 27 | /** 28 | * Writes 8 bits to the shift register, and enables the storage register when finished (latch). 29 | * The default order is MSBFIRST (most significant bit first), but it can be changed to LSBFIRST. 30 | */ 31 | void write(byte value, uint8_t order = MSBFIRST) { 32 | digitalWrite(latchPin, LOW); // So the outputs don't change while sending in bits 33 | shiftOut(this->dataPin, this->clockPin, order, value); 34 | digitalWrite(latchPin, HIGH); // The outputs update at once 35 | } 36 | 37 | private: 38 | byte dataPin; 39 | byte clockPin; 40 | byte latchPin; 41 | 42 | }; 43 | 44 | #endif -------------------------------------------------------------------------------- /in-cv/patterns/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "commonjs": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:jest/recommended" 11 | ], 12 | "parserOptions": { 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "jest" 17 | ], 18 | "rules": { 19 | "semi": [ "error", "always" ], 20 | "no-console": "off", 21 | "no-unused-vars": [ "warn" ], 22 | "no-empty": [ "error", { "allowEmptyCatch": true } ], 23 | "radix": "error" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /in-cv/patterns/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | patterns.h.gch 4 | patterns.test.*.json -------------------------------------------------------------------------------- /in-cv/patterns/cli.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const math = require('mathjs'); 4 | 5 | // Constants 6 | const DAC_BITS = 12; // DAC bit resolution 7 | const DAC_VREF = 4.096; // DAC reference voltage 8 | const DURATION_RESOLUTION = 16; // The smallest possible note, for example 32 for 32th notes (use only multiples of 2) 9 | const SEMITONE = 1 / 12; 10 | const TUNING_NOTE = 'C2'; 11 | const NOTES = { 12 | 'C': SEMITONE * 0, 13 | 'D': SEMITONE * 2, 14 | 'E': SEMITONE * 4, 15 | 'F': SEMITONE * 5, 16 | 'G': SEMITONE * 7, 17 | 'A': SEMITONE * 9, 18 | 'B': SEMITONE * 11, 19 | }; 20 | 21 | /** 22 | * Command line script 23 | */ 24 | const cli = () => { 25 | 26 | // Read patterns from TXT file 27 | const patternsPath = process.argv[2]; 28 | if (!patternsPath) throw new Error("No patterns file specified!\nUsage: npm start -- patterns.txt"); 29 | const patterns = fs.readFileSync(patternsPath).toString().trim().split("\n").filter(p => p.trim() != ''); 30 | if (patterns.length == 0) throw new Error("No patterns found in the specified file"); 31 | 32 | // Parse all patterns 33 | const cvs = []; 34 | const durations = []; 35 | const slides = []; 36 | const acciaccaturas = []; 37 | const names = []; 38 | for (const p of patterns.map(p => parsePattern(p, DURATION_RESOLUTION))) { 39 | cvs.push(p.cv); 40 | durations.push(p.duration); 41 | slides.push(p.slide); 42 | acciaccaturas.push(p.acciaccatura); 43 | names.push(p.name); 44 | } 45 | 46 | // Map of unique note CV values 47 | const cvsMap = {}; 48 | const cvsUniq = [ 0 ]; // Collect unique CVs, let 0 index point to 0 value 49 | const cvsNamesMap = {}; // Maps note integer values to note names, for comments and report 50 | for (let i = 0; i < cvs.length; i++) { 51 | for (let j = 0; j < cvs[i].length; j++) { 52 | const int = cvToInt(cvs[i][j], DAC_VREF, DAC_BITS); 53 | if (cvsUniq.indexOf(int) == -1) { 54 | cvsUniq.push(int); 55 | cvsNamesMap[int] = names[i][j][0].toUpperCase() + names[i][j].slice(1); 56 | } 57 | } 58 | } 59 | cvsUniq.sort(); 60 | for (let i = 0; i < cvsUniq.length; i++) { 61 | cvsMap[cvsUniq[i]] = i; 62 | } 63 | 64 | // Setup code generation 65 | let codeMatrices = ""; // Data arrays 66 | let codePointers = ""; // Pointers arrays 67 | const codeMatrixAndPointers = (type, name, value) => { 68 | 69 | // Lay out the matrix as many distinct arrays with different size. 70 | // Lay out a pointers array for pointing to those distinct arrays. 71 | codePointers += "const " + type + "* const " + name + "[] PROGMEM = {\n"; 72 | for (let i = 0; i < patterns.length; i++) { 73 | const n = (i + 1).toString().padStart(2, "0"); 74 | const pattern = patterns[i]; 75 | codeMatrices += "const " + type + " " + name + "_" + n + "[] PROGMEM = { " + value(i) + " };"; 76 | codeMatrices += " // " + (Array.isArray(pattern) ? pattern.join(" ") : pattern).trim() + "\n"; // Text notation comment 77 | codePointers += "\t" + name + "_" + n + ",\n"; 78 | } 79 | codeMatrices += "\n"; 80 | codePointers += "};\n\n"; 81 | 82 | }; 83 | 84 | // Notes CV unique values 85 | let codeCVs = "const unsigned int PATTERNS_CV[] = {\n"; 86 | for (let i = 0; i < cvsUniq.length; i++) { 87 | const cvName = (cvsNamesMap[cvsUniq[i]] || '- (pause)'); 88 | codeCVs += "\t" + cvsUniq[i].toString().padStart(4) + ", // " + cvName + "\n"; 89 | } 90 | codeCVs += "};\n\n"; 91 | 92 | // Notes CV indexes 93 | codeMatrixAndPointers("byte", "PATTERNS_CV_INDEX", i => { 94 | return cvs[i].map((v) => { 95 | const int = cvToInt(v, DAC_VREF, DAC_BITS); 96 | return (int > 0 ? cvsMap[int] : 0).toString().padStart(2); 97 | }).join(", "); 98 | }); 99 | 100 | // Notes durations 101 | const patternsDurations = []; 102 | let shortestNoteDuration = Infinity; 103 | let longestNoteDuration = 0; 104 | let longestPatternDuration = 0; 105 | let secondLongestPatternDuration = 0; 106 | let longestPatternIndex = 0; 107 | let secondLongestPatternIndex = 0; 108 | codeMatrixAndPointers("byte", "PATTERNS_DURATION", i => { 109 | let patternDuration = 0; 110 | const code = durations[i].map(v => { 111 | patternDuration += v; // Sum up total pattern duration 112 | if (v < shortestNoteDuration) shortestNoteDuration = v; // Find shortest note 113 | if (v > longestNoteDuration) longestNoteDuration = v; // Find longest note 114 | return v.toString().padStart(2); 115 | }).join(", "); 116 | patternsDurations.push(patternDuration); 117 | if (patternDuration > longestPatternDuration) { 118 | secondLongestPatternDuration = longestPatternDuration; 119 | longestPatternDuration = patternDuration; 120 | secondLongestPatternIndex = longestPatternIndex; 121 | longestPatternIndex = i; 122 | } else if (patternDuration > secondLongestPatternDuration) { 123 | secondLongestPatternDuration = patternDuration; 124 | secondLongestPatternIndex = i; 125 | } 126 | return "/* Total: " + patternDuration.toString().padStart(3) + " */ " + code; 127 | }); 128 | 129 | // Slides 130 | codeMatrixAndPointers("byte", "PATTERNS_SLIDE", i => { 131 | const binaryBytes = []; 132 | for (let j = 0; j < slides[i].length; j += 8) { 133 | let binaryByte = "B"; 134 | for (let k = 0; k < 8; k++) binaryByte += slides[i][j + k] ? "1" : "0"; 135 | binaryBytes.push(binaryByte); 136 | } 137 | return binaryBytes.join(", "); 138 | }); 139 | 140 | // Acciaccatura 141 | let codeAcciaccatura = "const unsigned int PATTERNS_ACCIACCATURA_CV[] PROGMEM = {\n"; 142 | for (let i = 0; i < patterns.length; i++) { 143 | const acciaccaturaCV = acciaccaturas[i] != null ? cvToInt(acciaccaturas[i], DAC_VREF, DAC_BITS) : 0; 144 | codeAcciaccatura += "\t" + acciaccaturaCV.toString().padStart(4) + ", // Pattern #" + (i + 1) + "\n"; 145 | } 146 | codeAcciaccatura += "};\n\n"; 147 | 148 | // Patterns size 149 | let codeLength = "const byte PATTERNS_SIZE[] PROGMEM = {\n"; 150 | for (let i = 0; i < patterns.length; i++) { 151 | codeLength += "\t" + cvs[i].length.toString().padStart(4) + ", // Pattern #" + (i + 1) + "\n"; 152 | } 153 | codeLength += "};\n\n"; 154 | 155 | // Create the code of the header ".h" file 156 | let code = "#ifndef patterns_h\n#define patterns_h\n\n#include \"Arduino.h\"\n#include \n\n"; 157 | code += "#define PATTERNS_N " + patterns.length + "\n"; // Patterns count 158 | code += "#define PATTERNS_DURATION_RESOLUTION " + DURATION_RESOLUTION + "\n"; // Duration resolution 159 | code += "#define PATTERNS_DURATION_MAX " + Math.max(...patternsDurations) + "\n"; // Longest pattern diration in units 160 | code += "#define TUNING_CV " + cvToInt(noteToCV(TUNING_NOTE), DAC_VREF, DAC_BITS) + "\n\n"; 161 | code += codeCVs + codeMatrices + codeAcciaccatura + codeLength + codePointers; 162 | code += "#endif"; 163 | 164 | // Save into a file 165 | const filename = "patterns.h"; 166 | fs.writeFileSync(__dirname + path.sep + filename, code); 167 | console.log("Done: " + patterns.length + " patterns saved in " + filename); 168 | console.log(); 169 | 170 | // Info about patterns length 171 | const gcd = patternsDurations.length > 1 ? math.gcd(...patternsDurations) : patternsDurations[0]; 172 | console.log("Lower note: " + cvsNamesMap[cvsUniq[1]] + " (" + cvsUniq[1] + ")"); 173 | console.log("Higher note: " + cvsNamesMap[cvsUniq[cvsUniq.length - 1]] + " (" + cvsUniq[cvsUniq.length - 1] + ")"); 174 | console.log("Number of unique notes: " + cvsUniq.length + " (including pause)"); 175 | console.log("Duration of the shortest note: " + shortestNoteDuration); 176 | console.log("Duration of the longest note: " + longestNoteDuration); 177 | console.log("Duration of the shortest pattern: " + Math.min(...patternsDurations)); 178 | console.log("Duration of the longest pattern: " + longestPatternDuration + " (#" + (longestPatternIndex + 1) + "), followed by " + secondLongestPatternDuration + " (#" + (secondLongestPatternIndex + 1) + ")"); 179 | console.log("Greatest possible resolution for patterns length: */" + Math.max(1, DURATION_RESOLUTION / gcd)); 180 | console.log(); 181 | 182 | }; 183 | 184 | /** 185 | * Parses a pattern and returns "cv", "duration" and "slide" arrays in an object 186 | */ 187 | const parsePattern = (pattern, durationUnits) => { 188 | 189 | if (pattern == null) throw new Error("Invalid empty pattern"); 190 | 191 | // Search for the acciaccatura at the beginning of the pattern. 192 | // It's supported only on the first note for memory usage reasons. 193 | let acciaccatura = null; 194 | const acciaccaturaMatches = pattern.match(/^\((.+)\)/); 195 | if (acciaccaturaMatches) { 196 | acciaccatura = noteToCV(acciaccaturaMatches[1]); 197 | pattern = pattern.substring(acciaccaturaMatches[0].length); 198 | } 199 | 200 | // Split notes string on whitespace, ensure a space after legato symbol 201 | const notes = pattern.replace(/~/g, "~ ").trim().split(/\s+/g); 202 | 203 | // Loop notes and build the arrays 204 | const names = []; 205 | const cvs = []; 206 | const durations = []; 207 | const slides = []; 208 | let legatoFlag = false; 209 | for (const note of notes) { 210 | const n = note.split('/'); 211 | if (n.length != 2) throw new Error("Invalid note: " + note); 212 | const cv = noteToCV(n[0]); 213 | const duration = noteDurationToInt(n[1], durationUnits); 214 | if (legatoFlag && Math.abs(cv - cvs[cvs.length - 1]) < 0.000001) { // Same-note legato? 215 | durations[durations.length - 1] += duration; // Simply increase last note duration 216 | } else { 217 | names.push(n[0]); 218 | cvs.push(cv); 219 | durations.push(duration); 220 | slides.push(false); 221 | if (legatoFlag) slides[slides.length - 2] = true; // Last note need to be slid 222 | } 223 | legatoFlag = note.trim().substr(-1) == "~"; 224 | } 225 | 226 | return { 227 | "cv": cvs, 228 | "duration": durations, 229 | "slide": slides, 230 | "acciaccatura": acciaccatura, 231 | "name": names, 232 | }; 233 | 234 | }; 235 | 236 | /** 237 | * Converts notes written like "C4", "D#5" to 1V/oct CV voltage value 238 | */ 239 | const noteToCV = (noteString) => { 240 | const s = (noteString && noteString.toString().replace(/\s+/g, '')) || '(blank)'; 241 | if (s[0] == '-') return 0; // Rest 242 | if (s[0] == null || NOTES[s[0].toUpperCase()] == null) throw new Error("Invalid note name: " + s); 243 | let value = NOTES[s[0].toUpperCase()]; 244 | let octavePosition = 1; 245 | if (s[1] && (s[1] == '#' || s[1] == 'b')) { 246 | octavePosition++; 247 | if (s[1] == '#') value += SEMITONE; 248 | if (s[1] == 'b') value -= SEMITONE; 249 | } 250 | if (s[octavePosition] == null) throw new Error("Invalid note octave: " + s); 251 | const octave = parseInt(s[octavePosition], 10); 252 | if (isNaN(octave)) throw new Error("Invalid note octave: " + s); 253 | value += octave; 254 | return value; 255 | }; 256 | 257 | /** 258 | * Returns the numbers of "units" of time to use to represente the note "duration", 259 | * for example, assuming "units" is 32 (i.e. a resolution of 32th note): 260 | * - a whole note written as "1" will return 32 261 | * - a quarter note written as "4" will return 8 262 | * - a half dotted note written as "2." will return 24 when "units" is 32 263 | */ 264 | const noteDurationToInt = (duration, units) => { 265 | const d = parseInt(duration, 10); 266 | if (isNaN(d) || d <= 0) throw new Error("Invalid note duration: " + (duration || (typeof duration))); 267 | let v = units / d; 268 | let dotValue = v / 2; 269 | for (let i = 0; i < duration.length; i++) { 270 | if (duration[i] == ".") { 271 | v += dotValue; 272 | dotValue /= 2; 273 | } 274 | } 275 | if (Math.abs(v - Math.round(v)) > 0.0001) throw new Error("Given duration cannot be expressed in integers: " + duration); 276 | return Math.round(v); 277 | }; 278 | 279 | /** 280 | * Returns the DAC integer value to push in order to get the given "cv" voltage value, 281 | * when "vref" is the maximum voltage and "bits" is the DAC resolution 282 | */ 283 | const cvToInt = (cv, vref, bits) => { 284 | if (typeof cv !== 'number') throw new Error("Invalid CV value type: " + (typeof cv)); 285 | if (typeof vref !== 'number') throw new Error("Invalid CV value type: " + (typeof cv)); 286 | if (typeof bits !== 'number') throw new Error("Invalid CV value type: " + (typeof cv)); 287 | const max = Math.pow(2, bits); 288 | const v = (cv / vref) * max; 289 | return Math.round(Math.max(0, Math.min(max - 1, v))); 290 | }; 291 | 292 | // Run if called directly, not for tests 293 | // https://stackoverflow.com/q/6398196/995958 294 | // https://nodejs.org/docs/latest/api/modules.html#modules_accessing_the_main_module 295 | if (require.main === module) { 296 | cli(); 297 | } else { 298 | module.exports = { 299 | parsePattern, 300 | noteToCV, 301 | noteDurationToInt, 302 | cvToInt 303 | }; 304 | } 305 | -------------------------------------------------------------------------------- /in-cv/patterns/cli.test.js: -------------------------------------------------------------------------------- 1 | const cli = require('./cli.js'); 2 | 3 | test('Notes are correctly converted to 1V/oct values', () => { 4 | 5 | const f = cli.noteToCV; 6 | const precision = 5; 7 | 8 | expect(f("C0")).toBeCloseTo(0, precision); 9 | expect(f("D0")).toBeCloseTo(2 / 12, precision); 10 | expect(f("C1")).toBeCloseTo(1, precision); 11 | expect(f("D1")).toBeCloseTo(1 + 2 / 12, precision); 12 | expect(f("E1")).toBeCloseTo(1 + 4 / 12, precision); 13 | expect(f("B1")).toBeCloseTo(2 - 1 / 12, precision); 14 | expect(f("C2")).toBeCloseTo(2, precision); 15 | expect(f("C4")).toBeCloseTo(4, precision); 16 | expect(f("C5")).toBeCloseTo(5, precision); 17 | 18 | // Accidentals 19 | expect(f("C#1")).toBeCloseTo(1 + 1 / 12, precision); 20 | expect(f("D#1")).toBeCloseTo(1 + 3 / 12, precision); 21 | expect(f("E#1")).toBeCloseTo(1 + 5 / 12, precision); 22 | expect(f("Bb1")).toBeCloseTo(2 - 2 / 12, precision); 23 | 24 | // Enharmonic equivalences 25 | expect(f("C#1")).toBeCloseTo(f("Db1"), precision); 26 | expect(f("E#1")).toBeCloseTo(f("F1"), precision); 27 | expect(f("B#1")).toBeCloseTo(f("C2"), precision); 28 | 29 | // Rests 30 | expect(f("-")).toBe(0); 31 | 32 | // Whitespace/case tolerance 33 | expect(f(" c0")).toBeCloseTo(f("C0"), precision); 34 | expect(f("c# 1 ")).toBeCloseTo(f("C#1"), precision); 35 | expect(f("B b 1")).toBeCloseTo(f("Bb1"), precision); 36 | expect(f(" - ")).toBe(0); 37 | 38 | // Ignore extra 39 | expect(f("C#1/8")).toBeCloseTo(f("C#1"), precision); 40 | expect(f("C# 1 ignored")).toBeCloseTo(f("C#1"), precision); 41 | 42 | }); 43 | 44 | test('Invalid notes correctly throw errors', () => { 45 | 46 | const f = cli.noteToCV; 47 | 48 | expect(() => f()).toThrow(); 49 | expect(() => f(null)).toThrow(); 50 | expect(() => f(undefined)).toThrow(); 51 | expect(() => f(false)).toThrow(); 52 | expect(() => f("")).toThrow(); 53 | expect(() => f(" ")).toThrow(); 54 | 55 | expect(() => f(0)).toThrow(); 56 | expect(() => f(1)).toThrow(); 57 | 58 | expect(() => f("H1")).toThrow(); 59 | expect(() => f("C##1")).toThrow(); 60 | expect(() => f("C")).toThrow(); 61 | expect(() => f("Cb")).toThrow(); 62 | expect(() => f("#1")).toThrow(); 63 | 64 | expect(() => f("foo")).toThrow(); 65 | expect(() => f("bar")).toThrow(); 66 | 67 | }); 68 | 69 | test('Notes durations are correctly converted to integers', () => { 70 | 71 | const f = cli.noteDurationToInt; 72 | 73 | expect(f("1", 16)).toBe(16); 74 | expect(f("1", 32)).toBe(32); 75 | expect(f("2", 32)).toBe(16); 76 | expect(f("4", 32)).toBe(8); 77 | expect(f("32", 32)).toBe(1); 78 | 79 | // Dotted notes 80 | expect(f("1.", 32)).toBe(32 + 16); 81 | expect(f("1..", 32)).toBe(32 + 16 + 8); 82 | expect(f("2.", 32)).toBe(16 + 8); 83 | expect(f("2..", 32)).toBe(16 + 8 + 4); 84 | expect(f("2...", 32)).toBe(16 + 8 + 4 + 2); 85 | expect(f("16.", 32)).toBe(2 + 1); 86 | 87 | // Non-integer durations 88 | expect(() => f("64", 32)).toThrow(); // Half the resolution 89 | expect(() => f("3", 32)).toThrow(); // Triplet 90 | 91 | expect(() => f()).toThrow(); 92 | expect(() => f(null)).toThrow(); 93 | expect(() => f("foo")).toThrow(); 94 | expect(() => f("bar")).toThrow(); 95 | 96 | }); 97 | 98 | test('Pattern are parsed correctly', () => { 99 | 100 | const f = cli.parsePattern; 101 | const precision = 5; 102 | 103 | expect(f("C3/2 C4/2").cv).toHaveLength(2); 104 | expect(f("C3/2 C4/2", 32).duration).toEqual([16, 16]); 105 | 106 | // Same-note legato 107 | expect(f("C3/2~C3/4", 32).cv).toHaveLength(1); 108 | expect(f("C3/2~C3/4", 32).duration).toEqual([16 + 8]); 109 | expect(f("C3/2~C3/2", 32).slide).toEqual([false]); 110 | expect(f("C3/2~ C3/2 D4/4", 32).cv).toHaveLength(2); 111 | expect(f("C3/2~ C3/2 -/4", 32).duration).toEqual([16 + 16, 8]); 112 | expect(f("C3/2~ C3/2 D4/4", 32).slide).toEqual([false, false]); 113 | expect(f("C3/8~C3/8 D4/4 C3/8~C3/8", 32).duration).toEqual([4 + 4, 8, 4 + 4]); 114 | 115 | // Slide 116 | expect(f("C3/2~ C3/2 D4/4", 32).slide).toEqual([false, false]); 117 | expect(f("C3/2~ C4/2 D4/4", 32).duration).toEqual([16, 16, 8]); 118 | expect(f("C3/2~ C4/2 D4/4", 32).slide).toEqual([true, false, false]); 119 | 120 | // Acciaccatura at the beginning of the pattern 121 | expect(f("C3/4", 32).acciaccatura).toBeNull(); 122 | expect(f("(C2)C3/4", 32).acciaccatura).toBeCloseTo(2, precision); 123 | expect(f("(C2)C3/4 C3/4", 32).acciaccatura).toBeCloseTo(2, precision); 124 | 125 | expect(() => f()).toThrow(); 126 | expect(() => f(null)).toThrow(); 127 | expect(() => f("")).toThrow(); 128 | expect(() => f("foo bar", 32)).toThrow(); 129 | expect(() => f("C3/4 (C2)C3/4", 32)).toThrow(); // Acciaccatura only at the beginning 130 | 131 | }); 132 | 133 | test('DAC integer values for CVs are correctly computed', () => { 134 | 135 | const f = cli.cvToInt; 136 | 137 | // Vref = 5V, 8 bits 138 | expect(f(0, 5, 8)).toBe(0); 139 | expect(f(2.5, 5, 8)).toBe(128); 140 | expect(f(4, 5, 8)).toBe(Math.round(256 * (4 / 5))); 141 | expect(f(5, 5, 8)).toBe(255); 142 | 143 | // Vref = 8V, 12 bits 144 | expect(f(0, 8, 12)).toBe(0); 145 | expect(f(4, 8, 12)).toBe(4096 / 2); 146 | 147 | // Vref = 4.096, 12 bits (MCP4728 internal Vref with X2 gain) 148 | expect(f(0, 4.096, 12)).toBe(0); 149 | expect(f(1, 4.096, 12)).toBe(1000); 150 | expect(f(4, 4.096, 12)).toBe(4000); 151 | expect(f(1.234, 4.096, 12)).toBe(1234); 152 | 153 | // Bounds 154 | expect(f(-1, 5, 8)).toBe(0); 155 | expect(f(99, 5, 8)).toBe(255); 156 | 157 | expect(() => f()).toThrow(); 158 | 159 | }); -------------------------------------------------------------------------------- /in-cv/patterns/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "in-cv-patterns", 3 | "version": "1.0.0", 4 | "description": "Generates patterns header file for In-CV Eurorack module", 5 | "scripts": { 6 | "start": "node cli.js", 7 | "test": "jest" 8 | }, 9 | "author": "joeSeggiola ", 10 | "license": "GPL-3.0-only", 11 | "private": true, 12 | "devDependencies": { 13 | "eslint": "^5.16.0", 14 | "eslint-plugin-jest": "^22.5.1", 15 | "jest": "^24.8.0" 16 | }, 17 | "dependencies": { 18 | "mathjs": "^7.6.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /in-cv/patterns/patterns.txt: -------------------------------------------------------------------------------- 1 | (c2)e2/4 2 | (c2)e2/8 f2/8 e2/4 3 | -/8 e2/8 f2/8 e2/8 4 | -/8 e2/8 f2/8 g2/8 5 | e2/8 f2/8 g2/8 -/8 6 | c3/1~c3/1 7 | -/4 -/4 -/4 -/8 c2/16 c2/16 c2/8 -/8 -/4 -/4 -/4 -/4 8 | g2/1. f2/1~f2/1 9 | b2/16 g2/16 -/8 -/4 -/4 -/4 10 | b2/16 g2/16 11 | f2/16 g2/16 b2/16 g2/16 b2/16 g2/16 12 | f2/8 g2/8 b2/1 c3/4 13 | b2/16 g2/8. g2/16 f2/16 g2/8 -/8. g2/16~g2/2. 14 | c3/1 b2/1 g2/1 f#2/1 15 | g2/16 -/8. -/4 -/4 -/4 16 | g2/16 b2/16 c3/16 b2/16 17 | b2/16 c3/16 b2/16 c3/16 b2/16 -/16 18 | e2/16 f#2/16 e2/16 f#2/16 e2/8. e2/16 19 | -/4. g3/4. 20 | e2/16 f#2/16 e2/16 f#2/16 g1/8. e2/16 f#2/16 e2/16 f#2/16 e2/16 21 | f#2/2. 22 | e2/4. e2/4. e2/4. e2/4. e2/4. f#2/4. g2/4. a2/4. b2/8 23 | e2/8 f#2/4. f#2/4. f#2/4. f#2/4. f#2/4. g2/4. a2/4. b2/4 24 | e2/8 f#2/8 g2/4. g2/4. g2/4. g2/4. g2/4. a2/4. b2/8 25 | e2/8 f#2/8 g2/8 a2/4. a2/4. a2/4. a2/4. a2/4. b2/4. 26 | e2/8 f#2/8 g2/8 a2/8 b2/4. b2/4. b2/4. b2/4. b2/4. 27 | e2/16 f#2/16 e2/16 f#2/16 g2/8 e2/16 g2/16 f#2/16 e2/16 f#2/16 e2/16 28 | e2/16 f#2/16 e2/16 f#2/16 e2/8. e2/16 29 | e2/2. g2/2. c3/2. 30 | c3/1. 31 | g2/16 f2/16 g2/16 b2/16 g2/16 b2/16 32 | f2/16 g2/16 f2/16 g2/16 b2/16 f2/16~f2/2. g2/4. 33 | g2/16 f2/16 -/8 34 | g2/16 f2/16 35 | f2/16 g2/16 b2/16 g2/16 b2/16 g2/16 b2/16 g2/16 b2/16 g2/16 -/8 -/4 -/4 -/4 bb2/4 g3/2. a3/8 g3/8~g3/8 b3/8 a3/4. g3/8 e3/2. g3/8 f#3/8~f#3/2. -/4 -/4 -/8 e3/8~e3/2 f3/1. 36 | f2/16 g2/16 b2/16 g2/16 b2/16 g2/16 37 | f2/16 g2/16 38 | f2/16 g2/16 b2/16 f2/16 g2/16 b2/16 39 | b2/16 g2/16 f2/16 g2/16 b2/16 c3/16 40 | b2/16 f2/16 41 | b2/16 g2/16 42 | c3/1 b2/1 a2/1 c3/1 43 | f3/16 e3/16 f3/16 e3/16 e3/8 e3/8 e3/8 f3/16 e3/16 44 | f3/8 e3/8~e3/8 e3/8 c3/4 45 | d3/4 d3/4 g2/4 46 | g2/16 d3/16 e3/16 d3/16 -/8 g2/8 -/8 g2/8 -/8 g2/8 g2/16 d3/16 e3/16 d3/16 47 | d3/16 e3/16 d3/8 48 | g2/1. g2/1 f2/1~f2/4 49 | f2/16 g2/16 bb2/16 g2/16 bb2/16 g2/16 50 | f2/16 g2/16 51 | f2/16 g2/16 bb2/16 f2/16 g2/16 bb2/16 52 | g2/16 bb2/16 53 | bb2/16 g2/16 -------------------------------------------------------------------------------- /in-cv/pcb/board-gerber.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/in-cv/pcb/board-gerber.zip -------------------------------------------------------------------------------- /in-cv/pcb/panel-art-readme.txt: -------------------------------------------------------------------------------- 1 | Copper area art is exported as a pure SVG unique path and imported in EasyEDA using: 2 | https://github.com/xsrf/easyeda-svg-import/ 3 | 4 | Import as: SVG Node 5 | Import scale: 1.04 6 | Layer: Top + TopSolderMask 7 | -------------------------------------------------------------------------------- /in-cv/pcb/panel-font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/in-cv/pcb/panel-font.ttf -------------------------------------------------------------------------------- /in-cv/pcb/panel-gerber.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/in-cv/pcb/panel-gerber.zip -------------------------------------------------------------------------------- /in-cv/pictures/IMG_20190720_185259.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/in-cv/pictures/IMG_20190720_185259.jpg -------------------------------------------------------------------------------- /in-cv/pictures/IMG_20190726_192809.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/in-cv/pictures/IMG_20190726_192809.jpg -------------------------------------------------------------------------------- /in-cv/pictures/IMG_20190727_130919.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/in-cv/pictures/IMG_20190727_130919.jpg -------------------------------------------------------------------------------- /in-cv/pictures/IMG_20190727_131911.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/in-cv/pictures/IMG_20190727_131911.jpg -------------------------------------------------------------------------------- /in-cv/pictures/IMG_20210323_125702_M.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/in-cv/pictures/IMG_20210323_125702_M.png -------------------------------------------------------------------------------- /in-cv/pictures/IMG_20210323_125702_R.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/in-cv/pictures/IMG_20210323_125702_R.jpg -------------------------------------------------------------------------------- /in-cv/pictures/IMG_20210323_125702_S.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/in-cv/pictures/IMG_20210323_125702_S.jpg -------------------------------------------------------------------------------- /in-cv/pictures/IMG_20210323_125702_T.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/in-cv/pictures/IMG_20210323_125702_T.jpg -------------------------------------------------------------------------------- /in-cv/pictures/IMG_20210323_125946_R.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/in-cv/pictures/IMG_20210323_125946_R.jpg -------------------------------------------------------------------------------- /in-cv/pictures/IMG_20210323_125946_S.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/in-cv/pictures/IMG_20210323_125946_S.jpg -------------------------------------------------------------------------------- /in-cv/pictures/IMG_20210328_141742.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/in-cv/pictures/IMG_20210328_141742.jpg -------------------------------------------------------------------------------- /in-cv/schematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/in-cv/schematic.png -------------------------------------------------------------------------------- /lib/Button.cpp: -------------------------------------------------------------------------------- 1 | #ifndef Button_h 2 | #define Button_h 3 | 4 | #include "Arduino.h" 5 | 6 | class Button { 7 | 8 | public: 9 | 10 | /** 11 | * Setup the button, specifying and optional debounce delay 12 | */ 13 | void init(byte pin, unsigned int debounceDelayMs = 0, bool invert = false, bool internalPullup = false) { 14 | 15 | this->pin = pin; 16 | this->debounceDelayMs = debounceDelayMs; 17 | this->invert = invert; 18 | 19 | this->lastPressedMs = 0; 20 | this->longPressStartMs = 0; 21 | this->shortOrLongPressStartMs = 0; 22 | 23 | this->readOnceFlag = false; 24 | this->readLongPressOnceFlag = false; 25 | this->readShortOrLongPressOnceFlag = false; 26 | 27 | pinMode(this->pin, internalPullup ? INPUT_PULLUP : INPUT); 28 | 29 | } 30 | 31 | /** 32 | * Get the button state, TRUE if the pin is HIGH. 33 | * Immediately reads presses, but the release can be delayed according to debouncing. 34 | */ 35 | bool read() { 36 | 37 | bool reading = digitalRead(this->pin); 38 | if (this->invert) reading = !reading; 39 | 40 | if (reading) { 41 | 42 | // Pressed: return TRUE 43 | this->lastPressedMs = millis(); // Remember time for debouncing 44 | if (this->longPressStartMs == 0) this->longPressStartMs = millis(); // Start long press detection 45 | return true; 46 | 47 | } else { 48 | 49 | // Released: wait for debouncing and return FALSE 50 | if (this->lastPressedMs > 0) { 51 | if (millis() - this->lastPressedMs >= debounceDelayMs) { 52 | this->lastPressedMs = 0; // Reset remembered time 53 | this->longPressStartMs = 0; // Stop long press detection 54 | } else { 55 | return true; // Waiting for debouncing... 56 | } 57 | } 58 | 59 | } 60 | 61 | return false; 62 | 63 | } 64 | 65 | /** 66 | * Same as read(), but returns TRUE only once, until the button is released. 67 | */ 68 | bool readOnce() { 69 | if (this->read()) { 70 | if (!this->readOnceFlag) { 71 | this->readOnceFlag = true; 72 | return true; 73 | } 74 | } else { 75 | this->readOnceFlag = false; 76 | } 77 | return false; 78 | } 79 | 80 | /** 81 | * Detect button long press, TRUE if the pin was HIGH for longer than given duration. 82 | */ 83 | bool readLongPress(unsigned long durationMs) { 84 | if (this->read()) { 85 | if (millis() - this->longPressStartMs >= durationMs) { 86 | return true; 87 | } 88 | } 89 | return false; 90 | } 91 | 92 | /** 93 | * Same as readLongPress(), but returns TRUE only once, until the button is released. 94 | */ 95 | bool readLongPressOnce(unsigned long durationMs) { 96 | if (this->readLongPress(durationMs)) { 97 | if (!this->readLongPressOnceFlag) { 98 | this->readLongPressOnceFlag = true; 99 | return true; 100 | } 101 | } else { 102 | this->readLongPressOnceFlag = false; 103 | } 104 | return false; 105 | } 106 | 107 | /* 108 | * A combined readOnce() and readLongPressOnce() for a multi-purpose button. 109 | * Returns 1 when the button is released before specified duration (short press). 110 | * Returns 2 as soon as the button has been pressed for specified duration. 111 | * Returns 0 in subsequent calls, while idle or while being pressed. 112 | */ 113 | byte readShortOrLongPressOnce(unsigned long longPressDurationMs) { 114 | byte r = 0; 115 | if (this->read()) { 116 | if (!this->readShortOrLongPressOnceFlag) { 117 | if (this->shortOrLongPressStartMs == 0) { 118 | this->shortOrLongPressStartMs = millis(); 119 | } else { 120 | if (millis() - this->shortOrLongPressStartMs >= longPressDurationMs) { 121 | this->readShortOrLongPressOnceFlag = true; 122 | r = 2; 123 | } 124 | } 125 | } 126 | } else { 127 | if (!this->readShortOrLongPressOnceFlag) { 128 | if (this->shortOrLongPressStartMs != 0) { 129 | r = 1; 130 | } 131 | } 132 | this->shortOrLongPressStartMs = 0; 133 | this->readShortOrLongPressOnceFlag = false; 134 | } 135 | return r; 136 | } 137 | 138 | private: 139 | byte pin; 140 | unsigned int debounceDelayMs; 141 | bool invert; 142 | unsigned long lastPressedMs; 143 | unsigned long longPressStartMs; 144 | unsigned long shortOrLongPressStartMs; 145 | bool readOnceFlag; 146 | bool readLongPressOnceFlag; 147 | bool readShortOrLongPressOnceFlag; 148 | 149 | }; 150 | 151 | #endif 152 | -------------------------------------------------------------------------------- /lib/CV.cpp: -------------------------------------------------------------------------------- 1 | #ifndef CV_h 2 | #define CV_h 3 | 4 | #include "Arduino.h" 5 | 6 | class CV { 7 | 8 | public: 9 | 10 | /** 11 | * Setup the analog input reader (CV input or knob), specifying optional thresholds 12 | */ 13 | void init(byte pin, int thresholdLow = 0, int thresholdHigh = 1023, bool invert = false) { 14 | 15 | this->pin = pin; 16 | 17 | this->thresholdLow = thresholdLow; 18 | this->thresholdHigh = thresholdHigh; 19 | this->invert = invert; 20 | 21 | } 22 | 23 | /** 24 | * Return the raw reading, as returned by analogRead() 25 | */ 26 | int readRaw() { 27 | return analogRead(this->pin); 28 | } 29 | 30 | /** 31 | * Return the reading as a float number between 0 and 1, included. 32 | * Optional thresholds are used to map the raw values into the returned 0..1 range. 33 | */ 34 | float read() { 35 | 36 | int r = this->readRaw(); 37 | float f; 38 | 39 | if (r <= this->thresholdLow) { 40 | f = 0.0; 41 | } else if (r >= this->thresholdHigh) { 42 | f = 1.0; 43 | } else { 44 | f = float(r - this->thresholdLow) / float(this->thresholdHigh - this->thresholdLow); 45 | } 46 | 47 | if (this->invert) { 48 | return 1.0 - f; 49 | } else { 50 | return f; 51 | } 52 | 53 | } 54 | 55 | private: 56 | byte pin; 57 | int thresholdLow; 58 | int thresholdHigh; 59 | bool invert; 60 | 61 | }; 62 | 63 | #endif -------------------------------------------------------------------------------- /lib/Led.cpp: -------------------------------------------------------------------------------- 1 | #ifndef Led_h 2 | #define Led_h 3 | 4 | #include "Arduino.h" 5 | 6 | class Led { 7 | 8 | public: 9 | 10 | /** 11 | * Setup the LED, specifying and optional minimum "on" duration for user visibility 12 | */ 13 | void init(byte pin, unsigned int minDurationMs = 0) { 14 | 15 | this->pin = pin; 16 | this->minDurationMs = minDurationMs; 17 | 18 | this->state = false; 19 | this->stateHardware = false; 20 | this->blinkMs = 0; 21 | this->lastOnMs = 0; 22 | 23 | pinMode(this->pin, OUTPUT); 24 | digitalWrite(this->pin, LOW); 25 | 26 | } 27 | 28 | /** 29 | * Turn the LED on or off. 30 | * If a minimum duration was set, it could not turn off if it was 31 | * on for too little, you need to call loop() to update the state. 32 | */ 33 | void set(bool state) { 34 | 35 | this->blinkMs = 0; // Stop blinking 36 | this->state = state; 37 | 38 | if (state) { 39 | 40 | // Remember last time it was requested to be on 41 | this->lastOnMs = millis(); 42 | 43 | // Turn on the LED if necessary 44 | if (!this->stateHardware) { 45 | this->stateHardware = true; 46 | digitalWrite(this->pin, HIGH); 47 | } 48 | 49 | } else { 50 | 51 | // Turn the LED off if necessary 52 | this->loop(); 53 | 54 | } 55 | 56 | } 57 | 58 | /** 59 | * Starts blinking with given period, until any other method is called. 60 | * Use duty to specify how long the LED will be on, and invert to flip the blinking phase. 61 | * This method can also be used make it fade, using a short period and duty to adjust brightness. 62 | * Make sure to call loop() to keep the LED blinking. 63 | */ 64 | void blink(unsigned int periodMs, float duty = 0.5, bool invert = false) { 65 | this->blinkMs = periodMs; 66 | this->blinkDuty = max(0, min(periodMs, duty * periodMs)); 67 | this->blinkStartedMs = millis() - (invert ? this->blinkDuty : 0); 68 | } 69 | 70 | /** 71 | * Turn the LED off if necessary, or keep it blinking. 72 | * Call this in the main loop. 73 | */ 74 | void loop() { 75 | 76 | if (this->blinkMs > 0) { 77 | 78 | unsigned long t = ((millis() - this->blinkStartedMs) % this->blinkMs); 79 | this->stateHardware = t < this->blinkDuty; 80 | digitalWrite(this->pin, this->stateHardware); 81 | 82 | } else { 83 | 84 | // Turn the LED off if necessary 85 | if (!this->state && this->stateHardware) { 86 | if (millis() - this->lastOnMs >= this->minDurationMs) { 87 | this->stateHardware = false; 88 | digitalWrite(this->pin, LOW); 89 | } 90 | } 91 | 92 | } 93 | 94 | } 95 | 96 | void on() { 97 | this->set(true); 98 | } 99 | 100 | void off() { 101 | this->set(false); 102 | } 103 | 104 | void toggle() { 105 | this->set(!state); 106 | } 107 | 108 | /** 109 | * Turn on the LED, then turn it off immediately. 110 | * A single impulse of light will be visible if LED's minDurationMs is long enough. 111 | */ 112 | void flash() { 113 | this->set(true); 114 | this->set(false); 115 | } 116 | 117 | /** 118 | * Set the optional minimum "on" duration for user visibility 119 | */ 120 | void setMinDurationMs(unsigned int minDurationMs = 0) { 121 | this->minDurationMs = minDurationMs; 122 | this->loop(); 123 | } 124 | 125 | private: 126 | byte pin; 127 | unsigned int minDurationMs; 128 | bool state; 129 | bool stateHardware; 130 | unsigned int blinkMs; 131 | unsigned long blinkStartedMs; 132 | unsigned int blinkDuty; 133 | unsigned long lastOnMs; 134 | 135 | }; 136 | 137 | #endif 138 | -------------------------------------------------------------------------------- /lib/MCP4728.cpp: -------------------------------------------------------------------------------- 1 | 2 | // LIBRARY FOR MCP4728 3 | // Link: https://github.com/hideakitai/MCP4728 4 | // Author: Hideaki Tai 5 | // License: MIT (https://github.com/hideakitai/MCP4728/blob/master/LICENSE) 6 | // Extended by Joe Seggiola to include optional LDAC 7 | 8 | #pragma once 9 | #ifndef MCP4728_H 10 | #define MCP4728_H 11 | 12 | #include "Arduino.h" 13 | #include 14 | 15 | class MCP4728 { 16 | 17 | public: 18 | 19 | enum class CMD { 20 | FAST_WRITE = 0x00, 21 | MULTI_WRITE = 0x40, 22 | SINGLE_WRITE = 0x58, 23 | SEQ_WRITE = 0x50, 24 | SELECT_VREF = 0x80, 25 | SELECT_GAIN = 0xC0, 26 | SELECT_PWRDOWN = 0xA0 27 | }; 28 | 29 | enum class VREF { VDD, INTERNAL_2_8V }; 30 | enum class PWR_DOWN { NORMAL, GND_1KOHM, GND_100KOHM, GND_500KOHM }; 31 | enum class GAIN { X1, X2 }; 32 | 33 | void init(TwoWire& w, uint8_t addr = 0, int8_t pin = -1) { 34 | wire_ = &w; 35 | addr_ = I2C_ADDR + addr; 36 | pin_ldac_ = pin; 37 | if (pin_ldac_ > -1) { 38 | pinMode(pin_ldac_, OUTPUT); 39 | enable(false); 40 | } 41 | readRegisters(); 42 | } 43 | 44 | void enable(bool b) { 45 | if (pin_ldac_ > -1) { 46 | digitalWrite(pin_ldac_, !b); 47 | } 48 | } 49 | 50 | uint8_t analogWrite(uint8_t ch, uint16_t data, bool b_eep = false) { 51 | if (b_eep) { 52 | eep_[ch].data = data > 0xFFF ? 0xFFF : data; 53 | return singleWrite(ch); 54 | } else { 55 | reg_[ch].data = data > 0xFFF ? 0xFFF : data; 56 | return fastWrite(); 57 | } 58 | } 59 | 60 | uint8_t analogWrite(uint16_t a, uint16_t b, uint16_t c, uint16_t d, bool b_eep = false) { 61 | if (b_eep) { 62 | reg_[0].data = eep_[0].data = a > 0xFFF ? 0xFFF : a; 63 | reg_[1].data = eep_[1].data = b > 0xFFF ? 0xFFF : b; 64 | reg_[2].data = eep_[2].data = c > 0xFFF ? 0xFFF : c; 65 | reg_[3].data = eep_[3].data = d > 0xFFF ? 0xFFF : d; 66 | return seqWrite(); 67 | } else { 68 | reg_[0].data = a > 0xFFF ? 0xFFF : a; 69 | reg_[1].data = b > 0xFFF ? 0xFFF : b; 70 | reg_[2].data = c > 0xFFF ? 0xFFF : c; 71 | reg_[3].data = d > 0xFFF ? 0xFFF : d; 72 | return fastWrite(); 73 | } 74 | } 75 | 76 | uint8_t selectVref(VREF a, VREF b, VREF c, VREF d) { 77 | reg_[0].vref = a; 78 | reg_[1].vref = b; 79 | reg_[2].vref = c; 80 | reg_[3].vref = d; 81 | uint8_t data = (uint8_t)CMD::SELECT_VREF; 82 | for (uint8_t i = 0; i < 4; ++i) bitWrite(data, 3 - i, (uint8_t)reg_[i].vref); 83 | wire_->beginTransmission(addr_); 84 | wire_->write(data); 85 | return wire_->endTransmission(); 86 | } 87 | 88 | uint8_t selectPowerDown(PWR_DOWN a, PWR_DOWN b, PWR_DOWN c, PWR_DOWN d) { 89 | reg_[0].pd = a; 90 | reg_[1].pd = b; 91 | reg_[2].pd = c; 92 | reg_[3].pd = d; 93 | uint8_t h = ((uint8_t)CMD::SELECT_PWRDOWN) | ((uint8_t)a << 2) | (uint8_t)b; 94 | uint8_t l = 0 | ((uint8_t)c << 6) | ((uint8_t)d << 4); 95 | wire_->beginTransmission(addr_); 96 | wire_->write(h); 97 | wire_->write(l); 98 | return wire_->endTransmission(); 99 | } 100 | 101 | uint8_t selectGain(GAIN a, GAIN b, GAIN c, GAIN d) { 102 | reg_[0].gain = a; 103 | reg_[1].gain = b; 104 | reg_[2].gain = c; 105 | reg_[3].gain = d; 106 | uint8_t data = (uint8_t)CMD::SELECT_GAIN; 107 | for (uint8_t i = 0; i < 4; ++i) bitWrite(data, 3 - i, (uint8_t)reg_[i].gain); 108 | wire_->beginTransmission(addr_); 109 | wire_->write(data); 110 | return wire_->endTransmission(); 111 | } 112 | 113 | void readRegisters() { 114 | wire_->requestFrom((int)addr_, 24); 115 | if (wire_->available() == 24) { 116 | for (uint8_t i = 0; i < 8; ++i) { 117 | uint8_t data[3]; 118 | bool isEeprom = i % 2; 119 | for (uint8_t i = 0; i < 3; ++i) data[i] = wire_->read(); 120 | uint8_t ch = (data[0] & 0x30) >> 4; 121 | if (isEeprom) { 122 | read_eep_[ch].vref = (VREF) ((data[1] & 0b10000000) >> 7); 123 | read_eep_[ch].pd = (PWR_DOWN)((data[1] & 0b01100000) >> 5); 124 | read_eep_[ch].gain = (GAIN) ((data[1] & 0b00010000) >> 4); 125 | read_eep_[ch].data = (uint16_t)((data[1] & 0b00001111) << 8 | data[2]); 126 | } else { 127 | read_reg_[ch].vref = (VREF) ((data[1] & 0b10000000) >> 7); 128 | read_reg_[ch].pd = (PWR_DOWN)((data[1] & 0b01100000) >> 5); 129 | read_reg_[ch].gain = (GAIN) ((data[1] & 0b00010000) >> 4); 130 | read_reg_[ch].data = (uint16_t)((data[1] & 0b00001111) << 8 | data[2]); 131 | } 132 | } 133 | } 134 | } 135 | 136 | uint8_t getVref(uint8_t ch, bool b_eep = false) { 137 | return b_eep ? (uint8_t)read_eep_[ch].vref : (uint8_t)read_reg_[ch].vref; 138 | } 139 | 140 | uint8_t getGain(uint8_t ch, bool b_eep = false) { 141 | return b_eep ? (uint8_t)read_eep_[ch].gain: (uint8_t)read_reg_[ch].gain; 142 | } 143 | 144 | uint8_t getPowerDown(uint8_t ch, bool b_eep = false) { 145 | return b_eep ? (uint8_t)read_eep_[ch].pd : (uint8_t)read_reg_[ch].pd; 146 | } 147 | 148 | uint16_t getDACData(uint8_t ch, bool b_eep = false) { 149 | return b_eep ? (uint16_t)read_eep_[ch].data : (uint16_t)read_reg_[ch].data; 150 | } 151 | 152 | private: 153 | 154 | uint8_t fastWrite() { 155 | wire_->beginTransmission(addr_); 156 | for (uint8_t i = 0; i < 4; ++i) { 157 | wire_->write((uint8_t)CMD::FAST_WRITE | highByte(reg_[i].data)); 158 | wire_->write(lowByte(reg_[i].data)); 159 | } 160 | return wire_->endTransmission(); 161 | } 162 | 163 | uint8_t multiWrite() { 164 | wire_->beginTransmission(addr_); 165 | for (uint8_t i = 0; i < 4; ++i) { 166 | wire_->write((uint8_t)CMD::MULTI_WRITE | (i << 1)); 167 | wire_->write(((uint8_t)reg_[i].vref << 7) | ((uint8_t)reg_[i].pd << 5) | ((uint8_t)reg_[i].gain << 4) | highByte(reg_[i].data)); 168 | wire_->write(lowByte(reg_[i].data)); 169 | } 170 | return wire_->endTransmission(); 171 | } 172 | 173 | uint8_t seqWrite() { 174 | wire_->beginTransmission(addr_); 175 | wire_->write((uint8_t)CMD::SEQ_WRITE); 176 | for (uint8_t i = 0; i < 4; ++i) { 177 | wire_->write(((uint8_t)eep_[i].vref << 7) | ((uint8_t)eep_[i].pd << 5) | ((uint8_t)eep_[i].gain << 4) | highByte(eep_[i].data)); 178 | wire_->write(lowByte(eep_[i].data)); 179 | } 180 | return wire_->endTransmission(); 181 | } 182 | 183 | uint8_t singleWrite(uint8_t ch) { 184 | wire_->beginTransmission(addr_); 185 | wire_->write((uint8_t)CMD::SINGLE_WRITE | (ch << 1)); 186 | wire_->write(((uint8_t)eep_[ch].vref << 7) | ((uint8_t)eep_[ch].pd << 5) | ((uint8_t)eep_[ch].gain << 4) | highByte(eep_[ch].data)); 187 | wire_->write(lowByte(eep_[ch].data)); 188 | return wire_->endTransmission(); 189 | } 190 | 191 | private: 192 | 193 | struct DACInputData { 194 | VREF vref; 195 | PWR_DOWN pd; 196 | GAIN gain; 197 | uint16_t data; 198 | }; 199 | 200 | const uint8_t I2C_ADDR {0x60}; 201 | 202 | uint8_t addr_ {I2C_ADDR}; 203 | int8_t pin_ldac_; 204 | 205 | DACInputData reg_[4]; 206 | DACInputData eep_[4]; 207 | DACInputData read_reg_[4]; 208 | DACInputData read_eep_[4]; 209 | 210 | TwoWire* wire_; 211 | 212 | }; 213 | 214 | #endif 215 | -------------------------------------------------------------------------------- /lib/MultiPointMap.cpp: -------------------------------------------------------------------------------- 1 | #ifndef MultiPointMap_h 2 | #define MultiPointMap_h 3 | 4 | #include "Arduino.h" 5 | #include 6 | 7 | class MultiPointMap { 8 | 9 | public: 10 | 11 | /** 12 | * Initialize a function that maps values using a multi-linear scale defined by equidistant 13 | * fixed points along the specified range. This is used to implement DACs calibration, and 14 | * it's adapted from Befaco MIDI Thing and Emilie Gillet's CVpal. 15 | */ 16 | void init(uint16_t range = 4000) { 17 | this->step = range / N; // Distance between two consecutive fixed points 18 | this->reset(); 19 | } 20 | 21 | /** 22 | * Map the given value to another value, interpolating between a pair of fixed points 23 | */ 24 | uint16_t map(uint16_t value) { 25 | uint8_t interval = value / this->step; // Index of the interval in which the given value falls 26 | if (interval > N - 1) interval = N - 1; 27 | int16_t a = interval == 0 ? 0 : this->points[interval - 1]; // Low interpolation point 28 | int16_t b = this->points[interval]; // High interpolation point 29 | return a + ((int32_t)(value - interval * step) * (b - a)) / step; // Linear interpolation 30 | } 31 | 32 | /** 33 | * Get the value of a fixed point 34 | */ 35 | uint16_t get(uint8_t i) { 36 | return this->points[i]; 37 | } 38 | 39 | /** 40 | * Set the value of a fixed point 41 | */ 42 | uint16_t set(uint8_t i, uint16_t value) { 43 | this->points[i] = value; 44 | } 45 | 46 | /** 47 | * Returns the distance between two consecutive points of the multi-linear scale 48 | */ 49 | uint16_t getStep() { 50 | return this->step; 51 | } 52 | 53 | /** 54 | * Return the number of points in the multi-linear scale 55 | */ 56 | uint8_t size() { 57 | return N; 58 | } 59 | 60 | /** 61 | * Load the map from the EEPROM memory starting from the given address. 62 | * If the loaded data is invalid, the points are initialized linearly. 63 | * Return the number of bytes read. 64 | */ 65 | int load(int address) { 66 | int size = 0; 67 | uint16_t checksum = 0, checksumLoaded = 0; 68 | for (uint8_t i = 0; i < N; i++) { 69 | EEPROM.get(address + size, this->points[i]); 70 | checksum += this->points[i]; 71 | size += sizeof(this->points[i]); 72 | } 73 | EEPROM.get(address + size, checksumLoaded); 74 | size += sizeof(checksumLoaded); 75 | if (checksum != checksumLoaded) { 76 | this->reset(); 77 | } 78 | return size; 79 | } 80 | 81 | /** 82 | * Write the map to the EEPROM memory starting from the given address. 83 | * Return the number of bytes written. 84 | */ 85 | int save(int address) { 86 | int size = 0; 87 | uint16_t checksum = 0; 88 | for (uint8_t i = 0; i < N; i++) { 89 | EEPROM.put(address + size, this->points[i]); 90 | checksum += this->points[i]; 91 | size += sizeof(this->points[i]); 92 | } 93 | EEPROM.put(address + size, checksum); 94 | size += sizeof(checksum); 95 | return size; 96 | } 97 | 98 | /** 99 | * Initialize the points of the multi-linear scale linearly. 100 | * The first point is assumed to be zero. 101 | */ 102 | void reset() { 103 | for (uint8_t i = 0; i < N; i++) { 104 | this->points[i] = (i + 1) * this->getStep(); 105 | } 106 | } 107 | 108 | private: 109 | 110 | static const uint8_t N = 8; 111 | 112 | uint16_t step; 113 | uint16_t points[N]; 114 | 115 | }; 116 | 117 | #endif 118 | -------------------------------------------------------------------------------- /lib/SR74HC595.cpp: -------------------------------------------------------------------------------- 1 | #ifndef SR74HC595_h 2 | #define SR74HC595_h 3 | 4 | #include "Arduino.h" 5 | 6 | class SR74HC595 { 7 | 8 | public: 9 | 10 | /** 11 | * Setup the shift register interface. The "clock pin" is for the shift register 12 | * clock (SCK), the "latch pin" is for the storage register clock (RCK). 13 | * https://www.arduino.cc/en/Tutorial/ShiftOut 14 | */ 15 | void init(byte dataPin, byte clockPin, byte latchPin) { 16 | 17 | this->dataPin = dataPin; 18 | this->clockPin = clockPin; 19 | this->latchPin = latchPin; 20 | 21 | pinMode(this->dataPin, OUTPUT); 22 | pinMode(this->clockPin, OUTPUT); 23 | pinMode(this->latchPin, OUTPUT); 24 | 25 | } 26 | 27 | /** 28 | * Writes 8 bits to the shift register, and enables the storage register when finished (latch). 29 | * The default order is MSBFIRST (most significant bit first), but it can be changed to LSBFIRST. 30 | */ 31 | void write(byte value, uint8_t order = MSBFIRST) { 32 | digitalWrite(latchPin, LOW); // So the outputs don't change while sending in bits 33 | shiftOut(this->dataPin, this->clockPin, order, value); 34 | digitalWrite(latchPin, HIGH); // The outputs update at once 35 | } 36 | 37 | private: 38 | byte dataPin; 39 | byte clockPin; 40 | byte latchPin; 41 | 42 | }; 43 | 44 | #endif -------------------------------------------------------------------------------- /midi4plus1/3d/plate.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/midi4plus1/3d/plate.fcstd -------------------------------------------------------------------------------- /midi4plus1/3d/plate.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/midi4plus1/3d/plate.stl -------------------------------------------------------------------------------- /midi4plus1/README.md: -------------------------------------------------------------------------------- 1 | MIDI 4+1 2 | ======== 3 | 4 | A DIY Arduino-powered MIDI to 4x CV/gate interface in 6 HP, with both polyphonic and monophonic voice allocation modes. 5 | 6 | **[Arduino code][1]** | **[BOM][2]** | [🛒 **Buy PCB and panel on Tindie**][7] | **[ModularGrid][6]** | **[YouTube demo][5]** | [🗨️ **Mod Wiggler**][3] | [🗨️ **Lines**][4] 7 | 8 | [1]: midi4plus1.ino 9 | [2]: midi4plus1-bom.csv 10 | [3]: https://www.modwiggler.com/forum/viewtopic.php?t=231861 11 | [4]: https://llllllll.co/t/midi-4-1-arduino-powered-polyphonic-and-monophonic-midi-to-4x-cv-gate-interface-in-6hp/32543 12 | [5]: https://youtu.be/g9WwDo7eYi4 13 | [6]: https://www.modulargrid.net/e/joeseggiola-midi-4-1 14 | [7]: https://www.tindie.com/products/joeseggiola/midi-41-a-4x-cvgate-interface-pcb-panel/ 15 | 16 | Features and limitations 17 | ------------------------ 18 | 19 | * Single MIDI input via DIN or [TRS Type A](https://www.midi.org/midi-articles/trs-specification-adopted-and-released) (now the MIDI standard). 20 | * Four 1V/oct CV/gate outputs with gate LEDs. 21 | * Button for cycling through five different modes, with coloured mode LED: 22 | * **Poly** (red): four-voices polyphony with priority to last, LRU strategy and voice stealing; 23 | * **Poly-first** (green): four-voices polyphony with priority to first and first-available strategy; 24 | * **Split poly+mono** (blue): split keyboard with three-voices polyphony on the left, and monophony on the right (priority to last); 25 | * **Split mono+poly** (pink): same as above, but flipped; 26 | * **Mono** (teal): four independent monophonic allocators, one for each MIDI channel 1 to 4. 27 | * Additional output which can work as one of the following: 28 | * Gate output that stays high while at least one polyphonic voice is active (logic OR), useful for single-filter setups; 29 | * Trigger output for MIDI clock, with customizable PPQ (can be enabled [in code](midi4plus1.ino#L23)). 30 | * Voices lock with a long-press of the mode button: all gates of currently held polyphonic voices stay high, ignoring key releases until next reallocation. 31 | 32 | The DACs range is 0-4V, so only the 4 center octaves are covered. To get more, it is necessary to add amplifiers 33 | on CV outputs. Split modes splits the keyboard on middle C. Both lowest MIDI octave and split octave are easily 34 | configurable in code. 35 | 36 | Uploading a new sketch requires disconnecting the MIDI input circuit from the Arduino RX pin: there's a jumper on the back of the PCB that can be moved from "MIDI" to "PROG". 37 | 38 | ### Calibration procedure 39 | 40 | Power on your modular system with the button pressed: the mode LED lights up in white to show you entered the DACs calibration procedure. Gate LEDs show which CV output is currently being calibrated. The procedure requires measuring the output voltage using a multimeter with mV precision (0.001V). 41 | 42 | The first CV output should be around 0.5V: use a MIDI instrument to send any note below middle C (MIDI note 60, usually C4 or C3) to decrease the measured value, or any note above middle C to increase it, until you get exactly 0.500V. Now press the button to advance to the next calibration point, that is 1.000V, and adjust the measured value again. There are 8 calibration points for each CV output (configurable in code), after which the calibration process is repeated for the second, third and fourth CV output, as shown by gate LEDs. 43 | 44 | At the end of the whole procedure, the module will reboot itself and all CV outputs will track 1V/oct accurately. Calibration data is stored in Arduino EEPROM memory, so it's persisted across reboots and won't be lost by uploading a new sketch. 45 | 46 | Schematic 47 | --------- 48 | 49 | ![](schematic.png) 50 | 51 | Pictures 52 | -------- 53 | 54 | ### New [PCB](pcb/) build 55 | 56 | [🛒 **Buy PCB and panel on Tindie**][7] 57 | 58 | 59 | 60 | ### Old [3D-printed](3d/) build 61 | 62 | 63 | 64 | Thanks 65 | ------ 66 | 67 | - Emilie Gillet's [CVpal][10] (polyphonic voice allocator, monophonic notes stack, calibration) 68 | - Befaco [MIDI Thing][11] (form factor, calibration) 69 | - François Best's [Arduino MIDI Library][12] 70 | 71 | [10]: https://github.com/pichenettes/cvpal 72 | [11]: https://github.com/Befaco/midithing 73 | [12]: https://github.com/FortySevenEffects/arduino_midi_library 74 | -------------------------------------------------------------------------------- /midi4plus1/lib/Button.cpp: -------------------------------------------------------------------------------- 1 | #ifndef Button_h 2 | #define Button_h 3 | 4 | #include "Arduino.h" 5 | 6 | class Button { 7 | 8 | public: 9 | 10 | /** 11 | * Setup the button, specifying and optional debounce delay 12 | */ 13 | void init(byte pin, unsigned int debounceDelayMs = 0, bool invert = false, bool internalPullup = false) { 14 | 15 | this->pin = pin; 16 | this->debounceDelayMs = debounceDelayMs; 17 | this->invert = invert; 18 | 19 | this->lastPressedMs = 0; 20 | this->longPressStartMs = 0; 21 | this->shortOrLongPressStartMs = 0; 22 | 23 | this->readOnceFlag = false; 24 | this->readLongPressOnceFlag = false; 25 | this->readShortOrLongPressOnceFlag = false; 26 | 27 | pinMode(this->pin, internalPullup ? INPUT_PULLUP : INPUT); 28 | 29 | } 30 | 31 | /** 32 | * Get the button state, TRUE if the pin is HIGH. 33 | * Immediately reads presses, but the release can be delayed according to debouncing. 34 | */ 35 | bool read() { 36 | 37 | bool reading = digitalRead(this->pin); 38 | if (this->invert) reading = !reading; 39 | 40 | if (reading) { 41 | 42 | // Pressed: return TRUE 43 | this->lastPressedMs = millis(); // Remember time for debouncing 44 | if (this->longPressStartMs == 0) this->longPressStartMs = millis(); // Start long press detection 45 | return true; 46 | 47 | } else { 48 | 49 | // Released: wait for debouncing and return FALSE 50 | if (this->lastPressedMs > 0) { 51 | if (millis() - this->lastPressedMs >= debounceDelayMs) { 52 | this->lastPressedMs = 0; // Reset remembered time 53 | this->longPressStartMs = 0; // Stop long press detection 54 | } else { 55 | return true; // Waiting for debouncing... 56 | } 57 | } 58 | 59 | } 60 | 61 | return false; 62 | 63 | } 64 | 65 | /** 66 | * Same as read(), but returns TRUE only once, until the button is released. 67 | */ 68 | bool readOnce() { 69 | if (this->read()) { 70 | if (!this->readOnceFlag) { 71 | this->readOnceFlag = true; 72 | return true; 73 | } 74 | } else { 75 | this->readOnceFlag = false; 76 | } 77 | return false; 78 | } 79 | 80 | /** 81 | * Detect button long press, TRUE if the pin was HIGH for longer than given duration. 82 | */ 83 | bool readLongPress(unsigned long durationMs) { 84 | if (this->read()) { 85 | if (millis() - this->longPressStartMs >= durationMs) { 86 | return true; 87 | } 88 | } 89 | return false; 90 | } 91 | 92 | /** 93 | * Same as readLongPress(), but returns TRUE only once, until the button is released. 94 | */ 95 | bool readLongPressOnce(unsigned long durationMs) { 96 | if (this->readLongPress(durationMs)) { 97 | if (!this->readLongPressOnceFlag) { 98 | this->readLongPressOnceFlag = true; 99 | return true; 100 | } 101 | } else { 102 | this->readLongPressOnceFlag = false; 103 | } 104 | return false; 105 | } 106 | 107 | /* 108 | * A combined readOnce() and readLongPressOnce() for a multi-purpose button. 109 | * Returns 1 when the button is released before specified duration (short press). 110 | * Returns 2 as soon as the button has been pressed for specified duration. 111 | * Returns 0 in subsequent calls, while idle or while being pressed. 112 | */ 113 | byte readShortOrLongPressOnce(unsigned long longPressDurationMs) { 114 | byte r = 0; 115 | if (this->read()) { 116 | if (!this->readShortOrLongPressOnceFlag) { 117 | if (this->shortOrLongPressStartMs == 0) { 118 | this->shortOrLongPressStartMs = millis(); 119 | } else { 120 | if (millis() - this->shortOrLongPressStartMs >= longPressDurationMs) { 121 | this->readShortOrLongPressOnceFlag = true; 122 | r = 2; 123 | } 124 | } 125 | } 126 | } else { 127 | if (!this->readShortOrLongPressOnceFlag) { 128 | if (this->shortOrLongPressStartMs != 0) { 129 | r = 1; 130 | } 131 | } 132 | this->shortOrLongPressStartMs = 0; 133 | this->readShortOrLongPressOnceFlag = false; 134 | } 135 | return r; 136 | } 137 | 138 | private: 139 | byte pin; 140 | unsigned int debounceDelayMs; 141 | bool invert; 142 | unsigned long lastPressedMs; 143 | unsigned long longPressStartMs; 144 | unsigned long shortOrLongPressStartMs; 145 | bool readOnceFlag; 146 | bool readLongPressOnceFlag; 147 | bool readShortOrLongPressOnceFlag; 148 | 149 | }; 150 | 151 | #endif 152 | -------------------------------------------------------------------------------- /midi4plus1/lib/Led.cpp: -------------------------------------------------------------------------------- 1 | #ifndef Led_h 2 | #define Led_h 3 | 4 | #include "Arduino.h" 5 | 6 | class Led { 7 | 8 | public: 9 | 10 | /** 11 | * Setup the LED, specifying and optional minimum "on" duration for user visibility 12 | */ 13 | void init(byte pin, unsigned int minDurationMs = 0) { 14 | 15 | this->pin = pin; 16 | this->minDurationMs = minDurationMs; 17 | 18 | this->state = false; 19 | this->stateHardware = false; 20 | this->blinkMs = 0; 21 | this->lastOnMs = 0; 22 | 23 | pinMode(this->pin, OUTPUT); 24 | digitalWrite(this->pin, LOW); 25 | 26 | } 27 | 28 | /** 29 | * Turn the LED on or off. 30 | * If a minimum duration was set, it could not turn off if it was 31 | * on for too little, you need to call loop() to update the state. 32 | */ 33 | void set(bool state) { 34 | 35 | this->blinkMs = 0; // Stop blinking 36 | this->state = state; 37 | 38 | if (state) { 39 | 40 | // Remember last time it was requested to be on 41 | this->lastOnMs = millis(); 42 | 43 | // Turn on the LED if necessary 44 | if (!this->stateHardware) { 45 | this->stateHardware = true; 46 | digitalWrite(this->pin, HIGH); 47 | } 48 | 49 | } else { 50 | 51 | // Turn the LED off if necessary 52 | this->loop(); 53 | 54 | } 55 | 56 | } 57 | 58 | /** 59 | * Starts blinking with given period, until any other method is called. 60 | * Use duty to specify how long the LED will be on, and invert to flip the blinking phase. 61 | * This method can also be used make it fade, using a short period and duty to adjust brightness. 62 | * Make sure to call loop() to keep the LED blinking. 63 | */ 64 | void blink(unsigned int periodMs, float duty = 0.5, bool invert = false) { 65 | this->blinkMs = periodMs; 66 | this->blinkDuty = max(0, min(periodMs, duty * periodMs)); 67 | this->blinkStartedMs = millis() - (invert ? this->blinkDuty : 0); 68 | } 69 | 70 | /** 71 | * Turn the LED off if necessary, or keep it blinking. 72 | * Call this in the main loop. 73 | */ 74 | void loop() { 75 | 76 | if (this->blinkMs > 0) { 77 | 78 | unsigned long t = ((millis() - this->blinkStartedMs) % this->blinkMs); 79 | this->stateHardware = t < this->blinkDuty; 80 | digitalWrite(this->pin, this->stateHardware); 81 | 82 | } else { 83 | 84 | // Turn the LED off if necessary 85 | if (!this->state && this->stateHardware) { 86 | if (millis() - this->lastOnMs >= this->minDurationMs) { 87 | this->stateHardware = false; 88 | digitalWrite(this->pin, LOW); 89 | } 90 | } 91 | 92 | } 93 | 94 | } 95 | 96 | void on() { 97 | this->set(true); 98 | } 99 | 100 | void off() { 101 | this->set(false); 102 | } 103 | 104 | void toggle() { 105 | this->set(!state); 106 | } 107 | 108 | /** 109 | * Turn on the LED, then turn it off immediately. 110 | * A single impulse of light will be visible if LED's minDurationMs is long enough. 111 | */ 112 | void flash() { 113 | this->set(true); 114 | this->set(false); 115 | } 116 | 117 | /** 118 | * Set the optional minimum "on" duration for user visibility 119 | */ 120 | void setMinDurationMs(unsigned int minDurationMs = 0) { 121 | this->minDurationMs = minDurationMs; 122 | this->loop(); 123 | } 124 | 125 | private: 126 | byte pin; 127 | unsigned int minDurationMs; 128 | bool state; 129 | bool stateHardware; 130 | unsigned int blinkMs; 131 | unsigned long blinkStartedMs; 132 | unsigned int blinkDuty; 133 | unsigned long lastOnMs; 134 | 135 | }; 136 | 137 | #endif -------------------------------------------------------------------------------- /midi4plus1/lib/MCP4728.cpp: -------------------------------------------------------------------------------- 1 | 2 | // LIBRARY FOR MCP4728 3 | // Link: https://github.com/hideakitai/MCP4728 4 | // Author: Hideaki Tai 5 | // License: MIT (https://github.com/hideakitai/MCP4728/blob/master/LICENSE) 6 | // Extended by Joe Seggiola to include optional LDAC 7 | 8 | #pragma once 9 | #ifndef MCP4728_H 10 | #define MCP4728_H 11 | 12 | #include "Arduino.h" 13 | #include 14 | 15 | class MCP4728 { 16 | 17 | public: 18 | 19 | enum class CMD { 20 | FAST_WRITE = 0x00, 21 | MULTI_WRITE = 0x40, 22 | SINGLE_WRITE = 0x58, 23 | SEQ_WRITE = 0x50, 24 | SELECT_VREF = 0x80, 25 | SELECT_GAIN = 0xC0, 26 | SELECT_PWRDOWN = 0xA0 27 | }; 28 | 29 | enum class VREF { VDD, INTERNAL_2_8V }; 30 | enum class PWR_DOWN { NORMAL, GND_1KOHM, GND_100KOHM, GND_500KOHM }; 31 | enum class GAIN { X1, X2 }; 32 | 33 | void init(TwoWire& w, uint8_t addr = 0, int8_t pin = -1) { 34 | wire_ = &w; 35 | addr_ = I2C_ADDR + addr; 36 | pin_ldac_ = pin; 37 | if (pin_ldac_ > -1) { 38 | pinMode(pin_ldac_, OUTPUT); 39 | enable(false); 40 | } 41 | readRegisters(); 42 | } 43 | 44 | void enable(bool b) { 45 | if (pin_ldac_ > -1) { 46 | digitalWrite(pin_ldac_, !b); 47 | } 48 | } 49 | 50 | uint8_t analogWrite(uint8_t ch, uint16_t data, bool b_eep = false) { 51 | if (b_eep) { 52 | eep_[ch].data = data > 0xFFF ? 0xFFF : data; 53 | return singleWrite(ch); 54 | } else { 55 | reg_[ch].data = data > 0xFFF ? 0xFFF : data; 56 | return fastWrite(); 57 | } 58 | } 59 | 60 | uint8_t analogWrite(uint16_t a, uint16_t b, uint16_t c, uint16_t d, bool b_eep = false) { 61 | if (b_eep) { 62 | reg_[0].data = eep_[0].data = a > 0xFFF ? 0xFFF : a; 63 | reg_[1].data = eep_[1].data = b > 0xFFF ? 0xFFF : b; 64 | reg_[2].data = eep_[2].data = c > 0xFFF ? 0xFFF : c; 65 | reg_[3].data = eep_[3].data = d > 0xFFF ? 0xFFF : d; 66 | return seqWrite(); 67 | } else { 68 | reg_[0].data = a > 0xFFF ? 0xFFF : a; 69 | reg_[1].data = b > 0xFFF ? 0xFFF : b; 70 | reg_[2].data = c > 0xFFF ? 0xFFF : c; 71 | reg_[3].data = d > 0xFFF ? 0xFFF : d; 72 | return fastWrite(); 73 | } 74 | } 75 | 76 | uint8_t selectVref(VREF a, VREF b, VREF c, VREF d) { 77 | reg_[0].vref = a; 78 | reg_[1].vref = b; 79 | reg_[2].vref = c; 80 | reg_[3].vref = d; 81 | uint8_t data = (uint8_t)CMD::SELECT_VREF; 82 | for (uint8_t i = 0; i < 4; ++i) bitWrite(data, 3 - i, (uint8_t)reg_[i].vref); 83 | wire_->beginTransmission(addr_); 84 | wire_->write(data); 85 | return wire_->endTransmission(); 86 | } 87 | 88 | uint8_t selectPowerDown(PWR_DOWN a, PWR_DOWN b, PWR_DOWN c, PWR_DOWN d) { 89 | reg_[0].pd = a; 90 | reg_[1].pd = b; 91 | reg_[2].pd = c; 92 | reg_[3].pd = d; 93 | uint8_t h = ((uint8_t)CMD::SELECT_PWRDOWN) | ((uint8_t)a << 2) | (uint8_t)b; 94 | uint8_t l = 0 | ((uint8_t)c << 6) | ((uint8_t)d << 4); 95 | wire_->beginTransmission(addr_); 96 | wire_->write(h); 97 | wire_->write(l); 98 | return wire_->endTransmission(); 99 | } 100 | 101 | uint8_t selectGain(GAIN a, GAIN b, GAIN c, GAIN d) { 102 | reg_[0].gain = a; 103 | reg_[1].gain = b; 104 | reg_[2].gain = c; 105 | reg_[3].gain = d; 106 | uint8_t data = (uint8_t)CMD::SELECT_GAIN; 107 | for (uint8_t i = 0; i < 4; ++i) bitWrite(data, 3 - i, (uint8_t)reg_[i].gain); 108 | wire_->beginTransmission(addr_); 109 | wire_->write(data); 110 | return wire_->endTransmission(); 111 | } 112 | 113 | void readRegisters() { 114 | wire_->requestFrom((int)addr_, 24); 115 | if (wire_->available() == 24) { 116 | for (uint8_t i = 0; i < 8; ++i) { 117 | uint8_t data[3]; 118 | bool isEeprom = i % 2; 119 | for (uint8_t i = 0; i < 3; ++i) data[i] = wire_->read(); 120 | uint8_t ch = (data[0] & 0x30) >> 4; 121 | if (isEeprom) { 122 | read_eep_[ch].vref = (VREF) ((data[1] & 0b10000000) >> 7); 123 | read_eep_[ch].pd = (PWR_DOWN)((data[1] & 0b01100000) >> 5); 124 | read_eep_[ch].gain = (GAIN) ((data[1] & 0b00010000) >> 4); 125 | read_eep_[ch].data = (uint16_t)((data[1] & 0b00001111) << 8 | data[2]); 126 | } else { 127 | read_reg_[ch].vref = (VREF) ((data[1] & 0b10000000) >> 7); 128 | read_reg_[ch].pd = (PWR_DOWN)((data[1] & 0b01100000) >> 5); 129 | read_reg_[ch].gain = (GAIN) ((data[1] & 0b00010000) >> 4); 130 | read_reg_[ch].data = (uint16_t)((data[1] & 0b00001111) << 8 | data[2]); 131 | } 132 | } 133 | } 134 | } 135 | 136 | uint8_t getVref(uint8_t ch, bool b_eep = false) { 137 | return b_eep ? (uint8_t)read_eep_[ch].vref : (uint8_t)read_reg_[ch].vref; 138 | } 139 | 140 | uint8_t getGain(uint8_t ch, bool b_eep = false) { 141 | return b_eep ? (uint8_t)read_eep_[ch].gain: (uint8_t)read_reg_[ch].gain; 142 | } 143 | 144 | uint8_t getPowerDown(uint8_t ch, bool b_eep = false) { 145 | return b_eep ? (uint8_t)read_eep_[ch].pd : (uint8_t)read_reg_[ch].pd; 146 | } 147 | 148 | uint16_t getDACData(uint8_t ch, bool b_eep = false) { 149 | return b_eep ? (uint16_t)read_eep_[ch].data : (uint16_t)read_reg_[ch].data; 150 | } 151 | 152 | private: 153 | 154 | uint8_t fastWrite() { 155 | wire_->beginTransmission(addr_); 156 | for (uint8_t i = 0; i < 4; ++i) { 157 | wire_->write((uint8_t)CMD::FAST_WRITE | highByte(reg_[i].data)); 158 | wire_->write(lowByte(reg_[i].data)); 159 | } 160 | return wire_->endTransmission(); 161 | } 162 | 163 | uint8_t multiWrite() { 164 | wire_->beginTransmission(addr_); 165 | for (uint8_t i = 0; i < 4; ++i) { 166 | wire_->write((uint8_t)CMD::MULTI_WRITE | (i << 1)); 167 | wire_->write(((uint8_t)reg_[i].vref << 7) | ((uint8_t)reg_[i].pd << 5) | ((uint8_t)reg_[i].gain << 4) | highByte(reg_[i].data)); 168 | wire_->write(lowByte(reg_[i].data)); 169 | } 170 | return wire_->endTransmission(); 171 | } 172 | 173 | uint8_t seqWrite() { 174 | wire_->beginTransmission(addr_); 175 | wire_->write((uint8_t)CMD::SEQ_WRITE); 176 | for (uint8_t i = 0; i < 4; ++i) { 177 | wire_->write(((uint8_t)eep_[i].vref << 7) | ((uint8_t)eep_[i].pd << 5) | ((uint8_t)eep_[i].gain << 4) | highByte(eep_[i].data)); 178 | wire_->write(lowByte(eep_[i].data)); 179 | } 180 | return wire_->endTransmission(); 181 | } 182 | 183 | uint8_t singleWrite(uint8_t ch) { 184 | wire_->beginTransmission(addr_); 185 | wire_->write((uint8_t)CMD::SINGLE_WRITE | (ch << 1)); 186 | wire_->write(((uint8_t)eep_[ch].vref << 7) | ((uint8_t)eep_[ch].pd << 5) | ((uint8_t)eep_[ch].gain << 4) | highByte(eep_[ch].data)); 187 | wire_->write(lowByte(eep_[ch].data)); 188 | return wire_->endTransmission(); 189 | } 190 | 191 | private: 192 | 193 | struct DACInputData { 194 | VREF vref; 195 | PWR_DOWN pd; 196 | GAIN gain; 197 | uint16_t data; 198 | }; 199 | 200 | const uint8_t I2C_ADDR {0x60}; 201 | 202 | uint8_t addr_ {I2C_ADDR}; 203 | int8_t pin_ldac_; 204 | 205 | DACInputData reg_[4]; 206 | DACInputData eep_[4]; 207 | DACInputData read_reg_[4]; 208 | DACInputData read_eep_[4]; 209 | 210 | TwoWire* wire_; 211 | 212 | }; 213 | 214 | #endif 215 | -------------------------------------------------------------------------------- /midi4plus1/lib/MultiPointMap.cpp: -------------------------------------------------------------------------------- 1 | #ifndef MultiPointMap_h 2 | #define MultiPointMap_h 3 | 4 | #include "Arduino.h" 5 | #include 6 | 7 | class MultiPointMap { 8 | 9 | public: 10 | 11 | /** 12 | * Initialize a function that maps values using a multi-linear scale defined by equidistant 13 | * fixed points along the specified range. This is used to implement DACs calibration, and 14 | * it's adapted from Befaco MIDI Thing and Emilie Gillet's CVpal. 15 | */ 16 | void init(uint16_t range = 4000) { 17 | this->step = range / N; // Distance between two consecutive fixed points 18 | this->reset(); 19 | } 20 | 21 | /** 22 | * Map the given value to another value, interpolating between a pair of fixed points 23 | */ 24 | uint16_t map(uint16_t value) { 25 | uint8_t interval = value / this->step; // Index of the interval in which the given value falls 26 | if (interval > N - 1) interval = N - 1; 27 | int16_t a = interval == 0 ? 0 : this->points[interval - 1]; // Low interpolation point 28 | int16_t b = this->points[interval]; // High interpolation point 29 | return a + ((int32_t)(value - interval * step) * (b - a)) / step; // Linear interpolation 30 | } 31 | 32 | /** 33 | * Get the value of a fixed point 34 | */ 35 | uint16_t get(uint8_t i) { 36 | return this->points[i]; 37 | } 38 | 39 | /** 40 | * Set the value of a fixed point 41 | */ 42 | uint16_t set(uint8_t i, uint16_t value) { 43 | this->points[i] = value; 44 | } 45 | 46 | /** 47 | * Returns the distance between two consecutive points of the multi-linear scale 48 | */ 49 | uint16_t getStep() { 50 | return this->step; 51 | } 52 | 53 | /** 54 | * Return the number of points in the multi-linear scale 55 | */ 56 | uint8_t size() { 57 | return N; 58 | } 59 | 60 | /** 61 | * Load the map from the EEPROM memory starting from the given address. 62 | * If the loaded data is invalid, the points are initialized linearly. 63 | * Return the number of bytes read. 64 | */ 65 | int load(int address) { 66 | int size = 0; 67 | uint16_t checksum = 0, checksumLoaded = 0; 68 | for (uint8_t i = 0; i < N; i++) { 69 | EEPROM.get(address + size, this->points[i]); 70 | checksum += this->points[i]; 71 | size += sizeof(this->points[i]); 72 | } 73 | EEPROM.get(address + size, checksumLoaded); 74 | size += sizeof(checksumLoaded); 75 | if (checksum != checksumLoaded) { 76 | this->reset(); 77 | } 78 | return size; 79 | } 80 | 81 | /** 82 | * Write the map to the EEPROM memory starting from the given address. 83 | * Return the number of bytes written. 84 | */ 85 | int save(int address) { 86 | int size = 0; 87 | uint16_t checksum = 0; 88 | for (uint8_t i = 0; i < N; i++) { 89 | EEPROM.put(address + size, this->points[i]); 90 | checksum += this->points[i]; 91 | size += sizeof(this->points[i]); 92 | } 93 | EEPROM.put(address + size, checksum); 94 | size += sizeof(checksum); 95 | return size; 96 | } 97 | 98 | /** 99 | * Initialize the points of the multi-linear scale linearly. 100 | * The first point is assumed to be zero. 101 | */ 102 | void reset() { 103 | for (uint8_t i = 0; i < N; i++) { 104 | this->points[i] = (i + 1) * this->getStep(); 105 | } 106 | } 107 | 108 | private: 109 | 110 | static const uint8_t N = 8; 111 | 112 | uint16_t step; 113 | uint16_t points[N]; 114 | 115 | }; 116 | 117 | #endif -------------------------------------------------------------------------------- /midi4plus1/midi4plus1-bom.csv: -------------------------------------------------------------------------------- 1 | Type,Description,Name,Value,Qty,Notes / Link 2 | IC,Arduino Nano,,,1,https://store.arduino.cc/products/arduino-nano 3 | IC,MIDI optocoupler,H11L1M,,1,"Or 6N139, but it requires a PCB modification" 4 | IC,DAC breakout board,GY-MCP4728,,1,https://www.aliexpress.com/item/33007068899.html 5 | Capacitor,"Bypass, ceramic",BYP,100nF,1,"Optional, close values should work the same" 6 | Capacitor,"Bypass, electrolytic",BYP,10µF,1,"Optional, close values should work the same" 7 | Diode,On MIDI input,1N4148,,1,"Or 1N914, or anything similar" 8 | Resistor,On MIDI input,,220Ω,2,Close values should work the same 9 | Resistor,For RGB LED,RML*,1kΩ,3,Adjust values depending on desired brightness 10 | Resistor,For gates LEDs,RL1..4,1kΩ,4,Adjust value depending on desired brightness 11 | Resistor,For gate-OR LED,RLO,1kΩ,1,Adjust value depending on desired brightness 12 | Button,"Tactile switch, 14mm",BTN,,1,Or shorter if glued to the panel or lifted using a piece of stripboard 13 | Connector,IDC 10 pin socket,,,1,"Eurorack power socket, or 2x5 male pin header" 14 | Connector,Jumper,MIDI / PROG,,1,Jumper to select MIDI or PROG for sketch uploading 15 | Connector,DIN 5-pin socket,MIDI-DIN,,1,https://www.aliexpress.com/item/32954620723.html 16 | Connector,Pin header 1x3 male,MIDI / PROG,,1,"For the jumper, you can break a longer one" 17 | Connector,Pin header 1x8 male,,,1,You can break a longer one 18 | Connector,Pin header 1x8 female,,,1,You can break a longer one 19 | Connector,Pin header 1x12 male,,,2,You can break a longer one 20 | Connector,Pin header 1x12 female,,,2,You can break a longer one 21 | Connector,Pin header 1x13 male,,,1,You can break a longer one 22 | Connector,Pin header 1x13 female,,,1,You can break a longer one 23 | Jack,CV and gates,PJ398SM,,9,https://www.thonk.co.uk/shop/thonkiconn/ 24 | Jack,MIDI TRS-A,PJ366ST,,1,https://www.thonk.co.uk/shop/thonkiconn/ 25 | LED,"Gates LEDs, 3mm",LED1..4,,4,"Red, or any colour you like" 26 | LED,"Gate-OR LED, 3mm",L-OR,,1,"Yellow, or any colour you like" 27 | LED,"Mode LED, 5mm RGB",,,1,Mouser: 604-WP154A4SUREQBFZW 28 | -------------------------------------------------------------------------------- /midi4plus1/mono.cpp: -------------------------------------------------------------------------------- 1 | #ifndef mono_h 2 | #define mono_h 3 | 4 | #include "Arduino.h" 5 | 6 | // Monophonic notes stack. 7 | 8 | // Based on Emilie Gillet's CVpal: 9 | // https://github.com/pichenettes/cvpal/blob/master/cvpal/note_stack.h 10 | 11 | #define CAPACITY 10 12 | #define FREE 0xFF 13 | 14 | class NoteStack { 15 | 16 | public: 17 | 18 | /** 19 | * Constructor 20 | */ 21 | void init() { 22 | this->clear(); 23 | } 24 | 25 | /** 26 | * Handle an incoming MIDI note and add it to the stack in the front position. 27 | */ 28 | void noteOn(byte note) { 29 | 30 | // Remove the note from the list first (in case it is already here) 31 | this->noteOff(note); 32 | 33 | // In case of saturation, remove the least recently played note 34 | if (this->size == CAPACITY) { 35 | byte leastRecentNote; 36 | for (byte i = 1; i <= CAPACITY; i++) { 37 | if (this->next[i] == 0) { 38 | leastRecentNote = this->note[i]; 39 | } 40 | } 41 | this->noteOff(leastRecentNote); 42 | } 43 | 44 | // Find a free slot to insert the new note 45 | byte freeSlot; 46 | for (byte i = 1; i <= CAPACITY; i++) { 47 | if (this->note[i] == FREE) { 48 | freeSlot = i; 49 | break; 50 | } 51 | } 52 | this->next[freeSlot] = this->root; // After the new note there will be the one that is currently the first 53 | this->note[freeSlot] = note; 54 | this->root = freeSlot; // The free slot now contains the first note 55 | this->size++; 56 | 57 | } 58 | 59 | /** 60 | * Handle an outgoing MIDI note and remove it from the stack. 61 | * Returns the most recent note after removing this, or -1 if there's no new note to play. 62 | */ 63 | int noteOff(byte note) { 64 | 65 | // Search the note to remove, and the one before it 66 | byte current = this->root; 67 | byte previous = 0; 68 | while (current) { 69 | if (this->note[current] == note) break; 70 | previous = current; 71 | current = this->next[current]; 72 | } 73 | 74 | if (current) { 75 | 76 | // Skip the note that will be removed 77 | if (previous) { 78 | this->next[previous] = this->next[current]; 79 | } else { 80 | this->root = this->next[current]; 81 | } 82 | 83 | // Free the slot 84 | this->next[current] = 0; 85 | this->note[current] = FREE; 86 | this->size--; 87 | 88 | } 89 | 90 | // Return the most recent note now, or -1 if none 91 | return this->size > 0 ? this->note[this->root] : -1; 92 | 93 | } 94 | 95 | /** 96 | * Clear the stack 97 | */ 98 | void clear() { 99 | this->size = 0; 100 | this->root = 0; 101 | for (byte i = 0; i < CAPACITY; i++) { 102 | this->note[i] = FREE; 103 | } 104 | } 105 | 106 | private: 107 | byte size; 108 | byte root; // Pointer (index) to head, base 1 109 | byte note[CAPACITY + 1]; // Note values, first element is a dummy note 110 | byte next[CAPACITY + 1]; // Pointers (index) to the next note, base 1 111 | 112 | }; 113 | 114 | #endif -------------------------------------------------------------------------------- /midi4plus1/pcb/board-gerber.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/midi4plus1/pcb/board-gerber.zip -------------------------------------------------------------------------------- /midi4plus1/pcb/panel-font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/midi4plus1/pcb/panel-font.ttf -------------------------------------------------------------------------------- /midi4plus1/pcb/panel-gerber.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/midi4plus1/pcb/panel-gerber.zip -------------------------------------------------------------------------------- /midi4plus1/pcb/panel.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 38 | 40 | 41 | 43 | image/svg+xml 44 | 46 | 47 | 48 | 49 | 50 | 55 | 66 | MIDI 4+1 79 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /midi4plus1/pictures/IMG_20200307_181436.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/midi4plus1/pictures/IMG_20200307_181436.jpg -------------------------------------------------------------------------------- /midi4plus1/pictures/IMG_20200413_181507.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/midi4plus1/pictures/IMG_20200413_181507.jpg -------------------------------------------------------------------------------- /midi4plus1/pictures/IMG_20210620_175313.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/midi4plus1/pictures/IMG_20210620_175313.jpg -------------------------------------------------------------------------------- /midi4plus1/pictures/IMG_20210620_175313_T.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/midi4plus1/pictures/IMG_20210620_175313_T.jpg -------------------------------------------------------------------------------- /midi4plus1/pictures/IMG_20210621_140800.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/midi4plus1/pictures/IMG_20210621_140800.jpg -------------------------------------------------------------------------------- /midi4plus1/pictures/IMG_20210621_140800_M.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/midi4plus1/pictures/IMG_20210621_140800_M.jpg -------------------------------------------------------------------------------- /midi4plus1/pictures/IMG_20210621_140800_T.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/midi4plus1/pictures/IMG_20210621_140800_T.jpg -------------------------------------------------------------------------------- /midi4plus1/pictures/IMG_20210621_140842.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/midi4plus1/pictures/IMG_20210621_140842.jpg -------------------------------------------------------------------------------- /midi4plus1/pictures/IMG_20210621_140903.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/midi4plus1/pictures/IMG_20210621_140903.jpg -------------------------------------------------------------------------------- /midi4plus1/pictures/IMG_20210621_140903_T.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/midi4plus1/pictures/IMG_20210621_140903_T.jpg -------------------------------------------------------------------------------- /midi4plus1/pictures/IMG_20210901_124851.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/midi4plus1/pictures/IMG_20210901_124851.jpg -------------------------------------------------------------------------------- /midi4plus1/poly.cpp: -------------------------------------------------------------------------------- 1 | #ifndef poly_h 2 | #define poly_h 3 | 4 | #include "Arduino.h" 5 | 6 | // Polyphonic voice allocator with two modes: 7 | // - LRU strategy and voice stealing, i.e. priority to last 8 | // - First-available strategy and priority to first 9 | 10 | // Based on Emilie Gillet's CVpal: 11 | // https://github.com/pichenettes/cvpal/blob/master/cvpal/voice_allocator.cc 12 | 13 | #define MAX 4 14 | 15 | class VoiceAllocator { 16 | 17 | public: 18 | 19 | enum class Mode { LAST, FIRST }; 20 | 21 | /** 22 | * Constructor 23 | */ 24 | void init() { 25 | this->setMode(Mode::LAST); 26 | this->setSize(0); 27 | this->clear(); 28 | for (byte i = 0; i < MAX; i++) { 29 | this->note[i] = 12; // C0 30 | } 31 | } 32 | 33 | /** 34 | * Set the allocation mode. 35 | * - Mode::LAST for LRU strategy and voice stealing, i.e. priority to last 36 | * - Mode::FIRST for first-available strategy and priority to first 37 | */ 38 | void setMode(Mode mode) { 39 | this->mode = mode; 40 | } 41 | 42 | /** 43 | * Set the polyphony size, i.e. the total number of available voices 44 | */ 45 | void setSize(byte size) { 46 | this->size = min(MAX, size); 47 | } 48 | 49 | /** 50 | * Handle an incoming MIDI note and returns the index of the allocated voice. 51 | * Returns -1 if no voice has been allocated. 52 | */ 53 | int noteOn(byte note) { 54 | 55 | if (this->size == 0) return -1; 56 | 57 | int voice = -1; 58 | 59 | if (this->mode == Mode::LAST) { 60 | 61 | // Check if there is a voice currently playing this note 62 | voice = this->find(note); 63 | 64 | // Try to find the least recently touched, currently inactive voice 65 | if (voice == -1) { 66 | for (byte i = 0; i < MAX; i++) { 67 | if (this->lru[i] < this->size && !this->active[this->lru[i]]) { 68 | voice = this->lru[i]; 69 | } 70 | } 71 | } 72 | 73 | // If all voices are active, use the least recently played note 74 | if (voice == -1) { 75 | for (byte i = 0; i < MAX; i++) { 76 | if (this->lru[i] < this->size) { 77 | voice = this->lru[i]; 78 | } 79 | } 80 | } 81 | 82 | // Mark the chosen voice as recently used 83 | this->touch(voice); 84 | 85 | } else if (this->mode == Mode::FIRST) { 86 | 87 | // Try to find the first currently inactive voice 88 | for (byte i = 0; i < this->size; i++) { 89 | if (!this->active[i]) { 90 | voice = i; 91 | break; 92 | } 93 | } 94 | 95 | // In case all voices are active, the new note will not be played 96 | if (voice == -1) { 97 | return -1; 98 | } 99 | 100 | } 101 | 102 | // Allocate the note 103 | this->note[voice] = note; 104 | this->active[voice] = true; 105 | 106 | return voice; 107 | 108 | } 109 | 110 | /** 111 | * Handle an outgoing MIDI note and returns the index of the freed voice. 112 | * Returns -1 if no voice has been freed. 113 | */ 114 | int noteOff(byte note) { 115 | int voice = this->find(note); 116 | if (voice != -1) { 117 | this->active[voice] = false; 118 | 119 | // Mark the freed voice as recently used 120 | if (this->mode == Mode::LAST) { 121 | this->touch(voice); 122 | } 123 | 124 | } 125 | return voice; 126 | } 127 | 128 | /** 129 | * Clear allocation state, i.e. sets all voices inactive and resets LRU order. 130 | */ 131 | void clear() { 132 | for (byte i = 0; i < MAX; i++) { 133 | this->active[i] = false; 134 | this->lru[i] = MAX - i - 1; 135 | } 136 | } 137 | 138 | private: 139 | 140 | int find(byte note) { 141 | for (byte i = 0; i < this->size; i++) { 142 | if (this->note[i] == note) return i; 143 | } 144 | return -1; 145 | } 146 | 147 | void touch(byte voice) { 148 | int s = MAX - 1; 149 | int d = MAX - 1; 150 | while (s >= 0) { 151 | if (this->lru[s] != voice) this->lru[d--] = this->lru[s]; 152 | s--; 153 | } 154 | this->lru[0] = voice; 155 | } 156 | 157 | private: 158 | Mode mode; 159 | byte size; // Number of available voices 160 | byte note[MAX]; // Note values for each voice 161 | bool active[MAX]; // Active state for each voice 162 | byte lru[MAX]; // Indexes of voices sorted by most recent usage 163 | 164 | }; 165 | 166 | #endif -------------------------------------------------------------------------------- /midi4plus1/schematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeSeggiola/arduino-eurorack-projects/f00040d036361e3ff976e256f3cbfcbadfedf8b5/midi4plus1/schematic.png -------------------------------------------------------------------------------- /tools/mcp4728_addr/MCP4728.cpp: -------------------------------------------------------------------------------- 1 | 2 | // LIBRARY FOR MCP4728 3 | // Link: https://github.com/hideakitai/MCP4728 4 | // Author: Hideaki Tai 5 | // License: MIT (https://github.com/hideakitai/MCP4728/blob/master/LICENSE) 6 | // Extended by Joe Seggiola to include optional LDAC 7 | 8 | #pragma once 9 | #ifndef MCP4728_H 10 | #define MCP4728_H 11 | 12 | #include "Arduino.h" 13 | #include 14 | 15 | class MCP4728 { 16 | 17 | public: 18 | 19 | enum class CMD { 20 | FAST_WRITE = 0x00, 21 | MULTI_WRITE = 0x40, 22 | SINGLE_WRITE = 0x58, 23 | SEQ_WRITE = 0x50, 24 | SELECT_VREF = 0x80, 25 | SELECT_GAIN = 0xC0, 26 | SELECT_PWRDOWN = 0xA0 27 | }; 28 | 29 | enum class VREF { VDD, INTERNAL_2_8V }; 30 | enum class PWR_DOWN { NORMAL, GND_1KOHM, GND_100KOHM, GND_500KOHM }; 31 | enum class GAIN { X1, X2 }; 32 | 33 | void init(TwoWire& w, uint8_t addr = 0, int8_t pin = -1) { 34 | wire_ = &w; 35 | addr_ = I2C_ADDR + addr; 36 | pin_ldac_ = pin; 37 | if (pin_ldac_ > -1) { 38 | pinMode(pin_ldac_, OUTPUT); 39 | enable(false); 40 | } 41 | readRegisters(); 42 | } 43 | 44 | void enable(bool b) { 45 | if (pin_ldac_ > -1) { 46 | digitalWrite(pin_ldac_, !b); 47 | } 48 | } 49 | 50 | uint8_t analogWrite(uint8_t ch, uint16_t data, bool b_eep = false) { 51 | if (b_eep) { 52 | eep_[ch].data = data > 0xFFF ? 0xFFF : data; 53 | return singleWrite(ch); 54 | } else { 55 | reg_[ch].data = data > 0xFFF ? 0xFFF : data; 56 | return fastWrite(); 57 | } 58 | } 59 | 60 | uint8_t analogWrite(uint16_t a, uint16_t b, uint16_t c, uint16_t d, bool b_eep = false) { 61 | if (b_eep) { 62 | reg_[0].data = eep_[0].data = a > 0xFFF ? 0xFFF : a; 63 | reg_[1].data = eep_[1].data = b > 0xFFF ? 0xFFF : b; 64 | reg_[2].data = eep_[2].data = c > 0xFFF ? 0xFFF : c; 65 | reg_[3].data = eep_[3].data = d > 0xFFF ? 0xFFF : d; 66 | return seqWrite(); 67 | } else { 68 | reg_[0].data = a > 0xFFF ? 0xFFF : a; 69 | reg_[1].data = b > 0xFFF ? 0xFFF : b; 70 | reg_[2].data = c > 0xFFF ? 0xFFF : c; 71 | reg_[3].data = d > 0xFFF ? 0xFFF : d; 72 | return fastWrite(); 73 | } 74 | } 75 | 76 | uint8_t selectVref(VREF a, VREF b, VREF c, VREF d) { 77 | reg_[0].vref = a; 78 | reg_[1].vref = b; 79 | reg_[2].vref = c; 80 | reg_[3].vref = d; 81 | uint8_t data = (uint8_t)CMD::SELECT_VREF; 82 | for (uint8_t i = 0; i < 4; ++i) bitWrite(data, 3 - i, (uint8_t)reg_[i].vref); 83 | wire_->beginTransmission(addr_); 84 | wire_->write(data); 85 | return wire_->endTransmission(); 86 | } 87 | 88 | uint8_t selectPowerDown(PWR_DOWN a, PWR_DOWN b, PWR_DOWN c, PWR_DOWN d) { 89 | reg_[0].pd = a; 90 | reg_[1].pd = b; 91 | reg_[2].pd = c; 92 | reg_[3].pd = d; 93 | uint8_t h = ((uint8_t)CMD::SELECT_PWRDOWN) | ((uint8_t)a << 2) | (uint8_t)b; 94 | uint8_t l = 0 | ((uint8_t)c << 6) | ((uint8_t)d << 4); 95 | wire_->beginTransmission(addr_); 96 | wire_->write(h); 97 | wire_->write(l); 98 | return wire_->endTransmission(); 99 | } 100 | 101 | uint8_t selectGain(GAIN a, GAIN b, GAIN c, GAIN d) { 102 | reg_[0].gain = a; 103 | reg_[1].gain = b; 104 | reg_[2].gain = c; 105 | reg_[3].gain = d; 106 | uint8_t data = (uint8_t)CMD::SELECT_GAIN; 107 | for (uint8_t i = 0; i < 4; ++i) bitWrite(data, 3 - i, (uint8_t)reg_[i].gain); 108 | wire_->beginTransmission(addr_); 109 | wire_->write(data); 110 | return wire_->endTransmission(); 111 | } 112 | 113 | void readRegisters() { 114 | wire_->requestFrom((int)addr_, 24); 115 | if (wire_->available() == 24) { 116 | for (uint8_t i = 0; i < 8; ++i) { 117 | uint8_t data[3]; 118 | bool isEeprom = i % 2; 119 | for (uint8_t i = 0; i < 3; ++i) data[i] = wire_->read(); 120 | uint8_t ch = (data[0] & 0x30) >> 4; 121 | if (isEeprom) { 122 | read_eep_[ch].vref = (VREF) ((data[1] & 0b10000000) >> 7); 123 | read_eep_[ch].pd = (PWR_DOWN)((data[1] & 0b01100000) >> 5); 124 | read_eep_[ch].gain = (GAIN) ((data[1] & 0b00010000) >> 4); 125 | read_eep_[ch].data = (uint16_t)((data[1] & 0b00001111) << 8 | data[2]); 126 | } else { 127 | read_reg_[ch].vref = (VREF) ((data[1] & 0b10000000) >> 7); 128 | read_reg_[ch].pd = (PWR_DOWN)((data[1] & 0b01100000) >> 5); 129 | read_reg_[ch].gain = (GAIN) ((data[1] & 0b00010000) >> 4); 130 | read_reg_[ch].data = (uint16_t)((data[1] & 0b00001111) << 8 | data[2]); 131 | } 132 | } 133 | } 134 | } 135 | 136 | uint8_t getVref(uint8_t ch, bool b_eep = false) { 137 | return b_eep ? (uint8_t)read_eep_[ch].vref : (uint8_t)read_reg_[ch].vref; 138 | } 139 | 140 | uint8_t getGain(uint8_t ch, bool b_eep = false) { 141 | return b_eep ? (uint8_t)read_eep_[ch].gain: (uint8_t)read_reg_[ch].gain; 142 | } 143 | 144 | uint8_t getPowerDown(uint8_t ch, bool b_eep = false) { 145 | return b_eep ? (uint8_t)read_eep_[ch].pd : (uint8_t)read_reg_[ch].pd; 146 | } 147 | 148 | uint16_t getDACData(uint8_t ch, bool b_eep = false) { 149 | return b_eep ? (uint16_t)read_eep_[ch].data : (uint16_t)read_reg_[ch].data; 150 | } 151 | 152 | private: 153 | 154 | uint8_t fastWrite() { 155 | wire_->beginTransmission(addr_); 156 | for (uint8_t i = 0; i < 4; ++i) { 157 | wire_->write((uint8_t)CMD::FAST_WRITE | highByte(reg_[i].data)); 158 | wire_->write(lowByte(reg_[i].data)); 159 | } 160 | return wire_->endTransmission(); 161 | } 162 | 163 | uint8_t multiWrite() { 164 | wire_->beginTransmission(addr_); 165 | for (uint8_t i = 0; i < 4; ++i) { 166 | wire_->write((uint8_t)CMD::MULTI_WRITE | (i << 1)); 167 | wire_->write(((uint8_t)reg_[i].vref << 7) | ((uint8_t)reg_[i].pd << 5) | ((uint8_t)reg_[i].gain << 4) | highByte(reg_[i].data)); 168 | wire_->write(lowByte(reg_[i].data)); 169 | } 170 | return wire_->endTransmission(); 171 | } 172 | 173 | uint8_t seqWrite() { 174 | wire_->beginTransmission(addr_); 175 | wire_->write((uint8_t)CMD::SEQ_WRITE); 176 | for (uint8_t i = 0; i < 4; ++i) { 177 | wire_->write(((uint8_t)eep_[i].vref << 7) | ((uint8_t)eep_[i].pd << 5) | ((uint8_t)eep_[i].gain << 4) | highByte(eep_[i].data)); 178 | wire_->write(lowByte(eep_[i].data)); 179 | } 180 | return wire_->endTransmission(); 181 | } 182 | 183 | uint8_t singleWrite(uint8_t ch) { 184 | wire_->beginTransmission(addr_); 185 | wire_->write((uint8_t)CMD::SINGLE_WRITE | (ch << 1)); 186 | wire_->write(((uint8_t)eep_[ch].vref << 7) | ((uint8_t)eep_[ch].pd << 5) | ((uint8_t)eep_[ch].gain << 4) | highByte(eep_[ch].data)); 187 | wire_->write(lowByte(eep_[ch].data)); 188 | return wire_->endTransmission(); 189 | } 190 | 191 | private: 192 | 193 | struct DACInputData { 194 | VREF vref; 195 | PWR_DOWN pd; 196 | GAIN gain; 197 | uint16_t data; 198 | }; 199 | 200 | const uint8_t I2C_ADDR {0x60}; 201 | 202 | uint8_t addr_ {I2C_ADDR}; 203 | int8_t pin_ldac_; 204 | 205 | DACInputData reg_[4]; 206 | DACInputData eep_[4]; 207 | DACInputData read_reg_[4]; 208 | DACInputData read_eep_[4]; 209 | 210 | TwoWire* wire_; 211 | 212 | }; 213 | 214 | #endif 215 | -------------------------------------------------------------------------------- /tools/mcp4728_addr/SoftI2cMaster.cpp: -------------------------------------------------------------------------------- 1 | /* Arduino SoftI2cMaster Library 2 | * Copyright (C) 2009 by William Greiman 3 | * 4 | * This file is part of the Arduino SoftI2cMaster Library 5 | * 6 | * This Library is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This Library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with the Arduino SoftI2cMaster Library. If not, see 18 | * . 19 | */ 20 | #include "./SoftI2cMaster.h" 21 | //------------------------------------------------------------------------------ 22 | // WARNING don't change anything unless you verify the change with a scope 23 | //------------------------------------------------------------------------------ 24 | // init pins and set bus high 25 | void SoftI2cMaster::init(uint8_t sclPin, uint8_t sdaPin) 26 | { 27 | sclPin_ = sclPin; 28 | sdaPin_ = sdaPin; 29 | pinMode(sclPin_, OUTPUT); 30 | digitalWrite(sdaPin_, HIGH); //Mark_H fix 31 | pinMode(sdaPin_, OUTPUT); 32 | digitalWrite(sclPin_, HIGH); 33 | digitalWrite(sdaPin_, HIGH); 34 | } 35 | //------------------------------------------------------------------------------ 36 | // read a byte and send Ack if last is false else Nak to terminate read 37 | uint8_t SoftI2cMaster::read(uint8_t last) 38 | { 39 | uint8_t b = 0; 40 | // make sure pullup enabled 41 | digitalWrite(sdaPin_, HIGH); 42 | pinMode(sdaPin_, INPUT); 43 | // read byte 44 | for (uint8_t i = 0; i < 8; i++) { 45 | // don't change this loop unless you verify the change with a scope 46 | b <<= 1; 47 | delayMicroseconds(I2C_DELAY_USEC); 48 | digitalWrite(sclPin_, HIGH); 49 | if (digitalRead(sdaPin_)) b |= 1; 50 | digitalWrite(sclPin_, LOW); 51 | } 52 | // send Ack or Nak 53 | digitalWrite(sdaPin_, HIGH); //Mark_H fix 54 | pinMode(sdaPin_, OUTPUT); 55 | digitalWrite(sdaPin_, last); 56 | digitalWrite(sclPin_, HIGH); 57 | delayMicroseconds(I2C_DELAY_USEC); 58 | digitalWrite(sclPin_, LOW); 59 | digitalWrite(sdaPin_, HIGH); 60 | return b; 61 | } 62 | //------------------------------------------------------------------------------ 63 | // send new address and read/write without stop 64 | uint8_t SoftI2cMaster::restart(uint8_t addressRW) 65 | { 66 | digitalWrite(sclPin_, HIGH); 67 | return start(addressRW); 68 | } 69 | //------------------------------------------------------------------------------ 70 | // issue a start condition for i2c address with read/write bit 71 | uint8_t SoftI2cMaster::start(uint8_t addressRW) 72 | { 73 | digitalWrite(sdaPin_, LOW); 74 | delayMicroseconds(I2C_DELAY_USEC); 75 | digitalWrite(sclPin_, LOW); 76 | return write(addressRW); 77 | } 78 | //------------------------------------------------------------------------------ 79 | // issue a stop condition 80 | void SoftI2cMaster::stop(void) 81 | { 82 | digitalWrite(sdaPin_, LOW); 83 | delayMicroseconds(I2C_DELAY_USEC); 84 | digitalWrite(sclPin_, HIGH); 85 | delayMicroseconds(I2C_DELAY_USEC); 86 | digitalWrite(sdaPin_, HIGH); 87 | delayMicroseconds(I2C_DELAY_USEC); 88 | } 89 | //------------------------------------------------------------------------------ 90 | // write byte and return true for Ack or false for Nak 91 | uint8_t SoftI2cMaster::write(uint8_t b) 92 | { 93 | // write byte 94 | for (uint8_t m = 0X80; m != 0; m >>= 1) { 95 | // don't change this loop unless you verivy the change with a scope 96 | digitalWrite(sdaPin_, m & b); 97 | digitalWrite(sclPin_, HIGH); 98 | delayMicroseconds(I2C_DELAY_USEC); 99 | digitalWrite(sclPin_, LOW); 100 | } 101 | // get Ack or Nak 102 | digitalWrite(sdaPin_, HIGH); 103 | pinMode(sdaPin_, INPUT); 104 | digitalWrite(sclPin_, HIGH); 105 | b = digitalRead(sdaPin_); 106 | digitalWrite(sclPin_, LOW); 107 | digitalWrite(sdaPin_, HIGH); //Mark_H fix 108 | pinMode(sdaPin_, OUTPUT); 109 | return b == 0; 110 | } 111 | 112 | //------------------------------------------------------------------------------ 113 | // write byte and return true for Ack or false for Nak 114 | uint8_t SoftI2cMaster::ldacwrite(uint8_t b, uint8_t ldacpin) 115 | { 116 | // write byte 117 | for (uint8_t m = 0X80; m != 0; m >>= 1) { 118 | // don't change this loop unless you verivy the change with a scope 119 | digitalWrite(sdaPin_, m & b); 120 | digitalWrite(sclPin_, HIGH); 121 | delayMicroseconds(I2C_DELAY_USEC); 122 | digitalWrite(sclPin_, LOW); 123 | } 124 | // get Ack or Nak 125 | digitalWrite(ldacpin, LOW); 126 | digitalWrite(sdaPin_, HIGH); 127 | 128 | pinMode(sdaPin_, INPUT); 129 | digitalWrite(sclPin_, HIGH); 130 | b = digitalRead(sdaPin_); 131 | digitalWrite(sclPin_, LOW); 132 | digitalWrite(sdaPin_, HIGH); //Mark_H fix 133 | pinMode(sdaPin_, OUTPUT); 134 | return b == 0; 135 | } 136 | -------------------------------------------------------------------------------- /tools/mcp4728_addr/SoftI2cMaster.h: -------------------------------------------------------------------------------- 1 | /* Arduino SoftI2cMaster Library 2 | * Copyright (C) 2009 by William Greiman 3 | * 4 | * This file is part of the Arduino SoftI2cMaster Library 5 | * 6 | * This Library is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This Library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with the Arduino SoftI2cMaster Library. If not, see 18 | * . 19 | */ 20 | #ifndef SOFT_I2C_MASTER 21 | #define SOFT_I2C_MASTER 22 | #include "./TwoWireBase.h" 23 | 24 | // delay used to tweek signals 25 | #define I2C_DELAY_USEC 10 26 | 27 | class SoftI2cMaster : public TwoWireBase { 28 | uint8_t sclPin_; 29 | uint8_t sdaPin_; 30 | public: 31 | /** init bus */ 32 | void init(uint8_t sclPin, uint8_t sdaPin); 33 | 34 | /** read a byte and send Ack if last is false else Nak to terminate read */ 35 | uint8_t read(uint8_t last); 36 | 37 | /** send new address and read/write bit without stop */ 38 | uint8_t restart(uint8_t addressRW); 39 | 40 | /** issue a start condition for i2c address with read/write bit */ 41 | uint8_t start(uint8_t addressRW); 42 | 43 | /** issue a stop condition */ 44 | void stop(void); 45 | 46 | /** write byte and return true for Ack or false for Nak */ 47 | uint8_t write(uint8_t b); 48 | 49 | /** write byte and return true for Ack or false for Nak */ 50 | uint8_t ldacwrite(uint8_t b, uint8_t); 51 | 52 | }; 53 | #endif //SOFT_I2C_MASTER -------------------------------------------------------------------------------- /tools/mcp4728_addr/TwoWireBase.h: -------------------------------------------------------------------------------- 1 | /* Arduino SoftI2cMaster and TwiMaster Libraries 2 | * Copyright (C) 2009 by William Greiman 3 | * 4 | * This file is part of the Arduino SoftI2cMaster and TwiMaster Libraries 5 | * 6 | * This Library is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This Library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with the Arduino SoftI2cMaster and TwiMaster Libraries. 18 | * If not, see . 19 | */ 20 | #ifndef TWO_WIRE_BASE_H 21 | #define TWO_WIRE_BASE_H 22 | #include 23 | 24 | // R/W direction bit to OR with address for start or restart 25 | #define I2C_READ 1 26 | #define I2C_WRITE 0 27 | 28 | class TwoWireBase { 29 | public: 30 | /** read a byte and send Ack if last is false else Nak to terminate read */ 31 | virtual uint8_t read(uint8_t last) = 0; 32 | 33 | /** send new address and read/write bit without stop */ 34 | virtual uint8_t restart(uint8_t addressRW) = 0; 35 | 36 | /** issue a start condition for i2c address with read/write bit */ 37 | virtual uint8_t start(uint8_t addressRW) = 0; 38 | 39 | /** issue a stop condition */ 40 | virtual void stop(void) = 0; 41 | 42 | /** write byte and return true for Ack or false for Nak */ 43 | virtual uint8_t write(uint8_t data) = 0; 44 | }; 45 | #endif // TWO_WIRE_BASE_H 46 | -------------------------------------------------------------------------------- /tools/mcp4728_addr/mcp4728_addr.ino: -------------------------------------------------------------------------------- 1 | 2 | // WRITE I2C ADDRESS (device ID) for MCP4728 ================================= 3 | // 4 | // Author: Neuroelec 5 | // Source (archived): http://web.archive.org/web/20130630070633/http://neuroelec.com:80/2011/02/soft-i2c-and-programmable-i2c-address 6 | // Extracted from: https://code.google.com/archive/p/neuroelec/source/default/source 7 | // I2C fix from: https://github.com/TrippyLighting/SoftI2cMaster 8 | // MCP4728 datasheet: http://ww1.microchip.com/downloads/en/devicedoc/22187e.pdf 9 | // 10 | // Because of critical timing of LDAC latch during the address write and read, 11 | // the library use software I2C master library just for address read and writing. 12 | // Included modified SoftI2cMaster library is required. 13 | // Original library is from fat16lib, http://forums.adafruit.com/viewtopic.php?f=25&t=13722 14 | // 15 | // If you are using new chip, device ID is 0. 16 | // If you don't know current device ID, just run this scketch and check the serial monitor. 17 | // Once you get current device ID, put proper current device ID in writeAddress() command. 18 | // 19 | // =========================================================================== 20 | 21 | #include 22 | 23 | #include "./SoftI2cMaster.h" 24 | #include "./MCP4728.cpp" 25 | 26 | #define SCL_PIN A5 27 | #define SDA_PIN A4 28 | #define LDAC_PIN 4 29 | 30 | #define CURRENT_DEVICE_ID 0 31 | #define NEW_DEVICE_ID 0 32 | 33 | SoftI2cMaster i2c; 34 | MCP4728 dac; 35 | 36 | void setup() { 37 | 38 | // Read and write address 39 | Serial.begin(9600); 40 | i2c.init(SCL_PIN, SDA_PIN); 41 | pinMode(LDAC_PIN, OUTPUT); 42 | writeAddress(CURRENT_DEVICE_ID, NEW_DEVICE_ID); 43 | delay(100); 44 | readAddress(); // Read current device ID 45 | delay(100); 46 | 47 | // Init I2C communication and DAC for testing 48 | Wire.begin(); 49 | Wire.setClock(400000); // Fast mode 50 | dac.init(Wire, NEW_DEVICE_ID, LDAC_PIN); 51 | dac.selectVref(MCP4728::VREF::INTERNAL_2_8V, MCP4728::VREF::INTERNAL_2_8V, MCP4728::VREF::INTERNAL_2_8V, MCP4728::VREF::INTERNAL_2_8V); 52 | dac.selectPowerDown(MCP4728::PWR_DOWN::NORMAL, MCP4728::PWR_DOWN::NORMAL, MCP4728::PWR_DOWN::NORMAL, MCP4728::PWR_DOWN::NORMAL); 53 | dac.selectGain(MCP4728::GAIN::X2, MCP4728::GAIN::X2, MCP4728::GAIN::X2, MCP4728::GAIN::X2); 54 | dac.enable(true); 55 | 56 | } 57 | 58 | void loop() { 59 | 60 | // Test ramp 61 | dac.analogWrite(0, 0, 0, 0); 62 | delay(2000); 63 | for (int i = 0; i < 4000; i++) { 64 | dac.analogWrite(i, i, i, i); 65 | delayMicroseconds(200); 66 | } 67 | delay(2000); 68 | 69 | } 70 | 71 | void readAddress() { 72 | digitalWrite(LDAC_PIN, HIGH); 73 | i2c.start(0B00000000); 74 | i2c.ldacwrite(0B00001100, LDAC_PIN); // Modified command for LDAC latch 75 | i2c.restart(0B11000001); 76 | uint8_t address = i2c.read(true); 77 | i2c.stop(); 78 | int readAddress = (address & 0B00001110) >> 1; 79 | Serial.print("Read address: "); 80 | Serial.print(readAddress, DEC); 81 | Serial.print(" ("); 82 | Serial.print(readAddress, BIN); 83 | Serial.println(")"); 84 | } 85 | 86 | void writeAddress(int oldAddress, int newAddress) { 87 | Serial.print("Writing address: "); 88 | Serial.print(newAddress, DEC); 89 | Serial.print(" ("); 90 | Serial.print(newAddress, BIN); 91 | Serial.println(")"); 92 | digitalWrite(LDAC_PIN, HIGH); 93 | i2c.start(0B11000000 | (oldAddress << 1)); 94 | i2c.ldacwrite(0B01100001 | (oldAddress << 2), LDAC_PIN); // Modified command for LDAC latch 95 | i2c.write(0B01100010 | (newAddress << 2)); 96 | i2c.write(0B01100011 | (newAddress << 2)); 97 | i2c.stop(); 98 | } 99 | --------------------------------------------------------------------------------