Don't over-zoom.
[spectral-waterfall.git] / waterfall.c
blob37a891806d9664088b59a118d3e103152f379c0f
1 #include <fcntl.h>
2 #include <fftw3.h>
3 #include <glib.h>
4 #include <math.h>
5 #include <gdk/gdkkeysyms.h>
6 #include <gtk/gtk.h>
7 #include <stdlib.h>
8 #include <string.h>
9 #include <unistd.h>
11 #define BUF_SAMPLES (INTEGRATION_SAMPLES * 3)
12 #define INTEGRATION_SAMPLES 44100
13 #define N_SLICES 64
14 #define SAMPLE_SIZE 2
16 struct waterfall_context {
17 GtkDrawingArea *drawingarea;
18 GdkPixbuf *original; /* A 1-to-1 map from spectral data to pixels. */
19 GdkPixbuf *resized; /* A backing store for the scaled spectrogram. */
20 struct {
21 GdkPixbuf *part1; /* Slices from zero to the slice cursor. */
22 GdkPixbuf *part2; /* Slices from the slice cursor to the end. */
23 int part1_size;
24 int part2_size;
25 } subpixbufs;
26 struct {
27 /* The size of the spectrogram drawing area. */
28 int width;
29 int height;
30 } size;
31 struct {
32 int f_low;
33 int f_high;
34 int noise_floor;
35 double sensitivity;
36 } zoom;
37 char *buf;
38 size_t buf_size;
39 size_t buf_index;
40 size_t integration_samples;
41 int slice;
42 int n_slices;
43 struct {
44 double *samples;
45 fftw_complex *spectrum;
46 fftw_plan p;
47 } fft;
50 void invalidate_spectrogram(struct waterfall_context *waterfall);
52 void waterfall_open_output(GtkFileChooser *chooser, gpointer user_data)
54 char *filename = gtk_file_chooser_get_filename(chooser);
55 printf("Open %s\n", filename);
56 g_free(filename);
58 gtk_widget_hide(GTK_WIDGET(chooser));
61 void waterfall_expose_spectrogram(GtkWidget *widget,
62 GdkEventExpose *event,
63 gpointer user_data)
65 GtkDrawingArea *spectrogram_drawingarea = GTK_DRAWING_AREA(widget);
66 struct waterfall_context *waterfall = user_data;
67 cairo_t *cr;
69 if (!waterfall->resized) {
70 double part1_size, part2_size;
71 int resolution = waterfall->zoom.f_high - waterfall->zoom.f_low + 1;
72 double scale_x = (double) waterfall->size.width / waterfall->n_slices;
73 double scale_y = (double) waterfall->size.height / resolution;
74 double offset_y = -waterfall->zoom.f_low * scale_y;
75 int row, rowstride, channels;
76 guchar *pixels;
78 part1_size = (double) waterfall->subpixbufs.part1_size / waterfall->n_slices * waterfall->size.width;
79 part2_size = waterfall->size.width - (int) part1_size;
80 printf("Expose parts %g (%d),%g (%d)\n",
81 part1_size, waterfall->subpixbufs.part1_size,
82 part2_size, waterfall->subpixbufs.part2_size);
84 /* Create a new backing store, then render into it. */
85 waterfall->resized = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, waterfall->size.width, waterfall->size.height);
86 if (waterfall->subpixbufs.part2) {
87 /* Render the oldest slices first. */
88 printf("Scale (%g) part2 to width %d at 0\n",
89 scale_x, (int) part2_size);
90 gdk_pixbuf_scale(waterfall->subpixbufs.part2, waterfall->resized,
91 0, 0, (int) part2_size, waterfall->size.height,
92 0, offset_y, scale_x, scale_y,
93 GDK_INTERP_NEAREST);
95 if (waterfall->subpixbufs.part1) {
96 /* Then render the more recent slices. */
97 printf("Scale (%g) part1 to width %d at %d\n",
98 scale_x, (int) part1_size, (int) part2_size);
99 gdk_pixbuf_scale(waterfall->subpixbufs.part1, waterfall->resized,
100 (int) part2_size, 0, (int) part1_size, waterfall->size.height,
101 part2_size, offset_y, scale_x, scale_y,
102 GDK_INTERP_NEAREST);
104 pixels = gdk_pixbuf_get_pixels(waterfall->resized);
105 rowstride = gdk_pixbuf_get_rowstride(waterfall->resized);
106 channels = gdk_pixbuf_get_n_channels(waterfall->resized);
107 /* Draw a narrow marker to represent the border between part1 and part2. */
108 for (row = 0; row < waterfall->size.height; row++) {
109 pixels[row*rowstride + ((int) part2_size % waterfall->size.width) * channels + 1] = 192;
113 cr = gdk_cairo_create(spectrogram_drawingarea->widget.window);
115 gdk_cairo_set_source_pixbuf(cr, waterfall->resized, 0, 0);
116 cairo_paint(cr);
118 cairo_destroy(cr);
121 void waterfall_configure_spectrogram(GtkWidget *widget,
122 GdkEventConfigure *event,
123 gpointer user_data)
125 struct waterfall_context *waterfall = user_data;
127 printf("configure %d %d,%d+%d,%d send_event=%d on window %p\n",
128 event->type, event->x, event->y, event->width, event->height, event->send_event, event->window);
130 waterfall->size.width = event->width;
131 waterfall->size.height = event->height;
133 invalidate_spectrogram(waterfall);
136 gboolean waterfall_key_press(GtkWidget *widget,
137 GdkEventKey *event,
138 gpointer user_data)
140 struct waterfall_context *waterfall = user_data;
141 int resolution = waterfall->zoom.f_high - waterfall->zoom.f_low + 1;
142 int headroom = waterfall->integration_samples/2 - waterfall->zoom.f_high;
143 gboolean zoom_changed = FALSE;
145 printf("key press\n");
146 switch (event->keyval) {
147 case GDK_KEY_Up:
148 printf("up\n");
149 waterfall->zoom.f_low -= MIN(waterfall->zoom.f_low, resolution / 2);
150 waterfall->zoom.f_high -= MIN(waterfall->zoom.f_low, resolution / 2);
151 zoom_changed = TRUE;
152 break;
153 case GDK_KEY_Down:
154 printf("down\n");
155 waterfall->zoom.f_low += MIN(headroom, resolution / 2);
156 waterfall->zoom.f_high += MIN(headroom, resolution / 2);
157 zoom_changed = TRUE;
158 break;
159 case GDK_KEY_plus:
160 printf("plus\n");
161 if (resolution / 4 > 1) {
162 waterfall->zoom.f_low += resolution / 4;
163 waterfall->zoom.f_high -= resolution / 4;
164 zoom_changed = TRUE;
166 break;
167 case GDK_KEY_minus:
168 printf("minus\n");
169 waterfall->zoom.f_low -= MIN(waterfall->zoom.f_low, resolution / 4);
170 waterfall->zoom.f_high += MIN(headroom, resolution / 4);
171 zoom_changed = TRUE;
172 break;
173 case GDK_KEY_bracketleft:
174 printf("[\n");
175 waterfall->zoom.noise_floor--;
176 break;
177 case GDK_KEY_bracketright:
178 printf("]\n");
179 waterfall->zoom.noise_floor++;
180 break;
181 case GDK_KEY_braceleft:
182 printf("{\n");
183 waterfall->zoom.sensitivity /= 1.5;
184 break;
185 case GDK_KEY_braceright:
186 printf("}\n");
187 waterfall->zoom.sensitivity *= 1.5;
188 break;
191 if (zoom_changed) {
192 invalidate_spectrogram(waterfall);
195 return FALSE;
198 void connect_signal(GtkBuilder *builder,
199 GObject *object,
200 const gchar *signal_name,
201 const gchar *handler_name,
202 GObject *connect_object,
203 GConnectFlags flags,
204 gpointer user_data)
206 static struct {
207 char const *name;
208 GCallback fn;
209 } handlers[] = {
210 { "gtk_main_quit", G_CALLBACK(&gtk_main_quit) },
211 { "gtk_widget_hide_on_delete", G_CALLBACK(&gtk_widget_hide_on_delete) },
212 { "gtk_widget_show", G_CALLBACK(&gtk_widget_show) },
213 { "waterfall_open_output", G_CALLBACK(&waterfall_open_output) },
214 { NULL, NULL }
216 int i;
218 for (i = 0; handlers[i].name; i++) {
219 if (!strcmp(handlers[i].name, handler_name)) {
220 GCallback handler = handlers[i].fn;
221 g_signal_connect_object(object, signal_name, handler,
222 connect_object, flags);
223 return;
227 /* TODO: Connect to some error-spewing function? */
230 void invalidate_spectrogram(struct waterfall_context *waterfall)
232 GdkRegion *visible_region;
234 /* Just invalidate everything. */
235 if (waterfall->resized) {
236 g_object_unref(G_OBJECT(waterfall->resized));
237 waterfall->resized = NULL;
239 visible_region = gdk_drawable_get_visible_region(waterfall->drawingarea->widget.window);
240 gdk_window_invalidate_region(waterfall->drawingarea->widget.window,
241 visible_region,
242 FALSE);
243 gdk_region_destroy(visible_region);
246 void split_pixbuf(struct waterfall_context *waterfall)
248 int max_resolution = waterfall->integration_samples/2 + 1;
250 if (waterfall->subpixbufs.part1) {
251 g_object_unref(G_OBJECT(waterfall->subpixbufs.part1));
253 if (waterfall->subpixbufs.part2) {
254 g_object_unref(G_OBJECT(waterfall->subpixbufs.part2));
257 waterfall->subpixbufs.part1_size = waterfall->slice % waterfall->n_slices;
258 waterfall->subpixbufs.part2_size = waterfall->n_slices - waterfall->subpixbufs.part1_size;
259 printf("Split %d,%d\n", waterfall->subpixbufs.part1_size, waterfall->subpixbufs.part2_size);
260 if (waterfall->subpixbufs.part1_size) {
261 waterfall->subpixbufs.part1 = gdk_pixbuf_new_subpixbuf(waterfall->original,
262 0, 0,
263 waterfall->subpixbufs.part1_size, max_resolution);
264 } else {
265 waterfall->subpixbufs.part1 = NULL;
267 if (waterfall->subpixbufs.part2_size) {
268 waterfall->subpixbufs.part2 = gdk_pixbuf_new_subpixbuf(waterfall->original,
269 waterfall->subpixbufs.part1_size, 0,
270 waterfall->subpixbufs.part2_size, max_resolution);
271 } else {
272 waterfall->subpixbufs.part2 = NULL;
276 gboolean waterfall_input(GIOChannel *source,
277 GIOCondition condition,
278 gpointer userdata)
280 struct waterfall_context *waterfall = userdata;
281 double normalization = log10(waterfall->integration_samples * 32768);
282 gsize n;
283 int batch_size = 0;
284 GError *err = NULL;
286 switch (g_io_channel_read_chars(source,
287 waterfall->buf,
288 waterfall->buf_size - waterfall->buf_index,
290 &err)) {
291 case G_IO_STATUS_NORMAL:
292 waterfall->buf_index += n;
293 break;
294 case G_IO_STATUS_AGAIN:
295 printf("Try again later\n");
296 g_error_free(err);
297 return TRUE;
298 case G_IO_STATUS_EOF:
299 printf("End of input\n");
300 exit(0);
301 break;
302 case G_IO_STATUS_ERROR:
303 printf("Error - %s\n", err->message);
304 exit(1);
305 break;
306 default:
307 exit(1);
308 break;
311 while (waterfall->buf_index >= waterfall->integration_samples*SAMPLE_SIZE) {
312 int i, row, rowstride, channels, modslice;
313 int resolution = waterfall->integration_samples/2 + 1;
314 guchar *pixels;
316 /* Analyze the spectrum. */
317 printf("Integrate %d %zu %d (%d)\n", batch_size, waterfall->buf_index,
318 waterfall->slice, waterfall->slice % waterfall->n_slices);
319 for (i = 0; i < waterfall->integration_samples; i++) {
320 int16_t x;
321 memcpy(&x, waterfall->buf + i*SAMPLE_SIZE, sizeof (x));
322 waterfall->fft.samples[i] = x;
324 fftw_execute(waterfall->fft.p);
326 /* Draw a new slice. */
327 modslice = waterfall->slice % waterfall->n_slices;
328 pixels = gdk_pixbuf_get_pixels(waterfall->original);
329 rowstride = gdk_pixbuf_get_rowstride(waterfall->original);
330 channels = gdk_pixbuf_get_n_channels(waterfall->original);
331 for (row = 0; row < resolution; row++) {
332 double signal_i = waterfall->fft.spectrum[row][0], signal_q = waterfall->fft.spectrum[row][1];
333 /* XXX Give invsqrt a chance to happen. */
334 double power = -log10(1.0 / hypot(signal_i, signal_q)) - normalization;
335 double z = (power + waterfall->zoom.noise_floor) * waterfall->zoom.sensitivity;
336 if (row > 50 && row < 60) {
337 printf("Power = %g (%g)\n", power, z);
339 /* Drawing to the original-size pixbuf, resampling happens in the expose event. */
340 pixels[row*rowstride + modslice * channels + 0] = z >= 256 ? MIN(z - 256, 255) : 0;
341 pixels[row*rowstride + modslice * channels + 1] = z >= 256 ? MAX(512 - z, 0) : MAX(z, 0);
342 pixels[row*rowstride + modslice * channels + 2] = z < 256 ? MIN(255 - z, 255) : 0;
344 waterfall->slice++;
346 /* Consume input. */
347 memmove(waterfall->buf,
348 waterfall->buf + waterfall->integration_samples*SAMPLE_SIZE,
349 waterfall->buf_size - waterfall->integration_samples*SAMPLE_SIZE);
350 waterfall->buf_index -= waterfall->integration_samples*SAMPLE_SIZE;
351 batch_size++;
354 /* Scroll the waterfall. */
355 split_pixbuf(waterfall);
356 invalidate_spectrogram(waterfall);
358 if (batch_size > 2) {
359 printf("I can't keep up (%d)\n", batch_size);
362 if (err) {
363 g_error_free(err);
366 return TRUE;
369 int main(int argc, char *argv[])
371 GtkBuilder *gtk_builder;
372 GtkWindow *waterfall_window;
373 GIOChannel *stdin_channel;
374 int stdin_flags;
375 GError *err = NULL;
376 struct waterfall_context waterfall = {
377 .buf_index = 0,
378 .integration_samples = INTEGRATION_SAMPLES,
379 .slice = 0,
380 .n_slices = N_SLICES,
381 .zoom = {
382 .f_low = 0,
383 .f_high = INTEGRATION_SAMPLES/2,
384 .noise_floor = 5,
385 .sensitivity = 200,
389 gtk_init(&argc, &argv);
391 gtk_builder = gtk_builder_new();
392 gtk_builder_add_from_file(gtk_builder, "waterfall.glade", NULL);
393 gtk_builder_connect_signals_full(gtk_builder, &connect_signal, NULL);
395 /* Give signal handlers a means of accessing gtk_builder. */
396 g_object_set(gtk_builder_get_object(gtk_builder,
397 "waterfall_window"),
398 "user-data", gtk_builder,
399 NULL);
401 waterfall_window = GTK_WINDOW(gtk_builder_get_object(gtk_builder, "waterfall_window"));
402 waterfall.drawingarea = GTK_DRAWING_AREA(gtk_builder_get_object(gtk_builder, "image_detail_drawingarea"));
404 g_signal_connect(G_OBJECT(waterfall.drawingarea), "expose-event",
405 G_CALLBACK(&waterfall_expose_spectrogram), &waterfall);
406 g_signal_connect(G_OBJECT(waterfall.drawingarea), "configure-event",
407 G_CALLBACK(&waterfall_configure_spectrogram), &waterfall);
408 g_signal_connect(G_OBJECT(waterfall_window), "key-press-event",
409 G_CALLBACK(&waterfall_key_press), &waterfall);
411 /* Don't block on stdin. */
412 stdin_flags = fcntl(0, F_GETFL, 0);
413 fcntl(0, F_SETFL, stdin_flags | O_NONBLOCK);
415 stdin_channel = g_io_channel_unix_new(0);
416 g_io_channel_set_encoding(stdin_channel, NULL, &err);
417 g_io_channel_set_buffer_size(stdin_channel, BUF_SAMPLES*SAMPLE_SIZE);
418 waterfall.buf = malloc(BUF_SAMPLES*SAMPLE_SIZE);
419 waterfall.buf_size = INTEGRATION_SAMPLES*SAMPLE_SIZE;
420 g_io_add_watch(stdin_channel, G_IO_IN, &waterfall_input, &waterfall);
422 waterfall.original = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, waterfall.n_slices, INTEGRATION_SAMPLES/2 + 1);
423 split_pixbuf(&waterfall);
425 waterfall.fft.samples = fftw_malloc(sizeof (*waterfall.fft.samples) * INTEGRATION_SAMPLES);
426 waterfall.fft.spectrum = fftw_malloc(sizeof (*waterfall.fft.spectrum) * (INTEGRATION_SAMPLES/2 + 1));
427 waterfall.fft.p = fftw_plan_dft_r2c_1d(INTEGRATION_SAMPLES,
428 waterfall.fft.samples, waterfall.fft.spectrum,
429 FFTW_ESTIMATE);
431 gtk_widget_show(GTK_WIDGET(waterfall_window));
433 invalidate_spectrogram(&waterfall);
435 gtk_main();
437 fftw_destroy_plan(waterfall.fft.p);
438 fftw_free(waterfall.fft.spectrum);
439 fftw_free(waterfall.fft.samples);