Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

if __name__ == "__main__":
import os
import sys

from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QSplashScreen

QApplication.setAttribute(Qt.ApplicationAttribute.AA_UseDesktopOpenGL)
app = QApplication([])
app = QApplication(sys.argv)
from PySide6.QtGui import QPixmap

from utility import resource_path
Expand Down Expand Up @@ -44,7 +45,6 @@
import logging.config
import logging.handlers
import pathlib
import sys

from PySide6.QtCore import QEventLoop

Expand Down
1 change: 1 addition & 0 deletions src/model/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class FilterTypeEnumeration(IntFlag):
Negative values indicate virtual filters.
"""

VFILTER_DIMMER_BRIGHTNESS_MIXIN = -13
VFILTER_SEQUENCER = -12
VFILTER_COLOR_MIXER = -11
VFILTER_IMPORT = -10
Expand Down
5 changes: 4 additions & 1 deletion src/model/ofl/fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ def load_fixture(file: str) -> OflFixture | None:
logger.error("Fixture definition %s not found.", file)
return None
with open(file, "r", encoding="UTF-8") as f:
ob: dict = json.load(f)
try:
ob: dict = json.load(f)
except json.decoder.JSONDecodeError as e:
logger.error("Fixture definition (%s) JSON error: %s", file, e)
ob.update({"fileName": file.split("/fixtures/")[1]})
return OflFixture.model_validate(ob)

Expand Down
213 changes: 213 additions & 0 deletions src/model/virtual_filters/range_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,219 @@ def instantiate_filters(self, filter_list: list[Filter]) -> None:
filter_list.append(filter_)


class DimmerGlobalBrightnessMixinVFilter(VirtualFilter):
"""V-Filter that allows brightness mixin for 8bit and 16bit values.

The filter allows the configuration of an input and a mixin input channel.
Their defaults are the global brightness and a constant 1.
If they're connected their input data typed can both be configured as either 8bit or 16bit.
The optional offset input channel needs to be a float. Reasonable values range from (-1, 1).

The outputs can be configured as 8bit or 16bit.
"""

def __init__(self, scene: Scene, filter_id: str, pos: tuple[int, int] | None = None) -> None:
"""Instantiate a new dimmer brightness mixin vfilter."""
super().__init__(scene, filter_id, FilterTypeEnumeration.VFILTER_DIMMER_BRIGHTNESS_MIXIN, pos=pos)
self._configuration_supported = True
self.filter_configurations.setdefault("has_16bit_output", "true")
self.filter_configurations.setdefault("has_8bit_output", "true")
self.filter_configurations.setdefault("input_method", "8bit")
self.filter_configurations.setdefault("input_method_mixin", "8bit")
self._out_data_types["dimmer_out8b"] = DataType.DT_8_BIT
self._out_data_types["dimmer_out16b"] = DataType.DT_16_BIT
self._in_data_types["offset"] = DataType.DT_DOUBLE
self.deserialize()

@override
def resolve_output_port_id(self, virtual_port_id: str) -> str | None:
out_16b = self.filter_configurations.get("has_16bit_output") == "true"
out_8b = self.filter_configurations.get("has_8bit_output") == "true"
if virtual_port_id == "dimmer_out8b":
if out_8b and out_16b:
return f"{self.filter_id}_16b_downsampler:value_upper"
if out_8b:
return f"{self._filter_id}_8b_range:value"
raise ValueError(f"Requested 8bit output port but 8bit output is disabled. Filter ID: {self.filter_id}")
if virtual_port_id == "dimmer_out16b":
if out_16b:
return f"{self._filter_id}_16b_range:value"
raise ValueError(f"Requested 16bit output port but 16bit output is disabled. Filter ID: {self.filter_id}")
raise ValueError("Unknown output port")

@override
def instantiate_filters(self, filter_list: list[Filter]) -> None:
out_16b = self.filter_configurations.get("has_16bit_output") == "true"
out_8b = self.filter_configurations.get("has_8bit_output") == "true"
needs_8bit_input = self.filter_configurations["input_method"] == "8bit"
required_mixin_input_method = 1 if self.filter_configurations["input_method_mixin"] == "8bit" else 2
needs_global_brightness_input = self.channel_links.get("input") is None
needs_const_mixin = self.channel_links.get("mixin") is None
needs_offset = self.channel_links.get("offset") is None

if needs_const_mixin and (not needs_global_brightness_input or not needs_offset):
const_mixin_filter = Filter(
self.scene,
f"{self.filter_id}_const_mixin",
FilterTypeEnumeration.FILTER_CONSTANT_FLOAT,
pos=self.pos
)
const_mixin_filter.initial_parameters["value"] = "1.0"
filter_list.append(const_mixin_filter)
mixin_port_name = f"{self.filter_id}_const_mixin:value"
required_mixin_input_method = 0
elif needs_const_mixin:
required_mixin_input_method = 0
mixin_port_name = None
else:
mixin_port_name = self.channel_links.get("mixin")

if needs_global_brightness_input:
global_brightness_filter = Filter(
self.scene,
f"{self.filter_id}_global_brightness_input",
FilterTypeEnumeration.FILTER_TYPE_MAIN_BRIGHTNESS,
pos=self.pos
)
filter_list.append(global_brightness_filter)
input_port_name = f"{self.filter_id}_global_brightness_input:brightness"
needs_8bit_input = False
else:
input_port_name = self.channel_links.get("input")

if needs_8bit_input:
range_8b_to_float_filter = self._generate_8b_to_float_range(filter_list, input_port_name)
input_port_name = range_8b_to_float_filter.resolve_output_port_id("value")
else:
range_16b_to_float_filter = self._generate_16b_to_float_range(filter_list, input_port_name)
input_port_name = range_16b_to_float_filter.resolve_output_port_id("value")

if required_mixin_input_method == 1:
range_8b_to_float_filter = self._generate_8b_to_float_range(filter_list, mixin_port_name)
mixin_port_name = range_8b_to_float_filter.resolve_output_port_id("value")
elif required_mixin_input_method == 2:
range_16b_to_float_filter = self._generate_16b_to_float_range(filter_list, mixin_port_name)
mixin_port_name = range_16b_to_float_filter.resolve_output_port_id("value")

if not (needs_global_brightness_input and needs_const_mixin and needs_offset):
if needs_offset:
offset_filter = Filter(
self.scene,
f"{self.filter_id}_offset",
FilterTypeEnumeration.FILTER_CONSTANT_FLOAT,
pos=self.pos
)
offset_filter.initial_parameters["value"] = "0.0"
filter_list.append(offset_filter)
offset_input_port = offset_filter.filter_id + ":value"
else:
offset_input_port = self.channel_links.get("offset")
mac_filter = Filter(
self.scene,
f"{self.filter_id}_mac",
FilterTypeEnumeration.FILTER_ARITHMETICS_MAC,
pos=self.pos
)
mac_filter.channel_links["factor1"] = input_port_name
mac_filter.channel_links["factor2"] = mixin_port_name
mac_filter.channel_links["summand"] = offset_input_port
filter_list.append(mac_filter)
input_port_name = mac_filter.filter_id + ":value"

if out_16b:
range16b_out_filter = Filter(
self.scene,
f"{self._filter_id}_16b_range",
FilterTypeEnumeration.FILTER_ADAPTER_FLOAT_TO_16BIT_RANGE,
pos=self.pos,
filter_configurations={
"lower_bound_in": "0.0",
"upper_bound_in": "1.0",
"lower_bound_out": "0",
"upper_bound_out": "65565",
"limit_range": "1"
}
)
range16b_out_filter.channel_links["value_in"] = input_port_name
filter_list.append(range16b_out_filter)
if out_8b:
downsampling_filter = Filter(
self.scene,
f"{self._filter_id}_16b_downsampler",
FilterTypeEnumeration.FILTER_ADAPTER_16BIT_TO_DUAL_8BIT,
pos=self.pos
)
downsampling_filter.channel_links["value"] = f"{range16b_out_filter.filter_id}:value"
filter_list.append(downsampling_filter)
else:
range8b_out_filter = Filter(
self.scene,
f"{self._filter_id}_8b_range",
FilterTypeEnumeration.FILTER_ADAPTER_FLOAT_TO_8BIT_RANGE,
pos=self.pos,
filter_configurations={
"lower_bound_in": "0.0",
"upper_bound_in": "1.0",
"lower_bound_out": "0",
"upper_bound_out": "255",
"limit_range": "1"
}
)
range8b_out_filter.channel_links["value_in"] = input_port_name
filter_list.append(range8b_out_filter)

def _generate_16b_to_float_range(self, filter_list: list[Filter], input_port_name: str) -> SixteenBitToFloatRange:
range_16b_to_float_filter = SixteenBitToFloatRange(
self.scene,
f"{self.filter_id}_16bit_to_float",
pos=self.pos
)
range_16b_to_float_filter.filter_configurations.update({
"lower_bound_in": "0",
"upper_bound_in": "65565",
"lower_bound_out": "0.0",
"upper_bound_out": "1.0",
"limit_range": "1"
})
range_16b_to_float_filter.channel_links["value_in"] = input_port_name
range_16b_to_float_filter.instantiate_filters(filter_list)
return range_16b_to_float_filter

def _generate_8b_to_float_range(self, filter_list: list[Filter], input_port_name: str) -> EightBitToFloatRange:
range_8b_to_float_filter = EightBitToFloatRange(
self.scene,
f"{self.filter_id}_8bit_to_float",
pos=self.pos
)
range_8b_to_float_filter.filter_configurations.update({
"lower_bound_in": "0",
"upper_bound_in": "255",
"lower_bound_out": "0.0",
"upper_bound_out": "1.0",
"limit_range": "1"
})
range_8b_to_float_filter.channel_links["value_in"] = input_port_name
range_8b_to_float_filter.instantiate_filters(filter_list)
return range_8b_to_float_filter

@override
def deserialize(self) -> None:
if self.filter_configurations.get("has_8bit_output") is None:
self.filter_configurations["has_8bit_output"] = "true"
if self.filter_configurations.get("has_16bit_output") is None:
self.filter_configurations["has_16bit_output"] = "false"
if self.filter_configurations.get("input_method") is None:
self.filter_configurations["input_method"] = "16bit"
if self.filter_configurations.get("input_method") == "8bit":
self._in_data_types["input"] = DataType.DT_8_BIT
else:
self._in_data_types["input"] = DataType.DT_16_BIT
if self.filter_configurations.get("input_method_mixin") == "8bit":
self._in_data_types["mixin"] = DataType.DT_8_BIT
else:
self._in_data_types["mixin"] = DataType.DT_16_BIT


class ColorGlobalBrightnessMixinVFilter(VirtualFilter):
"""V-Filter that provides the global brightness property."""

Expand Down
5 changes: 3 additions & 2 deletions src/model/virtual_filters/vfilter_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from model.virtual_filters.pan_tilt_constant import PanTiltConstantFilter
from model.virtual_filters.range_adapters import (
ColorGlobalBrightnessMixinVFilter,
DimmerGlobalBrightnessMixinVFilter,
EightBitToFloatRange,
SixteenBitToFloatRange,
)
Expand Down Expand Up @@ -51,11 +52,11 @@ def construct_virtual_filter_instance(
return None
case FilterTypeEnumeration.VFILTER_POSITION_CONSTANT:
return PanTiltConstantFilter(scene, filter_id, pos=pos)

case FilterTypeEnumeration.VFILTER_DIMMER_BRIGHTNESS_MIXIN:
return DimmerGlobalBrightnessMixinVFilter(scene, filter_id, pos=pos)
case FilterTypeEnumeration.VFILTER_CUES:
return CueFilter(scene, filter_id, pos=pos)
case FilterTypeEnumeration.VFILTER_EFFECTSSTACK:
# TODO implement effects stack virtual filter (as described in issue #87)
return EffectsStack(scene, filter_id, pos=pos)
case FilterTypeEnumeration.VFILTER_AUTOTRACKER:
return AutoTrackerFilter(scene, filter_id, pos=pos)
Expand Down
4 changes: 3 additions & 1 deletion src/view/show_mode/editor/filter_settings_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from .node_editor_widgets.autotracker_settings import AutotrackerSettingsWidget
from .node_editor_widgets.color_mixing_setup_widget import ColorMixingSetupWidget
from .node_editor_widgets.column_select import ColumnSelect
from .node_editor_widgets.dimmer_brightness_mixin_config_widget import DimmerBrightnessMixinConfigWidget
from .node_editor_widgets.import_vfilter_settings_widget import ImportVFilterSettingsWidget
from .node_editor_widgets.lua_widget import LuaScriptConfigWidget
from .node_editor_widgets.sequencer_editor.widget import SequencerEditor
Expand Down Expand Up @@ -133,7 +134,8 @@ def check_if_filter_has_special_widget(filter_: Filter) -> NodeEditorFilterConfi
return ColorMixingSetupWidget()
if filter_.filter_type == FilterTypeEnumeration.VFILTER_SEQUENCER:
return SequencerEditor(f=filter_)

if filter_.filter_type == FilterTypeEnumeration.VFILTER_DIMMER_BRIGHTNESS_MIXIN:
return DimmerBrightnessMixinConfigWidget()
return None


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Module contains dimmer brightness mixin node config widget."""

