Programming a 10bit Attiny85 pitch shifter

Started by Ksander, January 04, 2023, 02:52:16 AM

Previous topic - Next topic

anotherjim


nodemand

#21
Hi!
Great initiative!
I'm also working on a similar solution. Real time pitch shifting. If you are @home in Assembly, the following page might be of interest:

http://elm-chan.org/works/vp/report.html

ASM code if available for download at the bottom of the page...
Also, as an microcontroller, the ATtiny1616 might be a good choice.
It has a DAC, hardware multiplication support and a 20MHz internal oscillator; so no crystal needed and at the time of writing it is actually available ;-).

parmalee

Alternately, you could try this with a board implementing either the ATSAMD21 or ATSAMD51 chips--such as the Adafruit Itsybitsy boards.  With these you could have either 10 bit output (at 44.1Khz sampling rate) with the 21 or 12 bit output with the 51.  And, if you were so inclined, use I2S with an additional decoder board to get 16 bit!  (IF you opt to go this route, make sure the chip has I2S capabilities--the 48 pin (smd) version of the 51 does not.)

Caveat:  This approach will require considerable adaptation from the technoblogy code.

Incidentally, I had previously attempted to sort of "fuse" the short sample player code from technoblogy with the method described for obtaining 10 or 12 bit pwm output.  Eventually, I just went with an ATSAMD21 board as it's got way more... well, way more everything as compared with the Attiny85, and I could store multiple (short) samples on a single board and have more than one outputting simultaneously.  Also, while somewhat larger than the Attiny, these boards are (or can be, depending upon which board you go with and how many pins and such you intend to utilize) still quite small--which I needed for my particular application. *

* OK, maybe I didn't need such, but I'm a bit obsessed with fitting things into the most compact enclosure conceivable.

patrick398

I've been having some fun with this little circuit, i was wondering how easy it would be to replace the push buttons which shift the pitch and just have a pot with continuous adjustment from top of the range to the bottom?

patrick398

I've had a go at implemented a pot to replace the two buttons but so far to no avail. I'm just doing an analogRead of the pot, mapping its value and calling it 'range' then replacing the OCR0A value with range. Not sure why it's not working (other than my feeble coding abilities)

I've made some other changes to the code so it makes some pretty wild sounds but all works fine with the buttons, just not with the pot.

Any thoughts?




/* Audio Pitch Shifter

   David Johnson-Davies - www.technoblogy.com - 11th February 2017
   ATtiny85 @ 8MHz (internal oscillator; BOD disabled)
   
   CC BY 4.0
   Licensed under a Creative Commons Attribution 4.0 International license:
   http://creativecommons.org/licenses/by/4.0/
*/

volatile uint8_t Buffer[256];             // Circular buffer
volatile uint8_t ReadPtr, WritePtr, LastPtr, New;

// Everything done by interrupts **********************************

// ADC conversion complete - save sample in buffer
ISR (ADC_vect) {
  Buffer[LastPtr] = New;
  New = ADCH + 128;
  Buffer[WritePtr] = (Buffer[WritePtr] + New)>>1;
  LastPtr = WritePtr;
  WritePtr = (WritePtr -5) & 0xFF;
}

// Timer interrupt - read from buffer and output to DAC
ISR (TIMER0_COMPA_vect) {
  OCR1A = Buffer[ReadPtr];
  ReadPtr = (ReadPtr + 5) & 0xFF;
}

// Read pot value and map to set range of pitch shift
int potValue = analogRead (0);
int range = map (potValue, 0, 1023, 18, 67);



// Setup **********************************************
 
void setup () {
  // Enable 64 MHz PLL and use as source for Timer1
  PLLCSR = 1<<PCKE | 1<<PLLE;     

  // Set up Timer/Counter1 for PWM output
  TIMSK = 0;                              // Timer interrupts OFF
  TCCR1 = 1<<PWM1A | 2<<COM1A0 | 1<<CS10; // PWM OCR1A, clear on match, 1:1 prescale
  OCR1A = 67;
  pinMode(1, OUTPUT);                     // Enable OC1A PWM output pin

  // Set up Timer/Counter0 to generate 20kHz interrupt
  TCCR0A = 2<<WGM00;                      // CTC mode
  TCCR0B = 2<<CS00;                       // /8 prescaler
  OCR0A = range;                             // set 0CR0A as mapped pot read
  TIMSK = TIMSK | 1<<OCIE0A;              // Enable interrupt
 
  // Set up ADC
  ADMUX = 2<<REFS0 | 1<<ADLAR | 7<<MUX0;  // Internal 1.1V ref, ADC2 vs ADC3, x20
  // Enable, auto trigger, interrupt, 250kHz ADC clock:
  ADCSRA = 1<<ADEN | 1<<ADSC | 1<<ADATE | 1<<ADIE | 4<<ADPS0; 
  ADCSRB = 1<<7 | 0<<ADTS0;               // Bipolar, free-running

  PCMSK = 1<<PINB0 | 1<<PINB2;            // Pin change interrupts on PB0 and PB2
  GIMSK = GIMSK | 1<<PCIE;                // Enable pin change interrupt

}
         
         
         
          // Enable pin change interrupt


