Delete vague Ubuntu-specific properties.
[spectral-waterfall.git] / waterfall.c
blobd177ba78f4b01d6e4fb4d4f397b5ecd3c4b1e572
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;
81 /* Create a new backing store, then render into it. */
82 waterfall->resized = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, waterfall->size.width, waterfall->size.height);
83 if (waterfall->subpixbufs.part2) {
84 /* Render the oldest slices first. */
85 gdk_pixbuf_scale(waterfall->subpixbufs.part2, waterfall->resized,
86 0, 0, (int) part2_size, waterfall->size.height,
87 0, offset_y, scale_x, scale_y,
88 GDK_INTERP_NEAREST);
90 if (waterfall->subpixbufs.part1) {
91 /* Then render the more recent slices. */
92 gdk_pixbuf_scale(waterfall->subpixbufs.part1, waterfall->resized,
93 (int) part2_size, 0, (int) part1_size, waterfall->size.height,
94 part2_size, offset_y, scale_x, scale_y,
95 GDK_INTERP_NEAREST);
97 pixels = gdk_pixbuf_get_pixels(waterfall->resized);
98 rowstride = gdk_pixbuf_get_rowstride(waterfall->resized);
99 channels = gdk_pixbuf_get_n_channels(waterfall->resized);
100 /* Draw a narrow marker to represent the border between part1 and part2. */
101 for (row = 0; row < waterfall->size.height; row++) {
102 pixels[row*rowstride + ((int) part2_size % waterfall->size.width) * channels + 1] = 192;
106 cr = gdk_cairo_create(spectrogram_drawingarea->widget.window);
108 gdk_cairo_set_source_pixbuf(cr, waterfall->resized, 0, 0);
109 cairo_paint(cr);
111 cairo_destroy(cr);
114 void waterfall_configure_spectrogram(GtkWidget *widget,
115 GdkEventConfigure *event,
116 gpointer user_data)
118 struct waterfall_context *waterfall = user_data;
120 printf("configure %d %d,%d+%d,%d send_event=%d on window %p\n",
121 event->type, event->x, event->y, event->width, event->height, event->send_event, event->window);
123 waterfall->size.width = event->width;
124 waterfall->size.height = event->height;
126 invalidate_spectrogram(waterfall);
129 gboolean waterfall_key_press(GtkWidget *widget,
130 GdkEventKey *event,
131 gpointer user_data)
133 struct waterfall_context *waterfall = user_data;
134 int resolution = waterfall->zoom.f_high - waterfall->zoom.f_low + 1;
135 int headroom = waterfall->integration_samples/2 - waterfall->zoom.f_high;
136 gboolean zoom_changed = FALSE;
138 printf("key press\n");
139 switch (event->keyval) {
140 case GDK_KEY_Up:
141 printf("up\n");
142 waterfall->zoom.f_low -= MIN(waterfall->zoom.f_low, resolution / 2);
143 waterfall->zoom.f_high -= MIN(waterfall->zoom.f_low, resolution / 2);
144 zoom_changed = TRUE;
145 break;
146 case GDK_KEY_Down:
147 printf("down\n");
148 waterfall->zoom.f_low += MIN(headroom, resolution / 2);
149 waterfall->zoom.f_high += MIN(headroom, resolution / 2);
150 zoom_changed = TRUE;
151 break;
152 case GDK_KEY_plus:
153 printf("plus\n");
154 if (resolution / 4 > 1) {
155 waterfall->zoom.f_low += resolution / 4;
156 waterfall->zoom.f_high -= resolution / 4;
157 zoom_changed = TRUE;
159 break;
160 case GDK_KEY_minus:
161 printf("minus\n");
162 waterfall->zoom.f_low -= MIN(waterfall->zoom.f_low, resolution / 4);
163 waterfall->zoom.f_high += MIN(headroom, resolution / 4);
164 zoom_changed = TRUE;
165 break;
166 case GDK_KEY_bracketleft:
167 printf("[\n");
168 waterfall->zoom.noise_floor--;
169 break;
170 case GDK_KEY_bracketright:
171 printf("]\n");
172 waterfall->zoom.noise_floor++;
173 break;
174 case GDK_KEY_braceleft:
175 printf("{\n");
176 waterfall->zoom.sensitivity /= 1.5;
177 break;
178 case GDK_KEY_braceright:
179 printf("}\n");
180 waterfall->zoom.sensitivity *= 1.5;
181 break;
184 if (zoom_changed) {
185 invalidate_spectrogram(waterfall);
188 return FALSE;
191 void connect_signal(GtkBuilder *builder,
192 GObject *object,
193 const gchar *signal_name,
194 const gchar *handler_name,
195 GObject *connect_object,
196 GConnectFlags flags,
197 gpointer user_data)
199 static struct {
200 char const *name;
201 GCallback fn;
202 } handlers[] = {
203 { "gtk_main_quit", G_CALLBACK(&gtk_main_quit) },
204 { "gtk_widget_hide_on_delete", G_CALLBACK(&gtk_widget_hide_on_delete) },
205 { "gtk_widget_show", G_CALLBACK(&gtk_widget_show) },
206 { "waterfall_open_output", G_CALLBACK(&waterfall_open_output) },
207 { NULL, NULL }
209 int i;
211 for (i = 0; handlers[i].name; i++) {
212 if (!strcmp(handlers[i].name, handler_name)) {
213 GCallback handler = handlers[i].fn;
214 g_signal_connect_object(object, signal_name, handler,
215 connect_object, flags);
216 return;
220 /* TODO: Connect to some error-spewing function? */
223 void invalidate_spectrogram(struct waterfall_context *waterfall)
225 GdkRegion *visible_region;
227 /* Just invalidate everything. */
228 if (waterfall->resized) {
229 g_object_unref(G_OBJECT(waterfall->resized));
230 waterfall->resized = NULL;
232 visible_region = gdk_drawable_get_visible_region(waterfall->drawingarea->widget.window);
233 gdk_window_invalidate_region(waterfall->drawingarea->widget.window,
234 visible_region,
235 FALSE);
236 gdk_region_destroy(visible_region);
239 void split_pixbuf(struct waterfall_context *waterfall)
241 int max_resolution = waterfall->integration_samples/2 + 1;
243 if (waterfall->subpixbufs.part1) {
244 g_object_unref(G_OBJECT(waterfall->subpixbufs.part1));
246 if (waterfall->subpixbufs.part2) {
247 g_object_unref(G_OBJECT(waterfall->subpixbufs.part2));
250 waterfall->subpixbufs.part1_size = waterfall->slice % waterfall->n_slices;
251 waterfall->subpixbufs.part2_size = waterfall->n_slices - waterfall->subpixbufs.part1_size;
252 if (waterfall->subpixbufs.part1_size) {
253 waterfall->subpixbufs.part1 = gdk_pixbuf_new_subpixbuf(waterfall->original,
254 0, 0,
255 waterfall->subpixbufs.part1_size, max_resolution);
256 } else {
257 waterfall->subpixbufs.part1 = NULL;
259 if (waterfall->subpixbufs.part2_size) {
260 waterfall->subpixbufs.part2 = gdk_pixbuf_new_subpixbuf(waterfall->original,
261 waterfall->subpixbufs.part1_size, 0,
262 waterfall->subpixbufs.part2_size, max_resolution);
263 } else {
264 waterfall->subpixbufs.part2 = NULL;
268 gboolean waterfall_input(GIOChannel *source,
269 GIOCondition condition,
270 gpointer userdata)
272 struct waterfall_context *waterfall = userdata;
273 double normalization = log10(waterfall->integration_samples * 32768);
274 gsize n;
275 int batch_size = 0;
276 GError *err = NULL;
278 switch (g_io_channel_read_chars(source,
279 waterfall->buf,
280 waterfall->buf_size - waterfall->buf_index,
282 &err)) {
283 case G_IO_STATUS_NORMAL:
284 waterfall->buf_index += n;
285 break;
286 case G_IO_STATUS_AGAIN:
287 printf("Try again later\n");
288 g_error_free(err);
289 return TRUE;
290 case G_IO_STATUS_EOF:
291 printf("End of input\n");
292 exit(0);
293 break;
294 case G_IO_STATUS_ERROR:
295 printf("Error - %s\n", err->message);
296 exit(1);
297 break;
298 default:
299 exit(1);
300 break;
303 while (waterfall->buf_index >= waterfall->integration_samples*SAMPLE_SIZE) {
304 int i, row, rowstride, channels, modslice;
305 int resolution = waterfall->integration_samples/2 + 1;
306 guchar *pixels;
308 /* Analyze the spectrum. */
309 printf("Integrate %d %zu %d (%d)\n", batch_size, waterfall->buf_index,
310 waterfall->slice, waterfall->slice % waterfall->n_slices);
311 for (i = 0; i < waterfall->integration_samples; i++) {
312 int16_t x;
313 memcpy(&x, waterfall->buf + i*SAMPLE_SIZE, sizeof (x));
314 waterfall->fft.samples[i] = x;
316 fftw_execute(waterfall->fft.p);
318 /* Draw a new slice. */
319 modslice = waterfall->slice % waterfall->n_slices;
320 pixels = gdk_pixbuf_get_pixels(waterfall->original);
321 rowstride = gdk_pixbuf_get_rowstride(waterfall->original);
322 channels = gdk_pixbuf_get_n_channels(waterfall->original);
323 for (row = 0; row < resolution; row++) {
324 double signal_i = waterfall->fft.spectrum[row][0], signal_q = waterfall->fft.spectrum[row][1];
325 /* XXX Give invsqrt a chance to happen. */
326 double power = -log10(1.0 / hypot(signal_i, signal_q)) - normalization;
327 double z = (power + waterfall->zoom.noise_floor) * waterfall->zoom.sensitivity;
328 if (row > 50 && row < 60) {
329 printf("Power = %g (%g)\n", power, z);
331 /* Drawing to the original-size pixbuf, resampling happens in the expose event. */
332 pixels[row*rowstride + modslice * channels + 0] = z >= 256 ? MIN(z - 256, 255) : 0;
333 pixels[row*rowstride + modslice * channels + 1] = z >= 256 ? MAX(511 - z, 0) : MAX(z, 0);
334 pixels[row*rowstride + modslice * channels + 2] = z < 256 ? MIN(255 - z, 255) : 0;
336 waterfall->slice++;
338 /* Consume input. */
339 memmove(waterfall->buf,
340 waterfall->buf + waterfall->integration_samples*SAMPLE_SIZE,
341 waterfall->buf_size - waterfall->integration_samples*SAMPLE_SIZE);
342 waterfall->buf_index -= waterfall->integration_samples*SAMPLE_SIZE;
343 batch_size++;
346 /* Scroll the waterfall. */
347 split_pixbuf(waterfall);
348 invalidate_spectrogram(waterfall);
350 if (batch_size > 2) {
351 printf("I can't keep up (%d)\n", batch_size);
354 if (err) {
355 g_error_free(err);
358 return TRUE;
361 int main(int argc, char *argv[])
363 GtkBuilder *gtk_builder;
364 GtkWindow *waterfall_window;
365 GIOChannel *stdin_channel;
366 int stdin_flags;
367 GError *err = NULL;
368 struct waterfall_context waterfall = {
369 .buf_index = 0,
370 .slice = 0,
371 .zoom = {
372 .f_low = 0,
373 .noise_floor = 5,
374 .sensitivity = 200,
378 waterfall.integration_samples = INTEGRATION_SAMPLES;
379 waterfall.n_slices = N_SLICES;
380 waterfall.zoom.f_high = waterfall.integration_samples/2;
382 gtk_init(&argc, &argv);
384 gtk_builder = gtk_builder_new();
385 gtk_builder_add_from_file(gtk_builder, "waterfall.glade", NULL);
386 gtk_builder_connect_signals_full(gtk_builder, &connect_signal, NULL);
388 /* Give signal handlers a means of accessing gtk_builder. */
389 g_object_set(gtk_builder_get_object(gtk_builder,
390 "waterfall_window"),
391 "user-data", gtk_builder,
392 NULL);
394 waterfall_window = GTK_WINDOW(gtk_builder_get_object(gtk_builder, "waterfall_window"));
395 waterfall.drawingarea = GTK_DRAWING_AREA(gtk_builder_get_object(gtk_builder, "image_detail_drawingarea"));
397 g_signal_connect(G_OBJECT(waterfall.drawingarea), "expose-event",
398 G_CALLBACK(&waterfall_expose_spectrogram), &waterfall);
399 g_signal_connect(G_OBJECT(waterfall.drawingarea), "configure-event",
400 G_CALLBACK(&waterfall_configure_spectrogram), &waterfall);
401 g_signal_connect(G_OBJECT(waterfall_window), "key-press-event",
402 G_CALLBACK(&waterfall_key_press), &waterfall);
404 /* Don't block on stdin. */
405 stdin_flags = fcntl(0, F_GETFL, 0);
406 fcntl(0, F_SETFL, stdin_flags | O_NONBLOCK);
408 stdin_channel = g_io_channel_unix_new(0);
409 g_io_channel_set_encoding(stdin_channel, NULL, &err);
410 g_io_channel_set_buffer_size(stdin_channel, BUF_SAMPLES*SAMPLE_SIZE);
411 waterfall.buf = malloc(BUF_SAMPLES*SAMPLE_SIZE);
412 waterfall.buf_size = waterfall.integration_samples * SAMPLE_SIZE;
413 g_io_add_watch(stdin_channel, G_IO_IN, &waterfall_input, &waterfall);
415 waterfall.original = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8,
416 waterfall.n_slices, waterfall.integration_samples/2 + 1);
417 split_pixbuf(&waterfall);
419 waterfall.fft.samples = fftw_malloc(sizeof (*waterfall.fft.samples) * waterfall.integration_samples);
420 waterfall.fft.spectrum = fftw_malloc(sizeof (*waterfall.fft.spectrum) * (waterfall.integration_samples/2 + 1));
421 waterfall.fft.p = fftw_plan_dft_r2c_1d(waterfall.integration_samples,
422 waterfall.fft.samples, waterfall.fft.spectrum,
423 FFTW_ESTIMATE);
425 gtk_widget_show(GTK_WIDGET(waterfall_window));
427 invalidate_spectrogram(&waterfall);
429 gtk_main();
431 fftw_destroy_plan(waterfall.fft.p);
432 fftw_free(waterfall.fft.spectrum);
433 fftw_free(waterfall.fft.samples);