aboutsummaryrefslogtreecommitdiffstats
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
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]>
-rw-r--r--.gitignore2
-rw-r--r--examples/src/main/java/com/jsyn/examples/ChebyshevSong.java25
-rw-r--r--examples/src/main/java/com/jsyn/examples/InvestigateCordic.java100
-rw-r--r--examples/src/main/java/com/jsyn/examples/MeasurePlateReverb.java129
-rw-r--r--examples/src/main/java/com/jsyn/examples/TuneReverb.java116
-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
14 files changed, 1181 insertions, 13 deletions
diff --git a/.gitignore b/.gitignore
index 2485ead..afa19b0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+*/.DS_Store
+.DS_Store
build/
.gradle/
diff --git a/examples/src/main/java/com/jsyn/examples/ChebyshevSong.java b/examples/src/main/java/com/jsyn/examples/ChebyshevSong.java
index 2dbab88..d4b8d15 100644
--- a/examples/src/main/java/com/jsyn/examples/ChebyshevSong.java
+++ b/examples/src/main/java/com/jsyn/examples/ChebyshevSong.java
@@ -17,16 +17,19 @@
package com.jsyn.examples;
import java.awt.BorderLayout;
+import java.awt.GridLayout;
import javax.swing.JApplet;
+import javax.swing.JPanel;
import com.jsyn.JSyn;
import com.jsyn.Synthesizer;
import com.jsyn.instruments.WaveShapingVoice;
import com.jsyn.scope.AudioScope;
import com.jsyn.swing.JAppletFrame;
-import com.jsyn.unitgen.Add;
import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.RoomReverb;
+import com.jsyn.unitgen.PassThrough;
import com.jsyn.util.PseudoRandom;
import com.jsyn.util.VoiceAllocator;
import com.softsynth.math.AudioMath;
@@ -40,7 +43,8 @@ import com.softsynth.shared.time.TimeStamp;
public class ChebyshevSong extends JApplet implements Runnable {
private Synthesizer synth;
- private Add mixer;
+ private PassThrough mixer; // use input as a summing node
+ private RoomReverb reverb;
private LineOut lineOut;
private AudioScope scope;
private volatile boolean go = false;
@@ -71,18 +75,19 @@ public class ChebyshevSong extends JApplet implements Runnable {
synth = JSyn.createSynthesizer();
// Use a submix so we can show it on the scope.
- synth.add(mixer = new Add());
+ synth.add(mixer = new PassThrough());
synth.add(lineOut = new LineOut());
-
- mixer.output.connect(0, lineOut.input, 0);
- mixer.output.connect(0, lineOut.input, 1);
+ synth.add(reverb = new RoomReverb(1.0));
+ mixer.output.connect(reverb.input);
+ mixer.output.connect(0, lineOut.input, 0); // dry
+ reverb.output.connect(0, lineOut.input, 1); // wet
WaveShapingVoice[] voices = new WaveShapingVoice[MAX_VOICES];
for (int i = 0; i < MAX_VOICES; i++) {
WaveShapingVoice voice = new WaveShapingVoice();
synth.add(voice);
voice.usePreset(0);
- voice.getOutput().connect(mixer.inputA);
+ voice.getOutput().connect(mixer.input);
voices[i] = voice;
}
allocator = new VoiceAllocator(voices);
@@ -94,11 +99,16 @@ public class ChebyshevSong extends JApplet implements Runnable {
// Use a scope to show the mixed output.
scope = new AudioScope(synth);
scope.addProbe(mixer.output);
+ scope.addProbe(reverb.output);
scope.setTriggerMode(AudioScope.TriggerMode.NORMAL);
scope.getView().setControlsVisible(false);
add(BorderLayout.CENTER, scope.getView());
scope.start();
+ JPanel southPanel = new JPanel();
+ southPanel.setLayout(new GridLayout(0, 1));
+ add(BorderLayout.SOUTH, southPanel);
+
/* Synchronize Java display. */
getParent().validate();
getToolkit().sync();
@@ -107,7 +117,6 @@ public class ChebyshevSong extends JApplet implements Runnable {
Thread thread = new Thread(this);
go = true;
thread.start();
-
}
@Override
diff --git a/examples/src/main/java/com/jsyn/examples/InvestigateCordic.java b/examples/src/main/java/com/jsyn/examples/InvestigateCordic.java
new file mode 100644
index 0000000..a1bcfb8
--- /dev/null
+++ b/examples/src/main/java/com/jsyn/examples/InvestigateCordic.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2023 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.examples;
+
+/**
+ * Experiment with Cordic oscillators.
+ * Implement using float and double values internally.
+ */
+class CordicOscillatorFloat {
+ private float mCosPhi;
+ private float x;
+ private float y = -1.0f;
+ private float mSinPhi;
+
+ CordicOscillatorFloat() {
+ setFrequency(441.0, 44100.0);
+ }
+
+ public void setFrequency(double frequency, double sampleRate) {
+ double radians = frequency * Math.PI * 2.0 / sampleRate;
+ mCosPhi = (float) Math.cos(radians);
+ mSinPhi = (float) Math.sin(radians);
+ }
+
+ public double generate() {
+ float x2 = x * mCosPhi - y * mSinPhi;
+ float y2 = y * mCosPhi + x * mSinPhi;
+ x = Math.min(x2, 1.0f);
+ y = y2;
+ return x;
+ }
+}
+
+
+class CordicOscillator {
+ private double mCosPhi;
+ private double x;
+ private double y = -1.0;
+ private double mSinPhi;
+
+ CordicOscillator() {
+ setFrequency(441.0, 44100.0);
+ }
+
+ public void setFrequency(double frequency, double sampleRate) {
+ double radians = frequency * Math.PI * 2.0 / sampleRate;
+ mCosPhi = Math.cos(radians);
+ mSinPhi = Math.sin(radians);
+ }
+
+ public double generate() {
+ double x2 = x * mCosPhi - y * mSinPhi;
+ double y2 = y * mCosPhi + x * mSinPhi;
+ x = x2; // Math.min(x2, 1.0);
+ y = y2;
+ return x;
+ }
+}
+
+public class InvestigateCordic
+{
+ public void test() {
+ CordicOscillator oscillator = new CordicOscillator();
+ oscillator.setFrequency(1.0, 44100.0);
+ for (int i = 0; i < 100; i++) {
+ double x = oscillator.generate();
+ System.out.println("x = " + x);
+ }
+ double peak = 0.0;
+ for (int n = 0; n < 200; n++) {
+ peak = 0.0;
+ for (int i = 0; i < 100000000; i++) {
+ double x = oscillator.generate();
+ if (x > peak) {
+ peak = x;
+ }
+ }
+ System.out.println(n + ": peak = " + peak);
+ }
+ }
+
+ public static void main(String[] args) {
+ new InvestigateCordic().test();
+ System.exit(0);
+ }
+}
diff --git a/examples/src/main/java/com/jsyn/examples/MeasurePlateReverb.java b/examples/src/main/java/com/jsyn/examples/MeasurePlateReverb.java
new file mode 100644
index 0000000..db05251
--- /dev/null
+++ b/examples/src/main/java/com/jsyn/examples/MeasurePlateReverb.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2022 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.examples;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.unitgen.EnvelopeDAHDSR;
+import com.jsyn.unitgen.ImpulseOscillator;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.PeakFollower;
+import com.jsyn.unitgen.PinkNoise;
+import com.jsyn.unitgen.PlateReverb;
+import com.jsyn.unitgen.SawtoothOscillator;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.SquareOscillator;
+import com.jsyn.unitgen.UnitOscillator;
+import com.jsyn.unitgen.WhiteNoise;
+import com.softsynth.math.AudioMath;
+
+/**
+ * Measure the decay time of a PlateReverb tail.
+ */
+public class MeasurePlateReverb {
+
+ private double measure(double size, double time, double damping) throws InterruptedException {
+ // Create a context for the synthesizer.
+ Synthesizer synth = JSyn.createSynthesizer();
+ synth.setRealTime(false);
+
+ // Add a signal source.
+ WhiteNoise source = new WhiteNoise();
+ PlateReverb reverb = new PlateReverb(size);
+ PeakFollower peak = new PeakFollower();
+ LineOut lineOut = new LineOut();
+
+ synth.add(source);
+ synth.add(peak);
+ synth.add(reverb);
+ synth.add(lineOut);
+
+ source.amplitude.set(1.0);
+ peak.halfLife.set(0.01);
+ reverb.time.set(time);
+ reverb.damping.set(damping);
+
+ source.output.connect(reverb.input);
+ reverb.output.connect(peak.input);
+ peak.output.connect(0, lineOut.input, 0);
+ reverb.output.connect(0, lineOut.input, 1);
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ lineOut.start();
+
+ // Sleep while the sound is generated in the background.
+ double rt60 = 0.0;
+ final double REFERENCE_DB = -60.0;
+ final double TARGET_DB = -30.0;
+ synth.sleepFor(1.0);
+ double original = peak.output.getValue();
+ source.amplitude.set(0.0);
+ double startTime = synth.getCurrentTime();
+// System.out.printf(" time, ratio, db\n");
+ double db = 1.0;
+ double elapsed;
+ int count = 0;
+ do {
+ synth.sleepUntil(startTime + (count++ * 0.1));
+ double level = peak.output.getValue();
+ elapsed = synth.getCurrentTime() - startTime;
+ double ratio = level / original;
+ db = AudioMath.amplitudeToDecibels(ratio);
+// System.out.printf(" %3.3f, %6.4f, %6.3f\n",
+// elapsed, ratio, db);
+ } while (db > TARGET_DB && elapsed < 30.0);
+ if (elapsed >= 30.0) {
+ System.out.println("TIMEOUT!");
+ }
+ // Time to reach reference;
+ rt60 = REFERENCE_DB * elapsed / db;
+ // Stop everything.
+ synth.stop();
+
+ return rt60;
+ }
+
+// private double estimateRT60(double size, double decay) {
+// return size * (0.52 - (4.7 * Math.log(1.0001 - (decay * decay))));
+// }
+
+ private void test() {
+ double damping = 0.0005;
+ for (double size = 0.2; size < 3.0; size *= 1.5) {
+ System.out.printf("\nsize = %5.2f\n", size);
+ System.out.printf("time, rt60\n");
+ for (double time = 0.1; time < 30.0; time *= 1.2) {
+ double rt60 = 0.0;
+ try {
+ rt60 = measure(size, time, damping);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ // double estimate = estimateRT60(size, decay);
+ System.out.printf("%5.3f, %6.4f\n",
+ time, rt60);
+ }
+ }
+ }
+
+ public static void main(String[] args) {
+ new MeasurePlateReverb().test();
+ System.exit(0);
+ }
+}
diff --git a/examples/src/main/java/com/jsyn/examples/TuneReverb.java b/examples/src/main/java/com/jsyn/examples/TuneReverb.java
new file mode 100644
index 0000000..e38a2f7
--- /dev/null
+++ b/examples/src/main/java/com/jsyn/examples/TuneReverb.java
@@ -0,0 +1,116 @@
+/*
+ * 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.examples;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.swing.JAppletFrame;
+import com.jsyn.swing.PortControllerFactory;
+import com.jsyn.unitgen.EnvelopeDAHDSR;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.Pan;
+import com.jsyn.unitgen.PinkNoise;
+import com.jsyn.unitgen.RoomReverb;
+import com.jsyn.unitgen.SawtoothOscillatorDPW;
+import com.jsyn.unitgen.SquareOscillator;
+import java.awt.GridLayout;
+import javax.swing.JApplet;
+
+/**
+ * Play various sounds interactively through a reverb.
+ */
+public class TuneReverb extends JApplet {
+ private Synthesizer synth;
+
+ private PinkNoise noise;
+ private SawtoothOscillatorDPW sawtooth;
+ // Use a square wave to trigger the envelope.
+ private SquareOscillator gatingOsc;
+ private EnvelopeDAHDSR dahdsr;
+ private Pan dryWet;
+ private RoomReverb reverb;
+ private LineOut lineOut;
+
+ @Override
+ public void init() {
+ synth = JSyn.createSynthesizer();
+
+ synth.add(noise = new PinkNoise());
+ synth.add(sawtooth = new SawtoothOscillatorDPW());
+ synth.add(gatingOsc = new SquareOscillator());
+ synth.add(dahdsr = new EnvelopeDAHDSR());
+ synth.add(dryWet = new Pan());
+ synth.add(reverb = new RoomReverb());
+ synth.add(lineOut = new LineOut());
+
+ // Connect the oscillator to both channels of the output.
+ gatingOsc.output.connect(dahdsr.input);
+ gatingOsc.frequency.set(0.5);
+ dahdsr.attack.set(0.01);
+ dahdsr.decay.set(0.05);
+ dahdsr.sustain.set(0.00);
+
+ noise.output.connect(dahdsr.amplitude);
+ sawtooth.output.connect(dahdsr.amplitude);
+ dahdsr.output.connect(dryWet.input);
+ dryWet.output.connect(1, reverb.input, 0);
+ dryWet.output.connect(0, lineOut.input, 0);
+ dryWet.output.connect(0, lineOut.input, 1);
+ reverb.output.connect(0, lineOut.input, 0);
+ reverb.output.connect(0, lineOut.input, 1);
+
+ // Arrange the faders in a stack.
+ setLayout(new GridLayout(0, 1));
+
+ gatingOsc.frequency.setup(0.1, 0.5, 4.0);
+ add(PortControllerFactory.createExponentialPortSlider(sawtooth.frequency));
+ add(PortControllerFactory.createExponentialPortSlider(sawtooth.amplitude));
+ add(PortControllerFactory.createExponentialPortSlider(noise.amplitude));
+ add(PortControllerFactory.createExponentialPortSlider(gatingOsc.frequency));
+ add(PortControllerFactory.createPortSlider(dryWet.pan));
+ add(PortControllerFactory.createExponentialPortSlider(reverb.preDelayMillis));
+ add(PortControllerFactory.createExponentialPortSlider(reverb.multiTap));
+ add(PortControllerFactory.createExponentialPortSlider(reverb.diffusion));
+ add(PortControllerFactory.createExponentialPortSlider(reverb.time));
+ add(PortControllerFactory.createExponentialPortSlider(reverb.damping));
+ validate();
+ }
+
+ @Override
+ public void start() {
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ // We only need to start the LineOut. It will pull data from the
+ // oscillator.
+ lineOut.start();
+ }
+
+ @Override
+ public void stop() {
+ synth.stop();
+ }
+
+ /* Can be run as either an application or as an applet. */
+ public static void main(String[] args) {
+ TuneReverb applet = new TuneReverb();
+ JAppletFrame frame = new JAppletFrame("Tune Reverb", applet);
+ frame.setSize(440, 600);
+ frame.setVisible(true);
+ frame.test();
+ }
+
+}
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]");
+ }
+}