Eurorack CV quantizer on an Arduino Nano
A control voltage snapped to the nearest note of a scale and sent out at one volt per octave through a filtered PWM DAC, with a trigger out and sample and hold. The whole build, an Arduino Nano and 8 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Advanced Runs in your browser. Free, and no account needed.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Eurorack-style CV quantizer on an Arduino Nano: a control voltage in, the
// nearest note of a scale out at one volt per octave, a trigger out on every
// new note, and a sample-and-hold mode clocked by a gate input.
//
// Wiring.
//
// CV in A0 (a knob here, standing in for another module's CV)
// gate in D2 (a button here, standing in for a clock or gate jack)
// mode slide switch on D7: track the input, or sample on each gate
// CV out D9: 10-bit PWM at 15.6 kHz, smoothed by 4.7k and 1 uF
// trig out D8: a 10 ms pulse on each new note (an LED here)
//
// The DAC is a PWM pin and a filter. Timer 1 runs a 10-bit PWM on D9 far
// above audio, and the resistor and capacitor average it into a steady
// voltage: 1023 counts is 5 V, so one semitone (1/12 V) is about 17 counts.
// Watch it on the scope: each note is a flat step, and the smoothing shows as
// a curve at the edge of every step. A real module would follow the filter
// with an op-amp buffer, and many use a dedicated DAC chip instead.
//
// In Mokxi: turn the knob and the output jumps from note to note; the Serial
// Monitor names each one. Slide the switch to sample-and-hold and the output
// only moves when you press the gate button.
//
// What is not simulated: real Eurorack voltages. Modules run on +12 V and
// -12 V and CV can swing past what an Arduino pin survives, so a real module
// scales and clamps every input (resistor divider and protection diodes or an
// op-amp) and scales the 0 to 5 V output up with an op-amp. The logic here is
// the part you would keep.
// ---------------------------------------------------------------------------
// Make it yours
// ---------------------------------------------------------------------------
const int CV_IN = A0;
const int GATE_IN = 2;
const int MODE_PIN = 7;
const int TRIG_OUT = 8; // the CV output is D9 (timer 1, OC1A), fixed
// The scale, as twelve bits from C (bit 0) up to B (bit 11). A 1 lets a
// note through.
#define MAJOR 0b101010110101 // C D E F G A B
#define MINOR_PENTATONIC 0b010010101001 // C Eb F G Bb
#define CHROMATIC 0b111111111111 // every note
const unsigned int SCALE = MAJOR;
// Shift the scale to another key: 0 is C, 2 is D, 9 is A.
const int ROOT = 0;
// Fine tuning: the output counts for 5 V. Measure the real output with a
// meter and adjust until each octave is exactly a volt.
const long FULL_SCALE = 1023;
const unsigned long TRIG_MS = 10;
// A gate from another module is clean, but the button standing in for it
// bounces: a new gate is only taken this long after the last one.
const unsigned long GATE_LOCK_MS = 20;
// ---------------------------------------------------------------------------
const char *const NAMES[12] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
int note = -1;
unsigned long trigAt = 0;
bool lastGate = false;
unsigned long gateAt = 0;
void dacWrite(long counts) {
if (counts < 0) counts = 0;
if (counts > 1023) counts = 1023;
OCR1A = (unsigned int)counts;
}
bool allowed(int semitone) {
int degree = ((semitone - ROOT) % 12 + 12) % 12;
return (SCALE >> degree) & 1;
}
// The nearest allowed semitone to the input, 0 to 60 (five octaves over 5 V).
int quantize(int reading) {
long semis = ((long)reading * 60 + 511) / 1023; // nearest semitone
for (int d = 0; d <= 6; d++) {
if (semis - d >= 0 && allowed((int)(semis - d))) return (int)(semis - d);
if (semis + d <= 60 && allowed((int)(semis + d))) return (int)(semis + d);
}
return (int)semis;
}
void output(int semitone) {
// one volt per octave: semitone/12 V, as counts of 5 V
dacWrite((semitone * FULL_SCALE * 2 + 60) / 120);
digitalWrite(TRIG_OUT, HIGH);
trigAt = millis();
long mv = (long)semitone * 1000 / 12;
Serial.print("Note ");
Serial.print(NAMES[semitone % 12]);
Serial.print(semitone / 12 + 1);
Serial.print(" ");
Serial.print(mv / 1000);
Serial.print('.');
long frac = mv % 1000;
if (frac < 100) Serial.print('0');
if (frac < 10) Serial.print('0');
Serial.print(frac);
Serial.println(" V");
}
void setup() {
pinMode(GATE_IN, INPUT_PULLUP);
pinMode(MODE_PIN, INPUT_PULLUP);
pinMode(TRIG_OUT, OUTPUT);
pinMode(9, OUTPUT);
// Timer 1: fast PWM, TOP = ICR1 = 1023 (10 bits), no prescaler: 15.6 kHz.
TCCR1A = (1 << COM1A1) | (1 << WGM11);
TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS10);
ICR1 = 1023;
Serial.begin(115200);
Serial.println("CV quantizer ready");
}
void loop() {
bool hold = digitalRead(MODE_PIN) == LOW;
bool gate = digitalRead(GATE_IN) == LOW;
bool rising = false;
if (gate != lastGate && millis() - gateAt >= GATE_LOCK_MS) {
rising = gate;
lastGate = gate;
gateAt = millis();
}
if (!hold || rising) {
int q = quantize(analogRead(CV_IN));
if (q != note || rising) {
note = q;
output(q);
}
}
if (trigAt && millis() - trigAt >= TRIG_MS) {
digitalWrite(TRIG_OUT, LOW);
trigAt = 0;
}
}
Parts list
10 parts, plus the jumper wires. Every one is in the editor's parts bin.
- 1 × Arduino Nano
- 1 × Full-size breadboard
- 1 × Potentiometer
- 1 × Resistor, 4.7k Ω
- 1 × Capacitor, 1 µF
- 1 × LED, yellow
- 1 × Resistor, 220 Ω
- 1 × Slide switch
- 1 × Pushbutton
- 1 × Oscilloscope
How it is wired
10 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.
- Ground: Arduino Nano pin GND; Capacitor, 1 µF pin 2; Resistor, 220 Ω pin 2; Slide switch pin 3; Pushbutton pin 2b; Oscilloscope pin GND
- Arduino Nano pin 2; Pushbutton pin 1b
- Arduino Nano pin 7; Slide switch pin 2
- Arduino Nano pin 8; LED, yellow pin A
- Arduino Nano pin 9; Resistor, 4.7k Ω pin 1; Oscilloscope pin CH2
- Ground: Arduino Nano pin GND; Potentiometer pin 1
- Arduino Nano pin 5V; Potentiometer pin 3
- Arduino Nano pin A0; Potentiometer pin 2
- Resistor, 4.7k Ω pin 2; Capacitor, 1 µF pin 1; Oscilloscope pin CH1
- LED, yellow pin C; Resistor, 220 Ω pin 1
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.