All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jaudiolibs.pipes.graph.Graph Maven / Gradle / Ivy

Go to download

Support for building and running graphs of Pipes UGens, based on the audio API inside PraxisCORE.

The newest version!
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2019 Neil C Smith.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 3 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * version 3 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License version 3
 * along with this work; if not, see http://www.gnu.org/licenses/
 *
 */
package org.jaudiolibs.pipes.graph;

import java.util.List;
import java.util.Objects;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableScheduledFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.DoubleBinaryOperator;
import java.util.function.DoubleUnaryOperator;
import org.jaudiolibs.pipes.Add;
import org.jaudiolibs.pipes.Pipe;
import org.jaudiolibs.pipes.Tee;
import org.jaudiolibs.pipes.units.AudioTable;
import org.jaudiolibs.pipes.units.Fn;
import org.jaudiolibs.pipes.units.Mod;

/**
 * A base audio graph class that replicates the audio API and UGen support
 * inside PraxisLIVE's audio nodes.#
 * 

* Unless specifically noted, methods in this class are not thread-safe, and * should only be called on the audio thread. */ public abstract class Graph { final Pipe[] inputs; final Pipe[] outputs; final List dependents; final Scheduler scheduler; double sampleRate; int blockSize; long samplePosition; /** * Base Graph constructor for graph with default 2 channels of audio input * and output. */ protected Graph() { this(2, 2); } /** * Base Graph constructor. * * @param inputCount number of input channels (can be zero) * @param outputCount number of output channels */ protected Graph(int inputCount, int outputCount) { inputs = new Pipe[inputCount]; outputs = new Pipe[outputCount]; for (int i = 0; i < inputs.length; i++) { inputs[i] = new Tee(); } for (int i = 0; i < outputs.length; i++) { outputs[i] = new Add(); } this.dependents = new CopyOnWriteArrayList<>(); this.scheduler = new Scheduler(this); dependents.add(scheduler); } void handleInit() { samplePosition = -blockSize; init(); } void handleUpdate() { samplePosition += blockSize; if (samplePosition < 0) { samplePosition = 0; } dependents.forEach(Dependent::update); update(); } /** * Method called to initialize the audio graph. */ protected abstract void init(); /** * Optional update hook called before every buffer is processed. */ protected void update() { } /** * Query the position in samples. * * @return position in samples */ public long position() { return samplePosition; } /** * Query the audio block size / size of each processed buffer. * * @return block size */ public int blockSize() { return blockSize; } /** * Query the sample rate in Hz. * * @return sample rate */ public double sampleRate() { return sampleRate; } /** * Query the time in milliseconds, based on the sample playback clock. * * @return time in milliseconds */ public long millis() { return (long) (samplePosition / sampleRate * 1000); } /** * Query the time in nanoseconds, based on the sample playback clock. * * @return time in nanoseconds */ public long nanos() { return (long) (samplePosition / sampleRate * 1000_000_000); } /** * Add a task to be run on the audio thread before the next buffer is * processed. This method is safe to call from another thread. * * @param task task to run */ public void invokeLater(Runnable task) { scheduler.execute(task); } /** * Get a {@link ScheduledExecutorService} for running tasks on the audio * thread. This method is safe to call from another thread. The scheduler * cannot be terminated and will throw an exception if attempting to * shutdown or await termination. * * @return executor service */ public ScheduledExecutorService scheduler() { return scheduler; } /** * Add a {@link Dependent} to the graph. * * @param dependent dependent to add */ protected void addDependent(Dependent dependent) { dependents.add(Objects.requireNonNull(dependent)); dependent.attach(this); } /** * Remove a {@link Dependent} from the graph. * * @param dependent dependent to remove */ protected void removeDependent(Dependent dependent) { dependents.remove(dependent); dependent.detach(this); } /** * Access the Pipe for the specified input channel. * * @param index input channel * @return input Pipe */ protected final Pipe in(int index) { return inputs[index]; } /** * Access the Pipe for the specified output channel. * * @param index output channel * @return output Pipe */ protected final Pipe out(int index) { return outputs[index]; } /** * Link the provided list of units (Pipes) together. The last provided Pipe * is returned. * * @param ugens list of Pipes to connect * @return last Pipe */ protected final Pipe link(Pipe... ugens) { int count = ugens.length; if (count < 1) { return null; } for (int i = ugens.length - 1; i > 0; i--) { ugens[i].addSource(ugens[i - 1]); } return ugens[ugens.length - 1]; } /** * Create an {@link Add} Pipe. This unit type can add together multiple * inputs. * * @return add pipe */ protected final Add add() { return new Add(); } /** * Create an {@link Add} Pipe, and add the provided Pipes as inputs to it. * This unit type can add together multiple inputs. Additional inputs may be * added later. * * @param ugens list of Pipes to add * @return add pipe */ protected final Add add(Pipe... ugens) { Add add = new Add(); for (Pipe ugen : ugens) { add.addSource(ugen); } return add; } /** * Create a {@link Mod} pipe. The Mod pipe accepts multiple inputs and an * optional function to combine them. The default function is to multiply * samples together. * * @return mod pipe */ protected final Mod mod() { return new Mod(); } /** * Create a {@link Mod} pipe, and add the provided Pipes as inputs to it. * The Mod pipe accepts multiple inputs and an optional function to combine * them. The default function is to multiply samples together. * * @return mod pipe */ protected final Mod mod(Pipe... ugens) { Mod mod = new Mod(); for (Pipe ugen : ugens) { mod.addSource(ugen); } return mod; } /** * Create a {@link Mod} pipe that combines its inputs by applying the * provided function to each sample. * * @param function operation to apply to each sample * @return mod pipe */ protected final Mod modFn(DoubleBinaryOperator function) { Mod mod = new Mod(); mod.function(function); return mod; } /** * Create a {@link Mod} pipe that combines its inputs by applying the * provided function to each sample. The provided Pipe will be added as the * first input. * * @param pipe input pipe * @param function operation to apply to each sample * @return mod pipe */ protected final Mod modFn(Pipe pipe, DoubleBinaryOperator function) { Mod mod = modFn(function); mod.addSource(pipe); return mod; } /** * Create a {@link Tee} pipe that can split its input to multiple outputs. * The input data is buffered, so the Tee may be connected to pipes that * feed into its input. This will create a delay of one block size. * * @return tee pipe */ protected final Tee tee() { return new Tee(); } /** * Create a {@link Fn} Pipe that applies the given operation to every * sample. * * @param function operation to apply to each sample * @return Fn pipe */ protected final Fn fn(DoubleUnaryOperator function) { return new Fn(function); } /** * Convert the given note expressed as a String (eg. "a3", "C#5") into its * frequency. An invalid note will return 0 (useful for eg. "X" for note * off) * * @param note String representation of a note * @return frequency in Hz */ protected final double noteToFrequency(String note) { int midi = noteToMidi(note); if (midi < 0) { return 0; } else { return midiToFrequency(midi); } } /** * Convert the given note expressed as a String (eg. "a3", "C#5") into its * equivalent MIDI note. An invalid note will return -1 (useful for eg. "X" * for note off) * * @param note String representation of a note * @return MIDI note number */ protected final int noteToMidi(String note) { return NoteUtils.noteToMidi(note); } /** * Convert the given MIDI note number into its frequency in Hz. * * @param midi note number * @return frequency in Hz */ protected final double midiToFrequency(int midi) { return NoteUtils.midiToFrequency(midi); } /** * Read from the first channel of the given {@link AudioTable} using a * normalized position (0 .. 1). Out of range values or a null table will * return silence. The returned sample is interpolated where required when * the position is between sample points. * * @param table audio data to read from * @param position position normalized 0 .. 1 * @return sample from table */ protected double tabread(AudioTable table, double position) { return table == null ? 0 : table.get(0, position * table.size()); } /** * An interface for types that can be attached to a Graph and will be called * before every new buffer is processed. */ public static interface Dependent { /** * Optional hook called when attached to the Graph. * * @param graph the Graph being added to */ public default void attach(Graph graph) { } /** * Optional hook to be informed when removed from the graph. * * @param graph the Graph being removed from */ public default void detach(Graph graph) { } /** * Called before every new buffer is processed. */ public void update(); } private static class Scheduler extends AbstractExecutorService implements ScheduledExecutorService, Dependent { private final Graph graph; private final ConcurrentLinkedQueue queue; private final DelayQueue> delayQueue; private long time; private Scheduler(Graph graph) { this.graph = graph; this.queue = new ConcurrentLinkedQueue<>(); this.delayQueue = new DelayQueue<>(); } @Override public void shutdown() { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override public List shutdownNow() { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override public boolean isShutdown() { return false; } @Override public boolean isTerminated() { return false; } @Override public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override public void execute(Runnable command) { queue.offer(command); } @Override public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { ScheduledFutureTask task = new ScheduledFutureTask(command, null, unit.toNanos(delay), 0); queue.offer(task); return task; } @Override public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) { ScheduledFutureTask task = new ScheduledFutureTask<>(callable, unit.toNanos(delay), 0); queue.offer(task); return task; } @Override public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { ScheduledFutureTask task = new ScheduledFutureTask(command, null, unit.toNanos(initialDelay), unit.toNanos(period)); queue.offer(task); return task; } @Override public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { return scheduleAtFixedRate(command, initialDelay, delay, unit); } @Override public void update() { this.time = graph.nanos(); Runnable r; while ((r = queue.poll()) != null) { if (r instanceof ScheduledFutureTask) { ScheduledFutureTask task = (ScheduledFutureTask) r; task.time = this.time + task.delay; delayQueue.offer(task); } else { r.run(); } } while ((r = delayQueue.poll()) != null) { r.run(); } } private class ScheduledFutureTask extends FutureTask implements RunnableScheduledFuture { private final long delay; private final long period; private long time; ScheduledFutureTask(Runnable runnable, T result, long delay, long period) { super(runnable, result); this.delay = delay; this.period = period; } ScheduledFutureTask(Callable callable, long delay, long period) { super(callable); this.delay = delay; this.period = period; } @Override public boolean isPeriodic() { return period > 0; } @Override public void run() { if (isPeriodic()) { if (runAndReset()) { time += period; delayQueue.offer(this); } } else { super.run(); } } @Override public long getDelay(TimeUnit unit) { return unit.convert(time - graph.nanos(), TimeUnit.NANOSECONDS); } @Override public int compareTo(Delayed other) { if (other == this) { return 0; } if (other instanceof ScheduledFuture) { ScheduledFutureTask o = (ScheduledFutureTask) other; long diff = time - o.time; return diff < 0 ? -1 : 1; } else { throw new UnsupportedOperationException(); } } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy