Compare commits
8 Commits
d947662acc
...
cb45876e3e
Author | SHA1 | Date | |
---|---|---|---|
|
cb45876e3e | ||
|
bc9a55cbeb | ||
|
dbe8c50296 | ||
|
a236214c6d | ||
|
83c4cba47c | ||
|
3c09c94786 | ||
|
02b8622bf6 | ||
|
bce9ecc2d0 |
84
README.md
84
README.md
@ -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
|
Building BarrVerb
|
||||||
=================
|
-----------------
|
||||||
|
|
||||||
1. clone the repository
|
1. clone the repository
|
||||||
|
|
||||||
@ -7,3 +78,14 @@ Building BarrVerb
|
|||||||
|
|
||||||
3. `make`
|
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.
|
||||||
|
|
||||||
|
@ -17,12 +17,60 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "barrverb.hpp"
|
#include "barrverb.hpp"
|
||||||
|
#include "rom.h"
|
||||||
|
|
||||||
START_NAMESPACE_DISTRHO
|
START_NAMESPACE_DISTRHO
|
||||||
|
|
||||||
BarrVerb::BarrVerb() : Plugin(kParameterCount, 1, 0) { // two parameters, one program, no states
|
BarrVerb::BarrVerb() : Plugin(kParameterCount, 64, 0) { // two parameters, one program, no states
|
||||||
// dummy
|
lowpass = new float[getBufferSize()];
|
||||||
deactivate();
|
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
|
// Initialisation functions
|
||||||
@ -33,23 +81,101 @@ void BarrVerb::initAudioPort(bool input, uint32_t index, AudioPort &port) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void BarrVerb::initProgramName(uint32_t index, String &programName) {
|
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
|
// Processing functions
|
||||||
|
|
||||||
void BarrVerb::activate() {
|
void BarrVerb::activate() {
|
||||||
// calculate filter coefficients
|
// calculate filter coefficients
|
||||||
|
printf("called activate()\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
void BarrVerb::deactivate() {
|
void BarrVerb::deactivate() {
|
||||||
// zero out the outputs, maybe
|
// zero out the outputs, maybe
|
||||||
|
printf("called deactivate()\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
void BarrVerb::run(const float **inputs, float **outputs, uint32_t frames) {
|
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
|
// create the plugin
|
||||||
Plugin *createPlugin() { return new BarrVerb(); }
|
Plugin *createPlugin() { return new BarrVerb(); }
|
||||||
END_NAMESPACE_DISTRHO
|
|
||||||
|
|
||||||
|
END_NAMESPACE_DISTRHO
|
||||||
|
@ -45,6 +45,7 @@ class BarrVerb : public Plugin {
|
|||||||
// Initialisation
|
// Initialisation
|
||||||
void initAudioPort(bool input, uint32_t index, AudioPort &port) override;
|
void initAudioPort(bool input, uint32_t index, AudioPort &port) override;
|
||||||
void initProgramName(uint32_t index, String &programName) override;
|
void initProgramName(uint32_t index, String &programName) override;
|
||||||
|
void loadProgram(uint32_t index) override;
|
||||||
|
|
||||||
// Processing
|
// Processing
|
||||||
void activate() override;
|
void activate() override;
|
||||||
@ -52,7 +53,16 @@ class BarrVerb : public Plugin {
|
|||||||
void run(const float **inputs, float **outputs, uint32_t frames) override;
|
void run(const float **inputs, float **outputs, uint32_t frames) override;
|
||||||
|
|
||||||
private:
|
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);
|
DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(BarrVerb);
|
||||||
};
|
};
|
||||||
|
1121
plugin/rom.h
Normal file
1121
plugin/rom.h
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user