-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmultigrain.ino
More file actions
366 lines (283 loc) · 7.93 KB
/
multigrain.ino
File metadata and controls
366 lines (283 loc) · 7.93 KB
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
/**
* @file multigrain.ino
* @author Alex Hiam - <alex@graycat.io>
* @copyright Copyright (c) 2018 - Gray Cat Labs, Alex Hiam <alex@graycat.io>
* @license Released under the MIT License
*
* @brief An alternate firmware for the GinkoSynthese Grains eurorack module.
* Grains: https://www.ginkosynthese.com/product/grains/
*/
#include "grains.h"
#define MODE_KNOB KNOB_1
#define CLOCK_GEN_MIN_PERIOD_US 1000
#define CLOCK_GEN_MAX_PERIOD_US 2000000
#define T_TO_G_MIN_WIDTH_US 0
#define T_TO_G_MAX_WIDTH_US 2000000
/**
* The different modes (states)
*/
typedef enum {
MODE_CLOCK_GEN=0, ///< Clock generator
MODE_T_TO_G, ///< Trigger to gate converter
MODE_NOISERING, ///< "Noisering"
N_MODES ///< Number of modes
} mode_t;
// The next value to be written to the PWM OCR register:
static volatile uint8_t sg_pwm_value = 0;
static mode_t sg_current_mode = MODE_CLOCK_GEN;
/**
* Samples the mode input and returns the currently selected mode.
*/
mode_t getMode() {
uint16_t mode_val;
static avg_t mode_avg;
// Read the mode knob:
mode_val = sampleAveraged(MODE_KNOB, &mode_avg);
if (mode_val < 128) {
// First 1/4
return MODE_CLOCK_GEN;
}
else if (mode_val < 512) {
// Second 1/4
return MODE_T_TO_G;
}
else {
// Second 1/2 of the knob is all noisering
return MODE_NOISERING;
}
}
void setup() {
pinMode(PWM_PIN, OUTPUT);
audioOn();
pinMode(LED_PIN, OUTPUT);
}
void loop() {
mode_t mode;
// Get the selected mode:
mode = getMode();
// Run the non-interrupt part of the modes:
switch (mode) {
case MODE_CLOCK_GEN:
runClockGen();
break;
case MODE_T_TO_G:
runTtoG();
break;
case MODE_NOISERING:
runNoiseRing();
break;
default:
// Shouldn't ever happen
break;
}
// We set the static global mode variable after running the mode
// routines, when there's a mode transition it's garunteed that
// the mode has been 'setup' before the interrupt part is run:
sg_current_mode = mode;
}
//=====clock generator===========================
static uint64_t sg_clock_gen_on_us = 0;
static uint64_t sg_clock_gen_off_us;
/**
* To be run in the main loop
*/
void runClockGen() {
static avg_t freq_avg, duty_avg;
static uint16_t last_freq=1025, last_duty=1025;
uint16_t freq, duty;
uint64_t period_us, on_us;
// Use a logaithmic response for the frequency knob for better control:
freq = mapLog(sampleAveraged(KNOB_2, &freq_avg));
duty = sampleAveraged(KNOB_3, &duty_avg);
if (freq != last_freq || duty != last_duty) {
// Only calculate/update values if changed
last_duty = duty;
last_freq = freq;
duty = map(duty, 0, 1023, 0, 110); // max of 110 gives some dead space at the top to ensure100%
duty = constrain(duty, 0, 100);
period_us = map(freq, 0, 1023, CLOCK_GEN_MAX_PERIOD_US, CLOCK_GEN_MIN_PERIOD_US);
// Just to be safe:
period_us = constrain(period_us, CLOCK_GEN_MIN_PERIOD_US, CLOCK_GEN_MAX_PERIOD_US);
// Clear on-time (disable output) before changing to reduce glitching:
sg_clock_gen_on_us = 0;
on_us = (period_us / 100) * duty;
sg_clock_gen_off_us = period_us - on_us;
sg_clock_gen_on_us = on_us;
}
}
/**
* To be run in the PWM ISR
*/
static void tickClockGen() {
static uint64_t last_transition_us=0;
static uint8_t state;
uint64_t t_us, elapsed, target;
// Get current time:
t_us = micros();
elapsed = t_us - last_transition_us;
if (sg_clock_gen_on_us) {
// Non-zero on-time
if (sg_clock_gen_off_us == 0) {
// 100% duty cycle
state = 0xff;
}
else {
// Get width of current phase:
target = state ? sg_clock_gen_on_us : sg_clock_gen_off_us;
if (elapsed >= target) {
// Width reached, transition
last_transition_us = t_us;
state ^= 0xff;
}
}
}
else {
// 0% duty cycle, disable output
state = 0;
}
sg_pwm_value = state;
digitalWrite(LED_PIN, state & 0x1);
}
//===============================================
//=====trigger to gate===========================
static uint64_t sg_t_to_g_width_us = 0;
static uint64_t sg_t_to_g_trigger_us = 0;
void runTtoG() {
static avg_t width_avg, prob_avg;
uint64_t pulse_width;
uint16_t prob;
pulse_width = mapExp(sampleAveraged(KNOB_3, &width_avg));
pulse_width = map(pulse_width, 0, 1023, T_TO_G_MIN_WIDTH_US, T_TO_G_MAX_WIDTH_US);
// Just to be safe:
pulse_width = constrain(pulse_width, T_TO_G_MIN_WIDTH_US, T_TO_G_MAX_WIDTH_US);
// Probability to skip a trigger:
prob = mapExp(sampleAveraged(KNOB_2, &prob_avg));
prob = map(prob, 0, 1023, 0, 100);
if (getTrigger(EDGE_RISING)) {
// WeTrigger received
if (prob < 100) {
if (prob == 0 || !random(prob)) {
sg_t_to_g_width_us = pulse_width;
sg_t_to_g_trigger_us = micros();
sg_pwm_value = 0xff;
digitalWrite(LED_PIN, 0x1);
}
}
// Otherwise skip at 100%, nothing to do
}
else {
// No new trigger - only update pulse_width if currently gating:
if (sg_pwm_value) {
sg_t_to_g_width_us = pulse_width;
}
// Otherwise it can falsely "retrigger" when increasing pulse width
else {
digitalWrite(LED_PIN, 0x0);
}
}
}
static inline void tickTtoG() {
uint64_t t_us, elapsed, target;
// Current time:
t_us = micros();
elapsed = t_us - sg_t_to_g_trigger_us;
if (sg_t_to_g_width_us) {
// Non-zero pulse width
if (elapsed >= sg_t_to_g_width_us) {
// Pulse over
sg_pwm_value = 0;
}
else {
sg_pwm_value = 0xff;
}
}
else {
sg_pwm_value = 0;
}
}
//===============================================
//=====noisering=================================
// Initial value from grainsring:
static uint16_t sg_noise_ring_bit_array = 0x8080;
static uint8_t sg_noise_ring_bits_to_write = 8;
static uint8_t sg_noise_ring_scale = 0;
void runNoiseRing() {
uint16_t new_bits;
uint8_t change, chance, next_value;
static avg_t bits_avg;
uint16_t scale;
// Read the mode knob:
scale = sampleAveraged(MODE_KNOB, &bits_avg);
scale = constrain(scale, 512, 1000);
// 5 possilbe scales:
sg_noise_ring_scale = map(scale, 512, 1000, 0, 4);
// Probabilities based on knob values
change = analogRead(KNOB_2) > random(1024);
chance = analogRead(KNOB_3) > random(1024);
if (getTrigger(EDGE_RISING)) {
// Rotate the bit array
next_value = bitRead(sg_noise_ring_bit_array, 15);
new_bits = sg_noise_ring_bit_array << 1;
if (change) {
next_value = chance;
}
bitWrite(new_bits, 0, next_value);
sg_noise_ring_bit_array = new_bits;
digitalWrite(LED_PIN, new_bits & 0x1);
}
}
static inline void tickNoiseRing() {
uint8_t mask = 0xff, value;
if (sg_noise_ring_bits_to_write == 1) {
// Currently disabled
sg_pwm_value = (sg_noise_ring_bit_array & 0x1) * 0xff;
}
else {
mask >>= 8 - sg_noise_ring_bits_to_write;
value = sg_noise_ring_bit_array & mask;
switch(sg_noise_ring_scale) {
case 1:
// Quantize to semitones
sg_pwm_value = mapSemitone(value);
break;
case 2:
// Quantize to major scale
sg_pwm_value = mapMajor(value);
break;
case 3:
// Quantize to minor scale
sg_pwm_value = mapMinor(value);
break;
case 4:
// Quantize to mixolydian scale
sg_pwm_value = mapMixolydian(value);
break;
case 0:
default:
// No quantization
sg_pwm_value = value;
break;
}
}
}
//===============================================
// PWM ISR
SIGNAL(PWM_INTERRUPT) {
switch (sg_current_mode) {
case MODE_CLOCK_GEN:
tickClockGen();
break;
case MODE_T_TO_G:
tickTtoG();
break;
case MODE_NOISERING:
tickNoiseRing();
break;
default:
// This should never happen, but just in case:
sg_pwm_value = 0;
break;
}
// Update the PWM output:
setPwm(sg_pwm_value);
}