#include <stdarg.h>
#include <lauxlib.h>
#include <lua.hpp>
+#include <movit/blur_effect.h>
#include <movit/deinterlace_effect.h>
#include <movit/effect.h>
#include <movit/effect_chain.h>
#include <movit/resample_effect.h>
#include <movit/resize_effect.h>
#include <movit/util.h>
+#include <movit/unsharp_mask_effect.h>
#include <movit/white_balance_effect.h>
#include <movit/ycbcr.h>
#include <movit/ycbcr_input.h>
#include <new>
#include <utility>
+#include "audio_mixer.h"
#include "defs.h"
#ifdef HAVE_CEF
#include "cef_capture.h"
#include "image_input.h"
#include "input_state.h"
#include "lua_utils.h"
+#include "mainwindow.h"
#include "pbo_frame_allocator.h"
#include "scene.h"
extern Mixer *global_mixer;
+constexpr unsigned Theme::MenuEntry::CHECKABLE;
+constexpr unsigned Theme::MenuEntry::CHECKED;
+
Theme *get_theme_updata(lua_State* L)
{
luaL_checktype(L, lua_upvalueindex(1), LUA_TLIGHTUSERDATA);
last_is_connected[signal_num] = userdata->last_is_connected;
last_frame_rate_nom[signal_num] = userdata->last_frame_rate_nom;
last_frame_rate_den[signal_num] = userdata->last_frame_rate_den;
+ last_pixel_format[signal_num] = userdata->pixel_format;
has_last_subtitle[signal_num] = userdata->has_last_subtitle;
last_subtitle[signal_num] = userdata->last_subtitle;
}
case IDENTITY_EFFECT:
return new IdentityEffect;
case WHITE_BALANCE_EFFECT:
+ case AUTO_WHITE_BALANCE_EFFECT:
return new WhiteBalanceEffect;
case RESAMPLE_EFFECT:
return new ResampleEffect;
return new MixEffect;
case LIFT_GAMMA_GAIN_EFFECT:
return new LiftGammaGainEffect;
+ case BLUR_EFFECT:
+ return new BlurEffect;
+ case UNSHARP_MASK_EFFECT:
+ return new UnsharpMaskEffect;
default:
fprintf(stderr, "Unhandled effect type %d\n", effect_type);
abort();
}
output_ycbcr_format.full_range = false;
- output_ycbcr_format.num_levels = 1 << global_flags.x264_bit_depth;
+ output_ycbcr_format.num_levels = 1 << global_flags.bit_depth;
- GLenum type = global_flags.x264_bit_depth > 8 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_BYTE;
+ GLenum type = global_flags.bit_depth > 8 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_BYTE;
chain->add_ycbcr_output(inout_format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED, output_ycbcr_format, YCBCR_OUTPUT_SPLIT_Y_AND_CBCR, type);
if (global_flags.use_zerocopy) {
chain->add_ycbcr_output(inout_format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED, output_ycbcr_format, YCBCR_OUTPUT_INTERLEAVED, type); // Add a copy where we'll only be using the Y component.
}
- chain->set_dither_bits(global_flags.x264_bit_depth > 8 ? 16 : 8);
+ chain->set_dither_bits(global_flags.bit_depth > 8 ? 16 : 8);
chain->set_output_origin(OUTPUT_ORIGIN_TOP_LEFT);
} else {
chain->add_output(inout_format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
EffectChain *chain = (EffectChain *)luaL_checkudata(L, 1, "EffectChain");
bool override_bounce = checkbool(L, 2);
bool deinterlace = checkbool(L, 3);
- bmusb::PixelFormat pixel_format = global_flags.ten_bit_input ? bmusb::PixelFormat_10BitYCbCr : bmusb::PixelFormat_8BitYCbCr;
+ bmusb::PixelFormat pixel_format = global_flags.bit_depth > 8 ? bmusb::PixelFormat_10BitYCbCr : bmusb::PixelFormat_8BitYCbCr;
// Needs to be nonowned to match add_video_input (see below).
return wrap_lua_object_nonowned<LiveInputWrapper>(L, "LiveInputWrapper", theme, chain, pixel_format, override_bounce, deinterlace, /*user_connectable=*/true);
return wrap_lua_object_nonowned<EffectBlueprint>(L, "EffectBlueprint", LIFT_GAMMA_GAIN_EFFECT);
}
+int BlurEffect_new(lua_State* L)
+{
+ assert(lua_gettop(L) == 0);
+ return wrap_lua_object_nonowned<EffectBlueprint>(L, "EffectBlueprint", BLUR_EFFECT);
+}
+
+int UnsharpMaskEffect_new(lua_State* L)
+{
+ assert(lua_gettop(L) == 0);
+ return wrap_lua_object_nonowned<EffectBlueprint>(L, "EffectBlueprint", UNSHARP_MASK_EFFECT);
+}
+
int InputStateInfo_get_width(lua_State* L)
{
assert(lua_gettop(L) == 2);
InputStateInfo *input_state_info = get_input_state_info(L, 1);
Theme *theme = get_theme_updata(L);
- int signal_num = theme->map_signal(luaL_checknumber(L, 2));
- lua_pushnumber(L, input_state_info->last_width[signal_num]);
+ int card_idx = theme->map_signal_to_card(luaL_checknumber(L, 2));
+ lua_pushnumber(L, input_state_info->last_width[card_idx]);
return 1;
}
assert(lua_gettop(L) == 2);
InputStateInfo *input_state_info = get_input_state_info(L, 1);
Theme *theme = get_theme_updata(L);
- int signal_num = theme->map_signal(luaL_checknumber(L, 2));
- lua_pushnumber(L, input_state_info->last_height[signal_num]);
+ int card_idx = theme->map_signal_to_card(luaL_checknumber(L, 2));
+ lua_pushnumber(L, input_state_info->last_height[card_idx]);
+ return 1;
+}
+
+int InputStateInfo_get_frame_height(lua_State* L)
+{
+ assert(lua_gettop(L) == 2);
+ InputStateInfo *input_state_info = get_input_state_info(L, 1);
+ Theme *theme = get_theme_updata(L);
+ int card_idx = theme->map_signal_to_card(luaL_checknumber(L, 2));
+ unsigned height = input_state_info->last_height[card_idx];
+ if (input_state_info->last_interlaced[card_idx]) {
+ height *= 2;
+ }
+ lua_pushnumber(L, height);
return 1;
}
assert(lua_gettop(L) == 2);
InputStateInfo *input_state_info = get_input_state_info(L, 1);
Theme *theme = get_theme_updata(L);
- int signal_num = theme->map_signal(luaL_checknumber(L, 2));
- lua_pushboolean(L, input_state_info->last_interlaced[signal_num]);
+ int card_idx = theme->map_signal_to_card(luaL_checknumber(L, 2));
+ lua_pushboolean(L, input_state_info->last_interlaced[card_idx]);
return 1;
}
assert(lua_gettop(L) == 2);
InputStateInfo *input_state_info = get_input_state_info(L, 1);
Theme *theme = get_theme_updata(L);
- int signal_num = theme->map_signal(luaL_checknumber(L, 2));
- lua_pushboolean(L, input_state_info->last_has_signal[signal_num]);
+ int card_idx = theme->map_signal_to_card(luaL_checknumber(L, 2));
+ lua_pushboolean(L, input_state_info->last_has_signal[card_idx]);
return 1;
}
assert(lua_gettop(L) == 2);
InputStateInfo *input_state_info = get_input_state_info(L, 1);
Theme *theme = get_theme_updata(L);
- int signal_num = theme->map_signal(luaL_checknumber(L, 2));
- lua_pushboolean(L, input_state_info->last_is_connected[signal_num]);
+ int card_idx = theme->map_signal_to_card(luaL_checknumber(L, 2));
+ lua_pushboolean(L, input_state_info->last_is_connected[card_idx]);
return 1;
}
assert(lua_gettop(L) == 2);
InputStateInfo *input_state_info = get_input_state_info(L, 1);
Theme *theme = get_theme_updata(L);
- int signal_num = theme->map_signal(luaL_checknumber(L, 2));
- lua_pushnumber(L, input_state_info->last_frame_rate_nom[signal_num]);
+ int card_idx = theme->map_signal_to_card(luaL_checknumber(L, 2));
+ lua_pushnumber(L, input_state_info->last_frame_rate_nom[card_idx]);
return 1;
}
assert(lua_gettop(L) == 2);
InputStateInfo *input_state_info = get_input_state_info(L, 1);
Theme *theme = get_theme_updata(L);
- int signal_num = theme->map_signal(luaL_checknumber(L, 2));
- lua_pushnumber(L, input_state_info->last_frame_rate_den[signal_num]);
+ int card_idx = theme->map_signal_to_card(luaL_checknumber(L, 2));
+ lua_pushnumber(L, input_state_info->last_frame_rate_den[card_idx]);
return 1;
}
assert(lua_gettop(L) == 2);
InputStateInfo *input_state_info = get_input_state_info(L, 1);
Theme *theme = get_theme_updata(L);
- int signal_num = theme->map_signal(luaL_checknumber(L, 2));
- if (!input_state_info->has_last_subtitle[signal_num]) {
+ int card_idx = theme->map_signal_to_card(luaL_checknumber(L, 2));
+ if (!input_state_info->has_last_subtitle[card_idx]) {
lua_pushnil(L);
} else {
- lua_pushstring(L, input_state_info->last_subtitle[signal_num].c_str());
+ lua_pushstring(L, input_state_info->last_subtitle[card_idx].c_str());
}
return 1;
}
+namespace {
+
+// Helper function to write e.g. “60” or “59.94”.
+string format_frame_rate(int nom, int den)
+{
+ char buf[256];
+ if (nom % den == 0) {
+ snprintf(buf, sizeof(buf), "%d", nom / den);
+ } else {
+ snprintf(buf, sizeof(buf), "%.2f", double(nom) / den);
+ }
+ return buf;
+}
+
+// Helper function to write e.g. “720p60”.
+string get_human_readable_resolution(const InputStateInfo *input_state_info, int signal_num)
+{
+ char buf[256];
+ if (input_state_info->last_interlaced[signal_num]) {
+ snprintf(buf, sizeof(buf), "%di", input_state_info->last_height[signal_num] * 2);
+
+ // Show field rate instead of frame rate; really for cosmetics only
+ // (and actually contrary to EBU recommendations, although in line
+ // with typical user expectations).
+ return buf + format_frame_rate(input_state_info->last_frame_rate_nom[signal_num] * 2,
+ input_state_info->last_frame_rate_den[signal_num]);
+ } else {
+ snprintf(buf, sizeof(buf), "%dp", input_state_info->last_height[signal_num]);
+ return buf + format_frame_rate(input_state_info->last_frame_rate_nom[signal_num],
+ input_state_info->last_frame_rate_den[signal_num]);
+ }
+}
+
+} // namespace
+
+int InputStateInfo_get_human_readable_resolution(lua_State* L)
+{
+ assert(lua_gettop(L) == 2);
+ InputStateInfo *input_state_info = get_input_state_info(L, 1);
+ Theme *theme = get_theme_updata(L);
+ int card_idx = theme->map_signal_to_card(luaL_checknumber(L, 2));
+
+ string str;
+ if (!input_state_info->last_is_connected[card_idx]) {
+ str = "disconnected";
+ } else if (input_state_info->last_height[card_idx] <= 0) {
+ str = "no signal";
+ } else if (!input_state_info->last_has_signal[card_idx]) {
+ if (input_state_info->last_height[card_idx] == 525) {
+ // Special mode for the USB3 cards.
+ str = "no signal";
+ } else {
+ str = get_human_readable_resolution(input_state_info, card_idx) + ", no signal";
+ }
+ } else {
+ str = get_human_readable_resolution(input_state_info, card_idx);
+ }
+
+ lua_pushstring(L, str.c_str());
+ return 1;
+}
+
+
int EffectBlueprint_set_int(lua_State *L)
{
assert(lua_gettop(L) == 3);
{ "new", Scene_new },
{ "__gc", Scene_gc },
{ "add_input", Scene::add_input },
+ { "add_white_balance", Scene::add_white_balance },
{ "add_effect", Scene::add_effect },
{ "add_optional_effect", Scene::add_optional_effect },
{ "finalize", Scene::finalize },
{ "display", Block_display },
{ "choose", Block_choose },
{ "enable", Block_enable },
+ { "enable_if", Block_enable_if },
{ "disable", Block_disable },
+ { "always_disable_if_disabled", Block_always_disable_if_disabled },
+ { "promise_to_disable_if_enabled", Block_promise_to_disable_if_enabled },
{ "set_int", Block_set_int },
{ "set_float", Block_set_float },
{ "set_vec3", Block_set_vec3 },
{ NULL, NULL }
};
+const luaL_Reg BlurEffect_funcs[] = {
+ { "new", BlurEffect_new },
+ { NULL, NULL }
+};
+
+const luaL_Reg UnsharpMaskEffect_funcs[] = {
+ { "new", UnsharpMaskEffect_new },
+ { NULL, NULL }
+};
+
// End of effects.
const luaL_Reg InputStateInfo_funcs[] = {
{ "get_width", InputStateInfo_get_width },
{ "get_height", InputStateInfo_get_height },
+ { "get_frame_width", InputStateInfo_get_width }, // Same as get_width().
+ { "get_frame_height", InputStateInfo_get_frame_height },
{ "get_interlaced", InputStateInfo_get_interlaced },
{ "get_has_signal", InputStateInfo_get_has_signal },
{ "get_is_connected", InputStateInfo_get_is_connected },
{ "get_frame_rate_nom", InputStateInfo_get_frame_rate_nom },
{ "get_frame_rate_den", InputStateInfo_get_frame_rate_den },
{ "get_last_subtitle", InputStateInfo_get_last_subtitle },
+ { "get_human_readable_resolution", InputStateInfo_get_human_readable_resolution },
{ NULL, NULL }
};
return true;
}
- signal_num = theme->map_signal(signal_num);
- connect_signal_raw(signal_num, *theme->input_state);
+ int card_idx = theme->map_signal_to_card(signal_num);
+ connect_card(card_idx, *theme->input_state);
return true;
}
-void LiveInputWrapper::connect_signal_raw(int signal_num, const InputState &input_state)
+void LiveInputWrapper::connect_card(int card_idx, const InputState &input_state)
{
- BufferedFrame first_frame = input_state.buffered_frames[signal_num][0];
+ BufferedFrame first_frame = input_state.buffered_frames[card_idx][0];
if (first_frame.frame == nullptr) {
// No data yet.
return;
}
}
- movit::YCbCrLumaCoefficients ycbcr_coefficients = input_state.ycbcr_coefficients[signal_num];
- bool full_range = input_state.full_range[signal_num];
+ movit::YCbCrLumaCoefficients ycbcr_coefficients = input_state.ycbcr_coefficients[card_idx];
+ bool full_range = input_state.full_range[card_idx];
- if (input_state.ycbcr_coefficients_auto[signal_num]) {
+ if (input_state.ycbcr_coefficients_auto[card_idx]) {
full_range = false;
// The Blackmagic driver docs claim that the device outputs Y'CbCr
BufferedFrame last_good_frame = first_frame;
for (unsigned i = 0; i < max(ycbcr_inputs.size(), rgba_inputs.size()); ++i) {
- BufferedFrame frame = input_state.buffered_frames[signal_num][i];
+ BufferedFrame frame = input_state.buffered_frames[card_idx][i];
if (frame.frame == nullptr) {
// Not enough data; reuse last frame (well, field).
// This is suboptimal, but we have nothing better.
ycbcr_inputs[i]->set_texture_num(0, userdata->tex_y[frame.field_number]);
ycbcr_inputs[i]->set_texture_num(1, userdata->tex_cb[frame.field_number]);
ycbcr_inputs[i]->set_texture_num(2, userdata->tex_cr[frame.field_number]);
- ycbcr_inputs[i]->change_ycbcr_format(userdata->ycbcr_format);
+ // YCbCrPlanar is used for video streams, where we can have metadata from the mux.
+ // Prefer that if there's no override. (Overrides are only available when using
+ // video as SRT cards.)
+ if (input_state.ycbcr_coefficients_auto[card_idx]) {
+ ycbcr_inputs[i]->change_ycbcr_format(userdata->ycbcr_format);
+ } else {
+ ycbcr_inputs[i]->change_ycbcr_format(input_ycbcr_format);
+ }
ycbcr_inputs[i]->set_width(width);
ycbcr_inputs[i]->set_height(height);
break;
}
if (deinterlace) {
- BufferedFrame frame = input_state.buffered_frames[signal_num][0];
+ BufferedFrame frame = input_state.buffered_frames[card_idx][0];
CHECK(deinterlace_effect->set_int("current_field_position", frame.field_number));
}
}
if (lua_pcall(L, 0, 1, 0) != 0) {
fprintf(stderr, "error running function `num_channels': %s\n", lua_tostring(L, -1));
+ fprintf(stderr, "Try Nageru.set_num_channels(...) at the start of the script instead.\n");
abort();
}
} // namespace
-Theme::Theme(const string &filename, const vector<string> &search_dirs, ResourcePool *resource_pool, unsigned num_cards)
- : resource_pool(resource_pool), num_cards(num_cards), signal_to_card_mapping(global_flags.default_stream_mapping)
+int Nageru_set_channel_name(lua_State *L)
+{
+ // NOTE: m is already locked.
+ Theme *theme = get_theme_updata(L);
+ unsigned channel = luaL_checknumber(L, 1);
+ const string text = checkstdstring(L, 2);
+ theme->channel_names[channel] = text;
+ lua_pop(L, 2);
+ return 0;
+}
+
+int Nageru_set_num_channels(lua_State *L)
+{
+ // NOTE: m is already locked.
+ Theme *theme = get_theme_updata(L);
+ if (theme->startup_finished) {
+ luaL_error(L, "set_num_channels() can only be called at startup.");
+ }
+ theme->num_channels = luaL_checknumber(L, 1);
+ lua_pop(L, 1);
+ return 0;
+}
+
+int Nageru_set_channel_signal(lua_State *L)
+{
+ // NOTE: m is already locked.
+ Theme *theme = get_theme_updata(L);
+ if (theme->startup_finished) {
+ luaL_error(L, "set_channel_signal() can only be called at startup.");
+ }
+ unsigned channel = luaL_checknumber(L, 1);
+ int signal = luaL_checknumber(L, 2);
+ theme->channel_signals[channel] = signal;
+ lua_pop(L, 2);
+ return 0;
+}
+
+int Nageru_set_supports_wb(lua_State *L)
+{
+ // NOTE: m is already locked.
+ Theme *theme = get_theme_updata(L);
+ if (theme->startup_finished) {
+ luaL_error(L, "set_supports_wb() can only be called at startup.");
+ }
+ unsigned channel = luaL_checknumber(L, 1);
+ bool supports_wb = checkbool(L, 2);
+ theme->channel_supports_wb[channel] = supports_wb;
+ lua_pop(L, 2);
+ return 0;
+}
+
+// NOTE: There's a race condition in all of the audio functions; if the mapping
+// is changed by the user underway, you might not be manipulating the bus you
+// expect. (You should not get crashes, though.) There's not all that much we
+// can do about it, short of locking the entire mixer while anything from the
+// theme runs.
+
+int Nageru_get_num_audio_buses(lua_State *L)
+{
+ if (global_audio_mixer == nullptr) {
+ // The audio mixer isn't set up until we know how many FFmpeg inputs we have.
+ luaL_error(L, "Audio functions can not be called before the theme is done initializing.");
+ }
+ lua_pushinteger(L, global_audio_mixer->num_buses());
+ return 1;
+}
+
+int Nageru_get_audio_bus_name(lua_State *L)
+{
+ if (global_audio_mixer == nullptr) {
+ // The audio mixer isn't set up until we know how many FFmpeg inputs we have.
+ luaL_error(L, "Audio functions can not be called before the theme is done initializing.");
+ }
+ int bus_index = luaL_checknumber(L, 1);
+ InputMapping input_mapping = global_audio_mixer->get_input_mapping();
+ if (bus_index < 0 || size_t(bus_index) >= input_mapping.buses.size()) {
+ // Doesn't fix the race, but fixes other out-of-bounds.
+ print_warning(L, "Theme called get_audio_bus_name() on nonexistent bus %d; returning nil.\n", bus_index);
+ lua_pushnil(L);
+ } else {
+ lua_pushstring(L, input_mapping.buses[bus_index].name.c_str());
+ }
+ return 1;
+}
+
+int Nageru_get_audio_bus_fader_level_db(lua_State *L)
+{
+ if (global_audio_mixer == nullptr) {
+ // The audio mixer isn't set up until we know how many FFmpeg inputs we have.
+ luaL_error(L, "Audio functions can not be called before the theme is done initializing.");
+ }
+
+ int bus_index = luaL_checknumber(L, 1);
+ if (bus_index < 0 || size_t(bus_index) >= global_audio_mixer->num_buses()) {
+ // Doesn't fix the race, but fixes other out-of-bounds.
+ print_warning(L, "Theme called get_audio_bus_fader_level_db() on nonexistent bus %d; returning 0.0.\n", bus_index);
+ lua_pushnumber(L, 0.0);
+ } else {
+ lua_pushnumber(L, global_audio_mixer->get_fader_volume(bus_index));
+ }
+ return 1;
+}
+
+int Nageru_set_audio_bus_fader_level_db(lua_State *L)
+{
+ if (global_audio_mixer == nullptr || global_mainwindow == nullptr) {
+ // The audio mixer isn't set up until we know how many FFmpeg inputs we have.
+ luaL_error(L, "Audio functions can not be called before the theme is done initializing.");
+ }
+
+ int bus_index = luaL_checknumber(L, 1);
+ if (bus_index < 0 || size_t(bus_index) >= global_audio_mixer->num_buses()) {
+ // Doesn't fix the race, but fixes other out-of-bounds.
+ print_warning(L, "Theme called set_audio_bus_fader_level_db() on nonexistent bus %d; ignoring.\n", bus_index);
+ return 0;
+ }
+ double level_db = luaL_checknumber(L, 2);
+
+ // Go through the UI, so that it gets updated.
+ global_mainwindow->set_fader_absolute(bus_index, level_db);
+ return 0;
+}
+
+int Nageru_get_audio_bus_mute(lua_State *L)
+{
+ if (global_audio_mixer == nullptr) {
+ // The audio mixer isn't set up until we know how many FFmpeg inputs we have.
+ luaL_error(L, "Audio functions can not be called before the theme is done initializing.");
+ }
+
+ int bus_index = luaL_checknumber(L, 1);
+ if (bus_index < 0 || size_t(bus_index) >= global_audio_mixer->num_buses()) {
+ // Doesn't fix the race, but fixes other out-of-bounds.
+ print_warning(L, "Theme called get_audio_bus_mute() on nonexistent bus %d; returning false.\n", bus_index);
+ lua_pushboolean(L, false);
+ } else {
+ lua_pushboolean(L, global_audio_mixer->get_mute(bus_index));
+ }
+ return 1;
+}
+
+int Nageru_set_audio_bus_mute(lua_State *L)
+{
+ if (global_audio_mixer == nullptr || global_mainwindow == nullptr) {
+ // The audio mixer isn't set up until we know how many FFmpeg inputs we have.
+ luaL_error(L, "Audio functions can not be called before the theme is done initializing.");
+ }
+
+ int bus_index = luaL_checknumber(L, 1);
+ if (bus_index < 0 || size_t(bus_index) >= global_audio_mixer->num_buses()) {
+ // Doesn't fix the race, but fixes other out-of-bounds.
+ print_warning(L, "Theme called set_audio_bus_mute() on nonexistent bus %d; ignoring.\n", bus_index);
+ return 0;
+ }
+ bool mute = checkbool(L, 2);
+
+ // Go through the UI, so that it gets updated.
+ if (mute != global_audio_mixer->get_mute(bus_index)) {
+ global_mainwindow->toggle_mute(bus_index);
+ }
+ return 0;
+}
+
+int Nageru_get_audio_bus_eq_level_db(lua_State *L)
+{
+ if (global_audio_mixer == nullptr) {
+ // The audio mixer isn't set up until we know how many FFmpeg inputs we have.
+ luaL_error(L, "Audio functions can not be called before the theme is done initializing.");
+ }
+
+ int bus_index = luaL_checknumber(L, 1);
+ int band = luaL_checknumber(L, 2);
+ if (bus_index < 0 || size_t(bus_index) >= global_audio_mixer->num_buses()) {
+ // Doesn't fix the race, but fixes other out-of-bounds.
+ print_warning(L, "Theme called get_audio_bus_eq_level_db() on nonexistent bus %d; returning 0.0.\n", bus_index);
+ lua_pushnumber(L, 0.0);
+ } else if (band != EQ_BAND_BASS && band != EQ_BAND_MID && band != EQ_BAND_TREBLE) {
+ print_warning(L, "Theme called get_audio_bus_eq_level_db() on nonexistent band; returning 0.0.\n", bus_index);
+ lua_pushnumber(L, 0.0);
+ } else {
+ lua_pushnumber(L, global_audio_mixer->get_eq(bus_index, EQBand(band)));
+ }
+ return 1;
+}
+
+int Nageru_set_audio_bus_eq_level_db(lua_State *L)
{
+ if (global_audio_mixer == nullptr || global_mainwindow == nullptr) {
+ // The audio mixer isn't set up until we know how many FFmpeg inputs we have.
+ luaL_error(L, "Audio functions can not be called before the theme is done initializing.");
+ }
+
+ int bus_index = luaL_checknumber(L, 1);
+ int band = luaL_checknumber(L, 2);
+ if (bus_index < 0 || size_t(bus_index) >= global_audio_mixer->num_buses()) {
+ // Doesn't fix the race, but fixes other out-of-bounds.
+ print_warning(L, "Theme called set_audio_bus_eq_level_db() on nonexistent bus %d; ignoring.\n", bus_index);
+ return 0;
+ } else if (band != EQ_BAND_BASS && band != EQ_BAND_MID && band != EQ_BAND_TREBLE) {
+ print_warning(L, "Theme called set_audio_bus_eq_level_db() on nonexistent band; returning 0.0.\n", bus_index);
+ return 0;
+ }
+ double level_db = luaL_checknumber(L, 3);
+
+ // Go through the UI, so that it gets updated.
+ global_mainwindow->set_eq_absolute(bus_index, EQBand(band), level_db);
+ return 0;
+}
+
+Theme::Theme(const string &filename, const vector<string> &search_dirs, ResourcePool *resource_pool)
+ : resource_pool(resource_pool), signal_to_card_mapping(global_flags.default_stream_mapping)
+{
+ // Defaults.
+ channel_names[0] = "Live";
+ channel_names[1] = "Preview";
+
L = luaL_newstate();
luaL_openlibs(L);
}
// Set up the API we provide.
- register_constants();
+ register_globals();
register_class("Scene", Scene_funcs);
register_class("Block", Block_funcs);
register_class("EffectBlueprint", EffectBlueprint_funcs);
register_class("ResizeEffect", ResizeEffect_funcs, RESIZE_EFFECT);
register_class("MultiplyEffect", MultiplyEffect_funcs, MULTIPLY_EFFECT);
register_class("MixEffect", MixEffect_funcs, MIX_EFFECT);
+ register_class("BlurEffect", BlurEffect_funcs, BLUR_EFFECT);
+ register_class("UnsharpMaskEffect", UnsharpMaskEffect_funcs, UNSHARP_MASK_EFFECT);
register_class("LiftGammaGainEffect", LiftGammaGainEffect_funcs, LIFT_GAMMA_GAIN_EFFECT);
register_class("InputStateInfo", InputStateInfo_funcs);
register_class("ThemeMenu", ThemeMenu_funcs);
}
assert(lua_gettop(L) == 0);
- // Ask it for the number of channels.
- num_channels = call_num_channels(L);
+ if (num_channels == -1) {
+ // Ask it for the number of channels.
+ num_channels = call_num_channels(L);
+ }
+ startup_finished = true;
}
Theme::~Theme()
{
+ theme_menu.reset();
lua_close(L);
}
-void Theme::register_constants()
+void Theme::register_globals()
{
// Set Nageru.VIDEO_FORMAT_BGRA = bmusb::PixelFormat_8BitBGRA, etc.
const vector<pair<string, int>> num_constants = {
{ "VIDEO_FORMAT_BGRA", bmusb::PixelFormat_8BitBGRA },
{ "VIDEO_FORMAT_YCBCR", bmusb::PixelFormat_8BitYCbCrPlanar },
+ { "CHECKABLE", MenuEntry::CHECKABLE },
+ { "CHECKED", MenuEntry::CHECKED },
+ { "EQ_BAND_BASS", EQ_BAND_BASS },
+ { "EQ_BAND_MID", EQ_BAND_MID },
+ { "EQ_BAND_TREBLE", EQ_BAND_TREBLE },
};
const vector<pair<string, string>> str_constants = {
{ "THEME_PATH", theme_path },
lua_settable(L, 1); // t[key] = value
}
+ const luaL_Reg Nageru_funcs[] = {
+ // Channel information.
+ { "set_channel_name", Nageru_set_channel_name },
+ { "set_num_channels", Nageru_set_num_channels },
+ { "set_channel_signal", Nageru_set_channel_signal },
+ { "set_supports_wb", Nageru_set_supports_wb },
+
+ // Audio.
+ { "get_num_audio_buses", Nageru_get_num_audio_buses },
+ { "get_audio_bus_name", Nageru_get_audio_bus_name },
+ { "get_audio_bus_fader_level_db", Nageru_get_audio_bus_fader_level_db },
+ { "set_audio_bus_fader_level_db", Nageru_set_audio_bus_fader_level_db },
+ { "get_audio_bus_eq_level_db", Nageru_get_audio_bus_eq_level_db },
+ { "set_audio_bus_eq_level_db", Nageru_set_audio_bus_eq_level_db },
+ { "get_audio_bus_mute", Nageru_get_audio_bus_mute },
+ { "set_audio_bus_mute", Nageru_set_audio_bus_mute },
+
+ { nullptr, nullptr }
+ };
+ lua_pushlightuserdata(L, this);
+ luaL_setfuncs(L, Nageru_funcs, 1); // for (name,f in funcs) { mt[name] = f, with upvalue {theme} }
+
lua_setglobal(L, "Nageru"); // Nageru = t
assert(lua_gettop(L) == 0);
}
// each FFmpeg or CEF input, so we'll do it here.
if (video_signal_connections.count(effect_chain)) {
for (const VideoSignalConnection &conn : video_signal_connections[effect_chain]) {
- conn.wrapper->connect_signal_raw(conn.source->get_card_index(), input_state);
+ conn.wrapper->connect_card(conn.source->get_card_index(), input_state);
}
}
#ifdef HAVE_CEF
if (html_signal_connections.count(effect_chain)) {
for (const CEFSignalConnection &conn : html_signal_connections[effect_chain]) {
- conn.wrapper->connect_signal_raw(conn.source->get_card_index(), input_state);
+ conn.wrapper->connect_card(conn.source->get_card_index(), input_state);
}
}
#endif
// TODO: Can we do better, e.g. by running setup_chain() and seeing what it references?
// Actually, setup_chain does maybe hold all the references we need now anyway?
- chain.input_frames.reserve(num_cards * FRAME_HISTORY_LENGTH);
- for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
+ chain.input_frames.reserve(MAX_VIDEO_CARDS * FRAME_HISTORY_LENGTH);
+ for (unsigned card_index = 0; card_index < MAX_VIDEO_CARDS; ++card_index) {
for (unsigned frame_num = 0; frame_num < FRAME_HISTORY_LENGTH; ++frame_num) {
chain.input_frames.push_back(input_state.buffered_frames[card_index][frame_num].frame);
}
string Theme::get_channel_name(unsigned channel)
{
lock_guard<mutex> lock(m);
+
+ // We never ask the legacy channel_name() about live and preview.
+ // The defaults are set in our constructor.
+ if (channel == 0 || channel == 1) {
+ return channel_names[channel];
+ }
+
lua_getglobal(L, "channel_name");
+ if (lua_isnil(L, -1)) {
+ lua_pop(L, 1);
+ if (channel_names.count(channel)) {
+ return channel_names[channel];
+ } else {
+ return "(no title)";
+ }
+ }
+
lua_pushnumber(L, channel);
if (lua_pcall(L, 1, 1, 0) != 0) {
fprintf(stderr, "error running function `channel_name': %s\n", lua_tostring(L, -1));
const char *ret = lua_tostring(L, -1);
if (ret == nullptr) {
fprintf(stderr, "function `channel_name' returned nil for channel %d\n", channel);
+ fprintf(stderr, "Try Nageru.set_channel_name(channel, name) at the start of the script instead.\n");
abort();
}
return retstr;
}
-int Theme::get_channel_signal(unsigned channel)
+int Theme::map_channel_to_signal(unsigned channel)
{
lock_guard<mutex> lock(m);
lua_getglobal(L, "channel_signal");
+ if (lua_isnil(L, -1)) {
+ lua_pop(L, 1);
+ if (channel_signals.count(channel)) {
+ return channel_signals[channel];
+ } else {
+ return -1;
+ }
+ }
+
lua_pushnumber(L, channel);
if (lua_pcall(L, 1, 1, 0) != 0) {
fprintf(stderr, "error running function `channel_signal': %s\n", lua_tostring(L, -1));
+ fprintf(stderr, "Try Nageru.set_channel_signal(channel, signal) at the start of the script instead.\n");
abort();
}
{
lock_guard<mutex> lock(m);
lua_getglobal(L, "supports_set_wb");
+ if (lua_isnil(L, -1)) {
+ lua_pop(L, 1);
+ if (channel_supports_wb.count(channel)) {
+ return channel_supports_wb[channel];
+ } else {
+ return false;
+ }
+ }
+
lua_pushnumber(L, channel);
if (lua_pcall(L, 1, 1, 0) != 0) {
fprintf(stderr, "error running function `supports_set_wb': %s\n", lua_tostring(L, -1));
+ fprintf(stderr, "Try Nageru.set_supports_wb(channel, bool) at the start of the script instead.\n");
abort();
}
return ret;
}
-void Theme::set_wb(unsigned channel, double r, double g, double b)
+void Theme::set_wb(unsigned channel, float r, float g, float b)
{
+ int signal = map_channel_to_signal(channel);
+
lock_guard<mutex> lock(m);
+ if (signal != -1) {
+ int card_idx = map_signal_to_card(signal);
+ white_balance_for_card[card_idx] = RGBTriplet{ r, g, b };
+ }
+
+ call_lua_wb_callback(channel, r, g, b);
+}
+
+void Theme::set_wb_for_card(int card_idx, float r, float g, float b)
+{
+ lock_guard<mutex> lock(m);
+ white_balance_for_card[card_idx] = RGBTriplet{ r, g, b };
+
+ for (const auto &channel_and_signal : channel_signals) {
+ if (map_signal_to_card(channel_and_signal.second) == card_idx) {
+ call_lua_wb_callback(channel_and_signal.first, r, g, b);
+ }
+ }
+}
+
+void Theme::call_lua_wb_callback(unsigned channel, float r, float g, float b)
+{
lua_getglobal(L, "set_wb");
+ if (lua_isnil(L, -1)) {
+ // The function doesn't exist, to just ignore. We've stored the white balance,
+ // and most likely, it will be picked up by auto white balance instead.
+ lua_pop(L, 1);
+ return;
+ }
lua_pushnumber(L, channel);
lua_pushnumber(L, r);
lua_pushnumber(L, g);
assert(lua_gettop(L) == 0);
}
+RGBTriplet Theme::get_white_balance_for_card(int card_idx)
+{
+ if (white_balance_for_card.count(card_idx)) {
+ return white_balance_for_card[card_idx];
+ } else {
+ return RGBTriplet{ 1.0, 1.0, 1.0 };
+ }
+}
+
vector<string> Theme::get_transition_names(float t)
{
lock_guard<mutex> lock(m);
return ret;
}
-int Theme::map_signal(int signal_num)
+int Theme::map_signal_to_card(int signal_num)
{
// Negative numbers map to raw signals.
if (signal_num < 0) {
}
int card_index;
- if (global_flags.output_card != -1 && num_cards > 1) {
+ if (global_flags.output_card != -1) {
// Try to exclude the output card from the default card_index.
- card_index = signal_num % (num_cards - 1);
+ card_index = signal_num % (global_flags.max_num_cards - 1);
if (card_index >= global_flags.output_card) {
++card_index;
}
- if (signal_num >= int(num_cards - 1)) {
+ if (signal_num >= int(global_flags.max_num_cards - 1)) {
print_warning(L, "Theme asked for input %d, but we only have %u input card(s) (card %d is busy with output).\n",
- signal_num, num_cards - 1, global_flags.output_card);
+ signal_num, global_flags.max_num_cards - 1, global_flags.output_card);
fprintf(stderr, "Mapping to card %d instead.\n", card_index);
}
} else {
- card_index = signal_num % num_cards;
- if (signal_num >= int(num_cards)) {
- print_warning(L, "Theme asked for input %d, but we only have %u card(s).\n", signal_num, num_cards);
+ card_index = signal_num % global_flags.max_num_cards;
+ if (signal_num >= int(global_flags.max_num_cards)) {
+ print_warning(L, "Theme asked for input %d, but we only have %u card(s).\n", signal_num, global_flags.max_num_cards);
fprintf(stderr, "Mapping to card %d instead.\n", card_index);
}
}
+ global_mixer->force_card_active(card_index);
signal_to_card_mapping[signal_num] = card_index;
return card_index;
}
-void Theme::set_signal_mapping(int signal_num, int card_num)
+void Theme::set_signal_mapping(int signal_num, int card_idx)
{
lock_guard<mutex> lock(map_m);
- assert(card_num < int(num_cards));
- signal_to_card_mapping[signal_num] = card_num;
+ assert(card_idx < MAX_VIDEO_CARDS);
+ signal_to_card_mapping[signal_num] = card_idx;
}
void Theme::transition_clicked(int transition_num, float t)
assert(lua_gettop(L) == 0);
}
-int Theme::set_theme_menu(lua_State *L)
+template <class T>
+void destroy(T &ref)
+{
+ ref.~T();
+}
+
+Theme::MenuEntry::~MenuEntry()
{
- for (const Theme::MenuEntry &entry : theme_menu) {
- luaL_unref(L, LUA_REGISTRYINDEX, entry.lua_ref);
+ if (is_submenu) {
+ destroy(submenu);
+ } else {
+ luaL_unref(entry.L, LUA_REGISTRYINDEX, entry.lua_ref);
}
- theme_menu.clear();
+}
- int num_elements = lua_gettop(L);
- for (int i = 1; i <= num_elements; ++i) {
- lua_rawgeti(L, i, 1);
- const string text = checkstdstring(L, -1);
+namespace {
+
+vector<unique_ptr<Theme::MenuEntry>> create_recursive_theme_menu(lua_State *L);
+
+unique_ptr<Theme::MenuEntry> create_theme_menu_entry(lua_State *L, int index)
+{
+ unique_ptr<Theme::MenuEntry> entry;
+
+ lua_rawgeti(L, index, 1);
+ const string text = checkstdstring(L, -1);
+ lua_pop(L, 1);
+
+ unsigned flags = 0;
+ if (lua_objlen(L, index) > 2) {
+ lua_rawgeti(L, index, 3);
+ flags = luaL_checknumber(L, -1);
lua_pop(L, 1);
+ }
- lua_rawgeti(L, i, 2);
+ lua_rawgeti(L, index, 2);
+ if (lua_istable(L, -1)) {
+ vector<unique_ptr<Theme::MenuEntry>> submenu = create_recursive_theme_menu(L);
+ entry.reset(new Theme::MenuEntry{ text, move(submenu) });
+ lua_pop(L, 1);
+ } else {
luaL_checktype(L, -1, LUA_TFUNCTION);
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
+ entry.reset(new Theme::MenuEntry{ text, L, ref, flags });
+ }
+ return entry;
+}
- theme_menu.push_back(MenuEntry{ text, ref });
+vector<unique_ptr<Theme::MenuEntry>> create_recursive_theme_menu(lua_State *L)
+{
+ vector<unique_ptr<Theme::MenuEntry>> menu;
+ size_t num_elements = lua_objlen(L, -1);
+ for (size_t i = 1; i <= num_elements; ++i) {
+ lua_rawgeti(L, -1, i);
+ menu.emplace_back(create_theme_menu_entry(L, -1));
+ lua_pop(L, 1);
}
+ return menu;
+}
+
+} // namespace
+
+int Theme::set_theme_menu(lua_State *L)
+{
+ theme_menu.reset();
+
+ vector<unique_ptr<MenuEntry>> root_menu;
+ int num_elements = lua_gettop(L);
+ for (int i = 1; i <= num_elements; ++i) {
+ root_menu.emplace_back(create_theme_menu_entry(L, i));
+ }
+ theme_menu.reset(new MenuEntry("", move(root_menu)));
+
lua_pop(L, num_elements);
assert(lua_gettop(L) == 0);
abort();
}
}
+
+string Theme::format_status_line(const string &disk_space_left_text, double file_length_seconds)
+{
+ lock_guard<mutex> lock(m);
+ lua_getglobal(L, "format_status_line");
+ if (lua_isnil(L, -1)) {
+ lua_pop(L, 1);
+ return disk_space_left_text;
+ }
+
+ lua_pushstring(L, disk_space_left_text.c_str());
+ lua_pushnumber(L, file_length_seconds);
+ if (lua_pcall(L, 2, 1, 0) != 0) {
+ fprintf(stderr, "error running function format_status_line(): %s\n", lua_tostring(L, -1));
+ abort();
+ }
+ string text = checkstdstring(L, 1);
+ lua_pop(L, 1);
+ assert(lua_gettop(L) == 0);
+ return text;
+}
+
+void Theme::remove_card(unsigned card_index)
+{
+ lock_guard<mutex> lock(map_m);
+ for (auto it = signal_to_card_mapping.begin(); it != signal_to_card_mapping.end(); ) {
+ if (it->second == int(card_index)) {
+ it = signal_to_card_mapping.erase(it);
+ } else {
+ ++it;
+ }
+ }
+}