Compare commits

...

8 Commits

Author SHA1 Message Date
ErroneousBosh
cb45876e3e clean up 2024-08-02 19:16:52 +01:00
Gordon JC Pearce
bc9a55cbeb Filled out README a bit 2024-08-02 10:22:11 +01:00
Gordon JC Pearce
dbe8c50296 Can select programs 2024-07-30 19:35:38 +01:00
Gordon JC Pearce
a236214c6d program names for midiverb 2024-07-30 19:03:05 +01:00
Gordon JC Pearce
83c4cba47c very crude downsampling 2024-07-29 22:09:09 +01:00
Gordon JC Pearce
3c09c94786 Runs but at wrong speed, very high DSP load 2024-07-29 21:56:06 +01:00
Gordon JC Pearce
02b8622bf6 Label filter computations correctly 2024-07-28 22:00:07 +01:00
Gordon JC Pearce
bce9ecc2d0 Various filters 2024-07-28 01:40:15 +01:00
4 changed files with 1351 additions and 12 deletions

View File

@ -1,5 +1,76 @@
BarrVerb - a DPF Reverb plugin
==============================
BarrVerb is a reverb plugin based on the legendary Alesis MIDIVerb from the
mid-1980s. It implements an emulation of Keith Barr's custom DSP, originally
implemented in LS-family logic ICs. It could also run the MIDIFex DSP code,
if a suitable ROM image could be found and "deinterleaved" to suit BarrVerb's
decoder.
I have to acknowledge the work of /u/thement_ on Reddit, who nerd-sniped me
into doing this in the first place, Eric Brombaugh on the synth-diy mailing
list who reverse-engineered the MIDIVerb, and Paul Schreiber who made a
a couple of videos of Eric's reverse-engineering of it from which I was able
to get the circuit diagram and a bit of explanation of the opcodes.
Some liberties were taken with the arithmetic in the unit in regards to how
it handles twos'-complement arithmetic. Eric Brombaugh explained that it
appears to have an off-by-one error that gets corrected over two instructions
but to simplify the process loop I just use normal arithmetic. This can cause
an error of up to +/- 2 DAC values in the output, which in practice is
audibly indistinguishable from doing it "100% accurately". On test it can be
measured by compiling the plugin with "accurate" and with "simple" maths,
subtracting the output of one from the other, and boosting the gain by around
50dB or so - you're never hearing that difference in practice.
A brief technical guide
-----------------------
The Alesis MIDIVerb has a simple DSP made out of discrete logic which can
carry out four operations. Unlike a general-purpose DSP it can only multiply
by 0.5, which is implemented by shifting one input to the adder right one place
and feeding the leftmost bit to both bit 15 and bit 14 to extend the sign. Bit
15 is also fed to the carry input of the adder chain, for the twos'-complement
add.
The "program counter" is an 8-bit counter clocked at 6MHz, which feeds the
lower 8 bits of the DSP EPROM address. This is latched into the DSP low byte
first. Each instruction consists of a two-bit opcode and 14-bit offset, which
is added to the memory pointer on each step to address 16-bit words within the
16kB DSP RAM. The instruction steps are latched in stages so that for a given
counter position, the offset comes from the instruction before, and the opcode
comes from the opcode before that, presumably to allow time for the latches to
settle. In this implementation, the ROM has been pre-"prefetched" so that the
value in `rom.h` at word 0 of any program actually contains the opcode from
word 126 and the offset from word 127 and so on, which simplifies the DSP loop.
There are no branches possible so unused code must be filled with a "dummy"
write to RAM. Three of the addresses are "magic" - at step 0 the ADC is loaded
into RAM at the address in the pointer register, at step 96 (60 hex) the
right channel output DAC is loaded with the "adder input" bus, and at step
112 (70 hex) the left channel output DAC is loaded. For each of these "magic"
addresses the ALU works as normal but the accumulator register is not loaded
with the output of the adder. In general the DSP code used seems to run an
instruction to load the adder input bus with the contents of RAM, pointing to
a "temporary" address where the effect outputs are stored.
Known limitations
-----------------
This is the first thing I've written from scratch using DPF, and as such
may not actually be very good.
This plugin will work at any sample rate but will only produce approximately
correct results at 48kHz because it does not downsample and upsample very
well. The input filter does not have the right response and there is no
reconstruction filter on the output, which you are unlikely to notice in use.
The DSP engine runs at 24kHz (or really, half the sample rate) rather than
the correct 6MHz/256 = 23.4375kHz, which you are unlikely to notice in use.
Building BarrVerb
=================
-----------------
1. clone the repository
@ -7,3 +78,14 @@ Building BarrVerb
3. `make`
You should now have a `./bin/` directory with `BarrVerb` as a standalone
Jack client, `BarrVerb.lv2` as an LV2 plugin, `BarrVerb.vst3` as a VST3
plugin, and `BarrVerb-vst.so` as a VST2 plugin. These have been tested on
Linux using Carla 2.4.2, but very little else. Further testing and patches
would be welcome.
This software is provided under the ISC licence as documented in the file
LICENCE which is fairly permissive. The file `rom.h` contains a permuted
version of the MIDIVerb ROM which has already been shared and distributed
widely, but must be considered to be copyrighted by the late Keith Barr.

View File

