fixed envelope sustain level, basic LFO + PWM

This commit is contained in:
Gordon JC Pearce 2024-10-16 21:41:08 +01:00
parent 34800af7a1
commit b8265f6938
4 changed files with 76 additions and 28 deletions

View File

@ -17,17 +17,19 @@
*/ */
#include "ic29.hpp" #include "ic29.hpp"
#include "ic29tables.hpp" #include "ic29tables.hpp"
Synth ic29; Synth ic29;
Synth::Synth() { Synth::Synth() {
d_debug("initialising synth\n"); d_debug("initialising synth\n");
envAtk = 0x20; envAtk = 0x00;
envDcy = 0x50; envDcy = 0x50;
envStn = 0x1f; envStn = 0x7f;
envRls = 0x3f; envRls = 0x3f;
portaCoeff = 0x0; portaCoeff = 0x0;
lfo.speed = 0x1f;
} }
void Synth::buildTables(double sampleRate) { void Synth::buildTables(double sampleRate) {
@ -43,6 +45,10 @@ void Synth::run() {
// handle a "loop" worth of envelopes, pitch calculations, etc // handle a "loop" worth of envelopes, pitch calculations, etc
// callled once every 4.3ms block of samples // callled once every 4.3ms block of samples
ic29.lfo.run();
masterPitch = 0x1818;
for (uint8_t i = 0; i < NUM_VOICES; i++) { for (uint8_t i = 0; i < NUM_VOICES; i++) {
ic29.voices[i].update(); ic29.voices[i].update();
} }
@ -60,14 +66,32 @@ void Synth::voiceOff(uint8_t voice) {
ic29.voices[voice].off(); ic29.voices[voice].off();
} }
void Synth::basePitch() { LFO::LFO() {
uint16_t pitch = 0x1818; lfoOut = 0;
phase = 0;
pitch += lfoPitch; // phase is where we are in the LFO delay cycle
pitch += bendPitch; // the delay envelope sets the depth of pitch and VCF modulation
// tuning too but that's zero by default; // 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() { Envelope::Envelope() {
@ -76,8 +100,7 @@ Envelope::Envelope() {
} }
void Envelope::run() { void Envelope::run() {
uint16_t tempStn = ic29.envStn << 7;
uint16_t tempStn = ic29.envStn << 6;
switch (phase) { switch (phase) {
case ENV_ATK: case ENV_ATK:
level += atkTable[ic29.envAtk]; level += atkTable[ic29.envAtk];
@ -88,9 +111,7 @@ void Envelope::run() {
break; break;
case ENV_DCY: case ENV_DCY:
if (level > tempStn) { 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 { } else {
level = tempStn; level = tempStn;
} }
@ -105,19 +126,26 @@ void Envelope::run() {
} }
Voice::Voice() { Voice::Voice() {
subosc = 1; subosc = .11;
} }
void Voice::calcPitch() { void Voice::calcPitch() {
uint16_t target = note << 8; 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) { if (ic29.portaCoeff != 0) {
// porta up // portamento up
if (pitch < target) { if (pitch < target) {
pitch += ic29.portaCoeff; pitch += ic29.portaCoeff;
if (pitch > target) pitch = target; if (pitch > target) pitch = target;
} }
// porta down // portamento down
if (pitch > target) { if (pitch > target) {
pitch -= ic29.portaCoeff; pitch -= ic29.portaCoeff;
if (pitch < target) pitch = target; if (pitch < target) pitch = target;
@ -126,17 +154,17 @@ void Voice::calcPitch() {
pitch = target; pitch = target;
} }
pitch += 0x1818; //ic29.masterPitch; pitch += ic29.masterPitch;
if (pitch < 0x3000) pitch = 0x3000; // lowest note if (pitch < 0x3000) pitch = 0x3000; // lowest note
if (pitch > 0x9700) pitch = 0x6700; // highest note if (pitch > 0x9700) pitch = 0x6700; // highest note
pitch -= 0x3000; pitch -= 0x3000;
//pitch &= 0xff00;
// interpolate between the two table values
double o1 = ic29.pitchTable[pitch >> 8]; double o1 = ic29.pitchTable[pitch >> 8];
double o2 = ic29.pitchTable[(pitch >> 8) + 1]; double o2 = ic29.pitchTable[(pitch >> 8) + 1];
double frac = (pitch & 0xff) / 255.0; double frac = (pitch & 0xff) / 256.0f;
omega = ((o2 - o1) * frac) + o1; omega = ((o2 - o1) * frac) + o1;
} }
@ -155,7 +183,7 @@ void Voice::on(uint8_t key) {
} }
void Voice::off() { void Voice::off() {
// I need to rethink this bit FIXME // sustain - I need to rethink this bit FIXME
voiceState = V_OFF; voiceState = V_OFF;
if (!ic29.sustained) { if (!ic29.sustained) {
env.off(); env.off();

View File

@ -20,6 +20,24 @@
#include "peacock.hpp" #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 { class Envelope {
public: public:
Envelope(); Envelope();
@ -99,9 +117,8 @@ class Synth {
int16_t lfoPitch; int16_t lfoPitch;
int16_t bendPitch; int16_t bendPitch;
Voice voices[NUM_VOICES]; Voice voices[NUM_VOICES];
LFO lfo;
void runLfo();
void basePitch();
}; };
// global // global

View File

@ -67,7 +67,7 @@ extern const uint8_t lfoDepthTable[128] = {
0xb0, 0xb4, 0xb8, 0xbc, 0xc0, 0xc4, 0xc8, 0xcc, 0xd0, 0xd4, 0xd8, 0xdc, 0xb0, 0xb4, 0xb8, 0xbc, 0xc0, 0xc4, 0xc8, 0xcc, 0xd0, 0xd4, 0xd8, 0xdc,
0xe0, 0xe4, 0xe8, 0xec, 0xf0, 0xf8, 0xff, 0xff}; 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, 0x0005, 0x000f, 0x0019, 0x0028, 0x0037, 0x0046, 0x0050, 0x005a, 0x0064,
0x006e, 0x0078, 0x0082, 0x008c, 0x0096, 0x00a0, 0x00aa, 0x00b4, 0x00be, 0x006e, 0x0078, 0x0082, 0x008c, 0x0096, 0x00a0, 0x00aa, 0x00b4, 0x00be,
0x00c8, 0x00d2, 0x00dc, 0x00e6, 0x00f0, 0x00fa, 0x0104, 0x010e, 0x0118, 0x00c8, 0x00d2, 0x00dc, 0x00e6, 0x00f0, 0x00fa, 0x0104, 0x010e, 0x0118,

View File

@ -30,7 +30,10 @@ static inline float poly3blep1(float t) {
void Voice::run(float *buffer, uint32_t samples) { void Voice::run(float *buffer, uint32_t samples) {
// generate a full block of samples for the oscillator // 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; float gain = env.level / 16384.0;
@ -52,15 +55,15 @@ void Voice::run(float *buffer, uint32_t samples) {
if (pulseStage) { if (pulseStage) {
if (phase < 1) break; // it's not time to reset the saw if (phase < 1) break; // it's not time to reset the saw
t = (phase - 1) / omega; t = (phase - 1) / omega;
y += poly3blep0(t) * (0.8 + 0.63 - subosc); y += poly3blep0(t) * (0.8 * saw + 0.63 - subosc);
delay += poly3blep1(t) * (0.8 + 0.63 - subosc); delay += poly3blep1(t) * (0.8 * saw + 0.63 - subosc);
pulseStage = 0; pulseStage = 0;
phase -= 1; phase -= 1;
subosc = -subosc; 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 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 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! // the signal at about 10Hz or so, preventing any PWM rumble from leaking through!