diff options
author | RubbaBoy <[email protected]> | 2020-07-06 02:33:28 -0400 |
---|---|---|
committer | Phil Burk <[email protected]> | 2020-10-30 11:19:34 -0700 |
commit | 46888fae6eb7b1dd386f7af7d101ead99ae61981 (patch) | |
tree | 8969bbfd68d2fb5c0d8b86da49ec2eca230a72ab /src/test/java/com | |
parent | c51e92e813dd481603de078f0778e1f75db2ab05 (diff) |
Restructured project, added gradle, JUnit, logger, and more
Added Gradle (and removed ant), modernized testing via the JUnit framework, moved standalone examples from the tests directory to a separate module, removed sparsely used Java logger and replaced it with SLF4J. More work could be done, however this is a great start to greatly improving the health of the codebase.
Diffstat (limited to 'src/test/java/com')
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"); + } +} |