@ -17,12 +17,60 @@
*/
#include "barrverb.hpp"
#include "rom.h"
START_NAMESPACE_DISTRHO
BarrVerb::BarrVerb() : Plugin(kParameterCount, 1, 0) { // two parameters, one program, no states
// dummy
deactivate();
BarrVerb::BarrVerb() : Plugin(kParameterCount, 64, 0) { // two parameters, one program, no states
lowpass = new float[getBufferSize()];
ram = new int16_t[16384];
/*
// calculate SVF params
// hardcoded values for now
float fc = 5019;
float F = fc / 48000; // assume 48kHz
float w = 2 * tan(3.14159 * F);
float a = w / 0.7845; // 1dB Chebyshev, 2-pole
float b = w * w;
// "corrected" SVF params, per Fons Adriaensen
c1_1 = (a + b) / (1 + a / 2 + b / 4);
c2_1 = b / (a + b);
d0_1 = c1_1 * c2_1 / 4;
fc = 9433;
F = fc / 48000; // assume 48kHz
w = 2 * tan(3.14159 * F);
a = w / 3.5594; // 1dB Chebyshev, 2-pole
b = w * w;
c1_2 = (a + b) / (1 + a / 2 + b / 4);
c2_2 = b / (a + b);
d0_2 = c1_2 * c2_2 / 4;*/
// calculate SVF params
// hardcoded values for now
float fc = 10000;
float F = fc / 48000; // assume 48kHz
float w = 2 * tan(3.14159 * F);
float a = w / 0.5412; // Butterworth 4-pole first stage
float b = w * w;
// "corrected" SVF params, per Fons Adriaensen
c1_1 = (a + b) / (1 + a / 2 + b / 4);
c2_1 = b / (a + b);
d0_1 = c1_1 * c2_1 / 4;
fc = 10000;
F = fc / 48000; // assume 48kHz
w = 2 * tan(3.14159 * F);
a = w / 1.3065; // Butterworth 4-pole second stage
b = w * w;
c1_2 = (a + b) / (1 + a / 2 + b / 4);
c2_2 = b / (a + b);
d0_2 = c1_2 * c2_2 / 4;
}
// Initialisation functions
@ -33,23 +81,101 @@ void BarrVerb::initAudioPort(bool input, uint32_t index, AudioPort &port) {
}
void BarrVerb::initProgramName(uint32_t index, String &programName) {
programName="Default Reverb";
programName = "init program"; //&prog_name[index & 0x3f];
programName = prog_name[index & 0x3f].c_str();
}
void BarrVerb::loadProgram(uint32_t index) {
prog_offset = (index & 0x3f) << 7;
}
// Processing functions
void BarrVerb::activate() {
// calculate filter coefficients
printf("called activate()\n");
}
void BarrVerb::deactivate() {
// zero out the outputs, maybe
printf("called deactivate()\n");
}
void BarrVerb::run(const float **inputs, float **outputs, uint32_t frames) {
// actual effects here
float x;
uint16_t opcode;
for (uint32_t i = 0; i < frames; i++) {
// smash to mono
lowpass[i] = (inputs[0][i] + inputs[1][i]) / 2;
// 10kHz lowpass filter, 2x oversampling
x = lowpass[i] - in_z1 - in_z2;
in_z2 += c2_1 * in_z1;
in_z1 += c1_1 * x;
x = (d0_1 * x + in_z2) - in_z12 - in_z22;
in_z22 += c2_2 * in_z12;
in_z12 += c1_2 * x;
lowpass[i] = d0_2 * x + in_z22;
}
// now run the DSP
for (uint32_t i=0; i < frames; i+=2) {
// run the actual DSP engine for each sample
for (uint8_t step = 0; step < 128; step++) {
opcode = rom[prog_offset + step];
switch (opcode & 0xc000) {
case 0x0000:
ai = ram[ptr];
li = acc + (ai >> 1);
break;
case 0x4000:
ai = ram[ptr];
li = (ai >> 1);
break;
case 0x8000:
ai = acc;
ram[ptr] = ai;
li = acc + (ai >> 1);
break;
case 0xc000:
ai = acc;
ram[ptr] = -ai;
li = -(ai >> 1);
break;
}
if (step == 0x00) {
// load RAM from ADC
ram[ptr] = (int)(lowpass[i] * 4096);
} else if (step == 0x60) {
// output right channel
outputs[1][i] = (float)ai / 4096;
outputs[1][i+1] = (float)ai / 4096;
} else if (step == 0x70) {
// output left channel
outputs[0][i] = (float)ai / 4096;
outputs[0][i+1] = (float)ai / 4096;
} else {
// everything else
// ADC and DAC operations don't affect the accumulator
// every other step ends with the accumulator latched from the Latch Input reg
acc = li;
}
// 16kW of RAM
ptr += opcode & 0x3fff;
ptr &= 0x3fff;
}
}
}
// create the plugin
Plugin* createPlugin() { return new BarrVerb(); }
END_NAMESPACE_DISTRHO
Plugin *createPlugin() { return new BarrVerb(); }
END_NAMESPACE_DISTRHO

View File

@ -45,6 +45,7 @@ class BarrVerb : public Plugin {
// Initialisation
void initAudioPort(bool input, uint32_t index, AudioPort &port) override;
void initProgramName(uint32_t index, String &programName) override;
void loadProgram(uint32_t index) override;
// Processing
void activate() override;
@ -52,7 +53,16 @@ class BarrVerb : public Plugin {
void run(const float **inputs, float **outputs, uint32_t frames) override;
private:
float c1, c2, d0, in_z1, in_z2, out_z1, out_z2;
float c1_1, c2_1, d0_1, c1_2, c2_2, d0_2, in_z1, in_z2, in_z12,in_z22, out_z1, out_z2;
int16_t ai, li, acc;
uint16_t ptr;
uint16_t prog_offset;
int16_t *ram;
float *lowpass;
DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(BarrVerb);
};

1121
plugin/rom.h Normal file

File diff suppressed because it is too large Load Diff