subsync

cli tool to synchronize srt subtitles automatically
git clone git://git.2f30.org/subsync.git
Log | Files | Refs | LICENSE

commit 2ce2432d7dfe4fd17e094acb3111cc314f8168c8
parent 0de028a721bae45ee0fb7b42644c6022482c78f6
Author: oblique <psyberbits@gmail.com>
Date:   Mon Apr  7 22:44:40 +0300

Rewrite subsync in C

Since subsync was the only code that I wrote in Go and since I forgot
Go, I decided to rewrite it in C.

Diffstat:
Makefile | 11++++++-----
README.md | 6+++---
list.h | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
subsync.c | 412+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
subsync.go | 296-------------------------------------------------------------------------------
5 files changed, 529 insertions(+), 304 deletions(-)
diff --git a/Makefile b/Makefile @@ -1,10 +1,11 @@ -GOPATH ?= $(PWD)/godir +CC := gcc +CFLAGS += -Wall -Wextra -std=gnu99 -O2 +LIBS := -lm all: subsync -subsync: subsync.go - GOPATH="$(GOPATH)" go get -d . - GOPATH="$(GOPATH)" go build -o $@ subsync.go +subsync: subsync.c list.h + $(CC) $(CFLAGS) $(LDFLAGS) $< $(LIBS) -o $@ clean: - rm -rf subsync godir + @rm -f subsync diff --git a/README.md b/README.md @@ -1,6 +1,6 @@ -subsync is a simple CLI tool written in Go, that synchronizes SubRip -(srt) subtitles automatically. You only have to know when the first -and the last subtitle must be shown. +subsync is a simple CLI tool that synchronizes SubRip (srt) subtitles +automatically. You only have to know when the first and the last +subtitle should be shown. ## examples diff --git a/list.h b/list.h @@ -0,0 +1,108 @@ +#ifndef __LIST_H +#define __LIST_H + +#include <stddef.h> +#include <stdint.h> + +struct list_head { + struct list_head *prev; + struct list_head *next; +}; + +#define container_of(ptr, type, member) ({ \ + const typeof(((type*)0)->member) *__mptr = (ptr); \ + (type*)((uintptr_t)__mptr - offsetof(type, member)); \ +}) + +#define LIST_HEAD_INIT(name) { &(name), &(name) } +#define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name) + +#define list_entry(ptr, type, member) container_of(ptr, type, member) +#define list_first_entry(head_ptr, type, member) container_of((head_ptr)->next, type, member) +#define list_last_entry(head_ptr, type, member) container_of((head_ptr)->prev, type, member) + +/* list_for_each - iterate over a list + * pos: the &struct list_head to use as a loop cursor. + * head: the head for your list. + */ +#define list_for_each(pos, head) \ + for (pos = (head)->next; pos != (head); \ + pos = pos->next) + +/* list_for_each_safe - iterate over a list safe against removal of list entry + * pos: the &struct list_head to use as a loop cursor. + * n: another &struct list_head to use as temporary storage + * head: the head for your list. + */ +#define list_for_each_safe(pos, n, head) \ + for (pos = (head)->next, n = pos->next; pos != (head); \ + pos = n, n = pos->next) + +static inline void +init_list_head(struct list_head *list) +{ + list->prev = list; + list->next = list; +} + +/* Note: list_empty() on entry does not return true after this, the entry is + * in an undefined state. + */ +static inline void +list_del(struct list_head *entry) +{ + entry->prev->next = entry->next; + entry->next->prev = entry->prev; + entry->next = NULL; + entry->prev = NULL; +} + +static inline void +list_add(struct list_head *entry, struct list_head *head) +{ + entry->next = head->next; + entry->prev = head; + head->next->prev = entry; + head->next = entry; +} + +static inline void +list_add_tail(struct list_head *entry, struct list_head *head) +{ + entry->next = head; + entry->prev = head->prev; + head->prev->next = entry; + head->prev = entry; +} + +static inline void +list_move_tail(struct list_head *entry, struct list_head *head) +{ + list_del(entry); + list_add_tail(entry, head); +} + +static inline int +list_empty(const struct list_head *head) +{ + return head->next == head; +} + +/* list_is_singular - tests whether a list has just one entry. */ +static inline int +list_is_singular(const struct list_head *head) +{ + return !list_empty(head) && (head->next == head->prev); +} + +/* list_is_last - tests whether @list is the last entry in list @head + * list: the entry to test + * head: the head of the list + */ +static inline int +list_is_last(const struct list_head *list, const struct list_head *head) +{ + return list->next == head; +} + +#endif /* __LIST_H */ diff --git a/subsync.c b/subsync.c @@ -0,0 +1,412 @@ +#include <stdio.h> +#include <stdlib.h> +#include <stdint.h> +#include <inttypes.h> +#include <string.h> +#include <assert.h> +#include <errno.h> +#include <getopt.h> +#include <math.h> +#include "list.h" + +#define VERSION "0.2.0" +#define SUB_MAX_BUF 1024 + +typedef uint64_t msec_t; +#define PRImsec PRIu64 + +struct srt_sub { + msec_t start; + msec_t end; + char *position; + char text[SUB_MAX_BUF]; + struct list_head list; +}; + +void init_srt_sub(struct srt_sub *sub) +{ + assert(sub != NULL); + sub->start = 0; + sub->end = 0; + sub->position = NULL; + sub->text[0] = '\0'; + sub->list.next = NULL; + sub->list.prev = NULL; +} + +void free_srt_sub(struct srt_sub *sub) +{ + assert(sub != NULL); + if (sub->position) + free(sub->position); + free(sub); +} + +void free_srt_sub_list(struct list_head *srt_head) +{ + struct list_head *pos, *tmp; + struct srt_sub *sub; + + assert(srt_head != NULL); + + list_for_each_safe(pos, tmp, srt_head) { + sub = list_entry(pos, struct srt_sub, list); + free_srt_sub(sub); + } +} + +void *xmalloc(size_t size) +{ + void *m = malloc(size); + + if (m == NULL) + abort(); + + return m; +} + +/* converts hh:mm:ss[,.]mss to milliseconds */ +int timestr_to_msec(const char *time, msec_t *msecs) +{ + char *tmp; + msec_t h, m, s, ms; + int res; + + assert(msecs != NULL || time != NULL); + + tmp = strchr(time, '.'); + if (tmp != NULL) + *tmp = ','; + + res = sscanf(time, "%" PRImsec ":%" PRImsec ":%" PRImsec ",%" PRImsec, &h, &m, &s, &ms); + if (res != 4 || m >= 60 || s >= 60 || ms >= 1000) { + fprintf(stderr, "Parsing error: Can not convert `%s' to milliseconds\n", time); + return -1; + } + + *msecs = h * 60 * 60 * 1000; + *msecs += m * 60 * 1000; + *msecs += s * 1000; + *msecs += ms; + + return 0; +} + +/* converts milliseconds to hh:mm:ss,mss */ +char *msec_to_timestr(msec_t msecs, char *timestr, size_t size) +{ + msec_t h, m, s, ms; + + assert(timestr != NULL || size == 0); + + h = msecs / (60 * 60 * 1000); + msecs %= 60 * 60 * 1000; + m = msecs / (60 * 1000); + msecs %= 60 * 1000; + s = msecs / 1000; + ms = msecs % 1000; + + snprintf(timestr, size, "%02" PRImsec ":%02" PRImsec ":%02" PRImsec ",%03" PRImsec, + h, m, s, ms); + + return timestr; +} + +char *strip_eol(char *str) +{ + size_t i; + + assert(str != NULL); + + for (i = 0; str[i] != '\0'; i++) { + if (str[i] == '\n') { + str[i] = '\0'; + if (i > 0 && str[i - 1] == '\r') + str[i - 1] = '\0'; + return str; + } + } + + return str; +} + +/* read SubRip (srt) file */ +int read_srt(FILE *fin, struct list_head *srt_head) +{ + int state = 0; + char *s, buf[SUB_MAX_BUF]; + struct srt_sub *sub = NULL; + + assert(fin != NULL || srt_head != NULL); + + while (1) { + s = fgets(buf, sizeof(buf), fin); + if (s == NULL) + break; + + strip_eol(buf); + + if (state == 0) { + /* drop empty lines */ + if (buf[0] == '\0') + continue; + /* drop subtitle number */ + state = 1; + } else if (state == 1) { + char start_time[20], end_time[20], position[50]; + int res; + + sub = xmalloc(sizeof(*sub)); + init_srt_sub(sub); + + /* parse start, end, and position */ + res = sscanf(buf, "%19s --> %19s%49[^\n]", start_time, end_time, position); + if (res < 2) { + fprintf(stderr, "Parsing error: Wrong file format\n"); + goto out_err; + } + + if (res == 3) + sub->position = strdup(position); + + res = timestr_to_msec(start_time, &sub->start); + if (res == -1) + goto out_err; + + res = timestr_to_msec(end_time, &sub->end); + if (res == -1) + goto out_err; + + state = 2; + } else if (state == 2) { + /* empty line indicates the end of the subtitle, + * so append it to the list */ + if (buf[0] == '\0') { + list_add_tail(&sub->list, srt_head); + sub = NULL; + state = 0; + continue; + } + /* save subtitle text */ + strncat(sub->text, buf, sizeof(sub->text) - strlen(sub->text) - 1); + strncat(sub->text, "\r\n", sizeof(sub->text) - strlen(sub->text) - 1); + } + } + + if (ferror(fin)) { + fprintf(stderr, "read: File error\n"); + goto out_err; + } + + return 0; + +out_err: + if (sub != NULL) + free_srt_sub(sub); + free_srt_sub_list(srt_head); + return -1; +} + +/* write SubRip (srt) file */ +void write_srt(FILE *fout, struct list_head *srt_head) +{ + struct list_head *pos; + struct srt_sub *sub; + unsigned int id = 1; + char tm[20]; + + assert(fout != NULL || srt_head != NULL); + + list_for_each(pos, srt_head) { + sub = list_entry(pos, struct srt_sub, list); + fprintf(fout, "%u\r\n", id++); + fprintf(fout, "%s", msec_to_timestr(sub->start, tm, sizeof(tm))); + fprintf(fout, " --> "); + fprintf(fout, "%s", msec_to_timestr(sub->end, tm, sizeof(tm))); + if (sub->position) + fprintf(fout, "%s", sub->position); + fprintf(fout, "\r\n%s\r\n", sub->text); + } +} + +/* synchronize subtitles by knowing the start time of the first and the last subtitle. + * to archive this we must use the linear equation: y = mx + b */ +void sync_srt(struct list_head *srt_head, msec_t synced_first, msec_t synced_last) +{ + long double slope, yint; + msec_t desynced_first, desynced_last; + struct list_head *pos; + struct srt_sub *sub; + + assert(srt_head != NULL); + + desynced_first = list_first_entry(srt_head, struct srt_sub, list)->start; + desynced_last = list_last_entry(srt_head, struct srt_sub, list)->start; + + /* m = (y2 - y1) / (x2 - x1) + * m: slope + * y2: synced_last + * y1: synced_first + * x2: desynced_last + * x1: desynced_first */ + slope = (long double)(synced_last - synced_first) + / (long double)(desynced_last - desynced_first); + + /* b = y - mx + * b: yint + * y: synced_last + * m: slope + * x: desynced_last */ + yint = synced_last - slope * desynced_last; + + list_for_each(pos, srt_head) { + sub = list_entry(pos, struct srt_sub, list); + /* y = mx + b + * y: sub->start and sub->end + * m: slope + * x: sub->start and sub->end + * b: yint */ + sub->start = llroundl(slope * sub->start + yint); + sub->end = llroundl(slope * sub->end + yint); + } +} + +void usage(void) +{ + fprintf(stderr, "Usage:\n"); + fprintf(stderr, " subsync [options]\n"); + fprintf(stderr, "\nOptions:\n"); + fprintf(stderr, " -h, --help Show this help\n"); + fprintf(stderr, " -f, --first-sub Time of the first subtitle\n"); + fprintf(stderr, " -l, --last-sub Time of the last subtitle\n"); + fprintf(stderr, " -i, --input Input file\n"); + fprintf(stderr, " -o, --output Output file"); + fprintf(stderr, " (if not specified, it overwrites the input file)\n"); + fprintf(stderr, " -v, --version Print version\n"); + fprintf(stderr, "\nExample:\n"); + fprintf(stderr, " subsync -f 00:01:33,492 -l 01:39:23,561 -i file.srt\n"); +} + +#define FLAG_F (1 << 0) +#define FLAG_L (1 << 1) + +int main(int argc, char *argv[]) +{ + struct list_head subs_head; + unsigned int flags = 0; + msec_t first_ms, last_ms; + char *input_path = NULL, *output_path = NULL; + FILE *fin = stdin, *fout = stdout; + int res; + + if (argc <= 1) { + usage(); + return 1; + } + + init_list_head(&subs_head); + + while (1) { + int c, option_index; + static struct option long_options[] = { + {"help", no_argument, 0, 'h'}, + {"first-sub", required_argument, 0, 'f'}, + {"last-sub", required_argument, 0, 'l'}, + {"input", required_argument, 0, 'i'}, + {"output", required_argument, 0, 'o'}, + {"version", required_argument, 0, 'v'}, + { 0, 0, 0, 0} + }; + + c = getopt_long(argc, argv, "f:l:i:o:hv", long_options, &option_index); + if (c == -1) + break; + + switch (c) { + case 'h': + usage(); + return 0; + case 'f': + flags |= FLAG_F; + res = timestr_to_msec(optarg, &first_ms); + if (res == -1) + return 1; + break; + case 'l': + flags |= FLAG_L; + res = timestr_to_msec(optarg, &last_ms); + if (res == -1) + return 1; + break; + case 'i': + input_path = optarg; + break; + case 'o': + output_path = optarg; + break; + case 'v': + printf("%s\n", VERSION); + return 0; + default: + return 1; + } + } + + if (optind < argc) { + int i; + fprintf(stderr, "Invalid argument%s:", argc - optind > 1 ? "s" : ""); + for (i = optind; i < argc; i++) + fprintf(stderr, " %s", argv[i]); + fprintf(stderr, "\n"); + return 1; + } + + if (input_path == NULL) { + fprintf(stderr, "You must specify an input file with -i option.\n"); + return 1; + } + + if (output_path == NULL) + output_path = input_path; + + /* read srt file */ + if (strcmp(input_path, "-") != 0) + fin = fopen(input_path, "r"); + + if (fin == NULL) { + fprintf(stderr, "open: %s: %s\n", input_path, strerror(errno)); + return 1; + } + + res = read_srt(fin, &subs_head); + fclose(fin); + if (res == -1) + return 1; + + /* if user didn't pass 'f' flag, then get the time of the first subtitle */ + if (!(flags & FLAG_F)) + first_ms = list_first_entry(&subs_head, struct srt_sub, list)->start; + + /* if user didn't pass 'l' flag, then get the time of the last subtitle */ + if (!(flags & FLAG_L)) + last_ms = list_last_entry(&subs_head, struct srt_sub, list)->start; + + /* sync subtitles */ + sync_srt(&subs_head, first_ms, last_ms); + + /* write subtitles */ + if (strcmp(output_path, "-") != 0) + fout = fopen(output_path, "w"); + + if (fout == NULL) { + fprintf(stderr, "open: %s: %s\n", output_path, strerror(errno)); + free_srt_sub_list(&subs_head); + return 1; + } + + write_srt(fout, &subs_head); + fclose(fout); + + free_srt_sub_list(&subs_head); + return 0; +} diff --git a/subsync.go b/subsync.go @@ -1,295 +0,0 @@ -package main - -import ( - "fmt" - "errors" - "strings" - "math" - "os" - "path" - "io" - "bufio" - "container/list" - "github.com/jessevdk/go-flags" -) - - -const ( - version = "0.1.3" -) - -type subtitle struct { - text string - start uint - end uint -} - -func die(err error) { - fmt.Fprintf(os.Stderr, "%v\n", err) - os.Exit(1) -} - -func roundFloat64(f float64) float64 { - val := f - float64(int64(f)) - if val >= 0.5 { - return math.Ceil(f) - } else if val > 0 { - return math.Floor(f) - } else if val <= -0.5 { - return math.Floor(f) - } else if val < 0 { - return math.Ceil(f) - } - return f -} - -/* converts hh:mm:ss,mss to milliseconds */ -func time_to_msecs(tm string) (uint, error) { - var msecs uint - var h, m, s, ms uint - - tm = strings.Replace(tm, ".", ",", 1) - num, err := fmt.Sscanf(tm, "%d:%d:%d,%d", &h, &m, &s, &ms) - - if num != 4 || err != nil { - return 0, errors.New("Parsing error: Can not covert `" + tm + "' to milliseconds.") - } - - msecs = h * 60 * 60 * 1000 - msecs += m * 60 * 1000 - msecs += s * 1000 - msecs += ms - - return msecs, nil -} - -/* converts milliseconds to hh:mm:ss,mss */ -func msecs_to_time(msecs uint) string { - var h, m, s, ms uint - - h = msecs / (60 * 60 * 1000) - msecs %= 60 * 60 * 1000 - m = msecs / (60 * 1000) - msecs %= 60 * 1000 - s = msecs / 1000 - ms = msecs % 1000 - - tm := fmt.Sprintf("%02d:%02d:%02d,%03d", h, m, s, ms) - - return tm -} - -/* read SubRip (srt) file */ -func read_srt(filename string) (*list.List, error) { - var state int = 0 - var subs *list.List - var sub *subtitle - - f, err := os.Open(filename) - if err != nil { - return nil, err - } - defer f.Close() - - r := bufio.NewReader(f) - subs = list.New() - sub = new(subtitle) - - for { - var ( - isprefix bool = true - err error = nil - ln, line []byte - ) - - for isprefix && err == nil { - line, isprefix, err = r.ReadLine() - if err != nil && err != io.EOF { - return nil, err - } - ln = append(ln, line...) - } - - if state == 0 { - if len(ln) == 0 { - if err == io.EOF { - break; - } - continue; - } - state = 1 - /* parse start, end times */ - } else if state == 1 { - tm := strings.Split(string(ln), " ") - if len(tm) != 3 || tm[1] != "-->" { - return nil, errors.New("Parsing error: Wrong file format") - } - sub.start, err = time_to_msecs(tm[0]) - if err != nil { - return nil, err - } - sub.end, err = time_to_msecs(tm[2]) - if err != nil { - return nil, err - } - state = 2 - /* parse the actual subtitle text */ - } else if state == 2 { - if len(ln) == 0 { - subs.PushBack(sub) - sub = new(subtitle) - state = 0 - } else { - sub.text += string(ln) + "\r\n" - } - } - - if err == io.EOF { - break; - } - } - - return subs, nil -} - -/* write SubRip (srt) file */ -func write_srt(filename string, subs *list.List) error { - var id int = 0 - - f, err := os.Create(filename) - if err != nil { - return err - } - defer f.Close() - - w := bufio.NewWriter(f) - defer w.Flush() - - for e := subs.Front(); e != nil; e = e.Next() { - id++ - sub := e.Value.(*subtitle) - fmt.Fprintf(w, "%d\r\n", id) - fmt.Fprintf(w, "%s --> %s\r\n", msecs_to_time(sub.start), msecs_to_time(sub.end)) - fmt.Fprintf(w, "%s\r\n", sub.text) - } - - return nil -} - -/* synchronize subtitles by knowing the time of the first and the last subtitle. - * to archive this we must use the linear equation: y = mx + b */ -func sync_subs(subs *list.List, synced_first_ms uint, synced_last_ms uint) { - var slope, yint float64 - - desynced_first_ms := subs.Front().Value.(*subtitle).start - desynced_last_ms := subs.Back().Value.(*subtitle).start - - /* m = (y2 - y1) / (x2 - x1) - * m: slope - * y2: synced_last_ms - * y1: synced_first_ms - * x2: desynced_last_ms - * x1: desynced_first_ms */ - slope = float64(synced_last_ms - synced_first_ms) / float64(desynced_last_ms - desynced_first_ms) - /* b = y - mx - * b: yint - * y: synced_last_ms - * m: slope - * x: desynced_last_ms */ - yint = float64(synced_last_ms) - slope * float64(desynced_last_ms) - - for e := subs.Front(); e != nil; e = e.Next() { - sub := e.Value.(*subtitle) - /* y = mx + b - * y: sub.start and sub.end - * m: slope - * x: sub.start and sub.end - * b: yint */ - sub.start = uint(roundFloat64(slope * float64(sub.start) + yint)) - sub.end = uint(roundFloat64(slope * float64(sub.end) + yint)) - } -} - -func main() { - var first_ms, last_ms uint - - var opts struct { - FirstTm string `short:"f" long:"first-sub" description:"Time of first subtitle"` - LastTm string `short:"l" long:"last-sub" description:"Time of last subtitle"` - InputFl string `short:"i" long:"input" description:"Input file"` - OutputFl string `short:"o" long:"output" description:"Output file"` - PrintVersion bool `short:"v" long:"version" description:"Print version"` - } - - _, err := flags.Parse(&opts) - if err != nil { - if err.(*flags.Error).Type == flags.ErrHelp { - fmt.Fprintf(os.Stderr, "Example:\n") - fmt.Fprintf(os.Stderr, " %s -f 00:01:33,492 -l 01:39:23,561 -i file.srt\n", - path.Base(os.Args[0])) - os.Exit(0) - } - os.Exit(1) - } - - if opts.PrintVersion { - fmt.Printf("subsync v%s\n", version) - os.Exit(0) - } - - if opts.InputFl == "" { - fmt.Fprintf(os.Stderr, "You must specify an input file with -i option.\n") - os.Exit(1) - } - - if opts.FirstTm != "" { - first_ms, err = time_to_msecs(opts.FirstTm) - if err != nil { - fmt.Fprintf(os.Stderr, "Please check the value of -f option.\n") - die(err) - } - } - - if opts.LastTm != "" { - last_ms, err = time_to_msecs(opts.LastTm) - if err != nil { - fmt.Fprintf(os.Stderr, "Please check the value of -l option.\n") - die(err) - } - } - - /* if output file is not set, use the input file */ - if opts.OutputFl == "" { - opts.OutputFl = opts.InputFl - } - - subs, err := read_srt(opts.InputFl) - if err != nil { - die(err) - } - - /* if time of the first synced subtitle is not set, - * use the time of the first desynced subtitle */ - if opts.FirstTm == "" { - first_ms = subs.Front().Value.(*subtitle).start - } - - /* if time of the last synced subtitle is not set, - * use the time of the last desynced subtitle */ - if opts.LastTm == "" { - last_ms = subs.Back().Value.(*subtitle).start - } - - if first_ms > last_ms { - fmt.Fprintf(os.Stderr, "First subtitle can not be after last subtitle.\n") - fmt.Fprintf(os.Stderr, "Please check the values of -f and/or -l options.\n") - os.Exit(1) - } - - sync_subs(subs, first_ms, last_ms) - - err = write_srt(opts.OutputFl, subs) - if err != nil { - die(err) - } -}- \ No newline at end of file