/* 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 #include #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; }