403 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			403 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| #include <math.h>
 | |
| #include <gtk/gtk.h>
 | |
| 
 | |
| #include "variable.h"
 | |
| 
 | |
| typedef struct {
 | |
|   gdouble angle;
 | |
|   gint64 stream_time;
 | |
|   gint64 clock_time;
 | |
|   gint64 frame_counter;
 | |
| } FrameData;
 | |
| 
 | |
| static FrameData *displayed_frame;
 | |
| static GtkWidget *window;
 | |
| static GList *past_frames;
 | |
| static Variable latency_error = VARIABLE_INIT;
 | |
| static Variable time_factor_stats = VARIABLE_INIT;
 | |
| static int dropped_frames = 0;
 | |
| static int n_frames = 0;
 | |
| 
 | |
| static gboolean pll;
 | |
| static int fps = 24;
 | |
| 
 | |
| /* Thread-safe frame queue */
 | |
| 
 | |
| #define MAX_QUEUE_LENGTH 5
 | |
| 
 | |
| static GQueue *frame_queue;
 | |
| static GMutex frame_mutex;
 | |
| static GCond frame_cond;
 | |
| 
 | |
| static void
 | |
| queue_frame (FrameData *frame_data)
 | |
| {
 | |
|   g_mutex_lock (&frame_mutex);
 | |
| 
 | |
|   while (frame_queue->length == MAX_QUEUE_LENGTH)
 | |
|     g_cond_wait (&frame_cond, &frame_mutex);
 | |
| 
 | |
|   g_queue_push_tail (frame_queue, frame_data);
 | |
| 
 | |
|   g_mutex_unlock (&frame_mutex);
 | |
| }
 | |
| 
 | |
| static FrameData *
 | |
| unqueue_frame (void)
 | |
| {
 | |
|   FrameData *frame_data;
 | |
| 
 | |
|   g_mutex_lock (&frame_mutex);
 | |
| 
 | |
|   if (frame_queue->length > 0)
 | |
|     {
 | |
|       frame_data = g_queue_pop_head (frame_queue);
 | |
|       g_cond_signal (&frame_cond);
 | |
|     }
 | |
|   else
 | |
|     {
 | |
|       frame_data = NULL;
 | |
|     }
 | |
| 
 | |
|   g_mutex_unlock (&frame_mutex);
 | |
| 
 | |
|   return frame_data;
 | |
| }
 | |
| 
 | |
| static FrameData *
 | |
| peek_pending_frame (void)
 | |
| {
 | |
|   FrameData *frame_data;
 | |
| 
 | |
|   g_mutex_lock (&frame_mutex);
 | |
| 
 | |
|   if (frame_queue->head)
 | |
|     frame_data = frame_queue->head->data;
 | |
|   else
 | |
|     frame_data = NULL;
 | |
| 
 | |
|   g_mutex_unlock (&frame_mutex);
 | |
| 
 | |
|   return frame_data;
 | |
| }
 | |
| 
 | |
| static FrameData *
 | |
| peek_next_frame (void)
 | |
| {
 | |
|   FrameData *frame_data;
 | |
| 
 | |
|   g_mutex_lock (&frame_mutex);
 | |
| 
 | |
|   if (frame_queue->head && frame_queue->head->next)
 | |
|     frame_data = frame_queue->head->next->data;
 | |
|   else
 | |
|     frame_data = NULL;
 | |
| 
 | |
|   g_mutex_unlock (&frame_mutex);
 | |
| 
 | |
|   return frame_data;
 | |
| }
 | |
| 
 | |
| /* Frame producer thread */
 | |
| 
 | |
| static gpointer
 | |
| create_frames_thread (gpointer data)
 | |
| {
 | |
|   int frame_count = 0;
 | |
| 
 | |
|   while (TRUE)
 | |
|     {
 | |
|       FrameData *frame_data = g_slice_new0 (FrameData);
 | |
|       frame_data->angle = 2 * M_PI * (frame_count % fps) / (double)fps;
 | |
|       frame_data->stream_time = (G_GINT64_CONSTANT (1000000) * frame_count) / fps;
 | |
| 
 | |
|       queue_frame (frame_data);
 | |
|       frame_count++;
 | |
|     }
 | |
| 
 | |
|   return NULL;
 | |
| }
 | |
