
(Updated Code : 15-06-2026 to remove the sync every 60 seconds, after running if for 2 days, noticed drifting in time, made other frame sync modifications as well)
I have a Leitch Studio Wall Clock and really wanted to get it working properly and usefully.
So with the help of Gemini – I coded up this little bit to get SMPTE timecode generated from the sound card, the code syncs every minute to the PC time to keep the generator in sync, and with the PTP Grandmaster clock locking an NTP server to nanosecond accuracy at home, I have pretty good time keeping on that NTP server for this clock.
Also now, with the update code, it will display the date (DD MM YY 00) in the Userbits if you can decode them.
This clock does not take PTP but can take 1PPS inpulses or SMTP time code.
This is a bit of a Gemini hack to create the timecode – it jams the generator ever 60 seconds, and if you put the timezone in correctly, should take into account Daylight Savings and so forth.
When running correctly it should display this on the terminal
[LTC TRANSMISSION LOCK] 10:43:59:12 | USER BITS: UB=00260625 | MONITORED WALL: 10:43:59.003
Package Requirements : gcc libasound2-dev libltc-dev
Debian / Ubuntu : sudo apt update sudo apt install gcc libasound2-dev libltc-dev
File : /etc/ltc_master.conf
# /etc/ltc_master.conf
# Set your local operating system timezone identifier
timezone = Pacific/Auckland
# ALSA audio hardware address to stream audio out of ("hw:0,0" or "null")
audio_device = hw:0,0
File : ltc_master.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
#include <sys/ioctl.h>
#include <syslog.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
#include <alsa/asoundlib.h>
#include <ltc.h>
#define SAMPLE_RATE 48000
#define FPS 25
#define CONFIG_PATH "/etc/ltc_master.conf"
#define TARGET_FRAME_NS 40000000LL
#define JITTER_THRESHOLD_NS 45000000LL
static char tz_setting[128] = "Pacific/Auckland";
static char device_name[128] = "hw:0,0";
static int null_device_mode = 0;
// Mutex-protected cross-thread state registers
static pthread_mutex_t state_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t ui_cond = PTHREAD_COND_INITIALIZER; // Condition variable to kick UI thread immediately
static int sync_hours = 0;
static int sync_minutes = 0;
static int sync_seconds = 0;
static int sync_day = 1;
static int sync_month = 1;
static int sync_year = 2026;
static int clock_needs_sync = 1;
static char active_os_time[32] = "00:00:00.000";
// Separate UI registers to isolate output string formatting overhead
static int ui_hours = 0;
static int ui_minutes = 0;
static int ui_seconds = 0;
static int ui_frame = 0;
static uint32_t ui_user_bits = 0;
static int ui_updated = 0;
static void load_config_file() {
FILE *fp = fopen(CONFIG_PATH, "r");
if (!fp) return;
char line[256];
while (fgets(line, sizeof(line), fp)) {
line[strcspn(line, "\r\n")] = 0;
if (line[0] == '#' || line[0] == '\0' || line[0] == ' ') continue;
char key[128], val[128];
if (sscanf(line, "%127[^= ] = %127s", key, val) == 2) {
if (strcmp(key, "timezone") == 0) {
strncpy(tz_setting, val, sizeof(tz_setting) - 1);
} else if (strcmp(key, "audio_device") == 0) {
strncpy(device_name, val, sizeof(device_name) - 1);
if (strcasecmp(device_name, "null") == 0) {
null_device_mode = 1;
}
}
}
}
fclose(fp);
setenv("TZ", tz_setting, 1);
tzset();
}
static int configure_alsa_hardware(snd_pcm_t *pcm) {
snd_pcm_hw_params_t *hw_params;
snd_pcm_hw_params_alloca(&hw_params);
if (snd_pcm_hw_params_any(pcm, hw_params) < 0) return -1;
if (snd_pcm_hw_params_set_access(pcm, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED) < 0) return -1;
if (snd_pcm_hw_params_set_format(pcm, hw_params, SND_PCM_FORMAT_S16_LE) < 0) return -1;
if (snd_pcm_hw_params_set_channels(pcm, hw_params, 2) < 0) return -1;
unsigned int rate = SAMPLE_RATE;
if (snd_pcm_hw_params_set_rate_near(pcm, hw_params, &rate, 0) < 0) return -1;
unsigned int period_time = 40000;
int dir = 0;
if (snd_pcm_hw_params_set_period_time_near(pcm, hw_params, &period_time, &dir) < 0) return -1;
unsigned int periods = 2;
if (snd_pcm_hw_params_set_periods_near(pcm, hw_params, &periods, &dir) < 0) return -1;
if (snd_pcm_hw_params(pcm, hw_params) < 0) return -1;
return 0;
}
/* -------------------------------------------------------------------------
THREAD 1: ASYNCHRONOUS HOUSEKEEPING & SYNC ENGINE
------------------------------------------------------------------------- */
void *sync_thread_worker(void *arg) {
(void)arg;
struct timespec ts;
struct tm tm_os;
int last_sec = -1;
while (1) {
clock_gettime(CLOCK_REALTIME, &ts);
localtime_r(&ts.tv_sec, &tm_os);
if (tm_os.tm_sec != last_sec) {
pthread_mutex_lock(&state_mutex);
sync_hours = tm_os.tm_hour;
sync_minutes = tm_os.tm_min;
sync_seconds = tm_os.tm_sec;
sync_day = tm_os.tm_mday;
sync_month = tm_os.tm_mon + 1;
sync_year = 1900 + tm_os.tm_year;
clock_needs_sync = 1;
snprintf(active_os_time, sizeof(active_os_time), "%02d:%02d:%02d.%03ld",
tm_os.tm_hour, tm_os.tm_min, tm_os.tm_sec, ts.tv_nsec / 1000000L);
pthread_mutex_unlock(&state_mutex);
last_sec = tm_os.tm_sec;
}
usleep(5000); // Polling faster (5ms) to catch the second boundary with less jitter
}
return NULL;
}
/* -------------------------------------------------------------------------
THREAD 2: HARDWARE-PACED REAL-TIME AUDIO GENERATOR
------------------------------------------------------------------------- */
void *audio_thread_worker(void *arg) {
(void)arg;
snd_pcm_t *pcm = NULL;
snd_pcm_status_t *status = NULL;
if (!null_device_mode) {
if (snd_pcm_open(&pcm, device_name, SND_PCM_STREAM_PLAYBACK, 0) < 0) {
fprintf(stderr, "CRITICAL: Audio engine device open failed.\n");
exit(1);
}
if (configure_alsa_hardware(pcm) < 0) {
fprintf(stderr, "CRITICAL: Audio engine hardware setup failed.\n");
exit(1);
}
snd_pcm_status_alloca(&status);
}
LTCEncoder *enc = ltc_encoder_create(SAMPLE_RATE, FPS, LTC_TV_625_50, 0);
ltc_encoder_set_filter(enc, 0.0);
size_t ltc_max_alloc_size = ltc_encoder_get_buffersize(enc) + 16;
ltcsnd_sample_t *ltc_buf = malloc(ltc_max_alloc_size * sizeof(ltcsnd_sample_t));
int16_t *audio = malloc(ltc_max_alloc_size * 2 * sizeof(int16_t));
SMPTETimecode rolling_tc;
struct timespec ts_loop_start, ts_loop_end;
unsigned long local_executed_frames = 0;
while (1) {
pthread_mutex_lock(&state_mutex);
if (sync_year >= 2026) {
rolling_tc.hours = sync_hours;
rolling_tc.mins = sync_minutes;
rolling_tc.secs = sync_seconds;
rolling_tc.frame = 0;
rolling_tc.days = sync_day;
rolling_tc.months = sync_month;
rolling_tc.years = sync_year % 100;
clock_needs_sync = 0;
pthread_mutex_unlock(&state_mutex);
break;
}
pthread_mutex_unlock(&state_mutex);
usleep(5000);
}
clock_gettime(CLOCK_MONOTONIC_RAW, &ts_loop_start);
while (1) {
// Check for absolute clock updates from Thread 1
pthread_mutex_lock(&state_mutex);
if (clock_needs_sync) {
// REMOVED 1-FRAME LATENCY COMPENSATOR: Sync precisely to the current wall-clock second frame boundary
rolling_tc.hours = sync_hours;
rolling_tc.mins = sync_minutes;
rolling_tc.secs = sync_seconds;
rolling_tc.frame = 0;
rolling_tc.days = sync_day;
rolling_tc.months = sync_month;
rolling_tc.years = sync_year % 100;
clock_needs_sync = 0;
}
uint8_t bcd_year = ((rolling_tc.years / 10) << 4) | (rolling_tc.years % 10);
uint8_t bcd_month = ((rolling_tc.months / 10) << 4) | (rolling_tc.months % 10);
uint8_t bcd_day = ((rolling_tc.days / 10) << 4) | (rolling_tc.days % 10);
uint8_t bcd_tz = 0x00;
ui_user_bits = ((uint32_t)bcd_tz << 24) | ((uint32_t)bcd_year << 16) |
((uint32_t)bcd_month << 8) | (uint32_t)bcd_day;
ui_hours = rolling_tc.hours;
ui_minutes = rolling_tc.mins;
ui_seconds = rolling_tc.secs;
ui_frame = rolling_tc.frame;
ui_updated = 1;
// Signal the UI thread immediately to run without scheduling delay
pthread_cond_signal(&ui_cond);
pthread_mutex_unlock(&state_mutex);
ltc_encoder_set_timecode(enc, &rolling_tc);
ltc_encoder_set_user_bits(enc, ui_user_bits);
ltc_encoder_encode_frame(enc);
int n = ltc_encoder_copy_buffer(enc, ltc_buf);
if (!null_device_mode && pcm) {
for (int i = 0; i < n; i++) {
int16_t sample = ((int16_t)ltc_buf[i] - 128) << 8;
audio[i * 2] = sample;
audio[i * 2 + 1] = (int16_t)(-sample);
}
snd_pcm_sframes_t written = snd_pcm_writei(pcm, audio, n);
if (written < 0) {
syslog(LOG_CRIT, "[ALSA CRITICAL XRUN] Error: %s at %02d:%02d:%02d:%02d",
snd_strerror(written), rolling_tc.hours, rolling_tc.mins, rolling_tc.secs, rolling_tc.frame);
snd_pcm_prepare(pcm);
} else if (written != n) {
syslog(LOG_WARNING, "[ALSA PARTIAL WRITE] Warning at %02d:%02d:%02d:%02d",
rolling_tc.hours, rolling_tc.mins, rolling_tc.secs, rolling_tc.frame);
snd_pcm_prepare(pcm);
}
} else {
usleep(40000);
}
clock_gettime(CLOCK_MONOTONIC_RAW, &ts_loop_end);
long long elapsed_ns = (ts_loop_end.tv_sec - ts_loop_start.tv_sec) * 1000000000LL +
(ts_loop_end.tv_nsec - ts_loop_start.tv_nsec);
local_executed_frames++;
if (elapsed_ns > JITTER_THRESHOLD_NS && local_executed_frames > 100) {
double actual_ms = (double)elapsed_ns / 1000000.0;
syslog(LOG_NOTICE, "[CADENCE JITTER WARNING] Frame Pace Dropped to %.2f ms at %02d:%02d:%02d:%02d (OS Wall: %02d:%02d:%02d)",
actual_ms, rolling_tc.hours, rolling_tc.mins, rolling_tc.secs, rolling_tc.frame, sync_hours, sync_minutes, sync_seconds);
}
ts_loop_start = ts_loop_end;
// Smooth frame stepping pipeline
rolling_tc.frame++;
if (rolling_tc.frame >= FPS) {
rolling_tc.frame = 0;
rolling_tc.secs++;
if (rolling_tc.secs >= 60) {
rolling_tc.secs = 0;
rolling_tc.mins++;
if (rolling_tc.mins >= 60) {
rolling_tc.mins = 0;
rolling_tc.hours++;
if (rolling_tc.hours >= 24) {
rolling_tc.hours = 0;
}
}
}
}
}
free(ltc_buf); free(audio);
ltc_encoder_free(enc);
if (pcm) snd_pcm_close(pcm);
return NULL;
}
/* -------------------------------------------------------------------------
THREAD 3: LOW-PRIORITY REAL-TIME CONSOLE UI DISPLAY
------------------------------------------------------------------------- */
void *ui_thread_worker(void *arg) {
(void)arg;
printf("\033[2J\033[H");
fflush(stdout);
while (1) {
pthread_mutex_lock(&state_mutex);
// Block until the audio thread signals that a new frame has been processed
while (!ui_updated) {
pthread_cond_wait(&ui_cond, &state_mutex);
}
printf("\r[LTC TRANSMISSION LOCK] %02d:%02d:%02d:%02d | USER BITS: UB=%08X | MONITORED WALL: %s\033[K",
ui_hours, ui_minutes, ui_seconds, ui_frame, ui_user_bits, active_os_time);
fflush(stdout);
ui_updated = 0;
pthread_mutex_unlock(&state_mutex);
}
return NULL;
}
/* -------------------------------------------------------------------------
MAIN CONFIGURATION & ENGINE INITIALIZATION
------------------------------------------------------------------------- */
int main(void) {
openlog("ltc_master", LOG_PID | LOG_CONS, LOG_DAEMON);
struct sched_param sched_reg;
sched_reg.sched_priority = 99;
if (sched_setscheduler(0, SCHED_FIFO, &sched_reg) < 0) {
syslog(LOG_ERR, "WARNING: Native SCHED_FIFO elevation failed. (Run binary as root)");
} else {
syslog(LOG_INFO, "SUCCESS: Hard-Realtime SCHED_FIFO 99 matrix lock applied to process lane.");
}
load_config_file();
printf("Initializing Dual-Thread Dedicated Timecode Matrix Engine...\n");
fflush(stdout);
pthread_t sync_thread, audio_thread, ui_thread;
pthread_create(&sync_thread, NULL, sync_thread_worker, NULL);
pthread_create(&audio_thread, NULL, audio_thread_worker, NULL);
pthread_create(&ui_thread, NULL, ui_thread_worker, NULL);
pthread_join(sync_thread, NULL);
pthread_join(audio_thread, NULL);
pthread_join(ui_thread, NULL);
closelog();
return 0;
}
To Compile execute : gcc ltc_master.c -o ltc_master -lasound -lltc
To Execute : ./ltc_master