from typing import override

from PySide6.QtWidgets import QButtonGroup, QCheckBox, QFormLayout, QHBoxLayout, QLabel, QRadioButton, QWidget

from view.show_mode.editor.node_editor_widgets import NodeEditorFilterConfigWidget


class DimmerBrightnessMixinConfigWidget(NodeEditorFilterConfigWidget):
"""Configuration widget for dimmer brightness mixin node."""

def __init__(self, parent: QWidget | None = None) -> None:
"""Load the filter and prepare a widget."""
super().__init__()
self._widget = QWidget(parent=parent)
layout = QFormLayout()
self._cb_has_16bit = QCheckBox(self._widget)
self._cb_has_16bit.setText("Enable 16bit output")
self._cb_has_16bit.checkStateChanged.connect(self._update_warning_visibility)
layout.addWidget(self._cb_has_16bit)
self._cb_has_8bit = QCheckBox(self._widget)
self._cb_has_8bit.setText("Enable 8bit output")
self._cb_has_8bit.checkStateChanged.connect(self._update_warning_visibility)
layout.addWidget(self._cb_has_8bit)
self._output_warning_label = QLabel("At least one output should be configured.", self._widget)
self._output_warning_label.setVisible(False)
self._output_warning_label.setStyleSheet("color: red;")
layout.addWidget(self._output_warning_label)

