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 "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) {
@ -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();

View File

@ -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

View File

@ -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,

View File

@ -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!