summaryrefslogtreecommitdiffstatshomepage
path: root/plugins/timecode/init.lua
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/timecode/init.lua')
-rw-r--r--plugins/timecode/init.lua350
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