General midi save as .MID support
[jpcrr.git] / scripts / NHMLFixup.lua
blob57862cfedd43eaea9ad61d5c28d1f89275b4d642
1 #!/usr/bin/env lua
2 ----------------------------------------------------------------------------------------------------------------------
3 ----------------------------------------------------------------------------------------------------------------------
4 -- NHMLFixup v8 by Ilari (2010-12-06).
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]
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.
11 -- Version v8 by Ilari (2010-12-06):
12 -- - Support Special timecode file "@CFR" that fixes up audio for CFR encode.
14 -- Version v7 by Ilari (2010-10-24):
15 -- - Fix bug in time division (use integer timestamps, not decimal ones).
17 -- Version v6 by Ilari (2010-10-24):
18 -- - Make it work on Lua 5.2 (work 4).
20 -- Version v5 by Ilari (2010-09-18):
21 -- - Move the files first out of way, since rename-over fails on Windows.
23 -- Version v4 by Ilari (2010-09-17):
24 -- - Change audio track ID if it collides with video track..
26 -- Version v3 by Ilari (2010-09-17):
27 -- - Support setting aspect ratio correction.
29 -- Version v2 by Ilari (2010-09-16):
30 -- - If sound and video NHMLs are wrong way around, automatically swap them.
31 -- - Check that one of the NHMLs is sound and other is video.
33 ----------------------------------------------------------------------------------------------------------------------
34 ----------------------------------------------------------------------------------------------------------------------
36 --Lua 5.2 fix:
37 unpack = unpack or table.unpack;
39 ----------------------------------------------------------------------------------------------------------------------
40 -- Function reduce_fraction(number numerator, number denumerator)
41 -- Returns reduced fraction.
42 ----------------------------------------------------------------------------------------------------------------------
43 reduce_fraction = function(numerator, denumerator)
44 local x, y = numerator, denumerator;
45 while y > 0 do
46 x, y = y, x % y;
47 end
48 return numerator / x, denumerator / x;
49 end
51 ----------------------------------------------------------------------------------------------------------------------
52 -- Function load_timecode_file(FILE file, number timescale)
53 -- Loads timecode data from file @file, using timescale of @timescale frames per second. Returns array of scaled
54 -- timecodes.
55 ----------------------------------------------------------------------------------------------------------------------
56 load_timecode_file = function(file, timescale)
57 local line, ret;
58 line = file:read("*l");
59 if line ~= "# timecode format v2" then
60 error("Timecode file is not in MKV timecodes v2 format");
61 end
62 ret = {};
63 while true do
64 line = file:read("*l");
65 if not line then
66 break;
67 end
68 local timecode = tonumber(line);
69 if not timecode then
70 error("Can't parse timecode '" .. line .. "'.");
71 end
72 table.insert(ret, math.floor(0.5 + timecode / 1000 * timescale));
73 end
74 return ret;
75 end
77 ----------------------------------------------------------------------------------------------------------------------
78 -- Function make_reverse_index_table(Array array)
79 -- Returns table, that has entry for each entry in given array @array with value being rank of value, 1 being smallest
80 -- and #array the largest. If @lookup is non-nil, values are looked up from that array.
81 ----------------------------------------------------------------------------------------------------------------------
82 make_reverse_index_table = function(array, lookup)
83 local sorted, ret;
84 local i;
85 sorted = {};
86 for i = 1,#array do
87 sorted[i] = array[i];
88 end
89 table.sort(sorted);
90 ret = {};
91 for i = 1,#sorted do
92 ret[sorted[i]] = (lookup and lookup[i]) or i;
93 end
94 return ret;
95 end
97 ----------------------------------------------------------------------------------------------------------------------
98 -- Function max_causality_violaton(Array CTS, Array DTS)
99 -- Return the maximum number of time units CTS and DTS values violate causality. #CTS must equal #DTS.
100 ----------------------------------------------------------------------------------------------------------------------
101 max_causality_violation = function(CTS, DTS)
102 local max_cv = 0;
103 local i;
104 for i = 1,#CTS do
105 max_cv = math.max(max_cv, DTS[i] - CTS[i]);
107 return max_cv;
110 ----------------------------------------------------------------------------------------------------------------------
111 -- Function fixup_video_times(Array sampledata, Array timecodes, Number spec_delay)
112 -- Fixes video timing of @sampledata (fields CTS and DTS) to be consistent with timecodes in @timecodes. Returns the
113 -- CTS offset of first sample (for fixing audio). @spec_delay is special delay to add (to fix A/V sync).
114 ----------------------------------------------------------------------------------------------------------------------
115 fixup_video_times = function(sampledata, timecodes, spec_delay)
116 local cts_tab = {};
117 local dts_tab = {};
118 local k, v, i;
120 if not timecodes then
121 local min_cts = 999999999999999999999;
122 for i = 1,#sampledata do
123 --Maximum causality violation is always zero in valid HHML.
124 sampledata[i].CTS = sampledata[i].CTS + spec_delay;
125 --Spec_delay should not apply to audio.
126 min_cts = math.min(min_cts, sampledata[i].CTS - spec_delay);
128 return min_cts;
131 if #sampledata ~= #timecodes then
132 error("Number of samples (" .. #sampledata .. ") does not match number of timecodes (" .. #timecodes
133 .. ").");
135 for i = 1,#sampledata do
136 cts_tab[i] = sampledata[i].CTS;
137 dts_tab[i] = sampledata[i].DTS;
139 cts_lookup = make_reverse_index_table(cts_tab, timecodes);
140 dts_lookup = make_reverse_index_table(dts_tab, timecodes);
142 -- Perform time translation and find max causality violation.
143 local max_cv = 0;
144 for i = 1,#sampledata do
145 sampledata[i].CTS = cts_lookup[sampledata[i].CTS];
146 sampledata[i].DTS = dts_lookup[sampledata[i].DTS];
147 max_cv = math.max(max_cv, sampledata[i].DTS - sampledata[i].CTS);
149 -- Add maximum causality violation to CTS to eliminate the causality violations.
150 -- Also find the minimum CTS.
151 local min_cts = 999999999999999999999;
152 for i = 1,#sampledata do
153 sampledata[i].CTS = sampledata[i].CTS + max_cv + spec_delay;
154 --Spec_delay should not apply to audio.
155 min_cts = math.min(min_cts, sampledata[i].CTS - spec_delay);
157 return min_cts;
160 ----------------------------------------------------------------------------------------------------------------------
161 -- Function fixup_video_times(Array sampledata, Number min_video_cts, Number video_timescale, Number audio_timescale)
162 -- Fixes video timing of @sampledata (field CTS) to be consistent with video minimum CTS of @cts. Video timescale
163 -- is assumed to be @video_timescale and audio timescale @audio_timescale.
164 ----------------------------------------------------------------------------------------------------------------------
165 fixup_audio_times = function(sampledata, min_video_cts, video_timescale, audio_timescale)
166 local fixup = math.floor(0.5 + min_video_cts * audio_timescale / video_timescale);
167 for i = 1,#sampledata do
168 sampledata[i].CTS = sampledata[i].CTS + fixup;
172 ----------------------------------------------------------------------------------------------------------------------
173 -- Function translate_NHML_TS_in(Array sampledata);
174 -- Translate NHML CTSOffset fields in @sampledata into CTS fields.
175 ----------------------------------------------------------------------------------------------------------------------
176 translate_NHML_TS_in = function(sampledata, default_dDTS)
177 local i;
178 local dts = 0;
179 for i = 1,#sampledata do
180 if not sampledata[i].DTS then
181 sampledata[i].DTS = dts + default_dDTS;
183 dts = sampledata[i].DTS;
184 if sampledata[i].CTSOffset then
185 sampledata[i].CTS = sampledata[i].CTSOffset + sampledata[i].DTS;
186 else
187 sampledata[i].CTS = sampledata[i].DTS;
192 ----------------------------------------------------------------------------------------------------------------------
193 -- Function translate_NHML_TS_out(Array sampledata);
194 -- Translate CTS fields in @sampledata into NHML CTSOffset fields.
195 ----------------------------------------------------------------------------------------------------------------------
196 translate_NHML_TS_out = function(sampledata)
197 local i;
198 for i = 1,#sampledata do
199 sampledata[i].CTSOffset = sampledata[i].CTS - sampledata[i].DTS;
200 if sampledata[i].CTSOffset < 0 then
201 error("INTERNAL ERROR: translate_NHML_TS_out: Causality violation: CTS=" .. tostring(
202 sampledata[i].CTS) .. " DTS=" .. tostring(sampledata[i].DTS) .. ".");
204 sampledata[i].CTS = nil;
208 ----------------------------------------------------------------------------------------------------------------------
209 -- Function map_table_to_number(Table tab);
210 -- Translate all numeric fields in table @tab into numbers.
211 ----------------------------------------------------------------------------------------------------------------------
212 map_table_to_number = function(tab)
213 local k, v;
214 for k, v in pairs(tab) do
215 local n = tonumber(v);
216 if n then
217 tab[k] = n;
222 ----------------------------------------------------------------------------------------------------------------------
223 -- Function map_fields_to_number(Array sampledata);
224 -- Translate all numeric fields in array @sampledata into numbers.
225 ----------------------------------------------------------------------------------------------------------------------
226 map_fields_to_number = function(sampledata)
227 local i;
228 for i = 1,#sampledata do
229 map_table_to_number(sampledata[i]);
233 ----------------------------------------------------------------------------------------------------------------------
234 -- Function escape_xml_text(String str)
235 -- Return XML escaping of text str.
236 ----------------------------------------------------------------------------------------------------------------------
237 escape_xml_text = function(str)
238 str = string.gsub(str, "&", "&amp;");
239 str = string.gsub(str, "<", "&lt;");
240 str = string.gsub(str, ">", "&gt;");
241 str = string.gsub(str, "\"", "&quot;");
242 str = string.gsub(str, "\'", "&apos;");
243 return str;
246 ----------------------------------------------------------------------------------------------------------------------
247 -- Function escape_xml_text(String str)
248 -- Return XML unescaping of text str.
249 ----------------------------------------------------------------------------------------------------------------------
250 unescape_xml_text = function(str)
251 str = string.gsub(str, "&apos;", "\'");
252 str = string.gsub(str, "&quot;", "\"");
253 str = string.gsub(str, "&gt;", ">");
254 str = string.gsub(str, "&lt;", "<");
255 str = string.gsub(str, "&amp;", "&");
256 return str;
259 ----------------------------------------------------------------------------------------------------------------------
260 -- Function serialize_table_to_xml_entity(File file, String tag, Table data, bool noclose);
261 -- Write @data as XML start tag of type @tag into @file. If noclose is true, then tag will not be closed.
262 ----------------------------------------------------------------------------------------------------------------------
263 serialize_table_to_xml_entity = function(file, tag, data, noclose)
264 local k, v;
265 file:write("<" .. tag .. " ");
266 for k, v in pairs(data) do
267 file:write(k .. "=\"" .. escape_xml_text(tostring(v)) .. "\" ");
269 if noclose then
270 file:write(">\n");
271 else
272 file:write("/>\n");
276 ----------------------------------------------------------------------------------------------------------------------
277 -- Function serialize_array_to_xml_entity(File file, String tag, Array data);
278 -- Write each element of @data as empty XML tag of type @tag into @file.
279 ----------------------------------------------------------------------------------------------------------------------
280 serialize_array_to_xml_entity = function(file, tag, data)
281 local i;
282 for i = 1,#data do
283 serialize_table_to_xml_entity(file, tag, data[i]);
287 ----------------------------------------------------------------------------------------------------------------------
288 -- Function write_NHML_data(File file, Table header, Table sampledata)
289 -- Write entiere NHML file.
290 ----------------------------------------------------------------------------------------------------------------------
291 write_NHML_data = function(file, header, sampledata)
292 file:write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n");
293 serialize_table_to_xml_entity(file, "NHNTStream", header, true);
294 serialize_array_to_xml_entity(file, "NHNTSample", sampledata);
295 file:write("</NHNTStream>\n");
298 ----------------------------------------------------------------------------------------------------------------------
299 -- Function open_file_checked(String file, String mode, bool for_write)
300 -- Return file handle to file (checking that open succeeds).
301 ----------------------------------------------------------------------------------------------------------------------
302 open_file_checked = function(file, mode, for_write)
303 local a, b;
304 a, b = io.open(file, mode);
305 if not a then
306 error("Can't open '" .. file .. "': " .. b);
308 return a;
311 ----------------------------------------------------------------------------------------------------------------------
312 -- Function call_with_file(fun, string file, String mode, ...);
313 -- Call fun with opened file handle to @file (in mode @mode) as first parameter.
314 ----------------------------------------------------------------------------------------------------------------------
315 call_with_file = function(fun, file, mode, ...)
316 -- FIXME: Handle nils returned from function.
317 local handle = open_file_checked(file, mode);
318 local ret = {fun(handle, ...)};
319 handle:close();
320 return unpack(ret);
323 ----------------------------------------------------------------------------------------------------------------------
324 -- Function xml_parse_tag(String line);
325 -- Returns the xml tag type for @line plus table of attributes.
326 ----------------------------------------------------------------------------------------------------------------------
327 xml_parse_tag = function(line)
328 -- More regexping...
329 local tagname;
330 local attr = {};
331 tagname = string.match(line, "<(%S+).*>");
332 if not tagname then
333 error("'" .. line .. "': Parse error.");
335 local k, v;
336 for k, v in string.gmatch(line, "([^ =]+)=\"([^\"]*)\"") do
337 attr[k] = unescape_xml_text(v);
339 return tagname, attr;
342 ----------------------------------------------------------------------------------------------------------------------
343 -- Function load_NHML(File file)
344 -- Loads NHML file @file. Returns header table and samples array.
345 ----------------------------------------------------------------------------------------------------------------------
346 load_NHML = function(file)
347 -- Let's regexp this shit...
348 local header = {};
349 local samples = {};
350 while true do
351 local line = file:read();
352 if not line then
353 error("Unexpected end of NHML file.");
355 local xtag, attributes;
356 xtag, attributes = xml_parse_tag(line);
357 if xtag == "NHNTStream" then
358 header = attributes;
359 elseif xtag == "NHNTSample" then
360 table.insert(samples, attributes);
361 elseif xtag == "/NHNTStream" then
362 break;
363 elseif xtag == "?xml" then
364 else
365 print("WARNING: Unrecognized tag '" .. xtag .. "'.");
368 return header, samples;
371 ----------------------------------------------------------------------------------------------------------------------
372 -- Function reame_errcheck(String old, String new)
373 -- Rename old to new. With error checking.
374 ----------------------------------------------------------------------------------------------------------------------
375 rename_errcheck = function(old, new, backup)
376 local a, b;
377 os.remove(backup);
378 a, b = os.rename(new, backup);
379 if not a then
380 error("Can't rename '" .. new .. "' -> '" .. backup .. "': " .. b);
382 a, b = os.rename(old, new);
383 if not a then
384 error("Can't rename '" .. old .. "' -> '" .. new .. "': " .. b);
388 ----------------------------------------------------------------------------------------------------------------------
389 -- Function compute_max_div(Integer ctsBound, Integer timescale, Integer maxCode, pictureOffset)
390 -- Compute maximum allowable timescale.
391 ----------------------------------------------------------------------------------------------------------------------
392 compute_max_div = function(ctsBound, timeScale, maxCode, pictureOffset)
393 -- Compute the logical number of frames.
394 local logicalFrames = ctsBound / pictureOffset;
395 local maxNumerator = math.floor(maxCode / logicalFrames);
396 -- Be conservative and assume numerator is rounded up. That is, solve the biggest maxdiv such that for all
397 -- 1 <= x <= maxdiv, maxNumerator >= ceil(x * pictureOffset / timeScale) is true.
398 -- Since maxNumerator is integer, this is equivalent to:
399 -- maxNumerator >= x * pictureOffset / timeScale
400 -- => maxNumerator * timeScale / pictureOffset >= x, thus
401 -- maxDiv = math.floor(maxNumerator * timeScale / pictureOffset);
402 return math.floor(maxNumerator * timeScale / pictureOffset);
405 ----------------------------------------------------------------------------------------------------------------------
406 -- Function rational_approximate(Integer origNum, Integer origDenum, Integer maxDenum)
407 -- Approximate origNum / origDenum using rational with maximum denumerator of maxDenum
408 ----------------------------------------------------------------------------------------------------------------------
409 rational_approximate = function(origNum, origDenum, maxDenum)
410 -- FIXME: Better approximations are possible.
411 local div = math.ceil(origDenum / maxDenum);
412 return math.floor(0.5 + origNum / div), math.floor(0.5 + origDenum / div);
415 ----------------------------------------------------------------------------------------------------------------------
416 -- Function fixup_mp4box_bug_cfr(Table header, Table samples, Integer pictureOffset, Integer maxdiv)
417 -- Fix MP4Box timecode bug for CFR video by approximating the framerate a bit.
418 ----------------------------------------------------------------------------------------------------------------------
419 fixup_mp4box_bug_cfr = function(header, samples, pictureOffset, maxdiv)
420 local oNum, oDenum;
421 local nNum, nDenum;
422 local i;
423 oNum, oDenum = pictureOffset, header.timeScale;
424 nNum, nDenum = rational_approximate(oNum, oDenum, maxdiv);
425 header.timeScale = nDenum;
426 for i = 1, #samples do
427 samples[i].DTS = math.floor(0.5 + samples[i].DTS / oNum * nNum);
428 samples[i].CTS = math.floor(0.5 + samples[i].CTS / oNum * nNum);
433 if #arg < 3 then
434 error("Syntax: NHMLFixup.lua <video.nhml> <audio.nhml> <timecodes.txt> [delay=<delay>] [tvaspect]");
437 -- Load the NHML files.
438 io.stdout:write("Loading '" .. arg[1] .. "'..."); io.stdout:flush();
439 video_header, video_samples = call_with_file(load_NHML, arg[1], "r");
440 io.stdout:write("Done.\n");
441 io.stdout:write("Loading '" .. arg[2] .. "'..."); io.stdout:flush();
442 audio_header, audio_samples = call_with_file(load_NHML, arg[2], "r");
443 io.stdout:write("Done.\n");
444 io.stdout:write("String to number conversion on video header..."); io.stdout:flush();
445 map_table_to_number(video_header);
446 io.stdout:write("Done.\n");
447 io.stdout:write("String to number conversion on video samples..."); io.stdout:flush();
448 map_fields_to_number(video_samples);
449 io.stdout:write("Done.\n");
450 io.stdout:write("String to number conversion on audio header..."); io.stdout:flush();
451 map_table_to_number(audio_header);
452 io.stdout:write("Done.\n");
453 io.stdout:write("String to number conversion on audio samples..."); io.stdout:flush();
454 map_fields_to_number(audio_samples);
455 io.stdout:write("Done.\n");
456 if video_header.streamType == 4 and audio_header.streamType == 5 then
457 -- Ok.
458 elseif video_header.streamType == 5 and audio_header.streamType == 4 then
459 print("WARNING: You got audio and video wrong way around. Swapping them for you...");
460 audio_header,audio_samples,arg[2],video_header,video_samples,arg[1] =
461 video_header,video_samples,arg[1],audio_header,audio_samples,arg[2];
462 else
463 error("Expected one video track and one audio track");
466 if video_header.trackID == audio_header.trackID then
467 print("WARNING: Audio and video have the same track id. Assigning new track id to audio track...");
468 audio_header.trackID = audio_header.trackID + 1;
471 io.stdout:write("Computing CTS for video samples..."); io.stdout:flush();
472 translate_NHML_TS_in(video_samples, video_header.DTS_increment or 0);
473 io.stdout:write("Done.\n");
474 io.stdout:write("Computing CTS for audio samples..."); io.stdout:flush();
475 translate_NHML_TS_in(audio_samples, audio_header.DTS_increment or 0);
476 io.stdout:write("Done.\n");
478 -- Alter timescale if needed and load the timecode data.
479 delay = 0;
480 rdelay = 0;
481 for i = 4,#arg do
482 if arg[i] == "tvaspect" then
483 do_aspect_fixup = true;
484 elseif string.sub(arg[i], 1, 6) == "delay=" then
485 local n = tonumber(string.sub(arg[i], 7, #(arg[i])));
486 if not n then
487 error("Bad delay.");
489 rdelay = n;
490 delay = math.floor(0.5 + rdelay / 1000 * video_header.timeScale);
495 MAX_MP4BOX_TIMECODE = 0x7FFFFFF;
496 if arg[3] ~= "@CFR" then
497 timecode_data = call_with_file(load_timecode_file, arg[3], "r", video_header.timeScale);
498 if timecode_data[#timecode_data] > MAX_MP4BOX_TIMECODE then
499 -- Workaround MP4Box bug.
500 divider = math.ceil(timecode_data[#timecode_data] / MAX_MP4BOX_TIMECODE);
501 print("Notice: Dividing timecodes by " .. divider .. " to workaround MP4Box timecode bug.");
502 io.stdout:write("Performing division..."); io.stdout:flush();
503 video_header.timeScale = math.floor(0.5 + video_header.timeScale / divider);
504 for i = 1,#timecode_data do
505 timecode_data[i] = math.floor(0.5 + timecode_data[i] / divider);
507 --Recompute delay.
508 delay = math.floor(0.5 + rdelay / 1000 * video_header.timeScale);
509 io.stdout:write("Done.\n");
511 else
512 timecode_data = nil;
513 local maxCTS = 0;
514 local i;
515 local DTSOffset = (video_samples[2] or video_samples[1]).DTS - video_samples[1].DTS;
516 if DTSOffset == 0 then
517 DTSOffset = 1;
519 for i = 1,#video_samples do
520 if video_samples[i].CTS > maxCTS then
521 maxCTS = video_samples[i].CTS;
523 if video_samples[i].DTS % DTSOffset ~= 0 then
524 error("Video is not CFR");
526 if (video_samples[i].CTS - video_samples[1].CTS) % DTSOffset ~= 0 then
527 error("Video is not CFR");
530 if video_samples[#video_samples].CTS > MAX_MP4BOX_TIMECODE then
531 --Workaround MP4Box bug.
532 local maxdiv = compute_max_div(maxCTS, video_header.timeScale, MAX_MP4BOX_TIMECODE, DTSOffset);
533 print("Notice: Restricting denumerator to " .. maxdiv .. " to workaround MP4Box timecode bug.");
534 io.stdout:write("Fixing timecodes..."); io.stdout:flush();
535 fixup_mp4box_bug_cfr(video_header, video_samples, DTSOffset, maxdiv);
536 --Recompute delay.
537 delay = math.floor(0.5 + rdelay / 1000 * video_header.timeScale);
538 io.stdout:write("Done.\n");
542 -- Do the actual fixup.
543 io.stdout:write("Fixing up video timecodes..."); io.stdout:flush();
544 audio_fixup = fixup_video_times(video_samples, timecode_data, delay);
545 io.stdout:write("Done.\n");
546 io.stdout:write("Fixing up audio timecodes..."); io.stdout:flush();
547 fixup_audio_times(audio_samples, audio_fixup, video_header.timeScale, audio_header.timeScale);
548 io.stdout:write("Done.\n");
550 if do_aspect_fixup then
551 video_header.parNum, video_header.parDen = reduce_fraction(4 * video_header.height, 3 * video_header.width);
554 -- Save the NHML files.
555 io.stdout:write("Computing CTSOffset for video samples..."); io.stdout:flush();
556 translate_NHML_TS_out(video_samples);
557 io.stdout:write("Done.\n");
558 io.stdout:write("Computing CTSOffset for audio samples..."); io.stdout:flush();
559 translate_NHML_TS_out(audio_samples);
560 io.stdout:write("Done.\n");
561 io.stdout:write("Saving '" .. arg[1] .. ".tmp'..."); io.stdout:flush();
562 call_with_file(write_NHML_data, arg[1] .. ".tmp", "w", video_header, video_samples);
563 io.stdout:write("Done.\n");
564 io.stdout:write("Saving '" .. arg[2] .. ".tmp'..."); io.stdout:flush();
565 call_with_file(write_NHML_data, arg[2] .. ".tmp", "w", audio_header, audio_samples);
566 io.stdout:write("Done.\n");
567 io.stdout:write("Renaming '" .. arg[1] .. ".tmp' -> '" .. arg[1] .. "'..."); io.stdout:flush();
568 rename_errcheck(arg[1] .. ".tmp", arg[1], arg[1] .. ".bak");
569 io.stdout:write("Done.\n");
570 io.stdout:write("Renaming '" .. arg[2] .. ".tmp' -> '" .. arg[2] .. "'..."); io.stdout:flush();
571 rename_errcheck(arg[2] .. ".tmp", arg[2], arg[2] .. ".bak");
572 io.stdout:write("Done.\n");
573 io.stdout:write("All done.\n");