void loop () {
}

patrick398

Sorry to keep banging on about this but i read that this may be easier to do with a rotary encoder. I'm guessing that's because it works using interrupts and so does the rest of the code...
Am i getting anywhere closer with this? I haven't tested it yet but just want to know if i'm in the ball park.

Thank!

/* Audio Pitch Shifter

   David Johnson-Davies - www.technoblogy.com - 11th February 2017
   ATtiny85 @ 8MHz (internal oscillator; BOD disabled)
   
   CC BY 4.0
   Licensed under a Creative Commons Attribution 4.0 International license:
   http://creativecommons.org/licenses/by/4.0/
*/

volatile uint8_t Buffer[256];             // Circular buffer
volatile uint8_t ReadPtr, WritePtr, LastPtr, New;

const int EncoderA = 3;
const int EncoderB = 4;

volatile int a0;
volatile int c0;
volatile int Count = 0;

// Everything done by interrupts **********************************

// ADC conversion complete - save sample in buffer
ISR (ADC_vect) {
  Buffer[LastPtr] = New;
  New = ADCH + 128;
  Buffer[WritePtr] = (Buffer[WritePtr] + New)>>1;
  LastPtr = WritePtr;
  WritePtr = (WritePtr + 1) & 0xFF;
}

// Timer interrupt - read from buffer and output to DAC
ISR (TIMER0_COMPA_vect) {
  OCR1A = Buffer[ReadPtr];
  ReadPtr = (ReadPtr + 1) & 0xFF;
}

// Pin change interrupt adjusts shift
ISR (PCINT0_vect) {
  int a = PINB>>EncoderA & 1;
  int b = PINB>>EncoderB & 1;
  if (a != a0) {
   OCR0A++;
    if (b != c0) {
      OCR0A--;

    }
  }


}

// Setup **********************************************
 
void setup () {
  // Enable 64 MHz PLL and use as source for Timer1
  PLLCSR = 1<<PCKE | 1<<PLLE;     

  // Set up Timer/Counter1 for PWM output
  TIMSK = 0;                              // Timer interrupts OFF
  TCCR1 = 1<<PWM1A | 2<<COM1A0 | 1<<CS10; // PWM OCR1A, clear on match, 1:1 prescale
  OCR1A = 128;
  pinMode(1, OUTPUT);                     // Enable OC1A PWM output pin

  // Set up Timer/Counter0 to generate 20kHz interrupt
  TCCR0A = 2<<WGM00;                      // CTC mode
  TCCR0B = 2<<CS00;                       // /8 prescaler
  OCR0A = 55;                             // 17.9kHz interrupt
  TIMSK = TIMSK | 1<<OCIE0A;              // Enable interrupt
 
  // Set up ADC
  ADMUX = 2<<REFS0 | 1<<ADLAR | 7<<MUX0;  // Internal 1.1V ref, ADC2 vs ADC3, x20
  // Enable, auto trigger, interrupt, 250kHz ADC clock:
  ADCSRA = 1<<ADEN | 1<<ADSC | 1<<ADATE | 1<<ADIE | 5<<ADPS0; 
  ADCSRB = 1<<7 | 0<<ADTS0;               // Bipolar, free-running

  // Set up buttons on PB0 and PB2
  pinMode(0, INPUT_PULLUP);
  pinMode(2, INPUT_PULLUP);
  PCMSK = 1<<PINB0 | 1<<PINB2;            // Pin change interrupts on PB0 and PB2
  GIMSK = GIMSK | 1<<PCIE;                // Enable pin change interrupt
}

void loop () {
}

patrick398

