diff --git a/.gitignore b/.gitignore index b74766a..600ba39 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ Makefile.in *.log *.trs *.tar.gz +/packaging/tg-timer.spec # build directories /deb diff --git a/Makefile.am b/Makefile.am index e9ae9cd..aaee120 100644 --- a/Makefile.am +++ b/Makefile.am @@ -28,7 +28,8 @@ LIBS = $(GTK_LIBS) \ AM_CPPFLAGS = -DPROGRAM_NAME=\"Tg\" -DVERSION=\"$(PACKAGE_VERSION)\" tg_timer_dbg_CPPFLAGS = $(AM_CPPFLAGS) -DDEBUG -AM_CFLAGS = $(GTK_CFLAGS) \ +AM_CFLAGS = $(WARNINGFLAGS) \ + $(GTK_CFLAGS) \ $(GTHREAD_CFLAGS) \ $(PORTAUDIO_CFLAGS) \ $(FFTW_CFLAGS) diff --git a/README.md b/README.md index b366e4c..4048944 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,17 @@ You can now launch tg by typing Binary .deb packages can be downloaded from https://tg.ciovil.li +### Fedora, CentOS or other Redhat-based + +Binary RPM packages are available from https://copr.fedorainfracloud.org/coprs/tpiepho/tg-timer/ + +This COPR repository can be added to dnf's list with: +```sh +dnf copr enable tpiepho/tg-timer +``` +Then tg-timer can be installed with `dnf install tg-timer`, or with any dnf +based GUI package installer. + ## Compiling from sources The source code of tg can probably be built by any C99 compiler, however @@ -107,3 +118,11 @@ cd tg ./configure make ``` + +To build an RPM on Fedora or another RPM based distro, install the build +prerequisites and checkout the source as for compiling (above), then run +`rpmbuild` to create the RPM: + +```sh +rpmbuild --build-in-place -bb packaging/tg-timer.spec +``` diff --git a/configure.ac b/configure.ac index ed85057..d9d4def 100644 --- a/configure.ac +++ b/configure.ac @@ -5,6 +5,7 @@ AC_INIT([Tg], [tg_version], [vacaboja@gmail.com], [tg-timer], [https://github.co AM_INIT_AUTOMAKE([-Wall foreign subdir-objects]) AC_PROG_CC +AC_LANG(C) AC_CHECK_LIB([pthread], [pthread_mutex_init], [], [AC_MSG_ERROR([pthread not found])]) AC_CHECK_LIB([m], [sqrt], [], [AC_MSG_ERROR([libm not found])]) PKG_CHECK_MODULES([GTK], [gtk+-3.0 glib-2.0]) @@ -18,9 +19,12 @@ AM_CONDITIONAL([BE_WINDOWS], [test x$OS = xWindows_NT]) AM_COND_IF([BE_WINDOWS], [AC_CONFIG_LINKS([icons/tg-timer.ico:icons/tg-timer.ico])]) CC_CHECK_LDFLAGS([-Wl,--as-needed], [AC_SUBST([AM_LDFLAGS], [-Wl,--as-needed])], []) -CC_CHECK_CFLAGS_APPEND([-Wall -Wextra], [], []) +CC_CHECK_FLAGS_APPEND([with_cflags], [CFLAGS], [-Wall -Wextra]) +AC_SUBST([WARNINGFLAGS], $with_cflags) -AC_OUTPUT([Makefile icons/Makefile]) +AC_CONFIG_FILES([Makefile icons/Makefile]) +AC_CONFIG_FILES([packaging/tg-timer.spec:packaging/tg-timer.inc.in:packaging/tg-timer.in.spec]) +AC_OUTPUT AC_MSG_RESULT([ $PACKAGE_NAME $VERSION diff --git a/m4/attributes.m4 b/m4/attributes.m4 index c7ef73e..023d9b8 100644 --- a/m4/attributes.m4 +++ b/m4/attributes.m4 @@ -1,6 +1,7 @@ dnl Macros to check the presence of generic (non-typed) symbols. dnl Copyright (c) 2006-2008 Diego Pettenò dnl Copyright (c) 2006-2008 xine project +dnl Copyright (c) 2012 Lucas De Marchi dnl dnl This program is free software; you can redistribute it and/or modify dnl it under the terms of the GNU General Public License as published by @@ -32,52 +33,32 @@ dnl distribute a modified version of the Autoconf Macro, you may extend dnl this special exception to the GPL to apply to your modified version as dnl well. -dnl Check if the flag is supported by compiler -dnl CC_CHECK_CFLAGS_SILENT([FLAG], [ACTION-IF-FOUND],[ACTION-IF-NOT-FOUND]) - -AC_DEFUN([CC_CHECK_CFLAGS_SILENT], [ - AC_CACHE_VAL(AS_TR_SH([cc_cv_cflags_$1]), - [ac_save_CFLAGS="$CFLAGS" - CFLAGS="$CFLAGS $1" - AC_COMPILE_IFELSE([int a;], - [eval "AS_TR_SH([cc_cv_cflags_$1])='yes'"], - [eval "AS_TR_SH([cc_cv_cflags_$1])='no'"]) - CFLAGS="$ac_save_CFLAGS" - ]) - - AS_IF([eval test x$]AS_TR_SH([cc_cv_cflags_$1])[ = xyes], - [$2], [$3]) -]) - -dnl Check if the flag is supported by compiler (cacheable) -dnl CC_CHECK_CFLAGS([FLAG], [ACTION-IF-FOUND],[ACTION-IF-NOT-FOUND]) - -AC_DEFUN([CC_CHECK_CFLAGS], [ - AC_CACHE_CHECK([if $CC supports $1 flag], - AS_TR_SH([cc_cv_cflags_$1]), - CC_CHECK_CFLAGS_SILENT([$1]) dnl Don't execute actions here! - ) - - AS_IF([eval test x$]AS_TR_SH([cc_cv_cflags_$1])[ = xyes], - [$2], [$3]) -]) - -dnl CC_CHECK_CFLAG_APPEND(FLAG, [action-if-found], [action-if-not-found]) -dnl Check for CFLAG and appends them to CFLAGS if supported -AC_DEFUN([CC_CHECK_CFLAG_APPEND], [ - AC_CACHE_CHECK([if $CC supports $1 flag], - AS_TR_SH([cc_cv_cflags_$1]), - CC_CHECK_CFLAGS_SILENT([$1]) dnl Don't execute actions here! - ) - - AS_IF([eval test x$]AS_TR_SH([cc_cv_cflags_$1])[ = xyes], - [CFLAGS="$CFLAGS $1"; DEBUG_CFLAGS="$DEBUG_CFLAGS $1"; $2], [$3]) +dnl Check if FLAG in ENV-VAR is supported by compiler and append it +dnl to WHERE-TO-APPEND variable. Note that we invert -Wno-* checks to +dnl -W* as gcc cannot test for negated warnings. If a C snippet is passed, +dnl use it, otherwise use a simple main() definition that just returns 0. +dnl CC_CHECK_FLAG_APPEND([WHERE-TO-APPEND], [ENV-VAR], [FLAG], [C-SNIPPET]) + +AC_DEFUN([CC_CHECK_FLAG_APPEND], [ + AC_CACHE_CHECK([if $CC supports flag $3 in envvar $2], + AS_TR_SH([cc_cv_$2_$3]), + [eval "AS_TR_SH([cc_save_$2])='${$2}'" + eval "AS_TR_SH([$2])='${cc_save_$2} -Werror `echo "$3" | sed 's/^-Wno-/-W/'`'" + AC_LINK_IFELSE([AC_LANG_SOURCE(ifelse([$4], [], + [int main(void) { return 0; } ], + [$4]))], + [eval "AS_TR_SH([cc_cv_$2_$3])='yes'"], + [eval "AS_TR_SH([cc_cv_$2_$3])='no'"]) + eval "AS_TR_SH([$2])='$cc_save_$2'"]) + + AS_IF([eval test x$]AS_TR_SH([cc_cv_$2_$3])[ = xyes], + [eval "$1='${$1} $3'"]) ]) -dnl CC_CHECK_CFLAGS_APPEND([FLAG1 FLAG2], [action-if-found], [action-if-not]) -AC_DEFUN([CC_CHECK_CFLAGS_APPEND], [ - for flag in $1; do - CC_CHECK_CFLAG_APPEND($flag, [$2], [$3]) +dnl CC_CHECK_FLAGS_APPEND([WHERE-TO-APPEND], [ENV-VAR], [FLAG1 FLAG2], [C-SNIPPET]) +AC_DEFUN([CC_CHECK_FLAGS_APPEND], [ + for flag in [$3]; do + CC_CHECK_FLAG_APPEND([$1], [$2], $flag, [$4]) done ]) @@ -89,7 +70,7 @@ AC_DEFUN([CC_CHECK_LDFLAGS], [ AS_TR_SH([cc_cv_ldflags_$1]), [ac_save_LDFLAGS="$LDFLAGS" LDFLAGS="$LDFLAGS $1" - AC_LINK_IFELSE([int main() { return 1; }], + AC_LINK_IFELSE([AC_LANG_SOURCE([int main() { return 1; }])], [eval "AS_TR_SH([cc_cv_ldflags_$1])='yes'"], [eval "AS_TR_SH([cc_cv_ldflags_$1])="]) LDFLAGS="$ac_save_LDFLAGS" @@ -112,13 +93,13 @@ AC_DEFUN([CC_NOUNDEFINED], [ *-freebsd* | *-openbsd*) ;; *) dnl First of all check for the --no-undefined variant of GNU ld. This allows - dnl for a much more readable commandline, so that people can understand what + dnl for a much more readable command line, so that people can understand what dnl it does without going to look for what the heck -z defs does. - for possible_flags in "-Wl,--no-undefined" "-Wl,-z,defs"; do - CC_CHECK_LDFLAGS([$possible_flags], [LDFLAGS_NOUNDEFINED="$possible_flags"]) - break + for possible_flags in "-Wl,--no-undefined" "-Wl,-z,defs"; do + CC_CHECK_LDFLAGS([$possible_flags], [LDFLAGS_NOUNDEFINED="$possible_flags"]) + break done - ;; + ;; esac AC_SUBST([LDFLAGS_NOUNDEFINED]) @@ -147,7 +128,7 @@ AC_DEFUN([CC_CHECK_ATTRIBUTE], [ AS_TR_SH([cc_cv_attribute_$1]), [ac_save_CFLAGS="$CFLAGS" CFLAGS="$CFLAGS $cc_cv_werror" - AC_COMPILE_IFELSE([$3], + AC_COMPILE_IFELSE([AC_LANG_SOURCE([$3])], [eval "AS_TR_SH([cc_cv_attribute_$1])='yes'"], [eval "AS_TR_SH([cc_cv_attribute_$1])='no'"]) CFLAGS="$ac_save_CFLAGS" @@ -254,8 +235,8 @@ AC_DEFUN([CC_FLAG_VISIBILITY], [ [cc_flag_visibility_save_CFLAGS="$CFLAGS" CFLAGS="$CFLAGS $cc_cv_werror" CC_CHECK_CFLAGS_SILENT([-fvisibility=hidden], - cc_cv_flag_visibility='yes', - cc_cv_flag_visibility='no') + cc_cv_flag_visibility='yes', + cc_cv_flag_visibility='no') CFLAGS="$cc_flag_visibility_save_CFLAGS"]) AS_IF([test "x$cc_cv_flag_visibility" = "xyes"], @@ -271,11 +252,11 @@ AC_DEFUN([CC_FUNC_EXPECT], [ [cc_cv_func_expect], [ac_save_CFLAGS="$CFLAGS" CFLAGS="$CFLAGS $cc_cv_werror" - AC_COMPILE_IFELSE( + AC_COMPILE_IFELSE([AC_LANG_SOURCE( [int some_function() { int a = 3; return (int)__builtin_expect(a, 3); - }], + }])], [cc_cv_func_expect=yes], [cc_cv_func_expect=no]) CFLAGS="$ac_save_CFLAGS" @@ -295,11 +276,11 @@ AC_DEFUN([CC_ATTRIBUTE_ALIGNED], [ [ac_save_CFLAGS="$CFLAGS" CFLAGS="$CFLAGS $cc_cv_werror" for cc_attribute_align_try in 64 32 16 8 4 2; do - AC_COMPILE_IFELSE([ + AC_COMPILE_IFELSE([AC_LANG_SOURCE([ int main() { static char c __attribute__ ((aligned($cc_attribute_align_try))) = 0; return c; - }], [cc_cv_attribute_aligned=$cc_attribute_align_try; break]) + }])], [cc_cv_attribute_aligned=$cc_attribute_align_try; break]) done CFLAGS="$ac_save_CFLAGS" ]) diff --git a/packaging/tg-timer.in.spec b/packaging/tg-timer.in.spec new file mode 100644 index 0000000..23a0767 --- /dev/null +++ b/packaging/tg-timer.in.spec @@ -0,0 +1,80 @@ +# If the configure script is run, it will define the package version by +# prepending a definition of AC_VERSION to this spec. +# If rpkg preprocesses this file, as in a COPR build, then it will generate the +# package version from the "version" file and saved in the macro RPKG_VERSION. +# This does not require running the configure script, which does not happen on +# COPR. + +# It's also possible to define the macro 'version' on the command line. The +# version used will be the first defined from 'version' macro, AC_VERSION, and +# then RPKG_VERSION. + +# rpkg replaces with command output, otherwise rpmbuild replaces 3 {'s with 2 {'s. +%define RPKG_VERSION {{{ sed s/-/_/ version }}} +%define RPKG_SNAPINFO {{{ echo .$(git log -1 --date=format:"%Y%m%d" --format="%ad")git$(git rev-parse --short HEAD) }}} +%define have_rpkg %([ "%{RPKG_VERSION}" = "{{ sed s/-/_/ version }}" ]; echo $?) +%if 0%{!?version:1} + %if %{have_rpkg} + %define version %{RPKG_VERSION} + %define snapinfo %{RPKG_SNAPINFO} + %endif + %{?AC_VERSION: %define version %{AC_VERSION}} + %{!?version: %{error:Need to define version, e.g. --define "version x.y.z", or preprocess this file with configure or rpkg}} +%endif + +# Define pkgrel when building to set release. Otherwise git rev is used for snapshot tag. +%if 0%{!?pkgrel:1} && 0%{!?snapinfo:1} +%{warn: pkgrel not defined, attempting to build snapshot from current git checkout} +%{warn: Define pkgrel, e.g. --define "pkgrel 1", if not building git snapshot} +%define snapinfo .%(git log -1 --date=format:"%Y%m%d" --format="%ad")git%(git rev-parse --short HEAD) +%define needgit 1 +%endif + +Name: tg-timer +Version: %{version} +Release: %{?pkgrel}%{!?pkgrel:1}%{?snapinfo}%{?dist} +Summary: Mechanical watch movement timegrapher +License: GPL2 +Group: Misc +URL: https://github.com/vacaboja/tg +Source: %name-%version.tar.gz +Packager: Trent Piepho +BuildRequires: gcc, gtk3-devel, portaudio-devel, fftw-devel +BuildRequires: desktop-file-utils, autoconf, automake +%{?needgit:BuildRequires: git} + +%description +Tg (tg-timer) is a program to evaluate the performance of mechanical watch +movements. Tg works with the noise produced by a watch mechanism, and it +produces real-time readings of the rate (or accuracy) and various other +operational parameters. + +%prep +%setup -q + +%build +autoreconf -fi +%configure +%make_build + +%install +%make_install + +%check +desktop-file-validate %{buildroot}%{_datadir}/applications/*.desktop + +%files +%license LICENSE +%doc README.md +%_bindir/%{name} +%_mandir/man1/%{name}.1* +%{_datadir}/applications/%{name}.desktop +%{_datadir}/icons/hicolor/*/apps/%{name}.png +%{_datadir}/icons/hicolor/scalable/apps/%{name}.svg +%{_datadir}/icons/hicolor/*/mimetypes/application-x-%{name}-data.png +%{_datadir}/icons/hicolor/scalable/mimetypes/application-x-%{name}-data.svg +%{_datadir}/mime/packages/%{name}.xml + +%changelog +* Sat Apr 03 2021 Trent Piepho 1:0.5.2 +- Initial version diff --git a/packaging/tg-timer.inc.in b/packaging/tg-timer.inc.in new file mode 100644 index 0000000..823201a --- /dev/null +++ b/packaging/tg-timer.inc.in @@ -0,0 +1,2 @@ +# Added by configure script to define package version +%define AC_VERSION @PACKAGE_VERSION@ diff --git a/src/algo.c b/src/algo.c index 1c744cb..9b8b85f 100644 --- a/src/algo.c +++ b/src/algo.c @@ -18,10 +18,6 @@ #include "tg.h" -struct filter { - double a0,a1,a2,b1,b2; -}; - static int int_cmp(const void *a, const void *b) { int x = *(int*)a; @@ -29,7 +25,7 @@ static int int_cmp(const void *a, const void *b) return xy ? 1 : 0; } -static void make_hp(struct filter *f, double freq) +void make_hp(struct filter *f, double freq) { double K = tan(M_PI * freq); double norm = 1 / (1 + K * sqrt(2) + K * K); @@ -84,8 +80,6 @@ void setup_buffers(struct processing_buffers *b) b->plan_e = fftwf_plan_dft_r2c_1d(b->sample_rate, b->tic_wf, b->tic_fft, FFTW_ESTIMATE); b->plan_f = fftwf_plan_dft_r2c_1d(b->sample_rate, b->slice_wf, b->slice_fft, FFTW_ESTIMATE); b->plan_g = fftwf_plan_dft_c2r_1d(b->sample_rate, b->slice_fft, b->slice_wf, FFTW_ESTIMATE); - b->hpf = malloc(sizeof(struct filter)); - make_hp(b->hpf,(double)FILTER_CUTOFF/b->sample_rate); b->lpf = malloc(sizeof(struct filter)); make_lp(b->lpf,(double)FILTER_CUTOFF/b->sample_rate); b->events = malloc(EVENTS_MAX * sizeof(uint64_t)); @@ -116,7 +110,6 @@ void pb_destroy(struct processing_buffers *b) fftwf_destroy_plan(b->plan_e); fftwf_destroy_plan(b->plan_f); fftwf_destroy_plan(b->plan_g); - free(b->hpf); free(b->lpf); free(b->events); #ifdef DEBUG @@ -301,7 +294,6 @@ static void prepare_data(struct processing_buffers *b, int run_noise_suppressor) int i; memset(b->samples + b->sample_count, 0, b->sample_count * sizeof(float)); - run_filter(b->hpf, b->samples, b->sample_count); if(run_noise_suppressor) noise_suppressor(b); for(i=0; i < b->sample_count; i++) @@ -452,7 +444,7 @@ static int compute_period(struct processing_buffers *b, int bph) if(count > 1) b->sigma = sqrt((sq_sum - count * estimate * estimate)/ (count-1)); else - b->sigma = b->period; + b->sigma = 0; // No std. dev. estimate possible with just 1 sample return 0; } @@ -898,8 +890,12 @@ static void compute_cal(struct calibration_data *cd) void process(struct processing_buffers *p, int bph, double la, int light) { prepare_data(p, !light); + p->ready = !compute_period(p,bph); - if(p->ready && p->period >= p->sample_rate / 2) { + /* Limit to 20% greater when period is known, or 500 ms when guessing period */ + const int min_bph = bph ? bph : TYP_BPH; + const int max_period = (int)(1.2 * 3600 * 2) * p->sample_rate / min_bph; + if(p->ready && p->period >= max_period) { debug("Detected period too long\n"); p->ready = 0; } @@ -907,6 +903,7 @@ void process(struct processing_buffers *p, int bph, double la, int light) debug("abort after compute_period()\n"); return; } + prepare_waveform(p); p->ready = !compute_parameters(p); if(!p->ready) { diff --git a/src/audio.c b/src/audio.c index 05574fe..9453b99 100644 --- a/src/audio.c +++ b/src/audio.c @@ -18,18 +18,101 @@ #include "tg.h" #include +#include /* Huge buffer of audio */ -float pa_buffers[PA_BUFF_SIZE]; -int write_pointer = 0; +float *pa_buffers; +unsigned int pa_buffer_size; +unsigned int write_pointer = 0; uint64_t timestamp = 0; pthread_mutex_t audio_mutex; -/* Data for PA callback to use */ -static struct callback_info { +/** A biquadratic filter. + * Saves the delay taps to allow the filter to continue across multiple calls. */ +struct biquad_filter { + struct filter f; //!< Filter coefficients, F(z) = a(z) / b(z) + double z1, z2; //!< Delay taps + int frequency;//!< Cut-off frequency +}; + +/** Data for PA callback to use */ +struct callback_info { int channels; //!< Number of channels bool light; //!< Light algorithm in use, copy half data -} info; + struct biquad_filter hpf; //!< High-pass filter run in callback +}; + +/** Static object for audio device state. + * There are calls that need this from the audio callback thread, the GUI thread, and + * the computer thread. Having each thread pass it in correctly would be really hard. + * It's better to maintain it in one place here in the audio code. Lacking class scope + * in C, we'll have to settle for static global scope. We only support once device at a + * time, so not supporting multiuple audio contexts isn't much of a drawback. + * */ +static struct audio_context { + PaStream *stream; //!< Audio input stream object + int device; //!< PortAudio device ID number + int sample_rate; //!< Requested sample rate (actual may differ) + double real_sample_rate;//!< Real rate as returned by PA + unsigned num_devices; //!< Number of audio devices for current driver + struct audio_device *devices; //!< Cached audio device info + //! Data callback will read, need to take care when modifying so as not to race. + struct callback_info info; +} actx = { + .device = -1, +}; + +/** Return effective sample rate. + * This takes into account the half speed decimation enabled by light mode */ +static inline double effective_sr(void) +{ + return actx.real_sample_rate / (actx.info.light ? 2 : 1); +} + +/* Initialize audio filter */ +static void init_audio_hpf(struct biquad_filter *filter, int cutoff, double sample_rate) +{ + make_hp(&filter->f, cutoff/sample_rate); + filter->z1 = 0.0; + filter->z2 = 0.0; + filter->frequency = cutoff; +} + +/** Set High pass filter cutoff frequency. + * + * Setting value to zero turns it off. Has no effect if frequency is not + * changed. + */ +void set_audio_hpf(int cutoff) +{ + if (actx.info.hpf.frequency != cutoff) { + make_hp(&actx.info.hpf.f, (double)cutoff / effective_sr()); + actx.info.hpf.frequency = cutoff; + } +} + +const struct filter* get_audio_hpf(void) +{ + return &actx.info.hpf.f; +} + +/* Apply a biquadratic filter to data. The delay values are updated in f, so + * that it is possible to process data in chunks using multiple calls. + */ +static void apply_biquad(struct biquad_filter *f, float *data, unsigned int count) +{ + unsigned int i; + double z1 = f->z1, z2 = f->z2; + for(i=0; if.a0 + z1; + z1 = in * f->f.a1 + z2 - f->f.b1 * out; + z2 = in * f->f.a2 - f->f.b2 * out; + data[i] = out; + } + f->z1 = z1; + f->z2 = z2; +} static int paudio_callback(const void *input_buffer, void *output_buffer, @@ -55,19 +138,19 @@ static int paudio_callback(const void *input_buffer, if(info->channels == 1) { for(i = even ? 0 : 1; i < frame_count; i += 2) { pa_buffers[wp++] = input_samples[i]; - if (wp >= PA_BUFF_SIZE) wp -= PA_BUFF_SIZE; + if (wp >= pa_buffer_size) wp -= pa_buffer_size; } } else { for(i = even ? 0 : 2; i < frame_count*2; i += 4) { pa_buffers[wp++] = input_samples[i] + input_samples[i+1]; - if (wp >= PA_BUFF_SIZE) wp -= PA_BUFF_SIZE; + if (wp >= pa_buffer_size) wp -= pa_buffer_size; } } /* Keep track if we have processed an even number of frames, so * we know if we should drop the 1st or 2nd frame next callback. */ if(frame_count % 2) even = !even; } else { - const unsigned len = MIN(frame_count, PA_BUFF_SIZE - wp); + const unsigned len = MIN(frame_count, pa_buffer_size - wp); if(info->channels == 1) { memcpy(pa_buffers + wp, input_samples, len * sizeof(*pa_buffers)); if(len < frame_count) @@ -79,8 +162,20 @@ static int paudio_callback(const void *input_buffer, for(i = len; i < frame_count; i++) pa_buffers[i - len] = input_samples[2u*i] + input_samples[2u*i + 1u]; } - wp = (wp + frame_count) % PA_BUFF_SIZE; + wp = (wp + frame_count) % pa_buffer_size; } + + /* Apply HPF to new data */ + if(info->hpf.frequency) { + struct biquad_filter *f = (struct biquad_filter *)&info->hpf; + if(write_pointer < wp) { + apply_biquad(f, pa_buffers + write_pointer, wp - write_pointer); + } else { + apply_biquad(f, pa_buffers + write_pointer, pa_buffer_size - write_pointer); + apply_biquad(f, pa_buffers, wp); + } + } + pthread_mutex_lock(&audio_mutex); write_pointer = wp; timestamp += frame_count; @@ -88,18 +183,235 @@ static int paudio_callback(const void *input_buffer, return 0; } -int start_portaudio(int *nominal_sample_rate, double *real_sample_rate) +static PaError open_stream(PaDeviceIndex index, unsigned int rate, bool light, PaStream **stream) { - PaStream *stream; + PaError err; + + long channels = Pa_GetDeviceInfo(index)->maxInputChannels; + if(channels == 0) { + error("Default audio device has no input channels"); + return paInvalidChannelCount; + } + if(channels > 2) channels = 2; + actx.info.channels = channels; + actx.info.light = light; + + err = Pa_OpenStream(stream, + &(PaStreamParameters){ + .device = index, + .channelCount = channels, + .sampleFormat = paFloat32, + .suggestedLatency = Pa_GetDeviceInfo(index)->defaultHighInputLatency, + }, + NULL, + rate, + paFramesPerBufferUnspecified, + paNoFlag, + paudio_callback, + &actx.info); + return err; +} + +/** Select audio device and enable recording. + * + * This will select `device` to be the active audio device and capture at the + * rate provided in `*nominal_sr`. If `*normal_sr` is zero, then a default rate + * is selected. + * + * It is safe to call if the device and rate are unchanged. This will be + * detected and nothing will be done. + * + * Light mode will use simple decimation to cut the sample rate in half. The + * values in nominal and real sr do not reflect this. + * + * @param device Device number, index of device from get_audio_devices() list + * @param[in,out] normal_sr Desired rate, or zero for default. Rate used on return. + * @param[out] real_sr Actual exact rate received, might be different than nominal_sr. + * @param hpf_freq The cutoff frequency of the high pass filter. 0 disables. + * @param light Use light mode (halve normal_sr) + * @returns zero or one on success or negative error code. 1 indicates no + * change in device or rate was needed. + */ +int set_audio_device(int device, int *nominal_sr, double *real_sr, int hpf_freq, bool light) +{ + PaError err; + + // FIXME: Use a list of rates and pick the first supported rate + if(*nominal_sr == 0) + *nominal_sr = PA_SAMPLE_RATE; + + if(actx.device == device && actx.sample_rate == *nominal_sr) { + set_audio_hpf(hpf_freq); + if(real_sr) *real_sr = actx.real_sample_rate; + return 1; // Already using this device at this rate + } + + if(actx.device != -1) { + // Stop current device + Pa_StopStream(actx.stream); + Pa_CloseStream(actx.stream); + actx.stream = NULL; + actx.device = -1; + } + + actx.sample_rate = *nominal_sr; + + // Start new one. It seems it doesn't succeed on the first try sometimes. + unsigned int n; + for(n = 5; n; n--) { + debug("Open device %d at %d Hz with %d tries left\n", device, actx.sample_rate, n); + err = open_stream(device, actx.sample_rate, light, &actx.stream); + if (err == paNoError) + break; + if (err != paDeviceUnavailable) + goto error; + usleep(500000); + } + if(!n) + goto error; + actx.real_sample_rate = Pa_GetStreamInfo(actx.stream)->sampleRate; + + init_audio_hpf(&actx.info.hpf, hpf_freq, effective_sr()); + + /* Allocate larger buffer if needed */ + const size_t buffer_size = actx.sample_rate << (NSTEPS + FIRST_STEP); + if(pa_buffer_size < buffer_size) { + if(pa_buffers) free(pa_buffers); + pa_buffers = calloc(buffer_size, sizeof(*pa_buffers)); + if(!pa_buffers) { + err = paInsufficientMemory; + goto error; + } + pa_buffer_size = buffer_size; + } + + err = Pa_StartStream(actx.stream); + if(err != paNoError) { + Pa_CloseStream(actx.stream); + goto error; + } + + /* Return sample rates used */ + *nominal_sr = actx.sample_rate; + if(real_sr) + *real_sr = actx.real_sample_rate; + actx.device = device; + return 0; + +error: + actx.stream = NULL; + actx.device = -1; + actx.sample_rate = 0; + actx.real_sample_rate = 0.0; + + const struct PaDeviceInfo* devinfo = Pa_GetDeviceInfo(device); + const char *err_str = Pa_GetErrorText(err); + error("Error opening audio device '%s' at %d Hz: %s", devinfo->name, *nominal_sr, err_str); + return err; +} + +/** Return current audio device. + * + * @return Index of current audio device, or -1 if none is active. + */ +int get_audio_device(void) +{ + return actx.device; +} + +/** Get list of devices. + * + * @param[out] devices Static list of devices. + * @return Number of devices or negative on error. + */ +int get_audio_devices(const struct audio_device **devices) +{ + const struct audio_device* devs = actx.devices; + *devices = devs; + return actx.num_devices; +} + +static bool check_audio_rate(int device, int rate) +{ + const long channels = Pa_GetDeviceInfo(device)->maxInputChannels; + const PaStreamParameters params = { + .device = device, + .channelCount = channels > 2 ? 2 : channels, + .sampleFormat = paFloat32, + }; + + return paFormatIsSupported == Pa_IsFormatSupported(¶ms, NULL, rate); +} + + +static int scan_audio_devices(void) +{ + const PaDeviceIndex default_input = Pa_GetDefaultInputDevice(); + const int n = Pa_GetDeviceCount(); + static const int rate_list[] = AUDIO_RATES; + + if (actx.devices) free(actx.devices); + actx.devices = calloc(n, sizeof(*actx.devices)); + if (!actx.devices) + return -ENOMEM; + + int i; + for (i = 0; i < n; i++) { + const struct PaDeviceInfo* devinfo = Pa_GetDeviceInfo(i); + debug("Device %2d: %2d %s%s\n", i, devinfo->maxInputChannels, devinfo->name, i == default_input ? " (default)" : ""); + actx.devices[i].name = devinfo->name; + actx.devices[i].good = devinfo->maxInputChannels > 0; + actx.devices[i].isdefault = i == default_input; + actx.devices[i].rates = 0; + if (actx.devices[i].good) { + unsigned r; + for (r = 0; r < ARRAY_SIZE(rate_list); r++) + if (check_audio_rate(i, rate_list[r])) + actx.devices[i].rates |= 1 << r; + } + } + actx.num_devices = n; + + return n; +} + +/** Start audio system. + * + * This will start the recording stream. Call this first before any other audio + * functions, as it initialize PortAudio and fills in the device list. + * + * If the selected device is not suitable, perhaps because the audio hardware has + * changed since the device number was saved, it will fallback to the default device. + * + * A sample rate of 0 will select the default sample rate. + * + * On error, audio is NOT running. + * + * The distinction between the nominal and real sample rate is somewhat ill-defined. + * Nothing uses real sample rate yet. + * + * @param device The device to use, or -1 for default. + * @param[in,out] normal_sample_rate The rate in Hz to use, or 0 for default. Returns + * actual rate selected. + * @param[out] real_sample_rate The exact rate used. + * @param hpf_freq The cutoff frequency of the high pass filter. 0 disables. + * @param light Use light mode (decimate to half supplied rate). + * @returns 0 on success, 1 on error. + * + */ +int start_portaudio(int device, int *nominal_sample_rate, double *real_sample_rate, int hpf_freq, bool light) +{ if(pthread_mutex_init(&audio_mutex,NULL)) { error("Failed to setup audio mutex"); return 1; } PaError err = Pa_Initialize(); - if(err!=paNoError) + if(err!=paNoError) { + error("Error initializing PortAudio: %s", Pa_GetErrorText(err)); goto error; + } #ifdef DEBUG if(testing) { @@ -109,30 +421,26 @@ int start_portaudio(int *nominal_sample_rate, double *real_sample_rate) } #endif - PaDeviceIndex default_input = Pa_GetDefaultInputDevice(); - if(default_input == paNoDevice) { - error("No default audio input device found"); - return 1; + if(scan_audio_devices() < 0) { + error("Unable to query audio devices"); + // Maybe default audio device will work anyway? } - long channels = Pa_GetDeviceInfo(default_input)->maxInputChannels; - if(channels == 0) { - error("Default audio device has no input channels"); - return 1; - } - if(channels > 2) channels = 2; - info.channels = channels; - info.light = false; - err = Pa_OpenDefaultStream(&stream,channels,0,paFloat32,PA_SAMPLE_RATE,paFramesPerBufferUnspecified,paudio_callback,&info); - if(err!=paNoError) - goto error; - err = Pa_StartStream(stream); - if(err!=paNoError) + PaDeviceIndex input; + // Use default input if no device selected or selected device is no longer available. + if(device < 0 || device >= (int)actx.num_devices || !actx.devices[device].good) { + input = Pa_GetDefaultInputDevice(); + if(input == paNoDevice) { + error("No default audio input device found"); + goto error; + } + } else + input = device; + + err = set_audio_device(input, nominal_sample_rate, real_sample_rate, hpf_freq, light); + if(err!=paNoError && err!=1) goto error; - const PaStreamInfo *info = Pa_GetStreamInfo(stream); - *nominal_sample_rate = PA_SAMPLE_RATE; - *real_sample_rate = info->sampleRate; #ifdef DEBUG end: #endif @@ -141,7 +449,7 @@ int start_portaudio(int *nominal_sample_rate, double *real_sample_rate) return 0; error: - error("Error opening audio input: %s", Pa_GetErrorText(err)); + error("Unable to start audio"); return 1; } @@ -156,63 +464,51 @@ int terminate_portaudio() return 0; } -uint64_t get_timestamp(int light) +uint64_t get_timestamp() { pthread_mutex_lock(&audio_mutex); - uint64_t ts = light ? timestamp / 2 : timestamp; + uint64_t ts = actx.info.light ? timestamp / 2 : timestamp; pthread_mutex_unlock(&audio_mutex); return ts; } -static void fill_buffers(struct processing_buffers *ps, int light) +void fill_buffers(struct processing_buffers *ps) { pthread_mutex_lock(&audio_mutex); - uint64_t ts = timestamp; + uint64_t ts = timestamp / (actx.info.light ? 2 : 1); int wp = write_pointer; pthread_mutex_unlock(&audio_mutex); - if(light) - ts /= 2; - int i; for(i = 0; i < NSTEPS; i++) { ps[i].timestamp = ts; int start = wp - ps[i].sample_count; - if (start < 0) start += PA_BUFF_SIZE; - int len = MIN((unsigned)ps[i].sample_count, PA_BUFF_SIZE - start); + if (start < 0) start += pa_buffer_size; + int len = MIN((unsigned)ps[i].sample_count, pa_buffer_size - start); memcpy(ps[i].samples, pa_buffers + start, len * sizeof(*pa_buffers)); if (len < ps[i].sample_count) memcpy(ps[i].samples + len, pa_buffers, (ps[i].sample_count - len) * sizeof(*pa_buffers)); } } -int analyze_pa_data(struct processing_data *pd, int bph, double la, uint64_t events_from) +/* Returns if buffer was processed ok */ +bool analyze_pa_data(struct processing_data *pd, int step, int bph, double la, uint64_t events_from) { - struct processing_buffers *p = pd->buffers; - fill_buffers(p, pd->is_light); + struct processing_buffers *p = &pd->buffers[step]; - int i; - debug("\nSTART OF COMPUTATION CYCLE\n\n"); - for(i=0; ilast_tic; - p[i].events_from = events_from; - process(&p[i], bph, la, pd->is_light); - if( !p[i].ready ) break; - debug("step %d : %f +- %f\n",i,p[i].period/p[i].sample_rate,p[i].sigma/p[i].sample_rate); - } - if(i) { - pd->last_tic = p[i-1].last_tic; - debug("%f +- %f\n",p[i-1].period/p[i-1].sample_rate,p[i-1].sigma/p[i-1].sample_rate); - } else - debug("---\n"); - return i; + p->last_tic = pd->last_tic; + p->events_from = events_from; + process(p, bph, la, pd->is_light); + debug("step %d : %f +- %f\n", step, p->period/p->sample_rate, p->sigma/p->sample_rate); + + return p->ready; } int analyze_pa_data_cal(struct processing_data *pd, struct calibration_data *cd) { struct processing_buffers *p = pd->buffers; - fill_buffers(p, pd->is_light); + fill_buffers(p); int i,j; debug("\nSTART OF CALIBRATION CYCLE\n\n"); @@ -228,18 +524,27 @@ int analyze_pa_data_cal(struct processing_data *pd, struct calibration_data *cd) /** Change to light mode * * Call to enable or disable light mode. Changing the mode will empty the audio - * buffer. Nothing will happen if the mode doesn't actually change. + * buffer. Nothing will happen if the mode doesn't actually change. Audio is + * downsampled by 2 in light mode. * - * @param light True for light mode, false for normal + * @param light True for light mode, false for normal mode */ void set_audio_light(bool light) { - if(info.light != light) { + if(actx.info.light != light) { + Pa_StopStream(actx.stream); pthread_mutex_lock(&audio_mutex); - info.light = light; - memset(pa_buffers, 0, sizeof(pa_buffers)); + + actx.info.light = light; + memset(pa_buffers, 0, sizeof(*pa_buffers) * pa_buffer_size); write_pointer = 0; timestamp = 0; + pthread_mutex_unlock(&audio_mutex); + + init_audio_hpf(&actx.info.hpf, FILTER_CUTOFF, effective_sr()); + PaError err = Pa_StartStream(actx.stream); + if(err != paNoError) + error("Error re-starting audio input: %s", Pa_GetErrorText(err)); } } diff --git a/src/computer.c b/src/computer.c index 03942d3..e5d95e8 100644 --- a/src/computer.c +++ b/src/computer.c @@ -48,12 +48,17 @@ struct snapshot *snapshot_clone(struct snapshot *s) t->events_wp = 0; t->events = NULL; } + if(s->d) { + t->d = malloc(sizeof(*t->d)); + *t->d = *s->d; + } return t; } void snapshot_destroy(struct snapshot *s) { if(s->pb) pb_destroy_clone(s->pb); + if(s->d) free(s->d); free(s->events); free(s); } @@ -91,19 +96,45 @@ static void compute_update_cal(struct computer *c) static void compute_update(struct computer *c) { - int signal = analyze_pa_data(c->pdata, c->actv->bph, c->actv->la, c->actv->events_from); - struct processing_buffers *p = c->pdata->buffers; - int i; - for(i=0; i=0 && p[i].sigma > p[i].period / 10000; i--); - if(i>=0) { + struct processing_data *pd = c->pdata; + struct processing_buffers *ps = pd->buffers; + int step = pd->last_step; + + pd->last_step = 0; + /* Do all buffers at once so that all computation interval(s) use the + * same data. Buffers for some intervals will probably not be used, but + * it's not expensive to fill them. Processing is the slow part. */ + fill_buffers(ps); + + debug("\nSTART OF COMPUTATION CYCLE\n\n"); + unsigned int stepmask = BITMASK(NSTEPS); // Mask of available steps + do { + stepmask &= ~BIT(step); + analyze_pa_data(c->pdata, step, c->actv->bph, c->actv->la, c->actv->events_from); + + if (ps[step].ready && ps[step].sigma < ps[step].period / 10000) { + // Try next step if it's available + if (stepmask & BIT(step+1)) step++; + } else { + // This step didn't pass, try a lesser step + step--; + } + } while(step >= 0 && stepmask & BIT(step)); + + if (step >= 0) { + debug("%f +- %f\n", ps[step].period/ps[step].sample_rate, ps[step].sigma/ps[step].sample_rate); + pd->last_tic = ps[step].last_tic; + pd->last_step = step; + if(c->actv->pb) pb_destroy_clone(c->actv->pb); - c->actv->pb = pb_clone(&p[i]); + c->actv->pb = pb_clone(&ps[step]); c->actv->is_old = 0; - c->actv->signal = i == NSTEPS-1 && p[i].amp < 0 ? signal-1 : signal; + /* Signal's range is 0 to NSTEPS, while step is -1 to NSTEPS-1, i.e. signal = step+1 */ + c->actv->signal = step+1; } else { + debug("---\n"); c->actv->is_old = 1; - c->actv->signal = -signal; + c->actv->signal = 0; } } @@ -251,6 +282,7 @@ struct computer *start_computer(int nominal_sr, int bph, double la, int cal, int pd->buffers = p; pd->last_tic = 0; pd->is_light = light; + pd->last_step = 0; struct calibration_data *cd = malloc(sizeof(struct calibration_data)); setup_cal_data(cd); @@ -267,11 +299,11 @@ struct computer *start_computer(int nominal_sr, int bph, double la, int cal, int memset(s->events,0,EVENTS_COUNT * sizeof(uint64_t)); s->events_wp = 0; s->events_from = 0; - s->trace_centering = 0; s->bph = bph; s->la = la; s->cal = cal; s->is_light = light; + s->d = NULL; struct computer *c = malloc(sizeof(struct computer)); c->cdata = cd; diff --git a/src/config.c b/src/config.c index 18d1ee9..220b294 100644 --- a/src/config.c +++ b/src/config.c @@ -44,6 +44,8 @@ g_key_file_save_to_file (GKeyFile *key_file, #define g_key_file_get_int g_key_file_get_integer #define g_key_file_set_int g_key_file_set_integer // the devil may take glib +#define g_key_file_get_bool g_key_file_get_boolean +#define g_key_file_set_bool g_key_file_set_boolean void load_config(struct main_window *w) { diff --git a/src/interface.c b/src/interface.c index 7c1d3ca..709ba51 100644 --- a/src/interface.c +++ b/src/interface.c @@ -229,7 +229,9 @@ static void recompute(struct main_window *w) w->computer_timeout = 0; lock_computer(w->computer); if(w->computer->recompute >= 0) { - if(w->is_light != w->computer->actv->is_light) { + const int effective_sr = w->nominal_sr / (w->is_light ? 2 : 1); + if(w->is_light != w->computer->actv->is_light || + effective_sr != w->computer->actv->nominal_sr) { kill_computer(w); } else { w->computer->bph = w->bph; @@ -422,7 +424,7 @@ static GtkWidget *make_tab_label(char *name, struct output_panel *panel_to_close static void add_new_tab(struct snapshot *s, char *name, struct main_window *w) { - struct output_panel *op = init_output_panel(NULL, s, 5); + struct output_panel *op = init_output_panel(NULL, s, 5, w->vertical_layout); GtkWidget *label = make_tab_label(name, op); gtk_widget_show_all(op->panel); @@ -587,6 +589,149 @@ static void save_current(GtkMenuItem *m, struct main_window *w) snapshot_destroy(snapshot); } +/* Get rate from rate list combo box which has a manual entry. Returns -1 on + * error (entered rate not parsable). */ +static int get_rate(GtkComboBox *rate_list) +{ + GtkTreeIter iter; + unsigned int rate; + + if(gtk_combo_box_get_active_iter(rate_list, &iter)) { + gtk_tree_model_get(gtk_combo_box_get_model(rate_list), &iter, 2, &rate, -1); + } else { + GtkEntry *entry = GTK_ENTRY(gtk_bin_get_child(GTK_BIN(rate_list))); + if (sscanf(gtk_entry_get_text(entry), "%u", &rate) != 1) + return -1; + if (rate < 1000) { // Too slow, must mean kHz not Hz + rate *= 1000; + } + char ratestr[16]; + snprintf(ratestr, sizeof(ratestr), "%u Hz", rate); + gtk_entry_set_text(entry, ratestr); + } + return rate; +} + +static void rate_entered(GtkWidget *rate_entry, struct main_window *w) +{ + UNUSED(rate_entry); + gtk_range_set_fill_level(w->hpf_range, get_rate(w->rate_list) / 2); +} + +static void rate_changed(GtkWidget *rate_list, struct main_window *w) +{ + /* Only act on combo list selections, not on every keystroke of manual + * entry. rate_entered() will handle manual entry finished. */ + if (gtk_combo_box_get_active(GTK_COMBO_BOX(rate_list)) == -1) + return; + gtk_range_set_fill_level(w->hpf_range, get_rate(w->rate_list) / 2); +} + +static void populate_rate_list(GtkComboBox *rate_list, unsigned int current_rate, unsigned int rates) +{ + unsigned int i; + GtkTreeIter iter; + static const unsigned int freqs[] = AUDIO_RATES; + static const char * const labels[] = AUDIO_RATE_LABELS; + + int active = -1; // -1 will set no active entry + g_signal_handlers_block_matched(G_OBJECT(rate_list), G_SIGNAL_MATCH_FUNC, 0,0,NULL, rate_changed, NULL); + GtkListStore *list = GTK_LIST_STORE(gtk_combo_box_get_model(rate_list)); + gtk_tree_model_get_iter_first(GTK_TREE_MODEL(list), &iter); + for(i=0; i < ARRAY_SIZE(freqs); i++, gtk_tree_model_iter_next(GTK_TREE_MODEL(list), &iter)) { + const bool supported = rates & (1u << i); + gtk_list_store_set(list, &iter, + 0, labels[i], + 1, supported, + 2, freqs[i], + -1); + + if (supported && current_rate == freqs[i]) + active = i; + } + g_signal_handlers_unblock_matched(G_OBJECT(rate_list), G_SIGNAL_MATCH_FUNC, 0,0,NULL, rate_changed, NULL); + gtk_combo_box_set_active(rate_list, active); + if (active == -1) { + GtkWidget *entry = gtk_bin_get_child(GTK_BIN(rate_list)); + char ratestr[16]; + snprintf(ratestr, sizeof(ratestr), "%u Hz", current_rate); + gtk_entry_set_text(GTK_ENTRY(entry), ratestr); + gtk_widget_activate(entry); + } +} + +static void device_changed(GtkWidget *device_list, struct main_window *w) +{ + UNUSED(device_list); + GtkTreeIter iter; + unsigned int rates; + + if(!gtk_combo_box_get_active_iter(w->device_list, &iter)) + return; + gtk_tree_model_get(gtk_combo_box_get_model(w->device_list), &iter, 2, &rates, -1); + + populate_rate_list(w->rate_list, w->nominal_sr, rates); +} + +static void audio_setup(GtkMenuItem *m, struct main_window *w) +{ + UNUSED(m); + int i; + + const int current_dev = get_audio_device(); + const struct audio_device *devices; + const int n = get_audio_devices(&devices); + if (n < 0) { + error("Failed to get audio device list: %d\n", n); + return; + } + // Populate list of devices + g_signal_handlers_block_matched(G_OBJECT(w->device_list), G_SIGNAL_MATCH_DATA, 0,0,NULL,NULL, w); + GtkListStore *list = GTK_LIST_STORE(gtk_combo_box_get_model(w->device_list)); + gtk_list_store_clear(list); + for(i=0; i < n; i++) + gtk_list_store_insert_with_values(list, NULL, -1, + 0, devices[i].name, + 1, devices[i].good, + 2, devices[i].rates, + -1); + g_signal_handlers_unblock_matched(G_OBJECT(w->device_list), G_SIGNAL_MATCH_DATA, 0,0,NULL,NULL, w); + + gtk_combo_box_set_active(w->device_list, current_dev); + + int response = gtk_dialog_run(GTK_DIALOG(w->audio_setup)); + gtk_widget_hide(w->audio_setup); + if (response != GTK_RESPONSE_OK) + return; // Cancel... + + int selected = gtk_combo_box_get_active(w->device_list); + if (selected == -1) + return; // Didn't select anything + + int new_rate = get_rate(w->rate_list); + if (new_rate == -1) + new_rate = w->nominal_sr; /* clear entry too? */ + + int hpf_freq = gtk_range_get_value(w->hpf_range); + + i = set_audio_device(selected, &new_rate, NULL, hpf_freq, w->is_light); + if (i == 0) { + w->nominal_sr = new_rate; + // Only save settings to config if it worked + w->audio_rate = new_rate; + w->hpf_freq = hpf_freq; + // If selected dev is the default, save -1 "default" to config + w->audio_device = devices[selected].isdefault ? -1 : selected; + recompute(w); + } else if (i < 0) { + /* Try to restore old settings */ + new_rate = w->nominal_sr; + i = set_audio_device(current_dev, &new_rate, NULL, w->hpf_freq, w->is_light); + if (i < 0) + error("Unable to restore previous audio settings. Audio not working."); + } +} + static void close_all(GtkMenuItem *m, struct main_window *w) { UNUSED(m); @@ -694,6 +839,119 @@ static void load(GtkMenuItem *m, struct main_window *w) gtk_widget_destroy(dialog); } +static void handle_layout(GtkCheckMenuItem *b, struct main_window *w) +{ + const bool vertical = gtk_check_menu_item_get_active(b) == TRUE; + + w->vertical_layout = vertical; + set_panel_layout(w->active_panel, vertical); + + int n = 0; + GtkWidget *panel; + while ((panel = gtk_notebook_get_nth_page(GTK_NOTEBOOK(w->notebook), n++))) { + struct output_panel *op = g_object_get_data(G_OBJECT(panel), "op-pointer"); + if(op) + set_panel_layout(op, vertical); + } +} + +/* Add a checkbox with name to the given menu, with initial state active and + * attach the supplied callback and parameter to the toggled signal. Set is set + * before attaching the signal, so the callback is not called when created. */ +static GtkWidget* add_checkbox(GtkWidget* menu, const char* name, bool active, GCallback callback, void* param) +{ + GtkWidget *checkbox = gtk_check_menu_item_new_with_label(name); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(checkbox), active); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), checkbox); + g_signal_connect(checkbox, "toggled", callback, param); + return checkbox; +} + +/* Add a menu item with given label to the given menu, with the supplied initial +* sensitivity, callback, and callback parameter. */ +static GtkWidget* add_menu_item(GtkWidget* menu, const char* label, bool sensitive, GCallback callback, void* param) +{ + GtkWidget *item = gtk_menu_item_new_with_label(label); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); + gtk_widget_set_sensitive(item, sensitive); + g_signal_connect(item, "activate", callback, param); + return item; +} + +/* Setup and populate the audio setup widow */ +static void init_audio_dialog(struct main_window *w) +{ + w->audio_setup = gtk_dialog_new_with_buttons("Audio Setup", GTK_WINDOW(w->window), + GTK_DIALOG_DESTROY_WITH_PARENT, + "_Cancel", GTK_RESPONSE_CANCEL, + "_OK", GTK_RESPONSE_OK, + NULL); + gtk_dialog_set_default_response(GTK_DIALOG(w->audio_setup), GTK_RESPONSE_OK); + GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(w->audio_setup)); + + GtkGrid *grid = GTK_GRID(gtk_grid_new()); + gtk_grid_set_row_spacing(grid, 12); + gtk_grid_set_column_spacing(grid, 6); + g_object_set(G_OBJECT(grid), "margin", 6, NULL); + gtk_container_add(GTK_CONTAINER(content), GTK_WIDGET(grid)); + + GtkWidget *label; + + gtk_grid_attach(grid, label = gtk_label_new("Audio Device"), 0, 0, 1, 1); + g_object_set(G_OBJECT(label), "halign", GTK_ALIGN_END, NULL); + + GtkCellRenderer *renderer; + GtkListStore *list; + + /* 0 Name; 1 suitable for recording; 2 bitmask of allowed rates */ + list = gtk_list_store_new(3, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_UINT); + GtkWidget *devices = gtk_combo_box_new_with_model(GTK_TREE_MODEL(list)); + g_object_unref(list); + w->device_list = GTK_COMBO_BOX(devices); + gtk_combo_box_set_active(w->device_list, get_audio_device()); + renderer = gtk_cell_renderer_text_new(); + gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(devices), renderer, TRUE); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(devices), renderer, "text", 0, "sensitive", 1, NULL); + gtk_widget_set_hexpand(devices, true); // One cell with hexpand affects the entire column + gtk_grid_attach(grid, devices, 1, 0, 1, 1); + + gtk_grid_attach(grid, label = gtk_label_new("Sample Rate"), 0, 1, 1, 1); + g_object_set(G_OBJECT(label), "halign", GTK_ALIGN_END, NULL); + + /* 0 Name; 1 Supported rate?; 2 integer rate in Hz */ + list = gtk_list_store_new(3, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_UINT); + int n; + GtkTreeIter iter; + for (n = NUM_AUDIO_RATES; n; n--) gtk_list_store_insert(list, &iter, -1); + GtkWidget *rates = gtk_combo_box_new_with_model_and_entry(GTK_TREE_MODEL(list)); + g_object_unref(list); + w->rate_list = GTK_COMBO_BOX(rates); + gtk_combo_box_set_entry_text_column(GTK_COMBO_BOX(rates), 0); + renderer = gtk_cell_renderer_text_new(); + gtk_cell_layout_clear(GTK_CELL_LAYOUT(rates)); + gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(rates), renderer, TRUE); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(rates), renderer, "text", 0, "sensitive", 1, NULL); + gtk_grid_attach(grid, rates, 1, 1, 1, 1); + + gtk_grid_attach(grid, label = gtk_label_new("High Pass Cutoff"), 0, 2, 1, 1); + g_object_set(G_OBJECT(label), "halign", GTK_ALIGN_END, NULL); + + GtkWidget *hpf = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, 0, 24000, 100); + w->hpf_range = GTK_RANGE(hpf); + gtk_scale_add_mark(GTK_SCALE(w->hpf_range), 0, GTK_POS_BOTTOM, "Off"); + gtk_scale_add_mark(GTK_SCALE(w->hpf_range), FILTER_CUTOFF, GTK_POS_BOTTOM, "Default"); + gtk_range_set_restrict_to_fill_level(w->hpf_range, true); + gtk_range_set_show_fill_level(w->hpf_range, true); + gtk_range_set_value(w->hpf_range, FILTER_CUTOFF); + gtk_grid_attach(grid, hpf, 1, 2, 1, 1); + + gtk_widget_show_all(content); + + g_signal_connect(G_OBJECT(w->device_list), "changed", G_CALLBACK(device_changed), w); + g_signal_connect(G_OBJECT(gtk_bin_get_child(GTK_BIN(w->rate_list))), "activate", G_CALLBACK(rate_entered), w); + g_signal_connect(G_OBJECT(w->rate_list), "changed", G_CALLBACK(rate_changed), w); +} + /* Set up the main window and populate with widgets */ static void init_main_window(struct main_window *w) { @@ -800,47 +1058,36 @@ static void init_main_window(struct main_window *w) gtk_box_pack_end(GTK_BOX(hbox), command_menu_button, FALSE, FALSE, 0); // ... Open - GtkWidget *open_item = gtk_menu_item_new_with_label("Open"); - gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), open_item); - g_signal_connect(open_item, "activate", G_CALLBACK(load), w); + add_menu_item(command_menu, "Open", true, G_CALLBACK(load), w); // ... Save - w->save_item = gtk_menu_item_new_with_label("Save current display"); - gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), w->save_item); - g_signal_connect(w->save_item, "activate", G_CALLBACK(save_current), w); - gtk_widget_set_sensitive(w->save_item, FALSE); + w->save_item = add_menu_item(command_menu, "Save current display", false, G_CALLBACK(save_current), w); // ... Save all - w->save_all_item = gtk_menu_item_new_with_label("Save all snapshots"); - gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), w->save_all_item); - g_signal_connect(w->save_all_item, "activate", G_CALLBACK(save_all), w); - gtk_widget_set_sensitive(w->save_all_item, FALSE); + w->save_all_item = add_menu_item(command_menu, "Save all snapshots", false, G_CALLBACK(save_all), w); gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), gtk_separator_menu_item_new()); // ... Light checkbox - GtkWidget *light_checkbox = gtk_check_menu_item_new_with_label("Light algorithm"); - gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), light_checkbox); - g_signal_connect(light_checkbox, "toggled", G_CALLBACK(handle_light), w); - gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(light_checkbox), w->is_light); + add_checkbox(command_menu, "Light algorithm", w->is_light, G_CALLBACK(handle_light), w); // ... Calibrate checkbox - w->cal_button = gtk_check_menu_item_new_with_label("Calibrate"); - gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), w->cal_button); - g_signal_connect(w->cal_button, "toggled", G_CALLBACK(handle_calibrate), w); + w->cal_button = add_checkbox(command_menu, "Calibrate", false, G_CALLBACK(handle_calibrate), w); + + // Layout checkbox + add_checkbox(command_menu, "Vertical", w->vertical_layout, G_CALLBACK(handle_layout), w); gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), gtk_separator_menu_item_new()); + // ... Audio Setup + w->audio_setup = add_menu_item(command_menu, "Audio setup", true, G_CALLBACK(audio_setup), w); + init_audio_dialog(w); + // ... Close all - w->close_all_item = gtk_menu_item_new_with_label("Close all snapshots"); - gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), w->close_all_item); - g_signal_connect(w->close_all_item, "activate", G_CALLBACK(close_all), w); - gtk_widget_set_sensitive(w->close_all_item, FALSE); + w->close_all_item = add_menu_item(command_menu, "Close all snapshots", false, G_CALLBACK(close_all), w); // ... Quit - GtkWidget *quit_item = gtk_menu_item_new_with_label("Quit"); - gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), quit_item); - g_signal_connect(quit_item, "activate", G_CALLBACK(handle_quit), w); + add_menu_item(command_menu, "Quit", true, G_CALLBACK(handle_quit), w); gtk_widget_show_all(command_menu); @@ -875,11 +1122,11 @@ guint refresh(struct main_window *w) lock_computer(w->computer); struct snapshot *s = w->computer->curr; if(s) { - double trace_centering = w->active_snapshot->trace_centering; + s->d = w->active_snapshot->d; + w->active_snapshot->d = NULL; snapshot_destroy(w->active_snapshot); w->active_snapshot = s; w->computer->curr = NULL; - s->trace_centering = trace_centering; if(w->computer->clear_trace && !s->calibrate) memset(s->events,0,s->events_count*sizeof(uint64_t)); if(s->calibrate && s->cal_state == 1 && s->cal_result != w->cal) { @@ -917,11 +1164,6 @@ static void start_interface(GApplication* app, void *p) struct main_window *w = malloc(sizeof(struct main_window)); - if(start_portaudio(&w->nominal_sr, &real_sr)) { - g_application_quit(app); - return; - } - w->app = GTK_APPLICATION(app); w->zombie = 0; @@ -931,9 +1173,20 @@ static void start_interface(GApplication* app, void *p) w->la = DEFAULT_LA; w->calibrate = 0; w->is_light = 0; + w->vertical_layout = true; + w->nominal_sr = 0; // Use default rate, e.g. PA_SAMPLE_RATE + w->audio_device = -1; + w->audio_rate = 0; + w->hpf_freq = FILTER_CUTOFF; load_config(w); + w->nominal_sr = w->audio_rate; + if(start_portaudio(w->audio_device, &w->nominal_sr, &real_sr, w->hpf_freq, w->is_light)) { + g_application_quit(app); + return; + } + if(w->la < MIN_LA || w->la > MAX_LA) w->la = DEFAULT_LA; if(w->bph < MIN_BPH || w->bph > MAX_BPH) w->bph = 0; if(w->cal < MIN_CAL || w->cal > MAX_CAL) @@ -954,7 +1207,7 @@ static void start_interface(GApplication* app, void *p) w->computer->curr = NULL; compute_results(w->active_snapshot); - w->active_panel = init_output_panel(w->computer, w->active_snapshot, 0); + w->active_panel = init_output_panel(w->computer, w->active_snapshot, 0, w->vertical_layout); init_main_window(w); diff --git a/src/output_panel.c b/src/output_panel.c index 0e7fc8b..0e6020f 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -18,6 +18,13 @@ #include "tg.h" +// Zoom slider ranges from 1 to 100 +static const double zoom_min = 1, zoom_max = 100, zoom_mid = (zoom_min + zoom_max)/2; +// Scale ranges from 1x beat length to zoomed in by 100x +static const double scale_min = 1, scale_max = 100; + +static inline double spb(const struct snapshot *snst); + cairo_pattern_t *black,*white,*red,*green,*blue,*blueish,*yellow; static void define_color(cairo_pattern_t **gc,double r,double g,double b) @@ -112,11 +119,20 @@ static double amplitude_to_time(double lift_angle, double amp) return asin(lift_angle / (2 * amp)) / M_PI; } -static double draw_watch_icon(cairo_t *c, int signal, int happy, int light) +/** Draw the watch graphic that has status info. + * + * @param[in,out] c Cairo context to use. + * @param signal Signal level, i.e. dots, 0 to NSTEPS inclusive. + * @param partial Specified signal level is only partially achieved. + * @param happy Green happy face or red frowny face. + * @param light Indicate light sampling mode. + * @return Y coodinate of top margin. + */ + +static double draw_watch_icon(cairo_t *c, int signal, bool partial, bool happy, bool light) { - happy = !!happy; - cairo_set_line_width(c,3); - cairo_set_source(c,happy?green:red); + cairo_set_line_width(c, 3); + cairo_set_source(c, happy ? green : red); cairo_move_to(c, OUTPUT_WINDOW_HEIGHT * 0.5, OUTPUT_WINDOW_HEIGHT * 0.5); cairo_line_to(c, OUTPUT_WINDOW_HEIGHT * 0.75, OUTPUT_WINDOW_HEIGHT * (0.75 - 0.5*happy)); cairo_move_to(c, OUTPUT_WINDOW_HEIGHT * 0.5, OUTPUT_WINDOW_HEIGHT * 0.5); @@ -126,7 +142,7 @@ static double draw_watch_icon(cairo_t *c, int signal, int happy, int light) cairo_stroke(c); int l = OUTPUT_WINDOW_HEIGHT * 0.8 / (2*NSTEPS - 1); int i; - cairo_set_line_width(c,1); + cairo_set_line_width(c, 1); for(i = 0; i < signal; i++) { cairo_move_to(c, OUTPUT_WINDOW_HEIGHT + 0.5*l, OUTPUT_WINDOW_HEIGHT * 0.9 - 2*i*l); cairo_line_to(c, OUTPUT_WINDOW_HEIGHT + 1.5*l, OUTPUT_WINDOW_HEIGHT * 0.9 - 2*i*l); @@ -134,7 +150,7 @@ static double draw_watch_icon(cairo_t *c, int signal, int happy, int light) cairo_line_to(c, OUTPUT_WINDOW_HEIGHT + 0.5*l, OUTPUT_WINDOW_HEIGHT * 0.9 - (2*i+1)*l); cairo_line_to(c, OUTPUT_WINDOW_HEIGHT + 0.5*l, OUTPUT_WINDOW_HEIGHT * 0.9 - 2*i*l); cairo_stroke_preserve(c); - cairo_fill(c); + if (i < signal-1 || !partial) cairo_fill(c); } if(light) { int l = OUTPUT_WINDOW_HEIGHT * 0.15; @@ -194,7 +210,8 @@ static gboolean output_draw_event(GtkWidget *widget, cairo_t *c, struct output_p struct processing_buffers *p = snst->pb; int old = snst->is_old; - double x = draw_watch_icon(c,snst->signal,snst->calibrate ? snst->signal==NSTEPS : snst->signal, snst->is_light); + double x = draw_watch_icon(c, snst->signal, snst->amp <= 0, + snst->signal >= (snst->calibrate ? NSTEPS : 1), snst->is_light); cairo_text_extents_t extents; @@ -319,17 +336,21 @@ static void expose_waveform( GtkAllocation temp; gtk_widget_get_allocation(da, &temp); - int width = temp.width; - int height = temp.height; + const int width = temp.width, height = temp.height; - gtk_widget_get_allocation(gtk_widget_get_toplevel(da), &temp); - int font = temp.width / 90; - if(font < 12) - font = 12; - int i; + int fontw = width / 40; + fontw = fontw < 12 ? 12 : fontw > 20 ? 20 : fontw; + int fonth = height / 12; + fonth = fonth < 12 ? 12 : fonth > 20 ? 20 : fonth; + const int font = MIN(fontw, fonth); + cairo_set_font_size(c, font); - cairo_set_font_size(c,font); + cairo_font_extents_t fextents; + cairo_font_extents(c, &fextents); + const int margin = 6; + + int i; for(i = 1-NEGATIVE_SPAN; i < POSITIVE_SPAN; i++) { int x = (NEGATIVE_SPAN + i) * width / (POSITIVE_SPAN + NEGATIVE_SPAN); cairo_move_to(c, x + .5, height / 2 + .5); @@ -346,7 +367,7 @@ static void expose_waveform( int x = (NEGATIVE_SPAN + i) * width / (POSITIVE_SPAN + NEGATIVE_SPAN); char s[10]; sprintf(s,"%d",i); - cairo_move_to(c,x+font/4,height-font/2); + cairo_move_to(c,x+font/4, height - margin); cairo_show_text(c,s); } } @@ -354,7 +375,7 @@ static void expose_waveform( cairo_text_extents_t extents; cairo_text_extents(c,"ms",&extents); - cairo_move_to(c,width - extents.x_advance - font/4,height-font/2); + cairo_move_to(c,width - extents.x_advance - font/4, height - margin); cairo_show_text(c,"ms"); struct snapshot *snst = op->snst; @@ -386,7 +407,7 @@ static void expose_waveform( char s[10]; sprintf(s,"%d",abs(i)); - cairo_move_to(c, x + font/4, font * 3 / 2); + cairo_move_to(c, x + font/4, margin + fextents.ascent); cairo_show_text(c,s); cairo_text_extents(c,s,&extents); last_x = x + font/4 + extents.x_advance; @@ -394,7 +415,7 @@ static void expose_waveform( } cairo_text_extents(c,"deg",&extents); - cairo_move_to(c,width - extents.x_advance - font/4,font * 3 / 2); + cairo_move_to(c,width - extents.x_advance - font/4, margin + fextents.ascent); cairo_show_text(c,"deg"); if(p) { @@ -526,129 +547,226 @@ static gboolean period_draw_event(GtkWidget *widget, cairo_t *c, struct output_p return FALSE; } +/* Return scale value from button. A scale of 1.0 means the beat length, while + * a scale of 0.10 would be one tenth of a beat length. */ +static double get_beatscale(GtkScaleButton *b) +{ + const double σ = log(scale_max/scale_min) / (zoom_max - zoom_min); + const double ω = pow(scale_max/scale_min, -1.0/(zoom_max - zoom_min)) / scale_max; + + const double zoom = gtk_scale_button_get_value(b); + const double scale = ω * exp(σ*zoom); + debug("Zoom slider %.0f to scale %.03f = %.0fx\n", zoom, scale, 1/scale); + + return scale; +} + +// Convert value in samples to milliseconds. +static inline double s2ms(const struct snapshot *snst, double samples) +{ + return samples * 1000.0 / snst->sample_rate; +} + +// Samples per beat +static inline double spb(const struct snapshot *snst) +{ + if (snst->calibrate) + return snst->nominal_sr; // one second per beat, no calibration applied + + return (snst->sample_rate * 3600) / snst->guessed_bph; +} + +// 1x1 box with upper left corner at x, y +static void box(cairo_t *c, double x, double y) +{ + cairo_move_to(c, x, y); + cairo_line_to(c, x+1, y); + cairo_line_to(c, x+1, y+1); + cairo_line_to(c, x, y+1); + cairo_close_path(c); + cairo_fill(c); +} + static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct output_panel *op) { int i; - struct snapshot *snst = op->snst; + const struct snapshot *snst = op->snst; + struct display *ssd = snst->d; uint64_t time = snst->timestamp ? snst->timestamp : get_timestamp(snst->is_light); - double sweep; - int zoom_factor; - double slope = 1000; // detected rate: 1000 -> do not display - if(snst->calibrate) { - sweep = snst->nominal_sr; - zoom_factor = PAPERSTRIP_ZOOM_CAL; - slope = (double) snst->cal * zoom_factor / (10 * 3600 * 24); - } else { - sweep = snst->sample_rate * 3600. / snst->guessed_bph; - zoom_factor = PAPERSTRIP_ZOOM; - if(snst->events_count && snst->events[snst->events_wp]) - slope = - snst->rate * zoom_factor / (3600. * 24.); + + bool stopped = false; + if( snst->events_count && + snst->events[snst->events_wp] && + time > 5 * snst->nominal_sr + snst->events[snst->events_wp]) { + time = 5 * snst->nominal_sr + snst->events[snst->events_wp]; + stopped = true; + } + + // Allocate initial display parameters. Will persist across each new + // snapshot after this. + if (!ssd) { + ssd = op->snst->d = calloc(1, sizeof(*snst->d)); + ssd->beat_scale = get_beatscale(GTK_SCALE_BUTTON(op->zoom_button)); } cairo_init(c); GtkAllocation temp; - gtk_widget_get_allocation (op->paperstrip_drawing_area, &temp); + gtk_widget_get_allocation(widget, &temp); + int width, height; - int width = temp.width; - int height = temp.height; + /* The paperstrip is coded to be vertical; horizontal uses cairo to rotate it. */ + if(op->vertical_layout) { + width = temp.width; + height = temp.height; + } else { + width = temp.height; + height = temp.width; - int stopped = 0; - if( snst->events_count && - snst->events[snst->events_wp] && - time > 5 * snst->nominal_sr + snst->events[snst->events_wp]) { - time = 5 * snst->nominal_sr + snst->events[snst->events_wp]; - stopped = 1; + cairo_translate(c, height, 0); + cairo_rotate(c, M_PI/2); } - int strip_width = round(width / (1 + PAPERSTRIP_MARGIN)); - - cairo_set_line_width(c,1.3); - - slope *= strip_width; - if(slope <= 2 && slope >= -2) { - for(i=0; i<4; i++) { - double y = 0; - cairo_move_to(c, (double)width * (i+.5) / 4, 0); - for(;;) { - double x = y * slope + (double)width * (i+.5) / 4; - x = fmod(x, width); - if(x < 0) x += width; - double nx = x + slope * (height - y); - if(nx >= 0 && nx <= width) { - cairo_line_to(c, nx, height); - break; - } else { - double d = slope > 0 ? width - x : x; - y += d / fabs(slope); - cairo_line_to(c, slope > 0 ? width : 0, y); - y += 1; - if(y > height) break; - cairo_move_to(c, slope > 0 ? 0 : width, y); - } + // Beat time in samples, which is the height of a row (measured in samples) + const double beat_length = spb(snst); + // Width of chart's displayed portion of the beat, in samples + const double chart_width = beat_length * ssd->beat_scale; + // Width in pixels of main chart area, which corresponds to chart_width in samples + const int strip_width = round(width / (1 + PAPERSTRIP_MARGIN)); + // Width in samples of one pixel + const double pixel_width = (double)chart_width / strip_width; + + /* Round time to multiple of beat rate, to avoid "jumping" of a point + * compared to others or grid lines while scrolling. E.g., points at + * 1.0 and 1.2, both are rounded to row 1. Advance time by 0.4, points + * now at 1.4 and 1.6, the first remains row 1, but the second is + * rounded to row 2, causing it to appear to jump. This is avoided by + * only advancing time by a multiple of a row. */ + time += (int)(beat_length + 0.5) - 1; + time -= time % (int)(beat_length + 0.5); + + // Beat error slope lines or calibration slope lines + if (snst->pb) { // pb == NULL means no rate, beat error, etc. + // Slope of rate lines, in pixels per beat + const double slope = (snst->calibrate ? -snst->cal/10.0 : snst->rate) * + strip_width / (24 * 3600) / ssd->beat_scale; + if (slope > -2 && slope < 2) { + cairo_set_line_width(c, 1.3); + cairo_set_source(c, blue); + /* X intercept of line starting at lower left corner, in quarter widths left+4 + * is intercept from lower right corner. Intercepts at top left/right corners + * are always 0 and 4. We need to draw lines from the lesser of the left corner + * intercepts to the greater of the right corner intercepts to cover the width + * of the chart at the top and botom. */ + const int left = ceil(-slope * height / width * 4 - 0.5); + for (i = MIN(left, 0); i < MAX(4, left+4); i++) { + // i is x position in quarter chart widths + const double x0 = (i + 0.5) / 4 * width; + cairo_move_to(c, x0, 0); + cairo_line_to(c, x0 + slope * height, height); } + cairo_stroke(c); } - cairo_set_source(c, blue); - cairo_stroke(c); } - cairo_set_line_width(c,1); + // Margin lines + const int left_margin = (width - strip_width) / 2; + const int right_margin = (width + strip_width) / 2; - int left_margin = (width - strip_width) / 2; - int right_margin = (width + strip_width) / 2; + cairo_set_line_width(c, 1); + cairo_set_source(c, green); cairo_move_to(c, left_margin + .5, .5); cairo_line_to(c, left_margin + .5, height - .5); cairo_move_to(c, right_margin + .5, .5); cairo_line_to(c, right_margin + .5, height - .5); - cairo_set_source(c, green); cairo_stroke(c); - double now = sweep*ceil(time/sweep); - double ten_s = snst->sample_rate * 10 / sweep; - double last_line = fmod(now/sweep, ten_s); - int last_tenth = floor(now/(sweep*ten_s)); - for(i=0;;i++) { - double y = 0.5 + round(last_line + i*ten_s); - if(y > height) break; - cairo_move_to(c, .5, y); - cairo_line_to(c, width-.5, y); - cairo_set_source(c, (last_tenth-i)%6 ? green : red); + // Time grid lines + cairo_set_line_width(c, 1); + // Space between lines in samples = 10 sec + const double line_spacing = 10 * snst->sample_rate; + // The topmost line is this many samples from start + const double top_line = fmod(time, line_spacing); + const int minute_offset = (int)(time / line_spacing) % 6; + for(i = 0; ; i++) { + const double position = top_line + i * line_spacing; // position in samples + const double row = round(position / beat_length); // …in pixels + if (row > height) + break; + cairo_move_to(c, 0, row); + cairo_line_to(c, width, row); + cairo_set_source(c, (i - minute_offset) % 6 ? green : red); cairo_stroke(c); } - cairo_set_source(c,stopped?yellow:white); - for(i = snst->events_wp;;) { - if(!snst->events_count || !snst->events[i]) break; - double event = now - snst->events[i] + snst->trace_centering + sweep * PAPERSTRIP_MARGIN / (2 * zoom_factor); - int column = floor(fmod(event, (sweep / zoom_factor)) * strip_width / (sweep / zoom_factor)); - int row = floor(event / sweep); - if(row >= height) break; - cairo_move_to(c,column,row); - cairo_line_to(c,column+1,row); - cairo_line_to(c,column+1,row+1); - cairo_line_to(c,column,row+1); - cairo_line_to(c,column,row); - cairo_fill(c); - if(column < width - strip_width && row > 0) { - column += strip_width; - row -= 1; - cairo_move_to(c,column,row); - cairo_line_to(c,column+1,row); - cairo_line_to(c,column+1,row+1); - cairo_line_to(c,column,row+1); - cairo_line_to(c,column,row); - cairo_fill(c); + // Ticks and tocks + cairo_set_line_width(c, 0); + cairo_set_source(c, stopped ? yellow : white); + /* Compute lag 1 difference between events, find residuals modulo beat + * length (BL) of those differences, convert to range (-BL/2, BL/2], and + * accumulate. + * While doing this, look for the previous anchor point, for which we + * have saved the value of its accumulated residuals, and find the + * offset needed to produce the same value. */ + + double display_offset = chart_width/2; // Value used if no anchor found + double offsets[snst->events_count]; + if (snst->events_count) { + double accumulated_offset = 0.0; + uint64_t prev_event = snst->events[snst->events_wp]; // Start with first event + for (i = snst->events_count; i > 0; i--) { // Scan order is newest to oldest + const int idx = (snst->events_wp + i) % snst->events_count; + const uint64_t event = snst->events[idx]; + if (!event) break; + + double residual = fmod(prev_event - event, beat_length); + if (residual > beat_length/2) residual -= beat_length; + accumulated_offset -= residual; + offsets[idx] = accumulated_offset; + + // Is this the anchor? + if (event == snst->d->anchor_time) + display_offset = ssd->anchor_offset - accumulated_offset; + + prev_event = event; } - if(--i < 0) i = snst->events_count - 1; - if(i == snst->events_wp) break; + // Save offset of newest point as new anchor + ssd->anchor_time = snst->events[snst->events_wp]; + ssd->anchor_offset = offsets[snst->events_wp] + display_offset; } - cairo_set_source(c,white); - cairo_set_line_width(c,2); + display_offset += left_margin * pixel_width; // Adjust for margin + for (i = snst->events_count; i > 0; i--) { + const int idx = (snst->events_wp + i) % snst->events_count; + const uint64_t event = snst->events[idx]; + if (!event) break; + + // Row 0 is at "time", each row is one beat earlier than that. + const double row = round((time - event) / beat_length); + if(row > height) break; + + double chart_phase = fmod(offsets[idx] + display_offset, chart_width); + if (chart_phase < 0) chart_phase += chart_width; + const double column = round(chart_phase / pixel_width); + + box(c, column, row); + if (column < width - strip_width) + box(c, column + strip_width, row); +#if DEBUG + const double cycles = (time - event) / beat_length; + debug("point %2d: %7lu, cycle %.1f, offset %.1f ms, chart phase = %.1f ms, column %.0f\n", idx, event, cycles, + s2ms(snst, offsets[idx] + display_offset), s2ms(snst, chart_phase), column); +#endif + } + cairo_stroke(c); + + // Legend line + cairo_set_source(c, white); + cairo_set_line_width(c, 2); cairo_move_to(c, left_margin + 3, height - 20.5); cairo_line_to(c, right_margin - 3, height - 20.5); cairo_stroke(c); - cairo_set_line_width(c,1); + cairo_set_line_width(c, 1); cairo_move_to(c, left_margin + .5, height - 20.5); cairo_line_to(c, left_margin + 5.5, height - 15.5); cairo_line_to(c, left_margin + 5.5, height - 25.5); @@ -660,20 +778,17 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp cairo_line_to(c, right_margin + .5, height - 20.5); cairo_fill(c); - char s[100]; - cairo_text_extents_t extents; + int font = width / 25; + cairo_set_font_size(c, font < 12 ? 12 : font > 24 ? 24 : font); - gtk_widget_get_allocation(gtk_widget_get_toplevel(widget), &temp); - int font = temp.width / 90; - if(font < 12) - font = 12; - cairo_set_font_size(c,font); + char s[32]; + snprintf(s, sizeof(s), "%.1f ms", s2ms(snst, chart_width)); - sprintf(s, "%.1f ms", snst->calibrate ? - 1000. / zoom_factor : - 3600000. / (snst->guessed_bph * zoom_factor)); - cairo_text_extents(c,s,&extents); - cairo_move_to(c, (width - extents.x_advance)/2, height - 30); + cairo_text_extents_t extents; + cairo_font_extents_t fextents; + cairo_text_extents(c, s, &extents); + cairo_font_extents(c, &fextents); + cairo_move_to(c, (width - extents.width)/2 - extents.x_bearing, (height - 25.5) + fextents.ascent - fextents.height); cairo_show_text(c,s); return FALSE; @@ -726,30 +841,22 @@ static void handle_center_trace(GtkButton *b, struct output_panel *op) struct snapshot *snst = op->snst; if(!snst || !snst->events) return; - uint64_t last_ev = snst->events[snst->events_wp]; - double new_centering; - if(last_ev) { - double sweep; - if(snst->calibrate) - sweep = (double) snst->nominal_sr / PAPERSTRIP_ZOOM_CAL; - else - sweep = snst->sample_rate * 3600. / (PAPERSTRIP_ZOOM * snst->guessed_bph); - new_centering = fmod(last_ev + .5*sweep , sweep); - } else - new_centering = 0; - snst->trace_centering = new_centering; + + const double chart_width = snst->d->beat_scale * spb(snst); + // Anchor point should be most recent point, or close enough to it + snst->d->anchor_offset = chart_width / 2; + gtk_widget_queue_draw(op->paperstrip_drawing_area); } static void shift_trace(struct output_panel *op, double direction) { struct snapshot *snst = op->snst; - double sweep; - if(snst->calibrate) - sweep = (double) snst->nominal_sr / PAPERSTRIP_ZOOM_CAL; - else - sweep = snst->sample_rate * 3600. / (PAPERSTRIP_ZOOM * snst->guessed_bph); - snst->trace_centering = fmod(snst->trace_centering + sweep * (1.+.1*direction), sweep); + + // Chart with in samples + const double chart_width = snst->d->beat_scale * spb(snst); + snst->d->anchor_offset += chart_width * 0.10 * direction; + gtk_widget_queue_draw(op->paperstrip_drawing_area); } @@ -765,6 +872,30 @@ static void handle_right(GtkButton *b, struct output_panel *op) shift_trace(op,1); } +static void handle_zoom_original(GtkScaleButton *b, struct output_panel *op) +{ + UNUSED(b); + gtk_scale_button_set_value(GTK_SCALE_BUTTON(op->zoom_button), zoom_mid); + gtk_widget_queue_draw(op->paperstrip_drawing_area); +} + +static void handle_zoom(GtkScaleButton *b, struct output_panel *op) +{ + struct display *ssd = op->snst->d; + if (!ssd) return; // Maybe chart hasn't been displayed even once yet? + + const double scale = get_beatscale(b); + + /* Attempt to position archor_offset at same point in chart in new scale */ + if (ssd->beat_scale) + ssd->anchor_offset *= scale / ssd->beat_scale; + + ssd->beat_scale = scale; + + gtk_widget_set_visible(op->zoom_orig_button, gtk_scale_button_get_value(b) != zoom_mid); + gtk_widget_queue_draw(op->paperstrip_drawing_area); +} + void op_set_snapshot(struct output_panel *op, struct snapshot *snst) { op->snst = snst; @@ -782,89 +913,212 @@ void op_destroy(struct output_panel *op) free(op); } -struct output_panel *init_output_panel(struct computer *comp, struct snapshot *snst, int border) +/* Wrappers around gtk_orientable_set_orientation() */ +static GtkOrientation vert_to_orient(bool vertical) { - struct output_panel *op = malloc(sizeof(struct output_panel)); - - op->computer = comp; - op->snst = snst; - - op->panel = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); - gtk_container_set_border_width(GTK_CONTAINER(op->panel), border); + return vertical ? GTK_ORIENTATION_VERTICAL : GTK_ORIENTATION_HORIZONTAL; +} - // Info area on top - op->output_drawing_area = gtk_drawing_area_new(); - gtk_widget_set_size_request(op->output_drawing_area, 0, OUTPUT_WINDOW_HEIGHT); - gtk_box_pack_start(GTK_BOX(op->panel),op->output_drawing_area, FALSE, TRUE, 0); - g_signal_connect (op->output_drawing_area, "draw", G_CALLBACK(output_draw_event), op); - gtk_widget_set_events(op->output_drawing_area, GDK_EXPOSURE_MASK); +static void set_orientation(GtkWidget *widget, bool vertical) +{ + gtk_orientable_set_orientation(GTK_ORIENTABLE(widget), vert_to_orient(vertical)); +} - GtkWidget *hbox2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); - gtk_box_pack_start(GTK_BOX(op->panel), hbox2, TRUE, TRUE, 0); +/* Creates the paperstrip, with buttons. Returns top level Widget that contains + * them. Vertical controls orientation of paper strip. */ +static GtkWidget* create_paperstrip(struct output_panel *op, bool vertical) +{ + GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); - GtkWidget *vbox2 = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); - gtk_box_pack_start(GTK_BOX(hbox2), vbox2, FALSE, TRUE, 0); + GtkWidget *overlay = gtk_overlay_new(); + gtk_box_pack_start(GTK_BOX(vbox), overlay, TRUE, TRUE, 0); // Paperstrip op->paperstrip_drawing_area = gtk_drawing_area_new(); - gtk_widget_set_size_request(op->paperstrip_drawing_area, 300, 0); - gtk_box_pack_start(GTK_BOX(vbox2), op->paperstrip_drawing_area, TRUE, TRUE, 0); + gtk_widget_set_size_request(op->paperstrip_drawing_area, 150, 150); + gtk_container_add(GTK_CONTAINER(overlay), op->paperstrip_drawing_area); g_signal_connect (op->paperstrip_drawing_area, "draw", G_CALLBACK(paperstrip_draw_event), op); gtk_widget_set_events(op->paperstrip_drawing_area, GDK_EXPOSURE_MASK); - GtkWidget *hbox3 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); - gtk_box_pack_start(GTK_BOX(vbox2), hbox3, FALSE, TRUE, 0); + GtkWidget *box = gtk_box_new(vert_to_orient(!vertical), 0); + gtk_container_set_border_width(GTK_CONTAINER(box), 15); + gtk_widget_set_margin_bottom(box, 5); + gtk_widget_set_halign(box, GTK_ALIGN_START); + gtk_widget_set_valign(box, vertical ? GTK_ALIGN_END : GTK_ALIGN_START); + gtk_widget_set_opacity(box, 0.8); + gtk_overlay_add_overlay(GTK_OVERLAY(overlay), box); + + op->zoom_button = gtk_scale_button_new(GTK_ICON_SIZE_BUTTON, zoom_min, zoom_max, 1, + (const char *[]){"zoom-in-symbolic", NULL}); + gtk_scale_button_set_value(GTK_SCALE_BUTTON(op->zoom_button), zoom_mid); + set_orientation(op->zoom_button, vertical); + g_signal_connect(op->zoom_button, "value-changed", G_CALLBACK(handle_zoom), op); + gtk_box_pack_start(GTK_BOX(box), op->zoom_button, FALSE, FALSE, 0); + + op->zoom_orig_button = gtk_button_new_from_icon_name("zoom-original-symbolic", GTK_ICON_SIZE_BUTTON); + gtk_button_set_relief(GTK_BUTTON(op->zoom_orig_button), GTK_RELIEF_NONE); + g_signal_connect(op->zoom_orig_button, "clicked", G_CALLBACK(handle_zoom_original), op); + gtk_box_pack_start(GTK_BOX(box), op->zoom_orig_button, FALSE, FALSE, 0); + gtk_widget_set_no_show_all(op->zoom_orig_button, true); + + // Buttons + GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); + gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, TRUE, 0); // < button - GtkWidget *left_button = gtk_button_new_with_label("<"); - gtk_box_pack_start(GTK_BOX(hbox3), left_button, TRUE, TRUE, 0); - g_signal_connect (left_button, "clicked", G_CALLBACK(handle_left), op); + op->left_button = gtk_button_new_from_icon_name( + vertical ? "pan-start-symbolic" : "pan-up-symbolic", GTK_ICON_SIZE_LARGE_TOOLBAR); + gtk_box_pack_start(GTK_BOX(hbox), op->left_button, TRUE, TRUE, 0); + g_signal_connect (op->left_button, "clicked", G_CALLBACK(handle_left), op); // CLEAR button - if(comp) { + if(op->computer) { op->clear_button = gtk_button_new_with_label("Clear"); - gtk_box_pack_start(GTK_BOX(hbox3), op->clear_button, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(hbox), op->clear_button, TRUE, TRUE, 0); g_signal_connect (op->clear_button, "clicked", G_CALLBACK(handle_clear_trace), op); - gtk_widget_set_sensitive(op->clear_button, !snst->calibrate); + gtk_widget_set_sensitive(op->clear_button, !op->snst->calibrate); } // CENTER button GtkWidget *center_button = gtk_button_new_with_label("Center"); - gtk_box_pack_start(GTK_BOX(hbox3), center_button, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(hbox), center_button, TRUE, TRUE, 0); g_signal_connect (center_button, "clicked", G_CALLBACK(handle_center_trace), op); // > button - GtkWidget *right_button = gtk_button_new_with_label(">"); - gtk_box_pack_start(GTK_BOX(hbox3), right_button, TRUE, TRUE, 0); - g_signal_connect (right_button, "clicked", G_CALLBACK(handle_right), op); + op->right_button = gtk_button_new_from_icon_name( + vertical ? "pan-end-symbolic" : "pan-down-symbolic", GTK_ICON_SIZE_LARGE_TOOLBAR); + gtk_box_pack_start(GTK_BOX(hbox), op->right_button, TRUE, TRUE, 0); + g_signal_connect (op->right_button, "clicked", G_CALLBACK(handle_right), op); + + return vbox; +} - GtkWidget *vbox3 = gtk_box_new(GTK_ORIENTATION_VERTICAL,10); - gtk_box_pack_start(GTK_BOX(hbox2), vbox3, TRUE, TRUE, 0); +/* Create the tic, toc, and period waveforms. Returns the GtkBox that contains + * them. Vertical controls how the waves are stacked. */ +static GtkWidget* create_waveforms(struct output_panel *op, bool vertical) +{ + GtkWidget *box = gtk_box_new(vert_to_orient(vertical), 10); // Tic waveform area op->tic_drawing_area = gtk_drawing_area_new(); - gtk_box_pack_start(GTK_BOX(vbox3), op->tic_drawing_area, TRUE, TRUE, 0); + gtk_widget_set_size_request(op->tic_drawing_area, 300, 150); + gtk_box_pack_start(GTK_BOX(box), op->tic_drawing_area, TRUE, TRUE, 0); g_signal_connect (op->tic_drawing_area, "draw", G_CALLBACK(tic_draw_event), op); gtk_widget_set_events(op->tic_drawing_area, GDK_EXPOSURE_MASK); // Toc waveform area op->toc_drawing_area = gtk_drawing_area_new(); - gtk_box_pack_start(GTK_BOX(vbox3), op->toc_drawing_area, TRUE, TRUE, 0); + gtk_widget_set_size_request(op->toc_drawing_area, 300, 150); + gtk_box_pack_start(GTK_BOX(box), op->toc_drawing_area, TRUE, TRUE, 0); g_signal_connect (op->toc_drawing_area, "draw", G_CALLBACK(toc_draw_event), op); gtk_widget_set_events(op->toc_drawing_area, GDK_EXPOSURE_MASK); // Period waveform area op->period_drawing_area = gtk_drawing_area_new(); - gtk_box_pack_start(GTK_BOX(vbox3), op->period_drawing_area, TRUE, TRUE, 0); + gtk_widget_set_size_request(op->period_drawing_area, 300, 150); + gtk_box_pack_start(GTK_BOX(box), op->period_drawing_area, TRUE, TRUE, 0); g_signal_connect (op->period_drawing_area, "draw", G_CALLBACK(period_draw_event), op); gtk_widget_set_events(op->period_drawing_area, GDK_EXPOSURE_MASK); #ifdef DEBUG op->debug_drawing_area = gtk_drawing_area_new(); - gtk_box_pack_start(GTK_BOX(vbox3), op->debug_drawing_area, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(box), op->debug_drawing_area, TRUE, TRUE, 0); g_signal_connect (op->debug_drawing_area, "draw", G_CALLBACK(debug_draw_event), op); gtk_widget_set_events(op->debug_drawing_area, GDK_EXPOSURE_MASK); #endif + return box; +} + +/* Create container and place paperstrip and waveforms in either vertical or + * horizontal paperstrip orientation. Puts container in the panel and shows it. */ +static void place_displays(struct output_panel *op, GtkWidget *paperstrip, GtkWidget *waveforms, bool vertical) +{ + op->vertical_layout = vertical; + + op->displays = gtk_paned_new(vert_to_orient(!vertical)); + gtk_paned_set_wide_handle(GTK_PANED(op->displays), TRUE); + + gtk_paned_pack1(GTK_PANED(op->displays), paperstrip, vertical ? FALSE : TRUE, FALSE); + + set_orientation(waveforms, vert_to_orient(vertical)); + gtk_paned_pack2(GTK_PANED(op->displays), waveforms, TRUE, FALSE); + + /* Make paperstrip arrows buttons point correct way */ + GtkWidget *left_arrow = gtk_button_get_image(GTK_BUTTON(op->left_button)); + gtk_image_set_from_icon_name(GTK_IMAGE(left_arrow), + vertical ? "pan-start-symbolic" : "pan-up-symbolic", GTK_ICON_SIZE_LARGE_TOOLBAR); + GtkWidget *right_arrow = gtk_button_get_image(GTK_BUTTON(op->right_button)); + gtk_image_set_from_icon_name(GTK_IMAGE(right_arrow), + vertical ? "pan-end-symbolic" : "pan-down-symbolic", GTK_ICON_SIZE_LARGE_TOOLBAR); + /* Orientation of zoom buttons in papestrip */ + GtkWidget *button_box = gtk_widget_get_parent(op->zoom_button); + gtk_widget_set_valign(button_box, vertical ? GTK_ALIGN_END : GTK_ALIGN_START); + set_orientation(button_box, !vertical); + set_orientation(op->zoom_button, vertical); + + gtk_box_pack_end(GTK_BOX(op->panel), op->displays, TRUE, TRUE, 0); + gtk_widget_show(op->displays); +} + +/* Create the paperstrip and waveforms, a container for them, and place it into + * the panel. Returns containing Widget. Vertical controls paperstrip + * orientation. */ +static GtkWidget *create_displays(struct output_panel *op, bool vertical) +{ + // The paperstrip and buttons + op->paperstrip_box = create_paperstrip(op, vertical); + // Tic/toc/period waveform area + op->waveforms_box = create_waveforms(op, vertical); + + place_displays(op, op->paperstrip_box, op->waveforms_box, vertical); + + return op->displays; +} + +/* Change orientation of existing output panel. Is a no-op if orientation is + * not changed. */ +void set_panel_layout(struct output_panel *op, bool vertical) +{ + if (op->vertical_layout == vertical) + return; + + /* Remove waveforms and paperstrip containers from displays container, + * then use place_displays() to put them into a new displays container. + * The need to be refed so they are not deleted when removed from the + * container. */ + g_object_ref(op->waveforms_box); + gtk_container_remove(GTK_CONTAINER(op->displays), op->waveforms_box); + + g_object_ref(op->paperstrip_box); + gtk_container_remove(GTK_CONTAINER(op->displays), op->paperstrip_box); + + gtk_widget_destroy(op->displays); op->displays = NULL; + place_displays(op, op->paperstrip_box, op->waveforms_box, vertical); + + /* They are now refed by op->displays so we don't need our refs anymore */ + g_object_unref(op->paperstrip_box); + g_object_unref(op->waveforms_box); +} + +struct output_panel *init_output_panel(struct computer *comp, struct snapshot *snst, int border, bool vertical) +{ + struct output_panel *op = malloc(sizeof(struct output_panel)); + + op->computer = comp; + op->snst = snst; + + op->panel = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); + gtk_container_set_border_width(GTK_CONTAINER(op->panel), border); + + // Info area on top + op->output_drawing_area = gtk_drawing_area_new(); + gtk_widget_set_size_request(op->output_drawing_area, 0, OUTPUT_WINDOW_HEIGHT); + gtk_box_pack_start(GTK_BOX(op->panel),op->output_drawing_area, FALSE, TRUE, 0); + g_signal_connect (op->output_drawing_area, "draw", G_CALLBACK(output_draw_event), op); + gtk_widget_set_events(op->output_drawing_area, GDK_EXPOSURE_MASK); + + create_displays(op, vertical); + return op; } diff --git a/src/serializer.c b/src/serializer.c index e772f66..fc808ec 100644 --- a/src/serializer.c +++ b/src/serializer.c @@ -347,7 +347,9 @@ static int serialize_snapshot(FILE *f, struct snapshot *s, char *name) SERIALIZE(double,rate); SERIALIZE(double,be); SERIALIZE(double,amp); - SERIALIZE(double,trace_centering); + SERIALIZE(double,d->beat_scale); + SERIALIZE(uint64_t,d->anchor_time); + SERIALIZE(double,d->anchor_offset); SERIALIZE(int,is_light); return serialize_struct_end(f); } @@ -370,10 +372,9 @@ static int scan_snapshot(FILE *f, struct snapshot **s, char **name) if(strcmp("realtime-snapshot", l)) return eat_object(f); - *s = malloc(sizeof(struct snapshot)); - memset(*s, 0, sizeof(struct snapshot)); - (*s)->pb = malloc(sizeof(struct processing_buffers)); - memset((*s)->pb, 0, sizeof(struct processing_buffers)); + *s = calloc(1, sizeof(**s)); + (*s)->pb = calloc(1, sizeof(*(*s)->pb)); + (*s)->d = calloc(1, sizeof(*(*s)->d)); *name = NULL; n = 0; @@ -423,7 +424,9 @@ static int scan_snapshot(FILE *f, struct snapshot **s, char **name) SCAN(double,rate); SCAN(double,be); SCAN(double,amp); - SCAN(double,trace_centering); + SCAN(double,d->beat_scale); + SCAN(uint64_t,d->anchor_time); + SCAN(double,d->anchor_offset); SCAN(int,is_light); if(eat_object(f)) goto error; @@ -453,6 +456,9 @@ static int scan_snapshot(FILE *f, struct snapshot **s, char **name) if((*s)->be < 0 || (*s)->be > 99.9) goto error; debug("serializer: checking amplitude\n"); if((*s)->amp < 0 || (*s)->amp > 360) goto error; + debug("serializer: checking scale\n"); + if((*s)->d->beat_scale == 0) (*s)->d->beat_scale = 1.0/PAPERSTRIP_ZOOM; + if((*s)->d->beat_scale < 0 || (*s)->d->beat_scale > 1) goto error; (*s)->pb->events = NULL; #ifdef DEBUG (*s)->pb->debug = NULL; @@ -532,6 +538,7 @@ int read_file(FILE *f, struct snapshot ***s, char ***names, uint64_t *cnt) if(0 != fscanf(f, " T;%n", &n) || !n) return 1; *s = NULL; *names = NULL; + *cnt = 0; for(;;) { if(scan_label(f,l)) goto error; if(!strcmp("__end__",l)) break; diff --git a/src/tg.h b/src/tg.h index 789f66c..17e505e 100644 --- a/src/tg.h +++ b/src/tg.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -43,7 +44,6 @@ #define NSTEPS 4 #define PA_SAMPLE_RATE 44100u -#define PA_BUFF_SIZE (PA_SAMPLE_RATE << (NSTEPS + FIRST_STEP)) #define OUTPUT_FONT 40 #define OUTPUT_WINDOW_HEIGHT 70 @@ -57,7 +57,8 @@ #define PAPERSTRIP_ZOOM_CAL 100 #define PAPERSTRIP_MARGIN .2 -#define MIN_BPH 12000 +#define MIN_BPH 8100 +#define TYP_BPH 12000 #define MAX_BPH 72000 #define DEFAULT_BPH 21600 #define MIN_LA 10 // deg @@ -75,6 +76,9 @@ #endif #define UNUSED(X) (void)(X) +#define BIT(n) (1u << (n)) +#define BITMASK(n) ((1u << (n)) - 1u) +#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) /* algo.c */ struct processing_buffers { @@ -83,7 +87,7 @@ struct processing_buffers { float *samples, *samples_sc, *waveform, *waveform_sc, *tic_wf, *slice_wf, *tic_c; fftwf_complex *fft, *sc_fft, *tic_fft, *slice_fft; fftwf_plan plan_a, plan_b, plan_c, plan_d, plan_e, plan_f, plan_g; - struct filter *hpf, *lpf; + struct filter *lpf; double period,sigma,be,waveform_max,phase,tic_pulse,toc_pulse,amp; double cal_phase; int waveform_max_i; @@ -108,6 +112,10 @@ struct calibration_data { uint64_t *events; }; +struct filter { + double a0,a1,a2,b1,b2; +}; + void setup_buffers(struct processing_buffers *b); void pb_destroy(struct processing_buffers *b); struct processing_buffers *pb_clone(struct processing_buffers *p); @@ -117,29 +125,50 @@ void setup_cal_data(struct calibration_data *cd); void cal_data_destroy(struct calibration_data *cd); int test_cal(struct processing_buffers *p); int process_cal(struct processing_buffers *p, struct calibration_data *cd); +void make_hp(struct filter *f, double freq); /* audio.c */ struct processing_data { struct processing_buffers *buffers; uint64_t last_tic; + int last_step; //!< Guess of step (buffers index) to try first, based on last iteration int is_light; }; -int start_portaudio(int *nominal_sample_rate, double *real_sample_rate); +#define AUDIO_RATES {22050, 44100, 48000, 96000, 192000 } +#define AUDIO_RATE_LABELS {"22.05 kHz", "44.1 kHz", "48 kHz", "96 kHz", "192 kHz" } +#define NUM_AUDIO_RATES ARRAY_SIZE((int[])AUDIO_RATES) + +int start_portaudio(int device, int *nominal_sample_rate, double *real_sample_rate, int hpf_freq, bool light); int terminate_portaudio(); -uint64_t get_timestamp(int light); -int analyze_pa_data(struct processing_data *pd, int bph, double la, uint64_t events_from); +uint64_t get_timestamp(); +void fill_buffers(struct processing_buffers *ps); +bool analyze_pa_data(struct processing_data *pd, int step, int bph, double la, uint64_t events_from); int analyze_pa_data_cal(struct processing_data *pd, struct calibration_data *cd); void set_audio_light(bool light); +struct audio_device { + const char* name; //!< Name of device from port audio + bool good; //!< Is this suitable or not? E.g., playback only. + bool isdefault; //!< This is the default device; + unsigned rates; //!< Bitmask of allowed rates from AUDIO_RATES + +}; +int get_audio_devices(const struct audio_device **devices); +int get_audio_device(void); +int set_audio_device(int device, int *nominal_sr, double *real_sr, int hpf_freq, bool light); +void set_audio_hpf(int cutoff); +const struct filter* get_audio_hpf(void); /* computer.c */ +struct display; + struct snapshot { struct processing_buffers *pb; int is_old; uint64_t timestamp; int is_light; - int nominal_sr; + int nominal_sr; // W/O calibration, but does include light mode decimation int calibrate; int bph; double la; // deg @@ -157,13 +186,14 @@ struct snapshot { int cal_result; // 0.1 s/d // data dependent on bph, la, cal - double sample_rate; + double sample_rate; // Includes calibration int guessed_bph; double rate; double be; double amp; - double trace_centering; + // State related to displaying the snapshot, not generated by computer + struct display *d; }; struct computer { @@ -196,24 +226,47 @@ void unlock_computer(struct computer *c); void compute_results(struct snapshot *s); /* output_panel.c */ +/* Snapshot display parameters, e.g. scale, centering. */ +struct display { + // Scaling factor for each beat. 1 means the chart is 1 beat wide, 0.5 + // means half a beat, etc. + double beat_scale; + /* Time of point used to anchor the paperstrip. Each paperstrip point's position is + * relative to the previous point. This point is the one with an absolute position that + * is kept the same, so that all the dots do not shift side to side as the scroll. */ + uint64_t anchor_time; + // Phase offset of point at anchor_time + double anchor_offset; +}; + struct output_panel { GtkWidget *panel; GtkWidget *output_drawing_area; + GtkWidget *displays; + GtkWidget *waveforms_box; GtkWidget *tic_drawing_area; GtkWidget *toc_drawing_area; GtkWidget *period_drawing_area; + GtkWidget *paperstrip_box; GtkWidget *paperstrip_drawing_area; GtkWidget *clear_button; + GtkWidget *left_button; + GtkWidget *right_button; + GtkWidget *zoom_button; + GtkWidget *zoom_orig_button; #ifdef DEBUG GtkWidget *debug_drawing_area; #endif + bool vertical_layout; + struct computer *computer; struct snapshot *snst; }; void initialize_palette(); -struct output_panel *init_output_panel(struct computer *comp, struct snapshot *snst, int border); +struct output_panel *init_output_panel(struct computer *comp, struct snapshot *snst, int border, bool vertical_layout); +void set_panel_layout(struct output_panel *op, bool vertical); void redraw_op(struct output_panel *op); void op_set_snapshot(struct output_panel *op, struct snapshot *snst); void op_set_border(struct output_panel *op, int i); @@ -235,6 +288,13 @@ struct main_window { GtkWidget *save_item; GtkWidget *save_all_item; GtkWidget *close_all_item; + + /* Audio Setup dialog */ + GtkWidget *audio_setup; + GtkComboBox *device_list; + GtkComboBox *rate_list; + GtkRange *hpf_range; + struct output_panel *active_panel; struct computer *computer; @@ -248,7 +308,12 @@ struct main_window { int bph; double la; // deg int cal; // 0.1 s/d - int nominal_sr; + int nominal_sr; // requested audio device rate + int audio_device;// Selected device + int audio_rate; // Selected rate + int hpf_freq; // Low-pass filter cutoff frequency + + bool vertical_layout; GKeyFile *config_file; gchar *config_file_name; @@ -272,7 +337,11 @@ void error(char *format,...); OP(bph, bph, int) \ OP(lift_angle, la, double) \ OP(calibration, cal, int) \ - OP(light_algorithm, is_light, int) + OP(light_algorithm, is_light, int) \ + OP(vertical_paperstrip, vertical_layout, bool) \ + OP(audio_device, audio_device, int) \ + OP(audio_rate, audio_rate, int) \ + OP(highpass_cutoff_freq, hpf_freq, int) struct conf_data { #define DEF(NAME,PLACE,TYPE) TYPE PLACE;