LTC / SMTP Time Code Generator

(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

Leave a Reply