For anyone still following, made some progress and got the rotary encoder working, kind of. It moves the pitch down but i can't seem to get it to shift the pitch up when the encoder is turned the other direction, any thoughts?

//Good settings - OCR0A = 18 + 3<<ADPS0;
//              - OCR0A = 67 + 4<<ADPS0
//              - OCR0A = 27 + 5<<ADPS0


/* Audio Pitch Shifter

   David Johnson-Davies - www.technoblogy.com - 11th February 2017
   ATtiny85 @ 8MHz (internal oscillator; BOD disabled)

   CC BY 4.0
   Licensed under a Creative Commons Attribution 4.0 International license:
   http://creativecommons.org/licenses/by/4.0/
*/

volatile uint8_t Buffer[256];             // Circular buffer
volatile uint8_t ReadPtr, WritePtr, LastPtr, New;
const int EncoderA = 0;
const int EncoderB = 2;
volatile int a0;
volatile int c0;
volatile int Count = 0;

// Everything done by interrupts **********************************

// ADC conversion complete - save sample in buffer
ISR (ADC_vect) {
  Buffer[LastPtr] = New;
  New = ADCH + 128;
  Buffer[WritePtr] = (Buffer[WritePtr] + New) >> 1;
  LastPtr = WritePtr;
  WritePtr = (WritePtr - 5) & 0xFF;
}

// Timer interrupt - read from buffer and output to DAC
ISR (TIMER0_COMPA_vect) {
  OCR1A = Buffer[ReadPtr];
  ReadPtr = (ReadPtr + 5) & 0xFF;
}

// Pin change interrupt adjusts shift
ISR (PCINT0_vect) {
  int a = PINB>>EncoderA & 20;
  int b = PINB>>EncoderB & 20;
  if (a != a0) {
   OCR0A++;
    if (b != c0) {
      OCR0A--;
    }
  }
}



// Setup **********************************************

void setup () {
  // Enable 64 MHz PLL and use as source for Timer1
  PLLCSR = 1 << PCKE | 1 << PLLE;

  // Set up Timer/Counter1 for PWM output
  TIMSK = 0;                              // Timer interrupts OFF
  TCCR1 = 1 << PWM1A | 2 << COM1A0 | 1 << CS10; // PWM OCR1A, clear on match, 1:1 prescale
  OCR1A = 67;
  pinMode(1, OUTPUT);                     // Enable OC1A PWM output pin

  // Set up Timer/Counter0 to generate 20kHz interrupt
  TCCR0A = 2 << WGM00;                    // CTC mode
  TCCR0B = 2 << CS00;                     // /8 prescaler
  OCR0A = 57;                             // 17.9kHz interrupt
  TIMSK = TIMSK | 1 << OCIE0A;            // Enable interrupt

  // Set up ADC
  ADMUX = 2 << REFS0 | 1 << ADLAR | 7 << MUX0; // Internal 1.1V ref, ADC2 vs ADC3, x20
  // Enable, auto trigger, interrupt, 250kHz ADC clock:
  ADCSRA = 1 << ADEN | 1 << ADSC | 1 << ADATE | 1 << ADIE | 5 << ADPS0;
  ADCSRB = 1 << 7 | 0 << ADTS0;           // Bipolar, free-running

// Set up buttons on PB0 and PB2
  pinMode(EncoderA, INPUT_PULLUP);
  pinMode(EncoderB, INPUT_PULLUP);
  PCMSK = 1<<PINB0 | 1<<PINB2;            // Pin change interrupts on PB0 and PB2
  GIMSK = GIMSK | 1<<PCIE;                // Enable pin change interrupt


}


void loop () {
}

Ksander

I had kind of forgotten about this thread!

I have a version with a pot too, which allows varying between -12 and +12 half steps. The problem is that two ADC uses introduces a lot of additional noise. I'll post the code later on.

Ksander

As promised. The pot is read when the button is pressed. It works, but is too noisy...


/* Audio Pitch Shifter
 
   Based on:
   David Johnson-Davies - www.technoblogy.com - 11th February 2017
   ATtiny85 @ 8MHz (internal oscillator; BOD disabled)
   
   CC BY 4.0
   Licensed under a Creative Commons Attribution 4.0 International license:
   http://creativecommons.org/licenses/by/4.0/

   20220706 - Ksander de Winkel - included button debouncing
   20221031 - Ksander de Winkel - implemented shifting by half notes
   20230115 - Ksander de Winkel - implemented us e of a potentiometer to set shift directly
*/

