aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/com/jsyn/engine/SynthesisEngine.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/jsyn/engine/SynthesisEngine.java')
-rw-r--r--src/main/java/com/jsyn/engine/SynthesisEngine.java700
1 files changed, 700 insertions, 0 deletions
diff --git a/src/main/java/com/jsyn/engine/SynthesisEngine.java b/src/main/java/com/jsyn/engine/SynthesisEngine.java
new file mode 100644
index 0000000..30872a8
--- /dev/null
+++ b/src/main/java/com/jsyn/engine/SynthesisEngine.java
@@ -0,0 +1,700 @@
+/*
+ * 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.util.ArrayList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.devices.AudioDeviceFactory;
+import com.jsyn.devices.AudioDeviceInputStream;
+import com.jsyn.devices.AudioDeviceManager;
+import com.jsyn.devices.AudioDeviceOutputStream;
+import com.jsyn.unitgen.UnitGenerator;
+import com.softsynth.shared.time.ScheduledCommand;
+import com.softsynth.shared.time.ScheduledQueue;
+import com.softsynth.shared.time.TimeStamp;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+//TODO Resolve problem with HearDAHDSR where "Rate" port.set is not reflected in knob. Engine not running.
+//TODO new tutorial and docs on website
+//TODO AutoStop on DAHDSR
+//TODO Test/example SequentialData queueOn and queueOff
+
+//TODO Abstract device interface. File device!
+//TODO Measure thread switching sync, performance for multi-core synthesis. Use 4 core pro.
+//TODO Optimize SineOscillatorPhaseModulated
+//TODO More circuits.
+//TODO DC blocker
+//TODO Swing scope probe UIs, auto ranging
+
+/**
+ * Internal implementation of JSyn Synthesizer. The public API is in the Synthesizer interface. This
+ * class might be used directly internally.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @see Synthesizer
+ */
+public class SynthesisEngine implements Synthesizer {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SynthesisEngine.class);
+
+ private final static int BLOCKS_PER_BUFFER = 8;
+ private final static int FRAMES_PER_BUFFER = Synthesizer.FRAMES_PER_BLOCK * BLOCKS_PER_BUFFER;
+ // I have measured JavaSound taking 1200 msec to close devices.
+ private static final int MAX_THREAD_STOP_TIME = 2000;
+
+ public static final int DEFAULT_FRAME_RATE = 44100;
+
+ private final AudioDeviceManager audioDeviceManager;
+ private EngineThread engineThread;
+ private final ScheduledQueue<ScheduledCommand> commandQueue = new ScheduledQueue<ScheduledCommand>();
+
+ private InterleavingBuffer inputBuffer;
+ private InterleavingBuffer outputBuffer;
+ private double inverseNyquist;
+ private long frameCount;
+ private boolean pullDataEnabled = true;
+ private boolean useRealTime = true;
+ private boolean started;
+ private int frameRate = DEFAULT_FRAME_RATE;
+ private double framePeriod = 1.0 / frameRate;
+
+ // List of all units added to the synth.
+ private final ArrayList<UnitGenerator> allUnitList = new ArrayList<UnitGenerator>();
+ // List of running units.
+ private final ArrayList<UnitGenerator> runningUnitList = new ArrayList<UnitGenerator>();
+ // List of units stopping because of autoStop.
+ private final ArrayList<UnitGenerator> stoppingUnitList = new ArrayList<UnitGenerator>();
+
+ private LoadAnalyzer loadAnalyzer;
+ // private int numOutputChannels;
+ // private int numInputChannels;
+ private final CopyOnWriteArrayList<Runnable> audioTasks = new CopyOnWriteArrayList<Runnable>();
+ private double mOutputLatency;
+ private double mInputLatency;
+ /** A fraction corresponding to exactly -96 dB. */
+ public static final double DB96 = (1.0 / 63095.73444801943);
+ /** A fraction that is approximately -90.3 dB. Defined as 1 bit of an S16. */
+ public static final double DB90 = (1.0 / (1 << 15));
+
+ public SynthesisEngine(AudioDeviceManager audioDeviceManager) {
+ this.audioDeviceManager = audioDeviceManager;
+ }
+
+ public SynthesisEngine() {
+ this(AudioDeviceFactory.createAudioDeviceManager());
+ }
+
+ @Override
+ public String getVersion() {
+ return JSyn.VERSION;
+ }
+
+ @Override
+ public int getVersionCode() {
+ return JSyn.VERSION_CODE;
+ }
+
+ @Override
+ public String toString() {
+ return "JSyn " + JSyn.VERSION_TEXT;
+ }
+
+ public boolean isPullDataEnabled() {
+ return pullDataEnabled;
+ }
+
+ /**
+ * If set true then audio data will be pulled from the output ports of connected unit
+ * generators. The final unit in a tree of units needs to be start()ed.
+ *
+ * @param pullDataEnabled
+ */
+ public void setPullDataEnabled(boolean pullDataEnabled) {
+ this.pullDataEnabled = pullDataEnabled;
+ }
+
+ private void setupAudioBuffers(int numInputChannels, int numOutputChannels) {
+ inputBuffer = new InterleavingBuffer(FRAMES_PER_BUFFER, Synthesizer.FRAMES_PER_BLOCK,
+ numInputChannels);
+ outputBuffer = new InterleavingBuffer(FRAMES_PER_BUFFER, Synthesizer.FRAMES_PER_BLOCK,
+ numOutputChannels);
+ }
+
+ public void terminate() {
+ }
+
+ class InterleavingBuffer {
+ private final double[] interleavedBuffer;
+ ChannelBlockBuffer[] blockBuffers;
+
+ InterleavingBuffer(int framesPerBuffer, int framesPerBlock, int samplesPerFrame) {
+ interleavedBuffer = new double[framesPerBuffer * samplesPerFrame];
+ // Allocate buffers for each channel of synthesis output.
+ blockBuffers = new ChannelBlockBuffer[samplesPerFrame];
+ for (int i = 0; i < blockBuffers.length; i++) {
+ blockBuffers[i] = new ChannelBlockBuffer(framesPerBlock);
+ }
+ }
+
+ int deinterleave(int inIndex) {
+ for (int jf = 0; jf < Synthesizer.FRAMES_PER_BLOCK; jf++) {
+ for (int iob = 0; iob < blockBuffers.length; iob++) {
+ ChannelBlockBuffer buffer = blockBuffers[iob];
+ buffer.values[jf] = interleavedBuffer[inIndex++];
+ }
+ }
+ return inIndex;
+ }
+
+ int interleave(int outIndex) {
+ for (int jf = 0; jf < Synthesizer.FRAMES_PER_BLOCK; jf++) {
+ for (int iob = 0; iob < blockBuffers.length; iob++) {
+ ChannelBlockBuffer buffer = blockBuffers[iob];
+ interleavedBuffer[outIndex++] = buffer.values[jf];
+ }
+ }
+ return outIndex;
+ }
+
+ public double[] getChannelBuffer(int i) {
+ return blockBuffers[i].values;
+ }
+
+ public void clear() {
+ for (int i = 0; i < blockBuffers.length; i++) {
+ blockBuffers[i].clear();
+ }
+ }
+ }
+
+ static class ChannelBlockBuffer {
+ private final double[] values;
+
+ ChannelBlockBuffer(int framesPerBlock) {
+ values = new double[framesPerBlock];
+ }
+
+ void clear() {
+ for (int i = 0; i < values.length; i++) {
+ values[i] = 0.0f;
+ }
+ }
+ }
+
+ @Override
+ public void start() {
+ // TODO Use constants.
+ start(DEFAULT_FRAME_RATE, -1, 0, -1, 2);
+ }
+
+ @Override
+ public void start(int frameRate) {
+ // TODO Use constants.
+ start(frameRate, -1, 0, -1, 2);
+ }
+
+ @Override
+ public synchronized void start(int frameRate, int inputDeviceID, int numInputChannels,
+ int outputDeviceID, int numOutputChannels) {
+ if (started) {
+ LOGGER.info("JSyn already started.");
+ return;
+ }
+
+ this.frameRate = frameRate;
+ this.framePeriod = 1.0 / frameRate;
+
+ setupAudioBuffers(numInputChannels, numOutputChannels);
+
+ LOGGER.info("Pure Java JSyn from www.softsynth.com, rate = " + frameRate + ", "
+ + (useRealTime ? "RT" : "NON-RealTime") + ", " + JSyn.VERSION_TEXT);
+
+ inverseNyquist = 2.0 / frameRate;
+
+ if (useRealTime) {
+ engineThread = new EngineThread(inputDeviceID, numInputChannels,
+ outputDeviceID, numOutputChannels);
+ LOGGER.debug("Synth thread old priority = " + engineThread.getPriority());
+ int engineThreadPriority = engineThread.getPriority() + 2 > Thread.MAX_PRIORITY ?
+ Thread.MAX_PRIORITY : engineThread.getPriority() + 2;
+ engineThread.setPriority(engineThreadPriority);
+ LOGGER.debug("Synth thread new priority = " + engineThread.getPriority());
+ engineThread.start();
+ }
+
+ started = true;
+ }
+
+ @Override
+ public boolean isRunning() {
+ Thread thread = engineThread;
+ return (thread != null) && thread.isAlive();
+ }
+
+ @Override
+ public synchronized void stop() {
+ if (!started) {
+ LOGGER.info("JSyn already stopped.");
+ return;
+ }
+
+ if (useRealTime) {
+ // Stop audio synthesis and all units.
+ if (engineThread != null) {
+ try {
+ // Interrupt now, otherwise audio thread will wait for audio I/O.
+ engineThread.requestStop();
+ engineThread.join(MAX_THREAD_STOP_TIME);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ synchronized (runningUnitList) {
+ runningUnitList.clear();
+ }
+ started = false;
+ }
+
+ private class EngineThread extends Thread
+ {
+ private AudioDeviceOutputStream audioOutputStream;
+ private AudioDeviceInputStream audioInputStream;
+ private volatile boolean go = true;
+
+ EngineThread(int inputDeviceID, int numInputChannels,
+ int outputDeviceID, int numOutputChannels) {
+ if (numInputChannels > 0) {
+ audioInputStream = audioDeviceManager.createInputStream(inputDeviceID, frameRate,
+ numInputChannels);
+ }
+ if (numOutputChannels > 0) {
+ audioOutputStream = audioDeviceManager.createOutputStream(outputDeviceID,
+ frameRate, numOutputChannels);
+ }
+ }
+
+ public void requestStop() {
+ go = false;
+ interrupt();
+ }
+
+ @Override
+ public void run() {
+ LOGGER.debug("JSyn synthesis thread starting.");
+ try {
+ if (audioInputStream != null) {
+ LOGGER.debug("JSyn synthesis thread trying to start audio INPUT!");
+ audioInputStream.start();
+ mInputLatency = audioInputStream.getLatency();
+ String msg = String.format("Input Latency in = %5.1f msec",
+ 1000 * mInputLatency);
+ LOGGER.debug(msg);
+ }
+ if (audioOutputStream != null) {
+ LOGGER.debug("JSyn synthesis thread trying to start audio OUTPUT!");
+ audioOutputStream.start();
+ mOutputLatency = audioOutputStream.getLatency();
+ String msg = String.format("Output Latency = %5.1f msec",
+ 1000 * mOutputLatency);
+ LOGGER.debug(msg);
+ // Buy some time while we fill the buffer.
+ audioOutputStream.write(outputBuffer.interleavedBuffer);
+ }
+ loadAnalyzer = new LoadAnalyzer();
+ while (go) {
+ boolean throttled = false;
+ if (audioInputStream != null) {
+ // This call will block when the input is empty.
+ audioInputStream.read(inputBuffer.interleavedBuffer);
+ throttled = true;
+ }
+
+ loadAnalyzer.start();
+ runAudioTasks();
+ generateNextBuffer();
+ loadAnalyzer.stop();
+
+ if (audioOutputStream != null) {
+ // This call will block when the output is full.
+ audioOutputStream.write(outputBuffer.interleavedBuffer);
+ throttled = true;
+ }
+ if (!throttled && isRealTime()) {
+ Thread.sleep(2); // avoid spinning and eating up CPU
+ }
+ }
+
+ } catch (Throwable e) {
+ e.printStackTrace();
+ go = false;
+
+ } finally {
+ LOGGER.info("JSyn synthesis thread in finally code.");
+ // Stop audio system.
+ if (audioInputStream != null) {
+ audioInputStream.stop();
+ }
+ if (audioOutputStream != null) {
+ audioOutputStream.stop();
+ }
+ }
+ LOGGER.debug("JSyn synthesis thread exiting.");
+ }
+ }
+
+ private void runAudioTasks() {
+ for (Runnable task : audioTasks) {
+ task.run();
+ }
+ }
+
+ // TODO We need to implement a sharedSleeper like we use in JSyn1.
+ public void generateNextBuffer() {
+ int outIndex = 0;
+ int inIndex = 0;
+ for (int i = 0; i < BLOCKS_PER_BUFFER; i++) {
+ if (inputBuffer != null) {
+ inIndex = inputBuffer.deinterleave(inIndex);
+ }
+
+ TimeStamp timeStamp = createTimeStamp();
+ // Try putting this up here so incoming time-stamped events will get
+ // scheduled later.
+ processScheduledCommands(timeStamp);
+ clearBlockBuffers();
+ synthesizeBuffer();
+
+ if (outputBuffer != null) {
+ outIndex = outputBuffer.interleave(outIndex);
+ }
+ frameCount += Synthesizer.FRAMES_PER_BLOCK;
+ }
+ }
+
+ @Override
+ public double getCurrentTime() {
+ return frameCount * framePeriod;
+ }
+
+ @Override
+ public TimeStamp createTimeStamp() {
+ return new TimeStamp(getCurrentTime());
+ }
+
+ private void processScheduledCommands(TimeStamp timeStamp) {
+ List<ScheduledCommand> timeList = commandQueue.removeNextList(timeStamp);
+
+ while (timeList != null) {
+ while (!timeList.isEmpty()) {
+ ScheduledCommand command = timeList.remove(0);
+ LOGGER.debug("processing " + command + ", at time " + timeStamp.getTime());
+ command.run();
+ }
+ // Get next list of commands at the given time.
+ timeList = commandQueue.removeNextList(timeStamp);
+ }
+ }
+
+ @Override
+ public void scheduleCommand(TimeStamp timeStamp, ScheduledCommand command) {
+ if ((Thread.currentThread() == engineThread) && (timeStamp.getTime() <= getCurrentTime())) {
+ command.run();
+ } else {
+ LOGGER.debug("scheduling " + command + ", at time " + timeStamp.getTime());
+ commandQueue.add(timeStamp, command);
+ }
+ }
+
+ @Override
+ public void scheduleCommand(double time, ScheduledCommand command) {
+ TimeStamp timeStamp = new TimeStamp(time);
+ scheduleCommand(timeStamp, command);
+ }
+
+ @Override
+ public void queueCommand(ScheduledCommand command) {
+ TimeStamp timeStamp = createTimeStamp();
+ scheduleCommand(timeStamp, command);
+ }
+
+ @Override
+ public void clearCommandQueue() {
+ commandQueue.clear();
+ }
+
+ private void clearBlockBuffers() {
+ outputBuffer.clear();
+ }
+
+ private void synthesizeBuffer() {
+ synchronized (runningUnitList) {
+ ListIterator<UnitGenerator> iterator = runningUnitList.listIterator();
+ while (iterator.hasNext()) {
+ UnitGenerator unit = iterator.next();
+ if (pullDataEnabled) {
+ unit.pullData(getFrameCount(), 0, Synthesizer.FRAMES_PER_BLOCK);
+ } else {
+ unit.generate(0, Synthesizer.FRAMES_PER_BLOCK);
+ }
+ }
+ // Remove any units that got auto stopped.
+ for (UnitGenerator ugen : stoppingUnitList) {
+ runningUnitList.remove(ugen);
+ ugen.flattenOutputs();
+ }
+ }
+ stoppingUnitList.clear();
+ }
+
+ public double[] getInputBuffer(int i) {
+ try {
+ return inputBuffer.getChannelBuffer(i);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new RuntimeException("Audio Input not configured in start() method.");
+ }
+ }
+
+ public double[] getOutputBuffer(int i) {
+ try {
+ return outputBuffer.getChannelBuffer(i);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new RuntimeException("Audio Output not configured in start() method.");
+ }
+ }
+
+ private void internalStopUnit(UnitGenerator unit) {
+ synchronized (runningUnitList) {
+ runningUnitList.remove(unit);
+ }
+ unit.flattenOutputs();
+ }
+
+ public void autoStopUnit(UnitGenerator unitGenerator) {
+ synchronized (stoppingUnitList) {
+ stoppingUnitList.add(unitGenerator);
+ }
+ }
+
+ @Override
+ public void startUnit(UnitGenerator unit, double time) {
+ startUnit(unit, new TimeStamp(time));
+ }
+
+ @Override
+ public void stopUnit(UnitGenerator unit, double time) {
+ stopUnit(unit, new TimeStamp(time));
+ }
+
+ @Override
+ public void startUnit(final UnitGenerator unit, TimeStamp timeStamp) {
+ // Don't start if it is a component in a circuit because it will be
+ // executed by the circuit.
+ if (unit.getCircuit() == null) {
+ scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ internalStartUnit(unit);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void stopUnit(final UnitGenerator unit, TimeStamp timeStamp) {
+ scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ internalStopUnit(unit);
+ }
+ });
+ }
+
+ @Override
+ public void startUnit(UnitGenerator unit) {
+ startUnit(unit, createTimeStamp());
+ }
+
+ @Override
+ public void stopUnit(UnitGenerator unit) {
+ stopUnit(unit, createTimeStamp());
+ }
+
+ private void internalStartUnit(UnitGenerator unit) {
+ // LOGGER.info( "internalStartUnit " + unit + " with circuit " +
+ // unit.getCircuit() );
+ if (unit.getCircuit() == null) {
+ synchronized (runningUnitList) {
+ if (!runningUnitList.contains(unit)) {
+ runningUnitList.add(unit);
+ }
+ }
+ }
+ // else
+ // {
+ // LOGGER.info(
+ // "internalStartUnit detected race condition !!!! from old JSyn" + unit
+ // + " with circuit " + unit.getCircuit() );
+ // }
+ }
+
+ public double getInverseNyquist() {
+ return inverseNyquist;
+ }
+
+ public double convertTimeToExponentialScaler(double duration) {
+ // Calculate scaler so that scaler^frames = target/source
+ double numFrames = duration * getFrameRate();
+ return Math.pow(DB90, (1.0 / numFrames));
+ }
+
+ @Override
+ public long getFrameCount() {
+ return frameCount;
+ }
+
+ /**
+ * @return the frameRate
+ */
+ @Override
+ public int getFrameRate() {
+ return frameRate;
+ }
+
+ /**
+ * @return the inverse of the frameRate for efficiency
+ */
+ @Override
+ public double getFramePeriod() {
+ return framePeriod;
+ }
+
+ /** Convert a short value to a double in the range -1.0 to almost 1.0. */
+ public static double convertShortToDouble(short sdata) {
+ return (sdata * (1.0 / Short.MAX_VALUE));
+ }
+
+ /**
+ * Convert a double value in the range -1.0 to almost 1.0 to a short. Double value is clipped
+ * before converting.
+ */
+ public static short convertDoubleToShort(double d) {
+ final double maxValue = ((double) (Short.MAX_VALUE - 1)) / Short.MAX_VALUE;
+ if (d > maxValue) {
+ d = maxValue;
+ } else if (d < -1.0) {
+ d = -1.0;
+ }
+ return (short) (d * Short.MAX_VALUE);
+ }
+
+ @Override
+ public void addAudioTask(Runnable blockTask) {
+ audioTasks.add(blockTask);
+ }
+
+ @Override
+ public void removeAudioTask(Runnable blockTask) {
+ audioTasks.remove(blockTask);
+ }
+
+ @Override
+ public double getUsage() {
+ // use temp so we don't have to synchronize
+ LoadAnalyzer temp = loadAnalyzer;
+ if (temp != null) {
+ return temp.getAverageLoad();
+ } else {
+ return 0.0;
+ }
+ }
+
+ @Override
+ public AudioDeviceManager getAudioDeviceManager() {
+ return audioDeviceManager;
+ }
+
+ @Override
+ public void setRealTime(boolean realTime) {
+ useRealTime = realTime;
+ }
+
+ @Override
+ public boolean isRealTime() {
+ return useRealTime;
+ }
+
+ public double getOutputLatency() {
+ return mOutputLatency;
+ }
+
+ public double getInputLatency() {
+ return mInputLatency;
+ }
+
+ @Override
+ public void add(UnitGenerator ugen) {
+ ugen.setSynthesisEngine(this);
+ allUnitList.add(ugen);
+ }
+
+ @Override
+ public void remove(UnitGenerator ugen) {
+ allUnitList.remove(ugen);
+ }
+
+ @Override
+ public void sleepUntil(double time) throws InterruptedException {
+ double timeToSleep = time - getCurrentTime();
+ while (timeToSleep > 0.0) {
+ if (useRealTime) {
+ long msecToSleep = (long) (1000 * timeToSleep);
+ if (msecToSleep <= 0) {
+ msecToSleep = 1;
+ }
+ Thread.sleep(msecToSleep);
+ } else {
+
+ generateNextBuffer();
+ }
+ timeToSleep = time - getCurrentTime();
+ }
+ }
+
+ @Override
+ public void sleepFor(double duration) throws InterruptedException {
+ sleepUntil(getCurrentTime() + duration);
+ }
+
+ public void printConnections() {
+ if (pullDataEnabled) {
+ ListIterator<UnitGenerator> iterator = runningUnitList.listIterator();
+ while (iterator.hasNext()) {
+ UnitGenerator unit = iterator.next();
+ unit.printConnections();
+ }
+ }
+
+ }
+
+}