NHMLFixup v10
[jpcrr.git] / scripts / NHMLFixup.lua
blob0d93989495176d850f98d96e8baa4d0c52ee638d
1 #!/usr/bin/env lua
2 ----------------------------------------------------------------------------------------------------------------------
3 ----------------------------------------------------------------------------------------------------------------------
4 -- NHMLFixup v10 by Ilari (2011-08-16).
5 -- Update timecodes in NHML Audio/Video track timing to conform to given MKV v2 timecodes file.
6 -- Syntax: NHMLFixup <video-nhml-file> <audio-nhml-file> <mkv-timecodes-file> [delay=<delay>] [tvaspect|widescreen]
7 -- <delay> is number of milliseconds to delay the video (in order to compensate for audio codec delay, reportedly
8 -- does not work right with some demuxers).
9 -- The 'tvaspect' option makes video track to be automatically adjusted to '4:3' aspect ratio.
10 -- The 'widescreen' option makes video track to be automatically adjusted to '16:9' aspect ratio.
12 -- Version v10 by Ilari (2011-08-16):
13 -- Work around MP4Box bug resulting in negative CTSOffset.
15 -- Version v9 by Ilari (2011-04-01):
16 -- - Support widescreen mode ("widescreen").
18 -- Version v8 by Ilari (2010-12-06):
19 -- - Support Special timecode file "@CFR" that fixes up audio for CFR encode.
21 -- Version v7 by Ilari (2010-10-24):
22 -- - Fix bug in time division (use integer timestamps, not decimal ones).
24 -- Version v6 by Ilari (2010-10-24):
25 -- - Make it work on Lua 5.2 (work 4).
27 -- Version v5 by Ilari (2010-09-18):
28 -- - Move the files first out of way, since rename-over fails on Windows.
30 -- Version v4 by Ilari (2010-09-17):
31 -- - Change audio track ID if it collides with video track..
33 -- Version v3 by Ilari (2010-09-17):
34 -- - Support setting aspect ratio correction.
36 -- Version v2 by Ilari (2010-09-16):
37 -- - If sound and video NHMLs are wrong way around, automatically swap them.
38 -- - Check that one of the NHMLs is sound and other is video.
40 ----------------------------------------------------------------------------------------------------------------------
41 ----------------------------------------------------------------------------------------------------------------------
43 --Lua 5.2 fix:
44 unpack = unpack or table.unpack;
46 ----------------------------------------------------------------------------------------------------------------------
47 -- Function reduce_fraction(number numerator, number denumerator)
48 -- Returns reduced fraction.
49 ----------------------------------------------------------------------------------------------------------------------
50 reduce_fraction = function(numerator, denumerator)
51 local x, y = numerator, denumerator;
52 while y > 0 do
53 x, y = y, x % y;
54 end
55 return numerator / x, denumerator / x;
56 end
58 ----------------------------------------------------------------------------------------------------------------------
59 -- Function load_timecode_file(FILE file, number timescale)
60 -- Loads timecode data from file @file, using timescale of @timescale frames per second. Returns array of scaled
61 -- timecodes.
62 ----------------------------------------------------------------------------------------------------------------------
63 load_timecode_file = function(file, timescale)
64 local line, ret;
65 line = file:read("*l");
66 if line ~= "# timecode format v2" then
67 error("Timecode file is not in MKV timecodes v2 format");
68 end
69 ret = {};
70 while true do
71 line = file:read("*l");
72 if not line then
73 break;
74 end
75 local timecode = tonumber(line);
76 if not timecode then
77 error("Can't parse timecode '" .. line .. "'.");
78 end
79 table.insert(ret, math.floor(0.5 + timecode / 1000 * timescale));
80 end
81 return ret;
82 end
84 ----------------------------------------------------------------------------------------------------------------------
85 -- Function make_reverse_index_table(Array array)
86 -- Returns table, that has entry for each entry in given array @array with value being rank of value, 1 being smallest
87 -- and #array the largest. If @lookup is non-nil, values are looked up from that array.
88 ----------------------------------------------------------------------------------------------------------------------
89 make_reverse_index_table = function(array, lookup)
90 local sorted, ret;
91 local i;
92 sorted = {};
93 for i = 1,#array do
94 sorted[i] = array[i];
95 end
96 table.sort(sorted);
97 ret = {};
98 for i = 1,#sorted do
99 ret[sorted[i]] = (lookup and lookup[i]) or i;
101 return ret;
104 ----------------------------------------------------------------------------------------------------------------------
105 -- Function max_causality_violaton(Array CTS, Array DTS)
106 -- Return the maximum number of time units CTS and DTS values violate causality. #CTS must equal #DTS.
107 ----------------------------------------------------------------------------------------------------------------------
108 max_causality_violation = function(CTS, DTS)
109 local max_cv = 0;
110 local i;
111 for i = 1,#CTS do
112 max_cv = math.max(max_cv, DTS[i] - CTS[i]);
114 return max_cv;
117 ----------------------------------------------------------------------------------------------------------------------
118 -- Function fixup_video_times(Array sampledata, Array timecodes, Number spec_delay)
119 -- Fixes video timing of @sampledata (fields CTS and DTS) to be consistent with timecodes in @timecodes. Returns the
120 -- CTS offset of first sample (for fixing audio). @spec_delay is special delay to add (to fix A/V sync).
121 ----------------------------------------------------------------------------------------------------------------------
122 fixup_video_times = function(sampledata, timecodes, spec_delay)
123 local cts_tab = {};
124 local dts_tab = {};
125 local k, v, i;
127 if not timecodes then
128 local min_cts = 999999999999999999999;
129 for i = 1,#sampledata do
130 --Maximum causality violation is always zero in valid HHML.
131 sampledata[i].CTS = sampledata[i].CTS + spec_delay;
132 --Spec_delay should not apply to audio.
133 min_cts = math.min(min_cts, sampledata[i].CTS - spec_delay);
135 return min_cts;
138 if #sampledata ~= #timecodes then
139 error("Number of samples (" .. #sampledata .. ") does not match number of timecodes (" .. #timecodes
140 .. ").");
142 for i = 1,#sampledata do
143 cts_tab[i] = sampledata[i].CTS;
144 dts_tab[i] = sampledata[i].DTS;
146 cts_lookup = make_reverse_index_table(cts_tab, timecodes);
147 dts_lookup = make_reverse_index_table(dts_tab, timecodes);
149 -- Perform time translation and find max causality violation.
150 local max_cv = 0;
151 for i = 1,#sampledata do
152 sampledata[i].CTS = cts_lookup[sampledata[i].CTS];
153 sampledata[i].DTS = dts_lookup[sampledata[i].DTS];
154 max_cv = math.max(max_cv, sampledata[i].DTS - sampledata[i].CTS);
156 -- Add maximum causality violation to CTS to eliminate the causality violations.
157 -- Also find the minimum CTS.
158 local min_cts = 999999999999999999999;
159 for i = 1,#sampledata do
160 sampledata[i].CTS = sampledata[i].CTS + max_cv + spec_delay;
161 --Spec_delay should not apply to audio.
162 min_cts = math.min(min_cts, sampledata[i].CTS - spec_delay);
164 return min_cts;
167 ----------------------------------------------------------------------------------------------------------------------
168 -- Function fixup_video_times(Array sampledata, Number min_video_cts, Number video_timescale, Number audio_timescale)
169 -- Fixes video timing of @sampledata (field CTS) to be consistent with video minimum CTS of @cts. Video timescale
170 -- is assumed to be @video_timescale and audio timescale @audio_timescale.
171 ----------------------------------------------------------------------------------------------------------------------
172 fixup_audio_times = function(sampledata, min_video_cts, video_timescale, audio_timescale)
173 local fixup = math.floor(0.5 + min_video_cts * audio_timescale / video_timescale);
174 for i = 1,#sampledata do
175 sampledata[i].CTS = sampledata[i].CTS + fixup;
179 ----------------------------------------------------------------------------------------------------------------------
180 -- Function translate_NHML_TS_in(Array sampledata);
181 -- Translate NHML CTSOffset fields in @sampledata into CTS fields.
182 ----------------------------------------------------------------------------------------------------------------------
183 translate_NHML_TS_in = function(sampledata, default_dDTS)
184 local i;
185 local dts = 0;
186 for i = 1,#sampledata do
187 if not sampledata[i].DTS then
188 sampledata[i].DTS = dts + default_dDTS;
190 dts = sampledata[i].DTS;
191 if sampledata[i].CTSOffset and sampledata[i].CTSOffset < 0 then
192 --Work around MP4Box bug.
193 print("WARNING: Negative CTSOffset, assuming buggy MP4Box");
194 sampledata[i].CTSOffset = sampledata[i].CTSOffset + 4294967296;
196 if sampledata[i].CTSOffset then
197 sampledata[i].CTS = sampledata[i].CTSOffset + sampledata[i].DTS;
198 else
199 sampledata[i].CTS = sampledata[i].DTS;
204 ----------------------------------------------------------------------------------------------------------------------
205 -- Function translate_NHML_TS_out(Array sampledata);
206 -- Translate CTS fields in @sampledata into NHML CTSOffset fields.
207 ----------------------------------------------------------------------------------------------------------------------
208 translate_NHML_TS_out = function(sampledata)
209 local i;
210 for i = 1,#sampledata do
211 sampledata[i].CTSOffset = sampledata[i].CTS - sampledata[i].DTS;
212 if sampledata[i].CTSOffset < 0 then
213 error("INTERNAL ERROR: translate_NHML_TS_out: Causality violation: CTS=" .. tostring(
214 sampledata[i].CTS) .. " DTS=" .. tostring(sampledata[i].DTS) .. ".");
216 sampledata[i].CTS = nil;
220 ----------------------------------------------------------------------------------------------------------------------
221 -- Function map_table_to_number(Table tab);
222 -- Translate all numeric fields in table @tab into numbers.
223 ----------------------------------------------------------------------------------------------------------------------
224 map_table_to_number = function(tab)
225 local k, v;
226 for k, v in pairs(tab) do
227 local n = tonumber(v);
228 if n then
229 tab[k] = n;
234 ----------------------------------------------------------------------------------------------------------------------
235 -- Function map_fields_to_number(Array sampledata);
236 -- Translate all numeric fields in array @sampledata into numbers.
237 ----------------------------------------------------------------------------------------------------------------------
238 map_fields_to_number = function(sampledata)
239 local i;
240 for i = 1,#sampledata do
241 map_table_to_number(sampledata[i]);
245 ----------------------------------------------------------------------------------------------------------------------
246 -- Function escape_xml_text(String str)
247 -- Return XML escaping of text str.
248 ----------------------------------------------------------------------------------------------------------------------
249 escape_xml_text = function(str)
250 str = string.gsub(str, "&", "&amp;");
251 str = string.gsub(str, "<", "&lt;");
252 str = string.gsub(str, ">", "&gt;");
253 str = string.gsub(str, "\"", "&quot;");
254 str = string.gsub(str, "\'", "&apos;");
255 return str;
258 ----------------------------------------------------------------------------------------------------------------------
259 -- Function escape_xml_text(String str)
260 -- Return XML unescaping of text str.
261 ----------------------------------------------------------------------------------------------------------------------
262 unescape_xml_text = function(str)
263 str = string.gsub(str, "&apos;", "\'");
264 str = string.gsub(str, "&quot;", "\"");
265 str = string.gsub(str, "&gt;", ">");
266 str = string.gsub(str, "&lt;", "<");
267 str = string.gsub(str, "&amp;", "&");
268 return str;
271 ----------------------------------------------------------------------------------------------------------------------
272 -- Function serialize_table_to_xml_entity(File file, String tag, Table data, bool noclose);
273 -- Write @data as XML start tag of type @tag into @file. If noclose is true, then tag will not be closed.
274 ----------------------------------------------------------------------------------------------------------------------
275 serialize_table_to_xml_entity = function(file, tag, data, noclose)
276 local k, v;
277 file:write("<" .. tag .. " ");
278 for k, v in pairs(data) do
279 file:write(k .. "=\"" .. escape_xml_text(tostring(v)) .. "\" ");
281 if noclose then
282 file:write(">\n");
283 else
284 file:write("/>\n");
288 ----------------------------------------------------------------------------------------------------------------------
289 -- Function serialize_array_to_xml_entity(File file, String tag, Array data);
290 -- Write each element of @data as empty XML tag of type @tag into @file.
291 ----------------------------------------------------------------------------------------------------------------------
292 serialize_array_to_xml_entity = function(file, tag, data)
293 local i;
294 for i = 1,#data do
295 serialize_table_to_xml_entity(file, tag, data[i]);
299 ----------------------------------------------------------------------------------------------------------------------
300 -- Function write_NHML_data(File file, Table header, Table sampledata)
301 -- Write entiere NHML file.
302 ----------------------------------------------------------------------------------------------------------------------
303 write_NHML_data = function(file, header, sampledata)
304 file:write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n");
305 serialize_table_to_xml_entity(file, "NHNTStream", header, true);
306 serialize_array_to_xml_entity(file, "NHNTSample", sampledata);
307 file:write("</NHNTStream>\n");
310 ----------------------------------------------------------------------------------------------------------------------
311 -- Function open_file_checked(String file, String mode, bool for_write)
312 -- Return file handle to file (checking that open succeeds).
313 ----------------------------------------------------------------------------------------------------------------------
314 open_file_checked = function(file, mode, for_write)
315 local a, b;
316 a, b = io.open(file, mode);
317 if not a then
318 error("Can't open '" .. file .. "': " .. b);
320 return a;
323 ----------------------------------------------------------------------------------------------------------------------
324 -- Function call_with_file(fun, string file, String mode, ...);
325 -- Call fun with opened file handle to @file (in mode @mode) as first parameter.
326 ----------------------------------------------------------------------------------------------------------------------
327 call_with_file = function(fun, file, mode, ...)
328 -- FIXME: Handle nils returned from function.
329 local handle = open_file_checked(file, mode);
330 local ret = {fun(handle, ...)};
331 handle:close();
332 return unpack(ret);
335 ----------------------------------------------------------------------------------------------------------------------
336 -- Function xml_parse_tag(String line);
337 -- Returns the xml tag type for @line plus table of attributes.
338 ----------------------------------------------------------------------------------------------------------------------
339 xml_parse_tag = function(line)
340 -- More regexping...
341 local tagname;
342 local attr = {};
343 tagname = string.match(line, "<(%S+).*>");
344 if not tagname then
345 error("'" .. line .. "': Parse error.");
347 local k, v;
348 for k, v in string.gmatch(line, "([^ =]+)=\"([^\"]*)\"") do
349 attr[k] = unescape_xml_text(v);
351 return tagname, attr;
354 ----------------------------------------------------------------------------------------------------------------------
355 -- Function load_NHML(File file)
356 -- Loads NHML file @file. Returns header table and samples array.
357 ----------------------------------------------------------------------------------------------------------------------
358 load_NHML = function(file)
359 -- Let's regexp this shit...
360 local header = {};
361 local samples = {};
362 while true do
363 local line = file:read();
364 if not line then
365 error("Unexpected end of NHML file.");
367 local xtag, attributes;
368 xtag, attributes = xml_parse_tag(line);
369 if xtag == "NHNTStream" then
370 header = attributes;
371 elseif xtag == "NHNTSample" then
372 table.insert(samples, attributes);
373 elseif xtag == "/NHNTStream" then
374 break;
375 elseif xtag == "?xml" then
376 else
377 print("WARNING: Unrecognized tag '" .. xtag .. "'.");
380 return header, samples;
383 ----------------------------------------------------------------------------------------------------------------------
384 -- Function reame_errcheck(String old, String new)
385 -- Rename old to new. With error checking.
386 ----------------------------------------------------------------------------------------------------------------------
387 rename_errcheck = function(old, new, backup)
388 local a, b;
389 os.remove(backup);
390 a, b = os.rename(new, backup);
391 if not a then
392 error("Can't rename '" .. new .. "' -> '" .. backup .. "': " .. b);
394 a, b = os.rename(old, new);
395 if not a then
396 error("Can't rename '" .. old .. "' -> '" .. new .. "': " .. b);
400 ----------------------------------------------------------------------------------------------------------------------
401 -- Function compute_max_div(Integer ctsBound, Integer timescale, Integer maxCode, pictureOffset)
402 -- Compute maximum allowable timescale.
403 ----------------------------------------------------------------------------------------------------------------------
404 compute_max_div = function(ctsBound, timeScale, maxCode, pictureOffset)
405 -- Compute the logical number of frames.
406 local logicalFrames = ctsBound / pictureOffset;
407 local maxNumerator = math.floor(maxCode / logicalFrames);
408 -- Be conservative and assume numerator is rounded up. That is, solve the biggest maxdiv such that for all
409 -- 1 <= x <= maxdiv, maxNumerator >= ceil(x * pictureOffset / timeScale) is true.
410 -- Since maxNumerator is integer, this is equivalent to:
411 -- maxNumerator >= x * pictureOffset / timeScale
412 -- => maxNumerator * timeScale / pictureOffset >= x, thus
413 -- maxDiv = math.floor(maxNumerator * timeScale / pictureOffset);
414 return math.floor(maxNumerator * timeScale / pictureOffset);
417 ----------------------------------------------------------------------------------------------------------------------
418 -- Function rational_approximate(Integer origNum, Integer origDenum, Integer maxDenum)
419 -- Approximate origNum / origDenum using rational with maximum denumerator of maxDenum
420 ----------------------------------------------------------------------------------------------------------------------
421 rational_approximate = function(origNum, origDenum, maxDenum)
422 -- FIXME: Better approximations are possible.
423 local div = math.ceil(origDenum / maxDenum);
424 return math.floor(0.5 + origNum / div), math.floor(0.5 + origDenum / div);
427 ----------------------------------------------------------------------------------------------------------------------
428 -- Function fixup_mp4box_bug_cfr(Table header, Table samples, Integer pictureOffset, Integer maxdiv)
429 -- Fix MP4Box timecode bug for CFR video by approximating the framerate a bit.
430 ----------------------------------------------------------------------------------------------------------------------
431 fixup_mp4box_bug_cfr = function(header, samples, pictureOffset, maxdiv)
432 local oNum, oDenum;
433 local nNum, nDenum;
434 local i;
435 oNum, oDenum = pictureOffset, header.timeScale;
436 nNum, nDenum = rational_approximate(oNum, oDenum, maxdiv);
437 header.timeScale = nDenum;
438 for i = 1, #samples do
439 samples[i].DTS = math.floor(0.5 + samples[i].DTS / oNum * nNum);
440 samples[i].CTS = math.floor(0.5 + samples[i].CTS / oNum * nNum);
445 if #arg < 3 then
446 error("Syntax: NHMLFixup.lua <video.nhml> <audio.nhml> <timecodes.txt> [delay=<delay>] [tvaspect|widescreen]");
449 -- Load the NHML files.
450 io.stdout:write("Loading '" .. arg[1] .. "'..."); io.stdout:flush();
451 video_header, video_samples = call_with_file(load_NHML, arg[1], "r");
452 io.stdout:write("Done.\n");
453 io.stdout:write("Loading '" .. arg[2] .. "'..."); io.stdout:flush();
454 audio_header, audio_samples = call_with_file(load_NHML, arg[2], "r");
455 io.stdout:write("Done.\n");
456 io.stdout:write("String to number conversion on video header..."); io.stdout:flush();
457 map_table_to_number(video_header);
458 io.stdout:write("Done.\n");
459 io.stdout:write("String to number conversion on video samples..."); io.stdout:flush();
460 map_fields_to_number(video_samples);
461 io.stdout:write("Done.\n");
462 io.stdout:write("String to number conversion on audio header..."); io.stdout:flush();
463 map_table_to_number(audio_header);
464 io.stdout:write("Done.\n");
465 io.stdout:write("String to number conversion on audio samples..."); io.stdout:flush();
466 map_fields_to_number(audio_samples);
467 io.stdout:write("Done.\n");
468 if video_header.streamType == 4 and audio_header.streamType == 5 then
469 -- Ok.
470 elseif video_header.streamType == 5 and audio_header.streamType == 4 then
471 print("WARNING: You got audio and video wrong way around. Swapping them for you...");
472 audio_header,audio_samples,arg[2],video_header,video_samples,arg[1] =
473 video_header,video_samples,arg[1],audio_header,audio_samples,arg[2];
474 else
475 error("Expected one video track and one audio track");
478 if video_header.trackID == audio_header.trackID then
479 print("WARNING: Audio and video have the same track id. Assigning new track id to audio track...");
480 audio_header.trackID = audio_header.trackID + 1;
483 io.stdout:write("Computing CTS for video samples..."); io.stdout:flush();
484 translate_NHML_TS_in(video_samples, video_header.DTS_increment or 0);
485 io.stdout:write("Done.\n");
486 io.stdout:write("Computing CTS for audio samples..."); io.stdout:flush();
487 translate_NHML_TS_in(audio_samples, audio_header.DTS_increment or 0);
488 io.stdout:write("Done.\n");
490 -- Alter timescale if needed and load the timecode data.
491 delay = 0;
492 rdelay = 0;
493 for i = 4,#arg do
494 if arg[i] == "tvaspect" then
495 do_aspect_fixup = 1;
496 elseif arg[i] == "widescreen" then
497 do_aspect_fixup = 2;
498 elseif string.sub(arg[i], 1, 6) == "delay=" then
499 local n = tonumber(string.sub(arg[i], 7, #(arg[i])));
500 if not n then
501 error("Bad delay.");
503 rdelay = n;
504 delay = math.floor(0.5 + rdelay / 1000 * video_header.timeScale);
509 MAX_MP4BOX_TIMECODE = 0x7FFFFFF;
510 if arg[3] ~= "@CFR" then
511 timecode_data = call_with_file(load_timecode_file, arg[3], "r", video_header.timeScale);
512 if timecode_data[#timecode_data] > MAX_MP4BOX_TIMECODE then
513 -- Workaround MP4Box bug.
514 divider = math.ceil(timecode_data[#timecode_data] / MAX_MP4BOX_TIMECODE);
515 print("Notice: Dividing timecodes by " .. divider .. " to workaround MP4Box timecode bug.");
516 io.stdout:write("Performing division..."); io.stdout:flush();
517 video_header.timeScale = math.floor(0.5 + video_header.timeScale / divider);
518 for i = 1,#timecode_data do
519 timecode_data[i] = math.floor(0.5 + timecode_data[i] / divider);
521 --Recompute delay.
522 delay = math.floor(0.5 + rdelay / 1000 * video_header.timeScale);
523 io.stdout:write("Done.\n");
525 else
526 timecode_data = nil;
527 local maxCTS = 0;
528 local i;
529 local DTSOffset = (video_samples[2] or video_samples[1]).DTS - video_samples[1].DTS;
530 if DTSOffset == 0 then
531 DTSOffset = 1;
533 for i = 1,#video_samples do
534 if video_samples[i].CTS > maxCTS then
535 maxCTS = video_samples[i].CTS;
537 if video_samples[i].DTS % DTSOffset ~= 0 then
538 error("Video is not CFR");
540 if (video_samples[i].CTS - video_samples[1].CTS) % DTSOffset ~= 0 then
541 error("Video is not CFR");
544 if video_samples[#video_samples].CTS > MAX_MP4BOX_TIMECODE then
545 --Workaround MP4Box bug.
546 local maxdiv = compute_max_div(maxCTS, video_header.timeScale, MAX_MP4BOX_TIMECODE, DTSOffset);
547 print("Notice: Restricting denumerator to " .. maxdiv .. " to workaround MP4Box timecode bug.");
548 io.stdout:write("Fixing timecodes..."); io.stdout:flush();
549 fixup_mp4box_bug_cfr(video_header, video_samples, DTSOffset, maxdiv);
550 --Recompute delay.
551 delay = math.floor(0.5 + rdelay / 1000 * video_header.timeScale);
552 io.stdout:write("Done.\n");
556 -- Do the actual fixup.
557 io.stdout:write("Fixing up video timecodes..."); io.stdout:flush();
558 audio_fixup = fixup_video_times(video_samples, timecode_data, delay);
559 io.stdout:write("Done.\n");
560 io.stdout:write("Fixing up audio timecodes..."); io.stdout:flush();
561 fixup_audio_times(audio_samples, audio_fixup, video_header.timeScale, audio_header.timeScale);
562 io.stdout:write("Done.\n");
564 if do_aspect_fixup == 1 then
565 video_header.parNum, video_header.parDen = reduce_fraction(4 * video_header.height, 3 * video_header.width);
567 if do_aspect_fixup == 2 then
568 video_header.parNum, video_header.parDen = reduce_fraction(16 * video_header.height, 9 * video_header.width);
571 -- Save the NHML files.
572 io.stdout:write("Computing CTSOffset for video samples..."); io.stdout:flush();
573 translate_NHML_TS_out(video_samples);
574 io.stdout:write("Done.\n");
575 io.stdout:write("Computing CTSOffset for audio samples..."); io.stdout:flush();
576 translate_NHML_TS_out(audio_samples);
577 io.stdout:write("Done.\n");
578 io.stdout:write("Saving '" .. arg[1] .. ".tmp'..."); io.stdout:flush();
579 call_with_file(write_NHML_data, arg[1] .. ".tmp", "w", video_header, video_samples);
580 io.stdout:write("Done.\n");
581 io.stdout:write("Saving '" .. arg[2] .. ".tmp'..."); io.stdout:flush();
582 call_with_file(write_NHML_data, arg[2] .. ".tmp", "w", audio_header, audio_samples);
583 io.stdout:write("Done.\n");
584 io.stdout:write("Renaming '" .. arg[1] .. ".tmp' -> '" .. arg[1] .. "'..."); io.stdout:flush();
585 rename_errcheck(arg[1] .. ".tmp", arg[1], arg[1] .. ".bak");
586 io.stdout:write("Done.\n");
587 io.stdout:write("Renaming '" .. arg[2] .. ".tmp' -> '" .. arg[2] .. "'..."); io.stdout:flush();
588 rename_errcheck(arg[2] .. ".tmp", arg[2], arg[2] .. ".bak");
589 io.stdout:write("Done.\n");
590 io.stdout:write("All done.\n");