aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorPhil Burk <[email protected]>2023-04-10 11:12:50 -0700
committerGitHub <[email protected]>2023-04-10 11:12:50 -0700
commit90db5489c352bc038d6d22e336ac7eefac221ed7 (patch)
tree645dc5bfab661acff6f10921485c2752dc56ac4d /src
parenta46f8c93193fe8bb1eb7b93e55c85e6f46d5b108 (diff)
Add PlateReverb, RoomReverb and MultiTapDelay units (#115)
PlateReverb is a simulation of a metal plate based on all-pass delays. RoomReverb uses a MultiTapDelay for early reflections and a PlateReverb for diffusion. Add a DSP package with utility classes used to build unit generators. Add TuneReverb app with faders for experimenting and hearing reverb. Add unit tests for SimpleDelay. Co-authored-by: Phil Burk <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/main/java/com/jsyn/dsp/AllPassDelay.java37
-rw-r--r--src/main/java/com/jsyn/dsp/SimpleDelay.java71
-rw-r--r--src/main/java/com/jsyn/engine/SynthesisEngine.java7
-rw-r--r--src/main/java/com/jsyn/swing/PortControllerFactory.java7
-rw-r--r--src/main/java/com/jsyn/unitgen/MultiTapDelay.java85
-rw-r--r--src/main/java/com/jsyn/unitgen/Pan.java1
-rw-r--r--src/main/java/com/jsyn/unitgen/PlateReverb.java366
-rw-r--r--src/main/java/com/jsyn/unitgen/RoomReverb.java175
-rw-r--r--src/test/java/com/jsyn/dsp/TestSimpleDelay.java73
9 files changed, 817 insertions, 5 deletions
diff --git a/src/main/java/com/jsyn/dsp/AllPassDelay.java b/src/main/java/com/jsyn/dsp/AllPassDelay.java
new file mode 100644
index 0000000..4afea19
--- /dev/null
+++ b/src/main/java/com/jsyn/dsp/AllPassDelay.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 Phil Burk
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.dsp;
+
+public class AllPassDelay {
+ private float[] mBuffer;
+ private int mCursor;
+ private float mCoefficient = 0.65f;
+
+ public AllPassDelay(int length, float coefficient) {
+ mBuffer = new float[length];
+ mCoefficient = coefficient;
+ }
+
+ public float process(float input) {
+ float z = mBuffer[mCursor];
+ float x = input - (z * mCoefficient);
+ mBuffer[mCursor] = x;
+ mCursor++;
+ if (mCursor >= mBuffer.length) mCursor = 0;
+ return z + (x * mCoefficient);
+ }
+}
diff --git a/src/main/java/com/jsyn/dsp/SimpleDelay.java b/src/main/java/com/jsyn/dsp/SimpleDelay.java
new file mode 100644
index 0000000..b4818c1
--- /dev/null
+++ b/src/main/java/com/jsyn/dsp/SimpleDelay.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2022 Phil Burk
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.dsp;
+
+/**
+ * Delay line based on a circular buffer.
+ */
+public class SimpleDelay {
+ private float[] mBuffer;
+ private int mCursor;
+
+ public SimpleDelay(int length) {
+ mBuffer = new float[length];
+ }
+
+ /**
+ * Read a value from the delay line.
+ * @param position positive delay in frames
+ * @return delayed value
+ */
+ public float read(int position) {
+ int index = mCursor - position;
+ if (index < 0) {
+ index += mBuffer.length;
+ }
+ return mBuffer[index];
+ }
+
+ /**
+ * Write a new value to the head of the delay line.
+ * This does not advance the cursor.
+ * @param input sample value
+ */
+ public void write(float input) {
+ mBuffer[mCursor] = input;
+ }
+
+ /**
+ * Advance the cursor position. Wrap around in a circle.
+ */
+ public void advance() {
+ mCursor++;
+ if (mCursor >= mBuffer.length) mCursor = 0;
+ }
+
+ /**
+ * Add a new value and return the oldest value in the delay line.
+ * @param input sample value
+ * @return oldest value
+ */
+ public float process(float input) {
+ float output = mBuffer[mCursor];
+ write(input);
+ advance();
+ return output;
+ }
+}
diff --git a/src/main/java/com/jsyn/engine/SynthesisEngine.java b/src/main/java/com/jsyn/engine/SynthesisEngine.java
index 34fffbe..6d985b4 100644
--- a/src/main/java/com/jsyn/engine/SynthesisEngine.java
+++ b/src/main/java/com/jsyn/engine/SynthesisEngine.java
@@ -224,8 +224,11 @@ public class SynthesisEngine implements Synthesizer {
setupAudioBuffers(numInputChannels, numOutputChannels);
- logger.info("Pure Java JSyn from www.softsynth.com, rate = " + frameRate + ", "
- + (useRealTime ? "RT" : "NON-RealTime") + ", " + JSyn.VERSION_TEXT);
+ if (false) {
+ logger.info("Pure Java JSyn from www.softsynth.com, rate = " + frameRate
+ + ", " + (useRealTime ? "RT" : "NON-RealTime")
+ + ", " + JSyn.VERSION_TEXT);
+ }
inverseNyquist = 2.0 / frameRate;
diff --git a/src/main/java/com/jsyn/swing/PortControllerFactory.java b/src/main/java/com/jsyn/swing/PortControllerFactory.java
index a73d047..0f98ea6 100644
--- a/src/main/java/com/jsyn/swing/PortControllerFactory.java
+++ b/src/main/java/com/jsyn/swing/PortControllerFactory.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -19,11 +19,12 @@ package com.jsyn.swing;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
+import com.jsyn.ports.SettablePort;
import com.jsyn.ports.UnitInputPort;
/**
* Factory class for making various controllers for JSyn ports.
- *
+ *
* @author Phil Burk (C) 2010 Mobileer Inc
*/
public class PortControllerFactory {
diff --git a/src/main/java/com/jsyn/unitgen/MultiTapDelay.java b/src/main/java/com/jsyn/unitgen/MultiTapDelay.java
new file mode 100644
index 0000000..17db94a
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/MultiTapDelay.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2023 Phil Burk
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.dsp.SimpleDelay;
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Delay with multiple read positions and associated gains.
+ */
+public class MultiTapDelay extends UnitFilter {
+
+ /** Pre-delay time in milliseconds. */
+ public UnitInputPort preDelayMillis;
+ private final int mMaxPreDelayFrames;
+ private SimpleDelay mPreDelay;
+ private SimpleDelay mDelay;
+ private final int[] mPositions;
+ private final float[] mGains;
+
+ private int mPreDelayFrames = 0;
+
+ /**
+ * Construct a delay line with specified taps.
+ * The allocated size of the delay line will be the maximum position plus the maxPreDelayFrames.
+ * @param positions delay index, eg. 172 for Z(n-172)
+ * @param gains multiplier for the corresponding position
+ * @param maxPreDelayFrames extra allocated frames for pre-delay before the taps
+ */
+ public MultiTapDelay(final int[] positions,
+ final float[] gains,
+ final int maxPreDelayFrames) {
+ mPositions = positions;
+ mGains = gains;
+
+ preDelayMillis = new UnitInputPort("PreDelayMillis");
+ double maxMillis = maxPreDelayFrames * 1000.0 / 44100; // TODO handle unknown frame rate better
+ preDelayMillis.setup(0.0, Math.min(10.0, maxMillis), maxMillis);
+ addPort(preDelayMillis);
+ mMaxPreDelayFrames = Math.max(1, maxPreDelayFrames);
+ mPreDelay = new SimpleDelay(maxPreDelayFrames);
+
+ int maxPosition = 0;
+ for (int position : positions) {
+ maxPosition = Math.max(maxPosition, position);
+ }
+ mDelay = new SimpleDelay(maxPosition);
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] outputs = output.getValues();
+
+ double preDelayMS = preDelayMillis.getValues()[0];
+ int preDelayFrames = (int)(preDelayMS * 0.001 * getFrameRate());
+ preDelayFrames = Math.max(1, Math.min(mMaxPreDelayFrames, preDelayFrames));
+
+ for (int i = start; i < limit; i++) {
+ mPreDelay.write((float) inputs[i]);
+ mDelay.write(mPreDelay.read(preDelayFrames));
+ mPreDelay.advance();
+ double sum = 0.0;
+ for (int tap = 0; tap < mPositions.length; tap++) {
+ sum += mDelay.read(mPositions[tap]) * mGains[tap];
+ }
+ mDelay.advance();
+ outputs[i] = sum; // mix taps
+ }
+ }
+}
diff --git a/src/main/java/com/jsyn/unitgen/Pan.java b/src/main/java/com/jsyn/unitgen/Pan.java
index bc90984..77b2694 100644
--- a/src/main/java/com/jsyn/unitgen/Pan.java
+++ b/src/main/java/com/jsyn/unitgen/Pan.java
@@ -41,6 +41,7 @@ public class Pan extends UnitGenerator {
public Pan() {
addPort(input = new UnitInputPort("Input"));
addPort(pan = new UnitInputPort("Pan"));
+ pan.setup(-1.0, 0.0, 1.0);
addPort(output = new UnitOutputPort(2, "Output"));
}
diff --git a/src/main/java/com/jsyn/unitgen/PlateReverb.java b/src/main/java/com/jsyn/unitgen/PlateReverb.java
new file mode 100644
index 0000000..88eef33
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/PlateReverb.java
@@ -0,0 +1,366 @@
+/*
+ * Copyright 2022 Phil Burk
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.dsp.AllPassDelay;
+import com.jsyn.dsp.SimpleDelay;
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.util.PseudoRandom;
+
+/**
+ * Simple reverberation effect based on a "figure eight"
+ * network of all-pass filters and delays.
+ *
+ * This reverb does not have a pre-delay or early reflections.
+ * It can be used as the "tail" of a more complex reverb that
+ * adds those functions.
+ *
+ * The algorithm is based on
+ * "Effect Design Part 1: Reverberator and Other Filters"
+ * by Jon Dattorro, CCRMA, Stanford University 1996
+ *
+ * @see InterpolatingDelay
+ */
+
+public class PlateReverb extends UnitGenerator {
+
+ /**
+ * Mono input.
+ */
+ public UnitInputPort input;
+
+ /**
+ * Approximate time in seconds to decay by -60 dB.
+ */
+ public UnitInputPort time;
+
+ /**
+ * Damping factor for the feedback filters.
+ * Must be between 0.0 and 1.0. Default is 0.5.
+ */
+ public UnitInputPort damping;
+
+ /**
+ * Stereo output.
+ */
+ public UnitOutputPort output;
+
+ private static final double MAX_DECAY = 0.98;
+ // These default values are based on table-1 of the paper by Jon Dattorro.
+ private static final float DECAY_DIFFUSION_1 = 0.70f;
+ private static final float DECAY_DIFFUSION_2 = 0.50f;
+ private static final float INPUT_DIFFUSION_1 = 0.75f;
+ private static final float INPUT_DIFFUSION_2 = 0.625f;
+ private static final float DAMPING = 0.5f; // Must match default comment above for damping port.
+ private static final float BANDWIDTH = 0.99995f;
+
+ private static class FastSineOscillator {
+ private float mPhaseIncrement = 0.0001f;
+ private float mPhaseDelta = mPhaseIncrement;
+ private float mPhase; // ranges from -PI/2 to PI/2
+ private static final float PHASE_LIMIT = (float) Math.PI * 0.5f;
+
+ void setFrequency(float frequency, float sampleRate) {
+ mPhaseIncrement = (float) (frequency * Math.PI / sampleRate);
+ }
+
+ float generate() {
+ // Generate a triangle wave
+ mPhase += mPhaseDelta;
+ if (mPhase > PHASE_LIMIT) {
+ mPhase = PHASE_LIMIT - (mPhase - PHASE_LIMIT);
+ mPhaseDelta = -mPhaseIncrement; // reverse direction
+ } else if (mPhase < -PHASE_LIMIT) {
+ mPhase = -PHASE_LIMIT + (-PHASE_LIMIT - mPhase);
+ mPhaseDelta = mPhaseIncrement; // reverse direction
+ }
+
+ // Factorial constants so code is easier to read.
+ final float IF3 = 1.0f / (2 * 3);
+ final float IF5 = IF3 / (4 * 5);
+ final float IF7 = IF5 / (6 * 7);
+ final float IF9 = IF7 / (8 * 9);
+ final float IF11 = IF9 / (10 * 11);
+
+ float x = mPhase;
+ float x2 = (x * x);
+ /* Taylor expansion factored into multiply-adds */
+ // TODO use fewer factors cuz just modulation
+ return x
+ * (x2 * (x2 * (x2 * (x2 * ((x2 * (-IF11)) + IF9) - IF7) + IF5) - IF3) + 1);
+ }
+ }
+
+ private static class RandomModulator {
+ private PseudoRandom randomNum = new PseudoRandom();;
+ protected float prevNoise, currNoise;
+ private float mPhase;
+ private float mPhaseIncrement;
+
+ void setFrequency(float frequency, float sampleRate) {
+ mPhaseIncrement = frequency / sampleRate;
+ }
+
+ // Generate ramps between random points between -1.0 and +1.0.
+ public float generate() {
+ mPhase += mPhaseIncrement;
+
+ // calculate new random value whenever phase passes 1.0
+ if (mPhase > 1.0) {
+ prevNoise = currNoise;
+ currNoise = (float) randomNum.nextRandomDouble();
+ // reset phase for interpolation
+ mPhase -= 1.0;
+ }
+
+ // interpolate current
+ return prevNoise + (mPhase * (currNoise - prevNoise));
+ }
+ }
+
+ /**
+ * Allpass delay modulated by a random ramp.
+ */
+ private static class VariableAllPassDelay {
+ RandomModulator mModulator = new RandomModulator();
+ private float[] mBuffer;
+ private int mLength;
+ private int mCursor;
+ private int mModulationDepth;
+ private float mCoefficient = 0.65f;
+
+ VariableAllPassDelay(int length, float coefficient) {
+ mLength = length;
+ mBuffer = new float[2 * length];
+ mCoefficient = coefficient;
+ setModulationDepth(40);
+ }
+
+ void setModulationDepth(int depthInFrames) {
+ mModulationDepth = Math.min(depthInFrames, mLength / 3);
+ }
+
+ void setFrequency(float frequency, float sampleRate) {
+ mModulator.setFrequency(frequency, sampleRate);
+ }
+
+ private float process(float input) {
+ int readCursor = mCursor - mLength;
+ readCursor += (int)(mModulator.generate() * mModulationDepth);
+ if (readCursor < 0) readCursor += mBuffer.length;
+
+ float z = mBuffer[readCursor];
+
+ float x = input - (z * mCoefficient );
+ mBuffer[mCursor] = x;
+ mCursor++;
+ if (mCursor >= mBuffer.length) mCursor = 0;
+ return z + (x * mCoefficient);
+ }
+ }
+
+ // y = x*c + y*(1-c)
+ private static class OnePoleLowPassFilter {
+ private float mDelay;
+ private float mCoefficient;
+
+ OnePoleLowPassFilter(float coefficient) {
+ mCoefficient = coefficient;
+ }
+
+ private float process(float input) {
+ float output = (input * mCoefficient)
+ + (mDelay * (1.0f - mCoefficient));
+ mDelay = output;
+ return output;
+ }
+
+ public void setCoefficient(float coefficient) {
+ mCoefficient = coefficient;
+ }
+ }
+
+ // One side of the figure eight.
+ private class ReverbSide {
+ VariableAllPassDelay variableDelay;
+ OnePoleLowPassFilter mLowPass = new OnePoleLowPassFilter(1.0f - DAMPING);
+ SimpleDelay mDelay1;
+ AllPassDelay mAllPassDelay;
+ SimpleDelay mDelay2;
+ private float outputScaler = 0.6f;
+ private float mOutput;
+
+ ReverbSide(int d1, int d2, int d3, int d4) {
+ // This all pass reverses the signs.
+ variableDelay = new VariableAllPassDelay(d1, 0.0f - DECAY_DIFFUSION_1);
+ mDelay1 = new SimpleDelay(d2);
+ mAllPassDelay = new AllPassDelay(d3, DECAY_DIFFUSION_2);
+ mDelay2 = new SimpleDelay(d4);
+ }
+
+ public void setFrequency(float frequency, float sampleRate) {
+ variableDelay.setFrequency(frequency, sampleRate);
+ }
+
+ private float process(float input) {
+ float temp = variableDelay.process(input);
+ mOutput = temp;
+ temp = mDelay1.process(temp);
+ mOutput -= temp;
+ temp = mLowPass.process(temp);
+ temp *= mDecay;
+ temp = mAllPassDelay.process(temp);
+ mOutput += temp;
+ temp = mDelay2.process(temp);
+ temp *= mDecay;
+ mOutput -= temp;
+ return temp;
+ }
+
+ private float getOutput() {
+ return mOutput * outputScaler;
+ }
+
+ public void setDamping(float damping) {
+ mLowPass.setCoefficient(1.0f - damping);
+ }
+ }
+
+ private float mDecay;
+ private float mLeftFeedback;
+ private float mRightFeedback;
+ private double mSize = 1.0;
+ private double mPreviousTime = -1.0;
+
+ private OnePoleLowPassFilter mBandwidthLowPass = new OnePoleLowPassFilter(BANDWIDTH);
+ private AllPassDelay mDiffusion1 = new AllPassDelay(142, INPUT_DIFFUSION_1);
+ private AllPassDelay mDiffusion2 = new AllPassDelay(107, INPUT_DIFFUSION_1);
+ private AllPassDelay mDiffusion3 = new AllPassDelay(379, INPUT_DIFFUSION_2);
+ private AllPassDelay mDiffusion4 = new AllPassDelay(277, INPUT_DIFFUSION_2);
+ private ReverbSide mLeftSide;
+ private ReverbSide mRightSide;
+
+
+ /**
+ * Create a PlateReverb with a default size of 1.0.
+ */
+ public PlateReverb() {
+ this(1.0);
+ }
+
+ /**
+ * This reverb uses multiple delay lines. The size parameter
+ * scales the allocated size. A value of 1.0 is the default.
+ * At low values the reverb will sound more metallic, like a comb filter.
+ * At larger values it will sound more echoey.
+ *
+ * The size value will be clipped between 0.05 and 5.0.
+ *
+ * @param size adjust internal delay sizes
+ */
+ public PlateReverb(double size) {
+
+ addPort(input = new UnitInputPort("Input"));
+
+ size = Math.max(0.05, Math.min(5.0, size));
+ mSize = size;
+ addPort(time = new UnitInputPort("Time"));
+ time.setup(0.01, 2.0, 30.0);
+ addPort(damping = new UnitInputPort("Damping"));
+ damping.setup(0.0001, DAMPING, 1.0);
+
+ addPort(output = new UnitOutputPort(2,"Output"));
+
+ // delay line sizes
+ // These are from the original paper.
+ // int[] zs = {142, 107, 379, 277, // diffusion
+ // 672, 4453, 1800, 3720, // left
+ // 908, 4217, 2656, 3163}; // right
+ // These are aligned to nearby primes.
+ int[] zs = {149, 107, 379, 277, // diffusion
+ 677, 4453, 1801, 3727, // left
+ 911, 4217, 2657, 3169}; // right
+
+ mDiffusion1 = new AllPassDelay((int)(zs[0] * size), INPUT_DIFFUSION_1);
+ mDiffusion2 = new AllPassDelay((int)(zs[1] * size), INPUT_DIFFUSION_1);
+ mDiffusion3 = new AllPassDelay((int)(zs[2] * size), INPUT_DIFFUSION_2);
+ mDiffusion4 = new AllPassDelay((int)(zs[3] * size), INPUT_DIFFUSION_2);
+ mLeftSide = new ReverbSide((int)(zs[4] * size), (int)(zs[5] * size),
+ (int)(zs[6] * size), (int)(zs[7] * size));
+ mRightSide = new ReverbSide((int)(zs[8] * size), (int)(zs[9] * size),
+ (int)(zs[10] * size), (int)(zs[11] * size));
+ mLeftSide.setFrequency(0.7f, 44100.0f); // TODO use actual sample rate
+ mRightSide.setFrequency(1.2f, 44100.0f); // TODO use actual sample rate
+ }
+
+ // Unfortunately, Java does not have a simple duple support.
+ // So we return void and then get teh two values from the left and
+ // right sides.
+ private void process(float x) {
+ x = mBandwidthLowPass.process(x);
+ x = mDiffusion1.process(x);
+ x = mDiffusion2.process(x);
+ x = mDiffusion3.process(x);
+ x = mDiffusion4.process(x);
+ // left side of the figure eight uses right side feedback
+ float leftSum = x + mRightFeedback;
+ mLeftFeedback = mLeftSide.process(leftSum);
+ // right side of the figure eight uses left side feedback
+ float rightSum = x + mLeftFeedback;
+ mRightFeedback = mRightSide.process(rightSum);
+ }
+
+
+ // This equation was derived from measuring the actual RT60 as a function
+ // of size and decay.
+ // time = size * (0.52 - (4.7 * Math.log(1.0001 - (decay * decay))));
+ // time/size = 0.52 - (4.7 * Math.log(1.0001 - (decay * decay)))
+ // time/size - 0.52 = -4.7 * Math.log(1.0001 - (decay * decay))
+ // (0.52 - (time/size))/ 4.7 = Math.log(1.0001 - (decay * decay))
+ // Math.exp((0.52 - (time/size))/ 4.7) = 1.0001 - (decay * decay)
+ // 1.001 - Math.exp((0.52 - (time/size))/ 4.7) = decay * decay
+ // decay = Math.sqrt(1.001 - Math.exp((0.52 - (time/size))/ 4.7))
+ private double convertTimeToDecay(double size, double time) {
+ double exponent = (0.52 - (time / size))/ 4.7;
+ double square = 1.001 - Math.exp(exponent); // TODO optimize
+ double decay = Math.sqrt(Math.max(0.0, square)); // avoid sqrt(negative)
+ return Math.min(MAX_DECAY, decay);
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] leftOutputs = output.getValues(0);
+ double[] rightOutputs = output.getValues(1);
+
+ double timeValue = (float) time.getValues()[0];
+ if (timeValue != mPreviousTime) {
+ mDecay = (float) convertTimeToDecay(mSize, timeValue);
+ mPreviousTime = timeValue;
+ }
+ float dampingValue = (float) damping.getValues()[0];
+ mLeftSide.setDamping(dampingValue);
+ mRightSide.setDamping(dampingValue);
+ for (int i = start; i < limit; i++) {
+ process((float) inputs[i]);
+ leftOutputs[i] = mLeftSide.getOutput();
+ rightOutputs[i] = mRightSide.getOutput();
+ }
+ }
+
+}
diff --git a/src/main/java/com/jsyn/unitgen/RoomReverb.java b/src/main/java/com/jsyn/unitgen/RoomReverb.java
new file mode 100644
index 0000000..eec1a5e
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/RoomReverb.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2022 Phil Burk
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * Simulate reverberation in a room using a MultiTapDelay to model the early reflections
+ * and a PlateReverb to provide diffusion.
+ *
+ * @author (C) 2022 Phil Burk, Mobileer Inc
+ * @see MultiTapDelay
+ * @see PlateReverb
+ */
+public class RoomReverb extends Circuit {
+ private static final double SIZE_SCALER_MIN = 0.05;
+ private static final double SIZE_SCALER_MAX = 5.0;
+ private static final int[] kPositions = {
+ 10, 197, 401,
+ 521, 733, 1117,
+ 1481, 2731, 4177,
+ 6073, 7927, 9463};
+ // Gains based on attenuation in air after a pre-delay.
+ // See spreadsheet MiscSynthCalculations
+ private static final float[] kGains = {
+ 0.1840f, -0.1543f, -0.1311f,
+ 0.1205f, -0.1054f, -0.0859f,
+ -0.0731f, -0.0484f, 0.0347f,
+ 0.0254f, 0.0201f, -0.0171f};
+
+ /**
+ * Mono input.
+ */
+ public UnitInputPort input;
+
+ /** Pre-delay time in milliseconds. */
+ public UnitInputPort preDelayMillis;
+
+ /**
+ * Approximate time in seconds to decay by -60 dB.
+ */
+ public UnitInputPort time;
+
+ /**
+ * Damping factor for the feedback filters.
+ * Must be <= 1.0. Default is 0.5.
+ */
+ public UnitInputPort damping;
+
+ /**
+ * Amount of multi-tap delay in the output mix.
+ * Must be between 0.0 and 1.0.
+ */
+ public UnitInputPort multiTap;
+
+ /**
+ * Amount of diffusion in the output mix.
+ * Must be between 0.0 and 1.0.
+ */
+ public UnitInputPort diffusion;
+
+ /**
+ * Stereo output.
+ */
+ public UnitOutputPort output;
+
+ private final PlateReverb mPlateReverb;
+ private final MultiTapDelay mMultiTapDelay;
+ private final RoomReverbMixer mRoomReverbMixer;
+
+ /**
+ * Construct a RoomReverb with a default size of 1.0.
+ */
+ public RoomReverb() {
+ this(1.0);
+ }
+
+ /**
+ * The size parameter scales the allocated size.
+ * A value of 1.0 is the default.
+ * At low values the reverb will sound more metallic, like a comb filter.
+ * At larger values it will have longer echos.
+ *
+ * The size value will be clipped between 0.05 and 5.0.
+ *
+ * @param size adjust internal delay sizes
+ */
+ public RoomReverb(double size) {
+ size = Math.max(SIZE_SCALER_MIN, Math.min(SIZE_SCALER_MAX, size));
+
+ int[] positions = new int[kPositions.length];
+ for (int tap = 0; tap < kPositions.length; tap++) {
+ positions[tap] = (int) (kPositions[tap] * size);
+ }
+ add(mMultiTapDelay = new MultiTapDelay(positions, kGains,
+ (int)(4000 * size) /* preDelayFrames */)); // roughly 80 msec max
+ add(mPlateReverb = new PlateReverb(1.0));
+ add(mRoomReverbMixer = new RoomReverbMixer());
+
+ mMultiTapDelay.output.connect(mPlateReverb.input);
+ mMultiTapDelay.output.connect(mRoomReverbMixer.multiTapInput);
+ mPlateReverb.output.connect(0, mRoomReverbMixer.diffusionInput, 0);
+ mPlateReverb.output.connect(1, mRoomReverbMixer.diffusionInput, 1);
+
+ // Assign ports
+ input = mMultiTapDelay.input;
+ addPort(input);
+ preDelayMillis = mMultiTapDelay.preDelayMillis;
+ addPort(preDelayMillis);
+ time = mPlateReverb.time;
+ addPort(time);
+ damping = mPlateReverb.damping;
+ addPort(damping);
+ multiTap = mRoomReverbMixer.multiTapGain;
+ addPort(multiTap);
+ diffusion = mRoomReverbMixer.diffusionGain;
+ addPort(diffusion);
+ output = mRoomReverbMixer.output;
+ addPort(output);
+ }
+
+ // Custom mixer for room reverb.
+ // This is faster than multiple small unit generators.
+ static class RoomReverbMixer extends UnitGenerator {
+ public UnitInputPort multiTapInput;
+ public UnitInputPort diffusionInput;
+
+ public UnitInputPort multiTapGain;
+ public UnitInputPort diffusionGain;
+ public UnitOutputPort output;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public RoomReverbMixer() {
+ addPort(multiTapInput = new UnitInputPort("MultiTapInput"));
+ addPort(diffusionInput = new UnitInputPort(2,"DiffusionInput"));
+ addPort(multiTapGain = new UnitInputPort("MultiTap"));
+ addPort(diffusionGain = new UnitInputPort(2,"Diffusion"));
+ multiTapGain.setup(0.0, 1.0, 1.0);
+ diffusionGain.setup(0.0, 1.0, 1.0);
+ addPort(output = new UnitOutputPort(2,"Output"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] multiTapInputs = multiTapInput.getValues();
+ double[] diffusionInputs0 = diffusionInput.getValues(0);
+ double[] diffusionInputs1 = diffusionInput.getValues(1);
+ double multiTapGainValue = multiTapGain.getValues()[start];
+ double diffusionGainValue = diffusionGain.getValues()[start];
+ double[] outputs0 = output.getValues(0);
+ double[] outputs1 = output.getValues(1);
+
+ for (int i = start; i < limit; i++) {
+ double multiTapScaled = multiTapInputs[i] * multiTapGainValue;
+ outputs0[i] = multiTapScaled + (diffusionInputs0[i] * diffusionGainValue);
+ outputs1[i] = multiTapScaled + (diffusionInputs1[i] * diffusionGainValue);
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/jsyn/dsp/TestSimpleDelay.java b/src/test/java/com/jsyn/dsp/TestSimpleDelay.java
new file mode 100644
index 0000000..400b6d7
--- /dev/null
+++ b/src/test/java/com/jsyn/dsp/TestSimpleDelay.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.dsp;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.jsyn.engine.SynthesisEngine;
+import com.jsyn.unitgen.Add;
+import com.jsyn.unitgen.Compare;
+import com.jsyn.unitgen.Divide;
+import com.jsyn.unitgen.Maximum;
+import com.jsyn.unitgen.Minimum;
+import com.jsyn.unitgen.Multiply;
+import com.jsyn.unitgen.MultiplyAdd;
+import com.jsyn.unitgen.PitchToFrequency;
+import com.jsyn.unitgen.PowerOfTwo;
+import com.jsyn.unitgen.Subtract;
+import com.jsyn.unitgen.UnitBinaryOperator;
+import com.softsynth.math.AudioMath;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Phil Burk, (C) 2009 Mobileer Inc
+ */
+public class TestSimpleDelay {
+
+ @Test
+ public void testProcess() {
+ SimpleDelay delay = new SimpleDelay(3);
+ assertEquals(0.0f, delay.process(0.0f), 0.00001, "Start with zero");
+ assertEquals(0.0f, delay.process(0.7f), 0.00001, "Add an impulse");
+ assertEquals(0.0f, delay.process(0.0f), 0.00001, "Waiting 0");
+ assertEquals(0.0f, delay.process(0.0f), 0.00001, "Waiting 1");
+ assertEquals(0.7f, delay.process(0.0f), 0.00001, "Got it.");
+ }
+
+ @Test
+ public void testAddRead() {
+ SimpleDelay delay = new SimpleDelay(3);
+ assertEquals(0.0f, delay.read(0), 0.00001, "read[0]");
+ assertEquals(0.0f, delay.read(1), 0.00001, "read[1]");
+ assertEquals(0.0f, delay.read(2), 0.00001, "read[2]");
+ delay.write(1.23f);
+ assertEquals(1.23f, delay.read(0), 0.00001, "w0 read[0]");
+ assertEquals(0.0f, delay.read(1), 0.00001, "w0 read[1]");
+ assertEquals(0.0f, delay.read(2), 0.00001, "w0 read[2]");
+ delay.advance();
+ delay.write(0.0f);
+ assertEquals(0.0f, delay.read(0), 0.00001, "w1 read[0]");
+ assertEquals(1.23f, delay.read(1), 0.00001, "w1 read[1]");
+ assertEquals(0.0f, delay.read(2), 0.00001, "w1 read[2]");
+ delay.advance();
+ delay.write(0.567f);
+ assertEquals(0.567f, delay.read(0), 0.00001, "w1 read[0]");
+ assertEquals(0.0f, delay.read(1), 0.00001, "w1 read[1]");
+ assertEquals(1.23f, delay.read(2), 0.00001, "w1 read[2]");
+ }
+}