From caa3df93f9f08a4f26be62e589f2bc6a14d23067 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Thu, 8 Feb 2024 15:15:03 -0800 Subject: [PATCH] add Ambisonic Decoder audio filter --- scripts/build-shotcut-msys2.sh | 23 + scripts/build-shotcut.sh | 24 + src/qml/filters/ambisonic_decoder/meta.qml | 49 ++ src/qml/filters/ambisonic_decoder/ui.qml | 504 +++++++++++++++++++++ src/qml/filters/ambisonic_decoder/vui.qml | 102 +++++ 5 files changed, 702 insertions(+) create mode 100644 src/qml/filters/ambisonic_decoder/meta.qml create mode 100644 src/qml/filters/ambisonic_decoder/ui.qml create mode 100644 src/qml/filters/ambisonic_decoder/vui.qml diff --git a/scripts/build-shotcut-msys2.sh b/scripts/build-shotcut-msys2.sh index 6a9d13719b..e118201062 100755 --- a/scripts/build-shotcut-msys2.sh +++ b/scripts/build-shotcut-msys2.sh @@ -61,6 +61,9 @@ ENABLE_GOPRO2GPX=1 ENABLE_OPENCV=1 OPENCV_HEAD=0 OPENCV_REVISION="4.7.0" +ENABLE_LIBSPATIALAUDIO=1 +LIBSPATIALAUDIO_HEAD=1 +LIBSPATIALAUDIO_REVISION= # QT_INCLUDE_DIR="$(pkg-config --variable=prefix QtCore)/include" QT_INCLUDE_DIR=${QTDIR:+${QTDIR}/include} @@ -186,6 +189,9 @@ function to_key { opencv_contrib) echo 16 ;; + libspatialaudio) + echo 17 + ;; *) echo UNKNOWN ;; @@ -380,6 +386,9 @@ function set_globals { if test "$ENABLE_OPENCV" = 1 ; then SUBDIRS="opencv opencv_contrib $SUBDIRS" fi + if test "$ENABLE_LIBSPATIALAUDIO" = 1 && test "$LIBSPATIALAUDIO_HEAD" = 1 -o "$LIBSPATIALAUDIO_REVISION" != ""; then + SUBDIRS="libspatialaudio $SUBDIRS" + fi SUBDIRS="$SUBDIRS mlt shotcut" fi @@ -422,6 +431,7 @@ function set_globals { REPOLOCS[14]="https://github.com/ddennedy/gopro2gpx.git" REPOLOCS[15]="https://github.com/opencv/opencv.git" REPOLOCS[16]="https://github.com/opencv/opencv_contrib.git" + REPOLOCS[17]="https://github.com/videolabs/libspatialaudio.git" # REPOTYPE Array holds the repo types. (Yes, this might be redundant, but easy for me) REPOTYPES[0]="git" @@ -441,6 +451,7 @@ function set_globals { REPOTYPES[14]="git" REPOTYPES[15]="git" REPOTYPES[16]="git" + REPOTYPES[17]="git" # And, set up the revisions REVISIONS[0]="" @@ -499,6 +510,10 @@ function set_globals { if test 0 = "$OPENCV_HEAD" -a "$OPENCV_REVISION" ; then REVISIONS[16]="$OPENCV_REVISION" fi + REVISIONS[17]="" + if test 0 = "$LIBSPATIALAUDIO_HEAD" -a "$LIBSPATIALAUDIO_REVISION" ; then + REVISIONS[17]="$LIBSPATIALAUDIO_REVISION_REVISION" + fi # Figure out the number of cores in the system. Used both by make and startup script CPUS=$(nproc) @@ -677,6 +692,14 @@ function set_globals { LDFLAGS_[15]="$LDFLAGS" BUILD[15]="ninja -C build -j $MAKEJ" INSTALL[15]="ninja -C build install" + + ##### + # libspatialaudio + CONFIG[17]="cmake -G Ninja -B build -D CMAKE_INSTALL_PREFIX=$FINAL_INSTALL_DIR $CMAKE_DEBUG_FLAG" + CFLAGS_[17]="$CFLAGS" + LDFLAGS_[17]="$LDFLAGS" + BUILD[17]="ninja -j $MAKEJ" + INSTALL[17]="ninja install" } function build_dav1d { diff --git a/scripts/build-shotcut.sh b/scripts/build-shotcut.sh index 2b1cb68aa8..8b344f9133 100755 --- a/scripts/build-shotcut.sh +++ b/scripts/build-shotcut.sh @@ -95,6 +95,9 @@ OPENCV_REVISION="4.7.0" ENABLE_LIBWEBP=1 LIBWEBP_HEAD=0 LIBWEBP_REVISION="v1.3.2" +ENABLE_LIBSPATIALAUDIO=1 +LIBSPATIALAUDIO_HEAD=1 +LIBSPATIALAUDIO_REVISION= PYTHON_VERSION_DEFAULT=3.8 PYTHON_VERSION_DARWIN=3.10 @@ -199,6 +202,9 @@ function to_key { movit) echo 5 ;; + libspatialaudio) + echo 6 + ;; shotcut) echo 7 ;; @@ -496,6 +502,9 @@ function set_globals { if test "$ENABLE_LIBWEBP" = 1 && test "$LIBWEBP_HEAD" = 1 -o "$LIBWEBP_REVISION" != ""; then SUBDIRS="libwebp $SUBDIRS" fi + if test "$ENABLE_LIBSPATIALAUDIO" = 1 && test "$LIBSPATIALAUDIO_HEAD" = 1 -o "$LIBSPATIALAUDIO_REVISION" != ""; then + SUBDIRS="libspatialaudio $SUBDIRS" + fi fi if [ "$DEBUG_BUILD" = "1" ]; then @@ -527,6 +536,7 @@ function set_globals { REPOLOCS[3]="https://github.com/mirror/x264.git" REPOLOCS[4]="https://chromium.googlesource.com/webm/libvpx.git" REPOLOCS[5]="https://github.com/ddennedy/movit.git" + REPOLOCS[6]="https://github.com/videolabs/libspatialaudio.git" REPOLOCS[7]="https://github.com/mltframework/shotcut.git" REPOLOCS[8]="https://github.com/swh/ladspa.git" REPOLOCS[10]="https://github.com/georgmartius/vid.stab.git" @@ -554,6 +564,7 @@ function set_globals { REPOTYPES[3]="git" REPOTYPES[4]="git" REPOTYPES[5]="git" + REPOTYPES[6]="git" REPOTYPES[7]="git" REPOTYPES[8]="git" REPOTYPES[9]="git" @@ -601,6 +612,10 @@ function set_globals { if test 0 = "$MOVIT_HEAD" -a "$MOVIT_REVISION" ; then REVISIONS[5]="$MOVIT_REVISION" fi + REVISIONS[6]="" + if test 0 = "$LIBSPATIALAUDIO_HEAD" -a "$LIBSPATIALAUDIO_REVISION" ; then + REVISIONS[6]="$LIBSPATIALAUDIO_REVISION_REVISION" + fi REVISIONS[7]="" if test 0 = "$SHOTCUT_HEAD" -a "$SHOTCUT_REVISION" ; then REVISIONS[7]="$SHOTCUT_REVISION" @@ -851,6 +866,15 @@ function set_globals { fi INSTALL[5]="make install" + ##### + # libspatialaudio + CONFIG[6]="cmake -G Ninja -B build -D CMAKE_INSTALL_PREFIX=$FINAL_INSTALL_DIR $CMAKE_DEBUG_FLAG" + [ "$TARGET_OS" = "Darwin" ] && CONFIG[6]="${CONFIG[6]} -DCMAKE_OSX_ARCHITECTURES='arm64;x86_64'" + CFLAGS_[6]="$CFLAGS" + LDFLAGS_[6]="$LDFLAGS" + BUILD[6]="ninja -j $MAKEJ" + INSTALL[6]="ninja install" + ##### # shotcut CONFIG[7]="cmake -G Ninja -D CMAKE_PREFIX_PATH=$QTDIR -D SHOTCUT_VERSION=$SHOTCUT_VERSION $CMAKE_DEBUG_FLAG" diff --git a/src/qml/filters/ambisonic_decoder/meta.qml b/src/qml/filters/ambisonic_decoder/meta.qml new file mode 100644 index 0000000000..70ae3a7e77 --- /dev/null +++ b/src/qml/filters/ambisonic_decoder/meta.qml @@ -0,0 +1,49 @@ +import QtQuick +import org.shotcut.qml + +Metadata { + type: Metadata.Filter + isAudio: true + name: qsTr("Ambisonic Decoder") + mlt_service: "ambisonic-decoder" + keywords: qsTr('spatial surround', 'search keywords for the Ambisonic Decoder audio filter') + ' ambisonic decoder' + objectName: "ambisonic-decoder" + qml: "ui.qml" + vui: "vui.qml" + + keyframes { + allowAnimateIn: true + allowAnimateOut: true + simpleProperties: ['yaw', "pitch", "roll", "zoom"] + parameters: [ + Parameter { + name: qsTr('Yaw') + property: 'yaw' + isCurve: true + minimum: -360 + maximum: 360 + }, + Parameter { + name: qsTr('Pitch', 'rotation around the side-to-side axis (roll, pitch, yaw)') + property: 'pitch' + isCurve: true + minimum: -180 + maximum: 180 + }, + Parameter { + name: qsTr('Roll') + property: 'roll' + isCurve: true + minimum: -180 + maximum: 180 + }, + Parameter { + name: qsTr('Zoom') + property: 'zoom' + isCurve: true + minimum: -100 + maximum: 100 + } + ] + } +} diff --git a/src/qml/filters/ambisonic_decoder/ui.qml b/src/qml/filters/ambisonic_decoder/ui.qml new file mode 100644 index 0000000000..ca7d768c16 --- /dev/null +++ b/src/qml/filters/ambisonic_decoder/ui.qml @@ -0,0 +1,504 @@ +/* + * Copyright (c) 2024 Meltytech, LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Shotcut.Controls as Shotcut + +Item { + property bool blockUpdate: true + property double yawStart: 0 + property double yawMiddle: 0 + property double yawEnd: 0 + property double pitchStart: 0 + property double pitchMiddle: 0 + property double pitchEnd: 0 + property double rollStart: 0 + property double rollMiddle: 0 + property double rollEnd: 0 + property double zoomStart: 0 + property double zoomMiddle: 0 + property double zoomEnd: 0 + + function updateSimpleKeyframes() { + if (filter.animateIn > 0 || filter.animateOut > 0) { + // When enabling simple keyframes, initialize the keyframes with the current value + if (filter.keyframeCount("yaw") <= 0) + yawStart = yawMiddle = yawEnd = filter.getDouble("yaw"); + if (filter.keyframeCount("pitch") <= 0) + pitchStart = pitchMiddle = pitchEnd = filter.getDouble("pitch"); + if (filter.keyframeCount("roll") <= 0) + rollStart = rollMiddle = rollEnd = filter.getDouble("roll"); + if (filter.keyframeCount("zoom") <= 0) + zoomStart = zoomMiddle = zoomEnd = filter.getDouble("zoom"); + } + setControls(); + updateProperty_yaw(null); + updateProperty_pitch(null); + updateProperty_roll(null); + updateProperty_zoom(null); + } + + function setControls() { + var position = getPosition(); + blockUpdate = true; + yawSlider.value = filter.getDouble("yaw", position); + yawKeyframesButton.checked = filter.animateIn <= 0 && filter.animateOut <= 0 && filter.keyframeCount("yaw") > 0; + pitchSlider.value = filter.getDouble("pitch", position); + pitchKeyframesButton.checked = filter.animateIn <= 0 && filter.animateOut <= 0 && filter.keyframeCount("pitch") > 0; + rollSlider.value = filter.getDouble("roll", position); + rollKeyframesButton.checked = filter.animateIn <= 0 && filter.animateOut <= 0 && filter.keyframeCount("roll") > 0; + zoomSlider.value = filter.getDouble("zoom", position) * 100; + zoomKeyframesButton.checked = filter.animateIn <= 0 && filter.animateOut <= 0 && filter.keyframeCount("zoom") > 0; + blockUpdate = false; + } + + function updateProperty_yaw(position) { + if (blockUpdate) + return; + var value = yawSlider.value; + if (position !== null) { + if (position <= 0 && filter.animateIn > 0) + yawStart = value; + else if (position >= filter.duration - 1 && filter.animateOut > 0) + yawEnd = value; + else + yawMiddle = value; + } + if (filter.animateIn > 0 || filter.animateOut > 0) { + filter.resetProperty("yaw"); + yawKeyframesButton.checked = false; + if (filter.animateIn > 0) { + filter.set("yaw", yawStart, 0); + filter.set("yaw", yawMiddle, filter.animateIn - 1); + } + if (filter.animateOut > 0) { + filter.set("yaw", yawMiddle, filter.duration - filter.animateOut); + filter.set("yaw", yawEnd, filter.duration - 1); + } + } else if (!yawKeyframesButton.checked) { + filter.resetProperty("yaw"); + filter.set("yaw", yawMiddle); + } else if (position !== null) { + filter.set("yaw", value, position); + } + } + + function updateProperty_pitch(position) { + if (blockUpdate) + return; + var value = pitchSlider.value; + if (position !== null) { + if (position <= 0 && filter.animateIn > 0) + pitchStart = value; + else if (position >= filter.duration - 1 && filter.animateOut > 0) + pitchEnd = value; + else + pitchMiddle = value; + } + if (filter.animateIn > 0 || filter.animateOut > 0) { + filter.resetProperty("pitch"); + pitchKeyframesButton.checked = false; + if (filter.animateIn > 0) { + filter.set("pitch", pitchStart, 0); + filter.set("pitch", pitchMiddle, filter.animateIn - 1); + } + if (filter.animateOut > 0) { + filter.set("pitch", pitchMiddle, filter.duration - filter.animateOut); + filter.set("pitch", pitchEnd, filter.duration - 1); + } + } else if (!pitchKeyframesButton.checked) { + filter.resetProperty("pitch"); + filter.set("pitch", pitchMiddle); + } else if (position !== null) { + filter.set("pitch", value, position); + } + } + + function updateProperty_roll(position) { + if (blockUpdate) + return; + var value = rollSlider.value; + if (position !== null) { + if (position <= 0 && filter.animateIn > 0) + rollStart = value; + else if (position >= filter.duration - 1 && filter.animateOut > 0) + rollEnd = value; + else + rollMiddle = value; + } + if (filter.animateIn > 0 || filter.animateOut > 0) { + filter.resetProperty("roll"); + rollKeyframesButton.checked = false; + if (filter.animateIn > 0) { + filter.set("roll", rollStart, 0); + filter.set("roll", rollMiddle, filter.animateIn - 1); + } + if (filter.animateOut > 0) { + filter.set("roll", rollMiddle, filter.duration - filter.animateOut); + filter.set("roll", rollEnd, filter.duration - 1); + } + } else if (!rollKeyframesButton.checked) { + filter.resetProperty("roll"); + filter.set("roll", rollMiddle); + } else if (position !== null) { + filter.set("roll", value, position); + } + } + + function updateProperty_zoom(position) { + if (blockUpdate) + return; + var value = zoomSlider.value / 100; + if (position !== null) { + if (position <= 0 && filter.animateIn > 0) + zoomStart = value; + else if (position >= filter.duration - 1 && filter.animateOut > 0) + zoomEnd = value; + else + zoomMiddle = value; + } + if (filter.animateIn > 0 || filter.animateOut > 0) { + filter.resetProperty("zoom"); + zoomKeyframesButton.checked = false; + if (filter.animateIn > 0) { + filter.set("zoom", zoomStart, 0); + filter.set("zoom", zoomMiddle, filter.animateIn - 1); + } + if (filter.animateOut > 0) { + filter.set("zoom", zoomMiddle, filter.duration - filter.animateOut); + filter.set("zoom", zoomEnd, filter.duration - 1); + } + } else if (!zoomKeyframesButton.checked) { + filter.resetProperty("zoom"); + filter.set("zoom", zoomMiddle); + } else if (position !== null) { + filter.set("zoom", value, position); + } + } + + function getPosition() { + return Math.max(producer.position - (filter.in - producer.in), 0); + } + + width: 350 + height: 200 + Component.onCompleted: { + if (filter.isNew) { + filter.set("yaw", 0); + } else { + yawMiddle = filter.getDouble("yaw", filter.animateIn); + if (filter.animateIn > 0) + yawStart = filter.getDouble("yaw", 0); + if (filter.animateOut > 0) + yawEnd = filter.getDouble("yaw", filter.duration - 1); + } + if (filter.isNew) { + filter.set("pitch", 0); + } else { + pitchMiddle = filter.getDouble("pitch", filter.animateIn); + if (filter.animateIn > 0) + pitchStart = filter.getDouble("pitch", 0); + if (filter.animateOut > 0) + pitchEnd = filter.getDouble("pitch", filter.duration - 1); + } + if (filter.isNew) { + filter.set("roll", 0); + } else { + rollMiddle = filter.getDouble("roll", filter.animateIn); + if (filter.animateIn > 0) + rollStart = filter.getDouble("roll", 0); + if (filter.animateOut > 0) + rollEnd = filter.getDouble("roll", filter.duration - 1); + } + if (filter.isNew) { + filter.set("zoom", 0); + } else { + zoomMiddle = filter.getDouble("zoom", filter.animateIn); + if (filter.animateIn > 0) + zoomStart = filter.getDouble("zoom", 0); + if (filter.animateOut > 0) + zoomEnd = filter.getDouble("zoom", filter.duration - 1); + } + if (filter.isNew) + filter.savePreset(preset.parameters); + setControls(); + } + + GridLayout { + columns: 4 + anchors.fill: parent + anchors.margins: 8 + + Label { + text: qsTr('Preset') + Layout.alignment: Qt.AlignRight + } + + Shotcut.Preset { + id: preset + + parameters: ["yaw", "pitch", "roll", "zoom"] + Layout.columnSpan: 3 + onBeforePresetLoaded: { + filter.resetProperty('yaw'); + filter.resetProperty('pitch'); + filter.resetProperty('roll'); + filter.resetProperty('zoom'); + } + onPresetSelected: { + yawMiddle = filter.getDouble("yaw", filter.animateIn); + if (filter.animateIn > 0) + yawStart = filter.getDouble("yaw", 0); + if (filter.animateOut > 0) + yawEnd = filter.getDouble("yaw", filter.duration - 1); + pitchMiddle = filter.getDouble("pitch", filter.animateIn); + if (filter.animateIn > 0) + pitchStart = filter.getDouble("pitch", 0); + if (filter.animateOut > 0) + pitchEnd = filter.getDouble("pitch", filter.duration - 1); + rollMiddle = filter.getDouble("roll", filter.animateIn); + if (filter.animateIn > 0) + rollStart = filter.getDouble("roll", 0); + if (filter.animateOut > 0) + rollEnd = filter.getDouble("roll", filter.duration - 1); + zoomMiddle = filter.getDouble("zoom", filter.animateIn); + if (filter.animateIn > 0) + zoomStart = filter.getDouble("zoom", 0); + if (filter.animateOut > 0) + zoomEnd = filter.getDouble("zoom", filter.duration - 1); + setControls(null); + } + } + + Label { + text: qsTr('Yaw') + Layout.alignment: Qt.AlignRight + } + + Shotcut.SliderSpinner { + id: yawSlider + + minimumValue: -360 + maximumValue: 360 + spinnerWidth: 120 + suffix: ' deg' + decimals: 3 + stepSize: 1 + onValueChanged: updateProperty_yaw(getPosition()) + } + + Shotcut.UndoButton { + id: yawUndo + + onClicked: yawSlider.value = 0 + } + + Shotcut.KeyframesButton { + id: yawKeyframesButton + + onToggled: { + var value = yawSlider.value; + if (checked) { + blockUpdate = true; + if (filter.animateIn > 0 || filter.animateOut > 0) { + filter.resetProperty("yaw"); + yawSlider.enabled = true; + } + filter.clearSimpleAnimation("yaw"); + blockUpdate = false; + filter.set("yaw", value, getPosition()); + } else { + filter.resetProperty("yaw"); + filter.set("yaw", value); + } + } + } + + Label { + text: qsTr('Pitch', 'rotation around the side-to-side axis (roll, pitch, yaw)') + Layout.alignment: Qt.AlignRight + } + + Shotcut.SliderSpinner { + id: pitchSlider + + minimumValue: -180 + maximumValue: 180 + spinnerWidth: 120 + suffix: ' deg' + decimals: 3 + stepSize: 1 + onValueChanged: updateProperty_pitch(getPosition()) + } + + Shotcut.UndoButton { + id: pitchUndo + + onClicked: pitchSlider.value = 0 + } + + Shotcut.KeyframesButton { + id: pitchKeyframesButton + + onToggled: { + var value = pitchSlider.value; + if (checked) { + blockUpdate = true; + if (filter.animateIn > 0 || filter.animateOut > 0) { + filter.resetProperty("pitch"); + pitchSlider.enabled = true; + } + filter.clearSimpleAnimation("pitch"); + blockUpdate = false; + filter.set("pitch", value, getPosition()); + } else { + filter.resetProperty("pitch"); + filter.set("pitch", value); + } + } + } + + Label { + text: qsTr('Roll') + Layout.alignment: Qt.AlignRight + } + + Shotcut.SliderSpinner { + id: rollSlider + + minimumValue: -180 + maximumValue: 180 + spinnerWidth: 120 + suffix: ' deg' + decimals: 3 + stepSize: 1 + onValueChanged: updateProperty_roll(getPosition()) + } + + Shotcut.UndoButton { + id: rollUndo + + onClicked: rollSlider.value = 0 + } + + Shotcut.KeyframesButton { + id: rollKeyframesButton + + onToggled: { + var value = rollSlider.value; + if (checked) { + blockUpdate = true; + if (filter.animateIn > 0 || filter.animateOut > 0) { + filter.resetProperty("roll"); + rollSlider.enabled = true; + } + filter.clearSimpleAnimation("roll"); + blockUpdate = false; + filter.set("roll", value, getPosition()); + } else { + filter.resetProperty("roll"); + filter.set("roll", value); + } + } + } + + Label { + text: qsTr('Zoom') + Layout.alignment: Qt.AlignRight + } + + Shotcut.SliderSpinner { + id: zoomSlider + + minimumValue: -100 + maximumValue: 100 + spinnerWidth: 120 + suffix: ' %' + decimals: 1 + stepSize: 1 + onValueChanged: updateProperty_zoom(getPosition()) + } + + Shotcut.UndoButton { + id: zoomUndo + + onClicked: zoomSlider.value = 0 + } + + Shotcut.KeyframesButton { + id: zoomKeyframesButton + + onToggled: { + var value = zoomSlider.value / 100; + if (checked) { + blockUpdate = true; + if (filter.animateIn > 0 || filter.animateOut > 0) { + filter.resetProperty("zoom"); + zoomSlider.enabled = true; + } + filter.clearSimpleAnimation("zoom"); + blockUpdate = false; + filter.set("zoom", value, getPosition()); + } else { + filter.resetProperty("zoom"); + filter.set("zoom", value); + } + } + } + + Item { + Layout.fillHeight: true + } + } + + Connections { + function onChanged() { + setControls(); + } + + function onInChanged() { + updateSimpleKeyframes(); + } + + function onOutChanged() { + updateSimpleKeyframes(); + } + + function onAnimateInChanged() { + updateSimpleKeyframes(); + } + + function onAnimateOutChanged() { + updateSimpleKeyframes(); + } + + function onPropertyChanged(name) { + setControls(); + } + + target: filter + } + + Connections { + function onPositionChanged() { + setControls(); + } + + target: producer + } +} diff --git a/src/qml/filters/ambisonic_decoder/vui.qml b/src/qml/filters/ambisonic_decoder/vui.qml new file mode 100644 index 0000000000..ed82915c10 --- /dev/null +++ b/src/qml/filters/ambisonic_decoder/vui.qml @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2024 Meltytech, LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import QtQuick +import Shotcut.Controls as Shotcut + +Shotcut.VuiBase { + property var keyframableParameters: ['yaw', 'pitch', 'roll', 'zoom'] + property var startValues: [] + property var middleValues: [] + property var endValues: [] + + function clamp(x, min, max) { + return Math.max(min, Math.min(max, x)); + } + + function getPosition() { + return Math.max(producer.position - (filter.in - producer.in), 0); + } + + function updateProperty(name, value) { + var index = keyframableParameters.indexOf(name); + var position = getPosition(); + if (position !== null) { + if (position <= 0 && filter.animateIn > 0) + startValues[index] = value; + else if (position >= filter.duration - 1 && filter.animateOut > 0) + endValues[index] = value; + else + middleValues[index] = value; + } + if (filter.animateIn > 0 || filter.animateOut > 0) { + filter.resetProperty(name); + if (filter.animateIn > 0) { + filter.set(name, startValues[index], 0); + filter.set(name, middleValues[index], filter.animateIn - 1); + } + if (filter.animateOut > 0) { + filter.set(name, middleValues[index], filter.duration - filter.animateOut); + filter.set(name, endValues[index], filter.duration - 1); + } + } else if (filter.keyframeCount(name) <= 0) { + filter.resetProperty(name); + filter.set(name, middleValues[index]); + } else if (position !== null) { + filter.set(name, value, position); + } + } + + Connections { + function onChanged() { + mouseArea.enabled = filter.get('disable') !== '1'; + } + + target: filter + } + + MouseArea { + id: mouseArea + + property double startYaw + property double startPitch + property double startRoll + property int startX + property int startY + + anchors.fill: parent + onPressed: { + startX = mouse.x; + startY = mouse.y; + startYaw = filter.getDouble('yaw', getPosition()); + startPitch = filter.getDouble('pitch', getPosition()); + startRoll = filter.getDouble('roll', getPosition()); + } + onPositionChanged: { + if (mouse.modifiers === Qt.ControlModifier) { + updateProperty('roll', clamp(startRoll + 0.5 * (startY - mouse.y), -180, 180)); + } else { + updateProperty('yaw', clamp(startYaw + 0.2 * (startX - mouse.x), -360, 360)); + updateProperty('pitch', clamp(startPitch + 0.1 * (mouse.y - startY), -180, 180)); + } + } + onWheel: { + var zoom = filter.getDouble('zoom', getPosition()); + console.log('' + wheel.angleDelta.y / 8 / 360); + updateProperty('zoom', clamp(zoom + wheel.angleDelta.y / 8 / 360, -1, 1)); + } + } +}