DSP Engine from Ethereal Dialpad
package as.adamsmith.etherealdialpad.dsp; | |
import android.media.AudioFormat; | |
import android.media.AudioManager; | |
import android.media.AudioTrack; | |
public class Dac extends UGen { | |
private final float[] localBuffer; | |
private boolean isClean; | |
private final AudioTrack track; | |
private final short [] target = new short[UGen.CHUNK_SIZE]; | |
private final short [] silentTarget = new short[UGen.CHUNK_SIZE]; | |
public Dac() { | |
localBuffer = new float[CHUNK_SIZE]; | |
int minSize = AudioTrack.getMinBufferSize( | |
UGen.SAMPLE_RATE, | |
AudioFormat.CHANNEL_CONFIGURATION_MONO, | |
AudioFormat.ENCODING_PCM_16BIT); | |
track = new AudioTrack( | |
AudioManager.STREAM_MUSIC, | |
UGen.SAMPLE_RATE, | |
AudioFormat.CHANNEL_CONFIGURATION_MONO, | |
AudioFormat.ENCODING_PCM_16BIT, | |
Math.max(UGen.CHUNK_SIZE*4, minSize), | |
AudioTrack.MODE_STREAM); | |
} | |
public boolean render(final float[] _buffer) { | |
if(!isClean) { | |
zeroBuffer(localBuffer); | |
isClean = true; | |
} | |
// localBuffer is always clean right here, does it stay that way? | |
isClean = !renderKids(localBuffer); | |
return !isClean; // we did some work if the buffer isn't clean | |
} | |
public void open() { | |
track.play(); | |
} | |
public void tick() { | |
render(localBuffer); | |
if(isClean) { | |
// sleeping is messy, so lets just queue this silent buffer | |
track.write(silentTarget, 0, silentTarget.length); | |
} else { | |
for(int i = 0; i < CHUNK_SIZE; i++) { | |
target[i] = (short)(32768.0f*localBuffer[i]); | |
} | |
track.write(target, 0, target.length); | |
} | |
} | |
public void close() { | |
track.stop(); | |
track.release(); | |
} | |
} |
package as.adamsmith.etherealdialpad.dsp; | |
public class Delay extends UGen { | |
final float[] line; | |
int pointer; | |
public Delay(int length) { | |
super(); | |
line = new float[length]; | |
} | |
public boolean render(final float[] buffer) { | |
renderKids(buffer); | |
final float[] localLine = line; | |
final int lineLength = line.length; | |
for(int i = 0; i < CHUNK_SIZE; i++) { | |
buffer[i] = buffer[i] - 0.5f*localLine[pointer]; | |
localLine[pointer] = buffer[i]; | |
pointer = (pointer+1)%lineLength; | |
} | |
// ugh, looks like we can never be sure it's silent | |
// without checking every sample because of the feedback | |
return true; | |
} | |
} |
package as.adamsmith.etherealdialpad.dsp; | |
public class ExpEnv extends UGen { | |
public static float hardFactor = 0.005f; | |
public static float softFactor = 0.00005f; | |
boolean state; | |
float attenuation; | |
float factor = softFactor; | |
final float idealMarker = 0.25f; | |
float marker = idealMarker; | |
public synchronized void setActive(boolean nextState) { | |
state = nextState; | |
} | |
public synchronized void setFactor(float nextFactor) { | |
factor = nextFactor; | |
} | |
public synchronized void setGain(float gain) { | |
marker = gain * idealMarker; | |
} | |
public synchronized boolean render(final float[] buffer) { | |
if(!state && attenuation < 0.0001f) return false; | |
if(!renderKids(buffer)) return false; | |
for(int i = 0; i < CHUNK_SIZE; i++) { | |
buffer[i] *= attenuation; | |
if(!state) { | |
attenuation += (0-attenuation)*factor; | |
} else { | |
attenuation += (marker-attenuation)*factor; | |
} | |
} | |
return true; | |
} | |
} |
package as.adamsmith.etherealdialpad.dsp; | |
import java.util.ArrayList; | |
public abstract class UGen { | |
public static final int CHUNK_SIZE = 256; | |
public static final int SAMPLE_RATE = 22050; | |
ArrayList<UGen> kids = new ArrayList<UGen>(0); | |
// fill CHUNK_SIZE samples | |
// and return true if you actually did any work | |
abstract public boolean render(final float[] buffer); | |
final public synchronized UGen chuck(UGen that) { | |
if(!that.kids.contains(this)) that.kids.add(this); | |
return that; // returns RHS | |
} | |
final public synchronized UGen unchuck(UGen that) { | |
if(that.kids.contains(this)) that.kids.remove(this); | |
return that; // returns RHS | |
} | |
protected void zeroBuffer(final float[] buffer) { | |
for(int i = 0; i < CHUNK_SIZE; i++) { | |
buffer[i] = 0; | |
} | |
} | |
protected boolean renderKids(final float[] buffer) { | |
boolean didSomeRealWork = false; | |
for(int k = 0; k < kids.size(); k++) { | |
didSomeRealWork |= kids.get(k).render(buffer); | |
} | |
return didSomeRealWork; | |
} | |
} |
init { | |
WtOsc ugOscA1 = new WtOsc(); | |
WtOsc ugOscA2 = new WtOsc(); | |
ExpEnv ugEnvA = new ExpEnv(); | |
ugOscA1.fillWithHardSin(7.0f); | |
ugOscA2.fillWithHardSin(2.0f); | |
Dac ugDac = new Dac(); | |
Delay ugDelay = new Delay(UGen.SAMPLE_RATE/2); | |
ugEnvA.chuck(ugDelay); | |
ugDelay.chuck(ugDac); | |
ugOscA1.chuck(ugEnvA); | |
ugOscA2.chuck(ugEnvA); | |
ugEnvA.setFactor(ExpEnv.hardFactor); | |
} | |
touch { | |
ugOscA1.setFreq(buildFrequency(scale, octaves, x)); | |
ugOscA2.setFreq(buildFrequency(scale, octaves, y)); | |
} | |
run { | |
ugDac.open(); | |
while(!isInterrupted()) { | |
ugDac.tick(); | |
} | |
ugDac.close(); | |
} |
package as.adamsmith.etherealdialpad.dsp; | |
import android.util.FloatMath; | |
public class WtOsc extends UGen { | |
public static final int BITS = 8; | |
public static final int ENTRIES = 1<<(BITS-1); | |
public static final int MASK = ENTRIES-1; | |
private float phase; | |
private float cyclesPerSample; | |
final float[] table; | |
public WtOsc () { | |
table = new float[ENTRIES]; | |
} | |
public synchronized void setFreq(float freq) { | |
cyclesPerSample = freq/SAMPLE_RATE; | |
} | |
public synchronized boolean render(final float[] buffer) { // assume t is in 0.0 to 1.0 | |
for(int i = 0; i < CHUNK_SIZE; i++) { | |
float scaled = phase*ENTRIES; | |
final float fraction = scaled-(int)scaled; | |
final int index = (int)scaled; | |
buffer[i] += (1.0f-fraction)*table[index&MASK]+fraction*table[(index+1)&MASK]; | |
phase = (phase+cyclesPerSample) - (int)phase; | |
} | |
return true; | |
} | |
public WtOsc fillWithSin() { | |
final float dt = (float)(2.0*Math.PI/ENTRIES); | |
for(int i = 0; i < ENTRIES; i++) { | |
table[i] = FloatMath.sin(i*dt); | |
} | |
return this; | |
} | |
public WtOsc fillWithHardSin(final float exp) { | |
final float dt = (float)(2.0*Math.PI/ENTRIES); | |
for(int i = 0; i < ENTRIES; i++) { | |
table[i] = (float) Math.pow(FloatMath.sin(i*dt),exp); | |
} | |
return this; | |
} | |
public WtOsc fillWithZero() { | |
for(int i = 0; i < ENTRIES; i++) { | |
table[i] = 0; | |
} | |
return this; | |
} | |
public WtOsc fillWithSqr() { | |
for(int i = 0; i < ENTRIES; i++) { | |
table[i] = i<ENTRIES/2?1f:-1f; | |
} | |
return this; | |
} | |
public WtOsc fillWithSqrDuty(float fraction) { | |
for(int i = 0; i < ENTRIES; i++) { | |
table[i] = (float)i/ENTRIES<fraction?1f:-1f; | |
} | |
return this; | |
} | |
public WtOsc fillWithSaw() { | |
float dt = (float)(2.0/ENTRIES); | |
for(int i = 0; i < ENTRIES; i++) { | |
table[i] = 1.0f-i*dt; | |
} | |
return this; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment