-
Notifications
You must be signed in to change notification settings - Fork 3
/
clockwithshift.ino
486 lines (398 loc) · 11.2 KB
/
clockwithshift.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
/*
Alternate firmware for the ginky synthese grains eurorack module
Code by a773 (atte.dk) and released under the GPL licence
*/
/* 11-9-2021 Adapted by Jesse Stevens of artist duo Cake Industries for Look Mum No Computer offbeat shift needs */
/* 16-10-2021 Further changes to allow for longer gaps between incoming beats and logic to handle multi/div changes between beats for Look Mum No Computer */
/* 24-10-2021 Reworked by Rohan Mitchell for easier multi/div changes between beats */
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#define TRIGGER_LENGTH 20
#define MULT_POT 2
#define DIV_POT 1
#define MODE_POT 0
#define BEATSHIFT_POT 5
#define CLOCK_IN 3
#define UPPER_POT_MAX 1024
#define MIDDLE_POT_MAX 1024
#define LOWER_POT_MAX 1024
#define NB_POT_SLICES 10
#define MODE_SIMPLE 0
#define MODE_COMPLEX 1
#define SHIFTED_OUT 11
#define UNSHIFTED_OUT 10
#define KNOB_READING_INTERVAL 250
/**
* Interface for reading and interpreting the control knobs.
*/
class Controls {
private:
const int SIMPLE_FACTORS[10] = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512};
const int COMPLEX_FACTORS[10] = {1, 3, 5, 7, 11, 13, 17, 19, 23, 29};
public:
int mult_reading;
int div_reading;
int mode_reading;
float beatshift;
int mode;
bool stopped;
Controls() {
mode = -1;
stopped = false;
}
void setup() {
pinMode(CLOCK_IN, INPUT_PULLUP);
pinMode(SHIFTED_OUT, OUTPUT);
pinMode(UNSHIFTED_OUT, OUTPUT);
}
void read() {
mult_reading = analogRead(MULT_POT);
div_reading = analogRead(DIV_POT);
mode_reading = analogRead(MODE_POT);
beatshift = analogRead(BEATSHIFT_POT);
Serial.print(get_mult());
Serial.print(", ");
Serial.print(get_div());
Serial.print(", ");
Serial.println(get_beatshift());
}
void updateSettings(bool edge) {
if (mode_reading < LOWER_POT_MAX/3) {
// CCW simple mode
if(edge) {
mode = MODE_SIMPLE;
stopped = false;
}
} else if(mode_reading > LOWER_POT_MAX/3*2) {
// CW complex mode
if(edge) {
mode = MODE_COMPLEX;
stopped = false;
}
} else {
// stopped
stopped = true;
}
}
/**
* Fetch the multiplication factor
* Output clock frequency = input clock frequency * multiplication factor
* This value is controlled by the upper pot. We divide the pot's range
* into NB_POT_SLICES slices, and then use those to look up a factor from
* either simple_factors (powers of two) or complex_factors (prime numbers).
*/
int get_mult() {
int slice = mult_reading * (NB_POT_SLICES) / UPPER_POT_MAX;
return slice2factor(slice, mode);
}
/**
* Fetch the division factor
* Output clock frequency = input clock frequency / division factor
* This value is controlled by the middle pot. We divide the pot's range
* into NB_POT_SLICES slices, and then use those to look up a factor from
* either simple_factors (powers of two) or complex_factors (prime numbers).
*/
int get_div() {
int slice = div_reading * (NB_POT_SLICES) / MIDDLE_POT_MAX;
int factor = slice2factor(slice, mode);
return factor;
}
/**
* Fetch the beatshift factor (scaled 0-1)
* This value is controlled by the lower pot. Of the 0-1023 range, we introduce
* a dead zone from 0-30, and then linearly interpolate from 0-1 for the
* remainder of the range.
*/
float get_beatshift() {
if (beatshift < 30) {
return 0;
} else {
return map(beatshift, 30, 1023, 0, 95) / 100.0;
}
}
private:
int slice2factor(int slice, int mode) {
if(mode == MODE_SIMPLE) {
return SIMPLE_FACTORS[slice];
} else {
return COMPLEX_FACTORS[slice];
}
}
};
/**
* GateReader reads the clock pin and detects edges.
*/
class GateReader {
private:
bool clock_high;
public:
GateReader() {
clock_high = false;
}
bool readEdge(long now) {
int gate = digitalRead(CLOCK_IN);
bool edge = false;
// My setup is reverse logic trigger (using NPN transistor as buffer on input)
if (gate == LOW && !clock_high) {
edge = true;
}
clock_high = gate == LOW;
return edge;
}
};
/**
* Given control inputs and fed input edges, TimeKeeper will report the output
* wavelength and when to fire an output trigger (taking mult and div factors
* into account).
*/
class TimeKeeper {
private:
Controls* controls;
int edge_count;
long last_edge;
int last_div;
long last_phrase_start;
long last_scaled_time;
long wavelength;
bool was_in_output_pulse;
bool fire_trigger;
long last_trigger;
public:
TimeKeeper(Controls* controls) {
this->controls = controls;
edge_count = 0;
last_edge = 0;
last_div = 0;
last_phrase_start = 0;
wavelength = 0;
was_in_output_pulse = false;
fire_trigger = false;
last_trigger = 0;
}
void update(long now, bool edge) {
if (edge) {
processEdge(now);
}
bool outputEdge = this->outputEdge(now);
// Detect start of phrase
if (edge && now > readyForEndOfPhrase()) {
last_phrase_start = now;
last_scaled_time = 0;
}
handleDivReduction(now);
fire_trigger = false;
if (debounce(now, haveWavelength() && outputEdge)) {
fire_trigger = true;
last_trigger = now;
}
}
long outputWavelength() {
return float(wavelength) / controls->get_mult();
}
// Returns whether to fire the output trigger
bool fireTrigger() {
return fire_trigger;
}
private:
// Accept an input clock and keep track of wavelength
void processEdge(long now) {
if (last_edge != 0) {
wavelength = now - last_edge;
}
last_edge = now;
}
/**
* To determine when to show output pulses, we sample an imaginary output waveform
* (a square wave at 50% duty cycle of the desired frequency), and then perform
* edge detection. This method will return true when we detect a leading edge of this
* imaginary output waveform.
*/
bool outputEdge(long now) {
bool edge = false;
if (inOutputPulse(now)) {
if (!was_in_output_pulse) {
was_in_output_pulse = true;
edge = true;
}
} else {
was_in_output_pulse = false;
}
return edge;
}
// Skip double-triggers
bool debounce(long now, bool trigger) {
if (trigger) {
int trigger_period = now - last_trigger;
if (trigger_period < beatLength() * 0.8) {
return false;
}
}
return trigger;
}
/**
* Sample an imaginary output waveform - a square wave at 50% duty cycle, with a wavelength
* equal to input_wavelength * multiplication_factor.
*/
bool inOutputPulse(long now) {
long offset = now - last_phrase_start;
float relative_time = offset / float(wavelength);
long scaled_time = relative_time * scaleFactor() * 2;
// Given a wavelength of 100ms and a multiplication factor of 2,
// our relative fraction and modulo values will be:
// 0 ms: 0; 0 % 2 = 0
// 25 ms: 1; 1 % 2 = 1
// 50 ms: 2; 2 % 2 = 0
// 75 ms: 3; 3 % 2 = 1
// 100 ms: 4; 4 % 2 = 0
// In this example, this gives us a new wavelength of 50ms
if (pastEndOfPhrase(now)) {
return false;
} else {
return scaled_time % 2 == 0;
}
}
// If divider has just been reduced, our phrase length has also
// been reduced, which may well leave us past the end of phrase.
// If this happens we would stop outputting triggers until the start
// of the next phrase, which would cause an undesired silence.
// Prevent this by resetting the start of the phrase.
void handleDivReduction(long now) {
if (divReducedThisFrame() && pastEndOfPhrase(now)) {
last_phrase_start = last_edge;
}
}
bool divReducedThisFrame() {
bool result = last_div > controls->get_div();
last_div = controls->get_div();
return result;
}
bool pastEndOfPhrase(long now) {
return now >= last_phrase_start + phraseLength();
}
/**
* Return a time when we've received all our clocks for the phrase
* and the next clock we get should be considered the start of the
* next phrase.
*/
long readyForEndOfPhrase() {
long delta = wavelength / 2;
return last_phrase_start + phraseLength() - delta;
}
// Returns whether we've determined a wavelength reading
bool haveWavelength() {
return wavelength > 0;
}
float scaleFactor() {
return controls->get_mult() / float(controls->get_div());
}
long beatLength() {
return wavelength / scaleFactor();
}
long phraseLength() {
return long(wavelength) * controls->get_div();
}
};
class TimeFollower {
private:
Controls* controls;
long last_signal;
long wavelength;
bool output_fired;
public:
TimeFollower(Controls* controls) {
this->controls = controls;
last_signal = 0;
wavelength = 0;
output_fired = false;
}
bool shouldFire(long now, bool signal) {
// Detect edges and track last signal
if (signal) {
if (last_signal > 0) {
wavelength = now - last_signal;
}
last_signal = now;
output_fired = false;
}
// Fire output
if (now >= last_signal + delayTime() && !output_fired) {
output_fired = true;
return true;
}
return false;
}
private:
long delayTime() {
return wavelength * controls->get_beatshift();
}
};
class Trigger {
private:
int pin;
int triggerlength;
bool clock_high;
public:
long last_trigger_out;
Trigger(int pin) {
this->pin = pin;
triggerlength = TRIGGER_LENGTH;
clock_high = false;
last_trigger_out = 0;
}
// Fire the trigger, for length in ms
void fire(long now, int triggerlength) {
digitalWrite(pin, HIGH);
this->triggerlength = triggerlength;
clock_high = true;
last_trigger_out = now;
}
// Update the trigger, setting pin to LOW when duration has expired
void update(long now) {
if( ((now - last_trigger_out) > triggerlength) && clock_high) {
digitalWrite(pin, LOW);
clock_high = false;
}
}
};
long now = 0;
unsigned long last_knob_read = 0;
GateReader gateReader;
Controls controls;
TimeKeeper timeKeeper(&controls);
TimeFollower timeFollower(&controls);
Trigger unshiftedTrigger(UNSHIFTED_OUT);
Trigger shiftedTrigger(SHIFTED_OUT);
void setup() {
controls.setup();
controls.read();
Serial.begin(115200);
}
void loop()
{
// Store the current time:
now = millis();
// Read the gate edge into "edge" variable (find edge of incoming gate):
bool edge = gateReader.readEdge(now);
// Check if we're in simple or complex numbers mode:
controls.updateSettings(edge);
// If it's been long enough, update readings from knobs for multi/div:
if (now - last_knob_read >= KNOB_READING_INTERVAL) {
controls.read();
last_knob_read = now;
}
// Vary the output trigger length to make sure when we're going fast we don't just keep outputs on:
int trigger_length = min(TRIGGER_LENGTH, timeKeeper.outputWavelength() / 2);
// Trigger update
unshiftedTrigger.update(now);
shiftedTrigger.update(now);
// Fire unshifted trigger
timeKeeper.update(now, edge);
if (timeKeeper.fireTrigger()) {
unshiftedTrigger.fire(now, trigger_length);
}
// Fire shifted trigger
if (timeFollower.shouldFire(now, timeKeeper.fireTrigger())) {
shiftedTrigger.fire(now, trigger_length);
}
}