aboutsummaryrefslogtreecommitdiffstats
path: root/src/test/java/com/jsyn
diff options
context:
space:
mode:
Diffstat (limited to 'src/test/java/com/jsyn')
-rw-r--r--src/test/java/com/jsyn/benchmarks/BenchJSyn.java228
-rw-r--r--src/test/java/com/jsyn/data/TestShortSample.java72
-rw-r--r--src/test/java/com/jsyn/engine/TestAudioOutput.java78
-rw-r--r--src/test/java/com/jsyn/engine/TestDevices.java75
-rw-r--r--src/test/java/com/jsyn/engine/TestEngine.java225
-rw-r--r--src/test/java/com/jsyn/engine/TestFifo.java245
-rw-r--r--src/test/java/com/jsyn/engine/TestWaveFileReadWrite.java114
-rw-r--r--src/test/java/com/jsyn/midi/TestMidiLoop.java87
-rw-r--r--src/test/java/com/jsyn/ports/TestQueuedDataPort.java549
-rw-r--r--src/test/java/com/jsyn/ports/TestSequentialData.java55
-rw-r--r--src/test/java/com/jsyn/ports/TestSet.java89
-rw-r--r--src/test/java/com/jsyn/research/BenchMultiThreading.java152
-rw-r--r--src/test/java/com/jsyn/research/RecordVariousRamps.java193
-rw-r--r--src/test/java/com/jsyn/swing/TestRangeModels.java53
-rw-r--r--src/test/java/com/jsyn/unitgen/CalibrateMoogFilter.java141
-rw-r--r--src/test/java/com/jsyn/unitgen/EnablingGate.java51
-rw-r--r--src/test/java/com/jsyn/unitgen/NonRealTimeTestCase.java42
-rw-r--r--src/test/java/com/jsyn/unitgen/RecordMoogFilter.java158
-rw-r--r--src/test/java/com/jsyn/unitgen/TestConnections.java111
-rw-r--r--src/test/java/com/jsyn/unitgen/TestDelay.java81
-rw-r--r--src/test/java/com/jsyn/unitgen/TestEnable.java81
-rw-r--r--src/test/java/com/jsyn/unitgen/TestEnvelopeAttackDecay.java130
-rw-r--r--src/test/java/com/jsyn/unitgen/TestEnvelopeDAHDSR.java355
-rw-r--r--src/test/java/com/jsyn/unitgen/TestFunction.java77
-rw-r--r--src/test/java/com/jsyn/unitgen/TestMath.java420
-rw-r--r--src/test/java/com/jsyn/unitgen/TestRamps.java205
-rw-r--r--src/test/java/com/jsyn/unitgen/TestUnitGate.java81
-rw-r--r--src/test/java/com/jsyn/util/DebugSampleLoader.java143
-rw-r--r--src/test/java/com/jsyn/util/TestFFT.java201
-rw-r--r--src/test/java/com/jsyn/util/TestPseudoRandom.java76
-rw-r--r--src/test/java/com/jsyn/util/TestVoiceAllocator.java111
31 files changed, 4679 insertions, 0 deletions
diff --git a/src/test/java/com/jsyn/benchmarks/BenchJSyn.java b/src/test/java/com/jsyn/benchmarks/BenchJSyn.java
new file mode 100644
index 0000000..017dc99
--- /dev/null
+++ b/src/test/java/com/jsyn/benchmarks/BenchJSyn.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2013 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.benchmarks;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.unitgen.PassThrough;
+import com.jsyn.unitgen.PitchDetector;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.SquareOscillator;
+import com.jsyn.unitgen.SquareOscillatorBL;
+import com.jsyn.unitgen.UnitOscillator;
+import com.softsynth.math.FourierMath;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Phil Burk (C) 2013 Mobileer Inc
+ */
+public class BenchJSyn {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(BenchJSyn.class);
+
+ private Synthesizer synth;
+ private long startTime;
+ private long endTime;
+ private PassThrough pass;
+
+ @Test
+ public void run() {
+ try {
+ // Run multiple times to see if HotSpot compiler or cache makes a difference.
+ for (int i = 0; i < 4; i++) {
+ benchmark();
+ }
+ } catch (InstantiationException | IllegalAccessException | InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void benchmark() throws InstantiationException, IllegalAccessException,
+ InterruptedException {
+ double realTime = 10.0;
+ int count = 40;
+
+ // benchFFTDouble();
+ // benchFFTFloat();
+ /*
+ * realTime = 20.0; benchmarkOscillator(SawtoothOscillator.class, count, realTime);
+ * benchmarkOscillator(SawtoothOscillatorDPW.class, count, realTime);
+ * benchmarkOscillator(SawtoothOscillatorBL.class, count, realTime);
+ */
+ benchmarkOscillator(SquareOscillator.class, count, realTime);
+ benchmarkOscillator(SquareOscillatorBL.class, count, realTime);
+
+ benchmarkOscillator(SineOscillator.class, count, realTime);
+ benchmarkPitchDetector(count, realTime);
+
+ }
+
+ public void benchFFTDouble() {
+ int size = 2048;
+ int bin = 5;
+ int count = 20000;
+ double[] ar = new double[size];
+ double[] ai = new double[size];
+ double[] magnitudes = new double[size];
+
+ double amplitude = 1.0;
+ addSineWave(size, bin, ar, amplitude);
+ LOGGER.debug("Bench double FFT");
+ startTiming();
+ for (int i = 0; i < count; i++) {
+ FourierMath.transform(1, size, ar, ai);
+ }
+
+ endTiming(FourierMath.class, count, size / (2.0 * 44100));
+ FourierMath.calculateMagnitudes(ar, ai, magnitudes);
+
+ assert (magnitudes[bin - 1] < 0.001);
+ assert (magnitudes[bin] > 0.5);
+ assert (magnitudes[bin + 1] < 0.001);
+
+ }
+
+ public void benchFFTFloat() {
+ int size = 2048;
+ int bin = 5;
+ int count = 20000;
+ float[] ar = new float[size];
+ float[] ai = new float[size];
+ float[] magnitudes = new float[size];
+
+ float amplitude = 1.0f;
+ addSineWave(size, bin, ar, amplitude);
+
+ LOGGER.debug("Bench float FFT");
+ startTiming();
+ for (int i = 0; i < count; i++) {
+ FourierMath.transform(1, size, ar, ai);
+ }
+
+ endTiming(FourierMath.class, count, size / (2.0 * 44100));
+ FourierMath.calculateMagnitudes(ar, ai, magnitudes);
+
+ assert (magnitudes[bin - 1] < 0.001);
+ assert (magnitudes[bin] > 0.5);
+ assert (magnitudes[bin + 1] < 0.001);
+
+ }
+
+ private void addSineWave(int size, int bin, double[] ar, double amplitude) {
+ double phase = 0.0;
+ double phaseIncrement = 2.0 * Math.PI * bin / size;
+ for (int i = 0; i < size; i++) {
+ ar[i] += Math.sin(phase) * amplitude;
+ // LOGGER.debug( i + " = " + ar[i] );
+ phase += phaseIncrement;
+ }
+ }
+
+ private void addSineWave(int size, int bin, float[] ar, float amplitude) {
+ float phase = 0.0f;
+ float phaseIncrement = (float) (2.0 * Math.PI * bin / size);
+ for (int i = 0; i < size; i++) {
+ ar[i] += (float) Math.sin(phase) * amplitude;
+ // LOGGER.debug( i + " = " + ar[i] );
+ phase += phaseIncrement;
+ }
+ }
+
+ private void stopSynth() {
+ synth.stop();
+ }
+
+ private void startSynth() {
+ synth = JSyn.createSynthesizer(); // Mac
+ // synth = JSyn.createSynthesizer( new JSynAndroidAudioDevice() ); // Android
+ synth.setRealTime(false);
+ pass = new PassThrough();
+ synth.add(pass);
+ synth.start();
+ pass.start();
+ }
+
+ private void benchmarkOscillator(Class<?> clazz, int count, double realTime)
+ throws InstantiationException, IllegalAccessException, InterruptedException {
+ startSynth();
+ for (int i = 0; i < count; i++) {
+ UnitOscillator osc = (UnitOscillator) clazz.newInstance();
+ osc.output.connect(pass.input);
+ synth.add(osc);
+ }
+ startTiming();
+ synth.sleepFor(realTime);
+ endTiming(clazz, count, realTime);
+ stopSynth();
+ }
+
+ private void benchmarkPitchDetector(int count, double realTime) throws InstantiationException,
+ IllegalAccessException, InterruptedException {
+ startSynth();
+
+ PitchDetector detector = new PitchDetector();
+ synth.add(detector);
+ double frequency = 198.0;
+ double period = synth.getFrameRate() / frequency;
+ // simple harmonic synthesis
+ for (int i = 0; i < count; i++) {
+ SineOscillator osc = new SineOscillator();
+ synth.add(osc);
+ osc.frequency.set(frequency * (i + 1));
+ osc.amplitude.set(0.5 * (1.0 - (i * 0.2)));
+ osc.output.connect(detector.input);
+ }
+ detector.start();
+ startTiming();
+ synth.sleepFor(realTime);
+ endTiming(PitchDetector.class, count, realTime);
+
+ double measuredPeriod = detector.period.getValue();
+ double confidence = detector.confidence.getValue();
+ LOGGER.debug("period = " + period + ", measured = " + measuredPeriod
+ + ", confidence = " + confidence);
+ if (confidence > 0.1) {
+ assert (Math.abs(measuredPeriod - period) < 0.1);
+ }
+ stopSynth();
+ }
+
+ private void endTiming(Class<?> clazz, int count, double realTime) {
+ endTime = System.nanoTime();
+ double elapsedTime = (endTime - startTime) * 1E-9;
+ double percent = 100.0 * elapsedTime / (realTime * count);
+ System.out.printf("%32s took %5.3f/%d seconds to process %5.4f of audio = %6.3f%c.\n",
+ clazz.getSimpleName(), elapsedTime, count, realTime, percent, '%');
+ }
+
+ private void startTiming() {
+ startTime = System.nanoTime();
+ }
+
+// /**
+// * @param args
+// */
+// public static void main(String[] args) {
+// new BenchJSyn().run();
+// }
+
+}
diff --git a/src/test/java/com/jsyn/data/TestShortSample.java b/src/test/java/com/jsyn/data/TestShortSample.java
new file mode 100644
index 0000000..6132e4e
--- /dev/null
+++ b/src/test/java/com/jsyn/data/TestShortSample.java
@@ -0,0 +1,72 @@
+/*
+ * 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.data;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @author Phil Burk, (C) 2009 Mobileer Inc
+ */
+public class TestShortSample {
+
+ @Test
+ public void testBytes() {
+ byte[] bar = {
+ 18, -3
+ };
+ short s = (short) ((bar[0] << 8) | (bar[1] & 0xFF));
+ assertEquals(0x12FD, s, "A");
+ }
+
+ @Test
+ public void testReadWrite() {
+ short[] data = {
+ 123, 456, -789, 111, 20000, -32768, 32767, 0, 9876
+ };
+ ShortSample sample = new ShortSample(data.length, 1);
+ assertEquals(data.length, sample.getNumFrames(), "Sample numFrames");
+
+ // Write and read entire sample.
+ sample.write(data);
+ short[] buffer = new short[data.length];
+ sample.read(buffer);
+
+ for (int i = 0; i < data.length; i++) {
+ assertEquals(data[i], buffer[i], "read = write");
+ }
+
+ // Write and read part of an array.
+ short[] partial = {
+ 333, 444, 555, 666, 777
+ };
+
+ sample.write(2, partial, 1, 3);
+ sample.read(1, buffer, 1, 5);
+
+ for (int i = 0; i < data.length; i++) {
+ if ((i >= 2) && (i <= 4)) {
+ assertEquals(partial[i - 1], buffer[i], "partial");
+ } else {
+ assertEquals(data[i], buffer[i], "read = write");
+ }
+ }
+
+ }
+
+}
diff --git a/src/test/java/com/jsyn/engine/TestAudioOutput.java b/src/test/java/com/jsyn/engine/TestAudioOutput.java
new file mode 100644
index 0000000..39e8211
--- /dev/null
+++ b/src/test/java/com/jsyn/engine/TestAudioOutput.java
@@ -0,0 +1,78 @@
+/*
+ * 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.engine;
+
+import java.io.IOException;
+
+import com.jsyn.devices.AudioDeviceManager;
+import com.jsyn.devices.AudioDeviceOutputStream;
+import com.jsyn.devices.javasound.JavaSoundAudioDevice;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Phil Burk, (C) 2009 Mobileer Inc
+ */
+public class TestAudioOutput {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TestAudioOutput.class);
+
+ @Test
+ public void testMonoSine() throws IOException {
+ LOGGER.debug("Test mono output.");
+ final int FRAMES_PER_BUFFER = 128;
+ final int SAMPLES_PER_FRAME = 1;
+ double[] buffer = new double[FRAMES_PER_BUFFER * SAMPLES_PER_FRAME];
+ AudioDeviceManager audioDevice = new JavaSoundAudioDevice();
+ AudioDeviceOutputStream audioOutput = audioDevice.createOutputStream(
+ audioDevice.getDefaultOutputDeviceID(), 44100, SAMPLES_PER_FRAME);
+ for (int i = 0; i < FRAMES_PER_BUFFER; i++) {
+ double angle = (i * Math.PI * 2.0) / FRAMES_PER_BUFFER;
+ buffer[i] = Math.sin(angle);
+ }
+ audioOutput.start();
+ for (int i = 0; i < 1000; i++) {
+ audioOutput.write(buffer);
+ }
+ audioOutput.stop();
+
+ }
+
+ @Test
+ public void testStereoSine() throws IOException {
+ LOGGER.debug("Test stereo output.");
+ final int FRAMES_PER_BUFFER = 128;
+ final int SAMPLES_PER_FRAME = 2;
+ double[] buffer = new double[FRAMES_PER_BUFFER * SAMPLES_PER_FRAME];
+ AudioDeviceManager audioDevice = new JavaSoundAudioDevice();
+ AudioDeviceOutputStream audioOutput = audioDevice.createOutputStream(
+ audioDevice.getDefaultOutputDeviceID(), 44100, SAMPLES_PER_FRAME);
+ int bi = 0;
+ for (int i = 0; i < FRAMES_PER_BUFFER; i++) {
+ double angle = (i * Math.PI * 2.0) / FRAMES_PER_BUFFER;
+ buffer[bi++] = Math.sin(angle);
+ buffer[bi++] = Math.sin(angle);
+ }
+ audioOutput.start();
+ for (int i = 0; i < 1000; i++) {
+ audioOutput.write(buffer);
+ }
+ audioOutput.stop();
+ }
+
+}
diff --git a/src/test/java/com/jsyn/engine/TestDevices.java b/src/test/java/com/jsyn/engine/TestDevices.java
new file mode 100644
index 0000000..307880e
--- /dev/null
+++ b/src/test/java/com/jsyn/engine/TestDevices.java
@@ -0,0 +1,75 @@
+/*
+ * 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.engine;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.devices.AudioDeviceFactory;
+import com.jsyn.devices.AudioDeviceManager;
+import com.jsyn.unitgen.LineIn;
+import com.jsyn.unitgen.LineOut;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class TestDevices {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TestDevices.class);
+
+ @Test
+ public void testPassThrough() {
+ Synthesizer synth;
+ LineIn lineIn;
+ LineOut lineOut;
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer(AudioDeviceFactory.createAudioDeviceManager(true));
+ // Add an audio input.
+ synth.add(lineIn = new LineIn());
+ // Add an audio output.
+ synth.add(lineOut = new LineOut());
+ // Connect the input to the output.
+ lineIn.output.connect(0, lineOut.input, 0);
+ lineIn.output.connect(1, lineOut.input, 1);
+
+ // Both stereo.
+ int numInputChannels = 2;
+ int numOutputChannels = 2;
+ synth.start(44100, AudioDeviceManager.USE_DEFAULT_DEVICE, numInputChannels,
+ AudioDeviceManager.USE_DEFAULT_DEVICE, numOutputChannels);
+
+ // We only need to start the LineOut. It will pull data from the LineIn.
+ lineOut.start();
+ LOGGER.debug("Audio passthrough started.");
+ // Sleep a while.
+ double sleepTime = 2.0;
+ try {
+ double time = synth.getCurrentTime();
+ // Sleep for a few seconds.
+ synth.sleepUntil(time + sleepTime);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ double synthTime = synth.getCurrentTime();
+ assertEquals(synthTime, 0.2, "Time has advanced. " + synthTime);
+ // Stop everything.
+ synth.stop();
+ LOGGER.debug("All done.");
+
+ }
+}
diff --git a/src/test/java/com/jsyn/engine/TestEngine.java b/src/test/java/com/jsyn/engine/TestEngine.java
new file mode 100644
index 0000000..0ba70d6
--- /dev/null
+++ b/src/test/java/com/jsyn/engine/TestEngine.java
@@ -0,0 +1,225 @@
+/*
+ * 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.engine;
+
+import com.jsyn.unitgen.Add;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.PitchDetector;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.ZeroCrossingCounter;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestEngine {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TestEngine.class);
+
+ @Test
+ public void testInitialization() {
+ final int DEFAULT_FRAME_RATE = 44100;
+ SynthesisEngine synthesisEngine = new SynthesisEngine();
+ assertEquals(0, synthesisEngine.getFrameCount(), "frameCount zero before starting");
+ assertEquals(DEFAULT_FRAME_RATE, synthesisEngine.getFrameRate(), "default frameRate");
+ assertTrue(synthesisEngine.isPullDataEnabled(), "default pullData");
+ }
+
+ public void checkPullData(boolean pullData) {
+ SynthesisEngine synthesisEngine = new SynthesisEngine();
+ assertTrue(synthesisEngine.isRealTime(), "default realTime");
+ synthesisEngine.setRealTime(false);
+
+ assertTrue(synthesisEngine.isPullDataEnabled(), "default pullData");
+ synthesisEngine.setPullDataEnabled(pullData);
+
+ SineOscillator sineOscillator = new SineOscillator();
+ synthesisEngine.add(sineOscillator);
+
+ LineOut lineOut = new LineOut();
+ synthesisEngine.add(lineOut);
+ sineOscillator.output.connect(0, lineOut.input, 0);
+
+ assertEquals(0.0, sineOscillator.output.getValue(), "initial sine value");
+
+ synthesisEngine.start();
+ if (!pullData) {
+ sineOscillator.start();
+ }
+ // We always have to start the LineOut.
+ lineOut.start();
+ synthesisEngine.generateNextBuffer();
+ synthesisEngine.generateNextBuffer();
+
+ double value = sineOscillator.output.getValue();
+ assertTrue(value > 0.0, "sine value after generation = " + value);
+ }
+
+ @Test
+ public void testPullDataFalse() {
+ checkPullData(false);
+ }
+
+ @Test
+ public void testPullDataTrue() {
+ checkPullData(true);
+ }
+
+ @Test
+ public void testMixedAdding() {
+ boolean gotCaught = false;
+ SynthesisEngine synthesisEngine1 = new SynthesisEngine();
+ synthesisEngine1.setRealTime(false);
+ synthesisEngine1.setPullDataEnabled(true);
+ SynthesisEngine synthesisEngine2 = new SynthesisEngine();
+ synthesisEngine2.setRealTime(false);
+ synthesisEngine2.setPullDataEnabled(true);
+
+ // Create a sineOscillator but do not add it to the synth!
+ SineOscillator sineOscillator = new SineOscillator();
+ LineOut lineOut = new LineOut();
+
+ synthesisEngine1.add(lineOut);
+ synthesisEngine2.add(sineOscillator);
+ try {
+ sineOscillator.output.connect(0, lineOut.input, 0);
+ } catch (RuntimeException e) {
+ gotCaught = true;
+ assertTrue(e.getMessage().contains("different synths"), "informative MPE message");
+ }
+
+ assertTrue(gotCaught, "caught NPE caused by forgetting synth.add");
+ }
+
+ @Test
+ public void testNotAdding() {
+ SynthesisEngine synthesisEngine = new SynthesisEngine();
+ synthesisEngine.setRealTime(false);
+ synthesisEngine.setPullDataEnabled(true);
+
+ // Create a sineOscillator but do not add it to the synth!
+ SineOscillator sineOscillator = new SineOscillator();
+
+ LineOut lineOut = new LineOut();
+ sineOscillator.output.connect(0, lineOut.input, 0);
+ synthesisEngine.add(lineOut);
+
+ assertEquals(0.0, sineOscillator.output.getValue(), "initial sine value");
+
+ synthesisEngine.start();
+ // We always have to start the LineOut.
+ lineOut.start();
+ boolean gotCaught = false;
+ try {
+ synthesisEngine.generateNextBuffer();
+ synthesisEngine.generateNextBuffer();
+ } catch (NullPointerException e) {
+ gotCaught = true;
+ assertTrue(e.getMessage().contains("forgot to add"), "informative MPE message");
+ }
+
+ assertTrue(gotCaught, "caught NPE caused by forgetting synth.add");
+ }
+
+ @Test
+ public void testMultipleStarts() throws InterruptedException {
+ SynthesisEngine synth = new SynthesisEngine();
+
+ // Create a sineOscillator but do not add it to the synth!
+ SineOscillator osc = new SineOscillator();
+ ZeroCrossingCounter counter = new ZeroCrossingCounter();
+ PitchDetector pitchDetector = new PitchDetector();
+ LineOut lineOut = new LineOut();
+ synth.add(osc);
+ synth.add(counter);
+ synth.add(lineOut);
+ synth.add(pitchDetector);
+ osc.output.connect(counter.input);
+ osc.output.connect(pitchDetector.input);
+ counter.output.connect(0, lineOut.input, 0);
+
+ assertEquals(0, counter.getCount(), "initial count");
+
+ int[] rates = {
+ 32000, 48000, 44100, 22050
+ };
+ for (int rate : rates) {
+ synth.start(rate);
+ lineOut.start();
+ pitchDetector.start();
+
+ double time = synth.getCurrentTime();
+ double interval = 1.0;
+ time += interval;
+
+ long previousFrameCount = counter.getCount();
+ synth.sleepUntil(time);
+
+ double frequencyMeasured = pitchDetector.frequency.get();
+ double confidenceMeasured = pitchDetector.confidence.get();
+ double oscFreq = osc.frequency.get();
+ String msg = "freq at " + rate + " Hz";
+ LOGGER.debug(msg);
+ assertEquals(oscFreq, frequencyMeasured, oscFreq * 0.1, msg);
+ assertEquals(0.9, confidenceMeasured, 0.1, "pitch confidence");
+
+ double expectedCount = interval * oscFreq;
+ double framesMeasured = counter.getCount() - previousFrameCount;
+ msg = "count at " + rate + " Hz";
+ LOGGER.debug(msg);
+ assertEquals(expectedCount, framesMeasured, expectedCount * 0.1, msg);
+
+ synth.stop();
+ }
+
+ }
+
+ @Test
+ public void testScheduler() throws InterruptedException {
+ SynthesisEngine synth = new SynthesisEngine();
+ synth.setRealTime(false);
+ Add adder = new Add();
+ synth.add(adder);
+ synth.start();
+ adder.start();
+ adder.inputA.set(4.0);
+ adder.inputB.set(10.0);
+ synth.sleepFor(0.1);
+ assertEquals(14.0, adder.output.get(), 0.01, "simple add");
+
+ // Schedule a set() in the near future.
+ double time = synth.getCurrentTime();
+ adder.inputA.set(7.0, time + 1.0);
+ synth.sleepFor(0.5);
+ assertEquals(14.0, adder.output.get(), 0.01, "before scheduled set");
+ synth.sleepFor(1.0);
+ assertEquals(17.0, adder.output.get(), 0.01, "after scheduled set");
+
+ // Schedule a set() in the near future then cancel it.
+ time = synth.getCurrentTime();
+ adder.inputA.set(5.0, time + 1.0);
+ synth.sleepFor(0.5);
+ assertEquals(17.0, adder.output.get(), 0.01, "before scheduled set");
+ synth.clearCommandQueue();
+ synth.sleepFor(1.0);
+ assertEquals(17.0, adder.output.get(), 0.01, "after canceled set");
+
+ synth.stop();
+ }
+}
diff --git a/src/test/java/com/jsyn/engine/TestFifo.java b/src/test/java/com/jsyn/engine/TestFifo.java
new file mode 100644
index 0000000..d057e19
--- /dev/null
+++ b/src/test/java/com/jsyn/engine/TestFifo.java
@@ -0,0 +1,245 @@
+/*
+ * 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.engine;
+
+import com.jsyn.io.AudioFifo;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class TestFifo {
+
+ @Test
+ public void testBasic() {
+ Thread watchdog = startWatchdog(600);
+
+ AudioFifo fifo = new AudioFifo();
+ fifo.setReadWaitEnabled(false);
+ fifo.allocate(8);
+ assertEquals(0, fifo.available(), "start empty");
+
+ assertEquals(Double.NaN, fifo.read(), "read back Nan when emopty");
+
+ fifo.write(1.0);
+ assertEquals(1, fifo.available(), "added one value");
+ assertEquals(1.0, fifo.read(), "read back same value");
+ assertEquals(0, fifo.available(), "back to empty");
+
+ for (int i = 0; i < fifo.size(); i++) {
+ assertEquals(i, fifo.available(), "adding data");
+ fifo.write(100.0 + i);
+ }
+ for (int i = 0; i < fifo.size(); i++) {
+ assertEquals(fifo.size() - i, fifo.available(), "removing data");
+ assertEquals(100.0 + i, fifo.read(), "reading back data");
+ }
+ watchdog.interrupt();
+ }
+
+ /**
+ * Wrap around several times to test masking.
+ */
+ @Test
+ public void testWrapping() {
+
+ final int chunk = 5;
+ AudioFifo fifo = new AudioFifo();
+ fifo.allocate(8);
+ double value = 1000.0;
+ for (int i = 0; i < (fifo.size() * chunk); i++) {
+ value = checkFifoChunk(fifo, value, chunk);
+ }
+
+ }
+
+ private double checkFifoChunk(AudioFifo fifo, double value, int chunk) {
+ for (int i = 0; i < chunk; i++) {
+ assertEquals(i, fifo.available(), "adding data");
+ fifo.write(value + i);
+ }
+ for (int i = 0; i < chunk; i++) {
+ assertEquals(chunk - i, fifo.available(), "removing data");
+ assertEquals(value + i, fifo.read(), "reading back data");
+ }
+ return value + chunk;
+ }
+
+ @Test
+ public void testBadSize() {
+ try {
+ AudioFifo fifo = new AudioFifo();
+ fifo.allocate(20); // not power of 2
+ fail("should not get here");
+ } catch (IllegalArgumentException ignored) {
+ return;
+ }
+
+ fail("should have caught size exception");
+ }
+
+ @Test
+ public void testSingleReadWait() {
+ final int chunk = 5;
+ final AudioFifo fifo = new AudioFifo();
+ fifo.allocate(8);
+
+ fifo.setWriteWaitEnabled(false);
+ fifo.setReadWaitEnabled(true);
+ final double value = 50.0;
+
+ // Schedule a delayed write in another thread.
+ new Thread(() -> {
+ try {
+ Thread.sleep(200);
+ for (int i = 0; i < chunk; i++) {
+ fifo.write(value + i);
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }).start();
+
+ Thread watchdog = startWatchdog(500);
+ for (int i = 0; i < chunk; i++) {
+ assertEquals(value + i, fifo.read(), "reading back data");
+ }
+ watchdog.interrupt();
+ }
+
+ private Thread startWatchdog(final int msec) {
+ Thread watchdog = new Thread(() -> {
+ try {
+ Thread.sleep(msec);
+ fail("test must still be waiting");
+ } catch (InterruptedException ignored) {
+ }
+ });
+ watchdog.start();
+ return watchdog;
+ }
+
+ @Test
+ public void testSingleWriteWait() {
+ final int chunk = 13;
+ final AudioFifo fifo = new AudioFifo();
+ fifo.allocate(8);
+
+ fifo.setWriteWaitEnabled(true);
+ fifo.setReadWaitEnabled(true);
+ final double value = 50.0;
+
+ // Schedule a delayed read in another thread.
+ Thread readThread = new Thread(() -> {
+ try {
+ Thread.sleep(200);
+ for (int i = 0; i < chunk; i++) {
+ // LOGGER.debug( "testSingleWriteWait: try to read" );
+ double got = fifo.read();
+ assertEquals(value + i, got, "adding data");
+ // LOGGER.debug( "testSingleWriteWait: read " + got );
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ });
+ readThread.start();
+
+ Thread watchdog = startWatchdog(500);
+ // Try to write more than will fit so we will hang.
+ for (int i = 0; i < chunk; i++) {
+ fifo.write(value + i);
+ }
+ watchdog.interrupt();
+
+ try {
+ readThread.join(200);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ assertFalse(readThread.isAlive(), "readThread should be done.");
+ }
+
+ @Test
+ public void testBlockReadWait() {
+ final int chunk = 50;
+ final AudioFifo fifo = new AudioFifo();
+ fifo.allocate(8);
+
+ fifo.setWriteWaitEnabled(false);
+ fifo.setReadWaitEnabled(true);
+ final double value = 300.0;
+ double[] readBuffer = new double[chunk];
+
+ // Schedule delayed writes in another thread.
+ new Thread(() -> {
+ int numWritten = 0;
+ double[] writeBuffer = new double[4];
+ try {
+ while (numWritten < chunk) {
+ Thread.sleep(30);
+ for (int i = 0; i < writeBuffer.length; i++) {
+ writeBuffer[i] = value + numWritten;
+ numWritten += 1;
+ }
+
+ fifo.write(writeBuffer);
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }).start();
+
+ Thread watchdog = startWatchdog(600);
+ fifo.read(readBuffer);
+ for (int i = 0; i < chunk; i++) {
+ assertEquals(value + i, readBuffer[i], "reading back data");
+ }
+ watchdog.interrupt();
+
+ }
+
+ @Test
+ public void testBlockReadAndWriteWaitStress() {
+ final int chunk = 10000000; // 10 Megabytes
+ final AudioFifo fifo = new AudioFifo();
+ fifo.allocate(8);
+
+ fifo.setWriteWaitEnabled(true);
+ fifo.setReadWaitEnabled(true);
+ final double value = 50.0;
+
+ // Schedule a delayed write in another thread.
+ new Thread(() -> {
+ try {
+ Thread.sleep(200);
+ for (int i = 0; i < chunk; i++) {
+ fifo.write(value + i);
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }).start();
+
+ Thread watchdog = startWatchdog(10000);
+ for (int i = 0; i < chunk; i++) {
+ assertEquals(value + i, fifo.read(), "reading back data");
+ }
+ watchdog.interrupt();
+ }
+}
diff --git a/src/test/java/com/jsyn/engine/TestWaveFileReadWrite.java b/src/test/java/com/jsyn/engine/TestWaveFileReadWrite.java
new file mode 100644
index 0000000..b5051e6
--- /dev/null
+++ b/src/test/java/com/jsyn/engine/TestWaveFileReadWrite.java
@@ -0,0 +1,114 @@
+/*
+ * 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.engine;
+
+import java.io.File;
+import java.io.IOException;
+
+import com.jsyn.data.FloatSample;
+import com.jsyn.util.SampleLoader;
+import com.jsyn.util.WaveFileWriter;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class TestWaveFileReadWrite {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TestWaveFileReadWrite.class);
+
+ public void checkWriteReadWave(int numChannels, float[] data) throws IOException {
+ File temp = File.createTempFile("test_wave", ".wav");
+ temp.deleteOnExit();
+ LOGGER.debug("Creating wave file " + temp);
+
+ WaveFileWriter writer = new WaveFileWriter(temp);
+ writer.setFrameRate(44100);
+ writer.setSamplesPerFrame(numChannels);
+ writer.setBitsPerSample(16);
+
+ for (var datum : data) {
+ writer.write(datum);
+ }
+ writer.close();
+
+ // TODO Make sure blow up if writing after close.
+ // writer.write( 0.7 );
+
+ FloatSample sample = SampleLoader.loadFloatSample(temp);
+ assertEquals(numChannels, sample.getChannelsPerFrame(), "stereo");
+ assertEquals(44100.0, sample.getFrameRate(), "frame rate");
+
+ for (int i = 0; i < data.length; i++) {
+ float v = data[i];
+ if (v > 1.0)
+ v = 1.0f;
+ else if (v < -1.0)
+ v = -1.0f;
+ assertEquals(v, sample.readDouble(i), 0.0001, "sample data");
+ }
+
+ }
+
+ @Test
+ public void testRamp() throws IOException {
+ float[] data = new float[200];
+ for (int i = 0; i < data.length; i++) {
+ data[i] = i / 1000.0f;
+ }
+
+ checkWriteReadWave(2, data);
+ }
+
+ @Test
+ public void testClippedSine() throws IOException {
+ float[] data = new float[200];
+ for (int i = 0; i < data.length; i++) {
+ double phase = i * Math.PI * 2.0 / 100;
+ data[i] = (float) (1.3 * Math.sin(phase));
+ }
+
+ checkWriteReadWave(2, data);
+ }
+
+ @Test
+ public void testArguments() throws IOException {
+ File temp = File.createTempFile("test_wave", ".wav");
+ temp.deleteOnExit();
+ LOGGER.debug("Creating wave file " + temp);
+
+ WaveFileWriter writer = new WaveFileWriter(temp);
+ writer.setBitsPerSample(16);
+ assertEquals(16, writer.getBitsPerSample(), "bitsPerSample");
+ writer.setBitsPerSample(24);
+ assertEquals(24, writer.getBitsPerSample(), "bitsPerSample");
+ try {
+ writer.setBitsPerSample(17);
+ fail("tried setting illegal value");
+ } catch (IllegalArgumentException e) {
+ // e.printStackTrace();
+ return;
+ } finally {
+ writer.close();
+ }
+
+ fail("17 generated exception");
+ }
+
+}
diff --git a/src/test/java/com/jsyn/midi/TestMidiLoop.java b/src/test/java/com/jsyn/midi/TestMidiLoop.java
new file mode 100644
index 0000000..fa7ba2c
--- /dev/null
+++ b/src/test/java/com/jsyn/midi/TestMidiLoop.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2010 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.midi;
+
+import javax.sound.midi.MidiDevice;
+import javax.sound.midi.MidiMessage;
+import javax.sound.midi.MidiUnavailableException;
+import javax.sound.midi.Receiver;
+
+import com.jsyn.devices.javasound.MidiDeviceTools;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Connect a USB MIDI Keyboard to the internal MIDI Synthesizer using JavaSound.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class TestMidiLoop {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TestMidiLoop.class);
+
+ @Test
+ private void midiLoop() {
+ try {
+ for (int result = 0, i = 0; i < 3 && result == 0; i++) {
+ result = test();
+ }
+ } catch (MidiUnavailableException | InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ // Write a Receiver to get the messages from a Transmitter.
+ static class CustomReceiver implements Receiver {
+ @Override
+ public void close() {
+ System.out.print("Receiver.close() was called.");
+ }
+
+ @Override
+ public void send(MidiMessage message, long timeStamp) {
+ byte[] bytes = message.getMessage();
+ LOGGER.debug("Got " + bytes.length + " bytes.");
+ }
+ }
+
+ public int test() throws MidiUnavailableException, InterruptedException {
+
+ int result = -1;
+ MidiDevice keyboard = MidiDeviceTools.findKeyboard();
+ Receiver receiver = new CustomReceiver();
+ // Just use default synthesizer.
+ if (keyboard != null) {
+ // If you forget to open them you will hear no sound.
+ keyboard.open();
+ // Put the receiver in the transmitter.
+ // This gives fairly low latency playing.
+ keyboard.getTransmitter().setReceiver(receiver);
+ LOGGER.debug("Play MIDI keyboard: " + keyboard.getDeviceInfo().getDescription());
+ result = 0;
+ Thread.sleep(4000);
+ LOGGER.debug("Close the keyboard. It may not work after this according to the docs!");
+ keyboard.close();
+ } else {
+ LOGGER.debug("Could not find a keyboard.");
+ }
+ return result;
+ }
+
+
+}
diff --git a/src/test/java/com/jsyn/ports/TestQueuedDataPort.java b/src/test/java/com/jsyn/ports/TestQueuedDataPort.java
new file mode 100644
index 0000000..65c0127
--- /dev/null
+++ b/src/test/java/com/jsyn/ports/TestQueuedDataPort.java
@@ -0,0 +1,549 @@
+/*
+ * 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.ports;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.data.FloatSample;
+import com.jsyn.data.SequentialData;
+import com.jsyn.data.ShortSample;
+import com.jsyn.unitgen.FixedRateMonoReader;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Test sample and envelope queuing and looping.
+ *
+ * @author Phil Burk, (C) 2009 Mobileer Inc
+ */
+public class TestQueuedDataPort {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TestQueuedDataPort.class);
+
+ private static Synthesizer synth;
+ private static final float[] floatData = {
+ 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f
+ };
+ private static FloatSample floatSample;
+ private static FixedRateMonoReader reader;
+
+ @BeforeAll
+ private static void setUp() {
+ synth = JSyn.createSynthesizer();
+ synth.setRealTime(false);
+ synth.start();
+ }
+
+ @AfterAll
+ private static void tearDown() {
+ synth.stop();
+ }
+
+ private void queueDirect(UnitDataQueuePort port, SequentialData data, int startFrame,
+ int numFrames) {
+ queueDirect(port, data, startFrame, numFrames, 0);
+ }
+
+ private void queueDirect(UnitDataQueuePort port, SequentialData data, int startFrame,
+ int numFrames, int numLoops) {
+ QueueDataCommand command = port.createQueueDataCommand(data, startFrame, numFrames);
+ command.setNumLoops(numLoops);
+ port.addQueuedBlock(command);
+ }
+
+ @Test
+ public void testQueueSingleShort() {
+ short[] data = {
+ 234, -9876, 4567
+ };
+ ShortSample sample = new ShortSample(data.length, 1);
+ sample.write(data);
+
+ UnitDataQueuePort dataQueue = new UnitDataQueuePort("test");
+ assertFalse(dataQueue.hasMore(), "start empty");
+
+ queueDirect(dataQueue, sample, 0, data.length);
+ checkQueuedData(data, dataQueue, 0, data.length);
+
+ assertFalse(dataQueue.hasMore(), "end empty");
+ }
+
+ @Test
+ public void testQueueSingleFloat() {
+ float[] data = {
+ 0.4f, 1.9f, 22.7f
+ };
+ FloatSample sample = new FloatSample(data.length, 1);
+ sample.write(data);
+
+ UnitDataQueuePort dataQueue = new UnitDataQueuePort("test");
+ assertFalse(dataQueue.hasMore(), "start empty");
+
+ queueDirect(dataQueue, sample, 0, data.length);
+ checkQueuedData(data, dataQueue, 0, data.length);
+
+ assertFalse(dataQueue.hasMore(), "end empty");
+ }
+
+ @Test
+ public void testQueueOutOfBounds() {
+ float[] data = {
+ 0.4f, 1.9f, 22.7f
+ };
+ FloatSample sample = new FloatSample(data.length, 1);
+ sample.write(data);
+
+ UnitDataQueuePort dataQueue = new UnitDataQueuePort("test");
+ boolean caught = false;
+ try {
+ queueDirect(dataQueue, sample, 0, sample.getNumFrames() + 1); // should cause an error!
+ } catch(IllegalArgumentException e) {
+ caught = true;
+ }
+ assertTrue(caught, "expect exception when we go past end of the array");
+
+ caught = false;
+ try {
+ queueDirect(dataQueue, sample, 1, sample.getNumFrames()); // should cause an error!
+ } catch(IllegalArgumentException e) {
+ caught = true;
+ }
+ assertTrue(caught, "expect exception when we go past end of the array");
+
+ caught = false;
+ try {
+ queueDirect(dataQueue, sample, -1, sample.getNumFrames()); // should cause an error!
+ } catch(IllegalArgumentException e) {
+ caught = true;
+ }
+ assertTrue(caught, "expect exception when we start before beginning of the array");
+ }
+
+ @Test
+ public void testQueueMultiple() {
+ short[] data = {
+ 234, 17777, -9876, 4567, -14287
+ };
+ ShortSample sample = new ShortSample(data.length, 1);
+ sample.write(data);
+
+ UnitDataQueuePort dataQueue = new UnitDataQueuePort("test");
+ assertFalse(dataQueue.hasMore(), "start empty");
+
+ queueDirect(dataQueue, sample, 1, 3);
+ queueDirect(dataQueue, sample, 0, 5);
+ queueDirect(dataQueue, sample, 2, 2);
+
+ checkQueuedData(data, dataQueue, 1, 3);
+ checkQueuedData(data, dataQueue, 0, 5);
+ checkQueuedData(data, dataQueue, 2, 2);
+
+ assertFalse(dataQueue.hasMore(), "end empty");
+ }
+
+ @Test
+ public void testQueueNoLoops() throws InterruptedException {
+ LOGGER.debug("testQueueNoLoops() ================");
+ UnitDataQueuePort dataQueue = setupFloatSample();
+
+ dataQueue.queueOn(floatSample, synth.createTimeStamp());
+ // Advance synth so that the queue command propagates to the engine.
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ // play entire sample
+ checkQueuedData(floatData, dataQueue, 0, floatData.length);
+
+ assertFalse(dataQueue.hasMore(), "end empty");
+ }
+
+ @Test
+ public void testQueueLoopForever() throws InterruptedException {
+ LOGGER.debug("testQueueLoopForever() ================");
+
+ UnitDataQueuePort dataQueue = setupFloatSample();
+
+ dataQueue.queue(floatSample, 0, 3);
+ dataQueue.queueLoop(floatSample, 3, 4);
+
+ // Advance synth so that the queue commands propagate to the engine.
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ checkQueuedData(floatData, dataQueue, 0, 3);
+ checkQueuedData(floatData, dataQueue, 3, 4);
+ checkQueuedData(floatData, dataQueue, 3, 4);
+ checkQueuedData(floatData, dataQueue, 3, 4);
+ checkQueuedData(floatData, dataQueue, 3, 1);
+
+ // queue final release
+ dataQueue.queue(floatSample, 3, 5);
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+ // current loop will finish
+ checkQueuedData(floatData, dataQueue, 4, 3);
+ // release portion will play
+ checkQueuedData(floatData, dataQueue, 3, 5);
+
+ assertFalse(dataQueue.hasMore(), "end empty");
+ }
+
+ @Test
+ public void testQueueLoopAtLeastOnce() throws InterruptedException {
+ LOGGER.debug("testQueueLoopAtLeastOnce() ================");
+
+ UnitDataQueuePort dataQueue = setupFloatSample();
+
+ dataQueue.queue(floatSample, 0, 3);
+ dataQueue.queueLoop(floatSample, 3, 2); // this should play at least once
+ dataQueue.queue(floatSample, 5, 2);
+
+ // Advance synth so that the queue commands propagate to the engine.
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ checkQueuedData(floatData, dataQueue, 0, 3);
+ checkQueuedData(floatData, dataQueue, 3, 2);
+ checkQueuedData(floatData, dataQueue, 5, 2);
+
+ assertFalse(dataQueue.hasMore(), "end empty");
+ }
+
+ @Test
+ public void testQueueNumLoops() throws InterruptedException {
+ LOGGER.debug("testQueueNumLoops() ================");
+ UnitDataQueuePort dataQueue = setupFloatSample();
+
+ dataQueue.queue(floatSample, 0, 2);
+
+ int numLoopsA = 5;
+ dataQueue.queueLoop(floatSample, 2, 3, numLoopsA);
+
+ dataQueue.queue(floatSample, 4, 2);
+
+ int numLoopsB = 3;
+ dataQueue.queueLoop(floatSample, 3, 4, numLoopsB);
+
+ dataQueue.queue(floatSample, 5, 2);
+
+ // Advance synth so that the queue commands propagate to the engine.
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ checkQueuedData(floatData, dataQueue, 0, 2);
+ for (int i = 0; i < (numLoopsA + 1); i++) {
+ LOGGER.debug("loop A #" + i);
+ checkQueuedData(floatData, dataQueue, 2, 3);
+ }
+ checkQueuedData(floatData, dataQueue, 4, 2);
+ for (int i = 0; i < (numLoopsB + 1); i++) {
+ LOGGER.debug("loop B #" + i);
+ checkQueuedData(floatData, dataQueue, 3, 4);
+ }
+
+ checkQueuedData(floatData, dataQueue, 5, 2);
+
+ assertFalse(dataQueue.hasMore(), "end empty");
+ }
+
+ private UnitDataQueuePort setupFloatSample() {
+ floatSample = new FloatSample(floatData.length, 1);
+ floatSample.write(floatData);
+
+ synth.add(reader = new FixedRateMonoReader());
+ UnitDataQueuePort dataQueue = reader.dataQueue;
+ assertFalse(dataQueue.hasMore(), "start empty");
+ return dataQueue;
+ }
+
+ @Test
+ public void testQueueSustainLoop() throws InterruptedException {
+ LOGGER.debug("testQueueSustainLoop() ================");
+
+ UnitDataQueuePort dataQueue = setupFloatSample();
+
+ // set up sustain loops ===========================
+ floatSample.setSustainBegin(2);
+ floatSample.setSustainEnd(4);
+ floatSample.setReleaseBegin(-1);
+ floatSample.setReleaseEnd(-1);
+
+ dataQueue.queueOn(floatSample, synth.createTimeStamp());
+ // Advance synth so that the queue command propagates to the engine.
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ checkQueuedData(floatData, dataQueue, 0, 2);
+ checkQueuedData(floatData, dataQueue, 2, 2);
+ checkQueuedData(floatData, dataQueue, 2, 2);
+ checkQueuedData(floatData, dataQueue, 2, 1); // looping
+
+ dataQueue.queueOff(floatSample, true); // queue off in middle of loop
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ checkQueuedData(floatData, dataQueue, 3, 5); // release
+ assertFalse(dataQueue.hasMore(), "end empty");
+ }
+
+ @Test
+ public void testQueueReleaseLoop() throws InterruptedException {
+ LOGGER.debug("testQueueReleaseLoop() ================");
+ UnitDataQueuePort dataQueue = setupFloatSample();
+
+ // set up sustain loops ===========================
+ floatSample.setSustainBegin(-1);
+ floatSample.setSustainEnd(-1);
+ floatSample.setReleaseBegin(4);
+ floatSample.setReleaseEnd(6);
+
+ dataQueue.queueOn(floatSample, synth.createTimeStamp());
+ // Advance synth so that the queue command propagates to the engine.
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ checkQueuedData(floatData, dataQueue, 0, 4);
+ checkQueuedData(floatData, dataQueue, 4, 2);
+ checkQueuedData(floatData, dataQueue, 4, 2);
+ checkQueuedData(floatData, dataQueue, 4, 2); // looping in release cuz no
+ // sustain loop
+
+ dataQueue.queueOff(floatSample, true); // queue off in middle of loop
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ checkQueuedData(floatData, dataQueue, 4, 2);
+ checkQueuedData(floatData, dataQueue, 4, 2); // still looping
+ assertTrue(dataQueue.hasMore(), "end full");
+ }
+
+ @Test
+ public void testQueueSustainReleaseLoops() throws InterruptedException {
+ LOGGER.debug("testQueueSustainReleaseLoops() ================");
+ UnitDataQueuePort dataQueue = setupFloatSample();
+
+ // set up sustain loops ===========================
+ floatSample.setSustainBegin(2);
+ floatSample.setSustainEnd(4);
+ floatSample.setReleaseBegin(5);
+ floatSample.setReleaseEnd(7);
+
+ dataQueue.queueOn(floatSample, synth.createTimeStamp());
+ // Advance synth so that the queue command propagates to the engine.
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ checkQueuedData(floatData, dataQueue, 0, 4);
+ checkQueuedData(floatData, dataQueue, 2, 2);
+ checkQueuedData(floatData, dataQueue, 2, 1); // middle of sustain loop
+
+ dataQueue.queueOff(floatSample, true); // queue off in middle of loop
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ checkQueuedData(floatData, dataQueue, 3, 2);
+ checkQueuedData(floatData, dataQueue, 5, 2); // release loop
+ checkQueuedData(floatData, dataQueue, 5, 2); // release loop
+ assertTrue(dataQueue.hasMore(), "end full");
+ }
+
+ @Test
+ private void checkQueuedData(short[] data, UnitDataQueuePort dataQueue, int offset,
+ int numFrames) {
+ for (int i = 0; i < numFrames; i++) {
+ assertTrue(dataQueue.hasMore(), "got data");
+ double value = dataQueue.readNextMonoDouble(synth.getFramePeriod());
+ assertEquals(data[i + offset] / 32768.0, value, 0.0001, "data matches");
+ }
+ }
+
+ private void checkQueuedData(float[] data, UnitDataQueuePort dataQueue, int offset,
+ int numFrames) {
+ for (int i = 0; i < numFrames; i++) {
+ assertTrue(dataQueue.hasMore(), "got data");
+ double value = dataQueue.readNextMonoDouble(synth.getFramePeriod());
+ assertEquals(data[i + offset], value, 0.0001, "data matches");
+ }
+ }
+
+ static class TestQueueCallback implements UnitDataQueueCallback {
+ boolean gotStarted = false;
+ boolean gotLooped = false;
+ boolean gotFinished = false;
+ QueueDataEvent lastEvent;
+
+ @Override
+ public void started(QueueDataEvent event) {
+ LOGGER.debug("Callback started.");
+ gotStarted = true;
+ lastEvent = event;
+ }
+
+ @Override
+ public void looped(QueueDataEvent event) {
+ LOGGER.debug("Callback looped.");
+ gotLooped = true;
+ lastEvent = event;
+ }
+
+ @Override
+ public void finished(QueueDataEvent event) {
+ LOGGER.debug("Callback finished.");
+ gotFinished = true;
+ lastEvent = event;
+ }
+ }
+
+ @Test
+ public void testQueueCallback() {
+ float[] data = {
+ 0.2f, -8.9f, 2.7f
+ };
+ FloatSample sample = new FloatSample(data.length, 1);
+ sample.write(data);
+
+ UnitDataQueuePort dataQueue = new UnitDataQueuePort("test");
+ assertFalse(dataQueue.hasMore(), "start empty");
+
+ // Create an object to be called when the queued data is done.
+ TestQueueCallback callback = new TestQueueCallback();
+
+ QueueDataCommand command = dataQueue.createQueueDataCommand(sample, 0, data.length);
+ command.setCallback(callback);
+ command.setNumLoops(2);
+ dataQueue.addQueuedBlock(command);
+
+ // Check to see if flags get set true by callback.
+ dataQueue.firePendingCallbacks();
+ assertFalse(callback.gotStarted, "not started yet");
+ assertFalse(callback.gotLooped, "not looped yet");
+ assertFalse(callback.gotFinished, "not finished yet");
+
+ checkQueuedData(data, dataQueue, 0, 1);
+ dataQueue.firePendingCallbacks();
+ assertTrue(callback.gotStarted, "should be started now");
+ assertFalse(callback.gotLooped, "not looped yet");
+ assertFalse(callback.gotFinished, "not finished yet");
+ assertEquals(dataQueue, callback.lastEvent.getSource(), "check source of event");
+ assertEquals(sample, callback.lastEvent.getSequentialData(), "check sample");
+ assertEquals(2, callback.lastEvent.getLoopsLeft(), "check loopCount");
+
+ checkQueuedData(data, dataQueue, 1, data.length - 1);
+ dataQueue.firePendingCallbacks();
+ assertTrue(callback.gotLooped, "should be looped now");
+ assertEquals(1, callback.lastEvent.getLoopsLeft(), "check loopCount");
+ assertFalse(callback.gotFinished, "not finished yet");
+
+ checkQueuedData(data, dataQueue, 0, data.length);
+ dataQueue.firePendingCallbacks();
+ assertEquals(0, callback.lastEvent.getLoopsLeft(), "check loopCount");
+
+ checkQueuedData(data, dataQueue, 0, data.length);
+ dataQueue.firePendingCallbacks();
+ assertTrue(callback.gotFinished, "should be finished now");
+
+ assertFalse(dataQueue.hasMore(), "end empty");
+ }
+
+ @Test
+ public void testImmediate() {
+ float[] data = {
+ 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f
+ };
+ FloatSample sample = new FloatSample(data.length, 1);
+ sample.write(data);
+
+ UnitDataQueuePort dataQueue = new UnitDataQueuePort("test");
+ dataQueue.queue(sample);
+
+ // Only play some of the data then interrupt it with an immediate block.
+ checkQueuedData(data, dataQueue, 0, 3);
+
+ QueueDataCommand command = dataQueue.createQueueDataCommand(sample, 7, 3);
+ command.setImmediate(true);
+ command.run(); // execute "immediate" operation and add to block list
+
+ // Should already be in new data.
+ checkQueuedData(data, dataQueue, 7, 3);
+ }
+
+ @Test
+ public void testCrossFade() {
+ float[] data1 = {
+ 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f
+ };
+ float[] data2 = {
+ 20.0f, 19.0f, 18.0f, 17.0f, 16.0f, 15.0f, 14.0f, 13.0f, 12.0f, 11.0f
+ };
+ FloatSample sample1 = new FloatSample(data1);
+ FloatSample sample2 = new FloatSample(data2);
+
+ UnitDataQueuePort dataQueue = new UnitDataQueuePort("test");
+ dataQueue.queue(sample1, 0, 4);
+
+ QueueDataCommand command = dataQueue.createQueueDataCommand(sample2, 1, 8);
+ command.setCrossFadeIn(3);
+ command.run(); // execute "immediate" operation and add to block list
+
+ // Only play some of the data then crossfade to another sample.
+ checkQueuedData(data1, dataQueue, 0, 4);
+
+ for (int i = 0; i < 3; i++) {
+ double factor = i / 3.0;
+ double value = ((1.0 - factor) * data1[i + 4]) + (factor * data2[i + 1]);
+ LOGGER.debug("i = " + i + ", factor = " + factor + ", value = " + value);
+
+ double actual = dataQueue.readNextMonoDouble(synth.getFramePeriod());
+ assertEquals(value, actual, 0.00001, "crossfade " + i);
+ }
+
+ // Should already be in new data.
+ checkQueuedData(data2, dataQueue, 4, 5);
+ }
+
+ @Test
+ public void testImmediateCrossFade() {
+ float[] data1 = {
+ 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f
+ };
+ float[] data2 = {
+ 20.0f, 19.0f, 18.0f, 17.0f, 16.0f, 15.0f, 14.0f, 13.0f, 12.0f, 11.0f
+ };
+ FloatSample sample1 = new FloatSample(data1);
+ FloatSample sample2 = new FloatSample(data2);
+
+ UnitDataQueuePort dataQueue = new UnitDataQueuePort("test");
+ dataQueue.queue(sample1, 0, 4);
+
+ // Only play some of the data then crossfade to another sample.
+ int beforeInterrupt = 2;
+ checkQueuedData(data1, dataQueue, 0, beforeInterrupt);
+
+ QueueDataCommand command = dataQueue.createQueueDataCommand(sample2, 1, 8);
+ command.setImmediate(true);
+ command.setCrossFadeIn(3);
+ command.run(); // execute "immediate" operation and add to block list
+
+ for (int i = 0; i < 3; i++) {
+ double factor = i / 3.0;
+ double value = ((1.0 - factor) * data1[i + beforeInterrupt]) + (factor * data2[i + 1]);
+ LOGGER.debug("i = " + i + ", factor = " + factor + ", value = " + value);
+
+ double actual = dataQueue.readNextMonoDouble(synth.getFramePeriod());
+ assertEquals(value, actual, 0.00001, "crossfade " + i);
+ }
+
+ // Should already be in new data.
+ checkQueuedData(data2, dataQueue, 4, 5);
+ }
+}
diff --git a/src/test/java/com/jsyn/ports/TestSequentialData.java b/src/test/java/com/jsyn/ports/TestSequentialData.java
new file mode 100644
index 0000000..2f27ec2
--- /dev/null
+++ b/src/test/java/com/jsyn/ports/TestSequentialData.java
@@ -0,0 +1,55 @@
+/*
+ * 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.ports;
+
+import com.jsyn.data.FloatSample;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class TestSequentialData {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TestSequentialData.class);
+
+ private final static float[] data1 = {
+ 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f
+ };
+
+ private final static float[] data2 = {
+ 20.0f, 19.0f, 18.0f, 17.0f, 16.0f, 15.0f, 14.0f, 13.0f, 12.0f, 11.0f
+ };
+
+ @Test
+ public void testCrossfade() {
+ var sample1 = new FloatSample(data1);
+ var sample2 = new FloatSample(data2);
+ SequentialDataCrossfade xfade = new SequentialDataCrossfade();
+ xfade.setup(sample1, 4, 3, sample2, 1, 6);
+
+ for (int i = 0; i < 3; i++) {
+ double factor = i / 3.0;
+ double value = ((1.0 - factor) * data1[i + 4]) + (factor * data2[i + 1]);
+ LOGGER.debug("i = " + i + ", factor = " + factor + ", value = " + value);
+ assertEquals(value, xfade.readDouble(i), 0.00001, "crossfade " + i);
+ }
+ for (int i = 3; i < 6; i++) {
+ assertEquals(sample2.readDouble(i + 1), xfade.readDouble(i), 0.00001, "crossfade " + i);
+ }
+ }
+}
diff --git a/src/test/java/com/jsyn/ports/TestSet.java b/src/test/java/com/jsyn/ports/TestSet.java
new file mode 100644
index 0000000..be426b7
--- /dev/null
+++ b/src/test/java/com/jsyn/ports/TestSet.java
@@ -0,0 +1,89 @@
+/*
+ * 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.ports;
+
+import com.jsyn.engine.SynthesisEngine;
+import com.jsyn.unitgen.Minimum;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class TestSet {
+
+ /** Internal value setting. */
+ @Test
+ public void testSetValue() {
+ int numParts = 4;
+ UnitInputPort port = new UnitInputPort(numParts, "Tester");
+ port.setValueInternal(0, 100.0);
+ port.setValueInternal(2, 120.0);
+ port.setValueInternal(1, 110.0);
+ port.setValueInternal(3, 130.0);
+ assertEquals(100.0, port.getValue(0), "check port value");
+ assertEquals(120.0, port.getValue(2), "check port value");
+ assertEquals(110.0, port.getValue(1), "check port value");
+ assertEquals(130.0, port.getValue(3), "check port value");
+ }
+
+ @Test
+ public void testSet() throws InterruptedException {
+ SynthesisEngine synthesisEngine = new SynthesisEngine();
+ synthesisEngine.setRealTime(false);
+ synthesisEngine.start();
+ synthesisEngine.sleepUntil(0.01);
+ Minimum min;
+ synthesisEngine.add(min = new Minimum());
+
+ double x = 33.99;
+ double y = 8.31;
+ min.inputA.set(x);
+ min.inputB.set(y);
+ synthesisEngine.sleepFor(0.01);
+ assertEquals(x, min.inputA.getValue(), "min set A");
+ assertEquals(y, min.inputB.getValue(), "min set B");
+ min.start();
+ synthesisEngine.sleepFor(0.01);
+
+ assertEquals(y, min.output.getValue(), "min output");
+ synthesisEngine.stop();
+ }
+
+ /** if we use a port index out of range we want to know now and not blow up the engine. */
+ @Test
+ public void testSetBadPort() throws InterruptedException {
+ SynthesisEngine synthesisEngine = new SynthesisEngine();
+ synthesisEngine.setRealTime(false);
+ synthesisEngine.start();
+ Minimum min;
+ synthesisEngine.add(min = new Minimum());
+
+ min.start();
+ try {
+ min.inputA.set(1, 23.45);
+ } catch (ArrayIndexOutOfBoundsException ignored) {
+ } catch (Exception e) {
+ fail("Catch port out of range, caught " + e);
+ }
+
+ // Don't blow up here.
+ synthesisEngine.sleepUntil(0.01);
+
+ synthesisEngine.stop();
+ }
+
+}
diff --git a/src/test/java/com/jsyn/research/BenchMultiThreading.java b/src/test/java/com/jsyn/research/BenchMultiThreading.java
new file mode 100644
index 0000000..24624c5
--- /dev/null
+++ b/src/test/java/com/jsyn/research/BenchMultiThreading.java
@@ -0,0 +1,152 @@
+/*
+ * 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.research;
+
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+// TODO: Use thread pools, or maybe JMH?
+public class BenchMultiThreading {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(BenchMultiThreading.class);
+
+ private static final int FRAMES_PER_BLOCK = 64;
+ int numThreads = 4;
+ int numLoops = 100000;
+ private ArrayList<CustomThread> threadList;
+
+ static class CustomThread extends Thread {
+ long frameCount = 0;
+ long desiredFrame = 0;
+ Object semaphore = new Object();
+ Object goSemaphore = new Object();
+ volatile boolean go = true;
+ long startNano;
+ long stopNano;
+ long maxElapsed;
+
+ @Override
+ public void run() {
+ try {
+ startNano = System.nanoTime();
+ while (go) {
+ // Watch for long delays.
+ stopNano = System.nanoTime();
+ long elapsed = stopNano - startNano;
+ startNano = System.nanoTime();
+ if (elapsed > maxElapsed) {
+ maxElapsed = elapsed;
+ }
+
+ synchronized (semaphore) {
+ // Audio synthesis would occur here.
+ frameCount += 1;
+ // LOGGER.debug( this + " generating frame " +
+ // frameCount );
+ semaphore.notify();
+ }
+ synchronized (goSemaphore) {
+ while (desiredFrame <= frameCount) {
+ goSemaphore.wait();
+ }
+ }
+ long stopNano = System.nanoTime();
+ }
+ } catch (InterruptedException e) {
+ LOGGER.debug("CustomThread interrupted. ");
+ }
+ LOGGER.debug("Finishing " + this);
+ }
+
+ public void abort() {
+ go = false;
+ interrupt();
+ }
+
+ public void waitForFrame(long targetFrame) throws InterruptedException {
+ synchronized (semaphore) {
+ while (frameCount < targetFrame) {
+ semaphore.wait();
+ }
+ }
+ }
+
+ public void generateFrame(long desiredFrame) {
+ synchronized (goSemaphore) {
+ this.desiredFrame = desiredFrame;
+ goSemaphore.notify();
+ }
+ }
+
+ }
+
+ @Test
+ public void testMultiThreads() {
+ threadList = new ArrayList<>();
+ for (int i = 0; i < numThreads; i++) {
+ CustomThread thread = new CustomThread();
+ threadList.add(thread);
+ thread.start();
+ }
+
+ long frameCount = 0;
+ long startTime = System.currentTimeMillis();
+ try {
+ for (int i = 0; i < numLoops; i++) {
+ frameCount += 1;
+ waitForThreads(frameCount);
+ // LOGGER.debug("got frame " + frameCount );
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ long stopTime = System.currentTimeMillis();
+ long elapsedTime = stopTime - startTime;
+ double elapsedSeconds = 0.001 * elapsedTime;
+ double blocksPerSecond = numLoops / elapsedSeconds;
+ System.out.format("blocksPerSecond = %10.3f\n", blocksPerSecond);
+ double framesPerSecond = blocksPerSecond * FRAMES_PER_BLOCK;
+ System.out.format("audio framesPerSecond = %10.3f at %d frames per block\n",
+ framesPerSecond, FRAMES_PER_BLOCK);
+
+ for (CustomThread thread : threadList) {
+ System.out.format("max elapsed time is %d nanos or %f msec\n", thread.maxElapsed,
+ (thread.maxElapsed / 1000000.0));
+ }
+ for (CustomThread thread : threadList) {
+ assertEquals(frameCount, thread.frameCount, "BlockCount must match ");
+ thread.abort();
+ }
+
+ }
+
+ private void waitForThreads(long frameCount) throws InterruptedException {
+ for (CustomThread thread : threadList) {
+ // Ask threads to wake up and generate up to this frame.
+ thread.generateFrame(frameCount);
+ }
+ for (CustomThread thread : threadList) {
+ // Wait for all the threads to catch up.
+ thread.waitForFrame(frameCount);
+ }
+ }
+}
diff --git a/src/test/java/com/jsyn/research/RecordVariousRamps.java b/src/test/java/com/jsyn/research/RecordVariousRamps.java
new file mode 100644
index 0000000..7abb2b1
--- /dev/null
+++ b/src/test/java/com/jsyn/research/RecordVariousRamps.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2014 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.
+ */
+/**
+ * Generate steps, linear ramps and smooth ramps.
+ *
+ * @author (C) 2014 Phil Burk
+ */
+
+package com.jsyn.research;
+
+import java.io.File;
+import java.io.IOException;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.unitgen.ContinuousRamp;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.LinearRamp;
+import com.jsyn.unitgen.Multiply;
+import com.jsyn.unitgen.PassThrough;
+import com.jsyn.unitgen.PowerOfTwo;
+import com.jsyn.unitgen.SawtoothOscillatorBL;
+import com.jsyn.unitgen.UnitFilter;
+import com.jsyn.unitgen.UnitOscillator;
+import com.jsyn.util.WaveRecorder;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class RecordVariousRamps {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(RecordVariousRamps.class);
+
+ private Synthesizer synth;
+ private UnitOscillator osc;
+ private Multiply multiplier;
+ private UnitFilter ramp;
+ private LinearRamp linearRamp;
+ private ContinuousRamp continuousRamp;
+ private LineOut lineOut;
+ private WaveRecorder recorder;
+ private PowerOfTwo powerOfTwo;
+ private static final int MODE_STEP = 0;
+ private static final int MODE_LINEAR = 1;
+ private static final int MODE_SMOOTH = 2;
+ private static final String[] modeNames = {
+ "step", "linear", "smooth"
+ };
+
+ private static final RampEvent[] rampData = {
+ new RampEvent(1.0, 1.5, 2.0), new RampEvent(-0.9, 0.5, 1.0),
+ new RampEvent(0.9, 0.5, 0.8), new RampEvent(-0.3, 0.5, 0.8),
+ new RampEvent(0.9, 0.5, 0.3), new RampEvent(-0.5, 0.5, 0.3),
+ new RampEvent(0.8, 2.0, 1.0),
+ };
+
+ private static class RampEvent {
+ double target;
+ double eventDuration;
+ double rampDuration;
+
+ RampEvent(double target, double eventDuration, double rampDuration) {
+ this.target = target;
+ this.eventDuration = eventDuration;
+ this.rampDuration = rampDuration;
+ }
+ }
+
+ private void test(int mode) throws IOException {
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer();
+ synth.setRealTime(false);
+
+ File waveFile = new File("ramp_pitch_" + modeNames[mode] + ".wav");
+ // Mono 16 bits.
+ recorder = new WaveRecorder(synth, waveFile, 1, 16);
+ LOGGER.debug("Writing to 16-bit WAV file " + waveFile.getAbsolutePath());
+
+ // Add some tone generators.
+ synth.add(osc = new SawtoothOscillatorBL());
+
+ // Add a controller that will sweep up.
+ synth.add(multiplier = new Multiply());
+ synth.add(powerOfTwo = new PowerOfTwo());
+ // Add an output unit.
+ synth.add(lineOut = new LineOut());
+ multiplier.inputB.set(660.0);
+
+ switch (mode) {
+ case MODE_STEP:
+ synth.add(ramp = new PassThrough());
+ break;
+ case MODE_LINEAR:
+ synth.add(ramp = linearRamp = new LinearRamp());
+ linearRamp.current.set(-1.0);
+ linearRamp.time.set(10.0);
+ break;
+ case MODE_SMOOTH:
+ synth.add(ramp = continuousRamp = new ContinuousRamp());
+ continuousRamp.current.set(-1.0);
+ continuousRamp.time.set(10.0);
+ break;
+ }
+
+ ramp.getInput().set(-1.0);
+ ramp.getOutput().connect(powerOfTwo.input);
+
+ powerOfTwo.output.connect(multiplier.inputA);
+ multiplier.output.connect(osc.frequency);
+
+ // Connect the oscillator to the left and right audio output.
+ osc.output.connect(0, lineOut.input, 0);
+ osc.output.connect(0, lineOut.input, 1);
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+
+ osc.output.connect(0, recorder.getInput(), 0);
+ // When we start the recorder it will pull data from the oscillator
+ // and sweeper.
+ recorder.start();
+
+ // We also need to start the LineOut if we want to hear it now.
+ lineOut.start();
+
+ // Get synthesizer time in seconds.
+ double nextEventTime = synth.getCurrentTime() + 1.0;
+ try {
+ synth.sleepUntil(nextEventTime);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ for (RampEvent rampEvent : rampData) {
+
+ switch (mode) {
+ case MODE_STEP:
+ break;
+ case MODE_LINEAR:
+ linearRamp.time.set(rampEvent.rampDuration);
+ break;
+ case MODE_SMOOTH:
+ continuousRamp.time.set(rampEvent.rampDuration);
+ break;
+ }
+ ramp.getInput().set(rampEvent.target);
+
+ nextEventTime += rampEvent.eventDuration;
+ LOGGER.debug("target = " + rampEvent.target + ", rampDur = "
+ + rampEvent.rampDuration + ", eventDur = " + rampEvent.eventDuration);
+ try {
+ synth.sleepUntil(nextEventTime);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (recorder != null) {
+ recorder.stop();
+ recorder.close();
+ }
+ // Stop everything.
+ synth.stop();
+ }
+
+ @Test
+ private void stepMode() throws IOException {
+ test(MODE_STEP);
+ }
+
+ @Test
+ public void linearMode() throws IOException {
+ test(MODE_LINEAR);
+ }
+
+ @Test
+ public void smoothMode() throws IOException {
+ test(MODE_SMOOTH);
+ }
+}
diff --git a/src/test/java/com/jsyn/swing/TestRangeModels.java b/src/test/java/com/jsyn/swing/TestRangeModels.java
new file mode 100644
index 0000000..5d12601
--- /dev/null
+++ b/src/test/java/com/jsyn/swing/TestRangeModels.java
@@ -0,0 +1,53 @@
+/*
+ * 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.swing;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class TestRangeModels {
+
+ public void checkDoubleRange(double dmin, double dmax, double dval) {
+ int resolution = 1000;
+ DoubleBoundedRangeModel model = new DoubleBoundedRangeModel("test", resolution, dmin, dmax,
+ dval);
+ assertEquals(dmin, model.getDoubleMinimum(), 0.0001, "setup min");
+ assertEquals(dmax, model.getDoubleMaximum(), 0.0001, "setup max");
+ assertEquals(dval, model.getDoubleValue(), 0.0001, "setup value");
+
+ model.setDoubleValue(dmin);
+ assertEquals(dmin, model.getDoubleValue(), 0.0001, "min double value");
+ assertEquals(0, model.getValue(), "min value");
+
+ double dmid = (dmax + dmin) / 2.0;
+ model.setDoubleValue(dmid);
+ assertEquals(dmid, model.getDoubleValue(), 0.0001, "middle double value");
+ assertEquals(resolution / 2, model.getValue(), "middle value");
+
+ model.setDoubleValue(dmax);
+ assertEquals(dmax, model.getDoubleValue(), 0.0001, "max double value");
+ assertEquals(resolution, model.getValue(), "max value");
+
+ }
+
+ @Test
+ public void testDoubleRange() {
+ checkDoubleRange(10.0, 20.0, 12.0);
+ checkDoubleRange(-1.0, 1.0, 0.5);
+ }
+}
diff --git a/src/test/java/com/jsyn/unitgen/CalibrateMoogFilter.java b/src/test/java/com/jsyn/unitgen/CalibrateMoogFilter.java
new file mode 100644
index 0000000..1e74aa8
--- /dev/null
+++ b/src/test/java/com/jsyn/unitgen/CalibrateMoogFilter.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2010 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.unitgen;
+
+import javax.swing.JApplet;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+
+/**
+ * Play a sawtooth through a 4-pole filter.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class CalibrateMoogFilter extends JApplet {
+ private Synthesizer synth;
+ private UnitOscillator oscillator;
+ private SineOscillator reference;
+ ZeroCrossingCounter zeroCounter;
+ PitchDetector pitchDetector;
+ ZeroCrossingCounter sineZeroCounter;
+ PitchDetector sinePitchDetector;
+ private FilterFourPoles filterMoog;
+ private LineOut lineOut;
+
+ @Override
+ public void init() {
+ synth = JSyn.createSynthesizer();
+ synth.setRealTime(false);
+ synth.add(oscillator = new SawtoothOscillatorBL());
+ synth.add(reference = new SineOscillator());
+ synth.add(filterMoog = new FilterFourPoles());
+ synth.add(pitchDetector = new PitchDetector());
+ synth.add(sinePitchDetector = new PitchDetector());
+ synth.add(zeroCounter = new ZeroCrossingCounter());
+ synth.add(sineZeroCounter = new ZeroCrossingCounter());
+ synth.add(lineOut = new LineOut());
+
+ oscillator.output.connect(filterMoog.input);
+ filterMoog.output.connect(zeroCounter.input);
+ zeroCounter.output.connect(pitchDetector.input);
+ reference.output.connect(0, lineOut.input, 0);
+ filterMoog.output.connect(0, lineOut.input, 1);
+
+ reference.output.connect(sineZeroCounter.input);
+ sineZeroCounter.output.connect(sinePitchDetector.input);
+
+ oscillator.frequency.set(130.0);
+ oscillator.amplitude.set(0.001);
+ filterMoog.frequency.set(440.0);
+ filterMoog.Q.set(4.1);
+ }
+
+ @Override
+ public void start() {
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ pitchDetector.start();
+ sinePitchDetector.start();
+ lineOut.start();
+ }
+
+ @Override
+ public void stop() {
+ pitchDetector.stop();
+ sinePitchDetector.stop();
+ lineOut.stop();
+ synth.stop();
+ }
+
+ public void test() {
+ init();
+ start();
+ try {
+ calibrate();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ stop();
+ }
+
+ private void calibrate() throws InterruptedException {
+ synth.sleepFor(2.0);
+ double freq = 100.0;
+ System.out
+ .printf("freq, moogFreq, ratio, moogConf, sineFreq, sineConf, moogZRate, sineZRate\n");
+ long startingFrameCount = synth.getFrameCount();
+ long startingMoogZeroCount = zeroCounter.getCount();
+ long startingSineZeroCount = sineZeroCounter.getCount();
+ for (int i = 0; i < 50; i++) {
+ reference.frequency.set(freq);
+ filterMoog.frequency.set(freq);
+ synth.sleepFor(2.0);
+
+ long endingFrameCount = synth.getFrameCount();
+ long elapsedFrames = endingFrameCount - startingFrameCount;
+ startingFrameCount = endingFrameCount;
+
+ long endingMoogZeroCount = zeroCounter.getCount();
+ long elapsedMoogZeros = endingMoogZeroCount - startingMoogZeroCount;
+ startingMoogZeroCount = endingMoogZeroCount;
+
+ long endingSineZeroCount = sineZeroCounter.getCount();
+ long elapsedSineZeros = endingSineZeroCount - startingSineZeroCount;
+ startingSineZeroCount = endingSineZeroCount;
+
+ double moogZeroRate = elapsedMoogZeros * (double) synth.getFrameRate() / elapsedFrames;
+ double sineZeroRate = elapsedSineZeros * (double) synth.getFrameRate() / elapsedFrames;
+
+ double moogMeasuredFreq = pitchDetector.frequency.get();
+ double moogConfidence = pitchDetector.confidence.get();
+ double sineMeasuredFreq = sinePitchDetector.frequency.get();
+ double sineConfidence = sinePitchDetector.confidence.get();
+ double ratio = freq / moogMeasuredFreq;
+ System.out.printf("%7.2f, %8.5f, %7.5f, %4.2f, %8.5f, %4.2f, %8.4f, %8.4f\n", freq,
+ moogMeasuredFreq, ratio, moogConfidence, sineMeasuredFreq, sineConfidence,
+ moogZeroRate, sineZeroRate);
+
+ freq *= 1.1;
+ }
+ }
+
+ public static void main(String[] args) {
+ new CalibrateMoogFilter().test();
+ }
+
+}
diff --git a/src/test/java/com/jsyn/unitgen/EnablingGate.java b/src/test/java/com/jsyn/unitgen/EnablingGate.java
new file mode 100644
index 0000000..daf36be
--- /dev/null
+++ b/src/test/java/com/jsyn/unitgen/EnablingGate.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010 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.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * This can be used to block the execution of upstream units. It can be placed at the output of a
+ * circuit and driven with an amplitude envelope.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class EnablingGate extends UnitFilter {
+ public UnitInputPort gate;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public EnablingGate() {
+ super();
+ addPort(gate = new UnitInputPort("Gate"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] aValues = input.getValues();
+ double[] bValues = gate.getValues();
+ double[] outputs = output.getValues();
+ for (int i = start; i < limit; i++) {
+ outputs[i] = aValues[i] * bValues[i];
+ }
+ // If we end up at zero then disable pulling of data.
+ // We do this at the end so that envelope can get started.
+ if (outputs[limit - 1] <= 0.0) {
+ setEnabled(false);
+ }
+ }
+
+}
diff --git a/src/test/java/com/jsyn/unitgen/NonRealTimeTestCase.java b/src/test/java/com/jsyn/unitgen/NonRealTimeTestCase.java
new file mode 100644
index 0000000..bec5762
--- /dev/null
+++ b/src/test/java/com/jsyn/unitgen/NonRealTimeTestCase.java
@@ -0,0 +1,42 @@
+/*
+ * 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.unitgen;
+
+import com.jsyn.engine.SynthesisEngine;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+
+public abstract class NonRealTimeTestCase {
+
+ protected SynthesisEngine synthesisEngine;
+
+ public NonRealTimeTestCase() {
+ super();
+ }
+
+ @BeforeEach
+ private void setUp() {
+ synthesisEngine = new SynthesisEngine();
+ synthesisEngine.setRealTime(false);
+ }
+
+ @AfterEach
+ private void tearDown() {
+ synthesisEngine.stop();
+ }
+
+}
diff --git a/src/test/java/com/jsyn/unitgen/RecordMoogFilter.java b/src/test/java/com/jsyn/unitgen/RecordMoogFilter.java
new file mode 100644
index 0000000..31a86be
--- /dev/null
+++ b/src/test/java/com/jsyn/unitgen/RecordMoogFilter.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2010 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.unitgen;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import javax.swing.JApplet;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.util.WaveRecorder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Measure actual frequency as a function of input frequency and Q.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class RecordMoogFilter extends JApplet {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(RecordMoogFilter.class);
+
+ private final static boolean SWEEP_Q = false;
+ private final static boolean SWEEP_FREQUENCY = true;
+ private final static int NUM_STEPS = 11;
+
+ private final static double MIN_Q = 0.0;
+ private final static double DEFAULT_Q = 9.0;
+ private final static double MAX_Q = 10.0;
+
+ private final static double MIN_FREQUENCY = 100.0;
+ private final static double DEFAULT_FREQUENCY = 500.0;
+ private final static double MAX_FREQUENCY = 4000.0;
+
+ private Synthesizer synth;
+ private WhiteNoise source;
+ private SineOscillator reference;
+ private FilterFourPoles filterMoog;
+ private LineOut lineOut;
+ private WaveRecorder recorder;
+
+ @Override
+ public void init() {
+ synth = JSyn.createSynthesizer();
+ synth.setRealTime(false);
+ synth.add(source = new WhiteNoise());
+ synth.add(filterMoog = new FilterFourPoles());
+ synth.add(reference = new SineOscillator());
+ synth.add(lineOut = new LineOut());
+
+ source.output.connect(filterMoog.input);
+ reference.output.connect(0, lineOut.input, 0);
+ filterMoog.output.connect(0, lineOut.input, 1);
+
+ reference.amplitude.set(0.5);
+ source.amplitude.set(0.5);
+ filterMoog.frequency.set(DEFAULT_FREQUENCY);
+ filterMoog.Q.set(DEFAULT_Q);
+
+ File waveFile = new File("temp_recording.wav");
+ // Default is stereo, 16 bits.
+ try {
+ recorder = new WaveRecorder(synth, waveFile);
+ } catch (FileNotFoundException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ LOGGER.debug("Writing to WAV file " + waveFile.getAbsolutePath());
+ }
+
+ @Override
+ public void start() {
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ lineOut.start();
+
+ reference.output.connect(0, recorder.getInput(), 0);
+ filterMoog.output.connect(0, recorder.getInput(), 1);
+ recorder.start();
+ }
+
+ @Override
+ public void stop() {
+ if (recorder != null) {
+ recorder.stop();
+ try {
+ recorder.close();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ lineOut.stop();
+ synth.stop();
+ }
+
+ public void test() {
+ init();
+ start();
+ try {
+ calibrate();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ stop();
+ }
+
+ private void calibrate() throws InterruptedException {
+ synth.sleepFor(0.2);
+ double freq = SWEEP_FREQUENCY ? MIN_FREQUENCY : DEFAULT_FREQUENCY;
+ double q = SWEEP_Q ? MIN_Q : DEFAULT_Q;
+ double stepQ = (MAX_Q - MIN_Q) / (NUM_STEPS - 1);
+ double scaleFrequency = Math.pow((MAX_FREQUENCY / MIN_FREQUENCY), (1.0 / (NUM_STEPS - 1)));
+ System.out.printf("freq, q, measured\n");
+ for (int i = 0; i < NUM_STEPS; i++) {
+ double refAmp = reference.amplitude.get();
+ reference.amplitude.set(0.0);
+ synth.sleepFor(0.1);
+ reference.amplitude.set(refAmp);
+
+ System.out.printf("%8.2f, %6.3f, \n", freq, q);
+ filterMoog.frequency.set(freq);
+ reference.frequency.set(freq);
+ filterMoog.Q.set(q);
+
+ synth.sleepFor(2.0);
+
+ if (SWEEP_FREQUENCY) {
+ freq *= scaleFrequency;
+ }
+ if (SWEEP_Q) {
+ q += stepQ;
+ }
+ }
+ }
+
+ public static void main(String[] args) {
+ new RecordMoogFilter().test();
+ }
+
+}
diff --git a/src/test/java/com/jsyn/unitgen/TestConnections.java b/src/test/java/com/jsyn/unitgen/TestConnections.java
new file mode 100644
index 0000000..9aee32f
--- /dev/null
+++ b/src/test/java/com/jsyn/unitgen/TestConnections.java
@@ -0,0 +1,111 @@
+/*
+ * 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.unitgen;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class TestConnections {
+ private Synthesizer synth;
+
+ private Add add1;
+ private Add add2;
+ private Add add3;
+
+ @BeforeEach
+ private void beforeEach() {
+ synth = JSyn.createSynthesizer();
+
+ synth.add(add1 = new Add());
+ synth.add(add2 = new Add());
+ synth.add(add3 = new Add());
+
+ add1.start();
+ add2.start();
+ add3.start();
+
+ add1.inputA.set(0.1);
+ add1.inputB.set(0.2);
+
+ add2.inputA.set(0.4);
+ add2.inputB.set(0.8);
+
+ add3.inputA.set(1.6);
+ add3.inputB.set(3.2);
+ }
+
+ @Test
+ public void testSet() throws InterruptedException {
+ synth.sleepFor(0.01);
+ assertEquals(0.3, add1.output.getValue(), 0.0001, "set inputs of adder");
+ }
+
+ @Test
+ public void testConnect() throws InterruptedException {
+ synth.sleepFor(0.01);
+ assertEquals(0.3, add1.output.getValue(), 0.0001, "set inputs of adder");
+ assertEquals(1.2, add2.output.getValue(), 0.0001, "set inputs of adder");
+
+ // Test different ways of connecting.
+ add1.output.connect(add2.inputB);
+ checkConnection();
+
+ add1.output.connect(0, add2.inputB, 0);
+ checkConnection();
+
+ add1.output.connect(add2.inputB.getConnectablePart(0));
+ checkConnection();
+
+ add1.output.getConnectablePart(0).connect(add2.inputB);
+ checkConnection();
+
+ add1.output.getConnectablePart(0).connect(add2.inputB.getConnectablePart(0));
+ checkConnection();
+
+ add2.inputB.connect(add1.output);
+ checkConnection();
+
+ add2.inputB.connect(0, add1.output, 0);
+ checkConnection();
+
+ add2.inputB.connect(add1.output.getConnectablePart(0));
+ checkConnection();
+
+ add2.inputB.getConnectablePart(0).connect(add1.output);
+ checkConnection();
+
+ add2.inputB.getConnectablePart(0).connect(add1.output.getConnectablePart(0));
+ checkConnection();
+ }
+
+ private void checkConnection() throws InterruptedException {
+ synth.sleepFor(0.01);
+ assertEquals(0.3, add1.output.getValue(), 0.0001, "connection should not change output");
+ assertEquals(0.7, add2.output.getValue(), 0.0001, "replace set value with output");
+
+ // Revert to set value after disconnection.
+ add1.output.disconnectAll();
+ synth.sleepFor(0.01);
+ assertEquals(0.3, add1.output.getValue(), 0.0001, "still the same");
+ assertEquals(1.2, add2.output.getValue(), 0.0001, "should revert to original set() value");
+ }
+
+}
diff --git a/src/test/java/com/jsyn/unitgen/TestDelay.java b/src/test/java/com/jsyn/unitgen/TestDelay.java
new file mode 100644
index 0000000..7e1a0b1
--- /dev/null
+++ b/src/test/java/com/jsyn/unitgen/TestDelay.java
@@ -0,0 +1,81 @@
+/*
+ * 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.unitgen;
+
+import com.jsyn.util.AudioStreamReader;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class TestDelay extends NonRealTimeTestCase {
+
+ @Test
+ public void testFloor() {
+ double x = -7.3;
+ int n = (int) Math.floor(x);
+ assertEquals(-8, n, "int");
+ }
+
+ public void checkInterpolatingDelay(int maxFrames, double delayFrames)
+ throws InterruptedException {
+ synthesisEngine.start();
+
+ System.out.printf("test delayFrames = %7.5f\n", delayFrames);
+ InterpolatingDelay delay = new InterpolatingDelay();
+ synthesisEngine.add(delay);
+ delay.allocate(maxFrames);
+ delay.delay.set(delayFrames / 44100.0);
+ SawtoothOscillator osc = new SawtoothOscillator();
+ synthesisEngine.add(osc);
+ osc.frequency.set(synthesisEngine.getFrameRate() / 4.0);
+ osc.amplitude.set(1.0);
+ osc.output.connect(delay.input);
+
+ int samplesPerFrame = 1;
+ AudioStreamReader reader = new AudioStreamReader(synthesisEngine, samplesPerFrame);
+ delay.output.connect(reader.getInput());
+
+ delay.start();
+ for (int i = 0; i < (3 * maxFrames); i++) {
+ if (reader.available() == 0) {
+ synthesisEngine.sleepFor(0.01);
+ }
+ double actual = reader.read();
+ double expected = 1 + i - delayFrames;
+ if (expected < 0.0) {
+ expected = 0.0;
+ }
+ // System.out.printf( "[%d] expected = %7.3f, delayed = %7.3f\n", i, expected, actual );
+ // assertEquals(expected, actual, 0.00001, "delayed output");
+ }
+ }
+
+ @Test
+ public void testSmall() throws InterruptedException {
+ checkInterpolatingDelay(40, 7.0);
+ }
+
+ @Test
+ public void testEven() throws InterruptedException {
+ checkInterpolatingDelay(44100, 13671.0);
+ }
+
+ @Test
+ public void testInterpolatingDelay() throws InterruptedException {
+ checkInterpolatingDelay(44100, 13671.4);
+ }
+}
diff --git a/src/test/java/com/jsyn/unitgen/TestEnable.java b/src/test/java/com/jsyn/unitgen/TestEnable.java
new file mode 100644
index 0000000..a244c61
--- /dev/null
+++ b/src/test/java/com/jsyn/unitgen/TestEnable.java
@@ -0,0 +1,81 @@
+/*
+ * 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.unitgen;
+
+import com.jsyn.engine.SynthesisEngine;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+public class TestEnable {
+ private SynthesisEngine synthesisEngine;
+
+ @BeforeEach
+ protected void beforeEach() {
+ synthesisEngine = new SynthesisEngine();
+ synthesisEngine.setRealTime(false);
+ }
+
+ @AfterEach
+ protected void afterEach() {
+ synthesisEngine.stop();
+ }
+
+ @Test
+ public void testEnablingGate() throws InterruptedException {
+ LinearRamp ramp = new LinearRamp();
+ synthesisEngine.add(ramp);
+ EnablingGate enabler = new EnablingGate();
+ synthesisEngine.add(enabler);
+ Add adder = new Add();
+ synthesisEngine.add(adder);
+
+ ramp.output.connect(enabler.input);
+ enabler.output.connect(adder.inputA);
+
+ // set up so ramp should equal time
+ ramp.current.set(0.0);
+ ramp.input.set(1.0);
+ ramp.time.set(1.0);
+ enabler.gate.set(1.0);
+
+ synthesisEngine.start();
+ double startTime = synthesisEngine.getCurrentTime();
+ // pull from final adder
+ adder.start();
+ synthesisEngine.sleepUntil(startTime + 0.1);
+ double tolerance = 0.002;
+ assertEquals(0.1, ramp.output.getValue(), tolerance, "ramp going up");
+ assertEquals(0.1, enabler.output.getValue(), tolerance, "enabler going up");
+ assertEquals(0.1, adder.output.getValue(), tolerance, "adder going up");
+ synthesisEngine.sleepUntil(startTime + 0.2);
+ assertEquals(0.2, adder.output.getValue(), tolerance, "start enabled");
+
+ // disable everything upstream
+ enabler.gate.set(0.0);
+
+ synthesisEngine.sleepUntil(startTime + 0.3);
+ assertEquals(0.2, ramp.output.getValue(), tolerance, "should not be pulled");
+ assertFalse(enabler.isEnabled(), "should be disabled");
+ assertEquals(0.0, enabler.output.getValue(), tolerance, "should be zero");
+ assertEquals(0.0, adder.output.getValue(), tolerance, "zero");
+
+ }
+}
diff --git a/src/test/java/com/jsyn/unitgen/TestEnvelopeAttackDecay.java b/src/test/java/com/jsyn/unitgen/TestEnvelopeAttackDecay.java
new file mode 100644
index 0000000..8328bb7
--- /dev/null
+++ b/src/test/java/com/jsyn/unitgen/TestEnvelopeAttackDecay.java
@@ -0,0 +1,130 @@
+/*
+ * 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.unitgen;
+
+import com.jsyn.engine.SynthesisEngine;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestEnvelopeAttackDecay extends TestUnitGate {
+ double attackTime;
+ double decayTime;
+
+ @BeforeEach
+ protected void beforeEach() {
+ synthesisEngine = new SynthesisEngine();
+ synthesisEngine.setRealTime(false);
+ attackTime = 0.2;
+ decayTime = 0.4;
+ }
+
+ @AfterEach
+ protected void afterEach() {
+ synthesisEngine.stop();
+ }
+
+ @Test
+ public void testOnOff() throws InterruptedException {
+ var envelope = new EnvelopeAttackDecay();
+ synthesisEngine.add(envelope);
+
+ envelope.attack.set(0.1);
+ envelope.decay.set(0.2);
+
+ synthesisEngine.start();
+ envelope.start();
+ time = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(time + 0.1);
+ assertEquals(0.0, envelope.output.getValue(), "still idling");
+
+ // Trigger the envelope using on/off
+ envelope.input.on();
+ time = synthesisEngine.getCurrentTime();
+ // Check end of attack cycle.
+ synthesisEngine.sleepUntil(time + 0.1);
+ assertTrue(envelope.output.getValue() > 0.8, "at peak");
+ envelope.input.off();
+ // Check end of decay cycle.
+ synthesisEngine.sleepUntil(time + 0.3);
+ assertTrue(envelope.output.getValue() < 0.1, "at peak");
+
+ synthesisEngine.sleepFor(0.1);
+
+ // Trigger the envelope using trigger()
+ envelope.input.trigger();
+ time = synthesisEngine.getCurrentTime();
+ // Check end of attack cycle.
+ synthesisEngine.sleepUntil(time + 0.1);
+ assertTrue(envelope.output.getValue() > 0.8, "at peak");
+ // Check end of decay cycle.
+ synthesisEngine.sleepUntil(time + 0.3);
+ assertTrue(envelope.output.getValue() < 0.1, "at peak");
+ }
+
+ @Test
+ public void testRetrigger() throws InterruptedException {
+ var envelope = new EnvelopeAttackDecay();
+ synthesisEngine.add(envelope);
+
+ envelope.attack.set(0.1);
+ envelope.decay.set(0.2);
+
+ synthesisEngine.start();
+ envelope.start();
+ time = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(time + 0.1);
+ assertEquals(0.0, envelope.output.getValue(), "still idling");
+
+ // Trigger the envelope using trigger()
+ envelope.input.trigger();
+ // Check end of attack cycle.
+ synthesisEngine.sleepFor(0.1);
+ assertEquals(1.0, envelope.output.getValue(), 0.1, "at peak");
+
+ // Decay half way.
+ synthesisEngine.sleepFor(0.1);
+ assertTrue(envelope.output.getValue() < 0.7, "at peak");
+
+ // Retrigger while decaying
+ envelope.input.trigger();
+ // Will get to top faster.
+ synthesisEngine.sleepFor(0.1);
+ assertEquals(1.0, envelope.output.getValue(), 0.1, "at peak");
+
+ // Check end of decay cycle.
+ synthesisEngine.sleepFor(0.2);
+ assertTrue(envelope.output.getValue() < 0.1, "at peak");
+
+ }
+
+ @Test
+ public void testAutoDisable() throws InterruptedException {
+ var ramp = new LinearRamp();
+ synthesisEngine.add(ramp);
+ var envelope = new EnvelopeAttackDecay();
+ envelope.attack.set(0.1);
+ envelope.decay.set(0.1);
+ synthesisEngine.add(envelope);
+ ramp.output.connect(envelope.amplitude);
+
+ checkAutoDisable(ramp, envelope);
+ }
+}
diff --git a/src/test/java/com/jsyn/unitgen/TestEnvelopeDAHDSR.java b/src/test/java/com/jsyn/unitgen/TestEnvelopeDAHDSR.java
new file mode 100644
index 0000000..618e823
--- /dev/null
+++ b/src/test/java/com/jsyn/unitgen/TestEnvelopeDAHDSR.java
@@ -0,0 +1,355 @@
+/*
+ * 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.unitgen;
+
+import com.jsyn.engine.SynthesisEngine;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestEnvelopeDAHDSR extends TestUnitGate {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TestEnvelopeDAHDSR.class);
+
+ double delayTime;
+ double attackTime;
+ double holdTime;
+ double decayTime;
+ double sustainLevel;
+ double releaseTime;
+
+ @BeforeEach
+ protected void beforeEach() {
+ synthesisEngine = new SynthesisEngine();
+ synthesisEngine.setRealTime(false);
+ delayTime = 0.1;
+ attackTime = 0.2;
+ holdTime = 0.3;
+ decayTime = 0.4;
+ sustainLevel = 0.5;
+ releaseTime = 0.6;
+ }
+
+ @AfterEach
+ protected void afterEach() {
+ synthesisEngine.stop();
+ }
+
+ @Test
+ public void testStages() throws InterruptedException {
+ EnvelopeDAHDSR ramp = checkToSustain();
+
+ // Change sustain level to simulate tremolo sustain.
+ sustainLevel = 0.7;
+ ramp.sustain.set(sustainLevel);
+ time += 0.01;
+ synthesisEngine.sleepUntil(time);
+ assertEquals(sustainLevel, ramp.output.getValue(), 0.01, "sustain moving delaying");
+
+ // Gate off to let envelope release.
+ ramp.input.set(0.0);
+ synthesisEngine.sleepUntil(time + (releaseTime * 0.1));
+ double releaseValue = ramp.output.getValue();
+ assertEquals(sustainLevel * 0.36, releaseValue, 0.01, "partway down release");
+ }
+
+ private EnvelopeDAHDSR checkToSustain() throws InterruptedException {
+ EnvelopeDAHDSR ramp = new EnvelopeDAHDSR();
+ synthesisEngine.add(ramp);
+
+ ramp.delay.set(delayTime);
+ ramp.attack.set(attackTime);
+ ramp.hold.set(holdTime);
+ ramp.decay.set(decayTime);
+ ramp.sustain.set(sustainLevel);
+ ramp.release.set(releaseTime);
+
+ synthesisEngine.start();
+ ramp.start();
+ time = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(time + (2.0 * delayTime));
+ assertEquals(0.0, ramp.output.getValue(), "still idling");
+
+ // Trigger the envelope.
+ ramp.input.set(1.0);
+ time = synthesisEngine.getCurrentTime();
+ // Check end of delay cycle.
+ synthesisEngine.sleepUntil(time + (delayTime * 0.9));
+ assertEquals(0.0, ramp.output.getValue(), 0.01, "still delaying");
+ // Half way up attack ramp.
+ synthesisEngine.sleepUntil(time + delayTime + (attackTime * 0.5));
+ assertEquals(0.5, ramp.output.getValue(), 0.01, "half attack");
+ // Holding after attack.
+ synthesisEngine.sleepUntil(time + delayTime + attackTime + (holdTime * 0.1));
+ assertEquals(1.0, ramp.output.getValue(), 0.01, "holding");
+ synthesisEngine.sleepUntil(time + delayTime + attackTime + (holdTime * 0.9));
+ assertEquals(1.0, ramp.output.getValue(), 0.01, "still holding");
+ synthesisEngine.sleepUntil(time + delayTime + attackTime + holdTime + decayTime);
+ time = synthesisEngine.getCurrentTime();
+ assertEquals(sustainLevel, ramp.output.getValue(), 0.01, "at sustain");
+ return ramp;
+ }
+
+ @Test
+ public void testRetrigger() throws InterruptedException {
+ EnvelopeDAHDSR ramp = checkToSustain();
+
+ // Gate off to let envelope release.
+ ramp.input.set(0.0);
+ synthesisEngine.sleepUntil(time + (releaseTime * 0.1));
+ double releaseValue = ramp.output.getValue();
+ assertEquals(sustainLevel * 0.36, releaseValue, 0.01, "partway down release");
+
+ // Retrigger during release phase.
+ time = synthesisEngine.getCurrentTime();
+ ramp.input.set(1.0);
+ // Check end of delay cycle.
+ synthesisEngine.sleepUntil(time + (delayTime * 0.9));
+ assertEquals(releaseValue, ramp.output.getValue(), 0.01, "still delaying");
+ // Half way up attack ramp from where it started.
+ synthesisEngine.sleepUntil(time + delayTime + (attackTime * 0.5));
+ assertEquals(releaseValue + 0.5, ramp.output.getValue(), 0.01, "half attack");
+
+ }
+
+ // I noticed a hang while playing with knobs.
+ @Test
+ public void testHang() throws InterruptedException {
+
+ delayTime = 0.0;
+ attackTime = 0.0;
+ holdTime = 0.0;
+ decayTime = 0.0;
+ sustainLevel = 0.3;
+ releaseTime = 3.0;
+
+ EnvelopeDAHDSR ramp = new EnvelopeDAHDSR();
+ synthesisEngine.add(ramp);
+
+ ramp.delay.set(delayTime);
+ ramp.attack.set(attackTime);
+ ramp.hold.set(holdTime);
+ ramp.decay.set(decayTime);
+ ramp.sustain.set(sustainLevel);
+ ramp.release.set(releaseTime);
+
+ synthesisEngine.start();
+ ramp.start();
+ // Trigger the envelope.
+ ramp.input.set(1.0);
+ time = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(time + 0.01);
+ assertEquals(sustainLevel, ramp.output.getValue(), "should jump to sustain level");
+
+ // Gate off to let envelope release.
+ ramp.input.set(0.0);
+ synthesisEngine.sleepUntil(time + 1.0);
+ double releaseValue = ramp.output.getValue();
+ assertTrue(sustainLevel > releaseValue, "partway down release");
+
+ holdTime = 0.5;
+ ramp.hold.set(holdTime);
+ decayTime = 0.5;
+ ramp.decay.set(decayTime);
+
+ // Retrigger during release phase and try to catch it at top of hold
+ time = synthesisEngine.getCurrentTime();
+ ramp.input.set(1.0);
+ // Check end of delay cycle.
+ synthesisEngine.sleepUntil(time + (holdTime * 0.1));
+ assertEquals(1.0, ramp.output.getValue(), 0.01, "should jump to hold");
+ }
+
+ @Test
+ public void testNegative() throws InterruptedException {
+ delayTime = -0.1;
+ attackTime = -0.2;
+ holdTime = -0.3;
+ decayTime = -0.4;
+ sustainLevel = 0.3;
+ releaseTime = -0.5;
+
+ EnvelopeDAHDSR ramp = new EnvelopeDAHDSR();
+ synthesisEngine.add(ramp);
+
+ ramp.delay.set(delayTime);
+ ramp.attack.set(attackTime);
+ ramp.hold.set(holdTime);
+ ramp.decay.set(decayTime);
+ ramp.sustain.set(sustainLevel);
+ ramp.release.set(releaseTime);
+
+ synthesisEngine.start();
+ ramp.start();
+ // Trigger the envelope.
+ ramp.input.set(1.0);
+ time = synthesisEngine.getCurrentTime();
+ time += 0.1;
+ synthesisEngine.sleepUntil(time + 0.01);
+ assertEquals(sustainLevel, ramp.output.getValue(), "should jump to sustain level");
+
+ ramp.sustain.set(sustainLevel = -0.4);
+ time += 0.1;
+ synthesisEngine.sleepUntil(time);
+ assertEquals(sustainLevel, ramp.output.getValue(), "sustain should clip at zero");
+
+ ramp.sustain.set(sustainLevel = 0.4);
+ time += 0.1;
+ synthesisEngine.sleepUntil(time);
+ assertEquals(sustainLevel, ramp.output.getValue(), "sustain should come back");
+
+ // Gate off to let envelope release.
+ ramp.input.set(0.0);
+ time += 0.1;
+ synthesisEngine.sleepUntil(time);
+ double releaseValue = ramp.output.getValue();
+ assertEquals(0.0, releaseValue, "release quickly");
+ }
+
+ @Test
+ public void testOnOff() throws InterruptedException {
+ EnvelopeDAHDSR ramp = new EnvelopeDAHDSR();
+ synthesisEngine.add(ramp);
+
+ ramp.delay.set(0.0);
+ ramp.attack.set(0.1);
+ ramp.hold.set(0.0);
+ ramp.decay.set(0.0);
+ ramp.sustain.set(0.9);
+ ramp.release.set(0.1);
+
+ synthesisEngine.start();
+ ramp.start();
+ time = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(time + 0.2);
+ assertEquals(0.0, ramp.output.getValue(), "still idling");
+
+ // Trigger the envelope.
+ ramp.input.on();
+ time = synthesisEngine.getCurrentTime();
+ // Check end of delay cycle.
+ synthesisEngine.sleepUntil(time + 0.2);
+ assertEquals(0.9, ramp.output.getValue(), 0.01, "at sustain");
+
+ // Release the envelope.
+ ramp.input.off();
+ time = synthesisEngine.getCurrentTime();
+ // Check end of delay cycle.
+ synthesisEngine.sleepUntil(time + 0.2);
+ assertEquals(0.0, ramp.output.getValue(), 0.01, "after release");
+ }
+
+ @Test
+ public void testAutoDisable() throws InterruptedException {
+
+ LinearRamp ramp = new LinearRamp();
+ synthesisEngine.add(ramp);
+ EnvelopeDAHDSR envelope = new EnvelopeDAHDSR();
+ synthesisEngine.add(envelope);
+ envelope.attack.set(0.1);
+ envelope.decay.set(0.1);
+ envelope.release.set(0.1);
+ envelope.sustain.set(0.1);
+ ramp.output.connect(envelope.amplitude);
+
+ checkAutoDisable(ramp, envelope);
+ }
+
+ static class GatedRampCircuit extends Circuit {
+ LinearRamp ramp;
+ EnvelopeDAHDSR envelope;
+
+ GatedRampCircuit() {
+ add(ramp = new LinearRamp());
+ add(envelope = new EnvelopeDAHDSR());
+ envelope.attack.set(0.1);
+ envelope.decay.set(0.1);
+ envelope.release.set(0.1);
+ envelope.sustain.set(0.1);
+
+ envelope.setupAutoDisable(this);
+ ramp.output.connect(envelope.amplitude);
+ }
+ }
+
+ @Test
+ public void testAutoDisableCircuit() throws InterruptedException {
+ GatedRampCircuit circuit = new GatedRampCircuit();
+ synthesisEngine.add(circuit);
+ checkAutoDisable(circuit.ramp, circuit.envelope);
+ }
+
+ public void checkReleaseTiming(double releaseTime, double tolerance)
+ throws InterruptedException {
+ delayTime = 0.0;
+ attackTime = 0.2;
+ holdTime = 0.0;
+ decayTime = 10.0;
+ sustainLevel = 1.0;
+
+ EnvelopeDAHDSR ramp = new EnvelopeDAHDSR();
+ synthesisEngine.add(ramp);
+
+ ramp.delay.set(delayTime);
+ ramp.attack.set(attackTime);
+ ramp.hold.set(holdTime);
+ ramp.decay.set(decayTime);
+ ramp.sustain.set(sustainLevel);
+ ramp.release.set(releaseTime);
+
+ synthesisEngine.start();
+ ramp.start();
+ // Trigger the envelope.
+ ramp.input.set(1.0);
+ time = synthesisEngine.getCurrentTime();
+ time += attackTime * 2;
+ synthesisEngine.sleepUntil(time);
+ assertEquals(sustainLevel, ramp.output.getValue(), "should be at to sustain level");
+
+ // Start envelope release.
+ ramp.input.set(0.0);
+ final double db90 = 20.0 * Math.log(1.0 / 32768.0) / Math.log(10.0);
+ LOGGER.debug("JSyns DB90 is actually " + db90);
+ int numSteps = 10;
+ for (int i = 0; i < 10; i++) {
+ time += releaseTime / numSteps;
+ synthesisEngine.sleepUntil(time);
+ double expectedDB = db90 * (i + 1) / numSteps;
+ double expectedAmplitude = sustainLevel * Math.pow(10.0, expectedDB / 20.0);
+ double releaseValue = ramp.output.getValue();
+ assertEquals(expectedAmplitude, releaseValue, tolerance, "release " + i + " at");
+ }
+ time += releaseTime / numSteps;
+ synthesisEngine.sleepUntil(time);
+ double releaseValue = ramp.output.getValue();
+ assertEquals(0.0, releaseValue, 0.0001, "env after release time should go to zero");
+ }
+
+ @Test
+ public void testReleaseTiming() throws InterruptedException {
+ checkReleaseTiming(0.1, 0.004);
+ checkReleaseTiming(1.0, 0.002);
+ checkReleaseTiming(2.5, 0.001);
+ checkReleaseTiming(10.0, 0.001);
+ }
+
+}
diff --git a/src/test/java/com/jsyn/unitgen/TestFunction.java b/src/test/java/com/jsyn/unitgen/TestFunction.java
new file mode 100644
index 0000000..65953cd
--- /dev/null
+++ b/src/test/java/com/jsyn/unitgen/TestFunction.java
@@ -0,0 +1,77 @@
+/*
+ * 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.unitgen;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.data.DoubleTable;
+import com.jsyn.data.Function;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @author Phil Burk, (C) 2009 Mobileer Inc
+ */
+public class TestFunction {
+ Synthesizer synth;
+
+ @BeforeEach
+ protected void beforeEach() {
+ synth = JSyn.createSynthesizer();
+ synth.setRealTime(false);
+ synth.start();
+ }
+
+ @AfterEach
+ protected void afterEach() {
+ synth.stop();
+ }
+
+ @Test
+ public void testDoubleTable() {
+ double[] data = {
+ 2.0, 0.0, 3.0
+ };
+ DoubleTable table = new DoubleTable(data);
+ assertEquals(2.0, table.evaluate(-1.4), "DoubleTable below");
+ assertEquals(2.0, table.evaluate(-1.0), "DoubleTable edge");
+ assertEquals(1.0, table.evaluate(-0.5), "DoubleTable mid");
+ assertEquals(0.0, table.evaluate(0.0), "DoubleTable zero");
+ assertEquals(0.75, table.evaluate(0.25), "DoubleTable mid");
+ assertEquals(3.0, table.evaluate(1.3), "DoubleTable above");
+
+ }
+
+ @Test
+ public void testFunctionEvaluator() throws InterruptedException {
+ FunctionEvaluator shaper = new FunctionEvaluator();
+ synth.add(shaper);
+ shaper.start();
+
+ Function cuber = x -> x * x * x;
+ shaper.function.set(cuber);
+
+ shaper.input.set(0.5);
+ synth.sleepFor(0.001);
+
+ assertEquals((0.5 * 0.5 * 0.5), shaper.output.getValue(), "Cuber");
+ }
+
+}
diff --git a/src/test/java/com/jsyn/unitgen/TestMath.java b/src/test/java/com/jsyn/unitgen/TestMath.java
new file mode 100644
index 0000000..7c33223
--- /dev/null
+++ b/src/test/java/com/jsyn/unitgen/TestMath.java
@@ -0,0 +1,420 @@
+/*
+ * 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.unitgen;
+
+import com.jsyn.engine.SynthesisEngine;
+import com.softsynth.math.AudioMath;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @author Phil Burk, (C) 2009 Mobileer Inc
+ */
+public class TestMath {
+ SynthesisEngine synthesisEngine;
+
+ @BeforeEach
+ protected void beforeEach() {
+ synthesisEngine = new SynthesisEngine();
+ }
+
+ @Test
+ public void testAdd() {
+ Add add = new Add();
+ add.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ add.inputA.setValueInternal(x);
+ add.inputB.setValueInternal(y);
+
+ add.generate();
+
+ assertEquals(x + y, add.output.getValue(), 0.001, "Add");
+ }
+
+ @Test
+ public void testPartialAdd() {
+ Add add = new Add();
+ add.setSynthesisEngine(synthesisEngine);
+
+ double x = 2.5;
+ double y = 9.7;
+ add.inputA.setValueInternal(x);
+ add.inputB.setValueInternal(y);
+
+ // Only generate a few values in the middle.
+ // This is to test low latency feedback loops.
+ // Only generate values for 2,3,4
+ add.generate(2, 5);
+
+ assertEquals(0.0, add.output.getValues()[0], 0.001, "Add partial");
+ assertEquals(0.0, add.output.getValues()[1], 0.001, "Add partial");
+ assertEquals(x + y, add.output.getValues()[2], 0.001, "Add partial");
+ assertEquals(x + y, add.output.getValues()[3], 0.001, "Add partial");
+ assertEquals(x + y, add.output.getValues()[4], 0.001, "Add partial");
+ assertEquals(0.0, add.output.getValues()[5], 0.001, "Add partial");
+ assertEquals(0.0, add.output.getValues()[6], 0.001, "Add partial");
+ assertEquals(0.0, add.output.getValues()[7], 0.001, "Add partial");
+
+ }
+
+ /**
+ * Unit test for Subtract.java - added by Lisa Tolentino 06/17/2009
+ */
+ @Test
+ public void testSubtract() {
+ Subtract sub = new Subtract();
+ sub.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ sub.inputA.setValueInternal(x);
+ sub.inputB.setValueInternal(y);
+
+ sub.generate();
+
+ assertEquals(x - y, sub.output.getValue(), 0.001, "Subtract");
+ }
+
+ @Test
+ public void testPartialSubtract() {
+ Subtract sub = new Subtract();
+ sub.setSynthesisEngine(synthesisEngine);
+
+ double x = 2.5;
+ double y = 9.7;
+ sub.inputA.setValueInternal(x);
+ sub.inputB.setValueInternal(y);
+
+ // Only generate a few values in the middle.
+ // This is to test low latency feedback loops.
+ // Only generate values for 2,3,4
+ sub.generate(2, 5);
+
+ assertEquals(0.0, sub.output.getValues()[0], 0.001, "Subtract partial");
+ assertEquals(0.0, sub.output.getValues()[1], 0.001, "Subtract partial");
+ assertEquals(x - y, sub.output.getValues()[2], 0.001, "Subtract partial");
+ assertEquals(x - y, sub.output.getValues()[3], 0.001, "Subtract partial");
+ assertEquals(x - y, sub.output.getValues()[4], 0.001, "Subtract partial");
+ assertEquals(0.0, sub.output.getValues()[5], 0.001, "Subtract partial");
+ assertEquals(0.0, sub.output.getValues()[6], 0.001, "Subtract partial");
+ assertEquals(0.0, sub.output.getValues()[7], 0.001, "Subtract partial");
+ }
+
+ /**
+ * Unit test for Multiply.java - added by Lisa Tolentino 06/19/2009
+ */
+ @Test
+ public void testMultiply() {
+ Multiply mult = new Multiply();
+ mult.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ mult.inputA.setValueInternal(x);
+ mult.inputB.setValueInternal(y);
+
+ mult.generate();
+
+ assertEquals(x * y, mult.output.getValue(), 0.001, "Multiply");
+ }
+
+ @Test
+ public void testPartialMultiply() {
+ Multiply mult = new Multiply();
+ mult.setSynthesisEngine(synthesisEngine);
+
+ double x = 2.5;
+ double y = 9.7;
+ mult.inputA.setValueInternal(x);
+ mult.inputB.setValueInternal(y);
+
+ // Only generate a few values in the middle.
+ // This is to test low latency feedback loops.
+ // Only generate values for 2,3,4
+ mult.generate(2, 5);
+
+ assertEquals(0.0, mult.output.getValues()[0], 0.001, "Multiply partial");
+ assertEquals(0.0, mult.output.getValues()[1], 0.001, "Multiply partial");
+ assertEquals(x * y, mult.output.getValues()[2], 0.001, "Multiply partial");
+ assertEquals(x * y, mult.output.getValues()[3], 0.001, "Multiply partial");
+ assertEquals(x * y, mult.output.getValues()[4], 0.001, "Multiply partial");
+ assertEquals(0.0, mult.output.getValues()[5], 0.001, "Multiply partial");
+ assertEquals(0.0, mult.output.getValues()[6], 0.001, "Multiply partial");
+ assertEquals(0.0, mult.output.getValues()[7], 0.001, "Multiply partial");
+ }
+
+ /**
+ * Unit test for Divide.java - added by Lisa Tolentino 06/19/2009
+ */
+ @Test
+ public void testDivide() {
+ Divide divide = new Divide();
+ divide.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ divide.inputA.setValueInternal(x);
+ divide.inputB.setValueInternal(y);
+
+ divide.generate();
+
+ assertEquals(x / y, divide.output.getValue(), 0.001, "Divide");
+ }
+
+ @Test
+ public void testPartialDivide() {
+ Divide divide = new Divide();
+ divide.setSynthesisEngine(synthesisEngine);
+
+ double x = 2.5;
+ double y = 9.7;
+ divide.inputA.setValueInternal(x);
+ divide.inputB.setValueInternal(y);
+
+ // Only generate a few values in the middle.
+ // This is to test low latency feedback loops.
+ // Only generate values for 2,3,4
+ divide.generate(2, 5);
+
+ assertEquals(0.0, divide.output.getValues()[0], 0.001, "Divide partial");
+ assertEquals(0.0, divide.output.getValues()[1], 0.001, "Divide partial");
+ assertEquals(x / y, divide.output.getValues()[2], 0.001, "Divide partial");
+ assertEquals(x / y, divide.output.getValues()[3], 0.001, "Divide partial");
+ assertEquals(x / y, divide.output.getValues()[4], 0.001, "Divide partial");
+ assertEquals(0.0, divide.output.getValues()[5], 0.001, "Divide partial");
+ assertEquals(0.0, divide.output.getValues()[6], 0.001, "Divide partial");
+ assertEquals(0.0, divide.output.getValues()[7], 0.001, "Divide partial");
+ }
+
+ /**
+ * Unit test for MultiplyAdd.java - added by Lisa Tolentino 06/19/2009
+ */
+ @Test
+ public void testMultiplyAdd() {
+ MultiplyAdd multAdd = new MultiplyAdd();
+ multAdd.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ double z = 2.28;
+ multAdd.inputA.setValueInternal(x);
+ multAdd.inputB.setValueInternal(y);
+ multAdd.inputC.setValueInternal(z);
+
+ multAdd.generate();
+
+ assertEquals((x * y) + z, multAdd.output.getValue(), 0.001, "MultiplyAdd");
+ }
+
+ @Test
+ public void testPartialMultiplyAdd() {
+ MultiplyAdd multAdd = new MultiplyAdd();
+ multAdd.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ double z = 2.28;
+ multAdd.inputA.setValueInternal(x);
+ multAdd.inputB.setValueInternal(y);
+ multAdd.inputC.setValueInternal(z);
+
+ // Only generate a few values in the middle.
+ // This is to test low latency feedback loops.
+ // Only generate values for 2,3,4
+ multAdd.generate(2, 5);
+
+ assertEquals(0.0, multAdd.output.getValues()[0], 0.001, "MultiplyAdd partial");
+ assertEquals(0.0, multAdd.output.getValues()[1], 0.001, "MultiplyAdd partial");
+ assertEquals((x * y) + z, multAdd.output.getValues()[2], 0.001, "MultiplyAdd partial");
+ assertEquals((x * y) + z, multAdd.output.getValues()[3], 0.001, "MultiplyAdd partial");
+ assertEquals((x * y) + z, multAdd.output.getValues()[4], 0.001, "MultiplyAdd partial");
+ assertEquals(0.0, multAdd.output.getValues()[5], 0.001, "MultiplyAdd partial");
+ assertEquals(0.0, multAdd.output.getValues()[6], 0.001, "MultiplyAdd partial");
+ assertEquals(0.0, multAdd.output.getValues()[7], 0.001, "MultiplyAdd partial");
+ }
+
+ /**
+ * Unit test for Compare.java - added by Lisa Tolentino 06/19/2009
+ */
+ @Test
+ public void testCompare() {
+ UnitBinaryOperator compare = new Compare();
+ compare.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ compare.inputA.setValueInternal(x);
+ compare.inputB.setValueInternal(y);
+
+ compare.generate();
+
+ assertEquals((x > y ? 1 : 0), compare.output.getValue(), 0.001, "Compare");
+ }
+
+ @Test
+ public void testPartialCompare() {
+ UnitBinaryOperator compare = new Compare();
+ compare.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ compare.inputA.setValueInternal(x);
+ compare.inputB.setValueInternal(y);
+
+ // Only generate a few values in the middle.
+ // This is to test low latency feedback loops.
+ // Only generate values for 2,3,4
+ compare.generate(2, 5);
+
+ assertEquals(0.0, compare.output.getValues()[0], 0.001, "Compare partial");
+ assertEquals(0.0, compare.output.getValues()[1], 0.001, "Compare partial");
+ assertEquals((x > y ? 1 : 0), compare.output.getValues()[2], 0.001, "Compare partial");
+ assertEquals((x > y ? 1 : 0), compare.output.getValues()[3], 0.001, "Compare partial");
+ assertEquals((x > y ? 1 : 0), compare.output.getValues()[4], 0.001, "Compare partial");
+ assertEquals(0.0, compare.output.getValues()[5], 0.001, "Compare partial");
+ assertEquals(0.0, compare.output.getValues()[6], 0.001, "Compare partial");
+ assertEquals(0.0, compare.output.getValues()[7], 0.001, "Compare partial");
+ }
+
+ /**
+ * Unit test for Maximum.java - added by Lisa Tolentino 06/20/2009
+ */
+ @Test
+ public void testMaximum() {
+ Maximum max = new Maximum();
+ max.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ max.inputA.setValueInternal(x);
+ max.inputB.setValueInternal(y);
+
+ max.generate();
+
+ assertEquals((x > y ? x : y), max.output.getValue(), 0.001, "Maximum");
+ }
+
+ @Test
+ public void testPartialMaximum() {
+ Maximum max = new Maximum();
+ max.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ max.inputA.setValueInternal(x);
+ max.inputB.setValueInternal(y);
+
+ // Only generate a few values in the middle.
+ // This is to test low latency feedback loops.
+ // Only generate values for 2,3,4
+ max.generate(2, 5);
+
+ assertEquals(0.0, max.output.getValues()[0], 0.001, "Maximum partial");
+ assertEquals(0.0, max.output.getValues()[1], 0.001, "Maximum partial");
+ assertEquals((x > y ? x : y), max.output.getValues()[2], 0.001, "Maximum partial");
+ assertEquals((x > y ? x : y), max.output.getValues()[3], 0.001, "Maximum partial");
+ assertEquals((x > y ? x : y), max.output.getValues()[4], 0.001, "Maximum partial");
+ assertEquals(0.0, max.output.getValues()[5], 0.001, "Maximum partial");
+ assertEquals(0.0, max.output.getValues()[6], 0.001, "Maximum partial");
+ assertEquals(0.0, max.output.getValues()[7], 0.001, "Maximum partial");
+ }
+
+ /**
+ * Unit test for Minimum.java - added by Lisa Tolentino 06/20/2009
+ */
+ @Test
+ public void testMinimum() {
+ Minimum min = new Minimum();
+ min.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ min.inputA.setValueInternal(x);
+ min.inputB.setValueInternal(y);
+
+ min.generate();
+
+ assertEquals((x < y ? x : y), min.output.getValue(), 0.001, "Minimum");
+ }
+
+ @Test
+ public void testPartialMinimum() {
+ Minimum min = new Minimum();
+ min.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ min.inputA.setValueInternal(x);
+ min.inputB.setValueInternal(y);
+
+ // Only generate a few values in the middle.
+ // This is to test low latency feedback loops.
+ // Only generate values for 2,3,4
+ min.generate(2, 5);
+
+ assertEquals(0.0, min.output.getValues()[0], 0.001, "Maximum partial");
+ assertEquals(0.0, min.output.getValues()[1], 0.001, "Maximum partial");
+ assertEquals((x < y ? x : y), min.output.getValues()[2], 0.001, "Maximum partial");
+ assertEquals((x < y ? x : y), min.output.getValues()[3], 0.001, "Maximum partial");
+ assertEquals((x < y ? x : y), min.output.getValues()[4], 0.001, "Maximum partial");
+ assertEquals(0.0, min.output.getValues()[5], 0.001, "Maximum partial");
+ assertEquals(0.0, min.output.getValues()[6], 0.001, "Maximum partial");
+ assertEquals(0.0, min.output.getValues()[7], 0.001, "Maximum partial");
+ }
+
+ @Test
+ public void testPowerOfTwo() {
+ PowerOfTwo powerOfTwo = new PowerOfTwo();
+ powerOfTwo.setSynthesisEngine(synthesisEngine);
+ final double smallValue = -1.5308084989341915E-17;
+ double[] values = {
+ 0.0, 1.3, 4.5, -0.5, -1.0, -2.8, smallValue, -smallValue, 1.0 - smallValue,
+ 1.0 + smallValue
+ };
+ for (double in : values) {
+ powerOfTwo.input.setValueInternal(in);
+ powerOfTwo.generate();
+ assertEquals(Math.pow(2.0, in), powerOfTwo.output.getValue(), 0.001, "PowerOfTwo");
+ }
+ }
+
+ @Test
+ public void testPitchToFrequency() {
+ PitchToFrequency ugen = new PitchToFrequency();
+ ugen.setSynthesisEngine(synthesisEngine);
+ final double smallValue = -1.5308084989341915E-17;
+ double[] values = {
+ 49.0, 49.5, 50.0 + smallValue,
+ 60.0 -smallValue,
+ 79.2, 12.9, 118.973
+ };
+ // Sanity check AudioMath
+ assertEquals(440.0, AudioMath.pitchToFrequency(69), 0.001, "PitchToFrequency");
+ assertEquals(660.0, AudioMath.pitchToFrequency(69+7.02), 0.1, "PitchToFrequency");
+
+ for (double pitch : values) {
+ ugen.input.setValueInternal(pitch);
+ ugen.generate();
+ assertEquals(ugen.output.getValue(), 0.001, "PitchToFrequency: " + AudioMath.pitchToFrequency(pitch));
+ }
+ }
+
+}
diff --git a/src/test/java/com/jsyn/unitgen/TestRamps.java b/src/test/java/com/jsyn/unitgen/TestRamps.java
new file mode 100644
index 0000000..40d968c
--- /dev/null
+++ b/src/test/java/com/jsyn/unitgen/TestRamps.java
@@ -0,0 +1,205 @@
+/*
+ * 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.unitgen;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class TestRamps extends NonRealTimeTestCase {
+
+ public void viewContinuousRamp(double duration, double startValue, double targetValue)
+ throws InterruptedException {
+ ContinuousRamp ramp = new ContinuousRamp();
+ synthesisEngine.add(ramp);
+
+ ramp.current.set(startValue);
+ ramp.input.set(startValue);
+ ramp.time.set(duration);
+
+ synthesisEngine.setRealTime(false);
+ synthesisEngine.start();
+ ramp.start();
+ synthesisEngine.sleepUntil(synthesisEngine.getCurrentTime() + 0.01);
+ ramp.input.set(targetValue);
+
+ double time = synthesisEngine.getCurrentTime();
+ int numLoops = 20;
+ double increment = duration / numLoops;
+ for (int i = 0; i < (numLoops + 1); i++) {
+ double value = ramp.output.getValue();
+ System.out.printf("i = %d, t = %9.5f, value = %8.4f\n", i, time, value);
+ time += increment;
+ synthesisEngine.sleepUntil(time);
+ }
+
+ synthesisEngine.stop();
+ }
+
+ public void checkContinuousRamp(double duration, double startValue, double targetValue)
+ throws InterruptedException {
+ ContinuousRamp ramp = new ContinuousRamp();
+ synthesisEngine.add(ramp);
+
+ ramp.current.set(startValue);
+ ramp.input.set(startValue);
+ ramp.time.set(duration);
+
+ synthesisEngine.setRealTime(false);
+ synthesisEngine.start();
+ ramp.start();
+ synthesisEngine.sleepUntil(synthesisEngine.getCurrentTime() + 0.01);
+ assertEquals(ramp.input.getValue(), ramp.output.getValue(), "start flat");
+
+ ramp.input.set(targetValue);
+ double startTime = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(startTime + (duration / 2));
+ assertEquals((targetValue + startValue) / 2.0, ramp.output.getValue(), 0.01, "ramping up");
+ synthesisEngine.sleepUntil(startTime + duration);
+ assertEquals(targetValue, ramp.output.getValue(), 0.01, "ramping up");
+ synthesisEngine.sleepUntil(startTime + duration + 0.1);
+ assertEquals(targetValue, ramp.output.getValue(), "flat again");
+
+ synthesisEngine.stop();
+ }
+
+ @Test
+ public void testContinuousRamp() throws InterruptedException {
+ viewContinuousRamp(4.0, 0.0, 1.0);
+ }
+
+ @Test
+ public void testExponentialRamp() throws InterruptedException {
+ ExponentialRamp ramp = new ExponentialRamp();
+ synthesisEngine.add(ramp);
+
+ double duration = 0.3;
+ ramp.current.set(1.0);
+ ramp.input.set(1.0);
+ ramp.time.set(duration);
+
+ synthesisEngine.start();
+ ramp.start();
+ synthesisEngine.sleepUntil(synthesisEngine.getCurrentTime() + 0.01);
+ assertEquals(ramp.input.getValue(), ramp.output.getValue(), "start flat");
+
+ ramp.input.set(8.0);
+ double startTime = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(startTime + 0.1);
+ assertEquals(2.0, ramp.output.getValue(), 0.01, "ramping up");
+ synthesisEngine.sleepUntil(startTime + 0.2);
+ assertEquals(4.0, ramp.output.getValue(), 0.01, "ramping up");
+ synthesisEngine.sleepUntil(startTime + 0.3);
+ assertEquals(8.0, ramp.output.getValue(), 0.01, "ramping up");
+ synthesisEngine.sleepUntil(startTime + 0.4);
+ assertEquals(8.0, ramp.output.getValue(), "flat again");
+ }
+
+ @Test
+ public void testLinearRamp() throws InterruptedException {
+ LinearRamp ramp = new LinearRamp();
+ synthesisEngine.add(ramp);
+
+ double duration = 0.4;
+ ramp.current.set(0.0);
+ ramp.input.set(0.0);
+ ramp.time.set(duration);
+
+ synthesisEngine.start();
+ ramp.start();
+ synthesisEngine.sleepUntil(synthesisEngine.getCurrentTime() + 0.01);
+ assertEquals(ramp.input.getValue(), ramp.output.getValue(), "start flat");
+
+ ramp.input.set(8.0);
+ double startTime = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(startTime + 0.1);
+ assertEquals(2.0, ramp.output.getValue(), 0.01, "ramping up");
+ synthesisEngine.sleepUntil(startTime + 0.2);
+ assertEquals(4.0, ramp.output.getValue(), 0.01, "ramping up");
+ synthesisEngine.sleepUntil(startTime + 0.3);
+ assertEquals(6.0, ramp.output.getValue(), 0.01, "ramping up");
+ synthesisEngine.sleepUntil(startTime + 0.4);
+ assertEquals(8.0, ramp.output.getValue(), "flat again");
+ }
+
+ @Test
+ public void testExponentialRampConnected() throws InterruptedException {
+ ExponentialRamp ramp = new ExponentialRamp();
+ PassThrough pass = new PassThrough();
+ synthesisEngine.add(ramp);
+ synthesisEngine.add(pass);
+
+ double duration = 0.3;
+ ramp.current.set(1.0);
+ pass.input.set(1.0);
+ ramp.time.set(duration);
+
+ // Send value through a connected unit.
+ pass.input.set(1.0);
+ pass.output.connect(ramp.input);
+
+ synthesisEngine.start();
+ ramp.start();
+ synthesisEngine.sleepUntil(synthesisEngine.getCurrentTime() + 0.01);
+ assertEquals(ramp.input.getValue(), ramp.output.getValue(), "start flat");
+
+ pass.input.set(8.0);
+ double startTime = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(startTime + 0.1);
+ assertEquals(2.0, ramp.output.getValue(), 0.01, "ramping up");
+ synthesisEngine.sleepUntil(startTime + 0.2);
+ assertEquals(4.0, ramp.output.getValue(), 0.01, "ramping up");
+ synthesisEngine.sleepUntil(startTime + 0.3);
+ assertEquals(8.0, ramp.output.getValue(), 0.01, "ramping up");
+ synthesisEngine.sleepUntil(startTime + 0.4);
+ assertEquals(8.0, ramp.output.getValue(), "flat again");
+ }
+
+ @Test
+ public void testLinearRampConnected() throws InterruptedException {
+ LinearRamp ramp = new LinearRamp();
+ PassThrough pass = new PassThrough();
+ synthesisEngine.add(ramp);
+ synthesisEngine.add(pass);
+
+ double duration = 0.4;
+ ramp.current.set(0.0);
+ pass.input.set(0.0);
+ ramp.time.set(duration);
+
+ // Send value through a connected unit.
+ pass.input.set(0.0);
+ pass.output.connect(ramp.input);
+
+ synthesisEngine.start();
+ ramp.start();
+ synthesisEngine.sleepUntil(synthesisEngine.getCurrentTime() + 0.01);
+ assertEquals(ramp.input.getValue(), ramp.output.getValue(), "start flat");
+
+ pass.input.set(8.0);
+ double startTime = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(startTime + 0.1);
+ assertEquals(2.0, ramp.output.getValue(), 0.01, "ramping up");
+ synthesisEngine.sleepUntil(startTime + 0.2);
+ assertEquals(4.0, ramp.output.getValue(), 0.01, "ramping up");
+ synthesisEngine.sleepUntil(startTime + 0.3);
+ assertEquals(6.0, ramp.output.getValue(), 0.01, "ramping up");
+ synthesisEngine.sleepUntil(startTime + 0.4);
+ assertEquals(8.0, ramp.output.getValue(), "flat again");
+ }
+
+}
diff --git a/src/test/java/com/jsyn/unitgen/TestUnitGate.java b/src/test/java/com/jsyn/unitgen/TestUnitGate.java
new file mode 100644
index 0000000..cba03e7
--- /dev/null
+++ b/src/test/java/com/jsyn/unitgen/TestUnitGate.java
@@ -0,0 +1,81 @@
+/*
+ * 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.unitgen;
+
+import com.jsyn.engine.SynthesisEngine;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestUnitGate {
+
+ protected SynthesisEngine synthesisEngine;
+ protected double time;
+
+ public void checkAutoDisable(LinearRamp ramp, UnitGate envelope) throws InterruptedException {
+ double tolerance = 0.01;
+ Add adder = new Add();
+ synthesisEngine.add(adder);
+
+ envelope.output.connect(adder.inputA);
+ if (ramp.getCircuit() != null) {
+ ramp.output.connect(adder.inputB);
+ }
+
+ envelope.input.setAutoDisableEnabled(true);
+ envelope.setEnabled(false);
+
+ // set up so ramp value should equal time
+ ramp.current.set(0.0);
+ ramp.input.set(1.0);
+ ramp.time.set(1.0);
+
+ synthesisEngine.start();
+ // pull from final adder
+ adder.start();
+
+ time = synthesisEngine.getCurrentTime();
+ time += 0.1;
+ synthesisEngine.sleepUntil(time);
+ assertEquals(0.0, envelope.output.getValue(), "still idling");
+ assertEquals(0.0, ramp.output.getValue(), tolerance, "ramp frozen at beginning");
+
+ // run multiple times to make sure we can retrigger the envelope.
+ for (int i = 0; i < 3; i++) {
+ double level = ramp.output.getValue();
+ // Trigger the envelope using trigger()
+ envelope.input.on();
+ time += 0.1;
+ level += 0.1;
+ synthesisEngine.sleepUntil(time);
+ assertEquals(level, ramp.output.getValue(), tolerance, "ramp going up " + i);
+ assertTrue(envelope.isEnabled(), "enabled at peak");
+
+ envelope.input.off();
+ time += 0.1;
+ level += 0.1;
+ synthesisEngine.sleepUntil(time);
+ assertEquals(level, ramp.output.getValue(), tolerance, "ramp going up more " + i);
+ assertEquals(0.0, envelope.output.getValue(), 0.1, "at bottom");
+
+ time += 0.2;
+ synthesisEngine.sleepUntil(time);
+ assertEquals(level, ramp.output.getValue(), tolerance, "ramp frozen " + i);
+ }
+ }
+
+}
diff --git a/src/test/java/com/jsyn/util/DebugSampleLoader.java b/src/test/java/com/jsyn/util/DebugSampleLoader.java
new file mode 100644
index 0000000..c0ddef5
--- /dev/null
+++ b/src/test/java/com/jsyn/util/DebugSampleLoader.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2010 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.util;
+
+import java.io.File;
+import java.io.IOException;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.data.FloatSample;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.VariableRateDataReader;
+import com.jsyn.unitgen.VariableRateMonoReader;
+import com.jsyn.unitgen.VariableRateStereoReader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Play a sample from a WAV file using JSyn.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class DebugSampleLoader {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DebugSampleLoader.class);
+
+ private Synthesizer synth;
+ private VariableRateDataReader samplePlayer;
+ private LineOut lineOut;
+
+ private void test() throws IOException {
+ // File sampleFile = new File("samples/cello_markers.wav");
+ // File sampleFile = new File("samples/Piano_A440_PT.aif");
+ File sampleFile = new File("samples/sine_400_loop_i16.wav");
+ // File sampleFile = new File("samples/TwoDiffPitchedSines_F32_PT.wav");
+ // File sampleFile = new File("samples/sine_400_u8.aif");
+ // File sampleFile = new File("samples/sine_400_s8.aif");
+ // File sampleFile = new File("samples/sine_400_ulaw.aif");
+ // File sampleFile = new File("samples/sine_400_ulaw.wav");
+
+ // File sampleFile = new File("samples/aaClarinet.wav");
+ // File sampleFile = new File("samples/sine_400_mono.wav");
+ // File sampleFile = new File("samples/sine_200_300_i16.wav");
+ // File sampleFile = new File("samples/sine_200_300_i24.wav");
+ // File sampleFile = new File("samples/M1F1-int16-AFsp.wav");
+ // File sampleFile = new File("samples/M1F1-int24-AFsp.wav");
+ // File sampleFile = new File("samples/M1F1-float32-AFsp.wav");
+ // File sampleFile = new File("samples/M1F1-int16WE-AFsp.wav");
+ // File sampleFile = new File("samples/M1F1-int24WE-AFsp.wav");
+ // File sampleFile = new File("samples/M1F1-float32WE-AFsp.wav");
+ // File sampleFile = new File("samples/sine_200_300_i16.aif");
+ // File sampleFile = new File("samples/sine_200_300_f32.wavex");
+ // File sampleFile = new File("samples/Sine32bit.aif");
+ // File sampleFile = new File("samples/Sine32bit.wav");
+ // File sampleFile = new File("samples/smartCue.wav");
+
+ // URL sampleFile = new URL("http://www.softsynth.com/samples/Clarinet.wav");
+
+ synth = JSyn.createSynthesizer();
+
+ FloatSample sample;
+ try {
+ // Add an output mixer.
+ synth.add(lineOut = new LineOut());
+
+ // Load the sample and display its properties.
+ SampleLoader.setJavaSoundPreferred(false);
+ sample = SampleLoader.loadFloatSample(sampleFile);
+ LOGGER.debug("Sample has: channels = " + sample.getChannelsPerFrame());
+ LOGGER.debug(" frames = " + sample.getNumFrames());
+ LOGGER.debug(" rate = " + sample.getFrameRate());
+ LOGGER.debug(" loopStart = " + sample.getSustainBegin());
+ LOGGER.debug(" loopEnd = " + sample.getSustainEnd());
+
+ if (sample.getChannelsPerFrame() == 1) {
+ synth.add(samplePlayer = new VariableRateMonoReader());
+ samplePlayer.output.connect(0, lineOut.input, 0);
+ } else if (sample.getChannelsPerFrame() == 2) {
+ synth.add(samplePlayer = new VariableRateStereoReader());
+ samplePlayer.output.connect(0, lineOut.input, 0);
+ samplePlayer.output.connect(1, lineOut.input, 1);
+ } else {
+ throw new RuntimeException("Can only play mono or stereo samples.");
+ }
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+
+ samplePlayer.rate.set(sample.getFrameRate());
+
+ // We only need to start the LineOut. It will pull data from the
+ // sample player.
+ lineOut.start();
+
+ // We can simply queue the entire file.
+ // Or if it has a loop we can play the loop for a while.
+ if (sample.getSustainBegin() < 0) {
+ LOGGER.debug("queue the sample");
+ samplePlayer.dataQueue.queue(sample);
+ } else {
+ LOGGER.debug("queueOn the sample");
+ samplePlayer.dataQueue.queueOn(sample);
+ synth.sleepFor(8.0);
+ LOGGER.debug("queueOff the sample");
+ samplePlayer.dataQueue.queueOff(sample);
+ }
+
+ // Wait until the sample has finished playing.
+ do {
+ synth.sleepFor(1.0);
+ } while (samplePlayer.dataQueue.hasMore());
+
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ // Stop everything.
+ synth.stop();
+ }
+
+ public static void main(String[] args) {
+ try {
+ new DebugSampleLoader().test();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/test/java/com/jsyn/util/TestFFT.java b/src/test/java/com/jsyn/util/TestFFT.java
new file mode 100644
index 0000000..5d130c5
--- /dev/null
+++ b/src/test/java/com/jsyn/util/TestFFT.java
@@ -0,0 +1,201 @@
+/*
+ * 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.util;
+
+import com.softsynth.math.FourierMath;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class TestFFT {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TestFFT.class);
+
+ public void checkSingleSineDouble(int size, int bin) {
+ double[] ar = new double[size];
+ double[] ai = new double[size];
+ double[] magnitudes = new double[size];
+
+ double amplitude = 1.0;
+ addSineWave(size, bin, ar, amplitude);
+
+ FourierMath.transform(1, size, ar, ai);
+ FourierMath.calculateMagnitudes(ar, ai, magnitudes);
+
+ assertEquals(0.0, magnitudes[bin-1], 0.000001, "magnitude");
+ assertEquals(amplitude, magnitudes[bin], 0.000001, "magnitude");
+ assertEquals(0.0, magnitudes[bin+1], 0.000001, "magnitude");
+ /*
+ for (int i = 0; i < magnitudes.length; i++) {
+ System.out.printf("%d = %9.7f\n", i, magnitudes[i]);
+ }
+*/
+
+ }
+
+ public void checkSingleSineFloat(int size, int bin) {
+ float[] ar = new float[size];
+ float[] ai = new float[size];
+ float[] magnitudes = new float[size];
+
+ double amplitude = 1.0;
+ addSineWave(size, bin, ar, amplitude);
+
+ FourierMath.transform(1, size, ar, ai);
+ FourierMath.calculateMagnitudes(ar, ai, magnitudes);
+
+ assertEquals(0.0f, magnitudes[bin-1], 0.000001, "magnitude");
+ assertEquals(amplitude, magnitudes[bin], 0.000001, "magnitude");
+ assertEquals(0.0f, magnitudes[bin+1], 0.000001, "magnitude");
+/*
+ for (int i = 0; i < magnitudes.length; i++) {
+ System.out.printf("%d = %9.7f\n", i, magnitudes[i]);
+ }
+*/
+ }
+
+ public void checkMultipleSine(int size, int[] bins, double[] amplitudes) {
+ double[] ar = new double[size];
+ double[] ai = new double[size];
+ double[] magnitudes = new double[size];
+
+ for(int i = 0; i<bins.length; i++) {
+ addSineWave(size, bins[i], ar, amplitudes[i]);
+ }
+
+ FourierMath.transform(1, size, ar, ai);
+ FourierMath.calculateMagnitudes(ar, ai, magnitudes);
+
+ for(int bin = 0; bin<size; bin++) {
+ System.out.printf("%d = %9.7f\n", bin, magnitudes[bin]);
+
+ double amplitude = 0.0;
+ for(int i = 0; i<bins.length; i++) {
+ if ((bin == bins[i]) || (bin == (size - bins[i]))) {
+ amplitude = amplitudes[i];
+ break;
+ }
+ }
+ assertEquals(amplitude, magnitudes[bin], 0.000001, "magnitude");
+ }
+
+ }
+
+ private void addSineWave(int size, int bin, double[] ar, double amplitude) {
+ double phase = 0.0;
+ double phaseIncrement = 2.0 * Math.PI * bin / size;
+ for (int i = 0; i < size; i++) {
+ ar[i] += Math.sin(phase) * amplitude;
+ phase += phaseIncrement;
+ }
+ }
+ private void addSineWave(int size, int bin, float[] ar, double amplitude) {
+ double phase = 0.0;
+ double phaseIncrement = 2.0 * Math.PI * bin / size;
+ for (int i = 0; i < size; i++) {
+ ar[i] += (float) (Math.sin(phase) * amplitude);
+ phase += phaseIncrement;
+ }
+ }
+
+ public void testSinglesDouble() {
+ checkSingleSineDouble(32, 1);
+ checkSingleSineDouble(32, 4);
+ checkSingleSineDouble(64, 5);
+ checkSingleSineDouble(256, 3);
+ }
+
+ public void testSinglesFloat() {
+ checkSingleSineFloat(32, 1);
+ checkSingleSineFloat(32, 4);
+ checkSingleSineFloat(64, 5);
+ checkSingleSineFloat(256, 3);
+ }
+
+ public void testMultipleSines32() {
+ int[] bins = { 1, 5 };
+ double[] amplitudes = { 1.0, 2.0 };
+ checkMultipleSine(32, bins, amplitudes);
+ }
+
+ public void testMultipleSines64() {
+ int[] bins = { 2, 4, 7 };
+ double[] amplitudes = { 1.0, 0.3, 0.5 };
+ checkMultipleSine(64, bins, amplitudes);
+ }
+
+ public void checkInverseFftDouble(int size, int bin) {
+ double[] ar1 = new double[size];
+ double[] ai1 = new double[size];
+ double[] ar2 = new double[size];
+ double[] ai2 = new double[size];
+
+ double amplitude = 1.0;
+ addSineWave(size, bin, ar1, amplitude);
+
+ // Save a copy of the source.
+ System.arraycopy(ar1, 0, ar2, 0, size);
+ System.arraycopy(ai1, 0, ai2, 0, size);
+
+ FourierMath.transform(1, size, ar1, ai1); // FFT
+
+ FourierMath.transform(-1, size, ar1, ai1); // IFFT
+
+ for (int i = 0; i < size; i++) {
+ assertEquals(ar2[i], ar1[i], 0.00001);
+ assertEquals(ai2[i], ai1[i], 0.00001);
+ }
+ }
+
+ public void checkInverseFftFloat(int size, int bin) {
+ float[] ar1 = new float[size];
+ float[] ai1 = new float[size];
+ float[] ar2 = new float[size];
+ float[] ai2 = new float[size];
+
+ double amplitude = 1.0;
+ addSineWave(size, bin, ar1, amplitude);
+
+ // Save a copy of the source.
+ System.arraycopy(ar1, 0, ar2, 0, size);
+ System.arraycopy(ai1, 0, ai2, 0, size);
+
+ FourierMath.transform(1, size, ar1, ai1); // FFT
+
+ FourierMath.transform(-1, size, ar1, ai1); // IFFT
+
+ for (int i = 0; i < size; i++) {
+ assertEquals(ar2[i], ar1[i], 0.00001);
+ assertEquals(ai2[i], ai1[i], 0.00001);
+ }
+ }
+
+ public void testInverseDouble() {
+ checkInverseFftDouble(32, 1);
+ checkInverseFftDouble(32, 2);
+ checkInverseFftDouble(128, 17);
+ checkInverseFftDouble(512, 23);
+ }
+
+ public void testInverseFloat() {
+ checkInverseFftFloat(32, 1);
+ checkInverseFftFloat(32, 2);
+ checkInverseFftFloat(128, 17);
+ checkInverseFftFloat(512, 23);
+ }
+}
diff --git a/src/test/java/com/jsyn/util/TestPseudoRandom.java b/src/test/java/com/jsyn/util/TestPseudoRandom.java
new file mode 100644
index 0000000..b37475f
--- /dev/null
+++ b/src/test/java/com/jsyn/util/TestPseudoRandom.java
@@ -0,0 +1,76 @@
+/*
+ * 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.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestPseudoRandom {
+ PseudoRandom pseudoRandom;
+ private int[] bins;
+ private final static int BIN_SHIFTER = 8;
+ private final static int BIN_COUNT = 1 << BIN_SHIFTER;
+ private final static int BIN_MASK = BIN_COUNT - 1;
+
+ @Test
+ public void testMath() {
+ long seed = 3964771111L;
+ int positiveInt = (int) (seed & 0x7FFFFFFF);
+ assertTrue((positiveInt >= 0), "masked random positive, " + positiveInt);
+ double rand = positiveInt * (1.0 / (1L << 31));
+ assertTrue((rand >= 0.0), "not too low, " + rand);
+ assertTrue((rand < 1.0), "not too high, " + rand);
+ }
+
+ @Test
+ public void testIntegerDistribution() {
+ int scaler = 100;
+ for (int i = 0; i < (bins.length * scaler); i++) {
+ int rand = pseudoRandom.nextRandomInteger();
+ int positiveInt = rand & 0x7FFFFFFF;
+ assertTrue((positiveInt >= 0), "masked random " + positiveInt);
+ int index = (rand >> (32 - BIN_SHIFTER)) & BIN_MASK;
+ bins[index] += 1;
+ }
+ checkDistribution(scaler);
+ }
+
+ @Test
+ public void test01Distribution() {
+ int scaler = 100;
+ for (int i = 0; i < (bins.length * scaler); i++) {
+ double rand = pseudoRandom.random();
+ assertTrue((rand >= 0.0), "not too low, #" + i + " = " + rand);
+ assertTrue((rand < 1.0), "not too high, #" + i + " = " + rand);
+ int index = (int) (rand * BIN_COUNT);
+ bins[index] += 1;
+ }
+ checkDistribution(scaler);
+ }
+
+ private void checkDistribution(int scaler) {
+ // Generate running average that should stay near scaler
+ double average = scaler;
+ double coefficient = 0.9;
+ for (int i = 0; i < (bins.length); i++) {
+ average = (average * coefficient) + (bins[i] * (1.0 - coefficient));
+ assertEquals(scaler, average, 0.2 * scaler, "average at " + i);
+ }
+ }
+}
diff --git a/src/test/java/com/jsyn/util/TestVoiceAllocator.java b/src/test/java/com/jsyn/util/TestVoiceAllocator.java
new file mode 100644
index 0000000..061e2ae
--- /dev/null
+++ b/src/test/java/com/jsyn/util/TestVoiceAllocator.java
@@ -0,0 +1,111 @@
+/*
+ * 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.util;
+
+import com.jsyn.instruments.SubtractiveSynthVoice;
+import com.jsyn.unitgen.UnitVoice;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestVoiceAllocator {
+ VoiceAllocator allocator;
+ int max = 4;
+ UnitVoice[] voices;
+
+ @BeforeEach
+ private void beforeEach() {
+ voices = new UnitVoice[max];
+ for (int i = 0; i < max; i++) {
+ voices[i] = new SubtractiveSynthVoice();
+ }
+
+ allocator = new VoiceAllocator(voices);
+ }
+
+ @Test
+ public void testAllocation() {
+ assertEquals(max, allocator.getVoiceCount(), "get max");
+
+ int tag1 = 61;
+ int tag2 = 62;
+ int tag3 = 63;
+ int tag4 = 64;
+ int tag5 = 65;
+ int tag6 = 66;
+ UnitVoice voice1 = allocator.allocate(tag1);
+ assertTrue((voice1 != null), "voice should be non-null");
+
+ UnitVoice voice2 = allocator.allocate(tag2);
+ assertTrue((voice2 != null), "voice should be non-null");
+ assertTrue((voice2 != voice1), "new voice ");
+
+ UnitVoice voice = allocator.allocate(tag1);
+ assertTrue((voice == voice1), "should be voice1 again ");
+
+ voice = allocator.allocate(tag2);
+ assertTrue((voice == voice2), "should be voice2 again ");
+
+ UnitVoice voice3 = allocator.allocate(tag3);
+ @SuppressWarnings("unused")
+ UnitVoice voice4 = allocator.allocate(tag4);
+
+ UnitVoice voice5 = allocator.allocate(tag5);
+ assertTrue((voice5 == voice1), "ran out so get voice1 as oldest");
+
+ voice = allocator.allocate(tag2);
+ assertTrue((voice == voice2), "should be voice2 again ");
+
+ // Now voice 3 should be the oldest cuz voice 2 was touched.
+ UnitVoice voice6 = allocator.allocate(tag6);
+ assertTrue((voice6 == voice3), "ran out so get voice3 as oldest");
+ }
+
+ @Test
+ public void testOff() {
+ int tag1 = 61;
+ int tag2 = 62;
+ int tag3 = 63;
+ int tag4 = 64;
+ int tag5 = 65;
+ int tag6 = 66;
+ UnitVoice voice1 = allocator.allocate(tag1);
+ UnitVoice voice2 = allocator.allocate(tag2);
+ UnitVoice voice3 = allocator.allocate(tag3);
+ UnitVoice voice4 = allocator.allocate(tag4);
+
+ assertTrue(allocator.isOn(tag3), "voice 3 should start on");
+ allocator.off(tag3);
+ assertFalse(allocator.isOn(tag3), "voice 3 should now be off");
+
+ allocator.off(tag2);
+
+ UnitVoice voice5 = allocator.allocate(tag5);
+ assertTrue((voice5 == voice3), "should get voice3 cuz off first");
+ UnitVoice voice6 = allocator.allocate(tag6);
+ assertTrue((voice6 == voice2), "should get voice2 cuz off second");
+ voice3 = allocator.allocate(tag3);
+ assertTrue((voice3 == voice1), "should get voice1 cuz on first");
+
+ voice1 = allocator.allocate(tag1);
+ assertTrue((voice1 == voice4), "should get voice4 cuz next up");
+ }
+}