├── HexKlox schematic.jpeg ├── IMG_20210428_064943-01.jpeg ├── LICENSE ├── M4TM_HexKlox_1.01.ino ├── M4TM_HexKlox_1.01_OLED.ino ├── M4TM_So_Clocks_1.00.ino ├── README.md └── SoClocks schematic.jpeg /HexKlox schematic.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozerik/HexKlox/8da72c10814dfe261fc241c266142fa0a80de67b/HexKlox schematic.jpeg -------------------------------------------------------------------------------- /IMG_20210428_064943-01.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozerik/HexKlox/8da72c10814dfe261fc241c266142fa0a80de67b/IMG_20210428_064943-01.jpeg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /M4TM_HexKlox_1.01.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | /* 6 | MODULAR FOR THE MASSES 7 | HexKlox 8 | Juanito Moore, April 2021 9 | 10 | This is a six-channel clock. The channels can be set to 11 | send triggers every 16th note, every 8th, quarter note, 12 | half note, et cetera, up to 64 bars or even MORE. It 13 | displays the BPM, and this clock can do swing, even with 14 | an external trigger. 15 | 16 | It uses a TM1637 four-digit display. I built my example 17 | with one built by RobotDYN, which includes the decmial 18 | points, but no colon (like for a clock display). 19 | 20 | There's three buttons, a right/up, left/down, and a reset 21 | button. There's one input for an external trigger. Finally, 22 | there's a potentiometer serving as a selector knob. 23 | 24 | 25 | 26 | -------USER MANUAL-------- 27 | There's eight positions the knob can be in. 28 | Position 1: 29 | BPM display and set mode. Right button increases the BPM. 30 | Left button decreases the BPM. Reset button held for one 31 | second resets the BPM to 120. All three buttons pressed 32 | at the same time resets all the clock divisions zero-points 33 | to the same zero point. 34 | Positions 2 through 6: 35 | The display will show a C for channel, the channel number, 36 | and the clock division. Right button increases the divisor. 37 | Left button decreases the divisor. Reset button resets the 38 | channel to start on the next clock tick. 39 | Position 7: 40 | SWING MODE! The display shows a S (or a 5 haha) and how 41 | many milliseconds your 16th note triggers will be swinging. 42 | The timing goes backward and forward, so your drum pattern 43 | can start on any clock tick... basically this makes the clock 44 | "downbeat agnostic" which, I would buy that album. 45 | Right button swings one way, left button swings the other way, 46 | reset button when held for two seconds saves the tempo and 47 | channel divisors and swing value into the Arduino's EEPROM memory. 48 | 49 | With an external trigger connected, the module will automatically 50 | use the incoming triggers as 16th note triggers. So the module 51 | expects four pulses (or peaks) per beat -- 4 PPB. The MIDI standard 52 | is 24 PPB, so if your clock output is going that fast. this module 53 | won't behave as expected. It should still track, but the BPM won't 54 | make any sense, and the swing won't work. You'll just have to divide 55 | down the clocks more. Yikes... and 24 isn't a power-of-two, so the 56 | divisors won't work without changing the code. 57 | 58 | Please find the schematic for this project over here: 59 | 60 | 61 | 62 | 63 | Version history: 64 | 1.00 released on April 28, 2021, initial release 65 | 66 | 67 | 68 | */ 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | TM1637TinyDisplay display(4, 5); // 4 = CLK 5 = DIO 78 | 79 | 80 | 81 | unsigned long debounce; 82 | unsigned long newTrig; 83 | unsigned long oldTrig; 84 | unsigned long EIS; 85 | float BPM; 86 | byte BPMcounter; 87 | float BPMs[20]; 88 | word counts[6]; 89 | long Interval; 90 | volatile unsigned long extInterval; 91 | unsigned long extIntervalSum[10]; 92 | word Increment; 93 | int swing; 94 | bool swingTrack; 95 | byte swingTrackV; 96 | int analogKnob; 97 | int knob; 98 | byte switchPos; 99 | byte tracker; 100 | byte tracker2; 101 | byte ext; 102 | const word triggerLength = 8000; // trigger length in microseconds 103 | byte divider[6]; 104 | 105 | /* 106 | The divisions[] constant is numerator of a fraction with 16 as the denominator. 107 | A 1 means "one sixteenth notes". 2 means "two sixteenth notes" which is 1/8th notes. 108 | 16/16 is whole notes. 32 means one trigger per two bars. And so on. The number shown 109 | on the channel-division-select mode is which number in this array is selected. 110 | So "C1 1" means channel 1 will be playing 1/16th notes. "C1 5" means whole notes. 111 | "C1 b" means the 11th number in this array will be selected (B is hex for 11) so that 112 | will be playing 1024/16th notes, which is... uh... 16 bars? 113 | 114 | These numbers can be changed here and you can get creative weird patterns going if 115 | that's something you're interested in. 116 | */ 117 | const word divisions[] = {0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096}; 118 | 119 | 120 | 121 | 122 | 123 | void setup() { 124 | pinMode(2, INPUT_PULLUP); // input for external trigger 125 | 126 | pinMode(8, OUTPUT); //PORTB B0000000x, trigger 1 output 127 | pinMode(9, OUTPUT); //PORTB B000000x0, trigger 2 output 128 | pinMode(10, OUTPUT); //PORTB B00000x00, trigger 3 output 129 | pinMode(11, OUTPUT); //PORTB B0000x000, trigger 4 output 130 | pinMode(12, OUTPUT); //PORTB B000x0000, trigger 5 output 131 | pinMode(13, OUTPUT); //PORTB B00x00000, trigger 6 output 132 | 133 | pinMode(14, INPUT_PULLUP); //PINC B0000000x "left" button, A0 134 | pinMode(15, INPUT_PULLUP); //PINC B000000x0 "right" button, A1 135 | pinMode(16, INPUT_PULLUP); //PINC B00000x00 reset timing button, A2 136 | 137 | //initialize the display 138 | display.setBrightness(7); 139 | uint8_t data[] = {0, 0, 0, 0}; 140 | display.setSegments(data); 141 | 142 | { // handles reading the contents of the EEPROM memory 143 | // this must be saved by moving knob to the swing setting 144 | // (fully clockwise) and holding the reset button for 2 seconds 145 | 146 | EEPROM.get(0, divider); 147 | byte address = sizeof(divider); 148 | EEPROM.get(address, Interval); 149 | address = address + sizeof(Interval); 150 | EEPROM.get(address, swing); 151 | if (divider[0] == 255) { // only gets run when the memory has never been written to 152 | divider[0] = 1; 153 | divider[1] = 1; 154 | divider[2] = 2; 155 | divider[3] = 4; 156 | divider[4] = 5; 157 | divider[5] = 9; 158 | Interval = 125000; // 4 PPB, 120 BPM 159 | } 160 | } // end of EEPROM reading 161 | 162 | 163 | Timer1.initialize(); 164 | Timer1.attachInterrupt(clockISR); // clock advance and trigger stuff 165 | 166 | attachInterrupt(digitalPinToInterrupt(2), extISR, FALLING); 167 | 168 | // Serial.begin(115200); 169 | 170 | } 171 | 172 | void extISR() { // this runs when the module gets an external trigger 173 | 174 | ext = 1; // tells the rest of the program that there's external triggering happening 175 | 176 | // averages ten incoming trigger intervals for a more steady BPM display 177 | if (BPMcounter > 9) BPMcounter = 0; 178 | newTrig = micros(); 179 | extIntervalSum[BPMcounter] = newTrig - oldTrig; 180 | oldTrig = newTrig; 181 | BPMcounter++; 182 | 183 | 184 | if (swing == 0) { 185 | swingTrackV = 1; // forces the clockISR() to play the triggers 186 | clockISR(); 187 | } else { 188 | swingTrack = !swingTrack; 189 | if (swingTrack) { // every other trigger THIS happens 190 | if (swing > 0) { // delays this trigger 191 | Timer1.setPeriod(swing); 192 | swingTrackV = 1; 193 | } else { 194 | for (byte i = 0; i < 6; i++) { 195 | counts[i]++; 196 | if (counts[i] >= divisions[divider[i]]) { 197 | counts[i] = 0; 198 | bitWrite(PORTB, i, 1); 199 | } 200 | } 201 | swingTrackV = 0; 202 | Timer1.setPeriod(triggerLength); 203 | } 204 | } else { // every other OTHER trigger THIS happens 205 | if (swing < 0) { // delays this trigger! 206 | Timer1.setPeriod(abs(swing)); 207 | swingTrackV = 1; 208 | } else { 209 | for (byte i = 0; i < 6; i++) { 210 | counts[i]++; 211 | if (counts[i] >= divisions[divider[i]]) { 212 | counts[i] = 0; 213 | bitWrite(PORTB, i, 1); 214 | } 215 | } 216 | swingTrackV = 0; 217 | Timer1.setPeriod(triggerLength); 218 | } 219 | } 220 | } 221 | EIS = 0; 222 | for (byte i = 0; i < 10; i++) { 223 | EIS = EIS + extIntervalSum[i]; 224 | } 225 | extInterval = EIS * 0.1; 226 | } 227 | 228 | 229 | void clockISR() { // here's the free-running clock 230 | swingTrackV++; 231 | 232 | if (bitRead(swingTrackV, 0) == 0) { // here's the PLAY NOW section 233 | for (byte i = 0; i < 6; i++) { 234 | counts[i]++; 235 | if (counts[i] >= divisions[divider[i]]) { 236 | counts[i] = 0; 237 | bitWrite(PORTB, i, 1); 238 | } 239 | } 240 | Timer1.setPeriod(triggerLength); 241 | } else if (ext == 0) { 242 | PORTB &= 11000000; //turns off triggers 243 | if (bitRead(swingTrackV, 1) == 0) { 244 | Timer1.setPeriod(Interval - triggerLength + swing); 245 | } else { 246 | Timer1.setPeriod(Interval - triggerLength - swing); 247 | } 248 | } else { // if the external trigger is in play... 249 | PORTB &= 11000000; 250 | Timer1.setPeriod(500000); // restart internal clock in 0.5 seconds unless there's another external trigger event 251 | } 252 | } 253 | 254 | 255 | 256 | void loop() { 257 | knob = map(analogRead(A5), 1, 1000, 0, 7); 258 | switchPos = 0; 259 | if (bitRead(PINC, 0) == 0) switchPos = 2; 260 | if (bitRead(PINC, 2) == 0) switchPos = 1; 261 | if (bitRead(PINC, 1) == 0) switchPos = 3; 262 | if ((PINC & B00000111) == 0) { // all three buttons pressed simultaneously -- resets all counts to zero 263 | switchPos = 0; 264 | for (byte i = 0; i < 6; i++) { 265 | counts[i] = 0; 266 | } 267 | } 268 | if (micros() - newTrig > 500000) ext = 0; 269 | if (switchPos == 0) tracker = 0; 270 | { // knob zero means show BPM, left and right buttons adjust BPM 271 | if (knob == 0) { 272 | display.showNumber(BPM, 1); 273 | 274 | { // change BPM 275 | if (tracker == 0 && switchPos > 0) { 276 | debounce = millis(); 277 | tracker = 1; 278 | switch (switchPos) { 279 | case 1: 280 | Interval += Increment; 281 | break; 282 | case 2: 283 | Interval -= Increment; 284 | break; 285 | } 286 | } 287 | if (tracker == 1) { 288 | if (millis() - debounce > 200) { 289 | switch (switchPos) { 290 | case 1: 291 | Interval += Increment; 292 | break; 293 | case 2: 294 | Interval -= Increment; 295 | break; 296 | } 297 | } 298 | if (switchPos == 3 && millis() - debounce > 2000) { 299 | tracker = 2; 300 | Interval = 125000; 301 | } 302 | } 303 | Interval = constrain(Interval, 68193, 272848); 304 | } // end of change BPM 305 | 306 | } else if (knob < 7) { // change the divisions of each channel 307 | if (bitRead(PINC, 1) == 0 && tracker == 0) { 308 | counts[knob - 1] = 6000; 309 | tracker = 1; 310 | } 311 | uint8_t data[] = {57, 0, 0, 0}; 312 | data[1] = display.encodeDigit(knob); 313 | data[3] = display.encodeDigit(divider[knob - 1]); 314 | display.setSegments(data); 315 | if (switchPos == 1 && tracker == 0) { 316 | divider[knob - 1]--; 317 | tracker = 10; 318 | } 319 | if (switchPos == 2 && tracker == 0) { 320 | divider[knob - 1]++; 321 | tracker = 10; 322 | } 323 | divider[knob - 1] = constrain(divider[knob - 1], 1, 13); 324 | } 325 | } 326 | 327 | 328 | if (knob == 7) { // swing adjustment 329 | uint8_t data[] = {109, 0, 0, 0}; // sets up the display 330 | if (tracker == 0) { 331 | if (switchPos == 2) { 332 | swing += 1000; 333 | tracker = 1; 334 | } 335 | if (switchPos == 1) { 336 | swing -= 1000; 337 | tracker = 1; 338 | } 339 | if (switchPos == 3) { 340 | debounce = millis(); 341 | tracker = 4; 342 | } 343 | } 344 | if (tracker == 4 && millis() - debounce > 2000) { 345 | EEPROM.put(0, divider); 346 | byte address = sizeof(divider); 347 | EEPROM.put(address, Interval); 348 | address = address + sizeof(Interval); 349 | EEPROM.put(address, swing); 350 | display.showNumber(8888); 351 | tracker = 5; 352 | } 353 | swing = constrain(swing, -60000, 60000); 354 | if (swing <= 1) { 355 | data[1] = 64; // minus sign for negative swing values 356 | } 357 | int TS1 = swing * 0.0001; 358 | data[2] = display.encodeDigit(abs(TS1)); 359 | int TS2 = (swing * 0.001); 360 | TS2 = TS2 % 10; 361 | data[3] = display.encodeDigit(abs(TS2)); 362 | display.setSegments(data); 363 | } 364 | 365 | Increment = Interval * 0.000834; // calculate increment so the BPM will go up by one decmial point per button press 366 | 367 | // snags the important values from the interrupt service routine for external BPM calculations 368 | noInterrupts(); 369 | byte ext1 = ext; 370 | unsigned long extInterval1 = extInterval; 371 | interrupts(); 372 | 373 | if (ext1 == 1) { 374 | BPM = 15000000.0 / extInterval1; 375 | } else BPM = 15000000.0 / Interval; // calculate the BPM when self-clocked 376 | } 377 | -------------------------------------------------------------------------------- /M4TM_HexKlox_1.01_OLED.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #define SCREEN_WIDTH 128 // OLED display width, in pixels 7 | #define SCREEN_HEIGHT 64 // OLED display height, in pixels 8 | #define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) 9 | Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); 10 | /* 11 | MODULAR FOR THE MASSES 12 | HexKlox 13 | Juanito Moore, April 2021 14 | 15 | OLED Display Version by Lacó Modular (Jeremie Agnone) 16 | with great help from Juanito Moore 17 | 18 | This is a six-channel clock. The channels can be set to 19 | send triggers every 16th note, every 8th, quarter note, 20 | half note, et cetera, up to 64 bars or even MORE. It 21 | displays the BPM, and this clock can do swing, even with 22 | an external trigger. 23 | 24 | 25 | There's three buttons, a right/up, left/down, and a reset 26 | button. There's one input for an external trigger. Finally, 27 | there's a potentiometer serving as a selector knob. 28 | 29 | 30 | 31 | -------USER MANUAL-------- 32 | There's eight positions the knob can be in. 33 | Position 1: 34 | BPM display and set mode. Right button increases the BPM. 35 | Left button decreases the BPM. Reset button held for one 36 | second resets the BPM to 120. All three buttons pressed 37 | at the same time resets all the clock divisions zero-points 38 | to the same zero point. 39 | Positions 2 through 6: 40 | The display will show a C for channel, the channel number, 41 | and the clock division. Right button increases the divisor. 42 | Left button decreases the divisor. Reset button resets the 43 | channel to start on the next clock tick. 44 | Position 7: 45 | SWING MODE! The display shows a S (or a 5 haha) and how 46 | many milliseconds your 16th note triggers will be swinging. 47 | The timing goes backward and forward, so your drum pattern 48 | can start on any clock tick... basically this makes the clock 49 | "downbeat agnostic" which, I would buy that album. 50 | Right button swings one way, left button swings the other way, 51 | reset button when held for two seconds saves the tempo and 52 | channel divisors and swing value into the Arduino's EEPROM memory. 53 | 54 | With an external trigger connected, the module will automatically 55 | use the incoming triggers as 16th note triggers. So the module 56 | expects four pulses (or peaks) per beat -- 4 PPB. The MIDI standard 57 | is 24 PPB, so if your clock output is going that fast. this module 58 | won't behave as expected. It should still track, but the BPM won't 59 | make any sense, and the swing won't work. You'll just have to divide 60 | down the clocks more. Yikes... and 24 isn't a power-of-two, so the 61 | divisors won't work without changing the code. 62 | 63 | Please find the schematic for this project over here: 64 | 65 | 66 | 67 | 68 | Version history: 69 | 1.01 OLED VERSION -- modded by Lacó Modular (Jeremie Agnone)* 70 | 1.01 modified to work with a bare Atmega328 DIP IC, also changed the project name to HexKlox 71 | 1.00 released on April 28, 2021, initial release 72 | 73 | 74 | * When using this OLED sketch, the potentiometer center pin needs to be connected to 75 | Arduino pin A7, and the OLED display module's SCL and SDA pins need to connect to the 76 | Arduino Nano (or Uno) like this: 77 | Arduino OLED 78 | A4-------SDA 79 | A5-------SCL 80 | If you need to use a bare DIP without the A6 or A7 pins, you can search this sketch for 81 | the one time it says A7, and change that to A3, and connect the potentiometer to that 82 | pin and you'll be all good (I think). 83 | 84 | 85 | */ 86 | 87 | 88 | unsigned long debounce; 89 | unsigned long newTrig; 90 | unsigned long oldTrig; 91 | unsigned long EIS; 92 | float BPM; 93 | byte BPMcounter; 94 | float BPMs[20]; 95 | word counts[6]; 96 | long Interval; 97 | volatile unsigned long extInterval; 98 | unsigned long extIntervalSum[10]; 99 | word Increment; 100 | int swing; 101 | bool swingTrack; 102 | byte swingTrackV; 103 | int analogKnob; 104 | int knob; 105 | byte switchPos; 106 | byte tracker; 107 | byte tracker2; 108 | byte ext; 109 | const word triggerLength = 8000; // trigger length in microseconds 110 | byte divider[6]; 111 | 112 | /* 113 | The divisions[] constant is numerator of a fraction with 16 as the denominator. 114 | A 1 means "one sixteenth notes". 2 means "two sixteenth notes" which is 1/8th notes. 115 | 16/16 is whole notes. 32 means one trigger per two bars. And so on. The number shown 116 | on the channel-division-select mode is which number in this array is selected. 117 | So "C1 1" means channel 1 will be playing 1/16th notes. "C1 5" means whole notes. 118 | "C1 b" means the 11th number in this array will be selected (B is hex for 11) so that 119 | will be playing 1024/16th notes, which is... uh... 16 bars? 120 | 121 | These numbers can be changed here and you can get creative weird patterns going if 122 | that's something you're interested in. 123 | */ 124 | const word divisions[] = {0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096}; 125 | 126 | 127 | // the code below is the BITMAP of the M4TM logo 128 | 129 | const unsigned char myBitmap [] PROGMEM = { 130 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 131 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x01, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 132 | 0xff, 0xff, 0xff, 0xff, 0xf0, 0x1f, 0xff, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 133 | 0xff, 0xff, 0xff, 0xff, 0x87, 0xfb, 0xff, 0x9f, 0xff, 0xfe, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 134 | 0xff, 0xff, 0xff, 0xf8, 0x3f, 0x33, 0xff, 0x9f, 0xff, 0xff, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 135 | 0xff, 0xff, 0xff, 0x81, 0xff, 0x83, 0xff, 0x9f, 0xff, 0xf1, 0xff, 0x07, 0xff, 0xff, 0xff, 0xff, 136 | 0xff, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0xc3, 0xff, 0xe0, 0xff, 0xf1, 0xff, 0xff, 0xff, 0xff, 137 | 0xff, 0xf7, 0xf0, 0xe1, 0xff, 0xf8, 0x00, 0x0f, 0xff, 0xcc, 0x7f, 0xfe, 0x7f, 0xff, 0xff, 0xff, 138 | 0xff, 0xff, 0xc7, 0xcc, 0xff, 0x07, 0xff, 0x18, 0x00, 0xff, 0x3f, 0xff, 0x9f, 0xff, 0xff, 0xff, 139 | 0xff, 0xff, 0x1f, 0xc0, 0xf8, 0xff, 0xfe, 0x0f, 0xff, 0x00, 0x1f, 0xfc, 0x67, 0xff, 0xff, 0xff, 140 | 0xfe, 0x7c, 0x3f, 0xf1, 0xc7, 0xff, 0xfc, 0xe3, 0xff, 0xff, 0xc0, 0xf1, 0x18, 0xff, 0xff, 0xff, 141 | 0xff, 0xf8, 0xff, 0xfe, 0x3f, 0xff, 0xf9, 0xf8, 0xff, 0xff, 0xfe, 0x20, 0x1e, 0x3f, 0xff, 0xff, 142 | 0xff, 0xf3, 0xff, 0xf9, 0xff, 0xff, 0xf3, 0xfe, 0x3f, 0xff, 0xff, 0x8e, 0x7f, 0x9f, 0xff, 0xff, 143 | 0xff, 0xc7, 0xc3, 0xe7, 0xff, 0xff, 0xe7, 0xff, 0x8f, 0xff, 0xff, 0xf3, 0x3e, 0x3f, 0xff, 0xff, 144 | 0xff, 0x8f, 0x81, 0x9f, 0xff, 0xff, 0x8f, 0xff, 0xe7, 0xff, 0xff, 0xfc, 0xf1, 0xff, 0xff, 0xff, 145 | 0xff, 0x1f, 0xc7, 0x3f, 0xff, 0xff, 0x9f, 0x80, 0xf1, 0xff, 0xff, 0xfe, 0x0c, 0xff, 0xff, 0xff, 146 | 0xfe, 0x3f, 0xfc, 0xff, 0xff, 0xff, 0x81, 0x3c, 0xfc, 0xff, 0xff, 0xff, 0xbe, 0x7f, 0xff, 0xff, 147 | 0xfe, 0x70, 0x33, 0xff, 0xff, 0xfe, 0x1c, 0x3c, 0x82, 0x00, 0x00, 0x00, 0x0f, 0x3f, 0xff, 0xff, 148 | 0xfc, 0xe1, 0xcf, 0xff, 0xff, 0xf9, 0xfc, 0x3e, 0x18, 0xff, 0xff, 0xff, 0x9f, 0x9f, 0xff, 0xff, 149 | 0xf9, 0xcf, 0x9f, 0xff, 0xf0, 0xf3, 0xfe, 0x3e, 0x1c, 0x0f, 0xff, 0xff, 0x3f, 0xcf, 0xff, 0xff, 150 | 0xf8, 0xe3, 0x7f, 0xff, 0x87, 0x03, 0xfe, 0x1f, 0x1f, 0xe7, 0xff, 0xfc, 0x7f, 0xe7, 0xff, 0xff, 151 | 0xfe, 0x0c, 0xff, 0xff, 0x3f, 0xe7, 0xff, 0x1f, 0xff, 0xcf, 0xff, 0xf8, 0xff, 0xf7, 0xff, 0xff, 152 | 0xff, 0xc1, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0x0f, 0xff, 0x0f, 0xff, 0xf3, 0xff, 0xf3, 0xc0, 0x7f, 153 | 0xff, 0x9f, 0xf0, 0x01, 0x9f, 0xff, 0xff, 0x80, 0x0f, 0x3f, 0xff, 0xe7, 0xff, 0x00, 0x3e, 0x7f, 154 | 0xff, 0x9f, 0x07, 0xff, 0x9f, 0xff, 0xff, 0xcf, 0xe7, 0x9f, 0xff, 0xc7, 0x81, 0xff, 0xe3, 0x3f, 155 | 0xff, 0xbf, 0x0f, 0xff, 0xdf, 0xff, 0xff, 0xe7, 0xe7, 0xcf, 0xff, 0x8d, 0xf0, 0x6c, 0x87, 0x9f, 156 | 0xff, 0x9f, 0xe3, 0xff, 0xcf, 0xff, 0xf1, 0xf3, 0xf3, 0xe7, 0xff, 0x9c, 0xe1, 0xe0, 0x03, 0x9f, 157 | 0xff, 0x9f, 0xf8, 0xff, 0xcf, 0xe7, 0xe0, 0xf9, 0xf3, 0xf3, 0xff, 0x3e, 0x79, 0xe0, 0x47, 0x8f, 158 | 0xff, 0xdf, 0xfe, 0x1f, 0xcf, 0xe1, 0xe6, 0x43, 0xf9, 0xf3, 0xff, 0x3f, 0x79, 0xff, 0xf0, 0x0f, 159 | 0xff, 0xdf, 0xff, 0xe3, 0xcf, 0xe6, 0x1f, 0x1f, 0xff, 0x01, 0xff, 0x8f, 0x3f, 0xf8, 0x1f, 0xff, 160 | 0xff, 0xcf, 0xff, 0xf8, 0x40, 0x03, 0xff, 0x04, 0x03, 0xff, 0xbf, 0xe7, 0x80, 0x07, 0x3f, 0xff, 161 | 0xfd, 0xcf, 0x00, 0x07, 0x00, 0x00, 0x00, 0x34, 0xf0, 0x0f, 0x00, 0x31, 0xff, 0xff, 0x9f, 0xff, 162 | 0x07, 0x01, 0xff, 0xe7, 0xf1, 0xff, 0xff, 0xf0, 0xff, 0xe6, 0x7f, 0x3c, 0xff, 0xff, 0x9f, 0xff, 163 | 0x3f, 0xff, 0xfc, 0x77, 0xf3, 0xff, 0xff, 0xf0, 0xff, 0xf0, 0xff, 0x3e, 0x7f, 0xff, 0x9f, 0xff, 164 | 0xb0, 0xf8, 0xf8, 0x73, 0xfb, 0xff, 0xff, 0xf8, 0xff, 0xf9, 0xff, 0x9f, 0x3f, 0xff, 0x9f, 0xff, 165 | 0x80, 0xf6, 0x7c, 0x1b, 0xf9, 0xc3, 0xf8, 0x00, 0x7f, 0xff, 0xff, 0x9f, 0x8f, 0xff, 0x3f, 0xef, 166 | 0xd3, 0xf0, 0xfc, 0xf9, 0xf8, 0x33, 0xfc, 0xfe, 0x7f, 0xff, 0xff, 0xdf, 0xe7, 0xff, 0x3f, 0xff, 167 | 0xcb, 0xff, 0xe0, 0x01, 0xfb, 0xf9, 0xfe, 0x7e, 0x7f, 0xff, 0xff, 0xcf, 0xf1, 0xff, 0x3f, 0xff, 168 | 0xef, 0xf0, 0x3f, 0xff, 0xfb, 0xfc, 0xff, 0x7e, 0x7c, 0xff, 0xef, 0xcf, 0xfc, 0xff, 0x3f, 0xff, 169 | 0xf4, 0x3f, 0x3f, 0xff, 0xf3, 0xfc, 0xff, 0x1e, 0x78, 0x3f, 0xc7, 0xcf, 0xfe, 0x7e, 0x7f, 0xff, 170 | 0xff, 0xff, 0xbf, 0xff, 0xf7, 0xfe, 0xff, 0xce, 0x09, 0x9f, 0x87, 0xcf, 0xff, 0x1e, 0x7f, 0xff, 171 | 0xff, 0xff, 0x9f, 0xff, 0xf7, 0xfe, 0x00, 0x0f, 0x81, 0xc0, 0x30, 0x48, 0xff, 0x0c, 0xff, 0xff, 172 | 0xff, 0xff, 0xcf, 0xff, 0xf7, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0x30, 0x0c, 0x00, 0x08, 0xff, 0xff, 173 | 0xff, 0xff, 0xe7, 0xff, 0xe7, 0xff, 0xff, 0xe1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 174 | 0xff, 0xff, 0xf3, 0xff, 0xe7, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xe3, 0xff, 0xff, 175 | 0xff, 0xff, 0xfb, 0xff, 0xe7, 0xff, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x1f, 0xcf, 0xff, 0xff, 176 | 0xff, 0xff, 0xf9, 0xff, 0xef, 0xff, 0x07, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xc6, 0x3f, 0xff, 0xff, 177 | 0xff, 0xff, 0xfc, 0xff, 0xef, 0xf8, 0x7f, 0xff, 0xff, 0xff, 0xff, 0x07, 0xf0, 0xff, 0xff, 0xff, 178 | 0xff, 0xff, 0xff, 0x3f, 0xce, 0x07, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x3e, 0x3c, 0x7f, 0xff, 0xff, 179 | 0xff, 0xfb, 0xff, 0x8f, 0xc1, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x03, 0xfe, 0x3f, 0x3f, 0xff, 0xff, 180 | 0xf8, 0xf3, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x07, 0xe1, 0xff, 0x8f, 0x1f, 0xff, 0xff, 181 | 0xf0, 0x00, 0x7f, 0xfc, 0xe6, 0x7f, 0xf8, 0x0f, 0xff, 0xff, 0xe1, 0xff, 0x98, 0x7f, 0xff, 0xfb, 182 | 0xff, 0x60, 0xff, 0xfe, 0x0f, 0xc0, 0x03, 0xff, 0xfc, 0x1f, 0xe1, 0xff, 0xc1, 0xff, 0xff, 0xfb, 183 | 0xfc, 0xef, 0x9f, 0xff, 0x9c, 0x6f, 0xe7, 0xf8, 0xfc, 0x7f, 0xf0, 0x38, 0x0f, 0xff, 0xff, 0xfd, 184 | 0xfc, 0x00, 0x0f, 0xff, 0x38, 0x07, 0xc3, 0xf1, 0xfe, 0x1f, 0xf0, 0xe0, 0xff, 0xff, 0xff, 0xf3, 185 | 0xfd, 0xa1, 0x1f, 0xfc, 0x72, 0x6f, 0x81, 0xf8, 0x7f, 0x8f, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0xf1, 186 | 0xfe, 0xff, 0x7f, 0xff, 0x83, 0xff, 0x9d, 0xf8, 0x7f, 0x1f, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xe6, 187 | 0xfe, 0x00, 0x0f, 0xff, 0xfe, 0x3f, 0xff, 0xfd, 0xff, 0xc0, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 188 | 0xfe, 0x48, 0x1f, 0xff, 0xff, 0x80, 0x00, 0x20, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 189 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x01, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 190 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 191 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff, 192 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xf7, 193 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xf0, 0x1f, 0xff 194 | }; 195 | 196 | 197 | void setup() { 198 | 199 | display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // OLED display address (0x3C) 200 | 201 | // displays the M4TM logo on startup 202 | display.clearDisplay(); // clears the display 203 | display.drawBitmap(0, 0, myBitmap, 128, 64, WHITE); // display.drawBitmap(x position, y position, bitmap data, bitmap width, bitmap height, color) 204 | display.display(); 205 | delay(4000); 206 | 207 | pinMode(2, INPUT_PULLUP); // input for external trigger 208 | pinMode(8, OUTPUT); //PORTB B0000000x, trigger 1 output 209 | pinMode(9, OUTPUT); //PORTB B000000x0, trigger 2 output 210 | pinMode(10, OUTPUT); //PORTB B00000x00, trigger 3 output 211 | pinMode(11, OUTPUT); //PORTB B0000x000, trigger 4 output 212 | pinMode(12, OUTPUT); //PORTB B000x0000, trigger 5 output 213 | pinMode(13, OUTPUT); //PORTB B00x00000, trigger 6 output 214 | 215 | pinMode(14, INPUT_PULLUP); //PINC B0000000x "left" button, A0 216 | pinMode(15, INPUT_PULLUP); //PINC B000000x0 "right" button, A1 217 | pinMode(16, INPUT_PULLUP); //PINC B00000x00 reset timing button, A2 218 | 219 | 220 | 221 | { // handles reading the contents of the EEPROM memory 222 | // this must be saved by moving knob to the swing setting 223 | // (fully clockwise) and holding the reset button for 2 seconds 224 | 225 | EEPROM.get(0, divider); 226 | byte address = sizeof(divider); 227 | EEPROM.get(address, Interval); 228 | address = address + sizeof(Interval); 229 | EEPROM.get(address, swing); 230 | if (divider[0] == 255) { // only gets run when the memory has never been written to 231 | divider[0] = 1; 232 | divider[1] = 1; 233 | divider[2] = 2; 234 | divider[3] = 4; 235 | divider[4] = 5; 236 | divider[5] = 9; 237 | Interval = 125000; // 4 PPB, 120 BPM 238 | } 239 | } // end of EEPROM reading 240 | 241 | 242 | Timer1.initialize(); 243 | Timer1.attachInterrupt(clockISR); // clock advance and trigger stuff 244 | 245 | attachInterrupt(digitalPinToInterrupt(2), extISR, FALLING); 246 | 247 | // Serial.begin(115200); 248 | 249 | } 250 | 251 | void extISR() { // this runs when the module gets an external trigger 252 | 253 | ext = 1; // tells the rest of the program that there's external triggering happening 254 | 255 | // averages ten incoming trigger intervals for a more steady BPM display 256 | if (BPMcounter > 9) BPMcounter = 0; 257 | newTrig = micros(); 258 | extIntervalSum[BPMcounter] = newTrig - oldTrig; 259 | oldTrig = newTrig; 260 | BPMcounter++; 261 | 262 | 263 | if (swing == 0) { 264 | swingTrackV = 1; // forces the clockISR() to play the triggers 265 | clockISR(); 266 | } else { 267 | swingTrack = !swingTrack; 268 | if (swingTrack) { // every other trigger THIS happens 269 | if (swing > 0) { // delays this trigger 270 | Timer1.setPeriod(swing); 271 | swingTrackV = 1; 272 | } else { 273 | for (byte i = 0; i < 6; i++) { 274 | counts[i]++; 275 | if (counts[i] >= divisions[divider[i]]) { 276 | counts[i] = 0; 277 | bitWrite(PORTB, i, 1); 278 | } 279 | } 280 | swingTrackV = 0; 281 | Timer1.setPeriod(triggerLength); 282 | } 283 | } else { // every other OTHER trigger THIS happens 284 | if (swing < 0) { // delays this trigger! 285 | Timer1.setPeriod(abs(swing)); 286 | swingTrackV = 1; 287 | } else { 288 | for (byte i = 0; i < 6; i++) { 289 | counts[i]++; 290 | if (counts[i] >= divisions[divider[i]]) { 291 | counts[i] = 0; 292 | bitWrite(PORTB, i, 1); 293 | } 294 | } 295 | swingTrackV = 0; 296 | Timer1.setPeriod(triggerLength); 297 | } 298 | } 299 | } 300 | EIS = 0; 301 | for (byte i = 0; i < 10; i++) { 302 | EIS = EIS + extIntervalSum[i]; 303 | } 304 | extInterval = EIS * 0.1; 305 | } 306 | 307 | 308 | void clockISR() { // here's the free-running clock 309 | swingTrackV++; 310 | 311 | if (bitRead(swingTrackV, 0) == 0) { // here's the PLAY NOW section 312 | for (byte i = 0; i < 6; i++) { 313 | counts[i]++; 314 | if (counts[i] >= divisions[divider[i]]) { 315 | counts[i] = 0; 316 | bitWrite(PORTB, i, 1); 317 | } 318 | } 319 | Timer1.setPeriod(triggerLength); 320 | } else if (ext == 0) { 321 | PORTB &= 11000000; //turns off triggers 322 | if (bitRead(swingTrackV, 1) == 0) { 323 | Timer1.setPeriod(Interval - triggerLength + swing); 324 | } else { 325 | Timer1.setPeriod(Interval - triggerLength - swing); 326 | } 327 | } else { // if the external trigger is in play... 328 | PORTB &= 11000000; 329 | Timer1.setPeriod(500000); // restart internal clock in 0.5 seconds unless there's another external trigger event 330 | } 331 | } 332 | 333 | 334 | 335 | void loop() { 336 | knob = map(analogRead(A7), 1, 1000, 0, 7); 337 | switchPos = 0; 338 | if (bitRead(PINC, 0) == 0) switchPos = 2; 339 | if (bitRead(PINC, 2) == 0) switchPos = 1; 340 | if (bitRead(PINC, 1) == 0) switchPos = 3; 341 | if ((PINC & B00000111) == 0) { // all three buttons pressed simultaneously -- resets all counts to zero 342 | switchPos = 0; 343 | for (byte i = 0; i < 6; i++) { 344 | counts[i] = 0; 345 | } 346 | } 347 | if (micros() - newTrig > 500000) ext = 0; 348 | if (switchPos == 0) tracker = 0; 349 | { // knob zero means show BPM, left and right buttons adjust BPM 350 | if (knob == 0) { 351 | 352 | 353 | display.clearDisplay();// 354 | display.setTextSize(2); 355 | display.setCursor(50,0); // Sets x.y cursor 356 | display.println("BPM"); //print to display 357 | display.setTextSize(4); // text size 358 | display.setTextColor(WHITE); // Set color (only white) 359 | display.setCursor(0,20); 360 | display.println(BPM, 1); 361 | display.display(); // sends buffer to display 362 | 363 | 364 | // display.showNumber(BPM, 1); 365 | // that line up there showed the BPM. Have the OLED show the BPM here 366 | 367 | 368 | 369 | 370 | 371 | 372 | { // change BPM 373 | if (tracker == 0 && switchPos > 0) { 374 | debounce = millis(); 375 | tracker = 1; 376 | switch (switchPos) { 377 | case 1: 378 | Interval += Increment; 379 | break; 380 | case 2: 381 | Interval -= Increment; 382 | break; 383 | } 384 | } 385 | if (tracker == 1) { 386 | if (millis() - debounce > 200) { 387 | switch (switchPos) { 388 | case 1: 389 | Interval += Increment; 390 | break; 391 | case 2: 392 | Interval -= Increment; 393 | break; 394 | } 395 | } 396 | if (switchPos == 3 && millis() - debounce > 2000) { 397 | tracker = 2; 398 | Interval = 125000; 399 | } 400 | } 401 | Interval = constrain(Interval, 68193, 272848); 402 | } // end of change BPM 403 | 404 | } else if (knob < 7) { // change the divisions of each channel 405 | if (bitRead(PINC, 1) == 0 && tracker == 0) { 406 | counts[knob - 1] = 6000; 407 | tracker = 1; 408 | } 409 | 410 | 411 | display.clearDisplay(); 412 | display.setTextSize(2); 413 | display.setTextColor(WHITE); 414 | display.setCursor(0,0); 415 | display.print("Chan"); 416 | display.setCursor(70,0); 417 | display.print("Div"); 418 | display.setTextSize(4); 419 | display.setCursor(0,20); 420 | display.println(knob); 421 | display.setCursor(70,20); 422 | display.println(divider[knob - 1]); 423 | display.display(); 424 | 425 | 426 | // that bit shows which channel is relevant (knob - 1) and 427 | // finally, what divider number (not the actual divider number, the number OF the number 428 | // which is (divider[knob-1]) 429 | 430 | 431 | 432 | 433 | 434 | if (switchPos == 1 && tracker == 0) { 435 | divider[knob - 1]--; 436 | tracker = 10; 437 | } 438 | if (switchPos == 2 && tracker == 0) { 439 | divider[knob - 1]++; 440 | tracker = 10; 441 | } 442 | divider[knob - 1] = constrain(divider[knob - 1], 1, 13); 443 | } 444 | } 445 | 446 | 447 | if (knob == 7) { // swing adjustment 448 | 449 | 450 | display.clearDisplay(); 451 | display.setTextSize(3); 452 | display.setTextColor(WHITE); 453 | display.setCursor(10,0); 454 | display.println("Swing"); 455 | display.display(); 456 | 457 | 458 | 459 | 460 | if (tracker == 0) { 461 | if (switchPos == 2) { 462 | swing += 1000; 463 | tracker = 1; 464 | } 465 | if (switchPos == 1) { 466 | swing -= 1000; 467 | tracker = 1; 468 | } 469 | if (switchPos == 3) { 470 | debounce = millis(); 471 | tracker = 4; 472 | } 473 | } 474 | if (tracker == 4 && millis() - debounce > 2000) { 475 | EEPROM.put(0, divider); 476 | byte address = sizeof(divider); 477 | EEPROM.put(address, Interval); 478 | address = address + sizeof(Interval); 479 | EEPROM.put(address, swing); 480 | 481 | 482 | display.setTextSize(3); 483 | display.setCursor(10,30); 484 | display.println("Saved"); 485 | display.display(); 486 | 487 | 488 | 489 | 490 | 491 | tracker = 5; 492 | } 493 | 494 | swing = constrain(swing, -60000, 60000); 495 | display.setTextSize(3); 496 | display.setCursor(10,30); 497 | display.println(swing*0.0001); 498 | display.display(); 499 | 500 | 501 | } 502 | 503 | Increment = Interval * 0.000834; // calculate increment so the BPM will go up by one decmial point per button press 504 | 505 | // snags the important values from the interrupt service routine for external BPM calculations 506 | noInterrupts(); 507 | byte ext1 = ext; 508 | unsigned long extInterval1 = extInterval; 509 | interrupts(); 510 | 511 | if (ext1 == 1) { 512 | BPM = 15000000.0 / extInterval1; 513 | } else BPM = 15000000.0 / Interval; // calculate the BPM when self-clocked 514 | } 515 | -------------------------------------------------------------------------------- /M4TM_So_Clocks_1.00.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | /* 6 | MODULAR FOR THE MASSES 7 | SO CLOCKS 8 | Juanito Moore, April 2021 9 | 10 | This is a six-channel clock. The channels can be set to 11 | send triggers every 16th note, every 8th, quarter note, 12 | half note, et cetera, up to 64 bars or even MORE. It 13 | displays the BPM, and this clock can do swing, even with 14 | an external trigger. 15 | 16 | It uses a TM1637 four-digit display. I built my example 17 | with one built by RobotDYN, which includes the decmial 18 | points, but no colon (like for a clock display). 19 | 20 | There's three buttons, a right/up, left/down, and a reset 21 | button. There's one input for an external trigger. Finally, 22 | there's a potentiometer serving as a selector knob. 23 | 24 | 25 | 26 | -------USER MANUAL-------- 27 | There's eight positions the knob can be in. 28 | Position 1: 29 | BPM display and set mode. Right button increases the BPM. 30 | Left button decreases the BPM. Reset button held for one 31 | second resets the BPM to 120. All three buttons pressed 32 | at the same time resets all the clock divisions zero-points 33 | to the same zero point. 34 | Positions 2 through 6: 35 | The display will show a C for channel, the channel number, 36 | and the clock division. Right button increases the divisor. 37 | Left button decreases the divisor. Reset button resets the 38 | channel to start on the next clock tick. 39 | Position 7: 40 | SWING MODE! The display shows a S (or a 5 haha) and how 41 | many milliseconds your 16th note triggers will be swinging. 42 | The timing goes backward and forward, so your drum pattern 43 | can start on any clock tick... basically this makes the clock 44 | "downbeat agnostic" which, I would buy that album. 45 | Right button swings one way, left button swings the other way, 46 | reset button when held for two seconds saves the tempo and 47 | channel divisors and swing value into the Arduino's EEPROM memory. 48 | 49 | With an external trigger connected, the module will automatically 50 | use the incoming triggers as 16th note triggers. So the module 51 | expects four pulses (or peaks) per beat -- 4 PPB. The MIDI standard 52 | is 24 PPB, so if your clock output is going that fast. this module 53 | won't behave as expected. It should still track, but the BPM won't 54 | make any sense, and the swing won't work. You'll just have to divide 55 | down the clocks more. Yikes... and 24 isn't a power-of-two, so the 56 | divisors won't work without changing the code. 57 | 58 | Please find the schematic for this project over here: 59 | 60 | 61 | 62 | 63 | Version history: 64 | 1.00 released on April 28, 2021, initial release 65 | 66 | 67 | 68 | */ 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | TM1637TinyDisplay display(4, 5); // 4 = CLK 5 = DIO 78 | 79 | 80 | 81 | unsigned long debounce; 82 | unsigned long newTrig; 83 | unsigned long oldTrig; 84 | unsigned long EIS; 85 | float BPM; 86 | byte BPMcounter; 87 | float BPMs[20]; 88 | word counts[6]; 89 | long Interval; 90 | volatile unsigned long extInterval; 91 | unsigned long extIntervalSum[10]; 92 | word Increment; 93 | int swing; 94 | bool swingTrack; 95 | byte swingTrackV; 96 | int analogKnob; 97 | int knob; 98 | byte switchPos; 99 | byte tracker; 100 | byte tracker2; 101 | byte ext; 102 | const word triggerLength = 8000; // trigger length in microseconds 103 | byte divider[6]; 104 | 105 | /* 106 | The divisions[] constant is numerator of a fraction with 16 as the denominator. 107 | A 1 means "one sixteenth notes". 2 means "two sixteenth notes" which is 1/8th notes. 108 | 16/16 is whole notes. 32 means one trigger per two bars. And so on. The number shown 109 | on the channel-division-select mode is which number in this array is selected. 110 | So "C1 1" means channel 1 will be playing 1/16th notes. "C1 5" means whole notes. 111 | "C1 b" means the 11th number in this array will be selected (B is hex for 11) so that 112 | will be playing 1024/16th notes, which is... uh... 16 bars? 113 | 114 | These numbers can be changed here and you can get creative weird patterns going if 115 | that's something you're interested in. 116 | */ 117 | const word divisions[] = {0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096}; 118 | 119 | 120 | 121 | 122 | 123 | void setup() { 124 | pinMode(2, INPUT_PULLUP); // input for external trigger 125 | 126 | pinMode(8, OUTPUT); //PORTB B0000000x, trigger 1 output 127 | pinMode(9, OUTPUT); //PORTB B000000x0, trigger 2 output 128 | pinMode(10, OUTPUT); //PORTB B00000x00, trigger 3 output 129 | pinMode(11, OUTPUT); //PORTB B0000x000, trigger 4 output 130 | pinMode(12, OUTPUT); //PORTB B000x0000, trigger 5 output 131 | pinMode(13, OUTPUT); //PORTB B00x00000, trigger 6 output 132 | 133 | pinMode(14, INPUT_PULLUP); //PINC B0000000x "left" button, A0 134 | pinMode(15, INPUT_PULLUP); //PINC B000000x0 "right" button, A1 135 | pinMode(16, INPUT_PULLUP); //PINC B00000x00 reset timing button, A2 136 | 137 | //initialize the display 138 | display.setBrightness(7); 139 | uint8_t data[] = {0, 0, 0, 0}; 140 | display.setSegments(data); 141 | 142 | { // handles reading the contents of the EEPROM memory 143 | // this must be saved by moving knob to the swing setting 144 | // (fully clockwise) and holding the reset button for 2 seconds 145 | 146 | EEPROM.get(0, divider); 147 | byte address = sizeof(divider); 148 | EEPROM.get(address, Interval); 149 | address = address + sizeof(Interval); 150 | EEPROM.get(address, swing); 151 | if (divider[0] == 255) { // only gets run when the memory has never been written to 152 | divider[0] = 1; 153 | divider[1] = 1; 154 | divider[2] = 2; 155 | divider[3] = 4; 156 | divider[4] = 5; 157 | divider[5] = 9; 158 | Interval = 125000; // 4 PPB, 120 BPM 159 | } 160 | } // end of EEPROM reading 161 | 162 | 163 | Timer1.initialize(); 164 | Timer1.attachInterrupt(clockISR); // clock advance and trigger stuff 165 | 166 | attachInterrupt(digitalPinToInterrupt(2), extISR, FALLING); 167 | 168 | // Serial.begin(115200); 169 | 170 | } 171 | 172 | void extISR() { // this runs when the module gets an external trigger 173 | 174 | ext = 1; // tells the rest of the program that there's external triggering happening 175 | 176 | // averages ten incoming trigger intervals for a more steady BPM display 177 | if (BPMcounter > 9) BPMcounter = 0; 178 | newTrig = micros(); 179 | extIntervalSum[BPMcounter] = newTrig - oldTrig; 180 | oldTrig = newTrig; 181 | BPMcounter++; 182 | 183 | 184 | if (swing == 0) { 185 | swingTrackV = 1; // forces the clockISR() to play the triggers 186 | clockISR(); 187 | } else { 188 | swingTrack = !swingTrack; 189 | if (swingTrack) { // every other trigger THIS happens 190 | if (swing > 0) { // delays this trigger 191 | Timer1.setPeriod(swing); 192 | swingTrackV = 1; 193 | } else { 194 | for (byte i = 0; i < 6; i++) { 195 | counts[i]++; 196 | if (counts[i] >= divisions[divider[i]]) { 197 | counts[i] = 0; 198 | bitWrite(PORTB, i, 1); 199 | } 200 | } 201 | swingTrackV = 0; 202 | Timer1.setPeriod(triggerLength); 203 | } 204 | } else { // every other OTHER trigger THIS happens 205 | if (swing < 0) { // delays this trigger! 206 | Timer1.setPeriod(abs(swing)); 207 | swingTrackV = 1; 208 | } else { 209 | for (byte i = 0; i < 6; i++) { 210 | counts[i]++; 211 | if (counts[i] >= divisions[divider[i]]) { 212 | counts[i] = 0; 213 | bitWrite(PORTB, i, 1); 214 | } 215 | } 216 | swingTrackV = 0; 217 | Timer1.setPeriod(triggerLength); 218 | } 219 | } 220 | } 221 | EIS = 0; 222 | for (byte i = 0; i < 10; i++) { 223 | EIS = EIS + extIntervalSum[i]; 224 | } 225 | extInterval = EIS * 0.1; 226 | } 227 | 228 | 229 | void clockISR() { // here's the free-running clock 230 | swingTrackV++; 231 | 232 | if (bitRead(swingTrackV, 0) == 0) { // here's the PLAY NOW section 233 | for (byte i = 0; i < 6; i++) { 234 | counts[i]++; 235 | if (counts[i] >= divisions[divider[i]]) { 236 | counts[i] = 0; 237 | bitWrite(PORTB, i, 1); 238 | } 239 | } 240 | Timer1.setPeriod(triggerLength); 241 | } else if (ext == 0) { 242 | PORTB &= 11000000; //turns off triggers 243 | if (bitRead(swingTrackV, 1) == 0) { 244 | Timer1.setPeriod(Interval - triggerLength + swing); 245 | } else { 246 | Timer1.setPeriod(Interval - triggerLength - swing); 247 | } 248 | } else { // if the external trigger is in play... 249 | PORTB &= 11000000; 250 | Timer1.setPeriod(500000); // restart internal clock in 0.5 seconds unless there's another external trigger event 251 | } 252 | } 253 | 254 | 255 | 256 | void loop() { 257 | knob = map(analogRead(A7), 1, 1000, 0, 7); 258 | switchPos = 0; 259 | if (bitRead(PINC, 0) == 0) switchPos = 2; 260 | if (bitRead(PINC, 2) == 0) switchPos = 1; 261 | if (bitRead(PINC, 1) == 0) switchPos = 3; 262 | if ((PINC & B00000111) == 0) { // all three buttons pressed simultaneously -- resets all counts to zero 263 | switchPos = 0; 264 | for (byte i = 0; i < 6; i++) { 265 | counts[i] = 0; 266 | } 267 | } 268 | if (micros() - newTrig > 500000) ext = 0; 269 | if (switchPos == 0) tracker = 0; 270 | { // knob zero means show BPM, left and right buttons adjust BPM 271 | if (knob == 0) { 272 | display.showNumber(BPM, 1); 273 | 274 | { // change BPM 275 | if (tracker == 0 && switchPos > 0) { 276 | debounce = millis(); 277 | tracker = 1; 278 | switch (switchPos) { 279 | case 1: 280 | Interval += Increment; 281 | break; 282 | case 2: 283 | Interval -= Increment; 284 | break; 285 | } 286 | } 287 | if (tracker == 1) { 288 | if (millis() - debounce > 200) { 289 | switch (switchPos) { 290 | case 1: 291 | Interval += Increment; 292 | break; 293 | case 2: 294 | Interval -= Increment; 295 | break; 296 | } 297 | } 298 | if (switchPos == 3 && millis() - debounce > 2000) { 299 | tracker = 2; 300 | Interval = 125000; 301 | } 302 | } 303 | Interval = constrain(Interval, 68193, 272848); 304 | } // end of change BPM 305 | 306 | } else if (knob < 7) { // change the divisions of each channel 307 | if (bitRead(PINC, 1) == 0 && tracker == 0) { 308 | counts[knob - 1] = 6000; 309 | tracker = 1; 310 | } 311 | uint8_t data[] = {57, 0, 0, 0}; 312 | data[1] = display.encodeDigit(knob); 313 | data[3] = display.encodeDigit(divider[knob - 1]); 314 | display.setSegments(data); 315 | if (switchPos == 1 && tracker == 0) { 316 | divider[knob - 1]--; 317 | tracker = 10; 318 | } 319 | if (switchPos == 2 && tracker == 0) { 320 | divider[knob - 1]++; 321 | tracker = 10; 322 | } 323 | divider[knob - 1] = constrain(divider[knob - 1], 1, 13); 324 | } 325 | } 326 | 327 | 328 | if (knob == 7) { // swing adjustment 329 | uint8_t data[] = {109, 0, 0, 0}; // sets up the display 330 | if (tracker == 0) { 331 | if (switchPos == 2) { 332 | swing += 1000; 333 | tracker = 1; 334 | } 335 | if (switchPos == 1) { 336 | swing -= 1000; 337 | tracker = 1; 338 | } 339 | if (switchPos == 3) { 340 | debounce = millis(); 341 | tracker = 4; 342 | } 343 | } 344 | if (tracker == 4 && millis() - debounce > 2000) { 345 | EEPROM.put(0, divider); 346 | byte address = sizeof(divider); 347 | EEPROM.put(address, Interval); 348 | address = address + sizeof(Interval); 349 | EEPROM.put(address, swing); 350 | display.showNumber(8888); 351 | tracker = 5; 352 | } 353 | swing = constrain(swing, -60000, 60000); 354 | if (swing <= 1) { 355 | data[1] = 64; // minus sign for negative swing values 356 | } 357 | int TS1 = swing * 0.0001; 358 | data[2] = display.encodeDigit(abs(TS1)); 359 | int TS2 = (swing * 0.001); 360 | TS2 = TS2 % 10; 361 | data[3] = display.encodeDigit(abs(TS2)); 362 | display.setSegments(data); 363 | } 364 | 365 | Increment = Interval * 0.000834; // calculate increment so the BPM will go up by one decmial point per button press 366 | 367 | // snags the important values from the interrupt service routine for external BPM calculations 368 | noInterrupts(); 369 | byte ext1 = ext; 370 | unsigned long extInterval1 = extInterval; 371 | interrupts(); 372 | 373 | if (ext1 == 1) { 374 | BPM = 15000000.0 / extInterval1; 375 | } else BPM = 15000000.0 / Interval; // calculate the BPM when self-clocked 376 | } 377 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HexKlox 2 | A six-channel clock generator with 0.1BPM accuracy and variable clock divisions and start points WITH SWING 3 | 4 | MODULAR FOR THE MASSES 5 | HexKlox 6 | Juanito Moore, April 2021 7 | 8 | This is a six-channel clock. The channels can be set to send triggers every 16th note, every 8th, quarter note, half note, et cetera, up to 64 bars or even MORE. It displays the BPM, and this clock can do swing, even with an external trigger. 9 | 10 | It uses a TM1637 four-digit display. I built my example with one built by RobotDYN, which includes the decmial points, but no colon (like for a clock display). 11 | 12 | There's three buttons, a right/up, left/down, and a reset button. There's one input for an external trigger. Finally, there's a potentiometer serving as a selector knob. 13 | 14 | 15 | 16 | -------USER MANUAL-------- 17 | There's eight positions the knob can be in. 18 | Position 1: 19 | BPM display and set mode. Right button increases the BPM. Left button decreases the BPM. Reset button held for one second resets the BPM to 120. All three buttons pressed at the same time resets all the clock divisions zero-points to the same zero point. 20 | Positions 2 through 6: 21 | The display will show a C for channel, the channel number, and the clock division. Right button increases the divisor. Left button decreases the divisor. Reset button resets the channel to start on the next clock tick. 22 | Position 7: 23 | SWING MODE! The display shows a S (or a 5 haha) and how many milliseconds your 16th note triggers will be swinging. The timing goes backward and forward, so your drum pattern can start on any clock tick... basically this makes the clock "downbeat agnostic" which, I would buy that album. Right button swings one way, left button swings the other way, reset button when held for two seconds saves the tempo and channel divisors and swing value into the Arduino's EEPROM memory. 24 | 25 | With an external trigger connected, the module will automatically use the incoming triggers as 16th note triggers. So the module expects four pulses (or peaks) per beat -- 4 PPB. The MIDI standard is 24 PPB, so if your clock output is going that fast. this module won't behave as expected. It should still track, but the BPM won't make any sense, and the swing won't work. You'll just have to divide down the clocks more. Yikes... and 24 isn't a power-of-two, so the divisors won't work without changing the code. 26 | 27 | 28 | 29 | 30 | 31 | Version history: 32 | 1.01 OLED version for use with the SSD1306 OLED 128x64 pixel display. Uses the Adafruit libraries 33 | 1.00 released on April 28, 2021, initial release 34 | 35 | -------------------------------------------------------------------------------- /SoClocks schematic.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozerik/HexKlox/8da72c10814dfe261fc241c266142fa0a80de67b/SoClocks schematic.jpeg --------------------------------------------------------------------------------