Posts for Ilari


Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
As to difficulties in encoding DOS (and this is not counting tools): * Multiple resolutions. 720x400 textmode + 640x400 graphics is common. * Sometimes one also sees 640x480 graphics mode (maybe in combination with 640x400) * Really odd resolutions like 640x398 graphics (Jazz Jackrabbit) * Oh, I have also seen 720x480 graphics (and also some SVGA modes). * Music/SFX balance may be off, requiring adjusting channel mixing. * Frame rate is normally about 70.08630289532294fps, but with those oddball modes, all bets are off. And frame rate can vary.
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
Experiments with listing movies by platform and game name bought lots of inconsistencies in naming movies to light. Some of those have already been fixed, lots aren't. If I make game names unique, strip all non-alphanumerics from game names, translate uppercase letters to lowercase, the following names are present multiple times: * batmanreturnofthejoker * beavisandbutthead * castlevaniacircleofthemoon * castlevaniarondoofblood * contrathehardcorps * kingsquestquestforthecrown * legendofzeldaocarinaoftime * metroidiireturnofsamus * princeofpersia * simpsonsbartvsthespacemutants * spidermanvsthekingpin * supermarioworld2yoshisisland * tinytoonadventuresbustershiddentreasure Most of those are probably case differences, or ": " vs. " - ". Then there is stuff like "Wario Land 2" vs. "Wario Land II". Oh, I earlier extracted this list of all-obsolete games. Some of these entries are actually legit (such as Air, Pokémon Red and Top Gear) and some have been fixed already (like Mission: Impossible). Some of entries seem to match entries of the above list * Air * Aladdin * Beavis and Butt-Head * Blue Shadow * B.O.B. * Contra - The Hard Corps * Donkey Kong Country 2 * Goonies, The * Hitler no Fukkatsu * Hudson's Adventure Island * King's Quest - Quest for the Crown * Kirby's Avalanche * Kirby Squeak Squad * Legend of Zelda 2 * Lion King * Looney Tunes * Olympus no Tatakai * Pokémon Red * Shinobi 3 - Return of the Ninja Master * Simpsons: Bart Vs. the Space Mutants * Spider-Man vs The Kingpin * Super Mario RPG * Tiny Toon Adventures - Buster's Hidden Treasure * Top Gear Comments?
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
Archive mirror: Direct download Collection
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
In case anyone wonders what NO_FPU does: It force-clears BIOS flag that says if machine has FPU (set) or not (clear). BIOS should probe this, but it just assumes the presence of FPU. This tries to trick the game into thinking that the machine has no FPU and fall back to software emulation of the FPU. Makes the game sightly more lag-prone, but: * Software emulation of the FPU is better in quality than the emulated FPU (which can cause graphical glitching or as in this case, completely hang the game). * "sightly more lag-prone" might not actually mean a single actual lag frame. * The game might not use floating point for anything that matters in-game anyway. Apparently there is another way to obtain FPU flag from BIOS, as NO_FPU does not work with all games (especially games using the most common DOS Extenders).
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
Trackmania appears to be windows game on PC. There's this: Thread #8750: Hourglass, for Windows TASing, but it is pretty preliminary... Trackmania also has DS and Wii versions. There is no Wii rerecording yet (not even alpha-quality), so that leaves only the DS...
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
Reduce clutter on Wiki Orphans page a bit: * Hide any page configured as user homepage from wiki orphans list (because all such pages are linked via Homepages list). * Hide any broken link entry from broken links list where referring page is in DeletedPages/ (such things usually aren't interesting).
Post subject: Re: What scares you most about encoding?
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
Flygon wrote:
Idea that came to mind, since I am shocked that more TASers can't encode. I'd like to know why they are so afraid.
Yeah, as Andymac said, it seems too complicated. Just take a look at the encoding guide size. Logo insertion, subtitling and getting audio to sync (even when emulator dumps have perfect A/V sync) aren't trivial things (it took me a lot of time or very hacky stuff to figure that out). Not everybody has a script that can take in dump and some parameters and spew out ready encode after some hours of processing. Well, someone with portable coding experience (that is, not me) could code an application that could perform logo insertion, subtitle insertion, encoding and muxing, complete with previews (in user-friendly manner)... But that would likely be a lot of work...
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
Ability to display all Movie Maintenance Log entries about single movie, filtering the rest out. Right now, if you know the movie you want to see log entries about, finding the entries seems to be almost impossible.
Post subject: Re: Castlevania 1 questions and LF finnish people
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
carmen_electra wrote:
1) Should I do the TAS level by level or play the game thorugh? I mean, when I finish one level, should I try to do it better or continue to next level (and then try to improve level in next run?)
Well, I personally continue to next level and when I have finished all levels, redo the whole run from beginning, constantly comparing to last run and trying to squeeze more frames (usually that proves to be successful). The reason why I tend to do it whole game at a time and not level at a time is that doing it whole game at a time tends to gain more knowledge about tricks and mechanics of the game, as well as giving more time to think about new tricks before actually needing them.
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
ElectroSpecter wrote:
Or in simpler terms, why does x/sqrt(x) equal sqrt(x)?
By the definition of square root, if x >= 0, then y = sqrt(x) <=> (y^2 = x and y >= 0). Now if x >= 0, x/sqrt(x) = y^2 / y = y = sqrt(x). Basically, x is square of sqrt(x), and one pair of sqrt's cancels each other out, leaving just sqrt(x).
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
If its really Windows game (even if it appears to be mostly to be DOS game), then no currently approved emulator can rerecord that. There are some attempts at rerecording Windows games: Thread #8750: Hourglass, for Windows TASing.
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
Comicalflop wrote:
Any game that has acceleration will have sub pixel positioning.
Actually, some games have acceleration but don't seem to have subpixels (DOS Mario&Luigi, DOS Jazz Jackrabbit). At least I didn't see any behavior of position addresses that would indicate presence of subpixels when TASing those games. Also some games measure position in units that are finer than 1 unit per pixel (often units per pixel is some power of two). There are no separate subpixel addresses in that case (DOS Skyroads, DOS Lada: The Ultimate Challenge).
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
eternaljwh wrote:
Why don't you kill more of the blue cars by ramming? You seem to avoid them far, far more than the red ones.
The reason for that is that places where one could send blue vehicles (I think they are actually tractors) to their doom by collisions are very rare. I don't know the exact cause (perhaps it is slow speed given to them, as speed limits also apply horizontally) but if you slam a blue vehicle, it is pushed quite little, far less than any other type of vehicle with exception of motorcycle. Most vehicles push very nicely. With motorcycles, the same thing about pushing is true, but at least those have so little hit points that another vehicle is enough to kill it after damage I do to it by slamming. This limits tractor slamming to mostly pushing them to path of another vehicle that would cause grief later. As for luck manipulation purposes, pushing vehicle to its doom uses the same amount of RNG numbers as destroying it with bullets. It might not affect the luck in same way however, because game might throw the RNG for random spawn and timer to next spawn (might not actually spawn anything) between what would be last moment to kill it with weapons normally and when it dies when pushing it to its doom.
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
Patashu wrote:
Is it worth showing off the secret level?
I don't think so. That level is just running, collecting score bonuses, long and it has no enemies. It has fast feet bonus, but is seemingly so long that speed bonus will expire mid-level.
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
Xarthok wrote:
Does Jazz move faster than any type of bullets travel? Is it maybe faster to use the blue launcher to trigger end of level sign earlier at 1:40?
I don't think so. The game performs very slow screen scroll to correct position after the exit sign is hit before it starts score countdown. So one should be as close as to correct screen position as possible when hitting the exit sign. And yes, I did optimize it so that score countdown started at earliest possible moment (that I could do). The same thing is reason why I don't blast the exit sign in episode 1 level 2 (Diamondus 2) from distance using toaster. And if I wanted to hit it real early, I would have used TNT (at that point, Jazz had four of them). TNT can hit exit signs even outside the screen (that's the way you enter the secret level in Holiday Hare '94). Edit: It is possible to hit the exit sign in level 2 from before last warp point using TNT, saving about 200 frames.
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
Game: Jazz Jackrabbit Holiday Hare '94 Platform: DOS Author: Me Progress: First complete version (needs to be optimized) Discussion: Thread #9447: Jazz Jackrabbit Link to video
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
I did first draft WIP (to end of the game) of Jazz Jackrabbit Holiday Hare '94 (there's also '95 version, that has totally different levels) Link to video This is first WIP and as such, I'll need to redo it at least once before I consider it submittable. And regarding killing that flying demon with fire shield, I have no idea why I did that. Also, after warping in level 3, Jazz appears to lose the fire shield for some reason... Well, I temporarily get something even better in exchange: Full immunity to damage (expires in few seconds with fire shield returning)
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
hopper wrote:
Speaking of versions, is this 1.0, 1.1, or 1.3?
I think it is v1.0 (VENDOR.DOC claims so, I haven't found version number from other places).
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
Actually, this game does have running (hold right control to run, keycode 157) and I do use it all the time... Walking is even slower. Yeah, the running speed (in the final room, running speed increases by 50%, and that is kept for 2nd quest) is bit slow.
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
Changelog summary going from r10.x to r11.2 (some major features, but not so much on game compat department): Implemented: - Lockout odd/even when in graphics mode (Fixes some gfx corruption issues). - Reduce number of windows used (now only main + keyboard + lua). - Implement splitline scroll reset (fixes statusbar jerking in some games). - Event stream access from Lua. - Savestate load via drag'n'drop. - Loadstate from Lua actually works instead of hanging the emu. - Complete Lua math library. - Create windows from within Lua (handy for memory watching). - Memory search. - Allow waiting all types of events at once within Lua. - Rewrite dumping code from ground up (with fair amount of second-system effect). - Raw dumps (which there is only one file now) can be played back in realtime without encoding to AVI. - Built-in font for drawn text (no more messing with font files). - Subtitle placeholders for stuff like movie length and rerecord count (computed by emulator itself). - Resolution-dependent scalers for dump to raw video conversion (mainly for HD encoding). - New VGA code supporting real VGA timings. - Monitor panel clicks get sent to Lua (e.g. for mouse rerecording).
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
Doesn't seem to be just you, I am seeding some torrents and all using TASVideos tracker are listed as "<Tracker> not working: Connection refused" (I'm using Qbittorrent). One of torrents is using different tracker and it works.
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
creaothceann wrote:
Btw. NTSC is 60/1.001 fields per second, which becomes frames per second due to the console's video signal. I'd think the emulator would match that framerate?
There are two matchings here: * The emulator needs to match framerate of console (as close as possible) * The console needs to near-match framerate of whatever relevant TV standard (in this case NTSC). The first match should be as exact as possible because it is emulation quality matter. The later match does not have to be exact. real TVs don't require exact rate match, they will sync if frequency is near enough. Too far and sync might be lost. So when watched on real NTSC TV, NES output really does have field rate of sightly over 60fps.
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
ogurets wrote:
Anyway, could you give me some links to these "multimedia TAS" videos? I'd be happy to see them!
"Multimedia" TASes take video each individual linear time segment in timeline and concatenate those into final video. You still need equivalent of savestates and verification is almost impossible (but you don't need input to play exactly same way every time). The only high-profile example I recall was Bisqwit's Chrono Cross TAS (9 hours!). There are also gamecube TASes (I think mostly partial demonstrations) produced that way (before the breakthrough with re-recording in Dolphin). And to highlight importance of verfication, there have been actual instances of video editing to make run faster than it really is (its difficult to tell if single frame has been dropped). BTW: This might be interesting: http://tasvideos.org/forum/viewtopic.php?t=8750
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
Based on the description, that sounds like "multimedia" TASing. "Real" TASing (as is done on this site) has these three elements: * Frame advance (step one frame at time; although one could do with extreme slowdown, frame advance is far preferable). * Savestates (point in time to return to, if needed) * Input movie (records the input given to game in linear time, can be played back later, even on different computer). The end result is sequence of input given to game, in linear time. This button sequence is far easier to check for cheating than video recording.
Emulator Coder, Experienced Forum User, Published Author, Skilled player (1142)
Joined: 5/1/2010
Posts: 1217
Here is bit newer version: Changes: - Check that one of the NHMLs is sound and one is video (one can extract wrong track if one isn't careful). - Add option to do aspect ratio correction to 4:3 DAR. - Don't attempt rename-overs (because windows doesn't like them). - Remove extra newline from beginning (breaks Lua script!) - Make it work on Lua5.2-work4 - Fix time division to actually produce integer timestamps, not decimal ones. - CFR audio fixup mode - Widescreen support Warning: The save button saves version that has CRLF line endings, so run DOS to UNIX line ending conversion before setting it +x (only appicable to Unix). Download NHMLFixup-v12.lua
Language: lua

#!/usr/bin/env lua ---------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------- -- NHMLFixup v12 by Ilari (2012-04-09). -- Update timecodes in NHML Audio/Video track timing to conform to given MKV v2 timecodes file. -- Syntax: NHMLFixup <video-nhml-file> <audio-nhml-file> <mkv-timecodes-file> [delay=<delay>] [tvaspect|widescreen] -- -- <delay> is number of milliseconds to delay the video (in order to compensate for audio codec delay, reportedly -- does not work right with some demuxers). -- The 'tvaspect' option makes video track to be automatically adjusted to '4:3' aspect ratio. -- The 'widescreen' option makes video track to be automatically adjusted to '16:9' aspect ratio. -- <frame> is frame to force overflow correction on. -- -- Version v12 by Ilari (2012-04-09): -- - Complain about CTSOffset errors. -- -- Version v11 by Ilari (2011-12-18): -- - Add option to force overflow correction on given frame. -- -- Version v10 by Ilari (2011-08-16): -- - Work around MP4Box bug resulting in negative CTSOffset. -- -- Version v9 by Ilari (2011-04-01): -- - Support widescreen mode ("widescreen"). -- -- Version v8 by Ilari (2010-12-06): -- - Support Special timecode file "@CFR" that fixes up audio for CFR encode. -- -- Version v7 by Ilari (2010-10-24): -- - Fix bug in time division (use integer timestamps, not decimal ones). -- -- Version v6 by Ilari (2010-10-24): -- - Make it work on Lua 5.2 (work 4). -- -- Version v5 by Ilari (2010-09-18): -- - Move the files first out of way, since rename-over fails on Windows. -- -- Version v4 by Ilari (2010-09-17): -- - Change audio track ID if it collides with video track.. -- -- Version v3 by Ilari (2010-09-17): -- - Support setting aspect ratio correction. -- -- Version v2 by Ilari (2010-09-16): -- - If sound and video NHMLs are wrong way around, automatically swap them. -- - Check that one of the NHMLs is sound and other is video. -- ---------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------- --Lua 5.2 fix: unpack = unpack or table.unpack; ---------------------------------------------------------------------------------------------------------------------- -- Function reduce_fraction(number numerator, number denumerator) -- Returns reduced fraction. ---------------------------------------------------------------------------------------------------------------------- reduce_fraction = function(numerator, denumerator) local x, y = numerator, denumerator; while y > 0 do x, y = y, x % y; end return numerator / x, denumerator / x; end ---------------------------------------------------------------------------------------------------------------------- -- Function load_timecode_file(FILE file, number timescale) -- Loads timecode data from file @file, using timescale of @timescale frames per second. Returns array of scaled -- timecodes. ---------------------------------------------------------------------------------------------------------------------- load_timecode_file = function(file, timescale) local line, ret; line = file:read("*l"); if line ~= "# timecode format v2" then error("Timecode file is not in MKV timecodes v2 format"); end ret = {}; while true do line = file:read("*l"); if not line then break; end local timecode = tonumber(line); if not timecode then error("Can't parse timecode '" .. line .. "'."); end table.insert(ret, math.floor(0.5 + timecode / 1000 * timescale)); end return ret; end ---------------------------------------------------------------------------------------------------------------------- -- Function make_reverse_index_table(Array array) -- Returns table, that has entry for each entry in given array @array with value being rank of value, 1 being smallest -- and #array the largest. If @lookup is non-nil, values are looked up from that array. ---------------------------------------------------------------------------------------------------------------------- make_reverse_index_table = function(array, lookup) local sorted, ret; local i; sorted = {}; for i = 1,#array do sorted[i] = array[i]; end table.sort(sorted); ret = {}; for i = 1,#sorted do ret[sorted[i]] = (lookup and lookup[i]) or i; end return ret; end ---------------------------------------------------------------------------------------------------------------------- -- Function max_causality_violaton(Array CTS, Array DTS) -- Return the maximum number of time units CTS and DTS values violate causality. #CTS must equal #DTS. ---------------------------------------------------------------------------------------------------------------------- max_causality_violation = function(CTS, DTS) local max_cv = 0; local i; for i = 1,#CTS do max_cv = math.max(max_cv, DTS[i] - CTS[i]); end return max_cv; end ---------------------------------------------------------------------------------------------------------------------- -- Function fixup_video_times(Array sampledata, Array timecodes, Number spec_delay) -- Fixes video timing of @sampledata (fields CTS and DTS) to be consistent with timecodes in @timecodes. Returns the -- CTS offset of first sample (for fixing audio). @spec_delay is special delay to add (to fix A/V sync). ---------------------------------------------------------------------------------------------------------------------- fixup_video_times = function(sampledata, timecodes, spec_delay) local cts_tab = {}; local dts_tab = {}; local k, v, i; if not timecodes then local min_cts = 999999999999999999999; for i = 1,#sampledata do --Maximum causality violation is always zero in valid HHML. sampledata[i].CTS = sampledata[i].CTS + spec_delay; --Spec_delay should not apply to audio. min_cts = math.min(min_cts, sampledata[i].CTS - spec_delay); end return min_cts; end if #sampledata ~= #timecodes then error("Number of samples (" .. #sampledata .. ") does not match number of timecodes (" .. #timecodes .. ")."); end for i = 1,#sampledata do cts_tab[i] = sampledata[i].CTS; dts_tab[i] = sampledata[i].DTS; end cts_lookup = make_reverse_index_table(cts_tab, timecodes); dts_lookup = make_reverse_index_table(dts_tab, timecodes); -- Perform time translation and find max causality violation. local max_cv = 0; for i = 1,#sampledata do sampledata[i].CTS = cts_lookup[sampledata[i].CTS]; sampledata[i].DTS = dts_lookup[sampledata[i].DTS]; max_cv = math.max(max_cv, sampledata[i].DTS - sampledata[i].CTS); end -- Add maximum causality violation to CTS to eliminate the causality violations. -- Also find the minimum CTS. local min_cts = 999999999999999999999; for i = 1,#sampledata do sampledata[i].CTS = sampledata[i].CTS + max_cv + spec_delay; --Spec_delay should not apply to audio. min_cts = math.min(min_cts, sampledata[i].CTS - spec_delay); end return min_cts; end ---------------------------------------------------------------------------------------------------------------------- -- Function fixup_video_times(Array sampledata, Number min_video_cts, Number video_timescale, Number audio_timescale) -- Fixes video timing of @sampledata (field CTS) to be consistent with video minimum CTS of @cts. Video timescale -- is assumed to be @video_timescale and audio timescale @audio_timescale. ---------------------------------------------------------------------------------------------------------------------- fixup_audio_times = function(sampledata, min_video_cts, video_timescale, audio_timescale) local fixup = math.floor(0.5 + min_video_cts * audio_timescale / video_timescale); for i = 1,#sampledata do sampledata[i].CTS = sampledata[i].CTS + fixup; end end ---------------------------------------------------------------------------------------------------------------------- -- Function translate_NHML_TS_in(Array sampledata); -- Translate NHML CTSOffset fields in @sampledata into CTS fields. ---------------------------------------------------------------------------------------------------------------------- translate_NHML_TS_in = function(sampledata, default_dDTS) local i; local dts = 0; for i = 1,#sampledata do if not sampledata[i].DTS then sampledata[i].DTS = dts + default_dDTS; end dts = sampledata[i].DTS; if (sampledata[i].CTSOffset and sampledata[i].CTSOffset < 0) then error("Invalid CTSOffset " .. tostring(sampledata[i].CTSOffset)); end if sampledata[i].CTSOffset then sampledata[i].CTS = sampledata[i].CTSOffset + sampledata[i].DTS; else sampledata[i].CTS = sampledata[i].DTS; end end end ---------------------------------------------------------------------------------------------------------------------- -- Function translate_NHML_TS_out(Array sampledata); -- Translate CTS fields in @sampledata into NHML CTSOffset fields. ---------------------------------------------------------------------------------------------------------------------- translate_NHML_TS_out = function(sampledata) local i; for i = 1,#sampledata do sampledata[i].CTSOffset = sampledata[i].CTS - sampledata[i].DTS; if sampledata[i].CTSOffset < 0 then error("INTERNAL ERROR: translate_NHML_TS_out: Causality violation: CTS=" .. tostring( sampledata[i].CTS) .. " DTS=" .. tostring(sampledata[i].DTS) .. "."); end sampledata[i].CTS = nil; end end ---------------------------------------------------------------------------------------------------------------------- -- Function map_table_to_number(Table tab); -- Translate all numeric fields in table @tab into numbers. ---------------------------------------------------------------------------------------------------------------------- map_table_to_number = function(tab) local k, v; for k, v in pairs(tab) do local n = tonumber(v); if n then tab[k] = n; end end end ---------------------------------------------------------------------------------------------------------------------- -- Function map_fields_to_number(Array sampledata); -- Translate all numeric fields in array @sampledata into numbers. ---------------------------------------------------------------------------------------------------------------------- map_fields_to_number = function(sampledata) local i; for i = 1,#sampledata do map_table_to_number(sampledata[i]); end end ---------------------------------------------------------------------------------------------------------------------- -- Function escape_xml_text(String str) -- Return XML escaping of text str. ---------------------------------------------------------------------------------------------------------------------- escape_xml_text = function(str) str = string.gsub(str, "&", "&amp;"); str = string.gsub(str, "<", "&lt;"); str = string.gsub(str, ">", "&gt;"); str = string.gsub(str, "\"", "&quot;"); str = string.gsub(str, "\'", "&apos;"); return str; end ---------------------------------------------------------------------------------------------------------------------- -- Function escape_xml_text(String str) -- Return XML unescaping of text str. ---------------------------------------------------------------------------------------------------------------------- unescape_xml_text = function(str) str = string.gsub(str, "&apos;", "\'"); str = string.gsub(str, "&quot;", "\""); str = string.gsub(str, "&gt;", ">"); str = string.gsub(str, "&lt;", "<"); str = string.gsub(str, "&amp;", "&"); return str; end ---------------------------------------------------------------------------------------------------------------------- -- Function serialize_table_to_xml_entity(File file, String tag, Table data, bool noclose); -- Write @data as XML start tag of type @tag into @file. If noclose is true, then tag will not be closed. ---------------------------------------------------------------------------------------------------------------------- serialize_table_to_xml_entity = function(file, tag, data, noclose) local k, v; file:write("<" .. tag .. " "); for k, v in pairs(data) do file:write(k .. "=\"" .. escape_xml_text(tostring(v)) .. "\" "); end if noclose then file:write(">\n"); else file:write("/>\n"); end end ---------------------------------------------------------------------------------------------------------------------- -- Function serialize_array_to_xml_entity(File file, String tag, Array data); -- Write each element of @data as empty XML tag of type @tag into @file. ---------------------------------------------------------------------------------------------------------------------- serialize_array_to_xml_entity = function(file, tag, data) local i; for i = 1,#data do serialize_table_to_xml_entity(file, tag, data[i]); end end ---------------------------------------------------------------------------------------------------------------------- -- Function write_NHML_data(File file, Table header, Table sampledata) -- Write entiere NHML file. ---------------------------------------------------------------------------------------------------------------------- write_NHML_data = function(file, header, sampledata) file:write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"); serialize_table_to_xml_entity(file, "NHNTStream", header, true); serialize_array_to_xml_entity(file, "NHNTSample", sampledata); file:write("</NHNTStream>\n"); end ---------------------------------------------------------------------------------------------------------------------- -- Function open_file_checked(String file, String mode, bool for_write) -- Return file handle to file (checking that open succeeds). ---------------------------------------------------------------------------------------------------------------------- open_file_checked = function(file, mode, for_write) local a, b; a, b = io.open(file, mode); if not a then error("Can't open '" .. file .. "': " .. b); end return a; end ---------------------------------------------------------------------------------------------------------------------- -- Function call_with_file(fun, string file, String mode, ...); -- Call fun with opened file handle to @file (in mode @mode) as first parameter. ---------------------------------------------------------------------------------------------------------------------- call_with_file = function(fun, file, mode, ...) -- FIXME: Handle nils returned from function. local handle = open_file_checked(file, mode); local ret = {fun(handle, ...)}; handle:close(); return unpack(ret); end ---------------------------------------------------------------------------------------------------------------------- -- Function xml_parse_tag(String line); -- Returns the xml tag type for @line plus table of attributes. ---------------------------------------------------------------------------------------------------------------------- xml_parse_tag = function(line) -- More regexping... local tagname; local attr = {}; tagname = string.match(line, "<(%S+).*>"); if not tagname then error("'" .. line .. "': Parse error."); end local k, v; for k, v in string.gmatch(line, "([^ =]+)=\"([^\"]*)\"") do attr[k] = unescape_xml_text(v); end return tagname, attr; end ---------------------------------------------------------------------------------------------------------------------- -- Function load_NHML(File file) -- Loads NHML file @file. Returns header table and samples array. ---------------------------------------------------------------------------------------------------------------------- load_NHML = function(file) -- Let's regexp this shit... local header = {}; local samples = {}; while true do local line = file:read(); if not line then error("Unexpected end of NHML file."); end local xtag, attributes; xtag, attributes = xml_parse_tag(line); if xtag == "NHNTStream" then header = attributes; elseif xtag == "NHNTSample" then table.insert(samples, attributes); elseif xtag == "/NHNTStream" then break; elseif xtag == "?xml" then else print("WARNING: Unrecognized tag '" .. xtag .. "'."); end end return header, samples; end ---------------------------------------------------------------------------------------------------------------------- -- Function reame_errcheck(String old, String new) -- Rename old to new. With error checking. ---------------------------------------------------------------------------------------------------------------------- rename_errcheck = function(old, new, backup) local a, b; os.remove(backup); a, b = os.rename(new, backup); if not a then error("Can't rename '" .. new .. "' -> '" .. backup .. "': " .. b); end a, b = os.rename(old, new); if not a then error("Can't rename '" .. old .. "' -> '" .. new .. "': " .. b); end end ---------------------------------------------------------------------------------------------------------------------- -- Function compute_max_div(Integer ctsBound, Integer timescale, Integer maxCode, pictureOffset) -- Compute maximum allowable timescale. ---------------------------------------------------------------------------------------------------------------------- compute_max_div = function(ctsBound, timeScale, maxCode, pictureOffset) -- Compute the logical number of frames. local logicalFrames = ctsBound / pictureOffset; local maxNumerator = math.floor(maxCode / logicalFrames); -- Be conservative and assume numerator is rounded up. That is, solve the biggest maxdiv such that for all -- 1 <= x <= maxdiv, maxNumerator >= ceil(x * pictureOffset / timeScale) is true. -- Since maxNumerator is integer, this is equivalent to: -- maxNumerator >= x * pictureOffset / timeScale -- => maxNumerator * timeScale / pictureOffset >= x, thus -- maxDiv = math.floor(maxNumerator * timeScale / pictureOffset); return math.floor(maxNumerator * timeScale / pictureOffset); end ---------------------------------------------------------------------------------------------------------------------- -- Function rational_approximate(Integer origNum, Integer origDenum, Integer maxDenum) -- Approximate origNum / origDenum using rational with maximum denumerator of maxDenum ---------------------------------------------------------------------------------------------------------------------- rational_approximate = function(origNum, origDenum, maxDenum) -- FIXME: Better approximations are possible. local div = math.ceil(origDenum / maxDenum); return math.floor(0.5 + origNum / div), math.floor(0.5 + origDenum / div); end ---------------------------------------------------------------------------------------------------------------------- -- Function fixup_mp4box_bug_cfr(Table header, Table samples, Integer pictureOffset, Integer maxdiv) -- Fix MP4Box timecode bug for CFR video by approximating the framerate a bit. ---------------------------------------------------------------------------------------------------------------------- fixup_mp4box_bug_cfr = function(header, samples, pictureOffset, maxdiv) local oNum, oDenum; local nNum, nDenum; local i; oNum, oDenum = pictureOffset, header.timeScale; nNum, nDenum = rational_approximate(oNum, oDenum, maxdiv); header.timeScale = nDenum; for i = 1, #samples do samples[i].DTS = math.floor(0.5 + samples[i].DTS / oNum * nNum); samples[i].CTS = math.floor(0.5 + samples[i].CTS / oNum * nNum); end end if #arg < 3 then error("Syntax: NHMLFixup.lua <video.nhml> <audio.nhml> <timecodes.txt> [delay=<delay>] [tvaspect|widescreen]"); end -- Load the NHML files. io.stdout:write("Loading '" .. arg[1] .. "'..."); io.stdout:flush(); video_header, video_samples = call_with_file(load_NHML, arg[1], "r"); io.stdout:write("Done.\n"); io.stdout:write("Loading '" .. arg[2] .. "'..."); io.stdout:flush(); audio_header, audio_samples = call_with_file(load_NHML, arg[2], "r"); io.stdout:write("Done.\n"); io.stdout:write("String to number conversion on video header..."); io.stdout:flush(); map_table_to_number(video_header); io.stdout:write("Done.\n"); io.stdout:write("String to number conversion on video samples..."); io.stdout:flush(); map_fields_to_number(video_samples); io.stdout:write("Done.\n"); io.stdout:write("String to number conversion on audio header..."); io.stdout:flush(); map_table_to_number(audio_header); io.stdout:write("Done.\n"); io.stdout:write("String to number conversion on audio samples..."); io.stdout:flush(); map_fields_to_number(audio_samples); io.stdout:write("Done.\n"); if video_header.streamType == 4 and audio_header.streamType == 5 then -- Ok. elseif video_header.streamType == 5 and audio_header.streamType == 4 then print("WARNING: You got audio and video wrong way around. Swapping them for you..."); audio_header,audio_samples,arg[2],video_header,video_samples,arg[1] = video_header,video_samples,arg[1],audio_header,audio_samples,arg[2]; else error("Expected one video track and one audio track"); end if video_header.trackID == audio_header.trackID then print("WARNING: Audio and video have the same track id. Assigning new track id to audio track..."); audio_header.trackID = audio_header.trackID + 1; end io.stdout:write("Computing CTS for video samples..."); io.stdout:flush(); translate_NHML_TS_in(video_samples, video_header.DTS_increment or 0); io.stdout:write("Done.\n"); io.stdout:write("Computing CTS for audio samples..."); io.stdout:flush(); translate_NHML_TS_in(audio_samples, audio_header.DTS_increment or 0); io.stdout:write("Done.\n"); -- Alter timescale if needed and load the timecode data. delay = 0; rdelay = 0; for i = 4,#arg do if arg[i] == "tvaspect" then do_aspect_fixup = 1; elseif arg[i] == "widescreen" then do_aspect_fixup = 2; elseif string.sub(arg[i], 1, 6) == "delay=" then local n = tonumber(string.sub(arg[i], 7, #(arg[i]))); if not n then error("Bad delay."); end rdelay = n; delay = math.floor(0.5 + rdelay / 1000 * video_header.timeScale); end end MAX_MP4BOX_TIMECODE = 0x7FFFFFF; if arg[3] ~= "@CFR" then timecode_data = call_with_file(load_timecode_file, arg[3], "r", video_header.timeScale); if timecode_data[#timecode_data] > MAX_MP4BOX_TIMECODE then -- Workaround MP4Box bug. divider = math.ceil(timecode_data[#timecode_data] / MAX_MP4BOX_TIMECODE); print("Notice: Dividing timecodes by " .. divider .. " to workaround MP4Box timecode bug."); io.stdout:write("Performing division..."); io.stdout:flush(); video_header.timeScale = math.floor(0.5 + video_header.timeScale / divider); for i = 1,#timecode_data do timecode_data[i] = math.floor(0.5 + timecode_data[i] / divider); end --Recompute delay. delay = math.floor(0.5 + rdelay / 1000 * video_header.timeScale); io.stdout:write("Done.\n"); end else timecode_data = nil; local maxCTS = 0; local i; local DTSOffset = (video_samples[2] or video_samples[1]).DTS - video_samples[1].DTS; if DTSOffset == 0 then DTSOffset = 1; end for i = 1,#video_samples do if video_samples[i].CTS > maxCTS then maxCTS = video_samples[i].CTS; end if video_samples[i].DTS % DTSOffset ~= 0 then error("Video is not CFR"); end if (video_samples[i].CTS - video_samples[1].CTS) % DTSOffset ~= 0 then error("Video is not CFR"); end end if video_samples[#video_samples].CTS > MAX_MP4BOX_TIMECODE then --Workaround MP4Box bug. local maxdiv = compute_max_div(maxCTS, video_header.timeScale, MAX_MP4BOX_TIMECODE, DTSOffset); print("Notice: Restricting denumerator to " .. maxdiv .. " to workaround MP4Box timecode bug."); io.stdout:write("Fixing timecodes..."); io.stdout:flush(); fixup_mp4box_bug_cfr(video_header, video_samples, DTSOffset, maxdiv); --Recompute delay. delay = math.floor(0.5 + rdelay / 1000 * video_header.timeScale); io.stdout:write("Done.\n"); end end -- Do the actual fixup. io.stdout:write("Fixing up video timecodes..."); io.stdout:flush(); audio_fixup = fixup_video_times(video_samples, timecode_data, delay); io.stdout:write("Done.\n"); io.stdout:write("Fixing up audio timecodes..."); io.stdout:flush(); fixup_audio_times(audio_samples, audio_fixup, video_header.timeScale, audio_header.timeScale); io.stdout:write("Done.\n"); if do_aspect_fixup == 1 then video_header.parNum, video_header.parDen = reduce_fraction(4 * video_header.height, 3 * video_header.width); end if do_aspect_fixup == 2 then video_header.parNum, video_header.parDen = reduce_fraction(16 * video_header.height, 9 * video_header.width); end -- Save the NHML files. io.stdout:write("Computing CTSOffset for video samples..."); io.stdout:flush(); translate_NHML_TS_out(video_samples); io.stdout:write("Done.\n"); io.stdout:write("Computing CTSOffset for audio samples..."); io.stdout:flush(); translate_NHML_TS_out(audio_samples); io.stdout:write("Done.\n"); io.stdout:write("Saving '" .. arg[1] .. ".tmp'..."); io.stdout:flush(); call_with_file(write_NHML_data, arg[1] .. ".tmp", "w", video_header, video_samples); io.stdout:write("Done.\n"); io.stdout:write("Saving '" .. arg[2] .. ".tmp'..."); io.stdout:flush(); call_with_file(write_NHML_data, arg[2] .. ".tmp", "w", audio_header, audio_samples); io.stdout:write("Done.\n"); io.stdout:write("Renaming '" .. arg[1] .. ".tmp' -> '" .. arg[1] .. "'..."); io.stdout:flush(); rename_errcheck(arg[1] .. ".tmp", arg[1], arg[1] .. ".bak"); io.stdout:write("Done.\n"); io.stdout:write("Renaming '" .. arg[2] .. ".tmp' -> '" .. arg[2] .. "'..."); io.stdout:flush(); rename_errcheck(arg[2] .. ".tmp", arg[2], arg[2] .. ".bak"); io.stdout:write("Done.\n"); io.stdout:write("All done.\n");