// Definitions
# define NMATCH 56                          // initial compare match value to set audio write frequency
# define adc_a 0b10110111                   // ADC settings for sampling audio 0b10110111 -> gain 20; 0b10110110 -> gain 1
# define adc_b 0b10110001                   // ADC settings for sampling an external potentiometer
# define NSAMP 21

// Declarations
volatile uint8_t AudioBuffer[256];               // Circular buffer
volatile uint8_t ADCBuffer[NSAMP];
volatile uint8_t ReadPtr, WritePtr, LastPtr, New, ADCHat;
volatile uint16_t ButtonDelayCounter = 0;
volatile signed char N = 0, idx;

// Interrupt called when an ADC conversion completes
// Write ADC value to variable(s)
// An ADC conversion takes 14 clock cycles; the sampling rate will be approximately 250/14 or 17857.1Hz.
ISR (ADC_vect) {

  AudioBuffer[WritePtr] = ADCH + 128;
  WritePtr = (WritePtr + 1) & 0xFF;
 
  //AudioBuffer[LastPtr] = New;
  //New = ADCH + 128;
  //AudioBuffer[WritePtr] = (AudioBuffer[WritePtr] + New)>>1; // average present and previous reading
  //AudioBuffer[WritePtr] = (AudioBuffer[(WritePtr-1) & 0xFF] + AudioBuffer[WritePtr] + New) / 3 ;
  //LastPtr = WritePtr;
  //WritePtr = (WritePtr + 1) & 0xFF;
}

// Interrupt called on timer 0
// Read from buffer and output to DAC
// Frequency = clk / prescaler / compare_match:
ISR (TIMER0_COMPA_vect) {
 
  OCR1A = AudioBuffer[ReadPtr];
  ReadPtr = (ReadPtr + 1) & 0xFF;
  if (ButtonDelayCounter>0) ButtonDelayCounter--;                 // added KW 20220706
 
}

// Pin change interrupt adjusts shift
// The shift affects reading frequency as 8 mHz / 8 prescaler / OCR0A
// Doubling OCR0A to 2*56 = 128 means octave DOWN
// Halving OCR to 56/2 = 28 means octave UP
// Intermediate steps adjust by semitones

// Pin change interrupt to
// 1. interrupt audio processing,
// 2. sample the potmeter,
// 3. set N
// 4. restart audio processing

ISR (PCINT0_vect) {

  if (ButtonDelayCounter == 0) {

    readPot();
   
  }
                                         
}

void readPot(){
 
  initADCb();
 
  while (ADCSRA & (1 << ADSC) ){/*do nothing*/}; // wait till any running conversion is complete
 
  for(int i = 0 ; i < NSAMP; i++){
    ADCSRA |= (1 << ADSC);         // start ADC measurement
    while (ADCSRA & (1 << ADSC) ){/*do nothing*/}; // wait till conversion complete
    ADCBuffer[i] = ADCH;          // write value in buffer
  }

//  ADCHat = (ADCBuffer[4]+ADCBuffer[3]+ADCBuffer[2])/3;

  qsort(ADCBuffer, NSAMP, sizeof(ADCBuffer[0]), compare);
  idx = floor(NSAMP/2);
  ADCHat = ADCBuffer[idx];
 
  N = map(ADCHat,0,255,-12,13);
  OCR0A = round( NMATCH / pow(1.0595,(int)N) ) - 1;
   
  ButtonDelayCounter = 500;             // added KW 20220706

  initADCa();
 
}

int compare (const void * a, const void * b)
{
  return ( *(int*)a - *(int*)b );
}

void initPWM() {
 
  // Set up Timer1 for PWM
 
  PLLCSR = 1<<PCKE | 1<<PLLE;             // Enable 64 MHz PLL and use as source for Timer1     
  TIMSK = 0;                              // Timer interrupts OFF
  TCCR1 = 1<<PWM1A | 2<<COM1A0 | 1<<CS10; // PWM OCR1A, clear on match, 1:1 prescale
  OCR1A = 128;                            // Inital PWM output value
  pinMode(1, OUTPUT);                     // Enable OC1A PWM output pin
 
}

void initAudioOut() {
 
  // Set up Timer0 to generate audio output interrupt
 
  TCCR0A = 2<<WGM00;                      // CTC mode
  TCCR0B = 2<<CS00;                       // /8 prescaler
  OCR0A = NMATCH - 1;                     // 17.9kHz interrupt (1M/56)
  TIMSK = TIMSK | 1<<OCIE0A;              // Enable interrupt
 
}


