0

I'm pretty new to Arduino programming. I have however been coding for a while. I'm currently trying to code a polyphonic piano using the Arduino IDE and a Digispark Attiny85 development board. To play multiple notes at once I'm using sine tables and fast PWM. This is my code:

int val = 1;
uint8_t C = 0;
uint8_t D = 0;
uint8_t E = 0;
uint8_t F = 0;
uint8_t G = 0;
uint8_t A = 0;
uint8_t B = 0;

static uint8_t  sin_C[123] = {16,16,17,18,19,20,20,21,22,23,23,24,25,25,26,27,27,28,28,29,29,30,30,30,31,31,31,31,31,31,31,31,31,31,31,31,31,31,30,30,30,29,29,28,28,27,27,26,25,25,24,23,23,22,21,20,20,19,18,17,16,16,15,14,13,12,11,11,10,9,8,8,7,6,6,5,4,4,3,3,2,2,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,2,2,3,3,4,4,5,6,6,7,8,8,9,10,11,11,12,13,14,15,15};
static uint8_t sin_D[110] = {16,16,17,18,19,20,21,22,23,23,24,25,26,26,27,28,28,29,29,30,30,30,31,31,31,31,31,31,31,31,31,31,31,31,30,30,30,29,29,28,27,27,26,25,25,24,23,22,21,20,20,19,18,17,16,15,14,13,12,11,11,10,9,8,7,6,6,5,4,4,3,2,2,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,2,2,3,3,4,5,5,6,7,8,8,9,10,11,12,13,14,15,15};
static uint8_t sin_E[98] = {16,17,18,19,20,21,22,23,23,24,25,26,27,27,28,29,29,30,30,31,31,31,31,31,31,31,31,31,31,31,30,30,30,29,28,28,27,26,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,5,4,3,3,2,1,1,1,0,0,0,0,0,0,0,0,0,0,0,1,1,2,2,3,4,4,5,6,7,8,8,9,10,11,12,13,14,15};
static uint8_t sin_F[92] = {16,17,18,19,20,21,22,23,24,25,26,27,27,28,29,29,30,30,31,31,31,31,31,31,31,31,31,31,30,30,30,29,28,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,3,2,1,1,1,0,0,0,0,0,0,0,0,0,0,1,1,2,2,3,4,4,5,6,7,8,9,10,11,12,13,14,15};
static uint8_t sin_G[82] = {16,17,18,19,20,22,23,24,25,26,27,28,28,29,30,30,31,31,31,31,31,31,31,31,31,30,30,29,29,28,27,26,25,24,23,22,21,20,19,17,16,15,14,12,11,10,9,8,7,6,5,4,3,2,2,1,1,0,0,0,0,0,0,0,0,0,1,1,2,3,3,4,5,6,7,8,9,11,12,13,14,15};
static uint8_t sin_A[73] = {16,17,18,20,21,22,24,25,26,27,28,29,29,30,31,31,31,31,32,31,31,31,31,30,29,29,28,27,26,25,24,22,21,20,18,17,16,14,13,11,10,9,7,6,5,4,3,2,2,1,0,0,0,0,0,0,0,0,0,1,2,2,3,4,5,6,7,9,10,11,13,14,15};
static uint8_t sin_B[65] = {16,17,19,20,22,23,24,26,27,28,29,30,30,31,31,31,32,31,31,31,30,30,29,28,27,26,24,23,22,20,19,17,16,14,12,11,9,8,7,5,4,3,2,1,1,0,0,0,0,0,0,0,1,1,2,3,4,5,7,8,9,11,12,14,15};

ISR(TIMER0_COMPA_vect) {
  C++;
  D++;
  E++;
  F++;
  G++;
  A++;
  B++;
  if(C>122) {
    C = 0;
  }
  if(D>109) {
    D = 0;
  }
  if(E>97) {
    E = 0;
  }
  if(F>91) {
    F = 0;
  }
  if(G>81) {
    G = 0;
  }
  if(A>72) {
    A = 0;
  }
  if(B>64) {
    B = 0;
  }
  int values[7] = {sin_C[C],sin_D[D],sin_E[E],sin_F[F],sin_G[G],sin_A[A],sin_B[B]};
  OCR0A = values[val];
}

void setup() {
  DDRB |= (1<<PB0);
  TCNT0 = 0;
  TCCR0A=0;
  TCCR0B=0;
  TCCR0A |=(1<<COM0A1);
  TCCR0A |=(1<<WGM01);
  TCCR0A |=(1<<WGM00);
  TCCR0B |= (1 << CS00);
  OCR0A=254;
  TIMSK |= (1<<OCIE0A);
}

void loop() {
}

Currently I'm not able to use a variable to control what note I should play, i.e this line doesn't work even though it compiles:

OCR0A = values[val];

where val is a global variable that I set.

I'm wondering if there is a certain way to do this or to achieve the same effect as if I substitute the line

OCR0A = values[1] + values[3] + values[5];

for the previously mentioned one the Attiny85 outputs a D chord like it should. However I want the notes to be controlled by buttons so I need to be able to change them as the program is running and a global variable is the only way I can think of doing that. Any solutions would be much appreciated.

Bence Kaulics
  • 7,066
  • 7
  • 33
  • 63

1 Answers1

0

Using global vars is the only way to communicate data between your main program loop and interrupt handlers.

While writing this response I've spotted that you should really consider using signed values in your sine tables. Signed addition is needed for the maths to work properly.

This line:

OCR0A = values[val];

can only produce a monophonic sound.

You are on the right path with this:

int values[7] = {sin_C[C],sin_D[D],sin_E[E],sin_F[F],sin_G[G],sin_A[A],sin_B[B]};

though.

So, yes, another global var should do the trick:

unsigned char notes_played;  // bits 0-7 map C-D notes

And in your ISR:

ISR(TIMER0_COMPA_vect)
{
    //...

    int values[7] = {sin_C[C],sin_D[D],sin_E[E],sin_F[F],sin_G[G],sin_A[A],sin_B[B]};

    int ocr_value = 0;
    for (unsigned char mask = 1, i = 0; mask < 0x80; mask <<= 1, ++i)
    {
        if (mask & note_played)
           ocr_value += (signed char)(values[i] - 16);  // hints that using signed values 
                                                        // in sine tables may be more 
                                                        // efficient.
    }

    // need to 'clip' OCR here?
    if (ocr_value < -(OCR_RANGE / 2))
        ocr_value = -(OCR_RANGE / 2);
    if (ocr_value > (OCR_RANGE / 2))
        ocr_value = (OCR_RANGE / 2);

    OCRA0 = (unsigned char)((ocr_value + OCR_ZERO) & 0xFF);
}

I've added OCR_RANGE and OCR_ZERO, which controls to the AC voltage range of the filtered output. range: 0-255. (OCR_RANGE = 256 may overflow). OCR_ZERO controls the mid-range point (typically 127), you should set the initial OCRA0 to OCR_ZERO in setup().

One final note. When declaring your sine tables:

static uint8_t sin_B[65] =       //...  This gets store in data space, which is limited
static const uint8_t sin_B[65] = //... This gets stored on flash, which has plenty of room.

Making the sine tables as big as possible, and the interrupt as fast as possible can really make a big difference in sound quality.

Michaël Roy
  • 6,338
  • 1
  • 15
  • 19