2024-10-09 21:43:52 +00:00
|
|
|
/*
|
|
|
|
Peacock-8 VA polysynth
|
|
|
|
|
|
|
|
Copyright 2024 Gordon JC Pearce <gordonjcp@gjcp.net>
|
|
|
|
|
|
|
|
Permission to use, copy, modify, and/or distribute this software for any
|
|
|
|
purpose with or without fee is hereby granted, provided that the above
|
|
|
|
copyright notice and this permission notice appear in all copies.
|
|
|
|
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
|
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
|
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
|
|
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
|
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
|
|
|
OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
|
|
|
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "ic29.hpp"
|
2024-10-16 20:41:08 +00:00
|
|
|
|
2024-10-15 23:10:25 +00:00
|
|
|
#include "ic29tables.hpp"
|
2024-10-09 21:43:52 +00:00
|
|
|
|
2024-10-13 22:27:54 +00:00
|
|
|
Synth ic29;
|
2024-10-12 22:36:34 +00:00
|
|
|
|
2024-10-11 23:15:00 +00:00
|
|
|
Synth::Synth() {
|
2024-10-13 20:07:01 +00:00
|
|
|
d_debug("initialising synth\n");
|
2024-10-16 20:41:08 +00:00
|
|
|
envAtk = 0x00;
|
2024-10-16 13:15:38 +00:00
|
|
|
envDcy = 0x50;
|
2024-10-16 20:41:08 +00:00
|
|
|
envStn = 0x7f;
|
2024-10-16 13:15:38 +00:00
|
|
|
envRls = 0x3f;
|
2024-10-13 22:27:54 +00:00
|
|
|
portaCoeff = 0x0;
|
2024-10-16 20:41:08 +00:00
|
|
|
lfo.speed = 0x1f;
|
2024-10-13 22:27:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Synth::buildTables(double sampleRate) {
|
|
|
|
for (uint8_t i = 0; i < 104; i++) {
|
2024-10-15 21:10:57 +00:00
|
|
|
// 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
|
2024-10-16 20:41:08 +00:00
|
|
|
pitchTable[i] = 260.15f * powf(2, (i - 36) / 12.0f) / sampleRate;
|
2024-10-13 22:27:54 +00:00
|
|
|
}
|
2024-10-11 23:15:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Synth::run() {
|
2024-10-13 20:07:01 +00:00
|
|
|
// handle a "loop" worth of envelopes, pitch calculations, etc
|
|
|
|
// callled once every 4.3ms block of samples
|
2024-10-12 22:36:34 +00:00
|
|
|
|
2024-10-16 20:41:08 +00:00
|
|
|
ic29.lfo.run();
|
|
|
|
|
|
|
|
masterPitch = 0x1818;
|
|
|
|
|
2024-10-13 20:07:01 +00:00
|
|
|
for (uint8_t i = 0; i < NUM_VOICES; i++) {
|
2024-10-15 21:10:57 +00:00
|
|
|
ic29.voices[i].update();
|
2024-10-12 22:36:34 +00:00
|
|
|
}
|
2024-10-11 23:15:00 +00:00
|
|
|
}
|
|
|
|
|
2024-10-09 21:51:37 +00:00
|
|
|
void Synth::voiceOn(uint8_t voice, uint8_t note) {
|
|
|
|
// enable synth voice, start it all running
|
2024-10-12 22:36:34 +00:00
|
|
|
voice &= 0x7f;
|
2024-10-15 21:10:57 +00:00
|
|
|
ic29.voices[voice].on(note);
|
2024-10-12 22:36:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Synth::voiceOff(uint8_t voice) {
|
|
|
|
// enable synth voice, start it all running
|
|
|
|
voice &= 0x7f;
|
2024-10-15 21:10:57 +00:00
|
|
|
ic29.voices[voice].off();
|
2024-10-13 22:27:54 +00:00
|
|
|
}
|
2024-10-15 21:10:57 +00:00
|
|
|
|
2024-10-16 20:41:08 +00:00
|
|
|
LFO::LFO() {
|
|
|
|
lfoOut = 0;
|
|
|
|
phase = 0;
|
2024-10-13 22:27:54 +00:00
|
|
|
|
2024-10-16 20:41:08 +00:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
void LFO::run() {
|
|
|
|
// slightly different from the real synth code which does not use signed
|
|
|
|
// variables, since the CPU doesn't support them
|
2024-10-13 22:27:54 +00:00
|
|
|
|
2024-10-16 20:41:08 +00:00
|
|
|
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);
|
2024-10-09 21:51:37 +00:00
|
|
|
}
|
2024-10-11 23:15:00 +00:00
|
|
|
|
|
|
|
Envelope::Envelope() {
|
|
|
|
level = 0;
|
|
|
|
phase = ENV_IDLE;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Envelope::run() {
|
2024-10-16 20:41:08 +00:00
|
|
|
uint16_t tempStn = ic29.envStn << 7;
|
2024-10-11 23:15:00 +00:00
|
|
|
switch (phase) {
|
|
|
|
case ENV_ATK:
|
2024-10-16 13:15:38 +00:00
|
|
|
level += atkTable[ic29.envAtk];
|
2024-10-11 23:15:00 +00:00
|
|
|
if (level > 0x3fff) {
|
|
|
|
level = 0x3fff;
|
|
|
|
phase = ENV_DCY;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case ENV_DCY:
|
2024-10-16 13:15:38 +00:00
|
|
|
if (level > tempStn) {
|
2024-10-16 20:41:08 +00:00
|
|
|
level = (((level - tempStn) * dcyTable[ic29.envDcy]) >> 16) + tempStn;
|
2024-10-11 23:15:00 +00:00
|
|
|
} else {
|
2024-10-16 13:15:38 +00:00
|
|
|
level = tempStn;
|
2024-10-11 23:15:00 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case ENV_RLS:
|
2024-10-16 13:15:38 +00:00
|
|
|
level = (level * dcyTable[ic29.envRls]) >> 16;
|
2024-10-11 23:15:00 +00:00
|
|
|
break;
|
|
|
|
case ENV_IDLE:
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Voice::Voice() {
|
2024-10-16 20:41:08 +00:00
|
|
|
subosc = .11;
|
2024-10-11 23:15:00 +00:00
|
|
|
}
|
2024-10-12 22:36:34 +00:00
|
|
|
|
2024-10-13 22:27:54 +00:00
|
|
|
void Voice::calcPitch() {
|
2024-10-15 21:10:57 +00:00
|
|
|
uint16_t target = note << 8;
|
2024-10-13 22:27:54 +00:00
|
|
|
|
2024-10-16 20:41:08 +00:00
|
|
|
// 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.
|
|
|
|
|
2024-10-13 22:27:54 +00:00
|
|
|
if (ic29.portaCoeff != 0) {
|
2024-10-16 20:41:08 +00:00
|
|
|
// portamento up
|
2024-10-13 22:27:54 +00:00
|
|
|
if (pitch < target) {
|
|
|
|
pitch += ic29.portaCoeff;
|
|
|
|
if (pitch > target) pitch = target;
|
|
|
|
}
|
2024-10-16 20:41:08 +00:00
|
|
|
// portamento down
|
2024-10-13 22:27:54 +00:00
|
|
|
if (pitch > target) {
|
|
|
|
pitch -= ic29.portaCoeff;
|
|
|
|
if (pitch < target) pitch = target;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
pitch = target;
|
|
|
|
}
|
|
|
|
|
2024-10-16 20:41:08 +00:00
|
|
|
pitch += ic29.masterPitch;
|
2024-10-15 21:10:57 +00:00
|
|
|
|
|
|
|
if (pitch < 0x3000) pitch = 0x3000; // lowest note
|
|
|
|
if (pitch > 0x9700) pitch = 0x6700; // highest note
|
|
|
|
|
|
|
|
pitch -= 0x3000;
|
|
|
|
|
2024-10-16 20:41:08 +00:00
|
|
|
// interpolate between the two table values
|
2024-10-13 22:27:54 +00:00
|
|
|
double o1 = ic29.pitchTable[pitch >> 8];
|
|
|
|
double o2 = ic29.pitchTable[(pitch >> 8) + 1];
|
2024-10-16 20:41:08 +00:00
|
|
|
double frac = (pitch & 0xff) / 256.0f;
|
2024-10-13 22:27:54 +00:00
|
|
|
|
|
|
|
omega = ((o2 - o1) * frac) + o1;
|
|
|
|
}
|
|
|
|
|
2024-10-15 21:10:57 +00:00
|
|
|
void Voice::update() {
|
|
|
|
// calculate the once-per-block values
|
|
|
|
env.run();
|
|
|
|
calcPitch();
|
|
|
|
// do filter values
|
|
|
|
}
|
|
|
|
|
|
|
|
void Voice::on(uint8_t key) {
|
|
|
|
voiceState = V_ON;
|
|
|
|
note = key;
|
|
|
|
env.on();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Voice::off() {
|
2024-10-16 20:41:08 +00:00
|
|
|
// sustain - I need to rethink this bit FIXME
|
2024-10-15 21:10:57 +00:00
|
|
|
voiceState = V_OFF;
|
|
|
|
if (!ic29.sustained) {
|
|
|
|
env.off();
|
|
|
|
}
|
|
|
|
}
|