self._input_method_rb_group = QButtonGroup(self._widget)
self._input_8bit = QRadioButton("8bit", self._widget)
self._input_method_rb_group.addButton(self._input_8bit)
self._input_16bit = QRadioButton("16bit", self._widget)
self._input_method_rb_group.addButton(self._input_16bit)
button_layout = QHBoxLayout()
button_layout.addWidget(self._input_8bit)
button_layout.addWidget(self._input_16bit)
layout.addRow("Input Port Data Type:", button_layout)

self._mixin_method_rb_group = QButtonGroup(self._widget)
self._mixin_8bit = QRadioButton("8bit", self._widget)
self._mixin_method_rb_group.addButton(self._mixin_8bit)
self._mixin_16bit = QRadioButton("16bit", self._widget)
self._mixin_method_rb_group.addButton(self._mixin_16bit)
button_layout = QHBoxLayout()
button_layout.addWidget(self._mixin_8bit)
button_layout.addWidget(self._mixin_16bit)
layout.addRow("Mixin Port Data Type:", button_layout)

self._widget.setLayout(layout)

def _update_warning_visibility(self) -> None:
self._output_warning_label.setVisible(not self._cb_has_8bit.isChecked() and not self._cb_has_16bit.isChecked())

@override
def _get_configuration(self) -> dict[str, str]:
return {
"has_16bit_output": "true" if self._cb_has_16bit.isChecked() else "false",
"has_8bit_output": "true" if self._cb_has_8bit.isChecked() else "false",
"input_method": "8bit" if self._input_8bit.isChecked() else "16bit",
"input_method_mixin": "8bit" if self._mixin_8bit.isChecked() else "16bit",
}

@override
def _load_configuration(self, conf: dict[str, str]) -> None:
self._cb_has_16bit.setChecked(conf.get("has_16bit_output", "false") == "true")
self._cb_has_8bit.setChecked(conf.get("has_8bit_output", "false") == "true")
self._input_8bit.setChecked(conf.get("input_method", "8bit") == "8bit")
self._input_16bit.setChecked(conf.get("input_method", "8bit") == "16bit")
self._mixin_8bit.setChecked(conf.get("input_method_mixin", "8bit") == "8bit")
self._mixin_16bit.setChecked(conf.get("input_method_mixin", "8bit") == "16bit")

@override
def get_widget(self) -> QWidget:
return self._widget

@override
def _load_parameters(self, parameters: dict[str, str]) -> dict:
# Nothing to do here
pass

@override
def _get_parameters(self) -> dict[str, str]:
return {}

@override
def parent_opened(self) -> None:
# Nothing to do here
pass
Loading