
(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 | User Bits: DATE_BCD ",
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 natively
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;
static int last_sec = -1;
static int last_frame = -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. Edge change detector: If the system hasn't advanced to the next frame
// boundary yet, wait a moment and loop back. This completely prevents frame duplication.
if (tm_now.tm_sec == last_sec && target_frame == last_frame) {
usleep(1000); // 1ms cool-off check
continue;
}
// Update sequence memory registers
last_sec = tm_now.tm_sec;
last_frame = target_frame;
// 4. Map UI variables directly from real-world calculations
hours = tm_now.tm_hour;
minutes = tm_now.tm_min;
seconds = tm_now.tm_sec;
frame = target_frame;
// 5. Structure time payload parameters for the native encoder
tc.hours = hours;
tc.mins = minutes;
tc.secs = seconds;
tc.frame = frame;
tc.days = tm_now.tm_mday;
tc.months = tm_now.tm_mon + 1;
tc.years = tm_now.tm_year % 100;
// 6. Native User Bits Injection Engine
// Formats your calendar parameters as standard BCD pairs (DDMMYY) directly
// across the 8 binary groups, preserving the encoder's internal phase engine.
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; // Remaining groups zeroed
// Pass structured configurations safely via standard API methods
ltc_encoder_set_timecode(enc, &tc);
ltc_encoder_set_user_bits(enc, user_bits);
// Let libltc calculate the audio vectors and parity bits natively
ltc_encoder_encode_frame(enc);
int n = ltc_encoder_copy_buffer(enc, ltc_buf);
// 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







