Programming a 10bit Attiny85 pitch shifter

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

Previous topic - Next topic

Ksander

Dear all,

On technoblogy.com, there is a pitch shifter project which I built as a pedal. I adapted the project such that the increments are in semitones. It works well, but the sound quality is not great. As an improvement, I was thinking whether using the full 10-bit ADC reading instead of just the most significant 8-bits, and then simulating 10-bit PWM by dividing the ADC reading in 4 8-bit chunks could work. I've implemented it in the code (below), and found that the principle seems to work, but the pitch shifting does not; with the adapted code, I can lower the pitch, but not increase it. I don't know much about programming, and would greatly appreciate some input on whether there is something wrong with my code, or whether this is some limitation of the attiny.

This is the code:


/* Audio Pitch Shifter

   Based on Pitch Shifter and 10-bit PWM projects by
   David Johnson-Davies - www.technoblogy.com -
   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/

   Everything is done by interrupts

   20220706 - KW - included button debouncing
   20221031 - KW - implemented shifting by semitones
   20230101 - KW - revised code to get 10-bit sampling and PWM, instead of 8-bit
*/

volatile uint16_t Buffer[128];                // Circular buffer
volatile uint8_t ReadPtr, WritePtr, LastPtr, New;
volatile uint16_t ButtonDelayCounter = 0;    // KW 20220706
volatile signed char N = 0;                  // KW 20221031
volatile int Cycle = 0; // KW 20230102 (from 10 bit PWM example at www.technoblogy.com)

// save ADC value in buffer
ISR (ADC_vect) {
  Buffer[WritePtr] = ADC + 512; // KW: add 512 such that the ADC value becomes unsigned
  WritePtr = (WritePtr + 1) & 0x7F; // KW: '& 0x7F' - wrap at 128 values
}

// Timer interrupt - read from buffer and output to DAC
ISR (TIMER0_COMPA_vect) {

  static int remain; // KW: divide the 10-bit ADC reading over four chunks
  if (Cycle == 0) remain = Buffer[ReadPtr];
  if (remain >= 256) { OCR1A = 255; remain = remain - 256; }
  else { OCR1A = remain; remain = 0; }
  Cycle = (Cycle + 1) & 0x03;

  if (Cycle == 3) { // KW 20230102: increment reading pointer when the four chunks have been generated
    ReadPtr = (ReadPtr + 1) & 0x7F;
    if (ButtonDelayCounter>0) ButtonDelayCounter--;                  // KW 20220706: for debouncing button presses
  }
 
}

// Pin change interrupt adjusts shift
ISR (PCINT0_vect) {
  int Buttons = PINB;
  if (ButtonDelayCounter == 0) {          // KW 20220706
    if ((Buttons & 0x01) == 0) {
      N++;                                // KW 20221031
    }
    else if ((Buttons & 0x04) == 0) {
      N--;                                // KW 20221031
    }
    OCR0A = round( 112 / pow(1.0595,N) ) - 1; // KW 20221031, KW 20230102: in- or decrement shifts in semitones
    ButtonDelayCounter = 500;             // KW 20220706
  }                                       // KW 20220706
}

// 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 interrupt at four times the ADC rate.
  // The ADC provides 10-bit resolution, whereas the PWM provides 8-bit resolution
  // by running the PWM at four times the speed of the ADC, we can simulate 10-bit PWM
  TCCR0A = 2<<WGM00;                      // CTC mode
  // TCCR0B = 2<<CS00;                       // /8 prescaler
  // OCR0A = 55;                             // 17.9kHz interrupt (1M/56)
  TCCR0B = 1<<CS00;                       // KW: /1 prescaler
  OCR0A = 112;                            // KW: 71428.57143Hz interrupt (8M/112), four times the original speed to get 4*8-bit = 10-bit PWM
  TIMSK = TIMSK | 1<<OCIE0A;              // Enable interrupt
 
  // Set up ADC: given that it takes 14 cycles to get an ADC reading, audio is sampled at:
  // 250kHz/14cycles = 17857.14285Hz
  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; no need for external circuitry, 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 () {
}

Kevin Mitchell

I'd have to take time to break this down to fully understand it, so these are only my first impressions.

That's pretty cool, but there are many pitfalls from what I understand. It would make a decent variable pitch shifting fuzz but not much anything else due to lack of resolution. However, you can set the internal clock closer to 16Mhz which would improve the output resolution - maybe smooth it out with an LPF as well. But of course other chucks of the code would have to be altered as well.

Why not declare readPTR as 16-bits to begin with? You can easily bit-shift it to get two separate 8-bit values later on when needed.
I'd love to get into this one myself. But there's a mountain of "gotta finish" projects in front of me  :o
  • SUPPORTER

Ksander

#2
Thanks for the response - clearly, it takes time to see what is going on in the code; I still don't understand completely myself  ;)

The reason the pointer is 8-bits is that it doesn't need to be larger, as the buffer contains only 128 values. The read and write pointers indicate which value from the buffer to write, and which to output to the pwm.When the rates at which they increment are different, you get a pitch shift.

anotherjim

The resolution is not that bad IMHO, I have a flanger built in a Tiny84 that does ok and the output is entirely from the code. It has 10-bit output in 2 PWM outputs but there are only the lowest 2 bits in one mixed 1/4 amplitude with the 8-bit other. I didn't dare run it with interrupts so it's assembly coded with no stack leaving all the RAM free for the buffer. Most of the important code functions while the ADC is thinking.
The sample rate is 31.25kHz. The Tiny84 doesn't have the pll speed up for the peripherals clock as the Tiny85 and runs on the internal 8Mhz clock. The 85 can double its clock to 16Mhz with the pll, but IIRC that stops the faster counter clock? A faster CPU may be more beneficial for fitting in the code.

Anyway, I've no idea how the algorithm employed here works for pitch shifting. I've thunk about it from time to time but that's as far as I've got. If I did try it I would pick a Tiny85.

I don't really do C code so although you have documented the code well, I haven't understood it well enough to offer any help!

Maybe get more views in the Digital forum?


Ksander

I already doubted where to put the topic, I might ask a moderator to move it...

My understanding of how the algorithm works is as follows:
Using one timer, the adc outputs samples at a rate of 17.9kHz, or thereabouts, and these are written to a circular buffer.
You then read from the buffer, and set the duty cycle of the PWM in accordance with the value read. When the writing and reading speed are the same, the output is like a bit crushed version of the input  If you read at half the frequency, you pitch down an octave; if you read at twice the writing frequency, you pitch up an octave. The other timer is used to set this speed.

By dividing the steps in between in cents, you get semitone increases. Which is my contribution  :icon_cool:
And soon hopefully also a functional 10-bit version...

Ksander


niektb

Are you using an arduino-like bootloader? I see the 'common' void setup and void loop structure but I don't see the accompanying includes :) cause if you do we can simplify it a lot I think!

But okay I'm trying to understand the code, am I correct when I think you want to run the PWM at 4 times the ADC speed but not exactly as you want to vary the PWM speed to get the pitch shifted effect? (Cause I see you're using Timer0) I think the consequence might be that you will not actually have 10-bit but sometimes a bit less and sometimes a bit more (but I don't think that's a problem per se)

I have done a tiny bit of C programming for ATTiny's but I'm not that good, do you have a link to the explanation of your original pitch shifter? (for understanding the register settings etc) I already found the one for the 10-bit output:
http://www.technoblogy.com/show?1NGL

anotherjim

I think it's always tricky to adapt code, you have to fit your mindset in with the original coders and adhere to the overall concept.

There is a quick and dirty trick to smoothing the output for this kind of thing and that is to maintain "new" output samples at the sample rate by creating average sample values. Always send the most recently read sample from the buffer to the PWM after averaging it with the previously sent sample. The resulting wave will move progressively between the "real" values from the buffer instead of repeating the same and creating a stepped waveform. It's artificial, but I think it sounds better for it.
Averaging is fairly fast for the MCU to do. The code just needs to perform a 16bit addition and divide by 2 by shifting the result right one place.

Ksander

Hi Niektb - thanks for taking an interest in the topic!

The link to the original pitch shifter article is here:

http://www.technoblogy.com/show?1L02

The original article is very nice, and allowed me to build a pedal. Still, I found that some useful details were lacking. Things I was able to figure out later are included as comments in the code.

The code that I copy/pasted above is exactly as I have it: there are no additional includes not shown. To actually program the chip I use the Arduino IDE with Spence Conde's attinyCore, and I use an Arduino Uno as ISP. I picked up this "how-to" mostly from technoblogy and some googling.

Your understanding of what I'm trying to do is correct. As a starting point, I run the PWM part at four times the ADC speed, such that the 10-bit ADC reading can be divided over four chunks which, presented sequentially, then have the same speed as the ADC. This is in principle the same trick as described in the other technoblogy post, although I used the compare match interrupt rather than the overflow interrupt. The reason for this is that changing the register value sets the frequency of the interrupt, and thereby allows to vary the write speed relative to the read speed, creating the pitch shifts. The code works partly; the chip gives audio output, and it also produces pitch shifts. However, only shifting down, or back up to the initial 0-shift works; trying to shift up from there does not bring about any notable increase in pitch.

I'm really not sure about the precision of the whole approach, as I'm still very new to electronics and programming. But, given that the 8-bit version works well, I'm just curious to see if it can be improved!

Ksander

Quote from: anotherjim on January 05, 2023, 05:32:54 AM
I think it's always tricky to adapt code, you have to fit your mindset in with the original coders and adhere to the overall concept.

