So far, I've encoded DOS games in 60 fps, but I've heard of higher and variable frame rates.
I'm planning to program a little something that would analyse a timecode v2 file, generated by the JPC-RR stream tools, to discover the shortest time between two frames, and to base on that result to find highest frame rate, and use this to create a CFR dump, so I can import it in an AviSynth script without any frame loss.
But before I do such a thing, if you have any easier way to convert a VFR DOS dump to a CFR dump with the VFR's highest frame rate, then shoot.
The usual rate for runs using JPC-RR r11.x (every DOS submission from 2871S onwards) is 125875/1796 fps (but not all use that, for instance Jazz Jackrabbit is 125875/2108 fps during action sequences).
JPC-RR r10.x runs (2870S was the last of those) always use 60fps.
If you use timecode data, be careful as some games may jump between multiple rates and the BIOS bootup is at 125875/1796 fps anyway (and resolution switches can cause timecodes to jump oddly).
Also, timecode output resolution is only 1ms. Timecode data at 1ns resolution could be extracted from the dump.
Oh, and newer versions (sadly, not the r11.2 that has a pre-built binary package) have a menu entry to show what frame rate the game is currently running with.
Oh, here's a script to parse dumps:
Download dumpparse.lua
Language: lua
current_segment_table = nil;
current_ts = 0;
file_offset = 0;
describe_type = function(major, minor)
return (
(major == 0) and (
"VIDEO_" .. (
(minor == 0) and "UNCOMPRESSED" or
(minor == 1) and "COMPRESSED" or
("UNKNOWN" .. minor)
)
) or (major == 1) and (
"PCM_" .. (
(minor == 0) and "VOLUME" or
(minor == 1) and "SAMPLE" or
("UNKNOWN" .. minor)
)
) or (major == 2) and (
"FM_" .. (
(minor == 0) and "VOLUME" or
(minor == 1) and "LOWWRITE" or
(minor == 2) and "HIGHWRITE" or
(minor == 3) and "RESET" or
("UNKNOWN" .. minor)
)
) or (major == 3) and (
"DUMMY_SUBTYPE" .. minor
) or (major == 4) and (
"SUBTITLE_" .. (
(minor == 0) and "SUBTITLE" or
("UNKNOWN" .. minor)
)
) or (major == 5) and (
"GAMEINFO_" .. (
(minor == 65) and "AUTHORS" or
(minor == 71) and "GAMENAME" or
(minor == 76) and "LENGTH" or
(minor == 82) and "RERECORDS" or
("UNKNOWN" .. minor)
)
) or (major == 6) and (
"MIDI_" .. (
(minor == 0) and "DATA" or
("UNKNOWN" .. minor)
)
) or ("UNKNOWN" .. major .. "_UNKNOWN" .. minor)
)
end
conditional_error = function(cond, msg)
if cond then
error(msg);
end
end
string_to_number = function(str)
local i, v;
if not str then
return nil;
end
v = 0;
for i = 1,#str do
v = 256 * v + string.byte(str, i);
end
return v;
end
read_fully = function(file, len)
local ret = file:read(len)
conditional_error(not ret or #ret < len, "Corrupt dump file: Unexpected end of file");
file_offset = file_offset + len;
return ret;
end
read_fully_or_none = function(file, len)
local ret = file:read(len)
if not ret or ret == "" then
return nil;
end
conditional_error(#ret < len, "Corrupt dump file: Unexpected end of file");
file_offset = file_offset + len;
return ret;
end
read_skip = function(file, len)
conditional_error(not file:seek("cur", len), "Corrupt dump file: Can't seek to end of packet");
file_offset = file_offset + len;
end
print_entry = function(chan_num, ts, minor, offset, length)
conditional_error(not current_segment_table, "Corrupt dump file: Entry with no segment table in effect");
conditional_error(chan_num == 0xFFFF, "Internal_error: print_entry() called with chan_num == 0xFFFF");
conditional_error(not current_segment_table[chan_num], "Corrupt dump file: Entry with channel #" ..
chan_num .. " not in segment table");
print(current_segment_table[chan_num].name, ts, describe_type(current_segment_table[chan_num].major, minor),
offset, length);
end
read_segment_table_body = function(file)
current_segment_table = {};
local entries = string_to_number(read_fully(file, 2));
conditional_error(entries == 0 or entries == 65535, "Corrupt dump file: Illegal number of channels " ..
"in segment (" .. entries .. ")");
local i;
for i = 1,entries do
local chan = string_to_number(read_fully(file, 2));
current_segment_table[chan] = {};
current_segment_table[chan].major = string_to_number(read_fully(file, 2));
local namelen = string_to_number(read_fully(file, 2));
current_segment_table[chan].name = read_fully(file, namelen);
end
end
handle_skip = function(file)
current_ts = current_ts + 4294967295;
end
specials = {
[string.char(255, 255, 255, 255)] = handle_skip,
["JPCRRMULTIDUMP"] = read_segment_table_body
};
handle_special = function(file)
local possible_indices = {};
local i = 1;
local mlen = 0;
for k, v in pairs(specials) do
possible_indices[i] = k;
i = i + 1;
end
while true do
local d = read_fully(file, 1);
local i = 1;
while i <= #possible_indices do
if string.sub(possible_indices[i], mlen + 1, mlen + 1) ~= d then
table.remove(possible_indices, i);
else
i = i + 1;
end
end
conditional_error(#possible_indices == 0, "Crroupt dump file: Unknown special type");
mlen = mlen + 1;
if #possible_indices == 1 and #possible_indices[1] == mlen then
break;
end
end
specials[possible_indices[1]](file);
end
handle_packet = function(file, chan)
if chan == 0xFFFF then
return handle_special(file);
end
local tsdelta = string_to_number(read_fully(file, 4));
local minor = string_to_number(read_fully(file, 1));
local len = 0;
while true do
local d = string_to_number(read_fully(file, 1));
if d >= 128 then
len = 128 * len + d - 128;
else
len = 128 * len + d;
break;
end
end
local offset = file_offset;
read_skip(file, len);
current_ts = current_ts + tsdelta;
print_entry(chan, current_ts, minor, offset, len);
end
process_stream = function(file)
file_offset = 0;
while true do
local chan = string_to_number(read_fully_or_none(file, 2));
if not chan then
return;
end
handle_packet(file, chan);
end
end
if not arg[1] then
error("Filename needed");
end
local file, err = io.open(arg[1], "rb");
if not file then
error("Can't open " .. arg[1] .. ": " .. err);
end
process_stream(file);
file:close();
While finding the highest frame rate can be useful in ensuring that no frame gets skipped, it is not the way to produce the best quality.
To make this easier to understand, suppose that you have got video material that has 3fps video and 4fps video. You see that 4fps is the highest, so you choose 4fps for the video. Now, when the input goes to 3fps mode, you will be producing video that has three frames of new material, one duplicate frame, three frames of new material, one duplicate frame, and so on. The end result is that it looks twitchy.
The perfect way would be to find the lowest common multiple (LCM). For 3fps and 4ps, it would be 12fps. For 4fps and 6fps, it would also be 12fps. Because 12 can be divided evenly with 3, 4 and 6.
Of course, it can get awkward for arbitrary rates; for example, the LCM for 125875/1796 fps and 125875/2108 fps is 31468.75 fps, which is unpractical. Instead of a mathematically correct LCM, it might make sense to try to find an outfps where for all infps, it holds that outfps/infps ≥ 1 and lim(abs(0.5−frac(outfps/infps)) → 0.5) and outfps ≤ max(200, max(infps)).
Joined: 11/4/2007
Posts: 1772
Location: Australia, Victoria
Bisqwit: This is why we dump Nintendo 64 games at 120fps and DeDup them in AviSynth, to drop frames that are duplicated. And there has been absolutely no twitch whatsoever with the varying FPS material that I have processed, that I have noticed, anyway.
Similary, if it was up to me, I'd dump DOS games at frame rates of up to 200fps (Note, I haven't actually encoded a DOS game myself so I don't claim to be an expert on the subject regarding DOS) and operate under the same philosophy.
Warp: The MP4 and MKV formats support variable frame rates.
As long as the original framerates are very close to 120, 60, 40, 30, 24, 20, 15 or 12 fps, i.e. integer ratios of 120fps, indeed there is no twitch. But if the source is, say, 50.000 fps, then upscaling to 120.000 fps means that out of every 5 frames, 3 frames are duplicated 2 times and 2 frames are duplicated 3 times. Which means, for input frame pattern "01234", the output will have "001112233344", for a total increase of framerate by a non-integer ratio of 2.4 (120/50). Which means that almost half of the frames are shown for a 50% longer time than the other frames. Of course, given that the input is (in the case of this example) already 50 fps to begin with, the difference is only in order of tens of milliseconds, but it needlessly falls into the "your mileage may vary" land.
Yes, but once you have pigeonholed it into a certain output fps, the original timings of the frames are lost.
To elaborate, think of this 50 -> 120 case:
As you can see, the original material had a stable 20 millisecond interval between frames (1/50 seconds). However, due to pigeonholing into material that has a 8.333 millisecond interval between the frames (1/120 seconds), in the deldupped material, which although has an average framerate of 50 fps, the intervals between two consecutive frames end up alternating between 16.67 milliseconds and 25 milliseconds rather than being a stable 20 milliseconds.
Joined: 11/4/2007
Posts: 1772
Location: Australia, Victoria
Ah, I see what you mean now.
Either way, I wouldn't be too worried about having staggered framerates. I've seen no users complain, and I've spoken with the publication team about it over time, and indeed, none of them have noticed either.