void initButtons() {
 
  // Set up pushbuttons on PB0 and PB2
   
  pinMode(0, INPUT_PULLUP);     // Pin PB0, initialized HIGH
  PCMSK = 1<<PINB0;             // Pin change interrupts on PB0 (for PB2 add " | 1<<PINB2")
  GIMSK = GIMSK | 1<<PCIE;      // Enable pin change interrupt

}

void initADCa() { // Set up ADC for audio
 
  // ADMUX register
 
  // 1<<REFS2 with next line...
  // 1<<REFS1 with next line...
  // 0<<REFS0 Internal 2.56v reference
  // 6<<REFS0 as above(?)
  // 1<<ADLAR left-adjusted data register
  // 7<<MUX0  differential inputs ADC2 (PB4) and ADC3 (PB3) with a gain of x20
  // Above is set in adc_a

  ADMUX = adc_a;
   
  // ADCSRA register

  // 1<<ADEN   ADC enabled
  // 1<<ADSC   Start conversions
  // 1<<ADATE  Auto triggering
  // 1<<ADIE   Enable the ADC conversion interrupt
  // 5<<ADPS0  Select a prescaler of divide by 32, giving an ADC clock of 8mHz/32 = 250kHz

    ADCSRA =
        (1 << ADEN)  |     // Enable ADC
        (1 << ADATE) |     // auto trigger enable
        (1 << ADIE)  |     // enable ADC interrupt
        (5 << ADPS0);      // Prescaler = 32

    // ADCSRB register

    //1<<BIN    Bipolar input mode (BIN = 7).
    //0<<ADTS0  Free running mode.
   
    ADCSRB = 1<<7 | 0<<ADTS0;               // Bipolar, free-running
   
    sei(); // enable interrupts
   
    ADCSRA |= (1 << ADSC); // start conversions
}

void initADCb() { // Set up ADC for potmeter
 
  // ADMUX register
 
  // 1<<REFS2 with next line...
  // 1<<REFS1 with next line...
  // 0<<REFS0 internal 2.56v reference
  // 6<<REFS0 as above(?)
  // 1<<ADLAR left-adjusted data register
  // 1<<MUX0  single ended input ADC1 (PB2)
  // above is set in adc_b

  ADMUX = adc_b;
   
  // ADCSRA register

  // 1<<ADEN   ADC enabled
  // 1<<ADSC   start conversions - set at end
  // 1<<ADATE  auto triggering
  // 1<<ADIE   enable the ADC conversion interrupt
  // 5<<ADPS0  select a prescaler of divide by 32, giving an ADC clock of 8mHz/32 = 250kHz

  ADCSRA =
      (1 << ADEN)  |     // Enable ADC
      (0 << ADATE) |     // auto trigger enable
      (0 << ADIE)  |     // enable ADC interrupt
      (0 << ADPS0);      // Prescaler = 32
     
  // ADCSRB register
 
  //0<<BIN    unipolar input mode (BIN = 7)
  //0<<ADTS0  free running mode
       
  ADCSRB = 0<<7;
}

void setup () {

  initPWM();      // Start PWM
  initAudioOut(); // Start sending samples to PWM
  initButtons();  // Start button interrupt
  readPot();      // Get initial reading for pitch shift
 
}

void loop () {
}


Ksander

#29
With some lessons learned from the Attiny85 'Tiny Ensemble' Chorus thread, I now got the 10-bit chromatic pitch shifter to work. Is it better than the 8-bit version? Well, a bit or two.

It works differently than before:

  • The ADC is single-ended. Input goes to PB3. The gain must be set externally using an op-amp, and the signal must be biased to Vcc/2 (where Vcc is 5v for the attiny);
  • A rotary encoder is used to change shift value. It connects to PB0 and PB2. The pushbutton that is in most rotary encoders can be connected to the reset pin, such that you can reset to no-shift by pressing the button;
  • 10-bit output is achieved by splitting the samples in 5-bit low and high parts, which are sent out to PB4 (high) and PB1 (low). These two signals should be summed with resistors that differ a factor 32 in value. I used 4k7 and 150k, which seems to be close enough, and I used a 8.2nF to ground for some filtering.

