From 0ebea86cdb7cd6fa2cfc67d0c84d6a57b44e2203 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Fri, 12 Feb 2021 15:46:07 -0800 Subject: [PATCH 01/39] Add code for boolean config file options --- src/config.c | 2 ++ 1 file changed, 2 insertions(+) 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) { From 13269eea89a18ab48161333039f855a0e7b7690d Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Thu, 18 Feb 2021 00:26:20 -0800 Subject: [PATCH 02/39] Create utilities for menu items Create some utilities for creating menu items so there isn't so much copy and paste of the code. --- src/interface.c | 55 +++++++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/src/interface.c b/src/interface.c index 7c1d3ca..707c766 100644 --- a/src/interface.c +++ b/src/interface.c @@ -694,6 +694,29 @@ static void load(GtkMenuItem *m, struct main_window *w) gtk_widget_destroy(dialog); } +/* 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; +} + /* Set up the main window and populate with widgets */ static void init_main_window(struct main_window *w) { @@ -800,47 +823,29 @@ 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); gtk_menu_shell_append(GTK_MENU_SHELL(command_menu), gtk_separator_menu_item_new()); // ... 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); From 41ee57a03c5c03be6317eda01e4b876e8525384e Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Thu, 18 Feb 2021 00:38:17 -0800 Subject: [PATCH 03/39] Break up the output panel GUI creation function It's quite large. Split it into new -functions that create the various parts of the output panel. The waveforms (tic, toc, period, debug), the paper strip chart and its controls, and the window under the output display that contains them, each get their own function. There is some minimal support for a vertical vs horizontal paperstrip chart, but it's not used yet. --- src/output_panel.c | 121 ++++++++++++++++++++++++++++++--------------- src/tg.h | 3 ++ 2 files changed, 84 insertions(+), 40 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index 0e7fc8b..81b567e 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -782,89 +782,130 @@ void op_destroy(struct output_panel *op) free(op); } -struct output_panel *init_output_panel(struct computer *comp, struct snapshot *snst, int border) +/* 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) { - 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); - - GtkWidget *hbox2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); - gtk_box_pack_start(GTK_BOX(op->panel), hbox2, TRUE, TRUE, 0); - - GtkWidget *vbox2 = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); - gtk_box_pack_start(GTK_BOX(hbox2), vbox2, FALSE, TRUE, 0); + GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); // 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_box_pack_start(GTK_BOX(vbox), op->paperstrip_drawing_area, TRUE, TRUE, 0); 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); + // 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(GTK_ORIENTATION_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_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_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_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->displays = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); + gtk_box_pack_start(GTK_BOX(op->displays), paperstrip, FALSE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(op->displays), waveforms, TRUE, TRUE, 0); + + 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 + GtkWidget *paperstrip = create_paperstrip(op, vertical); + // Tic/toc/period waveform area + GtkWidget *waveforms = create_waveforms(op, vertical); + + place_displays(op, paperstrip, waveforms, vertical); + + return op->displays; +} + +struct output_panel *init_output_panel(struct computer *comp, struct snapshot *snst, int border) +{ + 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, true); + return op; } diff --git a/src/tg.h b/src/tg.h index 789f66c..86785d4 100644 --- a/src/tg.h +++ b/src/tg.h @@ -200,11 +200,14 @@ struct output_panel { GtkWidget *panel; GtkWidget *output_drawing_area; + GtkWidget *displays; GtkWidget *tic_drawing_area; GtkWidget *toc_drawing_area; GtkWidget *period_drawing_area; GtkWidget *paperstrip_drawing_area; GtkWidget *clear_button; + GtkWidget *left_button; + GtkWidget *right_button; #ifdef DEBUG GtkWidget *debug_drawing_area; #endif From 4e68612842def7dc3eacfde613e62d7e0abadc52 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Thu, 18 Feb 2021 01:47:15 -0800 Subject: [PATCH 04/39] Use resizble paned widget for paperstrip and waveforms This lets the size of the paperstrip vs the waveforms be adjusted by sliding the pane between them. To get the layout right, we need to give the waveforms a minimum size. 300x150 seems about the minimum. It should look basically the same as it did before. Each snapshot has its own unique pane position. Adjusting one tab in the notebook doesn't adjust the other tabs. It might be nice if that weren't the case and they were all synced. --- src/output_panel.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index 81b567e..095d31b 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -835,18 +835,21 @@ static GtkWidget* create_waveforms(struct output_panel *op, bool vertical) // Tic waveform area op->tic_drawing_area = gtk_drawing_area_new(); + 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_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_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); @@ -865,9 +868,10 @@ static GtkWidget* create_waveforms(struct output_panel *op, bool vertical) * 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->displays = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); - gtk_box_pack_start(GTK_BOX(op->displays), paperstrip, FALSE, TRUE, 0); - gtk_box_pack_start(GTK_BOX(op->displays), waveforms, TRUE, TRUE, 0); + op->displays = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL); + gtk_paned_set_wide_handle(GTK_PANED(op->displays), TRUE); + gtk_paned_pack1(GTK_PANED(op->displays), paperstrip, FALSE, FALSE); + gtk_paned_pack2(GTK_PANED(op->displays), waveforms, TRUE, FALSE); gtk_box_pack_end(GTK_BOX(op->panel), op->displays, TRUE, TRUE, 0); gtk_widget_show(op->displays); From 6db5467e74c03e486e0f3bfae52c1a8985901dc8 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Tue, 12 Jan 2021 08:32:14 -0800 Subject: [PATCH 05/39] Fix race with light mode switch and audio callback When light mode is toggled, it's possible for the buffer pointer or timestamp to get messed up if it happens while the callback is running. The callback will read the current values when it starts, then light mode switch resets them to zero, and then when the callback finishes it updates them based on the original values, effectvely undoing to reset to zero. This doesn't cause a crash, but it does mean the timestamp isn't quite right. It also will cause problems if any code wants to process the audio data as it arrives. It could cause a single callback to be processed with the incorrect high pass filter. The easiest way to fix this is to pause the input stream while doing the mode switch. It could be fixed by making the entire callback a critical section or having the mode switch done asyncrounsly via the callback, but this makes the callback more complex and adds overhead. It seems better to add the extra work to the mode switch rather than the callback, since the former is a rare operation that isn't performance critical while the latter is. --- src/audio.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/audio.c b/src/audio.c index 05574fe..5e7cea1 100644 --- a/src/audio.c +++ b/src/audio.c @@ -25,6 +25,9 @@ int write_pointer = 0; uint64_t timestamp = 0; pthread_mutex_t audio_mutex; +/* Audio input stream object */ +static PaStream *stream; + /* Data for PA callback to use */ static struct callback_info { int channels; //!< Number of channels @@ -90,8 +93,6 @@ static int paudio_callback(const void *input_buffer, int start_portaudio(int *nominal_sample_rate, double *real_sample_rate) { - PaStream *stream; - if(pthread_mutex_init(&audio_mutex,NULL)) { error("Failed to setup audio mutex"); return 1; @@ -235,11 +236,18 @@ int analyze_pa_data_cal(struct processing_data *pd, struct calibration_data *cd) void set_audio_light(bool light) { if(info.light != light) { + Pa_StopStream(stream); pthread_mutex_lock(&audio_mutex); + info.light = light; memset(pa_buffers, 0, sizeof(pa_buffers)); write_pointer = 0; timestamp = 0; + pthread_mutex_unlock(&audio_mutex); + + PaError err = Pa_StartStream(stream); + if(err != paNoError) + error("Error re-starting audio input: %s", Pa_GetErrorText(err)); } } From 6664f844cb993dd3930edfb863e0ad44cb2f27db Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Mon, 22 Mar 2021 20:07:03 -0700 Subject: [PATCH 06/39] Fix uninitialized variable warning in read_file() Silences a compiler warning. This could actually happen if the save file had no snapshots. It should always have some, but format does allow for zero, which would probably crash. --- src/serializer.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/serializer.c b/src/serializer.c index e772f66..3676f29 100644 --- a/src/serializer.c +++ b/src/serializer.c @@ -532,6 +532,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; From 74f764fec2d7a043af47837e492af46775a14028 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Tue, 30 Mar 2021 23:56:45 -0700 Subject: [PATCH 07/39] Processes only need steps for large speedup I measured this as reducing CPU usage from ~78% to ~45%. Almost doubling the speed, which is expected as described below. Tg processes the audio in a series of steps that double in size: 2, 4, 8, and then 16 seconds long. This is the one to four dots shown in the display. Tg starts at step 1 (2 seconds) and goes up, stopping if a step doesn't pass. Data from smaller steps isn't used if a larger step passes. This means when running at a constant step 4 with good signal, CPU usage is almost double what it needs to be, since steps 1-3 are processed and unused and add up to 14 seconds, almost as much as step 4's 16 seconds. Change this to start at the previous iteration's step. With good signal, only step 4 will be processed and CPU usage is cut almost in half. If the previous step fails, it will try smaller steps. If it passes and is not yet the top step, it will try larger steps. End result should end up on the same step as before, but get there sooner. It could be slower if the step drops a lot, e.g. from 4 to 0, but this happens far less often than the step saying the same or nearly the same. To do this, I moved the step logic out of analyze_pa_data() and entirely into compute_update(). analyze_pa_data() will now just processes one step and compute_update() decides which step(s). Previously, analyze_pa_data() did all steps and compute_update() decided which step to actually use. There is a small change to the algorithm. To be good, a step needs to pass a number of changes done in process(). Then there is one more check, of sigma vs period, done in compute_update(). Previously a step didn't need to pass the sigma check and it still counted enough to increase last_tic and show up as a signal dot in the display. But the data wasn't actually used unless it passed to sigma check too. I no longer keep track of "partially" passing steps like this. Either it passes all checks, including the sigma check, or not. last_tic and signal level only count fully passing steps. I see no practical difference in my tests, but I think it could show up with some kind of marginal signal that has a high error in period estimation with the longer steps. --- src/audio.c | 29 ++++++++++------------------- src/computer.c | 45 ++++++++++++++++++++++++++++++++++++--------- src/tg.h | 6 +++++- 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/src/audio.c b/src/audio.c index 05574fe..d8a86db 100644 --- a/src/audio.c +++ b/src/audio.c @@ -164,7 +164,7 @@ uint64_t get_timestamp(int light) return ts; } -static void fill_buffers(struct processing_buffers *ps, int light) +void fill_buffers(struct processing_buffers *ps, int light) { pthread_mutex_lock(&audio_mutex); uint64_t ts = timestamp; @@ -187,26 +187,17 @@ static void fill_buffers(struct processing_buffers *ps, int light) } } -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) diff --git a/src/computer.c b/src/computer.c index 03942d3..938785e 100644 --- a/src/computer.c +++ b/src/computer.c @@ -91,19 +91,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, pd->is_light); + + 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 == NSTEPS-1 && ps[step].amp < 0 ? step : step+1; } else { + debug("---\n"); c->actv->is_old = 1; - c->actv->signal = -signal; + c->actv->signal = 0; } } @@ -251,6 +277,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); diff --git a/src/tg.h b/src/tg.h index 789f66c..04f3e11 100644 --- a/src/tg.h +++ b/src/tg.h @@ -75,6 +75,8 @@ #endif #define UNUSED(X) (void)(X) +#define BIT(n) (1u << (n)) +#define BITMASK(n) ((1u << (n)) - 1u) /* algo.c */ struct processing_buffers { @@ -122,13 +124,15 @@ int process_cal(struct processing_buffers *p, struct calibration_data *cd); 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); 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); +void fill_buffers(struct processing_buffers *ps, int light); +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); From 87e06226bc5b945ab3fa242ff6f6bc8fec7bbc23 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Thu, 1 Apr 2021 16:15:57 -0700 Subject: [PATCH 08/39] Draw partial signal dot for no amplitude Previously max signal level (4 dots) but with no amplitude measured was counted as signal level 3. But level 3 or lower with no amplitude sill counts as the same level. Have compute_update() no longer do this signal level adjustment and just report the level used, which indicates the averaging interval. The dot graphic will now indicate "no amplitude" by using a hollow dot for the final signal level's dot. This way the number of dots always shows the averaging interval and a hollow dot always shows that the signal is too poor to measure amplitude. --- src/computer.c | 2 +- src/output_panel.c | 24 +++++++++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/computer.c b/src/computer.c index 938785e..3f86ea3 100644 --- a/src/computer.c +++ b/src/computer.c @@ -125,7 +125,7 @@ static void compute_update(struct computer *c) c->actv->pb = pb_clone(&ps[step]); c->actv->is_old = 0; /* Signal's range is 0 to NSTEPS, while step is -1 to NSTEPS-1, i.e. signal = step+1 */ - c->actv->signal = step == NSTEPS-1 && ps[step].amp < 0 ? step : step+1; + c->actv->signal = step+1; } else { debug("---\n"); c->actv->is_old = 1; diff --git a/src/output_panel.c b/src/output_panel.c index 0e7fc8b..835e4f8 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -112,11 +112,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 +135,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 +143,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 +203,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; From 2f1f7f943ae75dd57db1c74a7dd4eedfd952d1f4 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Wed, 7 Apr 2021 18:09:38 -0700 Subject: [PATCH 09/39] Update m4/attributes.m4 to a newer upstream version This was the last one from systemd before they switched to Meson. Needed to update some of the autoconf script to match a change w.r.t. CFLAGS in the upstream attributes.m4. --- Makefile.am | 3 +- configure.ac | 4 ++- m4/attributes.m4 | 93 +++++++++++++++++++----------------------------- 3 files changed, 42 insertions(+), 58 deletions(-) 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/configure.ac b/configure.ac index ed85057..6be5898 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,7 +19,8 @@ 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]) diff --git a/m4/attributes.m4 b/m4/attributes.m4 index c7ef73e..51ac88b 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 ]) @@ -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" ]) From 53f6de7ba13766a3974181bb9c34815d318c568e Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Wed, 7 Apr 2021 18:10:37 -0700 Subject: [PATCH 10/39] Fix warning from m4/attributes.m4 Another AC_LANG_SOURCE warning that wasn't fixed yet. --- m4/attributes.m4 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/m4/attributes.m4 b/m4/attributes.m4 index 51ac88b..023d9b8 100644 --- a/m4/attributes.m4 +++ b/m4/attributes.m4 @@ -70,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" From e1e43678c98e065340d46adf0a849f618e93df97 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Sat, 13 Feb 2021 15:30:14 -0800 Subject: [PATCH 11/39] Fix issue preventing BPH below 14400 from working Periods (two beats) longer than 500 ms were rejected as too long. This effectively limits the low end of BPH to 14400. Any slower and the correct period length is greater than 500 ms, e.g. 12000 BPH is a 600 ms period. Change the limit to be 20% slower than the nominal value for the BPH. If the BPH is set, then use that, when BPH is guessed, use MIN_BPH. A MIN_BPH of 12000 will increases the max period to 720 ms when guessing. It could be longer if one were able to set a slower than MIN_BPH period length. --- src/algo.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/algo.c b/src/algo.c index 1c744cb..36f1601 100644 --- a/src/algo.c +++ b/src/algo.c @@ -898,8 +898,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 : MIN_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 +911,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) { From 5fa7289ba1be6d745bb63341bd4dcc3cb9ef67b5 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Tue, 13 Apr 2021 01:23:50 -0700 Subject: [PATCH 12/39] Lower min BPH to 8100 Allow lower BPH values. Create another setting that affects the BPH guessing code so it doesn't get confused by fractions of faster beats. Lowest value is 2.25 Hz or 8100 BPH. Lower rates do not work well or at all. Tg uses a minimum 2 second detection window and needs to see two periods in this window. So that it can see the 1st period matches the 2nd period and the time between them is about what it should be from the BPH. In order to get two complete periods (two beats each) in a 2 second window, one needs 7200 BPH. Below this and there might only be one period in the window and nothing to detect. But even at 7200, the beats at the beginning and end of the window might be cut off, so it's necessary to go a little faster to be reliable. --- src/algo.c | 2 +- src/tg.h | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/algo.c b/src/algo.c index 36f1601..b7d11f7 100644 --- a/src/algo.c +++ b/src/algo.c @@ -901,7 +901,7 @@ void process(struct processing_buffers *p, int bph, double la, int light) p->ready = !compute_period(p,bph); /* Limit to 20% greater when period is known, or 500 ms when guessing period */ - const int min_bph = bph ? bph : MIN_BPH; + 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"); diff --git a/src/tg.h b/src/tg.h index 789f66c..4aedd82 100644 --- a/src/tg.h +++ b/src/tg.h @@ -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 From 22e2c68d5b2b123232015c8ffe3222cde884570c Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Sat, 3 Apr 2021 15:26:45 -0700 Subject: [PATCH 13/39] Add an RPM spec file for building on Fedora, etc. This works on Fedora, Redhat, CentOS, etc. It also works for autobuilding on COPR. It will regenerate the autoconf code, which allows it to work from the git tree, i.e. without the configure script that is only in the release tarballs. The version number will be injected automatically, both when building with autoconf and on COPR, which was quite a challenge. When the configure script is run, it will preprocess the spec file and an include file for it and inject the version. The spec file's macro logic will detect that this has been done and use it. But on COPR the SRPM is created by rpkg without running configure, and there is no way to add extra commands to do it, so this won't work. However, rpkg has its own preprocessing systems for spec files and so that is used to inject the version number. The spec file macro logic will use this if the autoconf injected version is not present. It's also possible to specify the version manually on the rpmbuild command line by defining the macro "version". If pkgrel is defined then a release is being generated and the RPM will be the release specified in pkgrel. If pkgrel is not defined, then it's assumed a snapshot is being built and the git date and commit are added following the Fedora guidelines for snapshots. Again, this needs to be done in two different ways to support building from the git checkout directly or using COPR, which builds binary RPMs from an SRPM rather than from a git checkout. --- .gitignore | 1 + README.md | 19 +++++++++ configure.ac | 4 +- packaging/tg-timer.in.spec | 80 ++++++++++++++++++++++++++++++++++++++ packaging/tg-timer.inc.in | 2 + 5 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 packaging/tg-timer.in.spec create mode 100644 packaging/tg-timer.inc.in 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/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..b54ca73 100644 --- a/configure.ac +++ b/configure.ac @@ -20,7 +20,9 @@ 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], [], []) -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/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@ From 05e85bc3ccf2f4b745e8e64275e2e208ba0e9dff Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Thu, 18 Feb 2021 02:23:26 -0800 Subject: [PATCH 14/39] Add vertical/horizontal paperstrip option This allow changing the layout so the paperstrip is horizontal, scrolling to the left, with the three waveforms arraigned horizontally below the paperstrip. The effect is to make the paperstrip longer and give it more space. For many tasks the paper strip is much more useful than the waveform displays. This allows adjusting the layout to be more useful in those cases. A new checkbox in the menu is added to control this. It can be changed on the fly and the widgets will be re-arraigned. A config file option, "vertical_paperstrip", is added to remember the current setting. Default vertical layout to true if not present in config file. If multiple snapshots are present, the change of orientation will affect all of them, rather that just the current viewed one. It would be possible to change this. --- src/interface.c | 24 ++++++++++++++-- src/output_panel.c | 70 ++++++++++++++++++++++++++++++++++++++-------- src/tg.h | 13 +++++++-- 3 files changed, 92 insertions(+), 15 deletions(-) diff --git a/src/interface.c b/src/interface.c index 707c766..9ae4cb7 100644 --- a/src/interface.c +++ b/src/interface.c @@ -422,7 +422,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); @@ -694,6 +694,22 @@ 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. */ @@ -839,6 +855,9 @@ static void init_main_window(struct main_window *w) // ... Calibrate checkbox 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()); // ... Close all @@ -936,6 +955,7 @@ static void start_interface(GApplication* app, void *p) w->la = DEFAULT_LA; w->calibrate = 0; w->is_light = 0; + w->vertical_layout = true; load_config(w); @@ -959,7 +979,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 095d31b..97b00cf 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -549,9 +549,19 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp GtkAllocation temp; gtk_widget_get_allocation (op->paperstrip_drawing_area, &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; + + cairo_translate(c, height, 0); + cairo_rotate(c, M_PI/2); + } int stopped = 0; if( snst->events_count && @@ -790,7 +800,7 @@ static GtkWidget* create_paperstrip(struct output_panel *op, bool vertical) // Paperstrip op->paperstrip_drawing_area = gtk_drawing_area_new(); - gtk_widget_set_size_request(op->paperstrip_drawing_area, 300, 0); + gtk_widget_set_size_request(op->paperstrip_drawing_area, 150, 150); gtk_box_pack_start(GTK_BOX(vbox), op->paperstrip_drawing_area, TRUE, TRUE, 0); 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); @@ -831,7 +841,7 @@ static GtkWidget* create_paperstrip(struct output_panel *op, bool vertical) * them. Vertical controls how the waves are stacked. */ static GtkWidget* create_waveforms(struct output_panel *op, bool vertical) { - GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); + GtkWidget *box = gtk_box_new(vertical ? GTK_ORIENTATION_VERTICAL : GTK_ORIENTATION_HORIZONTAL, 10); // Tic waveform area op->tic_drawing_area = gtk_drawing_area_new(); @@ -868,11 +878,24 @@ static GtkWidget* create_waveforms(struct output_panel *op, bool vertical) * 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->displays = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL); + op->vertical_layout = vertical; + + op->displays = gtk_paned_new(vertical ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL); gtk_paned_set_wide_handle(GTK_PANED(op->displays), TRUE); - gtk_paned_pack1(GTK_PANED(op->displays), paperstrip, FALSE, FALSE); + + gtk_paned_pack1(GTK_PANED(op->displays), paperstrip, vertical ? FALSE : TRUE, FALSE); + + gtk_orientable_set_orientation(GTK_ORIENTABLE(waveforms), vertical ? GTK_ORIENTATION_VERTICAL : GTK_ORIENTATION_HORIZONTAL); 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); + gtk_box_pack_end(GTK_BOX(op->panel), op->displays, TRUE, TRUE, 0); gtk_widget_show(op->displays); } @@ -883,16 +906,41 @@ static void place_displays(struct output_panel *op, GtkWidget *paperstrip, GtkWi static GtkWidget *create_displays(struct output_panel *op, bool vertical) { // The paperstrip and buttons - GtkWidget *paperstrip = create_paperstrip(op, vertical); + op->paperstrip_box = create_paperstrip(op, vertical); // Tic/toc/period waveform area - GtkWidget *waveforms = create_waveforms(op, vertical); + op->waveforms_box = create_waveforms(op, vertical); - place_displays(op, paperstrip, waveforms, vertical); + place_displays(op, op->paperstrip_box, op->waveforms_box, vertical); return op->displays; } -struct output_panel *init_output_panel(struct computer *comp, struct snapshot *snst, int border) +/* 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)); @@ -909,7 +957,7 @@ struct output_panel *init_output_panel(struct computer *comp, struct snapshot *s 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, true); + create_displays(op, vertical); return op; } diff --git a/src/tg.h b/src/tg.h index 86785d4..f92d640 100644 --- a/src/tg.h +++ b/src/tg.h @@ -201,22 +201,28 @@ struct output_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; #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); @@ -253,6 +259,8 @@ struct main_window { int cal; // 0.1 s/d int nominal_sr; + bool vertical_layout; + GKeyFile *config_file; gchar *config_file_name; struct conf_data *conf_data; @@ -275,7 +283,8 @@ 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) struct conf_data { #define DEF(NAME,PLACE,TYPE) TYPE PLACE; From 6f088c01f695ad80e814ba7116d42fa3878522ff Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Sun, 7 Mar 2021 21:43:36 -0800 Subject: [PATCH 15/39] Scale font using paperstrip/waveform window size Using the size of the main app window, what was done, doesn't work so well now that the space allocation between the paperstrip and waveforms can be changed. This way making the waveforms (or paperstrip) smaller will give a smaller font that fits in the smaller space better. --- src/output_panel.c | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index 97b00cf..c34b597 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -322,10 +322,9 @@ static void expose_waveform( int width = temp.width; int height = temp.height; - gtk_widget_get_allocation(gtk_widget_get_toplevel(da), &temp); - int font = temp.width / 90; - if(font < 12) - font = 12; + int font = width / 25; + font = font < 12 ? 12 : font > 24 ? 24 : font; + int i; cairo_set_font_size(c,font); @@ -670,18 +669,14 @@ 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); - - sprintf(s, "%.1f ms", snst->calibrate ? + char s[32]; + snprintf(s, sizeof(s), "%.1f ms", snst->calibrate ? 1000. / zoom_factor : 3600000. / (snst->guessed_bph * zoom_factor)); + cairo_text_extents_t extents; cairo_text_extents(c,s,&extents); cairo_move_to(c, (width - extents.x_advance)/2, height - 30); cairo_show_text(c,s); From 9f1f263862cab37f42a6ad2cdfd7932fce086bfe Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Sun, 7 Mar 2021 02:58:10 -0800 Subject: [PATCH 16/39] Move snapshot display parameters to their own struct Create a new struct for the output_panel snapshot display parameters. Instead of being mixed into the snapshot data from the computer, there will just be a pointer to the display parameter struct in the snapshot. The clone and delete snapshot functions will clone and delete the display parameters, if they exist. The computer's snapshots don't have display parameters so the pointer will just be NULL for them. Each time a new snapshot is passed from the computer to the interface, the pointer to the display parameters can be kept so that each parameter doesn't need to be copied. paperstrip_draw_event() will allocate a new display parameter when it gets a snapshot for the first time. This will make it a lot easier to add new display parameters just by adding them to the struct. And they can be initialized in the output panel code that uses them and knows what they should be. The computer doesn't need to know anything about them. This does break the savefile format slightly since the struct field names are coded into the savefile. Maybe that design has some drawbacks when it comes to backward compatibility. In this case the trace centering parameter is the only one broken and it doesn't really matter. --- src/computer.c | 6 +++++- src/interface.c | 4 ++-- src/output_panel.c | 16 ++++++++++++---- src/serializer.c | 11 +++++------ src/tg.h | 10 +++++++++- 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/computer.c b/src/computer.c index 03942d3..a4f2bda 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); } @@ -267,7 +272,6 @@ 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; diff --git a/src/interface.c b/src/interface.c index 9ae4cb7..59f145d 100644 --- a/src/interface.c +++ b/src/interface.c @@ -899,11 +899,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) { diff --git a/src/output_panel.c b/src/output_panel.c index c34b597..299cce3 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -528,11 +528,19 @@ static gboolean period_draw_event(GtkWidget *widget, cairo_t *c, struct output_p 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 + + // Allocate initial display parameters. Will persist across each new + // snapshot after this. + if (!ssd) { + ssd = op->snst->d = calloc(1, sizeof(*snst->d)); + } + if(snst->calibrate) { sweep = snst->nominal_sr; zoom_factor = PAPERSTRIP_ZOOM_CAL; @@ -628,7 +636,7 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp 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); + double event = now - snst->events[i] + snst->d->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; @@ -742,7 +750,7 @@ static void handle_center_trace(GtkButton *b, struct output_panel *op) new_centering = fmod(last_ev + .5*sweep , sweep); } else new_centering = 0; - snst->trace_centering = new_centering; + snst->d->trace_centering = new_centering; gtk_widget_queue_draw(op->paperstrip_drawing_area); } @@ -754,7 +762,7 @@ static void shift_trace(struct output_panel *op, double direction) 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); + snst->d->trace_centering = fmod(snst->d->trace_centering + sweep * (1.+.1*direction), sweep); gtk_widget_queue_draw(op->paperstrip_drawing_area); } diff --git a/src/serializer.c b/src/serializer.c index e772f66..f7eee8f 100644 --- a/src/serializer.c +++ b/src/serializer.c @@ -347,7 +347,7 @@ 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->trace_centering); SERIALIZE(int,is_light); return serialize_struct_end(f); } @@ -370,10 +370,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 +422,7 @@ 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->trace_centering); SCAN(int,is_light); if(eat_object(f)) goto error; diff --git a/src/tg.h b/src/tg.h index f92d640..cd94301 100644 --- a/src/tg.h +++ b/src/tg.h @@ -133,6 +133,8 @@ int analyze_pa_data_cal(struct processing_data *pd, struct calibration_data *cd) void set_audio_light(bool light); /* computer.c */ +struct display; + struct snapshot { struct processing_buffers *pb; int is_old; @@ -163,7 +165,8 @@ struct snapshot { double be; double amp; - double trace_centering; + // State related to displaying the snapshot, not generated by computer + struct display *d; }; struct computer { @@ -196,6 +199,11 @@ void unlock_computer(struct computer *c); void compute_results(struct snapshot *s); /* output_panel.c */ +/* Snapshot display parameters, e.g. scale, centering. */ +struct display { + double trace_centering; +}; + struct output_panel { GtkWidget *panel; From 5e3212e59dac4f78703e9a05233309057d3220f6 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Wed, 17 Mar 2021 16:49:28 -0700 Subject: [PATCH 17/39] Size fonts based on drawing area Since the space allocation between the paperstrip and waveforms can be changed now, using just the main window size to control font size doesn't work as well. E.g., a small waveform in a large app window that is mostly paperstrip will still get a large font, because the app window is large. Also adjust font spacing some. Use the vertical size of the font to adjust legend in paperstrip and provide consistent margins in waveforms. --- src/output_panel.c | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index 299cce3..4d32ccd 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -319,16 +319,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; - int font = width / 25; - font = font < 12 ? 12 : font > 24 ? 24 : font; + 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); - int i; + cairo_font_extents_t fextents; + cairo_font_extents(c, &fextents); - cairo_set_font_size(c,font); + 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); @@ -345,7 +350,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); } } @@ -353,7 +358,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; @@ -385,7 +390,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; @@ -393,7 +398,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) { @@ -685,8 +690,10 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp 1000. / zoom_factor : 3600000. / (snst->guessed_bph * zoom_factor)); cairo_text_extents_t extents; - cairo_text_extents(c,s,&extents); - cairo_move_to(c, (width - extents.x_advance)/2, height - 30); + 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; From 5fc7d6733fb4f205e2d93415a4311e3866a86fa3 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Wed, 17 Mar 2021 16:53:51 -0700 Subject: [PATCH 18/39] Use widget parameter instead of state in paperstrip expose Doesn't depend on state having the widget saved or what name is used this way. Also avoids unused parameter warning. --- src/output_panel.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/output_panel.c b/src/output_panel.c index 4d32ccd..8058512 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -560,7 +560,7 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp cairo_init(c); GtkAllocation temp; - gtk_widget_get_allocation (op->paperstrip_drawing_area, &temp); + gtk_widget_get_allocation(widget, &temp); int width, height; /* The paperstrip is coded to be vertical; horizontal uses cairo to rotate it. */ From 7b81287b1e72e33650756caa6f93b453e8fe69c2 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Wed, 17 Mar 2021 17:29:19 -0700 Subject: [PATCH 19/39] Add zoom slider for paperstrip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a pair of buttons that overlay the paperstrip. One is a Gtk slider button which allows zooming in and out with a slider. The other will reset to the default zoom. This zoom is just zoom the chart in the horizontal direction, i.e. error in each beat. Not vertically, which would show more or fewer beats at once. The zoom ranges from the 1.0 to 0.01. E.g., for a 10 beat/sec movement, the chart's width will be at most 100 ms, an entire beat, to 10 ms, the default, down to 1 ms. The forced switch from 0.1 in normal mode to 0.01 in calibration mode is removed. If one wants to zoom in for calibration, then one is free to do that. Due to the way the paperstrip is drawn, only zoom values that are exact integer fractions will work. E.g., 0.5 = 1/2, 0.2 = 1/5, and 0.10 = 1/10, will all work. But 0.12 = 1/8.333 will not, because 8â…“ is not an integer. This makes the initial zoom jumps quite large. --- src/output_panel.c | 89 ++++++++++++++++++++++++++++++++++++++++++---- src/serializer.c | 5 +++ src/tg.h | 3 ++ 3 files changed, 90 insertions(+), 7 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index 8058512..b808b9c 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) @@ -530,6 +537,27 @@ 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); + + // Zoom must be integral fraction for display to work + return 1.0 / round(1.0 / scale); +} + +// Convert value in samples to milliseconds. +static inline double s2ms(const struct snapshot *snst, double samples) +{ + return samples * 1000.0 / snst->sample_rate; +} + static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct output_panel *op) { int i; @@ -537,25 +565,27 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp struct display *ssd = snst->d; uint64_t time = snst->timestamp ? snst->timestamp : get_timestamp(snst->is_light); double sweep; - int zoom_factor; + double zoom_factor; double slope = 1000; // detected rate: 1000 -> do not display // 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)); } + zoom_factor = 1/ssd->beat_scale; 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.); } + // Width of chart's displayed portion of the beat, in samples + const double chart_width = sweep * ssd->beat_scale; cairo_init(c); @@ -585,6 +615,7 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp int strip_width = round(width / (1 + PAPERSTRIP_MARGIN)); + // Beat error slope lines or calibration slope lines cairo_set_line_width(c,1.3); slope *= strip_width; @@ -616,6 +647,7 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp cairo_set_line_width(c,1); + // Margin lines int left_margin = (width - strip_width) / 2; int right_margin = (width + strip_width) / 2; cairo_move_to(c, left_margin + .5, .5); @@ -625,6 +657,7 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp cairo_set_source(c, green); cairo_stroke(c); + // Time grid lines double now = sweep*ceil(time/sweep); double ten_s = snst->sample_rate * 10 / sweep; double last_line = fmod(now/sweep, ten_s); @@ -638,6 +671,7 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp cairo_stroke(c); } + // Ticks and tocks dots cairo_set_source(c,stopped?yellow:white); for(i = snst->events_wp;;) { if(!snst->events_count || !snst->events[i]) break; @@ -665,6 +699,7 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp if(i == snst->events_wp) break; } + // Legend line cairo_set_source(c,white); cairo_set_line_width(c,2); cairo_move_to(c, left_margin + 3, height - 20.5); @@ -686,9 +721,8 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp cairo_set_font_size(c, font < 12 ? 12 : font > 24 ? 24 : font); char s[32]; - snprintf(s, sizeof(s), "%.1f ms", snst->calibrate ? - 1000. / zoom_factor : - 3600000. / (snst->guessed_bph * zoom_factor)); + snprintf(s, sizeof(s), "%.1f ms", s2ms(snst, chart_width)); + cairo_text_extents_t extents; cairo_font_extents_t fextents; cairo_text_extents(c, s, &extents); @@ -785,6 +819,25 @@ 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); + + ssd->beat_scale = scale; + + gtk_widget_queue_draw(op->paperstrip_drawing_area); +} + void op_set_snapshot(struct output_panel *op, struct snapshot *snst) { op->snst = snst; @@ -808,13 +861,35 @@ static GtkWidget* create_paperstrip(struct output_panel *op, bool vertical) { GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); + 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, 150, 150); - gtk_box_pack_start(GTK_BOX(vbox), op->paperstrip_drawing_area, TRUE, TRUE, 0); + 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 *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 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, GTK_ALIGN_END); + gtk_widget_set_opacity(box, 0.8); + gtk_overlay_add_overlay(GTK_OVERLAY(overlay), box); + + GtkWidget *zoom_orig = gtk_button_new_from_icon_name("zoom-original-symbolic", GTK_ICON_SIZE_BUTTON); + gtk_button_set_relief(GTK_BUTTON(zoom_orig), GTK_RELIEF_NONE); + g_signal_connect(zoom_orig, "clicked", G_CALLBACK(handle_zoom_original), op); + gtk_box_pack_start(GTK_BOX(box), zoom_orig, FALSE, FALSE, 0); + + 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); + 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); + // Buttons GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, TRUE, 0); diff --git a/src/serializer.c b/src/serializer.c index f7eee8f..485afc4 100644 --- a/src/serializer.c +++ b/src/serializer.c @@ -348,6 +348,7 @@ static int serialize_snapshot(FILE *f, struct snapshot *s, char *name) SERIALIZE(double,be); SERIALIZE(double,amp); SERIALIZE(double,d->trace_centering); + SERIALIZE(double,d->beat_scale); SERIALIZE(int,is_light); return serialize_struct_end(f); } @@ -423,6 +424,7 @@ static int scan_snapshot(FILE *f, struct snapshot **s, char **name) SCAN(double,be); SCAN(double,amp); SCAN(double,d->trace_centering); + SCAN(double,d->beat_scale); SCAN(int,is_light); if(eat_object(f)) goto error; @@ -452,6 +454,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; diff --git a/src/tg.h b/src/tg.h index cd94301..f35c277 100644 --- a/src/tg.h +++ b/src/tg.h @@ -202,6 +202,9 @@ void compute_results(struct snapshot *s); /* Snapshot display parameters, e.g. scale, centering. */ struct display { double trace_centering; + // Scaling factor for each beat. 1 means the chart is 1 beat wide, 0.5 + // means half a beat, etc. + double beat_scale; }; struct output_panel { From 0ab8b06d0d1d7de7e39e71a5ecc4ba119d198d8f Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Wed, 17 Mar 2021 17:51:26 -0700 Subject: [PATCH 20/39] Redo paperstrip display, new dot algorithm Re-write most of the code for generating the paperstrip. A new algorithm is used to place the dots which has an advantage in that it can display correctly with any level of zoom, not just at integer scales. What we do is find each event's relative position from the one before it. A relative offset of 0 would mean the event is exactly a multiple of the beat time from the previous event. A positive or negative value means it is earlier or later than expected. We can then scale this offset by any value. Since only the relative position between events is used there is no longer a problem when the accumulated offset wraps around a beat length but not the chart width. With all event positions relative, we need some absolute position from which to start. This is done by saving the absolute offset of the most recent event as an anchor. The next display will place the previous anchor at the same absolute offset and update the anchor to the new most recent event. This way the dots do not shift side-to-side as they scroll. Having done this, much of the other code relating to the paperstrip becomes simpler and there is a net reduction in lines of code. The dots are placed in nearly the same positions as before, with one significant difference. The slope is reversed, i.e. a movement running fast will slope up and to the left. This works better for the math, and appears to match what other timegraphers do. --- src/output_panel.c | 268 ++++++++++++++++++++++++++------------------- src/serializer.c | 6 +- src/tg.h | 7 +- 3 files changed, 166 insertions(+), 115 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index b808b9c..24c3480 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -548,8 +548,7 @@ static double get_beatscale(GtkScaleButton *b) const double scale = ω * exp(σ*zoom); debug("Zoom slider %.0f to scale %.03f = %.0fx\n", zoom, scale, 1/scale); - // Zoom must be integral fraction for display to work - return 1.0 / round(1.0 / scale); + return scale; } // Convert value in samples to milliseconds. @@ -558,15 +557,40 @@ 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; 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; - double zoom_factor; - double slope = 1000; // detected rate: 1000 -> do not display + + 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. @@ -575,18 +599,6 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp ssd->beat_scale = get_beatscale(GTK_SCALE_BUTTON(op->zoom_button)); } - zoom_factor = 1/ssd->beat_scale; - if(snst->calibrate) { - sweep = snst->nominal_sr; - slope = (double) snst->cal * zoom_factor / (10 * 3600 * 24); - } else { - sweep = snst->sample_rate * 3600. / snst->guessed_bph; - if(snst->events_count && snst->events[snst->events_wp]) - slope = - snst->rate * zoom_factor / (3600. * 24.); - } - // Width of chart's displayed portion of the beat, in samples - const double chart_width = sweep * ssd->beat_scale; - cairo_init(c); GtkAllocation temp; @@ -605,107 +617,143 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp cairo_rotate(c, M_PI/2); } - 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; - } - - int strip_width = round(width / (1 + PAPERSTRIP_MARGIN)); + // 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 -= time % (int)(beat_length + 0.5); // Beat error slope lines or calibration slope lines - 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); - } - } - } + // 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_line_width(c,1); - // Margin lines - int left_margin = (width - strip_width) / 2; - int right_margin = (width + strip_width) / 2; + const int left_margin = (width - strip_width) / 2; + const 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); // Time grid lines - 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); + 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); } - // Ticks and tocks dots - 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->d->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; + } + + 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_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); @@ -780,30 +828,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->d->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->d->trace_centering = fmod(snst->d->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); } @@ -833,6 +873,10 @@ static void handle_zoom(GtkScaleButton *b, struct output_panel *op) 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_queue_draw(op->paperstrip_drawing_area); diff --git a/src/serializer.c b/src/serializer.c index 485afc4..f74141e 100644 --- a/src/serializer.c +++ b/src/serializer.c @@ -347,8 +347,9 @@ static int serialize_snapshot(FILE *f, struct snapshot *s, char *name) SERIALIZE(double,rate); SERIALIZE(double,be); SERIALIZE(double,amp); - SERIALIZE(double,d->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); } @@ -423,8 +424,9 @@ static int scan_snapshot(FILE *f, struct snapshot **s, char **name) SCAN(double,rate); SCAN(double,be); SCAN(double,amp); - SCAN(double,d->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; diff --git a/src/tg.h b/src/tg.h index f35c277..396da46 100644 --- a/src/tg.h +++ b/src/tg.h @@ -201,10 +201,15 @@ void compute_results(struct snapshot *s); /* output_panel.c */ /* Snapshot display parameters, e.g. scale, centering. */ struct display { - double trace_centering; // 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 { From cd45a9f478bbf8a21671db46384c5dcb45cbe047 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Mon, 22 Mar 2021 15:42:01 -0700 Subject: [PATCH 21/39] Fix missing initialization in snapshot The snapshot display parameters should be initialized to NULL. Really, it would be better to always use calloc() so that every field is automatically initialized to zero/NULL rather than needing to remember to manaully do it for each field. --- src/computer.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/computer.c b/src/computer.c index a4f2bda..ea68576 100644 --- a/src/computer.c +++ b/src/computer.c @@ -276,6 +276,7 @@ struct computer *start_computer(int nominal_sr, int bph, double la, int cal, int s->la = la; s->cal = cal; s->is_light = light; + s->d = NULL; struct computer *c = malloc(sizeof(struct computer)); c->cdata = cd; From a161e643ced44f8f7925f79fc3e13ce364f711da Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Mon, 22 Mar 2021 20:03:08 -0700 Subject: [PATCH 22/39] Don't draw slope when no rate is known Check to see if the computer has figured out a rate, beat error, amplitude, etc. before drawing the rate slope lines. Otherwise the value of rate is uninitialized. --- src/output_panel.c | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index 24c3480..f0befa4 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -635,25 +635,27 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp time -= time % (int)(beat_length + 0.5); // Beat error slope lines or calibration slope lines - // 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); + 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_stroke(c); } // Margin lines From a187e556bf06ce349af996dec7983364e871b591 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Mon, 29 Mar 2021 17:28:16 -0700 Subject: [PATCH 23/39] Hide zoom reset button when at original scale When at the default zoom scale, hide the zoom reset button. No need to have it on the screen since it won't do anything. And it tells you that the scale is already at default. I noticed the zoom reset button in Chrome disappeared when at 100% and liked this feature. --- src/output_panel.c | 12 +++++++----- src/tg.h | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index f0befa4..baec60e 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -881,6 +881,7 @@ static void handle_zoom(GtkScaleButton *b, struct output_panel *op) 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); } @@ -925,17 +926,18 @@ static GtkWidget* create_paperstrip(struct output_panel *op, bool vertical) gtk_widget_set_opacity(box, 0.8); gtk_overlay_add_overlay(GTK_OVERLAY(overlay), box); - GtkWidget *zoom_orig = gtk_button_new_from_icon_name("zoom-original-symbolic", GTK_ICON_SIZE_BUTTON); - gtk_button_set_relief(GTK_BUTTON(zoom_orig), GTK_RELIEF_NONE); - g_signal_connect(zoom_orig, "clicked", G_CALLBACK(handle_zoom_original), op); - gtk_box_pack_start(GTK_BOX(box), zoom_orig, FALSE, FALSE, 0); - 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); 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); diff --git a/src/tg.h b/src/tg.h index 396da46..8d03342 100644 --- a/src/tg.h +++ b/src/tg.h @@ -227,6 +227,7 @@ struct output_panel { GtkWidget *left_button; GtkWidget *right_button; GtkWidget *zoom_button; + GtkWidget *zoom_orig_button; #ifdef DEBUG GtkWidget *debug_drawing_area; #endif From e6cc7a55a2503c6c25e7860853f4bbd7fa165319 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Tue, 30 Mar 2021 19:02:19 -0700 Subject: [PATCH 24/39] Round chart step up, rather than down, to beat length Rounding the current time down might cause the newest event to appear to be from the future. That confuses the paper strip math. Round up so that doesn't happen. --- src/output_panel.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/output_panel.c b/src/output_panel.c index baec60e..a65153e 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -632,6 +632,7 @@ static gboolean paperstrip_draw_event(GtkWidget *widget, cairo_t *c, struct outp * 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 From 21965d1e63ae1b679a6985b3748c42618eb7a9e8 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Wed, 31 Mar 2021 13:35:14 -0700 Subject: [PATCH 25/39] Utility functions for gtk orientation Turn a boolean vertical flag into a gtk orientation. Was tired of repeating this long expression. --- src/output_panel.c | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index a65153e..3646061 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -903,6 +903,17 @@ void op_destroy(struct output_panel *op) free(op); } +/* Wrappers around gtk_orientable_set_orientation() */ +static GtkOrientation vert_to_orient(bool vertical) +{ + return vertical ? GTK_ORIENTATION_VERTICAL : GTK_ORIENTATION_HORIZONTAL; +} + +static void set_orientation(GtkWidget *widget, bool vertical) +{ + gtk_orientable_set_orientation(GTK_ORIENTABLE(widget), vert_to_orient(vertical)); +} + /* 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) @@ -975,7 +986,7 @@ static GtkWidget* create_paperstrip(struct output_panel *op, bool vertical) * them. Vertical controls how the waves are stacked. */ static GtkWidget* create_waveforms(struct output_panel *op, bool vertical) { - GtkWidget *box = gtk_box_new(vertical ? GTK_ORIENTATION_VERTICAL : GTK_ORIENTATION_HORIZONTAL, 10); + GtkWidget *box = gtk_box_new(vert_to_orient(vertical), 10); // Tic waveform area op->tic_drawing_area = gtk_drawing_area_new(); @@ -1014,12 +1025,12 @@ static void place_displays(struct output_panel *op, GtkWidget *paperstrip, GtkWi { op->vertical_layout = vertical; - op->displays = gtk_paned_new(vertical ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_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); - gtk_orientable_set_orientation(GTK_ORIENTABLE(waveforms), vertical ? GTK_ORIENTATION_VERTICAL : GTK_ORIENTATION_HORIZONTAL); + set_orientation(waveforms, vert_to_orient(vertical)); gtk_paned_pack2(GTK_PANED(op->displays), waveforms, TRUE, FALSE); /* Make paperstrip arrows buttons point correct way */ From fcf239970b7316a71cb2897ab72c289d6d710be4 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Wed, 31 Mar 2021 13:37:33 -0700 Subject: [PATCH 26/39] Adjust zoom button for vertical/horizontal layout Rotating the paperstrip just does the graphics, not the GUI widgets, so they weren't working as well in horizontal mode. Add some code to pack and orient them well for either vertical or horizontal layout mode. --- src/output_panel.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/output_panel.c b/src/output_panel.c index 3646061..370bf6d 100644 --- a/src/output_panel.c +++ b/src/output_panel.c @@ -930,17 +930,18 @@ static GtkWidget* create_paperstrip(struct output_panel *op, bool vertical) 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 *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 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, GTK_ALIGN_END); + 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); @@ -1040,6 +1041,11 @@ static void place_displays(struct output_panel *op, GtkWidget *paperstrip, GtkWi 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); From 4282a1c870f14cb36a41689d50617edfa35ca358 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Tue, 12 Jan 2021 08:19:01 -0800 Subject: [PATCH 27/39] Read config file before starting audio This way settings that affect audio can be read in before audio starts. Since the config file is now read before starting audio, it is possible to start in light or normal mode as configured. If light mode was configured, then previously audio would be started in normal mode and then immediatly switch to light mode after processing a few callbacks. --- src/audio.c | 4 ++-- src/interface.c | 10 +++++----- src/tg.h | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/audio.c b/src/audio.c index 5e7cea1..05890c2 100644 --- a/src/audio.c +++ b/src/audio.c @@ -91,7 +91,7 @@ static int paudio_callback(const void *input_buffer, return 0; } -int start_portaudio(int *nominal_sample_rate, double *real_sample_rate) +int start_portaudio(int *nominal_sample_rate, double *real_sample_rate, bool light) { if(pthread_mutex_init(&audio_mutex,NULL)) { error("Failed to setup audio mutex"); @@ -122,7 +122,7 @@ int start_portaudio(int *nominal_sample_rate, double *real_sample_rate) } if(channels > 2) channels = 2; info.channels = channels; - info.light = false; + info.light = light; err = Pa_OpenDefaultStream(&stream,channels,0,paFloat32,PA_SAMPLE_RATE,paFramesPerBufferUnspecified,paudio_callback,&info); if(err!=paNoError) goto error; diff --git a/src/interface.c b/src/interface.c index 7c1d3ca..55c564d 100644 --- a/src/interface.c +++ b/src/interface.c @@ -917,11 +917,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; @@ -934,6 +929,11 @@ static void start_interface(GApplication* app, void *p) load_config(w); + if(start_portaudio(&w->nominal_sr, &real_sr, 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) diff --git a/src/tg.h b/src/tg.h index 789f66c..24eecea 100644 --- a/src/tg.h +++ b/src/tg.h @@ -125,7 +125,7 @@ struct processing_data { int is_light; }; -int start_portaudio(int *nominal_sample_rate, double *real_sample_rate); +int start_portaudio(int *nominal_sample_rate, double *real_sample_rate, 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); From cbd3a00c4328c28fbae4043cd14ce70f80bb2546 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Sun, 18 Apr 2021 16:43:49 -0700 Subject: [PATCH 28/39] Change one sample sigma estimation to 0 When there is just one period in the processing buffer it is not possible to calculate the sample standard deviation (sigma). In this case, the period value was being used as the sigma estimate, which effectively gives a huge sigma when there is 1 period in the buffer. This results in the processing buffer being rejected as bad and a larger buffer, which would have multiple periods, is never tried. One period per buffer would happen with a combination of long period and short buffer, e.g. a small BPH in light mode, since light mode uses buffers half as long as normal mode. Use 0 as the sigma value for 1 sample. This means the buffer will pass the sigma check and a larger buffer will be tried. --- src/algo.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/algo.c b/src/algo.c index 1c744cb..fa52137 100644 --- a/src/algo.c +++ b/src/algo.c @@ -452,7 +452,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; } From fbf47a85fe276d574118258c4466f545ad6452e6 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Fri, 8 Jan 2021 16:58:07 -0800 Subject: [PATCH 29/39] Run high pass filter in audio thread This moves the high pass filter out of prepare_data() and into the audio callback. prepare_data() will process the same data many times, up to 300 times, as it is repeatedly called to processes buffers that mostly overlap the last set of buffers. Running the HPF on incoming audio will process about 300x less data without changing the result. This produces about a 4% overall speed improvement. In the next step in the audio processing, the noise filter, each sample depends on every other sample in the buffer, so all samples need to be re-processed each time the buffers change. So it's not possible to do more processing past the HPF as the audio is received. I also benchmarked code that would apply the HPF while coping the data out of from the portaudio buffer to the ring buffer. I.e., a biquad filter function that did not modify the data in place, but instead had separate input and output pointers. Dealing with the audio combinations, mono/stereo, normal/light, took more code and there was no significant speed improvement. I also tried moving the HPF out of the audio callback and into the compute thread, while still applying the filter only once to new audio, but this was slower, doesn't spread the load across multple CPUS as well, and was more complex. --- src/algo.c | 10 +-------- src/audio.c | 59 +++++++++++++++++++++++++++++++++++++++++++++----- src/computer.c | 2 +- src/tg.h | 9 ++++++-- 4 files changed, 62 insertions(+), 18 deletions(-) diff --git a/src/algo.c b/src/algo.c index 1c744cb..40d8ada 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++) diff --git a/src/audio.c b/src/audio.c index 05890c2..4449c24 100644 --- a/src/audio.c +++ b/src/audio.c @@ -21,19 +21,52 @@ /* Huge buffer of audio */ float pa_buffers[PA_BUFF_SIZE]; -int write_pointer = 0; +unsigned int write_pointer = 0; uint64_t timestamp = 0; pthread_mutex_t audio_mutex; /* Audio input stream object */ static PaStream *stream; +/** A biquadratic filter. + * Saves the delay taps to allow the filter to continue across multiple calls. */ +static struct biquad_filter { + struct filter f; //!< Filter coefficients, F(z) = a(z) / b(z) + double z1, z2; //!< Delay taps +} audio_hpf; + /* Data for PA callback to use */ static struct callback_info { int channels; //!< Number of channels bool light; //!< Light algorithm in use, copy half data } info; +/* Initialize audio filter */ +static void init_audio_hpf(struct biquad_filter *filter, double cutoff, double sample_rate) +{ + make_hp(&filter->f, cutoff/sample_rate); + filter->z1 = 0.0; + filter->z2 = 0.0; +} + +/* 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, unsigned long frame_count, @@ -84,6 +117,15 @@ static int paudio_callback(const void *input_buffer, } wp = (wp + frame_count) % PA_BUFF_SIZE; } + + /* Apply HPF to new data */ + if(write_pointer < wp) { + apply_biquad(&audio_hpf, pa_buffers + write_pointer, wp - write_pointer); + } else { + apply_biquad(&audio_hpf, pa_buffers + write_pointer, PA_BUFF_SIZE - write_pointer); + apply_biquad(&audio_hpf, pa_buffers, wp); + } + pthread_mutex_lock(&audio_mutex); write_pointer = wp; timestamp += frame_count; @@ -127,13 +169,15 @@ int start_portaudio(int *nominal_sample_rate, double *real_sample_rate, bool lig if(err!=paNoError) goto error; - err = Pa_StartStream(stream); - if(err!=paNoError) - goto error; - const PaStreamInfo *info = Pa_GetStreamInfo(stream); *nominal_sample_rate = PA_SAMPLE_RATE; *real_sample_rate = info->sampleRate; + + init_audio_hpf(&audio_hpf, FILTER_CUTOFF, PA_SAMPLE_RATE / (light ? 2 : 1)); + + err = Pa_StartStream(stream); + if(err!=paNoError) + goto error; #ifdef DEBUG end: #endif @@ -232,8 +276,10 @@ int analyze_pa_data_cal(struct processing_data *pd, struct calibration_data *cd) * buffer. Nothing will happen if the mode doesn't actually change. * * @param light True for light mode, false for normal + * @param sample_rate Nominal sampling rate. Should take into account any + * downsampling done in light mode */ -void set_audio_light(bool light) +void set_audio_light(bool light, int sample_rate) { if(info.light != light) { Pa_StopStream(stream); @@ -246,6 +292,7 @@ void set_audio_light(bool light) pthread_mutex_unlock(&audio_mutex); + init_audio_hpf(&audio_hpf, FILTER_CUTOFF, sample_rate); PaError err = Pa_StartStream(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..672a344 100644 --- a/src/computer.c +++ b/src/computer.c @@ -236,7 +236,7 @@ void computer_destroy(struct computer *c) struct computer *start_computer(int nominal_sr, int bph, double la, int cal, int light) { if(light) nominal_sr /= 2; - set_audio_light(light); + set_audio_light(light, nominal_sr); struct processing_buffers *p = malloc(NSTEPS * sizeof(struct processing_buffers)); int first_step = light ? FIRST_STEP_LIGHT : FIRST_STEP; diff --git a/src/tg.h b/src/tg.h index 24eecea..7e9edd9 100644 --- a/src/tg.h +++ b/src/tg.h @@ -83,7 +83,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 +108,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,6 +121,7 @@ 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 { @@ -130,7 +135,7 @@ 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); int analyze_pa_data_cal(struct processing_data *pd, struct calibration_data *cd); -void set_audio_light(bool light); +void set_audio_light(bool light, int sample_rate); /* computer.c */ struct snapshot { From 13e27434ed9418fb634e2c7b00636c7a4a64beac Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Mon, 22 Mar 2021 19:44:05 -0700 Subject: [PATCH 30/39] Dynamically allocate sound buffer The buffer used for audio data is a static size based on what's needed for 32 seconds of data at the fixed 44.1kHz sampling rate. This doesn't work as well if one wants to change the sample rate and possibly support high rates. Dynamically allocate it. If the sampling rate were to increase, re-allocate a larger buffer for it. --- src/audio.c | 31 ++++++++++++++++++++++--------- src/tg.h | 1 - 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/audio.c b/src/audio.c index afd7fc3..6d7602c 100644 --- a/src/audio.c +++ b/src/audio.c @@ -20,7 +20,8 @@ #include /* Huge buffer of audio */ -float pa_buffers[PA_BUFF_SIZE]; +float *pa_buffers; +unsigned int pa_buffer_size; unsigned int write_pointer = 0; uint64_t timestamp = 0; pthread_mutex_t audio_mutex; @@ -91,19 +92,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) @@ -115,14 +116,14 @@ 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(write_pointer < wp) { apply_biquad(&audio_hpf, pa_buffers + write_pointer, wp - write_pointer); } else { - apply_biquad(&audio_hpf, pa_buffers + write_pointer, PA_BUFF_SIZE - write_pointer); + apply_biquad(&audio_hpf, pa_buffers + write_pointer, pa_buffer_size - write_pointer); apply_biquad(&audio_hpf, pa_buffers, wp); } @@ -175,6 +176,18 @@ int start_portaudio(int *nominal_sample_rate, double *real_sample_rate, bool lig init_audio_hpf(&audio_hpf, FILTER_CUTOFF, PA_SAMPLE_RATE / (light ? 2 : 1)); + /* Allocate larger buffer if needed */ + const size_t buffer_size = *nominal_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(stream); if(err!=paNoError) goto error; @@ -224,8 +237,8 @@ void fill_buffers(struct processing_buffers *ps, int light) 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)); @@ -277,7 +290,7 @@ void set_audio_light(bool light, int sample_rate) pthread_mutex_lock(&audio_mutex); info.light = light; - memset(pa_buffers, 0, sizeof(pa_buffers)); + memset(pa_buffers, 0, sizeof(*pa_buffers) * pa_buffer_size); write_pointer = 0; timestamp = 0; diff --git a/src/tg.h b/src/tg.h index 0241c7d..561c51e 100644 --- a/src/tg.h +++ b/src/tg.h @@ -43,7 +43,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 From 61e65b9e07c52e35b13538ce8caf82a0607326c7 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Sat, 27 Mar 2021 23:39:30 -0700 Subject: [PATCH 31/39] Add ARRAY_SIZE macro Commonly used macro to get the number of items in a statically sized array. --- src/tg.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tg.h b/src/tg.h index 561c51e..77240ad 100644 --- a/src/tg.h +++ b/src/tg.h @@ -77,6 +77,7 @@ #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 { From d2c24c04dc87478d4fa03cd189f02377e640bce8 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Mon, 3 Feb 2020 23:58:34 -0800 Subject: [PATCH 32/39] Refactor audio opening code for runtime re-opening This changes will make it easier to change audio device or rates after Portaudio is already started. The code needed to do this is taken out of start_portaudio() and put into new functions open_stream() and set_audio_device(). The latter function is global and can be used to change device or rate. State needed to keep track of audio device, rate, streams, etc. is moved into a static global struct. A different sampling rate than PA_SAMPLE_RATE can be used if start_portaudio() is passed that value in nominal_sample_rate. --- src/audio.c | 209 +++++++++++++++++++++++++++++++++++++++--------- src/interface.c | 1 + 2 files changed, 171 insertions(+), 39 deletions(-) diff --git a/src/audio.c b/src/audio.c index 6d7602c..1cc6130 100644 --- a/src/audio.c +++ b/src/audio.c @@ -26,9 +26,6 @@ unsigned int write_pointer = 0; uint64_t timestamp = 0; pthread_mutex_t audio_mutex; -/* Audio input stream object */ -static PaStream *stream; - /** A biquadratic filter. * Saves the delay taps to allow the filter to continue across multiple calls. */ static struct biquad_filter { @@ -42,6 +39,22 @@ static struct callback_info { bool light; //!< Light algorithm in use, copy half data } info; +/** 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 +} actx = { + .device = -1, +}; + /* Initialize audio filter */ static void init_audio_hpf(struct biquad_filter *filter, double cutoff, double sample_rate) { @@ -134,6 +147,151 @@ static int paudio_callback(const void *input_buffer, return 0; } +static PaError open_stream(PaDeviceIndex index, unsigned int rate, bool light, 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; + info.channels = channels; + 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, + &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[light] 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, 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) { + 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(&audio_hpf, FILTER_CUTOFF, actx.real_sample_rate / (light ? 2 : 1)); + + /* 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; +} + +/** 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. + * + * 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[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 light Use light mode (decimate to half supplied rate). + * @returns 0 on success, 1 on error. + * + */ int start_portaudio(int *nominal_sample_rate, double *real_sample_rate, bool light) { if(pthread_mutex_init(&audio_mutex,NULL)) { @@ -142,8 +300,10 @@ int start_portaudio(int *nominal_sample_rate, double *real_sample_rate, bool lig } PaError err = Pa_Initialize(); - if(err!=paNoError) + if(err!=paNoError) { + error("Error initializing PortAudio: %s", Pa_GetErrorText(err)); goto error; + } #ifdef DEBUG if(testing) { @@ -156,41 +316,13 @@ int start_portaudio(int *nominal_sample_rate, double *real_sample_rate, bool lig PaDeviceIndex default_input = Pa_GetDefaultInputDevice(); if(default_input == paNoDevice) { error("No default audio input device found"); - return 1; - } - 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 = light; - err = Pa_OpenDefaultStream(&stream,channels,0,paFloat32,PA_SAMPLE_RATE,paFramesPerBufferUnspecified,paudio_callback,&info); - if(err!=paNoError) goto error; - - const PaStreamInfo *info = Pa_GetStreamInfo(stream); - *nominal_sample_rate = PA_SAMPLE_RATE; - *real_sample_rate = info->sampleRate; - - init_audio_hpf(&audio_hpf, FILTER_CUTOFF, PA_SAMPLE_RATE / (light ? 2 : 1)); - - /* Allocate larger buffer if needed */ - const size_t buffer_size = *nominal_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(stream); - if(err!=paNoError) + err = set_audio_device(default_input, nominal_sample_rate, real_sample_rate, light); + if(err!=paNoError && err!=1) goto error; + #ifdef DEBUG end: #endif @@ -199,7 +331,7 @@ int start_portaudio(int *nominal_sample_rate, double *real_sample_rate, bool lig return 0; error: - error("Error opening audio input: %s", Pa_GetErrorText(err)); + error("Unable to start audio"); return 1; } @@ -280,13 +412,12 @@ int analyze_pa_data_cal(struct processing_data *pd, struct calibration_data *cd) * buffer. Nothing will happen if the mode doesn't actually change. * * @param light True for light mode, false for normal - * @param sample_rate Nominal sampling rate. Should take into account any * downsampling done in light mode */ void set_audio_light(bool light, int sample_rate) { if(info.light != light) { - Pa_StopStream(stream); + Pa_StopStream(actx.stream); pthread_mutex_lock(&audio_mutex); info.light = light; @@ -297,7 +428,7 @@ void set_audio_light(bool light, int sample_rate) pthread_mutex_unlock(&audio_mutex); init_audio_hpf(&audio_hpf, FILTER_CUTOFF, sample_rate); - PaError err = Pa_StartStream(stream); + PaError err = Pa_StartStream(actx.stream); if(err != paNoError) error("Error re-starting audio input: %s", Pa_GetErrorText(err)); } diff --git a/src/interface.c b/src/interface.c index 83e83eb..54f42ed 100644 --- a/src/interface.c +++ b/src/interface.c @@ -951,6 +951,7 @@ static void start_interface(GApplication* app, void *p) w->calibrate = 0; w->is_light = 0; w->vertical_layout = true; + w->nominal_sr = 0; // Use default rate, e.g. PA_SAMPLE_RATE load_config(w); From c664e90455678509ee89fba78182908630e39cef Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Sun, 18 Apr 2021 22:44:19 -0700 Subject: [PATCH 33/39] Move HPF and callback info to audio context Since the HPF is mostly used by the audio callback, put it into the callback info struct. And put the audio callback struct into the audio context struct. --- src/audio.c | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/audio.c b/src/audio.c index 1cc6130..e032cc9 100644 --- a/src/audio.c +++ b/src/audio.c @@ -28,16 +28,17 @@ pthread_mutex_t audio_mutex; /** A biquadratic filter. * Saves the delay taps to allow the filter to continue across multiple calls. */ -static struct biquad_filter { +struct biquad_filter { struct filter f; //!< Filter coefficients, F(z) = a(z) / b(z) double z1, z2; //!< Delay taps -} audio_hpf; +}; -/* Data for PA callback to use */ -static struct callback_info { +/** 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 @@ -51,6 +52,8 @@ static struct audio_context { 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 + //! Data callback will read, need to take care when modifying so as not to race. + struct callback_info info; } actx = { .device = -1, }; @@ -133,11 +136,12 @@ static int paudio_callback(const void *input_buffer, } /* Apply HPF to new data */ + struct biquad_filter *f = (struct biquad_filter *)&info->hpf; if(write_pointer < wp) { - apply_biquad(&audio_hpf, pa_buffers + write_pointer, wp - write_pointer); + apply_biquad(f, pa_buffers + write_pointer, wp - write_pointer); } else { - apply_biquad(&audio_hpf, pa_buffers + write_pointer, pa_buffer_size - write_pointer); - apply_biquad(&audio_hpf, pa_buffers, wp); + apply_biquad(f, pa_buffers + write_pointer, pa_buffer_size - write_pointer); + apply_biquad(f, pa_buffers, wp); } pthread_mutex_lock(&audio_mutex); @@ -157,8 +161,8 @@ static PaError open_stream(PaDeviceIndex index, unsigned int rate, bool light, P return paInvalidChannelCount; } if(channels > 2) channels = 2; - info.channels = channels; - info.light = light; + actx.info.channels = channels; + actx.info.light = light; err = Pa_OpenStream(stream, &(PaStreamParameters){ @@ -172,7 +176,7 @@ static PaError open_stream(PaDeviceIndex index, unsigned int rate, bool light, P paFramesPerBufferUnspecified, paNoFlag, paudio_callback, - &info); + &actx.info); return err; } @@ -233,7 +237,7 @@ int set_audio_device(int device, int *nominal_sr, double *real_sr, bool light) goto error; actx.real_sample_rate = Pa_GetStreamInfo(actx.stream)->sampleRate; - init_audio_hpf(&audio_hpf, FILTER_CUTOFF, actx.real_sample_rate / (light ? 2 : 1)); + init_audio_hpf(&actx.info.hpf, FILTER_CUTOFF, actx.real_sample_rate / (light ? 2 : 1)); /* Allocate larger buffer if needed */ const size_t buffer_size = actx.sample_rate << (NSTEPS + FIRST_STEP); @@ -416,18 +420,18 @@ int analyze_pa_data_cal(struct processing_data *pd, struct calibration_data *cd) */ void set_audio_light(bool light, int sample_rate) { - if(info.light != light) { + if(actx.info.light != light) { Pa_StopStream(actx.stream); pthread_mutex_lock(&audio_mutex); - info.light = light; + 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(&audio_hpf, FILTER_CUTOFF, sample_rate); + init_audio_hpf(&actx.info.hpf, FILTER_CUTOFF, sample_rate); PaError err = Pa_StartStream(actx.stream); if(err != paNoError) error("Error re-starting audio input: %s", Pa_GetErrorText(err)); From 44ea8d461a56d61a30e63e70a7f59b5e58861fb0 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Sun, 18 Apr 2021 22:56:17 -0700 Subject: [PATCH 34/39] Let audio module keep track of light mode and sample rate The audio code needs to know, and does know, the sample rate and light mode, since it is the code that configures portaudio to the requested sample rate and does the light mode decimation. Add a method to get the effective sample rate from the audio context, taking light mode into account. Code that does with the current audio state can use that instead of being passed that info. set_audio_light() doesn't need sample rate passed to it anymore. fill_buffers() and get_timestamp() don't need light mode passed to them anymore. --- src/audio.c | 32 ++++++++++++++++++-------------- src/computer.c | 4 ++-- src/tg.h | 6 +++--- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/audio.c b/src/audio.c index e032cc9..5e5c589 100644 --- a/src/audio.c +++ b/src/audio.c @@ -58,6 +58,13 @@ static struct audio_context { .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, double cutoff, double sample_rate) { @@ -237,7 +244,7 @@ int set_audio_device(int device, int *nominal_sr, double *real_sr, bool light) goto error; actx.real_sample_rate = Pa_GetStreamInfo(actx.stream)->sampleRate; - init_audio_hpf(&actx.info.hpf, FILTER_CUTOFF, actx.real_sample_rate / (light ? 2 : 1)); + init_audio_hpf(&actx.info.hpf, FILTER_CUTOFF, effective_sr()); /* Allocate larger buffer if needed */ const size_t buffer_size = actx.sample_rate << (NSTEPS + FIRST_STEP); @@ -350,24 +357,21 @@ 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; } -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; @@ -397,7 +401,7 @@ bool analyze_pa_data(struct processing_data *pd, int step, int bph, double la, u 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"); @@ -413,12 +417,12 @@ 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 - * downsampling done in light mode + * @param light True for light mode, false for normal mode */ -void set_audio_light(bool light, int sample_rate) +void set_audio_light(bool light) { if(actx.info.light != light) { Pa_StopStream(actx.stream); @@ -431,7 +435,7 @@ void set_audio_light(bool light, int sample_rate) pthread_mutex_unlock(&audio_mutex); - init_audio_hpf(&actx.info.hpf, FILTER_CUTOFF, sample_rate); + 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 3f6767e..e5d95e8 100644 --- a/src/computer.c +++ b/src/computer.c @@ -104,7 +104,7 @@ static void compute_update(struct computer *c) /* 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, pd->is_light); + fill_buffers(ps); debug("\nSTART OF COMPUTATION CYCLE\n\n"); unsigned int stepmask = BITMASK(NSTEPS); // Mask of available steps @@ -267,7 +267,7 @@ void computer_destroy(struct computer *c) struct computer *start_computer(int nominal_sr, int bph, double la, int cal, int light) { if(light) nominal_sr /= 2; - set_audio_light(light, nominal_sr); + set_audio_light(light); struct processing_buffers *p = malloc(NSTEPS * sizeof(struct processing_buffers)); int first_step = light ? FIRST_STEP_LIGHT : FIRST_STEP; diff --git a/src/tg.h b/src/tg.h index 77240ad..334f4c1 100644 --- a/src/tg.h +++ b/src/tg.h @@ -136,11 +136,11 @@ struct processing_data { int start_portaudio(int *nominal_sample_rate, double *real_sample_rate, bool light); int terminate_portaudio(); -uint64_t get_timestamp(int light); -void fill_buffers(struct processing_buffers *ps, int light); +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, int sample_rate); +void set_audio_light(bool light); /* computer.c */ struct display; From 11f2fd9bc9b74caac8deaa1c8ab2cf6401ae4099 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Sun, 28 Mar 2021 00:16:34 -0700 Subject: [PATCH 35/39] Add interface for querying audio device list A new exported interface, get_audio_device(), get_audio_devices(), and set_audio_device(), is added to audio.c. It allows querying what devices are present, what sample rates they support, what device is active, and changing the active device and rate. Portaudio's ALSA driver won't let you know what rates are allowed if the device is currently active, due to the way it tries to re-open the device to query the rate. This also causes a problem querying rates on a direct hardware device, e.g. "hw:0,0", if a virtual device, e.g. "pulse" or "dmix", is active and using that hardware. So the common use case I want to do, where "pulse" is the default on startup and then I want to switch to the direct hardware device to avoid resampling, and get lower latency and better time stamping of audio, doesn't work. Because the hardware device is "in use" through pulse. To get around this, a predefined list of common rates are queried on start up and the results cached. This way it's not necessary to query devices after audio capture has started. --- src/audio.c | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/tg.h | 15 +++++++++++ 2 files changed, 88 insertions(+) diff --git a/src/audio.c b/src/audio.c index 5e5c589..75ed546 100644 --- a/src/audio.c +++ b/src/audio.c @@ -18,6 +18,7 @@ #include "tg.h" #include +#include /* Huge buffer of audio */ float *pa_buffers; @@ -52,6 +53,8 @@ static struct audio_context { 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 = { @@ -284,6 +287,71 @@ int set_audio_device(int device, int *nominal_sr, double *real_sr, bool light) 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 @@ -324,6 +392,11 @@ int start_portaudio(int *nominal_sample_rate, double *real_sample_rate, bool lig } #endif + if(scan_audio_devices() < 0) { + error("Unable to query audio devices"); + // Maybe default audio device will work anyway? + } + PaDeviceIndex default_input = Pa_GetDefaultInputDevice(); if(default_input == paNoDevice) { error("No default audio input device found"); diff --git a/src/tg.h b/src/tg.h index 334f4c1..874e8a5 100644 --- a/src/tg.h +++ b/src/tg.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -134,6 +135,10 @@ struct processing_data { int is_light; }; +#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 *nominal_sample_rate, double *real_sample_rate, bool light); int terminate_portaudio(); uint64_t get_timestamp(); @@ -141,6 +146,16 @@ 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, bool light); /* computer.c */ struct display; From 1b839ee8db951e7721946acec1f77f517c689d4e Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Sun, 28 Mar 2021 00:10:03 -0700 Subject: [PATCH 36/39] Add audio device and rate selection dialog Lets one select the sound device and the rate. All devices appear in the list, but non-suitable ones will be grayed out. There is a predefined list of common rates and non-supported rates for the selected device will be grayed out. The rate combo box allows typing in a rate not in the list and that rate will be tried. This design allows for any rate, but doesn't waste too much time querying the audio devices for all possible rates on startup. If setting the new device/rate doesn't work, it will automatically go back to the previous values. The dialog is modal. It's necessary to select new settings and hit OK or Cancel beforing going back to the app. Mask the dialog handler when the rate list is populated so callbacks aren't run as new items as added. --- src/interface.c | 187 +++++++++++++++++++++++++++++++++++++++++++++++- src/tg.h | 12 +++- 2 files changed, 195 insertions(+), 4 deletions(-) diff --git a/src/interface.c b/src/interface.c index 54f42ed..022fbc0 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; @@ -587,6 +589,125 @@ 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 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 + 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; + } + 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? */ + + i = set_audio_device(selected, &new_rate, NULL, w->is_light); + if (i == 0) { + w->nominal_sr = new_rate; + 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->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); @@ -733,6 +854,66 @@ static GtkWidget* add_menu_item(GtkWidget* menu, const char* label, bool sensiti 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_widget_show_all(content); + + g_signal_connect(G_OBJECT(w->device_list), "changed", G_CALLBACK(device_changed), w); +} + /* Set up the main window and populate with widgets */ static void init_main_window(struct main_window *w) { @@ -860,6 +1041,10 @@ static void init_main_window(struct main_window *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 = add_menu_item(command_menu, "Close all snapshots", false, G_CALLBACK(close_all), w); diff --git a/src/tg.h b/src/tg.h index 874e8a5..8ff5982 100644 --- a/src/tg.h +++ b/src/tg.h @@ -166,7 +166,7 @@ struct snapshot { 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 @@ -184,7 +184,7 @@ 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; @@ -286,6 +286,12 @@ 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; + struct output_panel *active_panel; struct computer *computer; @@ -299,7 +305,7 @@ struct main_window { int bph; double la; // deg int cal; // 0.1 s/d - int nominal_sr; + int nominal_sr; // requested audio device rate bool vertical_layout; From 0e74ea7612a17658531b661b08ad9bd8f845fb4e Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Sat, 27 Mar 2021 17:16:24 -0700 Subject: [PATCH 37/39] Save audio device and rate selection to config This will save the audio device and rate selected to the config file. Initially the saved device and rate are both codes for "default", so the current behavior of using the Portaudio default device at 44.1 kHz remains. Only if the audio setup dialog is used to select something different, and that selection is started successfully, does a specific device and rate get saved. Additionally, selecting the default device in audio setup is still saved as "default", rather than the specific device that happens to be the current default. If the saved device is not available when starting up it will automatically revert to the default. --- src/audio.c | 23 ++++++++++++++++------- src/interface.c | 9 ++++++++- src/tg.h | 8 ++++++-- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/audio.c b/src/audio.c index 75ed546..ab55ee9 100644 --- a/src/audio.c +++ b/src/audio.c @@ -357,6 +357,9 @@ static int scan_audio_devices(void) * 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. @@ -364,6 +367,7 @@ static int scan_audio_devices(void) * 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. @@ -371,7 +375,7 @@ static int scan_audio_devices(void) * @returns 0 on success, 1 on error. * */ -int start_portaudio(int *nominal_sample_rate, double *real_sample_rate, bool light) +int start_portaudio(int device, int *nominal_sample_rate, double *real_sample_rate, bool light) { if(pthread_mutex_init(&audio_mutex,NULL)) { error("Failed to setup audio mutex"); @@ -397,13 +401,18 @@ int start_portaudio(int *nominal_sample_rate, double *real_sample_rate, bool lig // Maybe default audio device will work anyway? } - PaDeviceIndex default_input = Pa_GetDefaultInputDevice(); - if(default_input == paNoDevice) { - error("No default audio input device found"); - goto error; - } + 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(default_input, nominal_sample_rate, real_sample_rate, light); + err = set_audio_device(input, nominal_sample_rate, real_sample_rate, light); if(err!=paNoError && err!=1) goto error; diff --git a/src/interface.c b/src/interface.c index 022fbc0..6c3499d 100644 --- a/src/interface.c +++ b/src/interface.c @@ -698,6 +698,10 @@ static void audio_setup(GtkMenuItem *m, struct main_window *w) i = set_audio_device(selected, &new_rate, NULL, w->is_light); if (i == 0) { w->nominal_sr = new_rate; + // Only save settings to config if it worked + w->audio_rate = new_rate; + // 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 */ @@ -1137,10 +1141,13 @@ static void start_interface(GApplication* app, void *p) 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; load_config(w); - if(start_portaudio(&w->nominal_sr, &real_sr, w->is_light)) { + w->nominal_sr = w->audio_rate; + if(start_portaudio(w->audio_device, &w->nominal_sr, &real_sr, w->is_light)) { g_application_quit(app); return; } diff --git a/src/tg.h b/src/tg.h index 8ff5982..2664da1 100644 --- a/src/tg.h +++ b/src/tg.h @@ -139,7 +139,7 @@ struct processing_data { #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 *nominal_sample_rate, double *real_sample_rate, bool light); +int start_portaudio(int device, int *nominal_sample_rate, double *real_sample_rate, bool light); int terminate_portaudio(); uint64_t get_timestamp(); void fill_buffers(struct processing_buffers *ps); @@ -306,6 +306,8 @@ struct main_window { double la; // deg int cal; // 0.1 s/d int nominal_sr; // requested audio device rate + int audio_device;// Selected device + int audio_rate; // Selected rate bool vertical_layout; @@ -332,7 +334,9 @@ void error(char *format,...); OP(lift_angle, la, double) \ OP(calibration, cal, int) \ OP(light_algorithm, is_light, int) \ - OP(vertical_paperstrip, vertical_layout, bool) + OP(vertical_paperstrip, vertical_layout, bool) \ + OP(audio_device, audio_device, int) \ + OP(audio_rate, audio_rate, int) struct conf_data { #define DEF(NAME,PLACE,TYPE) TYPE PLACE; From dc4679e0613dd6d88e7b82084a5bba1bd27a7092 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Tue, 6 Apr 2021 23:24:46 -0700 Subject: [PATCH 38/39] Add audio interface for setting/changing HPF cutoff frequency Extend audio interface to allow setting the HPF cutoff frequency when opening the audio device as well as changing it on the fly. Add a method to get the current filter parameters. Also add ability to disable HPF by changing cutoff to 0. --- src/audio.c | 49 +++++++++++++++++++++++++++++++++++++------------ src/interface.c | 6 +++--- src/tg.h | 6 ++++-- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/audio.c b/src/audio.c index ab55ee9..9453b99 100644 --- a/src/audio.c +++ b/src/audio.c @@ -32,6 +32,7 @@ pthread_mutex_t audio_mutex; 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 */ @@ -69,11 +70,30 @@ static inline double effective_sr(void) } /* Initialize audio filter */ -static void init_audio_hpf(struct biquad_filter *filter, double cutoff, double sample_rate) +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 @@ -146,12 +166,14 @@ static int paudio_callback(const void *input_buffer, } /* Apply HPF to new data */ - 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); + 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); @@ -205,11 +227,12 @@ static PaError open_stream(PaDeviceIndex index, unsigned int rate, bool light, P * @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[light] light Use light mode (halve normal_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, bool light) +int set_audio_device(int device, int *nominal_sr, double *real_sr, int hpf_freq, bool light) { PaError err; @@ -218,6 +241,7 @@ int set_audio_device(int device, int *nominal_sr, double *real_sr, bool light) *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 } @@ -247,7 +271,7 @@ int set_audio_device(int device, int *nominal_sr, double *real_sr, bool light) goto error; actx.real_sample_rate = Pa_GetStreamInfo(actx.stream)->sampleRate; - init_audio_hpf(&actx.info.hpf, FILTER_CUTOFF, effective_sr()); + 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); @@ -371,11 +395,12 @@ static int scan_audio_devices(void) * @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, bool light) +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"); @@ -412,7 +437,7 @@ int start_portaudio(int device, int *nominal_sample_rate, double *real_sample_ra } else input = device; - err = set_audio_device(input, nominal_sample_rate, real_sample_rate, light); + err = set_audio_device(input, nominal_sample_rate, real_sample_rate, hpf_freq, light); if(err!=paNoError && err!=1) goto error; diff --git a/src/interface.c b/src/interface.c index 6c3499d..ff5fe4c 100644 --- a/src/interface.c +++ b/src/interface.c @@ -695,7 +695,7 @@ static void audio_setup(GtkMenuItem *m, struct main_window *w) if (new_rate == -1) new_rate = w->nominal_sr; /* clear entry too? */ - i = set_audio_device(selected, &new_rate, NULL, w->is_light); + i = set_audio_device(selected, &new_rate, NULL, FILTER_CUTOFF, w->is_light); if (i == 0) { w->nominal_sr = new_rate; // Only save settings to config if it worked @@ -706,7 +706,7 @@ static void audio_setup(GtkMenuItem *m, struct main_window *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->is_light); + i = set_audio_device(current_dev, &new_rate, NULL, FILTER_CUTOFF, w->is_light); if (i < 0) error("Unable to restore previous audio settings. Audio not working."); } @@ -1147,7 +1147,7 @@ static void start_interface(GApplication* app, void *p) load_config(w); w->nominal_sr = w->audio_rate; - if(start_portaudio(w->audio_device, &w->nominal_sr, &real_sr, w->is_light)) { + if(start_portaudio(w->audio_device, &w->nominal_sr, &real_sr, FILTER_CUTOFF, w->is_light)) { g_application_quit(app); return; } diff --git a/src/tg.h b/src/tg.h index 2664da1..5c1a442 100644 --- a/src/tg.h +++ b/src/tg.h @@ -139,7 +139,7 @@ struct processing_data { #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, bool light); +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(); void fill_buffers(struct processing_buffers *ps); @@ -155,7 +155,9 @@ struct audio_device { }; 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, bool light); +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; From f2f39dd3273386f951ada4eed4e8015d02d29043 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Tue, 6 Apr 2021 03:25:22 -0700 Subject: [PATCH 39/39] Allow configuring HPF cutoff frequency from audio setup dialog Add a control to the setup dialog for the HPF cutoff frequency. Also save it to the config file. Slider has ticks for Off and also for the default 3kHz frequency. The fill level of the slider is set to limit the cutoff to the Nyquist frequency of the selected sampling rate. To do that, I needed to get a callback on rate combobox change, and the manual entry of the combobox made that a challenge. --- src/interface.c | 41 ++++++++++++++++++++++++++++++++++++++--- src/tg.h | 5 ++++- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/interface.c b/src/interface.c index ff5fe4c..709ba51 100644 --- a/src/interface.c +++ b/src/interface.c @@ -612,6 +612,21 @@ static int get_rate(GtkComboBox *rate_list) 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; @@ -620,6 +635,7 @@ static void populate_rate_list(GtkComboBox *rate_list, unsigned int current_rate 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)) { @@ -633,6 +649,7 @@ static void populate_rate_list(GtkComboBox *rate_list, unsigned int current_rate 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)); @@ -695,18 +712,21 @@ static void audio_setup(GtkMenuItem *m, struct main_window *w) if (new_rate == -1) new_rate = w->nominal_sr; /* clear entry too? */ - i = set_audio_device(selected, &new_rate, NULL, FILTER_CUTOFF, w->is_light); + 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, FILTER_CUTOFF, w->is_light); + 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."); } @@ -913,9 +933,23 @@ static void init_audio_dialog(struct main_window *w) 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 */ @@ -1143,11 +1177,12 @@ static void start_interface(GApplication* app, void *p) 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, FILTER_CUTOFF, w->is_light)) { + if(start_portaudio(w->audio_device, &w->nominal_sr, &real_sr, w->hpf_freq, w->is_light)) { g_application_quit(app); return; } diff --git a/src/tg.h b/src/tg.h index 5c1a442..17e505e 100644 --- a/src/tg.h +++ b/src/tg.h @@ -293,6 +293,7 @@ struct main_window { GtkWidget *audio_setup; GtkComboBox *device_list; GtkComboBox *rate_list; + GtkRange *hpf_range; struct output_panel *active_panel; @@ -310,6 +311,7 @@ struct main_window { 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; @@ -338,7 +340,8 @@ void error(char *format,...); 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(audio_rate, audio_rate, int) \ + OP(highpass_cutoff_freq, hpf_freq, int) struct conf_data { #define DEF(NAME,PLACE,TYPE) TYPE PLACE;