diff options
author | John Denker <jsd@av8n.com> | 2024-03-16 11:21:23 -0700 |
---|---|---|
committer | John Denker <jsd@av8n.com> | 2024-03-16 11:42:15 -0700 |
commit | 9852b855db2a65ea6eb5e877411634820214ddf0 (patch) | |
tree | c4c4504b34ef81d67891ed137ec2cfaabb4d6d04 | |
parent | 634d365a03cb0581a062cd3cf4db9ae69f1cde26 (diff) |
-rw-r--r-- | src/CMakeLists.txt | 33 | ||||
-rw-r--r-- | src/Getopt.cxx | 31 | ||||
-rw-r--r-- | src/Getopt.h | 4 | ||||
-rw-r--r-- | src/alsa_pcm.cxx | 355 | ||||
-rw-r--r-- | src/alsa_pcm.h | 46 | ||||
-rw-r--r-- | src/bad_thing.h | 12 | ||||
-rw-r--r-- | src/bin/.gitignore | 0 | ||||
-rw-r--r-- | src/biquad.cxx | 80 | ||||
-rw-r--r-- | src/biquad.h | 42 | ||||
-rw-r--r-- | src/gui.cxx | 755 | ||||
-rw-r--r-- | src/gui.h | 13 | ||||
-rw-r--r-- | src/gui_class.cxx | 5 | ||||
-rw-r--r-- | src/gui_class.h | 217 | ||||
-rw-r--r-- | src/iir_bp.cxx | 133 | ||||
-rw-r--r-- | src/iir_bp.h | 17 | ||||
-rw-r--r-- | src/krunch.cxx | 308 | ||||
-rw-r--r-- | src/krunch.h | 24 | ||||
-rw-r--r-- | src/lockin.cxx | 748 | ||||
-rw-r--r-- | src/lockin.h | 99 | ||||
-rw-r--r-- | src/makefile | 16 | ||||
-rw-r--r-- | src/refout.cxx | 247 | ||||
-rw-r--r-- | src/refout.h | 32 | ||||
-rw-r--r-- | src/sources.txt | 10 | ||||
-rw-r--r-- | src/thrower.cxx | 12 | ||||
-rw-r--r-- | src/thrower.h | 27 |
25 files changed, 3266 insertions, 0 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..8b0d72a --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.13) # CMake version check +project(lockin) # Create project "simple_example" +set(CMAKE_CXX_STANDARD 17) # Enable c++17 standard (20 would not work) +add_definitions(-DQT_NO_VERSION_TAGGING) +# do not use: -Wstrict-overflow=2 # disaster for Qt include-files +SET(CMAKE_CXX_FLAGS "-g2 -fPIC -Wall" ) + +set(CMAKE_AUTOMOC ON) # hackish module object compiler + +# All the .cxx source files of the project: +file(STRINGS sources.txt SOURCE_FILES) + +# Add executable target with source files listed in SOURCE_FILES variable +add_executable(lockin ${SOURCE_FILES}) + +target_link_libraries(lockin "-lQt5Gui -lQt5Core -lQt5Widgets -lqwt-qt5") +target_link_libraries(lockin "-lasound -lgsl") + +target_include_directories(lockin PUBLIC + /usr/include/x86_64-linux-gnu/qt5/ + /usr/include/x86_64-linux-gnu/qt5/QtWidgets + /usr/include/x86_64-linux-gnu/qt5/QtGui + /usr/include/x86_64-linux-gnu/qt5/QtCore + /usr/include/qwt/ +) + +############ find_package(ALSA) # not necessary, not helpful AFAICT + +find_package(Qt5 COMPONENTS Widgets) # necessary + +include(CMakePrintHelpers) +message("files: a b c ${SOURCE_FILES}") +cmake_print_variables(SOURCE_FILES) diff --git a/src/Getopt.cxx b/src/Getopt.cxx new file mode 100644 index 0000000..49673bc --- /dev/null +++ b/src/Getopt.cxx @@ -0,0 +1,31 @@ +////////////////////////////////////////////////////////////////////// +// Provide a C++ interface to getopt. +// Calculate things that can be calculated, to ensure consistency, +// and so the programmer doesn't need to duplicate effort. + +using namespace std; + +#include <iostream> /* needed by some compilers */ +#include "Getopt.h" +#include <iostream> +#include <string> + +int getopt_long(int argc, char * const argv[], + const struct option *longopts, int *longindex){ + + string optstring; + const struct option * pp; + for (pp = longopts; pp->name; pp++){ + if (pp->val < 0 || pp->val > 255) { + cerr << "Cannot handle val " << pp->val + << " for option '" << pp->name << "'" << endl; + } else { +// cerr << pp->name << "'" << char(pp->val) << "'" << endl; + optstring += char(pp->val); + if (pp->has_arg == 1) optstring += ':'; + if (pp->has_arg == 2) optstring += "::"; + } + } +// cerr << optstring << endl; + return getopt_long(argc, argv, optstring.c_str(), longopts, longindex); +} diff --git a/src/Getopt.h b/src/Getopt.h new file mode 100644 index 0000000..ed759be --- /dev/null +++ b/src/Getopt.h @@ -0,0 +1,4 @@ +#include <getopt.h> + +int getopt_long(int argc, char * const argv[], + const struct option *longopts, int *longindex); diff --git a/src/alsa_pcm.cxx b/src/alsa_pcm.cxx new file mode 100644 index 0000000..2bbe5e6 --- /dev/null +++ b/src/alsa_pcm.cxx @@ -0,0 +1,355 @@ +#include "alsa_pcm.h" +#include <iostream> +#include <iomanip> +#include <sstream> + +extern int verbosity; + +// note that if the error code is -999, +// we exit with non-error status (0) +void alsa_pcm::error(const char* msg, + const int ecode, snd_pcm_hw_params_t* hwparams) { + using namespace std; + if (ecode != -999) { + cerr << "Device won't accept parameter '" << msg << "'" + << " ecode '" << ecode << "'" << endl; + } + else { + cerr << msg << " for " << moniker << endl; + } + snd_pcm_hw_params_dump(hwparams, errlog); + int sync = snd_pcm_hw_params_can_sync_start(hwparams); + cout << "sync_start: " << sync << endl; + int sbits = snd_pcm_hw_params_get_sbits(hwparams); + if (sbits <= 0) try_formats(hwparams); // ignore errors from here + sbits = snd_pcm_hw_params_get_sbits(hwparams); // try again + if (sbits > 0) { + cerr << "Allegedly can do " + << sbits << " significant bits per sample." << endl; + } else { +// Can't determine sigbits if the card is capable of +// multiple sample-sizes and we haven't chosen one yet. + cerr << "Can't (yet) determine sigbits per sample. Result was: " + << sbits << " == " + << snd_strerror(sbits) << endl; + sbits = 0; + } + + snd_pcm_close(phndl); + snd_output_close(errlog); + if (ecode == -999) exit(0); // weird code for non-error + exit(1); +} + +// Try various sample formats. +int alsa_pcm::try_formats(snd_pcm_hw_params_t* hwparams){ + int err; + format = SND_PCM_FORMAT_S32_LE; + err = snd_pcm_hw_params_set_format(phndl, hwparams, format); + if (err >= 0) return 0; + + format = SND_PCM_FORMAT_S16_LE; + err = snd_pcm_hw_params_set_format(phndl, hwparams, format); + if (err >= 0) return 0; + + format = SND_PCM_FORMAT_S8; + err = snd_pcm_hw_params_set_format(phndl, hwparams, format); + if (err >= 0) return 0; + + return -999; +} + +int alsa_pcm::getrate(const std::string carddev, + snd_pcm_stream_t stream_dir, + const int dump_flag){ + int err; + + direction = stream_dir; +// Connect error reporting to stderr + if (!errlog) snd_output_stdio_attach(&errlog, stderr, 0); + + int mode = 0; // could have been SND_PCM_NONBLOCK; + err = snd_pcm_open(&phndl, carddev.c_str(), direction, mode); + if (err < 0){ + fprintf(stderr, "Error opening soundcard %s for %s: %s\n", + carddev.c_str(), moniker, snd_strerror(err)); + snd_output_close(errlog); + return 1; + } + +// now that we've grabbed the device, possibly without blocking +// if it was busy, we now switch to blocking mode for all I/O: + err = snd_pcm_nonblock(phndl, 0); + +// Allocate a hwparams struct... + snd_pcm_hw_params_t* hwparams; + snd_pcm_hw_params_alloca(&hwparams); + +// ... and initialize it to the "wide open" ranges +// allowed by phndl: + err = snd_pcm_hw_params_any(phndl, hwparams); + if (err < 0) error("Initialization error", -999, hwparams); + +// User can force a dump of parameters, +// here at a nice early stage: + if (dump_flag < 0) error("Possibilities are:", -999, hwparams); + + snd_pcm_hw_params_get_rate_max(hwparams, &max_rate, 0); + return 0; +} + +int alsa_pcm::setup(const int _rate){ + rate = _rate; + int err; + +// Allocate a hwparams struct... + snd_pcm_hw_params_t* hwparams; + snd_pcm_hw_params_alloca(&hwparams); + +// ... and initialize it to the "wide open" ranges +// allowed by phndl: + err = snd_pcm_hw_params_any(phndl, hwparams); + if (err < 0) error("Initialization error", -999, hwparams); + +// Access method, interleaved or non-interleaved + err = snd_pcm_hw_params_set_access(phndl, hwparams, + SND_PCM_ACCESS_RW_INTERLEAVED); + if (err < 0) error("access method", SND_PCM_ACCESS_RW_INTERLEAVED, + hwparams); + +// Try various sample formats: + int rslt = try_formats(hwparams); + if (rslt < 0) + error("DSP error: format not S32_LE nor S16_LE nor S8", rslt, + hwparams); + + if (!phndl) return -998; + err = snd_pcm_hw_params_set_rate(phndl, hwparams, rate, 0); + if (err < 0) error("rate", rate, hwparams); + +// Number of channels we want, stereo = 2 + unsigned int nchan_max; + snd_pcm_hw_params_get_channels_max(hwparams, &nchan_max); + nchan = nchan_max; + if (direction == SND_PCM_STREAM_PLAYBACK && nchan_max > 2){ + nchan = 2; // calibrator doesn't need more than 2 channels + } + err = snd_pcm_hw_params_set_channels(phndl, hwparams, nchan); + if (err < 0) error("Cannot set number of channels: ", nchan, hwparams); + +// The period size. For all practical purposes this is synonymous +// to OSS/Free's fragment size. +// Note that this in frames (frame = nr_channels * sample_width) +// For example, a setup using FPP=1024, stereo, 16-bit format +// gives a period size of 4096 bytes (1024 x 2 x 2 bytes). + if (1) { + err = snd_pcm_hw_params_set_period_size(phndl, hwparams, FPP, 0); + if (err < 0) error("period-size", FPP, hwparams); + } + +// The number of periods we want to allocate + { + unsigned int periods = 4; // reasonable guess + unsigned int minper, maxper; + if (snd_pcm_hw_params_get_periods_min(hwparams, &minper, 0)==0 + && periods < minper) periods = minper; + if (snd_pcm_hw_params_get_periods_max(hwparams, &maxper, 0)==0 + && periods > maxper) periods = maxper; + err = snd_pcm_hw_params_set_periods(phndl, hwparams, periods, 0); +#ifdef testing + fprintf(stderr, "periods: %d (%d -- %d) \n", + periods, minper, maxper); +#endif + if (periods <= 1) { + fprintf(stderr, "Warning: %d periods is not really enough; " + "we should have at least 2 for double buffering\n", periods); + if (periods==0) exit(1); + fprintf(stderr, "Continuing anyway....\n"); + } + if (err < 0) error("#-of-periods", periods, hwparams); + } + +// Finally set up our hardware with the selected values + err = snd_pcm_hw_params(phndl, hwparams); + if (err < 0) { + fprintf(stderr, "Unable to set hardware parameter:\n"); + snd_pcm_hw_params_dump(hwparams, errlog); + return 2; + } + + err = sbits = snd_pcm_hw_params_get_sbits(hwparams); + if (sbits <= 0 || sbits > 32) { +// if driver won't tell us how many are significant, +// assume they all are: + sbits = snd_pcm_samples_to_bytes(phndl, 8); + fprintf(stderr, "Trouble %d determining sig bits; assuming %d\n", + err, sbits); + } + + if (verbosity >= 2) { + fprintf(stderr, ">>>>>> After setup (%s)\n", moniker); + snd_pcm_hw_params_dump(hwparams, errlog); + } + +// At this point you can start sending PCM data to the device + return 0; +} + +// the last time the stream was started or stopped +snd_htimestamp_t alsa_pcm::ss_time(){ + snd_pcm_status_t* sts(0); + snd_pcm_status_alloca(&sts); + snd_htimestamp_t tstamp; // this is a struct timespec + int err = snd_pcm_status(phndl, sts); + if (err < 0) { + fprintf(stderr, "Setup: can't get status?\n"); + tstamp.tv_sec = 0; + tstamp.tv_nsec = 0; + return tstamp; + } + snd_pcm_status_get_trigger_htstamp(sts, &tstamp); + return tstamp; +} + +// same as above, but returns current time +snd_htimestamp_t alsa_pcm::now(){ + snd_pcm_status_t* sts(0); + snd_pcm_status_alloca(&sts); + snd_htimestamp_t tstamp; // this is a struct timespec + int err = snd_pcm_status(phndl, sts); + if (err < 0) { + fprintf(stderr, "Setup: can't get status?\n"); + tstamp.tv_sec = 0; + tstamp.tv_nsec = 0; + return tstamp; + } + snd_pcm_status_get_htstamp(sts, &tstamp); + return tstamp; +} + +std::string alsa_pcm::alsa_state_name() { + + snd_pcm_state_t state = snd_pcm_state(phndl); + +#define checkit(XX) if (state == XX) return #XX + + checkit(SND_PCM_STATE_OPEN); + checkit(SND_PCM_STATE_SETUP); + checkit(SND_PCM_STATE_PREPARED); + checkit(SND_PCM_STATE_RUNNING); + checkit(SND_PCM_STATE_XRUN); + checkit(SND_PCM_STATE_DRAINING); + checkit(SND_PCM_STATE_PAUSED); + checkit(SND_PCM_STATE_SUSPENDED); + checkit(SND_PCM_STATE_DISCONNECTED); + return "??unknown state??"; +} + +#define command "whatever" +#if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 95) +#define error(...) do {\ + fprintf(stderr, "%s: %s:%d: ", command, __FUNCTION__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + putc('\n', stderr); \ +} while (0) +#else +#define error(args...) do {\ + fprintf(stderr, "%s: %s:%d: ", command, __FUNCTION__, __LINE__); \ + fprintf(stderr, ##args); \ + putc('\n', stderr); \ +} while (0) +#endif + +int dummy(int const xx); + +void device_list(const snd_pcm_stream_t direction){ +#define _(XX) XX + snd_ctl_t *handle; + int card, err, dev, idx; + snd_ctl_card_info_t *info; + snd_pcm_info_t *pcminfo; + snd_ctl_card_info_alloca(&info); + snd_pcm_info_alloca(&pcminfo); + + card = -1; + if (snd_card_next(&card) < 0 || card < 0) { + error(_("no soundcards found...")); + return; + } + printf(_("**** List of %s Hardware Devices ****\n"), + snd_pcm_stream_name(direction)); + +// loop over all cards: + for (int ii=0;; ii++) { + using namespace std; + if (card < 0) break; +// use dummy to defend against false compiler warning, +// and possibly wrong code generation: +// "assuming signed overflow does not occur when simplifying conditional to constant" + if (dummy(ii)) fprintf(stdout, "\n"); + stringstream name; + name << "hw:" << card; + if ((err = snd_ctl_open(&handle, name.str().c_str(), 0)) < 0) { + error("control open (%i): %s", card, snd_strerror(err)); + goto next_card; + } + if ((err = snd_ctl_card_info(handle, info)) < 0) { + error("control hardware info (%i): %s", card, snd_strerror(err)); + snd_ctl_close(handle); + goto next_card; + } + cout << "card " << setw(2) << card + << " ID: " << snd_ctl_card_info_get_id(info) + << " ... name: " << snd_ctl_card_info_get_name(info) + << endl; + + cout << " driver: " << snd_ctl_card_info_get_driver(info) + << endl; + cout << " mixer: " << snd_ctl_card_info_get_mixername(info) + << endl; + dev = -1; +// loop over all devices on the card + for (int ndev=0; ; ndev++) { + unsigned int count; + if (snd_ctl_pcm_next_device(handle, &dev)<0) + error("snd_ctl_pcm_next_device"); + if (dev < 0) + break; + snd_pcm_info_set_device(pcminfo, dev); + snd_pcm_info_set_subdevice(pcminfo, 0); + snd_pcm_info_set_stream(pcminfo, direction); + if ((err = snd_ctl_pcm_info(handle, pcminfo)) < 0) { + if (err != -ENOENT) + error("control digital audio info (%i): %s", card, snd_strerror(err)); + continue; + } + cout << " device " << setw(2) << dev + << " ID: " << snd_pcm_info_get_id(pcminfo) + << " ... name: " << snd_pcm_info_get_name(pcminfo) + << endl; + count = snd_pcm_info_get_subdevices_count(pcminfo); + int avail = snd_pcm_info_get_subdevices_avail(pcminfo); + cout << " subdevices available: " << setw(2) << avail + << " total: " << count + << endl; + for (idx = 0; idx < (int)count; idx++) { + snd_pcm_info_set_subdevice(pcminfo, idx); + if ((err = snd_ctl_pcm_info(handle, pcminfo)) < 0) { + error("control digital audio playback info (%i): %s", + card, snd_strerror(err)); + } else { + cout << " subdevice " << setw(2) << idx + << " ... name: " + << snd_pcm_info_get_subdevice_name(pcminfo) + << endl; + } + } + } + snd_ctl_close(handle); + next_card: + if (snd_card_next(&card) < 0) { + error("snd_card_next"); + break; + } + } +} diff --git a/src/alsa_pcm.h b/src/alsa_pcm.h new file mode 100644 index 0000000..fafd857 --- /dev/null +++ b/src/alsa_pcm.h @@ -0,0 +1,46 @@ +#ifndef ALSA_PCM__H +#define ALSA_PCM__H + +#include <string> +#include <alsa/asoundlib.h> + +const int FPP(1024); // frames per period + +class alsa_pcm{ +public: + snd_pcm_t* phndl; // pcm handle +#if 0 + DO NOT save a pointer to any snd_pcm_hw_params_t object; + alsa allocates space _on the stack_ and it goes out of + scope quickly. + snd_pcm_hw_params_t *hwparams; +#endif + snd_output_t* errlog; + unsigned int nchan; // samples per frame; 2 ==> stereo + unsigned int max_rate; + unsigned int rate; // frames per second + int sbits; // significant bits + snd_pcm_stream_t direction; +// int sizeofsamp; // bytes per sample + snd_pcm_format_t format; + const char* moniker; + snd_htimestamp_t ss_time(); // start or stop time (usually start time) + snd_htimestamp_t now(); // current time + void error(const char*, const int, snd_pcm_hw_params_t*); + int getrate(const std::string carddevice, + snd_pcm_stream_t stream_dir, + const int dumpflag); + int setup(const int rate); +// Constructor doesn't do much, +// so it is cheap to allocate one that you're not +// necessarily going to use: + inline alsa_pcm(const char* mm) + : phndl(0), errlog(0), moniker(mm) + {} + int try_formats(snd_pcm_hw_params_t*); + std::string alsa_state_name(); +}; + +void device_list(const snd_pcm_stream_t direction); + +#endif /* ALSA_PCM__H */ diff --git a/src/bad_thing.h b/src/bad_thing.h new file mode 100644 index 0000000..ec13f1c --- /dev/null +++ b/src/bad_thing.h @@ -0,0 +1,12 @@ +#ifndef BAD_THING__H +#define BAD_THING__H +class bad_thing: public std::exception{ + const char* msg; + virtual const char* what() const throw() { + return msg; + } +public: + bad_thing(const char* _msg) + : msg(_msg) {} +}; +#endif diff --git a/src/bin/.gitignore b/src/bin/.gitignore new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/bin/.gitignore diff --git a/src/biquad.cxx b/src/biquad.cxx new file mode 100644 index 0000000..372b89e --- /dev/null +++ b/src/biquad.cxx @@ -0,0 +1,80 @@ +#include <iostream> +#include "biquad.h" +#include "bad_thing.h" +using namespace std; + +// gory-detail constructor +biquad::biquad(vector<double> const _c, vector<double> const _d, + vector<C> _zeros, vector<C> _poles) +: c(_c), d(_d), zeros(_zeros), poles(_poles), + ws1(0), ws2(0) +{ + if (d[0] != 1.) throw bad_thing("biquad: d0 must be 1."); +} + +// constructor in terms of zero and pole position, +// assuming conjugate pairs: +biquad::biquad(C const zero, C const pole) +///// nume = (z-zero)(z-zerobar) +///// = z^2 - 2 zero.real + |zero|^2 +: biquad({1., -2.*zero.real(), norm(zero)}, + {1., -2.*pole.real(), norm(pole)}, + {zero, conj(zero)}, + {pole, conj(pole)}) +{} + +C constexpr biquad::C0; +C constexpr biquad::C1; + +vector<C> solve(vector<double> const poly) { + double disc(1 - 4. * poly[0] * poly[2] / poly[1] / poly[1]); + C inner(biquad::C1 + sqrt(C(disc, 0))); + C root1 = C(-poly[1]/2/poly[0], 0) * inner; + return { root1, + C(poly[2]/poly[0], 0) / root1 }; +} + +// constructor in terms of coefficients: +biquad::biquad(vector<double> const _c, vector<double> const _d) +: biquad(_c, _d, + {C0, C0}, {C0, C0}) +{ + zeros = solve(c); + poles = solve(d); +} + +double biquad::step(double const Vin){ + double q1(ws1); + double q2(ws2); + double Vout = c[0]*Vin + q1; + ws1 = c[1]*Vin - d[1]*Vout + q2; + ws2 = c[2]*Vin - d[2]*Vout; + return Vout; +} + +// transfer function +C biquad::xfunc(C const z) const { + C nume = (c[0]*z + c[1])*z + c[2]; + C denom = (z + d[1])*z + d[2]; + return nume / denom; +} + +void biquad::please_normalize(C const z) { + double denom(abs(xfunc(z))); +// cout << "denom: " << denom << endl; +// cout << "c2: " << c[2] << endl; +// cout << "z: " << z << endl; + for (auto &coeff : c) { + coeff /= denom; + } +} + +// same as above, but with some error checking: + +void biquad::normalize(C const z) { + if (abs(abs(z) - 1.) > 1e-10) { +// + throw bad_thing("normalizing to z not on unit circle"); + } + please_normalize(z); +} diff --git a/src/biquad.h b/src/biquad.h new file mode 100644 index 0000000..d897062 --- /dev/null +++ b/src/biquad.h @@ -0,0 +1,42 @@ +#ifndef BIQUAD__H +#define BIQUAD__H +#include <complex> +#include <vector> + +// typical usage: +// biquad butterworth({1., 2., 1.}, +// {1., -1.99911142347079540116, 0.99911181807963833634}); + +typedef std::complex<double> C; + +class biquad{ +public: +// d0 must always be 1 + std::vector<double> c, d; + std::vector<C> zeros, poles; + double ws1, ws2; // remembered weighted sums + + C static constexpr C0 = C(0, 0); + C static constexpr C1 = C(1, 0); +// gory-detail constructor +biquad(std::vector<double> const _c, std::vector<double> const _d, + std::vector<C> _zeros, std::vector<C> _poles); + +// constructor in terms of coefficients: +biquad(std::vector<double> const _c, std::vector<double> const _d); + +// constructor in terms of zero and pole position, +// assuming conjugate pairs: + biquad(C const zero, C const pole); + +// time-domain evaluation: + double step(double const Vin); + +// z-plane transfer function + C xfunc(const C z) const; + + void normalize(C const z = 1); + void please_normalize(C const z = 1); +}; + +#endif diff --git a/src/gui.cxx b/src/gui.cxx new file mode 100644 index 0000000..75f1f9c --- /dev/null +++ b/src/gui.cxx @@ -0,0 +1,755 @@ +#include <iostream> +#include <iomanip> +#include "gui_class.h" +#include <gsl/gsl_fit.h> +#include <iostream> +using namespace std; + +#define STRETCH /* nothing */ + +extern int verbosity; // should be in a .h file somewhere + +///////////////// +// some kludgey global variables, +// mostly for inter-thread communication +int pcmRate(0); +double cpkp(0); // cycles per krunch period +double fpkp(0); // frames per krunch period: +double actOutFreq(0); // actual refout frequency +double refOutAmp_dB(-15.); // default should not be not too loud +double VoCal_dB(0.); +double ViCal_dB(0.); +double timeShift(0.); // in seconds +double phaseShift(0.); // in degrees +double lockerPhase(0.); // in degrees +int repaintFrame(0); +// +// Note: more globals can be found in lockin.cxx + +class colorer { +public: + Qt::GlobalColor code; + const char* name; + + colorer(const Qt::GlobalColor _code, const char* const _name) + : code(_code), name(_name) + {} +}; + +#define X(foo) colorer(Qt::foo, #foo) + +static colorer color_list[] = {X(red), X(blue), X(black)}; + +#undef X + +static int color_list_size = sizeof(color_list) / sizeof(color_list[0]); + +#if 0 /* code not needed at present */ +double now(){ + timespec xx; + +#ifdef _POSIX_MONOTONIC_CLOCK + clock_gettime(CLOCK_MONOTONIC, &xx); +#else + clock_gettime(CLOCK_MONOTONIC, &xx); +#endif + + return xx.tv_sec + xx.tv_nsec*1e-9; +} +#endif + +myWindow::myWindow() { +// The top myWindow and its layout : horizontal: + // topWindow == this + topLayout = new QHBoxLayout; + setLayout(topLayout); + + ctrlCol = new ctrl_column(this); + topLayout->addWidget(ctrlCol->group); + + for (int ii = 0; ii < myWindow::numRsltPanels; ii++){ + rsltPanel[ii] = new rslt_panel(this); + topLayout->addWidget(rsltPanel[ii]->group); + } + +// setting the indication to its current value won't +// change the indicator, but will cause it to update +// the associated plot + for (unsigned int ii = 0; ii < ndc8r.size(); ii++){ + indicator* foo = ndc8r[ii]; + foo->setIndication(0,0, foo->rpBox->value(), foo->ipBox->value()); + } + + frameTimer = new timer; +} + +void myWindow::flush() { + if (!repaintFrame) return; + repaintFrame = 0; + for (int ii = 0; ii < numRsltPanels; ii++){ + rsltPanel[ii]->plot->flush(); + } +} + +ctrl_column::ctrl_column(myWindow* topwin){ + group = new QGroupBox; + group->setFlat(0); + layout = new QVBoxLayout; + group->setLayout(layout); + + refOutGroup = new refOut_grouper(topwin); + layout->addWidget(refOutGroup->group); + + layout->insertStretch(-1); + + entrailsGroup = new entrails_grouper(topwin); + layout->addWidget(entrailsGroup->group); +} + + +// the refOut group and its layout: vertical: +refOut_grouper::refOut_grouper(myWindow* _topwin) +: topwin(_topwin), + VLabel(0) +{ +// the freq box + group = new QGroupBox("Ref Out"); + layout = new QVBoxLayout; + group->setLayout(layout); +//group->setStyleSheet("background-color: lavenderblush;"); +//group->setStyleSheet("background-color: rgba(100, 0, 0, 150);"); +//group->setStyleSheet("background-color: lightcyan;"); + group->setStyleSheet(".QGroupBox{background-color: #E8ffFF}"); + freqLabel = new QLabel; + freqLabel->setText("Frequency"); + + freqBox = new QDoubleSpinBox; + freqBox->setRange(0.0, 10000.0); + freqBox->setSingleStep(1); + freqBox->setValue(440); + freqBox->setKeyboardTracking(0); + freqBox->setSuffix(" Hz"); + connect(freqBox, SIGNAL(valueChanged(double)), + this, SLOT(freqChanged(double))); + + actFreqLabel = new QLabel; + actFreqLabel->setText("Actual"); + + actFreqBox = new QDoubleSpinBox; + actFreqBox->setRange(0.0, 10000.0); + actFreqBox->setValue(440); + actFreqBox->setReadOnly(1); + actFreqBox->setButtonSymbols(QAbstractSpinBox::NoButtons); + actFreqBox->setSuffix(" Hz"); + + dBLabel = new QLabel; + dBLabel->setText("Amplitude"); + + dBBox = new QDoubleSpinBox; + dBBox->setRange(-1000.0, 1000.0); + dBBox->setDecimals(3); + dBBox->setSingleStep(1); +//later: dBBox->setValue(refOutAmp_dB); + dBBox->setKeyboardTracking(0); + dBBox->setSuffix(" dBV"); + connect(dBBox, SIGNAL(valueChanged(double)), + this, SLOT(ampChanged(double))); + +// express amplitude in V, not just in dB: +//?? VLabel = new QLabel; +//?? VLabel->setText("???"); + + VBox = new QDoubleSpinBox; + VBox->setRange(-1e300, +1e300); + VBox->setReadOnly(1); + VBox->setDecimals(6); + VBox->setButtonSymbols(QAbstractSpinBox::NoButtons); + VBox->setSuffix(" V"); + + dBBox->setValue(refOutAmp_dB); // will throw ampChanged signal + + layout->addWidget(freqLabel STRETCH); + layout->addWidget(freqBox STRETCH); + layout->addWidget(actFreqLabel STRETCH); + layout->addWidget(actFreqBox STRETCH); + layout->addWidget(dBLabel STRETCH); + layout->addWidget(dBBox STRETCH); + if (VLabel) layout->addWidget(VLabel STRETCH); + layout->addWidget(VBox STRETCH); +} + +// the entrails group and its layout: vertical: +entrails_grouper::entrails_grouper(myWindow* _topwin) +: topwin(_topwin), + tweakFreq(tweakSize), + tweakPhase(tweakSize), + tweakPtr(0), + howmany(0) +{ + group = new QGroupBox("Entrails"); + layout = new QVBoxLayout; + group->setLayout(layout); + group->setStyleSheet("QGroupBox{background-color: #ffffe0}"); + + VoCalLabel = new QLabel; + VoCalLabel->setText("Vo Calibration"); + + VoCalBox = new QDoubleSpinBox; + VoCalBox->setRange(-1e300, +1e300); + VoCalBox->setSingleStep(1); + VoCalBox->setKeyboardTracking(0); + VoCalBox->setSuffix(" dBV FS"); + connect(VoCalBox, SIGNAL(valueChanged(double)), + this, SLOT(VoChanged(double))); + VoCalBox->setValue(VoCal_dB); + + ViCalLabel = new QLabel; + ViCalLabel->setText("Vi Calibration"); + + ViCalBox = new QDoubleSpinBox; + ViCalBox->setRange(-1e300, +1e300); + ViCalBox->setSingleStep(1); + ViCalBox->setKeyboardTracking(0); + ViCalBox->setSuffix(" dBV FS"); + connect(ViCalBox, SIGNAL(valueChanged(double)), + this, SLOT(ViChanged(double))); + ViCalBox->setValue(ViCal_dB); + + rateLabel = new QLabel; + rateLabel->setText("Krunch Rate"); + + rateBox = new QDoubleSpinBox; + rateBox->setRange(0.0, 1e300); + rateBox->setSingleStep(1); + rateBox->setValue(6.); + rateBox->setKeyboardTracking(0); + rateBox->setSuffix(" Hz"); + connect(rateBox, SIGNAL(valueChanged(double)), + this, SLOT(rateChanged(double))); + + actRateLabel = new QLabel; + actRateLabel->setText("Actual"); + + actRateBox = new QDoubleSpinBox; + actRateBox->setRange(0.0, 1e300); + actRateBox->setValue(6.); + actRateBox->setReadOnly(1); + actRateBox->setSuffix(" Hz"); + actRateBox->setButtonSymbols(QAbstractSpinBox::NoButtons); + + pcmLabel = new QLabel; + pcmLabel->setText("PCM Rate"); + + pcmBox = new QDoubleSpinBox; + pcmBox->setRange(-1e300, +1e300); + pcmBox->setValue(440); + pcmBox->setReadOnly(1); + pcmBox->setSuffix(" Hz"); + pcmBox->setButtonSymbols(QAbstractSpinBox::NoButtons); + + timeShiftLabel = new QLabel; + timeShiftLabel->setText("Time Shift"); + + timeShiftBox = new QDoubleSpinBox; + timeShiftBox->setRange(-1000.0, 1000.0); + timeShiftBox->setDecimals(6); + timeShiftBox->setSingleStep(1e-6); + timeShiftBox->setValue(timeShift); + timeShiftBox->setKeyboardTracking(0); + timeShiftBox->setSuffix(" s"); + connect(timeShiftBox, SIGNAL(valueChanged(double)), + this, SLOT(timeShiftChanged(double))); + + phaseShiftLabel = new QLabel; + phaseShiftLabel->setText("Phase Shift"); + + phaseShiftBox = new QDoubleSpinBox; + phaseShiftBox->setRange(-1e300, +1e300); + phaseShiftBox->setDecimals(2); + phaseShiftBox->setSingleStep(1); + phaseShiftBox->setValue(phaseShift); + phaseShiftBox->setKeyboardTracking(0); + phaseShiftBox->setSuffix(QString::fromUtf8(" °")); + connect(phaseShiftBox, SIGNAL(valueChanged(double)), + this, SLOT(phaseShiftChanged(double))); + + tweakButton = new QPushButton("&Tweak"); + connect(tweakButton, SIGNAL(clicked()), + this, SLOT(tweakButtonClicked())); + + layout->addWidget(VoCalLabel STRETCH); + layout->addWidget(VoCalBox STRETCH); + + layout->addWidget(ViCalLabel STRETCH); + layout->addWidget(ViCalBox STRETCH); + + layout->addWidget(rateLabel STRETCH); + layout->addWidget(rateBox STRETCH); + layout->addWidget(actRateLabel STRETCH); + layout->addWidget(actRateBox STRETCH); + layout->addWidget(pcmLabel STRETCH); + layout->addWidget(pcmBox STRETCH); + layout->addWidget(timeShiftLabel STRETCH); + layout->addWidget(timeShiftBox STRETCH); + layout->addWidget(phaseShiftLabel STRETCH); + layout->addWidget(phaseShiftBox STRETCH); + layout->addWidget(tweakButton STRETCH); +} + +rslt_panel::rslt_panel(myWindow* _topWin) +: topWin(_topWin) +{ + group = new QGroupBox; + group->setFlat(0); + layout = new QHBoxLayout; + group->setLayout(layout); + plot = new myPlot("plotname"); + block = new blockOfIndicators(topWin, plot); +// layout->addWidget(plot, 0, Qt::AlignHCenter); + layout->addWidget(plot); + layout->addWidget(block->group); + layout->insertStretch(-1); +} + +double quantize125(double arg){ + if (arg < sqrt(2.)) return 1.; + if (arg < sqrt(10.)) return 2.; + if (arg < sqrt(50.)) return 5.; + return 10.; +} + +double logstep(double arg){ + double ctc = pow(10., floor(log10(arg))); + double mant = arg / ctc; + return quantize125(mant) * ctc; +} + +scaleBoxer::scaleBoxer(indicator* _parent) +: parent(_parent) +{} + +void scaleBoxer::stepBy(int steps){ + double newval(1); + if (steps == 1) newval = logstep(2.0 * logstep(value())); + if (steps == -1) newval = logstep(0.5 * logstep(value())); + + if (steps == 10) newval = logstep(10.0 * value()); + if (steps == -10) newval = logstep( 0.1 * value()); + parent->setDecim(newval); + setValue(newval); +} + +void indicator::setDecim(const double newval){ + int decim = -int(floor(log10(newval))); + scaleBox->setDecimals(std::max(0, decim)); + rpBox->setDecimals(std::max(0, 2+decim)); + ipBox->setDecimals(std::max(0, 2+decim)); + magBox->setDecimals(std::max(0, 2+decim)); +} + + +// An indicator and its internal layout: grid: +indicator::indicator(myPlot* _plot, int _locker) +: plot(_plot), + plotcur(0), + locker(_locker) +{ + if (plot) { + plotcur = plot->assign_curve(); + } + + group = new QGroupBox; + group->setFlat(0); + QString style = ".QGroupBox{"; + if (plot) { + style += "border-top: 4px solid "; + style += color_list[plotcur % color_list_size].name; + style += ";"; + } + style += "background-color: #D0ffD0;"; + style += "}"; + group->setStyleSheet(style); + + layout = new QGridLayout; + group->setLayout(layout); + + scaleLabel = new QLabel; + scaleLabel->setText("Scale"); + + scaleBox = new scaleBoxer(this); + scaleBox->setKeyboardTracking(0); + scaleBox->setRange(-1e300, +1e300); + double sc(pow(10., ViCal_dB/20.)); + sc /= 2.5; // half scale, where full scale is 5 divisions + sc = logstep(sc); + scaleBox->setDecimals(10); + scaleBox->setValue(sc); + scaleBox->setSuffix(" V/div"); + + rpLabel = new QLabel; + rpLabel->setText("Rp"); + + rpBox = new QDoubleSpinBox; + rpBox->setReadOnly(1); + rpBox->setButtonSymbols(QAbstractSpinBox::NoButtons); + rpBox->setRange(-1e300, +1e300); + rpBox->setValue(0); + rpBox->setSuffix(" V"); + + ipLabel = new QLabel; + ipLabel->setText("Ip"); + + ipBox = new QDoubleSpinBox; + ipBox->setReadOnly(1); + ipBox->setButtonSymbols(QAbstractSpinBox::NoButtons); + ipBox->setRange(-1e300, +1e300); + ipBox->setValue(0); + ipBox->setSuffix(" V"); + + magLabel = new QLabel; + magLabel->setText("Mag"); + + magBox = new QDoubleSpinBox; + magBox->setReadOnly(1); + magBox->setButtonSymbols(QAbstractSpinBox::NoButtons); + magBox->setRange(-1e300, +1e300); + magBox->setValue(0.1234); + magBox->setSuffix(" V"); + + phaseLabel = new QLabel; + phaseLabel->setText("Phase"); + + phaseBox = new QDoubleSpinBox; + phaseBox->setReadOnly(1); + phaseBox->setButtonSymbols(QAbstractSpinBox::NoButtons); + phaseBox->setRange(-1e300, +1e300); + phaseBox->setSuffix(QString::fromUtf8(" °")); + + setDecim(sc); + + int row(0); + layout->addWidget(scaleLabel, row, 0); + row++; + layout->addWidget(scaleBox, row, 0); + row++; + layout->addWidget(rpLabel, row, 0); + layout->addWidget(ipLabel, row, 1); + row++; + layout->addWidget(rpLabel, row, 0); + layout->addWidget(ipLabel, row, 1); + row++; + layout->addWidget(rpBox, row, 0); + layout->addWidget(ipBox, row, 1); + row++; + layout->addWidget(magLabel, row, 0); + layout->addWidget(phaseLabel, row, 1); + row++; + layout->addWidget(magBox, row, 0); + layout->addWidget(phaseBox, row, 1); +} + +blockOfIndicators::blockOfIndicators(myWindow* topwin, myPlot* plot) +{ + group = new QGroupBox; + group->setFlat(0); + group->setStyleSheet("border:0;"); + layout = new QVBoxLayout; + indicator* temp; + group->setLayout(layout); + + temp = new indicator(plot, 1/* phaselock */); + topwin->ndc8r.push_back(temp); + layout->addWidget(temp->group); + + temp = new indicator(plot); + topwin->ndc8r.push_back(temp); + layout->addWidget(temp->group); + + temp = new indicator(plot); + topwin->ndc8r.push_back(temp); + layout->addWidget(temp->group); + + layout->insertStretch(-1); +} + +void indicator::setIndication(const double rp0, const double ip0, + const double rp1, const double ip1){ + double rp(rp1-rp0); + double ip(ip1-ip0); + double mag(sqrt(rp*rp + ip*ip)); + double phase(0); + if (mag) phase = atan2(ip, rp); + rpBox->setValue(rp); + ipBox->setValue(ip); + magBox->setValue(mag); + double phDeg = phase * 180 / M_PI; + phaseBox->setValue(phDeg); + if (locker) lockerPhase = phDeg; + if (plot) { + double denom = scaleBox->value(); // volts per division + denom *= plot->divsPerUnit; // volts full scale + plot->setReading(plotcur, rp0/denom, ip0/denom, rp1/denom, ip1/denom); + } +} + +void myPlot::flush() { + if (need_replot) replot(); +} + +void myPlot::setReading(const int ndx, + const double rp0, const double ip0, + const double rp1, const double ip1){ + int npts(2); + double xxx[npts]; + double yyy[npts]; + xxx[0] = rp0; + yyy[0] = ip0; + xxx[1] = rp1; + yyy[1] = ip1; +////////////////// curve[ndx]->setData(xxx, yyy, npts); + curve[ndx]->setSamples(xxx, yyy, npts); + need_replot = 1; +} + +// There is no such thing as a scale change on the plot. +// Rescale the data instead. +#ifdef OLD_SCALE_IDEA +void myPlot::scaleChange(double newScale){ + setAxisScale(QwtPlot::xBottom, -newScale, newScale); + setAxisScale(QwtPlot::yLeft, -newScale, newScale); + replot(); +} +#endif + +// Unless you set keyboardTracking to false, +// this is useless when typing in digits : +// signals too early and too often. +void refOut_grouper::freqChanged(double /* newfreq not used */) +{ + topwin->actualFreqs(); +} + +void entrails_grouper::VoChanged(double newVo) +{ + VoCal_dB = newVo; +//wrong topwin->ctrlCol->refOutGroup->VBox-> +//wrong setValue(pow(10., (refOutAmp_dB + VoCal_dB)/20.)); +} + +void entrails_grouper::ViChanged(double newVi) +{ + ViCal_dB = newVi; +} + +void refOut_grouper::ampChanged(double newAmp) +{ + refOutAmp_dB = newAmp; +//wrong: VBox->setValue(pow(10., (refOutAmp_dB + VoCal_dB)/20.)); + VBox->setValue(pow(10., (refOutAmp_dB)/20.)); +} + +void entrails_grouper::rateChanged(double /* newRate not used */) +{ + topwin->actualFreqs(); +} + +// beware the fmod of a negative number is negative +double pval(const double angle){ + double rslt = fmod(angle, 360.); + if (rslt > 180.) rslt -= 360.; + if (rslt < -180.) rslt += 360.; + return rslt; +} + +void entrails_grouper::timeShiftChanged(double newTimeShift) +{ +//-- std::cout << "timeShiftChange: " << newTimeShift << std::endl; + +// Calculate new phase such that changing the timeShift +// doesn't change the phase; it is only supposed to +// change d(phase)/d(frequency). + double newPhase = phaseShift - (newTimeShift - timeShift) * actOutFreq * 360.; + newPhase = pval(newPhase); + timeShift = newTimeShift; + phaseShiftBox->setValue(newPhase); +} + +void entrails_grouper::phaseShiftChanged(double newPhaseShift) +{ +//-- std::cout << "phaseShiftChange: " << newPhaseShift << std::endl; + + while (newPhaseShift > 360.) newPhaseShift -= 360.; + while (newPhaseShift < -360.) newPhaseShift += 360.; + + phaseShift = newPhaseShift; +} + +class bad_thing: public std::exception{ + const char* msg; + virtual const char* what() const throw() { + return msg; + } +public: + bad_thing(const char* _msg) + : msg(_msg) {} +}; + +double max(const std::valarray<double>& foo){ + unsigned int howmany(foo.size()); + if (howmany == 0) throw bad_thing("max of empty list"); + double rslt = foo[0]; + for (unsigned int ii = 1; ii < howmany; ii++) { + rslt = std::max(rslt, foo[ii]); + } + return rslt; +} + +double min(const std::valarray<double>& foo){ + unsigned int howmany(foo.size()); + if (howmany == 0) throw bad_thing("min of empty list"); + double rslt = foo[0]; + for (unsigned int ii = 1; ii < howmany; ii++) { + rslt = std::min(rslt, foo[ii]); + } + return rslt; +} + +void entrails_grouper::tweakButtonClicked(){ + using namespace std; + double rawPhase = (lockerPhase + phaseShift) + + actOutFreq*timeShift * 360.; + + if (tweakPtr >= 10) tweakPtr = 0; + tweakFreq[tweakPtr] = actOutFreq; + tweakPhase[tweakPtr] = rawPhase; + tweakPtr++; + if (howmany < tweakPtr) howmany = tweakPtr; + // otherwise howmany stays at its maximum, i.e. tweakSize. + + valarray<double> myFreq(&tweakFreq[0], howmany); + valarray<double> myPhase(&tweakPhase[0], howmany); + + double big = max(myFreq); + double little = min(myFreq); + if (big == little) { + double newPhase = myPhase.sum() / howmany - timeShift*myFreq[0]*360.; +// This will change the value in the box, and raise +// the valueChanged signal: + phaseShiftBox->setValue(pval(newPhase)); + } else do { + valarray<double> cookedPhase(myPhase); + valarray<double> cookedFreq(myFreq); + for (unsigned int ii = 0; ii < howmany; ii++) { + cookedFreq[ii] -= actOutFreq; + cookedPhase[ii] -= phaseShift + timeShift*myFreq[ii]*360.; + cookedPhase[ii] = pval(cookedPhase[ii]); + } + if (max(cookedPhase) > 90. || min(cookedPhase) < -90.) { + cout << "Phase range too big; can't tweak." << endl; + break; + } + double intercept, slope; + double cv00, cv01, cv11; + double sumsq; + gsl_fit_linear(&cookedFreq[0], 1, + &cookedPhase[0], 1, howmany, + &intercept, &slope, + &cv00, &cv01, &cv11, + &sumsq); + + double newTime = timeShift + slope / 360.; + double newPhase = phaseShift + intercept + - (newTime - timeShift) * actOutFreq * 360.; +// must do time shift first: + timeShiftBox->setValue(newTime); + phaseShiftBox->setValue(pval(newPhase)); + + } while (0); +} + +void myWindow::actualFreqs() { + using namespace std; + double d_out_freq = ctrlCol->refOutGroup->freqBox->value(); + { // calculatte cycles per krunch period + double d_krunch_rate = ctrlCol->entrailsGroup->rateBox->value(); + if (d_krunch_rate == 0.) cpkp = 1; + else cpkp = round(d_out_freq/d_krunch_rate); + if (cpkp < 1.) cpkp = 1.; + } +// frames per krunch period: + fpkp = round(cpkp * pcmRate / d_out_freq); + double actUp = pcmRate / fpkp; + ctrlCol->entrailsGroup->actRateBox->setValue(actUp); + ctrlCol->entrailsGroup->pcmBox->setValue(pcmRate); + + actOutFreq = cpkp * actUp; + ctrlCol->refOutGroup->actFreqBox->setValue(actOutFreq); + if (verbosity > 0) { + cout.precision(4); + cout << fixed; + cout << " d_out_freq: " << d_out_freq + << " cpkp: " << cpkp + << " fpkp: " << fpkp + << " actUp: " << setw(10) << actUp + << " actOutFreq: " << actOutFreq + << endl; + } +} + +myPlot::myPlot(const QString name) : QwtPlot(QwtText(name)), + grid(), divsPerUnit(5), need_replot(0) + { +// FIXME : should calculate these sizes: + setMinimumWidth(500); setMaximumWidth(500); + setMinimumHeight(500); setMaximumHeight(500); + +// Set up axis, permanently -1 to 1 in both directions. +// We are ASSUMING the library function will give us +// five minor divisions per unit (ten total). + setAxisScale(QwtPlot::xBottom,-1.0, 1.0, 1.); + setAxisScale(QwtPlot::yLeft, -1.0, 1.0, 1.); + enableAxis(QwtPlot::xBottom, 0); + enableAxis(QwtPlot::yLeft, 0); + setStyleSheet("border:0;"); + + grid.enableXMin(true); + grid.enableYMin(true); + grid.setMajorPen(QPen(Qt::white, 2)); + grid.setMinorPen(QPen(Qt::white, 1)); +// was: (QPen(Qt::white, 1, Qt::DotLine)); + grid.attach(this); +} + +int myPlot::assign_curve(){ + int ndx = curve.size(); + + curve.push_back(new QwtPlotCurve); + + int npts(2); + double xxx[npts]; + double yyy[npts]; + xxx[0] = 0; + yyy[0] = 0; + xxx[1] = sin(.1 * ndx); + yyy[1] = cos(.2 * ndx); + +///////////// curve[ndx]->setData(xxx, yyy, npts); + curve[ndx]->setSamples(xxx, yyy, npts); + curve[ndx]->setPen(QPen(color_list[ndx % color_list_size].code, 4)); + curve[ndx]->attach(this); + + return ndx; +} + +//////////////////////////////////// + +timer::timer(QWidget *parent) +: QWidget(parent) +{ + startTimer(33); // 30.303 repaints per second +} + +void timer::timerEvent(QTimerEvent* /* event not used */){ + using namespace std; + repaintFrame = 1; +} diff --git a/src/gui.h b/src/gui.h new file mode 100644 index 0000000..92d0bdc --- /dev/null +++ b/src/gui.h @@ -0,0 +1,13 @@ +#ifndef GUI__H +#define GUI__H +extern double actOutFreq; +extern double refOutAmp_dB; +extern double VoCal_dB; +extern double ViCal_dB; +extern double cpkp; +extern double fpkp; +extern int pcmRate; +extern double timeShift; +extern double phaseShift; +extern int repaintFrame; +#endif /* GUI__H */ diff --git a/src/gui_class.cxx b/src/gui_class.cxx new file mode 100644 index 0000000..ea69d35 --- /dev/null +++ b/src/gui_class.cxx @@ -0,0 +1,5 @@ +#include "gui_class.h" + +void foobar() { + ctrl_column foobar(0); +} diff --git a/src/gui_class.h b/src/gui_class.h new file mode 100644 index 0000000..2b17ff3 --- /dev/null +++ b/src/gui_class.h @@ -0,0 +1,217 @@ +#ifndef GUI_CLASS__H +#define GUI_CLASS__H + +#include <cmath> +#include <vector> +#include <valarray> +#include <QApplication> +#include <QTextEdit> +#include <QtGui> +#include <QLabel> + +#include <qwt_plot.h> +#include <qwt_plot_curve.h> +#include <qwt_plot_grid.h> + +#include <QDoubleSpinBox> +#include <QGroupBox> +#include <QGridLayout> +#include <QPushButton> + +class myWindow; // forward reference +class indicator; + +//////////////////////////////////////// +class myPlot : public QwtPlot { +public: + std::vector<QwtPlotCurve*> curve; + QwtPlotGrid grid; + int divsPerUnit; + int need_replot; + + myPlot(const QString); +// the following could be a slot, but doesn't need to be + void setReading(const int, const double, const double, + const double, const double); + int assign_curve(); + void flush(); +}; + +//////////////////////////////////////// +class scaleBoxer : public QDoubleSpinBox { +public: + indicator* parent; + virtual void stepBy(int); + + scaleBoxer(indicator* parent); +}; + +//////////////////////////////////////// +class indicator : public QWidget { + + Q_OBJECT; + +// Note: Q_SLOTS is #defined to be nothing. +// Slots are handlers for inbound signals. +public Q_SLOTS: + void setIndication(const double, const double, const double, const double); + +public: + QGroupBox* group; + QGridLayout* layout; + QLabel* rpLabel; + QDoubleSpinBox* rpBox; + QLabel* ipLabel; + QDoubleSpinBox* ipBox; + QLabel* magLabel; + QDoubleSpinBox* magBox; + QLabel* phaseLabel; + QDoubleSpinBox* phaseBox; + QLabel* scaleLabel; + scaleBoxer* scaleBox; + myPlot* plot; + int plotcur; // which "curve" to use within the plot + int locker; + + indicator(myPlot* plot, int locker = 0); + void setDecim(const double newval); +}; + +//////////////////////////////////////// +class blockOfIndicators : public QWidget { +public: + QGroupBox* group; + QVBoxLayout* layout; + + blockOfIndicators(myWindow*, myPlot*); +}; + +//////////////////////////////////////// +class rslt_panel : public QWidget { +public: + myWindow* topWin; + QGroupBox* group; + QHBoxLayout* layout; + blockOfIndicators* block; + myPlot* plot; + + rslt_panel(myWindow*); +}; + +//////////////////////////////////////// +class entrails_grouper : public QWidget { + + Q_OBJECT + +public Q_SLOTS: + void VoChanged(double); + void ViChanged(double); + void rateChanged(double); + void timeShiftChanged(double); + void phaseShiftChanged(double); + void tweakButtonClicked(); + +public: + myWindow* topwin; + QGroupBox* group; + QVBoxLayout* layout; + + QLabel* VoCalLabel; + QDoubleSpinBox* VoCalBox; + QLabel* ViCalLabel; + QDoubleSpinBox* ViCalBox; + + QLabel* rateLabel; + QDoubleSpinBox* rateBox; // goal rate + + QLabel* actRateLabel; + QDoubleSpinBox* actRateBox; // actual rate + QLabel* pcmLabel; + QDoubleSpinBox* pcmBox; + QLabel* timeShiftLabel; + QDoubleSpinBox* timeShiftBox; // loopback time delay + QLabel* phaseShiftLabel; + QDoubleSpinBox* phaseShiftBox; // loopback phase lag + QPushButton* tweakButton; + + static const int tweakSize = 10; + std::valarray<double> tweakFreq; + std::valarray<double> tweakPhase; + unsigned int tweakPtr; + unsigned int howmany; + + entrails_grouper(myWindow*); +}; + +//////////////////////////////////////// +class refOut_grouper : public QWidget { + Q_OBJECT + +public Q_SLOTS: + void freqChanged(double); + void ampChanged(double); + +public: + myWindow* topwin; + QGroupBox* group; + QVBoxLayout* layout; + QLabel* freqLabel; + QDoubleSpinBox* freqBox; // goal frequency + QLabel* actFreqLabel; + QDoubleSpinBox* actFreqBox; // actual frequency + QLabel* dBLabel; + QDoubleSpinBox* dBBox; // amplitude in dB + QLabel* VLabel; + QDoubleSpinBox* VBox; // amplitude in V + + refOut_grouper(myWindow*); +}; + +//////////////////////////////////////// +class ctrl_column : public QWidget { + Q_OBJECT; +public: + QGroupBox* group; + QVBoxLayout* layout; + refOut_grouper* refOutGroup; + entrails_grouper* entrailsGroup; + + ctrl_column(myWindow* _parent); +// hack +// something trivial we can instantiate, to persuade the compiler to +// instantiate (emit) "vtables" for everything in this file: + ctrl_column(int){}; +}; + +////////////////////////////////////// + +class timer : public QWidget{ +public: + timer(QWidget *parent = 0); + void timerEvent(QTimerEvent *event); +}; + +//////////////////////////////////////// +class myWindow : public QWidget { + Q_OBJECT + +public: + myWindow(); + QHBoxLayout* topLayout; + + QDoubleSpinBox* updRateBox; + QLabel* updRateLabel; + + static const int numRsltPanels = 1; + ctrl_column* ctrlCol; + rslt_panel* rsltPanel[numRsltPanels]; + std::vector<indicator*> ndc8r; + timer* frameTimer; + + void actualFreqs(); + +public slots: + void flush(); +}; + +#endif /* GUI_CLASS__H */ diff --git a/src/iir_bp.cxx b/src/iir_bp.cxx new file mode 100644 index 0000000..b742dd1 --- /dev/null +++ b/src/iir_bp.cxx @@ -0,0 +1,133 @@ +#include "iir_bp.h" + +//////////////////////////////////////// +// negative of a vector, component-by-component: +// +std::vector<C> neg(const std::vector<C>& foo) { + using namespace std; + int siz = foo.size(); + vector<C> rslt(siz); + for (int ii = 0; ii < siz; ii++) { + rslt[ii] = -foo[ii]; + } + return rslt; +} + +//////////////////////////////////////// +// expand_poly - multiplies a set of binomials together and returns +// the coefficients of the resulting polynomial. +// +// All polynomials (input and output) are represented using the +// /reduced/ representation, meaning the coefficient of the highest +// power of x is assumed to be 1, and is not included in the +// representation. +// +// The multiplication has the following form: +// +// (x+c[0]) * (x+c[1]) *...* (x+c[n-1]) +// +// On input, the c[i] is the coefficient of x^0 in the ith binomial. +// Each c[i] is a complex number. +// +// The resulting polynomial has the following form: +// +// x^n + a[0]*x^n-1 + a[1]*x^n-2 +...+ a[n-2]*x + a[n-1] +// +// The a[i] coefficients can in general be complex but in typical +// digital-filter applications should turn out to be real. +// +std::vector<C> expand_poly(const std::vector<C>& cvec) { + int n = cvec.size(); + + std::vector<C> a(n); + for (int ii = 0; ii < n; ii++) a[ii] = 0.; + + for (int i = 0; i < n; ++i) { + for (int j = i; j > 0; --j) a[j] += cvec[i] * a[j-1]; + a[0] += cvec[i]; + } + return a; +} + +//////////////////////////////////////// +// Calculate poles for Butterworth low pass filter. +// +std::vector<C> dpole_bwlp (int n, double fcf, double extraPole) +{ + using namespace std; + + int needed(n); + if (extraPole) needed += 2; + vector<C> dpole(needed); + + double theta = 2. * M_PI * fcf; + double st = sin(theta); + double ct = cos(theta); + + for (int k = 0; k < n; ++k) { + // angle, as seen from (1,0) + // as we go around the small circlet of poles: + double polang = M_PI * (double)(2*k+1)/(double)(2*n); + double denom = 1.0 + st*sin(polang); + dpole[k] = C(ct/denom, st*cos(polang)/denom); + } + if (extraPole) { + double thetaZero = 2. * M_PI * extraPole; + dpole[n] = C(cos(thetaZero), sin(thetaZero)); + dpole[n+1] = C(cos(thetaZero), -sin(thetaZero)); + } + return dpole; +} + +//////////////////////////////////////// +// return the real part of a vector of complex numbers +// also convert from reduced to non-reduced representation, +// by inserting rslt[0] = 1 and shifting everything one place +std::vector<double> rxpoly(const std::vector<C>& foo){ + std::vector<double> rslt(1+foo.size()); + + rslt[0] = 1.; + for (unsigned int k = 0; k < foo.size(); ++k) + rslt[1+k] = foo[k].real(); + return (rslt); +} + + +//////////////////////////////////////// +// calculate the d coefficients for a butterworth lowpass filter. +// +// Returns result in the non-reduced representation, +// i.e. a vector of n+1 doubles, +// where dcof[0] is the coefficient in front of x^n +// However, dcof[0] will always be 1.0. +// +std::vector<double> dcof_bwlp (int n, double fcf, double extraPole) { + return rxpoly(expand_poly(neg(dpole_bwlp(n, fcf, extraPole)))); +} + +//////////////////////////////////////// +// calculate the c coefficients for a butterworth lowpass filter. +// details same as above. +// +std::vector<double> ccof_bwlp(const int n, const double extraZero) { + return rxpoly(expand_poly(neg(croot_bwlp(n, extraZero)))); +} + +//////////////////////////////////////// +std::vector<C> croot_bwlp (int n, double extraZero) { + using namespace std; + + int needed(n); + if (extraZero) needed += 2; + vector<C> dpole(needed); + + for (int k = 0; k < n; ++k) { + dpole[k] = -1; + } + if (extraZero) { + double thetaZero = 2. * M_PI * extraZero; + dpole[n] = C(cos(thetaZero), sin(thetaZero)); + dpole[n+1] = C(cos(thetaZero), -sin(thetaZero)); + } + return dpole; +} diff --git a/src/iir_bp.h b/src/iir_bp.h new file mode 100644 index 0000000..762c25c --- /dev/null +++ b/src/iir_bp.h @@ -0,0 +1,17 @@ +#ifndef IIR_BP_H +#define IIR_BP_H + +#include <vector> +#include <complex> + +typedef std::complex<double> C; + +std::vector<C> neg(const std::vector<C>& foo); +std::vector<C> expand_poly(const std::vector<C>& cvec); +std::vector<double> rxpoly(const std::vector<C>& foo); +std::vector<C> dpole_bwlp( int n, double fcf, double extraPole=0.); +std::vector<double> dcof_bwlp( int n, double fcf, double extraPole=0.); +std::vector<double> ccof_bwlp(const int n, const double extraZero=0.); +std::vector<C> croot_bwlp (int n, double extraZero=0.); + +#endif diff --git a/src/krunch.cxx b/src/krunch.cxx new file mode 100644 index 0000000..a7adf8f --- /dev/null +++ b/src/krunch.cxx @@ -0,0 +1,308 @@ +#include "krunch.h" +#include "thrower.h" +#include <valarray> +#include <iostream> +#include <iomanip> +#include "refout.h" /* for class ref_to_krunch */ +#include "gui.h" +//??? #include "iir_bp.h" +#include "bad_thing.h" +//?? #include "biquad.h" + +// forward reference: +void dumpit_cout(const double rp, const double ip); + +// typical usage: +//?? biquad butterworth({1., 2., 1.}, +//?? {1., -1.99911142347079540116, 0.99911181807963833634}); + +////////////////////////////////////////////////////////////////////// +// main entry point for krunch job +// +void* krunch(void* _arg){ + using namespace std; + krunch_arg* arg((krunch_arg*) _arg); + + int num_indic = arg->topwin->ndc8r.size(); + + thrower* toss = new thrower[num_indic]; + + thrower* flushy = new thrower; + + for (int ii = 0; ii < num_indic; ii++) { + QMetaObject::Connection rslt = QObject::connect( + &toss[ii], SIGNAL(_newReading(double,double, double,double)), + arg->topwin->ndc8r[ii], + SLOT(setIndication(double,double, double,double))); + + if (!rslt) { + cout << "Krunch: Failed to connect, column " << ii + << " rslt: " << rslt << endl; + exit(3); + } + } + + { + QMetaObject::Connection rslt = QObject::connect( flushy, SIGNAL(_flush()), + arg->topwin, SLOT(flush()) ); + + if (!rslt) { + cout << "Krunch: Failed to connect flush: " + << rslt << endl; + exit(3); + } + } + + alsa_pcm* pcm = arg->pcm; + int nframe = arg->setup->nframe; + + int left_idx = 0; + int right_idx = 1; + +/// Kludge test: left_idx = 10; right_idx = 11; +// Be careful to index outside array boundaries: + int topchan = pcm->nchan - 1; + if (left_idx > topchan) left_idx = topchan; + if (right_idx > topchan) right_idx = topchan; + +#if 0 + if (snd_pcm_state(arg->otherpcm->phndl) != SND_PCM_STATE_RUNNING) { + cout << "Krunch waits: " + << arg->otherpcm->alsa_state_name() + << endl; + } +#endif + while (snd_pcm_state(arg->otherpcm->phndl) != SND_PCM_STATE_RUNNING) { + usleep(1000); + } + + double left_real(0); + double left_ineg(0); // the *negative* of the imaginary part + double right_real(0); + double right_ineg(0); // the *negative* of the imaginary part + +//#define PHASE_CHANGE +#ifdef PHASE_CHANGE + double old_phase(0); +#endif + double old_cpkp(0), old_fpkp(0); + double theta(0); + double dtheta(0); + int kperno(0); + +////////////////// +// main krunch loop +// +// this is a SINGLE loop over TWO variables; +// the cap_buffer is filled in units of nframe +// and emptied (processed) in units of fpkp. +// Parallel code appears in refout.cxx + int cap_end(nframe * pcm->nchan); + valarray<datum> cap_buffer(cap_end); + int cap_idx(cap_end); + int ref_end(123456); + int ref_idx(ref_end); + double decalage(0); + + for (;;) { // loop over all samples + +// loop control for capture (read) process: + if (cap_idx >= nframe) { + int didread = arg->setup->read_stuff(pcm, &cap_buffer[0]); + if (didread != nframe) { + fprintf(stderr, "Krunch: ignoring peculiar buffer size %d not %d\n", + didread, nframe); + continue; + } + cap_idx = 0; + } + +// Loop control for reference process. +// Note that frequency and phase-offset only changes at +// the boundary between krunch periods. + if (ref_idx == ref_end) { + if (kperno == 0) { + decalage = arg->otherpcm->ss_time() - pcm->ss_time(); + } + +#if 0 + cout << "declalage: " << decalage + << " samples: " << decalage * pcm->rate + << " skipme: " << skipme + << endl; +#endif + ref_to_krunch kper; + int didread = read(arg->pipefd, &kper, sizeof(kper)); + if (didread != sizeof(kper)) { + fprintf(stderr, "Krunch: could not read from pipe: "); + perror(0); + exeunt(1); + } + if (kperno != kper.period) { + fprintf(stderr, "Krunch: phase error: expecting %d got %d\n", + kperno, kper.period); + exeunt(1); + } + double freq = double(pcm->rate) * double(kper.cpkp) + / double(kper.fpkp); +// the phase offset: + theta = (decalage + timeShift) * freq * 2. * M_PI; + theta += phaseShift * M_PI / 180.; +#ifdef PHASE_CHANGE + if (theta != old_phase){ + cout << "Krunch: new phase: " << theta + << " decalage: " << decalage + << " timeShift: " << timeShift + << " phaseShift: " << timeShift + << " freq: " << freq + << endl; + old_phase = theta; + } +#endif + left_real = left_ineg = 0.; + right_real = right_ineg = 0.; + ref_idx = 0; + ref_end = kper.fpkp; + dtheta = 2. * M_PI * kper.cpkp / kper.fpkp; + // cpkp == cycles per krunch period + // fpkp == frames per krunch period + if (old_cpkp != kper.cpkp || old_fpkp != kper.fpkp){ +#if 0 + cout.precision(6); + cout << fixed; + cout << "Krunch switching to: " << freq + << setprecision(1) + << " cpkp: " << kper.cpkp + << " fpkp: " << kper.fpkp + << " dtheta: " << setprecision(20) << dtheta + << endl; +#endif + old_cpkp = kper.cpkp; + old_fpkp = kper.fpkp; + } + kperno++; + } + double left = cap_buffer[cap_idx * pcm->nchan + left_idx ]; + double right = cap_buffer[cap_idx * pcm->nchan + right_idx]; + + left_real += left * cos(theta); + left_ineg += left * sin(theta); + + right_real += right * cos(theta); + right_ineg += right * sin(theta); + + theta += dtheta; + cap_idx++; + ref_idx++; + +// Do a little post-processing. +// This does not replace or even affect the +// loop-control above. + if (ref_idx >= ref_end) { + +// put in the minus sign here, to convert ineg to the +// actual imaginary part with the proper sign. + do { + double unit = pow(10., ViCal_dB / 20.); + unit /= full_swing; +// account for the number of points of points in this krunch period: + unit /= double(ref_end); +// account for the fact that integral(sin^2) is only 0.5, not 1: + unit *= 2.; + if (num_indic <= 0) break; + toss[0].newReading(0,0, left_real * unit, -left_ineg * unit); + if (num_indic <= 1) break; + toss[1].newReading(0,0, right_real * unit, -right_ineg * unit); + if (num_indic <= 2) break; + toss[2].newReading(right_real * unit, -right_ineg * unit, + left_real * unit, -left_ineg * unit); + } while (0); + flushy->flush(); + } + } + // should never reach here + return 0; +} + +//////////////////// +void dumpit_cout(const double rp, const double ip){ + using namespace std; + cout.precision(4); + int wid(12); + double mag(sqrt(rp*rp + ip*ip)); + double phase(0); + if (mag) phase = atan2(ip, rp); + cout + << " : " << fixed << setw(wid) << rp + << " " << fixed << setw(wid) << ip + << " " << fixed << setw(wid) << mag + << " " << fixed << setw(wid) << phase * 180 / M_PI + << endl; +} + +// Just like snd_pcm_readi(), +// except the buff is always of the type of datum (int32_t), +// no matter what type the raw hardware uses. +// Return value: +// nonnegative: # of frames read +// negative: error code +int readi(const alsa_pcm* pcm, datum* buff, const int nframe){ + using namespace std; + if (pcm->format == SND_PCM_FORMAT_S32_LE) { + return snd_pcm_readi(pcm->phndl, buff, nframe); + } else if (pcm->format == SND_PCM_FORMAT_S16_LE) { + valarray<int16_t> tmp(nframe * pcm->nchan); + int rslt = snd_pcm_readi(pcm->phndl, &tmp[0], nframe); + if (rslt <= 0) return rslt; + int16_t* from (&tmp[0]); + datum* to (buff); + int fudge(1<<16); + for (unsigned int ii = 0; ii < rslt * pcm->nchan; ii++){ + *to++ = *from++ * fudge; + } + return rslt; + } else { + cerr << "Don't know how to convert pcm data from format " + << snd_pcm_format_name(pcm->format) << endl; + exeunt(1); + } + return 0; // defeat stupid compiler warning +} + +//////////////////////////////////////// +// Read some data and maybe write checkfile +int snark::read_stuff(alsa_pcm* pcm, datum* cap_buffer){ + + int sts; + int frames_read; + + for (;;){ // keep trying to read + frames_read = readi(pcm, &cap_buffer[0], nframe); + if (frames_read == nframe) break; // good! + if (frames_read > 0 || + frames_read == -EPIPE) { // recover from overruns + if (xrun_verbosity) fprintf(stderr, + "Read_stuff overrun: requested %i got %i: %s" NL, + nframe, frames_read, snd_strerror(frames_read)); + snd_pcm_prepare(pcm->phndl); + } else { // unrecoverable error + fprintf(stderr, "Read_stuff requested %i got %i: %s" NL, + nframe, frames_read, snd_strerror(frames_read)); + exeunt(1); + } + } + + int gotbytes = snd_pcm_frames_to_bytes(pcm->phndl, frames_read); + + if (wcheck_fd >= 0) { + sts = write(wcheck_fd, &cap_buffer[0], gotbytes); + if (sts != gotbytes) { + fprintf(stderr, "Write error on raw output file (%i): %m" NL, wcheck_fd); + exeunt(1); + } + close(wcheck_fd); + wcheck_fd = -1; + } + + return frames_read; +} diff --git a/src/krunch.h b/src/krunch.h new file mode 100644 index 0000000..f24413b --- /dev/null +++ b/src/krunch.h @@ -0,0 +1,24 @@ +#ifndef KRUNCH__H +#define KRUNCH__H + +#include "gui_class.h" +#include "lockin.h" + +void* krunch(void* _arg); // thread entry point + +class krunch_arg { +public: + myWindow* topwin; + alsa_pcm* pcm; + alsa_pcm* otherpcm; + snark* setup; + int pipefd; + krunch_arg(myWindow* const _topwin, + alsa_pcm* const _pcm, alsa_pcm* const _otherpcm, + snark* const _setup, const int _pipefd) + : topwin(_topwin), pcm(_pcm), otherpcm(_otherpcm), + setup(_setup), pipefd(_pipefd) + {} +}; + +#endif /* KRUNCH__H */ 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; +} diff --git a/src/lockin.h b/src/lockin.h new file mode 100644 index 0000000..9a6a515 --- /dev/null +++ b/src/lockin.h @@ -0,0 +1,99 @@ +#ifndef LOCKIN_H +#define LOCKIN_H +#include <limits.h> /* for INT_MAX */ +#include <valarray> +#include <cmath> +#include <string> +#include "alsa_pcm.h" + +// used when converting between syslog and fprintf: +#define NL "\n" + +const double two16(1<<16); +const double two32(two16*two16); +const int bPB(8); // bits per byte +void exeunt(int signum); +std::string invent_driverfile(const std::string justcard); +std::string invent_ctlfile(const std::string justcard); + +typedef int32_t datum; // all internal calculations are 32bit + +const double pi(M_PI); +const double kT(1.38e-23 * (273.15 + 20)); +const double twopi = 2 * pi; + +// reference level is saturation minus 15 dB +const double full_swing(INT_MAX); + +// sine wave RMS is sqrt(2) less than peak: +const double full_swing_2(full_swing/sqrt(2.0)); + +// some non-constant global variables, +// for inter-thread communication: +extern std::string progname; +extern int verbosity; +extern timespec prog_start_time; +extern int xrun_verbosity; // be verbose about underrun and overrun +// +// Note: more globals are found in gui.h + +inline double operator-(const timespec a, const timespec b){ + return (a.tv_sec-b.tv_sec) + (a.tv_nsec - b.tv_nsec)*1e-9; +} + +class Keeper{ +public: + const alsa_pcm* pcm; + const int fpp; // frames per period + std::valarray<datum> buf; + int finbuf; // frames in buffer at the moment + int nchan; + Keeper(const alsa_pcm*, const int fsize); +}; + +snd_pcm_sframes_t snd_pcm_writei(Keeper& kk, const datum* buffer, + snd_pcm_uframes_t nframes); + + +class snark { +public: + snark(); // constructor + std::string card_device; // principal input, e.g. "0,0" + std::string wcheck_fname; // place to write raw data (checkfile) + int nframe; + int onepass; // exit after one pass + int desired_rate; + int channel_mask; + double kout; // output voltage scale, assuming sine-wave + // ... which has RMS 3dB below full_swing + double zref; // reference resistor + double reffreq; + double refival; + double refamp; + std::string mixer_ctlfile; + + +// not commandline options, but derived therefrom: + int wcheck_fd; + std::string justcard; // the "card" part of that, before the comman + double sps; // entropy (bits per sample) + double snooze; // time to snooze between actions if not + // actually writing to output fifo, + // e.g. during calibration. + +//// functions + void usage(const int err); + void cmdline(int argc, char** argv); + void open_aux_io(); + int read_stuff(alsa_pcm*, datum*); + void printx( + const int colored, + const char* name, + const double foo_1, + const double foo_2, + double plogp, + const alsa_pcm* pcm + ); +}; + +#endif diff --git a/src/makefile b/src/makefile new file mode 100644 index 0000000..5ae971e --- /dev/null +++ b/src/makefile @@ -0,0 +1,16 @@ + + +all: + cmake --build bin + +clean: + cd bin; make -f Makefile clean + +shipme := $(shell cat sources.txt) makefile bin CMakeLists.txt sources.txt + +togo : ALWAYS + echo $(shipme:%=src/%) > $@ + +.PHONY: ALWAYS + +##tar -c --no-rec $(shipme) | gzip > $@ diff --git a/src/refout.cxx b/src/refout.cxx new file mode 100644 index 0000000..3642380 --- /dev/null +++ b/src/refout.cxx @@ -0,0 +1,247 @@ +using namespace std; +#include <iostream> +#include <iomanip> +#include <unistd.h> +#include "refout.h" +#include "lockin.h" +#include "gui.h" +#include <alsa/asoundlib.h> +#ifdef TUNER +# include <sys/ioctl.h> /* for ioctl, TCGETA */ +# include <termios.h> /* for ECHO */ +#endif + +datum smash(double foo) { + if (foo > INT_MAX) return INT_MAX; + if (foo < -INT_MAX) return -INT_MAX; + return datum(foo); +} + +#ifdef TUNER + +int fn = fileno(stdin); +string kbd("\t1q2we4r5t6yu8i9op-[=]"); +string keymap("C+D+EF+G+A+BC"); +const double halfstep(pow(2.0, 1.0/12.0)); +int octave(4); +int kbkey(9); // A4 = 440 Hz +int abskey(57); + +void dokey(int newkey){ + abskey = newkey; + ofreq = 440*pow(halfstep, abskey-57); + int myoct = abskey / 12; + int mykey = abskey - myoct*12; + string keyname; + char keylet = keymap[mykey]; + if (keylet == '+') { + keyname += keymap[mykey-1]; + keyname += '#'; + } else keyname += keylet; + + fprintf(stderr, "\r\033[K %2s%1d %6.2f\r", keyname.c_str(), myoct, ofreq); + fflush(stderr); +} + +int escmode(0); + +void chkey(){ + char cc; + int nn = read(fn, &cc, 1); + if (nn <= 0) return; + if (cc == 033) { + escmode = 1; + return; + } + if (escmode) { + if (cc == '~') escmode = 0; + if (cc != 'O' && isupper(cc)) escmode = 0; + if (cc == 'B' || cc == 'D') dokey(abskey-1); + else if (cc == 'A' || cc == 'C') dokey(abskey+1); + return; + } + string::size_type where = kbd.find(cc); + if (where != kbd.npos) { + kbkey = where; + } else if (cc == ',') { + octave--; + if (octave < 0) octave = 0; + } else if (cc == '.') { + octave++; + if (octave > 8) octave = 8; + } else { + int foo; + foo = cc; + fprintf(stderr, "%04o\n", foo); + return; + } + dokey(12*octave + kbkey); +} + +void setkey(){ + kbd += char(127); // the "backspace" key + kbd += '\\'; + kbd += char(10); // the "return" key +} + +#else +inline void chkey(){} // does nothing +inline void setkey(){} // does nothing +#endif + +void* refout(void* _arg){ + + ref_arg* arg = (ref_arg*) _arg; + + setkey(); // FIXME (doesn't do anything at the moment) + + double reftime = .2; // # of seconds in output buffer + int refframe = int(arg->pcm->rate * reftime); // # of frames + int bufsize(refframe * arg->pcm->nchan); + + if (verbosity) cerr << "Refout starts, ofreq: " << actOutFreq + << " INT_MAX: " << INT_MAX + << " sizeof(datum): " << sizeof(datum) + << " refframe: " << refframe + << " bufsize: " << bufsize + << endl; + +#ifdef TUNER + { + termio save; + // If ioctl fails, we're probably not connected to a terminal. + int rslt; + rslt = ioctl(fn, TCGETA, &save); + termio t(save); + t.c_lflag &= ~ECHO; + t.c_lflag &= ~ICANON; + t.c_cc[VMIN] = 1; + t.c_cc[VTIME] = 0; + t.c_cc[VERASE] = 0; + + ioctl(fn, TCSETA, &t); + int temp(1); + ioctl(fn, FIONBIO, &temp); + } +#endif + + Keeper keep(arg->pcm, FPP); + + datum* ptr; + datum foo[2]; + int flip; + +///////////////// +// main refout loop +// +// this is a SINGLE loop over TWO variables; +// the cap_buffer is emptied in units of refframe +// and filled in units of fpkp. +// Parallel code appears in krunch.cxx + valarray<datum> refout_buffer(bufsize); + int out_idx(0); + int ref_end(fpkp); + int ref_idx(0); + double theta0(0); + double theta1(0); + double old_cpkp(0), old_fpkp(0); + double dtheta0(0); + double dtheta1(0); + int kperno(0); + + for (;;) { // loop over all samples + +// loop control for output (write) process + if (out_idx >= refframe) { + int todo = out_idx; + out_idx = 0; + datum* obuf_ptr(&refout_buffer[0]); + + while (todo) { // keep trying to get this buffer out + int rslt = snd_pcm_writei(keep, obuf_ptr, todo); + if (rslt == todo) break; // good! + if (rslt > 0) { // short write = harmless + // but should be rare + cout << "Refout: short write; probably harmless." << endl; + todo -= rslt; + obuf_ptr += rslt; + continue; + } + if (rslt == -EPIPE) { // explicit underrrun + if (xrun_verbosity) fprintf(stderr, + "Refout underrun: wrote %d got %d" NL, todo, rslt); + snd_pcm_prepare(arg->pcm->phndl); // recover from underruns + continue; + } + // some error we don't understand + fprintf(stderr, "Refout wrote %d got %d" NL, todo, rslt); + snd_pcm_prepare(arg->pcm->phndl); // try to recover + } + } + +// Loop control for reference process. +// Note that dtheta0 only changes at the boundary +// between krunch periods. + if (ref_idx == ref_end) { + theta0 = 0.; + // theta1 is free-running + ref_idx = 0; + ref_end = fpkp; + dtheta0 = 2. * M_PI * cpkp / fpkp; + // cpkp == cycles per krunch period + // fpkp == frames per krunch period + if (old_cpkp != cpkp || old_fpkp != fpkp){ + if (verbosity > 0) { + cout.precision(6); + cout << fixed; + double freq = arg->pcm->rate / fpkp * cpkp; + cout << "Refout switching to: " << freq + << setprecision(1) + << " cpkp: " << cpkp + << " fpkp: " << fpkp + << " dtheta: " << setprecision(20) << dtheta0 + << " rate: " << arg->pcm->rate + << endl; + } + old_cpkp = cpkp; + old_fpkp = fpkp; + } + } + if (ref_idx == 0) { + ref_to_krunch kper(kperno, int(cpkp), int(fpkp)); + int didwrite = write(arg->pipefd, &kper, sizeof(kper)); + if (didwrite != sizeof(kper)) { + fprintf(stderr, "refout: write to pipe failed:"); + perror(0); + exeunt(1); + } + kperno++; + } + + + dtheta1 = dtheta0 * arg->ival; + + chkey(); // FIXME + + double expo((refOutAmp_dB - VoCal_dB)/20); + if (expo > 100) expo = 100; + double gainFactor(pow(10, expo)); + double amplitude(full_swing * gainFactor); + + foo[0] = smash(amplitude * cos(theta0)); + foo[1] = smash(amplitude * cos(theta1)); + +// Fill all channels in frame (not just the first two): + ptr = &refout_buffer[out_idx * arg->pcm->nchan]; + for (unsigned int jj = 0; jj < arg->pcm->nchan; jj++) { + flip = jj & 1; // copy foo[0] or foo[1] + *ptr++ = foo[flip]; + } + + theta0 += dtheta0; + theta1 += dtheta1; + out_idx++; + ref_idx++; + } + return 0; +} diff --git a/src/refout.h b/src/refout.h new file mode 100644 index 0000000..b12e404 --- /dev/null +++ b/src/refout.h @@ -0,0 +1,32 @@ +#include "lockin.h" + +class ref_arg { +public: + double ival; + alsa_pcm* pcm; + int pipefd; + ref_arg(const double _ival, alsa_pcm* const _pcm, + const int _pipefd) + : ival(_ival), pcm(_pcm), + pipefd(_pipefd) + {} +}; + +void* refout(void* arg); + +// for communication with krunch: + +class ref_to_krunch{ +public: + int period; // sequence number + int cpkp; // cycles in this krunch period + int fpkp; // frames in this krunch period + +// trivial constructor: + ref_to_krunch(){} + +// nontrivial constructor: + ref_to_krunch(const int _period, const int _cpkp, const int _fpkp) + : period(_period), cpkp(_cpkp), fpkp(_fpkp) + {} +}; diff --git a/src/sources.txt b/src/sources.txt new file mode 100644 index 0000000..7bc1954 --- /dev/null +++ b/src/sources.txt @@ -0,0 +1,10 @@ +lockin.cxx +Getopt.cxx +alsa_pcm.cxx +gui.cxx +refout.cxx +krunch.cxx +biquad.cxx +iir_bp.cxx +gui_class.cxx +thrower.cxx diff --git a/src/thrower.cxx b/src/thrower.cxx new file mode 100644 index 0000000..9369ab9 --- /dev/null +++ b/src/thrower.cxx @@ -0,0 +1,12 @@ +#include "thrower.h" + + void thrower::newReading(const double rp0, const double ip0, + const double rp1, const double ip1) + { + emit _newReading(rp0, ip0, rp1, ip1); + } + + + void thrower::flush() { + emit _flush(); + } diff --git a/src/thrower.h b/src/thrower.h new file mode 100644 index 0000000..5e4c4fb --- /dev/null +++ b/src/thrower.h @@ -0,0 +1,27 @@ +#ifndef THROWER_H +#define THROWER_H + +#include <QApplication> + +///////////////////////////////// +// This exists only for the purpose of being something to throw. +// It is essentially private to krunch.cxx +// Apparently it has to exist in a .h file (not in krunch.cxx) +// because of the way the meta-object-compiler works. +// +class thrower : public QObject{ + + Q_OBJECT + +Q_SIGNALS: // outbound signals + void _newReading(const double, const double, const double, const double); + void _flush(); + +public: + void newReading(const double rp0, const double ip0, + const double rp1, const double ip1); + + void flush(); +}; + +#endif /* THROWER_H */ |