1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-11-29 19:28:47 -05:00
sdrangel/plugins/channelrx/demoddatv/leansdr/gui.h

754 lines
21 KiB
C++

// Copyright (C) 2018-2019, 2021 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// This file is part of LeanSDR Copyright (C) 2016-2018 <pabr@pabr.org>.
// See the toplevel README for more information.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#ifndef LEANSDR_GUI_H
#define LEANSDR_GUI_H
#include <sys/time.h>
#include "framework.h"
namespace leansdr
{
//////////////////////////////////////////////////////////////////////
// GUI blocks
//////////////////////////////////////////////////////////////////////
#ifdef GUI
#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
static const int DEFAULT_GUI_DECIMATION = 64;
struct gfx
{
Display *display;
int screen;
int w, h;
Window window;
GC gc;
Pixmap dbuf;
gfx(scheduler *sch, const char *name)
{
window_placement *wp;
for (wp = sch->windows; wp && wp->name; ++wp)
if (!strcmp(wp->name, name))
break;
if (wp && wp->name)
init(wp->name, wp->x, wp->y, wp->w, wp->h);
else
{
fprintf(stderr, "No placement hints for window '%s'\n", name);
init(name, -1, -1, 320, 240);
}
}
gfx(const char *name, int _x, int _y, int _w, int _h)
{
init(name, _x, _y, _w, _h);
}
void init(const char *name, int _x, int _y, int _w, int _h)
{
buttons = 0;
clicks = 0;
mmoved = false;
w = _w;
h = _h;
display = XOpenDisplay(getenv("DISPLAY"));
if (!display)
fatal("display");
screen = DefaultScreen(display);
XSetWindowAttributes xswa;
xswa.event_mask = (ExposureMask |
StructureNotifyMask |
ButtonPressMask |
ButtonReleaseMask |
KeyPressMask |
KeyReleaseMask |
PointerMotionMask);
xswa.background_pixel = BlackPixel(display, screen);
window = XCreateWindow(display, DefaultRootWindow(display),
100, 100, w, h, 10, CopyFromParent, InputOutput,
CopyFromParent, CWEventMask | CWBackPixel,
&xswa);
if (!window)
fatal("window");
XStoreName(display, window, name);
XMapWindow(display, window);
if (_x >= 0 && _y >= 0)
XMoveWindow(display, window, _x, _y);
dbuf = XCreatePixmap(display, window, w, h, DefaultDepth(display, screen));
gc = XCreateGC(display, dbuf, 0, NULL);
if (!gc)
fatal("gc");
}
void clear()
{
setfg(0, 0, 0);
XFillRectangle(display, dbuf, gc, 0, 0, w, h);
}
void show()
{
XCopyArea(display, dbuf, window, gc, 0, 0, w, h, 0, 0);
}
void sync()
{
XSync(display, False);
}
void events()
{
XEvent ev;
while (XCheckWindowEvent(display, window, -1, &ev))
{
switch (ev.type)
{
case ButtonPress:
{
int b = ev.xbutton.button;
buttons |= 1 << b;
clicks |= 1 << b;
mx = ev.xbutton.x;
my = ev.xbutton.y;
break;
}
case ButtonRelease:
{
int b = ev.xbutton.button;
buttons &= ~(1 << b);
mx = ev.xbutton.x;
my = ev.xbutton.y;
break;
}
case MotionNotify:
mx = ev.xbutton.x;
my = ev.xbutton.y;
mmoved = true;
break;
}
}
}
void setfg(unsigned char r, unsigned char g, unsigned char b)
{
XColor c;
c.red = r << 8;
c.green = g << 8;
c.blue = b << 8;
c.flags = DoRed | DoGreen | DoBlue;
if (!XAllocColor(display, DefaultColormap(display, screen), &c))
fatal("color");
XSetForeground(display, gc, c.pixel);
}
void point(int x, int y)
{
XDrawPoint(display, dbuf, gc, x, y);
}
void line(int x0, int y0, int x1, int y1)
{
XDrawLine(display, dbuf, gc, x0, y0, x1, y1);
}
void text(int x, int y, const char *s)
{
XDrawString(display, dbuf, gc, x, y, s, strlen(s));
}
void transient_text(int x, int y, const char *s)
{
XDrawString(display, window, gc, x, y, s, strlen(s));
}
int buttons; // Mask of button states (2|4|8)
int clicks; // Same, accumulated (must be cleared by owner)
int mx, my; // Cursor position
bool mmoved; // Pointer moved (must be cleared by owner)
};
template <typename T>
struct cscope : runnable
{
T xymin, xymax;
unsigned long decimation;
unsigned long pixels_per_frame;
cstln_base **cstln; // Optional ptr to optional constellation
cscope(scheduler *sch, pipebuf<std::complex<T>> &_in, T _xymin, T _xymax,
const char *_name = NULL)
: runnable(sch, _name ? _name : _in.name),
xymin(_xymin), xymax(_xymax),
decimation(DEFAULT_GUI_DECIMATION), pixels_per_frame(1024),
cstln(NULL),
in(_in), phase(0), g(sch, name)
{
}
void run()
{
while (in.readable() >= pixels_per_frame)
{
if (!phase)
{
draw_begin();
g.setfg(0, 255, 0);
std::complex<T> *p = in.rd(), *pend = p + pixels_per_frame;
for (; p < pend; ++p)
g.point(g.w * (p->re - xymin) / (xymax - xymin),
g.h - g.h * (p->im - xymin) / (xymax - xymin));
if (cstln && (*cstln))
{
// Plot constellation points
g.setfg(255, 255, 255);
for (int i = 0; i < (*cstln)->nsymbols; ++i)
{
std::complex<signed char> *p = &(*cstln)->symbols[i];
int x = g.w * (p->re - xymin) / (xymax - xymin);
int y = g.h - g.h * (p->im - xymin) / (xymax - xymin);
for (int d = -2; d <= 2; ++d)
{
g.point(x + d, y);
g.point(x, y + d);
}
}
}
g.show();
g.sync();
}
in.read(pixels_per_frame);
if (++phase >= decimation)
phase = 0;
}
}
//private:
pipereader<std::complex<T>> in;
unsigned long phase;
gfx g;
void draw_begin()
{
g.clear();
g.setfg(0, 255, 0);
g.line(g.w / 2, 0, g.w / 2, g.h);
g.line(0, g.h / 2, g.w, g.h / 2);
}
};
template <typename T>
struct wavescope : runnable
{
T ymin, ymax;
unsigned long decimation;
float hgrid;
wavescope(scheduler *sch, pipebuf<T> &_in,
T _ymin, T _ymax, const char *_name = NULL)
: runnable(sch, _name ? _name : _in.name),
ymin(_ymin), ymax(_ymax),
decimation(DEFAULT_GUI_DECIMATION), hgrid(0),
in(_in),
phase(0), g(sch, name), x(0)
{
g.clear();
}
void run()
{
while (in.readable() >= g.w)
{
if (!phase)
plot(in.rd(), g.w);
in.read(g.w);
if (++phase >= decimation)
phase = 0;
}
}
void plot(T *p, int count)
{
T *pend = p + count;
g.clear();
g.setfg(128, 128, 0);
g.line(0, g.h / 2, g.w - 1, g.h / 2);
if (hgrid)
{
for (float x = 0; x < g.w; x += hgrid)
g.line(x, 0, x, g.h - 1);
}
g.setfg(0, 255, 0);
for (int x = 0; p < pend; ++x, ++p)
{
T v = *p;
g.point(x, g.h - 1 - (g.h - 1) * (v - ymin) / (ymax - ymin));
}
g.show();
g.sync();
}
private:
pipereader<T> in;
int phase;
gfx g;
int x;
};
template <typename T>
struct slowmultiscope : runnable
{
struct chanspec
{
pipebuf<T> *in;
const char *name, *format;
unsigned char rgb[3];
float scale;
float ymin, ymax;
enum flag
{
DEFAULT = 0,
ASYNC = 1, // Read whatever is available
COUNT = 2, // Display number of items read instead of value
SUM = 4, // Display sum of values
LINE = 8, // Connect points
WRAP = 16, // Modulo min..max
DISABLED = 32, // Ignore channel
} flags;
};
unsigned long samples_per_pixel;
float sample_freq; // Sample rate in Hz (used for cursor operations)
slowmultiscope(scheduler *sch, const chanspec *specs, int nspecs,
const char *_name)
: runnable(sch, _name ? _name : "slowmultiscope"),
samples_per_pixel(1), sample_freq(1),
g(sch, name), t(0), x(0), total_samples(0)
{
chans = new channel[nspecs];
nchans = 0;
for (int i = 0; i < nspecs; ++i)
{
if (specs[i].flags & chanspec::DISABLED)
continue;
chans[nchans].spec = specs[i];
chans[nchans].in = new pipereader<T>(*specs[i].in);
chans[nchans].accum = 0;
++nchans;
}
g.clear();
}
void run()
{
// Asynchronous channels: Read all available data
for (channel *c = chans; c < chans + nchans; ++c)
{
if (c->spec.flags & chanspec::ASYNC)
run_channel(c, c->in->readable());
}
// Synchronous channels: Read up to one pixel worth of data
// between display syncs.
unsigned long count = samples_per_pixel;
for (channel *c = chans; c < chans + nchans; ++c)
if (!(c->spec.flags & chanspec::ASYNC))
count = min(count, c->in->readable());
for (int n = count; n--;)
{
for (channel *c = chans; c < chans + nchans; ++c)
if (!(c->spec.flags & chanspec::ASYNC))
run_channel(c, 1);
}
t += count;
g.show();
// Print instantatenous values as text
for (int i = 0; i < nchans; ++i)
{
channel *c = &chans[i];
g.setfg(c->spec.rgb[0], c->spec.rgb[1], c->spec.rgb[2]);
char text[256];
sprintf(text, c->spec.format, c->print_val);
g.transient_text(5, 20 + 16 * i, text);
}
run_gui();
if (t >= samples_per_pixel)
{
t = 0;
++x;
if (x >= g.w)
x = 0;
g.setfg(0, 0, 0);
g.line(x, 0, x, g.h - 1);
}
run_gui();
g.sync();
total_samples += count;
}
private:
int nchans;
struct channel
{
chanspec spec;
pipereader<T> *in;
float accum;
int prev_y;
float print_val;
} * chans;
void run_channel(channel *c, int nr)
{
g.setfg(c->spec.rgb[0], c->spec.rgb[1], c->spec.rgb[2]);
int y = -1;
while (nr--)
{
float v = *c->in->rd() * c->spec.scale;
if (c->spec.flags & chanspec::COUNT)
++c->accum;
else if (c->spec.flags & chanspec::SUM)
c->accum += v;
else
{
c->print_val = v;
float nv = (v - c->spec.ymin) / (c->spec.ymax - c->spec.ymin);
if (c->spec.flags & chanspec::WRAP)
nv = nv - floor(nv);
y = g.h - g.h * nv;
}
c->in->read(1);
}
// Display count/sum channels only when the cursor is about to move.
if ((c->spec.flags & (chanspec::COUNT | chanspec::SUM)) &&
t + 1 >= samples_per_pixel)
{
T v = c->accum;
y = g.h - 1 - g.h * (v - c->spec.ymin) / (c->spec.ymax - c->spec.ymin);
c->accum = 0;
c->print_val = v;
}
if (y >= 0)
{
if (c->spec.flags & chanspec::LINE)
{
if (x)
g.line(x - 1, c->prev_y, x, y);
c->prev_y = y;
}
else
g.point(x, y);
}
}
void run_gui()
{
g.events();
// Print cursor time
float ct = g.mx * samples_per_pixel / sample_freq;
float tt = total_samples / sample_freq;
char text[256];
sprintf(text, "%.3f / %.3f s", ct, tt);
g.setfg(255, 255, 255);
g.transient_text(g.w * 3 / 4, 20, text);
}
gfx g;
unsigned long t; // Time for synchronous channels
int x;
int total_samples;
};
template <typename T>
struct spectrumscope : runnable
{
unsigned long size;
float amax;
unsigned long decimation;
spectrumscope(scheduler *sch, pipebuf<std::complex<T>> &_in,
T _max, const char *_name = NULL)
: runnable(sch, _name ? _name : _in.name),
size(4096), amax(_max / sqrtf(size)),
decimation(DEFAULT_GUI_DECIMATION),
in(_in),
nmarkers(0),
phase(0), g(sch, name), fft(NULL)
{
}
void mark_freq(float f)
{
if (nmarkers == MAX_MARKERS)
fail("Too many markers");
markers[nmarkers++] = f;
}
void run()
{
while (in.readable() >= size)
{
if (!phase)
do_fft(in.rd());
in.read(size);
if (++phase >= decimation)
phase = 0;
}
}
private:
pipereader<std::complex<T>> in;
static const int MAX_MARKERS = 4;
float markers[MAX_MARKERS];
int nmarkers;
int phase;
gfx g;
cfft_engine<float> *fft;
void do_fft(std::complex<T> *input)
{
draw_begin();
if (!fft || fft->n != size)
{
if (fft)
delete fft;
fft = new cfft_engine<float>(size);
}
std::complex<T> *pin = input, *pend = pin + size;
std::complex<float> data[size], *pout = data;
g.setfg(255, 0, 0);
for (int x = 0; pin < pend; ++pin, ++pout, ++x)
{
pout->re = (float)pin->re;
pout->im = (float)pin->im;
// g.point(x, g.h/2-pout->re*g.h/2/ymax);
}
fft->inplace(data, true);
g.setfg(0, 255, 0);
for (int i = 0; i < size; ++i)
{
int x = ((i < size / 2) ? i + size / 2 : i - size / 2) * g.w / size;
std::complex<float> v = data[i];
;
float y = hypot(v.real(), v.imag());
g.line(x, g.h - 1, x, g.h - 1 - y * g.h / amax);
}
if (g.buttons)
{
char s[256];
float f = 2.4e6 * (g.mx - g.w / 2) / g.w;
sprintf(s, "%f", f);
g.text(16, 16, s);
}
g.show();
g.sync();
}
void draw_begin()
{
g.clear();
g.setfg(255, 255, 255);
g.line(g.w / 2, 0, g.w / 2, g.h);
g.setfg(255, 0, 0);
for (int i = 0; i < nmarkers; ++i)
{
int x = g.w * (0.5 + markers[i]);
g.line(x, 0, x, g.h);
}
}
};
template <typename T>
struct rfscope : runnable
{
unsigned long size;
unsigned long decimation;
float Fs; // Sampling freq for display (Hz)
float Fc; // Center freq for display (Hz)
int ncursors;
float *cursors; // Cursor frequencies
float hzoom; // Horizontal zoom factor
float db0, dbrange; // Vertical range db0 .. db0+dbrange
float bw; // Smoothing bandwidth
rfscope(scheduler *sch, pipebuf<std::complex<T>> &_in,
const char *_name = NULL)
: runnable(sch, _name ? _name : _in.name),
size(4096), decimation(DEFAULT_GUI_DECIMATION),
Fs(1), Fc(0), ncursors(0), hzoom(1),
db0(-25), dbrange(50), bw(0.05),
in(_in), phase(0), g(sch, name), fft(NULL), filtered(NULL)
{
}
void run()
{
while (in.readable() >= size)
{
if (!phase)
do_fft(in.rd());
in.read(size);
if (++phase >= decimation)
phase = 0;
}
}
private:
pipereader<std::complex<T>> in;
int phase;
gfx g;
cfft_engine<float> *fft;
float *filtered;
void do_fft(std::complex<T> *input)
{
g.events();
draw_begin();
if (!fft || fft->n != size)
{
if (fft)
delete fft;
fft = new cfft_engine<float>(size);
}
// Convert to std::complex<float> and transform
std::complex<T> *pin = input, *pend = pin + size;
std::complex<float> data[size], *pout = data;
for (int x = 0; pin < pend; ++pin, ++pout, ++x)
{
pout->re = (float)pin->re;
pout->im = (float)pin->im;
}
fft->inplace(data, true);
float amp2[size];
for (int i = 0; i < size; ++i)
{
std::complex<float> &v = data[i];
;
amp2[i] = (v.real() * v.real() + v.imag() * v.imag()) * size;
}
if (!filtered)
{
filtered = new float[size];
for (int i = 0; i < size; ++i)
filtered[i] = amp2[i];
}
float bwcomp = 1 - bw;
g.setfg(0, 255, 0);
for (int i = 0; i < size; ++i)
{
filtered[i] = amp2[i] * bw + filtered[i] * bwcomp;
float db = filtered[i] ? 10 * logf(filtered[i]) / logf(10) : db0;
int is = (i < size / 2) ? i : i - size;
int x = g.w / 2 + is * hzoom * g.w / size;
int y = g.h - 1 - (db - db0) * g.h / dbrange;
g.line(x, g.h - 1, x, y);
}
if (g.buttons)
{
char s[256];
float freq = Fc + Fs * (g.mx - g.w / 2) / g.w / hzoom;
float val = db0 + (float)((g.h - 1) - g.my) * dbrange / g.h;
sprintf(s, "%f.3 Hz %f.2 dB", freq, val);
g.setfg(255, 255, 255);
g.text(16, 16, s);
}
// Draw cursors
g.setfg(255, 255, 0);
for (int i = 0; i < ncursors; ++i)
{
int x = g.w / 2 + (cursors[i] - Fc) * hzoom * g.w / Fs;
g.line(x, 0, x, g.h - 1);
}
g.show();
g.sync();
}
void draw_begin()
{
g.clear();
// dB scale
g.setfg(64, 64, 64);
for (float db = floorf(db0); db < db0 + dbrange; ++db)
{
int y = g.h - 1 - (db - db0) * g.h / dbrange;
g.line(0, y, g.w - 1, y);
}
// DC line
g.setfg(255, 255, 255);
g.line(g.w / 2, 0, g.w / 2, g.h);
}
};
template <typename T>
struct genscope : runnable
{
struct render
{
int x, y;
char dir; // 'h'orizontal or 'v'ertical
};
struct chanspec
{
pipebuf<T> *in; // NULL if disabled
render r;
};
genscope(scheduler *sch, chanspec *specs, int _nchans,
const char *_name = NULL)
: runnable(sch, _name ? _name : "genscope"),
nchans(_nchans),
g(sch, name)
{
chans = new channel[nchans];
for (int i = 0; i < nchans; ++i)
{
if (!specs[i].in)
{
chans[i].in = NULL;
}
else
{
chans[i].spec = specs[i];
chans[i].in = new pipereader<T>(*specs[i].in);
}
}
g.clear();
gettimeofday(&tv, NULL);
}
struct channel
{
chanspec spec;
pipereader<T> *in;
} * chans;
int nchans;
struct timeval tv;
void run()
{
g.setfg(0, 255, 0);
for (channel *pc = chans; pc < chans + nchans; ++pc)
{
if (!pc->in)
continue;
int n = pc->in->readable();
T last = pc->in->rd()[n - 1];
pc->in->read(n);
int dx = pc->spec.r.dir == 'h' ? last : 0;
int dy = pc->spec.r.dir == 'v' ? last : 0;
dx /= 3;
dy /= 3;
g.line(pc->spec.r.x - dx, pc->spec.r.y - dy,
pc->spec.r.x + dx, pc->spec.r.y + dy);
char txt[16];
sprintf(txt, "%d", (int)last);
g.text(pc->spec.r.x + 5, pc->spec.r.y - 2, txt);
}
struct timeval newtv;
gettimeofday(&newtv, NULL);
int dt = (newtv.tv_sec - tv.tv_sec) * 1000 + (newtv.tv_usec - tv.tv_usec) / 1000;
if (dt > 100)
{
fprintf(stderr, "#");
g.show();
g.sync();
g.clear();
tv = newtv;
}
}
private:
gfx g;
};
#endif // GUI
} // namespace leansdr
#endif // LEANSDR_GUI_H