
(Updated Code : 09-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.
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;
// Shared variables directly reflecting the real-world clock for the UI drawer
static int hours = 0;
static int minutes = 0;
static int seconds = 0;
static int frame = 0;
/* -------------------------------------------------------
STRICT 8-CHARACTER WIDE SEVEN-SEGMENT BROADCAST FONT
-------------------------------------------------------- */
static const char *font[11][5] = {
{" ###### ", " # # ", " # # ", " # # ", " ###### "}, // 0
{" ## ", " ## ", " ## ", " ## ", " ## "}, // 1
{" ###### ", " # ", " ###### ", " # ", " ###### "}, // 2
{" ###### ", " # ", " ###### ", " # ", " ###### "}, // 3
{" # # ", " # # ", " ###### ", " # ", " # "}, // 4
{" ###### ", " # ", " ###### ", " # ", " ###### "}, // 5
{" ###### ", " # ", " ###### ", " # # ", " ###### "}, // 6
{" ###### ", " # ", " # ", " # ", " # "}, // 7
{" ###### ", " # # ", " ###### ", " # # ", " ###### "}, // 8
{" ###### ", " # # ", " ###### ", " # ", " ###### "}, // 9
{" ", " ## ", " ", " ## ", " "} // : (Colon - Index 10)
};
/* -------------------------------------------------------
CONFIG FILE PARSER (READS TZ ENVIRONMENT STRINGS)
-------------------------------------------------------- */
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();
}
/* -------------------------------------------------------
VISUAL DISPLAY DRAW ENGINE
-------------------------------------------------------- */
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 = 88;
int horizontal_padding = (term_cols - total_font_width) / 2;
if (horizontal_padding < 0) horizontal_padding = 0;
printf("\033[H");
int top_padding = (term_rows - 11) / 2;
for (int i = 0; i < top_padding; i++) printf("\n");
printf("%*sSMPTE LTC MASTER CLOCK TRANSMITTER\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 < 5; row++) {
printf("%*s", horizontal_padding, "");
for (int d = 0; d < 11; d++) {
printf("%s", font[digits[d]][row]);
}
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 + 11;
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), " LTC STATUS | Device: %s | Mode: %dfps EBU | Zone: %s | Sync Mode: STATELESS_LOCK ",
null_device_mode ? "SIMULATED" : device_name, FPS, tz_setting);
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);
}
/* -------------------------------------------------------
MAIN EXECUTION CONTROLLER
-------------------------------------------------------- */
int main() {
load_config_file();
printf("\033[2J");
snd_pcm_t *pcm = 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, 50000) < 0) {
fprintf(stderr, "ALSA configuration failed\n");
if (pcm) snd_pcm_close(pcm);
return 1;
}
}
// Create libltc encoder instance matching 25fps PAL EBU standard
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;
struct tm tm_now;
SMPTETimecode tc;
// Direct tracking variables to catch frame edge changes across loop ticks
static int last_sec = -1;
static int last_frame = -1;
static int first_run_check = 1;
while (1) {
// 1. Fetch current absolute nanosecond time directly from the kernel
clock_gettime(CLOCK_REALTIME, &ts);
localtime_r(&ts.tv_sec, &tm_now);
// 2. Derive the explicit target frame number from the real-world timeline
int target_frame = (int)((ts.tv_nsec * FPS) / 1000000000LL);
// 3. THE GOLDEN RULE TICKET: If the system hasn't advanced to the next frame
// boundary yet, wait a moment and loop back. This completely prevents duplicate frame generation.
if (tm_now.tm_sec == last_sec && target_frame == last_frame) {
usleep(1000); // 1ms sleep loop check to keep execution calm
continue;
}
// Update tracking history for the edge change detector
last_sec = tm_now.tm_sec;
last_frame = target_frame;
// 4. Map global metrics directly from real-world wall clock calculations
hours = tm_now.tm_hour;
minutes = tm_now.tm_min;
seconds = tm_now.tm_sec;
frame = target_frame;
// 5. Structure payload parameters for the encoder
tc.hours = hours;
tc.mins = minutes;
tc.secs = seconds;
tc.frame = frame;
tc.years = tm_now.tm_year % 100;
tc.months = tm_now.tm_mon + 1;
tc.days = tm_now.tm_mday;
tc.timezone[0] = '\0';
// 6. Generate the pure LTC frame data
ltc_encoder_set_timecode(enc, &tc);
ltc_encoder_encode_frame(enc);
int n = ltc_encoder_copy_buffer(enc, ltc_buf);
// Debug diagnostic check to log true generated audio sample sizes per block
if (first_run_check) {
fprintf(stderr, "\033[JGenerated %d samples per LTC frame.\n", n);
sleep(1); // Brief pause so you can visually verify the sample length on initialization
first_run_check = 0;
}
// 7. Route audio blocks down to the sound hardware
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; // Left pin
audio[i * 2 + 1] = sample; // Right pin
}
if (snd_pcm_writei(pcm, audio, n) < 0)
snd_pcm_prepare(pcm);
}
// 8. Render your custom seven-segment block visualization matrix
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
