Define and use N_SLICES.
[spectral-waterfall.git] / waterfall.c
blob77d49e262c170324133e46ad1ad9dc9c9992c152
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 int 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 * 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 waterfall->zoom.f_low += resolution / 4;
162 waterfall->zoom.f_high -= resolution / 4;
163 zoom_changed = TRUE;
164 break;
165 case GDK_KEY_minus:
166 printf("minus\n");
167 waterfall->zoom.f_low -= MIN(waterfall->zoom.f_low, resolution / 4);
168 waterfall->zoom.f_high += MIN(headroom, resolution / 4);
169 zoom_changed = TRUE;
170 break;
171 case GDK_KEY_bracketleft:
172 printf("[\n");
173 waterfall->zoom.noise_floor--;
174 break;
175 case GDK_KEY_bracketright:
176 printf("]\n");
177 waterfall->zoom.noise_floor++;
178 break;
179 case GDK_KEY_braceleft:
180 printf("{\n");
181 waterfall->zoom.sensitivity = MAX(waterfall->zoom.sensitivity / 2, 1);
182 break;
183 case GDK_KEY_braceright:
184 printf("}\n");
185 waterfall->zoom.sensitivity *= 2;
186 break;
189 if (zoom_changed) {
190 invalidate_spectrogram(waterfall);
193 return FALSE;
196 void connect_signal(GtkBuilder *builder,
197 GObject *object,
198 const gchar *signal_name,
199 const gchar *handler_name,
200 GObject *connect_object,
201 GConnectFlags flags,
202 gpointer user_data)
204 static struct {
205 char const *name;
206 GCallback fn;
207 } handlers[] = {
208 { "gtk_main_quit", G_CALLBACK(&gtk_main_quit) },
209 { "gtk_widget_hide_on_delete", G_CALLBACK(&gtk_widget_hide_on_delete) },
210 { "gtk_widget_show", G_CALLBACK(&gtk_widget_show) },
211 { "waterfall_open_output", G_CALLBACK(&waterfall_open_output) },
212 { NULL, NULL }
214 int i;
216 for (i = 0; handlers[i].name; i++) {
217 if (!strcmp(handlers[i].name, handler_name)) {
218 GCallback handler = handlers[i].fn;
219 g_signal_connect_object(object, signal_name, handler,
220 connect_object, flags);
221 return;
225 /* TODO: Connect to some error-spewing function? */
228 void invalidate_spectrogram(struct waterfall_context *waterfall)
230 GdkRegion *visible_region;
232 /* Just invalidate everything. */
233 if (waterfall->resized) {
234 g_object_unref(G_OBJECT(waterfall->resized));
235 waterfall->resized = NULL;
237 visible_region = gdk_drawable_get_visible_region(waterfall->drawingarea->widget.window);
238 gdk_window_invalidate_region(waterfall->drawingarea->widget.window,
239 visible_region,
240 FALSE);
241 gdk_region_destroy(visible_region);
244 void split_pixbuf(struct waterfall_context *waterfall)
246 int max_resolution = waterfall->integration_samples/2 + 1;
248 if (waterfall->subpixbufs.part1) {
249 g_object_unref(G_OBJECT(waterfall->subpixbufs.part1));
251 if (waterfall->subpixbufs.part2) {
252 g_object_unref(G_OBJECT(waterfall->subpixbufs.part2));
255 waterfall->subpixbufs.part1_size = waterfall->slice % waterfall->n_slices;
256 waterfall->subpixbufs.part2_size = waterfall->n_slices - waterfall->subpixbufs.part1_size;
257 printf("Split %d,%d\n", waterfall->subpixbufs.part1_size, waterfall->subpixbufs.part2_size);
258 if (waterfall->subpixbufs.part1_size) {
259 waterfall->subpixbufs.part1 = gdk_pixbuf_new_subpixbuf(waterfall->original,
260 0, 0,
261 waterfall->subpixbufs.part1_size, max_resolution);
262 } else {
263 waterfall->subpixbufs.part1 = NULL;
265 if (waterfall->subpixbufs.part2_size) {
266 waterfall->subpixbufs.part2 = gdk_pixbuf_new_subpixbuf(waterfall->original,
267 waterfall->subpixbufs.part1_size, 0,
268 waterfall->subpixbufs.part2_size, max_resolution);
269 } else {
270 waterfall->subpixbufs.part2 = NULL;
274 gboolean waterfall_input(GIOChannel *source,
275 GIOCondition condition,
276 gpointer userdata)
278 struct waterfall_context *waterfall = userdata;
279 double normalization = log10(waterfall->integration_samples * 32768);
280 gsize n;
281 int batch_size = 0;
282 GError *err = NULL;
284 switch (g_io_channel_read_chars(source,
285 waterfall->buf,
286 waterfall->buf_size - waterfall->buf_index,
288 &err)) {
289 case G_IO_STATUS_NORMAL:
290 waterfall->buf_index += n;
291 break;
292 case G_IO_STATUS_AGAIN:
293 printf("Try again later\n");
294 g_error_free(err);
295 return TRUE;
296 case G_IO_STATUS_EOF:
297 printf("End of input\n");
298 exit(0);
299 break;
300 case G_IO_STATUS_ERROR:
301 printf("Error - %s\n", err->message);
302 exit(1);
303 break;
304 default:
305 exit(1);
306 break;
309 while (waterfall->buf_index >= waterfall->integration_samples*SAMPLE_SIZE) {
310 int i, row, rowstride, channels, modslice;
311 int resolution = waterfall->integration_samples/2 + 1;
312 guchar *pixels;
314 /* Analyze the spectrum. */
315 printf("Integrate %d %zu %d (%d)\n", batch_size, waterfall->buf_index,
316 waterfall->slice, waterfall->slice % waterfall->n_slices);
317 for (i = 0; i < waterfall->integration_samples; i++) {
318 int16_t x;
319 memcpy(&x, waterfall->buf + i*SAMPLE_SIZE, sizeof (x));
320 waterfall->fft.samples[i] = x;
322 fftw_execute(waterfall->fft.p);
324 /* Draw a new slice. */
325 modslice = waterfall->slice % waterfall->n_slices;
326 pixels = gdk_pixbuf_get_pixels(waterfall->original);
327 rowstride = gdk_pixbuf_get_rowstride(waterfall->original);
328 channels = gdk_pixbuf_get_n_channels(waterfall->original);
329 for (row = 0; row < resolution; row++) {
330 double signal_i = waterfall->fft.spectrum[row][0], signal_q = waterfall->fft.spectrum[row][1];
331 /* XXX Give invsqrt a chance to happen. */
332 double power = -log10(1.0 / hypot(signal_i, signal_q)) - normalization;
333 double z = (power + waterfall->zoom.noise_floor) * waterfall->zoom.sensitivity;
334 if (row > 50 && row < 60) {
335 printf("Power = %g (%g)\n", power, z);
337 /* Drawing to the original-size pixbuf, resampling happens in the expose event. */
338 pixels[row*rowstride + modslice * channels + 0] = z >= 256 ? MIN(z - 256, 255) : 0;
339 pixels[row*rowstride + modslice * channels + 1] = z >= 256 ? MAX(512 - z, 0) : MAX(z, 0);
340 pixels[row*rowstride + modslice * channels + 2] = z < 256 ? MIN(255 - z, 255) : 0;
342 waterfall->slice++;
344 /* Consume input. */
345 memmove(waterfall->buf,
346 waterfall->buf + waterfall->integration_samples*SAMPLE_SIZE,
347 waterfall->buf_size - waterfall->integration_samples*SAMPLE_SIZE);
348 waterfall->buf_index -= waterfall->integration_samples*SAMPLE_SIZE;
349 batch_size++;
352 /* Scroll the waterfall. */
353 split_pixbuf(waterfall);
354 invalidate_spectrogram(waterfall);
356 if (batch_size > 2) {
357 printf("I can't keep up (%d)\n", batch_size);
360 if (err) {
361 g_error_free(err);
364 return TRUE;
367 int main(int argc, char *argv[])
369 GtkBuilder *gtk_builder;
370 GtkWindow *waterfall_window;
371 GIOChannel *stdin_channel;
372 int stdin_flags;
373 GError *err = NULL;
374 struct waterfall_context waterfall = {
375 .buf_index = 0,
376 .integration_samples = INTEGRATION_SAMPLES,
377 .slice = 0,
378 .n_slices = N_SLICES,
379 .zoom = {
380 .f_low = 0,
381 .f_high = INTEGRATION_SAMPLES/2,
382 .noise_floor = 5,
383 .sensitivity = 200,
387 gtk_init(&argc, &argv);
389 gtk_builder = gtk_builder_new();
390 gtk_builder_add_from_file(gtk_builder, "waterfall.glade", NULL);
391 gtk_builder_connect_signals_full(gtk_builder, &connect_signal, NULL);
393 /* Give signal handlers a means of accessing gtk_builder. */
394 g_object_set(gtk_builder_get_object(gtk_builder,
395 "waterfall_window"),
396 "user-data", gtk_builder,
397 NULL);
399 waterfall_window = GTK_WINDOW(gtk_builder_get_object(gtk_builder, "waterfall_window"));
400 waterfall.drawingarea = GTK_DRAWING_AREA(gtk_builder_get_object(gtk_builder, "image_detail_drawingarea"));
402 g_signal_connect(G_OBJECT(waterfall.drawingarea), "expose-event",
403 G_CALLBACK(&waterfall_expose_spectrogram), &waterfall);
404 g_signal_connect(G_OBJECT(waterfall.drawingarea), "configure-event",
405 G_CALLBACK(&waterfall_configure_spectrogram), &waterfall);
406 g_signal_connect(G_OBJECT(waterfall_window), "key-press-event",
407 G_CALLBACK(&waterfall_key_press), &waterfall);
409 /* Don't block on stdin. */
410 stdin_flags = fcntl(0, F_GETFL, 0);
411 fcntl(0, F_SETFL, stdin_flags | O_NONBLOCK);
413 stdin_channel = g_io_channel_unix_new(0);
414 g_io_channel_set_encoding(stdin_channel, NULL, &err);
415 g_io_channel_set_buffer_size(stdin_channel, BUF_SAMPLES*SAMPLE_SIZE);
416 waterfall.buf = malloc(BUF_SAMPLES*SAMPLE_SIZE);
417 waterfall.buf_size = INTEGRATION_SAMPLES*SAMPLE_SIZE;
418 g_io_add_watch(stdin_channel, G_IO_IN, &waterfall_input, &waterfall);
420 waterfall.original = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, waterfall.n_slices, INTEGRATION_SAMPLES/2 + 1);
421 split_pixbuf(&waterfall);
423 waterfall.fft.samples = fftw_malloc(sizeof (*waterfall.fft.samples) * INTEGRATION_SAMPLES);
424 waterfall.fft.spectrum = fftw_malloc(sizeof (*waterfall.fft.spectrum) * (INTEGRATION_SAMPLES/2 + 1));
425 waterfall.fft.p = fftw_plan_dft_r2c_1d(INTEGRATION_SAMPLES,
426 waterfall.fft.samples, waterfall.fft.spectrum,
427 FFTW_ESTIMATE);
429 gtk_widget_show(GTK_WIDGET(waterfall_window));
431 invalidate_spectrogram(&waterfall);
433 gtk_main();
435 fftw_destroy_plan(waterfall.fft.p);
436 fftw_free(waterfall.fft.spectrum);
437 fftw_free(waterfall.fft.samples);