There is a quick and dirty trick to smoothing the output for this kind of thing and that is to maintain "new" output samples at the sample rate by creating average sample values. Always send the most recently read sample from the buffer to the PWM after averaging it with the previously sent sample. The resulting wave will move progressively between the "real" values from the buffer instead of repeating the same and creating a stepped waveform. It's artificial, but I think it sounds better for it.
Averaging is fairly fast for the MCU to do. The code just needs to perform a 16bit addition and divide by 2 by shifting the result right one place.

I think that this is also done in the original article. I found that including this procedure here introduced high frequency noise. I'm not sure why, but considering that it would be an optimization, I'd first like to get the shifting to work properly.

Ripthorn

#10
This is cool! I do quite a bit with ATTiny's and a pitch shifter has been on my list. If your issue is with resolution, I would do it the easy way and use an ATTiny841 which has the one 8 bit timer (for Timer0) and two 10-bit timers for PWM and the like. In fact, I will probably try it myself and see what happens, but I have one project whose firmware is fighting me right now that I need to polish off first. I'll read through the source code and yours and see if anything jumps out at me. Thanks for bringing this to my attention!

EDIT: Looks like the 841 doesn't have the 64MHz PLL, either. Still, some cool possibilities here
Exact science is not an exact science - Nikola Tesla in The Prestige
https://scientificguitarist.wixsite.com/home

anotherjim

Things can be done to condition the input and output signals, but yes you should get the code workings before worrying about it.
If you're using the 1k/100nF PWM output filter in the article, it's very heavy-handed as it cuts all treble over 1.6Khz, but deal with that later.

Ksander

Quote from: Ripthorn on January 05, 2023, 02:23:53 PM
Thanks for bringing this to my attention!

You're very welcome. It is cool indeed. The 8-bit version is already fun.

Regarding better hardware, I'm actually working on implementing some effects on the Raspberry Pi Pico, which has great features. However, there is still a lot to learn for me there!

Ripthorn

The Raspberry Pi zero is what I would like to use. I have a Pico on hand for development.

One question about your code: Is there a particular reason you are using a signed char for N and not a signed float? The documentation for pow states that the exponent must be a float and I don't see you ever casting N to a float prior to the call to pow.
Exact science is not an exact science - Nikola Tesla in The Prestige
https://scientificguitarist.wixsite.com/home

Ksander

#14
Quote from: Ripthorn on January 05, 2023, 03:09:10 PM
The Raspberry Pi zero is what I would like to use. I have a Pico on hand for development.

One question about your code: Is there a particular reason you are using a signed char for N and not a signed float? The documentation for pow states that the exponent must be a float and I don't see you ever casting N to a float prior to the call to pow.

I didn't know the exponent had to be a float (edit: shouldn't that be a double?). I chose a char as it seemed to be the smallest size variable suitable. I'll change to a float to see if it makes a difference...

niektb

Since we're on the topic on increasing bit depth, may it perhaps be easier to parallel 2 pwm outputs?
http://www.openmusiclabs.com/learning/digital/pwm-dac/dual-pwm-circuits/index.html

Ripthorn

Quote from: Ksander on January 05, 2023, 03:30:43 PM
Quote from: Ripthorn on January 05, 2023, 03:09:10 PM
The Raspberry Pi zero is what I would like to use. I have a Pico on hand for development.

One question about your code: Is there a particular reason you are using a signed char for N and not a signed float? The documentation for pow states that the exponent must be a float and I don't see you ever casting N to a float prior to the call to pow.

I didn't know the exponent had to be a float (edit: shouldn't that be a double?). I chose a char as it seemed to be the smallest size variable suitable. I'll change to a float to see if it makes a difference...

The documentation states both base and exponent need to be floats because it supports fractional exponents. I hope that helps you. Keep us posted.
Exact science is not an exact science - Nikola Tesla in The Prestige
https://scientificguitarist.wixsite.com/home

Ksander

Quote from: niektb on January 05, 2023, 06:24:27 PM
Since we're on the topic on increasing bit depth, may it perhaps be easier to parallel 2 pwm outputs?
http://www.openmusiclabs.com/learning/digital/pwm-dac/dual-pwm-circuits/index.html

yes, It was suggested also in an earlier comment. I want to look into that too!

Ksander

Quote from: anotherjim on January 04, 2023, 03:06:03 PM
The resolution is not that bad IMHO, I have a flanger built in a Tiny84 that does ok ...

Am I correct that there is a demo of this flanger on SoundCloud? It sounds really great. Could you share some more info on how the PWM channel combination was implemented? Is it sending the 2 LSB to one channel and the 8MSB to another, and then summing these after scaling the LSB down a factor 4 with a voltage divider?

Ksander

#19
Quote from: niektb on January 05, 2023, 06:24:27 PM
Since we're on the topic on increasing bit depth, may it perhaps be easier to parallel 2 pwm outputs?
http://www.openmusiclabs.com/learning/digital/pwm-dac/dual-pwm-circuits/index.html

For some reason this response appeared in a very tiny font in my browser. Very interesting, thanks!