From b8265f6938631449af1ecbc92765e0de1e7f0822 Mon Sep 17 00:00:00 2001 From: Gordon JC Pearce Date: Wed, 16 Oct 2024 21:41:08 +0100 Subject: [PATCH] fixed envelope sustain level, basic LFO + PWM --- plugin/ic29.cpp | 70 ++++++++++++++++++++++++++++++------------- plugin/ic29.hpp | 21 +++++++++++-- plugin/ic29tables.hpp | 2 +- plugin/oscillator.cpp | 11 ++++--- 4 files changed, 76 insertions(+), 28 deletions(-) diff --git a/plugin/ic29.cpp b/plugin/ic29.cpp index 6df42eb..57f9007 100644 --- a/plugin/ic29.cpp +++ b/plugin/ic29.cpp @@ -17,17 +17,19 @@ */ #include "ic29.hpp" + #include "ic29tables.hpp" Synth ic29; Synth::Synth() { d_debug("initialising synth\n"); - envAtk = 0x20; + envAtk = 0x00; envDcy = 0x50; - envStn = 0x1f; + envStn = 0x7f; envRls = 0x3f; portaCoeff = 0x0; + lfo.speed = 0x1f; } void Synth::buildTables(double sampleRate) { @@ -35,7 +37,7 @@ void Synth::buildTables(double sampleRate) { // slightly flat middle C from ROM divider table // actually adjusted a little so that the notes are bang on // on the real synth the tuning knob is tweaked a little off to pull it in - pitchTable[i] = 260.15f * powf(2, (i - 36) / 12.0f) / sampleRate; + pitchTable[i] = 260.15f * powf(2, (i - 36) / 12.0f) / sampleRate; } } @@ -43,6 +45,10 @@ void Synth::run() { // handle a "loop" worth of envelopes, pitch calculations, etc // callled once every 4.3ms block of samples + ic29.lfo.run(); + + masterPitch = 0x1818; + for (uint8_t i = 0; i < NUM_VOICES; i++) { ic29.voices[i].update(); } @@ -60,14 +66,32 @@ void Synth::voiceOff(uint8_t voice) { ic29.voices[voice].off(); } -void Synth::basePitch() { - uint16_t pitch = 0x1818; +LFO::LFO() { + lfoOut = 0; + phase = 0; - pitch += lfoPitch; - pitch += bendPitch; - // tuning too but that's zero by default; + // phase is where we are in the LFO delay cycle + // the delay envelope sets the depth of pitch and VCF modulation + // running normally the amplitude is maxed out, and when the first + // key is struck the holdoff timer and envelope will be reset to zero + delayPhase = LFO_RUN; +} - masterPitch = pitch; +void LFO::run() { + // slightly different from the real synth code which does not use signed + // variables, since the CPU doesn't support them + + lfoOut += phase ? lfoRateTable[speed] : -lfoRateTable[speed]; + if (lfoOut > 0x1fff) { + lfoOut = 0x1fff; + phase = 0; + } + if (lfoOut < -0x1fff) { + lfoOut = -0x1fff; + phase = 1; + } + + //printf("lfoOut=%04x\n", lfoOut); } Envelope::Envelope() { @@ -76,8 +100,7 @@ Envelope::Envelope() { } void Envelope::run() { - - uint16_t tempStn = ic29.envStn << 6; + uint16_t tempStn = ic29.envStn << 7; switch (phase) { case ENV_ATK: level += atkTable[ic29.envAtk]; @@ -88,9 +111,7 @@ void Envelope::run() { break; case ENV_DCY: if (level > tempStn) { -// level = ((level * ic29.envDcy) >> 16; - level = (((level - tempStn) * dcyTable[ic29.envDcy]) >> 16) + tempStn; - + level = (((level - tempStn) * dcyTable[ic29.envDcy]) >> 16) + tempStn; } else { level = tempStn; } @@ -105,19 +126,26 @@ void Envelope::run() { } Voice::Voice() { - subosc = 1; + subosc = .11; } void Voice::calcPitch() { uint16_t target = note << 8; + // Portamento is a linear change of pitch - it'll take twice as long + // to jump two octaves as it takes to jump one + // By comparison "glide" is like an RC filter, for example in the TB303 + // This is implemented here by adding on a step value until you pass + // the desired final pitch. Once that happens the value is clamped to the + // desired pitch. + if (ic29.portaCoeff != 0) { - // porta up + // portamento up if (pitch < target) { pitch += ic29.portaCoeff; if (pitch > target) pitch = target; } - // porta down + // portamento down if (pitch > target) { pitch -= ic29.portaCoeff; if (pitch < target) pitch = target; @@ -126,17 +154,17 @@ void Voice::calcPitch() { pitch = target; } - pitch += 0x1818; //ic29.masterPitch; + pitch += ic29.masterPitch; if (pitch < 0x3000) pitch = 0x3000; // lowest note if (pitch > 0x9700) pitch = 0x6700; // highest note pitch -= 0x3000; - //pitch &= 0xff00; + // interpolate between the two table values double o1 = ic29.pitchTable[pitch >> 8]; double o2 = ic29.pitchTable[(pitch >> 8) + 1]; - double frac = (pitch & 0xff) / 255.0; + double frac = (pitch & 0xff) / 256.0f; omega = ((o2 - o1) * frac) + o1; } @@ -155,7 +183,7 @@ void Voice::on(uint8_t key) { } void Voice::off() { - // I need to rethink this bit FIXME + // sustain - I need to rethink this bit FIXME voiceState = V_OFF; if (!ic29.sustained) { env.off(); diff --git a/plugin/ic29.hpp b/plugin/ic29.hpp index 245da2a..c0320c6 100644 --- a/plugin/ic29.hpp +++ b/plugin/ic29.hpp @@ -20,6 +20,24 @@ #include "peacock.hpp" +class LFO { + public: + LFO(); + void run(); + int16_t lfoOut; + uint8_t speed; + + private: + uint8_t + phase; + uint16_t holdoff; + uint16_t envelope; + enum { LFO_RUN, + LFO_HOLDOFF, + LFO_RAMP } delayPhase; + static const uint16_t lfoRateTable[128]; +}; + class Envelope { public: Envelope(); @@ -99,9 +117,8 @@ class Synth { int16_t lfoPitch; int16_t bendPitch; Voice voices[NUM_VOICES]; + LFO lfo; - void runLfo(); - void basePitch(); }; // global diff --git a/plugin/ic29tables.hpp b/plugin/ic29tables.hpp index 2ff3acb..3a7a514 100644 --- a/plugin/ic29tables.hpp +++ b/plugin/ic29tables.hpp @@ -67,7 +67,7 @@ extern const uint8_t lfoDepthTable[128] = { 0xb0, 0xb4, 0xb8, 0xbc, 0xc0, 0xc4, 0xc8, 0xcc, 0xd0, 0xd4, 0xd8, 0xdc, 0xe0, 0xe4, 0xe8, 0xec, 0xf0, 0xf8, 0xff, 0xff}; -extern const uint16_t lfoRateTable[128] = { +const uint16_t LFO::lfoRateTable[128] = { 0x0005, 0x000f, 0x0019, 0x0028, 0x0037, 0x0046, 0x0050, 0x005a, 0x0064, 0x006e, 0x0078, 0x0082, 0x008c, 0x0096, 0x00a0, 0x00aa, 0x00b4, 0x00be, 0x00c8, 0x00d2, 0x00dc, 0x00e6, 0x00f0, 0x00fa, 0x0104, 0x010e, 0x0118, diff --git a/plugin/oscillator.cpp b/plugin/oscillator.cpp index 6dcdcbf..685c0d0 100644 --- a/plugin/oscillator.cpp +++ b/plugin/oscillator.cpp @@ -30,7 +30,10 @@ static inline float poly3blep1(float t) { void Voice::run(float *buffer, uint32_t samples) { // generate a full block of samples for the oscillator - float y, out, pw = 0.0, t; + float y, out, pw = .50, t; + + float saw = 0; + pw = 0.5-(ic29.lfo.lfoOut + 0x2000) / 61600.0f; float gain = env.level / 16384.0; @@ -52,15 +55,15 @@ void Voice::run(float *buffer, uint32_t samples) { if (pulseStage) { if (phase < 1) break; // it's not time to reset the saw t = (phase - 1) / omega; - y += poly3blep0(t) * (0.8 + 0.63 - subosc); - delay += poly3blep1(t) * (0.8 + 0.63 - subosc); + y += poly3blep0(t) * (0.8 * saw + 0.63 - subosc); + delay += poly3blep1(t) * (0.8 * saw + 0.63 - subosc); pulseStage = 0; phase -= 1; subosc = -subosc; } } - delay += (0.8 - (1.6 * phase)); // magic numbers observed on oscilloscope from real synth + delay += saw * (0.8 - (1.6 * phase)); // magic numbers observed on oscilloscope from real synth delay += (0.63 - (pw * 1.26)) + (pulseStage ? -0.63f : 0.63f); // add in the scaled pulsewidth to restore DC level // the DC correction is important because the hardware synth is AC-coupled effectively high-passing // the signal at about 10Hz or so, preventing any PWM rumble from leaking through!