Tiny Ensemble - an Attiny85 powered chorus effect

Started by Ksander, October 27, 2023, 04:33:25 PM

Previous topic - Next topic

Ben N

A thought: Along with chorus, this seems like it might be a fruitful approach for ADT effects, which many readily available digital delay chips can't manage because their delay time won't go short enough.

Now carry on with the fascinating filtering/noise reduction discussion.
  • SUPPORTER

Ksander

Just an update: after some more fiddling, I now got Vcc set as ADC reference with external bias of the input signal (at half the supply to the Attiny), and changed the ADC to single-ended input. This frees up pins (depth/rate?) and gets rid of the '+128' that was in the code. It works, but does not improve, nor  does it worsen, the noise. I also got kicad, which is much nicer to draw circuits with than LTSpice. 

ElectricDruid

Nice work, sounds like progress.

As far as noise goes, this is a digital system, so we know pretty well where the noise comes from, and what you have to do to get rid of it. Of course, on an Attiny some options may well not be available.

1) Aliasing - too many high frequencies above Nyquist going in. Improve the input filtering.
Aliasing is easy to identify because if you input a smoothly rising tone, you hear a *falling* tone at the output.
2) Quantisation distortion - not enough bitdepth to accurately represent the signal. Increase the bit depth. The rough rule-of-thumb is that S/N for an n-bit signal is 2 + (6*n) dB. 8-bit gives you 50dB, 10-bit gives you 62dB, 12-bit gives you 74dB. Note that we need 12-bit signals to match the 73dB of a MN3207 BBD!
3) Imaging - output signal not getting properly turned back into an analogue signal. Improve the output filtering.

Of course, there's other stuff can go wrong during the digital processing that can introduce noise, but that's a maths problem.

I suspect that here the biggest issue is (2), and I doubt there's much more we can do about that. What's the fastest the ADC can run, and how many bits can it generate at that speed?


Ksander

I'll go over your reply later, but I wanted to quickly share the schematic made with kicad. I think it is accurate...

The circuit is powered using a 9v battery. This source is used to power the op-amp IC, and to create a 4.5v reference to bias the input to the op-amp (middle left). The 9v battery also powers the 78L05, which has a voltage divider attached to it to bias the buffered signal at half the Vcc for the Attiny (5v and 2.5v, top left).

The input signal is first buffered using one of the op-amps (middle left, buffered signal). It is then split; one part goes to the output 'mixer' (top right), the other part goes to the other op-amp to amplify the signal (below the first op-amp). The amplified signal is LP filtered and sent to the Attiny, with clipping diodes in front of itto protect the Attiny. It is biased at 1/2 the Attiny Vcc, which is used as the ADC reference voltage --I think this makes the most of the ADC resolution. The signal is then mo-delay-ted using the Attiny, and the output is sent to the output 'mixer' (modulated signal), where it is also filtered and scaled down to get closer to the input signal.

There are 10k pull-down resistors attached to unused pins on the Attiny (bottom right). These may be used for additional inputs.



In the code, the ADC setup must be changed accordingly:

  // Set up ADC
  ADMUX = 0<<REFS0 | 1<<ADLAR | 2<<MUX0;  // Vcc as ref, left align, ADC2 (PB4) single ended input;
  ADCSRA = 1<<ADEN | 1<<ADSC | 1<<ADATE | 1<<ADIE | 5<<ADPS0; // Enable, auto trigger, interrupt, 250kHz ADC clock:
  ADCSRB = 0<<7 | 0<<ADTS0;               // unipolar, free-running

Technoblogy has a post showing how to get 10-bit PWM from the Attiny. I've attempted to implement this, but so far without success...

I'd like to hear if anyone tries this circuit

ElectricDruid

Have you got "Output" flags on your schematic flipped around so they look like inputs? Is that why they all have a line through the middle?

I'd expect the connection to be to the flat end, and the pointy end to point to the right.

(*Least* important thing about the schematic, but the *first* thing I noticed!)

Ksander

Quote from: ElectricDruid on November 13, 2023, 03:45:14 PMHave you got "Output" flags on your schematic flipped around so they look like inputs? Is that why they all have a line through the middle?

I'd expect the connection to be to the flat end, and the pointy end to point to the right.

(*Least* important thing about the schematic, but the *first* thing I noticed!)

 ;D yes, probably!

antonis

U1B might exhibit a DC gain from unity up to x4.5 [200k/(47k+10k) +1].. :icon_wink:
Connect R5 to GND with a series cap..
"I'm getting older while being taught all the time" Solon the Athenian..
"I don't mind  being taught all the time but I do mind a lot getting old" Antonis the Thessalonian..

Ksander

Quote from: ElectricDruid on November 12, 2023, 05:19:26 PM...

I suspect that here the biggest issue is (2), and I doubt there's much more we can do about that. What's the fastest the ADC can run, and how many bits can it generate at that speed?


Regarding the ADC speed, the datasheet says that best ADC resolution (10-bit) can be attained between 50-200kHz, but that when less resolution suffices, speeds up to 1MHz can be used. I've seen a forum post somewhere that it works up to 2MHz, but that quality get progressively more shitty when going higher. I've done a few things since the last post:

1. soldered the circuit on a prototyping board. This proved to be quite an improvement over the breadboard version, since now -when I don't play the guitar- there is no audible noise at all, meaning that the 3rd order filter does its job. However, when I do play, there is some noise, which I think is quantization noise. It sounds like there is a mild bit crushing going on in the modulated signal.
2. I also experimented with increasing the ADC speed from 250kHz to 500kHz and then averaging two readings before writing it to the buffer, and similarly, reading at 1MHz and averaging four readings before writing to the buffer. Neither made any difference in the sound quality though; the same kind of quantization noise can be heard. From reading up on ADC's some more, it appears that differential ADC is less noisy than single-ended (as in the original circuit). So I also tried this, but now referencing the negative ADC channel to Vcc/2, where also the amplified guitar signal positive ADC channel is biased at. This did indeed reduce more noise.

However, when I compare the recording I posted in the opening post to what I have before me right now, unfortunately these efforts do not appear to have improved the sound quality :-[ . Mostly, I think this means that setting the gain outside the attiny doesn't really improve things. It is a pity, because I really like this IC, but maybe I have to accept that the attiny just doesn't have enough oomph for good audio processing.

One thing that I want to explore still is using two (now unused) digital pins to read a rotary encoder, and use this set the modulation-oscillation speed. And then, hopefully, come up with some code to store the set speed in EEPROM when the circuit powers down (brown-out detection?)...

ElectricDruid

10-bit ADC resolution is your best case, and that's a theoretical limit of 62dB S/N right there. You can't get away from that. You can maybe *mitigate* it, by using companding or something, but you're not going to get close to BBD quality with such a chip. It's just not possible.

If you pushed the sample rate up to 200KHz at 10-bit (if that's possible while maintaining accuracy) then perhaps you could over-sample at 50KHz and gain most of a couple of extra bits. That gets you close to 12-bit, 74dB S/N, and like that you *are* in the 1970s BBD ballpark.
But then you have to deal with 12-bit samples. The simplest way is two bytes, but then that halves your memory. The more complicated way is 1.5 bytes, which gives you 2/3rds the delay you have now, but implies a lot more overhead getting bits from a nibble here and a nibble there, etc etc.

For these low end chips, I think really we have to be amazed by what they *can* do rather than shocked at what they can't. Doing *any* sort of real-time audio on a little 8-bit processor like this is frankly astounding. Super-duper audio quality would be nice, but it's not going to happen. The thing about a dog riding a bicycle is not that the dog rides well, but that it rides at all!


Ksander

Reading with a higher bitrate is relatively straightforward, and a shorter buffer may still prove sufficient for a modulation type effect. I think the bigger challenge is getting 10 bits out. Niektb has suggested combining two PWM pins, and scaling their outputs externally with resistors. Here the problem is that resistors aren't sufficiently precise. Another method may be writing at 4 times the speed, and dividing a 10 bit variable over 4 8-bit cycles. This works because one of the timers can generate continuously high or low values. Unfortunately I can't get this to work - it justgives me a squealing sound.

ElectricDruid

Could you do something similar to what you'Ve done on the way in? So use a faster output sample rate, and then send samples that average to the right level?

For example, say we want 9-bits at 32KHz. We could output 8-bit data at 64KHz. For the final bit, we would have to look at what individual *pairs* of samples are doing. Two outputs of (for example) 72 gives us a final value of 72. One output of 72 and one output of 73 gives us an effective value of 72.5 across the pair. So we've gained a half-bit inbetween our bits by using a sample rate twice as fast as we need.
This can be extended to get two extra bits. You need a sample rate four times faster than your final output. Then you have four samples to send, so (to use the same example) we could send 72,72,72,72 or 73,72,72,72 or 73,72,73,72, or 73,73,73,72 which gives us value of 72.0, 72.25, 72.5, 72.75. Then start on 73 the same way!

I haven't tried this ever, so I don't know how well it works.

One thing that keeps coming back to me is the old DSP-G1 project by Jan Ostman - a decade ago already. That was based on some little 8-pin chip and I'd love to know what it was because he apparently got a lot out of it. It might be a better choice for something like this. <goes digging>

Oh, more details in this thread:
https://modwiggler.com/forum/viewtopic.php?t=120304
The chip was the NXP LPC-810 ARM Cortex M0+ MCU. Don't know anything about it, and he apparently did some hacking to create the output DAC. Ah well...

Ksander


Ksander

#52
It's alive! I got the 10-bit version working, and it is sufficient to get rid of the quantization noise that was audible in the 8-bit version, while also providing a nice chorus effect!

It works by merging some tricks that can be found at technoblogy, and also suggested by ElectricDruid.
Specifically, the code takes 10-bit ADC readings at 250kHz, which, with a 14 clock cycle conversion time, works out to 17.857kHz sampling rate, stores these in a 128 value 16-bit buffer. This gives about 8ms of audio in the buffer. Values are subsequently written to the 8-bit PWM pin at 4 times the sampling rate, but while dividing the 10-bit reading over four 8-bit chunks (which gives the same read/write speeds).
The modulation works in a similar way as before, but my head is a bit fuzzy and I'm not exactly sure what the period of the modulation is - I think about 3.7s.

There is some memory still available, so the delay time might still be increased, and I switched back to differential ADC at some point, but this may now also be unnecessary.

For anyone interested, here is the code:


/* Attiny85 Chorus by Ksander N. de Winkel

   10-Bit modulated delay for audio (guitar/microphone) signals.
   Combining the (scaled down) output with the original input signal yields a chorus effect.
   The maximum delay is about 8ms, and the period of modulation is 3.67s.
   The period of the modulation can be changed by setting the wrapping value of Counter.
   
   Based on Attiny85 pitch shifter 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/

*/

// differential ADC, external biasing
volatile uint16_t Buffer[128];               // Circular buffer
volatile uint16_t Sample;
volatile uint8_t Cycle, WritePtr, LastPtr, Ptr, Counter, Index, Shift;

// Everything done by interrupts

// Write sample to buffer when ADC conversion is complete
ISR (ADC_vect) {

    Buffer[WritePtr] = ADC;
    LastPtr = WritePtr;
    WritePtr = (WritePtr + 1) & 0x7F;

}

// Read from buffer and output to DAC
ISR (TIMER0_COMPA_vect) {

  static int remain;

  if (Cycle == 0) {
   
    if (Counter == 0) {
      Index = (Index + 1) & 0xFF;
      Shift = abs(Index - 127);
    }
   
    Ptr = (LastPtr - Shift) & 0x7F;   
    remain = (Buffer[Ptr]+Sample) >> 1;
    Sample = remain; 
    Counter = (Counter + 1) & 0xFF;
   
  }
 
  if (remain >= 256) { OCR1A = 255; remain = remain - 256; }
  else { OCR1A = remain; remain = 0; }
  Cycle = (Cycle + 1) & 0x03;
 
}

// Setup
void setup () {

  pinMode(0, INPUT_PULLUP);
  pinMode(2, INPUT_PULLUP);
 
  // 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 prescaler
  OCR1A = 128;                            // Initial value
  pinMode(1, OUTPUT);                     // Enable OC1A PWM output pin

  // Set up Timer/Counter0 to generate write to PWM interrupt
  TCCR0A = 2<<WGM00;                      // CTC mode
  TCCR0B = 2<<CS00;                       // /8 clock prescaler
  OCR0A = 13;                             // 1M/14 = 71.4kHz interrupt
  TIMSK = TIMSK | 1<<OCIE0A;              // Enable interrupt
 
  // Set up ADC
  ADMUX = 0<<REFS0 | 0<<ADLAR | 6<<MUX0;  // Vcc as ref | Right align | ADC2 vs ADC3 (PB4, PB3) differential input
  ADCSRA = 1<<ADEN | 1<<ADSC | 1<<ADATE | 1<<ADIE | 5<<ADPS0; // Enable | auto trigger | interrupt | /32 prescaler - 250kHz ADC clock
  ADCSRB = 1<<7 | 0<<ADTS0;               // differential ADC | free-running

}

void loop () {
}


The cycling dog is getting somewhere.

ElectricDruid

Quote from: Ksander on November 20, 2023, 02:52:45 PMThe cycling dog is getting somewhere.

Lol, fantastic! Next stop, the yellow jersey in the Tour De France?

Ksander


Ksander

Quote from: niektb on October 28, 2023, 12:13:24 PMWhat you could also try (if memory allows) is to raise the buffer type to uint16_t and use a Dual PWM output (where one port outputs the least significant bits and the other the most):

Should raise the output resolution to about 14-bits and effectively reduce the noise-floor somewhat.


Could you please elaborate? I assume that to do this, I would need to:
  • left-align the ADC sample such that the ADCH has the 8 MSB from L/R, and the ADCL register has the 2MSB and 6 zeros, from L/R
  • store ADCH and ADCL in buffers, and read from those
  • output the reading from the ADCH buffer to one PWM pin, and the reading from the ADCL to another PWM pin
  • scale the pin with the ADCH output with the 3k9 resistor, and the ADCL with the 499k resistor

I further assume that the 4n7 capacitor is for filtering, and that there should also be a decoupling cap inserted in the schematic?

How sensitive is this to the precision of the resistors? I have implemented what is described above and it appears to be proportional to the input signal somehow, but that is about it...

FiveseveN

Has this been mentioned yet? Take a look at how the pedalSHIELD UNO does it: https://electrosmash.com/pedalshield-uno
I think this is the earliest discussion of the dual DAC (goes into some depth): http://www.openmusiclabs.com/learning/digital/pwm-dac/
Quote from: R.G. on July 31, 2018, 10:34:30 PMDoes the circuit sound better when oriented to magnetic north under a pyramid?

ElectricDruid

If you're going for a dual-PWM output, it would be best to have the same number of bits for each output. The reason being that that will give you the highest-possible PWM frequency. So if we want a 12-bit output, two 6-bit PWM channels gives better performance than a 4-bit channel and an 8-bit channel (the 8-bit channel will be slower).

The resistor values have to match the relationship of the bits they represent. So for my 12-bit example, one channel does the six least significant bits, and the other does the six most significant bits. That means the high bits are x64 larger than the low bits. The mixing needs to be in the same ratio, so the low bits get a resistor x64 larger than than the high bits to reduce them to 1/64th of the level. If you want an accurate output, the error in the high bits' resistor should be smaller than 1/2LSB - that's 1/2 of /64, 1/128th - So 1% resistors would be *close but not quite* for this job, and 0.1% tolerance would be perfect. Or hand-pick a few from some 1% resistors.


Ksander

Thanks for the input, both. Those links have really nice explanations. Still, I'm a bit confused. Taking your example and making it even a bit more simple, is this understanding correct:

Say I want a 10-bit output, I would use two 5-bit channels. To get 5-bit PWM, should I set the PWM timer counter to wrap when it reaches 31 (OCR1C set at 31 and CTC1 bit set for clear on match)? Then there would be 0:31 = 32 = 2^5 values.

Next, I would take the 10 bit sample, and send the lower 5 bits to one PWM pin, and the upper 5 bits to another, like so:

 
OCR1A = (Buffer[ReadPtr] >> 5) & 0x1F;
OCR1B = (Buffer[ReadPtr]) & 0x1F;

Finally, I would scale the outputs using resistors: 4k7 and 150k should be close?

I have tried the above approach, and get some noise until I a play a note. As soon as I do, there is no output anymore. The IC seems to freeze.

ElectricDruid

Quote from: Ksander on November 29, 2023, 03:55:26 PMThanks for the input, both. Those links have really nice explanations. Still, I'm a bit confused. Taking your example and making it even a bit more simple, is this understanding correct:

Say I want a 10-bit output, I would use two 5-bit channels. To get 5-bit PWM, should I set the PWM timer counter to wrap when it reaches 31 (OCR1C set at 31 and CTC1 bit set for clear on match)? Then there would be 0:31 = 32 = 2^5 values.
I don't know the ATTiny specifically, but generally that's how it works, yes. You should do some experiments to check you've got the PWM working how you think you have. The output frequency will be the PWM module clock / 32, so check that. Another simple test it to feed the output a simple incrementing count. Then you can see the pulse width getting wider and wider and then snapping back to zero on the 'scope.

QuoteNext, I would take the 10 bit sample, and send the lower 5 bits to one PWM pin, and the upper 5 bits to another, like so:

 
OCR1A = (Buffer[ReadPtr] >> 5) & 0x1F;
OCR1B = (Buffer[ReadPtr]) & 0x1F;
Yes.

QuoteFinally, I would scale the outputs using resistors: 4k7 and 150k should be close?
Yes, close enough for starters.

QuoteI have tried the above approach, and get some noise until I a play a note. As soon as I do, there is no output anymore. The IC seems to freeze.
That sounds a lot like the chip is crashing. Experiment with the PWM output until you're sure you've got two working 5-bit PWM channels. Then you can combine them and see if you can get a 10-bit output. If it's crashing, you'll have to take stuff out until it's simple enough that it works, and then put things back in until it breaks again, and then have a close look at whatever that last thing you put in was!