Phillip Stevens
Published © CC BY-SA

Goldilocks Analogue Synthesizer

Triple oscillator synthesizer, with cross modulation, bi-quad filter & space delay. Direct digital synthesis using wave-form look-up tables.

AdvancedFull instructions provided1,734
Goldilocks Analogue Synthesizer

Things used in this project

Hardware components

Goldilocks Analogue
×1

Software apps and online services

sourceforge

Story

Read more

Schematics

Goldilocks Analogue Prototype 3 Schematic

Schematic of prototype version 3.

Code

Example FTDI Chip EVE Coprocessor Commands

C/C++
// text
FT_GPU_CoCmd_Text_P(phost, 300,  8, 27, OPT_CENTER, PSTR("VCF"));
FT_GPU_CoCmd_Text_P(phost, 300, 25, 26, OPT_CENTER, PSTR("CUTOFF"));
FT_GPU_CoCmd_Text_P(phost, 300, 95, 26, OPT_CENTER, PSTR("PEAK"));

// toggles
FT_API_Write_CoCmd(TAG(LFO_WAVE));
FT_GPU_CoCmd_Toggle_P(phost, 13,242,46,18, OPT_3D, synth.lfo.wave, PSTR("SIN" "\xFF" "TRI"));

FT_API_Write_CoCmd(TAG(KBD_TOGGLE));
FT_GPU_CoCmd_Toggle_P(phost, 405,130,60,26, OPT_3D, synth.kbd_toggle, PSTR("CONCRT" "\xFF" "VERDI"));

// dials
FT_API_Write_CoCmd(TAG(DELAY_FEEDBACK));
FT_GPU_CoCmd_Dial(phost, 365,125,20, OPT_3D, synth.delay_feedback); // DELAY FEEDBACK

FT_API_Write_CoCmd(TAG(MASTER));
FT_GPU_CoCmd_Dial(phost, 440,55,26, OPT_3D, synth.master); // MASTER

Example FTDI Chip Touch Tag and Touch Tracker Commands

C/C++
readTag = FT_GPU_HAL_Rd8(phost, REG_TOUCH_TAG);

