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 ----------------------------------------------------------------------------------------------------------------------
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
;
55 return numerator
/ x
, denumerator
/ x
;
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
62 ----------------------------------------------------------------------------------------------------------------------
63 load_timecode_file
= function(file
, timescale
)
65 line
= file
:read("*l");
66 if line
~= "# timecode format v2" then
67 error("Timecode file is not in MKV timecodes v2 format");
71 line
= file
:read("*l");
75 local timecode
= tonumber(line
);
77 error("Can't parse timecode '" .. line
.. "'.");
79 table.insert(ret
, math
.floor(0.5 + timecode
/ 1000 * timescale
));
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
)
99 ret
[sorted
[i]]
= (lookup
and lookup
[i
]) or i
;
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
)
112 max_cv
= math
.max(max_cv
, DTS
[i
] - CTS
[i
]);
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
)
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
);
138 if #sampledata
~= #timecodes
then
139 error("Number of samples (" .. #sampledata
.. ") does not match number of timecodes (" .. #timecodes
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.
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
);
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
)
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
;
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
)
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
)
226 for k
, v
in pairs(tab
) do
227 local n
= tonumber(v
);
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
)
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
, "&", "&");
251 str
= string.gsub(str
, "<", "<");
252 str
= string.gsub(str
, ">", ">");
253 str
= string.gsub(str
, "\"", """);
254 str
= string.gsub(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
, "'", "\'");
264 str
= string.gsub(str
, """, "\"");
265 str
= string.gsub(str
, ">", ">");
266 str
= string.gsub(str
, "<", "<");
267 str
= string.gsub(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
)
277 file
:write("<" .. tag .. " ");
278 for k
, v
in pairs(data
) do
279 file
:write(k
.. "=\"" .. escape_xml_text(tostring(v
)) .. "\" ");
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
)
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
)
316 a
, b
= io
.open(file
, mode
);
318 error("Can't open '" .. file
.. "': " .. b
);
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
, ...)};
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
)
343 tagname
= string.match(line
, "<(%S+).*>");
345 error("'" .. line
.. "': Parse error.");
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...
363 local line
= file
:read();
365 error("Unexpected end of NHML file.");
367 local xtag
, attributes
;
368 xtag
, attributes
= xml_parse_tag(line
);
369 if xtag
== "NHNTStream" then
371 elseif xtag
== "NHNTSample" then
372 table.insert(samples
, attributes
);
373 elseif xtag
== "/NHNTStream" then
375 elseif xtag
== "?xml" then
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
)
390 a
, b
= os
.rename(new
, backup
);
392 error("Can't rename '" .. new
.. "' -> '" .. backup
.. "': " .. b
);
394 a
, b
= os
.rename(old
, new
);
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
)
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
);
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
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];
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.
494 if arg
[i
] == "tvaspect" then
496 elseif arg
[i
] == "widescreen" then
498 elseif string.sub(arg
[i
], 1, 6) == "delay=" then
499 local n
= tonumber(string.sub(arg
[i
], 7, #(arg
[i
])));
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
);
522 delay
= math
.floor(0.5 + rdelay
/ 1000 * video_header
.timeScale
);
523 io
.stdout
:write("Done.\n");
529 local DTSOffset
= (video_samples
[2] or video_samples
[1]).DTS
- video_samples
[1].DTS
;
530 if DTSOffset
== 0 then
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
);
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");