Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save duckythescientist/bd47419a94fa12e84708e6d50ffabd1d to your computer and use it in GitHub Desktop.
Save duckythescientist/bd47419a94fa12e84708e6d50ffabd1d to your computer and use it in GitHub Desktop.
Karplus Strong for a single board ATtiny85 Eurorack module
/*
Make Better Choices
Single-board/panel Eurorack module
1 audio/cv input
1 audio/cv output
1 gate/trigger input or output
1 potentiometer
2 RGB LEDs
This firmware is an implementation of the Karplus Strong plucked-string
synthesis algorithm including an extension for precise (non-quantized) tuning.
The analog input is a 1V/oct CV control for the string pitch.
The potentiometer knob is an additional pitch control (added to the CV).
The digitial IO is a gate INPUT to trigger a pluck of the string.
..., and the audio is 8-bit.
These are because of my questionable design decisions when making this board.
I wouldn't fault you for telling me to "make better choices".
--------------------------------------------------------------------------------
References:
Extensions of the Karplus Strong Algorithm, Jaffe and Smith
(So far I've only implemented their precise tuning algorithm)
http://musicweb.ucsd.edu/~trsmyth/papers/KSExtensions.pdf
https://doi.org/10.2307/3680063
Allpass interpolated delay line (for precise tuning):
https://ccrma.stanford.edu/~jos/pasp/First_Order_Allpass_Interpolation.html
Adafruit's Neopixel library (for HSV and gamma):
https://github.com/adafruit/Adafruit_NeoPixel
ATTinyCore (for Arduino support and an attiny-optimized neopixel library)
https://github.com/SpenceKonde/ATTinyCore
Mozzi sound synthesis library
I started by trying to port it to the Attiny85, but it was too much work to
navigate the spaghetti (that inevitably happens when supporting multiple architectures).
But it would be very useful for running on supported hardware, and some of their
synthesis code can be directly used here.
https://github.com/sensorium/Mozzi
10-bit DAC from the Atiny85 (not currently implemented)
http://www.technoblogy.com/show?1NGL
Precise CV values from an 8-bit DAC (not currently implemented):
https://www.hackster.io/janost/diy-usb-midi-to-cv-0a5469
*/
#if F_CPU != 16000000UL
#error timing may not be accurate
#endif
#include <stdint.h>
#include <stdbool.h>
#include <avr/interrupt.h>
#include <avr/io.h>
#include <avr/delay.h>
#include <tinyNeoPixel_Static.h> // https://github.com/SpenceKonde/ATTinyCore
#include "neopixel_gamma.h"
#define XSTR(x) STR(x)
#define STR(x) #x
#define LED_DAT_PIN 0
#define AOUT_PIN 1 // OC1A, OC0B
#define DIO_PIN 2 // INT0
#define POT_PIN 3 // ADC3
#define POT_PIN_AMUX 3
#define AIN_PIN 4 // ADC2
#define AIN_PIN_AMUX 2
#define NUM_LEDS 2
#define SEAL_LED 0 // The seal with eel is the 0th LED
#define STRIPE_LED 1 // The stripe is the 1st LED
byte pixel_data[NUM_LEDS * 3];
tinyNeoPixel leds = tinyNeoPixel(NUM_LEDS, LED_DAT_PIN, NEO_GRB, pixel_data);
//#define TIMER0_TOP_VAL ((F_CPU / 8UL / AUDIO_RATE) - 1UL)
// #pragma message "The value of TIMER0_TOP_VAL is: " XSTR(TIMER0_TOP_VAL)
/*
Setting PWM_MAX to 255 gives the highest resolution.
Setting to lower values can be useful to have values exactly match
desired CV quantizations. I think 240 is perfect for 4 per halfstep.
Reference: https://www.hackster.io/janost/diy-usb-midi-to-cv-0a5469
*/
#define PWM_MAX 255
/*
ADC clock should be 50-200kHz (divided from system clock) optimally.
Normal ADC conversion takes 13 clocks
...
Timer 1 will handle PWM
*/
#define POT_DECIMATION 512UL
#define AIN_SCALE_BITS 4
#define POT_SCALE_BITS 2
volatile int32_t ain_value = 0; // Most recent value from the audio/CV input
volatile int32_t pot_value = 0; // Most recent value from the potentiometer
#define ADC_PRESCALER 64
#define ADC_PRESCALER_ADPS 6 // Prescaler select bits, table 17-5
volatile uint16_t adc_tick = 0;
#define KS_LENGTH 300
#define LOWEST_NOTE 55 // Note A1
int8_t ks_buffer[KS_LENGTH];
uint16_t ks_index = 0;
uint16_t ks_current_length = KS_LENGTH - 20;
uint8_t should_pluck = 1;
int16_t ks_partial_delay = 128;
uint8_t ks_stretch_factor = 1;
void setup() {
DDRB = (1 << LED_DAT_PIN) | (1 << AOUT_PIN); // | (1<<DIO_PIN)
PORTB = 0;
cli(); // Disable interrupts while configuring the timers and ADC
// Timer0 config
TCCR0A = (1 << WGM01); // Do nothing to OC0x pins, CTC mode (TOP==OCR0A)
TCCR0B = (1 << CS01); // Prescale by 8
OCR0A = 255;
// Timer1 config
TCCR1 = (1 << PWM1A) | (1 << COM1A1) | (1 << CS10); // PWM on OC1A, prescale 1
GTCCR = 0; // Make sure that nothing else funky is configured
OCR1A = PWM_MAX / 2; // Start with half-scale PWM
OCR1C = PWM_MAX; // Set the max value
TIMSK = (1 << OCIE0A); // Interrupt on match with OCR0A
ADMUX = AIN_PIN_AMUX; // Vcc reference, start with AIN_PIN selected
// ADCSRA = (1<<ADEN) | (1<<ADIE) | ADC_PRESCALER_ADPS; // Enable but don't start yet, interrupt on, set prescaler
ADCSRA = (1 << ADEN) | ADC_PRESCALER_ADPS; // Enable but don't start yet, set prescaler
ADCSRB = 0; // Nothing fancy
DIDR0 = (1 << POT_PIN) | (1 << AIN_PIN); // Disable digital buffers on analog-only pins
MCUCR = (1 << ISC01) | (1 << ISC00); // INT0 interrupt on rising
GIMSK = (1 << INT0); // Enable the INT0 interrupt
leds.setBrightness(0x7F); // TODO: change this for the real hardware
leds.setPixelColor(SEAL_LED, 0xFF0000);
leds.setPixelColor(STRIPE_LED, 0xFF0000);
_delay_ms(100);
leds.show();
_delay_ms(1000);
sei(); // Enable interrupts
ADCSRA |= (1 << ADSC); // Start conversion
}
void loop() {
// Do nothing. Everything gets called from interrupts.
}
void ui_tick() {
// Actually not used for the Karplus Strong implementation
// because we only update things when we get a trigger signal.
}
void pluck() {
static uint8_t time_color_angle = 0;
leds.setPixelColor(SEAL_LED, 0);
leds.setPixelColor(STRIPE_LED, 0);
leds.show();
ADMUX = POT_PIN_AMUX;
ADCSRA |= (1 << ADSC); // Start conversion
while (ADCSRA & (1 << ADSC));
pot_value = 0;
for (int i = 0; i < (1 << POT_SCALE_BITS); i++) {
ADCSRA |= (1 << ADSC); // Start conversion
while (ADCSRA & (1 << ADSC));
pot_value += 1023 - ADC;
}
ADMUX = AIN_PIN_AMUX;
ADCSRA |= (1 << ADSC); // Start conversion
while (ADCSRA & (1 << ADSC));
ain_value = 0;
for (int i = 0; i < (1 << AIN_SCALE_BITS); i++) {
ADCSRA |= (1 << ADSC); // Start conversion
while (ADCSRA & (1 << ADSC));
ain_value += ADC;
}
uint8_t wheel_angle = ((ain_value >> AIN_SCALE_BITS) + (pot_value >> POT_SCALE_BITS)) & 0xFF;
float volt_per_oct = min(5.0 * ain_value / 1024.0 / ((float)(1 << AIN_SCALE_BITS)), 1200);
// volt_per_oct = round(volt_per_oct * 12.0) / 12.0;
volt_per_oct += pot_value / 1024.0 / ((float)(1 << POT_SCALE_BITS)) / 6.0 - 1.0 / 12.0;
volt_per_oct *= 5.030 / 5.000; // Correct for voltage regulator error, board#2
// volt_per_oct *= 5.006 / 5.000; // Correct for voltage regulator error, board#1
float freq = LOWEST_NOTE * pow(2, volt_per_oct);
solve_frequency((float)freq);
time_color_angle += 4;
leds.setPixelColor(SEAL_LED, gamma24(ColorHue8(time_color_angle)));
leds.setPixelColor(STRIPE_LED, gamma24(ColorHue8(wheel_angle)));
leds.show();
// Pluck the string (by filling the buffer with random data)
for (uint16_t i = 0; i < ks_current_length; i++) {
uint16_t r = rng16();
// Supposedly this 16-bit -> 8-bit mixing is good. FastLED does it.
ks_buffer[i] = (uint8_t)(((uint8_t)(r & 0xFF)) + ((uint8_t)(r >> 8)));
}
}
void solve_frequency(float freq) {
// Implement the improved frequency stuffs from Jaffe and Smith.
// Also from the Stanford Allpass filter link.
// This is all float right now, but I'm not sure it's worth trying
// to optimize everything to be fixed-point.
uint8_t timer0_top = (uint8_t)ceil(F_CPU / 8UL / (KS_LENGTH - 4) / freq) - 1;
if (timer0_top < 100) timer0_top = 100;
OCR0A = timer0_top;
float sample_rate = F_CPU / 8UL / ((float)(1 + timer0_top));
float perfect_period = sample_rate / freq;
float eps = 0.1;
int N = (int)(perfect_period - 0.5 - eps);
float shift = perfect_period - N - 0.5;
float C = (1 - shift) / (1 + shift); // This is technically an approximation.
// The ?-long array only provides (?-1) samples of delay because we need
// samples at both (n-N) and (n-(N+1))
ks_current_length = N + 1;
if (ks_current_length > KS_LENGTH) ks_current_length = KS_LENGTH;
ks_partial_delay = (uint16_t)(C * 256);
}
uint8_t ks_tick() {
static int16_t last_ks_output_2x = 0;
static int16_t last_lowpass_2x = 0;
uint16_t next_index = (ks_index + 1) % ks_current_length;
// Don't do the divide by two part of the average yet so we can keep more resolution.
int16_t lowpass_2x = (int16_t)ks_buffer[ks_index] + (int16_t)ks_buffer[next_index];
// // Attempt at decay stretching. Kinda' works but gets crunchy.
// // This has definite promise, but I think it's too much math at this sample rate.
// // I get weird artifacts (e.g. string detuning) on some plucks.
// int16_t lowpass_2x = ((int16_t)ks_buffer[ks_index]*1 + (int16_t)ks_buffer[next_index]*(ks_stretch_factor*2 - 1)) / ks_stretch_factor;
int16_t ks_output_2x = ks_partial_delay * (lowpass_2x - last_ks_output_2x) / 256L + last_lowpass_2x;
// // Semi-efficient extra decay. Best for only the low strings.
// ks_output_2x = (ks_output_2x * 61) / 64;
last_ks_output_2x = ks_output_2x;
last_lowpass_2x = lowpass_2x;
ks_buffer[ks_index] = (ks_output_2x >> 1);
ks_index = next_index;
return ((ks_output_2x >> 1) + 128) & 0xFF;
}
ISR(TIMER0_COMPA_vect) {
OCR1A = ks_tick();
}
ISR(INT0_vect) {
pluck();
}
ISR(ADC_vect) {
if (adc_tick == 0) {
ADMUX = POT_PIN_AMUX;
ADCSRA |= (1 << ADSC); // Start conversion
}
else if (adc_tick == 1) {
// Ignore previous conversion
ADCSRA |= (1 << ADSC); // Start conversion
}
else if (adc_tick == 2) {
int32_t val_now = 1023 - ADC;
int32_t delta = val_now - (pot_value >> POT_SCALE_BITS);
pot_value += delta;
ADMUX = AIN_PIN_AMUX;
ADCSRA |= (1 << ADSC); // Start conversion
}
else if (adc_tick == 3) {
// Ignore previous conversion
ADCSRA |= (1 << ADSC); // Start conversion
}
else {
int32_t delta = ADC - (ain_value >> AIN_SCALE_BITS);
ain_value += delta;
ADCSRA |= (1 << ADSC); // Start conversion
}
adc_tick = (adc_tick + 1) % 64;
}
// create a random integer from 0 - 65535
// https://engineeringnotes.blogspot.com/2015/07/a-fast-random-function-for-arduinoc.html
uint16_t rng16() {
static uint16_t y = 1;
// if (seed != 0) y += (seed && 0x1FFF); // seeded with a different number
y ^= y << 2;
y ^= y >> 7;
y ^= y << 7;
return (y);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment