diff options
Diffstat (limited to 'src/main/java/com/jsyn/engine/SynthesisEngine.java')
-rw-r--r-- | src/main/java/com/jsyn/engine/SynthesisEngine.java | 700 |
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(); + } + } + + } + +} |