LTC / SMTP Time Code Generator


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;

static int hours = 0;
static int minutes = 0;
static int seconds = 0;
static int frame = 0;
static char last_sync_string[32] = "NONE";

/* -------------------------------------------------------
   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);

    // Explicitly inject targeted layout timezone straight into the application runtime environment
    setenv("TZ", tz_setting, 1);
    tzset();
}

static void get_smpte_time(struct tm *out) {
    time_t now = time(NULL);
    // Automatically applies complex local DST parameters via internal zone tracking
    localtime_r(&now, out);
}

static void jam_sync_execute() {
    struct tm tm;
    get_smpte_time(&tm);
    hours = tm.tm_hour;
    minutes = tm.tm_min;
    seconds = tm.tm_sec;
    frame = 0;
    snprintf(last_sync_string, sizeof(last_sync_string), "%02d:%02d:%02d", hours, minutes, seconds);
}

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: 25fps EBU | Zone: %s | Last Sync: %s ",
             null_device_mode ? "SIMULATED" : device_name, tz_setting, last_sync_string);

    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;
    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;
        }
    }

    LTCEncoder *enc = ltc_encoder_create(SAMPLE_RATE, FPS, LTC_TV_625_50, 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;
    }

    jam_sync_execute();

    while (1) {
        struct tm tm;
        get_smpte_time(&tm);

        // Synchronize on minute changes
        if (tm.tm_sec == 0 && seconds == 59 && frame == (FPS - 1)) {
            jam_sync_execute();
        }

        SMPTETimecode tc;
        tc.hours = hours;
        tc.mins  = minutes;
        tc.secs  = seconds;
        tc.frame = frame;
        tc.years  = tm.tm_year % 100;
        tc.months = tm.tm_mon + 1;
        tc.days   = tm.tm_mday;

        ltc_encoder_set_timecode(enc, &tc);
        ltc_encoder_encode_frame(enc);
        int n = ltc_encoder_copy_buffer(enc, ltc_buf);

        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);
        } else {
            usleep(40000);
        }

        draw_ui();

        frame++;
        if (frame >= FPS) {
            frame = 0;
            seconds++;
            if (seconds >= 60) {
                seconds = 0;
                minutes++;
                if (minutes >= 60) {
                    minutes = 0;
                    hours = (hours + 1) % 24;
                }
            }
        }
    }

    free(ltc_buf);
    free(audio);
    ltc_encoder_free(enc);
    if (pcm) snd_pcm_close(pcm);
    return 0;
}

To Compile execute :   gcc -O2 -o ltc_master ltc_master.c -lasound -lltc
To Execute : ./ltc_master

Leave a Reply