242 lines
8.0 KiB
C
242 lines
8.0 KiB
C
/* nekobee DSSI software synthesizer plugin
|
|
*
|
|
* Copyright (C) 2023 Gordonjcp, with attributions inline
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License as
|
|
* published by the Free Software Foundation; either version 2 of
|
|
* the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be
|
|
* useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
|
* PURPOSE. See the GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public
|
|
* License along with this program; if not, write to the Free
|
|
* Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
|
|
* MA 02111-1307, USA.
|
|
*/
|
|
|
|
// complete rewrite of the voice engine
|
|
|
|
#include <math.h>
|
|
#include <stdint.h>
|
|
|
|
#include "nekobee_synth.h"
|
|
#include "nekobee_voice.h"
|
|
|
|
// centre oscillator around Middle C
|
|
// conveniently the middle of the 303's range
|
|
#define REF_NOTE 60
|
|
|
|
float nekobee_pitch[129];
|
|
float logpot[129];
|
|
|
|
void nekobee_init_tables(void) {
|
|
// create tables used by Nekobee to save on expensive calculations
|
|
// mostly involving exponentiation!
|
|
// tables are scaled to 128 values for ease of calculation with MIDI
|
|
|
|
// it's worth noting that a real 303 only responds over four octaves
|
|
// although in theory its DAC could do five
|
|
|
|
// it's a bit of a waste defining 128 MIDI notes in the expo scale
|
|
|
|
uint8_t i;
|
|
float x;
|
|
|
|
for (i = 0; i < 128; i++) {
|
|
// expo pitch scale (MIDI note number to VCO control current)
|
|
nekobee_pitch[i] = powf(2, (i - REF_NOTE) / 12.0f);
|
|
// log pot scale used for volume, decay, cutoff, and env mod
|
|
// for a range of "0 to 1" scaled to 0-127, gives a log response
|
|
// with 50% of "pot rotation" giving 15% output
|
|
x = i / 127.0f; // pot input from 0 to 1
|
|
logpot[i] = 0.03225 * powf(32, x) - 0.03225;
|
|
}
|
|
// one extra value so we don't need to bounds check the linear interpolator
|
|
logpot[128] = logpot[127];
|
|
nekobee_pitch[128] = nekobee_pitch[127];
|
|
|
|
return;
|
|
}
|
|
|
|
void vco(nekobee_synth_t *synth, uint32_t count) {
|
|
// generate a bandlimited oscillator
|
|
// uses polyblep for bandlimiting
|
|
// massive and endless thanks to Mystran
|
|
// https://www.kvraudio.com/forum/viewtopic.php?t=398553
|
|
|
|
nekobee_voice_t *voice = synth->voice;
|
|
struct blosc_t *osc = &voice->osc;
|
|
|
|
uint32_t i;
|
|
|
|
float phase = osc->phase; // current running phase 0..1
|
|
float delay = osc->delay; // delay sample for polyblep
|
|
float out, t; // output sample, temporary value for blep
|
|
|
|
// calculate omega for phase shift
|
|
float w = nekobee_pitch[voice->key] * 261.63 * synth->deltat;
|
|
|
|
// FIXME this only does saws
|
|
|
|
for (i = 0; i < count; i++) {
|
|
phase += w;
|
|
out = delay;
|
|
delay = 0;
|
|
|
|
if (phase > 1.0f) {
|
|
t = (phase - 1) / w;
|
|
out -= 0.5 * t * t; // other polynomials are available
|
|
t = 1 - t;
|
|
delay += 0.5 * t * t;
|
|
phase -= 1.0f;
|
|
}
|
|
delay += phase; // save value for next time
|
|
voice->osc_audio[i] =
|
|
0.5 - out; // save output in buffer, remove DC offset
|
|
}
|
|
osc->phase = phase;
|
|
osc->delay = delay;
|
|
}
|
|
|
|
void vcf(nekobee_synth_t *synth, float *out, uint32_t count) {
|
|
// run a 4-pole ladder filter over a block
|
|
// this is a crude implementation that only approximates the complex
|
|
// behaviour of the "real" ladder filter
|
|
|
|
// to calculate the cutoff frequency we need to solve the expo converter
|
|
// not as bad as it sounds!
|
|
// the equation is IcQ11 = IcQ10 * exp(-VbQ10 / 26)
|
|
// VbQ10 is the voltage on Q10's base, IcQ10 is the collector current
|
|
// this is supplied from the cutoff pot
|
|
|
|
nekobee_voice_t *voice = synth->voice;
|
|
|
|
float delay1 = voice->delay1, delay2 = voice->delay2,
|
|
delay3 = voice->delay3, delay4 = voice->delay4;
|
|
|
|
float vcf_eg = voice->vcf_eg;
|
|
float vca_eg = voice->vca_eg;
|
|
float vca_slug = voice->vca_slug;
|
|
|
|
float delayhp = voice->delayhp;
|
|
|
|
// to get the correct cutoff first we need Q10's collector current
|
|
// The top of VR3 Cutoff is fed from 12V, the bottom from the emitter
|
|
// of Q9 at around 3.2V through a 10k resistor. So, 8.8V between "rails"
|
|
// gives us (8.8*10k)/(10k+50k) = 1.47V at the bottom
|
|
// The wiper of VR3 goes through R73 100k and TM3 470k, which we'll assume
|
|
// is set to about half, call it 300k in total
|
|
// So IcQ10 is given by (Vcutoff - Vbias - Vbe) / 300
|
|
// For ease of working I just assume that Vbias is 0V and that the envelope
|
|
// can go negative, from about 7V to about -3V the range of Vcutoff is
|
|
// then 1.47 to 8.88-1.47 so 7.41V max
|
|
|
|
float Vcutoff = 1.47 + 7.41 * logpot[(int)floor(synth->cutoff)];
|
|
|
|
// similarly the envelope modulation pot is 50k log in series with 10k
|
|
// but the top of the pot is fed with the envelope voltage, about 10V at
|
|
// peak
|
|
|
|
float Renvmod = .167 + .833 * logpot[(int)floor(synth->envmod)];
|
|
|
|
// subtract the 3.2V offset from Q9
|
|
float Venvmod = (voice->vcf_eg - 3.2) * Renvmod;
|
|
|
|
// R63 and R71 form a voltage divider, 2.2k / (220k + 2.2k) = 0.0099
|
|
// multiply by 1000 to get a voltage in mV
|
|
|
|
Venvmod *= 9.901;
|
|
float Vbe1 = Venvmod;
|
|
|
|
// .3 is 300k expressed as MOhm
|
|
// if we expressed it in Ohms output would be in A
|
|
float IcQ10 = (Vcutoff - 0.65) / .3; // 100k + TM3, IcQ10 in uA
|
|
float IcQ11 = IcQ10 * exp(Vbe1 / 26.0); // in uA
|
|
|
|
// printf("Vbe1 = %04f, IcQ10 = %04f, IcQ11 = %04f\n", Vbe1, IcQ10, IcQ11);
|
|
|
|
float cutoff = IcQ11 * 96.67; // approximate Hz-per-uA
|
|
|
|
float ct = 6.2832 * cutoff * synth->deltat;
|
|
|
|
ct *= 0.25; // 4x oversampling
|
|
ct = ct / (1 + ct);
|
|
// printf("cutoff = %04fHz, ct=%f\n", cutoff, ct);
|
|
|
|
float hpc = 6.28 * 16 * synth->deltat;
|
|
float fout, fb, hp;
|
|
|
|
for (uint32_t i = 0; i < count; i++) {
|
|
for (uint32_t ovs = 0; ovs < 4; ovs++) {
|
|
float in = voice->osc_audio[i];
|
|
|
|
float clip = 1.0;
|
|
fb = (in - ((fout - .33*in) * synth->resonance * 4)) / clip;
|
|
|
|
//fb = in * synth->resonance*5;
|
|
|
|
if (fb > 1) fb=1;
|
|
if (fb < -1) fb=-1;
|
|
|
|
fb = 1.5 * fb - 0.5 * fb*fb*fb;
|
|
|
|
fb *= clip;
|
|
|
|
//fb *= 0.5;
|
|
|
|
delay1 = ((fb - delay1) * ct) + delay1;
|
|
delay2 = ((delay1 - delay2) * ct) + delay2;
|
|
delay3 = ((delay2 - delay3) * ct) + delay3;
|
|
delay4 = ((delay3 - delay4) * ct) + delay4;
|
|
hp = ((delay4 - delayhp) * hpc ) + delayhp;
|
|
delayhp = hp;
|
|
fout = delay4-hp;
|
|
}
|
|
vca_slug = ((vca_eg - vca_slug)*(3000*synth->deltat))+vca_slug;
|
|
out[i] = fout * vca_slug;
|
|
vcf_eg *= 1 - voice->vcf_tc;
|
|
vca_eg *= 1 - voice->vca_tc;
|
|
|
|
}
|
|
voice->delay1 = delay1;
|
|
voice->delay2 = delay2;
|
|
voice->delay3 = delay3;
|
|
voice->delay4 = delay4;
|
|
|
|
voice->vcf_eg = vcf_eg;
|
|
voice->vca_eg = vca_eg;
|
|
voice->vca_slug = vca_slug;
|
|
|
|
voice->delayhp = delayhp;
|
|
}
|
|
|
|
void nekobee_voice_render(nekobee_synth_t *synth, float *out, uint32_t count) {
|
|
// generate "count" samples into the buffer at out
|
|
|
|
// FIXME factor this out into the control code
|
|
// Decay is about 2.5 seconds with the pot all the way down, and 200ms with
|
|
// it all the way up this is set by a 1M log pot in series with a 68k
|
|
// resistor, a 1uF capacitor, and a bunch of other stuff to give a correct
|
|
// DC offset and a log tailoff right at the very bottom
|
|
|
|
if (synth->voice->velocity < 90) {
|
|
printf("accent off\n");
|
|
synth->voice->vcf_tc =
|
|
(1 / ((68 + 1000 * logpot[(int)synth->decay]) * 0.001)) * synth->deltat;
|
|
} else {
|
|
synth->voice->vcf_tc = 1/(68*0.001)*synth->deltat;
|
|
printf("accent on\n");
|
|
}
|
|
// printf("tc = %f deltat=%f pot=%f\n",synth->voice->vcf_tc,
|
|
// synth->deltat,logpot[(int)synth->decay]);
|
|
|
|
vco(synth, count);
|
|
|
|
vcf(synth, out, count);
|
|
return;
|
|
}
|