Delete pointless text entry.
[spectral-waterfall.git] / waterfall.c
blob02acf0a8f9d81f7ab811f851d94f65a4c699b096
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 SAMPLE_SIZE 2
15 struct waterfall_context {
16 GtkDrawingArea *drawingarea;
17 GdkPixbuf *original; /* A 1-to-1 map from spectral data to pixels. */
18 GdkPixbuf *resized; /* A backing store for the scaled spectrogram. */
19 struct {
20 GdkPixbuf *part1; /* Slices from zero to the slice cursor. */
21 GdkPixbuf *part2; /* Slices from the slice cursor to the end. */
22 int part1_size;
23 int part2_size;
24 } subpixbufs;
25 struct {
26 /* The size of the spectrogram drawing area. */
27 int width;
28 int height;
29 } size;
30 struct {
31 int f_low;
32 int f_high;
33 } zoom;
34 char *buf;
35 size_t buf_size;
36 size_t buf_index;
37 size_t integration_samples;
38 int slice;
39 int n_slices;
40 struct {
41 double *samples;
42 fftw_complex *spectrum;
43 fftw_plan p;
44 } fft;
47 void invalidate_spectrogram(struct waterfall_context *waterfall);
49 void waterfall_open_output(GtkFileChooser *chooser, gpointer user_data)
51 char *filename = gtk_file_chooser_get_filename(chooser);
52 printf("Open %s\n", filename);
53 g_free(filename);
55 gtk_widget_hide(GTK_WIDGET(chooser));
58 void waterfall_expose_spectrogram(GtkWidget *widget,
59 GdkEventExpose *event,
60 gpointer user_data)
62 GtkDrawingArea *spectrogram_drawingarea = GTK_DRAWING_AREA(widget);
63 struct waterfall_context *waterfall = user_data;
64 cairo_t *cr;
66 if (!waterfall->resized) {
67 double part1_size, part2_size;
68 int resolution = waterfall->zoom.f_high - waterfall->zoom.f_low + 1;
69 double scale_x = (double) waterfall->size.width / waterfall->n_slices;
70 double scale_y = (double) waterfall->size.height / resolution;
71 double offset_y = -waterfall->zoom.f_low * scale_y;
72 int row, rowstride, channels;
73 guchar *pixels;
75 part1_size = (double) waterfall->subpixbufs.part1_size / waterfall->n_slices * waterfall->size.width;
76 part2_size = waterfall->size.width - (int) part1_size;
77 printf("Expose parts %g (%d),%g (%d)\n",
78 part1_size, waterfall->subpixbufs.part1_size,
79 part2_size, waterfall->subpixbufs.part2_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 printf("Scale (%g) part2 to width %d at 0\n",
86 scale_x, (int) part2_size);
87 gdk_pixbuf_scale(waterfall->subpixbufs.part2, waterfall->resized,
88 0, 0, (int) part2_size, waterfall->size.height,
89 0, offset_y, scale_x, scale_y,
90 GDK_INTERP_NEAREST);
92 if (waterfall->subpixbufs.part1) {
93 /* Then render the more recent slices. */
94 printf("Scale (%g) part1 to width %d at %d\n",
95 scale_x, (int) part1_size, (int) part2_size);
96 gdk_pixbuf_scale(waterfall->subpixbufs.part1, waterfall->resized,
97 (int) part2_size, 0, (int) part1_size, waterfall->size.height,
98 part2_size, offset_y, scale_x, scale_y,
99 GDK_INTERP_NEAREST);
101 pixels = gdk_pixbuf_get_pixels(waterfall->resized);
102 rowstride = gdk_pixbuf_get_rowstride(waterfall->resized);
103 channels = gdk_pixbuf_get_n_channels(waterfall->resized);
104 /* Draw a narrow marker to represent the border between part1 and part2. */
105 for (row = 0; row < waterfall->size.height; row++) {
106 pixels[row*rowstride + (int) part2_size * channels + 1] = 192;
110 cr = gdk_cairo_create(spectrogram_drawingarea->widget.window);
112 gdk_cairo_set_source_pixbuf(cr, waterfall->resized, 0, 0);
113 cairo_paint(cr);
115 cairo_destroy(cr);
118 void waterfall_configure_spectrogram(GtkWidget *widget,
119 GdkEventConfigure *event,
120 gpointer user_data)
122 struct waterfall_context *waterfall = user_data;
124 printf("configure %d %d,%d+%d,%d send_event=%d on window %p\n",
125 event->type, event->x, event->y, event->width, event->height, event->send_event, event->window);
127 waterfall->size.width = event->width;
128 waterfall->size.height = event->height;
130 invalidate_spectrogram(waterfall);
133 gboolean waterfall_key_press(GtkWidget *widget,
134 GdkEventKey *event,
135 gpointer user_data)
137 struct waterfall_context *waterfall = user_data;
138 int resolution = waterfall->zoom.f_high - waterfall->zoom.f_low + 1;
139 int headroom = waterfall->integration_samples/2 - waterfall->zoom.f_high;
140 gboolean zoom_changed = FALSE;
142 printf("key press\n");
143 switch (event->keyval) {
144 case GDK_KEY_Up:
145 printf("up\n");
146 waterfall->zoom.f_low -= MIN(waterfall->zoom.f_low, resolution / 2);
147 waterfall->zoom.f_high -= MIN(waterfall->zoom.f_low, resolution / 2);
148 zoom_changed = TRUE;
149 break;
150 case GDK_KEY_Down:
151 printf("down\n");
152 waterfall->zoom.f_low += MIN(headroom, resolution / 2);
153 waterfall->zoom.f_high += MIN(headroom, resolution / 2);
154 zoom_changed = TRUE;
155 break;
156 case GDK_KEY_plus:
157 printf("plus\n");
158 waterfall->zoom.f_low += resolution / 4;
159 waterfall->zoom.f_high -= resolution / 4;
160 zoom_changed = TRUE;
161 break;
162 case GDK_KEY_minus:
163 printf("minus\n");
164 waterfall->zoom.f_low -= MIN(waterfall->zoom.f_low, resolution / 4);
165 waterfall->zoom.f_high += MIN(headroom, resolution / 4);
166 zoom_changed = TRUE;
167 break;
170 if (zoom_changed) {
171 invalidate_spectrogram(waterfall);
174 return FALSE;
177 void connect_signal(GtkBuilder *builder,
178 GObject *object,
179 const gchar *signal_name,
180 const gchar *handler_name,
181 GObject *connect_object,
182 GConnectFlags flags,
183 gpointer user_data)
185 static struct {
186 char const *name;
187 GCallback fn;
188 } handlers[] = {
189 { "gtk_main_quit", G_CALLBACK(&gtk_main_quit) },
190 { "gtk_widget_hide_on_delete", G_CALLBACK(&gtk_widget_hide_on_delete) },
191 { "gtk_widget_show", G_CALLBACK(&gtk_widget_show) },
192 { "waterfall_open_output", G_CALLBACK(&waterfall_open_output) },
193 { NULL, NULL }
195 int i;
197 for (i = 0; handlers[i].name; i++) {
198 if (!strcmp(handlers[i].name, handler_name)) {
199 GCallback handler = handlers[i].fn;
200 g_signal_connect_object(object, signal_name, handler,
201 connect_object, flags);
202 return;
206 /* TODO: Connect to some error-spewing function? */
209 void invalidate_spectrogram(struct waterfall_context *waterfall)
211 GdkRegion *visible_region;
213 /* Just invalidate everything. */
214 if (waterfall->resized) {
215 g_object_unref(G_OBJECT(waterfall->resized));
216 waterfall->resized = NULL;
218 visible_region = gdk_drawable_get_visible_region(waterfall->drawingarea->widget.window);
219 gdk_window_invalidate_region(waterfall->drawingarea->widget.window,
220 visible_region,
221 FALSE);
222 gdk_region_destroy(visible_region);
225 void split_pixbuf(struct waterfall_context *waterfall)
227 int max_resolution = waterfall->integration_samples/2 + 1;
229 if (waterfall->subpixbufs.part1) {
230 g_object_unref(G_OBJECT(waterfall->subpixbufs.part1));
232 if (waterfall->subpixbufs.part2) {
233 g_object_unref(G_OBJECT(waterfall->subpixbufs.part2));
236 waterfall->subpixbufs.part1_size = waterfall->slice % waterfall->n_slices;
237 waterfall->subpixbufs.part2_size = waterfall->n_slices - waterfall->subpixbufs.part1_size;
238 printf("Split %d,%d\n", waterfall->subpixbufs.part1_size, waterfall->subpixbufs.part2_size);
239 if (waterfall->subpixbufs.part1_size) {
240 waterfall->subpixbufs.part1 = gdk_pixbuf_new_subpixbuf(waterfall->original,
241 0, 0,
242 waterfall->subpixbufs.part1_size, max_resolution);
243 } else {
244 waterfall->subpixbufs.part1 = NULL;
246 if (waterfall->subpixbufs.part2_size) {
247 waterfall->subpixbufs.part2 = gdk_pixbuf_new_subpixbuf(waterfall->original,
248 waterfall->subpixbufs.part1_size, 0,
249 waterfall->subpixbufs.part2_size, max_resolution);
250 } else {
251 waterfall->subpixbufs.part2 = NULL;
255 gboolean waterfall_input(GIOChannel *source,
256 GIOCondition condition,
257 gpointer userdata)
259 struct waterfall_context *waterfall = userdata;
260 double normalization = log10(waterfall->integration_samples * 32768);
261 gsize n;
262 int batch_size = 0;
263 GError *err = NULL;
265 switch (g_io_channel_read_chars(source,
266 waterfall->buf,
267 waterfall->buf_size - waterfall->buf_index,
269 &err)) {
270 case G_IO_STATUS_NORMAL:
271 waterfall->buf_index += n;
272 break;
273 case G_IO_STATUS_AGAIN:
274 printf("Try again later\n");
275 g_error_free(err);
276 return TRUE;
277 case G_IO_STATUS_EOF:
278 printf("End of input\n");
279 exit(0);
280 break;
281 case G_IO_STATUS_ERROR:
282 printf("Error - %s\n", err->message);
283 exit(1);
284 break;
285 default:
286 exit(1);
287 break;
290 while (waterfall->buf_index >= waterfall->integration_samples*SAMPLE_SIZE) {
291 int i, row, rowstride, channels, modslice;
292 int resolution = waterfall->integration_samples/2 + 1;
293 guchar *pixels;
295 /* Analyze the spectrum. */
296 printf("Integrate %d %zu %d (%d)\n", batch_size, waterfall->buf_index,
297 waterfall->slice, waterfall->slice % waterfall->n_slices);
298 for (i = 0; i < waterfall->integration_samples; i++) {
299 int16_t x;
300 memcpy(&x, waterfall->buf + i*SAMPLE_SIZE, sizeof (x));
301 waterfall->fft.samples[i] = x;
303 fftw_execute(waterfall->fft.p);
305 /* Draw a new slice. */
306 modslice = waterfall->slice % waterfall->n_slices;
307 pixels = gdk_pixbuf_get_pixels(waterfall->original);
308 rowstride = gdk_pixbuf_get_rowstride(waterfall->original);
309 channels = gdk_pixbuf_get_n_channels(waterfall->original);
310 for (row = 0; row < resolution; row++) {
311 double signal_i = waterfall->fft.spectrum[row][0], signal_q = waterfall->fft.spectrum[row][1];
312 /* XXX Give invsqrt a chance to happen. */
313 double power = -log10(1.0 / hypot(signal_i, signal_q)) - normalization;
314 double z = (power + 5) * 200;
315 if (row > 50 && row < 60) {
316 printf("Power = %g (%g)\n", power, z);
318 /* Drawing to the original-size pixbuf, resampling happens in the expose event. */
319 pixels[row*rowstride + modslice * channels + 0] = z >= 256 ? z - 256 : 0;
320 pixels[row*rowstride + modslice * channels + 1] = z >= 256 ? 512 - z : z;
321 pixels[row*rowstride + modslice * channels + 2] = z < 256 ? 255 - z : 0;
323 waterfall->slice++;
325 /* Consume input. */
326 memmove(waterfall->buf,
327 waterfall->buf + waterfall->integration_samples*SAMPLE_SIZE,
328 waterfall->buf_size - waterfall->integration_samples*SAMPLE_SIZE);
329 waterfall->buf_index -= waterfall->integration_samples*SAMPLE_SIZE;
330 batch_size++;
333 /* Scroll the waterfall. */
334 split_pixbuf(waterfall);
335 invalidate_spectrogram(waterfall);
337 if (batch_size > 2) {
338 printf("I can't keep up (%d)\n", batch_size);
341 if (err) {
342 g_error_free(err);
345 return TRUE;
348 int main(int argc, char *argv[])
350 GtkBuilder *gtk_builder;
351 GtkWindow *waterfall_window;
352 GIOChannel *stdin_channel;
353 int stdin_flags;
354 GError *err = NULL;
355 struct waterfall_context waterfall = {
356 .buf_index = 0,
357 .integration_samples = INTEGRATION_SAMPLES,
358 .slice = 0,
359 .n_slices = 20,
360 .zoom = {
361 .f_low = 0,
362 .f_high = INTEGRATION_SAMPLES/2,
366 gtk_init(&argc, &argv);
368 gtk_builder = gtk_builder_new();
369 gtk_builder_add_from_file(gtk_builder, "waterfall.glade", NULL);
370 gtk_builder_connect_signals_full(gtk_builder, &connect_signal, NULL);
372 /* Give signal handlers a means of accessing gtk_builder. */
373 g_object_set(gtk_builder_get_object(gtk_builder,
374 "waterfall_window"),
375 "user-data", gtk_builder,
376 NULL);
378 waterfall_window = GTK_WINDOW(gtk_builder_get_object(gtk_builder, "waterfall_window"));
379 waterfall.drawingarea = GTK_DRAWING_AREA(gtk_builder_get_object(gtk_builder, "image_detail_drawingarea"));
381 g_signal_connect(G_OBJECT(waterfall.drawingarea), "expose-event",
382 G_CALLBACK(&waterfall_expose_spectrogram), &waterfall);
383 g_signal_connect(G_OBJECT(waterfall.drawingarea), "configure-event",
384 G_CALLBACK(&waterfall_configure_spectrogram), &waterfall);
385 g_signal_connect(G_OBJECT(waterfall_window), "key-press-event",
386 G_CALLBACK(&waterfall_key_press), &waterfall);
388 /* Don't block on stdin. */
389 stdin_flags = fcntl(0, F_GETFL, 0);
390 fcntl(0, F_SETFL, stdin_flags | O_NONBLOCK);
392 stdin_channel = g_io_channel_unix_new(0);
393 g_io_channel_set_encoding(stdin_channel, NULL, &err);
394 g_io_channel_set_buffer_size(stdin_channel, BUF_SAMPLES*SAMPLE_SIZE);
395 waterfall.buf = malloc(BUF_SAMPLES*SAMPLE_SIZE);
396 waterfall.buf_size = INTEGRATION_SAMPLES*SAMPLE_SIZE;
397 g_io_add_watch(stdin_channel, G_IO_IN, &waterfall_input, &waterfall);
399 waterfall.original = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, waterfall.n_slices, INTEGRATION_SAMPLES/2 + 1);
400 split_pixbuf(&waterfall);
402 waterfall.fft.samples = fftw_malloc(sizeof (*waterfall.fft.samples) * INTEGRATION_SAMPLES);
403 waterfall.fft.spectrum = fftw_malloc(sizeof (*waterfall.fft.spectrum) * (INTEGRATION_SAMPLES/2 + 1));
404 waterfall.fft.p = fftw_plan_dft_r2c_1d(INTEGRATION_SAMPLES,
405 waterfall.fft.samples, waterfall.fft.spectrum,
406 FFTW_ESTIMATE);
408 gtk_widget_show(GTK_WIDGET(waterfall_window));
410 invalidate_spectrogram(&waterfall);
412 gtk_main();
414 fftw_destroy_plan(waterfall.fft.p);
415 fftw_free(waterfall.fft.spectrum);
416 fftw_free(waterfall.fft.samples);