subsync.c (9084B)
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <stdint.h> 4 #include <inttypes.h> 5 #include <string.h> 6 #include <assert.h> 7 #include <errno.h> 8 #include <getopt.h> 9 #include <math.h> 10 #include "list.h" 11 12 #define VERSION "0.2.0" 13 #define SUB_MAX_BUF 1024 14 15 typedef uint64_t msec_t; 16 #define PRImsec PRIu64 17 18 struct srt_sub { 19 msec_t start; 20 msec_t end; 21 char *position; 22 char text[SUB_MAX_BUF]; 23 struct list_head list; 24 }; 25 26 void init_srt_sub(struct srt_sub *sub) 27 { 28 assert(sub != NULL); 29 sub->start = 0; 30 sub->end = 0; 31 sub->position = NULL; 32 sub->text[0] = '\0'; 33 sub->list.next = NULL; 34 sub->list.prev = NULL; 35 } 36 37 void free_srt_sub(struct srt_sub *sub) 38 { 39 assert(sub != NULL); 40 if (sub->position) 41 free(sub->position); 42 free(sub); 43 } 44 45 void free_srt_sub_list(struct list_head *srt_head) 46 { 47 struct list_head *pos, *tmp; 48 struct srt_sub *sub; 49 50 assert(srt_head != NULL); 51 52 list_for_each_safe(pos, tmp, srt_head) { 53 sub = list_entry(pos, struct srt_sub, list); 54 free_srt_sub(sub); 55 } 56 } 57 58 void *xmalloc(size_t size) 59 { 60 void *m = malloc(size); 61 62 if (m == NULL) 63 abort(); 64 65 return m; 66 } 67 68 /* converts hh:mm:ss[,.]mss to milliseconds */ 69 int timestr_to_msec(const char *time, msec_t *msecs) 70 { 71 char *tmp; 72 msec_t h, m, s, ms; 73 int res; 74 75 assert(msecs != NULL || time != NULL); 76 77 tmp = strchr(time, '.'); 78 if (tmp != NULL) 79 *tmp = ','; 80 81 res = sscanf(time, "%" PRImsec ":%" PRImsec ":%" PRImsec ",%" PRImsec, &h, &m, &s, &ms); 82 if (res != 4 || m >= 60 || s >= 60 || ms >= 1000) { 83 fprintf(stderr, "Parsing error: Can not convert `%s' to milliseconds\n", time); 84 return -1; 85 } 86 87 *msecs = h * 60 * 60 * 1000; 88 *msecs += m * 60 * 1000; 89 *msecs += s * 1000; 90 *msecs += ms; 91 92 return 0; 93 } 94 95 /* converts milliseconds to hh:mm:ss,mss */ 96 char *msec_to_timestr(msec_t msecs, char *timestr, size_t size) 97 { 98 msec_t h, m, s, ms; 99 100 assert(timestr != NULL || size == 0); 101 102 h = msecs / (60 * 60 * 1000); 103 msecs %= 60 * 60 * 1000; 104 m = msecs / (60 * 1000); 105 msecs %= 60 * 1000; 106 s = msecs / 1000; 107 ms = msecs % 1000; 108 109 snprintf(timestr, size, "%02" PRImsec ":%02" PRImsec ":%02" PRImsec ",%03" PRImsec, 110 h, m, s, ms); 111 112 return timestr; 113 } 114 115 char *strip_eol(char *str) 116 { 117 size_t i; 118 119 assert(str != NULL); 120 121 for (i = 0; str[i] != '\0'; i++) { 122 if (str[i] == '\n') { 123 str[i] = '\0'; 124 if (i > 0 && str[i - 1] == '\r') 125 str[i - 1] = '\0'; 126 return str; 127 } 128 } 129 130 return str; 131 } 132 133 /* read SubRip (srt) file */ 134 int read_srt(FILE *fin, struct list_head *srt_head) 135 { 136 int state = 0; 137 char *s, buf[SUB_MAX_BUF]; 138 struct srt_sub *sub = NULL; 139 140 assert(fin != NULL || srt_head != NULL); 141 142 while (1) { 143 s = fgets(buf, sizeof(buf), fin); 144 if (s == NULL) 145 break; 146 147 strip_eol(buf); 148 149 if (state == 0) { 150 /* drop empty lines */ 151 if (buf[0] == '\0') 152 continue; 153 /* drop subtitle number */ 154 state = 1; 155 } else if (state == 1) { 156 char start_time[20], end_time[20], position[50]; 157 int res; 158 159 sub = xmalloc(sizeof(*sub)); 160 init_srt_sub(sub); 161 162 /* parse start, end, and position */ 163 res = sscanf(buf, "%19s --> %19s%49[^\n]", start_time, end_time, position); 164 if (res < 2) { 165 fprintf(stderr, "Parsing error: Wrong file format\n"); 166 goto out_err; 167 } 168 169 if (res == 3) 170 sub->position = strdup(position); 171 172 res = timestr_to_msec(start_time, &sub->start); 173 if (res == -1) 174 goto out_err; 175 176 res = timestr_to_msec(end_time, &sub->end); 177 if (res == -1) 178 goto out_err; 179 180 state = 2; 181 } else if (state == 2) { 182 /* empty line indicates the end of the subtitle, 183 * so append it to the list */ 184 if (buf[0] == '\0') { 185 list_add_tail(&sub->list, srt_head); 186 sub = NULL; 187 state = 0; 188 continue; 189 } 190 /* save subtitle text */ 191 strncat(sub->text, buf, sizeof(sub->text) - strlen(sub->text) - 1); 192 strncat(sub->text, "\r\n", sizeof(sub->text) - strlen(sub->text) - 1); 193 } 194 } 195 196 if (ferror(fin)) { 197 fprintf(stderr, "read: File error\n"); 198 goto out_err; 199 } 200 201 return 0; 202 203 out_err: 204 if (sub != NULL) 205 free_srt_sub(sub); 206 free_srt_sub_list(srt_head); 207 return -1; 208 } 209 210 /* write SubRip (srt) file */ 211 void write_srt(FILE *fout, struct list_head *srt_head) 212 { 213 struct list_head *pos; 214 struct srt_sub *sub; 215 unsigned int id = 1; 216 char tm[20]; 217 218 assert(fout != NULL || srt_head != NULL); 219 220 list_for_each(pos, srt_head) { 221 sub = list_entry(pos, struct srt_sub, list); 222 fprintf(fout, "%u\r\n", id++); 223 fprintf(fout, "%s", msec_to_timestr(sub->start, tm, sizeof(tm))); 224 fprintf(fout, " --> "); 225 fprintf(fout, "%s", msec_to_timestr(sub->end, tm, sizeof(tm))); 226 if (sub->position) 227 fprintf(fout, "%s", sub->position); 228 fprintf(fout, "\r\n%s\r\n", sub->text); 229 } 230 } 231 232 /* synchronize subtitles by knowing the start time of the first and the last subtitle. 233 * to achieve this we must use the linear equation: y = mx + b */ 234 void sync_srt(struct list_head *srt_head, msec_t synced_first, msec_t synced_last) 235 { 236 long double slope, yint; 237 msec_t desynced_first, desynced_last; 238 struct list_head *pos; 239 struct srt_sub *sub; 240 241 assert(srt_head != NULL); 242 243 desynced_first = list_first_entry(srt_head, struct srt_sub, list)->start; 244 desynced_last = list_last_entry(srt_head, struct srt_sub, list)->start; 245 246 /* m = (y2 - y1) / (x2 - x1) 247 * m: slope 248 * y2: synced_last 249 * y1: synced_first 250 * x2: desynced_last 251 * x1: desynced_first */ 252 slope = (long double)(synced_last - synced_first) 253 / (long double)(desynced_last - desynced_first); 254 255 /* b = y - mx 256 * b: yint 257 * y: synced_last 258 * m: slope 259 * x: desynced_last */ 260 yint = synced_last - slope * desynced_last; 261 262 list_for_each(pos, srt_head) { 263 sub = list_entry(pos, struct srt_sub, list); 264 /* y = mx + b 265 * y: sub->start and sub->end 266 * m: slope 267 * x: sub->start and sub->end 268 * b: yint */ 269 sub->start = llroundl(slope * sub->start + yint); 270 sub->end = llroundl(slope * sub->end + yint); 271 } 272 } 273 274 void usage(void) 275 { 276 fprintf(stderr, "Usage:\n"); 277 fprintf(stderr, " subsync [options]\n"); 278 fprintf(stderr, "\nOptions:\n"); 279 fprintf(stderr, " -h, --help Show this help\n"); 280 fprintf(stderr, " -f, --first-sub Time of the first subtitle\n"); 281 fprintf(stderr, " -l, --last-sub Time of the last subtitle\n"); 282 fprintf(stderr, " -i, --input Input file\n"); 283 fprintf(stderr, " -o, --output Output file"); 284 fprintf(stderr, " (if not specified, it overwrites the input file)\n"); 285 fprintf(stderr, " -v, --version Print version\n"); 286 fprintf(stderr, "\nExample:\n"); 287 fprintf(stderr, " subsync -f 00:01:33,492 -l 01:39:23,561 -i file.srt\n"); 288 } 289 290 #define FLAG_F (1 << 0) 291 #define FLAG_L (1 << 1) 292 293 int main(int argc, char *argv[]) 294 { 295 struct list_head subs_head; 296 unsigned int flags = 0; 297 msec_t first_ms, last_ms; 298 char *input_path = NULL, *output_path = NULL; 299 FILE *fin = stdin, *fout = stdout; 300 int res; 301 302 if (argc <= 1) { 303 usage(); 304 return 1; 305 } 306 307 init_list_head(&subs_head); 308 309 while (1) { 310 int c, option_index; 311 static struct option long_options[] = { 312 {"help", no_argument, 0, 'h'}, 313 {"first-sub", required_argument, 0, 'f'}, 314 {"last-sub", required_argument, 0, 'l'}, 315 {"input", required_argument, 0, 'i'}, 316 {"output", required_argument, 0, 'o'}, 317 {"version", required_argument, 0, 'v'}, 318 { 0, 0, 0, 0} 319 }; 320 321 c = getopt_long(argc, argv, "f:l:i:o:hv", long_options, &option_index); 322 if (c == -1) 323 break; 324 325 switch (c) { 326 case 'h': 327 usage(); 328 return 0; 329 case 'f': 330 flags |= FLAG_F; 331 res = timestr_to_msec(optarg, &first_ms); 332 if (res == -1) 333 return 1; 334 break; 335 case 'l': 336 flags |= FLAG_L; 337 res = timestr_to_msec(optarg, &last_ms); 338 if (res == -1) 339 return 1; 340 break; 341 case 'i': 342 input_path = optarg; 343 break; 344 case 'o': 345 output_path = optarg; 346 break; 347 case 'v': 348 printf("%s\n", VERSION); 349 return 0; 350 default: 351 return 1; 352 } 353 } 354 355 if (optind < argc) { 356 int i; 357 fprintf(stderr, "Invalid argument%s:", argc - optind > 1 ? "s" : ""); 358 for (i = optind; i < argc; i++) 359 fprintf(stderr, " %s", argv[i]); 360 fprintf(stderr, "\n"); 361 return 1; 362 } 363 364 if (input_path == NULL) { 365 fprintf(stderr, "You must specify an input file with -i option.\n"); 366 return 1; 367 } 368 369 if (output_path == NULL) 370 output_path = input_path; 371 372 /* read srt file */ 373 if (strcmp(input_path, "-") != 0) 374 fin = fopen(input_path, "r"); 375 376 if (fin == NULL) { 377 fprintf(stderr, "open: %s: %s\n", input_path, strerror(errno)); 378 return 1; 379 } 380 381 res = read_srt(fin, &subs_head); 382 fclose(fin); 383 if (res == -1) 384 return 1; 385 386 /* if user didn't pass 'f' flag, then get the time of the first subtitle */ 387 if (!(flags & FLAG_F)) 388 first_ms = list_first_entry(&subs_head, struct srt_sub, list)->start; 389 390 /* if user didn't pass 'l' flag, then get the time of the last subtitle */ 391 if (!(flags & FLAG_L)) 392 last_ms = list_last_entry(&subs_head, struct srt_sub, list)->start; 393 394 /* sync subtitles */ 395 sync_srt(&subs_head, first_ms, last_ms); 396 397 /* write subtitles */ 398 if (strcmp(output_path, "-") != 0) 399 fout = fopen(output_path, "w"); 400 401 if (fout == NULL) { 402 fprintf(stderr, "open: %s: %s\n", output_path, strerror(errno)); 403 free_srt_sub_list(&subs_head); 404 return 1; 405 } 406 407 write_srt(fout, &subs_head); 408 fclose(fout); 409 410 free_srt_sub_list(&subs_head); 411 return 0; 412 }