| 
 | |
| /* Clock management:
 | |
|  *
 | |
|  * The logic here, which is activated by the --pll argument
 | |
|  * demonstrates adjusting the playback rate so that the frames exactly match
 | |
|  * when they are displayed both frequency and phase. If there was an
 | |
|  * accompanying audio track, you would need to resample the audio to match
 | |
|  * the clock.
 | |
|  *
 | |
|  * The algorithm isn't exactly a PLL - I wrote it first that way, but
 | |
|  * it oscillicated before coming into sync and this approach was easier than
 | |
|  * fine-tuning the PLL filter.
 | |
|  *
 | |
|  * A more complicated algorithm could also establish sync when the playback
 | |
|  * rate isn't exactly an integral divisor of the VBlank rate, such as 24fps
 | |
|  * video on a 60fps display.
 | |
|  */
 | |
| #define PRE_BUFFER_TIME 500000
 | |
| 
 | |
| static gint64 stream_time_base;
 | |
| static gint64 clock_time_base;
 | |
| static double time_factor = 1.0;
 | |
| static double frequency_time_factor = 1.0;
 | |
| static double phase_time_factor = 1.0;
 | |
| 
 | |
| static gint64
 | |
| stream_time_to_clock_time (gint64 stream_time)
 | |
| {
 | |
|   return clock_time_base + (stream_time - stream_time_base) * time_factor;
 | |
| }
 | |
| 
 | |
| static void
 | |
| adjust_clock_for_phase (gint64 frame_clock_time,
 | |
|                         gint64 presentation_time)
 | |
| {
 | |
|   static gint count = 0;
 | |
|   static gint64 previous_frame_clock_time;
 | |
|   static gint64 previous_presentation_time;
 | |
|   gint64 phase = presentation_time - frame_clock_time;
 | |
| 
 | |
|   count++;
 | |
|   if (count >= fps) /* Give a second of warmup */
 | |
|     {
 | |
|       gint64 time_delta = frame_clock_time - previous_frame_clock_time;
 | |
|       gint64 previous_phase = previous_presentation_time - previous_frame_clock_time;
 | |
| 
 | |
|       double expected_phase_delta;
 | |
| 
 | |
|       stream_time_base += (frame_clock_time - clock_time_base) / time_factor;
 | |
|       clock_time_base = frame_clock_time;
 | |
| 
 | |
|       expected_phase_delta = time_delta * (1 - phase_time_factor);
 | |
| 
 | |
|       /* If the phase is increasing that means the computed clock times are
 | |
|        * increasing too slowly. We increase the frequency time factor to compensate,
 | |
|        * but decrease the compensation so that it takes effect over 1 second to
 | |
|        * avoid jitter */
 | |
|       frequency_time_factor += (phase - previous_phase - expected_phase_delta) / (double)time_delta / fps;
 | |
| 
 | |
|       /* We also want to increase or decrease the frequency to bring the phase
 | |
|        * into sync. We do that again so that the phase should sync up over 1 seconds
 | |
|        */
 | |
|       phase_time_factor = 1 + phase / 2000000.;
 | |
| 
 | |
|       time_factor = frequency_time_factor * phase_time_factor;
 | |
|     }
 | |
| 
 | |
|   previous_frame_clock_time = frame_clock_time;
 | |
|   previous_presentation_time = presentation_time;
 | |
| }
 | |
| 
 | |
| /* Drawing */
 | |
| 
 | |
| static gboolean
 | |
| on_window_draw (GtkWidget *widget,
 | |
|                 cairo_t   *cr)
 | |
