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.
|
|
|
|
*/
|
|
|
|
|
2024-10-19 22:11:28 +00:00
|
|
|
#include "voiceboard.hpp"
|
2024-10-16 20:41:08 +00:00
|
|
|
|
2024-10-20 00:08:50 +00:00
|
|
|
#include <math.h>
|
|
|
|
|
|
|
|
#include "DistrhoPlugin.hpp"
|
2024-10-15 23:10:25 +00:00
|
|
|
#include "ic29tables.hpp"
|
2024-10-09 21:43:52 +00:00
|
|
|
|
2024-10-20 00:08:50 +00:00
|
|
|
#define DEBUG
|
2024-10-12 22:36:34 +00:00
|
|
|
|
2024-10-11 23:15:00 +00:00
|
|
|
Synth::Synth() {
|
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-19 18:49:24 +00:00
|
|
|
|
|
|
|
// precompute a table of values to map the filter value to a filter coefficient
|
|
|
|
// the ROM preset for adjusting the VCF scale and centre presets cutoff to $31
|
|
|
|
// and key scale to $7f, which corresponds to C4 = 248Hz and C6 = 992Hz, B3 and B5
|
|
|
|
|
2024-10-20 00:08:50 +00:00
|
|
|
for (uint16_t i = 0; i < 256; i++) {
|
2024-10-19 18:49:24 +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-20 00:08:50 +00:00
|
|
|
lfo.run();
|
2024-10-16 20:41:08 +00:00
|
|
|
|
|
|
|
masterPitch = 0x1818;
|
2024-10-20 00:08:50 +00:00
|
|
|
uint16_t vcoLfoDepth = lfoDepthTable[patchRam.vcoLfoMod] + modWheel;
|
2024-10-18 22:57:14 +00:00
|
|
|
vcoLfoDepth = (vcoLfoDepth < 0xff) ? vcoLfoDepth : 0xff;
|
|
|
|
masterPitch += (lfo.lfoOut * vcoLfoDepth) >> 11;
|
|
|
|
|
|
|
|
if (pitchBend < 0x0100) pitchBend = 0x0100;
|
|
|
|
masterPitch += (((pitchBend >> 8) - 0x20) * vcoBend) / 1.281;
|
2024-10-16 20:41:08 +00:00
|
|
|
|
2024-10-16 23:01:46 +00:00
|
|
|
// need to calculate VCF "base" setting
|
|
|
|
// various on/off switches
|
|
|
|
|
|
|
|
// PWM is bit 0 sw2, 0 = fixed 1 = lfo
|
|
|
|
// 0 sets EA to 0x3fff, 1 adds
|
2024-10-20 00:08:50 +00:00
|
|
|
uint16_t pwmVal = 0x2000 - lfo.lfoOut;
|
|
|
|
if (patchRam.switch2 & 0x01) pwmVal = 0x3fff;
|
|
|
|
pwm = 0.5 - pwmVal / 32768.0f * (patchRam.pwmLfoMod / 106.0f);
|
2024-10-16 23:01:46 +00:00
|
|
|
|
2024-10-18 23:19:35 +00:00
|
|
|
// generate the voices, then
|
2024-10-20 00:08:50 +00:00
|
|
|
for (uint32_t i = 0; i < bufferSize; i++) {
|
|
|
|
tr21 = (tr21 * 519) + 3;
|
2024-10-20 21:12:22 +00:00
|
|
|
// noise[i] = (1 - (tr21 & 0x00ffffff) / 8388608.0f) * (patchRam.noiseLevel * 0.0063);
|
2024-10-18 23:19:35 +00:00
|
|
|
}
|
|
|
|
|
2024-12-31 15:13:28 +00:00
|
|
|
// printf("updating\n");
|
2024-10-20 21:12:22 +00:00
|
|
|
|
2024-10-13 20:07:01 +00:00
|
|
|
for (uint8_t i = 0; i < NUM_VOICES; i++) {
|
2024-12-31 15:13:28 +00:00
|
|
|
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-20 00:08:50 +00:00
|
|
|
|
2024-10-20 21:12:22 +00:00
|
|
|
d_debug("voiceOn %02x %02x", voice, note);
|
2024-10-20 00:08:50 +00:00
|
|
|
|
|
|
|
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-20 00:08:50 +00:00
|
|
|
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-17 23:56:37 +00:00
|
|
|
lfoOut += phase ? lfoRateTable[rate] : -lfoRateTable[rate];
|
2024-10-16 20:41:08 +00:00
|
|
|
if (lfoOut > 0x1fff) {
|
|
|
|
lfoOut = 0x1fff;
|
|
|
|
phase = 0;
|
|
|
|
}
|
|
|
|
if (lfoOut < -0x1fff) {
|
|
|
|
lfoOut = -0x1fff;
|
|
|
|
phase = 1;
|
|
|
|
}
|
|
|
|
|
2024-10-16 23:01:46 +00:00
|
|
|
// printf("lfoOut=%04x\n", lfoOut);
|
2024-10-09 21:51:37 +00:00
|
|
|
}
|
2024-10-11 23:15:00 +00:00
|
|
|
|
2024-10-20 21:12:22 +00:00
|
|
|
uint16_t Envelope::atk = 0x10;
|
|
|
|
uint16_t Envelope::dcy = 0x3f;
|
|
|
|
uint16_t Envelope::stn = 0x3f;
|
|
|
|
uint16_t Envelope::rls = 0x1f;
|
|
|
|
|
2024-10-11 23:15:00 +00:00
|
|
|
Envelope::Envelope() {
|
|
|
|
level = 0;
|
2024-10-17 23:56:37 +00:00
|
|
|
phase = ENV_RLS;
|
2024-10-11 23:15:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Envelope::run() {
|
2024-10-20 21:12:22 +00:00
|
|
|
uint16_t temp = stn << 7;
|
2024-10-11 23:15:00 +00:00
|
|
|
switch (phase) {
|
|
|
|
case ENV_ATK:
|
2024-10-20 21:12:22 +00:00
|
|
|
level += atkTable[atk];
|
2024-10-11 23:15:00 +00:00
|
|
|
if (level > 0x3fff) {
|
|
|
|
level = 0x3fff;
|
|
|
|
phase = ENV_DCY;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case ENV_DCY:
|
2024-10-20 21:12:22 +00:00
|
|
|
if (level > temp) {
|
|
|
|
level = (((level - temp) * dcyTable[dcy]) >> 16) + temp;
|
2024-10-11 23:15:00 +00:00
|
|
|
} else {
|
2024-10-20 21:12:22 +00:00
|
|
|
level = temp;
|
2024-10-11 23:15:00 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case ENV_RLS:
|
2024-10-20 21:12:22 +00:00
|
|
|
level = (level * dcyTable[rls]) >> 16;
|
2024-10-11 23:15:00 +00:00
|
|
|
break;
|
|
|
|
case ENV_IDLE:
|
|
|
|
default:
|
|
|
|
break;
|
2024-10-20 21:12:22 +00:00
|
|
|
}
|
2024-10-11 23:15:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Voice::Voice() {
|
|
|
|
}
|
2024-10-12 22:36:34 +00:00
|
|
|
|
2024-10-13 22:27:54 +00:00
|
|
|
void Voice::calcPitch() {
|
2024-12-31 15:13:28 +00:00
|
|
|
uint16_t ea;
|
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-12-31 15:13:28 +00:00
|
|
|
|
|
|
|
if (ic29.portaCoeff != 0) {
|
|
|
|
// portamento up
|
|
|
|
if (pitch < target) {
|
|
|
|
pitch += ic29.portaCoeff;
|
|
|
|
if (pitch > target) pitch = target;
|
|
|
|
}
|
|
|
|
// portamento down
|
|
|
|
if (pitch > target) {
|
|
|
|
pitch -= ic29.portaCoeff;
|
|
|
|
if (pitch < target) pitch = target;
|
2024-10-13 22:27:54 +00:00
|
|
|
}
|
2024-12-31 15:13:28 +00:00
|
|
|
} else {
|
|
|
|
pitch = target;
|
|
|
|
}
|
2024-10-13 22:27:54 +00:00
|
|
|
|
2024-12-31 15:13:28 +00:00
|
|
|
ea = pitch + ic29.masterPitch;
|
2024-10-15 21:10:57 +00:00
|
|
|
|
2024-12-31 15:13:28 +00:00
|
|
|
if (ea < 0x3000) ea = 0x3000; // lowest note
|
|
|
|
if (ea > 0x9700) ea = 0x6700; // highest note
|
2024-10-15 21:10:57 +00:00
|
|
|
|
2024-12-31 15:13:28 +00:00
|
|
|
ea -= 0x3000;
|
2024-10-15 21:10:57 +00:00
|
|
|
|
2024-12-31 15:13:28 +00:00
|
|
|
// interpolate between the two table values
|
|
|
|
double o1 = ic29.pitchTable[ea >> 8];
|
|
|
|
double o2 = ic29.pitchTable[(ea >> 8) + 1];
|
|
|
|
double frac = (ea & 0xff) / 256.0f;
|
2024-10-13 22:27:54 +00:00
|
|
|
|
2024-12-31 15:13:28 +00:00
|
|
|
omega = ((o2 - o1) * frac) + o1;
|
2024-10-20 21:12:22 +00:00
|
|
|
}
|
2024-10-17 22:57:28 +00:00
|
|
|
|
2024-10-20 21:12:22 +00:00
|
|
|
void Voice::update() {
|
|
|
|
// calculate the once-per-block values
|
|
|
|
env.run();
|
|
|
|
calcPitch();
|
2024-12-31 15:13:28 +00:00
|
|
|
// printf("env=%04x\n", env.level);
|
2024-10-20 21:12:22 +00:00
|
|
|
/*
|
|
|
|
pw = (patchRam.switch1 & 0x08) ? ic29.pwm : 0.0f;
|
|
|
|
saw = (ic29.patchRam.switch1 & 0x10) ? 1.0f : 0.0f;
|
|
|
|
sub = ic29.patchRam.subLevel / 128.0f;
|
2024-10-17 23:56:37 +00:00
|
|
|
|
2024-10-20 21:12:22 +00:00
|
|
|
ic29.lfo.rate = ic29.patchRam.lfoRate;
|
|
|
|
*/
|
|
|
|
// do filter values
|
2024-10-15 21:10:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Voice::on(uint8_t key) {
|
2024-10-20 00:08:50 +00:00
|
|
|
d_debug("Voice on key %02x\n", key);
|
2024-10-15 21:10:57 +00:00
|
|
|
voiceState = V_ON;
|
2024-10-17 23:56:37 +00:00
|
|
|
if (note != key) {
|
|
|
|
phase = 0;
|
|
|
|
}
|
2024-10-15 21:10:57 +00:00
|
|
|
note = key;
|
2024-10-20 21:12:22 +00:00
|
|
|
if (env.inRelease()) {
|
2024-10-17 23:56:37 +00:00
|
|
|
env.on(); // FIXME move to synth update code
|
|
|
|
}
|
2024-10-15 21:10:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Voice::off() {
|
2024-10-20 00:08:50 +00:00
|
|
|
d_debug("Voice off\n");
|
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;
|
2024-10-20 00:08:50 +00:00
|
|
|
env.off();
|
2024-10-15 21:10:57 +00:00
|
|
|
}
|