Hackster is hosting Hackster Holidays, Finale: Livestream & Giveaway Drawing. Watch previous episodes or stream live on Tuesday!Stream Hackster Holidays, Finale on Tuesday!
John Bradnam
Published © GPL3+

Arduino Soundlab

A 3D printed Arduino Nano based synthesizer that produces a wide range of amazing sounds generated using the FM synthesis technique.

IntermediateFull instructions provided20 hours6,584
Arduino Soundlab

Things used in this project

Hardware components

Arduino Nano R3
Arduino Nano R3
×1
45mm 10K Slider Potentimeter
37mm travel, 15mm shaft. 8 linear, 1 log
×9
Tactile Switch, Top Actuated
Tactile Switch, Top Actuated
6mm shaft
×17
TEA2025 Stereo audio amplifier IC
×1
Passive components
7 x 10uF 16V low profile capacitors, 2 x 0.15uF & 2 x 0.1uF Monolithic capacitors, 1 x 10K & 1 x 560 ohm 0805 resistor,
×1
Linear Regulator (7805)
Linear Regulator (7805)
TO-252 SMD variant
×1
Speaker: 3W, 4 ohms
Speaker: 3W, 4 ohms
Around 50mm diameter, no more than 10mm high. Either 4 or 8 ohms
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

STL Files

STL files for printing (see description)

Schematics

Schematic

PCB

Eagle Files

Schematic & PCB in Eagle format

Code

SoundLabV1.ino

C/C++
// Arduino polyphonic FM sound 
// * 31250 Hz sampling rate
// * 9-bit resolution
// * 4-fold polyphony (4 different tones can play simulatenously)
// * 2 full octaves on 24 keys
// * FM-synthesis with time-varying modulation amplitude
// * ADSR envelopes
// * 8 pots for varying instrument parameters
// * multiplexed input allows to expand to 40keys 
// Through PWM with timer1, sound is generated on pin 9
// by Rolf Oldeman May 2019 
// Licence CC BY-NC-SA 2.5 https://creativecommons.org/licenses/by-nc-sa/2.5/

//2019-09-18 - jbrad2089@gmail.com - Swapped D8 & D11 to fix error on PCB 
//2022-09-06 - jbrad2089@gmail.com - Add conversion from log pots to linear values

//Uncomment next line if you are using linear pots
#define POTS_ARE_LOG

#define MAP_TABLE_ITEMS 9
//(slide pos)      0   5  10  15  20 22.5 25 27.5  30
int linValues[] = {0,171,341,512,683,768,853,939,1023};
int logValues[] = {0, 31, 94,153,376,610,793,973,1023};

#define nokey 255

#define npot 8
byte ipot=0;

//set up array with sine values in signed 8-bit numbers 
const float pi = 3.14159265;
char sine[256];
void setsine() {
  for (int i = 0; i < 256; ++i) {
    sine[i] = (sin(2 * 3.14159265 * (i + 0.5) / 256)) * 128;
  }
}

//set up array with exponential values mapping 0-255 -> 2^8 - 2^16
unsigned int exp8[256];
void setexp8(){
  for (int i = 0; i < 256; ++i) {
    exp8[i] = 256*exp(log(2.0)*(i+0.5)/32.0);
  }
}

//setup frequencies/phase increments, starting at C4=0 to B5. (A4 is defined as 440Hz)
unsigned int tone_inc[24];
void settones() {
  for (byte i=0; i<24; i++){
    tone_inc[i]= 587.0 * pow(2.0, ( (i-9) / 12.0)) * 65536.0 / (16000000.0/512) + 0.5;
  }
}

byte butstatD1=0xFF;
byte butstatD2=0xFF;
byte butstatD3=0xFF;
byte prevbutstatD1=0xFF;
byte prevbutstatD2=0xFF;
byte prevbutstatD3=0xFF;

void setup() {

  //disable all inerrupts to avoid glitches
  noInterrupts();

  //setup the array with sine values
  setsine();

  //setup the array with sine values
  setexp8();

  //setup array with tone frequency phase increments
  settones();

  //setup PORTD (pins D0-D7) for input with pull-up 
  DDRD=0B00000000; PORTD=0B11111111; 

  //setup PORTB (pins D8-D13) to high impedance, and D9 to output
  DDRB=0B00000010; PORTB=0B00000000;

  //setup PORTC (pins A0-A7) for input
  DDRC=0B00000000; PORTC=00000000;

  //Set a fast PWM signal on pin D9, TIMER1A, 9-bit resolution, 31250Hz
  TCCR1A = 0B10000010; //9-bit fast PWM
  TCCR1B = 0B00001001;

  //setup the ADC 
  ADCSRA=B11110100; // prescale 16 -> 13 mus per sample, auto trigger
  ADCSRB=B00000000; // freerun
  ADMUX =B01100000; // Vcc ref, left-align, ch0

}