| {
 | |
|   GdkRectangle allocation;
 | |
|   double cx, cy, r;
 | |
| 
 | |
|   cairo_set_source_rgb (cr, 1., 1., 1.);
 | |
|   cairo_paint (cr);
 | |
| 
 | |
|   cairo_set_source_rgb (cr, 0., 0., 0.);
 | |
|   gtk_widget_get_allocation (widget, &allocation);
 | |
| 
 | |
|   cx = allocation.width / 2.;
 | |
|   cy = allocation.height / 2.;
 | |
|   r = MIN (allocation.width, allocation.height) / 2.;
 | |
| 
 | |
|   cairo_arc (cr, cx, cy, r,
 | |
|              0, 2 * M_PI);
 | |
|   cairo_stroke (cr);
 | |
|   if (displayed_frame)
 | |
|     {
 | |
|       cairo_move_to (cr, cx, cy);
 | |
|       cairo_line_to (cr,
 | |
|                      cx + r * cos(displayed_frame->angle - M_PI / 2),
 | |
|                      cy + r * sin(displayed_frame->angle - M_PI / 2));
 | |
|       cairo_stroke (cr);
 | |
| 
 | |
|       if (displayed_frame->frame_counter == 0)
 | |
|         {
 | |
|           GdkFrameClock *frame_clock = gtk_widget_get_frame_clock (window);
 | |
|           displayed_frame->frame_counter = gdk_frame_clock_get_frame_counter (frame_clock);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|   return FALSE;
 | |
| }
 | |
| 
 | |
| static void
 | |
| collect_old_frames (void)
 | |
| {
 | |
|   GdkFrameClock *frame_clock = gtk_widget_get_frame_clock (window);
 | |
|   GList *l, *l_next;
 | |
| 
 | |
|   for (l = past_frames; l; l = l_next)
 | |
|     {
 | |
|       FrameData *frame_data = l->data;
 | |
|       GdkFrameTimings *timings;
 | |
|       gboolean remove = FALSE;
 | |
|       l_next = l->next;
 | |
| 
 | |
|       timings = gdk_frame_clock_get_timings (frame_clock,
 | |
|                                              frame_data->frame_counter);
 | |
|       if (timings == NULL)
 | |
|         {
 | |
|           remove = TRUE;
 | |
|         }
 | |
|       else if (gdk_frame_timings_get_complete (timings))
 | |
|         {
 | |
|           gint64 presentation_time = gdk_frame_timings_get_predicted_presentation_time (timings);
 | |
|           gint64 refresh_interval = gdk_frame_timings_get_refresh_interval (timings);
 | |
| 
 | |
|           if (pll &&
 | |
|               presentation_time && refresh_interval &&
 | |
|               presentation_time > frame_data->clock_time - refresh_interval / 2 &&
 | |
|               presentation_time < frame_data->clock_time + refresh_interval / 2)
 | |
|             adjust_clock_for_phase (frame_data->clock_time, presentation_time);
 | |
| 
 | |
|           if (presentation_time)
 | |
|             variable_add (&latency_error,
 | |
|                           presentation_time - frame_data->clock_time);
 | |
| 
 | |
|           remove = TRUE;
 | |
|         }
 | |
| 
 | |
|       if (remove)
 | |
|         {
 | |
|           past_frames = g_list_delete_link (past_frames, l);
 | |
|           g_slice_free (FrameData, frame_data);
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| static void
 | |
| print_statistics (void)
 | |
| {
 | |
|   gint64 now = g_get_monotonic_time ();
 | |
|   static gint64 last_print_time = 0;
 | |
| 
 | |
|   if (last_print_time == 0)
 | |
|     last_print_time = now;
 | |
|   else if (now -last_print_time > 5000000)
 | |
|     {
 | |
|       g_print ("dropped_frames: %d/%d\n",
 | |
|                dropped_frames, n_frames);
 | |
|       g_print ("collected_frames: %g/%d\n",
 | |
|                latency_error.weight, n_frames);
 | |
|       g_print ("latency_error: %g +/- %g\n",
 | |
|                variable_mean (&latency_error),
 | |
|                variable_standard_deviation (&latency_error));
 | |
|       if (pll)
 | |
|         g_print ("playback rate adjustment: %g +/- %g %%\n",
 | |
|                  (variable_mean (&time_factor_stats) - 1) * 100,
 | |
|                  variable_standard_deviation (&time_factor_stats) * 100);
 | |
|       variable_init (&latency_error);
 | |
|       variable_init (&time_factor_stats);
 | |
|       dropped_frames = 0;
 | |
|       n_frames = 0;
 | |
|       last_print_time = now;
 | |
|     }
 | |
| }
 | |
| 
 | |
| static void
 | |
| on_update (GdkFrameClock *frame_clock,
 | |
|            gpointer       data)
 | |
| {
 | |
|   GdkFrameTimings *timings = gdk_frame_clock_get_current_timings (frame_clock);
 | |
|   gint64 frame_time = gdk_frame_timings_get_frame_time (timings);
 | |
|   gint64 predicted_presentation_time = gdk_frame_timings_get_predicted_presentation_time (timings);
 | |
|   gint64 refresh_interval;
 | |
|   FrameData *pending_frame;
 | |
| 
 | |
|   if (clock_time_base == 0)
 | |
|     clock_time_base = frame_time + PRE_BUFFER_TIME;
 | |
| 
 | |
|   gdk_frame_clock_get_refresh_info (frame_clock, frame_time,
 | |
|                                     &refresh_interval, NULL);
 | |
| 
 | |
|   pending_frame = peek_pending_frame ();
 | |
|   if (stream_time_to_clock_time (pending_frame->stream_time)
 | |
|       < predicted_presentation_time + refresh_interval / 2)
 | |
|     {
 | |
|       while (TRUE)
 | |
|         {
 | |
|           FrameData *next_frame = peek_next_frame ();
 | |
|           if (next_frame &&
 | |
|               stream_time_to_clock_time (next_frame->stream_time)
 | |
|               < predicted_presentation_time + refresh_interval / 2)
 | |
|             {
 | |
|               g_slice_free (FrameData, unqueue_frame ());
 | |
|               n_frames++;
 | |
|               dropped_frames++;
 | |
|               pending_frame = next_frame;
 | |
|             }
 | |
|           else
 | |
|             break;
 | |
|         }
 | |
| 
 | |
|       if (displayed_frame)
 | |
|         past_frames = g_list_prepend (past_frames, displayed_frame);
 | |
| 
 | |
|       n_frames++;
 | |
|       displayed_frame = unqueue_frame ();
 | |
|       displayed_frame->clock_time = stream_time_to_clock_time (displayed_frame->stream_time);
 | |
| 
 | |
|       displayed_frame->frame_counter = gdk_frame_timings_get_frame_counter (timings);
 | |
|       variable_add (&time_factor_stats, time_factor);
 | |
| 
 | |
|       collect_old_frames ();
 | |
|       print_statistics ();
 | |
| 
 | |
|       gtk_widget_queue_draw (window);
 | |
|     }
 | |
| }
 | |
| 
 | |
| static GOptionEntry options[] = {
 | |
|   { "pll", 'p', 0, G_OPTION_ARG_NONE, &pll, "Sync frame rate to refresh", NULL },
 | |
|   { "fps", 'f', 0, G_OPTION_ARG_INT, &fps, "Frame rate", "FPS" },
 | |
|   { NULL }
 | |
| };
 | |
| 
 | |
| int
 | |
| main(int argc, char **argv)
 | |
| {
 | |
|   GError *error = NULL;
 | |
|   GdkFrameClock *frame_clock;
 | |
| 
 | |
|   if (!gtk_init_with_args (&argc, &argv, "",
 | |
|                            options, NULL, &error))
 | |
|     {
 | |
|       g_printerr ("Option parsing failed: %s\n", error->message);
 | |
|       return 1;
 | |
|     }
 | |
| 
 | |
|   window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
 | |
|   gtk_widget_set_app_paintable (window, TRUE);
 | |
|   gtk_window_set_default_size (GTK_WINDOW (window), 300, 300);
 | |
| 
 | |
|   g_signal_connect (window, "draw",
 | |
|                     G_CALLBACK (on_window_draw), NULL);
 | |
|   g_signal_connect (window, "destroy",
 | |
|                     G_CALLBACK (gtk_main_quit), NULL);
 | |
| 
 | |
|   gtk_widget_show (window);
 | |
| 
 | |
|   frame_queue = g_queue_new ();
 | |
|   g_mutex_init (&frame_mutex);
 | |
|   g_cond_init (&frame_cond);
 | |
| 
 | |
|   g_thread_new ("Create Frames", create_frames_thread, NULL);
 | |
| 
 | |
|   frame_clock = gtk_widget_get_frame_clock (window);
 | |
|   g_signal_connect (frame_clock, "update",
 | |
|                     G_CALLBACK (on_update), NULL);
 | |
|   gdk_frame_clock_begin_updating (frame_clock);
 | |
| 
 | |
|   gtk_main ();
 | |
| 
 | |
|   return 0;
 | |
| }
 | 
