diff options
Diffstat (limited to 'src/lockin.cxx')
-rw-r--r-- | src/lockin.cxx | 748 |
1 files changed, 748 insertions, 0 deletions
diff --git a/src/lockin.cxx b/src/lockin.cxx new file mode 100644 index 0000000..09f6afd --- /dev/null +++ b/src/lockin.cxx @@ -0,0 +1,748 @@ +// https://doc.qt.io/qt-6/qtexamplesandtutorials.html +// especially: +// https://doc.qt.io/qt-6/qtcharts-qmloscilloscope-example.html +// code: +// https://code.qt.io/qt/qtcharts.git +// https://code.qt.io/cgit/qt/qtcharts.git/tree/examples/charts/qmloscilloscope?h=6.6 +// efficiency: +// https://stackoverflow.com/questions/54993973/efficient-curve-plotting-using-qwtplotcurve + +/////////////////////////////////////////////////////////////////// +// software lockin +// i.e. software lock-in amplifier +// i.e. software synchronous detector +// i.e. software synchronous analyzer +// i.e. software synchronous wave analyzer + +using namespace std; +#include <iostream> +#include <iomanip> +#include <stdlib.h> +#include <stdio.h> +#include "Getopt.h" +#include <unistd.h> +#include <signal.h> +#include <fcntl.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/ioctl.h> +#include <sys/wait.h> +#include <asm/types.h> +#include <time.h> +#include <sys/time.h> +#include <errno.h> +#include <string> +#include <vector> +#include <valarray> + +// On debian, you need to install the development package, +// i.e. libasound2-dev (not just libasound2) to provide +// alsa/asoundlib.h and the associated runtime libraries: +#include <alsa/asoundlib.h> + +#include <qwt_plot.h> +#include <qwt_plot_curve.h> +#include <qwt_plot_grid.h> + +#include "lockin.h" + +#include "gui.h" +#include "gui_class.h" +#include "refout.h" +#include "lockin.h" +#include "krunch.h" + +#ifdef FOOBAR +#include "alsactl.h" +#else /* backwards compatibility */ +//# warning You may want to use ./excl.patch to implement ALSA exclusive access. +# define SND_CTL_RDONLY 0 +#endif + +// a few non-constant global variables: +int verbosity=0; +int xrun_verbosity(1); // be verbose about underrun and overrun +string progname; +timespec prog_start_time; + +// Forward references: + +void config_mixer(const string ctlfile, const string justcard); +void discard(int signum); +void trap_exit(int signum); +void trap_detach(int signum); +void drain(const char* draindev); +void drain(const int fd); +int isfile(const char* fname); + +// Functions + +string fixup_ctlfile(string ctlfile, const string justcard, const int mode=0); + +void snark::usage(const int err) { + (err ? cerr : cout) << +"Harvest entropy from the audio I/O system.\n" +"Usage: " << progname << " [options]\n\n" +"Options:\n" +"--buffer-size, -b [] Number of frames to read at a time. [= " + << nframe << "].\n" +"--card, -c [] Sound card, subdevice to use. [= " + << card_device << "].\n" +" -c -1 List available capture cards.\n" +" -c -2 List available playback cards.\n" +"--mixer-ctl, -m [] Mixer set-up from alsactl file. " + "[= " << fixup_ctlfile(mixer_ctlfile, justcard) << "];\n" +" -m \"\" ==> don't set up; just use inherited settings.\n" +"--Amplitude, -A [] RefOut amplitude / dBV " + "[= " << refOutAmp_dB << "]\n" +"--Channel-mask -C [] 1==>left 2==>right 3==> stereo etc.\n" +"--Frequency, -F [] RefOut frequency / Hz (useful= 440).\n" +"--Delta-f, -D [] RefOut 2nd chan freq relative to 1st" + " [= " << refival << "].\n" +"--Vo-cal, -O [] Output voltage calibration / dBV full scale [0]\n" +"--Vi-cal, -I [] Input voltage calibration / dBV full scale [0]\n" +" (-45 dbV FS is plausible for mic input)\n" +"--Time-shift -T [] Advance reference by ... sec [0].\n" +"--Phase-shift, -P [] Advance reference by ... degrees [0].\n" +"--frame-rate, -N [] Audio measurement rate, frames per second;\n" +" -N 0 ==> default ==> max rate supported by the hardware.\n" +" -N -1 ==> probe for capture capabilities.\n" +" -N -2 ==> probe for output capabilities.\n" +"--Zref, -Z [] External reference resistor / Ohms " + "[= " << zref << "]\n" +"--verbose, -v Print extra debugging info (-vv ==> even more).\n" +"--write-check, -w [] Write checkfile containing one buffer of audio.\n" +"--1pass, -1 Exit after one pass through main loop.\n" +<< endl; +} + +// mode 0 is normal +string fixup_ctlfile(string ctlfile, const string justcard, const int mode){ + if (ctlfile == "<>") { + ctlfile = mode==0 ? invent_ctlfile(justcard) + : invent_driverfile(justcard); + } + // else cmdline has explict ctlfile, use that + +// prepend explicit path, if needed: + + if (ctlfile.length()) { + char mx = ctlfile[0]; + if (mx != '.' && mx != '/') { + ctlfile = "/etc/" + progname + "/" + ctlfile; + const char* homish = getenv("HOME"); + if (!homish) homish = "/root"; + string home(homish); + if (home != "/root") { + ctlfile = home + ctlfile; + } + } // else . or / means use it verbatim + } + return ctlfile; +} + +// Just like snd_pcm_writei(), +// except the buff is always of the type ofdatum (int32_t), +// no matter what type the raw hardware uses. +// Return value: +// nonnegative: # of frames written +// negative: error code +int writei(const alsa_pcm* pcm, const datum* buff, const int nframe){ + if (pcm->format == SND_PCM_FORMAT_S32_LE) { + return snd_pcm_writei(pcm->phndl, buff, nframe); + } else if (pcm->format == SND_PCM_FORMAT_S16_LE) { + valarray<int16_t> tmp(nframe * pcm->nchan); + int16_t* to (&tmp[0]); + const datum* from (buff); + const int fudge(1<<16); + for (unsigned int ii = 0; ii < nframe * pcm->nchan; ii++){ + *to++ = *from++ / fudge; + } + return snd_pcm_writei(pcm->phndl, &tmp[0], nframe); + } else if (pcm->format == SND_PCM_FORMAT_S8) { + valarray<char> tmp(nframe * pcm->nchan); + char* to (&tmp[0]); + const datum* from (buff); + const int fudge(1<<24); + for (unsigned int ii = 0; ii < nframe * pcm->nchan; ii++){ + *to++ = *from++ / fudge; + } + return snd_pcm_writei(pcm->phndl, &tmp[0], nframe); + } else { + cerr << "Don't know how to convert pcm data to format " + << snd_pcm_format_name(pcm->format) << endl; + exeunt(1); + } + return 0; // defeat stupid compiler warning +} + +// Just like the library routine of the same name, except: +// 1) The first arg is a "keeper", +// which means that we will accept writes that are +// not an integer multiple of the period. +// 2) The second arg is a datum* (not void*). +// We call writei() to convert things to the sample-size +// of the actual device. +snd_pcm_sframes_t snd_pcm_writei(Keeper& kk, const datum* buffer, + snd_pcm_uframes_t nframes){ + int rslt; + int todo(nframes); + int head(0); // frames taken from head of buffer + const datum* mybuf(buffer); + datum* start = &kk.buf[0]; + if (kk.finbuf) { + head = kk.fpp - kk.finbuf; + memcpy(start + kk.nchan*kk.finbuf, buffer, kk.nchan*sizeof(datum)*head); + rslt = writei(kk.pcm, start, kk.fpp); + if (rslt <= 0) return rslt; // no change in kk + if (rslt != kk.fpp) return -98; + todo -= head; + kk.finbuf = 0; + } + mybuf += kk.nchan*head; + rslt = writei(kk.pcm, mybuf, todo); + if (rslt < 0) return rslt; // some error, let caller deal with it + if (rslt == 0) return head; // head got written, nothing more +// OK, at this point rslt must be positive. + int left(todo - rslt); // number of frames left to do.... + if (!left) return nframes; // Nothing? Great! + if (left < kk.fpp) { // can we keep it for next time? + kk.finbuf = left; + memcpy(start, mybuf + kk.nchan*rslt, kk.nchan*sizeof(datum)*left); + return nframes; + } +// here with a short write that doesn't fit in our keep-buffer: + kk.finbuf = 0; + return head + rslt; +} + +// Constructor +Keeper::Keeper(const alsa_pcm* _dsp, const int _fpp) + : pcm(_dsp), // remember the args + fpp(_fpp), + buf(pcm->nchan * _fpp), // allocate the buffer + finbuf(0), + nchan(_dsp->nchan) +{} // no code, just initializers (above) + + +// Constructor +snark::snark() : + card_device("PCH"), + wcheck_fname(""), + nframe(2048), + onepass(0), + desired_rate(0), + channel_mask(-1), + kout(0), + zref(1e6), // assume reference = 1 megohm + reffreq(440.), // default : concert A440 + refival(440.5/440.), + mixer_ctlfile(""), + wcheck_fd(-1), + sps(0), + snooze(.5) +{} // no code, just the initializers (above) + + +string crtRed ("\033[1;31m"); +string crtNormal("\033[0;39m"); + +vector<int> clean_me; + +void trapme(){ + signal(SIGHUP, trap_exit); // hangup + signal(SIGINT, trap_exit); // interrupt + signal(SIGTERM, trap_exit); // terminate + signal(SIGPIPE, trap_exit); // broken pipe + signal(SIGCHLD, SIG_DFL); +} + + +double rms(const int* ptr, const int nn){ + if (!nn) return 0; + double val; + double sum(0); + for (int ii = 0; ii < nn; ii++){ + val = ptr[ii]; + sum += val*val; + } + sum /= nn; + return sqrt(sum); +} + +int main(int argc, char** argv) { + +// first, initialize the gui, so it can calculate +// some things (e.g. actOutFreq) that other tasks +// (e.g. refout) will need. + int xargc(1); + char tmpfoo[5]; + strncpy(tmpfoo, "foo", sizeof tmpfoo); + char * xargv[xargc] = {tmpfoo}; + QApplication app(xargc, xargv); + app.setStyle("plastique"); + + progname = argv[0]; + string::size_type where = progname.rfind('/'); // find final slash + if (where != progname.npos) { + progname = progname.substr(1+where); + } + snark foo; // holder for cmdline options ... + foo.cmdline(argc, argv); // ... process them + trapme(); // install trap handlers + +// be sure to setup the capture handler first, so +// on a soundblaster16 capture gets the 16-bit DMA +// channel and refout gets the 8-bit channel + alsa_pcm in("capture"); + int err; + if (foo.desired_rate >= -1) { + err = in.getrate("hw:" + foo.card_device, + SND_PCM_STREAM_CAPTURE, foo.desired_rate); + if (err) exit(1); // msgs have already been printed + } + + alsa_pcm out("refout"); + if (foo.reffreq // off ==> don't even initialize + || foo.desired_rate < 0) { // unless we are just dumping params + err = out.getrate("hw:" + foo.card_device, + SND_PCM_STREAM_PLAYBACK, foo.desired_rate); + if (err) exit(1); // msgs have already been printed + } + + int rate = min(in.max_rate, out.max_rate); + if (foo.desired_rate) rate = min(rate, foo.desired_rate); + pcmRate = rate; + + in.setup(rate); + out.setup(rate); + +// synchronize the two alsa_pcm sub-devices: + int linkerr = snd_pcm_link(in.phndl, out.phndl); + if (linkerr < 0) { + cout << "Lockin: synchronization link failed: " + << snd_strerror(linkerr) + << endl; + } + +// Be sure to configure mixer _after_ opening the pcm handlers, +// because we want to hold exclusive access to the mixer (control +// device) but snd_pcm_open wants to temporarily open the control +// device for its own reasons. + + config_mixer(foo.mixer_ctlfile, foo.justcard); + + foo.open_aux_io(); // checkfile, mostly + + prog_start_time = out.now(); + if (verbosity) { + fprintf(stderr, "starting %s" NL, progname.c_str()); + } + + app.setStyle("plastique"); + QWidget ancestor; + QwtPlot asdf(&ancestor); +// myWindow is derived from QWidget .... class myWindow : public QWidget + myWindow topWindow; + topWindow.ctrlCol->refOutGroup->freqBox->setValue(foo.reffreq); + topWindow.actualFreqs(); // initialize actual freqs + + topWindow.show(); + + int pipefd[2]; + if (pipe(pipefd)) { + fprintf(stderr, "Could not open pipe: "); + perror(0); + exeunt(1); + } + + ref_arg refarg(foo.refival, &out, pipefd[1/* writing */]); + pthread_t refout_id; + pthread_create(&refout_id, 0, &refout, &refarg); + + pthread_t krunch_id; + krunch_arg karg(&topWindow, &in, &out, &foo, pipefd[0/* reading */]); + pthread_create(&krunch_id, 0, &krunch, &karg); + + int rslt = app.exec(); + cout << "Qt returns: " << rslt << endl; + + exeunt(0); +} + +string purify(const string foo) { + string rslt; + for (string::size_type ii = 0; ii < foo.length(); ii++) { + char ch = foo[ii]; + if (isupper(ch)) ch += 'a' - 'A'; + if (isalnum(ch)) rslt += ch; + } + return rslt; +} + +// return a string like "usbaudio.ctl" +// based on ALSA's notion of the DRIVER name +string invent_driverfile(const string justcard){ + string driver("unknown"); + snd_ctl_t* chndl; // control handle + string xxx = "hw:" + justcard; + int err = snd_ctl_open(&chndl, xxx.c_str(), SND_CTL_RDONLY); + if (err < 0) { + chndl = 0; + cerr << "Error opening control channel to card " + << justcard << endl; + // proceed with driver = "unknown" + } else { + snd_ctl_card_info_t *info; + snd_ctl_card_info_t **info_p; /* defeat paranoid compiler warning */ + info_p = &info; + snd_ctl_card_info_alloca(info_p); + err = snd_ctl_card_info(chndl, info); + if (err < 0) { + cerr << "Can't get card info: " << err << endl; + } else { +#if 0 + int wid(18); + cerr << left; + // example: hw:0 + cerr << setw(wid) << "Ctl name:" + << snd_ctl_name(chndl) << endl; + + // example: card0 (but could be changed in modules.conf): + cerr << setw(wid) << "Card ID:" + << snd_ctl_card_info_get_id(info) << endl; + + // example: Sound Blaster Extigy + cerr << setw(wid) << "Card name:" + << snd_ctl_card_info_get_name(info) << endl; + + // example: M Audio Delta 1010 at 0x1400, irq 22 + cerr << setw(wid) << "Card longname:" + << snd_ctl_card_info_get_longname(info) << endl; + + // example: ICE1712 + cerr << setw(wid) << "Driver:" + << snd_ctl_card_info_get_driver(info) << endl; + + cerr << setw(wid) << "Mixer name:" + << snd_ctl_card_info_get_mixername(info) << endl; +#endif + driver = snd_ctl_card_info_get_driver(info); + } + } + return purify(driver) + ".ctl"; +} + +// return a string like "soundblasterextigy.ctl" +// based on ALSA's notion of the CTL name +string invent_ctlfile(const string justcard){ + string driver("unknown"); + snd_ctl_t* chndl; // control handle + string xxx = "hw:" + justcard; + int err = snd_ctl_open(&chndl, xxx.c_str(), SND_CTL_RDONLY); + if (err < 0) { + chndl = 0; + cerr << "Error opening control channel to card " + << justcard << endl; + // proceed with driver = "unknown" + } else { + snd_ctl_card_info_t *info; + snd_ctl_card_info_t **info_p; /* defeat paranoid compiler warning */ + info_p = &info; + snd_ctl_card_info_alloca(info_p); + err = snd_ctl_card_info(chndl, info); + if (err < 0) { + cerr << "Can't get card ID info: " << err << endl; + } else { + driver = snd_ctl_card_info_get_name(info); + } + } + return purify(driver) + ".ctl"; +} + +static string digits = "0123456789"; + +void dump_io() { + for (int ii = 0; ii<getdtablesize(); ii++){ + int rslt = fcntl(ii, F_GETFL); + if (rslt != -1) { + cout << "FD " << ii << " flags " << rslt << endl; + } else if (0) { + cout << "FD " << ii << " errno " << errno + << " == " << strerror(errno) << endl; + } + } +} + +// Process command-line switches, et cetera +void snark::cmdline(int argc, char **argv) { + +// Note basic default values were set up in constructor. +// Now process options: + int ch; + time_t mytime; + struct tm* now; + const int ALT(128); + static struct option long_options[] = { + {"buffer-size", 1, NULL, 'b'}, + {"card", 1, NULL, 'c'}, + {"help", 0, NULL, 'h'}, + {"mixer-ctl", 1, NULL, 'm'}, + {"output-fifo", 1, NULL, 'o'}, + {"verbose", 0, NULL, 'v'}, + {"write", 1, NULL, 'w'}, + {"amplitude", 1, NULL, 'A'}, + {"Amplitude", 1, NULL, 'A'}, + {"channel-mask", 1, NULL, 'C'}, + {"Channel-mask", 1, NULL, 'C'}, + {"frequency", 1, NULL, 'F'}, + {"Frequency", 1, NULL, 'F'}, + {"delta-f", 1, NULL, 'D'}, + {"delta-f", 1, NULL, 'D'}, + {"Vo-cal", 1, NULL, 'O'}, + {"Vi-cal", 1, NULL, 'I'}, + {"Time-shift", 1, NULL, 'T'}, + {"time-shift", 1, NULL, 'T'}, + {"Phase-shift", 1, NULL, 'P'}, + {"phase-shift", 1, NULL, 'P'}, + {"kout", 1, NULL, 'K'}, + {"Kout", 1, NULL, 'K'}, + {"frame-rate", 1, NULL, 'N'}, + {"quantum-sig", 1, NULL, 'Q'}, + {"Quantum-sig", 1, NULL, 'Q'}, + {"rin", 1, NULL, 'R'}, + {"Rin", 1, NULL, 'R'}, + {"zref", 1, NULL, 'Z'}, + {"Zref", 1, NULL, 'Z'}, + {"1pass", 0, NULL, '1'}, + {"xrun-verbose", 0, NULL, ALT|'x'}, + {"hack", 0, NULL, ALT|'h'}, + {"snooze", 1, NULL, ALT|'s'}, + {NULL, 0, NULL, 0} + }; + +////////////////////////////////////////////////////////////////////// + + mytime = time(0); + srand(mytime); + mixer_ctlfile = "<>"; // fill in default, later + now = localtime (&mytime); + if (now) {}; + int helpme(0); + +// Process commandline options + while(1) { +// int getopt_long(int argc, char * const argv[], +// const struct option *longopts, int *longindex); + + ch = getopt_long (argc, argv, long_options, NULL); + if (ch == -1) + break; + + if (optarg) if (*optarg == ':' || *optarg == '=') optarg++; + switch(ch) { + case '1': + onepass = 1; + break; + case 'b': + nframe = atoi(optarg); + break; + case 'c': + card_device = optarg; + if (card_device == "?" + || card_device == "" + || card_device == "-1") { + device_list(SND_PCM_STREAM_CAPTURE); + exit(0); + } + if (card_device == "-2") { + device_list(SND_PCM_STREAM_PLAYBACK); + exit(0); + } + break; + case 'm': + mixer_ctlfile = optarg; + break; + case 'w': + wcheck_fname = optarg; + break; + case 'N': + desired_rate = atoi(optarg); + break; + case 'C': + channel_mask = atoi(optarg); + break; + case 'K': + kout = atof(optarg); + break; + case 'Z': + zref = atof(optarg); + break; + case 'F': + reffreq = atof(optarg); + break; + case 'I': + ViCal_dB = atof(optarg); + break; + case 'O': + VoCal_dB = atof(optarg); + break; + case 'T': + timeShift = atof(optarg); + break; + case 'P': + phaseShift = atof(optarg); + break; + case 'A': + refOutAmp_dB = atof(optarg); + break; + case 'D': + refival = atof(optarg); + break; + case 'v': + verbosity ++; + break; + case 'h': + helpme++; + goto helper; + case ALT|'x': + xrun_verbosity++; + break; + case ALT|'h': + dump_io(); + exit(0); + break; + case ALT|'s': + snooze = atof(optarg); + break; + case '?': // optarg() uses this for any unrecognized + // option, and has already complained about it. + cerr << "For help, try\n " << argv[0] + << " --help" << endl; + exit(1); + default: + int chx(ch&~ALT); + fprintf(stderr, "Sorry, option %s'%c' not implemented.\n", + ch!=chx? "ALT+" : "", chx); + exit(1); + } + } +helper:;;;; + + if (!helpme) if (optind < argc) { + printf ("Extraneous verbiage: "); + while (optind < argc) cerr << argv[optind++] << " " ; + cerr << endl; + exit(1); + } + + string::size_type where = card_device.find_first_not_of(digits); + if (where == card_device.npos) justcard = card_device; + else justcard = card_device.substr(0, where); + + if(helpme) { + usage(0); + exit(0); // no need to exeunt() + } + +}// cmdline + + +void config_mixer(const string ctlfilestr, const string justcard){ + if (!ctlfilestr.length()) return; + + string ctlfile = fixup_ctlfile(ctlfilestr, justcard); + +// probe for existence: + int rslt = open(ctlfile.c_str(), O_RDONLY); + if (rslt < 0) { + fprintf(stderr, "Could not open alsactl file '%s': %m\n", + ctlfile.c_str()); + + string mixerfile = fixup_ctlfile(ctlfilestr, justcard, 1).c_str(); + int retry = open(mixerfile.c_str(), O_RDONLY); + if (retry < 0) { + fprintf(stderr, "... will use inherited mixer settings.\n"); + return; + } + fprintf(stderr, "... using fallback: %s\n", mixerfile.c_str()); + ctlfile = mixerfile; + close(retry); + } else close(rslt); // probe has been completed + + +#ifdef ALSAHACK + int load = load_state(ctlfile.c_str(), justcard.c_str(), 1, 1); + + if (load) { + fprintf(stderr, "error in alsa load_state ... " NL); + fprintf(stderr, " ... using ctlfile '%s' " NL, ctlfile.c_str()); + fprintf(stderr, " ... using card '%s' " NL, justcard.c_str()); + exit(1); + } +#endif +} + +// Open miscellaneous i/o features: +void snark::open_aux_io(){ + if (wcheck_fname.length()) { + wcheck_fd = open(wcheck_fname.c_str(), O_WRONLY|O_CREAT|O_TRUNC, 0600); + if (wcheck_fd == -1) { + fprintf(stderr, "Couldn't open checkfile for writing: %m" NL); + exit(1); + } + } +} + +void snark::printx( + const int colored, + const char* name, + const double foo_1, + const double foo_2, + double plogp, + const alsa_pcm* /*pcm*/ +) { + + double stdev = sqrt(foo_2 - foo_1*foo_1); + + double dbdev(-999); + if (stdev != 0.0) dbdev = 20 * log10(stdev / full_swing_2); + if (colored) cout << crtRed; + printf("%6s: 1st %9.3g, 2nd %9.3g, stdev %9.3g " + "= %7.3fdB, plp %6.3f", + name, foo_1, foo_2, stdev, dbdev, plogp); +// switch back to black BEFORE printing the newline; +// otherwise bad things happen when piping to /bin/head + if (colored) cout << crtNormal; + cout << endl; +} + +const double microsec(1e-6); + +void cleanup(int sts){ + for (string::size_type ii = 0; ii < clean_me.size(); ii++) { + int pid = clean_me[ii]; + if (pid > 0) kill(pid, SIGQUIT); + } + cout << crtNormal; + exit(sts); +} + +void trap_exit(int signum) { + fprintf(stderr, "%s stopping on signal %i" NL, progname.c_str(), signum); + cleanup(0); +} + +void discard(int signum) { + fprintf(stderr, "Discarding signal %i" NL, signum); +} + +void exeunt(int sts){ + fprintf(stderr, "%s exiting, status %i" NL, progname.c_str(), sts); + cleanup(sts); +} + +int dummy(const int xx) { + return xx; +} |