if (readTag > 0x80)// tag is greater than 0x80 and therefore is a dial.
{
	TrackRegisterVal.u32 = FT_GPU_HAL_Rd32(phost, REG_TRACKER);

	switch (TrackRegisterVal.touch.tag)
	{
	case (VCO1_PITCH):
		synth.vco1.pitch = TrackRegisterVal.touch.value & 0xe000;
		break;
	// continues...
	}

Setting LUT phase increments

C/C++
//  setting the phase increment for VCO1 is frequency * LUT size / sample rate.
//  << 1 in SAMPLE_RATE is residual scale to create 24.8 fixed point number.
// The LUT is already pre-scaled << 7 in the calculation.
// The LUT can't be pre-scaled to << 8 because this creates numbers too large for uint32_t to hold,
// and we want to allow the option to vary the SAMPLE_RATE at compilation time, so it has to stay in the calculation.
synth.vco1.phase_increment = (uint32_t)pgm_read_dword(synth.note_table_ptr + stop * NOTES + note) / (SAMPLE_RATE >> 1);

// set the VCO2 phase increment to be -1 octave to +1 octave from VCO1, with centre dial frequency identical.
if (synth.vco2.pitch & 0x8000) // upper half dial
	synth.vco2.phase_increment = ((synth.vco1.phase_increment >> 4) * synth.vco2.pitch ) >> 11;
else // lower half dial
	synth.vco2.phase_increment = (synth.vco1.phase_increment >> 1) + (((synth.vco1.phase_increment >> 4) * synth.vco2.pitch) >> 12);
 
// set the LFO phase increment to be from 0 Hz to 32 Hz.
synth.lfo.phase_increment = ((uint32_t)synth.lfo.pitch * LUT_SIZE / ((uint32_t)SAMPLE_RATE << 4) );

Oscillator Code

C/C++
///////////// Now do the VCO1 ////////////////////

// This will be modulated by the VCO2 value (depending on the XMOD intensity),
// and the LFO intensity.
if( synth.vco1.toggle )
{
	// Increment the phase (index into waveform LUT) by the calculated phase increment.
	// Both the phase and phase_increment are stored as 24.8 in uint32_t.
	// The fractional component of the phase and phase_increment is needed to ensure the wave
	// is tracked accurately.
	synth.vco1.phase += synth.vco1.phase_increment;

	// calculate how much the LFO affects the VCO1 phase increment
	if (synth.lfo.toggle)
	{
		// increment the phase (index into LUT) by the calculated phase increment including the LFO output.
		synth.vco1.phase += (uint32_t)outLFO; // increment on the fractional component 8.8, limiting the effect.
	}

	// calculate how much the VCO2 XMOD affects the VCO1 phase increment
	if (synth.vco2.toggle)
	{
		// increment the phase (index into LUT) by the calculated phase increment including the LFO output.
		synth.vco1.phase += (uint32_t)outXMOD; // increment on the fractional component 8.8, limiting the effect.
	}

	// if we've gone over the waveform LUT boundary -> loop back
	synth.vco1.phase &= 0x000fffff; // this is a faster way doing the table
						// wrap around, which is possible
						// because our table is a multiple of 2^n.
						// Remember the lowest byte (0xff) is fractions of LUT steps.
						// The table is 0xfff.ff bytes long.

	currentPhase = (uint16_t)(synth.vco1.phase >> 8); // remove the fractional phase component.

	// get first sample from the defined LUT for VCO1 and store it in temp1
	temp1 = pgm_read_word(synth.vco1.wave_table_ptr + currentPhase);
	++currentPhase; // go to next sample

	currentPhase &= 0x0fff;	// check if we've gone over the boundary.
				// we can do this because it is a multiple of 2^n.

	// get second sample from the LUT for VCO1 and put it in temp2
	temp2 = pgm_read_word(synth.vco1.wave_table_ptr + currentPhase);

	// interpolate between samples
	// multiply each sample by the fractional distance
	// to the actual location value
	frac = (uint8_t)(synth.vco1.phase & 0x000000ff); // fetch the lower 8bits

	// the optimised assembly code Multiply routines come from Open Music Labs.
	MultiSU16X8toH16(temp3, temp2, frac);

	// scaled sample 2 is now in temp3, and since we are done with
	// temp2, we can reuse it for the next result
	MultiSU16X8toH16(temp2, temp1, 0xff - frac);
	// temp2 now has the scaled sample 1
	temp2 += temp3; // add samples together to get an average
	// our resultant wave is now in temp2

	// set amplitude with volume
	// multiply our wave by the volume value
	MultiSU16X16toH16(outVCO1, temp2, synth.vco1.volume);
	// our VCO1 wave is now in outVCO1
}
else // if there is no note being played, then shift the output value towards mute.
{
	outVCO1 >>= 1;
}

Mixer and Space Delay Code

C/C++
////////////// mix the two oscillators //////////////////

// irrespective of whether a note is playing or not.
// combine the outputs
temp1 = (outVCO1 >> 1) + (outVCO2 >> 1);

////////// Resonant Low Pass Filter here  ///////////////

IIRFilter( &filter, &temp1);

///////// Do the space delay function ///////////////////

// Get the target number of buffer items we need, which is the delay.
MultiU16X16toH16Round( buffCount, (uint16_t)(sizeof(int16_t) * DELAY_BUFFER), synth.delay_time);

// Get a sample back from the delay buffer, some time later,
if( ringBuffer_GetCount(&delayBuffer) >= buffCount )
{
	temp0.u8[1] = ringBuffer_Pop(&delayBuffer);
	temp0.u8[0] = ringBuffer_Pop(&delayBuffer);
}
else // or else wait until we have samples available.
{
	temp0.u16 = 0x0000;
}

if (synth.delay_time) // If the delay time is set to be non zero,
{
	// do the space delay function, irrespective of whether a note is playing or not,
	// and combine the output sample with the delayed sample.
	temp1 += temp0.u16;

	// multiply our sample by the feedback value
	MultiSU16X16toH16Round(temp0.u16, temp1, synth.delay_feedback);
}
else
	ringBuffer_Flush(&delayBuffer);	// otherwise flush the buffer if the delay is set to zero.

// and push it into the delay buffer if buffer space is available
if( ringBuffer_GetCount(&delayBuffer) <= buffCount )
{
	ringBuffer_Poke(&delayBuffer, temp0.u8[1]);
	ringBuffer_Poke(&delayBuffer, temp0.u8[0]);
}
// else drop the space delay sample (probably because the delay has been reduced).


////////////// Finally, set the output volume //////////////////

// multiply our wave by the volume value
MultiSU16X16toH16(temp2, temp1, synth.master);

// and output wave on both A & B channel, shifted to (+)ve values only because this is what the DAC needs.
*ch_A = *ch_B = temp2 + 0x8000;

Sampling Interrupt triggered by 8 Bit Timer 0

C/C++
ISR(TIMER0_COMPA_vect) __attribute__ ((hot, flatten));
ISR(TIMER0_COMPA_vect)
{
	// MCP4822 data transfer routine
	// move data to the MCP4822 - done first for regularity (reduced jitter).
	// &'s are necessary on data_in variables
	DAC_out (ch_A_ptr, ch_B_ptr);

	// audio processing routine - do whatever processing on input is required - prepare output for next sample.
	// Fire the global audio handler, if set.
	if (audioHandler!=NULL)
		audioHandler(ch_A_ptr, ch_B_ptr);
}

Biquad IIR Low Pass Filter - Setup Code

C/C++
Calculates the filtering coefficients for a biquad IIR filter, relevant for low pass filtering. Separate set up code exists for Band Pass and High Pass Filtering.
Individual filter structures can be declared containing the state variables for individual filters. This allows filters to be chained or paralleled for further signal processing.
/**************** IIR Filter *****************/
//========================================================
// second order IIR -- "Direct Form I Transposed"
//  a(0)*y(n) = b(0)*x(n) + b(1)*x(n-1) +  b(2)*x(n-2)
//                   - a(1)*y(n-1) -  a(2)*y(n-2)
// assumes a(0) = IIRSCALEFACTOR = 32

// http://en.wikipedia.org/wiki/Digital_biquad_filter
// https://www.hackster.io/bruceland/dsp-on-8-bit-microcontroller
// http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt

typedef struct {
  uint16_t sample_rate;	// sample rate in Hz
  uint16_t cutoff;	// normalised cutoff frequency, 0-65536. maximum is sample_rate/2
  uint16_t peak;	// normalised Q factor, 0-65536. maximum is Q_MAXIMUM
  int16_t b0,b1,b2,a1,a2; // Coefficients in 8.8 format
  int16_t xn_1, xn_2;	//IIR state variables
  int16_t yn_1, yn_2; //IIR state variables
} filter_t;

void setIIRFilterLPF( filter_t *filter ) // Low Pass Filter Setting
{
  if ( !(filter->sample_rate) )
    filter->sample_rate = SAMPLE_RATE;

  if ( !(filter->cutoff) )
    filter->cutoff = UINT16_MAX >> 1;
        // 1/4 of sample rate = filter->sample_rate>>2

  if ( !(filter->peak) )
    filter->peak =  (uint16_t)(M_SQRT1_2 * UINT16_MAX / Q_MAXIMUM);
        // 1/sqrt(2) effectively

  double frequency = ((double)filter->cutoff * (filter->sample_rate>>1)) / UINT16_MAX;
  double q = (double)filter->peak * Q_MAXIMUM / UINT16_MAX;
  double w0 = (2.0 * M_PI * frequency) / filter->sample_rate;
  double sinW0 = sin(w0);
  double cosW0 = cos(w0);
  double alpha = sinW0 / (q * 2.0f);
  double scale = IIRSCALEFACTOR / (1 + alpha); // a0 = 1 + alpha

  filter->b0	= \
  filter->b2	= float2int( ((1.0 - cosW0) / 2.0) * scale );
  filter->b1	= float2int(  (1.0 - cosW0) * scale );

  filter->a1	= float2int( (-2.0 * cosW0) * scale );
  filter->a2	= float2int( (1.0 - alpha) * scale );
}   

Biquad IIR Filter - Signal Chain Code

C/C++
This function processes the signal Xn and produces the filtered version Yn returned in the location of the input signal. Because this function is used in the signal chain, at the sample reconstruction rate, it uses optimised multiplication, and fixed arithmetic.
The filter structure contains the relevant coefficients and the state variables, so filtering can be chained or paralleled to produce different outcomes.
//========================================================
// second order IIR -- "Direct Form I Transposed"
//  a(0)*y(n) = b(0)*x(n) + b(1)*x(n-1) +  b(2)*x(n-2)
//                   - a(1)*y(n-1) -  a(2)*y(n-2)
// assumes a(0) = IIRSCALEFACTOR = 32

// http://en.wikipedia.org/wiki/Digital_biquad_filter
// https://www.hackster.io/bruceland/dsp-on-8-bit-microcontroller
// http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt

// interim values in 24.8 format
// returns y(n) in place of x(n)
void IIRFilter( filter_t *filter, int16_t * xn )
{
 int32_t yn;			// current output
 int32_t  accum;		// temporary accumulator

 // sum the 5 terms of the biquad IIR filter
 // and update the state variables
 // as soon as possible
 MultiS16X16to32(yn,filter->xn_2,filter->b2);
 filter->xn_2 = filter->xn_1;

 MultiS16X16to32(accum,filter->xn_1,filter->b1);
 yn += accum;
 filter->xn_1 = *xn;

 MultiS16X16to32(accum,*xn,filter->b0);
 yn += accum;

 MultiS16X16to32(accum,filter->yn_2,filter->a2);
 yn -= accum;
 filter->yn_2 = filter->yn_1;

 MultiS16X16to32(accum,filter->yn_1,filter->a1);
 yn -= accum;

 filter->yn_1 = yn >> (IIRSCALEFACTORSHIFT + 8); // divide by a(0) = 32 & shift to 16.0 bit outcome from 24.8 interim steps

 *xn = filter->yn_1; // being 16 bit yn, so that's what we return.
}

Simple ADSR Envelope Generator

C/C++
A simple envelope generator, using a precalculated (1-e^-x) table to get a nice exponential attack and release.
Decay has not been implemented
///////////////// calculate the adsr /////////////////////

switch (synth.adsr)
{
 case off: // wait for a note to be played
  if ( synth.note == FT_FALSE )
  {	// if there is no note being played, then reset the VCO increments.
   synth.vco1.phase = \
   synth.vco2.phase = \
   synth.lfo.phase  = \
   temp1 			 = 0x00;
  }
  else
  {	// set the adsr to attack and start producing sounds
   synth.adsr = attack;
   synth.adsr_phase = 0x00;
  }
  break;

 case attack:
  currentPhase = pgm_read_word(synth.adsr_table_ptr + synth.adsr_phase);
  MultiSU16X16toH16Round( temp1, temp2, currentPhase );

  if ( ++synth.adsr_phase > 0x07ff ) // attack state is for 2047 samples.
  {
   synth.adsr = decay;
   synth.adsr_phase = 0x0000;
  }
  break;

 case decay:
  temp1 = temp2;
  if (synth.note == FT_FALSE)
  {
   synth.adsr = release;
   synth.adsr_phase = 0x0000;
  }
  else
  {
   synth.adsr = sustain;
   synth.adsr_phase = 0x0000;
  }
  break;

 case sustain:
  temp1 = temp2;
  if ( synth.note == FT_FALSE )
  {
   synth.adsr = release;
   synth.adsr_phase = 0x0000;
  }
  break;

 case release:
  currentPhase = pgm_read_word(synth.adsr_table_ptr + synth.adsr_phase);
  MultiSU16X16toH16Round( temp1, temp2, UINT16_MAX - currentPhase );

  if ( ++synth.adsr_phase > 0x07ff) // release state is for 2047 samples.
  {
   synth.adsr = off;
   synth.adsr_phase = 0x0000;
  }
  else if ( synth.note != FT_FALSE )
  {
   synth.adsr = attack;
   synth.adsr_phase = 0x0000;
  }
  break;
}

Credits

Phillip Stevens

Phillip Stevens

16 projects • 84 followers
You can flog a dead horse to water, but the grass is always greener on the flip side.

Comments