//initialize the main parameters of the pulse length setting
#define nch 4 //number of channels that can produce sound simultaneously
unsigned int phase[nch]  = {0,0,0,0};
int          inc[nch]    = {0,0,0,0};
byte         amp[nch]    = {0,0,0,0};
unsigned int FMphase[nch]= {0,0,0,0};
unsigned int FMinc[nch]  = {0,0,0,0};
unsigned int FMamp[nch]  = {0,0,0,0};

// main function (forced inline) to update the pulse length
inline void setPWM() __attribute__((always_inline));
inline void setPWM() {

  //wait for the timer to complete loop
  while ((TIFR1 & 0B00000001) == 0);

  //Clear(!) the overflow bit by writing a 1 to it
  TIFR1 |= 0B00000001;

  //increment the phases of the FM
  FMphase[0] += FMinc[0];
  FMphase[1] += FMinc[1];
  FMphase[2] += FMinc[2];
  FMphase[3] += FMinc[3];

  //increment the phases of the note
  phase[0] += inc[0];
  phase[1] += inc[1];
  phase[2] += inc[2];
  phase[3] += inc[3];

  //calculate the output value and set pulse width for timer2
  int val = sine[(phase[0]+sine[FMphase[0]>>8]*FMamp[0]) >> 8] * amp[0];
  val += sine[(phase[1]+sine[FMphase[1]>>8]*FMamp[1]) >> 8] * amp[1];
  val += sine[(phase[2]+sine[FMphase[2]>>8]*FMamp[2]) >> 8] * amp[2];
  val += sine[(phase[3]+sine[FMphase[3]>>8]*FMamp[3]) >> 8] * amp[3];

  //set the pulse length
  OCR1A = val/128 + 256;
}

//instrument parameters  
unsigned int ADSR_a  = 0;  
unsigned int ADSR_d  = 0;
unsigned int ADSR_s  = 0;    
unsigned int ADSR_r  = 0;
unsigned int FM_inc  = 0;
unsigned int FM_a1   = 0;
unsigned int FM_a2   = 0;
unsigned int FM_dec  = 0;

//properties of each note played
byte         iADSR[nch]     = {0, 0, 0, 0}; 
unsigned int envADSR[nch]   = {0, 0, 0, 0}; 
byte         amp_base[nch]  = {0, 0, 0, 0};
unsigned int inc_base[nch]  = {0, 0, 0, 0};
unsigned int FMexp[nch]     = {0, 0, 0, 0};
unsigned int FMval[nch]     = {0, 0, 0, 0};
byte         keych[nch]     = {0, 0, 0, 0}; 
unsigned int tch[nch]       = {0, 0, 0, 0}; 


// main loop. Duration of loop is determined by number of setPWM calls
// Each setPWMcall corresponds to 512 cycles=32mus
// Tloop= 32mus * #setPWM. #setPWM=16 gives Tloop=0.512ms
void loop() {

  //read and interpret input buttons
  prevbutstatD1 = butstatD1;
  prevbutstatD2 = butstatD2;
  prevbutstatD3 = butstatD3;
  DDRB|=0B00001000; delayMicroseconds(3); butstatD3 = PIND; DDRB&=0B11110111;
  DDRB|=0B00000100; delayMicroseconds(3); butstatD2 = PIND; DDRB&=0B11111011;
  DDRB|=0B00000001; delayMicroseconds(3); butstatD1 = PIND; DDRB&=0B11111110;

  //if((butstatD1&1)==0)PORTB|=0B00100000;
  //else PORTB&=0B11011111;
  
  byte keypressed = nokey;
  byte keyreleased = nokey;
  if(butstatD1!=prevbutstatD1){
    if ( (butstatD1 & (1<<0)) == 0 and (prevbutstatD1 & (1<<0)) >  0 ) keypressed  =  0;
    if ( (butstatD1 & (1<<0)) >  0 and (prevbutstatD1 & (1<<0)) == 0 ) keyreleased =  0;
    if ( (butstatD1 & (1<<1)) == 0 and (prevbutstatD1 & (1<<1)) >  0 ) keypressed  =  1;
    if ( (butstatD1 & (1<<1)) >  0 and (prevbutstatD1 & (1<<1)) == 0 ) keyreleased =  1;
    if ( (butstatD1 & (1<<2)) == 0 and (prevbutstatD1 & (1<<2)) >  0 ) keypressed  =  2;
    if ( (butstatD1 & (1<<2)) >  0 and (prevbutstatD1 & (1<<2)) == 0 ) keyreleased =  2;
    if ( (butstatD1 & (1<<3)) == 0 and (prevbutstatD1 & (1<<3)) >  0 ) keypressed  =  3;
    if ( (butstatD1 & (1<<3)) >  0 and (prevbutstatD1 & (1<<3)) == 0 ) keyreleased =  3;
    if ( (butstatD1 & (1<<4)) == 0 and (prevbutstatD1 & (1<<4)) >  0 ) keypressed  =  4;
    if ( (butstatD1 & (1<<4)) >  0 and (prevbutstatD1 & (1<<4)) == 0 ) keyreleased =  4;
    if ( (butstatD1 & (1<<5)) == 0 and (prevbutstatD1 & (1<<5)) >  0 ) keypressed  =  5;
    if ( (butstatD1 & (1<<5)) >  0 and (prevbutstatD1 & (1<<5)) == 0 ) keyreleased =  5;
    if ( (butstatD1 & (1<<6)) == 0 and (prevbutstatD1 & (1<<6)) >  0 ) keypressed  =  6;
    if ( (butstatD1 & (1<<6)) >  0 and (prevbutstatD1 & (1<<6)) == 0 ) keyreleased =  6;
    if ( (butstatD1 & (1<<7)) == 0 and (prevbutstatD1 & (1<<7)) >  0 ) keypressed  =  7;
    if ( (butstatD1 & (1<<7)) >  0 and (prevbutstatD1 & (1<<7)) == 0 ) keyreleased =  7;
  }
  if(butstatD2!=prevbutstatD2){
    if ( (butstatD2 & (1<<0)) == 0 and (prevbutstatD2 & (1<<0)) >  0 ) keypressed  =  8;
    if ( (butstatD2 & (1<<0)) >  0 and (prevbutstatD2 & (1<<0)) == 0 ) keyreleased =  8;
    if ( (butstatD2 & (1<<1)) == 0 and (prevbutstatD2 & (1<<1)) >  0 ) keypressed  =  9;
    if ( (butstatD2 & (1<<1)) >  0 and (prevbutstatD2 & (1<<1)) == 0 ) keyreleased =  9;
    if ( (butstatD2 & (1<<2)) == 0 and (prevbutstatD2 & (1<<2)) >  0 ) keypressed  = 10;
    if ( (butstatD2 & (1<<2)) >  0 and (prevbutstatD2 & (1<<2)) == 0 ) keyreleased = 10;
    if ( (butstatD2 & (1<<3)) == 0 and (prevbutstatD2 & (1<<3)) >  0 ) keypressed  = 11;
    if ( (butstatD2 & (1<<3)) >  0 and (prevbutstatD2 & (1<<3)) == 0 ) keyreleased = 11;
    if ( (butstatD2 & (1<<4)) == 0 and (prevbutstatD2 & (1<<4)) >  0 ) keypressed  = 12;
    if ( (butstatD2 & (1<<4)) >  0 and (prevbutstatD2 & (1<<4)) == 0 ) keyreleased = 12;
    if ( (butstatD2 & (1<<5)) == 0 and (prevbutstatD2 & (1<<5)) >  0 ) keypressed  = 13;
    if ( (butstatD2 & (1<<5)) >  0 and (prevbutstatD2 & (1<<5)) == 0 ) keyreleased = 13;
    if ( (butstatD2 & (1<<6)) == 0 and (prevbutstatD2 & (1<<6)) >  0 ) keypressed  = 14;
    if ( (butstatD2 & (1<<6)) >  0 and (prevbutstatD2 & (1<<6)) == 0 ) keyreleased = 14;
    if ( (butstatD2 & (1<<7)) == 0 and (prevbutstatD2 & (1<<7)) >  0 ) keypressed  = 15;
    if ( (butstatD2 & (1<<7)) >  0 and (prevbutstatD2 & (1<<7)) == 0 ) keyreleased = 15;
  }
  if(butstatD3!=prevbutstatD3){
    if ( (butstatD3 & (1<<0)) == 0 and (prevbutstatD3 & (1<<0)) >  0 ) keypressed  = 16;
    if ( (butstatD3 & (1<<0)) >  0 and (prevbutstatD3 & (1<<0)) == 0 ) keyreleased = 16;
    if ( (butstatD3 & (1<<1)) == 0 and (prevbutstatD3 & (1<<1)) >  0 ) keypressed  = 17;
    if ( (butstatD3 & (1<<1)) >  0 and (prevbutstatD3 & (1<<1)) == 0 ) keyreleased = 17;
    if ( (butstatD3 & (1<<2)) == 0 and (prevbutstatD3 & (1<<2)) >  0 ) keypressed  = 18;
    if ( (butstatD3 & (1<<2)) >  0 and (prevbutstatD3 & (1<<2)) == 0 ) keyreleased = 18;
    if ( (butstatD3 & (1<<3)) == 0 and (prevbutstatD3 & (1<<3)) >  0 ) keypressed  = 19;
    if ( (butstatD3 & (1<<3)) >  0 and (prevbutstatD3 & (1<<3)) == 0 ) keyreleased = 19;
    if ( (butstatD3 & (1<<4)) == 0 and (prevbutstatD3 & (1<<4)) >  0 ) keypressed  = 20;
    if ( (butstatD3 & (1<<4)) >  0 and (prevbutstatD3 & (1<<4)) == 0 ) keyreleased = 20;
    if ( (butstatD3 & (1<<5)) == 0 and (prevbutstatD3 & (1<<5)) >  0 ) keypressed  = 21;
    if ( (butstatD3 & (1<<5)) >  0 and (prevbutstatD3 & (1<<5)) == 0 ) keyreleased = 21;
    if ( (butstatD3 & (1<<6)) == 0 and (prevbutstatD3 & (1<<6)) >  0 ) keypressed  = 22;
    if ( (butstatD3 & (1<<6)) >  0 and (prevbutstatD3 & (1<<6)) == 0 ) keyreleased = 22;
    if ( (butstatD3 & (1<<7)) == 0 and (prevbutstatD3 & (1<<7)) >  0 ) keypressed  = 23;
    if ( (butstatD3 & (1<<7)) >  0 and (prevbutstatD3 & (1<<7)) == 0 ) keyreleased = 23;
  }
  
  setPWM(); //#1

  //setup one POT
  ADMUX &=B11111000; ADMUX |= ipot;
  
  setPWM(); //#2

  //readout one pot
#ifdef POTS_ARE_LOG
  uint16_t potval = 255 - ADCH;
  potval = multiMap(potval << 2, logValues, linValues, MAP_TABLE_ITEMS) >> 2;
#else
  byte potval=ADCH;
#endif

  //interpret pot values  
  if(ipot==7) ADSR_a  = exp8[255-potval]>>4;     // range: 4096-256-16 = 8ms-128ms-2s  
  if(ipot==6) ADSR_d  = exp8[255-potval]>>4;
  if(ipot==5) ADSR_s  =      potval<<8;          // range 0-255 = 0.0-0.5-1.0
  if(ipot==4) ADSR_r  = exp8[255-potval]>>4;
  if(ipot==3) FM_inc  = exp8[potval]>>4;         // range 16-256-4096 = fm/fc 0.06 - 1.0 - 16
  if(ipot==2) FM_a1   = (exp8[potval]-258)>>4;   // range 0 -240 -4080 ~ beta 0 - 1 -16
  if(ipot==1) FM_a2   = (exp8[potval]-258)>>4;
  if(ipot==0) FM_dec  = exp8[255-potval]>>4;
  unsigned int FM_a0=FM_a2;
  int          FM_da=FM_a1-FM_a2;  
  ipot++;
  if (ipot>=npot) ipot=0;
  
  setPWM(); //#3


  //find the best channel to start a new note
  byte nextch = 255;
  //first check if the key is still being played
  if (iADSR[0] > 0 and keypressed == keych[0])nextch = 0;
  if (iADSR[1] > 0 and keypressed == keych[1])nextch = 1;
  if (iADSR[2] > 0 and keypressed == keych[2])nextch = 2;
  if (iADSR[3] > 0 and keypressed == keych[3])nextch = 3;
  //then check for an empty channel
  if (nextch == 255) {
    if (iADSR[0] == 0)nextch = 0;
    if (iADSR[1] == 0)nextch = 1;
    if (iADSR[2] == 0)nextch = 2;
    if (iADSR[3] == 0)nextch = 3;
  }
  //otherwise use the channel with the longest playing note
  if (nextch == 255) {
    nextch = 0;
    if (tch[0] > tch[nextch])nextch = 0;
    if (tch[1] > tch[nextch])nextch = 1;
    if (tch[2] > tch[nextch])nextch = 2;
    if (tch[3] > tch[nextch])nextch = 3;
  }

  setPWM(); //#4

  //initiate new note if needed
  if (keypressed != nokey) {
    phase[nextch]=0;
    amp_base[nextch] = 64;
    inc_base[nextch] = tone_inc[keypressed];
    iADSR[nextch] = 1;
    FMphase[nextch]=0;
    FMexp[nextch]=0xFFFF;
    keych[nextch] = keypressed;
    tch[nextch] = 0;
  }

  setPWM(); //#5

  //stop a note if the button is released
  if (keyreleased != nokey) {
    if (keych[0] == keyreleased)iADSR[0] = 4;
    if (keych[1] == keyreleased)iADSR[1] = 4;
    if (keych[2] == keyreleased)iADSR[2] = 4;
    if (keych[3] == keyreleased)iADSR[3] = 4;
  }
  
  setPWM(); //#6

  //update FM decay exponential 
  FMexp[0]-=(long)FMexp[0]*FM_dec>>16;
  FMexp[1]-=(long)FMexp[1]*FM_dec>>16;
  FMexp[2]-=(long)FMexp[2]*FM_dec>>16;
  FMexp[3]-=(long)FMexp[3]*FM_dec>>16;
  
  setPWM(); //#7
  
  //adjust the ADSR envelopes
  for (byte ich = 0; ich < nch; ich++) {
    if (iADSR[ich] == 4) { // in RELEASE
      if (envADSR[ich] <= ADSR_r) {
        envADSR[ich] = 0;
        iADSR[ich] = 0;
      }
      else envADSR[ich] -= ADSR_r;
    }
    if (iADSR[ich] == 3) { // in SUSTAIN
      envADSR[ich] = ADSR_s;
    }
    if (iADSR[ich] == 2) { // in DECAY
      if (envADSR[ich] <= (ADSR_s + ADSR_d)) {
        envADSR[ich] = ADSR_s;
        iADSR[ich] = 3;
      }
      else envADSR[ich] -= ADSR_d;
    }
    if (iADSR[ich] == 1) { // in ATTACK
      if ((0xFFFF - envADSR[ich]) <= ADSR_a) {
        envADSR[ich] = 0xFFFF;
        iADSR[ich] = 2;
      }
      else envADSR[ich] += ADSR_a;
    }
    tch[ich]++;
    setPWM(); //#8-11
  }

  //update the tone for channel 0
  amp[0] = (amp_base[0] * (envADSR[0] >> 8)) >> 8;
  inc[0] = inc_base[0];
  FMamp[0] = FM_a0 + ((long)FM_da * FMexp[0]>>16);
  FMinc[0] = ((long)inc_base[0]*FM_inc)/256;
  setPWM(); //#12

  //update the tone for channel 1
  amp[1] = (amp_base[1] * (envADSR[1] >> 8)) >> 8;
  inc[1] = inc_base[1];
  FMamp[1] = FM_a0 + ((long)FM_da * FMexp[1]>>16);
  FMinc[1] = ((long)inc_base[1]*FM_inc)/256;
  setPWM(); //#13

  //update the tone for channel 2
  amp[2] = (amp_base[2] * (envADSR[2] >> 8)) >> 8;
  inc[2] = inc_base[2];
  FMamp[2] = FM_a0 + ((long)FM_da * FMexp[2]>>16);
  FMinc[2] = ((long)inc_base[2]*FM_inc)/256;
  setPWM(); //#14

  //update the tone for channel 3
  amp[3] = (amp_base[3] * (envADSR[3] >> 8)) >> 8;
  inc[3] = inc_base[3];
  FMamp[3] = FM_a0 + ((long)FM_da * FMexp[3]>>16);
  FMinc[3] = ((long)inc_base[3]*FM_inc)/256;
  setPWM(); //#15

  //update counters
  tch[0]++;
  tch[1]++;
  tch[2]++;
  tch[3]++;

  setPWM(); //#16

}

//--------------------------------------------------------------------------
// Map a value from on table to another
int multiMap(int val, int* _in, int* _out, uint8_t size)
{
  // take care the value is within range
  // val = constrain(val, _in[0], _in[size-1]);
  if (val <= _in[0]) return _out[0];
  if (val >= _in[size-1]) return _out[size-1];

  // search right interval
  uint8_t pos = 1;  // _in[0] allready tested
  while(val > _in[pos]) pos++;

  // this will handle all exact "points" in the _in array
  if (val == _in[pos]) return _out[pos];

  // interpolate in the right segment for the rest
  return (val - _in[pos-1]) * (_out[pos] - _out[pos-1]) / (_in[pos] - _in[pos-1]) + _out[pos-1];
}

Credits

John Bradnam
147 projects • 181 followers
Thanks to rgco and oskitone.

Comments