There are some downsides compared to the 8-bit version: the 16-bit samples take up more memory, so the buffer is smaller. It holds some 10ms of audio. Given that the low e string note on a guitar has a period of about 12ms, this means that the buffer can't hold even one period. This becomes audible when shifting the pitch, in the form of a popping sound. The other downside is that the reading and outputting to PWM seems to be too slow. I do not know why. I corrected for this by fine-tuning the OCR1A values. I did this by using a 440Hz test signal; pitching it through the range of +/-12 semitones, measuring the output frequency and then loosely optimizing the OCR1A value for each. Expected frequencies are given by the equation: 440/(2^(1/12)^N).

On the upside, this scheme may be quite easily adapted to higher bit-rates, by oversampling the ADC and setting higher bit rates for the PWM pins.

Anyway, for anyone interested, here is the code:

/* 10-bit Audio Pitch Shifter

   Approximately chromatic glitchy 10-bit pitch shifter by Ksander de Winkel
   Thanks to the folks at DIYstompboxes.com!
 
   Based on projects by David Johnson-Davies - www.technoblogy.com - 11th February 2017
   ATtiny85 @ 8MHz (internal oscillator; BOD disabled)
   
   CC BY 4.0
   Licensed under a Creative Commons Attribution 4.0 International license:
   http://creativecommons.org/licenses/by/4.0/

*/
#include <avr/pgmspace.h>
#include <util/delay.h>
#define NWRAP 180
#define F_CPU 8000000UL

volatile uint8_t A, B, WritePtr, ReadPtr, N = 12;
volatile uint16_t Sample, Buffer[NWRAP+1];

const int EncoderA = 0;
const int EncoderB = 2;

volatile int a0;
volatile int c0;

// Pitch rate
static const uint8_t ocr[] PROGMEM = {103,97,92,86,82,77,73,68,65,61,57,54,51,47,45,43,40,38,35,33,32,30,29,26,25};

// Everything done by interrupts

// Save sample in buffer when ADC conversion is complete
ISR (ADC_vect) {
 
  Buffer[WritePtr] = (ADC + Sample) >> 1;
  WritePtr = (WritePtr + 1);
  if (WritePtr>NWRAP) WritePtr = 0;
  Sample = Buffer[WritePtr];

}

// Timer interrupt
ISR (TIMER0_COMPA_vect) {

  A = (Buffer[ReadPtr]) & 0x1F;
  B = (Buffer[ReadPtr] >> 5) & 0x1F;
 
  OCR1A = A;
  OCR1B = B;
 
  ReadPtr = (ReadPtr + 1);
  if (ReadPtr>NWRAP) ReadPtr = 0;
 
}

// Called when encoder value changes
void ChangeValue (bool Up) {
  N = max(min(N + (Up ? 1 : -1), 24), 0);
  OCR0A = pgm_read_byte(&ocr[N]);
}

// Pin change interrupt service routine
ISR (PCINT0_vect) {
 
  int a = PINB>>EncoderA & 1;
  int b = PINB>>EncoderB & 1;

  if (a != a0) {              // A changed
    a0 = a;
    if (b != c0) {
      c0 = b;
      ChangeValue(a == b);
    }
  }
}

// Setup

void setup () {
 
//  // Enable 64 MHz PLL and use as source for Timer1
//  PLLCSR = 0<<PCKE | 1<<PLLE ;

  // Set up timer/PWM items
  PLLCSR |= (1 << PLLE); //Enable PLL

  //Wait for PLL to lock
  _delay_us(100);
 
  while ((PLLCSR & (1<<PLOCK)) == 0x00)
    {
        // Do nothing until plock bit is set
    }

  // Enable clock source bit
  PLLCSR |= (1 << PCKE);     

  // Set up Timer/Counter1 for PWM output
  TIMSK = 0;                              // Timer interrupts OFF
  TCCR1 = 1<<CTC1 | 1<<PWM1A | 2<<COM1A0 | 1<<CS10; // CTC mode | PWM OCR1A | clear on match | /1 prescaler
  GTCCR = 1<<PWM1B | 2<<COM1B0;
  OCR1A = 15; // High bits
  OCR1B = 15; // Low bits
  OCR1C = 31;
  pinMode(1, OUTPUT);                     // Enable OC1A PWM output pin (PB1)
  pinMode(4, OUTPUT);                     // Enable OC1B PWM output pin (PB4)
 
  // Set up Timer/Counter0 to generate interrupt
  TCCR0A = 2<<WGM00;                      // CTC mode
  TCCR0B = 2<<CS00;                       // /8 prescaler
  OCR0A = 51;                             // 17.9kHz interrupt (1M/56)
  TIMSK = TIMSK | 1<<OCIE0A;              // Enable interrupt
 
  // Set up ADC
  ADMUX = 0<<REFS0 | 0<<ADLAR | 3<<MUX0;  // Vcc as Vref | right-align | ADC3 (PB3)
  ADCSRA = 1<<ADEN | 1<<ADSC | 1<<ADATE | 1<<ADIE | 5<<ADPS0; // Enable | Start conversion | auto trigger | interrupt enable | 250kHz ADC clock:
  ADCSRB = 0<<7 | 0<<ADTS0;               // Single-ended, free-running

  // Set up buttons on PB0 and PB2
  pinMode(EncoderA, INPUT_PULLUP);
  pinMode(EncoderB, INPUT_PULLUP);

  // Configure pin change interrupts
  PCMSK = 1<<EncoderA;
  GIMSK = 1<<PCIE;                       // Enable interrupt
  GIFR = 1<<PCIF;                        // Clear interrupt flag
 
}

