mame/plugins/timecode/init.lua
Vas Crabb b67b969bf0 -Improved some Lua APIs:
* Moved several machine lifecycle callbacks to the notifier/subscriber
  model.  The old callback registration model is still available for
  them for now, but prints a deprecation warning.
* Added pre-save/post-load notifications.
* Use a single allocated timer rather than one anonymous timer per
  waiter.  Waiters no longer prevent saved states from being loaded.
* Clean up outstanding waiters on stop or state load rather than just
  leaking them.
* Started documenting parts of the emulator interface object that should
  be relatively stable.

-imagedev/avivideo.cpp: Fixed an object leak on unload.  Also changed
 some other media image devices to use smart pointers.
2023-04-07 06:20:40 +10:00

350 lines
11 KiB
Lua

-- 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