
(Updated Code : 10-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

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 <unistd.h>
#include <alsa/asoundlib.h>
#include <ltc.h>
#define SAMPLE_RATE 48000
#define FPS 25
#define CONFIG_PATH "/etc/ltc_master.conf"
static char tz_setting[128] = "Pacific/Auckland";
static char device_name[128] = "hw:0,0";
static int null_device_mode = 0;
static int hours = 0;
static int minutes = 0;
static int seconds = 0;
static int frame = 0;
static int ui_day = 1;
static int ui_month = 1;
static int ui_year = 2026;
// Shared buffers for raw OS diagnostics
static char raw_os_time[32] = "00:00:00.000";
/* -------------------------------------------------------
AUTHENTIC BROADCAST MATRIX FONT MAP
-------------------------------------------------------- */
static const uint8_t font[11][7][5] = {
{ {0,1,1,1,0}, {1,0,0,1,1}, {1,0,1,0,1}, {1,1,0,0,1}, {1,0,0,0,1}, {1,0,0,0,1}, {0,1,1,1,0} }, // 0
{ {0,0,1,0,0}, {0,1,1,0,0}, {0,0,1,0,0}, {0,0,1,0,0}, {0,0,1,0,0}, {0,0,1,0,0}, {1,1,1,1,1} }, // 1
{ {0,1,1,1,0}, {1,0,0,0,1}, {0,0,0,0,1}, {0,0,1,1,0}, {0,1,0,0,0}, {1,0,0,0,0}, {1,1,1,1,1} }, // 2
{ {1,1,1,1,0}, {0,0,0,0,1}, {0,0,0,0,1}, {0,1,1,1,0}, {0,0,0,0,1}, {0,0,0,0,1}, {1,1,1,1,0} }, // 3
{ {0,0,1,1,0}, {0,1,0,1,0}, {1,0,0,1,0}, {1,1,1,1,1}, {0,0,0,1,0}, {0,0,0,1,0}, {0,0,0,1,0} }, // 4
{ {1,1,1,1,1}, {1,0,0,0,0}, {1,0,0,0,0}, {1,1,1,1,0}, {0,0,0,0,1}, {0,0,0,0,1}, {1,1,1,1,0} }, // 5
{ {0,1,1,1,0}, {1,0,0,0,0}, {1,0,0,0,0}, {1,1,1,1,0}, {1,0,0,0,1}, {1,0,0,0,1}, {0,1,1,1,0} }, // 6
{ {1,1,1,1,1}, {0,0,0,0,1}, {0,0,0,1,0}, {0,0,1,0,0}, {0,0,1,0,0}, {0,0,1,0,0}, {0,0,1,0,0} }, // 7
{ {0,1,1,1,0}, {1,0,0,0,1}, {1,0,0,0,1}, {0,1,1,1,0}, {1,0,0,0,1}, {1,0,0,0,1}, {0,1,1,1,0} }, // 8
{ {0,1,1,1,0}, {1,0,0,0,1}, {1,0,0,0,1}, {0,1,1,1,1}, {0,0,0,0,1}, {0,0,0,0,1}, {0,1,1,1,0} }, // 9
{ {0,0,0,0,0}, {0,0,1,0,0}, {0,0,0,0,0}, {0,0,0,0,0}, {0,0,0,0,0}, {0,0,1,0,0}, {0,0,0,0,0} } // :
};
static void load_config_file() {
FILE *fp = fopen(CONFIG_PATH, "r");
if (!fp) {
printf("Notice: Missing %s. Defaulting to Pacific/Auckland on hw:0,0\n", CONFIG_PATH);
sleep(2);
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 void draw_ui() {
struct winsize w;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
int term_cols = (w.ws_col > 0) ? w.ws_col : 80;
int term_rows = (w.ws_row > 0) ? w.ws_row : 24;
int digits[11] = {
hours / 10, hours % 10, 10,
minutes / 10, minutes % 10, 10,
seconds / 10, seconds % 10, 10,
frame / 10, frame % 10
};
int total_font_width = 75;
int horizontal_padding = (term_cols - total_font_width) / 2;
if (horizontal_padding < 0) horizontal_padding = 0;
printf("\033[H");
int top_padding = (term_rows - 20) / 2;
if (top_padding < 0) top_padding = 0;
for (int i = 0; i < top_padding; i++) printf("\n");
printf("%*sSMPTE LTC STUDIO CLOCK GENERATOR\n", horizontal_padding, "");
printf("%*s", horizontal_padding, "");
for (int i = 0; i < total_font_width; i++) printf("-");
printf("\n\n");
for (int row = 0; row < 7; row++) {
for (int v_stroke = 0; v_stroke < 2; v_stroke++) {
printf("%*s", horizontal_padding, "");
for (int d = 0; d < 11; d++) {
int digit = digits[d];
for (int col = 0; col < 5; col++) {
if (font[digit][row][col] == 1) {
printf("\033[1;32m•\033[0m");
} else {
printf(" ");
}
}
printf(" ");
}
printf("\n");
}
}
printf("\n%*s", horizontal_padding, "");
for (int i = 0; i < total_font_width; i++) printf("-");
printf("\n\n");
int structural_lines_used = top_padding + 20;
int bottom_padding = term_rows - structural_lines_used - 1;
for (int i = 0; i < bottom_padding; i++) printf("\n");
printf("\033[7m");
char status_text[256];
snprintf(status_text, sizeof(status_text),
" MASTER LTC | Out: %s | Mode: %dfps EBU | Payload BCD: %02d%02d%02d00 | OS WALL CLOCK: %s ",
null_device_mode ? "SIMULATED" : device_name, FPS, ui_day, ui_month, ui_year % 100, raw_os_time);
printf("%s", status_text);
int characters_left = term_cols - (int)strlen(status_text);
for (int i = 0; i < characters_left; i++) printf(" ");
printf("\033[0m");
fflush(stdout);
}
int main() {
load_config_file();
printf("\033[2J");
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, "ALSA target device '%s' open failed.\n", device_name);
return 1;
}
if (snd_pcm_set_params(pcm, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED, 2, SAMPLE_RATE, 1, 20000) < 0) {
fprintf(stderr, "ALSA configuration failed\n");
if (pcm) snd_pcm_close(pcm);
return 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_size = ltc_encoder_get_buffersize(enc);
ltcsnd_sample_t *ltc_buf = malloc(ltc_size * sizeof(ltcsnd_sample_t));
int16_t *audio = malloc(ltc_size * 2 * sizeof(int16_t));
if (!ltc_buf || !audio) {
fprintf(stderr, "Buffer allocation failed\n");
if (pcm) snd_pcm_close(pcm);
return 1;
}
struct timespec ts_trigger;
struct timespec ts_hardware;
struct tm tm_playout;
struct tm tm_raw_os;
SMPTETimecode tc;
int last_sec = -1;
int last_frame = -1;
const long frame_duration_ns = 1000000000L / FPS; // 40,000,000 ns per frame
while (1) {
// 1. Capture Raw Time Execution Anchor
clock_gettime(CLOCK_REALTIME, &ts_trigger);
localtime_r(&ts_trigger.tv_sec, &tm_raw_os);
snprintf(raw_os_time, sizeof(raw_os_time), "%02d:%02d:%02d.%03ld",
tm_raw_os.tm_hour, tm_raw_os.tm_min, tm_raw_os.tm_sec, ts_trigger.tv_nsec / 1000000L);
snd_pcm_sframes_t delay_frames = 0;
ts_hardware = ts_trigger; // Fallback alignment target
// 2. High-Grade Hardware Status Query
if (!null_device_mode && pcm) {
if (snd_pcm_status(pcm, status) == 0) {
delay_frames = snd_pcm_status_get_delay(status);
snd_pcm_status_get_htstamp(status, &ts_hardware);
// Fallback protection against raw uninitialized hardware clock frames
if (ts_hardware.tv_sec == 0) {
ts_hardware = ts_trigger;
}
// Defend timeline metrics against negative values or ring buffer overflows
if (delay_frames < 0) {
delay_frames = 0;
}
}
}
// 3. Compensation Correction (+n Frame Shift)
// Add the buffer length we are ABOUT to write to calculate the actual output time of this packet.
long long projected_delay_frames = (long long)delay_frames + (long long)ltc_size;
long long delay_ns = (projected_delay_frames * 1000000000LL) / SAMPLE_RATE;
// Unified Playout Target calculated relative to hardware-synchronized timestamp
long long total_ns = ts_hardware.tv_nsec + delay_ns;
time_t playout_sec = ts_hardware.tv_sec + (total_ns / 1000000000LL);
long playout_nsec = (long)(total_ns % 1000000000LL);
localtime_r(&playout_sec, &tm_playout);
int target_frame = (int)(playout_nsec / frame_duration_ns);
if (target_frame >= FPS) target_frame = FPS - 1;
// 4. Edge Detector Checking Soundcard Buffer States
if (tm_playout.tm_sec == last_sec && target_frame == last_frame) {
long ns_into_current_frame = playout_nsec % frame_duration_ns;
long ns_remaining = frame_duration_ns - ns_into_current_frame;
useconds_t sleep_us = (useconds_t)(ns_remaining / 1000L);
if (sleep_us > 10000) sleep_us = 10000;
if (sleep_us < 500) sleep_us = 500;
usleep(sleep_us);
continue;
}
last_sec = tm_playout.tm_sec;
last_frame = target_frame;
// 5. Update Structural Global Variables
hours = tm_playout.tm_hour;
minutes = tm_playout.tm_min;
seconds = tm_playout.tm_sec;
frame = target_frame;
ui_day = tm_playout.tm_mday;
ui_month = tm_playout.tm_mon + 1;
ui_year = 1900 + tm_playout.tm_year;
// 6. Assemble Synchronized LTC Data Block
tc.hours = hours;
tc.mins = minutes;
tc.secs = seconds;
tc.frame = frame;
tc.days = ui_day;
tc.months = ui_month;
tc.years = ui_year % 100;
uint32_t d_tens = tc.days / 10, d_units = tc.days % 10;
uint32_t m_tens = tc.months / 10, m_units = tc.months % 10;
uint32_t y_tens = tc.years / 10, y_units = tc.years % 10;
unsigned long user_bits = (d_tens << 28) | (d_units << 24) |
(m_tens << 20) | (m_units << 16) |
(y_tens << 12) | (y_units << 8) |
(0x00 << 4) | 0x00;
ltc_encoder_set_timecode(enc, &tc);
ltc_encoder_set_user_bits(enc, user_bits);
ltc_encoder_encode_frame(enc);
int n = ltc_encoder_copy_buffer(enc, ltc_buf);
// 7. Commit to Hardware Buffer Immediately
if (!null_device_mode) {
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] = sample;
}
if (snd_pcm_writei(pcm, audio, n) < 0)
snd_pcm_prepare(pcm);
}
// 8. Paint Interface Post-Handshake
draw_ui();
}
free(ltc_buf);
free(audio);
ltc_encoder_free(enc);
if (pcm) snd_pcm_close(pcm);
return 0;
}
To Compile execute : gcc ltc_master.c -o ltc_master -lasound -lltc
To Execute : ./ltc_master







