diff options
Diffstat (limited to 'plugins/timecode/init.lua')
-rw-r--r-- | plugins/timecode/init.lua | 350 |
1 files changed, 350 insertions, 0 deletions
diff --git a/plugins/timecode/init.lua b/plugins/timecode/init.lua new file mode 100644 index 00000000000..1de94c034ba --- /dev/null +++ b/plugins/timecode/init.lua @@ -0,0 +1,350 @@ +-- license:BSD-3-Clause +-- copyright-holders:Vas Crabb +local exports = { + name = 'timecode', + version = '0.0.1', + description = 'Timecode recorder plugin', + license = 'BSD-3-Clause', + author = { name = 'Vas Crabb' } } + + +local timecode = exports + +local frame_subscription, stop_subscription + +function timecode.startplugin() + local file -- the timecode log file + local write -- whether to record a timecode on the next emulated frame + local text -- name of current part + local frame_count -- emulated frame counter + local start_frame -- start frame count for current part + local start_time -- start time for current part + local total_time -- total time of parts so far this session + local count -- current timecode number + local show_counter -- whether to show elapsed time since last timecode + local show_total -- whether to show the total time of parts + + local frame_mode -- 0 to count frames, 1 to assume 60 Hz + local hotkey_seq -- input sequence to record timecode + local hotkey_pressed -- whether the hotkey was pressed on the last frame update + local hotkey_cfg -- configuration string for the hotkey + + local item_framemode -- menu index of frame mode item + local item_hotkey -- menu index of hotkey item + local commonui -- common UI helpers + local hotkey_poller -- helper for configuring hotkey + + + local function get_settings_path() + return manager.machine.options.entries.homepath:value():match('([^;]+)') .. '/timecode' + end + + + local function set_default_hotkey() + hotkey_seq = manager.machine.input:seq_from_tokens('KEYCODE_F12 NOT KEYCODE_LSHIFT NOT KEYCODE_RSHIFT NOT KEYCODE_LALT NOT KEYCODE_RALT') + hotkey_cfg = nil + end + + + local function load_settings() + -- set defaults + frame_mode = 1 + set_default_hotkey() + + -- try to open configuration file + local cfgname = get_settings_path() .. '/plugin.cfg' + local cfgfile = io.open(cfgname, 'r') + if not cfgfile then + return -- probably harmless, configuration just doesn't exist yet + end + + -- parse settings as JSON + local json = require('json') + local settings = json.parse(cfgfile:read('a')) + cfgfile:close() + if not settings then + emu.print_error(string.format('Error loading timecode recorder settings: error parsing file "%s" as JSON', cfgname)) + return + end + + -- recover frame mode + local count_frames = settings.count_frames + if count_frames ~= nil then + frame_mode = count_frames and 0 or 1 + end + + -- recover hotkey assignment + hotkey_cfg = settings.hotkey + if hotkey_cfg then + local seq = manager.machine.input:seq_from_tokens(hotkey_cfg) + if seq then + hotkey_seq = seq + end + end + end + + + local function save_settings() + local path = get_settings_path() + local attr = lfs.attributes(path) + if not attr then + lfs.mkdir(path) + elseif attr.mode ~= 'directory' then + emu.print_error(string.format('Error saving timecode recorder settings: "%s" is not a directory', path)) + return + end + local json = require('json') + local settings = { count_frames = frame_mode == 0 } + if hotkey_cfg then + settings.hotkey = hotkey_cfg + end + local data = json.stringify(settings, { indent = true }) + local cfgname = path .. '/plugin.cfg' + local cfgfile = io.open(cfgname, 'w') + if not cfgfile then + emu.print_error(string.format('Error saving timecode recorder settings: error opening file "%s" for writing', cfgname)) + return + end + cfgfile:write(data) + cfgfile:close() + end + + + local function process_frame() + if (not file) or manager.machine.paused then + return + end + if write then + write = false + count = count + 1 + show_total = true + + -- time from beginning of playback in milliseconds, HH:MM:SS.fff and frames + local curtime = manager.machine.time + local sec_start = curtime.seconds + local msec_start = (sec_start * 1000) + curtime.msec + local msec_start_str = string.format('%015d', msec_start) + local curtime_str = string.format( + '%02d:%02d:%02d.%03d', + sec_start // (60 * 60), + (sec_start // 60) % 60, + sec_start % 60, + msec_start % 1000) + local frame_start_str = string.format('%015d', (frame_mode == 0) and frame_count or (msec_start * 60 // 1000)) + + -- elapsed from previous timecode in milliseconds, HH:MM:SS.fff and frames + local elapsed = curtime - start_time + local sec_elapsed = elapsed.seconds + local msec_elapsed = (sec_elapsed * 1000) + elapsed.msec + local msec_elapsed_str = string.format('%015d', msec_elapsed) + local elapsed_str = string.format( + '%02d:%02d:%02d.%03d', + sec_elapsed // (60 * 60), + (sec_elapsed // 60) % 60, + sec_elapsed % 60, + msec_elapsed % 1000) + local frame_elapsed_str = string.format('%015d', (frame_mode == 0) and (frame_count - start_frame) or (msec_elapsed * 60 // 1000)) + + -- update start of part + start_frame = frame_count + start_time = curtime + + local message + local key + if count == 1 then + text = 'INTRO' + show_counter = true + message = string.format(_p('plugin-timecode', 'TIMECODE: Intro started at %s'), curtime_str) + key = 'INTRO_START' + elseif count == 2 then + total_time = total_time + elapsed + show_counter = false + message = string.format(_p('plugin-timecode', 'TIMECODE: Intro duration %s'), elapsed_str) + key = 'INTRO_STOP' + elseif count == 3 then + text = 'GAMEPLAY' + show_counter = true + message = string.format(_p('plugin-timecode', 'TIMECODE: Gameplay started at %s'), curtime_str) + key = 'GAMEPLAY_START' + elseif count == 4 then + total_time = total_time + elapsed + show_counter = false + message = string.format(_p('plugin-timecode', 'TIMECODE: Gameplay duration %s'), elapsed_str) + key = 'GAMEPLAY_STOP' + elseif (count % 2) == 1 then + local extrano = (count - 3) // 2 + text = string.format('EXTRA %d', extrano) + show_counter = true + message = string.format(_p('plugin-timecode', 'TIMECODE: Extra %d started at %s'), extrano, curtime_str) + key = string.format('EXTRA_START_%03d', extrano) + else + local extrano = (count - 4) // 2 + total_time = total_time + elapsed + show_counter = false + message = string.format(_p('plugin-timecode', 'TIMECODE: Extra %d duration %s'), extrano, elapsed_str) + key = string.format('EXTRA_STOP_%03d', extrano) + end + + emu.print_info(message) + manager.machine:popmessage(message) + + file:write( + string.format( + '%-19s %s %s %s %s %s %s\n', + key, + curtime_str, elapsed_str, + msec_start_str, msec_elapsed_str, + frame_start_str, frame_elapsed_str)) + end + frame_count = frame_count + 1 + end + + + local function process_frame_done() + local machine = manager.machine + if show_counter then + -- show duration of current part + local counter = (machine.time - start_time).seconds + local counter_str = string.format( + machine.paused and _p('plugin-timecode', ' %s%s%02d:%02d [paused] ') or _p('plugin-timecode', ' %s%s%02d:%02d '), + text, + (#text > 0) and ' ' or '', + (counter // 60) % 60, + counter % 60) + machine.render.ui_container:draw_text('right', 0, counter_str, 0xf0f01010, 0xff000000) + end + if show_total then + -- show total time for all parts so far + local total = ((show_counter and (machine.time - start_time) or emu.attotime()) + total_time).seconds + total_str = string.format(_p('plugin-timecode', 'TOTAL %02d:%02d '), (total // 60) % 60, total % 60) + machine.render.ui_container:draw_text('left', 0, total_str, 0xf010f010, 0xff000000) + end + if file then + local pressed = machine.input:seq_pressed(hotkey_seq) + if (not hotkey_pressed) and pressed then + write = true + end + hotkey_pressed = pressed + end + end + + + local function start() + file = nil + show_counter = false + show_total = false + load_settings() + + -- only do timecode recording if we're doing input recording + local options = manager.machine.options.entries + local filename = options.record:value() + if #filename > 0 then + filename = filename .. '.timecode' + emu.print_info(string.format('Record input timecode file: %s', filename)) + file = emu.file(options.input_directory:value(), 0x0e) -- FIXME: magic number for flags + local openerr = file:open(filename) + if openerr then + -- TODO: this used to throw a fatal error and log the error description + emu.print_error('Failed to open file for input timecode recording') + file = nil + else + write = false + text = '' + frame_count = 0 + start_frame = 0 + start_time = emu.attotime() + total_time = emu.attotime() + count = 0 + show_counter = false + show_total = false + hotkey_pressed = false + + file:write('# ==========================================\n') + file:write('# TIMECODE FILE FOR VIDEO PREVIEW GENERATION\n') + file:write('# ==========================================\n') + file:write('#\n') + file:write('# VIDEO_PART: code of video timecode\n') + file:write('# START: start time (hh:mm:ss.mmm)\n') + file:write('# ELAPSED: elapsed time (hh:mm:ss.mmm)\n') + file:write('# MSEC_START: start time (milliseconds)\n') + file:write('# MSEC_ELAPSED: elapsed time (milliseconds)\n') + file:write('# FRAME_START: start time (frames)\n') + file:write('# FRAME_ELAPSED: elapsed time (frames)\n') + file:write('#\n') + file:write('# VIDEO_PART======= START======= ELAPSED===== MSEC_START===== MSEC_ELAPSED=== FRAME_START==== FRAME_ELAPSED==\n') + end + end + end + + + local function stop() + -- close the file if we're recording + if file then + file:close() + file = nil + end + + -- try to save settings + save_settings() + end + + + local function menu_callback(index, event) + if hotkey_poller then + if hotkey_poller:poll() then + if hotkey_poller.sequence then + hotkey_seq = hotkey_poller.sequence + hotkey_cfg = manager.machine.input:seq_to_tokens(hotkey_seq) + end + hotkey_poller = nil + return true + end + elseif index == item_framemode then + if (event == 'select') or (event == 'left') or (event == 'right') then + frame_mode = (frame_mode ~= 0) and 0 or 1 + return true + end + elseif index == item_hotkey then + if event == 'select' then + if not commonui then + commonui = require('commonui') + end + hotkey_poller = commonui.switch_polling_helper() + return true + elseif event == 'clear' then + set_default_hotkey() + return true + end + end + return false + end + + + local function menu_populate() + local result = { } + table.insert(result, { _p('plugin-timecode', 'Timecode Recorder'), '', 'off' }) + table.insert(result, { '---', '', '' }) + + local frame_mode_val = (frame_mode > 0) and _p('plugin-timecode', 'Assume 60 Hz') or _p('plugins-timecode', 'Count emulated frames') + table.insert(result, { _p('plugin-timecode', 'Frame numbers'), frame_mode_val, (frame_mode > 0) and 'l' or 'r' }) + item_framemode = #result + + table.insert(result, { _p('plugin-timecode', 'Hotkey'), manager.machine.input:seq_name(hotkey_seq), hotkey_poller and 'lr' or '' }) + item_hotkey = #result + + if hotkey_poller then + return hotkey_poller:overlay(result) + else + return result + end + end + + + frame_subscription = emu.add_machine_frame_notifier(process_frame) + emu.register_frame_done(process_frame_done) + emu.register_prestart(start) + stop_subscription = emu.add_machine_stop_notifier(stop) + emu.register_menu(menu_callback, menu_populate, _p('plugin-timecode', 'Timecode Recorder')) +end + +return exports |