void loop () {
}

ElectricDruid

You're starting to really push these little chips, Ksander! Excellent stuff, and well done for sticking with it. 8)

I'll have to get some AVRs and have a play myself.

Ksander

Yes, please do and share your experiences and wisdom  :)

These IC's are quite limited in what they can do, but have the size of an op-amp and, even though it is still a challenge, to me at least, the datasheet and arduino environment are just accessible enough for a beginner. With a little persistence, you can make them do a lot. For instance, I also have one act as a waveform generator, like on one of your dedicated IC's!

parmalee

#32
Thank you for all the work you've put into this, Ksander!

Upon reviewing all of the commercially available pedals that do pitch shifting--well, detuning to be more precise--I have discovered that none of them will shift more than + or - 30 cents.  For what I'm doing right now (with a harmonium) I need the ability to shift certain tones upwards precisely 50 cents (a quarter-tone) with the flick of a switch.  Well, a momentary button will likely prove more efficient.  Regardless, this is great! 

I can easily adapt your code and follow in the footsteps of some of my favorite nutters in the Arabic world who've made all sorts of wacky modifications to pianos, accordions and the like in order to achieve this feat.  So, thanks again!


Edit:  I was attempting to do something similar with an ATSAMD51 board, but I find working with the timers considerably more complicated.  Also the datasheets (quite massive) seem to be a whole lot less thorough than those for the Attiny and Atmega series.  Kinda frustrating as those chips seem to have a lot more potential for working with higher fidelity audio.

parmalee

Expanding upon that a bit:

The quarter tone/50 cent ratio (in 24 TET) is approximately 1.0293, so with an OCR0A value of 101 (102), a 50 cent increment would be 104.989.  104 (105) ought to be just fine for all but those with some sort of superhuman perfect pitch, I figure.  LaMonte Young might cringe at that, but that's his problem.  :icon_biggrin:

Ksander

Quote from: parmalee on December 27, 2023, 03:05:25 AMExpanding upon that a bit:

The quarter tone/50 cent ratio (in 24 TET) is approximately 1.0293, so with an OCR0A value of 101 (102), a 50 cent increment would be 104.989.  104 (105) ought to be just fine for all but those with some sort of superhuman perfect pitch, I figure.  LaMonte Young might cringe at that, but that's his problem.  :icon_biggrin:

It's a great idea to use a momentary button to get such shifts. Have you built the circuit yet?

parmalee

Quote from: Ksander on December 27, 2023, 03:12:21 PMIt's a great idea to use a momentary button to get such shifts. Have you built the circuit yet?

I'm going to try to put it together this weekend--have to go pick up a rescue dog we're adopting (hopefully) a couple of hours away from us.  Priorities!

Apparently the custom in Arabic music is to implement some sort of device to drop the pitch of certain notes 50 cents as needed.  If you listen to pieces by Oum Khulthum, Warda, et al from the late 60s/early 70s you can often hear this on whatever transistor organs they happened to be using.  With pianos and accordions they tend to make various sorts of mechanical modifications to their instruments.  I'm not inclined towards doing that with my harmonium, I'll stick with electronic means.

Che Chen had one of his guitars modded with extra frets to play various quarter tone intervals, drawing inspiration largely from Mauritanian music.  Lot of good videos of him playing said guitar in 75 Dollar Bill, but I've always enjoyed this one particularly: