├── .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 | 
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 |
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 |
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 | 
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 |
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 | 
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 | 
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 |
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 |
--------------------------------------------------------------------------------