Skip to content

Instantly share code, notes, and snippets.

@nocarryr
Last active October 1, 2022 19:59
Show Gist options
  • Save nocarryr/4729d36743269e84d137493d1c9294ae to your computer and use it in GitHub Desktop.
Save nocarryr/4729d36743269e84d137493d1c9294ae to your computer and use it in GitHub Desktop.
NDI Multiviewer
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);
}
}
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");
}
}
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());
}
}
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;
}
}
//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;
}
}
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");
}
//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);
}
}
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);
}
}
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