Jan’s design had controls for Envelope, Ensemble and Modulation only, for a 3-octave keyboard. My modification was to add a volume control (50k log potentiometer) and passive low pass filter (10k linear potentiometer + fixed 1k as specified by Jan), and adapt the code for a 4 octave keyboard with a different diode matrix shape.
A modification of the code allows* for a 4-octave keyboard with a differently shaped diode matrix. The code has two main changes: one in the keyscanner opening pin D13 as Output and opening PORTB for scanning; and one in the header defining an array (“keytranslate”) that re-maps the notes to account for a different shape.
*I couldn’t get the Low C to work, because this keyboard was wired such that the key would need it’s own individual pin.
#include <avr/interrupt.h>
#include <avr/io.h>
#include <avr/pgmspace.h>
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif
// Standard Arduino Pins
#define digitalPinToPortReg(P) \
(((P) >= 0 && (P) <= 7) ? &PORTD : (((P) >= 8 && (P) <= 13) ? &PORTB : &PORTC))
#define digitalPinToDDRReg(P) \
(((P) >= 0 && (P) <= 7) ? &DDRD : (((P) >= 8 && (P) <= 13) ? &DDRB : &DDRC))
#define digitalPinToPINReg(P) \
(((P) >= 0 && (P) <= 7) ? &PIND : (((P) >= 8 && (P) <= 13) ? &PINB : &PINC))
#define digitalPinToBit(P) \
(((P) >= 0 && (P) <= 7) ? (P) : (((P) >= 8 && (P) <= 13) ? (P) - 8 : (P) - 14))
#define digitalReadFast(P) bitRead(*digitalPinToPINReg(P), digitalPinToBit(P))
#define digitalWriteFast(P, V) bitWrite(*digitalPinToPortReg(P), digitalPinToBit(P), (V))
const unsigned char PS_2 = (1 << ADPS0);;
const unsigned char PS_4 = (1 << ADPS1);
const unsigned char PS_8 = (1 << ADPS1) | (1 << ADPS0);
const unsigned char PS_16 = (1 << ADPS2);
const unsigned char PS_32 = (1 << ADPS2) | (1 << ADPS0);
const unsigned char PS_64 = (1 << ADPS2) | (1 << ADPS1);
const unsigned char PS_128 = (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
uint32_t NOTES[12]={208065>>2,220472>>2,233516>>2,247514>>2,262149>>2,277738>>2,294281>>2,311779>>2,330390>>2,349956>>2,370794>>2,392746>>2};
int8_t keytable[48];
int8_t oldkeytable[48];
const uint8_t ATTrates[32]={
1,2,3,4,5,8,12,20,32,37,43,51,64,85,128,255,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF
};
const uint8_t RELrates[32]={
1,2,3,4,5,8,12,20,32,37,43,51,64,85,128,255,255,128,85,64,51,43,37,32,20,12,8,5,4,3,2,1
};
const uint8_t sinetable[256] PROGMEM = {
127,130,133,136,139,143,146,149,152,155,158,161,164,167,170,173,176,178,181,184,187,190,192,195,198,200,203,205,208,210,212,215,217,219,221,223,225,227,229,231,233,234,236,238,239,240,
242,243,244,245,247,248,249,249,250,251,252,252,253,253,253,254,254,254,254,254,254,254,253,253,253,252,252,251,250,249,249,248,247,245,244,243,242,240,239,238,236,234,233,231,229,227,225,223,
221,219,217,215,212,210,208,205,203,200,198,195,192,190,187,184,181,178,176,173,170,167,164,161,158,155,152,149,146,143,139,136,133,130,127,124,121,118,115,111,108,105,102,99,96,93,90,87,84,81,78,
76,73,70,67,64,62,59,56,54,51,49,46,44,42,39,37,35,33,31,29,27,25,23,21,20,18,16,15,14,12,11,10,9,7,6,5,5,4,3,2,2,1,1,1,0,0,0,0,0,0,0,1,1,1,2,2,3,4,5,5,6,7,9,10,11,12,14,15,16,18,20,21,23,25,27,29,31,
33,35,37,39,42,44,46,49,51,54,56,59,62,64,67,70,73,76,78,81,84,87,90,93,96,99,102,105,108,111,115,118,121,124
};
const uint8_t keytranslate[48]={4, 10, 16, 22, 28, 34, 40, 46, 3, 9, 15, 21, 27, 33, 39, 45, 2, 8, 14, 20, 26, 32, 38, 44, 1, 7, 13, 19, 25, 31, 37, 43, 0, 6, 12, 18, 24, 30, 36, 42, 5, 11, 17, 23, 29, 35, 41, 47};
volatile uint8_t lfocounter;
volatile uint8_t lfocounter2;
volatile uint16_t lfoval;
volatile uint16_t lfoval2;
volatile uint8_t GATED=1;
uint8_t OSCNOTES[4];
int16_t volume=0;
uint8_t ENVsmoothing;
uint8_t envcnt=10;
//-------- Synth parameters --------------
uint32_t FREQ[16]={0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; //DCO pitch
volatile uint32_t DETUNE=0; //Osc spread or detune
volatile uint8_t CUTOFF=0; //freq 0-255
volatile uint8_t RESONANCE=0; //resonance=0-255
volatile uint16_t LFO=32; //Lfo rate 0-255
volatile uint8_t VCA=255; //VCA level 0-255
volatile uint8_t ATTACK=1; // ENV Attack rate 0-255
volatile uint8_t RELEASE=1; // ENV Release rate 0-255
volatile uint8_t ENVELOPE=0; // ENV Shape
volatile uint8_t TRIG=0; //MIDItrig 1=note ON
volatile int16_t BEND; //Pitchbend
volatile int16_t MOD; //MODwheel
//-----------------------------------------
volatile int16_t BENDoffset; //Pitchbend center
volatile uint32_t olddetune;
uint32_t DCOPH[16];
uint8_t integrators[16];
uint8_t delayline[256];
volatile uint8_t writepointer;
volatile uint8_t PHASERMIX;
uint8_t DCO;
int16_t DCF;
int16_t ENV;
int16_t M0;
int16_t M1;
int16_t M2;
int16_t M3;
int16_t M4;
int16_t M5;
int16_t M6;
int16_t MX1;
int16_t MX2;
int8_t coefficient;
ISR(TIMER1_COMPA_vect) {
//-------------------- 8 DCO block ------------------------------------------
DCO=0;
for (uint8_t i=0;i<8;i++) {
if (integrators[i]) integrators[i]--; //Decrement integrators
DCOPH[i] += FREQ[i]; //Add freq to phaseacc's
if (DCOPH[i]&0x800000) { //Check for integrator reset
DCOPH[i]&=0x7FFFFF; //Trim NCO
integrators[i]=28; //Reset integrator
}
DCO+=integrators[i];
}
writepointer++;
delayline[writepointer]=DCO;
DCO+=(delayline[(writepointer-lfoval2)&255]*PHASERMIX)>>8;
//---------------------------------------------------------------------------
//------------------ VCA block ------------------------------------
#define M(MX, MX1, MX2) \
asm volatile ( \
"clr r26 \n\t"\
"mulsu %B1, %A2 \n\t"\
"movw %A0, r0 \n\t"\
"mul %A1, %A2 \n\t"\
"add %A0, r1 \n\t"\
"adc %B0, r26 \n\t"\
"clr r1 \n\t"\
: \
"=&r" (MX) \
: \
"a" (MX1), \
"a" (MX2) \
:\
"r26"\
)
if ((ATTACK==255)&&(TRIG==1)) VCA=255;
if (!(envcnt--)) {
envcnt=20;
if (VCA<volume) VCA++;
if (VCA>volume) VCA--;
}
M(ENV, (int16_t)DCO, VCA);
OCR2A = ENV;
//-----------------------------------------------------------------
//-------------- Calc Sample freq ---------------------------------
OCR1A = 758-lfoval;
//-----------------------------------------------------------------
}
ISR(TIMER0_COMPA_vect) {
//------------------------------ LFO Block -----------------------
lfocounter+=LFO;
lfoval=(pgm_read_byte_near( sinetable + lfocounter ) * MOD)>>10; //LFO for pitch
lfoval2=pgm_read_byte_near( sinetable + (lfocounter2++) ); //LFO for the Phaser
//-----------------------------------------------------------------
//--------------------- ENV block ---------------------------------
if ((TRIG==1)&&(volume<255)) {
volume+=ATTACK;
if (volume>255) volume=255;
}
if ((TRIG==0)&&(volume>0)) {
volume-=RELEASE;
if (volume<0) volume=0;
}
//-----------------------------------------------------------------
}
/*
ISR(USART_RX_vect)
{
//Midiin.sendByte(UDR0);
}
*/
void setup() {
//Keyscanner inputs
pinMode(2,INPUT_PULLUP);
pinMode(3,INPUT_PULLUP);
pinMode(4,INPUT_PULLUP);
pinMode(5,INPUT_PULLUP);
pinMode(6,INPUT_PULLUP);
pinMode(7,INPUT_PULLUP);
pinMode(8,INPUT_PULLUP);
pinMode(9,INPUT_PULLUP);
//Keyscanner outputs
pinMode(13, OUTPUT);
pinMode(14, OUTPUT);
pinMode(15, OUTPUT);
pinMode(16, OUTPUT);
pinMode(17, OUTPUT);
pinMode(18, OUTPUT);
//PWM and GATE outputs
pinMode(11, OUTPUT);
pinMode(10, OUTPUT);
// Set up Timer 1 to send a sample every interrupt.
cli();
// Set CTC mode
// Have to set OCR1A *after*, otherwise it gets reset to 0!
TCCR1B = (TCCR1B & ~_BV(WGM13)) | _BV(WGM12);
TCCR1A = TCCR1A & ~(_BV(WGM11) | _BV(WGM10));
// No prescaler
TCCR1B = (TCCR1B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10);
// Set the compare register (OCR1A).
// OCR1A is a 16-bit register, so we have to do this with
// interrupts disabled to be safe.
OCR1A = 758;//F_CPU / SAMPLE_RATE;
// Enable interrupt when TCNT1 == OCR1A
TIMSK1 |= _BV(OCIE1A);
//set timer0 interrupt at 61Hz
TCCR0A = 0;// set entire TCCR0A register to 0
TCCR0B = 0;// same for TCCR0B
TCNT0 = 0;//initialize counter value to 0
// set compare match register for 62hz increments
OCR0A = 255;// = 61Hz
// turn on CTC mode
TCCR0A |= (1 << WGM01);
// Set CS01 and CS00 bits for prescaler 1024
TCCR0B |= (1 << CS02) | (0 << CS01) | (1 << CS00); //1024 prescaler
// enable timer compare interrupt
TIMSK0 |= (1 << OCIE0A);
sei();
// Set baud rate to 31,250. Requires modification if clock speed is not 16MHz.
UBRR0H = ((F_CPU / 16 + 31250 / 2) / 31250 - 1) >> 8;
UBRR0L = ((F_CPU / 16 + 31250 / 2) / 31250 - 1);
// Set frame format to 8 data bits, no parity, 1 stop bit
UCSR0C |= (1<<UCSZ01)|(1<<UCSZ00);
// enable rx
UCSR0B |= _BV(RXEN0);
// USART RX interrupt enable bit on
UCSR0B |= _BV(RXCIE0);
// Set up Timer 2 to do pulse width modulation on the speaker
// pin.
// Use internal clock (datasheet p.160)
ASSR &= ~(_BV(EXCLK) | _BV(AS2));
// Set fast PWM mode (p.157)
TCCR2A |= _BV(WGM21) | _BV(WGM20);
TCCR2B &= ~_BV(WGM22);
// Do non-inverting PWM on pin OC2A (p.155)
// On the Arduino this is pin 11.
TCCR2A = (TCCR2A | _BV(COM2A1)) & ~_BV(COM2A0);
TCCR2A &= ~(_BV(COM2B1) | _BV(COM2B0));
// No prescaler (p.158)
TCCR2B = (TCCR2B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10);
// Set initial pulse width to the first sample.
OCR2A = 128;
// set up the ADC
BENDoffset=analogRead(7);
ADCSRA &= ~PS_128; // remove bits set by Arduino library
// you can choose a prescaler from above.
// PS_16, PS_32, PS_64 or PS_128
ADCSRA |= PS_128; // set our own prescaler to 16
ADMUX = 69;
sbi(ADCSRA, ADSC);
}
//---------------- Get the base frequency for the MIDI note ---------------
uint32_t MIDI2FREQ(uint8_t note) {
uint8_t key=note%12;
if (note<36) return (NOTES[key]>>(1+(35-note)/12));
if (note>47) return (NOTES[key]<<((note-36)/12));
return NOTES[key];
}
//-------------------------------------------------------------------------
//---------------- Handle Notes---------------------------------------
void handleMIDINOTE(uint8_t status,uint8_t note,uint8_t vel) {
uint8_t i;
uint32_t freq;
if ((!vel)&&(status==0x90)) status=0x80;
if (status==0x80) {
for (i=0;i<4;i++) {
if (OSCNOTES[i]==note) {
if (!GATED) {
FREQ[i<<1]=0;
FREQ[(i<<1)|1]=0;
}
OSCNOTES[i]=0;
}
}
if (!(OSCNOTES[0]|OSCNOTES[1]|OSCNOTES[2]|OSCNOTES[3])) TRIG=0;
return;
}
if (status==0x90) {
if ((!TRIG)&&(GATED)) {
for (i=0;i<8;i++) {
FREQ[i]=0;
}
}
i=0;
while (i<4) {
if (!OSCNOTES[i]) {
freq=MIDI2FREQ(note);
FREQ[i<<1]=freq;
FREQ[(i<<1)|1]=FREQ[i<<1]+(((FREQ[i<<1]/50)>>0)*DETUNE/127);
OSCNOTES[i]=note;
if (!TRIG) {
TRIG=1;
}
return;
}
i++;
}
}
}
//-------------------------------------------------------------------------
void loop() {
//Serial.begin(9600);
uint8_t k=0;
uint8_t z;
uint8_t w=0;
int8_t MUX=5;
while(1) {
//------------------ Key scanner -----------------------------
PORTC|=0x1F;
PORTB|=B00100000;
if ((k&0x38)==(0x00<<3)) PORTC&=B11111110;
if ((k&0x38)==(0x01<<3)) PORTC&=B11111101;
if ((k&0x38)==(0x02<<3)) PORTC&=B11111011;
if ((k&0x38)==(0x03<<3)) PORTC&=B11110111;
if ((k&0x38)==(0x04<<3)) PORTC&=B11101111;
if ((k&0x38)==(0x05<<3)) PORTB^=B00100000;
keytable[k]=digitalReadFast((k&7)+2);
if (oldkeytable[k]!=keytable[k]) { //Handle keyevent
oldkeytable[k]=keytable[k];
if (keytable[k]==0) {
handleMIDINOTE(0x90,keytranslate[k]+25,127);
}
else {
handleMIDINOTE(0x80,keytranslate[k]+25,0);
}
}
k++;
if (k==48) {
k=0;
}
digitalWriteFast(10,TRIG); //JK: MIDI Gate Signal
//---------------------------------------------------------------
//--------------- ADC block -------------------------------------
while (bit_is_set(ADCSRA, ADSC)); //Wait for ADC EOC
if (MUX==7) DETUNE=((ADCL+(ADCH<<8))>>3);
if (MUX==7) MOD=((ADCL+(ADCH<<8))>>2);
if (MUX==6) PHASERMIX=((ADCL+(ADCH<<8))>>2);
if (MUX==5) ENVELOPE=((ADCL+(ADCH<<8))>>5);
if (MUX==5) ATTACK=ATTrates[ENVELOPE];
if (MUX==5) RELEASE=RELrates[ENVELOPE];
if (RELEASE==255) GATED=0;
if (RELEASE!=255) GATED=1;
if (DETUNE!=olddetune) {
olddetune=DETUNE;
for (uint8_t i=0;i<4;i++) {
if (FREQ[i<<1]) {
FREQ[(i<<1)|1]=FREQ[i<<1]+(((FREQ[i<<1]/50)>>0)*DETUNE/127);
}
}
}
//Serial.print(CUTOFF,DEC);
//Serial.print("\n");
MUX++;
if (MUX>7) MUX=5;
ADMUX = 64 | MUX; //Select MUX
sbi(ADCSRA, ADSC); //start next conversation
//--------------------------------------------------------------------
}
}