-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patheurorack-quantizer-software.ino
More file actions
332 lines (278 loc) · 10.6 KB
/
eurorack-quantizer-software.ino
File metadata and controls
332 lines (278 loc) · 10.6 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
/***
POLYKIT Quantizer
https://polykit.rocks/quantizer
License: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
***/
#include <SPI.h>
// =====================
// Hardware configuration
// =====================
// Adjust these to your wiring. Arduino Nano SPI uses MOSI=11, SCK=13
// Control inputs
const int CV_IN_PIN = A0; // Analog CV input (0-5V expected via conditioning)
const int SCALE_POT_PIN = A1; // Pot to pick scale index
const int TRANSPOSE_POT_PIN = A2; // Pot for transpose (-24..+24 semitones)
// Trigger and gate I/O
const int TRIGGER_IN_PIN = 6; // Digital trigger input for sampling (rising-edge)
const int GATE_OUT_PIN = 8; // Digital gate output, pulses when output changes
const int LED_PIN = 7; // Activity LED: blinks on gate or trigger
// MCP4821 SPI
const int DAC_CS_PIN = 10; // Chip Select for MCP4821 (use a dedicated SS pin)
const int DAC_LDAC_PIN = 9; // LDAC pin to latch output (active LOW)
// =====================
// Quantizer configuration
// =====================
// Calibration: output voltage = OUTPUT_OFFSET_V + (semitones/12.0) *
// OUTPUT_VOLTS_PER_OCT With MCP4821 gain=2x, full-scale is ~4.096V. External
// scaling may be required for 1V/oct.
const float OUTPUT_VOLTS_PER_OCT = 1.000f; // Set via calibration chain to achieve 1.000 V/oct
const float OUTPUT_OFFSET_V = 0.000f; // Offset voltage at 0 semitones
const float OUTPUT_STAGE_GAIN = 2.0f; // Op-amp stage doubles DAC voltage (set to 2.0)
// DAC configuration
const bool DAC_GAIN_2X = true; // true => 2x gain (0..4.096V), false => 1x (0..2.048V)
// DAC non-linearity compensation (Polynomial Correction)
const bool DAC_POLY_CORRECTION_ENABLED = true;
// Polynomial coefficients: correction = 1.0 + A*n + B*n² + C*n³
// where n = normalized code (0.0 to 1.0)
// Calibrate by measuring actual output vs desired output at multiple points
// Typical values: A=0.0, B=0.0001 to 0.001, C=0.0 (start with B only)
const float DAC_CORR_A = 0.0f; // Linear term (usually 0)
const float DAC_CORR_B = 0.001f; // Quadratic term (adjust during calibration)
const float DAC_CORR_C = 0.0f; // Cubic term (usually 0, for fine-tuning)
// Behavior
const unsigned long GATE_PULSE_MS = 10; // Gate pulse duration when note changes
const int MIN_SEMITONE = 0; // Clamp range if desired
const int MAX_SEMITONE = 12 * 10; // 10 octaves range for safety
// Analog filtering / hysteresis
const int ANALOG_SAMPLES = 5; // Simple box filter for CV input
// =====================
// Musical scales (degrees within 12-TET)
// =====================
struct Scale {
const char* name;
const uint8_t* degrees; // sorted ascending unique degrees [0..11]
uint8_t count;
};
const uint8_t SCALE_CHROMATIC[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
const uint8_t SCALE_MAJOR[] = {0, 2, 4, 5, 7, 9, 11};
const uint8_t SCALE_NAT_MINOR[] = {0, 2, 3, 5, 7, 8, 10};
const uint8_t SCALE_PENT_MAJOR[] = {0, 2, 4, 7, 9};
const uint8_t SCALE_PENT_MINOR[] = {0, 3, 5, 7, 10};
const uint8_t SCALE_DORIAN[] = {0, 2, 3, 5, 7, 9, 10};
const uint8_t SCALE_MIXOLYDIAN[] = {0, 2, 4, 5, 7, 9, 10};
const uint8_t SCALE_WHOLE[] = {0, 2, 4, 6, 8, 10};
const uint8_t SCALE_HARM_MINOR[] = {0, 2, 3, 5, 7, 8, 11};
const uint8_t SCALE_BYPASS_DUMMY[] = {0};
const Scale SCALES[] = {
{"Chromatic", SCALE_CHROMATIC, sizeof(SCALE_CHROMATIC)},
{"Major", SCALE_MAJOR, sizeof(SCALE_MAJOR)},
{"Natural Minor", SCALE_NAT_MINOR, sizeof(SCALE_NAT_MINOR)},
{"Pent Major", SCALE_PENT_MAJOR, sizeof(SCALE_PENT_MAJOR)},
{"Pent Minor", SCALE_PENT_MINOR, sizeof(SCALE_PENT_MINOR)},
{"Dorian", SCALE_DORIAN, sizeof(SCALE_DORIAN)},
{"Mixolydian", SCALE_MIXOLYDIAN, sizeof(SCALE_MIXOLYDIAN)},
{"Whole Tone", SCALE_WHOLE, sizeof(SCALE_WHOLE)},
{"Harm Minor", SCALE_HARM_MINOR, sizeof(SCALE_HARM_MINOR)},
{"Bypass (No Quantize)", SCALE_BYPASS_DUMMY, sizeof(SCALE_BYPASS_DUMMY)},
};
const int NUM_SCALES = sizeof(SCALES) / sizeof(SCALES[0]);
// =====================
// State
// =====================
volatile bool triggerRise = false; // set via polling edge detect in loop
int lastQuantizedSemitone = INT32_MIN; // sentinel
unsigned long gateHighUntilMs = 0;
unsigned long ledHighUntilMs = 0;
float lastSampledVin = 0.0f;
int lastTranspose = 0;
int lastScaleIdx = 0;
bool updateDac = false;
bool lastTrigger = false;
bool gatePulseRequested = false;
// =====================
// Helpers
// =====================
static inline float readAveragedVoltage(int pin, int samples) {
long acc = 0;
for (int i = 0; i < samples; ++i) {
acc += analogRead(pin);
}
float avg = (float)acc / (float)samples;
// Arduino Nano default reference is 5.0V
return (avg / 1023.0f) * 5.0f;
}
static inline int readTransposeSemitones() {
int raw = analogRead(TRANSPOSE_POT_PIN); // 0..1023
// Map to -24..+24 semitones with center deadband
int semis = map(raw, 0, 1023, -24, 24);
return semis;
}
static inline int readScaleIndex() {
int raw = analogRead(SCALE_POT_PIN); // 0..1023
int idx = map(raw, 0, 1023, 0, NUM_SCALES - 1);
return idx;
}
static inline int nearestDegreeInScale(int degree12, const Scale& scale) {
// degree12 in [0..11]
int best = scale.degrees[0];
int bestDist = 128;
for (uint8_t i = 0; i < scale.count; ++i) {
int d = scale.degrees[i];
int dist = abs(d - degree12);
if (dist < bestDist) {
bestDist = dist;
best = d;
}
}
return best;
}
static inline int quantizeSemitoneToScale(int semitone, const Scale& scale) {
int octave = (semitone >= 0) ? (semitone / 12) : ((semitone - 11) / 12); // floor for negatives
int degree = semitone - octave * 12;
if (degree < 0) degree += 12; // ensure 0..11
int qdeg = nearestDegreeInScale(degree, scale);
return octave * 12 + qdeg;
}
static inline uint16_t voltageToDacCode(float vOutAtJack) {
// Compensate for output stage gain so desired jack voltage is achieved
float vToDac = vOutAtJack / OUTPUT_STAGE_GAIN;
float vfs = DAC_GAIN_2X ? 4.096f : 2.048f;
if (vToDac < 0.0f) vToDac = 0.0f;
if (vToDac > vfs) vToDac = vfs;
int desiredCode = round((vToDac / vfs) * 4095.0f);
if (desiredCode < 0) desiredCode = 0;
if (desiredCode > 4095) desiredCode = 4095;
// Apply polynomial correction for non-linearity compensation
if (DAC_POLY_CORRECTION_ENABLED) {
float normalized = (float)desiredCode / 4095.0f; // Normalize to 0.0-1.0
// Polynomial correction: code_corrected = code * (1 + A*n + B*n² + C*n³)
float n = normalized;
float n2 = n * n;
float n3 = n2 * n;
float correction = 1.0f + DAC_CORR_A * n + DAC_CORR_B * n2 + DAC_CORR_C * n3;
int correctedCode = (int)(desiredCode * correction + 0.5f);
if (correctedCode < 0) correctedCode = 0;
if (correctedCode > 4095) correctedCode = 4095;
return (uint16_t)correctedCode;
}
return (uint16_t)desiredCode;
}
static inline void dacWriteMCP4821(uint16_t code) {
// Build control word: [15:12]= 0 0 GA SHDN, [11:0]=data
uint16_t ctrl = 0;
// GA: 0 => 2x, 1 => 1x
if (DAC_GAIN_2X) {
// GA=0
ctrl |= (0 << 13);
} else {
ctrl |= (1 << 13);
}
// SHDN=1 (active)
ctrl |= (1 << 12);
uint16_t word = ctrl | (code & 0x0FFF);
digitalWrite(DAC_CS_PIN, LOW);
SPI.transfer((word >> 8) & 0xFF);
SPI.transfer(word & 0xFF);
digitalWrite(DAC_CS_PIN, HIGH);
delayMicroseconds(1);
// Pulse LDAC low to transfer input register to output
digitalWrite(DAC_LDAC_PIN, LOW);
delayMicroseconds(1);
digitalWrite(DAC_LDAC_PIN, HIGH);
}
static inline void setGatePulse() {
digitalWrite(GATE_OUT_PIN, HIGH);
gateHighUntilMs = millis() + GATE_PULSE_MS;
// Mirror to LED
digitalWrite(LED_PIN, HIGH);
ledHighUntilMs = millis() + GATE_PULSE_MS;
}
static inline void serviceGatePulse() {
if (gateHighUntilMs != 0 && millis() >= gateHighUntilMs) {
digitalWrite(GATE_OUT_PIN, LOW);
gateHighUntilMs = 0;
}
}
static inline void setLedPulse() {
digitalWrite(LED_PIN, HIGH);
ledHighUntilMs = millis() + GATE_PULSE_MS;
}
static inline void serviceLedPulse() {
if (ledHighUntilMs != 0 && millis() >= ledHighUntilMs) {
digitalWrite(LED_PIN, LOW);
ledHighUntilMs = 0;
}
}
// =====================
// Arduino setup/loop
// =====================
void setup() {
pinMode(DAC_CS_PIN, OUTPUT);
digitalWrite(DAC_CS_PIN, HIGH); // keep HIGH, pulse LOW for chip select
pinMode(DAC_LDAC_PIN, OUTPUT);
digitalWrite(DAC_LDAC_PIN, HIGH); // keep HIGH; pulse LOW to update DAC output
pinMode(GATE_OUT_PIN, OUTPUT);
digitalWrite(GATE_OUT_PIN, LOW);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
pinMode(TRIGGER_IN_PIN, INPUT);
SPI.begin();
SPI.beginTransaction(SPISettings(20000000, MSBFIRST, SPI_MODE0));
analogReference(DEFAULT); // 5V on Nano
}
void loop() {
serviceGatePulse();
serviceLedPulse();
float vin = readAveragedVoltage(CV_IN_PIN, ANALOG_SAMPLES);
if (vin != lastSampledVin) {
lastSampledVin = vin;
updateDac = true;
}
// Read controls
int transpose = readTransposeSemitones();
int scaleIdx = readScaleIndex();
if (transpose != lastTranspose || scaleIdx != lastScaleIdx) {
lastTranspose = transpose;
lastScaleIdx = scaleIdx;
updateDac = true;
}
bool trigger = digitalRead(TRIGGER_IN_PIN);
if (trigger == HIGH && trigger != lastTrigger) {
gatePulseRequested = true;
}
lastTrigger = trigger;
if (updateDac && trigger == HIGH) {
float estSemisF = (lastSampledVin) * 12.0f; // 12 semis per volt
int estSemis = (int)roundf(estSemisF);
const Scale& scale = SCALES[scaleIdx];
bool passthrough = (scaleIdx == (NUM_SCALES - 1)); // last scale = bypass
if (passthrough) {
// Apply transpose in semitones, no scale quantization
float vOut = OUTPUT_OFFSET_V + (lastSampledVin + (transpose / 12.0f)) *
OUTPUT_VOLTS_PER_OCT;
uint16_t code = voltageToDacCode(vOut);
dacWriteMCP4821(code);
} else {
// Apply transpose and quantize to selected scale
long targetSemis = (long)estSemis + (long)transpose;
if (targetSemis < MIN_SEMITONE) targetSemis = MIN_SEMITONE;
if (targetSemis > MAX_SEMITONE) targetSemis = MAX_SEMITONE;
int quantizedSemis = quantizeSemitoneToScale((int)targetSemis, scale);
if (quantizedSemis != lastQuantizedSemitone) {
// Compute output voltage from semitones
float vOut =
OUTPUT_OFFSET_V + (quantizedSemis / 12.0f) * OUTPUT_VOLTS_PER_OCT;
uint16_t code = voltageToDacCode(vOut);
dacWriteMCP4821(code);
lastQuantizedSemitone = quantizedSemis;
gatePulseRequested = true;
}
}
updateDac = false;
}
if (gatePulseRequested) {
setGatePulse();
gatePulseRequested = false;
}
delay(1);
}