Last active
October 1, 2022 19:59
-
-
Save nocarryr/4729d36743269e84d137493d1c9294ae to your computer and use it in GitHub Desktop.
NDI Multiviewer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
double NINF = Double.NEGATIVE_INFINITY; | |
class AudioMeter { | |
double[] rmsDbfs, rmsDbu, peakDbfs, peakDbu, peakAmp; | |
int sampleRate, nChannels, blockSize, stride; | |
float avgTime = .1; | |
int[] bufferLength; | |
//float[] ticks = {0, -6, -12, -18, -24, -36, -48, -60, -90, -140}; | |
float[] ticks = {0, -10, -20, -30, -40, -50, -60, -70}; | |
float maxTick = 0; | |
float minTick = -70; | |
int nTickContainers; | |
int maxChannels = 2; | |
int channelOffset = 0; | |
Box boundingBox; | |
Box[] channelBoxes; | |
MeterTickContainer[] tickContainers; | |
AudioMeterChannel[] meterChannels; | |
AudioMeter(int fs, int nch, int _blockSize){ | |
boundingBox = new Box(0, 0, 20, 20); | |
sampleRate = fs; | |
nChannels = nch; | |
blockSize = _blockSize; | |
rmsDbfs = new double[nChannels]; | |
rmsDbu = new double[nChannels]; | |
peakDbfs = new double[nChannels]; | |
peakDbu = new double[nChannels]; | |
peakAmp = new double[nChannels]; | |
bufferLength = new int[nChannels]; | |
float tickWidth = boundingBox.getWidth() / (maxChannels / 2); | |
float channelWidth = tickWidth / 3; | |
tickContainers = new MeterTickContainer[int(maxChannels / 2)]; | |
meterChannels = new AudioMeterChannel[maxChannels]; | |
for (int i=0; i<nChannels; i++){ | |
rmsDbfs[i] = NINF; | |
rmsDbu[i] = NINF; | |
peakDbfs[i] = NINF; | |
peakDbu[i] = NINF; | |
peakAmp[i] = 0; | |
bufferLength[i] = 0; | |
} | |
nTickContainers = 0; | |
int tickIdx = -1; | |
for (int i=0; i<maxChannels; i++){ | |
if (i % 2 == 0){ | |
tickIdx += 1; | |
MeterTickContainer tickContainer = new MeterTickContainer(this); | |
tickContainer.setWidth(tickWidth); | |
tickContainer.setX(tickWidth * tickIdx + (channelWidth * (i % 2)) + boundingBox.getX()); | |
tickContainer.setY(boundingBox.getY()); | |
//if (i == 0) { | |
// tickContainer.setX(boundingBox.getX()); | |
//} else { | |
// tickContainer.setX(tickContainers[tickIdx-1].getRight()); | |
//} | |
tickContainers[tickIdx] = tickContainer; | |
nTickContainers += 1; | |
} | |
meterChannels[i] = new AudioMeterChannel(this, i); | |
Box b = boundingBox.copy(); | |
b.setWidth(channelWidth); | |
b.setX(channelWidth * i * 2 + boundingBox.getX()); | |
//if (i % 2 != 0){ | |
// b.setX(tickContainers[tickIdx].getX()); | |
//} else { | |
// b.setRight(tickContainers[tickIdx].getRight()); | |
//} | |
//b.setX(b.getWidth() * i + boundingBox.getX()); | |
meterChannels[i].setBoundingBox(b); | |
} | |
} | |
void setBoundingBox(Box b){ | |
boundingBox = b.copy(); | |
float tickWidth = boundingBox.getWidth() / (maxChannels / 2); | |
float channelWidth = tickWidth / 3; | |
for (int i=0; i<nTickContainers; i++){ | |
Box t = b.copy(); | |
t.setWidth(tickWidth); | |
t.setX(tickWidth * i + (channelWidth * (i % 2)) + b.getX()); | |
tickContainers[i].setBox(t); | |
} | |
for (int i=0; i<maxChannels; i++){ | |
b.setWidth(channelWidth); | |
b.setX(channelWidth * i * 2 + boundingBox.getX()); | |
meterChannels[i].setBoundingBox(b); | |
} | |
} | |
float dbToYPos(double dbVal){ | |
double dbMax = maxTick; | |
double dbMin = minTick; | |
double dbScale = Math.abs(dbMax - dbMin); | |
float h = boundingBox.getHeight(); | |
if (dbVal == NINF){ | |
return h; | |
} | |
double pos = dbVal / dbScale; | |
return (float)pos * -h; | |
} | |
float dbToYPos(double dbVal, boolean withOffset){ | |
float result = dbToYPos(dbVal); | |
if (withOffset){ | |
result += boundingBox.getY(); | |
} | |
return result; | |
} | |
void render(PGraphics canvas){ | |
canvas.noFill(); | |
canvas.stroke(128); | |
boundingBox.drawRect(canvas); | |
for (int i=0; i<nTickContainers; i++){ | |
tickContainers[i].render(canvas); | |
} | |
for (int i=0; i<maxChannels; i++){ | |
meterChannels[i].render(canvas); | |
} | |
} | |
void processSamples(DevolayAudioFrame frame){ | |
int size = frame.getSamples(); | |
int stride = frame.getChannelStride(); | |
int nch = frame.getChannels(); | |
ByteBuffer data = frame.getData().order(ByteOrder.LITTLE_ENDIAN); | |
double[] chPeaks = new double[nch]; | |
double[] chSums = new double[nch]; | |
for (int i=0; i<nch; i++){ | |
chPeaks[i] = 0; | |
chSums[i] = 0; | |
} | |
for (int ch=0; ch<nch; ch++){ | |
for (int samp=0; samp<size; samp++){ | |
Float vf = data.getFloat(); | |
double v = vf.doubleValue(); | |
double vabs = Math.abs(v); | |
if (vabs > chPeaks[ch]){ | |
chPeaks[ch] = vabs; | |
} | |
v *= .1; | |
chSums[ch] += v * v; | |
} | |
} | |
for (int ch=0; ch<nch; ch++){ | |
double vabs = chPeaks[ch] * .1; | |
peakAmp[ch] = vabs; | |
peakDbfs[ch] = 10 * Math.log10(vabs); | |
peakDbu[ch] = peakDbfs[ch] + 24; | |
double mag = Math.sqrt(chSums[ch] / size); | |
if (mag == 0){ | |
rmsDbfs[ch] = NINF; | |
} else { | |
rmsDbfs[ch] = 10 * Math.log10(mag); | |
rmsDbu[ch] = rmsDbfs[ch] + 24; | |
} | |
bufferLength[ch] = size; | |
} | |
} | |
} | |
class AudioMeterChannel { | |
AudioMeter parent; | |
Box boundingBox; | |
int index; | |
float greenStop = -12, yellowStart = -6, redStart = -1; | |
color greenBg = 0xff008000, yellowBg = 0xff808000, redBg = 0xff800000; | |
//color greenBg = color(0, 128, 0), yellowBg = color(128, 128, 0), redBg = color(128, 0, 0); | |
PShape greenRect, yellowRect, redRect; | |
PShape[] bgShapes; | |
PImage[] bgImgs; | |
color bgColors[] = {0xff00ff00, 0xffffff00, 0xffff0000}; | |
Box meterBox; | |
Box greenBox, greenYellowBox, yellowRedBox; | |
Box[] bgBoxes; | |
AudioMeterChannel(AudioMeter _parent, int _index){ | |
parent = _parent; | |
index = _index; | |
boundingBox = new Box(0, 0, 10, 100); | |
meterBox = boundingBox.copy(); | |
bgShapes = new PShape[3]; | |
bgImgs = new PImage[3]; | |
bgBoxes = new Box[3]; | |
buildImages(); | |
buildGradientBoxes(); | |
//Box b = new Box(0, 0, 20, 10); | |
} | |
int channelIndex(){ | |
return index + parent.channelOffset; | |
} | |
void buildImages(){ | |
for (int i=0; i<bgImgs.length; i++){ | |
bgImgs[i] = new PImage(50, 100, ARGB); | |
} | |
PImage gImg = bgImgs[0], gyImg = bgImgs[1], yrImg = bgImgs[2]; | |
Arrays.fill(bgImgs[0].pixels, greenBg); | |
Arrays.fill(bgImgs[1].pixels, yellowBg); | |
Arrays.fill(bgImgs[2].pixels, redBg); | |
fillVGradient(bgImgs[0], greenBg, greenBg); | |
fillVGradient(bgImgs[1], yellowBg, greenBg); | |
fillVGradient(bgImgs[2], redBg, yellowBg); | |
//alphaGradient(bgImgs[0], 0, 1, 0, 1); | |
//alphaGradient(bgImgs[1], 0, 1, 0, 1); | |
//alphaGradient(bgImgs[2], 0, 1, 0, 1); | |
} | |
void fillVGradient(PImage img, color c1, color c2) { | |
//img.loadPixels(); | |
//int i = 0, w = img.width; | |
int a1 = (c1 & 0xff000000) >> 24, | |
a2 = (c2 & 0xff000000) >> 24, | |
r1 = (c1 & 0xff0000) >> 16, | |
r2 = (c2 & 0xff0000) >> 16, | |
g1 = (c1 & 0xff00) >> 8, | |
g2 = (c2 & 0xff00) >> 8, | |
b1 = c1 & 0xff, | |
b2 = c2 & 0xff; | |
int w = img.width; | |
float h = img.height; | |
int i = 0; | |
for (int y=0; y<(int)h; y++) { | |
float inter = y / h; | |
color c = mvApp.lerpColor(c1, c2, inter); | |
//int a = int(lerp(a1, a2, inter)) >> 24, | |
// r = int(lerp(r1, r2, inter)) >> 16, | |
// g = int(lerp(g1, g2, inter)) >> 8, | |
// b = int(lerp(b1, b2, inter)) & 0xff; | |
//color c = a | r | g | b; | |
//println(y, inter, Integer.toHexString(c)); | |
for (int x=0; x<w; x++){ | |
i = y * w + x; | |
img.pixels[i] = c; | |
//i += i; | |
} | |
} | |
assert i+1 == w*h; | |
assert img.pixels.length == w*h; | |
img.updatePixels(); | |
} | |
void setBoundingBox(Box b){ | |
boundingBox = b.copy(); | |
meterBox = b.copy(); | |
try { | |
buildGradientBoxes(); | |
} catch(Exception e){ | |
e.printStackTrace(); | |
throw(e); | |
} | |
} | |
void buildGradientBoxes(){ | |
float bottomPos = dbToYPos(-90, true), | |
greenPos = dbToYPos(greenStop, true), | |
yellowPos = dbToYPos(yellowStart, true), | |
redPos = dbToYPos(redStart, true), | |
topPos = dbToYPos(0, true); | |
//assert dbToYPos(0, true) == boundingBox.getY(); | |
//assert dbToYPos(-90, true) == boundingBox.getBottom(); | |
Box baseBox = boundingBox.copy(); | |
//baseBox.setPos(new Point(0, 0)); | |
greenBox = baseBox.copy(); | |
greenBox.setHeight(baseBox.getBottom() - greenPos); | |
greenBox.setY(greenPos); | |
bgBoxes[0] = greenBox; | |
greenYellowBox = baseBox.copy(); | |
greenYellowBox.setHeight(greenPos - yellowPos); | |
greenYellowBox.setBottom(greenPos); | |
bgBoxes[1] = greenYellowBox; | |
yellowRedBox = baseBox.copy(); | |
yellowRedBox.setHeight(yellowPos - topPos); | |
yellowRedBox.setY(topPos); | |
//yellowRedBox.setBottom(greenYellowBox.getY()); | |
//yellowRedBox.setY(baseBox.getY()); | |
bgBoxes[2] = yellowRedBox; | |
} | |
float dbToYPos(double dbVal){ | |
return parent.dbToYPos(dbVal); | |
} | |
float dbToYPos(double dbVal, boolean withOffset){ | |
return parent.dbToYPos(dbVal, withOffset); | |
} | |
void render(PGraphics canvas){ | |
canvas.stroke(255); | |
canvas.fill(0); | |
for (int i=0; i<bgShapes.length; i++){ | |
Box b = bgBoxes[i]; | |
canvas.image(bgImgs[i], b.getX(), b.getY(), b.getWidth(), b.getHeight()); | |
} | |
int chIdx = channelIndex(); | |
// mask over the meter images making them dark above RMS level | |
meterBox.setHeight(parent.dbToYPos(parent.rmsDbfs[chIdx], false)); | |
canvas.noStroke(); | |
canvas.fill(0xa0000000); | |
meterBox.drawRect(canvas); | |
double peakDbfs = parent.peakDbfs[chIdx]; | |
float peakY = dbToYPos(peakDbfs, true); | |
color peakColor; | |
if (peakDbfs <= greenStop){ | |
peakColor = greenBg; | |
} else if (peakDbfs < redStart){ | |
peakColor = yellowBg; | |
} else { | |
peakColor = redBg; | |
} | |
canvas.stroke(peakColor); | |
canvas.line(boundingBox.getX(), peakY, boundingBox.getRight(), peakY); | |
} | |
} | |
class MeterTickContainer extends Box { | |
AudioMeter meter; | |
TickLabel[] tickLabels; | |
color bgColor = 0x80000000; | |
MeterTickContainer(AudioMeter _meter){ | |
super(); | |
meter = _meter; | |
tickLabels = new TickLabel[meter.ticks.length]; | |
for (int i=0; i<tickLabels.length; i++){ | |
TickLabel t = new TickLabel(this, meter.ticks[i]); | |
tickLabels[i] = t; | |
} | |
setPos(meter.boundingBox.getPos()); | |
setSize(meter.boundingBox.getSize()); | |
} | |
void updateGeometry(){ | |
super.updateGeometry(); | |
for (int i=0; i<tickLabels.length; i++){ | |
tickLabels[i].calcTickPosition(); | |
} | |
} | |
void render(PGraphics canvas){ | |
canvas.noStroke(); | |
canvas.fill(bgColor); | |
drawRect(canvas); | |
for (int i=0; i<tickLabels.length; i++){ | |
tickLabels[i].render(canvas); | |
} | |
} | |
} | |
class TickLabel extends TextBox { | |
MeterTickContainer parent; | |
AudioMeter meter; | |
float dbValue; | |
float realTickPos; | |
TickLabel(MeterTickContainer _parent, float _dbValue){ | |
super(); | |
parent = _parent; | |
meter = parent.meter; | |
dbValue = _dbValue; | |
text = String.format("%d", int(dbValue)); | |
setTextSize(10); | |
drawBackground = false; | |
int v = CENTER; | |
if (dbValue == meter.minTick){ | |
v = BOTTOM; | |
} else if (dbValue == meter.maxTick){ | |
v = TOP; | |
} | |
setSize(new Point(parent.getWidth(), 10)); | |
setAlign(CENTER, v); | |
calcTickPosition(); | |
} | |
void calcTickPosition(){ | |
setWidth(parent.getWidth()); | |
setX(parent.getX()); | |
float yp = meter.dbToYPos(dbValue, true); | |
realTickPos = yp; | |
int _vAlign = getVAlign(); | |
if (_vAlign == CENTER){ | |
setVCenter(yp); | |
} else if (_vAlign == BOTTOM){ | |
setBottom(yp); | |
} else if (_vAlign == TOP){ | |
setY(yp); | |
} else { | |
throw new Error("Invalid valign"); | |
} | |
setHCenter(parent.getHCenter()); | |
} | |
void render(PGraphics canvas){ | |
super.render(canvas); | |
//canvas.stroke(255); | |
//float y = realTickPos; | |
//canvas.line(getX(), y, getRight(), y); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.lang.reflect.Field; | |
import java.lang.reflect.Method; | |
import java.lang.reflect.InvocationTargetException; | |
class ConfigBase{ | |
HashMap<String,String> _fieldMap; | |
ConfigBase(JSONObject json){ | |
_getFieldMap(); | |
setValuesFromJSON(json); | |
} | |
ConfigBase(){ | |
_getFieldMap(); | |
} | |
void setValuesFromJSON(JSONObject json){ | |
for (Map.Entry<String,String> entry : _fieldMap.entrySet()){ | |
String className = entry.getValue(); | |
boolean isarr = false; | |
if (className.endsWith("[]")){ | |
isarr = true; | |
className = className.substring(0, className.length()); | |
} | |
//Class<?> c; | |
//if (className == "int"){ | |
// c = Class.forName("int"); | |
//} | |
//try { | |
// c = Class.forName(entry.getKey()); | |
//} catch(ClassNotFoundException e){ | |
// e.printStackTrace(); | |
// continue; | |
//} | |
Object value = getValueFromJSON(className, isarr, entry.getKey(), json); | |
try { | |
Field f = this.getClass().getDeclaredField(entry.getKey()); | |
try { | |
f.set(this, value); | |
} catch(IllegalAccessException e){ | |
e.printStackTrace(); | |
throw(new Error(entry.getKey())); | |
} | |
} catch(NoSuchFieldException e){ | |
e.printStackTrace(); | |
throw(new Error(String.format("key=%s, cls=%s, value=%s", entry.getKey(), this.getClass(), value))); | |
} | |
} | |
} | |
Object getValueFromJSON(String className, boolean isarr, String fieldName, JSONObject json){ | |
//try { | |
Object value; | |
if (className == "int"){ | |
//f.setInt(this, json.getInt(fieldName)); | |
value = json.getInt(fieldName); | |
} else if (className == "boolean"){ | |
//f.setBoolean(this, json.getBoolean(fieldName)); | |
value = json.getBoolean(fieldName); | |
} else if (className == "String"){ | |
value = json.getString(fieldName); | |
} else if (className == "Point"){ | |
value = new Point(json.getJSONObject(fieldName)); | |
//f.set(this, p); | |
} else if (className == "Box"){ | |
value = new Box(json.getJSONObject(fieldName)); | |
} else { | |
throw(new Error(String.format("could not serialize field '%s', className='%s', cls=%s", fieldName, className, this.getClass()))); | |
} | |
return value; | |
} | |
JSONObject serialize(){ | |
JSONObject json = new JSONObject(); | |
for (Map.Entry<String,String> entry : _fieldMap.entrySet()){ | |
String className = entry.getValue(); | |
boolean isarr = false; | |
if (className.endsWith("[]")){ | |
isarr = true; | |
className = className.substring(0, className.length()); | |
} | |
Object value; | |
try { | |
Field f = this.getClass().getDeclaredField(entry.getKey()); | |
try { | |
value = f.get(this); | |
} catch(IllegalAccessException e){ | |
e.printStackTrace(); | |
continue; | |
} | |
} catch(NoSuchFieldException e){ | |
e.printStackTrace(); | |
throw(new Error(String.format("key=%s, cls=%s", entry.getKey(), this.getClass()))); | |
} | |
setJSONValue(className, isarr, entry.getKey(), value, json); | |
} | |
return json; | |
} | |
void setJSONValue(String className, boolean isarr, String fieldName, Object value, JSONObject json){ | |
if (value instanceof ConfigBase){ | |
try { | |
Class c = value.getClass(); | |
Method m = c.getMethod("serialize"); | |
Object _json = m.invoke(value); | |
json.setJSONObject(fieldName, (JSONObject)_json); | |
//if (_json instanceof JSONArray){ | |
// json.setJSONArray(fieldName, (JSONArray)_json); | |
//} else { | |
// json.setJSONObject(fieldName, (JSONObject)_json); | |
//} | |
} catch(NoSuchMethodException e){ | |
e.printStackTrace(); | |
throw(new Error(String.format("key=%s, cls=%s", fieldName, this.getClass()))); | |
} catch(IllegalAccessException e){ | |
e.printStackTrace(); | |
throw(new Error(String.format("key=%s, cls=%s", fieldName, this.getClass()))); | |
} catch(InvocationTargetException e){ | |
e.printStackTrace(); | |
throw(new Error(String.format("key=%s, cls=%s", fieldName, this.getClass()))); | |
} | |
} else if (className == "int"){ | |
json.setInt(fieldName, (int)value); | |
} else if (className == "boolean"){ | |
json.setBoolean(fieldName, (boolean)value); | |
} else if (className == "String"){ | |
json.setString(fieldName, (String)value); | |
} else if (className == "Point"){ | |
Point p = (Point)value; | |
json.setJSONObject(fieldName, p.serialize()); | |
} else if (className == "Box"){ | |
Box b = (Box)value; | |
json.setJSONObject(fieldName, b.serialize()); | |
} else { | |
throw(new Error(String.format("could not set value '%s' for field '%s' (className='%s'), cls=%s", value, fieldName, className, this.getClass()))); | |
} | |
} | |
void _getFieldMap(){ | |
_fieldMap = new HashMap<String,String>(); | |
} | |
} | |
class Config extends ConfigBase { | |
AppConfig app; | |
GridConfig windowGrid; | |
Config(JSONObject json){ super(json); } | |
Config(){ | |
super(); | |
app = new AppConfig(); | |
windowGrid = new GridConfig(); | |
} | |
void update(MultiviewApplet applet){ | |
app.update(applet); | |
windowGrid.update(applet); | |
} | |
Object getValueFromJSON(String className, boolean isarr, String fieldName, JSONObject json){ | |
if (className == "AppConfig"){ | |
return new AppConfig(json.getJSONObject(fieldName)); | |
} else if (className == "GridConfig"){ | |
return new GridConfig(json.getJSONObject(fieldName)); | |
} else { | |
return super.getValueFromJSON(className, isarr, fieldName, json); | |
} | |
} | |
//void setJSONValue(String className, boolean isarr, String fieldName, Object value, JSONObject json){ | |
// if (className == "AppConfig"){ | |
// AppConfig c = (AppConfig)value; | |
// json.setJSONObject(fieldName, c. | |
void _getFieldMap(){ | |
_fieldMap = new HashMap<String,String>(); | |
_fieldMap.put("app", "AppConfig"); | |
_fieldMap.put("windowGrid", "GridConfig"); | |
} | |
} | |
class AppConfig extends ConfigBase { | |
boolean fullScreen; | |
int displayNumber; | |
Point canvasSize; | |
Box windowBounds; | |
AppConfig(JSONObject json){ super(json); } | |
AppConfig(){ | |
super(); | |
fullScreen = false; | |
displayNumber = -1; | |
canvasSize = new Point(640, 360); | |
windowBounds = new Box(0, 0, 640, 360); | |
} | |
void update(MultiviewApplet applet){ | |
fullScreen = applet.isFullScreen; | |
canvasSize = new Point(applet.width, applet.height); | |
//windowBounds = applet.getWindowDims(); | |
} | |
void _getFieldMap(){ | |
_fieldMap = new HashMap<String,String>(); | |
_fieldMap.put("fullScreen", "boolean"); | |
_fieldMap.put("displayNumber", "int"); | |
_fieldMap.put("canvasSize", "Point"); | |
_fieldMap.put("windowBounds", "Box"); | |
} | |
} | |
class GridConfig extends ConfigBase { | |
int cols, rows; | |
Point padding; | |
Point outputSize; | |
WindowConfig[] windows; | |
GridConfig(JSONObject json){ super(json); } | |
GridConfig(){ | |
super(); | |
cols = 2; | |
rows = 2; | |
padding = new Point(2, 2); | |
outputSize = new Point(640, 360); | |
windows = new WindowConfig[4]; | |
String windowNames[] = {"A", "B", "C", "D"}; | |
int i = 0; | |
for (int x=0; x<cols; x++){ | |
for (int y=0; y<rows; y++){ | |
WindowConfig w = new WindowConfig(); | |
w.col = x; | |
w.row = y; | |
w.name = windowNames[i]; | |
windows[i] = w; | |
i += 1; | |
} | |
} | |
//update(mvApp.windowGrid); | |
} | |
//GridConfig(WindowGrid grid){ | |
// super(); | |
// setValuesFromJSON(grid.serialize()); | |
//} | |
void update(MultiviewApplet applet){ | |
update(applet.windowGrid); | |
} | |
void update(WindowGrid grid){ | |
setValuesFromJSON(grid.serialize()); | |
} | |
Object getValueFromJSON(String className, boolean isarr, String fieldName, JSONObject json){ | |
if (className.startsWith("WindowConfig")){ | |
JSONArray jsonArr = json.getJSONArray(fieldName); | |
WindowConfig[] w = new WindowConfig[jsonArr.size()]; | |
for (int i=0; i<jsonArr.size(); i++){ | |
w[i] = new WindowConfig(jsonArr.getJSONObject(i)); | |
} | |
return w; | |
} else { | |
return super.getValueFromJSON(className, isarr, fieldName, json); | |
} | |
} | |
void setJSONValue(String className, boolean isarr, String fieldName, Object value, JSONObject json){ | |
if (className.startsWith("WindowConfig")){ | |
JSONArray _json = new JSONArray(); | |
for (int i=0; i<windows.length; i++){ | |
_json.append(windows[i].serialize()); | |
} | |
json.setJSONArray(fieldName, _json); | |
} else { | |
super.setJSONValue(className, isarr, fieldName, value, json); | |
} | |
} | |
void _getFieldMap(){ | |
_fieldMap = new HashMap<String,String>(); | |
_fieldMap.put("cols", "int"); | |
_fieldMap.put("rows", "int"); | |
_fieldMap.put("padding", "Point"); | |
_fieldMap.put("outputSize", "Point"); | |
_fieldMap.put("windows", "WindowConfig[]"); | |
} | |
} | |
//class WindowConfigs extends ConfigBase { | |
// WindowConfig[] windows; | |
//} | |
class WindowConfig extends ConfigBase { | |
String name, ndiSourceName; | |
int col, row; | |
WindowConfig(JSONObject json){ super(json); } | |
WindowConfig(){ | |
super(); | |
name = ""; | |
ndiSourceName = ""; | |
col = 0; | |
row = 0; | |
} | |
void _getFieldMap(){ | |
_fieldMap = new HashMap<String,String>(); | |
_fieldMap.put("name", "String"); | |
_fieldMap.put("ndiSourceName", "String"); | |
_fieldMap.put("col", "int"); | |
_fieldMap.put("row", "int"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Point { | |
float x, y; | |
Point(float _x, float _y){ | |
x = _x; | |
y = _y; | |
} | |
Point(JSONObject json){ | |
x = json.getFloat("x"); | |
y = json.getFloat("y"); | |
} | |
JSONObject serialize(){ | |
JSONObject json = new JSONObject(); | |
json.setFloat("x", x); | |
json.setFloat("y", y); | |
return json; | |
} | |
Point copy(){ | |
return new Point(x, y); | |
} | |
void add(Point other){ | |
x += other.x; | |
y += other.y; | |
} | |
String toStr(){ | |
return String.format("(%f, %f)", x, y); | |
} | |
} | |
class Box { | |
Point pos, size; | |
Box(){ | |
pos = new Point(0, 0); | |
size = new Point(1, 1); | |
} | |
Box(float x, float y, float w, float h){ | |
pos = new Point(x, y); | |
size = new Point(w, h); | |
//updateGeometry(); | |
} | |
Box(Point _pos, Point _size){ | |
pos = _pos.copy(); | |
size = _size.copy(); | |
//updateGeometry(); | |
} | |
Box(Point _pos, float w, float h){ | |
pos = _pos.copy(); | |
size = new Point(w, h); | |
//updateGeometry(); | |
} | |
Box(float x, float y, Point _size){ | |
pos = new Point(x, y); | |
size = _size.copy(); | |
//updateGeometry(); | |
} | |
Box(Box _b){ | |
pos = _b.getPos(); | |
size = _b.getSize(); | |
//updateGeometry(); | |
} | |
Box(JSONObject json){ | |
pos = new Point(json.getJSONObject("pos")); | |
size = new Point(json.getJSONObject("size")); | |
} | |
JSONObject serialize(){ | |
JSONObject json = new JSONObject(); | |
json.setJSONObject("pos", pos.serialize()); | |
json.setJSONObject("size", size.serialize()); | |
return json; | |
} | |
Box copy(){ | |
return new Box(this); | |
} | |
void move(Point dxy){ | |
pos.add(dxy); | |
//setPos(pos.add(dxy)); | |
} | |
float getAspectRatioW(){ | |
return getHeight() / getWidth(); | |
} | |
void setAspectRatioW(float ar){ | |
setWidth(getHeight() / ar); | |
} | |
float getAspectRatioH(){ | |
return getWidth() / getHeight(); | |
} | |
void setAspectRatioH(float ar){ | |
setHeight(getWidth() / ar); | |
} | |
void translate(float dx, float dy){ | |
pos.x += dx; | |
pos.y += dy; | |
} | |
void translate(Point p){ | |
translate(p.x, p.y); | |
} | |
void setBox(Box b){ | |
pos.x = b.pos.x; | |
pos.y = b.pos.y; | |
size.x = b.size.x; | |
size.y = b.size.y; | |
updateGeometry(); | |
} | |
Point getPos(){ | |
return pos.copy(); | |
} | |
void setPos(Point p){ | |
pos.x = p.x; | |
pos.y = p.y; | |
updateGeometry(); | |
} | |
Point getSize(){ | |
return size.copy(); | |
} | |
void setSize(Point s){ | |
size.x = s.x; | |
size.y = s.y; | |
updateGeometry(); | |
} | |
float getX(){ | |
return pos.x; | |
} | |
void setX(float x){ | |
pos.x = x; | |
updateGeometry(); | |
} | |
float getY(){ | |
return pos.y; | |
} | |
void setY(float y){ | |
pos.y = y; | |
updateGeometry(); | |
} | |
float getWidth(){ | |
return size.x; | |
} | |
void setWidth(float w){ | |
size.x = w; | |
updateGeometry(); | |
} | |
float getHeight(){ | |
return size.y; | |
} | |
void setHeight(float h){ | |
size.y = h; | |
updateGeometry(); | |
} | |
float getRight(){ | |
return pos.x + getWidth(); | |
} | |
void setRight(float r){ | |
pos.x = r - getWidth(); | |
updateGeometry(); | |
} | |
float getBottom(){ | |
return pos.y + getHeight(); | |
} | |
void setBottom(float b){ | |
pos.y = b - getHeight(); | |
//assert getBottom() == b; | |
updateGeometry(); | |
} | |
float getHCenter(){ | |
return pos.x + getWidth() / 2; | |
} | |
void setHCenter(float c){ | |
pos.x = c - getWidth() / 2; | |
//assert getHCenter() == c; | |
updateGeometry(); | |
} | |
float getVCenter(){ | |
return pos.y + getHeight() / 2; | |
} | |
void setVCenter(float c){ | |
pos.y = c - getHeight() / 2; | |
updateGeometry(); | |
} | |
Point getCenter(){ | |
return new Point(getHCenter(), getVCenter()); | |
} | |
void setCenter(Point c){ | |
pos.x = c.x - getWidth() / 2; | |
pos.y = c.y - getHeight() / 2; | |
updateGeometry(); | |
} | |
Point getTopLeft(){ | |
return new Point(getX(), getY()); | |
} | |
Point getTopCenter(){ | |
return new Point(getHCenter(), getY()); | |
} | |
void setTopCenter(Point p){ | |
pos.x = p.x - getWidth() / 2; | |
pos.y = p.y; | |
updateGeometry(); | |
} | |
Point getTopRight(){ | |
return new Point(getRight(), getY()); | |
} | |
Point getMiddleLeft(){ | |
return new Point(getX(), getVCenter()); | |
} | |
Point getMiddleRight(){ | |
return new Point(getRight(), getVCenter()); | |
} | |
Point getBottomLeft(){ | |
return new Point(getX(), getBottom()); | |
} | |
void setBottomLeft(Point p){ | |
pos.x = p.x; | |
pos.y = p.y - getHeight(); | |
} | |
Point getBottomCenter(){ | |
return new Point(getHCenter(), getBottom()); | |
} | |
void setBottomCenter(Point p){ | |
pos.x = p.x - getWidth() / 2; | |
pos.y = p.y - getHeight(); | |
updateGeometry(); | |
} | |
Point getBottomRight(){ | |
return new Point(getRight(), getBottom()); | |
} | |
float getTotalArea(){ | |
return getWidth() * getHeight(); | |
} | |
void updateGeometry(){ } | |
void drawRect(PGraphics canvas){ | |
canvas.rect(getX(), getY(), getWidth(), getHeight()); | |
} | |
void drawImage(PGraphics canvas, PImage img){ | |
canvas.image(img, getX(), getY(), getWidth(), getHeight()); | |
} | |
void fillRect(PShape canvas, color c){ | |
canvas.fill(c); | |
} | |
String toStr(){ | |
return String.format("%s, %s", pos.toStr(), size.toStr()); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.*; | |
import java.nio.*; | |
class FrameHandler { | |
boolean connecting = false; | |
boolean maybeConnected = false; | |
long numFrames = 0, droppedFrames = 0, totalFrames = 0; | |
int maxRenders, inFlight, maxInFlight; | |
String sourceName = ""; | |
Deque<Integer> readQueue, writeQueue; | |
FrameThread frameThread; | |
DevolayReceiver ndiReceiver; | |
DevolayVideoFrame videoFrame; | |
DevolayAudioFrame audioFrame; | |
DevolayMetadataFrame metadataFrame; | |
int nextWriteIndex = 0, nextReadIndex = -1; | |
NDIImageHandler[] images; | |
NDIAudioHandler audio; | |
Object stateLockObj; | |
ReentrantReadWriteLock rwLock; | |
Lock rLock; | |
Lock wLock; | |
private boolean _isOpen = false; | |
FrameHandler(){ | |
stateLockObj = new Object(); | |
rwLock = new ReentrantReadWriteLock(); | |
rLock = rwLock.readLock(); | |
wLock = rwLock.writeLock(); | |
maxRenders = 0; | |
inFlight = 0; | |
maxInFlight = 0; | |
readQueue = new ArrayDeque<Integer>(); | |
writeQueue = new ArrayDeque<Integer>(); | |
images = new NDIImageHandler[4]; | |
for (int i=0; i<images.length; i++){ | |
images[i] = new NDIImageHandler(this, i); | |
} | |
audio = new NDIAudioHandler(this); | |
fillWriteQueue(); | |
assert writeQueue.size() == images.length - 1; | |
//open(); | |
} | |
public void open(){ | |
if (_isOpen){ | |
return; | |
} | |
assert frameThread == null; | |
frameThread = new FrameThread(this); | |
frameThread.start(); | |
_isOpen = true; | |
} | |
public void close(){ | |
if (!_isOpen){ | |
return; | |
} | |
maybeConnected = false; | |
if (frameThread != null){ | |
synchronized(stateLockObj){ | |
frameThread.running = false; | |
stateLockObj.notifyAll(); | |
} | |
frameThread = null; | |
} | |
disconnect(); | |
_isOpen = false; | |
} | |
public boolean isOpen(){ | |
return _isOpen; | |
} | |
NDIImageHandler getNextReadImage(){ | |
rLock.lock(); | |
NDIImageHandler result = null; | |
int idx = -1; | |
try { | |
if (readQueue.size() == 0){ | |
idx = nextReadIndex; | |
} else { | |
idx = readQueue.pop(); | |
if (readQueue.size() == 0){ | |
readQueue.addFirst(idx); | |
} | |
} | |
if (idx != -1){ | |
result = images[idx]; | |
} | |
nextReadIndex = idx; | |
} finally { | |
rLock.unlock(); | |
} | |
return result; | |
} | |
private void fillWriteQueue(){ | |
wLock.lock(); | |
try { | |
rLock.lock(); | |
try { | |
for (int i=0; i<images.length; i++){ | |
if (writeQueue.contains(i) || i == nextReadIndex || i == nextWriteIndex){ | |
continue; | |
} else if (readQueue.contains(i)){ | |
readQueue.remove(i); | |
//continue; | |
} | |
writeQueue.addLast(i); | |
} | |
} finally { | |
rLock.unlock(); | |
} | |
} finally { | |
wLock.unlock(); | |
} | |
} | |
NDIImageHandler getNextWriteImage(){ | |
wLock.lock(); | |
NDIImageHandler result = null; | |
int idx = -1; | |
try { | |
if (writeQueue.size() > 0){ | |
idx = writeQueue.pop(); | |
} else { | |
idx = -1; | |
} | |
nextWriteIndex = idx; | |
fillWriteQueue(); | |
if (idx != -1){ | |
result = images[idx]; | |
} | |
} finally { | |
wLock.unlock(); | |
} | |
return result; | |
} | |
void setImageWriteComplete(NDIImageHandler img){ | |
wLock.lock(); | |
try { | |
rLock.lock(); | |
try { | |
img.readReady = true; | |
readQueue.addLast(img.index); | |
inFlight = readQueue.size(); | |
if (inFlight > maxInFlight){ | |
maxInFlight = inFlight; | |
} | |
} finally { | |
rLock.unlock(); | |
} | |
} finally { | |
wLock.unlock(); | |
} | |
} | |
private void resetQueues(){ | |
wLock.lock(); | |
try { | |
rLock.lock(); | |
try { | |
nextReadIndex = -1; | |
nextWriteIndex = 0; | |
readQueue.clear(); | |
writeQueue.clear(); | |
} finally { | |
rLock.unlock(); | |
} | |
} finally { | |
wLock.unlock(); | |
} | |
} | |
private void notifyConnected(){ | |
if (!maybeConnected){ | |
return; | |
} | |
if (frameThread != null){ | |
synchronized(stateLockObj){ | |
stateLockObj.notifyAll(); | |
} | |
} | |
} | |
public void connectToSource(DevolaySource source){ | |
if (source == null && !maybeConnected){ | |
return; | |
} | |
println("connectToSource"); | |
synchronized(stateLockObj){ | |
_connectToSource(source); | |
} | |
} | |
private void _connectToSource(DevolaySource source){ | |
if (ndiReceiver != null){ | |
resetQueues(); | |
ndiReceiver.connect(source); | |
if (source == null){ | |
//ndiReceiver.connect(null); | |
//disconnect(); | |
sourceName = ""; | |
maybeConnected = false; | |
} else { | |
sourceName = source.getSourceName(); | |
maybeConnected = true; | |
} | |
notifyConnected(); | |
return; | |
} | |
if (source == null){ | |
maybeConnected = false; | |
sourceName = ""; | |
return; | |
} | |
println("create ndiReceiver"); | |
connecting = true; | |
try { | |
ndiReceiver = new DevolayReceiver(source, DevolayReceiver.ColorFormat.RGBX_RGBA, DevolayReceiver.RECEIVE_BANDWIDTH_HIGHEST, false, null); | |
videoFrame = new DevolayVideoFrame(); | |
audioFrame = new DevolayAudioFrame(); | |
metadataFrame = new DevolayMetadataFrame(); | |
maybeConnected = true; | |
println("receiver created"); | |
sourceName = source.getSourceName(); | |
} catch (Exception e){ | |
maybeConnected = false; | |
e.printStackTrace(); | |
throw(e); | |
} finally { | |
connecting = false; | |
println("maybeConnected: ", maybeConnected); | |
} | |
notifyConnected(); | |
} | |
public void disconnect(){ | |
synchronized(stateLockObj){ | |
_disconnect(); | |
} | |
} | |
private void _disconnect(){ | |
if (ndiReceiver != null){ | |
ndiReceiver.close(); | |
ndiReceiver = null; | |
} | |
if (videoFrame != null){ | |
videoFrame.close(); | |
videoFrame = null; | |
} | |
if (audioFrame != null){ | |
audioFrame.close(); | |
audioFrame = null; | |
} | |
if (metadataFrame != null){ | |
metadataFrame.close(); | |
metadataFrame = null; | |
} | |
sourceName = ""; | |
ndiReceiver = null; | |
connecting = false; | |
numFrames = 0; | |
droppedFrames = 0; | |
maxRenders = 0; | |
inFlight = 0; | |
maxInFlight = 0; | |
maybeConnected = false; | |
resetQueues(); | |
} | |
boolean isConnected(){ | |
if (sourceName == ""){ | |
maybeConnected = false; | |
return false; | |
} | |
if (ndiReceiver == null){ | |
maybeConnected = false; | |
return false; | |
} | |
if (ndiReceiver.getConnectionCount() == 0){ | |
maybeConnected = false; | |
return false; | |
} | |
maybeConnected = true; | |
return true; | |
} | |
DevolayFrameType getFrame(int timeout) { | |
DevolayFrameType frameType = DevolayFrameType.NONE; | |
//try { | |
//frameType = ndiReceiver.receiveCapture(videoFrame, audioFrame, metadataFrame, timeout); | |
frameType = ndiReceiver.receiveCapture(videoFrame, audioFrame, null, timeout); | |
//} finally { | |
// lastFrameType = frameType; | |
//} | |
if (frameType == DevolayFrameType.VIDEO){ | |
numFrames += 1; | |
} | |
if (frameType != DevolayFrameType.NONE){ | |
DevolayPerformanceData performanceData = new DevolayPerformanceData(); | |
try { | |
ndiReceiver.queryPerformance(performanceData); | |
droppedFrames = performanceData.getDroppedVideoFrames(); | |
totalFrames = performanceData.getTotalVideoFrames(); | |
//if (_droppedFrames != droppedFrames){ | |
// droppedFrames = _droppedFrames; | |
//} | |
} finally { | |
performanceData.close(); | |
} | |
} | |
return frameType; | |
} | |
} | |
class NDIImageHandler implements PConstants{ | |
FrameHandler parent; | |
int index; | |
Point resolution; | |
PImage image; | |
ReentrantReadWriteLock rwLock; | |
Lock rLock; | |
Lock wLock; | |
boolean writeReady = true, readReady = true, isBlank = true; | |
int numRenders; | |
NDIImageHandler(FrameHandler _parent, int _index){ | |
parent = _parent; | |
index = _index; | |
resolution = new Point(1920, 1080); | |
image = new PImage((int)resolution.x, (int)resolution.y, ARGB); | |
rwLock = new ReentrantReadWriteLock(); | |
rLock = rwLock.readLock(); | |
wLock = rwLock.writeLock(); | |
numRenders = 0; | |
} | |
int getWidth(){ return (int)resolution.x; } | |
int getHeight(){ return (int)resolution.y; } | |
void setWidth(float w){ resolution.x = w; } | |
void setHeight(float h){ resolution.y = h; } | |
void setResolution(float w, float h){ | |
resolution.x = w; | |
resolution.y = h; | |
} | |
boolean drawToCanvas(PGraphics canvas, Box dims){ | |
boolean acquired = wLock.tryLock(); | |
if (!acquired){ | |
return false; | |
} | |
try { | |
if (!parent.maybeConnected && !isBlank){ | |
Arrays.fill(image.pixels, 0xff000000); | |
isBlank = true; | |
} | |
//assert readReady; | |
image.updatePixels(); | |
canvas.image(image, dims.getX(), dims.getY(), dims.getWidth(), dims.getHeight()); | |
numRenders += 1; | |
if (numRenders > parent.maxRenders){ | |
parent.maxRenders = numRenders; | |
} | |
} finally { | |
//writeReady = true; | |
//readReady = false; | |
wLock.unlock(); | |
//parent.incrementReadIndex(); | |
} | |
return true; | |
} | |
boolean setImagePixels(DevolayVideoFrame videoFrame){ | |
//println("setImagePixels"); | |
boolean result = false; | |
wLock.lock(); | |
readReady = false; | |
try { | |
assert writeReady; | |
result = _setImagePixels(videoFrame); | |
readReady = true; | |
//writeReady = false; | |
numRenders = 0; | |
isBlank = false; | |
} catch (Exception e){ | |
e.printStackTrace(); | |
throw(e); | |
} finally { | |
wLock.unlock(); | |
} | |
//parent.incrementWriteIndex(); | |
return result; | |
} | |
boolean _setImagePixels(DevolayVideoFrame videoFrame){ | |
int frameWidth = videoFrame.getXResolution(); | |
int frameHeight = videoFrame.getYResolution(); | |
DevolayFrameFourCCType fourCC = videoFrame.getFourCCType(); | |
assert (fourCC == DevolayFrameFourCCType.RGBA || fourCC == DevolayFrameFourCCType.RGBX); | |
if (frameWidth == 0 || frameHeight == 0){ | |
System.out.println("frameSize = 0"); | |
setResolution(0, 0); | |
return false; | |
} | |
if (getWidth() != frameWidth || getHeight() != frameHeight){ | |
System.out.println(String.format("resize image to %dx%d", frameWidth, frameHeight)); | |
setResolution(frameWidth, frameHeight); | |
if (getWidth() != image.width || getHeight() != image.height){ | |
image.init(frameWidth, frameHeight, ARGB); | |
} | |
assert image.pixels.length == getWidth() * getHeight(); | |
} | |
assert videoFrame.getLineStride() == frameWidth * 4; | |
if (fourCC == DevolayFrameFourCCType.RGBA){ | |
videoFrameToImageArr_RGBA(videoFrame, image.pixels); | |
} else { | |
videoFrameToImageArr_RGBX(videoFrame, image.pixels); | |
} | |
image.updatePixels(); | |
return true; | |
} | |
} | |
class NDIAudioHandler { | |
FrameHandler parent; | |
int sampleRate, nChannels, blockSize, stride; | |
private boolean initialized = false; | |
boolean meterChanged = false; | |
AudioMeter meter; | |
NDIAudioHandler(FrameHandler _parent){ | |
parent = _parent; | |
initialized = false; | |
meter = new AudioMeter(1, 4, 1); | |
} | |
void setInitData(DevolayAudioFrame frame){ | |
sampleRate = frame.getSampleRate(); | |
nChannels = frame.getChannels(); | |
blockSize = frame.getSamples(); | |
//Box bbox = meter.boundingBox.copy(); | |
synchronized(this){ | |
meter = new AudioMeter(sampleRate, nChannels, blockSize); | |
meterChanged = true; | |
} | |
//meter.boundingBox = bbox; | |
//setMeterChanged(true); | |
} | |
void processFrame(){ | |
DevolayAudioFrame frame = parent.audioFrame; | |
if (!initialized){ | |
setInitData(frame); | |
initialized = true; | |
} | |
stride = frame.getChannelStride(); | |
//meter.processSamples(frame.getData(), frame.getSamples(), frame.getChannelStride()); | |
meter.processSamples(frame); | |
} | |
} | |
class FrameThread extends Thread { | |
FrameHandler handler; | |
boolean running = false; | |
Exception error; | |
FrameThread(FrameHandler _handler){ | |
handler = _handler; | |
} | |
public void run(){ | |
println("FrameThread run start"); | |
running = true; | |
while (running){ | |
try { | |
if (!handler.maybeConnected){ | |
synchronized (handler.stateLockObj){ | |
try{ | |
while (!handler.maybeConnected){ | |
handler.stateLockObj.wait(); | |
} | |
} catch (InterruptedException e) { | |
} | |
} | |
//println("first wait complete"); | |
if (!running){ | |
break; | |
} | |
if (!handler.maybeConnected){ | |
break; | |
} | |
} | |
//println("getting frame"); | |
DevolayFrameType ft = handler.getFrame(100); | |
//println(ft); | |
switch (ft){ | |
case VIDEO: | |
NDIImageHandler img = null; | |
//println("locking"); | |
synchronized(handler.stateLockObj){ | |
if (!handler.maybeConnected){ | |
continue; | |
} | |
img = handler.getNextWriteImage(); | |
if (img != null){ | |
//println("got img"); | |
img.setImagePixels(handler.videoFrame); | |
handler.setImageWriteComplete(img); | |
} else { | |
println("img is null :("); | |
} | |
} | |
break; | |
case AUDIO: | |
handler.audio.processFrame(); | |
break; | |
} | |
} catch(Exception e){ | |
e.printStackTrace(); | |
throw(e); | |
} | |
} | |
running = false; | |
println("FrameThread run stop"); | |
} | |
} | |
void videoFrameToImageArr_RGBA(DevolayVideoFrame videoFrame, int[] pixelArray){ | |
int frameWidth = videoFrame.getXResolution(); | |
int frameHeight = videoFrame.getYResolution(); | |
ByteBuffer framePixels = videoFrame.getData(); | |
IntBuffer framePixelsInt = framePixels.asIntBuffer(); | |
int numPixels = frameWidth * frameHeight; | |
for (int i=0; i<numPixels; i++){ | |
int colorValue = framePixelsInt.get(); | |
int alpha = colorValue & 0xff; | |
colorValue = (colorValue >> 8) | alpha << 24; | |
pixelArray[i] = colorValue; | |
} | |
} | |
void videoFrameToImageArr_RGBX(DevolayVideoFrame videoFrame, int[] pixelArray){ | |
int frameWidth = videoFrame.getXResolution(); | |
int frameHeight = videoFrame.getYResolution(); | |
ByteBuffer framePixels = videoFrame.getData(); | |
IntBuffer framePixelsInt = framePixels.asIntBuffer(); | |
int numPixels = frameWidth * frameHeight; | |
int alphaMask = 0xff << 24; | |
for (int i=0; i<numPixels; i++){ | |
pixelArray[i] = (framePixelsInt.get() >> 8) | alphaMask; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//import java.util.Map; | |
import java.awt.Frame; | |
import java.awt.Shape; | |
import java.awt.Rectangle; | |
import java.awt.GraphicsEnvironment; | |
import java.awt.GraphicsDevice; | |
import java.awt.DisplayMode; | |
import processing.awt.*; | |
import processing.awt.ShimAWT; | |
import java.io.File; | |
import me.walkerknapp.devolay.*; | |
import controlP5.*; | |
MultiviewApplet mvApp; | |
PFont baseWindowFont; | |
int confSaveInterval = 60; | |
float resizeCheckInterval = .25; | |
boolean baseloopInitial = true; | |
float sourceUpdateTimeInterval = 10; | |
ControlP5 basecp5; | |
JSONObject loadConfig(){ | |
File confFile = getConfigFile(); | |
System.out.println("loadConfig: " + confFile.getPath()); | |
if (!confFile.exists()){ | |
return new JSONObject(); | |
} | |
return loadJSONObject(confFile.getPath()); | |
} | |
Config getConfig(){ | |
File confFile = getConfigFile(); | |
System.out.println("loadConfig: " + confFile.getPath()); | |
if (!confFile.exists()){ | |
return new Config(); | |
} | |
JSONObject json = loadJSONObject(confFile.getPath()); | |
return new Config(json); | |
} | |
void setup(){ | |
String[] args = {"--sketch-path="+sketchPath(), "NDI Multiviewer"}; | |
mvApp = new MultiviewApplet(); | |
PApplet.runSketch(args, mvApp); | |
basecp5 = new ControlP5(this); | |
baseWindowFont = createFont("Georgia", 12); | |
size(200, 100); | |
frameRate(10); | |
basecp5.addButton("fullScreenToggle") | |
.setValue(0) | |
.setSwitch(true) | |
.setLabel("Fullscreen"); | |
basecp5.addTextlabel("appSizeLbl") | |
.setText(String.format("(%d, %d)", (int)mvApp.width, (int)mvApp.height)) | |
.setPosition(0, 50) | |
.setFont(baseWindowFont); | |
} | |
void draw(){ | |
background(0); | |
Textlabel lbl = (Textlabel)basecp5.getController("appSizeLbl"); | |
lbl.setText(String.format("(%d, %d)", (int)mvApp.width, (int)mvApp.height)); | |
String txt0 = String.format("Base fps=%d, frame=%06d", (int)frameRate, (int)frameCount); | |
String txt1 = String.format("mvApp fps=%d, frame=%06d", (int)mvApp.frameRate, (int)mvApp.frameCount); | |
textAlign(RIGHT, TOP); | |
text(txt0, 0, 0); | |
textAlign(RIGHT, BOTTOM); | |
text(txt1, 0, height); | |
if (baseloopInitial && !mvApp.loopInitial){ | |
basecp5.getController("fullScreenToggle").setValue(mvApp.isFullScreen ? 1 : 0); | |
baseloopInitial = false; | |
} | |
} | |
public void fullScreenToggle(boolean value){ | |
if (!baseloopInitial){ | |
mvApp.setFullScreen(value); | |
} | |
} | |
public class MultiviewApplet extends PApplet { | |
Config config; | |
WindowGrid windowGrid; | |
PFont windowFont; | |
DevolayFinder ndiFinder; | |
DevolaySource[] ndiSourceArray; | |
Object ndiSourceLock = new Object(); | |
Object ndiSourceNotify = new Object(); | |
boolean isFullScreen = false; | |
boolean updatingSources = false; | |
boolean sourcesUpdated = false; | |
boolean loopInitial = true; | |
int lastSourceUpdateFrame = 0; | |
int lastConfSaveFrame = -1; | |
int nextConfSaveFrame = -1; | |
HashMap<String,DevolaySource> ndiSources; | |
Box windowBounds; | |
ControlP5 cp5; | |
public void settings() { | |
config = getConfig(); | |
isFullScreen = config.app.fullScreen; | |
if (config.app.fullScreen){ | |
fullScreen(P3D, config.app.displayNumber); | |
} else { | |
int maxWidth = displayWidth - 100; | |
int maxHeight = displayHeight - 100; | |
if (config.app.canvasSize.x >= maxWidth){ | |
config.app.canvasSize.x = maxWidth; | |
} | |
if (config.app.canvasSize.y >= maxHeight){ | |
config.app.canvasSize.y = maxHeight; | |
} | |
size((int)config.app.canvasSize.x, (int)config.app.canvasSize.y, P3D); | |
} | |
} | |
public void setFullScreen(boolean value){ | |
if (value == isFullScreen){ | |
return; | |
} | |
isFullScreen = value; | |
saveConfig(); | |
} | |
StringList getDisplays(){ | |
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); | |
GraphicsDevice defaultDevice = ge.getDefaultScreenDevice(); | |
GraphicsDevice[] devices = ge.getScreenDevices(); | |
StringList result = new StringList(); | |
for (int i=0; i<devices.length; i++){ | |
GraphicsDevice device = devices[i]; | |
DisplayMode mode = device.getDisplayMode(); | |
String suffix = (device == defaultDevice) ? "(default)" : ""; | |
String s = String.format("%d x %d%s", mode.getWidth(), mode.getHeight(), suffix); | |
//result.append(device.getIDString()); | |
result.append(s); | |
} | |
return result; | |
} | |
public void closeBtn(int value){ | |
exit(); | |
} | |
public void fullScreenToggle(boolean value){ | |
setFullScreen(value); | |
} | |
public void setup(){ | |
this.surface.setResizable(true); | |
this.frameRate(60); | |
cp5 = new ControlP5(this); | |
Box btnBox = new Box(0, 0, 40, 20); | |
btnBox.setRight(width); | |
cp5.addButton("closeBtn") | |
.setLabel("Close") | |
.setPosition(btnBox.getX(), btnBox.getY()) | |
.setSize((int)btnBox.getWidth(), (int)btnBox.getHeight()); | |
btnBox.setRight(btnBox.getX() - 10); | |
cp5.addButton("fullScreenToggle") | |
.setLabel("Fullscreen") | |
.setPosition(btnBox.getX(), btnBox.getY()) | |
.setSize((int)btnBox.getWidth(), (int)btnBox.getHeight()) | |
.setValue(config.app.fullScreen ? 1 : 0) | |
.setSwitch(true); | |
windowFont = createFont("Georgia", 12, true); | |
ndiSourceArray = new DevolaySource[0]; | |
ndiSources = new HashMap<String,DevolaySource>(); | |
System.out.println("loadingLibraries..."); | |
Devolay.loadLibraries(); | |
ndiFinder = new DevolayFinder(); | |
System.out.println("Creating WindowGrid..."); | |
config.windowGrid.outputSize = new Point(this.width, this.height); | |
windowGrid = new WindowGrid(config.windowGrid); | |
} | |
public void draw() { | |
checkResize(); | |
updateNdiSources(); | |
if (this.exitCalled){ | |
System.out.println("Closing resources"); | |
windowGrid.close(); | |
return; | |
} | |
g.background(0); | |
windowGrid.render(g); | |
loopInitial = false; | |
} | |
void checkResize(){ | |
//int frInterval = secondsToFrame(resizeCheckInterval); | |
if ((int)frameCount % 120 != 0){ | |
return; | |
} | |
if ((int)this.width != windowGrid.outWidth || (int)this.height != windowGrid.outHeight){ | |
println("resize canvas"); | |
cp5.setGraphics(this, 0, 0); | |
Box btnBox = new Box(0, 0, 40, 20); | |
btnBox.setRight(width); | |
Button btn = (Button)cp5.getController("closeBtn"); | |
btn.setPosition(btnBox.getX(), btnBox.getY()) | |
.setSize((int)btnBox.getWidth(), (int)btnBox.getHeight()); | |
btnBox.setRight(btnBox.getX() - 10); | |
btn = (Button)cp5.getController("fullScreenToggle"); | |
btn.setPosition(btnBox.getX(), btnBox.getY()) | |
.setSize((int)btnBox.getWidth(), (int)btnBox.getHeight()); | |
windowGrid.setOutputSize((int)this.width, (int)this.height); | |
saveConfig(); | |
} | |
} | |
void saveConfig(JSONObject json){ | |
File confFile = getConfigFile(); | |
System.out.println("saveConfig: " + confFile.getAbsolutePath()); | |
saveJSONObject(json, confFile.getPath()); | |
} | |
void saveConfig(Config c){ | |
try { | |
JSONObject json = c.serialize(); | |
saveConfig(json); | |
} catch(Exception e){ | |
e.printStackTrace(); | |
throw(e); | |
} | |
} | |
void saveConfig(){ | |
config.update(this); | |
saveConfig(config); | |
} | |
void confAutoSave(){ | |
if (nextConfSaveFrame == -1 || frameCount >= nextConfSaveFrame){ | |
//windowBounds = getWindowDims(); | |
System.out.println("autosave config"); | |
saveConfig(); | |
lastConfSaveFrame = frameCount; | |
nextConfSaveFrame = frameCount + secondsToFrame(confSaveInterval); | |
} | |
} | |
Box getWindowDims(){ | |
PSurfaceAWT.SmoothCanvas nativeWin = (PSurfaceAWT.SmoothCanvas)this.surface.getNative(); | |
java.awt.Rectangle bBox = nativeWin.getFrame().getBounds(); | |
Box b = new Box(bBox.x, bBox.y, bBox.width, bBox.height); | |
return b; | |
} | |
void updateNdiSources(){ | |
if (sourcesUpdated){ | |
System.out.println("sourcesUpdated"); | |
sourcesUpdated = false; | |
lastSourceUpdateFrame = this.frameCount; | |
windowGrid.updateNdiSources(); | |
} | |
if (updatingSources){ | |
return; | |
} | |
thread("_updateNDISources"); | |
} | |
void _updateNDISources() { | |
System.out.println("updateNdiSources"); | |
int timeout = 8000; | |
int maxTries = 5; | |
updatingSources = true; | |
//DevolayFinder finder = new DevolayFinder(); | |
DevolayFinder finder = ndiFinder; | |
int numAttempts = 0; | |
boolean changed = false; | |
if (!loopInitial){ | |
changed = finder.waitForSources(timeout); | |
if (!changed){ | |
sourcesUpdated = false; | |
updatingSources = false; | |
println("updateExit"); | |
return; | |
} | |
} | |
synchronized(ndiSourceLock){ | |
DevolaySource[] sources = new DevolaySource[0]; | |
sources = finder.getCurrentSources(); | |
ndiSources.clear(); | |
for (int i=0;i<sources.length;i++){ | |
ndiSources.put(sources[i].getSourceName(), sources[i]); | |
System.out.println(sources[i].getSourceName()); | |
} | |
ndiSourceArray = sources; | |
sourcesUpdated = true; | |
updatingSources = false; | |
ndiSourceLock.notifyAll(); | |
println("updateExit"); | |
} | |
} | |
int secondsToFrame(float sec){ | |
float fr = this.frameRate; | |
if (fr == 0){ | |
fr = 1; | |
} | |
return (int)(fr * sec); | |
} | |
float frameToSeconds(int f){ | |
float fr = this.frameRate; | |
if (fr == 0){ | |
fr = 1; | |
} | |
return f / fr; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io.File; | |
enum Platform { | |
LINUX, MAC, WINDOWS, UNKNOWN; | |
} | |
Platform getPlatform(){ | |
if (System.getProperty("os.name").indexOf("Mac") != -1){ | |
return Platform.MAC; | |
} else if (System.getProperty("os.name").indexOf("Windows") != -1){ | |
return Platform.WINDOWS; | |
} else if (System.getProperty("os.name").indexOf("Linux") != -1){ | |
return Platform.LINUX; | |
} | |
return Platform.UNKNOWN; | |
} | |
String joinPath(StringList args){ | |
StringList parts = new StringList(); | |
//for (int i=0; i<args.size(); i++){ | |
for (String s : args){ | |
//String s = args[i]; | |
if (s.contains("/")){ | |
for (String _s : s.split("/")){ | |
if (_s.length() > 0){ | |
parts.append(_s); | |
} | |
} | |
} else if (s.length() > 0){ | |
parts.append(s); | |
} | |
} | |
return parts.join("/"); | |
} | |
File getUserConfigDir(){ | |
StringList dirNames = new StringList(); | |
dirNames.append(System.getProperty("user.home")); | |
//Path p; | |
switch(getPlatform()){ | |
case LINUX: | |
//dirNames.append(System.getenv("HOME")); | |
dirNames.append(".config"); | |
break; | |
case MAC: | |
//dirNames.append(System.getenv("HOME")); | |
dirNames.append("Library"); | |
dirNames.append("Preferences"); | |
break; | |
case WINDOWS: | |
dirNames.clear(); | |
dirNames.append(System.getenv("LOCALAPPDATA")); | |
break; | |
case UNKNOWN: | |
} | |
return new File(joinPath(dirNames), "ndiMultiview"); | |
} | |
File getConfigFile(){ | |
//String userHome = System.getProperty("user.home"); | |
//assert userHome != null; | |
//return new File("config.json"); | |
File confDir = getUserConfigDir(); | |
if (!confDir.exists()){ | |
confDir.mkdir(); | |
} | |
return new File(confDir, "config.json"); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//import java.util.Map; | |
//import static java.util.Map.entry; | |
class TextBox extends Box { | |
private int hAlign, vAlign; | |
private int bgColor, fgColor; | |
public String text; | |
public boolean drawBackground = true; | |
private Point textPos; | |
private boolean textPosOverride; | |
private int textSize; | |
TextBox(){ | |
super(); | |
initDefaults(); | |
} | |
TextBox(Point _pos, Point _size, String _text, int _hAlign, int _vAlign, int _textSize, int _bg, int _fg){ | |
super(_pos, _size); | |
initDefaults(); | |
text = _text; | |
hAlign = _hAlign; | |
vAlign = _vAlign; | |
textSize = _textSize; | |
bgColor = _bg; | |
fgColor = _fg; | |
//updateGeometry(); | |
} | |
TextBox(Box _b, String _text, int _hAlign, int _vAlign, int _textSize, int _bg, int _fg){ | |
super(_b); | |
initDefaults(); | |
text = _text; | |
hAlign = _hAlign; | |
vAlign = _vAlign; | |
textSize = _textSize; | |
bgColor = _bg; | |
fgColor = _fg; | |
//updateGeometry(); | |
} | |
TextBox(Point _pos, Point _size) { | |
super(_pos, _size); | |
initDefaults(); | |
//updateGeometry(); | |
} | |
TextBox(Point _pos, float w, float h){ | |
super(_pos, w, h); | |
initDefaults(); | |
//updateGeometry(); | |
} | |
TextBox(float x, float y, Point _size){ | |
super(x, y, _size); | |
initDefaults(); | |
//updateGeometry(); | |
} | |
void initDefaults(){ | |
textPosOverride = false; | |
text = ""; | |
hAlign = CENTER; | |
vAlign = CENTER; | |
bgColor = 0x60303030; | |
fgColor = 255; | |
textSize = 12; | |
textPos = new Point(0, 0); | |
updateGeometry(); | |
} | |
//@Override | |
TextBox copy(){ | |
Box b = new Box(this); | |
return new TextBox(b, text, hAlign, vAlign, textSize, bgColor, fgColor); | |
} | |
void setAlign(int _hAlign){ | |
if (_hAlign == hAlign){ | |
return; | |
} | |
hAlign = _hAlign; | |
updateGeometry(); | |
} | |
void setAlign(int _hAlign, int _vAlign){ | |
if (_hAlign == hAlign && _vAlign == vAlign){ | |
return; | |
} | |
hAlign = _hAlign; | |
vAlign = _vAlign; | |
updateGeometry(); | |
} | |
int getHAlign(){ return hAlign; } | |
int getVAlign(){ return vAlign; } | |
int getTextSize(){ return textSize; } | |
void setTextSize(int value){ textSize = value; } | |
Point getTextPos(){ | |
return textPos.copy(); | |
} | |
void setTextPos(Point p){ | |
setTextPos(p.x, p.y); | |
} | |
void setTextPos(float x, float y){ | |
textPos.x = x; | |
textPos.y = y; | |
textPosOverride = true; | |
} | |
void setTextPosRelative(Point p){ | |
Point offset = getPos(); | |
setTextPos(p.x + offset.x, p.y + offset.y); | |
} | |
void updateGeometry(){ | |
super.updateGeometry(); | |
if (textPosOverride){ | |
return; | |
} | |
Point _textPos = new Point(-1, -1); | |
if (hAlign == LEFT){ | |
_textPos.x = getX(); | |
} else if (hAlign == CENTER){ | |
_textPos.x = getHCenter(); | |
} else if (hAlign == RIGHT){ | |
_textPos.x = getRight(); | |
} | |
if (vAlign == TOP){ | |
_textPos.y = getY(); | |
} else if (vAlign == CENTER){ | |
_textPos.y = getVCenter(); | |
} else if (vAlign == BOTTOM){ | |
_textPos.y = getBottom(); | |
} | |
textPos = _textPos; | |
} | |
void render(PGraphics canvas){ | |
if (drawBackground){ | |
canvas.fill(bgColor); | |
//canvas.noStroke(); | |
canvas.stroke(128); | |
canvas.rect(getX(), getY(), getWidth(), getHeight()); | |
} | |
canvas.fill(fgColor); | |
canvas.textFont(mvApp.windowFont); | |
canvas.textSize(textSize); | |
canvas.textAlign(hAlign, vAlign); | |
canvas.text(text, textPos.x, textPos.y); | |
} | |
String toStr(){ | |
String s = super.toStr(); | |
return String.format("TextBox '%s': text='%s', textPos=%s, bgColor=%d, fgColor=%d", s, text, textPos.toStr(), bgColor, fgColor); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.List; | |
import java.util.Map; | |
import java.util.concurrent.locks.*; | |
class Window { | |
String name = ""; | |
int col,row; | |
Point padding; | |
Box boundingBox, frameBox, meterBox; | |
String ndiSourceName = ""; | |
int numFrames = 0, numDraws = 0; | |
long droppedFrames = 0; | |
boolean frameReady = false; | |
boolean gettingFrame = false; | |
boolean connecting = false; | |
boolean canvasReady = false; | |
boolean clearImageOnNextFrame = false; | |
boolean maybeConnected = false; | |
PImage srcImage; | |
TextBox nameLabel, formatLabel, statsLabel; | |
FrameHandler frameHandler; | |
WindowControls controls; | |
Window(String _name, int _col, int _row, float _x, float _y, float _w, float _h, Point _padding, String _ndiSourceName) { | |
name = _name; | |
col = _col; | |
row = _row; | |
padding = _padding; | |
boundingBox = new Box(_x, _y, _w, _h); | |
ndiSourceName = _ndiSourceName; | |
init(); | |
} | |
Window(String _name, int _col, int _row, Box _boundingBox, Point _padding, String _ndiSourceName) { | |
name = _name; | |
col = _col; | |
row = _row; | |
padding = _padding; | |
boundingBox = _boundingBox; | |
ndiSourceName = _ndiSourceName; | |
init(); | |
} | |
Window(JSONObject json, Box _boundingBox, Point _padding){ | |
name = json.getString("name"); | |
col = json.getInt("col"); | |
row = json.getInt("row"); | |
padding = _padding; | |
boundingBox = _boundingBox; | |
ndiSourceName = json.getString("ndiSourceName"); | |
init(); | |
} | |
Window(WindowConfig config, Box _boundingBox, Point _padding){ | |
name = config.name; | |
col = config.col; | |
row = config.row; | |
padding = _padding; | |
boundingBox = _boundingBox; | |
ndiSourceName = config.ndiSourceName; | |
init(); | |
} | |
JSONObject serialize(){ | |
JSONObject json = new JSONObject(); | |
json.setInt("col", col); | |
json.setInt("row", row); | |
json.setString("name", name); | |
json.setString("ndiSourceName", ndiSourceName); | |
return json; | |
} | |
private void init(){ | |
frameBox = calcFrameBox(); | |
nameLabel = new TextBox(frameBox.getTopLeft(), 100, 18); | |
formatLabel = new TextBox(frameBox.getTopLeft(), 260, 18); | |
statsLabel = new TextBox(frameBox.getTopLeft(), 200, 60); | |
nameLabel.setBottomCenter(frameBox.getBottomCenter()); | |
nameLabel.setAlign(CENTER, BOTTOM); | |
formatLabel.setTopCenter(frameBox.getTopCenter()); | |
formatLabel.setAlign(CENTER, TOP); | |
statsLabel.setBottomLeft(frameBox.getBottomLeft()); | |
statsLabel.setAlign(LEFT, BOTTOM); | |
System.out.println(String.format("%s bBox: %s, frame: %s", getId(), boundingBox.toStr(), frameBox.toStr())); | |
frameHandler = new FrameHandler(); | |
meterBox = calcMeterBox(); | |
frameHandler.audio.meter.setBoundingBox(meterBox); | |
controls = new WindowControls(this); | |
} | |
Box calcFrameBox(){ | |
Point pos = new Point(boundingBox.getX() + padding.x, boundingBox.getY() + padding.y); | |
Point size = new Point(boundingBox.size.x - padding.x*2, boundingBox.size.y - padding.y*2); | |
Box b = new Box(pos, size.copy()); | |
b.setAspectRatioH(16.0/9.0); | |
if (b.getHeight() > size.y){ | |
b.setSize(size.copy()); | |
b.setAspectRatioW(9.0/16.0); | |
} | |
b.setCenter(boundingBox.getCenter()); | |
return b; | |
} | |
Box calcMeterBox(){ | |
//float w = 0.5 * frameHandler.audio.nChannels; | |
Box b = frameBox.copy(); | |
b.setHeight(frameBox.getHeight()*.8); | |
b.setWidth(frameBox.getWidth()*.125); | |
b.setX(frameBox.getX()+3); | |
b.setVCenter(frameBox.getVCenter()); | |
return b; | |
} | |
void setBoundingBox(Box _boundingBox){ | |
boundingBox = _boundingBox; | |
frameBox = calcFrameBox(); | |
nameLabel.setBottomCenter(frameBox.getBottomCenter()); | |
formatLabel.setTopCenter(frameBox.getTopCenter()); | |
synchronized(frameHandler.audio){ | |
meterBox = calcMeterBox(); | |
frameHandler.audio.meter.setBoundingBox(meterBox); | |
frameHandler.audio.meterChanged = false; | |
} | |
controls.initControls(); | |
} | |
String getId(){ | |
return String.format("%02d-%02d", col, row); | |
} | |
void setName(String _name){ | |
setName(_name, true); | |
} | |
void setName(String _name, boolean updateControls){ | |
System.out.println("setName: '" + _name + "'"); | |
if (_name == name){ | |
return; | |
} | |
name = _name; | |
mvApp.saveConfig(); | |
if (updateControls){ | |
controls.updateFieldValues(); | |
} | |
} | |
void setSourceName(String srcName){ | |
setSourceName(srcName, true); | |
} | |
void setSourceName(String srcName, boolean updateControls){ | |
if (srcName == ndiSourceName){ | |
return; | |
} | |
ndiSourceName = srcName; | |
mvApp.saveConfig(); | |
System.out.println(getId()+" ndiSourceName: '"+srcName+"'"); | |
connectToSource(); | |
if (updateControls){ | |
updateNdiSources(); | |
} | |
} | |
void updateNdiSources(){ | |
controls.updateFieldValues(); | |
} | |
void connectToSource(){ | |
DevolaySource src = null; | |
if (ndiSourceName == frameHandler.sourceName){ | |
return; | |
} | |
synchronized(mvApp.ndiSourceLock){ | |
if (ndiSourceName != ""){ | |
if (mvApp.ndiSources.containsKey(ndiSourceName)){ | |
src = mvApp.ndiSources.get(ndiSourceName); | |
} | |
} | |
frameHandler.connectToSource(src); | |
maybeConnected = frameHandler.maybeConnected; | |
if (maybeConnected){ | |
frameHandler.open(); | |
} | |
} | |
} | |
void close(){ | |
frameHandler.close(); | |
maybeConnected = false; | |
} | |
void disconnect(){ | |
frameHandler.disconnect(); | |
clearImageOnNextFrame = true; | |
maybeConnected = false; | |
} | |
boolean isConnected(){ | |
boolean result = frameHandler.isConnected(); | |
maybeConnected = result; | |
return result; | |
} | |
boolean canConnect(){ | |
return (ndiSourceName.length() > 0); | |
} | |
void render(PGraphics canvas){ | |
canvas.stroke(0); | |
canvas.fill(0); | |
canvas.rect(boundingBox.pos.x, boundingBox.pos.y, boundingBox.size.x, boundingBox.size.y); | |
canvas.stroke(255); | |
canvas.rect(frameBox.pos.x-1, frameBox.pos.y-1, frameBox.size.x+2, frameBox.size.y+2); | |
NDIImageHandler img = frameHandler.getNextReadImage(); | |
int imgIdx = -1; | |
if (img != null && !img.isBlank){ | |
img.drawToCanvas(canvas, frameBox); | |
imgIdx = img.index; | |
} | |
nameLabel.text = name; | |
nameLabel.render(canvas); | |
formatLabel.text = String.format("Dropped %d frames out of %d total", frameHandler.droppedFrames, frameHandler.totalFrames); | |
//formatLabel.text = String.format("%02d", imgIdx); | |
//formatLabel.text = String.format("%dx%d", srcWidth, srcHeight); | |
formatLabel.render(canvas); | |
//statsLabel.text = String.format("maxRenders: %d, imgIdx: %d\ninFlight: %d / %d\nwriteQueue: %d, readQueue: %d", | |
// frameHandler.maxRenders, imgIdx, frameHandler.inFlight, frameHandler.maxInFlight, | |
// frameHandler.writeQueue.size(), frameHandler.readQueue.size() | |
//); | |
statsLabel.text = String.format("peak: %5.1f, amplitude: %08.6f\nrms: %5.1f dB\nblockSize: %s, bfrLen: %s\n stride: %d, nChannels: %d", | |
frameHandler.audio.meter.peakDbfs[0], frameHandler.audio.meter.peakAmp[0], frameHandler.audio.meter.rmsDbfs[0], | |
frameHandler.audio.meter.blockSize, frameHandler.audio.meter.bufferLength[0], frameHandler.audio.stride, frameHandler.audio.meter.nChannels | |
); | |
//statsLabel.render(canvas); | |
synchronized(frameHandler.audio){ | |
if (frameHandler.audio.meterChanged){ | |
meterBox = calcMeterBox(); | |
frameHandler.audio.meter.setBoundingBox(meterBox); | |
frameHandler.audio.meterChanged = false; | |
} | |
frameHandler.audio.meter.render(canvas); | |
} | |
} | |
} | |
class WindowControls { | |
Window win; | |
String winId; | |
boolean controlsCreated = false; | |
DropdownList sourceDropdown; | |
Button editNameBtn; | |
boolean editNameEnabled = false; | |
Textfield editNameField; | |
WindowControls(Window _win){ | |
win = _win; | |
winId = _win.getId(); | |
initControls(true); | |
} | |
void initControls(){ | |
initControls(false); | |
} | |
void initControls(boolean create){ | |
if (!controlsCreated && !create){ | |
return; | |
} | |
System.out.println("initControls: win.getId = '" + win.getId() + "', myId='" + winId + "'"); | |
String dropdownId = winId + "-dropdown"; | |
Point ddPos = win.frameBox.getPos(); | |
if (sourceDropdown == null){ | |
buildSourceDropdown(dropdownId, ddPos); | |
} else { | |
setWidgetPos(sourceDropdown, ddPos); | |
} | |
String editNameBtnId = winId + "-editNameBtn"; | |
String editNameFieldId = winId + "-editNameField"; | |
Box editNameBtnBox = new Box(win.nameLabel); | |
editNameBtnBox.setX(win.nameLabel.getRight() + 8); | |
Box editNameFieldBox = new Box(win.nameLabel); | |
editNameFieldBox.setRight(win.nameLabel.getX() - 8); | |
if (editNameBtn == null){ | |
buildEditNameControls(editNameBtnId, editNameBtnBox, editNameFieldId, editNameFieldBox); | |
} else { | |
if (!editNameEnabled){ | |
editNameField.setText(win.name); | |
} | |
setWidgetBox(editNameBtn, editNameBtnBox); | |
setWidgetBox(editNameField, editNameFieldBox); | |
} | |
controlsCreated = true; | |
} | |
Controller setWidgetPos(Controller widget, Point pos){ | |
widget.setPosition(pos.x, pos.y); | |
return widget; | |
} | |
Controller setWidgetPos(Controller widget, Box b){ | |
return setWidgetPos(widget, b.getPos()); | |
} | |
Controller setWidgetSize(Controller widget, Point size){ | |
widget.setSize((int)size.x, (int)size.y); | |
return widget; | |
} | |
Controller setWidgetSize(Controller widget, Box b){ | |
return setWidgetSize(widget, b.getSize()); | |
} | |
Controller setWidgetBox(Controller widget, Box b){ | |
setWidgetPos(widget, b); | |
setWidgetSize(widget, b); | |
return widget; | |
} | |
void updateFieldValues(){ | |
if (controlsCreated){ | |
updateDropdownItems(); | |
if (!editNameEnabled){ | |
editNameField.setText(win.name); | |
} | |
} | |
} | |
void updateDropdownItems(){ | |
if (sourceDropdown == null){ | |
return; | |
} | |
List<String> itemNames = new ArrayList<String>(); | |
int selIndex = -2; | |
itemNames.add("None"); | |
synchronized(mvApp.ndiSourceLock){ | |
for (int i=0; i<mvApp.ndiSourceArray.length; i++){ | |
String srcName = mvApp.ndiSourceArray[i].getSourceName(); | |
itemNames.add(srcName); | |
if (srcName == win.ndiSourceName){ | |
selIndex = i+1; | |
} | |
} | |
} | |
if (win.ndiSourceName != "" && !itemNames.contains(win.ndiSourceName)){ | |
//if (selIndex == -2 && win.ndiSourceName != ""){ | |
selIndex = itemNames.size(); | |
itemNames.add(win.ndiSourceName); | |
} | |
sourceDropdown.setItems(itemNames); | |
if (selIndex >= 0){ | |
sourceDropdown.setValue(selIndex); | |
sourceDropdown.getCaptionLabel().setText(itemNames.get(selIndex)); | |
if (selIndex >= 1){ | |
System.out.println(String.format("%s selIndex=%d, value=%d", winId, selIndex, (int)sourceDropdown.getValue())); | |
} | |
} | |
} | |
void buildSourceDropdown(String name, Point pos){ | |
sourceDropdown = mvApp.cp5.addDropdownList(name) | |
.setOpen(false) | |
.plugTo(this, "onSourceDropdown"); | |
setWidgetPos(sourceDropdown, pos); | |
updateDropdownItems(); | |
} | |
void buildEditNameControls(String btnId, Box btnBox, String txtFieldId, Box txtBox){ | |
editNameBtn = mvApp.cp5.addButton(btnId) | |
.setLabel("Edit Name") | |
.setValue(1) | |
.setSwitch(true) | |
.plugTo(this, "onEditNameBtn"); | |
setWidgetBox(editNameBtn, btnBox); | |
editNameField = mvApp.cp5.addTextfield(txtFieldId) | |
.setText(win.name) | |
.setVisible(false) | |
.setAutoClear(false) | |
.plugTo(this, "onEditNameField"); | |
setWidgetBox(editNameField, txtBox); | |
} | |
void onSourceDropdown(int idx){ | |
if (sourceDropdown.isOpen()){ | |
Map<String,Object> item = sourceDropdown.getItem(idx); | |
String srcName = (String)item.get("name"); | |
if (srcName == "None"){ | |
srcName = ""; | |
} | |
System.out.println(String.format("idx=%d, srcName=%s", idx, srcName)); | |
win.setSourceName(srcName, false); | |
sourceDropdown.close(); | |
} | |
} | |
void onEditNameBtn(boolean btnOn){ | |
if (editNameEnabled != btnOn){ | |
editNameEnabled = btnOn; | |
System.out.println(String.format("editNameEnabled = %s", editNameEnabled)); | |
editNameField.setVisible(editNameEnabled); | |
if (editNameEnabled){ | |
editNameField.setFocus(true); | |
} | |
} | |
} | |
void onEditNameField(String txtValue){ | |
if (editNameField.isVisible() && editNameEnabled){ | |
win.setName(txtValue, false); | |
editNameField.setFocus(false); | |
editNameEnabled = false; | |
editNameBtn.setOff(); | |
editNameField.setVisible(false); | |
} | |
} | |
Box getWidgetBox(Controller widget){ | |
float[] xy; | |
float w, h; | |
xy = widget.getPosition(); | |
w = widget.getWidth(); | |
h = widget.getHeight(); | |
return new Box(xy[0], xy[1], w, h); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class WindowGrid { | |
int cols, rows, outWidth, outHeight; | |
Point outputSize; | |
Point padding; | |
Box boundingBox; | |
HashMap<String,Window> windowMap; | |
Window[][] windows; | |
TextBox fpsText; | |
//HashMap<String,FrameThread> updateThreads; | |
WindowGrid(int _cols, int _rows, int _outWidth, int _outHeight) { | |
cols = _cols; | |
rows = _rows; | |
padding = new Point(2, 2); | |
outWidth = _outWidth; | |
outHeight = _outHeight; | |
outputSize = new Point(outWidth, outHeight); | |
buildDefaultWindows(); | |
init(); | |
} | |
WindowGrid(JSONObject json, int _outWidth, int _outHeight){ | |
cols = json.getInt("cols"); | |
rows = json.getInt("rows"); | |
padding = new Point(json.getJSONObject("padding")); | |
outWidth = _outWidth; | |
outHeight = _outHeight; | |
outputSize = new Point(outWidth, outHeight); | |
init(); | |
JSONArray winJson = json.getJSONArray("windows"); | |
for (int i=0; i<winJson.size(); i++){ | |
addWindow(winJson.getJSONObject(i)); | |
} | |
} | |
WindowGrid(GridConfig config){ | |
cols = config.cols; | |
rows = config.rows; | |
padding = config.padding.copy(); | |
outputSize = config.outputSize.copy(); | |
outWidth = (int)outputSize.x; | |
outHeight = (int)outputSize.y; | |
init(); | |
for (int i=0; i<config.windows.length; i++){ | |
addWindow(config.windows[i]); | |
} | |
} | |
private void buildDefaultWindows(){ | |
int i = 0; | |
for (int x=0; x < cols; x++){ | |
for (int y=0; y < rows; y++){ | |
addWindow(String.format("%d", i), x, y, ""); | |
} | |
} | |
} | |
private void init(){ | |
boundingBox = new Box(0, 0, outWidth, outHeight); | |
fpsText = new TextBox(boundingBox.getPos(), 100, 20); | |
fpsText.bgColor = 0xff404040; | |
fpsText.setBottomCenter(boundingBox.getBottomCenter()); | |
fpsText.setAlign(CENTER, CENTER); | |
//fpsText.setTextPos(fpsText.getCenter()); | |
//fpsText.setTextPos(fpsText.getHCenter(), fpsText.getY() + 10); | |
System.out.println("windowGrid bBox: "+boundingBox.toStr()); | |
windowMap = new HashMap<String,Window>(); | |
//updateThreads = new HashMap<String,FrameThread>(); | |
windows = new Window[cols][rows]; | |
} | |
JSONObject serialize(){ | |
JSONObject json = new JSONObject(); | |
json.setInt("cols", cols); | |
json.setInt("rows", rows); | |
json.setJSONObject("padding", padding.serialize()); | |
json.setJSONObject("outputSize", outputSize.serialize()); | |
JSONArray winJson = new JSONArray(); | |
for (Window win : windowMap.values()){ | |
winJson.append(win.serialize()); | |
} | |
json.setJSONArray("windows", winJson); | |
return json; | |
} | |
void updateNdiSources(){ | |
for (Window win : windowMap.values()){ | |
win.updateNdiSources(); | |
} | |
} | |
void close(){ | |
System.out.println("closing windows..."); | |
for (Window win : windowMap.values()){ | |
win.close(); | |
} | |
System.out.println("windows closed"); | |
} | |
Box calcBox(int col, int row){ | |
//if (outWidth == 0 || outHeight == 0){ | |
// return new Box(0, 0, 0, 0); | |
//} | |
float w = outWidth / rows; | |
float h = outHeight / cols; | |
return new Box(w * row, h * col, w, h); | |
} | |
Window addWindow(String name, int col, int row, String ndiSourceName){ | |
String winId = String.format("%02d-%02d", col, row); | |
assert !windowMap.containsKey(winId); | |
Box winBox = calcBox(col, row); | |
Window win = new Window(name, col, row, winBox, padding, ndiSourceName); | |
windows[col][row] = win; | |
windowMap.put(win.getId(), win); | |
return win; | |
} | |
Window addWindow(JSONObject json){ | |
int col = json.getInt("col"), row = json.getInt("row"); | |
String winId = String.format("%02d-%02d", col, row); | |
assert !windowMap.containsKey(winId); | |
Box winBox = calcBox(col, row); | |
Window win = new Window(json, winBox, padding); | |
windows[col][row] = win; | |
windowMap.put(win.getId(), win); | |
return win; | |
} | |
Window addWindow(WindowConfig config){ | |
int col = config.col, row = config.row; | |
String winId = String.format("%02d-%02d", col, row); | |
assert !windowMap.containsKey(winId); | |
Box winBox = calcBox(col, row); | |
Window win = new Window(config, winBox, padding); | |
windows[col][row] = win; | |
windowMap.put(win.getId(), win); | |
return win; | |
} | |
void setOutputSize(int w, int h){ | |
outWidth = w; | |
outHeight = h; | |
outputSize.x = w; | |
outputSize.y = h; | |
boundingBox = new Box(0, 0, outWidth, outHeight); | |
System.out.println("windowGrid bBox: "+boundingBox.toStr()); | |
fpsText.setBottomCenter(boundingBox.getBottomCenter()); | |
//padding.x = Math.round(w / 200.0); | |
//padding.y = Math.round(h / 200.0); | |
for (Window win : windowMap.values()){ | |
win.setBoundingBox(calcBox(win.col, win.row)); | |
} | |
} | |
void setOutputSize(Point s){ | |
setOutputSize((int)s.x, (int)s.y); | |
} | |
void render(PGraphics canvas){ | |
try { | |
synchronized(mvApp.ndiSourceLock){ | |
for (Window win : windowMap.values()){ | |
if (!win.isConnected()){ | |
if (win.canConnect()){ | |
win.connectToSource(); | |
} | |
} | |
} | |
} | |
for (Window win : windowMap.values()){ | |
win.render(canvas); | |
} | |
fpsText.text = String.format("%dfps", (int)mvApp.frameRate); | |
fpsText.render(canvas); | |
} catch(Exception e){ | |
close(); | |
e.printStackTrace(); | |
throw(e); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment