6 #include <gdk/gdkkeysyms.h>
13 #define SAMPLE_RATE 44100
17 struct waterfall_context
{
18 GtkDrawingArea
*drawingarea
;
19 GtkRuler
*frequency_ruler
;
21 GdkPixbuf
*original
; /* A 1-to-1 map from spectral data to pixels. */
22 GdkPixbuf
*resized
; /* A backing store for the scaled spectrogram. */
24 GdkPixbuf
*part1
; /* Slices from zero to the slice cursor. */
25 GdkPixbuf
*part2
; /* Slices from the slice cursor to the end. */
30 /* The size of the spectrogram drawing area. */
44 size_t integration_samples
;
49 fftw_complex
*spectrum
;
54 void invalidate_spectrogram(struct waterfall_context
*waterfall
);
56 void waterfall_open_output(GtkFileChooser
*chooser
, gpointer user_data
)
58 char *filename
= gtk_file_chooser_get_filename(chooser
);
59 printf("Open %s\n", filename
);
62 gtk_widget_hide(GTK_WIDGET(chooser
));
65 void waterfall_expose_spectrogram(GtkWidget
*widget
,
66 GdkEventExpose
*event
,
69 GtkDrawingArea
*spectrogram_drawingarea
= GTK_DRAWING_AREA(widget
);
70 struct waterfall_context
*waterfall
= user_data
;
73 if (!waterfall
->resized
) {
74 double part1_size
, part2_size
;
75 int resolution
= waterfall
->zoom
.f_high
- waterfall
->zoom
.f_low
+ 1;
76 double scale_x
= (double) waterfall
->size
.width
/ waterfall
->n_slices
;
77 double scale_y
= (double) waterfall
->size
.height
/ resolution
;
78 double offset_y
= -waterfall
->zoom
.f_low
* scale_y
;
79 int row
, rowstride
, channels
;
82 part1_size
= (double) waterfall
->subpixbufs
.part1_size
/ waterfall
->n_slices
* waterfall
->size
.width
;
83 part2_size
= waterfall
->size
.width
- (int) part1_size
;
85 /* Create a new backing store, then render into it. */
86 waterfall
->resized
= gdk_pixbuf_new(GDK_COLORSPACE_RGB
, FALSE
, 8, waterfall
->size
.width
, waterfall
->size
.height
);
87 if (waterfall
->subpixbufs
.part2
) {
88 /* Render the oldest slices first. */
89 gdk_pixbuf_scale(waterfall
->subpixbufs
.part2
, waterfall
->resized
,
90 0, 0, (int) part2_size
, waterfall
->size
.height
,
91 0, offset_y
, scale_x
, scale_y
,
94 if (waterfall
->subpixbufs
.part1
) {
95 /* Then render the more recent slices. */
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
,
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
% waterfall
->size
.width
) * channels
+ 1] = 192;
110 cr
= gdk_cairo_create(spectrogram_drawingarea
->widget
.window
);
112 gdk_cairo_set_source_pixbuf(cr
, waterfall
->resized
, 0, 0);
118 void waterfall_configure_spectrogram(GtkWidget
*widget
,
119 GdkEventConfigure
*event
,
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
,
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
) {
146 waterfall
->zoom
.f_low
-= MIN(waterfall
->zoom
.f_low
, resolution
/ 2);
147 waterfall
->zoom
.f_high
-= MIN(waterfall
->zoom
.f_low
, resolution
/ 2);
152 waterfall
->zoom
.f_low
+= MIN(headroom
, resolution
/ 2);
153 waterfall
->zoom
.f_high
+= MIN(headroom
, resolution
/ 2);
158 if (resolution
/ 4 > 1) {
159 waterfall
->zoom
.f_low
+= resolution
/ 4;
160 waterfall
->zoom
.f_high
-= resolution
/ 4;
166 waterfall
->zoom
.f_low
-= MIN(waterfall
->zoom
.f_low
, resolution
/ 4);
167 waterfall
->zoom
.f_high
+= MIN(headroom
, resolution
/ 4);
170 case GDK_KEY_bracketleft
:
172 waterfall
->zoom
.noise_floor
--;
174 case GDK_KEY_bracketright
:
176 waterfall
->zoom
.noise_floor
++;
178 case GDK_KEY_braceleft
:
180 waterfall
->zoom
.sensitivity
/= 1.5;
182 case GDK_KEY_braceright
:
184 waterfall
->zoom
.sensitivity
*= 1.5;
189 invalidate_spectrogram(waterfall
);
195 void connect_signal(GtkBuilder
*builder
,
197 const gchar
*signal_name
,
198 const gchar
*handler_name
,
199 GObject
*connect_object
,
207 { "gtk_main_quit", G_CALLBACK(>k_main_quit
) },
208 { "gtk_widget_hide_on_delete", G_CALLBACK(>k_widget_hide_on_delete
) },
209 { "gtk_widget_show", G_CALLBACK(>k_widget_show
) },
210 { "waterfall_open_output", G_CALLBACK(&waterfall_open_output
) },
215 for (i
= 0; handlers
[i
].name
; i
++) {
216 if (!strcmp(handlers
[i
].name
, handler_name
)) {
217 GCallback handler
= handlers
[i
].fn
;
218 g_signal_connect_object(object
, signal_name
, handler
,
219 connect_object
, flags
);
224 /* TODO: Connect to some error-spewing function? */
227 void invalidate_spectrogram(struct waterfall_context
*waterfall
)
229 GdkRegion
*visible_region
;
231 /* Just invalidate everything. */
232 if (waterfall
->resized
) {
233 g_object_unref(G_OBJECT(waterfall
->resized
));
234 waterfall
->resized
= NULL
;
236 visible_region
= gdk_drawable_get_visible_region(waterfall
->drawingarea
->widget
.window
);
237 gdk_window_invalidate_region(waterfall
->drawingarea
->widget
.window
,
240 gdk_region_destroy(visible_region
);
242 gtk_ruler_set_range(waterfall
->frequency_ruler
,
243 waterfall
->zoom
.f_low
* waterfall
->zoom
.hertz_scale
,
244 waterfall
->zoom
.f_high
* waterfall
->zoom
.hertz_scale
,
245 waterfall
->zoom
.f_high
* waterfall
->zoom
.hertz_scale
,
246 (waterfall
->zoom
.f_high
- waterfall
->zoom
.f_low
) * waterfall
->zoom
.hertz_scale
);
249 void split_pixbuf(struct waterfall_context
*waterfall
)
251 int max_resolution
= waterfall
->integration_samples
/2 + 1;
253 if (waterfall
->subpixbufs
.part1
) {
254 g_object_unref(G_OBJECT(waterfall
->subpixbufs
.part1
));
256 if (waterfall
->subpixbufs
.part2
) {
257 g_object_unref(G_OBJECT(waterfall
->subpixbufs
.part2
));
260 waterfall
->subpixbufs
.part1_size
= waterfall
->slice
% waterfall
->n_slices
;
261 waterfall
->subpixbufs
.part2_size
= waterfall
->n_slices
- waterfall
->subpixbufs
.part1_size
;
262 if (waterfall
->subpixbufs
.part1_size
) {
263 waterfall
->subpixbufs
.part1
= gdk_pixbuf_new_subpixbuf(waterfall
->original
,
265 waterfall
->subpixbufs
.part1_size
, max_resolution
);
267 waterfall
->subpixbufs
.part1
= NULL
;
269 if (waterfall
->subpixbufs
.part2_size
) {
270 waterfall
->subpixbufs
.part2
= gdk_pixbuf_new_subpixbuf(waterfall
->original
,
271 waterfall
->subpixbufs
.part1_size
, 0,
272 waterfall
->subpixbufs
.part2_size
, max_resolution
);
274 waterfall
->subpixbufs
.part2
= NULL
;
278 gboolean
waterfall_input(GIOChannel
*source
,
279 GIOCondition condition
,
282 struct waterfall_context
*waterfall
= userdata
;
283 double normalization
= log10(waterfall
->integration_samples
* 32768);
288 switch (g_io_channel_read_chars(source
,
290 waterfall
->buf_size
- waterfall
->buf_index
,
293 case G_IO_STATUS_NORMAL
:
294 waterfall
->buf_index
+= n
;
296 case G_IO_STATUS_AGAIN
:
297 printf("Try again later\n");
300 case G_IO_STATUS_EOF
:
301 printf("End of input\n");
304 case G_IO_STATUS_ERROR
:
305 printf("Error - %s\n", err
->message
);
313 while (waterfall
->buf_index
>= waterfall
->integration_samples
*SAMPLE_SIZE
) {
314 int i
, row
, rowstride
, channels
, modslice
;
315 int resolution
= waterfall
->integration_samples
/2 + 1;
318 /* Analyze the spectrum. */
319 printf("Integrate %d %zu %d (%d)\n", batch_size
, waterfall
->buf_index
,
320 waterfall
->slice
, waterfall
->slice
% waterfall
->n_slices
);
321 for (i
= 0; i
< waterfall
->integration_samples
; i
++) {
323 memcpy(&x
, waterfall
->buf
+ i
*SAMPLE_SIZE
, sizeof (x
));
324 waterfall
->fft
.samples
[i
] = x
;
326 fftw_execute(waterfall
->fft
.p
);
328 /* Draw a new slice. */
329 modslice
= waterfall
->slice
% waterfall
->n_slices
;
330 pixels
= gdk_pixbuf_get_pixels(waterfall
->original
);
331 rowstride
= gdk_pixbuf_get_rowstride(waterfall
->original
);
332 channels
= gdk_pixbuf_get_n_channels(waterfall
->original
);
333 for (row
= 0; row
< resolution
; row
++) {
334 double signal_i
= waterfall
->fft
.spectrum
[row
][0], signal_q
= waterfall
->fft
.spectrum
[row
][1];
335 /* XXX Give invsqrt a chance to happen. */
336 double power
= -log10(1.0 / hypot(signal_i
, signal_q
)) - normalization
;
337 double z
= (power
+ waterfall
->zoom
.noise_floor
) * waterfall
->zoom
.sensitivity
;
338 if (row
> 50 && row
< 60) {
339 printf("Power = %g (%g)\n", power
, z
);
341 /* Drawing to the original-size pixbuf, resampling happens in the expose event. */
342 pixels
[row
*rowstride
+ modslice
* channels
+ 0] = z
>= 256 ? MIN(z
- 256, 255) : 0;
343 pixels
[row
*rowstride
+ modslice
* channels
+ 1] = z
>= 256 ? MAX(511 - z
, 0) : MAX(z
, 0);
344 pixels
[row
*rowstride
+ modslice
* channels
+ 2] = z
< 256 ? MIN(255 - z
, 255) : 0;
349 memmove(waterfall
->buf
,
350 waterfall
->buf
+ waterfall
->integration_samples
*SAMPLE_SIZE
,
351 waterfall
->buf_size
- waterfall
->integration_samples
*SAMPLE_SIZE
);
352 waterfall
->buf_index
-= waterfall
->integration_samples
*SAMPLE_SIZE
;
356 /* Scroll the waterfall. */
357 split_pixbuf(waterfall
);
358 invalidate_spectrogram(waterfall
);
360 if (batch_size
> 2) {
361 printf("I can't keep up (%d)\n", batch_size
);
371 int main(int argc
, char *argv
[])
373 GtkBuilder
*gtk_builder
;
374 GtkWindow
*waterfall_window
;
375 GIOChannel
*stdin_channel
;
378 struct waterfall_context waterfall
= {
388 waterfall
.integration_samples
= SAMPLE_RATE
/10;
389 waterfall
.n_slices
= N_SLICES
;
392 int c
= getopt(argc
, argv
, "H:t:");
399 waterfall
.n_slices
= atoi(optarg
);
402 waterfall
.integration_samples
= atof(optarg
) * SAMPLE_RATE
;
405 fprintf(stderr
, "%s: '%c': Invalid option\n", argv
[0], c
);
410 waterfall
.zoom
.f_high
= waterfall
.integration_samples
/2;
411 waterfall
.zoom
.hertz_scale
= 21.050 / waterfall
.zoom
.f_high
;
413 gtk_init(&argc
, &argv
);
415 gtk_builder
= gtk_builder_new();
416 gtk_builder_add_from_file(gtk_builder
, "waterfall.glade", NULL
);
417 gtk_builder_connect_signals_full(gtk_builder
, &connect_signal
, NULL
);
419 /* Give signal handlers a means of accessing gtk_builder. */
420 g_object_set(gtk_builder_get_object(gtk_builder
,
422 "user-data", gtk_builder
,
425 waterfall_window
= GTK_WINDOW(gtk_builder_get_object(gtk_builder
, "waterfall_window"));
426 waterfall
.drawingarea
= GTK_DRAWING_AREA(gtk_builder_get_object(gtk_builder
, "image_detail_drawingarea"));
427 waterfall
.frequency_ruler
= GTK_RULER(gtk_builder_get_object(gtk_builder
, "frequency_ruler"));
428 waterfall
.time_ruler
= GTK_RULER(gtk_builder_get_object(gtk_builder
, "time_ruler"));
430 gtk_ruler_set_range(waterfall
.time_ruler
,
431 N_SLICES
* waterfall
.integration_samples
/ 44100.0,
433 N_SLICES
* waterfall
.integration_samples
/ 44100.0,
434 N_SLICES
* waterfall
.integration_samples
/ 44100.0);
436 g_signal_connect(G_OBJECT(waterfall
.drawingarea
), "expose-event",
437 G_CALLBACK(&waterfall_expose_spectrogram
), &waterfall
);
438 g_signal_connect(G_OBJECT(waterfall
.drawingarea
), "configure-event",
439 G_CALLBACK(&waterfall_configure_spectrogram
), &waterfall
);
440 g_signal_connect(G_OBJECT(waterfall_window
), "key-press-event",
441 G_CALLBACK(&waterfall_key_press
), &waterfall
);
443 /* Don't block on stdin. */
444 stdin_flags
= fcntl(0, F_GETFL
, 0);
445 fcntl(0, F_SETFL
, stdin_flags
| O_NONBLOCK
);
447 stdin_channel
= g_io_channel_unix_new(0);
448 g_io_channel_set_encoding(stdin_channel
, NULL
, &err
);
449 g_io_channel_set_buffer_size(stdin_channel
, waterfall
.integration_samples
* SAMPLE_SIZE
);
450 waterfall
.buf
= malloc(waterfall
.integration_samples
* SAMPLE_SIZE
);
451 waterfall
.buf_size
= waterfall
.integration_samples
* SAMPLE_SIZE
;
452 g_io_add_watch(stdin_channel
, G_IO_IN
, &waterfall_input
, &waterfall
);
454 waterfall
.original
= gdk_pixbuf_new(GDK_COLORSPACE_RGB
, FALSE
, 8,
455 waterfall
.n_slices
, waterfall
.integration_samples
/2 + 1);
456 split_pixbuf(&waterfall
);
458 waterfall
.fft
.samples
= fftw_malloc(sizeof (*waterfall
.fft
.samples
) * waterfall
.integration_samples
);
459 waterfall
.fft
.spectrum
= fftw_malloc(sizeof (*waterfall
.fft
.spectrum
) * (waterfall
.integration_samples
/2 + 1));
460 waterfall
.fft
.p
= fftw_plan_dft_r2c_1d(waterfall
.integration_samples
,
461 waterfall
.fft
.samples
, waterfall
.fft
.spectrum
,
464 gtk_widget_show(GTK_WIDGET(waterfall_window
));
466 invalidate_spectrogram(&waterfall
);
470 fftw_destroy_plan(waterfall
.fft
.p
);
471 fftw_free(waterfall
.fft
.spectrum
);
472 fftw_free(waterfall
.fft
.samples
);