diff --git a/.gitattributes b/.gitattributes index 73d0f2945..70661ac67 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,5 +2,7 @@ * text=auto # Enforce LF line endings on source files -*.h text eol=lf -*.cpp text eol=lf +*.h text eol=lf +*.cpp text eol=lf +*.sh text eol=lf +*.desktop text eol=lf diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..3e9ed6db6 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: olivevideoeditor +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..199220e1b --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,17 @@ +--- +name: Default issue template +labels: 'Triage' +--- + + +**Commit Hash** + + +**Platform** + + +**Summary** + + +**Additional Information / Output** + diff --git a/.github/ISSUE_TEMPLATE/00-olive_unsupported.md b/.github/ISSUE_TEMPLATE/00-olive_unsupported.md new file mode 100644 index 000000000..2fd86f774 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/00-olive_unsupported.md @@ -0,0 +1,14 @@ +--- +name: Legacy Olive 0.1.x issue ⛔ +about: >- + Olive 0.1 is no longer supported and issues made about it will be + automatically closed. +title: '[UNSUPPORTED] ' +labels: 'Legacy (Unsupported)' +assignees: '' +--- +# Olive 0.1 is unsupported + +Unfortunately no one is supporting Olive 0.1 at this time. Any reports +pertaining to it will be closed to keep the issue tracker focused on supported +versions. diff --git a/.github/ISSUE_TEMPLATE/01-crash_issue.md b/.github/ISSUE_TEMPLATE/01-crash_issue.md new file mode 100644 index 000000000..250770fcd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01-crash_issue.md @@ -0,0 +1,31 @@ +--- +name: Crash +about: Report a fatal crash that resulted in Olive unexpectedly closing. +title: '[CRASH] ' +labels: 'Crash, Triage' +assignees: '' +--- + + +**Commit Hash** + + +**Platform** + + +**Summary** + + +**Steps to Reproduce** + +1. +2. +3. + +
Crash Report

+
+
+
+
+ +**Additional Information** diff --git a/.github/ISSUE_TEMPLATE/50-build_issue.md b/.github/ISSUE_TEMPLATE/50-build_issue.md new file mode 100644 index 000000000..3f50177f3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/50-build_issue.md @@ -0,0 +1,22 @@ +--- +name: Build/Packaging +about: >- + Report an issue related to compiling or packaging. + Note that we do not officially support custom build + configurations and may not address issues involving them. +title: '[BUILD] ' +labels: 'Building/Packaging, Triage' +assignees: '' +--- + + +**Commit Hash** + + +**Platform** + + +**Summary** + + +**Additional Information / Output** diff --git a/.github/ISSUE_TEMPLATE/50-cache_issue.md b/.github/ISSUE_TEMPLATE/50-cache_issue.md new file mode 100644 index 000000000..049da13e6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/50-cache_issue.md @@ -0,0 +1,22 @@ +--- +name: Disk Cache +about: >- + Report an issue related to the disk cache system, including failure to cache, + frames being set to appear at the wrong time, disk space or memory usage + issues, etc. +title: '[CACHE] ' +labels: 'Disk Cache, Triage' +assignees: '' +--- + + +**Commit Hash** + + +**Platform** + + +**Summary** + + +**Additional Information / Output** diff --git a/.github/ISSUE_TEMPLATE/50-codec_issue.md b/.github/ISSUE_TEMPLATE/50-codec_issue.md new file mode 100644 index 000000000..623f24968 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/50-codec_issue.md @@ -0,0 +1,21 @@ +--- +name: Codec +about: >- + Report an issue related to codec handling, including importing footage or any + footage usage issues while editing. +title: '[CODEC] ' +labels: 'Codec, Triage' +assignees: '' +--- + + +**Commit Hash** + + +**Platform** + + +**Summary** + + +**Additional Information / Output** diff --git a/.github/ISSUE_TEMPLATE/50-color_issue.md b/.github/ISSUE_TEMPLATE/50-color_issue.md new file mode 100644 index 000000000..e779730a2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/50-color_issue.md @@ -0,0 +1,21 @@ +--- +name: Color Management +about: >- + Report an issue related to the management of pixels and color, including + inaccurate results, color inconsistencies, etc. +title: '[COLOR] ' +labels: 'Color Management, Triage' +assignees: '' +--- + + +**Commit Hash** + + +**Platform** + + +**Summary** + + +**Additional Information / Output** diff --git a/.github/ISSUE_TEMPLATE/50-editing_issue.md b/.github/ISSUE_TEMPLATE/50-editing_issue.md new file mode 100644 index 000000000..a96d26652 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/50-editing_issue.md @@ -0,0 +1,21 @@ +--- +name: Timeline/Editing +about: >- + Report an issue related to the overall editing experience, including usage of + the timeline, interchange, synchronization, multi-camera support, etc. +title: '[EDIT] ' +labels: 'Timeline/Editing, Triage' +assignees: '' +--- + + +**Commit Hash** + + +**Platform** + + +**Summary** + + +**Additional Information / Output** diff --git a/.github/ISSUE_TEMPLATE/50-export_issue.md b/.github/ISSUE_TEMPLATE/50-export_issue.md new file mode 100644 index 000000000..22bda9eb2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/50-export_issue.md @@ -0,0 +1,21 @@ +--- +name: Export +about: >- + Report an issue related to exporting videos from Olive, including errors + while exporting, issues with the resulting video, etc. +title: '[EXPORT] ' +labels: 'Export, Triage' +assignees: '' +--- + + +**Commit Hash** + + +**Platform** + + +**Summary** + + +**Additional Information / Output** diff --git a/.github/ISSUE_TEMPLATE/50-node_issue.md b/.github/ISSUE_TEMPLATE/50-node_issue.md new file mode 100644 index 000000000..a272691d0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/50-node_issue.md @@ -0,0 +1,21 @@ +--- +name: Node/Compositing +about: >- + Report an issue related to the node-based compositing system, including node graph + usability issues, issues working with node effects, issues with composited output, etc. +title: '[NODES] ' +labels: 'Nodes/Compositing, Triage' +assignees: '' +--- + + +**Commit Hash** + + +**Platform** + + +**Summary** + + +**Additional Information / Output** diff --git a/.github/ISSUE_TEMPLATE/50-playback_issue.md b/.github/ISSUE_TEMPLATE/50-playback_issue.md new file mode 100644 index 000000000..2b64327c1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/50-playback_issue.md @@ -0,0 +1,21 @@ +--- +name: Playback +about: >- + Report an issue related to the playback of video or audio, including laggy or + inconsistent playback, failure to playback, etc. +title: '[PLAYBACK] ' +labels: 'Playback, Triage' +assignees: '' +--- + + +**Commit Hash** + + +**Platform** + + +**Summary** + + +**Additional Information / Output** diff --git a/.github/ISSUE_TEMPLATE/50-project_issue.md b/.github/ISSUE_TEMPLATE/50-project_issue.md new file mode 100644 index 000000000..b9c9abd11 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/50-project_issue.md @@ -0,0 +1,21 @@ +--- +name: Project Management +about: >- + Report an issue related to project management, including working with and + organizing imported files. +title: '[PROJECT] ' +labels: 'Project, Triage' +assignees: '' +--- + + +**Commit Hash** + + +**Platform** + + +**Summary** + + +**Additional Information / Output** diff --git a/.github/ISSUE_TEMPLATE/50-renderer_issue.md b/.github/ISSUE_TEMPLATE/50-renderer_issue.md new file mode 100644 index 000000000..a9b4a8413 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/50-renderer_issue.md @@ -0,0 +1,21 @@ +--- +name: Renderer +about: >- + Report an issue related to rendering, including corrupted frames, incorrect + render results, unexpected black or white frames, etc. +title: '[RENDER] ' +labels: 'Renderer, Triage' +assignees: '' +--- + + +**Commit Hash** + + +**Platform** + + +**Summary** + + +**Additional Information / Output** diff --git a/.github/ISSUE_TEMPLATE/50-ui_issue.md b/.github/ISSUE_TEMPLATE/50-ui_issue.md new file mode 100644 index 000000000..972f06416 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/50-ui_issue.md @@ -0,0 +1,21 @@ +--- +name: User Interface +about: >- + Report an issue related to general user interface usability, including + behavior issues, usability issues, look and feel, etc. +title: '[UI] ' +labels: 'User Interface, Triage' +assignees: '' +--- + + +**Commit Hash** + + +**Platform** + + +**Summary** + + +**Additional Information / Output** diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index c7c528362..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: "[Bug]" -labels: '' -assignees: '' - ---- - -**Your Setup:** -- Olive version (window title or Help > About): -- Source: (e.g. AppImage, PPA, AUR, website etc.) -- Operating system: (e.g. Ubuntu 18.04 64-bit) -- CPU: (e.g. Intel i5-4300U) -- RAM: (e.g. 8GB) -- GPU: (e.g. NVIDIA Geforce GT 1030 2GB (Driver ver xxx.xx.xx)) - -**Describe the Bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -** Debug Log and/or Crash Report ** - -``` -[Paste log/report] -``` - -**Additional Details** -Add any other information about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..9258aedf7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Community Support + url: https://discordapp.com/invite/4Ae9KZn + about: Join the Olive Discord server diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 4aa43fc9e..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: "[Feature Request]" -labels: '' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Do you know any libraries or sources that can help the Olive Team include the feature?** -List open-source libraries related to the feature. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..40381ae35 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,451 @@ +name: CI + +on: + push: + branches: + - master + paths-ignore: + - '.github/ISSUE_TEMPLATE/**' + - '.github/FUNDING.yml' + - 'docker/**' + - 'CONTRIBUTING.md' + - 'README.md' + pull_request: + branches: + - master + paths-ignore: + - '.github/ISSUE_TEMPLATE/**' + - '.github/FUNDING.yml' + - 'app/ts/*.ts' + - 'docker/**' + - 'CONTRIBUTING.md' + - 'README.md' + +env: + DOWNLOAD_TOOL: curl -fLOSs --retry 2 --retry-delay 60 + UPLOAD_TOOL: curl -X POST --retry 2 --retry-delay 60 + +jobs: + linux: + strategy: + fail-fast: false + matrix: + include: + - build-type: RelWithDebInfo + cc-compiler: gcc + cxx-compiler: g++ + compiler-name: GCC 9.3.1 + cmake-gen: Ninja + os-name: Linux (CentOS 7) + - build-type: RelWithDebInfo + cc-compiler: clang + cxx-compiler: clang++ + compiler-name: Clang 10.0.0 + cmake-gen: Ninja + os-name: Linux (CentOS 7) + name: | + ${{ matrix.os-name }} + <${{ matrix.compiler-name }}, + ${{ matrix.build-type }}, + ${{ matrix.cmake-gen }}> + runs-on: ubuntu-latest + container: + image: olivevideoeditor/ci-olive:2021.4 + + steps: + - name: Checkout Source Code + uses: actions/checkout@v2 + + - name: Generate Patreon List + env: + PATREON_KEY: ${{ secrets.PATREON_KEY }} + run: | + pip3 install requests + cd $GITHUB_WORKSPACE/app/dialog/about + python3 patreon.py + if: github.event_name == 'push' + continue-on-error: true + + - name: Configure CMake + run: | + mkdir build + cd build + cmake .. -G "${{ matrix.cmake-gen }}" \ + -DCMAKE_BUILD_TYPE="${{ matrix.build-type }}" \ + -DCMAKE_C_COMPILER="${{ matrix.cc-compiler }}" \ + -DCMAKE_CXX_COMPILER="${{ matrix.cxx-compiler }}" + + - name: Build + working-directory: build + run: | + cmake --build . + + - name: Test + working-directory: build + run: | + ctest -C ${{ matrix.build-type }} -V + + - name: Create Package + id: package + working-directory: build + env: + ARCH: x86_64 + run: | + # Create install tree + cmake --install app --prefix appdir/usr + + # Inject custom AppRun (linuxdeployqt won't replace if it already exists) + cp $GITHUB_WORKSPACE/app/packaging/linux/AppRun appdir + + # Process AppDir + /usr/local/linuxdeployqt-x86_64.AppImage \ + appdir/usr/share/applications/org.olivevideoeditor.Olive.desktop \ + -exclude-libs=libQt5Pdf.so,libQt5Qml.so,libQt5QmlModels.so,libQt5Quick.so,libQt5VirtualKeyboard.so \ + -bundle-non-qt-libs \ + -executable=appdir/usr/bin/crashpad_handler \ + -executable=appdir/usr/bin/minidump_stackwalk \ + -executable=appdir/usr/bin/olive-crashhandler \ + --appimage-extract-and-run + + # Dump Crashpad symbols + dump_syms appdir/usr/bin/olive-editor > olive-editor.sym + # HACK: For some reason, minidump_stackwalk reads identifier as all 0s + SYM_DIR=appdir/usr/share/olive-editor/symbols/olive-editor/000000000000000000000000000000000 + mkdir -p "$SYM_DIR" + mv olive-editor.sym "$SYM_DIR" + + # Package AppImage + $DOWNLOAD_TOOL https://github.com/AppImage/AppImageKit/releases/download/12/appimagetool-x86_64.AppImage + chmod +x appimagetool-x86_64.AppImage + VERSION=${GITHUB_SHA::8} ./appimagetool-x86_64.AppImage appdir --appimage-extract-and-run + + # Set env variables + filename=$(echo Olive*.AppImage) + pkgname="${filename/x86_64/Linux-x86_64}" + mv "${filename}" "${pkgname}" + echo "::set-output name=pkgname::${pkgname}" + echo "::set-output name=artifact::${filename/x86_64/Linux-x86_64-${{ matrix.cc-compiler }}}" + + - name: Upload Artifact to GitHub + uses: actions/upload-artifact@v2 + continue-on-error: true + with: + name: ${{ steps.package.outputs.artifact }} + path: build/Olive*.AppImage + + windows: + strategy: + matrix: + include: + - build-type: RelWithDebInfo + compiler-name: MSVC 16.x + os-name: Windows + os-arch: x86_64 + os: windows-latest + cmake-gen: Ninja + name: | + ${{ matrix.os-name }} + <${{ matrix.compiler-name }}, + ${{ matrix.build-type }}, + ${{ matrix.cmake-gen }}> + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout Source Code + uses: actions/checkout@v2 + + - name: Automatically Generate Package Name + shell: bash + env: + PLATFORM: ${{ matrix.os-name }} + ARCH: ${{ matrix.os-arch }} + run: | + echo "PKGNAME=$(echo Olive-${GITHUB_SHA::8}-${PLATFORM}-${ARCH})" >> $GITHUB_ENV + + - name: Create Build Folder + run: | + cmake -E make_directory ${{ runner.workspace }}/build + + - name: Enable Developer Command Prompt (Windows) + uses: ilammy/msvc-dev-cmd@v1 + + - name: Acquire Dependencies + shell: bash + working-directory: ${{ runner.workspace }} + run: | + $DOWNLOAD_TOOL https://github.com/olive-editor/vcpkg-registry/releases/download/continuous/olive-dep-x64-windows.zip + 7z x olive-dep-x64-windows.zip + VCPKG_DIR=(vcpkg-export-*) + echo "VCPKG_ROOT=$(pwd -W)/$VCPKG_DIR" >> $GITHUB_ENV + + - name: Acquire Google Crashpad + shell: bash + working-directory: ${{ runner.workspace }} + run: | + $DOWNLOAD_TOOL https://github.com/olive-editor/crashpad/releases/download/continuous/crashpad-win32-RelWithDebInfo.tar.gz + tar xzf crashpad-win32-RelWithDebInfo.tar.gz + echo "$(pwd -W)/install" >> $GITHUB_PATH + echo "$(pwd -W)/install/bin" >> $GITHUB_PATH + echo "$(pwd -W)/install/crashpad" >> $GITHUB_PATH + + - name: Generate Patreon List + shell: bash + env: + PATREON_KEY: ${{ secrets.PATREON_KEY }} + run: | + pip3 install requests + cd $GITHUB_WORKSPACE/app/dialog/about + python3 patreon.py + if: github.event_name == 'push' + continue-on-error: true + + - name: Configure CMake + shell: bash + working-directory: ${{ runner.workspace }}/build + run: | + cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} -G "${{ matrix.cmake-gen }}" \ + -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" + + - name: Build + working-directory: ${{ runner.workspace }}/build + shell: bash + run: | + cmake --build . + + - name: Test + working-directory: ${{ runner.workspace }}/build + shell: bash + run: | + PATH=$PATH:$VCPKG_ROOT/installed/x64-windows/bin ctest -C ${{ matrix.build-type }} -V + + - name: Create Package + working-directory: ${{ runner.workspace }}/build + shell: bash + env: + ORIGINAL_WORKSPACE: ${{ runner.workspace }} + run: | + mkdir olive-editor + cp app/olive-editor.exe olive-editor + cp app/crashhandler/olive-crashhandler.exe olive-editor + cp app/olive-editor.pdb olive-editor + $VCPKG_ROOT/installed/x64-windows/tools/qt5/bin/windeployqt --no-angle olive-editor/olive-crashhandler.exe + $VCPKG_ROOT/installed/x64-windows/tools/qt5/bin/windeployqt --no-angle olive-editor/olive-editor.exe + cp $(cygpath $VCPKG_ROOT)/installed/x64-windows/bin/*.dll olive-editor + cp $(cygpath $ORIGINAL_WORKSPACE)/install/bin/*.dll olive-editor + cp $(cygpath $ORIGINAL_WORKSPACE)/install/crashpad/out/Default/crashpad_handler.exe olive-editor + cp $(cygpath $ORIGINAL_WORKSPACE)/install/bin/minidump_stackwalk.exe olive-editor + + - name: Export Crashpad symbols + working-directory: ${{ runner.workspace }}/build + shell: bash + run: | + curl -fLSs https://github.com/google/breakpad/blob/master/src/tools/windows/binaries/dump_syms.exe?raw=true > dump_syms.exe + ./dump_syms app/olive-editor.pdb > olive-editor.sym + SYM_HEADER=($(head -n 1 olive-editor.sym)) # Read first line of symbol file + SYM_DIR=olive-editor/symbols/olive-editor.pdb/${SYM_HEADER[3]} + mkdir -p "$SYM_DIR" + mv olive-editor.sym "$SYM_DIR" + + - name: Deploy Packages + working-directory: ${{ runner.workspace }}/build + shell: bash + run: | + # Create Installer Executable + #$DOWNLOAD_TOOL http://web.archive.org/web/20210226132532/http://download.microsoft.com/download/3/2/2/3224B87F-CFA0-4E70-BDA3-3DE650EFEBA5/vcredist_x64.exe + cp $(cygpath $GITHUB_WORKSPACE)/app/packaging/windows/nsis/* . + cp $(cygpath $GITHUB_WORKSPACE)/LICENSE . + $DOWNLOAD_TOOL https://nsis.sourceforge.io/mediawiki/images/6/68/ShellExecAsUser_amd64-Unicode.7z + 7z e ShellExecAsUser_amd64-Unicode.7z Plugins/x86-unicode/ShellExecAsUser.dll + makensis -V4 -DX64 "-XOutFile $PKGNAME.exe" "-X!AddPluginDir /x86-unicode $(pwd -W)" olive.nsi + + # Create Portable ZIP + echo -n > olive-editor/portable + 7z a $PKGNAME.zip olive-editor + + - name: Upload Installer Artifact to GitHub + uses: actions/upload-artifact@v2 + with: + name: ${{ env.PKGNAME }}.exe + path: ${{ runner.workspace }}/build/${{ env.PKGNAME }}.exe + + - name: Upload Portable Artifact to GitHub + uses: actions/upload-artifact@v2 + continue-on-error: true + with: + name: ${{ env.PKGNAME }}.zip + path: ${{ runner.workspace }}/build/${{ env.PKGNAME }}.zip + + macos: + strategy: + matrix: + include: + - build-type: RelWithDebInfo + compiler-name: Clang LLVM + os-name: macOS + os-arch: x86_64 + os: macos-10.15 + cmake-gen: Ninja + min-deploy: 10.13 + - build-type: RelWithDebInfo + compiler-name: Clang LLVM + os-name: macOS + os-arch: arm64 + os: macos-11.0 + cmake-gen: Ninja + min-deploy: 11.0 + env: + DEP_LOCATION: /opt/olive-editor + name: | + ${{ matrix.os-name }} + <${{ matrix.os-arch }}, + ${{ matrix.compiler-name }}, + ${{ matrix.build-type }}, + ${{ matrix.cmake-gen }}> + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout Source Code + uses: actions/checkout@v2 + + - name: Automatically Generate Package Name + shell: bash + env: + PLATFORM: ${{ matrix.os-name }} + ARCH: ${{ matrix.os-arch }} + run: | + echo "PKGNAME=$(echo Olive-${GITHUB_SHA::8}-${PLATFORM}-${ARCH})" >> $GITHUB_ENV + + - name: Create Build Folder + run: | + cmake -E make_directory ${{ runner.workspace }}/build + + - name: Acquire Dependencies + shell: bash + working-directory: ${{ runner.workspace }} + run: | + $DOWNLOAD_TOOL https://github.com/olive-editor/dependencies/releases/download/continuous/olive-dep-mac-${{ matrix.os-arch }}.tar.gz + sudo tar xzf olive-dep-mac-${{ matrix.os-arch }}.tar.gz -C / + + - name: Acquire Google Crashpad + shell: bash + working-directory: ${{ runner.workspace }} + run: | + $DOWNLOAD_TOOL https://github.com/olive-editor/crashpad/releases/download/continuous/crashpad-mac-${{ matrix.os-arch }}.tar.gz + sudo tar xzf crashpad-mac-${{ matrix.os-arch }}.tar.gz -C / + if: matrix.os-arch == 'x86_64' # TEMPORARY: Crashpad isn't compiling on ARM64 yet for some reason + + - name: Generate Patreon List + env: + PATREON_KEY: ${{ secrets.PATREON_KEY }} + run: | + pip3 install requests + cd $GITHUB_WORKSPACE/app/dialog/about + python3 patreon.py + if: github.event_name == 'push' + continue-on-error: true + + - name: Configure CMake + shell: bash + working-directory: ${{ runner.workspace }}/build + run: | + brew install ninja + PATH=$DEP_LOCATION:$DEP_LOCATION/bin:$DEP_LOCATION/include:$DEP_LOCATION/lib:$DEP_LOCATION/crashpad:$PATH \ + cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=${{ matrix.min-deploy }} -G "${{ matrix.cmake-gen }}" \ + -DCMAKE_OSX_ARCHITECTURES="${{ matrix.os-arch }}" + + - name: Build + working-directory: ${{ runner.workspace }}/build + shell: bash + run: | + cmake --build . + + - name: Test + working-directory: ${{ runner.workspace }}/build + shell: bash + run: | + ctest -C ${{ matrix.build-type }} -V + if: matrix.os-arch == 'x86_64' # ARM64 tests naturally won't be able to run on x86_64 runners + + - name: Create Package + working-directory: ${{ runner.workspace }}/build + shell: bash + run: | + BUNDLE_NAME="Olive.app" + brew install dylibbundler + + if [ "${{ matrix.os-arch }}" == "x86_64" ] + then + DYLIBBUNDLER_EXTRA_ARGS="-x $BUNDLE_NAME/Contents/MacOS/olive-crashhandler" + fi + + mv app/$BUNDLE_NAME . + mkdir $BUNDLE_NAME/Contents/Frameworks + dylibbundler -b -ns -x "$BUNDLE_NAME/Contents/MacOS/Olive" -s "$DEP_LOCATION/lib" -d "$BUNDLE_NAME/Contents/Frameworks" -p "@executable_path/../Frameworks" $DYLIBBUNDLER_EXTRA_ARGS + + # Copy Qt frameworks and plugins + cp -Ra $DEP_LOCATION/lib/Qt*.framework $BUNDLE_NAME/Contents/Frameworks + cp -Ra $DEP_LOCATION/plugins $BUNDLE_NAME/Contents + + # HACK: On x86_64, dylibbundler doesn't resolve this symlink. Weirdly it does on ARM64, + # but perhaps I'll bring it up with them soon. + cp -a $BUNDLE_NAME/Contents/Frameworks/libpng16.16.37.0.dylib $BUNDLE_NAME/Contents/Frameworks/libpng16.16.dylib + + if [ "${{ matrix.os-arch }}" == "x86_64" ] + then + # Crashpad symbols + $DEP_LOCATION/bin/dump_syms $BUNDLE_NAME/Contents/MacOS/Olive > Olive.sym + SYM_HEADER=($(head -n 1 Olive.sym)) # Read first line of symbol file + SYM_DIR=$BUNDLE_NAME/Contents/Resources/symbols/Olive/${SYM_HEADER[3]} + mkdir -p "$SYM_DIR" + mv Olive.sym "$SYM_DIR" + fi + + - name: Sign Application + working-directory: ${{ runner.workspace }}/build + shell: bash + env: + BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + P12_PASSWORD: ${{ secrets.P12_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + BUNDLE_NAME="Olive.app" + + # Install certificate + CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + + # import certificate from secrets + echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode --output $CERTIFICATE_PATH + + # create temporary keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # import certificate to keychain + security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + + # HACK: Remove unsignable frameworks + rm -r $BUNDLE_NAME/Contents/Frameworks/QtUiPlugin.framework + if [ "${{ matrix.os-arch }}" == "arm64" ] + then + rm -r $BUNDLE_NAME/Contents/Frameworks/QtZlib.framework + fi + + # Sign application + codesign --deep --sign "Developer ID Application: Olive Studios LLC" $BUNDLE_NAME + + - name: Deploy Packages + working-directory: ${{ runner.workspace }}/build + shell: bash + run: | + zip -r $PKGNAME.zip Olive.app + + - name: Upload Artifact to GitHub + uses: actions/upload-artifact@v2 + continue-on-error: true + with: + name: ${{ env.PKGNAME }}.zip + path: ${{ runner.workspace }}/build/${{ env.PKGNAME }}.zip diff --git a/.gitignore b/.gitignore index 3f42a5ddc..43c997614 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,43 @@ -*.pro.user* -Makefile -.qmake.stash -effects/frei0r -ts/*.qm -docs -history +# CMake artifacts +/build*/ -.vscode +# Doxygen +/docs/ + +# Visual Studio (Code) +.localhistory/ +.history/ +.vscode/ +.vs/ +/out/ +CmakeSettings.json +*.code-workspace + +# clangd's index and likely other things that need not be in the repository +.cache/ + +# macOS General +.DS_Store +.AppleDouble +.LSOverride + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Windows folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares (Windows) +$RECYCLE.BIN/ + +# Windows shortcuts +*.lnk # -# Qt ignores taken from https://github.com/github/gitignore +# Qt ignores taken from https://github.com/github/gitignore/blob/master/Qt.gitignore # # C++ objects and libs @@ -20,6 +48,7 @@ history *.la *.lai *.so +*.so.* *.dll *.dylib @@ -42,6 +71,8 @@ ui_*.h *.jsc Makefile* *build-* +*.qm +*.prl # Qt unit tests target_wrapper.* @@ -61,3 +92,5 @@ compile_commands.json # QtCreator local machine specific files for imported projects *creator.user* + +*_qmlcache.qrc diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6c923a0b0..000000000 --- a/.travis.yml +++ /dev/null @@ -1,38 +0,0 @@ -language: cpp - -matrix: - include: - - os: linux - env: ARCH=x86_64 - compiler: gcc - sudo: require - dist: trusty - - os: osx - addons: - homebrew: - packages: - - ffmpeg - - qt5 - - grep - - opencolorio - - openimageio - update: true - # Can't build on Windows - affected by https://travis-ci.community/t/current-known-issues-please-read-this-before-posting-a-new-topic/264/10 - # - os: windows - -before_install: - - source ./.travis/before_install.sh - -install: - - source ./.travis/install.sh - -script: - - source ./.travis/script.sh - -after_success: - - source ./.travis/after_success.sh - -branches: - except: - - # Do not build tags that we create when we upload to GitHub Releases - - /^(?i:continuous)/ diff --git a/.travis/after_success.sh b/.travis/after_success.sh deleted file mode 100644 index 95e2457ff..000000000 --- a/.travis/after_success.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -# Check if there's been a new commit since this build, and if so don't upload it - -GREP_PATH=grep - -if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then - GREP_PATH=ggrep -fi - -if [ "$TRAVIS_TAG" != "" ]; then - -fi - -# Get current repo commit from GitHub (problems arose from trying to pipe cURL directly into grep, so we buffer it through a file) -REMOTE=$(curl -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/repos/olive-editor/olive/commits/master | $GREP_PATH -Po '(?<=: \")(([a-z0-9])\w+)(?=\")' -m 1 --) -LOCAL=$(git rev-parse HEAD) - -if [ "$TRAVIS_TAG" != "" || "$REMOTE" == "$LOCAL" ] -then - echo "[INFO] Still current. Uploading..." - - # retrieve upload tool - #wget -c https://github.com/probonopd/uploadtool/raw/master/upload.sh - - #if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then - - # upload final package - #bash upload.sh Olive*.zip - - #elif [[ "$TRAVIS_OS_NAME" == "linux" ]]; then - - #find appdir -executable -type f -exec ldd {} \; | grep " => /usr" | cut -d " " -f 2-3 | sort | uniq - - # upload final package - #bash upload.sh Olive*.AppImage* - - #fi - -else - - echo "[INFO] No longer current. $REMOTE vs $LOCAL - aborting upload." - -fi diff --git a/.travis/before_install.sh b/.travis/before_install.sh deleted file mode 100644 index 9eeea5ee8..000000000 --- a/.travis/before_install.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then - - # Qt 5.10.1 (may have to source newer version at some point) - sudo add-apt-repository ppa:beineri/opt-qt-5.10.1-trusty -y - - # FFmpeg 4.x (uses libs from 3.x repo) - sudo add-apt-repository ppa:jonathonf/ffmpeg-3 -y - sudo add-apt-repository ppa:jonathonf/ffmpeg-4 -y - - # OpenImageIO - sudo add-apt-repository ppa:olive-editor/openimageio -y - - # Update apt - sudo apt-get update -qq - -fi diff --git a/.travis/install.sh b/.travis/install.sh deleted file mode 100644 index 350e7ab81..000000000 --- a/.travis/install.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then - - #export PATH="/usr/local/opt/qt/bin:/usr/local/opt/python@2/libexec/bin:$PATH" - -elif [[ "$TRAVIS_OS_NAME" == "linux" ]]; then - - sudo apt-get -y -o Dpkg::Options::="--force-overwrite" install qt510base qt510multimedia qt510svg qttools5-dev libavformat-dev libavcodec-dev libavfilter-dev libavutil-dev libswscale-dev libswresample-dev cmake libopencolorio-dev libopenimageio-dev libopenexr-dev - source /opt/qt*/bin/qt*-env.sh - -fi diff --git a/.travis/script.sh b/.travis/script.sh deleted file mode 100644 index ad6e3f5d0..000000000 --- a/.travis/script.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/bash - -if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then - - # Generate Makefile - cmake . - - # Make - make -j$(sysctl -n hw.ncpu) - - BUNDLE_PATH=$(find . -name "Olive.app") - - echo Found app at: $BUNDLE_PATH - - # Move Qt deps into bundle - macdeployqt $BUNDLE_PATH - - # Fix other deps that macdeployqt missed - wget -c -nv https://github.com/arl/macdeployqtfix/raw/master/macdeployqtfix.py - python2 macdeployqtfix.py $BUNDLE_PATH/Contents/MacOS/Olive /usr/local/Cellar/qt5/5.*/ - - # Distribute in zip - zip -r Olive-$(git rev-parse --short HEAD)-macOS.zip $BUNDLE_PATH - -elif [[ "$TRAVIS_OS_NAME" == "linux" ]]; then - - # Generate Makefile - cmake . -DQt5LinguistTools_DIR=/usr/lib/x86_64-linux-gnu/cmake/Qt5LinguistTools - - # Make - make -j$(nproc) - - # Use `make install` on `appdir` to place files in the correct place - make DESTDIR=appdir -j$(nproc) install - - # Download linuxdeployqt - wget -c -nv "https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage" - chmod a+x linuxdeployqt-continuous-x86_64.AppImage - - unset QTDIR; unset QT_PLUGIN_PATH ; unset LD_LIBRARY_PATH - - # linuxdeployqt uses this for naming the file - export VERSION=$(git rev-parse --short HEAD) - - # Use linuxdeployqt to set up dependencies - ./linuxdeployqt-continuous-x86_64.AppImage appdir/usr/local/share/applications/*.desktop -extra-plugins=imageformats/libqsvg.so -appimage - -fi diff --git a/CMakeLists.txt b/CMakeLists.txt index 9c86f9c02..cd0c6d89a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2019 Olive Team +# Copyright (C) 2021 Olive Team # # 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 @@ -14,14 +14,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -cmake_minimum_required(VERSION 3.9 FATAL_ERROR) +cmake_minimum_required(VERSION 3.13 FATAL_ERROR) project(olive-editor VERSION 0.2.0 LANGUAGES CXX) -option(UPDATE_TS "Update translations" OFF) option(BUILD_DOXYGEN "Build Doxygen documentation" OFF) +option(BUILD_TESTS "Build unit tests" ON) +option(USE_WERROR "Error on compile warning" ON) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) @@ -29,32 +30,97 @@ set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) +# Set compiler options +if(MSVC) + set(OLIVE_COMPILE_OPTIONS + /wd4267 + /wd4244 + /experimental:external + /external:anglebrackets + /external:W0 + "$<$:/O2>" + "$<$:/MP>" + ) + if (USE_WERROR) + list(APPEND OLIVE_COMPILE_OPTIONS "/WX") + endif() +else() + set(OLIVE_COMPILE_OPTIONS + "$<$:-O2>" + -Wuninitialized + -pedantic-errors + -Wall + -Wextra + -Wno-unused-parameter + -Wshadow + ) + if (USE_WERROR) + list(APPEND OLIVE_COMPILE_OPTIONS "-Werror") + endif() +endif() + set(OLIVE_DEFINITIONS -DQT_DEPRECATED_WARNINGS) +if (WIN32) + list(APPEND OLIVE_DEFINITIONS -DUNICODE -D_UNICODE) +endif() list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") +# Link OpenGL if(UNIX AND NOT APPLE AND NOT DEFINED OpenGL_GL_PREFERENCE) set(OpenGL_GL_PREFERENCE LEGACY) endif() - find_package(OpenGL REQUIRED) +list(APPEND OLIVE_LIBRARIES OpenGL::GL) -find_package(OpenColorIO REQUIRED) +# Link OpenColorIO +find_package(OpenColorIO 2.0.0 REQUIRED) +list(APPEND OLIVE_LIBRARIES ${OCIO_LIBRARIES}) +list(APPEND OLIVE_INCLUDE_DIRS ${OCIO_INCLUDE_DIRS}) -find_package(OpenImageIO REQUIRED) +# Link OpenImageIO +find_package(OpenImageIO 2.1.12 REQUIRED) +list(APPEND OLIVE_LIBRARIES ${OIIO_LIBRARIES}) +list(APPEND OLIVE_INCLUDE_DIRS ${OIIO_INCLUDE_DIRS}) -find_package(Qt5 5.6 REQUIRED - COMPONENTS +# Link OpenEXR +find_package(OpenEXR REQUIRED) +list(APPEND OLIVE_LIBRARIES ${OPENEXR_LIBRARIES}) +list(APPEND OLIVE_INCLUDE_DIRS ${OPENEXR_INCLUDES}) + +# Link Qt 5 +set(QT_LIBRARIES Core Gui Widgets - Multimedia OpenGL Svg LinguistTools + Concurrent +) +if (UNIX AND NOT APPLE) + list(APPEND QT_LIBRARIES DBus) +endif() +find_package(Qt5 5.6 REQUIRED + COMPONENTS + ${QT_LIBRARIES} + OPTIONAL_COMPONENTS + Network +) +if (NOT Qt5Network_FOUND) + message(" Qt5::Network module not found, crash reporting will be disabled.") +endif() +list(APPEND OLIVE_LIBRARIES + Qt5::Core + Qt5::Gui + Qt5::Widgets + Qt5::OpenGL + Qt5::Svg + Qt5::Concurrent ) -find_package(FFMPEG 3.4 REQUIRED +# Link FFmpeg +find_package(FFMPEG 3.0 REQUIRED COMPONENTS avutil avcodec @@ -63,36 +129,88 @@ find_package(FFMPEG 3.4 REQUIRED swscale swresample ) +list(APPEND OLIVE_INCLUDE_DIRS ${FFMPEG_INCLUDE_DIRS}) +list(APPEND OLIVE_LIBRARIES + FFMPEG::avutil + FFMPEG::avcodec + FFMPEG::avformat + FFMPEG::avfilter + FFMPEG::swscale + FFMPEG::swresample +) +# Link PortAudio +find_package(PortAudio REQUIRED) +set(CMAKE_REQUIRED_INCLUDES ${PORTAUDIO_INCLUDE_DIRS}) +include(CheckIncludeFileCXX) +check_include_file_cxx( "pa_jack.h" PA_HAS_JACK) +if (PA_HAS_JACK) + list(APPEND OLIVE_DEFINITIONS PA_HAS_JACK) +endif() + +list(APPEND OLIVE_INCLUDE_DIRS ${PORTAUDIO_INCLUDE_DIRS}) +list(APPEND OLIVE_LIBRARIES ${PORTAUDIO_LIBRARIES}) + +# Optional: Link OpenTimelineIO +find_package(OpenTimelineIO) +if (OpenTimelineIO_FOUND) + list(APPEND OLIVE_DEFINITIONS USE_OTIO) + list(APPEND OLIVE_INCLUDE_DIRS ${OTIO_INCLUDE_DIRS}) + list(APPEND OLIVE_LIBRARIES ${OTIO_LIBRARIES}) +else() + message(" OpenTimelineIO interchange will be disabled.") +endif() + +# Optional: Link Google Crashpad +find_package(GoogleCrashpad) +if (GoogleCrashpad_FOUND) + list(APPEND OLIVE_DEFINITIONS USE_CRASHPAD) + list(APPEND OLIVE_INCLUDE_DIRS ${CRASHPAD_INCLUDE_DIRS}) + list(APPEND OLIVE_LIBRARIES ${CRASHPAD_LIBRARIES}) +else() + message(" Automatic crash reporting will be disabled.") + + if (APPLE) + # Enables use of special functions for slider dragging, only linked if Crashpad isn't found + # because Crashpad links it itself and will cause duplicate references if we also link it + list(APPEND OLIVE_LIBRARIES "-framework ApplicationServices") + endif() +endif() + +if (WIN32) + list(APPEND OLIVE_DEFINITIONS "-DUNICODE -D_UNICODE") +elseif (APPLE) + list(APPEND OLIVE_LIBRARIES "-framework IOKit") +elseif(UNIX) + list(APPEND OLIVE_LIBRARIES Qt5::DBus) +endif() + +# Generate Git hash +set(PROJECT_LONG_VERSION ${PROJECT_VERSION}) if(EXISTS "${CMAKE_SOURCE_DIR}/.git") find_package(Git) if(GIT_FOUND) - execute_process(COMMAND ${GIT_EXECUTABLE} log -1 --format=%h + execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short=8 HEAD WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE ) + + set(PROJECT_LONG_VERSION ${PROJECT_VERSION}-${GIT_HASH}) endif() -elseif(UNIX AND NOT APPLE AND NOT ${CMAKE_SYSTEM_NAME} MATCHES "OpenBSD") - # Fallback for Ubuntu/Launchpad (extracts Git hash from debian/changelog rather than Git repo) - # (see https://answers.launchpad.net/launchpad/+question/678556) - execute_process(COMMAND sh -c "grep -Po '(?<=-)(([a-z0-9])\\w+)(?=\\+)' -m 1 changelog" - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/debian - OUTPUT_VARIABLE GIT_HASH - OUTPUT_STRIP_TRAILING_WHITESPACE - ) -endif() -if(DEFINED GIT_HASH) - message("Olive: Git hash = " "${GIT_HASH}") - list(APPEND OLIVE_DEFINITIONS -DGITHASH="${GIT_HASH}") -else() - message("Olive: No Git hash defined!") endif() +# Optional: Find Doxygen if requested if(BUILD_DOXYGEN) find_package(Doxygen) endif() set(CMAKE_INCLUDE_CURRENT_DIR ON) +list(APPEND OLIVE_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/ext) add_subdirectory(app) + +if (BUILD_TESTS) +enable_testing() +add_subdirectory(tests) +endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f74cc0883..ef3600f54 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,36 @@ # Contributing to Olive -### Standards +Thank you for your interest in contributing to Olive! -When contributing to Olive, it's recommended to use the following rules: +## Reporting issues -* [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html) -* 120 column limit \ No newline at end of file +Bug reports help to make the software more stable and usable. +Please read the pinned [issue #1175](https://github.com/olive-editor/olive/issues/1175) +for guidelines before you create a new issue. + +## Writing code + +Code contributions are welcome. Note that the code base is rapidly changing in +the current stage of development however. There is some documentation in the form +of code comments, including Javadoc in header files. Feel free to reach out via +an issue or pull request if you have questions about the architecture or +implementation details. + +### Code Standards + +In order to keep the code as readable and maintainable as possible, code +submitted should abide by the following standards: + +* The code style generally follows the + [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html) + including, but not limited to: + * Indentation is 2 spaces wide, spaces only (no tabs) + * `lowercase_underscored_variable_names` + * `lowercase_underscored_functions()` or `SentenceCaseFunctions()` + * `class SentenceCaseClassesAndStructs {}` + * `kSentenceCaseConstants` prepended with a lowercase `k` + * `UPPERCASE_UNDERSCORED_MACROS` for variables or same style as functions for macro functions + * `class_member_variables_` end with a `_` +* 100 column limit (where it doesn't impair readability) +* Unix line endings (only LF no CRLF) +* Javadoc documentation where appropriate diff --git a/README.md b/README.md index faf62793f..eda479b1c 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,16 @@ -# Olive Video Editor [![Build Status](https://travis-ci.org/olive-editor/olive.svg?branch=master)](https://travis-ci.org/olive-editor/olive) [![Build Status](https://ci.appveyor.com/api/projects/status/5s4jabxayg51rv95?svg=true)](https://ci.appveyor.com/project/itsmattkc/olive) +# Olive Video Editor [![Build status](https://github.com/olive-editor/olive/workflows/CI/badge.svg?branch=master)](https://github.com/olive-editor/olive/actions?query=branch%3Amaster) Olive is a free non-linear video editor for Windows, macOS, and Linux. -![screen](https://www.olivevideoeditor.org/img/screenshot.jpg) +![screen](https://olivevideoeditor.org/img/020-2.png) -**Discover more:** [Website](https://www.olivevideoeditor.org/) | [Twitter](https://twitter.com/oliveteam) | [Discord](https://discord.gg/4Ae9KZn) | [Code Documentation](http://olivevideoeditor.org/doxygen/) +**Discover more:** [Website](https://www.olivevideoeditor.org/) | [Binaries](https://olivevideoeditor.org/download.php) | [Patreon](https://www.patreon.com/olivevideoeditor) | [Twitter](https://twitter.com/oliveteam) | [Wiki](https://github.com/olive-editor/olive/wiki/Overview-Guide) | [Community Discord (Unofficial)](https://discord.gg/4Ae9KZn) -**NOTE: The issue tracker is temporarily closed while Olive's core is getting rewritten. We apologize for any inconvenience. Please check back soon for the next release, as well as the [Patreon page](https://www.patreon.com/olivevideoeditor) for news and updates.** +**NOTE: Olive is alpha software and is considered highly unstable. While we highly appreciate users testing and providing usage information, please use at your own risk.** +## Binaries - -## Getting Started with Olive: - -Installation of Olive is available at: https://olivevideoeditor.org/download.php - -Instructions on how to use Olive are available on our wiki: https://github.com/olive-editor/olive/wiki/Overview-Guide - -**NOTE: Olive is Alpha Software, and it should be treated as highly unstable. Work at your own risk.** +[Nightly binaries are available on the website.](https://olivevideoeditor.org/download.php) ## Support Olive: @@ -24,10 +18,6 @@ Please consider supporting Olive: [![Become a Patron](https://olivevideoeditor.org/img/become_a_patron_button.png)](https://www.patreon.com/olivevideoeditor) -**NOTE: It is strongly discouraged to use or compile the `master` branch in its current state as it's under heavy restructuring. Please use the 0.1.x code from the "Releases" tab instead.** - ## Compiling from Source: Compiling instructions for Windows, macOS, and Linux can be found [on the main site](https://olivevideoeditor.org/compile.php). - -Olive has Doxygen-compatible documentation hosted at http://olivevideoeditor.org/doxygen/. You can also run `doxygen` in the source root directory to generate a local copy. diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 60b0cceb9..64d77da16 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2019 Olive Team +# Copyright (C) 2021 Olive Team # # 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 @@ -14,112 +14,78 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +# Set Olive sources and resources set(OLIVE_SOURCES - ${OLIVE_SOURCES} core.h core.cpp - main.cpp ) +#set(OLIVE_RESOURCES) + +# Add subdirectories, which will populate the above variables add_subdirectory(audio) +add_subdirectory(cli) +add_subdirectory(codec) add_subdirectory(common) add_subdirectory(config) -add_subdirectory(decoder) add_subdirectory(dialog) add_subdirectory(node) +add_subdirectory(packaging) add_subdirectory(panel) -add_subdirectory(project) add_subdirectory(render) +add_subdirectory(shaders) add_subdirectory(task) +add_subdirectory(threading) add_subdirectory(timeline) +add_subdirectory(ts) add_subdirectory(tool) add_subdirectory(ui) add_subdirectory(undo) add_subdirectory(widget) add_subdirectory(window) -set(OLIVE_TARGET "olive-editor") -if(APPLE) - set(OLIVE_TARGET "Olive") -endif() +# Add translations +qt5_add_translation(OLIVE_QM_FILES ${OLIVE_TS_FILES}) + +set(QRC_BODY "") +foreach(QM_FILE ${OLIVE_QM_FILES}) + get_filename_component(QM_FILENAME_COMPONENT ${QM_FILE} NAME_WE) + string(APPEND QRC_BODY "${QM_FILE}\n") +endforeach() +configure_file(ts/translations.qrc.in ts/translations.qrc @ONLY) set(OLIVE_RESOURCES ${OLIVE_RESOURCES} - #effects/internal/internalshaders.qrc + ${CMAKE_CURRENT_BINARY_DIR}/ts/translations.qrc ) -add_executable(${OLIVE_TARGET} +# Add version object +add_library(olive-version-obj + OBJECT + version.cpp + version.h +) +target_link_libraries(olive-version-obj PRIVATE Qt5::Core) +target_compile_options(olive-version-obj PRIVATE -DAPPVERSION="${PROJECT_VERSION}" -DAPPVERSIONLONG="${PROJECT_LONG_VERSION}" ) + +# Add main library +add_library(libolive-editor + OBJECT ${OLIVE_SOURCES} ${OLIVE_RESOURCES} - ${OLIVE_EFFECTS} - ${OLIVE_QM_FILES} ) -if(APPLE) - SET_TARGET_PROPERTIES(${OLIVE_TARGET} PROPERTIES - MACOSX_BUNDLE TRUE - MACOSX_FRAMEWORK_IDENTIFIER org.olivevideoeditor.Olive - ) -endif() +# Remove prefix - prevents CMake calling it "liblibolive-editor" +set_target_properties(libolive-editor PROPERTIES PREFIX "") -target_compile_definitions(${OLIVE_TARGET} PRIVATE ${OLIVE_DEFINITIONS}) - -if(MSVC) - # Enabling this threw warnings from FFmpeg headers that broke compilation. See if this is fixable. - #target_compile_options(${OLIVE_TARGET} PRIVATE /W4 /WX) -else() - target_compile_options(${OLIVE_TARGET} PRIVATE -Werror -Wuninitialized -pedantic-errors -Wall -Wextra -Wconversion -Wsign-conversion) -endif() - -target_include_directories( - ${OLIVE_TARGET} - PRIVATE - ${OPENCOLORIO_INCLUDE_DIR} - ${OIIO_INCLUDE_DIRS} - ${FFMPEG_INCLUDE_DIRS} +# Add application +add_executable(olive-editor + main.cpp + $ + $ ) -target_link_libraries(${OLIVE_TARGET} - PRIVATE - OpenGL::GL - Qt5::Core - Qt5::Gui - Qt5::Widgets - Qt5::Multimedia - Qt5::OpenGL - Qt5::Svg - FFMPEG::avutil - FFMPEG::avcodec - FFMPEG::avformat - FFMPEG::avfilter - FFMPEG::swscale - FFMPEG::swresample - ${OPENCOLORIO_LIBRARIES} - ${OIIO_LIBRARIES} -) - -set(OLIVE_EFFECTS - # FIXME: Empty variable -) - -set(OLIVE_TS_FILES - # FIXME: Empty variable -) - -if(UPDATE_TS) - qt5_create_translation(OLIVE_QM_FILES ${CMAKE_SOURCE_DIR} ${OLIVE_TS_FILES}) -else() - qt5_add_translation(OLIVE_QM_FILES ${OLIVE_TS_FILES}) -endif() - -if(UNIX AND NOT APPLE) - install(TARGETS ${OLIVE_TARGET} RUNTIME DESTINATION bin) - install(FILES ${OLIVE_EFFECTS} DESTINATION share/olive-editor/effects) - install(FILES ${OLIVE_QM_FILES} DESTINATION share/olive-editor/ts) -endif() - -add_subdirectory(packaging) - +# Create docs if doxygen was found if(DOXYGEN_FOUND) set(DOXYGEN_PROJECT_NAME "Olive") set(DOXYGEN_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/docs") @@ -127,3 +93,56 @@ if(DOXYGEN_FOUND) set(DOXYGEN_EXTRACT_PRIVATE "YES") doxygen_add_docs(docs ALL ${OLIVE_SOURCES}) endif() + +# Platform-specific deployment preferences +if (WIN32) + # Set Windows application icon + target_sources(olive-editor PRIVATE packaging/windows/resources.rc) + + # Preserve folder structure in visual studio + source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${OLIVE_SOURCES}) + +elseif(APPLE) + # Set Mac application icon + set(OLIVE_ICON packaging/macos/olive.icns) + target_sources(olive-editor PRIVATE ${OLIVE_ICON}) + + # Set Mac bundle properties + set_target_properties(olive-editor PROPERTIES + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/packaging/macos/MacOSXBundleInfo.plist.in + MACOSX_BUNDLE_GUI_IDENTIFIER org.olivevideoeditor.Olive + MACOSX_BUNDLE_ICON_FILE olive.icns + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION} + MACOSX_BUNDLE_BUNDLE_NAME "Olive" + MACOSX_BUNDLE_INFO_STRING "Olive ${PROJECT_LONG_VERSION}" + MACOSX_BUNDLE_COPYRIGHT "©2018-2021 Olive Studios LLC and others. Published under the GNU General Public License version 3.0." + RESOURCE "${OLIVE_ICON}" + OUTPUT_NAME "Olive" + ) +elseif(UNIX) + # Set Linux-specific properties for application + install(TARGETS olive-editor RUNTIME DESTINATION bin) +endif() + +# Set link libraries +target_link_libraries(olive-editor PRIVATE ${OLIVE_LIBRARIES}) +target_link_libraries(libolive-editor PRIVATE ${OLIVE_LIBRARIES}) + +# Set compile options +target_compile_options(olive-editor PRIVATE ${OLIVE_COMPILE_OPTIONS}) +target_compile_options(libolive-editor PRIVATE ${OLIVE_COMPILE_OPTIONS}) + +# Set global definitions +target_compile_definitions(olive-editor PRIVATE ${OLIVE_DEFINITIONS}) +target_compile_definitions(libolive-editor PRIVATE ${OLIVE_DEFINITIONS}) + +# Set include dirs +target_include_directories(olive-editor PRIVATE ${OLIVE_INCLUDE_DIRS}) +target_include_directories(libolive-editor PRIVATE ${OLIVE_INCLUDE_DIRS}) + +# Add crash handler +if (GoogleCrashpad_FOUND AND Qt5Network_FOUND) + add_subdirectory(crashhandler) +endif() diff --git a/app/audio/CMakeLists.txt b/app/audio/CMakeLists.txt index 419f30cdb..5d190af1f 100644 --- a/app/audio/CMakeLists.txt +++ b/app/audio/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2019 Olive Team +# Copyright (C) 2021 Olive Team # # 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 @@ -16,11 +16,15 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} - audio/audiohybriddevice.h - audio/audiohybriddevice.cpp - audio/audiomanager.h audio/audiomanager.cpp - audio/sampleformat.h - audio/sampleformat.cpp + audio/audiomanager.h + audio/audiovisualwaveform.cpp + audio/audiovisualwaveform.h + audio/packedprocessor.cpp + audio/packedprocessor.h + audio/planarprocessor.cpp + audio/planarprocessor.h + audio/tempoprocessor.cpp + audio/tempoprocessor.h PARENT_SCOPE ) diff --git a/app/audio/audiohybriddevice.cpp b/app/audio/audiohybriddevice.cpp deleted file mode 100644 index 13a207d1b..000000000 --- a/app/audio/audiohybriddevice.cpp +++ /dev/null @@ -1,151 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - 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 . - -***/ - -#include "audiohybriddevice.h" - -#include -#include - -AudioHybridDevice::AudioHybridDevice(QObject *parent) : - QIODevice(parent), - device_(nullptr), - sample_index_(0), - enable_sending_samples_(false) -{ - -} - -void AudioHybridDevice::Push(const QByteArray& samples) -{ - Stop(); - - pushed_samples_ = samples; - sample_index_ = 0; - - if (!pushed_samples_.isEmpty()) { - emit HasSamples(); - } -} - -void AudioHybridDevice::Stop() -{ - // Whatever is happening, stop it - pushed_samples_.clear(); - - if (device_ != nullptr) { - device_->close(); - device_ = nullptr; - } -} - -void AudioHybridDevice::ConnectDevice(QIODevice *device) -{ - // Clear any previous device or pushed sample - Stop(); - - device_ = device; - - if (device_ != nullptr) { - emit HasSamples(); - } -} - -bool AudioHybridDevice::IsIdle() -{ - return device_ == nullptr && pushed_samples_.isEmpty(); -} - -void AudioHybridDevice::SetEnableSendingSamples(bool e) -{ - enable_sending_samples_ = e; -} - -qint64 AudioHybridDevice::readData(char *data, qint64 maxSize) -{ - qint64 read_size = read_internal(data, maxSize); - - if (enable_sending_samples_ && read_size > 0) { - // FIXME: Assumes float and stereo - float* samples = reinterpret_cast(data); - int sample_count = static_cast(read_size / static_cast(sizeof(float))); - int channels = 2; - - // Create array of samples to send - QVector averages(channels); - averages.fill(0); - - // Add all samples together - for (int i=0;i(qAbs(samples[i]))); - } - - emit SentSamples(averages); - } - - return read_size; -} - -qint64 AudioHybridDevice::writeData(const char *data, qint64 maxSize) -{ - Q_UNUSED(data) - Q_UNUSED(maxSize) - - // This device doesn't support writing - - return -1; -} - -qint64 AudioHybridDevice::read_internal(char *data, qint64 maxSize) -{ - // If a device is connected, passthrough to it - if (device_ != nullptr) { - qint64 read_count = device_->read(data, maxSize); - - // Stop reading this device - if (device_->atEnd()) { - device_->close(); - device_ = nullptr; - } - - return read_count; - } - - // If there are samples to push, push those - if (!pushed_samples_.isEmpty()) { - qint64 read_count = qMin(maxSize, pushed_samples_.size() - sample_index_); - - memcpy(data, pushed_samples_.data()+sample_index_, static_cast(read_count)); - - sample_index_ += read_count; - - if (sample_index_ == pushed_samples_.size()) { - pushed_samples_.clear(); - } - - if (read_count < maxSize) { - memset(data + read_count, 0, static_cast(maxSize - read_count)); - } - - return maxSize; - } - - memset(data, 0, static_cast(maxSize)); - return maxSize; -} diff --git a/app/audio/audiohybriddevice.h b/app/audio/audiohybriddevice.h deleted file mode 100644 index 4a7c63dd1..000000000 --- a/app/audio/audiohybriddevice.h +++ /dev/null @@ -1,144 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - 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 . - -***/ - -#ifndef AUDIOHYBRIDDEVICE_H -#define AUDIOHYBRIDDEVICE_H - -#include - -/** - * @brief A device that can be connected to QAudioOutput and provides both "push" and "pull" functionality - * - * By default, a QAudioOutput works in either "push" or "pull" mode. Either it can automatically "pull" from a - * QIODevice (reading from it whenever it requires samples), or we can "push" samples to it constantly (on a timer and - * often in another thread) to make sure the buffer never underruns. - * - * This class functions as a "pull" device for the QAudioOutput, so the audio device automatically reads from it, but - * then provides both "pull" and "push" functionality to the rest of the system. A QIODevice (like a raw PCM file) - * can be connected and pulled from (good for continuous audio playback), and any amount of samples can also be pushed - * (good for short bursts of sound, e.g. audio scrubbing). - * - * This does not support any "queuing", running ConnectDevice() or Push() will immediately discard anything that's - * currently being sent to the device and replace it. - */ -class AudioHybridDevice : public QIODevice -{ - Q_OBJECT -public: - AudioHybridDevice(QObject* parent = nullptr); - - void Push(const QByteArray &samples); - - /** - * @brief Stop all audio output - * - * Whatever is being done (pulling from QIODevice or samples), it is stopped and cleared placing this into - * "idle" state. - * - * Note that audio playback may not stop immediately after calling this function as the audio output may still have - * samples in its buffer to output (QAudioOutput::stop() should be called as well for more immediate feedback). This - * will prevent any further samples from being sent however. - * - * \see IsIdle() - */ - void Stop(); - - /** - * @brief Connect a QIODevice (e.g. QFile) to start sending to the audio output - * - * This will clear any pushed samples or QIODevices currently being read and will start reading from this next time - * the audio output requests data. - */ - void ConnectDevice(QIODevice* device); - - /** - * @brief Returns true if there are no more samples to be sent - * - * This is true if a device was connected or samples were pushed but we reached the end and no more data is available - * to be sent. - * - * In this state, this device will continue returning an empty buffer (all zeroes) to the device to workaround - * QAudioOutput's buffer underrun prevention with pushed samples. Therefore the QAudioOutput will never be put into - * QAudio::IdleState. Therefore only way to determine whether the output is no longer receiving usable audio is to - * check this function. - */ - bool IsIdle(); - - /** - * @brief If enabled, this will emit the SentSamples() signal whenever samples are sent to the output device - */ - void SetEnableSendingSamples(bool e); - -signals: - /** - * @brief Signal emitted when this leaves "idle" state and has valid audio data that is ready to be sent - */ - void HasSamples(); - - /** - * @brief Signal emitted when samples are sent to the output device - * - * This sends an array of values corresponding to the channel count. Each value is an average of all the samples just - * sent to that channel. Useful for connecting to AudioMonitor. - * - * Can be disabled with SetEnableSendingSamples() to save CPU usage. - */ - void SentSamples(QVector averages); - -protected: - /** - * @brief Internal QIODevice function for reading - * - * Returns either QIODevice::read() from the connected device, or samples from the pushed sample buffer if there is - * none. - * - * If there is neither, this function returns a complete buffer of zeroes (i.e. silence). This is to allow - * pushed samples that don't fulfill the QAudioOutput's internal buffer size. If it cannot fill its internal buffer - * with the samples we send, it won't play anything until it receives more samples (enough to fill the buffer) - * in an attempt to prevent buffer underrun). - * - * Since we don't care about buffer underrun with short bursts of sound, we return silent samples if the QAudioOutput - * requests it. However this does mean the QAudioOutput never returns to QAudio::IdleState (since we never know if a - * read is just to fill the "remainder" of a buffer - which needs zeroes - or just a general read - which technically - * doesn't). It's recommended to check IsIdle() in tandem with the QAudioOutput::notify() signal to - * determine whether the QAudioOutput is effectively idle and can be stopped. - */ - virtual qint64 readData(char *data, qint64 maxSize) override; - - /** - * @brief Internal QIODevice function for writing - * - * This class does not support writing, so this always returns -1 (QIODevice's documented error code for writing) - */ - virtual qint64 writeData(const char *data, qint64 maxSize) override; - -private: - qint64 read_internal(char *data, qint64 maxSize); - - QIODevice* device_; - - QByteArray pushed_samples_; - qint64 sample_index_; - - bool enable_sending_samples_; - -}; - -#endif // AUDIOHYBRIDDEVICE_H diff --git a/app/audio/audiomanager.cpp b/app/audio/audiomanager.cpp index 99a1612b3..201b78ce6 100644 --- a/app/audio/audiomanager.cpp +++ b/app/audio/audiomanager.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -20,8 +20,16 @@ #include "audiomanager.h" +#ifdef PA_HAS_JACK +#include +#endif + +#include + #include "config/config.h" +namespace olive { + AudioManager* AudioManager::instance_ = nullptr; void AudioManager::CreateInstance() @@ -42,223 +50,183 @@ AudioManager *AudioManager::instance() return instance_; } -void AudioManager::RefreshDevices() +void AudioManager::SetOutputNotifyInterval(int n) { - if (refreshing_devices_) { + output_buffer_->set_notify_interval(n); +} + +int OutputCallback(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData) +{ + PreviewAudioDevice *device = static_cast(userData); + + qint64 max_read = frameCount * device->bytes_per_frame(); + qint64 read_count = device->read(reinterpret_cast(output), max_read); + if (read_count < max_read) { + memset(reinterpret_cast(output) + read_count, 0, max_read - read_count); + } + + return paContinue; +} + +void AudioManager::PushToOutput(const AudioParams ¶ms, const QByteArray &samples) +{ + if (output_device_ == paNoDevice) { return; } - input_devices_.clear(); - output_devices_.clear(); + if (output_params_ != params || output_stream_ == nullptr) { + output_params_ = params; - refreshing_devices_ = true; + CloseOutputStream(); - refresh_thread_.start(QThread::LowPriority); -} + PaStreamParameters p; -bool AudioManager::IsRefreshing() -{ - return refreshing_devices_; -} + p.channelCount = output_params_.channel_count(); + p.device = output_device_; + p.hostApiSpecificStreamInfo = nullptr; + p.sampleFormat = GetPortAudioSampleFormat(output_params_.format()); + p.suggestedLatency = Pa_GetDeviceInfo(output_device_)->defaultLowOutputLatency; -void AudioManager::PushToOutput(const QByteArray &samples) -{ - output_manager_.Push(samples); -} + Pa_OpenStream(&output_stream_, nullptr, &p, output_params_.sample_rate(), paFramesPerBufferUnspecified, paNoFlag, OutputCallback, output_buffer_); -void AudioManager::StartOutput(QIODevice *device) -{ - if (output_ == nullptr) { - return; + output_buffer_->set_bytes_per_frame(output_params_.samples_to_bytes(1)); } - output_manager_.ConnectDevice(device); + output_buffer_->write(samples); + + if (!Pa_IsStreamActive(output_stream_)) { + Pa_StartStream(output_stream_); + } +} + +void AudioManager::ClearBufferedOutput() +{ + output_buffer_->clear(); +} + +PaSampleFormat AudioManager::GetPortAudioSampleFormat(AudioParams::Format fmt) +{ + switch (fmt) { + case AudioParams::kFormatUnsigned8: + return paUInt8; + case AudioParams::kFormatSigned16: + return paInt16; + case AudioParams::kFormatSigned32: + return paInt32; + case AudioParams::kFormatFloat32: + return paFloat32; + case AudioParams::kFormatSigned64: + case AudioParams::kFormatFloat64: + case AudioParams::kFormatInvalid: + case AudioParams::kFormatCount: + break; + } + + return 0; +} + +void AudioManager::CloseOutputStream() +{ + if (output_stream_) { + if (Pa_IsStreamActive(output_stream_)) { + StopOutput(); + } + Pa_CloseStream(output_stream_); + output_stream_ = nullptr; + } } void AudioManager::StopOutput() { - output_manager_.Stop(); - - if (output_ != nullptr) { - output_->stop(); + // Abort the stream so playback stops immediately + if (output_stream_) { + Pa_AbortStream(output_stream_); + ClearBufferedOutput(); } } -void AudioManager::SetOutputDevice(const QAudioDeviceInfo &info) +void AudioManager::SetOutputDevice(PaDeviceIndex device) { - qInfo() << "Setting output audio device to" << info.deviceName(); + if (device == paNoDevice) { + qInfo() << "No output device found"; + } else { + qInfo() << "Setting output audio device to" << Pa_GetDeviceInfo(device)->name; + } - StopOutput(); + output_device_ = device; - output_device_info_ = info; + CloseOutputStream(); +} - if (output_params_.is_valid()) { - QAudioFormat format; - format.setSampleRate(output_params_.sample_rate()); - format.setChannelCount(output_params_.channel_count()); - format.setCodec("audio/pcm"); - format.setByteOrder(QAudioFormat::LittleEndian); +void AudioManager::SetInputDevice(PaDeviceIndex device) +{ + if (device == paNoDevice) { + qInfo() << "No input device found"; + } else { + qInfo() << "Setting input audio device to" << Pa_GetDeviceInfo(device)->name; + } - switch (output_params_.format()) { - case SAMPLE_FMT_U8: - format.setSampleSize(8); - format.setSampleType(QAudioFormat::UnSignedInt); - break; - case SAMPLE_FMT_S16: - format.setSampleSize(16); - format.setSampleType(QAudioFormat::SignedInt); - break; - case SAMPLE_FMT_S32: - format.setSampleSize(32); - format.setSampleType(QAudioFormat::SignedInt); - break; - case SAMPLE_FMT_S64: - format.setSampleSize(64); - format.setSampleType(QAudioFormat::SignedInt); - break; - case SAMPLE_FMT_FLT: - format.setSampleSize(32); - format.setSampleType(QAudioFormat::Float); - break; - case SAMPLE_FMT_DBL: - format.setSampleSize(64); - format.setSampleType(QAudioFormat::Float); - break; - case SAMPLE_FMT_COUNT: - case SAMPLE_FMT_INVALID: - abort(); + input_device_ = device; +} + +void AudioManager::HardReset() +{ + CloseOutputStream(); + Pa_Terminate(); + Pa_Initialize(); +} + +PaDeviceIndex AudioManager::FindConfigDeviceByName(bool is_output_device) +{ + QString entry = is_output_device ? QStringLiteral("AudioOutput") : QStringLiteral("AudioInput"); + + return FindDeviceByName(Config::Current()[entry].toString(), is_output_device); +} + +PaDeviceIndex AudioManager::FindDeviceByName(const QString &s, bool is_output_device) +{ + if (!s.isEmpty()) { + for (PaDeviceIndex i=0, end=Pa_GetDeviceCount(); imaxOutputChannels) || (!is_output_device && device->maxInputChannels)) + && !s.compare(device->name)) { + return i; + } } - - output_ = std::unique_ptr(new QAudioOutput(info, format, this)); - connect(output_.get(), SIGNAL(notify()), this, SLOT(OutputNotified())); } - // Un-comment this to get debug information about what the audio output is doing - //connect(output_.get(), SIGNAL(stateChanged(QAudio::State)), this, SLOT(OutputStateChanged(QAudio::State))); -} - -void AudioManager::SetOutputParams(const AudioRenderingParams ¶ms) -{ - if (output_params_ != params) { - output_params_ = params; - - // Refresh output device - SetOutputDevice(output_device_info_); - } -} - -void AudioManager::SetInputDevice(const QAudioDeviceInfo &info) -{ - input_ = std::unique_ptr(new QAudioInput(info, QAudioFormat(), this)); - input_device_info_ = info; -} - -const QList &AudioManager::ListInputDevices() -{ - return input_devices_; -} - -const QList &AudioManager::ListOutputDevices() -{ - return output_devices_; + return is_output_device ? Pa_GetDefaultOutputDevice() : Pa_GetDefaultInputDevice(); } AudioManager::AudioManager() : - output_(nullptr), - input_(nullptr), - input_file_(nullptr), - refreshing_devices_(false) + output_stream_(nullptr) { - connect(&refresh_thread_, SIGNAL(ListsReady()), this, SLOT(RefreshThreadDone())); +#ifdef PA_HAS_JACK + // PortAudio doesn't do a strcpy, so we need a const char that's readily accessible (i.e. not + // a QString converted to UTF-8) + PaJack_SetClientName("Olive"); +#endif - RefreshDevices(); + Pa_Initialize(); - connect(&output_manager_, SIGNAL(HasSamples()), this, SLOT(OutputManagerHasSamples())); - connect(&output_manager_, SIGNAL(SentSamples(QVector)), this, SIGNAL(SentSamples(QVector))); + // Get device from config + PaDeviceIndex output_device = FindConfigDeviceByName(true); + PaDeviceIndex input_device = FindConfigDeviceByName(false); - output_manager_.SetEnableSendingSamples(true); - output_manager_.open(AudioHybridDevice::ReadOnly); + SetOutputDevice(output_device); + SetInputDevice(input_device); + + output_buffer_ = new PreviewAudioDevice(this); + output_buffer_->open(PreviewAudioDevice::ReadWrite); + connect(output_buffer_, &PreviewAudioDevice::Notify, this, &AudioManager::OutputNotify); } -void AudioManager::RefreshThreadDone() +AudioManager::~AudioManager() { - output_devices_ = refresh_thread_.output_devices(); - input_devices_ = refresh_thread_.input_devices(); + CloseOutputStream(); - refreshing_devices_ = false; - - QString preferred_audio_output = Config::Current()["PreferredAudioOutput"].toString(); - - if (output_ == nullptr - || (!preferred_audio_output.isEmpty() && output_device_info_.deviceName() != preferred_audio_output)) { - if (preferred_audio_output.isEmpty()) { - SetOutputDevice(QAudioDeviceInfo::defaultOutputDevice()); - } else { - foreach (const QAudioDeviceInfo& info, output_devices_) { - if (info.deviceName() == preferred_audio_output) { - SetOutputDevice(info); - break; - } - } - } - } - - QString preferred_audio_input = Config::Current()["PreferredAudioInput"].toString(); - - if (input_ == nullptr - || (!preferred_audio_input.isEmpty() && input_device_info_.deviceName() != preferred_audio_input)) { - if (preferred_audio_input.isEmpty()) { - SetInputDevice(QAudioDeviceInfo::defaultInputDevice()); - } else { - foreach (const QAudioDeviceInfo& info, input_devices_) { - if (info.deviceName() == preferred_audio_input) { - SetInputDevice(info); - break; - } - } - } - } - - emit DeviceListReady(); + Pa_Terminate(); } -void AudioManager::OutputManagerHasSamples() -{ - if (output_ != nullptr && output_->state() != QAudio::ActiveState) { - output_->start(&output_manager_); - } -} - -void AudioManager::OutputStateChanged(QAudio::State state) -{ - qDebug() << state; -} - -void AudioManager::OutputNotified() -{ - if (output_manager_.IsIdle()) { - static_cast(sender())->stop(); - } -} - -AudioRefreshDevicesThread::AudioRefreshDevicesThread() -{ -} - -void AudioRefreshDevicesThread::run() -{ - output_devices_ = QAudioDeviceInfo::availableDevices(QAudio::AudioOutput); - input_devices_ = QAudioDeviceInfo::availableDevices(QAudio::AudioInput); - - emit ListsReady(); -} - -const QList& AudioRefreshDevicesThread::input_devices() -{ - return input_devices_; -} - -const QList& AudioRefreshDevicesThread::output_devices() -{ - return output_devices_; } diff --git a/app/audio/audiomanager.h b/app/audio/audiomanager.h index 9a6c2f765..0d0329bd6 100644 --- a/app/audio/audiomanager.h +++ b/app/audio/audiomanager.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -22,35 +22,17 @@ #define AUDIOMANAGER_H #include -#include -#include +#include #include +#include -#include "audiohybriddevice.h" +#include "audiovisualwaveform.h" +#include "common/define.h" #include "render/audioparams.h" +#include "render/audioplaybackcache.h" +#include "render/previewaudiodevice.h" -/** - * @brief A thread for refreshing the total list of devices on the system - * - * Refreshing devices causes a noticeable pause in execution. Doing it another thread is intended to avoid this. - */ -class AudioRefreshDevicesThread : public QThread { - Q_OBJECT -public: - AudioRefreshDevicesThread(); - - virtual void run() override; - - const QList& input_devices(); - const QList& output_devices(); - -signals: - void ListsReady(); - -private: - QList input_devices_; - QList output_devices_; -}; +namespace olive { /** * @brief Audio input and output management class @@ -67,70 +49,56 @@ public: static AudioManager* instance(); - void RefreshDevices(); + void SetOutputNotifyInterval(int n); - bool IsRefreshing(); + void PushToOutput(const AudioParams ¶ms, const QByteArray& samples); - void PushToOutput(const QByteArray& samples); + void ClearBufferedOutput(); - /** - * @brief Start playing audio from QIODevice - * - * This takes ownership of the QIODevice and will delete it when StopOutput() is called - */ - void StartOutput(QIODevice* device); - - /** - * @brief Stop audio output immediately - */ void StopOutput(); - void SetOutputDevice(const QAudioDeviceInfo& info); + PaDeviceIndex GetOutputDevice() const + { + return output_device_; + } - void SetOutputParams(const AudioRenderingParams& params); + PaDeviceIndex GetInputDevice() const + { + return input_device_; + } - void SetInputDevice(const QAudioDeviceInfo& info); + void SetOutputDevice(PaDeviceIndex device); - const QList& ListInputDevices(); - const QList& ListOutputDevices(); + void SetInputDevice(PaDeviceIndex device); + + void HardReset(); + + static PaDeviceIndex FindConfigDeviceByName(bool is_output_device); + static PaDeviceIndex FindDeviceByName(const QString &s, bool is_output_device); signals: - void DeviceListReady(); - - void SentSamples(QVector averages); + void OutputNotify(); private: AudioManager(); - QList input_devices_; + virtual ~AudioManager() override; - QList output_devices_; + static PaSampleFormat GetPortAudioSampleFormat(AudioParams::Format fmt); + + void CloseOutputStream(); static AudioManager* instance_; - AudioHybridDevice output_manager_; + PaDeviceIndex output_device_; + PaStream *output_stream_; + AudioParams output_params_; + PreviewAudioDevice *output_buffer_; - std::unique_ptr output_; - QAudioDeviceInfo output_device_info_; - AudioRenderingParams output_params_; - - std::unique_ptr input_; - QAudioDeviceInfo input_device_info_; - QIODevice* input_file_; - - bool refreshing_devices_; - - AudioRefreshDevicesThread refresh_thread_; - -private slots: - void RefreshThreadDone(); - - void OutputManagerHasSamples(); - - void OutputStateChanged(QAudio::State state); - - void OutputNotified(); + PaDeviceIndex input_device_; }; +} + #endif // AUDIOMANAGER_H diff --git a/app/audio/audiovisualwaveform.cpp b/app/audio/audiovisualwaveform.cpp new file mode 100644 index 000000000..af439bca0 --- /dev/null +++ b/app/audio/audiovisualwaveform.cpp @@ -0,0 +1,475 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "audiovisualwaveform.h" + +#include +#include + +#include "config/config.h" +#include "common/cpuoptimize.h" +#include "common/functiontimer.h" + +namespace olive { + +const rational AudioVisualWaveform::kMinimumSampleRate = rational(1, 8); +const rational AudioVisualWaveform::kMaximumSampleRate = 1024; + +AudioVisualWaveform::AudioVisualWaveform() : + channels_(0) +{ + for (rational i=kMinimumSampleRate; i<=kMaximumSampleRate; i*=2) { + mipmapped_data_.insert({i, Sample()}); + } +} + +void AudioVisualWaveform::OverwriteSamplesFromBuffer(SampleBufferPtr samples, int sample_rate, const rational &start, double target_rate, Sample& data, int &start_index, int &samples_length) +{ + start_index = time_to_samples(start, target_rate); + samples_length = time_to_samples(static_cast(samples->sample_count()) / static_cast(sample_rate), target_rate); + + int end_index = start_index + samples_length; + if (data.size() < end_index) { + data.resize(end_index); + } + + double chunk_size = double(sample_rate) / double(target_rate); + + for (int i=0; isample_count()); + + Sample summary = SumSamples(samples, + src_start, + src_end - src_start); + + memcpy(&data.data()[i + start_index], + summary.constData(), + summary.size() * sizeof(SamplePerChannel)); + } +} + +void AudioVisualWaveform::OverwriteSamplesFromMipmap(const AudioVisualWaveform::Sample &input, double input_sample_rate, int &input_start, int &input_length, const rational &start, double output_rate, AudioVisualWaveform::Sample &output_data) +{ + int start_index = time_to_samples(start, output_rate); + int samples_length = time_to_samples(static_cast(input_length / channels_) / input_sample_rate, output_rate); + + int end_index = start_index + samples_length; + if (output_data.size() < end_index) { + output_data.resize(end_index); + } + + // We guarantee mipmaps are powers of two so integer division should be perfectly accurate here + int chunk_size = input_sample_rate / output_rate; + + for (int i=0; ifirst.toDouble(), it->second, input_start, input_length); + // } + + // Process the largest mipmap directly for the samples + auto current_mipmap = mipmapped_data_.rbegin(); + int input_start, input_length; + OverwriteSamplesFromBuffer(samples, sample_rate, start, current_mipmap->first.toDouble(), current_mipmap->second, input_start, input_length); + + while (true) { + // For each smaller mipmap, we just process from the mipmap before it, making each one + // exponentially faster to create + auto previous_mipmap = current_mipmap; + current_mipmap++; + if (current_mipmap == mipmapped_data_.rend()) { + break; + } + + OverwriteSamplesFromMipmap(previous_mipmap->second, previous_mipmap->first.toDouble(), + input_start, input_length, start, current_mipmap->first.toDouble(), + current_mipmap->second); + } + + rational sample_length(samples->sample_count(), sample_rate); + length_ = qMax(length_, start + sample_length); +} + +void AudioVisualWaveform::OverwriteSums(const AudioVisualWaveform &sums, const rational &dest, const rational& offset, const rational& length) +{ + for (auto it=mipmapped_data_.begin(); it!=mipmapped_data_.end(); it++) { + rational rate = it->first; + + Sample& our_arr = it->second; + const Sample& their_arr = sums.mipmapped_data_.at(rate); + + double rate_dbl = rate.toDouble(); + + // Get our destination sample + int our_start_index = time_to_samples(dest, rate_dbl); + + // Get our source sample + int their_start_index = time_to_samples(offset, rate_dbl); + if (their_start_index >= their_arr.size()) { + continue; + } + + // Determine how much we're copying + int copy_len = their_arr.size() - their_start_index; + if (!length.isNull()) { + copy_len = qMin(copy_len, time_to_samples(length, rate_dbl)); + } + + // Determine end index of our array + int end_index = our_start_index + copy_len; + if (our_arr.size() < end_index) { + our_arr.resize(end_index); + } + + memcpy(reinterpret_cast(our_arr.data()) + our_start_index * sizeof(SamplePerChannel), + reinterpret_cast(their_arr.constData()) + their_start_index * sizeof(SamplePerChannel), + copy_len * sizeof(SamplePerChannel)); + } + + length_ = qMax(length_, dest + ((length.isNull()) ? sums.length() - offset : length)); +} + +void AudioVisualWaveform::OverwriteSilence(const rational &start, const rational &length) +{ + for (auto it=mipmapped_data_.begin(); it!=mipmapped_data_.end(); it++) { + rational rate = it->first; + + Sample& our_arr = it->second; + + double rate_dbl = rate.toDouble(); + + // Get our destination sample + int our_start_index = time_to_samples(start, rate_dbl); + int our_length_index = time_to_samples(length, rate_dbl); + int our_end_index = our_start_index + our_length_index; + + if (our_arr.size() < our_end_index) { + our_arr.resize(our_end_index); + } + + memset(reinterpret_cast(our_arr.data()) + our_start_index * sizeof(SamplePerChannel), 0, our_length_index * sizeof(SamplePerChannel)); + } +} + +void AudioVisualWaveform::Shift(const rational &from, const rational &to) +{ + for (auto it=mipmapped_data_.begin(); it!=mipmapped_data_.end(); it++) { + rational rate = it->first; + double rate_dbl = rate.toDouble(); + Sample& data = it->second; + + int from_index = time_to_samples(from, rate_dbl); + int to_index = time_to_samples(to, rate_dbl); + + if (from_index == to_index) { + continue; + } + + if (from_index >= data.size()) { + continue; + } + + if (from_index > to_index) { + // Shifting backwards <- + data.remove(to_index, from_index - to_index); + } else { + // Shifting forwards -> + data.insert(from_index, to_index - from_index, {0, 0}); + } + } + + length_ = qMax(rational(0), length_ + (to-from)); +} + +void AudioVisualWaveform::TrimIn(const rational &length) +{ + for (auto it=mipmapped_data_.begin(); it!=mipmapped_data_.end(); it++) { + rational rate = it->first; + double rate_dbl = rate.toDouble(); + Sample& data = it->second; + + int chop_length = time_to_samples(length, rate_dbl); + + if (chop_length == 0) { + continue; + } + + if (chop_length > 0) { + data = data.mid(chop_length); + } else { + data.insert(0, -chop_length, SamplePerChannel()); + } + } + + length_ = qMax(rational(0), length_ - length); +} + +AudioVisualWaveform AudioVisualWaveform::Mid(const rational &offset) const +{ + AudioVisualWaveform mid = *this; + + mid.TrimIn(offset); + + return mid; +} + +AudioVisualWaveform::Sample AudioVisualWaveform::GetSummaryFromTime(const rational &start, const rational &length) const +{ + // Find mipmap that requires + auto using_mipmap = GetMipmapForScale(length.flipped().toDouble()); + + double rate_dbl = using_mipmap->first.toDouble(); + + int start_sample = time_to_samples(start, rate_dbl); + int sample_length = time_to_samples(length, rate_dbl); + + const QVector &mipmap_data = using_mipmap->second; + + // Determine if the array actually has this sample + sample_length = qMin(sample_length, mipmap_data.size() - start_sample); + + // Based on the above `min`, if sample length <= 0, that means start_sample >= the size of the + // array and nothing can be returned. + if (sample_length > 0) { + return ReSumSamples(&mipmap_data.constData()[start_sample], sample_length, channels_); + } + + // Return null samples + return AudioVisualWaveform::Sample(channel_count(), {0, 0}); +} + +void ExpandMinMaxChannel(float *a, int start, int length, float &min_val, float &max_val) +{ +#if defined(Q_PROCESSOR_X86) || defined(Q_PROCESSOR_ARM) + // SSE optimized + + // load the first 4 elements of 'a' into min and max (they are 4 * 32 = 128 bits) + __m128 max = _mm_loadu_ps(a + start); + __m128 min = _mm_loadu_ps(a + start); + + // loop over 'a' and compare current elements with min and max 4 by 4. + // we need to make sure we don't read out of boundaries should 'a' lenght be not mod. 4 + for(int i = 4; i < length-4; i+=4) { + __m128 cur = _mm_loadu_ps(a + start + i); + max = _mm_max_ps(max, cur); + min = _mm_min_ps(min, cur); + } + // so we read the last 4 (or less) elements in a safe manner. + __m128 cur = _mm_loadu_ps(a + length - 4); + max = _mm_max_ps(max, cur); + min = _mm_min_ps(min, cur); + // this potentially overlaps up to the last 3 elements but it's not an issue. + + // min and max will contain 4 min and max. To get the absolute min and max + // we need to compare the 4 values over themselves by shuffling each time. + for (int i = 0; i < 3; i++) { + max = _mm_max_ps(max, _mm_shuffle_ps(max, max, 0x93)); + min = _mm_min_ps(min, _mm_shuffle_ps(min, min, 0x93)); + } + // now min and max contain 4 identical items each representing min and max value respectively. + + // and we store the first one into a float variable. + _mm_store_ss(&max_val, max); + _mm_store_ss(&min_val, min); + // I bet you don't find annotated low level code very often. +#else + // Standard unoptimized function + int end = start + length; + for (int i=start; iaudio_params().channel_count(); + AudioVisualWaveform::Sample summed_samples(channels); + + for (int channel=0; channelaudio_params().channel_count(); channel++) { + ExpandMinMaxChannel(samples->data(channel), start_index, length, summed_samples[channel].min, summed_samples[channel].max); + } + + // for reference: this approximation is n x faster (and less accurate) for a n-tracks clip + // for (int i=start_index; idata(i%channels)[i]); + // } + + return summed_samples; +} + +AudioVisualWaveform::Sample AudioVisualWaveform::ReSumSamples(const SamplePerChannel* samples, + int nb_samples, + int nb_channels) +{ + AudioVisualWaveform::Sample summed_samples(nb_channels); + + for (int i=0;i summed_samples[j].max) { + summed_samples[j].max = sample.max; + } + } + } + + return summed_samples; +} + +void AudioVisualWaveform::DrawSample(QPainter *painter, const Sample& sample, int x, int y, int height, bool rectified) +{ + if (sample.isEmpty()) { + return; + } + + int channel_height = height / sample.size(); + int channel_half_height = channel_height / 2; + + for (int i=0;idrawLine(x, + channel_bottom - diff, + x, + channel_bottom); + } else { + int channel_mid = y + channel_height * i + channel_half_height; + + // We subtract the sample so that positive Y values go up on the screen rather than down, + // which is how waveforms are usually rendered + painter->drawLine(x, + channel_mid - qRound(min * static_cast(channel_half_height)), + x, + channel_mid - qRound(max * static_cast(channel_half_height))); + } + } +} + +void AudioVisualWaveform::DrawWaveform(QPainter *painter, const QRect& rect, const double& scale, const AudioVisualWaveform &samples, const rational& start_time) +{ + if (samples.mipmapped_data_.empty()) { + return; + } + + auto using_mipmap = samples.GetMipmapForScale(scale); + + rational rate = using_mipmap->first; + double rate_dbl = rate.toDouble(); + const Sample& arr = using_mipmap->second; + + int start_sample_index = samples.time_to_samples(start_time, rate_dbl); + + if (start_sample_index >= arr.size()) { + return; + } + + int next_sample_index = start_sample_index; + int sample_index; + + Sample summary; + int summary_index = -1; + + const QRect& viewport = painter->viewport(); + QPoint top_left = painter->transform().map(viewport.topLeft()); + + int start = qMax(rect.x(), -top_left.x()); + int end = qMin(rect.right(), -top_left.x() + viewport.width()); + + bool rectified = Config::Current()[QStringLiteral("RectifiedWaveforms")].toBool(); + + for (int i=start;i(i - rect.x() + 1) / scale) * samples.channel_count()); + + if (summary_index != sample_index) { + summary = AudioVisualWaveform::ReSumSamples(&arr.at(sample_index), + qMax(samples.channel_count(), next_sample_index - sample_index), + samples.channel_count()); + summary_index = sample_index; + } + + DrawSample(painter, summary, i, rect.y(), rect.height(), rectified); + } +} + +int AudioVisualWaveform::time_to_samples(const rational &time, double sample_rate) const +{ + return time_to_samples(time.toDouble(), sample_rate); +} + +int AudioVisualWaveform::time_to_samples(const double &time, double sample_rate) const +{ + return qFloor(time * sample_rate) * channels_; +} + +std::map::const_iterator AudioVisualWaveform::GetMipmapForScale(double scale) const +{ + // Find largest mipmap for this scale (or the largest if we don't find one sufficient) + for (auto it=mipmapped_data_.cbegin(); it!=mipmapped_data_.cend(); it++) { + if (it->first.toDouble() >= scale) { + return it; + } + } + + // We don't have a mipmap large enough for this scale, so just return the largest we have + return std::prev(mipmapped_data_.cend()); +} + +} diff --git a/app/audio/audiovisualwaveform.h b/app/audio/audiovisualwaveform.h new file mode 100644 index 000000000..5659e46ff --- /dev/null +++ b/app/audio/audiovisualwaveform.h @@ -0,0 +1,135 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef SUMSAMPLES_H +#define SUMSAMPLES_H + +#include +#include + +#include "codec/samplebuffer.h" + +namespace olive { + +/** + * @brief A buffer of data used to store a visual representation of audio + * + * This differs from a SampleBuffer as the data in an AudioVisualWaveform has been reduced + * significantly and optimized for visual display. + */ +class AudioVisualWaveform { +public: + AudioVisualWaveform(); + + struct SamplePerChannel { + float min; + float max; + }; + + using Sample = QVector; + + int channel_count() const + { + return channels_; + } + + void set_channel_count(int channels) + { + channels_ = channels; + } + + const rational& length() const + { + return length_; + } + + /** + * @brief Writes samples into the visual waveform buffer + * + * Starting at `start`, writes samples over anything in the buffer, expanding it if necessary. + */ + void OverwriteSamples(SampleBufferPtr samples, int sample_rate, const rational& start = 0); + + /** + * @brief Replaces sums at a certain range in this visual waveform + * + * @param sums + * + * The sums to write over our current ones with. + * + * @param dest + * + * Where in this visual waveform these sums should START being written to. + * + * @param offset + * + * Where in the `sums` parameter this should start reading from. Defaults to 0. + * + * @param length + * + * Maximum length of `sums` to overwrite with. + */ + void OverwriteSums(const AudioVisualWaveform& sums, const rational& dest, const rational& offset = 0, const rational &length = 0); + + void OverwriteSilence(const rational &start, const rational &length); + + void Shift(const rational& from, const rational& to); + + void TrimIn(const rational &length); + + AudioVisualWaveform Mid(const rational &offset) const; + + Sample GetSummaryFromTime(const rational& start, const rational& length) const; + + static Sample SumSamples(SampleBufferPtr samples, int start_index, int length); + + static Sample ReSumSamples(const SamplePerChannel *samples, int nb_samples, int nb_channels); + + static void DrawSample(QPainter* painter, const Sample &sample, int x, int y, int height, bool rectified); + + static void DrawWaveform(QPainter* painter, const QRect &rect, const double &scale, const AudioVisualWaveform& samples, const rational &start_time); + + // Must be a power of 2 + static const rational kMinimumSampleRate; + static const rational kMaximumSampleRate; + +private: + void OverwriteSamplesFromBuffer(SampleBufferPtr samples, int sample_rate, const rational& start, double target_rate, Sample &data, int &start_index, int &samples_length); + + void OverwriteSamplesFromMipmap(const Sample& input, double input_sample_rate, int &input_start, int &input_length, const rational& start, double output_rate, Sample &output_data); + + int time_to_samples(const rational& time, double sample_rate) const; + int time_to_samples(const double& time, double sample_rate) const; + + std::map::const_iterator GetMipmapForScale(double scale) const; + + int channels_; + + std::map mipmapped_data_; + + rational length_; + +}; + +} + +Q_DECLARE_METATYPE(olive::AudioVisualWaveform) + +#endif // SUMSAMPLES_H diff --git a/app/audio/packedprocessor.cpp b/app/audio/packedprocessor.cpp new file mode 100644 index 000000000..f748da5c2 --- /dev/null +++ b/app/audio/packedprocessor.cpp @@ -0,0 +1,101 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "packedprocessor.h" + +#include "common/ffmpegutils.h" + +namespace olive { + +PackedProcessor::PackedProcessor() : + swr_ctx_(nullptr) +{ +} + +PackedProcessor::~PackedProcessor() +{ + Close(); +} + +bool PackedProcessor::Open(const AudioParams ¶ms) +{ + if (IsOpen()) { + return true; + } + + swr_ctx_ = swr_alloc_set_opts(nullptr, + params.channel_layout(), + FFmpegUtils::GetFFmpegSampleFormat(params.format(), false), + params.sample_rate(), + params.channel_layout(), + FFmpegUtils::GetFFmpegSampleFormat(params.format(), true), + params.sample_rate(), + 0, + nullptr); + + if (!swr_ctx_) { + qCritical() << "Failed to allocate resample context"; + return false; + } + + if (swr_init(swr_ctx_) < 0) { + qCritical() << "Failed to init resample context"; + swr_free(&swr_ctx_); + return false; + } + + return true; +} + +QByteArray PackedProcessor::Convert(SampleBufferPtr planar) +{ + if (!IsOpen()) { + qCritical() << "Tried to convert while closed"; + return QByteArray(); + } + + int nb_samples = planar->sample_count(); + if (nb_samples == 0) { + return QByteArray(); + } + + QByteArray output(planar->audio_params().samples_to_bytes(nb_samples), Qt::Uninitialized); + uint8_t *output_data = reinterpret_cast(output.data()); + + int ret = swr_convert(swr_ctx_, &output_data, nb_samples, + const_cast(reinterpret_cast(planar->to_raw_ptrs())), + nb_samples); + if (ret < 0) { + char buf[200]; + av_strerror(ret, buf, 200); + qDebug() << "Packed processor failed with error:" << buf << ret; + } + + return output; +} + +void PackedProcessor::Close() +{ + if (swr_ctx_) { + swr_free(&swr_ctx_); + } +} + +} diff --git a/app/audio/packedprocessor.h b/app/audio/packedprocessor.h new file mode 100644 index 000000000..8b1bb59c5 --- /dev/null +++ b/app/audio/packedprocessor.h @@ -0,0 +1,60 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef PACKEDPROCESSOR_H +#define PACKEDPROCESSOR_H + +extern "C" { +#include +} + +#include "codec/samplebuffer.h" +#include "render/audioparams.h" + +namespace olive { + +class PackedProcessor +{ +public: + PackedProcessor(); + + ~PackedProcessor(); + + DISABLE_COPY_MOVE(PackedProcessor) + + bool Open(const AudioParams ¶ms); + + QByteArray Convert(SampleBufferPtr planar); + + void Close(); + + bool IsOpen() const + { + return swr_ctx_; + } + +private: + SwrContext *swr_ctx_; + +}; + +} + +#endif // PACKEDPROCESSOR_H diff --git a/app/audio/planarprocessor.cpp b/app/audio/planarprocessor.cpp new file mode 100644 index 000000000..abd2be85b --- /dev/null +++ b/app/audio/planarprocessor.cpp @@ -0,0 +1,104 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "planarprocessor.h" + +#include "common/ffmpegutils.h" + +namespace olive { + +PlanarProcessor::PlanarProcessor() : + swr_ctx_(nullptr) +{ +} + +PlanarProcessor::~PlanarProcessor() +{ + Close(); +} + +bool PlanarProcessor::Open(const AudioParams ¶ms) +{ + if (IsOpen()) { + return true; + } + + swr_ctx_ = swr_alloc_set_opts(nullptr, + params.channel_layout(), + FFmpegUtils::GetFFmpegSampleFormat(params.format(), true), + params.sample_rate(), + params.channel_layout(), + FFmpegUtils::GetFFmpegSampleFormat(params.format(), false), + params.sample_rate(), + 0, + nullptr); + + if (!swr_ctx_) { + qCritical() << "Failed to allocate resample context"; + return false; + } + + if (swr_init(swr_ctx_) < 0) { + qCritical() << "Failed to init resample context"; + swr_free(&swr_ctx_); + return false; + } + + params_ = params; + + return true; +} + +SampleBufferPtr PlanarProcessor::Convert(const QByteArray &packed) +{ + if (!IsOpen()) { + qCritical() << "Tried to convert while closed"; + return nullptr; + } + + if (packed.isEmpty()) { + return nullptr; + } + + int nb_samples_per_channel = params_.bytes_to_samples(packed.size()); + + SampleBufferPtr output = SampleBuffer::CreateAllocated(params_, nb_samples_per_channel); + + const uint8_t *input = reinterpret_cast(packed.constData()); + int ret = swr_convert(swr_ctx_, + reinterpret_cast(output->to_raw_ptrs()), nb_samples_per_channel, + &input, nb_samples_per_channel); + if (ret < 0) { + char buf[200]; + av_strerror(ret, buf, 200); + qDebug() << "Planar processor failed with error:" << buf << ret; + } + + return output; +} + +void PlanarProcessor::Close() +{ + if (swr_ctx_) { + swr_free(&swr_ctx_); + } +} + +} diff --git a/app/audio/planarprocessor.h b/app/audio/planarprocessor.h new file mode 100644 index 000000000..ad5167a95 --- /dev/null +++ b/app/audio/planarprocessor.h @@ -0,0 +1,62 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef PLANARPROCESSOR_H +#define PLANARPROCESSOR_H + +extern "C" { +#include +} + +#include "codec/samplebuffer.h" +#include "render/audioparams.h" + +namespace olive { + +class PlanarProcessor +{ +public: + PlanarProcessor(); + + ~PlanarProcessor(); + + DISABLE_COPY_MOVE(PlanarProcessor) + + bool Open(const AudioParams ¶ms); + + SampleBufferPtr Convert(const QByteArray &packed); + + void Close(); + + bool IsOpen() const + { + return swr_ctx_; + } + +private: + SwrContext *swr_ctx_; + + AudioParams params_; + +}; + +} + +#endif // PLANARPROCESSOR_H diff --git a/app/audio/sampleformat.cpp b/app/audio/sampleformat.cpp deleted file mode 100644 index 08fff1777..000000000 --- a/app/audio/sampleformat.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "sampleformat.h" diff --git a/app/audio/tempoprocessor.cpp b/app/audio/tempoprocessor.cpp new file mode 100644 index 000000000..f22946227 --- /dev/null +++ b/app/audio/tempoprocessor.cpp @@ -0,0 +1,269 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "tempoprocessor.h" + +extern "C" { +#include +#include +#include +} + +#include + +#include "common/ffmpegutils.h" + +namespace olive { + +TempoProcessor::TempoProcessor() : + filter_graph_(nullptr), + buffersrc_ctx_(nullptr), + buffersink_ctx_(nullptr), + open_(false) +{ +} + +TempoProcessor::~TempoProcessor() +{ + Close(); +} + +bool TempoProcessor::IsOpen() const +{ + return open_; +} + +const double &TempoProcessor::GetSpeed() const +{ + return speed_; +} + +bool TempoProcessor::Open(const AudioParams ¶ms, const double& speed) +{ + if (open_) { + return true; + } + + params_ = params; + speed_ = speed; + + // Create AVFilterGraph instance + filter_graph_ = avfilter_graph_alloc(); + if (!filter_graph_) { + qCritical() << "Failed to create AVFilterGraph"; + Close(); + return false; + } + + // Set up audio buffer args + char filter_args[200]; + snprintf(filter_args, 200, "time_base=%d/%d:sample_rate=%d:sample_fmt=%d:channel_layout=0x%" PRIx64, + 1, + params_.sample_rate(), + params_.sample_rate(), + FFmpegUtils::GetFFmpegSampleFormat(params_.format()), + params.channel_layout()); + + // Create buffer and buffersink + if (avfilter_graph_create_filter(&buffersrc_ctx_, avfilter_get_by_name("abuffer"), "in", filter_args, nullptr, filter_graph_) < 0) { + qCritical() << "Failed to create audio buffer source"; + Close(); + return false; + } + + if (avfilter_graph_create_filter(&buffersink_ctx_, avfilter_get_by_name("abuffersink"), "out", nullptr, nullptr, filter_graph_) < 0) { + qCritical() << "Failed to create audio buffer sink"; + Close(); + return false; + } + + // Create audio tempo filters: FFmpeg's atempo can only be set between 0.5 and 2.0. If the requested speed is outside + // those boundaries, we need to daisychain more than one together. + double base = (speed_ > 1.0) ? 2.0 : 0.5; + double speed_log = log(speed_) / log(base); + + // This is the number of how many 0.5 or 2.0 tempos we need to daisychain + int whole = qFloor(speed_log); + + // Set speed_log to the remainder + speed_log -= whole; + + AVFilterContext* previous_filter = buffersrc_ctx_; + + for (int i=0;i<=whole;i++) { + double filter_tempo = (i == whole) ? qPow(base, speed_log) : base; + + if (qFuzzyCompare(filter_tempo, 1.0)) { + // This filter would do nothing + continue; + } + + previous_filter = CreateTempoFilter(filter_graph_, + previous_filter, + filter_tempo); + + if (!previous_filter) { + qCritical() << "Failed to create audio tempo filter"; + Close(); + return false; + } + } + + // Link the last filter to the buffersink + if (avfilter_link(previous_filter, 0, buffersink_ctx_, 0) != 0) { + qCritical() << "Failed to link final filter and buffer sink"; + Close(); + return false; + } + + // Config graph + if (avfilter_graph_config(filter_graph_, nullptr) < 0) { + qCritical() << "Failed to configure filter graph"; + Close(); + return false; + } + + timestamp_ = 0; + + open_ = true; + + flushed_ = false; + + return true; +} + +void TempoProcessor::Push(const QByteArray &packed) +{ + if (!IsOpen()) { + qWarning() << "Tried to push to closed TempoProcessor"; + return; + } + + if (flushed_) { + qWarning() << "Tried to push to flushed TempoProcessor"; + return; + } + + AVFrame* src_frame = av_frame_alloc(); + + if (!src_frame) { + qCritical() << "Failed to allocate source frame"; + return; + } + + // Allocate a buffer for the number of samples we got + src_frame->sample_rate = params_.sample_rate(); + src_frame->format = FFmpegUtils::GetFFmpegSampleFormat(params_.format()); + src_frame->channel_layout = params_.channel_layout(); + src_frame->nb_samples = params_.bytes_to_samples(packed.size()); + src_frame->pts = timestamp_; + timestamp_ += src_frame->nb_samples; + + if (av_frame_get_buffer(src_frame, 0) < 0) { + qCritical() << "Failed to allocate buffer for source frame"; + av_frame_free(&src_frame); + return; + } + + // Copy buffer from data array to frame + memcpy(src_frame->data[0], packed, packed.size()); + + int ret = av_buffersrc_add_frame_flags(buffersrc_ctx_, src_frame, AV_BUFFERSRC_FLAG_KEEP_REF); + + if (ret < 0) { + qCritical() << "Failed to feed buffer source" << ret; + } + + if (src_frame) { + av_frame_free(&src_frame); + } +} + +void TempoProcessor::Flush() +{ + if (!flushed_) { + int ret = av_buffersrc_add_frame_flags(buffersrc_ctx_, nullptr, AV_BUFFERSRC_FLAG_KEEP_REF); + if (ret < 0) { + qCritical() << "Failed to feed buffer source" << ret; + } + flushed_ = true; + } +} + +QByteArray TempoProcessor::Pull() +{ + QByteArray b; + AVFrame *processed_frame = av_frame_alloc(); + + // Try to pull samples from the buffersink + int ret = av_buffersink_get_frame(buffersink_ctx_, processed_frame); + + if (ret < 0) { + // We couldn't pull for some reason, if the error was EAGAIN, we just need to send more samples. Otherwise the + // error might be fatal... + if (ret != AVERROR(EAGAIN)) { + qCritical() << "Failed to pull from buffersink" << ret; + } + + av_frame_free(&processed_frame); + return b; + } + + b.resize(params_.samples_to_bytes(processed_frame->nb_samples)); + + // Copy the bytes + memcpy(b.data(), processed_frame->data[0], b.size()); + + // If the index has reached the limit of this processed frame, we can dispose of the frame now + av_frame_free(&processed_frame); + + return b; +} + +void TempoProcessor::Close() +{ + open_ = false; + + if (filter_graph_) { + avfilter_graph_free(&filter_graph_); + filter_graph_ = nullptr; + } + + buffersrc_ctx_ = nullptr; + buffersink_ctx_ = nullptr; +} + +AVFilterContext *TempoProcessor::CreateTempoFilter(AVFilterGraph* graph, AVFilterContext* link, const double &tempo) +{ + // Set up tempo param, which is taken as a C string + char speed_param[20]; + snprintf(speed_param, 20, "%f", tempo); + + AVFilterContext* tempo_ctx = nullptr; + + if (avfilter_graph_create_filter(&tempo_ctx, avfilter_get_by_name("atempo"), "atempo", speed_param, nullptr, graph) >= 0 + && avfilter_link(link, 0, tempo_ctx, 0) == 0) { + return tempo_ctx; + } + + return nullptr; +} + +} diff --git a/app/audio/tempoprocessor.h b/app/audio/tempoprocessor.h new file mode 100644 index 000000000..c56eafc5d --- /dev/null +++ b/app/audio/tempoprocessor.h @@ -0,0 +1,85 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef TEMPOPROCESSOR_H +#define TEMPOPROCESSOR_H + +#ifdef __MINGW32__ +#ifndef __USE_MINGW_ANSI_STDIO +#define __USE_MINGW_ANSI_STDIO +#endif +#endif + +#include + +extern "C" { +#include +} + +#include "render/audioparams.h" + +namespace olive { + +class TempoProcessor +{ +public: + TempoProcessor(); + + ~TempoProcessor(); + + DISABLE_COPY_MOVE(TempoProcessor) + + bool IsOpen() const; + + const double& GetSpeed() const; + + bool Open(const AudioParams& params, const double &speed); + + void Push(const QByteArray &packed); + + void Flush(); + + QByteArray Pull(); + + void Close(); + +private: + static AVFilterContext* CreateTempoFilter(AVFilterGraph *graph, AVFilterContext *link, const double& tempo); + + AVFilterGraph* filter_graph_; + + AVFilterContext* buffersrc_ctx_; + + AVFilterContext* buffersink_ctx_; + + AudioParams params_; + + int64_t timestamp_; + + double speed_; + + bool open_; + + bool flushed_; +}; + +} + +#endif // TEMPOPROCESSOR_H diff --git a/app/decoder/oiio/CMakeLists.txt b/app/cli/CMakeLists.txt similarity index 88% rename from app/decoder/oiio/CMakeLists.txt rename to app/cli/CMakeLists.txt index 5a20b7719..b96b46395 100644 --- a/app/decoder/oiio/CMakeLists.txt +++ b/app/cli/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2019 Olive Team +# Copyright (C) 2021 Olive Team # # 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 @@ -14,9 +14,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +add_subdirectory(cliprogress) +add_subdirectory(clitask) + set(OLIVE_SOURCES ${OLIVE_SOURCES} - decoder/oiio/oiiodecoder.h - decoder/oiio/oiiodecoder.cpp PARENT_SCOPE ) diff --git a/app/node/menu.cpp b/app/cli/cliexport/cliexportmanager.cpp similarity index 82% rename from app/node/menu.cpp rename to app/cli/cliexport/cliexportmanager.cpp index 17f05e266..7e84ed84c 100644 --- a/app/node/menu.cpp +++ b/app/cli/cliexport/cliexportmanager.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -18,11 +18,13 @@ ***/ -#include "menu.h" +#include "cliexportmanager.h" -#include "blend/alphaover/alphaover.h" +namespace olive { -NodeMenu::NodeMenu(QWidget *parent) : - QMenu(parent) +CLIExportManager::CLIExportManager() { + +} + } diff --git a/app/cli/cliexport/cliexportmanager.h b/app/cli/cliexport/cliexportmanager.h new file mode 100644 index 000000000..dee831bb7 --- /dev/null +++ b/app/cli/cliexport/cliexportmanager.h @@ -0,0 +1,36 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef CLIEXPORTMANAGER_H +#define CLIEXPORTMANAGER_H + +#include "task/export/export.h" + +namespace olive { + +class CLIExportManager : public QObject +{ +public: + CLIExportManager(); +}; + +} + +#endif // CLIEXPORTMANAGER_H diff --git a/app/cli/cliprogress/CMakeLists.txt b/app/cli/cliprogress/CMakeLists.txt new file mode 100644 index 000000000..0dcf47f90 --- /dev/null +++ b/app/cli/cliprogress/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + cli/cliprogress/cliprogressdialog.h + cli/cliprogress/cliprogressdialog.cpp + PARENT_SCOPE +) diff --git a/app/cli/cliprogress/cliprogressdialog.cpp b/app/cli/cliprogress/cliprogressdialog.cpp new file mode 100644 index 000000000..d4ffa0095 --- /dev/null +++ b/app/cli/cliprogress/cliprogressdialog.cpp @@ -0,0 +1,106 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "cliprogressdialog.h" + +#include + +namespace olive { + +CLIProgressDialog::CLIProgressDialog(const QString& title, QObject *parent) : + QObject(parent), + title_(title), + progress_(-1), + drawn_(false) +{ + SetProgress(0); +} + +void CLIProgressDialog::Update() +{ + if (drawn_) { + // We've been here before, do a carriage return back to the start of the terminal line + std::cout << "\r"; + } else { + drawn_ = true; + } + + // FIXME: Get real column count + int columns = 80; + + int title_columns = columns / 2 - 1; + + // Print "title" text + QString sized_title = title_; + + if (title_.size() > title_columns) { + sized_title = title_.left(title_columns - 3).append(QStringLiteral("...")); + } else { + sized_title = title_; + } + + std::cout << sized_title.toUtf8().constData(); + + // Pad out the rest of the title area if necessary + for (int i=sized_title.size(); i +#include -class TrackReference +#include "common/define.h" + +namespace olive { + +class CLIProgressDialog : public QObject { public: - TrackReference(); + CLIProgressDialog(const QString &title, QObject* parent = nullptr); - TrackReference(const TrackType& type, const int& index); - - const TrackType& type() const; - - const int& index() const; - - bool operator==(const TrackReference& ref) const; +public slots: + void SetProgress(double p); private: - TrackType type_; + void Update(); + + QString title_; + + double progress_; + + bool drawn_; - int index_; }; -#endif // TRACKREFERENCE_H +} + +#endif // CLIPROGRESSDIALOG_H diff --git a/app/node/color/opacity/CMakeLists.txt b/app/cli/clitask/CMakeLists.txt similarity index 88% rename from app/node/color/opacity/CMakeLists.txt rename to app/cli/clitask/CMakeLists.txt index 13c645217..0c904acb7 100644 --- a/app/node/color/opacity/CMakeLists.txt +++ b/app/cli/clitask/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2019 Olive Team +# Copyright (C) 2021 Olive Team # # 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 @@ -16,7 +16,7 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} - node/color/opacity/opacity.h - node/color/opacity/opacity.cpp + cli/clitask/clitaskdialog.h + cli/clitask/clitaskdialog.cpp PARENT_SCOPE ) diff --git a/app/cli/clitask/clitaskdialog.cpp b/app/cli/clitask/clitaskdialog.cpp new file mode 100644 index 000000000..45288f283 --- /dev/null +++ b/app/cli/clitask/clitaskdialog.cpp @@ -0,0 +1,37 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "clitaskdialog.h" + +namespace olive { + +CLITaskDialog::CLITaskDialog(Task *task, QObject* parent) : + CLIProgressDialog(task->GetTitle(), parent), + task_(task) +{ + connect(task_, &Task::ProgressChanged, this, &CLITaskDialog::SetProgress); +} + +bool CLITaskDialog::Run() +{ + return task_->Start(); +} + +} diff --git a/app/cli/clitask/clitaskdialog.h b/app/cli/clitask/clitaskdialog.h new file mode 100644 index 000000000..5bdac7ad5 --- /dev/null +++ b/app/cli/clitask/clitaskdialog.h @@ -0,0 +1,44 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef CLITASKDIALOG_H +#define CLITASKDIALOG_H + +#include "cli/cliprogress/cliprogressdialog.h" +#include "task/task.h" + +namespace olive { + +class CLITaskDialog : public CLIProgressDialog +{ + Q_OBJECT +public: + CLITaskDialog(Task *task, QObject* parent = nullptr); + + bool Run(); + +private: + Task* task_; + +}; + +} + +#endif // CLITASKDIALOG_H diff --git a/app/codec/CMakeLists.txt b/app/codec/CMakeLists.txt new file mode 100644 index 000000000..fa6e81c25 --- /dev/null +++ b/app/codec/CMakeLists.txt @@ -0,0 +1,39 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# 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 . + +add_subdirectory(ffmpeg) +add_subdirectory(oiio) + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + codec/conformmanager.cpp + codec/conformmanager.h + codec/decoder.cpp + codec/decoder.h + codec/encoder.cpp + codec/encoder.h + codec/exportcodec.cpp + codec/exportcodec.h + codec/exportformat.cpp + codec/exportformat.h + codec/frame.cpp + codec/frame.h + codec/planarfiledevice.cpp + codec/planarfiledevice.h + codec/samplebuffer.cpp + codec/samplebuffer.h + PARENT_SCOPE +) diff --git a/app/codec/conformmanager.cpp b/app/codec/conformmanager.cpp new file mode 100644 index 000000000..70d012a94 --- /dev/null +++ b/app/codec/conformmanager.cpp @@ -0,0 +1,126 @@ +#include "conformmanager.h" + +#include + +#include "task/taskmanager.h" + +namespace olive { + +ConformManager *ConformManager::instance_ = nullptr; + +ConformManager::Conform ConformManager::GetConformState(const QString &decoder_id, const QString &cache_path, const Decoder::CodecStream &stream, const AudioParams ¶ms, bool wait) +{ + // Mutex because we'll need to check the status of a conform task + QMutexLocker locker(&mutex_); + + // Return existing conform if exists + QVector filenames = GetConformedFilename(cache_path, stream, params); + if (AllConformsExist(filenames)) { + return {kConformExists, filenames, nullptr}; + } + + ConformTask *conforming_task = nullptr; + + foreach (const ConformData &data, conforming_) { + if (data.stream == stream && data.params == params) { + // Already creating conform in a task + conforming_task = data.task; + break; + } + } + + if (!conforming_task) { + // Not conforming yet, create a task to do so + + // We conform to a different filename until it's done to make it clear even across sessions + // whether this conform is ready or not + QVector working_filenames = filenames; + for (int i=0; imoveToThread(TaskManager::instance()->thread()); + QMetaObject::invokeMethod(TaskManager::instance(), "AddTask", Qt::QueuedConnection, Q_ARG(Task *, conforming_task)); + + conforming_.append({stream, params, conforming_task, working_filenames, filenames}); + } + + if (wait) { + do { + conform_done_condition_.wait(&mutex_); + } while (!AllConformsExist(filenames)); + return {kConformExists, filenames, nullptr}; + } + + return {kConformGenerating, QVector(), conforming_task}; +} + +QVector ConformManager::GetConformedFilename(const QString &cache_path, const Decoder::CodecStream &stream, const AudioParams ¶ms) +{ + QVector filenames(params.channel_count()); + + for (int i=0; i &filenames) +{ + foreach (const QString &fn, filenames) { + if (!QFileInfo::exists(fn)) { + return false; + } + } + + return true; +} + +void ConformManager::ConformTaskFinished(Task *task, bool succeeded) +{ + QMutexLocker locker(&mutex_); + + ConformData data; + + // Remove conform data from list + for (int i=0; i +#include + +#include "decoder.h" +#include "task/conform/conform.h" + +namespace olive { + +class ConformManager : public QObject +{ + Q_OBJECT +public: + static void CreateInstance() + { + if (!instance_) { + instance_ = new ConformManager(); + } + } + + static void DestroyInstance() + { + delete instance_; + instance_ = nullptr; + } + + static ConformManager *instance() + { + return instance_; + } + + enum ConformState { + kConformExists, + kConformGenerating + }; + + struct Conform { + ConformState state; + QVector filenames; + ConformTask *task; + }; + + /** + * @brief Get conform state, and start conforming if no conform exists + * + * Thread-safe. + */ + Conform GetConformState(const QString &decoder_id, const QString &cache_path, const Decoder::CodecStream &stream, const AudioParams ¶ms, bool wait); + +signals: + void ConformReady(); + +private: + ConformManager() = default; + + static ConformManager *instance_; + + QMutex mutex_; + + QWaitCondition conform_done_condition_; + + struct ConformData { + Decoder::CodecStream stream; + AudioParams params; + ConformTask *task; + QVector working_filename; + QVector finished_filename; + }; + + QVector conforming_; + + /** + * @brief Get the destination filename of an audio stream conformed to a set of parameters + */ + static QVector GetConformedFilename(const QString &cache_path, const Decoder::CodecStream &stream, const AudioParams ¶ms); + + static bool AllConformsExist(const QVector &filenames); + +private slots: + void ConformTaskFinished(Task *task, bool succeeded); + +}; + +} + +#endif // CONFORMMANAGER_H diff --git a/app/codec/decoder.cpp b/app/codec/decoder.cpp new file mode 100644 index 000000000..7d33a0e3f --- /dev/null +++ b/app/codec/decoder.cpp @@ -0,0 +1,332 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "decoder.h" + +#include +#include + +#include "codec/ffmpeg/ffmpegdecoder.h" +#include "codec/planarfiledevice.h" +#include "codec/oiio/oiiodecoder.h" +#include "common/ffmpegutils.h" +#include "common/filefunctions.h" +#include "common/timecodefunctions.h" +#include "conformmanager.h" +#include "node/project/project.h" +#include "task/taskmanager.h" + +namespace olive { + +const rational Decoder::kAnyTimecode = RATIONAL_MIN; + +Decoder::Decoder() +{ + UpdateLastAccessed(); +} + +bool Decoder::Open(const CodecStream &stream) +{ + QMutexLocker locker(&mutex_); + + UpdateLastAccessed(); + + if (stream_.IsValid()) { + // Decoder is already open. Return TRUE if the stream is the stream we have, or FALSE if not. + if (stream_ == stream) { + return true; + } else { + qWarning() << "Tried to open a decoder that was already open with another stream"; + return false; + } + } else { + // Stream was not open, try opening it now + if (!stream.IsValid()) { + // Cannot open null stream + qCritical() << "Decoder attempted to open null stream"; + return false; + } + + if (!stream.Exists()) { + // Cannot open file that doesn't exist + qCritical() << "Decoder attempted to open file that doesn't exist"; + return false; + } + + // Set stream + stream_ = stream; + + // Try open internal + if (OpenInternal()) { + return true; + } else { + // Unset stream + qCritical() << "Failed to open" << stream_.filename() << "stream" << stream_.stream(); + CloseInternal(); + stream_.Reset(); + return false; + } + } +} + +FramePtr Decoder::RetrieveVideo(const rational &timecode, const RetrieveVideoParams ÷r, const QAtomicInt *cancelled) +{ + QMutexLocker locker(&mutex_); + + UpdateLastAccessed(); + + if (!stream_.IsValid()) { + qCritical() << "Can't retrieve video on a closed decoder"; + return nullptr; + } + + if (!SupportsVideo()) { + qCritical() << "Decoder doesn't support video"; + return nullptr; + } + + if (cancelled && *cancelled) { + return nullptr; + } + + return RetrieveVideoInternal(timecode, divider, cancelled); +} + +Decoder::RetrieveAudioData Decoder::RetrieveAudio(const TimeRange &range, const AudioParams ¶ms, const QString& cache_path, Footage::LoopMode loop_mode, RenderMode::Mode mode) +{ + QMutexLocker locker(&mutex_); + + UpdateLastAccessed(); + + if (!stream_.IsValid()) { + qCritical() << "Can't retrieve audio on a closed decoder"; + return {kInvalid, nullptr, nullptr}; + } + + if (!SupportsAudio()) { + qCritical() << "Decoder doesn't support audio"; + return {kInvalid, nullptr, nullptr}; + } + + // Get conform state from ConformManager + ConformManager::Conform conform = ConformManager::instance()->GetConformState(id(), cache_path, stream_, params, (mode == RenderMode::kOnline)); + if (conform.state == ConformManager::kConformGenerating) { + return {kWaitingForConform, nullptr, conform.task}; + } + + // See if we got the conform + SampleBufferPtr out_buffer = RetrieveAudioFromConform(conform.filenames, range, loop_mode, params); + + return {kOK, out_buffer, nullptr}; +} + +qint64 Decoder::GetLastAccessedTime() +{ + QMutexLocker locker(&mutex_); + return last_accessed_; +} + +void Decoder::Close() +{ + QMutexLocker locker(&mutex_); + + UpdateLastAccessed(); + + if (stream_.IsValid()) { + CloseInternal(); + stream_.Reset(); + } else { + qWarning() << "Tried to close a decoder that wasn't open"; + } +} + +bool Decoder::ConformAudio(const QVector &output_filenames, const AudioParams ¶ms, const QAtomicInt *cancelled) +{ + return ConformAudioInternal(output_filenames, params, cancelled); +} + +/* + * DECODER STATIC PUBLIC MEMBERS + */ + +QVector Decoder::ReceiveListOfAllDecoders() +{ + QVector decoders; + + // The order in which these decoders are added is their priority when probing. Hence FFmpeg should usually be last, + // since it supports so many formats and we presumably want to override those formats with a more specific decoder. + decoders.append(std::make_shared()); + decoders.append(std::make_shared()); + + return decoders; +} + +DecoderPtr Decoder::CreateFromID(const QString &id) +{ + if (id.isEmpty()) { + return nullptr; + } + + // Create list to iterate through + QVector decoder_list = ReceiveListOfAllDecoders(); + + foreach (DecoderPtr d, decoder_list) { + if (d->id() == id) { + return d; + } + } + + return nullptr; +} + +int64_t Decoder::GetTimeInTimebaseUnits(const rational &time, const rational &timebase, int64_t start_time) +{ + return Timecode::time_to_timestamp(time, timebase) + start_time; +} + +void Decoder::SignalProcessingProgress(int64_t ts, int64_t duration) +{ + if (duration != AV_NOPTS_VALUE && duration != 0) { + emit IndexProgress(static_cast(ts) / static_cast(duration)); + } +} + +QString Decoder::TransformImageSequenceFileName(const QString &filename, const int64_t& number) +{ + int digit_count = GetImageSequenceDigitCount(filename); + + QFileInfo file_info(filename); + + QString original_basename = file_info.completeBaseName(); + + QString new_basename = original_basename.left(original_basename.size() - digit_count) + .append(QStringLiteral("%1").arg(number, digit_count, 10, QChar('0'))); + + return file_info.dir().filePath(file_info.fileName().replace(original_basename, new_basename)); +} + +int Decoder::GetImageSequenceDigitCount(const QString &filename) +{ + QString basename = QFileInfo(filename).completeBaseName(); + + // See if basename contains a number at the end + int digit_count = 0; + + for (int i=basename.size()-1;i>=0;i--) { + if (basename.at(i).isDigit()) { + digit_count++; + } else { + break; + } + } + + return digit_count; +} + +int64_t Decoder::GetImageSequenceIndex(const QString &filename) +{ + int digit_count = GetImageSequenceDigitCount(filename); + + QFileInfo file_info(filename); + + QString original_basename = file_info.completeBaseName(); + + QString number_only = original_basename.mid(original_basename.size() - digit_count); + + return number_only.toLongLong(); +} + +FramePtr Decoder::RetrieveVideoInternal(const rational &timecode, const RetrieveVideoParams ÷r, const QAtomicInt *cancelled) +{ + Q_UNUSED(timecode) + Q_UNUSED(divider) + Q_UNUSED(cancelled) + return nullptr; +} + +bool Decoder::ConformAudioInternal(const QVector &filenames, const AudioParams ¶ms, const QAtomicInt* cancelled) +{ + Q_UNUSED(filenames) + Q_UNUSED(cancelled) + Q_UNUSED(params) + return false; +} + +SampleBufferPtr Decoder::RetrieveAudioFromConform(const QVector &conform_filenames, const TimeRange& range, Footage::LoopMode loop_mode, const AudioParams &input_params) +{ + PlanarFileDevice input; + if (input.open(conform_filenames, QFile::ReadOnly)) { + SampleBufferPtr sample_buffer = SampleBuffer::CreateAllocated(input_params, range.length()); + + qint64 read_index = input_params.time_to_bytes(range.in()) / input_params.channel_count(); + qint64 write_index = 0; + + const qint64 buffer_length_in_bytes = sample_buffer->sample_count() * input_params.bytes_per_sample_per_channel(); + + while (write_index < buffer_length_in_bytes) { + if (loop_mode == Footage::kLoopModeLoop) { + while (read_index >= input.size()) { + read_index -= input.size(); + } + + while (read_index < 0) { + read_index += input.size(); + } + } + + qint64 write_count = 0; + + if (read_index < 0) { + // Reading before 0, write silence here until audio data would actually start + write_count = qMin(-read_index, buffer_length_in_bytes); + sample_buffer->silence_bytes(write_index, write_index + write_count); + } else if (read_index >= input.size()) { + // Reading after data length, write silence until the end of the buffer + write_count = buffer_length_in_bytes - write_index; + sample_buffer->silence_bytes(write_index, write_index + write_count); + } else { + write_count = qMin(input.size() - read_index, buffer_length_in_bytes - write_index); + input.seek(read_index); + input.read(reinterpret_cast(sample_buffer->to_raw_ptrs()), write_count, write_index); + } + + read_index += write_count; + write_index += write_count; + } + + input.close(); + + return sample_buffer; + } + + return nullptr; +} + +void Decoder::UpdateLastAccessed() +{ + last_accessed_ = QDateTime::currentMSecsSinceEpoch(); +} + +uint qHash(Decoder::CodecStream stream, uint seed) +{ + return qHash(stream.filename(), seed) ^ qHash(stream.stream(), seed); +} + +} diff --git a/app/codec/decoder.h b/app/codec/decoder.h new file mode 100644 index 000000000..0e30ce455 --- /dev/null +++ b/app/codec/decoder.h @@ -0,0 +1,331 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef DECODER_H +#define DECODER_H + +extern "C" { +#include +} + +#include +#include +#include +#include +#include + +#include "codec/frame.h" +#include "codec/samplebuffer.h" +#include "common/rational.h" +#include "node/project/footage/footage.h" +#include "node/project/footage/footagedescription.h" +#include "task/task.h" + +namespace olive { + +class Decoder; +using DecoderPtr = std::shared_ptr; + +#define DECODER_DEFAULT_DESTRUCTOR(x) virtual ~x() override {CloseInternal();} + +/** + * @brief A decoder's is the main class for bringing external media into Olive + * + * Its responsibilities are to serve as + * abstraction from codecs/decoders and provide complete frames. These frames can be video or audio data and are + * provided as Frame objects in shared pointers to alleviate the responsibility of memory handling. + * + * The main function in a decoder is Retrieve() which should return complete image/audio data. A decoder should + * alleviate all the complexities of codec compression from the rest of the application (i.e. a decoder should never + * return a partial frame or require other parts of the system to interface directly with the codec). Often this will + * necessitate pre-emptively caching, indexing, or even fully transcoding media before using it which can be implemented + * through the Analyze() function. + * + * A decoder does NOT perform any pixel/sample format conversion. Frames should pass through the PixelService + * to be utilized in the rest of the rendering pipeline. + */ +class Decoder : public QObject +{ + Q_OBJECT +public: + enum RetrieveState { + kReady, + kFailedToOpen, + kIndexUnavailable + }; + + Decoder(); + + /** + * @brief Unique decoder ID + */ + virtual QString id() const = 0; + + virtual bool SupportsVideo(){return false;} + virtual bool SupportsAudio(){return false;} + + class CodecStream + { + public: + CodecStream() : + stream_(-1) + { + } + + CodecStream(const QString& filename, int stream) : + filename_(filename), + stream_(stream) + { + } + + bool IsValid() const + { + return !filename_.isEmpty() && stream_ >= 0; + } + + bool Exists() const + { + return QFileInfo::exists(filename_); + } + + void Reset() + { + *this = CodecStream(); + } + + bool operator==(const CodecStream& rhs) const + { + return filename_ == rhs.filename_ && stream_ == rhs.stream_; + } + + const QString& filename() const + { + return filename_; + } + + int stream() const + { + return stream_; + } + + private: + QString filename_; + + int stream_; + + }; + + /** + * @brief Open stream for decoding + * + * This function is thread safe. + * + * Returns TRUE if stream could be opened successfully. Also returns TRUE if the decoder is + * already open and the stream == the stream provided. Returns FALSE if the stream couldn't + * be opened OR if already open and the stream is NOT the same. + */ + bool Open(const CodecStream& stream); + + static const rational kAnyTimecode; + + struct RetrieveVideoParams + { + RetrieveVideoParams() + { + divider = 1; + src_interlacing = VideoParams::kInterlaceNone; + dst_interlacing = VideoParams::kInterlaceNone; + } + + int divider; + VideoParams::Interlacing src_interlacing; + VideoParams::Interlacing dst_interlacing; + + void reset() + { + *this = RetrieveVideoParams(); + } + + bool operator==(const RetrieveVideoParams& rhs) const + { + return divider == rhs.divider && src_interlacing == rhs.src_interlacing && dst_interlacing == rhs.dst_interlacing; + } + + bool operator!=(const RetrieveVideoParams& rhs) const + { + return !(*this == rhs); + } + }; + + /** + * @brief Retrieves a video frame from footage + * + * This function will always return a valid frame unless a fatal error occurs (in such case, + * nullptr will return). If the timecode is before the start of the footage, this function should + * return the first frame. Likewise, if it is after the timecode, this function should return the + * last frame. + * + * This function is thread safe and can only run while the decoder is open. \see Open() + */ + FramePtr RetrieveVideo(const rational& timecode, const RetrieveVideoParams& divider, const QAtomicInt *cancelled = nullptr); + + enum RetrieveAudioStatus { + kInvalid = -1, + kOK, + kWaitingForConform + }; + + struct RetrieveAudioData { + RetrieveAudioStatus status; + SampleBufferPtr samples; + Task *task; + }; + + /** + * @brief Retrieve audio data from footage + * + * This function will always return a sample buffer unless a fatal error occurs (in such case, + * nullptr will return). The SampleBuffer should always have enough audio for the range provided. + * + * This function is thread safe and can only run while the decoder is open. \see Open() + */ + RetrieveAudioData RetrieveAudio(const TimeRange& range, const AudioParams& params, const QString &cache_path, Footage::LoopMode loop_mode, RenderMode::Mode mode); + + /** + * @brief Determine the last time this decoder instance was used in any way + */ + qint64 GetLastAccessedTime(); + + /** + * @brief Generate a Footage object from a file + * + * If this decoder is able to parse this file, it will return a valid FootagePtr. Otherwise, it + * will return nullptr. + * + * For sub-classes, this function should be effectively static. We can't do virtual static + * functions in C++, but it should hold and access no state during its run. + * + * This function is re-entrant. + */ + virtual FootageDescription Probe(const QString& filename, const QAtomicInt* cancelled) const = 0; + + /** + * @brief Closes media/deallocates memory + * + * This function is thread safe and can only run while the decoder is open. \see Open() + */ + void Close(); + + /** + * @brief Conform audio stream + */ + bool ConformAudio(const QVector &output_filenames, const AudioParams ¶ms, const QAtomicInt *cancelled = nullptr); + + /** + * @brief Create a Decoder instance using a Decoder ID + * + * @return + * + * A Decoder instance or nullptr if a Decoder with this ID does not exist + */ + static DecoderPtr CreateFromID(const QString& id); + + static QString TransformImageSequenceFileName(const QString& filename, const int64_t& number); + + static int GetImageSequenceDigitCount(const QString& filename); + + static int64_t GetImageSequenceIndex(const QString& filename); + + static QVector ReceiveListOfAllDecoders(); + +protected: + /** + * @brief Internal open function + * + * Sub-classes must override this function. Function will already be mutexed, so there is no need + * to worry about thread safety. Also many other sanity checks will be done before this, so + * sub-classes only need to worry about their own opening functions. It is guaranteed that the + * decoder is not open yet and that the footage stream was from that sub-classes probe function. + * + * Return TRUE if everything opened successfully and the decoder is ready to work. Otherwise, + * return FALSE. If this function returns false, Decoder will call CloseInternal to clean any + * memory allocated during OpenInternal. + */ + virtual bool OpenInternal() = 0; + + /** + * @brief Internal close function + * + * Sub-classes must override this function. Function should be able to safely clear all allocated + * memory. It may be called even if Open() didn't complete or RetrieveVideo() was never called. + */ + virtual void CloseInternal() = 0; + + /** + * @brief Internal frame retrieval function + * + * Sub-classes must override this function IF they support video. Function is already mutexed + * so sub-classes don't need to worry about thread safety. + */ + virtual FramePtr RetrieveVideoInternal(const rational& timecode, const RetrieveVideoParams& divider, const QAtomicInt *cancelled); + + virtual bool ConformAudioInternal(const QVector& filenames, const AudioParams ¶ms, const QAtomicInt* cancelled); + + void SignalProcessingProgress(int64_t ts, int64_t duration); + + /** + * @brief Return currently open stream + * + * This function is NOT thread safe and should therefore only be called by thread safe functions. + */ + const CodecStream& stream() const + { + return stream_; + } + + static int64_t GetTimeInTimebaseUnits(const rational& time, const rational& timebase, int64_t start_time); + +signals: + /** + * @brief While indexing, this signal will provide progress as a percentage (0-100 inclusive) if + * available + */ + void IndexProgress(double); + +private: + void UpdateLastAccessed(); + + SampleBufferPtr RetrieveAudioFromConform(const QVector &conform_filenames, const TimeRange &range, Footage::LoopMode loop_mode, const AudioParams ¶ms); + + CodecStream stream_; + + QMutex mutex_; + + qint64 last_accessed_; + +}; + +uint qHash(Decoder::CodecStream stream, uint seed = 0); + +} + +Q_DECLARE_METATYPE(olive::Decoder::RetrieveState) + +#endif // DECODER_H diff --git a/app/codec/encoder.cpp b/app/codec/encoder.cpp new file mode 100644 index 000000000..eec326526 --- /dev/null +++ b/app/codec/encoder.cpp @@ -0,0 +1,353 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "encoder.h" + +#include + +#include "common/timecodefunctions.h" +#include "ffmpeg/ffmpegencoder.h" +#include "oiio/oiioencoder.h" + +namespace olive { + +const QRegularExpression Encoder::kImageSequenceContainsDigits = QRegularExpression(QStringLiteral("\\[[#]+\\]")); +const QRegularExpression Encoder::kImageSequenceRemoveDigits = QRegularExpression(QStringLiteral("[\\-\\.\\ \\_]?\\[[#]+\\]")); + +Encoder::Encoder(const EncodingParams ¶ms) : + params_(params) +{ +} + +const EncodingParams &Encoder::params() const +{ + return params_; +} + +QString Encoder::GetFilenameForFrame(const rational &frame) +{ + if (params().video_is_image_sequence()) { + // Transform! + int64_t frame_index = Timecode::time_to_timestamp(frame, params().video_params().frame_rate_as_time_base()); + int digits = GetImageSequencePlaceholderDigitCount(params().filename()); + QString frame_index_str = QStringLiteral("%1").arg(frame_index, digits, 10, QChar('0')); + + QString f = params_.filename(); + f.replace(kImageSequenceContainsDigits, frame_index_str); + return f; + } else { + // Keep filename + return params_.filename(); + } +} + +int Encoder::GetImageSequencePlaceholderDigitCount(const QString &filename) +{ + int start = filename.indexOf(kImageSequenceContainsDigits); + int digit_count = 0; + for (int i=start+1; i &EncodingParams::video_opts() const +{ + return video_opts_; +} + +const int64_t &EncodingParams::video_bit_rate() const +{ + return video_bit_rate_; +} + +const int64_t &EncodingParams::video_min_bit_rate() const +{ + return video_min_bit_rate_; +} + +const int64_t &EncodingParams::video_max_bit_rate() const +{ + return video_max_bit_rate_; +} + +const int64_t &EncodingParams::video_buffer_size() const +{ + return video_buffer_size_; +} + +const int &EncodingParams::video_threads() const +{ + return video_threads_; +} + +const QString &EncodingParams::video_pix_fmt() const +{ + return video_pix_fmt_; +} + +bool EncodingParams::audio_enabled() const +{ + return audio_enabled_; +} + +const ExportCodec::Codec &EncodingParams::audio_codec() const +{ + return audio_codec_; +} + +const AudioParams &EncodingParams::audio_params() const +{ + return audio_params_; +} + +bool EncodingParams::subtitles_enabled() const +{ + return subtitles_enabled_; +} + +ExportCodec::Codec EncodingParams::subtitles_codec() const +{ + return subtitles_codec_; +} + +const rational &EncodingParams::GetExportLength() const +{ + return export_length_; +} + +void EncodingParams::SetExportLength(const rational &export_length) +{ + export_length_ = export_length; +} + +void EncodingParams::Save(QXmlStreamWriter *writer) const +{ + writer->writeTextElement(QStringLiteral("filename"), filename_); + + writer->writeStartElement(QStringLiteral("video")); + + writer->writeAttribute(QStringLiteral("enabled"), QString::number(video_enabled_)); + + if (video_enabled_) { + writer->writeTextElement(QStringLiteral("codec"), QString::number(video_codec_)); + writer->writeTextElement(QStringLiteral("width"), QString::number(video_params_.width())); + writer->writeTextElement(QStringLiteral("height"), QString::number(video_params_.height())); + writer->writeTextElement(QStringLiteral("format"), QString::number(video_params_.format())); + writer->writeTextElement(QStringLiteral("timebase"), video_params_.time_base().toString()); + writer->writeTextElement(QStringLiteral("divider"), QString::number(video_params_.divider())); + writer->writeTextElement(QStringLiteral("bitrate"), QString::number(video_bit_rate_)); + writer->writeTextElement(QStringLiteral("minbitrate"), QString::number(video_max_bit_rate_)); + writer->writeTextElement(QStringLiteral("maxbitrate"), QString::number(video_max_bit_rate_)); + writer->writeTextElement(QStringLiteral("bufsize"), QString::number(video_buffer_size_)); + writer->writeTextElement(QStringLiteral("threads"), QString::number(video_threads_)); + + if (!video_opts_.isEmpty()) { + writer->writeStartElement(QStringLiteral("opts")); + + QHash::const_iterator i; + for (i=video_opts_.constBegin(); i!=video_opts_.constEnd(); i++) { + writer->writeStartElement(QStringLiteral("entry")); + + writer->writeTextElement(QStringLiteral("key"), i.key()); + writer->writeTextElement(QStringLiteral("value"), i.value()); + + writer->writeEndElement(); // entry + } + + writer->writeEndElement(); // opts + } + } + + writer->writeEndElement(); // video + + writer->writeStartElement(QStringLiteral("audio")); + + writer->writeAttribute(QStringLiteral("enabled"), QString::number(audio_enabled_)); + + if (audio_enabled_) { + writer->writeTextElement(QStringLiteral("codec"), QString::number(audio_codec_)); + writer->writeTextElement(QStringLiteral("samplerate"), QString::number(audio_params_.sample_rate())); + writer->writeTextElement(QStringLiteral("channellayout"), QString::number(audio_params_.channel_layout())); + writer->writeTextElement(QStringLiteral("format"), QString::number(audio_params_.format())); + } + + writer->writeEndElement(); // audio +} + +Encoder* Encoder::CreateFromID(Type id, const EncodingParams& params) +{ + switch (id) { + case kEncoderTypeNone: + break; + case kEncoderTypeFFmpeg: + return new FFmpegEncoder(params); + case kEncoderTypeOIIO: + return new OIIOEncoder(params); + } + + return nullptr; +} + +Encoder::Type Encoder::GetTypeFromFormat(ExportFormat::Format f) +{ + switch (f) { + case ExportFormat::kFormatDNxHD: + case ExportFormat::kFormatMatroska: + case ExportFormat::kFormatQuickTime: + case ExportFormat::kFormatMPEG4: + case ExportFormat::kFormatWAV: + case ExportFormat::kFormatAIFF: + case ExportFormat::kFormatMP3: + case ExportFormat::kFormatFLAC: + case ExportFormat::kFormatOgg: + case ExportFormat::kFormatWebM: + case ExportFormat::kFormatSRT: + return kEncoderTypeFFmpeg; + case ExportFormat::kFormatOpenEXR: + case ExportFormat::kFormatPNG: + case ExportFormat::kFormatTIFF: + return kEncoderTypeOIIO; + case ExportFormat::kFormatCount: + break; + } + + return kEncoderTypeNone; +} + +Encoder *Encoder::CreateFromFormat(ExportFormat::Format f, const EncodingParams ¶ms) +{ + return CreateFromID(GetTypeFromFormat(f), params); +} + +QStringList Encoder::GetPixelFormatsForCodec(ExportCodec::Codec c) const +{ + return QStringList(); +} + +} diff --git a/app/codec/encoder.h b/app/codec/encoder.h new file mode 100644 index 000000000..ace7bf7a0 --- /dev/null +++ b/app/codec/encoder.h @@ -0,0 +1,205 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef ENCODER_H +#define ENCODER_H + +#include +#include +#include +#include + +#include "codec/exportcodec.h" +#include "codec/exportformat.h" +#include "codec/frame.h" +#include "codec/samplebuffer.h" +#include "common/timerange.h" +#include "node/block/subtitle/subtitle.h" +#include "render/audioparams.h" +#include "render/subtitleparams.h" +#include "render/videoparams.h" + +namespace olive { + +class Encoder; +using EncoderPtr = std::shared_ptr; + +class EncodingParams { +public: + EncodingParams(); + + void SetFilename(const QString& filename); + + void EnableVideo(const VideoParams& video_params, const ExportCodec::Codec& vcodec); + void EnableAudio(const AudioParams& audio_params, const ExportCodec::Codec &acodec); + void EnableSubtitles(const ExportCodec::Codec &scodec); + + void set_video_option(const QString& key, const QString& value); + void set_video_bit_rate(const int64_t& rate); + void set_video_min_bit_rate(const int64_t& rate); + void set_video_max_bit_rate(const int64_t& rate); + void set_video_buffer_size(const int64_t& sz); + void set_video_threads(const int& threads); + void set_video_pix_fmt(const QString& s); + void set_video_is_image_sequence(bool s) + { + video_is_image_sequence_ = s; + } + + const QString& filename() const; + + bool video_enabled() const; + const ExportCodec::Codec& video_codec() const; + const VideoParams& video_params() const; + const QHash& video_opts() const; + const int64_t& video_bit_rate() const; + const int64_t& video_min_bit_rate() const; + const int64_t& video_max_bit_rate() const; + const int64_t& video_buffer_size() const; + const int& video_threads() const; + const QString& video_pix_fmt() const; + bool video_is_image_sequence() const + { + return video_is_image_sequence_; + } + + bool audio_enabled() const; + const ExportCodec::Codec &audio_codec() const; + const AudioParams& audio_params() const; + + const int64_t& audio_bit_rate() const + { + return audio_bit_rate_; + } + + void set_audio_bit_rate(const int64_t& b) + { + audio_bit_rate_ = b; + } + + bool subtitles_enabled() const; + ExportCodec::Codec subtitles_codec() const; + + const rational& GetExportLength() const; + void SetExportLength(const rational& GetExportLength); + + virtual void Save(QXmlStreamWriter* writer) const; + +private: + QString filename_; + + bool video_enabled_; + ExportCodec::Codec video_codec_; + VideoParams video_params_; + QHash video_opts_; + int64_t video_bit_rate_; + int64_t video_min_bit_rate_; + int64_t video_max_bit_rate_; + int64_t video_buffer_size_; + int video_threads_; + QString video_pix_fmt_; + bool video_is_image_sequence_; + + bool audio_enabled_; + ExportCodec::Codec audio_codec_; + AudioParams audio_params_; + int64_t audio_bit_rate_; + + bool subtitles_enabled_; + ExportCodec::Codec subtitles_codec_; + + rational export_length_; + +}; + +class Encoder : public QObject +{ + Q_OBJECT +public: + Encoder(const EncodingParams& params); + + enum Type { + kEncoderTypeNone = -1, + kEncoderTypeFFmpeg, + kEncoderTypeOIIO + }; + + /** + * @brief Create a Encoder instance using a Encoder ID + * + * @return + * + * A Encoder instance or nullptr if a Decoder with this ID does not exist + */ + static Encoder *CreateFromID(Type id, const EncodingParams ¶ms); + + static Type GetTypeFromFormat(ExportFormat::Format f); + + static Encoder *CreateFromFormat(ExportFormat::Format f, const EncodingParams ¶ms); + + virtual QStringList GetPixelFormatsForCodec(ExportCodec::Codec c) const; + + const EncodingParams& params() const; + + virtual VideoParams::Format GetDesiredPixelFormat() const + { + return VideoParams::kFormatInvalid; + } + + const QString& GetError() const + { + return error_; + } + + QString GetFilenameForFrame(const rational& frame); + + static int GetImageSequencePlaceholderDigitCount(const QString& filename); + + static bool FilenameContainsDigitPlaceholder(const QString &filename); + static QString FilenameRemoveDigitPlaceholder(QString filename); + + static const QRegularExpression kImageSequenceContainsDigits; + static const QRegularExpression kImageSequenceRemoveDigits; + +public slots: + virtual bool Open() = 0; + + virtual bool WriteFrame(olive::FramePtr frame, olive::rational time) = 0; + virtual bool WriteAudio(olive::SampleBufferPtr audio) = 0; + virtual bool WriteSubtitle(const SubtitleBlock *sub_block) = 0; + + virtual void Close() = 0; + +protected: + void SetError(const QString& err) + { + error_ = err; + } + +private: + EncodingParams params_; + + QString error_; + +}; + +} + +#endif // ENCODER_H diff --git a/app/codec/exportcodec.cpp b/app/codec/exportcodec.cpp new file mode 100644 index 000000000..254a9989c --- /dev/null +++ b/app/codec/exportcodec.cpp @@ -0,0 +1,106 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "exportcodec.h" + +extern "C" { +#include +#include +} + +namespace olive { + +QString ExportCodec::GetCodecName(ExportCodec::Codec c) +{ + switch (c) { + case kCodecDNxHD: + return tr("DNxHD"); + case kCodecH264: + return tr("H.264"); + case kCodecH264rgb: + return tr("H.264 RGB"); + case kCodecH265: + return tr("H.265"); + case kCodecOpenEXR: + return tr("OpenEXR"); + case kCodecPNG: + return tr("PNG"); + case kCodecProRes: + return tr("ProRes"); + case kCodecCineform: + return tr("Cineform"); + case kCodecTIFF: + return tr("TIFF"); + case kCodecMP2: + return tr("MP2"); + case kCodecMP3: + return tr("MP3"); + case kCodecAAC: + return tr("AAC"); + case kCodecPCM: + return tr("PCM (Uncompressed)"); + case kCodecFLAC: + return tr("FLAC"); + case kCodecOpus: + return tr("Opus"); + case kCodecVorbis: + return tr("Vorbis"); + case kCodecVP9: + return tr("VP9"); + case kCodecSRT: + return tr("SubRip SRT"); + case kCodecCount: + break; + } + + return tr("Unknown"); +} + +bool ExportCodec::IsCodecAStillImage(ExportCodec::Codec c) +{ + switch (c) { + case kCodecDNxHD: + case kCodecH264: + case kCodecH264rgb: + case kCodecH265: + case kCodecProRes: + case kCodecCineform: + case kCodecMP2: + case kCodecMP3: + case kCodecAAC: + case kCodecPCM: + case kCodecVorbis: + case kCodecOpus: + case kCodecFLAC: + case kCodecVP9: + case kCodecSRT: + return false; + case kCodecOpenEXR: + case kCodecPNG: + case kCodecTIFF: + return true; + case kCodecCount: + break; + } + + return false; +} + +} diff --git a/app/codec/exportcodec.h b/app/codec/exportcodec.h new file mode 100644 index 000000000..f4d17dfa0 --- /dev/null +++ b/app/codec/exportcodec.h @@ -0,0 +1,72 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef EXPORTCODEC_H +#define EXPORTCODEC_H + +#include +#include + +#include "common/define.h" +#include "render/subtitleparams.h" + +namespace olive { + +class ExportCodec : public QObject +{ + Q_OBJECT +public: + enum Codec { + // Video codecs + kCodecDNxHD, + kCodecH264, + kCodecH264rgb, + kCodecH265, + kCodecOpenEXR, + kCodecPNG, + kCodecProRes, + kCodecCineform, + kCodecTIFF, + kCodecVP9, + + // Audio codecs + kCodecMP2, + kCodecMP3, + kCodecAAC, + kCodecPCM, + kCodecOpus, + kCodecVorbis, + kCodecFLAC, + + // Subtitle codecs + kCodecSRT, + + kCodecCount + }; + + static QString GetCodecName(Codec c); + + static bool IsCodecAStillImage(Codec c); + +}; + +} + +#endif // EXPORTCODEC_H diff --git a/app/codec/exportformat.cpp b/app/codec/exportformat.cpp new file mode 100644 index 000000000..59561d50b --- /dev/null +++ b/app/codec/exportformat.cpp @@ -0,0 +1,214 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "exportformat.h" + +#include "encoder.h" + +namespace olive { + +QString ExportFormat::GetName(olive::ExportFormat::Format f) +{ + switch (f) { + case kFormatDNxHD: + return tr("DNxHD"); + case kFormatMatroska: + return tr("Matroska Video"); + case kFormatMPEG4: + return tr("MPEG-4 Video"); + case kFormatOpenEXR: + return tr("OpenEXR"); + case kFormatPNG: + return tr("PNG"); + case kFormatTIFF: + return tr("TIFF"); + case kFormatQuickTime: + return tr("QuickTime"); + case kFormatWAV: + return tr("Wave Audio"); + case kFormatAIFF: + return tr("AIFF"); + case kFormatMP3: + return tr("MP3"); + case kFormatFLAC: + return tr("FLAC"); + case kFormatOgg: + return tr("Ogg"); + case kFormatWebM: + return tr("WebM"); + case kFormatSRT: + return tr("SubRip SRT"); + + case kFormatCount: + break; + } + + return tr("Unknown"); +} + +QString ExportFormat::GetExtension(ExportFormat::Format f) +{ + switch (f) { + case kFormatDNxHD: + return QStringLiteral("mxf"); + case kFormatMatroska: + return QStringLiteral("mkv"); + case kFormatMPEG4: + return QStringLiteral("mp4"); + case kFormatOpenEXR: + return QStringLiteral("exr"); + case kFormatPNG: + return QStringLiteral("png"); + case kFormatTIFF: + return QStringLiteral("tiff"); + case kFormatQuickTime: + return QStringLiteral("mov"); + case kFormatWAV: + return QStringLiteral("wav"); + case kFormatAIFF: + return QStringLiteral("aiff"); + case kFormatMP3: + return QStringLiteral("mp3"); + case kFormatFLAC: + return QStringLiteral("flac"); + case kFormatOgg: + return QStringLiteral("ogg"); + case kFormatWebM: + return QStringLiteral("webm"); + case kFormatSRT: + return QStringLiteral("srt"); + case kFormatCount: + break; + } + + return QString(); +} + +QList ExportFormat::GetVideoCodecs(ExportFormat::Format f) +{ + switch (f) { + case kFormatDNxHD: + return {ExportCodec::kCodecDNxHD}; + case kFormatMatroska: + return {ExportCodec::kCodecH264, ExportCodec::kCodecH264rgb, ExportCodec::kCodecH265, ExportCodec::kCodecVP9}; + case kFormatMPEG4: + return {ExportCodec::kCodecH264, ExportCodec::kCodecH264rgb, ExportCodec::kCodecH265}; + case kFormatOpenEXR: + return {ExportCodec::kCodecOpenEXR}; + case kFormatPNG: + return {ExportCodec::kCodecPNG}; + case kFormatTIFF: + return {ExportCodec::kCodecTIFF}; + case kFormatQuickTime: + return {ExportCodec::kCodecH264, ExportCodec::kCodecH264rgb, ExportCodec::kCodecH265, ExportCodec::kCodecProRes, ExportCodec::kCodecCineform}; + case kFormatWebM: + return {ExportCodec::kCodecVP9}; + case kFormatOgg: + case kFormatWAV: + case kFormatAIFF: + case kFormatMP3: + case kFormatFLAC: + case kFormatSRT: + case kFormatCount: + break; + } + + return {}; +} + +QList ExportFormat::GetAudioCodecs(ExportFormat::Format f) +{ + switch (f) { + // Video/audio formats + case kFormatDNxHD: + return {ExportCodec::kCodecPCM}; + case kFormatMatroska: + return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3, ExportCodec::kCodecPCM, ExportCodec::kCodecVorbis, ExportCodec::kCodecOpus, ExportCodec::kCodecFLAC}; + case kFormatMPEG4: + return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3}; + case kFormatQuickTime: + return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3, ExportCodec::kCodecPCM}; + case kFormatWebM: + return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3, ExportCodec::kCodecPCM, ExportCodec::kCodecVorbis, ExportCodec::kCodecOpus}; + + // Audio only formats + case kFormatWAV: + return {ExportCodec::kCodecPCM}; + case kFormatAIFF: + return {ExportCodec::kCodecPCM}; + case kFormatMP3: + return {ExportCodec::kCodecMP3}; + case kFormatFLAC: + return {ExportCodec::kCodecFLAC}; + case kFormatOgg: + return {ExportCodec::kCodecOpus, ExportCodec::kCodecVorbis, ExportCodec::kCodecPCM}; + + // Video only formats + case kFormatOpenEXR: + case kFormatPNG: + case kFormatTIFF: + case kFormatSRT: + case kFormatCount: + break; + + } + + return {}; +} + +QList ExportFormat::GetSubtitleCodecs(Format f) +{ + switch (f) { + case kFormatDNxHD: + case kFormatMPEG4: + case kFormatOpenEXR: + case kFormatQuickTime: + case kFormatPNG: + case kFormatTIFF: + case kFormatWAV: + case kFormatAIFF: + case kFormatMP3: + case kFormatFLAC: + case kFormatOgg: + case kFormatWebM: + case kFormatCount: + break; + case kFormatMatroska: + case kFormatSRT: + return {ExportCodec::kCodecSRT}; + } + + return {}; +} + +QStringList ExportFormat::GetPixelFormatsForCodec(ExportFormat::Format f, ExportCodec::Codec c) +{ + Encoder* e = Encoder::CreateFromFormat(f, EncodingParams()); + QStringList list; + + if (e) { + list = e->GetPixelFormatsForCodec(c); + delete e; + } + + return list; +} + +} diff --git a/app/codec/exportformat.h b/app/codec/exportformat.h new file mode 100644 index 000000000..36be9db58 --- /dev/null +++ b/app/codec/exportformat.h @@ -0,0 +1,67 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef EXPORTFORMAT_H +#define EXPORTFORMAT_H + +#include +#include + +#include "common/define.h" +#include "exportcodec.h" + +namespace olive { + +class ExportFormat : public QObject +{ + Q_OBJECT +public: + enum Format { + kFormatDNxHD, + kFormatMatroska, + kFormatMPEG4, + kFormatOpenEXR, + kFormatQuickTime, + kFormatPNG, + kFormatTIFF, + kFormatWAV, + kFormatAIFF, + kFormatMP3, + kFormatFLAC, + kFormatOgg, + kFormatWebM, + kFormatSRT, + + kFormatCount + }; + + static QString GetName(Format f); + static QString GetExtension(Format f); + static QList GetVideoCodecs(ExportFormat::Format f); + static QList GetAudioCodecs(ExportFormat::Format f); + static QList GetSubtitleCodecs(ExportFormat::Format f); + + static QStringList GetPixelFormatsForCodec(Format f, ExportCodec::Codec c); + +}; + +} + +#endif // EXPORTFORMAT_H diff --git a/app/codec/ffmpeg/CMakeLists.txt b/app/codec/ffmpeg/CMakeLists.txt new file mode 100644 index 000000000..0f9aa9838 --- /dev/null +++ b/app/codec/ffmpeg/CMakeLists.txt @@ -0,0 +1,26 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + codec/ffmpeg/ffmpegdecoder.h + codec/ffmpeg/ffmpegdecoder.cpp + codec/ffmpeg/ffmpegencoder.h + codec/ffmpeg/ffmpegencoder.cpp + codec/ffmpeg/ffmpegframepool.h + codec/ffmpeg/ffmpegframepool.cpp + PARENT_SCOPE +) diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp new file mode 100644 index 000000000..9b2a0da1b --- /dev/null +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -0,0 +1,1129 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "ffmpegdecoder.h" + +extern "C" { +#include +#include +#include +#include +#include +#include +} + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "codec/planarfiledevice.h" +#include "common/define.h" +#include "common/ffmpegutils.h" +#include "common/filefunctions.h" +#include "common/functiontimer.h" +#include "common/timecodefunctions.h" +#include "render/framehashcache.h" +#include "render/diskmanager.h" + +namespace olive { + +FFmpegDecoder::FFmpegDecoder() : + filter_graph_(nullptr), + buffersrc_ctx_(nullptr), + buffersink_ctx_(nullptr), + pool_(QThread::idealThreadCount()*2), + is_working_(false), + cache_at_zero_(false), + cache_at_eof_(false) +{ +} + +bool FFmpegDecoder::OpenInternal() +{ + if (instance_.Open(stream().filename().toUtf8(), stream().stream())) { + AVStream* s = instance_.avstream(); + + // Store one second in the source's timebase + second_ts_ = qRound64(av_q2d(av_inv_q(s->time_base))); + + if (s->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + // Get an Olive compatible AVPixelFormat + ideal_pix_fmt_ = FFmpegUtils::GetCompatiblePixelFormat(static_cast(s->codecpar->format)); + + // Determine which Olive native pixel format we retrieved + // Note that FFmpeg doesn't support float formats + native_pix_fmt_ = GetNativePixelFormat(ideal_pix_fmt_); + native_channel_count_ = GetNativeChannelCount(ideal_pix_fmt_); + + if (native_pix_fmt_ == VideoParams::kFormatInvalid + || native_channel_count_ == 0) { + qDebug() << "Failed to find valid native pixel format for" << ideal_pix_fmt_; + return false; + } + } + + return true; + } + + return false; +} + +/*FramePtr FFmpegDecoder::RetrieveStillImage(const rational &timecode, const int ÷r) +{ + // This is a still image + QString img_filename = stream().filename(); + + int64_t ts; + + // If it's an image sequence, we'll probably need to transform the filename + if (stream().GetStream().video_type() == Track::kVideoTypeImageSequence) { + ts = stream().GetTimeInTimebaseUnits(timecode); + + img_filename = TransformImageSequenceFileName(stream().filename(), ts); + } else { + ts = 0; + } + + AVPacket* pkt = av_packet_alloc(); + AVFrame* frame = av_frame_alloc(); + FramePtr output_frame = nullptr; + + Instance i; + i.Open(img_filename.toUtf8(), stream().GetRealStreamIndex()); + + int ret = i.GetFrame(pkt, frame); + + if (ret >= 0) { + VideoParams video_params = stream().video_params(); + + // Create frame to return + output_frame = Frame::Create(); + output_frame->set_video_params(VideoParams(frame->width, + frame->height, + native_pix_fmt_, + native_channel_count_, + video_params.pixel_aspect_ratio(), + video_params.interlacing(), + divider)); + output_frame->set_timestamp(timecode); + output_frame->allocate(); + + uint8_t* copy_data = reinterpret_cast(output_frame->data()); + int copy_linesize = output_frame->linesize_bytes(); + + FFmpegBufferToNativeBuffer(frame->data, frame->linesize, ©_data, ©_linesize); + } else { + qWarning() << "Failed to retrieve still image from decoder"; + } + + i.Close(); + + av_frame_free(&frame); + av_packet_free(&pkt); + + return output_frame; +}*/ + +FramePtr FFmpegDecoder::RetrieveVideoInternal(const rational &timecode, const RetrieveVideoParams ¶ms, const QAtomicInt *cancelled) +{ + if (!InitScaler(params)) { + return nullptr; + } + + AVStream* s = instance_.avstream(); + + // Retrieve frame + FFmpegFramePool::ElementPtr return_frame = RetrieveFrame(timecode, cancelled); + + // We found the frame, we'll return a copy + if (return_frame) { + FramePtr copy = Frame::Create(); + copy->set_video_params(VideoParams(s->codecpar->width, + s->codecpar->height, + native_pix_fmt_, + native_channel_count_, + av_guess_sample_aspect_ratio(instance_.fmt_ctx(), s, nullptr), // May be incorrect, + VideoParams::kInterlaceNone, + filter_params_.divider)); + copy->set_timestamp(timecode); + copy->allocate(); + + // This data will already match the frame + memcpy(copy->data(), return_frame->data(), copy->allocated_size()); + + return copy; + } + + return nullptr; +} + +void FFmpegDecoder::CloseInternal() +{ + ClearFrameCache(); + + instance_.Close(); +} + +int FFmpegDecoder::GetFilteredFrame(AVPacket* packet, AVFrame* output_frame) +{ + // Ensure scaler is correct for these parameters + int ret; + + AVFrame* working_frame = av_frame_alloc(); + + // Try to pull frame from buffersink + while ((ret = av_buffersink_get_frame(buffersink_ctx_, output_frame)) == AVERROR(EAGAIN)) { + // If no frame is ready in the buffersink, pull from codec + ret = instance_.GetFrame(packet, working_frame); + + if (ret >= 0) { + // Override this frame's interlacing parameters from user + switch (filter_params_.src_interlacing) { + case VideoParams::kInterlaceNone: + working_frame->interlaced_frame = 0; + break; + case VideoParams::kInterlacedTopFirst: + working_frame->interlaced_frame = 1; + working_frame->top_field_first = 1; + break; + case VideoParams::kInterlacedBottomFirst: + working_frame->interlaced_frame = 1; + working_frame->top_field_first = 0; + break; + } + + // If succeeded in pulling from codec, send to buffer source + ret = av_buffersrc_add_frame_flags(buffersrc_ctx_, working_frame, AV_BUFFERSRC_FLAG_KEEP_REF); + + if (ret < 0) { + // If failed to send to buffer source, return break and error code + qDebug() << "Failed to feed filter graph:" << FFmpegError(ret); + break; + } + } else { + // If failed to read from decoder, return break and error code + break; + } + } + + av_frame_free(&working_frame); + + return ret; +} + +QString FFmpegDecoder::id() const +{ + return QStringLiteral("ffmpeg"); +} + +FootageDescription FFmpegDecoder::Probe(const QString &filename, const QAtomicInt *cancelled) const +{ + // Return value + FootageDescription desc(id()); + + // Variable for receiving errors from FFmpeg + int error_code; + + // Convert QString to a C string + QByteArray filename_c = filename.toUtf8(); + + // Open file in a format context + AVFormatContext* fmt_ctx = nullptr; + error_code = avformat_open_input(&fmt_ctx, filename_c, nullptr, nullptr); + + // Handle format context error + if (error_code == 0) { + + // Retrieve metadata about the media + avformat_find_stream_info(fmt_ctx, nullptr); + + int64_t footage_duration = fmt_ctx->duration; + + // Dump it into the Footage object + for (unsigned int i=0;inb_streams;i++) { + + // FFmpeg AVStream + AVStream* avstream = fmt_ctx->streams[i]; + + // Find decoder for this stream, if it exists we can proceed + const AVCodec* decoder = avcodec_find_decoder(avstream->codecpar->codec_id); + + if (decoder + && (avstream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO + || avstream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO + || avstream->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE)) { + + if (avstream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + + bool image_is_still = false; + rational pixel_aspect_ratio; + rational frame_rate; + VideoParams::Interlacing interlacing = VideoParams::kInterlaceNone; + + { + // Read at least two frames to get more information about this video stream + AVPacket* pkt = av_packet_alloc(); + AVFrame* frame = av_frame_alloc(); + + { + Instance instance; + instance.Open(filename_c, avstream->index); + + // Read first frame and retrieve some metadata + if (instance.GetFrame(pkt, frame) >= 0) { + // Check if video is interlaced and what field dominance it has if so + if (frame->interlaced_frame) { + if (frame->top_field_first) { + interlacing = VideoParams::kInterlacedTopFirst; + } else { + interlacing = VideoParams::kInterlacedBottomFirst; + } + } + + pixel_aspect_ratio = av_guess_sample_aspect_ratio(instance.fmt_ctx(), + instance.avstream(), + frame); + + frame_rate = av_guess_frame_rate(instance.fmt_ctx(), + instance.avstream(), + frame); + } + + // Read second frame + int ret = instance.GetFrame(pkt, frame); + + if (ret >= 0) { + // Check if we need a manual duration + if (avstream->duration == AV_NOPTS_VALUE) { + if (footage_duration == AV_NOPTS_VALUE) { + + // Manually read through file for duration + int64_t new_dur; + + do { + new_dur = frame->pts; + } while (instance.GetFrame(pkt, frame) >= 0); + + avstream->duration = new_dur; + + } else { + + // Fallback to footage duration + avstream->duration = Timecode::rescale_timestamp_ceil(footage_duration, rational(1, AV_TIME_BASE), avstream->time_base); + + } + } + } else if (ret == AVERROR_EOF) { + // Video has only one frame in it, treat it like a still image + image_is_still = true; + } + + instance.Close(); + } + + av_frame_free(&frame); + av_packet_free(&pkt); + } + + AVPixelFormat compatible_pix_fmt = FFmpegUtils::GetCompatiblePixelFormat(static_cast(avstream->codecpar->format)); + + VideoParams stream; + stream.set_stream_index(i); + stream.set_width(avstream->codecpar->width); + stream.set_height(avstream->codecpar->height); + stream.set_video_type((image_is_still) ? VideoParams::kVideoTypeStill : VideoParams::kVideoTypeVideo); + stream.set_format(GetNativePixelFormat(compatible_pix_fmt)); + stream.set_channel_count(GetNativeChannelCount(compatible_pix_fmt)); + stream.set_interlacing(interlacing); + stream.set_pixel_aspect_ratio(pixel_aspect_ratio); + stream.set_frame_rate(frame_rate); + stream.set_start_time(avstream->start_time); + stream.set_time_base(avstream->time_base); + stream.set_duration(avstream->duration); + + // Defaults to false, requires user intervention if incorrect + stream.set_premultiplied_alpha(false); + + desc.AddVideoStream(stream); + + } else if (avstream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + + // Create an audio stream object + uint64_t channel_layout = avstream->codecpar->channel_layout; + if (!channel_layout) { + channel_layout = static_cast(av_get_default_channel_layout(avstream->codecpar->channels)); + } + + if (avstream->duration == AV_NOPTS_VALUE) { + // Loop through stream until we get the whole duration + if (footage_duration == AV_NOPTS_VALUE) { + Instance instance; + instance.Open(filename_c, avstream->index); + + AVPacket* pkt = av_packet_alloc(); + AVFrame* frame = av_frame_alloc(); + + int64_t new_dur; + + do { + new_dur = frame->pts; + } while (instance.GetFrame(pkt, frame) >= 0); + + avstream->duration = new_dur; + + av_frame_free(&frame); + av_packet_free(&pkt); + + instance.Close(); + } else { + + avstream->duration = Timecode::rescale_timestamp_ceil(footage_duration, rational(1, AV_TIME_BASE), avstream->time_base); + + } + } + + AudioParams stream; + stream.set_stream_index(i); + stream.set_channel_layout(channel_layout); + stream.set_sample_rate(avstream->codecpar->sample_rate); + stream.set_format(AudioParams::kInternalFormat); + stream.set_time_base(avstream->time_base); + stream.set_duration(avstream->duration); + desc.AddAudioStream(stream); + + } else if (avstream->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) { + + qDebug() << "Subtitle probing: Stub"; + + } + + } + + } + + } + + // Free all memory + avformat_close_input(&fmt_ctx); + + return desc; +} + +QString FFmpegDecoder::FFmpegError(int error_code) +{ + char err[1024]; + av_strerror(error_code, err, 512); + return QStringLiteral("%1 %2").arg(QString::number(error_code), err); +} + +bool FFmpegDecoder::ConformAudioInternal(const QVector &filenames, const AudioParams ¶ms, const QAtomicInt *cancelled) +{ + // Iterate through each audio frame and extract the PCM data + + // Seek to starting point + instance_.Seek(0); + + // Handle NULL channel layout + uint64_t channel_layout = ValidateChannelLayout(instance_.avstream()); + if (!channel_layout) { + qCritical() << "Failed to determine channel layout of audio file, could not conform"; + return false; + } + + // Create resampling context + SwrContext* resampler = swr_alloc_set_opts(nullptr, + params.channel_layout(), + FFmpegUtils::GetFFmpegSampleFormat(params.format(), true), + params.sample_rate(), + channel_layout, + static_cast(instance_.avstream()->codecpar->format), + instance_.avstream()->codecpar->sample_rate, + 0, + nullptr); + + swr_init(resampler); + + AVPacket* pkt = av_packet_alloc(); + AVFrame* frame = av_frame_alloc(); + int ret; + + bool success = false; + + int64_t duration = instance_.avstream()->duration; + if (duration == 0 || duration == AV_NOPTS_VALUE) { + duration = instance_.fmt_ctx()->duration; + if (!(duration == 0 || duration == AV_NOPTS_VALUE)) { + // Rescale from AVFormatContext timebase to AVStream timebase + duration = av_rescale_q_rnd(duration, {1, AV_TIME_BASE}, instance_.avstream()->time_base, AV_ROUND_UP); + } + } + + PlanarFileDevice wave_out; + if (wave_out.open(filenames, QFile::WriteOnly)) { + int nb_channels = params.channel_count(); + SampleBuffer data; + data.set_audio_params(params); + + while (true) { + // Check if we have a `cancelled` ptr and its value + if (cancelled && *cancelled) { + break; + } + + ret = instance_.GetFrame(pkt, frame); + + if (ret < 0) { + + if (ret == AVERROR_EOF) { + success = true; + } else { + char err_str[512]; + av_strerror(ret, err_str, 512); + qWarning() << "Failed to conform:" << ret << err_str; + } + break; + + } + + // Allocate buffers + int nb_samples = swr_get_out_samples(resampler, frame->nb_samples); + int nb_bytes_per_channel = params.samples_to_bytes(nb_samples) / nb_channels; + data.set_sample_count(nb_bytes_per_channel); + data.allocate(); + + // Resample audio to our destination parameters + nb_samples = swr_convert(resampler, + reinterpret_cast(data.to_raw_ptrs()), + nb_samples, + const_cast(frame->data), + frame->nb_samples); + + // If no error, write to files + if (nb_samples > 0) { + // Update byte count for the number of samples we actually received + nb_bytes_per_channel = params.samples_to_bytes(nb_samples) / nb_channels; + + // Write to files + wave_out.write(const_cast(reinterpret_cast(data.to_raw_ptrs())), nb_bytes_per_channel); + } + + // Free buffer + data.destroy(); + + // Handle error now after freeing + if (nb_samples < 0) { + char err_str[512]; + av_strerror(nb_samples, err_str, 512); + qWarning() << "libswresample failed with error:" << nb_samples << err_str; + break; + } + + SignalProcessingProgress(frame->pts, duration); + } + + wave_out.close(); + } else { + qWarning() << "Failed to open WAVE output for indexing"; + } + + swr_free(&resampler); + + av_frame_free(&frame); + av_packet_free(&pkt); + + return success; +} + +VideoParams::Format FFmpegDecoder::GetNativePixelFormat(AVPixelFormat pix_fmt) +{ + switch (pix_fmt) { + case AV_PIX_FMT_RGB24: + case AV_PIX_FMT_RGBA: + return VideoParams::kFormatUnsigned8; + case AV_PIX_FMT_RGB48: + case AV_PIX_FMT_RGBA64: + return VideoParams::kFormatUnsigned16; + default: + return VideoParams::kFormatInvalid; + } +} + +int FFmpegDecoder::GetNativeChannelCount(AVPixelFormat pix_fmt) +{ + switch (pix_fmt) { + case AV_PIX_FMT_RGB24: + case AV_PIX_FMT_RGB48: + return VideoParams::kRGBChannelCount; + case AV_PIX_FMT_RGBA: + case AV_PIX_FMT_RGBA64: + return VideoParams::kRGBAChannelCount; + default: + return 0; + } +} + +uint64_t FFmpegDecoder::ValidateChannelLayout(AVStream* stream) +{ + if (stream->codecpar->channel_layout) { + return stream->codecpar->channel_layout; + } + + return av_get_default_channel_layout(stream->codecpar->channels); +} + +const char *FFmpegDecoder::GetInterlacingModeInFFmpeg(VideoParams::Interlacing interlacing) +{ + if (interlacing == VideoParams::kInterlacedTopFirst) { + return "tff"; + } else { + return "bff"; + } +} + +/* OLD UNUSED CODE: Keeping this around in case the code proves useful + +void FFmpegDecoder::CacheFrameToDisk(AVFrame *f) +{ + QFile save_frame(GetIndexFilename().append(QString::number(f->pts))); + if (save_frame.open(QFile::WriteOnly)) { + + // Save frame to media index + int cached_buffer_sz = av_image_get_buffer_size(static_cast(f->format), + f->width, + f->height, + 1); + + QByteArray cached_frame(cached_buffer_sz, Qt::Uninitialized); + + av_image_copy_to_buffer(reinterpret_cast(cached_frame.data()), + cached_frame.size(), + f->data, + f->linesize, + static_cast(f->format), + f->width, + f->height, + 1); + + save_frame.write(qCompress(cached_frame, 1)); + save_frame.close(); + + DiskManager::instance()->CreatedFile(save_frame.fileName(), QByteArray()); + } + + // See if we stored this frame in the disk cache + + QByteArray frame_loader; + if (!got_frame) { + QFile compressed_frame(GetIndexFilename().append(QString::number(target_ts))); + if (compressed_frame.exists() + && compressed_frame.size() > 0 + && compressed_frame.open(QFile::ReadOnly)) { + DiskManager::instance()->Accessed(compressed_frame.fileName()); + + // Read data + frame_loader = qUncompress(compressed_frame.readAll()); + + av_image_fill_arrays(input_data, + input_linesize, + reinterpret_cast(frame_loader.data()), + static_cast(avstream_->codecpar->format), + avstream_->codecpar->width, + avstream_->codecpar->height, + 1); + + got_frame = true; + } + } +} +*/ + +void FFmpegDecoder::ClearFrameCache() +{ + if (!cached_frames_.isEmpty()) { + cached_frames_.clear(); + cache_at_eof_ = false; + cache_at_zero_ = false; + + // Filter graph may rely on "continuous" video frames, so we free the scaler here + FreeScaler(); + InitScaler(filter_params_); + } +} + +FFmpegFramePool::ElementPtr FFmpegDecoder::RetrieveFrame(const rational& time, const QAtomicInt *cancelled) +{ + int64_t target_ts = GetTimeInTimebaseUnits(time, instance_.avstream()->time_base, instance_.avstream()->start_time); + + const int64_t min_seek = -instance_.avstream()->start_time; + int64_t seek_ts = target_ts; + bool still_seeking = false; + + if (filter_params_.src_interlacing != VideoParams::kInterlaceNone) { + // If we are de-interlacing, the timebase is doubled because we get one frame per field, so we + // double the target timestamp too + target_ts *= 2; + } + + if (time != kAnyTimecode) { + // If the frame wasn't in the frame cache, see if this frame cache is too old to use + if (cached_frames_.isEmpty() + || (target_ts < cached_frames_.first()->timestamp() || target_ts > cached_frames_.last()->timestamp() + 2*second_ts_)) { + ClearFrameCache(); + + instance_.Seek(seek_ts); + if (seek_ts == min_seek) { + cache_at_zero_ = true; + } + + still_seeking = true; + } else { + // Search cache for frame + FFmpegFramePool::ElementPtr cached_frame = GetFrameFromCache(target_ts); + if (cached_frame) { + return cached_frame; + } + } + } + + int ret; + AVPacket* pkt = av_packet_alloc(); + FFmpegFramePool::ElementPtr return_frame = nullptr; + + // Allocate a new frame + AVFrame* working_frame = av_frame_alloc(); + + while (true) { + // Break out of loop if we've cancelled + if (cancelled && *cancelled) { + break; + } + + // Pull from the decoder + av_frame_unref(working_frame); + ret = GetFilteredFrame(pkt, working_frame); + + // Handle any errors that aren't EOF (EOF is handled later on) + if (ret < 0 && ret != AVERROR_EOF) { + qCritical() << "Failed to retrieve frame:" << ret; + break; + } + + if (still_seeking) { + // Handle a failure to seek (occurs on some media) + // We'll only be here if the frame cache was emptied earlier + if (!cache_at_zero_ && (ret == AVERROR_EOF || working_frame->pts > target_ts)) { + + seek_ts = qMax(min_seek, seek_ts - second_ts_); + instance_.Seek(seek_ts); + if (seek_ts == min_seek) { + cache_at_zero_ = true; + } + continue; + + } else { + + still_seeking = false; + + } + } + + + if (ret == AVERROR_EOF) { + + // Handle an "expected" EOF by using the last frame of our cache + cache_at_eof_ = true; + + if (cached_frames_.isEmpty()) { + qCritical() << "Unexpected codec EOF - unable to retrieve frame"; + } else { + return_frame = cached_frames_.last(); + } + + break; + + } else { + + // Cut down to thread count - 1 before we acquire a new frame + if (cached_frames_.size() == QThread::idealThreadCount()) { + RemoveFirstFrame(); + } + + FFmpegFramePool::ElementPtr cached = pool_.Get(); + + if (!cached) { + qCritical() << "Frame pool failed to return a valid frame - out of memory?"; + break; + } + + // Store in queue, converting to native format + uint8_t* destination_data = cached->data(); + int destination_linesize = Frame::generate_linesize_bytes(working_frame->width, native_pix_fmt_, native_channel_count_); + + av_image_copy(&destination_data, &destination_linesize, const_cast(working_frame->data), working_frame->linesize, static_cast(working_frame->format), working_frame->width, working_frame->height); + + // Set timestamp so this frame can be identified later + cached->set_timestamp(working_frame->pts); + + // Store frame before just in case + FFmpegFramePool::ElementPtr previous; + if (cached_frames_.isEmpty()) { + previous = nullptr; + } else { + previous = cached_frames_.last(); + } + + // Append this frame and signal to other threads that a new frame has arrived + cached_frames_.append(cached); + + // If this is a valid frame, see if this or the frame before it are the one we need + if (cached->timestamp() == target_ts || time == kAnyTimecode) { + return_frame = cached; + break; + } else if (cached->timestamp() > target_ts) { + if (!previous && cache_at_zero_) { + return_frame = cached; + break; + } else { + return_frame = previous; + break; + } + } + } + } + + av_frame_free(&working_frame); + av_packet_free(&pkt); + + return return_frame; +} + +bool FFmpegDecoder::InitScaler(const RetrieveVideoParams& params) +{ + if (params == filter_params_ && filter_graph_) { + // We have an appropriate filter for these parameters, just return true + return true; + } + + // We need to (re)create the filter, delete current if necessary + ClearFrameCache(); + + // Set our params to this + filter_params_ = params; + + // Allocate filter graph + filter_graph_ = avfilter_graph_alloc(); + if (!filter_graph_) { + qWarning() << "Failed to allocate filter graph"; + return false; + } + + AVStream* s = instance_.avstream(); + + int src_width = s->codecpar->width; + int src_height = s->codecpar->height; + + // Define filter parameters + static const int kFilterArgSz = 1024; + char filter_args[kFilterArgSz]; + snprintf(filter_args, kFilterArgSz, "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", + src_width, + src_height, + s->codecpar->format, + s->time_base.num, + s->time_base.den, + s->codecpar->sample_aspect_ratio.num, + s->codecpar->sample_aspect_ratio.den); + + // Create path in and out of the filter graph (the buffer in and the buffersink out) + avfilter_graph_create_filter(&buffersrc_ctx_, avfilter_get_by_name("buffer"), "in", filter_args, nullptr, filter_graph_); + avfilter_graph_create_filter(&buffersink_ctx_, avfilter_get_by_name("buffersink"), "out", nullptr, nullptr, filter_graph_); + + // Link filters as necessary + AVFilterContext *last_filter = buffersrc_ctx_; + + // Add interlacing filter if necessary + if (filter_params_.src_interlacing != VideoParams::kInterlaceNone) { + // Footage is interlaced, our renderer works in progressive so we'll need to de-interlace + AVFilterContext* interlace_filter; + + snprintf(filter_args, kFilterArgSz, "mode=1:parity=%s", + GetInterlacingModeInFFmpeg(filter_params_.src_interlacing)); + avfilter_graph_create_filter(&interlace_filter, avfilter_get_by_name("yadif"), "yadif", filter_args, nullptr, filter_graph_); + + avfilter_link(last_filter, 0, interlace_filter, 0); + last_filter = interlace_filter; + } + + // Add scale filter if necessary + int dst_width, dst_height; + if (filter_params_.divider > 1) { + AVFilterContext* scale_filter; + + dst_width = VideoParams::GetScaledDimension(src_width, filter_params_.divider); + dst_height = VideoParams::GetScaledDimension(src_height, filter_params_.divider); + + snprintf(filter_args, kFilterArgSz, "w=%d:h=%d:flags=fast_bilinear:interl=%d", + dst_width, + dst_height, + params.dst_interlacing != VideoParams::kInterlaceNone); + + avfilter_graph_create_filter(&scale_filter, avfilter_get_by_name("scale"), "scale", filter_args, nullptr, filter_graph_); + + avfilter_link(last_filter, 0, scale_filter, 0); + last_filter = scale_filter; + } else { + dst_width = src_width; + dst_height = src_height; + } + + // Add format filter if necessary + if (ideal_pix_fmt_ != s->codecpar->format) { + AVFilterContext* format_filter; + + snprintf(filter_args, kFilterArgSz, "pix_fmts=%u", ideal_pix_fmt_); + + avfilter_graph_create_filter(&format_filter, avfilter_get_by_name("format"), "format", filter_args, nullptr, filter_graph_); + + avfilter_link(last_filter, 0, format_filter, 0); + last_filter = format_filter; + } + + // Finally, link the last filter with the buffersink + avfilter_link(last_filter, 0, buffersink_ctx_, 0); + + // Configure graph + if (int ret = avfilter_graph_config(filter_graph_, nullptr) < 0) { + qDebug() << "Failed to configure graph:" << FFmpegError(ret); + return false; + } + + // Configure frame pool + if (pool_.width() != dst_width || pool_.height() != dst_height) { + // Set new frame pool parameters + pool_.SetParameters(dst_width, dst_height, native_pix_fmt_, native_channel_count_); + } + + return true; +} + +void FFmpegDecoder::FreeScaler() +{ + if (filter_graph_) { + avfilter_graph_free(&filter_graph_); + filter_graph_ = nullptr; + buffersrc_ctx_ = nullptr; + buffersink_ctx_ = nullptr; + } +} + +FFmpegFramePool::ElementPtr FFmpegDecoder::GetFrameFromCache(const int64_t &t) const +{ + if (t < cached_frames_.first()->timestamp()) { + + if (cache_at_zero_) { + cached_frames_.first()->access(); + return cached_frames_.first(); + } + + } else if (t > cached_frames_.last()->timestamp()) { + + if (cache_at_eof_) { + cached_frames_.last()->access(); + return cached_frames_.last(); + } + + } else { + + // We already have this frame in the cache, find it + for (int i=0;itimestamp() == t // Test for an exact match + || (i < cached_frames_.size() - 1 && cached_frames_.at(i+1)->timestamp() > t)) { // Or for this frame to be the "closest" + + this_frame->access(); + return this_frame; + + } + } + } + + return nullptr; +} + +void FFmpegDecoder::RemoveFirstFrame() +{ + cached_frames_.removeFirst(); + cache_at_zero_ = false; +} + +FFmpegDecoder::Instance::Instance() : + fmt_ctx_(nullptr), + codec_ctx_(nullptr), + opts_(nullptr) +{ +} + +bool FFmpegDecoder::Instance::Open(const char *filename, int stream_index) +{ + // Open file in a format context + int error_code = avformat_open_input(&fmt_ctx_, filename, nullptr, nullptr); + + // Handle format context error + if (error_code != 0) { + qCritical() << "Failed to open input:" << filename << FFmpegError(error_code); + return false; + } + + // Get stream information from format + error_code = avformat_find_stream_info(fmt_ctx_, nullptr); + + // Handle get stream information error + if (error_code < 0) { + qCritical() << "Failed to find stream info:" << FFmpegError(error_code); + return false; + } + + // Get reference to correct AVStream + avstream_ = fmt_ctx_->streams[stream_index]; + + // Find decoder + const AVCodec* codec = avcodec_find_decoder(avstream_->codecpar->codec_id); + + // Handle failure to find decoder + if (codec == nullptr) { + qCritical() << "Failed to find appropriate decoder for this codec:" + << filename + << stream_index + << avstream_->codecpar->codec_id; + return false; + } + + // Allocate context for the decoder + codec_ctx_ = avcodec_alloc_context3(codec); + if (codec_ctx_ == nullptr) { + qCritical() << "Failed to allocate codec context"; + return false; + } + + // Copy parameters from the AVStream to the AVCodecContext + error_code = avcodec_parameters_to_context(codec_ctx_, avstream_->codecpar); + + // Handle failure to copy parameters + if (error_code < 0) { + qCritical() << "Failed to copy parameters from AVStream to AVCodecContext"; + return false; + } + + // Set multithreading setting + error_code = av_dict_set(&opts_, "threads", "auto", 0); + + // Handle failure to set multithreaded decoding + if (error_code < 0) { + qCritical() << "Failed to set codec options, performance may suffer"; + } + + // Open codec + error_code = avcodec_open2(codec_ctx_, codec, &opts_); + if (error_code < 0) { + char buf[512]; + av_strerror(error_code, buf, 512); + qCritical() << "Failed to open codec" << codec->id << error_code << buf; + return false; + } + + return true; +} + +void FFmpegDecoder::Instance::Close() +{ + if (opts_) { + av_dict_free(&opts_); + opts_ = nullptr; + } + + if (codec_ctx_) { + avcodec_free_context(&codec_ctx_); + codec_ctx_ = nullptr; + } + + if (fmt_ctx_) { + avformat_close_input(&fmt_ctx_); + fmt_ctx_ = nullptr; + } +} + +int FFmpegDecoder::Instance::GetFrame(AVPacket *pkt, AVFrame *frame) +{ + bool eof = false; + + int ret; + + // Clear any previous frames + av_frame_unref(frame); + + while ((ret = avcodec_receive_frame(codec_ctx_, frame)) == AVERROR(EAGAIN) && !eof) { + + // Find next packet in the correct stream index + do { + // Free buffer in packet if there is one + av_packet_unref(pkt); + + // Read packet from file + ret = av_read_frame(fmt_ctx_, pkt); + } while (pkt->stream_index != avstream_->index && ret >= 0); + + if (ret == AVERROR_EOF) { + // Don't break so that receive gets called again, but don't try to read again + eof = true; + + // Send a null packet to signal end of + avcodec_send_packet(codec_ctx_, nullptr); + } else if (ret < 0) { + // Handle other error by breaking loop and returning the code we received + break; + } else { + // Successful read, send the packet + ret = avcodec_send_packet(codec_ctx_, pkt); + + // We don't need the packet anymore, so free it + av_packet_unref(pkt); + + if (ret < 0) { + break; + } + } + } + + return ret; +} + +void FFmpegDecoder::Instance::Seek(int64_t timestamp) +{ + avcodec_flush_buffers(codec_ctx_); + av_seek_frame(fmt_ctx_, avstream_->index, timestamp, AVSEEK_FLAG_BACKWARD); +} + +} diff --git a/app/codec/ffmpeg/ffmpegdecoder.h b/app/codec/ffmpeg/ffmpegdecoder.h new file mode 100644 index 000000000..f3590b212 --- /dev/null +++ b/app/codec/ffmpeg/ffmpegdecoder.h @@ -0,0 +1,170 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef FFMPEGDECODER_H +#define FFMPEGDECODER_H + +// Fixes weird define issue when including +#include + +extern "C" { +#include +#include +#include +#include +} + +#include +#include +#include +#include + +#include "codec/decoder.h" +#include "ffmpegframepool.h" + +namespace olive { + +/** + * @brief A Decoder derivative that wraps FFmpeg functions as on Olive decoder + */ +class FFmpegDecoder : public Decoder +{ + Q_OBJECT +public: + // Constructor + FFmpegDecoder(); + + // Destructor + DECODER_DEFAULT_DESTRUCTOR(FFmpegDecoder) + + virtual QString id() const override; + + virtual bool SupportsVideo() override{return true;} + virtual bool SupportsAudio() override{return true;} + + virtual FootageDescription Probe(const QString &filename, const QAtomicInt *cancelled) const override; + +protected: + virtual bool OpenInternal() override; + virtual FramePtr RetrieveVideoInternal(const rational &timecode, const RetrieveVideoParams& params, const QAtomicInt *cancelled) override; + virtual bool ConformAudioInternal(const QVector& filenames, const AudioParams ¶ms, const QAtomicInt* cancelled) override; + virtual void CloseInternal() override; + +private: + class Instance + { + public: + Instance(); + + ~Instance() + { + Close(); + } + + bool Open(const char* filename, int stream_index); + + void Close(); + + /** + * @brief Uses the FFmpeg API to retrieve a packet (stored in pkt_) and decode it (stored in frame_) + * + * @return + * + * An FFmpeg error code, or >= 0 on success + */ + int GetFrame(AVPacket* pkt, AVFrame* frame); + + void Seek(int64_t timestamp); + + AVFormatContext* fmt_ctx() const + { + return fmt_ctx_; + } + + AVStream* avstream() const + { + return avstream_; + } + + private: + AVFormatContext* fmt_ctx_; + AVCodecContext* codec_ctx_; + AVStream* avstream_; + AVDictionary* opts_; + + }; + + int GetFilteredFrame(AVPacket *packet, AVFrame *frame); + + /** + * @brief Handle an FFmpeg error code + * + * Uses the FFmpeg API to retrieve a descriptive string for this error code and sends it to Error(). As such, this + * function also automatically closes the Decoder. + * + * @param error_code + */ + static QString FFmpegError(int error_code); + + bool InitScaler(const RetrieveVideoParams ¶ms); + void FreeScaler(); + + static VideoParams::Format GetNativePixelFormat(AVPixelFormat pix_fmt); + static int GetNativeChannelCount(AVPixelFormat pix_fmt); + + static uint64_t ValidateChannelLayout(AVStream *stream); + + static const char* GetInterlacingModeInFFmpeg(VideoParams::Interlacing interlacing); + + FFmpegFramePool::ElementPtr GetFrameFromCache(const int64_t& t) const; + + void ClearFrameCache(); + + FFmpegFramePool::ElementPtr RetrieveFrame(const rational &time, const QAtomicInt *cancelled); + + void RemoveFirstFrame(); + + RetrieveVideoParams filter_params_; + AVFilterGraph* filter_graph_; + AVFilterContext* buffersrc_ctx_; + AVFilterContext* buffersink_ctx_; + AVPixelFormat ideal_pix_fmt_; + VideoParams::Format native_pix_fmt_; + int native_channel_count_; + + FFmpegFramePool pool_; + + int64_t second_ts_; + + QList cached_frames_; + + bool is_working_; + QMutex is_working_mutex_; + + bool cache_at_zero_; + bool cache_at_eof_; + + Instance instance_; + +}; + +} + +#endif // FFMPEGDECODER_H diff --git a/app/codec/ffmpeg/ffmpegencoder.cpp b/app/codec/ffmpeg/ffmpegencoder.cpp new file mode 100644 index 000000000..c1ea0927a --- /dev/null +++ b/app/codec/ffmpeg/ffmpegencoder.cpp @@ -0,0 +1,883 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "ffmpegencoder.h" + +extern "C" { +#include +} + +#include + +#include "common/ffmpegutils.h" +#include "common/timecodefunctions.h" + +namespace olive { + +FFmpegEncoder::FFmpegEncoder(const EncodingParams ¶ms) : + Encoder(params), + fmt_ctx_(nullptr), + video_stream_(nullptr), + video_codec_ctx_(nullptr), + video_alpha_scale_ctx_(nullptr), + video_noalpha_scale_ctx_(nullptr), + audio_stream_(nullptr), + audio_codec_ctx_(nullptr), + audio_resample_ctx_(nullptr), + audio_frame_(nullptr), + open_(false) +{ +} + +QStringList FFmpegEncoder::GetPixelFormatsForCodec(ExportCodec::Codec c) const +{ + QStringList pix_fmts; + + const AVCodec* codec_info = GetEncoder(c); + + if (codec_info) { + for (int i=0; codec_info->pix_fmts[i]!=-1; i++) { + const char* pix_fmt_name = av_get_pix_fmt_name(codec_info->pix_fmts[i]); + pix_fmts.append(pix_fmt_name); + } + } + + return pix_fmts; +} + +bool FFmpegEncoder::Open() +{ + if (open_) { + return true; + } + + int error_code; + + // Convert QString to C string that FFmpeg expects + QByteArray filename_bytes = params().filename().toUtf8(); + const char* filename_c_str = filename_bytes.constData(); + + // Create output format context + error_code = avformat_alloc_output_context2(&fmt_ctx_, nullptr, nullptr, filename_c_str); + + // Check error code + if (error_code < 0) { + FFmpegError(tr("Failed to allocate output context"), error_code); + return false; + } + + // Initialize a video stream if it's enabled + if (params().video_enabled()) { + if (!InitializeStream(AVMEDIA_TYPE_VIDEO, &video_stream_, &video_codec_ctx_, params().video_codec())) { + return false; + } + + // This is the format we will expect frames received in Write() to be in + VideoParams::Format native_pixel_fmt = params().video_params().format(); + + // This is the format we will need to convert the frame to for swscale to understand it + video_conversion_fmt_ = FFmpegUtils::GetCompatiblePixelFormat(native_pixel_fmt); + + // This is the equivalent pixel format above as an AVPixelFormat that swscale can understand + AVPixelFormat src_alpha_pix_fmt = FFmpegUtils::GetFFmpegPixelFormat(video_conversion_fmt_, + VideoParams::kRGBAChannelCount); + + AVPixelFormat src_noalpha_pix_fmt = FFmpegUtils::GetFFmpegPixelFormat(video_conversion_fmt_, + VideoParams::kRGBChannelCount); + + if (src_alpha_pix_fmt == AV_PIX_FMT_NONE || src_noalpha_pix_fmt == AV_PIX_FMT_NONE) { + SetError(tr("Failed to find suitable pixel format for this buffer")); + return false; + } + + // This is the pixel format the encoder wants to encode to + AVPixelFormat encoder_pix_fmt = video_codec_ctx_->pix_fmt; + + // Set up a scaling context - if the native pixel format is not equal to the encoder's, we'll need to convert it + // before encoding. Even if we don't, this may be useful for converting between linesizes, etc. + video_alpha_scale_ctx_ = sws_getContext(params().video_params().width(), + params().video_params().height(), + src_alpha_pix_fmt, + params().video_params().width(), + params().video_params().height(), + encoder_pix_fmt, + 0, + nullptr, + nullptr, + nullptr); + + video_noalpha_scale_ctx_ = sws_getContext(params().video_params().width(), + params().video_params().height(), + src_noalpha_pix_fmt, + params().video_params().width(), + params().video_params().height(), + encoder_pix_fmt, + 0, + nullptr, + nullptr, + nullptr); + } + + // Initialize an audio stream if it's enabled + if (params().audio_enabled()) { + if (!InitializeStream(AVMEDIA_TYPE_AUDIO, &audio_stream_, &audio_codec_ctx_, params().audio_codec())) { + return false; + } + } + + // Initialize a subtitle stream if it's enabled + if (params().subtitles_enabled()) { + if (!InitializeStream(AVMEDIA_TYPE_SUBTITLE, &subtitle_stream_, &subtitle_codec_ctx_, params().subtitles_codec())) { + return false; + } + } + + av_dump_format(fmt_ctx_, 0, filename_c_str, 1); + + // Open output file for writing + error_code = avio_open(&fmt_ctx_->pb, filename_c_str, AVIO_FLAG_WRITE); + if (error_code < 0) { + FFmpegError(tr("Failed to open IO context"), error_code); + return false; + } + + // Write header + error_code = avformat_write_header(fmt_ctx_, nullptr); + if (error_code < 0) { + FFmpegError(tr("Failed to write format header"), error_code); + return false; + } + + open_ = true; + return true; +} + +bool FFmpegEncoder::WriteFrame(FramePtr frame, rational time) +{ + bool success = false; + + AVFrame* encoded_frame = av_frame_alloc(); + + int error_code; + const char* input_data; + int input_linesize; + + // Frame must be video + encoded_frame->width = frame->width(); + encoded_frame->height = frame->height(); + encoded_frame->format = video_codec_ctx_->pix_fmt; + + // Set interlacing + if (frame->video_params().interlacing() != VideoParams::kInterlaceNone) { + encoded_frame->interlaced_frame = 1; + + if (frame->video_params().interlacing() == VideoParams::kInterlacedTopFirst) { + encoded_frame->top_field_first = 1; + } else { + encoded_frame->top_field_first = 0; + } + } + + error_code = av_frame_get_buffer(encoded_frame, 0); + if (error_code < 0) { + FFmpegError(tr("Failed to create AVFrame buffer"), error_code); + goto fail; + } + + // We may need to convert this frame to a frame that swscale will understand + if (frame->format() != video_conversion_fmt_) { + frame = frame->convert(video_conversion_fmt_); + } + + // Use swscale context to convert formats/linesizes + input_data = frame->const_data(); + input_linesize = frame->linesize_bytes(); + + error_code = sws_scale((frame->channel_count() == VideoParams::kRGBAChannelCount) ? video_alpha_scale_ctx_ : video_noalpha_scale_ctx_, + reinterpret_cast(&input_data), + &input_linesize, + 0, + frame->height(), + encoded_frame->data, + encoded_frame->linesize); + + + if (error_code < 0) { + FFmpegError(tr("Failed to scale frame"), error_code); + goto fail; + } + + encoded_frame->pts = qRound64(time.toDouble() / av_q2d(video_codec_ctx_->time_base)); + + success = WriteAVFrame(encoded_frame, video_codec_ctx_, video_stream_); + +fail: + av_frame_free(&encoded_frame); + + return success; +} + +bool FFmpegEncoder::WriteAudio(SampleBufferPtr audio) +{ + if (!InitializeResampleContext(audio)) { + qCritical() << "Failed to initialize resample context"; + return false; + } + + bool result = true; + + // Create input buffer + int input_sample_count = 0; + uint8_t** input_data = nullptr; + if (audio) { + input_sample_count = audio->sample_count(); + int input_linesize; + + av_samples_alloc_array_and_samples(&input_data, &input_linesize, audio->audio_params().channel_count(), + input_sample_count, FFmpegUtils::GetFFmpegSampleFormat(audio->audio_params().format(), true), 0); + + for (int i=0; iaudio_params().channel_count(); i++) { + memcpy(input_data[i], audio->data(i), input_sample_count * audio->audio_params().bytes_per_sample_per_channel()); + } + } + + // Create output buffer + int output_sample_count = input_sample_count ? swr_get_out_samples(audio_resample_ctx_, input_sample_count) : 102400; + uint8_t** output_data = nullptr; + int output_linesize; + av_samples_alloc_array_and_samples(&output_data, &output_linesize, audio_stream_->codecpar->channels, + output_sample_count, static_cast(audio_stream_->codecpar->format), 0); + + // Perform conversion + int converted = swr_convert(audio_resample_ctx_, output_data, output_sample_count, const_cast(input_data), input_sample_count); + if (converted > 0) { + // Split sample buffer into frames + for (int i=0; idata, output_data, audio_frame_offset_, i, + copy_length, + audio_frame_->channels, static_cast(audio_frame_->format)); + + audio_frame_offset_ += copy_length; + i += copy_length; + + if (audio_frame_offset_ == audio_max_samples_ || (i == converted && !input_data)) { + // Got all the samples we needed, write the frame + audio_frame_->pts = av_rescale_q(audio_write_count_, {1, audio_codec_ctx_->sample_rate}, audio_codec_ctx_->time_base); + + WriteAVFrame(audio_frame_, audio_codec_ctx_, audio_stream_); + audio_write_count_ += audio_frame_offset_; + audio_frame_offset_ = 0; + } + } + } else if (converted < 0) { + FFmpegError(tr("Failed to resample audio"), converted); + result = false; + } + + if (!input_data && audio_frame_offset_ > 0) { + audio_frame_->nb_samples = audio_frame_offset_; + audio_frame_->pts = av_rescale_q(audio_write_count_, {1, audio_codec_ctx_->sample_rate}, audio_codec_ctx_->time_base); + WriteAVFrame(audio_frame_, audio_codec_ctx_, audio_stream_); + } + + // Free buffers created + if (output_data) { + av_freep(&output_data[0]); + av_freep(&output_data); + } + + if (input_data) { + av_freep(&input_data[0]); + av_freep(&input_data); + } + + return result; +} + +QString GetAssTime(const rational &time) +{ + int64_t total_centiseconds = qRound64(time.toDouble() * 100); + + int64_t cs = total_centiseconds % 100; + int64_t ss = (total_centiseconds / 100) % 60; + int64_t mm = (total_centiseconds / 6000) % 60; + int64_t hh = total_centiseconds / 360000; + + return QStringLiteral("%1:%2:%3.%4").arg( + QString::number(hh), + QStringLiteral("%1").arg(mm, 2, 10, QLatin1Char('0')), + QStringLiteral("%1").arg(ss, 2, 10, QLatin1Char('0')), + QStringLiteral("%1").arg(cs, 2, 10, QLatin1Char('0')) + ); +} + +bool FFmpegEncoder::WriteSubtitle(const SubtitleBlock *sub_block) +{ + AVSubtitle subtitle; + memset(&subtitle, 0, sizeof(subtitle)); + + AVSubtitleRect rect; + memset(&rect, 0, sizeof(rect)); + + QString ass_line = QStringLiteral("Dialogue: 0,%1,%2,Default,,0,0,0,,%3").arg( + GetAssTime(sub_block->in()), + GetAssTime(sub_block->out()), + sub_block->GetText() + ); + + QByteArray utf8_sub = sub_block->GetText().toUtf8(); + QByteArray utf8_ass = ass_line.toUtf8(); + + rect.type = SUBTITLE_ASS; + rect.text = utf8_sub.data(); + rect.ass = utf8_ass.data(); + + AVSubtitleRect *rect_array = ▭ + subtitle.num_rects = 1; + subtitle.rects = &rect_array; + + subtitle.pts = Timecode::time_to_timestamp(sub_block->in(), subtitle_codec_ctx_->time_base, Timecode::kFloor); + subtitle.end_display_time = qRound64(sub_block->length().toDouble() * 1000); + + QVector out_buf(1024 * 1024); + + int sub_sz = avcodec_encode_subtitle(subtitle_codec_ctx_, out_buf.data(), out_buf.size(), &subtitle); + if (sub_sz < 0) { + return false; + } + + AVPacket *pkt = av_packet_alloc(); + + pkt->stream_index = subtitle_stream_->index; + pkt->data = out_buf.data(); + pkt->size = sub_sz; + pkt->pts = subtitle.pts; + pkt->duration = av_rescale_q(subtitle.end_display_time, {1, 1000}, subtitle_codec_ctx_->time_base); + pkt->dts = pkt->pts; + av_packet_rescale_ts(pkt, subtitle_codec_ctx_->time_base, subtitle_stream_->time_base); + + int err = av_interleaved_write_frame(fmt_ctx_, pkt); + bool ret = true; + + if (err < 0) { + FFmpegError(tr("Failed to write interleaved packet"), err); + ret = false; + } + + av_packet_free(&pkt); + + return ret; +} + +/* +void FFmpegEncoder::WriteAudio(AudioParams pcm_info, QIODevice* file) +{ + + + // Keep track of sample count to use as each frame's timebase + int sample_counter = 0; + + while (true) { + // Calculate how many samples should input this frame + int64_t samples_needed = av_rescale_rnd(maximum_frame_samples + swr_get_delay(swr_ctx, pcm_info.sample_rate()), + audio_codec_ctx_->sample_rate, + pcm_info.sample_rate(), + AV_ROUND_UP); + + // Calculate how many bytes this is + int max_read = pcm_info.samples_to_bytes(samples_needed); + + // Read bytes from PCM + QByteArray input_data = file->read(max_read); + + // Use swresample to convert the data into the correct format + const char* input_data_array = input_data.constData(); + int converted = swr_convert(swr_ctx, + + // output data + frame->data, + + // output sample count (maximum amount of samples in output) + maximum_frame_samples, + + // input data + reinterpret_cast(&input_data_array), + + // input sample count (maximum amount of samples we read from pcm file) + pcm_info.bytes_to_samples(input_data.size())); + + // Update the frame's number of samples to the amount we actually received + frame->nb_samples = converted; + + // Update frame timestamp + frame->pts = sample_counter; + + // Increment timestamp for the next frame by the amount of samples in this one + sample_counter += converted; + + // Write the frame + if (!WriteAVFrame(frame, audio_codec_ctx_, audio_stream_)) { + qCritical() << "Failed to write audio AVFrame"; + break; + } + + // Break if we've reached the end point + if (file->atEnd()) { + break; + } + } +} +*/ + +void FFmpegEncoder::Close() +{ + if (open_) { + // Flush encoders + FlushEncoders(); + + // We've written a header, so we'll write a trailer + av_write_trailer(fmt_ctx_); + avio_closep(&fmt_ctx_->pb); + + open_ = false; + } + + if (audio_resample_ctx_) { + swr_init(audio_resample_ctx_); + audio_resample_ctx_ = nullptr; + } + + if (audio_frame_) { + av_frame_free(&audio_frame_); + audio_frame_ = nullptr; + } + + if (video_alpha_scale_ctx_) { + sws_freeContext(video_alpha_scale_ctx_); + video_alpha_scale_ctx_ = nullptr; + } + + if (video_noalpha_scale_ctx_) { + sws_freeContext(video_noalpha_scale_ctx_); + video_noalpha_scale_ctx_ = nullptr; + } + + if (video_codec_ctx_) { + avcodec_free_context(&video_codec_ctx_); + video_codec_ctx_ = nullptr; + } + + if (audio_codec_ctx_) { + avcodec_free_context(&audio_codec_ctx_); + audio_codec_ctx_ = nullptr; + } + + if (fmt_ctx_) { + // NOTE: This also frees video_stream_ and audio_stream_ + avformat_free_context(fmt_ctx_); + fmt_ctx_ = nullptr; + video_stream_ = nullptr; + audio_stream_ = nullptr; + } +} + +void FFmpegEncoder::FFmpegError(const QString& context, int error_code) +{ + char err[1024]; + av_strerror(error_code, err, 1024); + + QString formatted_err = tr("%1: %2 %3").arg(context, err, QString::number(error_code)); + qDebug() << formatted_err; + SetError(formatted_err); +} + +bool FFmpegEncoder::WriteAVFrame(AVFrame *frame, AVCodecContext* codec_ctx, AVStream* stream) +{ + // Send raw frame to the encoder + int error_code = avcodec_send_frame(codec_ctx, frame); + if (error_code < 0) { + FFmpegError(tr("Failed to send frame to encoder"), error_code); + return false; + } + + bool succeeded = false; + + AVPacket* pkt = av_packet_alloc(); + + // Retrieve packets from encoder + while (error_code >= 0) { + error_code = avcodec_receive_packet(codec_ctx, pkt); + + // EAGAIN just means the encoder wants another frame before encoding + if (error_code == AVERROR(EAGAIN)) { + break; + } else if (error_code < 0) { + FFmpegError(tr("Failed to receive packet from decoder"), error_code); + goto fail; + } + + // Set packet stream index + pkt->stream_index = stream->index; + + av_packet_rescale_ts(pkt, codec_ctx->time_base, stream->time_base); + + // Write packet to file + error_code = av_interleaved_write_frame(fmt_ctx_, pkt); + if (error_code < 0) { + FFmpegError(tr("Failed to write interleaved packet"), error_code); + goto fail; + } + + // Unref packet in case we're getting another + av_packet_unref(pkt); + } + + succeeded = true; + +fail: + av_packet_free(&pkt); + + return succeeded; +} + +bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AVCodecContext** codec_ctx_ptr, const ExportCodec::Codec& codec) +{ + if (type != AVMEDIA_TYPE_VIDEO && type != AVMEDIA_TYPE_AUDIO && type != AVMEDIA_TYPE_SUBTITLE) { + SetError(tr("Cannot initialize a stream that is not a video, audio, or subtitle type")); + return false; + } + + // Find encoder + const AVCodec* encoder = GetEncoder(codec); + if (!encoder) { + SetError(tr("Failed to find codec for 0x%1").arg(codec, 16)); + return false; + } + + if (encoder->type != type) { + SetError(tr("Retrieved unexpected codec type %1 for codec %2").arg(QString::number(encoder->type), codec)); + return false; + } + + if (!InitializeCodecContext(stream_ptr, codec_ctx_ptr, encoder)) { + return false; + } + + // Set codec parameters + AVCodecContext* codec_ctx = *codec_ctx_ptr; + AVStream* stream = *stream_ptr; + + if (type == AVMEDIA_TYPE_VIDEO) { + codec_ctx->width = params().video_params().width(); + codec_ctx->height = params().video_params().height(); + codec_ctx->sample_aspect_ratio = params().video_params().pixel_aspect_ratio().toAVRational(); + codec_ctx->time_base = params().video_params().frame_rate_as_time_base().toAVRational(); + codec_ctx->framerate = params().video_params().frame_rate().toAVRational(); + codec_ctx->pix_fmt = av_get_pix_fmt(params().video_pix_fmt().toUtf8()); + + if (params().video_params().interlacing() != VideoParams::kInterlaceNone) { + // FIXME: I actually don't know what these flags do, the documentation helpfully doesn't + // explain them at all. I hope using both of them is the right thing to do. + codec_ctx->flags |= AV_CODEC_FLAG_INTERLACED_DCT | AV_CODEC_FLAG_INTERLACED_ME; + + + if (params().video_params().interlacing() == VideoParams::kInterlacedTopFirst) { + codec_ctx->field_order = AV_FIELD_TT; + } else { + codec_ctx->field_order = AV_FIELD_BB; + + if (codec == ExportCodec::kCodecH264 || codec == ExportCodec::kCodecH264rgb) { + // For some reason, FFmpeg doesn't set libx264's bff flag so we have to do it ourselves + av_opt_set(codec_ctx->priv_data, "x264opts", "bff=1", AV_OPT_SEARCH_CHILDREN); + } + } + } + + // Set custom options + { + for (auto i=params().video_opts().begin();i!=params().video_opts().end();i++) { + av_opt_set(codec_ctx->priv_data, i.key().toUtf8(), i.value().toUtf8(), AV_OPT_SEARCH_CHILDREN); + } + + if (params().video_bit_rate() > 0) { + codec_ctx->bit_rate = params().video_bit_rate(); + } + + if (params().video_min_bit_rate() > 0) { + codec_ctx->rc_min_rate = params().video_min_bit_rate(); + } + + if (params().video_max_bit_rate() > 0) { + codec_ctx->rc_max_rate = params().video_max_bit_rate(); + } + + if (params().video_buffer_size() > 0) { + codec_ctx->rc_buffer_size = static_cast(params().video_buffer_size()); + } + } + + } else if (type == AVMEDIA_TYPE_AUDIO) { + + // Assume audio stream + codec_ctx->sample_rate = params().audio_params().sample_rate(); + codec_ctx->channel_layout = params().audio_params().channel_layout(); + codec_ctx->channels = av_get_channel_layout_nb_channels(codec_ctx->channel_layout); + codec_ctx->sample_fmt = encoder->sample_fmts[0]; + codec_ctx->time_base = {1, codec_ctx->sample_rate}; + + if (params().audio_bit_rate() > 0) { + codec_ctx->bit_rate = params().audio_bit_rate(); + } + + } else if (type == AVMEDIA_TYPE_SUBTITLE) { + + codec_ctx->time_base = av_get_time_base_q(); + + QByteArray ass_header = SubtitleParams::GenerateASSHeader().toUtf8(); + codec_ctx->subtitle_header = new uint8_t[ass_header.size()]; + memcpy(codec_ctx->subtitle_header, ass_header.constData(), ass_header.size()); + codec_ctx->subtitle_header_size = ass_header.size(); + + } + + if (!SetupCodecContext(stream, codec_ctx, encoder)) { + return false; + } + + return true; +} + +bool FFmpegEncoder::InitializeCodecContext(AVStream **stream, AVCodecContext **codec_ctx, const AVCodec* codec) +{ + *stream = avformat_new_stream(fmt_ctx_, nullptr); + if (!(*stream)) { + SetError(tr("Failed to allocate AVStream")); + return false; + } + + // Allocate a codec context + *codec_ctx = avcodec_alloc_context3(codec); + if (!(*codec_ctx)) { + SetError(tr("Failed to allocate AVCodecContext")); + return false; + } + + return true; +} + +bool FFmpegEncoder::SetupCodecContext(AVStream* stream, AVCodecContext* codec_ctx, const AVCodec* codec) +{ + int error_code; + + if (fmt_ctx_->oformat->flags & AVFMT_GLOBALHEADER) { + codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + } + + AVDictionary* codec_opts = nullptr; + + // Set thread count + if (params().video_threads() == 0) { + av_dict_set(&codec_opts, "threads", "auto", 0); + } else { + QString thread_val = QString::number(params().video_threads()); + av_dict_set(&codec_opts, "threads", thread_val.toUtf8(), 0); + } + + // Try to open encoder + error_code = avcodec_open2(codec_ctx, codec, &codec_opts); + if (error_code < 0) { + FFmpegError(tr("Failed to open encoder"), error_code); + return false; + } + + // Copy context settings to codecpar object + error_code = avcodec_parameters_from_context(stream->codecpar, codec_ctx); + if (error_code < 0) { + FFmpegError(tr("Failed to copy codec parameters to stream"), error_code); + return false; + } + + if (codec->type == AVMEDIA_TYPE_VIDEO) { + stream->avg_frame_rate = codec_ctx->framerate; + } + + return true; +} + +void FFmpegEncoder::FlushEncoders() +{ + if (video_codec_ctx_) { + FlushCodecCtx(video_codec_ctx_, video_stream_); + } + + if (audio_codec_ctx_) { + WriteAudio(nullptr); + + FlushCodecCtx(audio_codec_ctx_, audio_stream_); + } + + if (fmt_ctx_) { + if (fmt_ctx_->oformat->flags & AVFMT_ALLOW_FLUSH) { + int r = av_interleaved_write_frame(fmt_ctx_, nullptr); + if (r < 0) { + FFmpegError(tr("Failed to write interleaved packet"), r); + } + } + } +} + +void FFmpegEncoder::FlushCodecCtx(AVCodecContext *codec_ctx, AVStream* stream) +{ + avcodec_send_frame(codec_ctx, nullptr); + AVPacket* pkt = av_packet_alloc(); + + int error_code; + do { + error_code = avcodec_receive_packet(codec_ctx, pkt); + + if (error_code < 0) { + break; + } + + pkt->stream_index = stream->index; + av_packet_rescale_ts(pkt, codec_ctx->time_base, stream->time_base); + int r = av_interleaved_write_frame(fmt_ctx_, pkt); + if (r < 0) { + FFmpegError(tr("Failed to write interleaved packet"), r); + break; + } + av_packet_unref(pkt); + } while (error_code >= 0); + + av_packet_free(&pkt); +} + +bool FFmpegEncoder::InitializeResampleContext(SampleBufferPtr audio) +{ + if (audio_resample_ctx_) { + return true; + } + + // Create resample context + audio_resample_ctx_ = swr_alloc_set_opts(nullptr, + static_cast(audio_codec_ctx_->channel_layout), + audio_codec_ctx_->sample_fmt, + audio_codec_ctx_->sample_rate, + static_cast(audio->audio_params().channel_layout()), + FFmpegUtils::GetFFmpegSampleFormat(audio->audio_params().format(), true), + audio->audio_params().sample_rate(), + 0, + nullptr); + if (!audio_resample_ctx_) { + return false; + } + + int err = swr_init(audio_resample_ctx_); + if (err < 0) { + FFmpegError(tr("Failed to create resampling context"), err); + return false; + } + + audio_max_samples_ = audio_codec_ctx_->frame_size; + if (!audio_max_samples_) { + // If not, use another frame size + if (params().video_enabled()) { + // If we're encoding video, use enough samples to cover roughly one frame of video + audio_max_samples_ = params().audio_params().time_to_samples(params().video_params().frame_rate_as_time_base()); + } else { + // If no video, just use an arbitrary number + audio_max_samples_ = 256; + } + } + + audio_frame_ = av_frame_alloc(); + if (!audio_frame_) { + return false; + } + + audio_frame_->channel_layout = audio_codec_ctx_->channel_layout; + audio_frame_->format = audio_codec_ctx_->sample_fmt; + audio_frame_->nb_samples = audio_max_samples_; + + err = av_frame_get_buffer(audio_frame_, 0); + if (err < 0) { + FFmpegError(tr("Failed to create audio frame"), err); + return false; + } + + audio_frame_offset_ = 0; + audio_write_count_ = 0; + + return true; +} + +const AVCodec *FFmpegEncoder::GetEncoder(ExportCodec::Codec c) +{ + switch (c) { + case ExportCodec::kCodecH264: + return avcodec_find_encoder_by_name("libx264"); + case ExportCodec::kCodecH264rgb: + return avcodec_find_encoder_by_name("libx264rgb"); + case ExportCodec::kCodecDNxHD: + return avcodec_find_encoder(AV_CODEC_ID_DNXHD); + case ExportCodec::kCodecProRes: + return avcodec_find_encoder(AV_CODEC_ID_PRORES); + case ExportCodec::kCodecCineform: + return avcodec_find_encoder(AV_CODEC_ID_CFHD); + case ExportCodec::kCodecH265: + return avcodec_find_encoder(AV_CODEC_ID_HEVC); + case ExportCodec::kCodecVP9: + return avcodec_find_encoder(AV_CODEC_ID_VP9); + case ExportCodec::kCodecOpenEXR: + return avcodec_find_encoder(AV_CODEC_ID_EXR); + case ExportCodec::kCodecPNG: + return avcodec_find_encoder(AV_CODEC_ID_PNG); + case ExportCodec::kCodecTIFF: + return avcodec_find_encoder(AV_CODEC_ID_TIFF); + case ExportCodec::kCodecMP2: + return avcodec_find_encoder(AV_CODEC_ID_MP2); + case ExportCodec::kCodecMP3: + return avcodec_find_encoder(AV_CODEC_ID_MP3); + case ExportCodec::kCodecAAC: + return avcodec_find_encoder(AV_CODEC_ID_AAC); + case ExportCodec::kCodecPCM: + return avcodec_find_encoder(AV_CODEC_ID_PCM_S16LE); + case ExportCodec::kCodecFLAC: + return avcodec_find_encoder(AV_CODEC_ID_FLAC); + case ExportCodec::kCodecOpus: + return avcodec_find_encoder(AV_CODEC_ID_OPUS); + case ExportCodec::kCodecVorbis: + return avcodec_find_encoder(AV_CODEC_ID_VORBIS); + case ExportCodec::kCodecSRT: + return avcodec_find_encoder(AV_CODEC_ID_SUBRIP); + case ExportCodec::kCodecCount: + // These are audio or invalid codecs and therefore have no pixel formats + break; + } + + return nullptr; +} + +} diff --git a/app/codec/ffmpeg/ffmpegencoder.h b/app/codec/ffmpeg/ffmpegencoder.h new file mode 100644 index 000000000..d75d3f4ea --- /dev/null +++ b/app/codec/ffmpeg/ffmpegencoder.h @@ -0,0 +1,108 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef FFMPEGENCODER_H +#define FFMPEGENCODER_H + +extern "C" { +#include +#include +#include +#include +#include +} + +#include "codec/encoder.h" + +namespace olive { + +class FFmpegEncoder : public Encoder +{ + Q_OBJECT +public: + FFmpegEncoder(const EncodingParams ¶ms); + + virtual QStringList GetPixelFormatsForCodec(ExportCodec::Codec c) const override; + + virtual bool Open() override; + + virtual bool WriteFrame(olive::FramePtr frame, olive::rational time) override; + + virtual bool WriteAudio(olive::SampleBufferPtr audio) override; + + virtual bool WriteSubtitle(const SubtitleBlock *sub_block) override; + + virtual void Close() override; + + virtual VideoParams::Format GetDesiredPixelFormat() const override + { + return video_conversion_fmt_; + } + +private: + /** + * @brief Handle an FFmpeg error code + * + * Uses the FFmpeg API to retrieve a descriptive string for this error code and sends it to Error(). As such, this + * function also automatically closes the Decoder. + * + * @param error_code + */ + void FFmpegError(const QString &context, int error_code); + + bool WriteAVFrame(AVFrame* frame, AVCodecContext *codec_ctx, AVStream *stream); + + bool InitializeStream(enum AVMediaType type, AVStream** stream, AVCodecContext** codec_ctx, const ExportCodec::Codec &codec); + bool InitializeCodecContext(AVStream** stream, AVCodecContext** codec_ctx, const AVCodec* codec); + bool SetupCodecContext(AVStream *stream, AVCodecContext *codec_ctx, const AVCodec *codec); + + void FlushEncoders(); + void FlushCodecCtx(AVCodecContext* codec_ctx, AVStream *stream); + + bool InitializeResampleContext(SampleBufferPtr audio); + + static const AVCodec *GetEncoder(ExportCodec::Codec c); + + AVFormatContext* fmt_ctx_; + + AVStream* video_stream_; + AVCodecContext* video_codec_ctx_; + SwsContext* video_alpha_scale_ctx_; + SwsContext* video_noalpha_scale_ctx_; + VideoParams::Format video_conversion_fmt_; + + AVStream* audio_stream_; + AVCodecContext* audio_codec_ctx_; + SwrContext* audio_resample_ctx_; + AVFrame* audio_frame_; + int audio_max_samples_; + int audio_frame_offset_; + int audio_write_count_; + + AVStream* subtitle_stream_; + AVCodecContext* subtitle_codec_ctx_; + + bool open_; + +}; + +} + +#endif // FFMPEGENCODER_H diff --git a/app/node/dependency.cpp b/app/codec/ffmpeg/ffmpegframepool.cpp similarity index 53% rename from app/node/dependency.cpp rename to app/codec/ffmpeg/ffmpegframepool.cpp index a8e98fa31..e8df84d67 100644 --- a/app/node/dependency.cpp +++ b/app/codec/ffmpeg/ffmpegframepool.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -18,43 +18,34 @@ ***/ -#include "dependency.h" +#include "ffmpegframepool.h" -#include "node.h" +#include "codec/frame.h" -NodeDependency::NodeDependency() : - node_(nullptr) +namespace olive { + +FFmpegFramePool::FFmpegFramePool(int element_count) : + MemoryPool(element_count), + width_(0), + height_(0), + format_(VideoParams::kFormatInvalid), + channel_count_(0) { } -NodeDependency::NodeDependency(Node *node, const TimeRange &range) : - node_(node), - range_(range) +void FFmpegFramePool::SetParameters(int width, int height, VideoParams::Format format, int channel_count) { + Clear(); + + width_ = width; + height_ = height; + format_ = format; + channel_count_ = channel_count; } -NodeDependency::NodeDependency(Node *node, const rational &in, const rational &out) : - node_(node), - range_(in, out) +size_t FFmpegFramePool::GetElementSize() { + return Frame::generate_linesize_bytes(width_, format_, channel_count_) * height_; } -Node *NodeDependency::node() const -{ - return node_; -} - -const rational& NodeDependency::in() const -{ - return range_.in(); -} - -const rational &NodeDependency::out() const -{ - return range_.out(); -} - -const TimeRange &NodeDependency::range() const -{ - return range_; } diff --git a/app/codec/ffmpeg/ffmpegframepool.h b/app/codec/ffmpeg/ffmpegframepool.h new file mode 100644 index 000000000..c3f9d050a --- /dev/null +++ b/app/codec/ffmpeg/ffmpegframepool.h @@ -0,0 +1,63 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef FFMPEGFRAMEPOOL_H +#define FFMPEGFRAMEPOOL_H + +#include "common/memorypool.h" +#include "render/videoparams.h" + +namespace olive { + +class FFmpegFramePool : public MemoryPool +{ + Q_OBJECT +public: + FFmpegFramePool(int element_count); + + void SetParameters(int width, int height, VideoParams::Format format, int channel_count); + + const int& width() const + { + return width_; + } + + const int& height() const + { + return height_; + } + +protected: + virtual size_t GetElementSize() override; + +private: + int width_; + + int height_; + + VideoParams::Format format_; + + int channel_count_; + +}; + +} + +#endif // FFMPEGFRAMEPOOL_H diff --git a/app/codec/footagemeta.h b/app/codec/footagemeta.h new file mode 100644 index 000000000..4d23025f1 --- /dev/null +++ b/app/codec/footagemeta.h @@ -0,0 +1,10 @@ +#ifndef FOOTAGEMETA_H +#define FOOTAGEMETA_H + +struct FootageData { + struct StreamData { + + }; +}; + +#endif // FOOTAGEMETA_H diff --git a/app/codec/frame.cpp b/app/codec/frame.cpp new file mode 100644 index 000000000..8701ce2ba --- /dev/null +++ b/app/codec/frame.cpp @@ -0,0 +1,180 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "frame.h" + +#include +#include +#include +#include + +#include "common/oiioutils.h" +#include "render/framemanager.h" + +namespace olive { + +Frame::Frame() : + data_(nullptr), + data_size_(0), + timestamp_(0) +{ +} + +Frame::~Frame() +{ + destroy(); +} + +FramePtr Frame::Create() +{ + return std::make_shared(); +} + +const VideoParams &Frame::video_params() const +{ + return params_; +} + +void Frame::set_video_params(const VideoParams ¶ms) +{ + params_ = params; + + linesize_ = generate_linesize_bytes(width(), params_.format(), params_.channel_count()); + linesize_pixels_ = linesize_ / params_.GetBytesPerPixel(); +} + +FramePtr Frame::Interlace(FramePtr top, FramePtr bottom) +{ + if (top->video_params() != bottom->video_params()) { + qCritical() << "Tried to interlace two frames that had incompatible parameters"; + return nullptr; + } + + FramePtr interlaced = Frame::Create(); + interlaced->set_video_params(top->video_params()); + interlaced->allocate(); + + int linesize = interlaced->linesize_bytes(); + + for (int i=0; iheight(); i++) { + FramePtr which = (i%2 == 0) ? top : bottom; + + memcpy(interlaced->data() + i*linesize, + which->const_data() + i*linesize, + linesize); + } + + return interlaced; +} + +int Frame::generate_linesize_bytes(int width, VideoParams::Format format, int channel_count) +{ + // Align to 32 bytes (not sure if this is necessary?) + return VideoParams::GetBytesPerPixel(format, channel_count) * ((width + 31) & ~31); +} + +Color Frame::get_pixel(int x, int y) const +{ + if (!contains_pixel(x, y)) { + return Color(); + } + + int byte_offset = y * linesize_bytes() + x * video_params().GetBytesPerPixel(); + + return Color(reinterpret_cast(data_ + byte_offset), video_params().format(), video_params().channel_count()); +} + +bool Frame::contains_pixel(int x, int y) const +{ + return (is_allocated() && x >= 0 && x < width() && y >= 0 && y < height()); +} + +void Frame::set_pixel(int x, int y, const Color &c) +{ + if (!contains_pixel(x, y)) { + return; + } + + int byte_offset = y * linesize_bytes() + x * video_params().GetBytesPerPixel(); + + c.toData(reinterpret_cast(data_ + byte_offset), video_params().format(), video_params().channel_count()); +} + +bool Frame::allocate() +{ + // Assume this frame is intended to be a video frame + if (!params_.is_valid()) { + qWarning() << "Tried to allocate a frame with invalid parameters"; + return false; + } + + if (is_allocated()) { + // Already allocated + return true; + } + + data_size_ = linesize_ * height(); + data_ = FrameManager::Allocate(data_size_); + + return true; +} + +void Frame::destroy() +{ + if (is_allocated()) { + FrameManager::Deallocate(data_size_, data_); + + data_size_ = 0; + data_ = nullptr; + } +} + +FramePtr Frame::convert(VideoParams::Format format) const +{ + // Create new params with destination format + VideoParams params = params_; + params.set_format(format); + + // Create new frame + FramePtr converted = Frame::Create(); + converted->set_video_params(params); + converted->set_timestamp(timestamp_); + converted->allocate(); + + // Do the conversion through OIIO for convenience + OIIO::ImageBuf src(OIIO::ImageSpec(width(), height(), + channel_count(), + OIIOUtils::GetOIIOBaseTypeFromFormat(this->format()))); + + OIIOUtils::FrameToBuffer(this, &src); + + OIIO::ImageBuf dst(OIIO::ImageSpec(converted->width(), converted->height(), + channel_count(), + OIIOUtils::GetOIIOBaseTypeFromFormat(format))); + + if (dst.copy_pixels(src)) { + OIIOUtils::BufferToFrame(&dst, converted.get()); + return converted; + } else { + return nullptr; + } +} + +} diff --git a/app/decoder/frame.h b/app/codec/frame.h similarity index 53% rename from app/decoder/frame.h rename to app/codec/frame.h index 9876d928f..5c1c4343a 100644 --- a/app/decoder/frame.h +++ b/app/codec/frame.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -24,83 +24,101 @@ #include #include +#include "common/define.h" #include "common/rational.h" -#include "render/audioparams.h" -#include "render/pixelformat.h" +#include "render/color.h" +#include "render/videoparams.h" + +namespace olive { class Frame; using FramePtr = std::shared_ptr; /** * @brief Video frame data or audio sample data from a Decoder - * - * Abstraction from AVFrame. Currently a simple AVFrame wrapper. - * - * This class does not support copying at this time. */ class Frame { public: - /// Normal constructor Frame(); + ~Frame(); + + DISABLE_COPY_MOVE(Frame) + static FramePtr Create(); - /** - * @brief Get frame's width in pixels - */ - const int& width(); - void set_width(const int& width); + const VideoParams& video_params() const; + void set_video_params(const VideoParams& params); - /** - * @brief Get frame's height in pixels - */ - const int& height(); - void set_height(const int& height); + static FramePtr Interlace(FramePtr top, FramePtr bottom); - const AudioRenderingParams& audio_params(); - void set_audio_params(const AudioRenderingParams& params); + static int generate_linesize_bytes(int width, VideoParams::Format format, int channel_count); - const int &sample_count(); - void set_sample_count(const int &sample_count); + int linesize_pixels() const + { + return linesize_pixels_; + } + + int linesize_bytes() const + { + return linesize_; + } + + int width() const + { + return params_.effective_width(); + } + + int height() const + { + return params_.effective_height(); + } + + VideoParams::Format format() const + { + return params_.format(); + } + + int channel_count() const + { + return params_.channel_count(); + } + + Color get_pixel(int x, int y) const; + bool contains_pixel(int x, int y) const; + void set_pixel(int x, int y, const Color& c); /** * @brief Get frame's timestamp. * * This timestamp is always a rational that will equate to the time in seconds. */ - const rational& timestamp(); - void set_timestamp(const rational& timestamp); + const rational& timestamp() const + { + return timestamp_; + } - /*const int64_t& native_timestamp(); - void set_native_timestamp(const int64_t& timestamp);*/ - - /** - * @brief Get frame's format - * - * @return - * - * Currently this will either be an olive::PixelFormat (video) or an olive::SampleFormat (audio). - */ - const olive::PixelFormat& format(); - void set_format(const olive::PixelFormat& format); - - /** - * @brief Returns a copy of the data in this frame as a QByteArray - * - * Will always do a deep copy. If you want to affect the data directly, use data() instead. - */ - QByteArray ToByteArray(); + void set_timestamp(const rational& timestamp) + { + timestamp_ = timestamp; + } /** * @brief Get the data buffer of this frame */ - char* data(); + char* data() + { + return data_; + } /** * @brief Get the const data buffer of this frame */ - const char* const_data(); + const char* const_data() const + { + return data_; + } /** * @brief Allocate memory buffer to store data based on parameters @@ -109,7 +127,15 @@ public: * * If a memory buffer has been previously allocated without destroying, this function will destroy it. */ - void allocate(); + bool allocate(); + + /** + * @brief Return whether the frame is allocated or not + */ + bool is_allocated() const + { + return data_; + } /** * @brief Destroy a memory buffer allocated with allocate() @@ -121,25 +147,29 @@ public: * * Returns 0 if nothing is allocated. */ - int allocated_size() const; + int allocated_size() const + { + return data_size_; + } + + FramePtr convert(VideoParams::Format format) const; private: - int width_; + VideoParams params_; - int height_; - - olive::PixelFormat format_; - - AudioRenderingParams audio_params_; - - int sample_count_; - - QByteArray data_; + char* data_; + int data_size_; rational timestamp_; - int64_t native_timestamp_; + int linesize_; + + int linesize_pixels_; }; +} + +Q_DECLARE_METATYPE(olive::FramePtr) + #endif // FRAME_H diff --git a/app/codec/oiio/CMakeLists.txt b/app/codec/oiio/CMakeLists.txt new file mode 100644 index 000000000..c4d407fe6 --- /dev/null +++ b/app/codec/oiio/CMakeLists.txt @@ -0,0 +1,24 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + codec/oiio/oiiodecoder.cpp + codec/oiio/oiiodecoder.h + codec/oiio/oiioencoder.cpp + codec/oiio/oiioencoder.h + PARENT_SCOPE +) diff --git a/app/codec/oiio/oiiodecoder.cpp b/app/codec/oiio/oiiodecoder.cpp new file mode 100644 index 000000000..dea90b2e6 --- /dev/null +++ b/app/codec/oiio/oiiodecoder.cpp @@ -0,0 +1,220 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "oiiodecoder.h" + +#include +#include +#include +#include +#include + +#include "common/define.h" +#include "common/oiioutils.h" +#include "config/config.h" +#include "core.h" + +namespace olive { + +QStringList OIIODecoder::supported_formats_; + +OIIODecoder::OIIODecoder() : + image_(nullptr), + buffer_(nullptr) +{ +} + +QString OIIODecoder::id() const +{ + return QStringLiteral("oiio"); +} + +FootageDescription OIIODecoder::Probe(const QString &filename, const QAtomicInt* cancelled) const +{ + Q_UNUSED(cancelled) + + FootageDescription desc(id()); + + // Filter out any file extensions that aren't expected to work - sometimes OIIO will crash trying + // to open a file that it can't if it's given one + if (!FileTypeIsSupported(filename)) { + return desc; + } + + std::string std_filename = filename.toStdString(); + + auto in = OIIO::ImageInput::open(std_filename); + + if (!in) { + return desc; + } + + // Filter out OIIO detecting an "FFmpeg movie", we have a native FFmpeg decoder that can handle + // it better + if (!strcmp(in->format_name(), "FFmpeg movie")) { + return desc; + } + + VideoParams video_params; + + video_params.set_stream_index(0); + video_params.set_width(in->spec().width); + video_params.set_height(in->spec().height); + video_params.set_format(OIIOUtils::GetFormatFromOIIOBasetype(static_cast(in->spec().format.basetype))); + video_params.set_channel_count(in->spec().nchannels); + video_params.set_pixel_aspect_ratio(OIIOUtils::GetPixelAspectRatioFromOIIO(in->spec())); + video_params.set_video_type(VideoParams::kVideoTypeStill); + + // OIIO automatically premultiplies alpha + // FIXME: We usually disassociate the alpha for the color management later, for 8-bit images this + // likely reduces the fidelity? + video_params.set_premultiplied_alpha(true); + + desc.AddVideoStream(video_params); + + // If we're here, we have a successful image open + in->close(); + + return desc; +} + +bool OIIODecoder::OpenInternal() +{ + // If we can open the filename provided, assume everything is working + return OpenImageHandler(stream().filename()); +} + +FramePtr OIIODecoder::RetrieveVideoInternal(const rational &timecode, const RetrieveVideoParams ÷r, const QAtomicInt *cancelled) +{ + Q_UNUSED(timecode) + Q_UNUSED(cancelled) + + FramePtr frame = Frame::Create(); + + frame->set_video_params(VideoParams(buffer_->spec().width, + buffer_->spec().height, + pix_fmt_, + channel_count_, + OIIOUtils::GetPixelAspectRatioFromOIIO(buffer_->spec()), + VideoParams::kInterlaceNone, // FIXME: Does OIIO deinterlace for us? + divider.divider)); + frame->allocate(); + + if (divider.divider == 1) { + + OIIOUtils::BufferToFrame(buffer_, frame.get()); + + } else { + + // Will need to resize the image + OIIO::ImageBuf dst(OIIO::ImageSpec(frame->width(), frame->height(), buffer_->spec().nchannels, buffer_->spec().format)); + + if (!OIIO::ImageBufAlgo::resample(dst, *buffer_)) { + qWarning() << "OIIO resize failed"; + } + + OIIOUtils::BufferToFrame(&dst, frame.get()); + + } + + return frame; +} + +void OIIODecoder::CloseInternal() +{ + CloseImageHandle(); +} + +bool OIIODecoder::FileTypeIsSupported(const QString& fn) +{ + // We prioritize OIIO over FFmpeg to pick up still images more effectively, but some OIIO decoders (notably OpenJPEG) + // will segfault entirely if given unexpected data (an MPEG-4 for instance). To workaround this issue, we use OIIO's + // "extension_list" attribute and match it with the extension of the file. + + // Check if we've created the supported formats list, create it if not + if (supported_formats_.isEmpty()) { + QStringList extension_list = QString::fromStdString(OIIO::get_string_attribute("extension_list")).split(';'); + + // The format of "extension_list" is "format:ext", we want to separate it into a simple list of extensions + foreach (const QString& ext, extension_list) { + QStringList format_and_ext = ext.split(':'); + + supported_formats_.append(format_and_ext.at(1).split(',')); + } + } + + if (!supported_formats_.contains(QFileInfo(fn).suffix(), Qt::CaseInsensitive)) { + return false; + } + + return true; +} + +bool OIIODecoder::OpenImageHandler(const QString &fn) +{ + image_ = OIIO::ImageInput::open(fn.toStdString()); + + if (!image_) { + return false; + } + + // Check if we can work with this pixel format + const OIIO::ImageSpec& spec = image_->spec(); + + // Store channel count + channel_count_ = spec.nchannels; + + // We use RGBA frames because that tends to be the native format of GPUs + pix_fmt_ = OIIOUtils::GetFormatFromOIIOBasetype(static_cast(spec.format.basetype)); + + if (pix_fmt_ == VideoParams::kFormatInvalid) { + qWarning() << "Failed to convert OIIO::ImageDesc to native pixel format"; + return false; + } + + OIIO::TypeDesc::BASETYPE type = OIIOUtils::GetOIIOBaseTypeFromFormat(pix_fmt_); + + if (type == OIIO::TypeDesc::UNKNOWN) { + qCritical() << "Failed to determine appropriate OIIO basetype from native format"; + return false; + } + + buffer_ = new OIIO::ImageBuf(OIIO::ImageSpec(spec.width, spec.height, spec.nchannels, type), + OIIO::InitializePixels::No); + + image_->read_image(type, buffer_->localpixels()); + + return true; +} + +void OIIODecoder::CloseImageHandle() +{ + if (image_) { + image_->close(); + image_ = nullptr; + } + + if (buffer_) { + delete buffer_; + buffer_ = nullptr; + } +} + +} diff --git a/app/decoder/oiio/oiiodecoder.h b/app/codec/oiio/oiiodecoder.h similarity index 52% rename from app/decoder/oiio/oiiodecoder.h rename to app/codec/oiio/oiiodecoder.h index 68dc3cb59..4232cb2d1 100644 --- a/app/decoder/oiio/oiiodecoder.h +++ b/app/codec/oiio/oiiodecoder.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -22,44 +22,50 @@ #define OIIODECODER_H #include +#include -#include "decoder/decoder.h" -#include "render/pixelservice.h" +#include "codec/decoder.h" + +namespace olive { class OIIODecoder : public Decoder { + Q_OBJECT public: OIIODecoder(); - virtual QString id() override; + DECODER_DEFAULT_DESTRUCTOR(OIIODecoder) - virtual bool Probe(Footage *f) override; + virtual QString id() const override; - virtual bool Open() override; + virtual bool SupportsVideo() override{return true;} - virtual FramePtr RetrieveVideo(const rational &timecode) override; + virtual FootageDescription Probe(const QString& filename, const QAtomicInt* cancelled) const override; - virtual void Close() override; - - virtual int64_t GetTimestampFromTime(const rational &time) override; - - virtual bool SupportsVideo() override; +protected: + virtual bool OpenInternal() override; + virtual FramePtr RetrieveVideoInternal(const rational &timecode, const RetrieveVideoParams& divider, const QAtomicInt *cancelled) override; + virtual void CloseInternal() override; private: std::unique_ptr image_; - int width_; + static bool FileTypeIsSupported(const QString& fn); - int height_; + bool OpenImageHandler(const QString& fn); - olive::PixelFormat pix_fmt_; + void CloseImageHandle(); - PixelFormatInfo pix_fmt_info_; + VideoParams::Format pix_fmt_; - bool is_rgba_; + int channel_count_; - FramePtr frame_; + OIIO::ImageBuf* buffer_; + + static QStringList supported_formats_; }; +} + #endif // OIIODECODER_H diff --git a/app/codec/oiio/oiioencoder.cpp b/app/codec/oiio/oiioencoder.cpp new file mode 100644 index 000000000..f872a758b --- /dev/null +++ b/app/codec/oiio/oiioencoder.cpp @@ -0,0 +1,81 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "oiioencoder.h" + +#include "common/oiioutils.h" + +namespace olive { + +OIIOEncoder::OIIOEncoder(const EncodingParams ¶ms) : + Encoder(params) +{ + +} + +bool OIIOEncoder::Open() +{ + return true; +} + +bool OIIOEncoder::WriteFrame(FramePtr frame, rational time) +{ + std::string filename = GetFilenameForFrame(time).toStdString(); + + auto output = OIIO::ImageOutput::create(filename); + if (!output) { + return false; + } + + OIIO::TypeDesc type = OIIOUtils::GetOIIOBaseTypeFromFormat(frame->format()); + OIIO::ImageSpec spec(frame->width(), frame->height(), frame->channel_count(), type); + + if (!output->open(filename, spec)) { + return false; + } + + if (!output->write_image(type, frame->data(), OIIO::AutoStride, frame->linesize_bytes())) { + return false; + } + + if (!output->close()) { + return false; + } + + return true; +} + +bool OIIOEncoder::WriteAudio(SampleBufferPtr audio) +{ + // Do nothing + return false; +} + +bool OIIOEncoder::WriteSubtitle(const SubtitleBlock *sub_block) +{ + return false; +} + +void OIIOEncoder::Close() +{ + // Do nothing +} + +} diff --git a/app/codec/oiio/oiioencoder.h b/app/codec/oiio/oiioencoder.h new file mode 100644 index 000000000..7325cdf49 --- /dev/null +++ b/app/codec/oiio/oiioencoder.h @@ -0,0 +1,47 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef OIIOENCODER_H +#define OIIOENCODER_H + +#include "codec/encoder.h" + +namespace olive { + +class OIIOEncoder : public Encoder +{ + Q_OBJECT +public: + OIIOEncoder(const EncodingParams ¶ms); + +public slots: + virtual bool Open() override; + + virtual bool WriteFrame(olive::FramePtr frame, olive::rational time) override; + virtual bool WriteAudio(SampleBufferPtr audio) override; + virtual bool WriteSubtitle(const SubtitleBlock *sub_block) override; + + virtual void Close() override; + +}; + +} + +#endif // OIIOENCODER_H diff --git a/app/codec/planarfiledevice.cpp b/app/codec/planarfiledevice.cpp new file mode 100644 index 000000000..c68d3ba8c --- /dev/null +++ b/app/codec/planarfiledevice.cpp @@ -0,0 +1,119 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "planarfiledevice.h" + +namespace olive { + +PlanarFileDevice::PlanarFileDevice(QObject *parent) : + QObject(parent) +{ + +} + +PlanarFileDevice::~PlanarFileDevice() +{ + close(); +} + +bool PlanarFileDevice::open(const QVector &filenames, QIODevice::OpenMode mode) +{ + if (isOpen()) { + // Already open + return false; + } + + files_.resize(filenames.size()); + files_.fill(nullptr); + + for (int i=0; iopen(mode)) { + close(); + return false; + } + } + + return true; +} + +qint64 PlanarFileDevice::read(char **data, qint64 bytes_per_channel, qint64 offset) +{ + qint64 ret = -1; + + if (isOpen()) { + for (int i=0; iread(data[i] + offset, bytes_per_channel); + } + } + + return ret; +} + +qint64 PlanarFileDevice::write(const char **data, qint64 bytes_per_channel, qint64 offset) +{ + qint64 ret = -1; + + if (isOpen()) { + for (int i=0; iwrite(data[i] + offset, bytes_per_channel); + } + } + + return ret; +} + +qint64 PlanarFileDevice::size() const +{ + if (isOpen()) { + return files_.first()->size(); + } else { + return 0; + } +} + +bool PlanarFileDevice::seek(qint64 pos) +{ + bool ret = true; + + for (int i=0; iseek(pos) & ret; + } + + return ret; +} + +void PlanarFileDevice::close() +{ + for (int i=0; iisOpen()) { + f->close(); + } + delete f; + } + } + files_.clear(); +} + +} diff --git a/app/codec/planarfiledevice.h b/app/codec/planarfiledevice.h new file mode 100644 index 000000000..d02dafa19 --- /dev/null +++ b/app/codec/planarfiledevice.h @@ -0,0 +1,64 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef PLANARFILEDEVICE_H +#define PLANARFILEDEVICE_H + +#include +#include + +#include "codec/samplebuffer.h" +#include "common/define.h" + +namespace olive { + +class PlanarFileDevice : public QObject +{ + Q_OBJECT +public: + PlanarFileDevice(QObject *parent = nullptr); + + virtual ~PlanarFileDevice() override; + + bool isOpen() const + { + return !files_.isEmpty(); + } + + bool open(const QVector &filenames, QIODevice::OpenMode mode); + + qint64 read(char **data, qint64 bytes_per_channel, qint64 offset = 0); + + qint64 write(const char **data, qint64 bytes_per_channel, qint64 offset = 0); + + qint64 size() const; + + bool seek(qint64 pos); + + void close(); + +private: + QVector files_; + +}; + +} + +#endif // PLANARFILEDEVICE_H diff --git a/app/codec/samplebuffer.cpp b/app/codec/samplebuffer.cpp new file mode 100644 index 000000000..1dede4ae5 --- /dev/null +++ b/app/codec/samplebuffer.cpp @@ -0,0 +1,231 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "samplebuffer.h" + +namespace olive { + +SampleBuffer::SampleBuffer() : + sample_count_per_channel_(0) +{ +} + +SampleBufferPtr SampleBuffer::Create() +{ + return std::make_shared(); +} + +SampleBufferPtr SampleBuffer::CreateAllocated(const AudioParams &audio_params, const rational &length) +{ + return CreateAllocated(audio_params, audio_params.time_to_samples(length)); +} + +SampleBufferPtr SampleBuffer::CreateAllocated(const AudioParams &audio_params, int samples_per_channel) +{ + SampleBufferPtr buffer = Create(); + + buffer->set_audio_params(audio_params); + buffer->set_sample_count(samples_per_channel); + buffer->allocate(); + + return buffer; +} + +const AudioParams &SampleBuffer::audio_params() const +{ + return audio_params_; +} + +void SampleBuffer::set_audio_params(const AudioParams ¶ms) +{ + if (is_allocated()) { + qWarning() << "Tried to set parameters on allocated sample buffer"; + return; + } + + audio_params_ = params; +} + +const int &SampleBuffer::sample_count() const +{ + return sample_count_per_channel_; +} + +void SampleBuffer::set_sample_count(const int &sample_count) +{ + if (is_allocated()) { + qWarning() << "Tried to set sample count on allocated sample buffer"; + return; + } + + sample_count_per_channel_ = sample_count; +} + +bool SampleBuffer::is_allocated() const +{ + return !data_.isEmpty(); +} + +void SampleBuffer::allocate() +{ + if (!audio_params_.is_valid()) { + qWarning() << "Tried to allocate sample buffer with invalid audio parameters"; + return; + } + + if (!sample_count_per_channel_) { + qWarning() << "Tried to allocate sample buffer with zero sample count"; + return; + } + + if (is_allocated()) { + qWarning() << "Tried to allocate already allocated sample buffer"; + return; + } + + data_.resize(audio_params_.channel_count()); + for (int i=0; i(sample_count_per_channel_) / speed); + + QVector< QVector > output_data; + + output_data.resize(audio_params_.channel_count()); + for (int i=0; i(i) * speed); + + for (int j=0;j(data_[i].data()) + start_byte, 0, end_byte - start_byte); + } +} + +void SampleBuffer::set(int channel, const float *data, int sample_offset, int sample_length) +{ + if (!is_allocated()) { + qWarning() << "Tried to fill an unallocated sample buffer"; + return; + } + + memcpy(&data_[channel].data()[sample_offset], data, sizeof(float) * sample_length); +} + +void SampleBuffer::update_raw() +{ + raw_ptrs_.resize(data_.size()); + for (int i=0; i. + +***/ + +#ifndef SAMPLEBUFFER_H +#define SAMPLEBUFFER_H + +#include + +#include "render/audioparams.h" + +namespace olive { + +class SampleBuffer; +using SampleBufferPtr = std::shared_ptr; + +/** + * @brief A buffer of audio samples + * + * Audio samples in this structure are always stored in PLANAR (separated by channel). This is done to simplify audio + * rendering code. This replaces the old system of using QByteArrays (containing packed audio) and while SampleBuffer + * replaces many of those in the rendering/processing side of things, QByteArrays are currently still in use for + * playback, including reading to and from the cache. + */ +class SampleBuffer +{ +public: + SampleBuffer(); + + static SampleBufferPtr Create(); + static SampleBufferPtr CreateAllocated(const AudioParams& audio_params, const rational& length); + static SampleBufferPtr CreateAllocated(const AudioParams& audio_params, int samples_per_channel); + + DISABLE_COPY_MOVE(SampleBuffer) + + const AudioParams& audio_params() const; + void set_audio_params(const AudioParams& params); + + const int &sample_count() const; + void set_sample_count(const int &sample_count); + + float* data(int channel) + { + return data_[channel].data(); + } + + const float* data(int channel) const + { + return data_.at(channel).constData(); + } + + float **to_raw_ptrs() + { + return raw_ptrs_.data(); + } + + bool is_allocated() const; + void allocate(); + void destroy(); + + void reverse(); + void speed(double speed); + void transform_volume(float f); + void transform_volume_for_channel(int channel, float volume); + void transform_volume_for_sample(int sample_index, float volume); + void transform_volume_for_sample_on_channel(int sample_index, int channel, float volume); + + void silence(); + void silence(int start_sample, int end_sample); + void silence_bytes(int start_byte, int end_byte); + + void set(int channel, const float* data, int sample_offset, int sample_length); + void set(int channel, const float* data, int sample_length) + { + set(channel, data, 0, sample_length); + } + +private: + void update_raw(); + + AudioParams audio_params_; + + int sample_count_per_channel_; + + QVector< QVector > data_; + QVector raw_ptrs_; + +}; + +} + +Q_DECLARE_METATYPE(olive::SampleBufferPtr) + +#endif // SAMPLEBUFFER_H diff --git a/app/common/CMakeLists.txt b/app/common/CMakeLists.txt index 37f01e99e..2938ad229 100644 --- a/app/common/CMakeLists.txt +++ b/app/common/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2019 Olive Team +# Copyright (C) 2021 Olive Team # # 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 @@ -16,25 +16,52 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} + common/bezier.cpp + common/bezier.h + common/cancelableobject.h common/channellayout.h common/clamp.h - common/debug.h + common/commandlineparser.cpp + common/commandlineparser.h + common/crashpadinterface.cpp + common/crashpadinterface.h + common/crashpadutils.h common/debug.cpp - common/filefunctions.h + common/debug.h + common/decibel.h + common/define.h + common/ffmpegutils.cpp + common/ffmpegutils.h common/filefunctions.cpp - common/flipmodifiers.h + common/filefunctions.h common/flipmodifiers.cpp + common/flipmodifiers.h + common/functiontimer.h + common/jobtime.cpp + common/jobtime.h common/lerp.h + common/memorypool.h + common/ocioutils.cpp + common/ocioutils.h + common/oiioutils.cpp + common/oiioutils.h + common/otioutils.h + common/qtutils.cpp + common/qtutils.h common/range.h - common/rational.h + common/ratiodialog.cpp + common/ratiodialog.h common/rational.cpp - common/qtversionabstraction.h - common/qtversionabstraction.cpp - common/threadedobject.h + common/rational.h common/threadedobject.cpp - common/timecodefunctions.h + common/threadedobject.h + common/threadsafemap.h common/timecodefunctions.cpp - common/timerange.h + common/timecodefunctions.h common/timerange.cpp + common/timerange.h + common/tohex.h + common/xmlutils.cpp + common/xmlutils.h PARENT_SCOPE ) diff --git a/app/common/autoscroll.h b/app/common/autoscroll.h new file mode 100644 index 000000000..4929348c7 --- /dev/null +++ b/app/common/autoscroll.h @@ -0,0 +1,39 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef AUTOSCROLL_H +#define AUTOSCROLL_H + +#include "common/define.h" + +namespace olive { + +class AutoScroll { +public: + enum Method { + kNone, + kPage, + kSmooth + }; +}; + +} + +#endif // AUTOSCROLL_H diff --git a/app/common/bezier.cpp b/app/common/bezier.cpp new file mode 100644 index 000000000..b1a5f5fba --- /dev/null +++ b/app/common/bezier.cpp @@ -0,0 +1,110 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "bezier.h" + +#include + +#include "common/clamp.h" + +namespace olive { + +Bezier::Bezier() : + x_(0), + y_(0), + cp1_x_(0), + cp1_y_(0), + cp2_x_(0), + cp2_y_(0) +{ +} + +Bezier::Bezier(double x, double y) : + x_(x), + y_(y), + cp1_x_(0), + cp1_y_(0), + cp2_x_(0), + cp2_y_(0) +{ +} + +Bezier::Bezier(double x, double y, double cp1_x, double cp1_y, double cp2_x, double cp2_y) : + x_(x), + y_(y), + cp1_x_(cp1_x), + cp1_y_(cp1_y), + cp2_x_(cp2_x), + cp2_y_(cp2_y) +{ +} + +double Bezier::QuadraticXtoT(double x, double a, double b, double c) +{ + // Clamp to prevent infinite loop + x = clamp(x, a, c); + + return CalculateTFromX(false, x, a, b, c, 0); +} + +double Bezier::QuadraticTtoY(double a, double b, double c, double t) +{ + return qPow(1.0 - t, 2)*a + 2*(1.0 - t)*t*b + qPow(t, 2)*c; +} + +double Bezier::CubicXtoT(double x, double a, double b, double c, double d) +{ + // Clamp to prevent infinite loop + x = clamp(x, a, d); + + return CalculateTFromX(true, x, a, b, c, d); +} + +double Bezier::CubicTtoY(double a, double b, double c, double d, double t) +{ + return qPow(1.0 - t, 3)*a + 3*qPow(1.0 - t, 2)*t*b + 3*(1.0 - t)*qPow(t, 2)*c + qPow(t, 3)*d; +} + +double Bezier::CalculateTFromX(bool cubic, double x, double a, double b, double c, double d) +{ + double bottom = 0.0; + double top = 1.0; + + while (true) { + if (bottom == top) { + return bottom; + } + + double mid = (bottom + top) * 0.5; + double test = cubic ? CubicTtoY(a, b, c, d, mid) : QuadraticTtoY(a, b, c, mid); + + if (qAbs(test - x) < 0.000001) { + return mid; + } else if (x > test) { + bottom = mid; + } else { + top = mid; + } + } + + return qSNaN(); +} + +} diff --git a/app/common/bezier.h b/app/common/bezier.h new file mode 100644 index 000000000..8d20a75ec --- /dev/null +++ b/app/common/bezier.h @@ -0,0 +1,103 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef BEZIER_H +#define BEZIER_H + +#include +#include + +#include "common/define.h" + +namespace olive { + +class Bezier +{ +public: + Bezier(); + Bezier(double x, double y); + Bezier(double x, double y, double cp1_x, double cp1_y, double cp2_x, double cp2_y); + + const double &x() const {return x_; } + const double &y() const {return y_; } + const double &cp1_x() const { return cp1_x_; } + const double &cp1_y() const { return cp1_y_; } + const double &cp2_x() const { return cp2_x_; } + const double &cp2_y() const { return cp2_y_; } + + QPointF ToPointF() const + { + return QPointF(x_, y_); + } + + QPointF ControlPoint1ToPointF() const + { + return QPointF(cp1_x_, cp1_y_); + } + + QPointF ControlPoint2ToPointF() const + { + return QPointF(cp2_x_, cp2_y_); + } + + void set_x(const double &x) { x_ = x; } + void set_y(const double &y) { y_ = y; } + void set_cp1_x(const double &cp1_x) { cp1_x_ = cp1_x; } + void set_cp1_y(const double &cp1_y) { cp1_y_ = cp1_y; } + void set_cp2_x(const double &cp2_x) { cp2_x_ = cp2_x; } + void set_cp2_y(const double &cp2_y) { cp2_y_ = cp2_y; } + + static double QuadraticXtoT(double x, double a, double b, double c); + + static double QuadraticTtoY(double a, double b, double c, double t); + + static double QuadraticXtoY(double x, const QPointF &a, const QPointF &b, const QPointF &c) + { + return QuadraticTtoY(a.y(), b.y(), c.y(), QuadraticXtoT(x, a.x(), b.x(), c.x())); + } + + static double CubicXtoT(double x, double a, double b, double c, double d); + + static double CubicTtoY(double a, double b, double c, double d, double t); + + static double CubicXtoY(double x, const QPointF &a, const QPointF &b, const QPointF &c, const QPointF &d) + { + return CubicTtoY(a.y(), b.y(), c.y(), d.y(), CubicXtoT(x, a.x(), b.x(), c.x(), d.x())); + } + +private: + static double CalculateTFromX(bool cubic, double x, double a, double b, double c, double d); + + double x_; + double y_; + + double cp1_x_; + double cp1_y_; + + double cp2_x_; + double cp2_y_; + +}; + +} + +Q_DECLARE_METATYPE(olive::Bezier) + +#endif // BEZIER_H diff --git a/app/common/cancelableobject.h b/app/common/cancelableobject.h new file mode 100644 index 000000000..4eafbb547 --- /dev/null +++ b/app/common/cancelableobject.h @@ -0,0 +1,58 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef CANCELABLEOBJECT_H +#define CANCELABLEOBJECT_H + +#include + +#include "common/define.h" + +namespace olive { + +class CancelableObject { +public: + CancelableObject() : + cancelled_(false) + { + } + + void Cancel() + { + cancelled_ = true; + CancelEvent(); + } + + const QAtomicInt& IsCancelled() const + { + return cancelled_; + } + +protected: + virtual void CancelEvent(){} + +private: + QAtomicInt cancelled_; + +}; + +} + +#endif // CANCELABLEOBJECT_H diff --git a/app/common/channellayout.h b/app/common/channellayout.h index 0273925a4..07e5994e2 100644 --- a/app/common/channellayout.h +++ b/app/common/channellayout.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 diff --git a/app/common/clamp.h b/app/common/clamp.h index dda706505..0ab1ffd4a 100644 --- a/app/common/clamp.h +++ b/app/common/clamp.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 diff --git a/app/common/commandlineparser.cpp b/app/common/commandlineparser.cpp new file mode 100644 index 000000000..150bdc244 --- /dev/null +++ b/app/common/commandlineparser.cpp @@ -0,0 +1,170 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "commandlineparser.h" + +#include +#include + +CommandLineParser::~CommandLineParser() +{ + foreach (const KnownOption& o, options_) { + delete o.option; + } + + foreach (const KnownPositionalArgument& a, positional_args_) { + delete a.option; + } +} + +const CommandLineParser::Option *CommandLineParser::AddOption(const QStringList &strings, const QString &description, bool takes_arg, const QString &arg_placeholder, bool hidden) +{ + Option* o = new Option(); + + options_.append({strings, description, o, takes_arg, arg_placeholder, hidden}); + + return o; +} + +const CommandLineParser::PositionalArgument *CommandLineParser::AddPositionalArgument(const QString &name, const QString &description, bool required) +{ + PositionalArgument* a = new PositionalArgument(); + + positional_args_.append({name, description, a, required}); + + return a; +} + +void CommandLineParser::Process(const QVector &argv) +{ + int positional_index = 0; + + for (int i=1; iSet(); + + if (o.takes_arg && i+1 < argv.size()) { + o.option->SetSetting(argv[i+1]); + i++; + } + + matched_known = true; + goto found_flag; + } + } + } + +found_flag: + if (!matched_known) { + qWarning() << "Unknown parameter:" << argv[i]; + } + + } else { + // Must be a positional flag + if (positional_index < positional_args_.size()) { + positional_args_[positional_index].option->SetSetting(argv[i]); + positional_index++; + } else { + qWarning() << "Unknown parameter:" << argv[i]; + } + } + } +} + +void CommandLineParser::PrintHelp(const char* filename) +{ + printf("%s %s\n", + QCoreApplication::applicationName().toUtf8().constData(), + QCoreApplication::applicationVersion().toUtf8().constData()); + + printf("Copyright (C) 2018-2021 Olive Team\n"); + + QString positional_args; + for (int i=0; i 0) { + positional_args.append(' '); + } + + positional_args.append('['); + positional_args.append(positional_args_.at(i).name); + positional_args.append(']'); + } + + const char* basename; +#ifdef Q_OS_WINDOWS + basename = strrchr(filename, '\\'); + if (!basename) { + basename = strrchr(filename, '/'); + } +#else + basename = strrchr(filename, '/'); +#endif + + if (basename) { + // Slash found, increment pointer to avoid showing the slash itself + basename++; + } else { + // If no slashes are found, assume string is already a basename + basename = filename; + } + + printf("Usage: %s [options] %s\n\n", basename, positional_args.toUtf8().constData()); + foreach (const KnownOption& o, options_) { + if (o.hidden) { + continue; + } + + QString all_args; + + for (int i=0; i 0) { + all_args.append(QStringLiteral(", ")); + } + + const QString& this_arg = o.args.at(i); + + all_args.append('-'); + all_args.append(this_arg); + } + + if (o.arg_placeholder.isEmpty()) { + printf(" %s\n", all_args.toUtf8().constData()); + } else { + printf(" %s <%s>\n", all_args.toUtf8().constData(), o.arg_placeholder.toUtf8().constData()); + } + + printf(" %s\n\n", o.description.toUtf8().constData()); + } + + printf("\n"); +} diff --git a/app/common/commandlineparser.h b/app/common/commandlineparser.h new file mode 100644 index 000000000..4b32a9134 --- /dev/null +++ b/app/common/commandlineparser.h @@ -0,0 +1,121 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef COMMANDLINEPARSER_H +#define COMMANDLINEPARSER_H + +#include +#include + +#include "common/define.h" + +/** + * @brief Command-line argument parser + * + * You may be wondering why we don't use QCommandLineParser instead of a custom implementation like + * this. The reason why is because QCommandLineParser requires a QApplication object of some kind + * to already have been created before it can parse anything, but we need to be able to control + * whether a QApplication (GUI-mode) or a QCoreApplication (CLI-mode) is created which is set by + * the user as a command line argument. Therefore we needed a custom implementation that could + * parse arguments without the need for a Q(Core)Application to be present already. + */ +class CommandLineParser +{ +public: + ~CommandLineParser(); + + DISABLE_COPY_MOVE(CommandLineParser) + + class PositionalArgument + { + public: + PositionalArgument() = default; + + const QString& GetSetting() const + { + return setting_; + } + + void SetSetting(const QString& s) + { + setting_ = s; + } + + private: + QString setting_; + + }; + + class Option : public PositionalArgument { + public: + Option() + { + is_set_ = false; + } + + bool IsSet() const + { + return is_set_; + } + + void Set() + { + is_set_ = true; + } + + private: + bool is_set_; + + }; + + CommandLineParser() = default; + + const Option* AddOption(const QStringList& strings, const QString& description, bool takes_arg = false, const QString& arg_placeholder = QString(), bool hidden = false); + + const PositionalArgument* AddPositionalArgument(const QString& name, const QString& description, bool required = false); + + void Process(const QVector &argv); + + void PrintHelp(const char* filename); + +private: + struct KnownOption { + QStringList args; + QString description; + Option* option; + bool takes_arg; + QString arg_placeholder; + bool hidden; + }; + + struct KnownPositionalArgument { + QString name; + QString description; + PositionalArgument* option; + bool required; + }; + + QVector options_; + + QVector positional_args_; + +}; + +#endif // COMMANDLINEPARSER_H diff --git a/app/common/constructors.h b/app/common/constructors.h deleted file mode 100644 index 592020687..000000000 --- a/app/common/constructors.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef CONSTRUCTORS_H -#define CONSTRUCTORS_H - -/** - * Copy/move deleters. Similar to Q_DISABLE_COPY_MOVE, et al. but those functions are not present in Qt < 5.13 so we - * use our own functions for portability. - */ - -#define DISABLE_COPY(Class) \ - Class(const Class &) = delete;\ - Class &operator=(const Class &) = delete; - -#define DISABLE_MOVE(Class) \ - Class(Class &&) = delete; \ - Class &operator=(Class &&) = delete; - -#define DISABLE_COPY_MOVE(Class) \ - DISABLE_COPY(Class) \ - DISABLE_MOVE(Class) - -#endif // CONSTRUCTORS_H diff --git a/app/common/qtversionabstraction.cpp b/app/common/cpuoptimize.h similarity index 74% rename from app/common/qtversionabstraction.cpp rename to app/common/cpuoptimize.h index b0a7ef975..2b9d009e4 100644 --- a/app/common/qtversionabstraction.cpp +++ b/app/common/cpuoptimize.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -18,12 +18,13 @@ ***/ -#include "qtversionabstraction.h" +#ifndef CPUOPTIMIZE_H +#define CPUOPTIMIZE_H -int QFontMetricsWidth(QFontMetrics fm, const QString& s) { -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - return fm.width(s); -#else - return fm.horizontalAdvance(s); +#if defined(Q_PROCESSOR_X86) +#include +#elif defined(Q_PROCESSOR_ARM) +#include #endif -} + +#endif // CPUOPTIMIZE_H diff --git a/app/common/crashpadinterface.cpp b/app/common/crashpadinterface.cpp new file mode 100644 index 000000000..808484f4d --- /dev/null +++ b/app/common/crashpadinterface.cpp @@ -0,0 +1,91 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "crashpadinterface.h" + +#ifdef USE_CRASHPAD + +#include +#include +#include +#include +#include +#include + +#include "crashpadutils.h" +#include "filefunctions.h" + +#ifdef OS_WIN +#include +#endif + +crashpad::CrashpadClient *client; + +bool InitializeCrashpad() +{ + QString report_path = QDir(olive::FileFunctions::GetTempFilePath()).filePath(QStringLiteral("reports")); + + QString handler_fn = olive::FileFunctions::GetFormattedExecutableForPlatform(QStringLiteral("crashpad_handler")); + + // Generate absolute path + QString handler_abs_path = QDir(QCoreApplication::applicationDirPath()).filePath(handler_fn); + + bool status = false; + + if (QFileInfo::exists(handler_abs_path)) { + base::FilePath handler(QSTRING_TO_BASE_STRING(handler_abs_path)); + + base::FilePath reports_dir(QSTRING_TO_BASE_STRING(report_path)); + + base::FilePath metrics_dir(QSTRING_TO_BASE_STRING(QDir(olive::FileFunctions::GetTempFilePath()).filePath(QStringLiteral("metrics")))); + + // Metadata that will be posted to the server with the crash report map + std::map annotations; + + // Disable crashpad rate limiting so that all crashes have dmp files + std::vector arguments; + arguments.push_back("--no-rate-limit"); + arguments.push_back("--no-upload-gzip"); + + // Initialize Crashpad database + std::unique_ptr database = crashpad::CrashReportDatabase::Initialize(reports_dir); + if (database == NULL) return false; + + // Disable automated crash uploads + crashpad::Settings *settings = database->GetSettings(); + if (settings == NULL) return false; + settings->SetUploadsEnabled(false); + + // Start crash handler + client = new crashpad::CrashpadClient(); + status = client->StartHandler(handler, reports_dir, metrics_dir, + "https://olivevideoeditor.org/crashpad/report.php", + annotations, arguments, true, true); + } + + // Override Crashpad exception filter with our own + if (!status) { + qWarning() << "Failed to start Crashpad, automatic crash reporting will be disabled"; + } + + return status; +} + +#endif // USE_CRASHPAD diff --git a/app/task/taskthread.cpp b/app/common/crashpadinterface.h similarity index 72% rename from app/task/taskthread.cpp rename to app/common/crashpadinterface.h index 234fbf8ef..69f968fe5 100644 --- a/app/task/taskthread.cpp +++ b/app/common/crashpadinterface.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -18,22 +18,16 @@ ***/ -#include "taskthread.h" +#ifndef CRASHPAD_INTERFACE_H +#define CRASHPAD_INTERFACE_H -#include "task/task.h" +#ifdef USE_CRASHPAD -TaskThread::TaskThread(Task *parent) : - parent_(parent), - result_(false) -{ -} +#include +#include -void TaskThread::run() -{ - result_ = parent_->Action(); -} +bool InitializeCrashpad(); -bool TaskThread::result() -{ - return result_; -} +#endif // USE_CRASHPAD + +#endif // CRASHPAD_INTERFACE_H diff --git a/app/common/crashpadutils.h b/app/common/crashpadutils.h new file mode 100644 index 000000000..d1e5406ca --- /dev/null +++ b/app/common/crashpadutils.h @@ -0,0 +1,40 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef CRASHPADUTILS_H +#define CRASHPADUTILS_H + +#include + +// Copied from base::FilePath to match its macro +#if defined(OS_POSIX) +// On most platforms, native pathnames are char arrays, and the encoding +// may or may not be specified. On Mac OS X, native pathnames are encoded +// in UTF-8. +#define QSTRING_TO_BASE_STRING(x) x.toStdString() +#define BASE_STRING_TO_QSTRING(x) QString::fromStdString(x) +#elif defined(OS_WIN) +// On Windows, for Unicode-aware applications, native pathnames are wchar_t +// arrays encoded in UTF-16. +#define QSTRING_TO_BASE_STRING(x) x.toStdWString() +#define BASE_STRING_TO_QSTRING(x) QString::fromStdWString(x) +#endif // OS_WIN + +#endif // CRASHPADUTILS_H diff --git a/app/common/debug.cpp b/app/common/debug.cpp index 644917dda..83603a8a0 100644 --- a/app/common/debug.cpp +++ b/app/common/debug.cpp @@ -1,5 +1,26 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + #include "debug.h" +namespace olive { void DebugHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { @@ -32,3 +53,5 @@ void DebugHandler(QtMsgType type, const QMessageLogContext &context, const QStri fflush(stderr); #endif } + +} diff --git a/app/common/debug.h b/app/common/debug.h index 39446adb8..36413fee0 100644 --- a/app/common/debug.h +++ b/app/common/debug.h @@ -1,8 +1,34 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + #ifndef DEBUG_H #define DEBUG_H #include +#include "common/define.h" + +namespace olive { + void DebugHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg); +} + #endif // DEBUG_H diff --git a/app/common/decibel.cpp b/app/common/decibel.cpp deleted file mode 100644 index c93efbc95..000000000 --- a/app/common/decibel.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "decibel.h" - -#include - -double amplitude_to_db(double amplitude) { - return (20.0*(qLn(amplitude)/qLn(10.0))); -} - -double db_to_amplitude(double db) { - return qPow(M_E, (db*qLn(10.0))/20.0); -} diff --git a/app/common/decibel.h b/app/common/decibel.h index 2c7bdedfe..61d57f276 100644 --- a/app/common/decibel.h +++ b/app/common/decibel.h @@ -1,18 +1,89 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + #ifndef DECIBEL_H #define DECIBEL_H -/** - * @brief Converts an amplitude into a value in decibels - * - * Converts a 0.0-1.0 amplitude into an infinity-0.0 decibel value - */ -double amplitude_to_db(double amplitude); +#include +#include -/** - * @brief Converts decibels into an amplitude value - * - * Converts an infinity-0.0 decibel value into a 0.0-1.0 amplitude - */ -double db_to_amplitude(double db); +namespace olive { + +class Decibel +{ +public: + static double fromLinear(double linear) + { + return double(20.0) * std::log10(linear); + } + + static double toLinear(double decibel) + { + double to_linear = std::pow(double(10.0), decibel / double(20.0)); + + // Minimum threshold that we figure is close enough to 0 that we may as well just return 0 + if (to_linear < 0.000001) { + return 0; + } else { + return to_linear; + } + } + + static double fromLogarithmic(double logarithmic) + { + if (logarithmic < 0.001) + return -200.0; + else if (logarithmic > 0.99) + return 0; + else + return 20.0 * std::log10(-std::log(1 - logarithmic) / LOG100); + } + + static double toLogarithmic(double decibel) + { + if (qFuzzyIsNull(decibel)) { + return 1; + } else { + return 1 - std::exp(-std::pow(10.0, decibel / 20.0) * LOG100); + } + } + + static double LinearToLogarithmic(double linear) + { + return 1 - std::exp(-linear * LOG100); + } + + static double LogarithmicToLinear(double logarithmic) + { + if (logarithmic > 0.99) { + return 1; + } else { + return -std::log(1 - logarithmic) / LOG100; + } + } + +private: + static constexpr double LOG100 = 4.60517018599; + +}; + +} #endif // DECIBEL_H diff --git a/app/common/define.h b/app/common/define.h index 967a5e878..9ec47e4fc 100644 --- a/app/common/define.h +++ b/app/common/define.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -21,7 +21,43 @@ #ifndef OLIVECOMMONDEFINE_H #define OLIVECOMMONDEFINE_H -const int kRGBChannels = 3; -const int kRGBAChannels = 4; +namespace olive { + +/// The minimum size an icon in ProjectExplorer can be +const int kProjectIconSizeMinimum = 16; + +/// The maximum size an icon in ProjectExplorer can be +const int kProjectIconSizeMaximum = 256; + +/// The default size an icon in ProjectExplorer can be +const int kProjectIconSizeDefault = 64; + +const int kBytesInGigabyte = 1073741824; + +} + +#define MACRO_NAME_AS_STR(s) #s +#define MACRO_VAL_AS_STR(s) MACRO_NAME_AS_STR(s) + +#define OLIVE_NS_CONST_ARG(x, y) QArgument("const " MACRO_VAL_AS_STR(olive) "::" #x, y) +#define OLIVE_NS_ARG(x, y) QArgument(MACRO_VAL_AS_STR(olive) "::" #x, y) +#define OLIVE_NS_RETURN_ARG(x, y) QReturnArgument(MACRO_VAL_AS_STR(olive) "::" #x, y) + +/** + * Copy/move deleters. Similar to Q_DISABLE_COPY_MOVE, et al. but those functions are not present in Qt < 5.13 so we + * use our own functions for portability. + */ + +#define DISABLE_COPY(Class) \ + Class(const Class &) = delete;\ + Class &operator=(const Class &) = delete; + +#define DISABLE_MOVE(Class) \ + Class(Class &&) = delete; \ + Class &operator=(Class &&) = delete; + +#define DISABLE_COPY_MOVE(Class) \ + DISABLE_COPY(Class) \ + DISABLE_MOVE(Class) #endif // OLIVECOMMONDEFINE_H diff --git a/app/node/output.cpp b/app/common/digit.h similarity index 69% rename from app/node/output.cpp rename to app/common/digit.h index c70fae381..d4e208944 100644 --- a/app/node/output.cpp +++ b/app/common/digit.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -18,25 +18,28 @@ ***/ -#include "output.h" +#ifndef DIGIT_H +#define DIGIT_H -#include "node/node.h" +#include -NodeOutput::NodeOutput(const QString &id) : - NodeParam(id) +namespace olive { + +inline int64_t GetDigitCount(int64_t input) { -} + input = std::abs(input); -NodeParam::Type NodeOutput::type() -{ - return kOutput; -} + int64_t lim = 10; + int64_t digit = 1; -QString NodeOutput::name() -{ - if (name_.isEmpty()) { - return tr("Output"); + while (input >= lim) { + lim *= 10; + digit++; } - return NodeParam::name(); + return digit; } + +} + +#endif // DIGIT_H diff --git a/app/common/ffmpegutils.cpp b/app/common/ffmpegutils.cpp new file mode 100644 index 000000000..6bc8b037f --- /dev/null +++ b/app/common/ffmpegutils.cpp @@ -0,0 +1,141 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "common/ffmpegutils.h" + +namespace olive { + +AVPixelFormat FFmpegUtils::GetCompatiblePixelFormat(const AVPixelFormat &pix_fmt) +{ + AVPixelFormat possible_pix_fmts[] = { + AV_PIX_FMT_RGB24, + AV_PIX_FMT_RGBA, + AV_PIX_FMT_RGB48, + AV_PIX_FMT_RGBA64, + AV_PIX_FMT_NONE + }; + + return avcodec_find_best_pix_fmt_of_list(possible_pix_fmts, + pix_fmt, + 1, + nullptr); +} + +AudioParams::Format FFmpegUtils::GetNativeSampleFormat(const AVSampleFormat &smp_fmt) +{ + switch (smp_fmt) { + case AV_SAMPLE_FMT_U8: + return AudioParams::kFormatUnsigned8; + case AV_SAMPLE_FMT_S16: + return AudioParams::kFormatSigned16; + case AV_SAMPLE_FMT_S32: + return AudioParams::kFormatSigned32; + case AV_SAMPLE_FMT_S64: + return AudioParams::kFormatSigned64; + case AV_SAMPLE_FMT_FLT: + return AudioParams::kFormatFloat32; + case AV_SAMPLE_FMT_DBL: + return AudioParams::kFormatFloat64; + case AV_SAMPLE_FMT_U8P : + case AV_SAMPLE_FMT_S16P: + case AV_SAMPLE_FMT_S32P: + case AV_SAMPLE_FMT_S64P: + case AV_SAMPLE_FMT_FLTP: + case AV_SAMPLE_FMT_DBLP: + case AV_SAMPLE_FMT_NONE: + case AV_SAMPLE_FMT_NB: + break; + } + + return AudioParams::kFormatInvalid; +} + +AVSampleFormat FFmpegUtils::GetFFmpegSampleFormat(const AudioParams::Format &smp_fmt, bool planar) +{ + switch (smp_fmt) { + case AudioParams::kFormatUnsigned8: + return planar ? AV_SAMPLE_FMT_U8P : AV_SAMPLE_FMT_U8; + case AudioParams::kFormatSigned16: + return planar ? AV_SAMPLE_FMT_S16P : AV_SAMPLE_FMT_S16; + case AudioParams::kFormatSigned32: + return planar ? AV_SAMPLE_FMT_S32P : AV_SAMPLE_FMT_S32; + case AudioParams::kFormatSigned64: + return planar ? AV_SAMPLE_FMT_S64P : AV_SAMPLE_FMT_S64; + case AudioParams::kFormatFloat32: + return planar ? AV_SAMPLE_FMT_FLTP : AV_SAMPLE_FMT_FLT; + case AudioParams::kFormatFloat64: + return planar ? AV_SAMPLE_FMT_DBLP : AV_SAMPLE_FMT_DBL; + case AudioParams::kFormatInvalid: + case AudioParams::kFormatCount: + break; + } + + return AV_SAMPLE_FMT_NONE; +} + +AVPixelFormat FFmpegUtils::GetFFmpegPixelFormat(const VideoParams::Format &pix_fmt, int channel_layout) +{ + if (channel_layout == VideoParams::kRGBChannelCount) { + switch (pix_fmt) { + case VideoParams::kFormatUnsigned8: + return AV_PIX_FMT_RGB24; + case VideoParams::kFormatUnsigned16: + return AV_PIX_FMT_RGB48; + case VideoParams::kFormatFloat16: + case VideoParams::kFormatFloat32: + case VideoParams::kFormatInvalid: + case VideoParams::kFormatCount: + break; + } + } else if (channel_layout == VideoParams::kRGBAChannelCount) { + switch (pix_fmt) { + case VideoParams::kFormatUnsigned8: + return AV_PIX_FMT_RGBA; + case VideoParams::kFormatUnsigned16: + return AV_PIX_FMT_RGBA64; + case VideoParams::kFormatFloat16: + case VideoParams::kFormatFloat32: + case VideoParams::kFormatInvalid: + case VideoParams::kFormatCount: + break; + } + } + + return AV_PIX_FMT_NONE; +} + +VideoParams::Format FFmpegUtils::GetCompatiblePixelFormat(const VideoParams::Format &pix_fmt) +{ + switch (pix_fmt) { + case VideoParams::kFormatUnsigned8: + return VideoParams::kFormatUnsigned8; + case VideoParams::kFormatUnsigned16: + case VideoParams::kFormatFloat16: + case VideoParams::kFormatFloat32: + return VideoParams::kFormatUnsigned16; + case VideoParams::kFormatInvalid: + case VideoParams::kFormatCount: + break; + } + + return VideoParams::kFormatInvalid; +} + +} diff --git a/app/common/ffmpegutils.h b/app/common/ffmpegutils.h new file mode 100644 index 000000000..2262dc299 --- /dev/null +++ b/app/common/ffmpegutils.h @@ -0,0 +1,64 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef FFMPEGABSTRACTION_H +#define FFMPEGABSTRACTION_H + +extern "C" { +#include +#include +} + +#include "render/audioparams.h" +#include "render/videoparams.h" + +namespace olive { + +class FFmpegUtils { +public: + /** + * @brief Returns an AVPixelFormat that can be used to convert a frame to a data type Olive supports with minimal data loss + */ + static AVPixelFormat GetCompatiblePixelFormat(const AVPixelFormat& pix_fmt); + + /** + * @brief Returns a native pixel format that can be used to convert from a native frame to an AVFrame with minimal data loss + */ + static VideoParams::Format GetCompatiblePixelFormat(const VideoParams::Format& pix_fmt); + + /** + * @brief Returns an FFmpeg pixel format for a given native pixel format + */ + static AVPixelFormat GetFFmpegPixelFormat(const VideoParams::Format& pix_fmt, int channel_layout); + + /** + * @brief Returns a native sample format type for a given AVSampleFormat + */ + static AudioParams::Format GetNativeSampleFormat(const AVSampleFormat& smp_fmt); + + /** + * @brief Returns an FFmpeg sample format type for a given native type + */ + static AVSampleFormat GetFFmpegSampleFormat(const AudioParams::Format &smp_fmt, bool planar = false); +}; + +} + +#endif // FFMPEGABSTRACTION_H diff --git a/app/common/filefunctions.cpp b/app/common/filefunctions.cpp index 9678da570..5c36ea0c7 100644 --- a/app/common/filefunctions.cpp +++ b/app/common/filefunctions.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -27,7 +27,11 @@ #include #include -QString GetUniqueFileIdentifier(const QString &filename) +#include "config/config.h" + +namespace olive { + +QString FileFunctions::GetUniqueFileIdentifier(const QString &filename) { QFileInfo info(filename); @@ -39,57 +43,206 @@ QString GetUniqueFileIdentifier(const QString &filename) hash.addData(info.absoluteFilePath().toUtf8()); - hash.addData(info.lastModified().toString().toUtf8()); + hash.addData(QString::number(info.lastModified().toMSecsSinceEpoch()).toUtf8()); QByteArray result = hash.result(); return QString(result.toHex()); } -QString GetMediaIndexLocation() -{ - QDir local_appdata_dir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)); - - QDir media_index_dir = local_appdata_dir.filePath("mediaindex"); - - // Attempt to ensure this folder exists - media_index_dir.mkpath("."); - - return media_index_dir.absolutePath(); -} - -QString GetMediaIndexFilename(const QString &filename) -{ - return QDir(GetMediaIndexLocation()).filePath(filename); -} - -QString GetMediaCacheLocation() -{ - QDir local_appdata_dir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)); - - QDir media_cache_dir = local_appdata_dir.filePath("mediacache"); - - // Attempt to ensure this folder exists - media_cache_dir.mkpath("."); - - return media_cache_dir.absolutePath(); -} - -QString GetConfigurationLocation() +QString FileFunctions::GetConfigurationLocation() { if (IsPortable()) { return GetApplicationPath(); } else { - return QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); + QString s = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + QDir(s).mkpath("."); + return s; } } -bool IsPortable() +bool FileFunctions::IsPortable() { return QFileInfo::exists(QDir(GetApplicationPath()).filePath("portable")); } -QString GetApplicationPath() +QString FileFunctions::GetApplicationPath() { return QCoreApplication::applicationDirPath(); } + +QString FileFunctions::GetTempFilePath() +{ + QString temp_path = QDir(QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)) + .filePath(QCoreApplication::organizationName())) + .filePath(QCoreApplication::applicationName()); + + // Ensure it exists + QDir(temp_path).mkpath("."); + + return temp_path; +} + +bool FileFunctions::CanCopyDirectoryWithoutOverwriting(const QString& source, const QString& dest) +{ + QFileInfoList info_list = QDir(source).entryInfoList(); + + foreach (const QFileInfo& info, info_list) { + // QDir::NoDotAndDotDot continues to not work, so we have to check manually + if (info.fileName() == QStringLiteral(".") || info.fileName() == QStringLiteral("..")) { + continue; + } + + QString dest_equivalent = QDir(dest).filePath(info.fileName()); + + if (info.isDir()) { + if (!CanCopyDirectoryWithoutOverwriting(info.absoluteFilePath(), dest_equivalent)) { + return false; + } + } else if (QFileInfo::exists(dest_equivalent)) { + return false; + } + } + + return true; +} + +void FileFunctions::CopyDirectory(const QString &source, const QString &dest, bool overwrite) +{ + QDir d(source); + + if (!d.exists()) { + qCritical() << "Failed to copy directory, source" << source << "didn't exist"; + return; + } + + QDir dest_dir(dest); + + if (!dest_dir.mkpath(QStringLiteral("."))) { + qCritical() << "Failed to create destination directory" << dest; + return; + } + + QFileInfoList l = d.entryInfoList(); + + foreach (const QFileInfo& info, l) { + // QDir::NoDotAndDotDot continues to not work, so we have to check manually + if (info.fileName() == QStringLiteral(".") || info.fileName() == QStringLiteral("..")) { + continue; + } + + QString dest_file_path = dest_dir.filePath(info.fileName()); + + if (info.isDir()) { + // Copy dir + CopyDirectory(info.absoluteFilePath(), dest_file_path, overwrite); + } else { + // Copy file + if (overwrite && QFile::exists(dest_file_path)) { + QFile::remove(dest_file_path); + } + + QFile::copy(info.absoluteFilePath(), dest_file_path); + } + } +} + +bool FileFunctions::DirectoryIsValid(const QString &dir, bool try_to_create) +{ + // Empty string is invalid + if (dir.isEmpty()) { + return false; + } + + QDir d(dir); + + // If directory already exists, this is valid + if (d.exists()) { + return true; + } + + // If we can create and creation is successful, this is valid + if (try_to_create && d.mkpath(".")) { + return true; + } + + // Otherwise, invalid + return false; +} + +QString FileFunctions::EnsureFilenameExtension(QString fn, const QString &extension) +{ + // No-op if either input is empty + if (!fn.isEmpty() && !extension.isEmpty()) { + QString extension_with_dot; + + extension_with_dot.append('.'); + extension_with_dot.append(extension); + + if (!fn.endsWith(extension_with_dot, Qt::CaseInsensitive)) { + fn.append(extension_with_dot); + } + } + + return fn; +} + +QString FileFunctions::ReadFileAsString(const QString &filename) +{ + QFile f(filename); + QString file_data; + if (f.open(QFile::ReadOnly | QFile::Text)) { + QTextStream text_stream(&f); + file_data = text_stream.readAll(); + f.close(); + } + return file_data; +} + +QString FileFunctions::GetSafeTemporaryFilename(const QString &original) +{ + int counter = 0; + + QFileInfo original_info(original); + QString basename = original_info.baseName(); + QString complete_suffix = original_info.completeSuffix(); + + // If we have a complete suffix, make sure there's a period in it + if (!complete_suffix.isEmpty()) { + complete_suffix.prepend('.'); + } + + QString temp_abs_path; + do { + temp_abs_path = original_info.dir().filePath( + QStringLiteral("%1.tmp%2%3").arg(basename, + QString::number(counter), + complete_suffix)); + counter++; + } while (QFileInfo::exists(temp_abs_path)); + + return temp_abs_path; +} + +bool FileFunctions::RenameFileAllowOverwrite(const QString &from, const QString &to) +{ + if (QFileInfo::exists(to) && !QFile::remove(to)) { + qCritical() << "Couldn't remove existing file" << to << "for overwrite"; + return false; + } + + // By this point, we can assume `to` either never existed or has now been deleted + if (!QFile::rename(from, to)) { + qCritical() << "Failed to rename file" << from << "to" << to; + return false; + } + + return true; +} + +QString FileFunctions::GetAutoRecoveryRoot() +{ + return QDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath(QStringLiteral("autorecovery")); +} + +} diff --git a/app/common/filefunctions.h b/app/common/filefunctions.h index d1f79abf9..122bf076c 100644 --- a/app/common/filefunctions.h +++ b/app/common/filefunctions.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -23,18 +23,82 @@ #include -bool IsPortable(); +#include "common/define.h" -QString GetUniqueFileIdentifier(const QString& filename); +namespace olive { -QString GetMediaIndexLocation(); +/** + * @brief A collection of static file and directory functions + */ +class FileFunctions { +public: + /** + * @brief Returns true if the application is running in portable mode + * + * In portable mode, any persistent configuration files should be made in a path relative to the application rather + * than in the user's home folder. + */ + static bool IsPortable(); -QString GetMediaIndexFilename(const QString& filename); + static QString GetUniqueFileIdentifier(const QString& filename); -QString GetMediaCacheLocation(); + static QString GetConfigurationLocation(); -QString GetConfigurationLocation(); + static QString GetApplicationPath(); -QString GetApplicationPath(); + static QString GetTempFilePath(); + + static bool CanCopyDirectoryWithoutOverwriting(const QString& source, const QString& dest); + + static void CopyDirectory(const QString& source, const QString& dest, bool overwrite = false); + + static bool DirectoryIsValid(const QString& dir, bool try_to_create); + + /** + * @brief Ensures a given filename has a certain extension + * + * Checks if the filename has the extension provided and appends it if not. The extension is + * checked case-insensitive. The extension should be provided with no dot (e.g. "ove" rather than + * ".ove"). + + * @return The filename provided either untouched or with the extension appended to it. + */ + static QString EnsureFilenameExtension(QString fn, const QString& extension); + + static QString ReadFileAsString(const QString& filename); + + /** + * @brief Returns a temporary filename that can be used while writing rather than the original + * + * If overwriting a file, it's safest to write to a new file first and then only replace it at + * the end so that if the program crashes or the user cancels the save half way through, the + * original file is still intact. + * + * This function returns a slight variant of the filename provided that's guaranteed to not exist + * and therefore won't overwrite anything important. + */ + static QString GetSafeTemporaryFilename(const QString& original); + + /** + * @brief Renames a file from `from` to `to`, deleting `to` if such a file already exists first + */ + static bool RenameFileAllowOverwrite(const QString& from, const QString& to); + + inline static QString GetFormattedExecutableForPlatform(QString unformatted) + { +#ifdef Q_OS_WINDOWS + unformatted.append(QStringLiteral(".exe")); +#endif + + return unformatted; + } + + static QString GetAutoRecoveryRoot(); + +}; + + + +} #endif // FILEFUNCTIONS_H diff --git a/app/common/flipmodifiers.cpp b/app/common/flipmodifiers.cpp index 3652babcf..95c624628 100644 --- a/app/common/flipmodifiers.cpp +++ b/app/common/flipmodifiers.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -20,6 +20,8 @@ #include "flipmodifiers.h" +namespace olive { + Qt::KeyboardModifiers FlipControlAndShiftModifiers(Qt::KeyboardModifiers e) { if (e & Qt::ControlModifier & Qt::ShiftModifier) { return e; @@ -35,3 +37,5 @@ Qt::KeyboardModifiers FlipControlAndShiftModifiers(Qt::KeyboardModifiers e) { return e; } + +} diff --git a/app/common/flipmodifiers.h b/app/common/flipmodifiers.h index 8fb754eda..3a9e538c0 100644 --- a/app/common/flipmodifiers.h +++ b/app/common/flipmodifiers.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -23,6 +23,12 @@ #include +#include "common/define.h" + +namespace olive { + Qt::KeyboardModifiers FlipControlAndShiftModifiers(Qt::KeyboardModifiers e); +} + #endif // FLIPMODIFIERS_H diff --git a/app/project/item/footage/audiostream.h b/app/common/functiontimer.h similarity index 52% rename from app/project/item/footage/audiostream.h rename to app/common/functiontimer.h index 4635c21b0..a36e4afc9 100644 --- a/app/project/item/footage/audiostream.h +++ b/app/common/functiontimer.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -18,37 +18,38 @@ ***/ -#ifndef AUDIOSTREAM_H -#define AUDIOSTREAM_H +#ifndef FUNCTIONTIMER_H +#define FUNCTIONTIMER_H -#include "common/rational.h" -#include "stream.h" +#include +#include -/** - * @brief A Stream derivative containing audio-specific information - */ -class AudioStream : public Stream -{ +#define TIME_THIS_FUNCTION FunctionTimer __f(__FUNCTION__) +#define START_TIMING {FunctionTimer *__f = new FunctionTimer(__FUNCTION__) +#define STOP_TIMING delete __f;}void() + +class FunctionTimer { public: - AudioStream(); + FunctionTimer(const char* s) + { + name_ = s; + time_ = QDateTime::currentMSecsSinceEpoch(); + } - virtual QString description() override; + ~FunctionTimer() + { + qint64 elapsed = (QDateTime::currentMSecsSinceEpoch() - time_); - const int& channels(); - void set_channels(const int& channels); - - const uint64_t& layout(); - void set_layout(const uint64_t& layout); - - const int& sample_rate(); - void set_sample_rate(const int& sample_rate); + if (elapsed > 1) { + qDebug() << name_ << "took" << elapsed; + } + } private: - int channels_; - uint64_t layout_; - int sample_rate_; + const char* name_; + + qint64 time_; + }; -using AudioStreamPtr = std::shared_ptr; - -#endif // AUDIOSTREAM_H +#endif // FUNCTIONTIMER_H diff --git a/app/common/jobtime.cpp b/app/common/jobtime.cpp new file mode 100644 index 000000000..43077ebf1 --- /dev/null +++ b/app/common/jobtime.cpp @@ -0,0 +1,30 @@ +#include "jobtime.h" + +#include + +namespace olive { + +uint64_t job_time_index = 0; +QMutex job_time_mutex; + +JobTime::JobTime() +{ + Acquire(); +} + +void JobTime::Acquire() +{ + job_time_mutex.lock(); + + value_ = job_time_index; + job_time_index++; + + job_time_mutex.unlock(); +} + +} + +QDebug operator<<(QDebug debug, const olive::JobTime& r) +{ + return debug.space() << r.value(); +} diff --git a/app/common/jobtime.h b/app/common/jobtime.h new file mode 100644 index 000000000..27988b92e --- /dev/null +++ b/app/common/jobtime.h @@ -0,0 +1,62 @@ +#ifndef JOBTIME_H +#define JOBTIME_H + +#include +#include + +namespace olive { + +class JobTime +{ +public: + JobTime(); + + void Acquire(); + + uint64_t value() const + { + return value_; + } + + bool operator==(const JobTime &rhs) const + { + return value_ == rhs.value_; + } + + bool operator!=(const JobTime &rhs) const + { + return value_ != rhs.value_; + } + + bool operator<(const JobTime &rhs) const + { + return value_ < rhs.value_; + } + + bool operator>(const JobTime &rhs) const + { + return value_ > rhs.value_; + } + + bool operator<=(const JobTime &rhs) const + { + return value_ <= rhs.value_; + } + + bool operator>=(const JobTime &rhs) const + { + return value_ >= rhs.value_; + } + +private: + uint64_t value_; + +}; + +} + +QDebug operator<<(QDebug debug, const olive::JobTime& r); + +Q_DECLARE_METATYPE(olive::JobTime) + +#endif // JOBTIME_H diff --git a/app/common/lerp.h b/app/common/lerp.h index d62e97ddd..454c55a01 100644 --- a/app/common/lerp.h +++ b/app/common/lerp.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -32,4 +32,9 @@ T lerp(T a, T b, double t) { return (a * (1.0 - t)) + (b * t); } +template +T lerp(T a, T b, float t) { + return (a * (1.0f - t)) + (b * t); +} + #endif // LERP_H diff --git a/app/common/memorypool.h b/app/common/memorypool.h new file mode 100644 index 000000000..8435dee0b --- /dev/null +++ b/app/common/memorypool.h @@ -0,0 +1,434 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef MEMORYPOOL_H +#define MEMORYPOOL_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/define.h" + +namespace olive { + +/** + * @brief MemoryPool base class + * + * A custom memory system that allocates can allocate several objects in a large chunk (as opposed to several small + * allocations). Improves performance and memory consumption. + * + * As a class, this base is usable by setting the template to an object of your choosing. The pool will then allocate + * `(element_count * sizeof(T))` per arena. Arenas are allocated and destroyed on the fly - when an arena fills up, + * another is allocated. + * + * `Get()` will return an ElementPtr. The original desired data can be accessed through ElementPtr::data(). This data + * will belong to the caller until ElementPtr goes out of scope and the memory is freed back into the pool. + */ +class MemoryPool : public QObject +{ + Q_OBJECT +public: + /** + * @brief Constructor + * @param element_count + * + * Number of elements per arena + */ + MemoryPool(int element_count) + { + element_count_ = element_count; + + clear_timer_ = new QTimer(); + clear_timer_->setInterval(kMaxEmptyArenaLife); + clear_timer_->moveToThread(qApp->thread()); + connect(clear_timer_, &QTimer::timeout, this, &MemoryPool::ClearEmptyArenas, Qt::DirectConnection); + QMetaObject::invokeMethod(clear_timer_, "start", Qt::QueuedConnection); + } + + /** + * @brief Destructor + * + * Deletes all arenas. + */ + virtual ~MemoryPool() + { + Clear(); + clear_timer_->deleteLater(); + } + + DISABLE_COPY_MOVE(MemoryPool) + + /** + * @brief Clears all arenas, freeing all of their memory + * + * Note that this function is not safe, any elements that are still out there will be invalid + * and accessing them will cause a crash. You'll need to make sure all elements are already + * relinquished before then. + */ + void Clear() + { + qDeleteAll(arenas_); + arenas_.clear(); + } + + /** + * @brief Returns whether any arenas are successfully allocated + */ + inline bool IsAllocated() const + { + return !arenas_.empty(); + } + + /** + * @brief Returns current number of allocated arenas + */ + inline int GetArenaCount() const + { + return arenas_.size(); + } + + class Arena; + + /** + * @brief A handle for a chunk of memory in an arena + * + * Calling Get() on the pool or arena will return a shared pointer to an element which will contain a pointer to + * the desired object/data in data(). When Element is destroyed (i.e. when ElementPtr goes out of scope), the memory + * is released back into the pool so it can be used by another class. + */ + class Element { + public: + /** + * @brief Element Constructor + * + * There is no need to use this outside of the memory pool's internal functions. + */ + Element(Arena* parent, uint8_t* data) + { + parent_ = parent; + data_ = data; + accessed_ = QDateTime::currentMSecsSinceEpoch(); + } + + /** + * @brief Element Destructor + * + * Automatically releases this element's memory back to the arena it was retrieved from. + */ + ~Element() + { + release(); + } + + DISABLE_COPY_MOVE(Element) + + /** + * @brief Access data represented in the pool + */ + inline uint8_t* data() const + { + return data_; + } + + inline const int64_t& timestamp() const + { + return timestamp_; + } + + inline void set_timestamp(const int64_t& timestamp) + { + timestamp_ = timestamp; + } + + /** + * @brief Register that this element has been accessed + * + * \see last_accessed() + */ + inline void access() + { + accessed_ = QDateTime::currentMSecsSinceEpoch(); + } + + /** + * @brief Returns the last time `access()` was called on this function + * + * Useful for determining the relative age of an element (i.e. if it hasn't been accessed for a certain amount of + * time, it can probably be freed back into the pool). This requires all usages to call `access()`. + */ + inline const int64_t& last_accessed() const + { + return accessed_; + } + + void release() + { + if (data_) { + parent_->Release(this); + data_ = nullptr; + } + } + + private: + Arena* parent_; + + uint8_t* data_; + + int64_t timestamp_; + + int64_t accessed_; + + }; + + using ElementPtr = std::shared_ptr; + + /** + * @brief A memory pool arena - a subsection of memory + * + * The pool itself does not store memory, it stores "arenas". This is so that the pool can handle the situation of + * an arena becoming full with no more memory to lend. A pool can automatically allocate another arena and continue + * providing memory (and freeing arenas when they're no longer in use). + */ + class Arena { + public: + Arena(MemoryPool* parent) + { + parent_ = parent; + data_ = nullptr; + allocated_sz_ = 0; + empty_time_ = QDateTime::currentMSecsSinceEpoch(); + } + + ~Arena() + { + std::list copy = lent_elements_; + foreach (Element* e, copy) { + e->release(); + } + + delete [] data_; + } + + DISABLE_COPY_MOVE(Arena) + + /** + * @brief Returns an element if there is free memory to do so + */ + ElementPtr Get() + { + QMutexLocker locker(&lock_); + + for (int i=0;i(this, + reinterpret_cast(data_ + i * element_sz_)); + lent_elements_.push_back(e.get()); + + return e; + } + } + + return nullptr; + } + + /** + * @brief Releases an element back into the pool for use elsewhere + */ + void Release(Element* e) + { + QMutexLocker locker(&lock_); + quintptr diff = reinterpret_cast(e->data()) - reinterpret_cast(data_); + + int index = diff / element_sz_; + + available_.replace(index, true); + + lent_elements_.remove(e); + + if (lent_elements_.empty()) { + empty_time_ = QDateTime::currentMSecsSinceEpoch(); + } + } + + int GetUsageCount() + { + QMutexLocker locker(&lock_); + return lent_elements_.size(); + } + + bool Allocate(size_t ele_sz, size_t nb_elements) + { + if (IsAllocated()) { + return true; + } + + element_sz_ = ele_sz; + + allocated_sz_ = element_sz_ * nb_elements; + + if ((data_ = new uint8_t[allocated_sz_])) { + available_.resize(nb_elements); + available_.fill(true); + + return true; + } else { + available_.clear(); + + return false; + } + } + + inline int GetElementCount() const + { + return available_.size(); + } + + inline bool IsAllocated() const + { + return data_; + } + + inline qint64 GetTimeArenaWasMadeEmpty() + { + QMutexLocker locker(&lock_); + return empty_time_; + } + + private: + MemoryPool* parent_; + + uint8_t* data_; + + size_t allocated_sz_; + + QVector available_; + + QMutex lock_; + + size_t element_sz_; + + std::list lent_elements_; + + qint64 empty_time_; + + }; + + /** + * @brief Retrieves an element from an available arena + */ + ElementPtr Get() + { + QMutexLocker locker(&lock_); + + // Attempt to get an element from an arena + foreach (Arena* a, arenas_) { + ElementPtr e = a->Get(); + + if (e) { + return e; + } + } + + // All arenas were empty, we'll need to create a new one + if (arenas_.empty()) { + qDebug() << "No arenas, creating new..."; + } else { + qDebug() << "All arenas are full, creating new..."; + } + + size_t ele_sz = GetElementSize(); + + if (!ele_sz) { + qCritical() << "Failed to create arena, element size was 0"; + return nullptr; + } + + if (element_count_ <= 0) { + qCritical() << "Failed to create arena, element count was invalid:" << element_count_; + return nullptr; + } + + Arena* a = new Arena(this); + if (!a->Allocate(ele_sz, element_count_)) { + qCritical() << "Failed to create arena, allocation failed. Out of memory?"; + delete a; + return nullptr; + } + + arenas_.push_back(a); + return a->Get(); + } + +protected: + /** + * @brief The size of each element + * + * Override this to use a custom size (e.g. a char array where T = char but the element size is > 1) + */ + virtual size_t GetElementSize() + { + return sizeof(uint8_t); + } + +private: + int element_count_; + + std::list arenas_; + + QMutex lock_; + + QTimer *clear_timer_; + + static const qint64 kMaxEmptyArenaLife = 5000; + +private slots: + void ClearEmptyArenas() + { + QMutexLocker locker(&lock_); + + const qint64 min_time = QDateTime::currentMSecsSinceEpoch() - kMaxEmptyArenaLife; + + for (auto it=arenas_.begin(); it!=arenas_.end(); ) { + Arena* arena = (*it); + + if (arena->GetUsageCount() == 0 && arena->GetTimeArenaWasMadeEmpty() <= min_time) { + qDebug() << "Removing an empty arena"; + delete arena; + it = arenas_.erase(it); + } else { + it++; + } + } + } + +}; + +} + +#endif // MEMORYPOOL_H diff --git a/app/node/input/media/media.cpp b/app/common/ocioutils.cpp similarity index 52% rename from app/node/input/media/media.cpp rename to app/common/ocioutils.cpp index 7bb66f79f..b296f0305 100644 --- a/app/node/input/media/media.cpp +++ b/app/common/ocioutils.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -18,26 +18,30 @@ ***/ -#include "media.h" +#include "ocioutils.h" -MediaInput::MediaInput() +namespace olive { + +OCIO::BitDepth OCIOUtils::GetOCIOBitDepthFromPixelFormat(VideoParams::Format format) { - footage_input_ = new NodeInput("footage_in"); - footage_input_->set_data_type(NodeInput::kFootage); - AddInput(footage_input_); + switch (format) { + case VideoParams::kFormatUnsigned8: + return OCIO::BIT_DEPTH_UINT8; + case VideoParams::kFormatUnsigned16: + return OCIO::BIT_DEPTH_UINT16; + break; + case VideoParams::kFormatFloat16: + return OCIO::BIT_DEPTH_F16; + break; + case VideoParams::kFormatFloat32: + return OCIO::BIT_DEPTH_F32; + break; + case VideoParams::kFormatInvalid: + case VideoParams::kFormatCount: + break; + } + + return OCIO::BIT_DEPTH_UNKNOWN; } -StreamPtr MediaInput::footage() -{ - return footage_input_->get_value_at_time(0).value(); -} - -void MediaInput::SetFootage(StreamPtr f) -{ - footage_input_->set_value_at_time(0, QVariant::fromValue(f)); -} - -void MediaInput::Retranslate() -{ - footage_input_->set_name(tr("Footage")); } diff --git a/app/common/ocioutils.h b/app/common/ocioutils.h new file mode 100644 index 000000000..6fa294de7 --- /dev/null +++ b/app/common/ocioutils.h @@ -0,0 +1,39 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef OCIOUTILS_H +#define OCIOUTILS_H + +#include +namespace OCIO = OCIO_NAMESPACE; + +#include "render/videoparams.h" + +namespace olive { + +class OCIOUtils +{ +public: + static OCIO::BitDepth GetOCIOBitDepthFromPixelFormat(VideoParams::Format format); +}; + +} + +#endif // OCIOUTILS_H diff --git a/app/common/oiioutils.cpp b/app/common/oiioutils.cpp new file mode 100644 index 000000000..682618e1a --- /dev/null +++ b/app/common/oiioutils.cpp @@ -0,0 +1,81 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "oiioutils.h" + +namespace olive { + +void OIIOUtils::FrameToBuffer(const Frame* frame, OIIO::ImageBuf *buf) +{ + buf->set_pixels(OIIO::ROI(), + buf->spec().format, + frame->const_data(), + OIIO::AutoStride, + frame->linesize_bytes()); +} + +void OIIOUtils::BufferToFrame(OIIO::ImageBuf *buf, Frame* frame) +{ + buf->get_pixels(OIIO::ROI(), + buf->spec().format, + frame->data(), + OIIO::AutoStride, + frame->linesize_bytes()); +} + +rational OIIOUtils::GetPixelAspectRatioFromOIIO(const OIIO::ImageSpec &spec) +{ + return rational::fromDouble(spec.get_float_attribute("PixelAspectRatio", 1)); +} + +VideoParams::Format OIIOUtils::GetFormatFromOIIOBasetype(OIIO::TypeDesc::BASETYPE type) +{ + switch (type) { + case OIIO::TypeDesc::UNKNOWN: + case OIIO::TypeDesc::NONE: + break; + + case OIIO::TypeDesc::INT8: + case OIIO::TypeDesc::INT16: + case OIIO::TypeDesc::INT32: + case OIIO::TypeDesc::UINT32: + case OIIO::TypeDesc::INT64: + case OIIO::TypeDesc::UINT64: + case OIIO::TypeDesc::STRING: + case OIIO::TypeDesc::PTR: + case OIIO::TypeDesc::LASTBASE: + case OIIO::TypeDesc::DOUBLE: + qDebug() << "Tried to use unknown OIIO base type"; + break; + + case OIIO::TypeDesc::UINT8: + return VideoParams::kFormatUnsigned8; + case OIIO::TypeDesc::UINT16: + return VideoParams::kFormatUnsigned16; + case OIIO::TypeDesc::HALF: + return VideoParams::kFormatFloat16; + case OIIO::TypeDesc::FLOAT: + return VideoParams::kFormatFloat32; + } + + return VideoParams::kFormatInvalid; +} + +} diff --git a/app/common/oiioutils.h b/app/common/oiioutils.h new file mode 100644 index 000000000..860a359f1 --- /dev/null +++ b/app/common/oiioutils.h @@ -0,0 +1,65 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef OIIOUTILS_H +#define OIIOUTILS_H + +#include +#include + +#include "codec/frame.h" +#include "render/videoparams.h" + +namespace olive { + +class OIIOUtils { +public: + static OIIO::TypeDesc::BASETYPE GetOIIOBaseTypeFromFormat(VideoParams::Format format) + { + switch (format) { + case VideoParams::kFormatUnsigned8: + return OIIO::TypeDesc::UINT8; + case VideoParams::kFormatUnsigned16: + return OIIO::TypeDesc::UINT16; + case VideoParams::kFormatFloat16: + return OIIO::TypeDesc::HALF; + case VideoParams::kFormatFloat32: + return OIIO::TypeDesc::FLOAT; + case VideoParams::kFormatInvalid: + case VideoParams::kFormatCount: + break; + } + + return OIIO::TypeDesc::UNKNOWN; + } + + static void FrameToBuffer(const Frame *frame, OIIO::ImageBuf* buf); + + static void BufferToFrame(OIIO::ImageBuf* buf, Frame* frame); + + static VideoParams::Format GetFormatFromOIIOBasetype(OIIO::TypeDesc::BASETYPE type); + + static rational GetPixelAspectRatioFromOIIO(const OIIO::ImageSpec& spec); + +}; + +} + +#endif // OIIOUTILS_H diff --git a/app/node/menu.h b/app/common/otioutils.h similarity index 78% rename from app/node/menu.h rename to app/common/otioutils.h index 625d3b6b3..1a3a32dd0 100644 --- a/app/node/menu.h +++ b/app/common/otioutils.h @@ -18,21 +18,12 @@ ***/ -#ifndef NODEMENU_H -#define NODEMENU_H +#ifndef OTIOUTILS_H +#define OTIOUTILS_H -#include +#ifdef USE_OTIO +#include +namespace OTIO = opentimelineio::OPENTIMELINEIO_VERSION; +#endif -#include "node.h" - -class NodeMenu : public QMenu -{ -public: - NodeMenu(QWidget* parent = nullptr); - -private: - - -}; - -#endif // NODEMENU_H +#endif // OTIOUTILS diff --git a/app/task/probe/probe.cpp b/app/common/power.h similarity index 60% rename from app/task/probe/probe.cpp rename to app/common/power.h index 297b16bee..29d172f5b 100644 --- a/app/task/probe/probe.cpp +++ b/app/common/power.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -18,27 +18,39 @@ ***/ -#include "probe.h" +#ifndef POWER_H +#define POWER_H -#include +#include -#include "decoder/decoder.h" +#include "common/define.h" -ProbeTask::ProbeTask(FootagePtr footage) : - footage_(footage) +namespace olive { + +uint32_t ceil_to_power_of_2(uint32_t v) { - QString base_filename = QFileInfo(footage_->filename()).fileName(); + v--; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v++; - set_text(tr("Probing \"%1\"").arg(base_filename)); + return v; } -bool ProbeTask::Action() +uint32_t floor_to_power_of_2(uint32_t x) { - footage_->LockDeletes(); + x = x | (x >> 1); + x = x | (x >> 2); + x = x | (x >> 4); + x = x | (x >> 8); + x = x | (x >> 16); - Decoder::ProbeMedia(footage_.get()); - - footage_->UnlockDeletes(); - - return true; + return x - (x >> 1); } + +} + +#endif // POWER_H diff --git a/app/common/qtutils.cpp b/app/common/qtutils.cpp new file mode 100644 index 000000000..757ca2617 --- /dev/null +++ b/app/common/qtutils.cpp @@ -0,0 +1,75 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "qtutils.h" + +namespace olive { + +int QtUtils::QFontMetricsWidth(QFontMetrics fm, const QString& s) { +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) + return fm.width(s); +#else + return fm.horizontalAdvance(s); +#endif +} + +QFrame *QtUtils::CreateHorizontalLine() +{ + QFrame* horizontal_line = new QFrame(); + horizontal_line->setFrameShape(QFrame::HLine); + horizontal_line->setFrameShadow(QFrame::Sunken); + return horizontal_line; +} + +int QtUtils::MessageBox(QWidget *parent, QMessageBox::Icon icon, const QString &title, const QString &message, QMessageBox::StandardButtons buttons) +{ + QMessageBox b(parent); + b.setIcon(icon); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(title); + b.setText(message); + + uint mask = QMessageBox::FirstButton; + while (mask <= QMessageBox::LastButton) { + uint sb = buttons & mask; + if (sb) { + b.addButton(static_cast(sb)); + } + mask <<= 1; + } + + return b.exec(); +} + +QDateTime QtUtils::GetCreationDate(const QFileInfo &info) +{ +#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) + return info.created(); +#else + return info.birthTime(); +#endif +} + +QString QtUtils::GetFormattedDateTime(const QDateTime &dt) +{ + return dt.toString(Qt::TextDate); +} + +} diff --git a/app/common/qtutils.h b/app/common/qtutils.h new file mode 100644 index 000000000..2247f89d8 --- /dev/null +++ b/app/common/qtutils.h @@ -0,0 +1,67 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef QTVERSIONABSTRACTION_H +#define QTVERSIONABSTRACTION_H + +/** + * + * A fairly simple header for reducing the amount of Qt version checks necessary throughout the code + * + */ + +#include +#include +#include +#include +#include + +#include "common/define.h" + +#ifdef MessageBox +#undef MessageBox +#endif + +namespace olive { + +class QtUtils { +public: + /** + * @brief Retrieves the width of a string according to certain QFontMetrics + * + * QFontMetrics::width() has been deprecatd in favor of QFontMetrics::horizontalAdvance(), but the + * latter was only introduced in 5.11+. This function wraps the latter for 5.11+ and the former for + * earlier. + */ + static int QFontMetricsWidth(QFontMetrics fm, const QString& s); + + static QFrame* CreateHorizontalLine(); + + static int MessageBox(QWidget *parent, QMessageBox::Icon icon, const QString& title, const QString& message, QMessageBox::StandardButtons buttons = QMessageBox::Ok); + + static QDateTime GetCreationDate(const QFileInfo &info); + + static QString GetFormattedDateTime(const QDateTime &dt); + +}; + +} + +#endif // QTVERSIONABSTRACTION_H diff --git a/app/common/qtversionabstraction.h b/app/common/qtversionabstraction.h deleted file mode 100644 index c4ed17e7f..000000000 --- a/app/common/qtversionabstraction.h +++ /dev/null @@ -1,40 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - 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 . - -***/ - -#ifndef QTVERSIONABSTRACTION_H -#define QTVERSIONABSTRACTION_H - -/** - * - * A fairly simple header for reducing the amount of Qt version checks necessary throughout the code - * - */ - -#include - -/** - * @brief Retrieves the width of a string according to certain QFontMetrics - * - * QFontMetrics::width() has been deprecatd in favor of QFontMetrics::horizontalAdvance(), but the latter was only - * introduced in 5.11+. This function wraps the latter for 5.11+ and the former for earlier. - */ -int QFontMetricsWidth(QFontMetrics fm, const QString& s); - -#endif // QTVERSIONABSTRACTION_H diff --git a/app/common/range.h b/app/common/range.h index 96de47c74..c983743c4 100644 --- a/app/common/range.h +++ b/app/common/range.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 diff --git a/app/common/ratiodialog.cpp b/app/common/ratiodialog.cpp new file mode 100644 index 000000000..8e5017f0b --- /dev/null +++ b/app/common/ratiodialog.cpp @@ -0,0 +1,91 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "ratiodialog.h" + +#include +#include + +namespace olive { + +double GetFloatRatioFromUser(QWidget* parent, + const QString& title, + bool* ok_in) +{ + QString s; + + forever { + bool ok; + + s = QInputDialog::getText(parent, + title, + QCoreApplication::translate("RatioDialog", "Enter custom ratio (e.g. \"4:3\", \"16/9\", etc.):"), + QLineEdit::Normal, + s, + &ok); + + if (!ok) { + // User cancelled dialog, do nothing + if (ok_in) { + *ok_in = false; + } + return qSNaN(); + } + + QStringList ratio_components = s.split(QRegExp(QStringLiteral(":|;|\\/"))); + + if (ratio_components.size() == 1) { + bool float_ok; + + double flt = ratio_components.at(0).toDouble(&float_ok); + + if (float_ok && flt > 0) { + if (ok_in) { + *ok_in = true; + } + return flt; + } + } else if (ratio_components.size() == 2) { + bool numer_ok, denom_ok; + + double num = ratio_components.at(0).toDouble(&numer_ok); + double den = ratio_components.at(1).toDouble(&denom_ok); + + if (numer_ok + && denom_ok + && num > 0 + && den > 0) { + // Exit loop and set this ratio + if (ok_in) { + *ok_in = true; + } + return num / den; + } + } + + QMessageBox::warning(parent, + QCoreApplication::translate("RatioDialog", "Invalid custom ratio"), + QCoreApplication::translate("RatioDialog", "Failed to parse \"%1\" into an aspect ratio. Please format a " + "rational fraction with a ':' or a '/' separator.").arg(s), + QMessageBox::Ok); + } +} + +} diff --git a/app/common/ratiodialog.h b/app/common/ratiodialog.h new file mode 100644 index 000000000..3b2a2fe5c --- /dev/null +++ b/app/common/ratiodialog.h @@ -0,0 +1,36 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef RATIODIALOG_H +#define RATIODIALOG_H + +#include + +#include "common/rational.h" + +namespace olive { + +double GetFloatRatioFromUser(QWidget* parent, + const QString& title, + bool* ok_in); + +} + +#endif // RATIODIALOG_H diff --git a/app/common/rational.cpp b/app/common/rational.cpp index 059b3057b..9aa20426c 100644 --- a/app/common/rational.cpp +++ b/app/common/rational.cpp @@ -3,197 +3,265 @@ #include "rational.h" -rational::rational(const AVRational &r) : - numer(r.num), - denom(r.den) +namespace olive { + +const rational rational::NaN = rational(0, 0); + +rational rational::fromDouble(const double &flt, bool* ok) { + if (qIsNaN(flt)) { + // Return NaN rational + if (ok) *ok = false; + return NaN; + } + + // Use FFmpeg function for the time being + AVRational r = av_d2q(flt, INT_MAX); + + if (r.den == 0) { + // If den == 0, we were unable to convert to a rational + if (ok) { + *ok = false; + } + } else { + // Otherwise, assume we received a real rational + if (ok) { + *ok = true; + } + } + + return r; } -//Function: print number to cout - -void rational::print(ostream &out) const +rational rational::fromString(const QString &str, bool* ok) { - out << this->numer << "/" << this->denom; + QStringList elements = str.split('/'); + + switch (elements.size()) { + case 1: + return rational(elements.first().toLongLong(ok)); + case 2: + return rational(elements.at(0).toLongLong(ok), elements.at(1).toLongLong(ok)); + default: + // Returns NaN with ok set to false + if (ok) { + *ok = false; + } + return NaN; + } } //Function: ensures denom >= 0 -void rational::fixSigns() +void rational::fix_signs() { - if(denom < 0) - { - denom = -denom; - numer = -numer; - } - if(numer == intType(0) || denom == intType(0)) - { - numer = intType(0); - denom = intType(0); - } + // Normalize so that denominator is always positive and only numerator is positive + if (denom_ < 0) { + denom_ = -denom_; + numer_ = -numer_; + } else if (denom_ == intType(0)) { + // Normalize to 0/0 (aka NaN) if denominator is zero + numer_ = intType(0); + } else if (numer_ == intType(0)) { + // Normalize to 0/1 if numerator is zero + denom_ = intType(1); + } } //Function: ensures lowest form void rational::reduce() { - // Euclidean often fails if numbers are negative, we abs it and re-neg it later if necessary - bool neg = numer < 0; + if (!isNull()) { + // Euclidean often fails if numbers are negative, we abs it and re-neg it later if necessary + bool neg = numer_ < 0; - numer = qAbs(numer); + numer_ = qAbs(numer_); - intType d = 1; + intType d = gcd(numer_, denom_); - if(denom != 0 && numer !=0) - d = gcd(numer, denom); - - if(d > 1) - { - numer /= d; - denom /= d; + if (d > 1) { + numer_ /= d; + denom_ /= d; } - if (neg) { - numer = -numer; + if (neg) { + numer_ = -numer_; + } } } //Function: finds greatest common denominator -intType rational::gcd(intType &x, intType &y) +intType rational::gcd(const intType &x, const intType &y) { - if(y == 0) + if (y == 0) { return x; - else - { - intType tmp = x % y; - - return gcd(y, tmp); - } + } else { + return gcd(y, x % y); + } } //Function: convert to double double rational::toDouble() const { - if(denom != 0) - return static_cast(numer) / static_cast(denom); - else - return static_cast(0); + if (denom_ != 0) { + return static_cast(numer_) / static_cast(denom_); + } else { + return qSNaN(); + } } AVRational rational::toAVRational() const { AVRational r; - r.num = static_cast(numer); - r.den = static_cast(denom); + r.num = static_cast(numer_); + r.den = static_cast(denom_); return r; } +#ifdef USE_OTIO +opentime::RationalTime rational::toRationalTime(double framerate) const +{ + // Is this the best way of doing this? + // Olive can store rationals as 0/0 which causes errors in OTIO + opentime::RationalTime time = opentime::RationalTime(numer_, denom_ == 0 ? 1 : denom_); + return time.rescaled_to(framerate); +} +#endif + rational rational::flipped() const { - return rational(denom, numer); + rational r = *this; + r.flip(); + return r; +} + +void rational::flip() +{ + if (!isNull()) { + std::swap(denom_, numer_); + } } bool rational::isNull() const +{ + return numerator() == 0; +} + +bool rational::isNaN() const { return denominator() == 0; } const intType &rational::numerator() const { - return numer; + return numer_; } const intType &rational::denominator() const { - return denom; + return denom_; +} + +QString rational::toString() const +{ + return QStringLiteral("%1/%2").arg(QString::number(numer_), QString::number(denom_)); } //Assignment Operators const rational& rational::operator=(const rational &rhs) { - if(this != &rhs) - { - numer = rhs.numer; - denom = rhs.denom; - } + if (this != &rhs) { + numer_ = rhs.numer_; + denom_ = rhs.denom_; + } + return *this; } const rational& rational::operator+=(const rational &rhs) { - if(numer * denom == intType(0) && rhs.numer * rhs.denom == intType(0)) - { - numer = intType(0); - denom = intType(0); - } - else - if(numer * denom != intType(0) && rhs.numer * rhs.denom == intType(0)) - { - + if (!isNaN()) { + if (rhs.isNaN()) { + // Set to NaN + denom_ = 0; + fix_signs(); + } else if (!rhs.isNull()) { + if (isNull()) { + numer_ = rhs.numer_; + denom_ = rhs.denom_; + } else { + numer_ = (numer_ * rhs.denom_) + (rhs.numer_ * denom_); + denom_ = denom_ * rhs.denom_; + fix_signs(); + reduce(); } - else - if(numer * denom == intType(0) && rhs.numer * rhs.denom != intType(0)) - { - numer = rhs.numer; - denom = rhs.denom; - } - else - { - numer = (numer * rhs.denom) + (rhs.numer * denom); - denom = denom * rhs.denom; - fixSigns(); - reduce(); - } + } + } + return *this; } const rational& rational::operator-=(const rational &rhs) { - if(numer * denom == intType(0) && rhs.numer * rhs.denom == intType(0)) - { - numer = intType(0); - denom = intType(0); - } - else - if(numer * denom != intType(0) && rhs.numer * rhs.denom == intType(0)) - { - + if (!isNaN()) { + if (rhs.isNaN()) { + // Set to NaN + denom_ = 0; + fix_signs(); + } else if (!rhs.isNull()) { + if (isNull()) { + numer_ = -rhs.numer_; + denom_ = rhs.denom_; + } else { + numer_ = (numer_ * rhs.denom_) - (rhs.numer_ * denom_); + denom_ = denom_ * rhs.denom_; + fix_signs(); + reduce(); } - else - if(numer * denom == intType(0) && rhs.numer * rhs.denom != intType(0)) - { - numer = -(rhs.numer); - denom = rhs.denom; - } - else - { - numer = (numer * rhs.denom) - (rhs.numer * denom); - denom = denom * rhs.denom; - fixSigns(); - reduce(); - } + } + } + return *this; } const rational& rational::operator/=(const rational &rhs) { - numer = numer * rhs.denom; - denom = denom * rhs.numer; - fixSigns(); - reduce(); + if (!isNaN()) { + if (rhs.isNaN()) { + // Set to NaN + denom_ = 0; + fix_signs(); + } else { + numer_ = numer_ * rhs.denom_; + denom_ = denom_ * rhs.numer_; + fix_signs(); + reduce(); + } + } + return *this; } const rational& rational::operator*=(const rational &rhs) { - numer = numer * rhs.numer; - denom = denom * rhs.denom; - fixSigns(); - reduce(); + if (!isNaN()) { + if (rhs.isNaN()) { + denom_ = 0; + fix_signs(); + } else { + numer_ = numer_ * rhs.numer_; + denom_ = denom_ * rhs.denom_; + fix_signs(); + reduce(); + } + } + return *this; } @@ -201,9 +269,9 @@ const rational& rational::operator*=(const rational &rhs) rational rational::operator+(const rational &rhs) const { - rational answer(*this); - answer += rhs; - return answer; + rational answer(*this); + answer += rhs; + return answer; } rational rational::operator-(const rational &rhs) const @@ -231,139 +299,92 @@ rational rational::operator*(const rational &rhs) const bool rational::operator<(const rational &rhs) const { - if(numer * denom == intType(0) && rhs.numer * rhs.denom == intType(0)) + if (isNaN() || rhs.isNaN()) { return false; - else - if(numer * denom != intType(0) && rhs.numer * rhs.denom == intType(0)) - { - if(numer * denom < intType(0)) - return true; - else - return false; - } - else - if(numer * denom == intType(0) && rhs.numer * rhs.denom != intType(0)) - { - if(rhs.numer * rhs.denom < intType(0)) - return false; - else - return true; - } - else - return ((numer * rhs.denom) < (denom * rhs.numer)); + } + + if (isNull() && rhs.isNull()) { + return false; + } + + if (rhs == RATIONAL_MAX + || *this == RATIONAL_MIN) { + // We will always either be LESS THAN (true) or EQUAL (false) + return (*this != rhs); + } + + if (*this == RATIONAL_MAX + || rhs == RATIONAL_MIN) { + // We will always be GREATER THAN (false) or EQUAL (false) + return false; + } + + if (!isNull() && rhs.isNull()) { + return (numer_ * denom_ < intType(0)); + } + + if (isNull() && !rhs.isNull()) { + return !(rhs.numer_ * rhs.denom_ < intType(0)); + } + + return ((numer_ * rhs.denom_) < (denom_ * rhs.numer_)); } bool rational::operator<=(const rational &rhs) const { - if(numer * denom == intType(0) && rhs.numer * rhs.denom == intType(0)) + if (isNaN() || rhs.isNaN()) { + return false; + } + + if (isNull() && rhs.isNull()) { return true; - else - if(numer * denom != intType(0) && rhs.numer * rhs.denom == intType(0)) - { - if(numer * denom < intType(0)) - return true; - else - return false; - } - else - if(numer * denom == intType(0) && rhs.numer * rhs.denom != intType(0)) - { - if(rhs.numer * rhs.denom < intType(0)) - return false; - else - return true; - } - else - return ((numer * rhs.denom) <= (denom * rhs.numer)); + } + + if (rhs == RATIONAL_MAX + || *this == RATIONAL_MIN) { + // We will always either be LESS THAN (true) or EQUAL (true) + return true; + } + + if (*this == RATIONAL_MAX + || rhs == RATIONAL_MIN) { + // We will always be GREATER THAN (false) or EQUAL (true) + return rhs == *this; + } + + if (!isNull() && rhs.isNull()) { + return (numer_ * denom_ < intType(0)); + } + + if (isNull() && !rhs.isNull()) { + return !(rhs.numer_ * rhs.denom_ < intType(0)); + } + + return ((numer_ * rhs.denom_) <= (denom_ * rhs.numer_)); } bool rational::operator>(const rational &rhs) const { - if(numer * denom == intType(0) && rhs.numer * rhs.denom == intType(0)) - return false; - else - if(numer * denom != intType(0) && rhs.numer * rhs.denom == intType(0)) - { - if(numer * denom > intType(0)) - return true; - else - return false; - } - else - if(numer * denom == intType(0) && rhs.numer * rhs.denom != intType(0)) - { - if(rhs.numer * rhs.denom > intType(0)) - return false; - else - return true; - } - else - return ((numer * rhs.denom) > (denom * rhs.numer)); + return rhs < *this; } bool rational::operator>=(const rational &rhs) const { - if(numer * denom == intType(0) && rhs.numer * rhs.denom == intType(0)) - return true; - else - if(numer * denom != intType(0) && rhs.numer * rhs.denom == intType(0)) - { - if(numer * denom > intType(0)) - return true; - else - return false; - } - else - if(numer * denom == intType(0) && rhs.numer * rhs.denom != intType(0)) - { - if(rhs.numer * rhs.denom > intType(0)) - return false; - else - return true; - } - else - - return ((numer * rhs.denom) >= (denom * rhs.numer)); + return rhs <= *this; } bool rational::operator==(const rational &rhs) const { - return (numer == rhs.numer && denom == rhs.denom); + if (isNaN() || rhs.isNaN()) { + return false; + } + + return (numer_ == rhs.numer_ && denom_ == rhs.denom_); } bool rational::operator!=(const rational &rhs) const { - return (numer != rhs.numer) || (denom != rhs.denom); - -} - -//Unary operators - -const rational& rational::operator++() -{ - numer += denom; - return *this; -} - -rational rational::operator++(int) -{ - rational tmp = *this; - numer += denom; - return tmp; -} - -const rational& rational::operator--() -{ - numer -= denom; - return *this; - -} - -rational rational::operator--(int) -{ - rational tmp; - numer -= denom; - return tmp; + return !(*this == rhs); } const rational& rational::operator+() const @@ -373,56 +394,60 @@ const rational& rational::operator+() const rational rational::operator-() const { - return rational(numer, -denom); + return rational(numer_, -denom_); } bool rational::operator!() const { - return !numer; + return !numer_; } //IO -ostream& operator<<(ostream &out, const rational &value) +std::ostream& operator<<(std::ostream &out, const rational &value) { - out << value.numer; - if(value.denom != 1) - { - out << '/' << value.denom; - return out; - } + out << value.numer_; + + if (value.denom_ != 1) { + out << '/' << value.denom_; + return out; + } + return out; } -istream& operator>>(istream &in, rational &value) +std::istream& operator>>(std::istream &in, rational &value) { - in >> value.numer; - value.denom = 1; + in >> value.numer_; + value.denom_ = 1; char ch; in.get(ch); - if(!in.eof()) - { - if(ch == '/') - { - in >> value.denom; - value.fixSigns(); - value.reduce(); - } - else - in.putback(ch); + if(!in.eof()) { + if(ch == '/') { + in >> value.denom_; + value.fix_signs(); + value.reduce(); + } else { + in.putback(ch); } + } return in; } -QDebug operator<<(QDebug debug, const rational &r) -{ - debug.nospace() << r.numerator() << "/" << r.denominator(); - return debug.space(); -} - uint qHash(const rational &r, uint seed) { - return qHash(r.toDouble(), seed); + return ::qHash(r.toDouble(), seed); +} + +} + +QDebug operator<<(QDebug debug, const olive::rational &r) +{ + if (r.isNaN()) { + return debug.space() << "NaN"; + } else { + return debug.space() << r.toDouble(); + } } diff --git a/app/common/rational.h b/app/common/rational.h index 519b8fcd1..074f1b864 100644 --- a/app/common/rational.h +++ b/app/common/rational.h @@ -7,6 +7,10 @@ #define RATIONAL_H #include +#ifdef USE_OTIO +#include +#endif + #include #include @@ -14,7 +18,9 @@ extern "C" { #include } -using namespace std; +#include "common/define.h" + +namespace olive { typedef int64_t intType; /* @@ -27,33 +33,34 @@ class rational { public: //constructors - rational(const intType &numerator = 0) - :numer(numerator), denom(1) + rational(const intType &numerator = 0) : + numer_(numerator), + denom_(1) { - if(numer == 0) - denom = 0; } - rational(const intType &numerator, const intType &denominator) - :numer(numerator), denom(denominator) + rational(const intType &numerator, const intType &denominator) : + numer_(numerator), + denom_(denominator) { - if(denom != intType(0)) - { - if(numer != intType(0)) - { - fixSigns(); - reduce(); - } - else - denom = intType(0); - } - else - numer = intType(0); + fix_signs(); + reduce(); } rational(const rational &rhs) = default; - rational(const AVRational& r); + rational(const AVRational& r) : + numer_(r.num), + denom_(r.den) + { + fix_signs(); + reduce(); + } + + static rational fromDouble(const double& flt, bool *ok = nullptr); + static rational fromString(const QString& str, bool* ok = nullptr); + + static const rational NaN; //Assignment Operators const rational& operator=(const rational &rhs); @@ -77,10 +84,6 @@ public: bool operator!=(const rational &rhs) const; //Unary operators - const rational& operator++(); //prefix - rational operator++(int); //postfix - const rational& operator--(); //prefix - rational operator--(int); //postfix const rational& operator+() const; rational operator-() const; bool operator!() const; @@ -90,42 +93,60 @@ public: AVRational toAVRational() const; +#ifdef USE_OTIO + static rational fromRationalTime(const opentime::RationalTime &t) + { + // Is this the best way to do this? + return fromDouble(t.to_seconds()); + } + + // Convert Olive ratioanls to opentime rationals with the given framerate (defaults to 24) + opentime::RationalTime toRationalTime(double framerate = 24) const; +#endif + // Produce "flipped" version rational flipped() const; + void flip(); - // Returns whether the rational is null or not + // Returns whether the rational is valid but equal to zero or not + // + // A NaN is always a null, but a null is not always a NaN bool isNull() const; - //Function: print number to cout - void print(ostream &out = cout) const; + // Returns whether this rational is not a valid number + bool isNaN() const; //IO - friend ostream& operator<<(ostream &out, const rational &value); - friend istream& operator>>(istream &in, rational &value); + friend std::ostream& operator<<(std::ostream &out, const rational &value); + friend std::istream& operator>>(std::istream &in, rational &value); const intType& numerator() const; const intType& denominator() const; + QString toString() const; + private: //numerator and denominator - intType numer; - intType denom; + intType numer_; + intType denom_; //Function: ensures denom >= 0 - void fixSigns(); + void fix_signs(); //Function: ensures lowest form void reduce(); //Function: finds greatest common denominator - intType gcd(intType &x, intType &y); + static intType gcd(const intType &x, const intType &y); }; -QDebug operator<<(QDebug debug, const rational& r); +#define RATIONAL_MIN rational(INT64_MIN, 1) +#define RATIONAL_MAX rational(INT64_MAX, 1) -#define RATIONAL_MIN rational(INT32_MIN, 1) -#define RATIONAL_MAX rational(INT32_MAX, 1) +uint qHash(const rational& r, uint seed = 0); -Q_DECLARE_METATYPE(rational) +} -uint qHash(const rational& r, uint seed); +QDebug operator<<(QDebug debug, const olive::rational& r); + +Q_DECLARE_METATYPE(olive::rational) #endif // RATIONAL_H diff --git a/app/common/threadedobject.cpp b/app/common/threadedobject.cpp index a180cfeba..dd34b66a9 100644 --- a/app/common/threadedobject.cpp +++ b/app/common/threadedobject.cpp @@ -1,5 +1,27 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + #include "threadedobject.h" +namespace olive { + void ThreadedObject::LockDeletes() { threadobj_delete_lock_++; @@ -31,3 +53,5 @@ bool ThreadedObject::TryLockMutex(int timeout) { return threadobj_main_lock_.tryLock(timeout); } + +} diff --git a/app/common/threadedobject.h b/app/common/threadedobject.h index d69577345..726877a11 100644 --- a/app/common/threadedobject.h +++ b/app/common/threadedobject.h @@ -1,8 +1,32 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + #ifndef THREADEDOBJECT_H #define THREADEDOBJECT_H #include +#include "common/define.h" + +namespace olive { + class ThreadedObject { public: @@ -21,4 +45,6 @@ private: QAtomicInt threadobj_delete_lock_; }; +} + #endif // THREADEDOBJECT_H diff --git a/app/common/threadsafemap.h b/app/common/threadsafemap.h new file mode 100644 index 000000000..0d4a6bc7e --- /dev/null +++ b/app/common/threadsafemap.h @@ -0,0 +1,27 @@ +#ifndef THREADSAFEMAP_H +#define THREADSAFEMAP_H + +#include +#include + +template +class ThreadSafeMap +{ +public: + ThreadSafeMap() = default; + + void insert(K key, V value) + { + mutex_.lock(); + map_.insert(key, value); + mutex_.unlock(); + } + +private: + QMutex mutex_; + + QMap map_; + +}; + +#endif // THREADSAFEMAP_H diff --git a/app/common/timecodefunctions.cpp b/app/common/timecodefunctions.cpp index 2783fd237..b80f8b237 100644 --- a/app/common/timecodefunctions.cpp +++ b/app/common/timecodefunctions.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -24,34 +24,25 @@ #include "config/config.h" -QString padded(int arg, int padding) { - return QString("%1").arg(arg, padding, 10, QChar('0')); +namespace olive { + +QString padded(int64_t arg, int padding) { + return QStringLiteral("%1").arg(arg, padding, 10, QChar('0')); } -QString olive::timestamp_to_timecode(const int64_t ×tamp, - const rational& timebase, - const TimecodeDisplay& display, - bool show_plus_if_positive) +QString Timecode::timestamp_to_timecode(const int64_t ×tamp, + const rational& timebase, + const Display& display, + bool show_plus_if_positive) { - double timestamp_dbl = (rational(timestamp) * timebase).toDouble(); + if (timebase.isNull()) { + return QStringLiteral("INVALID TIMEBASE"); + } - // Determine what symbol to separate frames (";" is used for drop frame, ":" is non-drop frame) - QString frame_token = ";"; + double timestamp_dbl = (rational(timestamp) * timebase).toDouble(); switch (display) { case kTimecodeNonDropFrame: - frame_token = ":"; - - // Convert timestamp from drop frame to non-drop frame - // FIXME: There's probably a better way to do this - if (timebase == rational(1001, 30000)) { - timestamp_dbl = timestamp_dbl / (30000.0/1001.0) * 30.0; - } else if (timebase == rational(1001, 60000)) { - timestamp_dbl = timestamp_dbl / (60000.0/1001.0) * 60.0; - } else if (timebase == rational(1001, 24000)) { - timestamp_dbl = timestamp_dbl / (24000.0/1001.0) * 24.0; - } - /* fall-through */ case kTimecodeDropFrame: case kTimecodeSeconds: { @@ -63,33 +54,72 @@ QString olive::timestamp_to_timecode(const int64_t ×tamp, prefix = "+"; } - timestamp_dbl = qAbs(timestamp_dbl); - - int total_seconds = qFloor(timestamp_dbl); - - int hours = total_seconds / 3600; - int mins = total_seconds / 60 - hours * 60; - int secs = total_seconds - mins * 60; - if (display == kTimecodeSeconds) { - int fraction = qRound((timestamp_dbl - total_seconds) * 1000); + timestamp_dbl = qAbs(timestamp_dbl); - return QString("%1%2:%3:%4.%5").arg(prefix, - padded(hours, 2), - padded(mins, 2), - padded(secs, 2), - padded(fraction, 3)); + int64_t total_seconds = qFloor(timestamp_dbl); + + int64_t hours = total_seconds / 3600; + int64_t mins = total_seconds / 60 - hours * 60; + int64_t secs = total_seconds - mins * 60; + int64_t fraction = qRound64((timestamp_dbl - static_cast(total_seconds)) * 1000); + + return QStringLiteral("%1%2:%3:%4.%5").arg(prefix, + padded(hours, 2), + padded(mins, 2), + padded(secs, 2), + padded(fraction, 3)); } else { - rational frame_rate = timebase.flipped(); + // Determine what symbol to separate frames (";" is used for drop frame, ":" is non-drop frame) + QString frame_token; + double frame_rate = timebase.flipped().toDouble(); + int rounded_frame_rate = qRound(frame_rate); + int64_t frames, secs, mins, hours; + int64_t f = qAbs(timestamp); - int frames = qRound((timestamp_dbl - total_seconds) * frame_rate.toDouble()); + if (display == kTimecodeDropFrame && TimebaseIsDropFrame(timebase)) { + frame_token = ";"; - return QString("%1%2:%3:%4%5%6").arg(prefix, - padded(hours, 2), - padded(mins, 2), - padded(secs, 2), - frame_token, - padded(frames, 2)); + /** + * CONVERT A FRAME NUMBER TO DROP FRAME TIMECODE + * + * Code by David Heidelberger, adapted from Andrew Duncan, further adapted for Olive by Olive Team + * Given an int called framenumber and a double called framerate + * Framerate should be 29.97, 59.94, or 23.976, otherwise the calculations will be off. + */ + + // If frame number is greater than 24 hrs, next operation will rollover clock + f %= (qRound(frame_rate*3600)*24); + + // Number of frames per ten minutes + int64_t framesPer10Minutes = qRound(frame_rate * 600); + int64_t d = f / framesPer10Minutes; + int64_t m = f % framesPer10Minutes; + + // Number of frames to drop on the minute marks is the nearest integer to 6% of the framerate + int64_t dropFrames = qRound(frame_rate * (2.0/30.0)); + + // Number of frames per minute is the round of the framerate * 60 minus the number of dropped frames + f += dropFrames*9*d; + if (m > dropFrames) { + f += dropFrames * ((m - dropFrames) / (qRound(frame_rate)*60 - dropFrames)); + } + } else { + frame_token = ":"; + } + + // non-drop timecode + hours = f / (3600*rounded_frame_rate); + mins = f / (60*rounded_frame_rate) % 60; + secs = f / rounded_frame_rate % 60; + frames = f % rounded_frame_rate; + + return QStringLiteral("%1%2:%3:%4%5%6").arg(prefix, + padded(hours, 2), + padded(mins, 2), + padded(secs, 2), + frame_token, + padded(frames, 2)); } } case kFrames: @@ -98,20 +128,194 @@ QString olive::timestamp_to_timecode(const int64_t ×tamp, return QString::number(qRound(timestamp_dbl * 1000)); } - return QString(); + return QStringLiteral("INVALID TIMECODE MODE"); } -rational olive::timestamp_to_time(const int64_t ×tamp, const rational &timebase) +int64_t Timecode::timecode_to_timestamp(const QString &timecode, const rational &timebase, const Display &display, bool* ok) +{ + double timebase_dbl = timebase.toDouble(); + + if (timecode.isEmpty()) { + goto err_fatal; + } + + switch (display) { + case kTimecodeNonDropFrame: + case kTimecodeDropFrame: + case kTimecodeSeconds: + { + const int kTimecodeElementCount = 4; + QStringList timecode_split = timecode.split(QRegExp("(:)|(;)|(\\.)")); + + bool valid; + + // We only deal with HH, MM, SS, and FF. Any values after that are ignored. + while (timecode_split.size() > kTimecodeElementCount) { + timecode_split.removeLast(); + } + + // Convert values to integers + QList timecode_numbers; + + bool negative = timecode.trimmed().startsWith('-'); + + foreach (const QString& element, timecode_split) { + valid = true; + + timecode_numbers.append((element.isEmpty()) ? 0 : qAbs(element.toLong(&valid))); + + // If element cannot be converted to a number, + if (!valid) { + goto err_fatal; + } + } + + // Ensure value size is always 4 + while (timecode_numbers.size() < 4) { + timecode_numbers.prepend(0); + } + + double frame_rate = timebase.flipped().toDouble(); + int rounded_frame_rate = qRound(frame_rate); + + int64_t hours = timecode_numbers.at(0); + int64_t mins = timecode_numbers.at(1); + int64_t secs = timecode_numbers.at(2); + int64_t frames = timecode_numbers.at(3); + + int64_t sec_count = (hours*3600 + mins*60 + secs); + int64_t timestamp = sec_count*rounded_frame_rate + frames; + + if (display == kTimecodeDropFrame && TimebaseIsDropFrame(timebase)) { + + // Number of frames to drop on the minute marks is the nearest integer to 6% of the framerate + int64_t dropFrames = qRound64(frame_rate * (2.0/30.0)); + + // d and m need to be calculated from + int64_t real_fr_ts = qRound64(static_cast(sec_count)*frame_rate) + frames; + + int64_t framesPer10Minutes = qRound(frame_rate * 600); + int64_t d = real_fr_ts / framesPer10Minutes; + int64_t m = real_fr_ts % framesPer10Minutes; + + if (m > dropFrames) { + timestamp -= dropFrames * ((m - dropFrames) / (qRound(frame_rate)*60 - dropFrames)); + } + timestamp -= dropFrames*9*d; + } + + if (ok) *ok = true; + + if (negative) timestamp = -timestamp; + + return timestamp; + } + case kMilliseconds: + { + bool valid; + double timecode_secs = timecode.toDouble(&valid); + + if (valid) { + // Convert milliseconds to seconds + timecode_secs *= 0.001; + + // Convert seconds to frames + timecode_secs /= timebase_dbl; + + if (ok) *ok = true; + return qRound(timecode_secs); + } else { + goto err_fatal; + } + } + case kFrames: + if (ok) *ok = true; + return timecode.toLong(ok); + } + +err_fatal: + if (ok) *ok = false; + return 0; +} + +rational Timecode::timecode_to_time(const QString &timecode, const rational &timebase, const Timecode::Display &display, bool *ok) +{ + int64_t timestamp = timecode_to_timestamp(timecode, timebase, display, ok); + return timestamp_to_time(timestamp, timebase); +} + +rational Timecode::snap_time_to_timebase(const rational &time, const rational &timebase, Rounding floor) +{ + // Just convert to a timestamp in timebase units and back + int64_t timestamp = time_to_timestamp(time, timebase, floor); + + return timestamp_to_time(timestamp, timebase); +} + +rational Timecode::timestamp_to_time(const int64_t ×tamp, const rational &timebase) { return rational(timestamp) * timebase; } -int64_t olive::time_to_timestamp(const rational &time, const rational &timebase) +QString Timecode::time_to_timecode(const rational &time, const rational &timebase, const Timecode::Display &display, bool show_plus_if_positive) { - return qRound64(time.toDouble() * timebase.flipped().toDouble()); + return timestamp_to_timecode(time_to_timestamp(time, timebase), timebase, display, show_plus_if_positive); } -olive::TimecodeDisplay olive::CurrentTimecodeDisplay() +bool Timecode::TimebaseIsDropFrame(const rational &timebase) { - return static_cast(Config::Current()["TimecodeDisplay"].toInt()); + return (timebase.numerator() != 1); +} + +QString Timecode::TimeToString(int64_t ms) +{ + int64_t total_seconds = ms / 1000; + int64_t ss = total_seconds % 60; + int64_t mm = (total_seconds / 60) % 60; + int64_t hh = total_seconds / 3600; + + return QStringLiteral("%1:%2:%3") + .arg(hh, 2, 10, QChar('0')) + .arg(mm, 2, 10, QChar('0')) + .arg(ss, 2, 10, QChar('0')); +} + +int64_t Timecode::time_to_timestamp(const rational &time, const rational &timebase, Rounding floor) +{ + return time_to_timestamp(time.toDouble(), timebase, floor); +} + +int64_t Timecode::time_to_timestamp(const double &time, const rational &timebase, Rounding floor) +{ + double d = time * timebase.flipped().toDouble(); + + switch (floor) { + case kRound: + default: + return qRound64(d); + case kFloor: + return qFloor(d); + case kCeil: + return qCeil(d); + } +} + +int64_t Timecode::rescale_timestamp(const int64_t &ts, const rational &source, const rational &dest) +{ + if (source == dest) { + return ts; + } + + return qRound64(static_cast(ts) * source.toDouble() / dest.toDouble()); +} + +int64_t Timecode::rescale_timestamp_ceil(const int64_t &ts, const rational &source, const rational &dest) +{ + if (source == dest) { + return ts; + } + + return qCeil(static_cast(ts) * source.toDouble() / dest.toDouble()); +} + } diff --git a/app/common/timecodefunctions.h b/app/common/timecodefunctions.h index 3bd06a72e..7252f44db 100644 --- a/app/common/timecodefunctions.h +++ b/app/common/timecodefunctions.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -27,24 +27,57 @@ namespace olive { -enum TimecodeDisplay { - kTimecodeDropFrame, - kTimecodeNonDropFrame, - kTimecodeSeconds, - kFrames, - kMilliseconds -}; - -TimecodeDisplay CurrentTimecodeDisplay(); - /** - * @brief Convert a timestamp (according to a rational timebase) to a user-friendly string representation + * @brief Functions for converting times/timecodes/timestamps + * + * Olive uses the following terminology through its code: + * + * `time` - time in seconds presented in a rational form + * `timebase` - the base time unit of an audio/video stream in seconds + * `timestamp` - an integer representation of a time in timebase units (in many cases is used like a frame number) + * `timecode` a user-friendly string representation of a time according to Timecode::Display */ -QString timestamp_to_timecode(const int64_t ×tamp, const rational& timebase, const TimecodeDisplay& display, bool show_plus_if_positive = false); +class Timecode { +public: + enum Display { + kTimecodeDropFrame, + kTimecodeNonDropFrame, + kTimecodeSeconds, + kFrames, + kMilliseconds + }; -int64_t time_to_timestamp(const rational& time, const rational& timebase); + enum Rounding { + kCeil, + kFloor, + kRound + }; -rational timestamp_to_time(const int64_t& timestamp, const rational& timebase); + /** + * @brief Convert a timestamp (according to a rational timebase) to a user-friendly string representation + */ + static QString timestamp_to_timecode(const int64_t ×tamp, const rational& timebase, const Display &display, bool show_plus_if_positive = false); + + static int64_t timecode_to_timestamp(const QString& timecode, const rational& timebase, const Display& display, bool *ok = nullptr); + static rational timecode_to_time(const QString& timecode, const rational& timebase, const Display& display, bool *ok = nullptr); + + static rational snap_time_to_timebase(const rational& time, const rational& timebase, Rounding floor = kRound); + + static int64_t time_to_timestamp(const rational& time, const rational& timebase, Rounding floor = kRound); + static int64_t time_to_timestamp(const double& time, const rational& timebase, Rounding floor = kRound); + + static int64_t rescale_timestamp(const int64_t& ts, const rational& source, const rational& dest); + static int64_t rescale_timestamp_ceil(const int64_t& ts, const rational& source, const rational& dest); + + static rational timestamp_to_time(const int64_t& timestamp, const rational& timebase); + + static QString time_to_timecode(const rational& time, const rational& timebase, const Display &display, bool show_plus_if_positive = false); + + static bool TimebaseIsDropFrame(const rational& timebase); + + static QString TimeToString(int64_t ms); + +}; } diff --git a/app/common/timelinecommon.h b/app/common/timelinecommon.h deleted file mode 100644 index 01cce97d3..000000000 --- a/app/common/timelinecommon.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef TIMELINECOMMON_H -#define TIMELINECOMMON_H - -namespace olive { - -namespace timeline { - -enum MovementMode { - kNone, - kMove, - kTrimIn, - kTrimOut -}; - -} - -} - - -#endif // TIMELINECOMMON_H diff --git a/app/common/timerange.cpp b/app/common/timerange.cpp index 78df8b438..c352e476d 100644 --- a/app/common/timerange.cpp +++ b/app/common/timerange.cpp @@ -1,7 +1,30 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + #include "timerange.h" +#include #include +namespace olive { + TimeRange::TimeRange(const rational &in, const rational &out) : in_(in), out_(out) @@ -48,19 +71,37 @@ bool TimeRange::operator==(const TimeRange &r) const return in() == r.in() && out() == r.out(); } -bool TimeRange::OverlapsWith(const TimeRange &a) const +bool TimeRange::operator!=(const TimeRange &r) const { - return Overlap(a, *this); + return in() != r.in() || out() != r.out(); } -TimeRange TimeRange::CombineWith(const TimeRange &a) const +bool TimeRange::OverlapsWith(const TimeRange &a, bool in_inclusive, bool out_inclusive) const +{ + bool doesnt_overlap_in = (in_inclusive) ? (a.out() < in()) : (a.out() <= in()); + + bool doesnt_overlap_out = (out_inclusive) ? (a.in() > out()) : (a.in() >= out()); + + return !doesnt_overlap_in && !doesnt_overlap_out; +} + +TimeRange TimeRange::Combined(const TimeRange &a) const { return Combine(a, *this); } -bool TimeRange::Overlap(const TimeRange &a, const TimeRange &b) +bool TimeRange::Contains(const TimeRange &compare, bool in_inclusive, bool out_inclusive) const { - return !(a.out() < b.in() || a.in() > b.out()); + bool contains_in = (in_inclusive) ? (compare.in() >= in()) : (compare.in() > in()); + + bool contains_out = (out_inclusive) ? (compare.out() <= out()) : (compare.out() < out()); + + return contains_in && contains_out; +} + +bool TimeRange::Contains(const rational &r) const +{ + return r >= in_ && r < out_; } TimeRange TimeRange::Combine(const TimeRange &a, const TimeRange &b) @@ -69,6 +110,60 @@ TimeRange TimeRange::Combine(const TimeRange &a, const TimeRange &b) qMax(a.out(), b.out())); } +TimeRange TimeRange::Intersected(const TimeRange &a) const +{ + return Intersect(a, *this); +} + +TimeRange TimeRange::Intersect(const TimeRange &a, const TimeRange &b) +{ + return TimeRange(qMax(a.in(), b.in()), + qMin(a.out(), b.out())); +} + +TimeRange TimeRange::operator+(const rational &rhs) const +{ + TimeRange answer(*this); + answer += rhs; + return answer; +} + +TimeRange TimeRange::operator-(const rational &rhs) const +{ + TimeRange answer(*this); + answer -= rhs; + return answer; +} + +const TimeRange &TimeRange::operator+=(const rational &rhs) +{ + set_range(in_ + rhs, out_ + rhs); + + return *this; +} + +const TimeRange &TimeRange::operator-=(const rational &rhs) +{ + set_range(in_ - rhs, out_ - rhs); + + return *this; +} + +std::list TimeRange::Split(const int &chunk_size) const +{ + std::list split_ranges; + + int start_time = qFloor(this->in().toDouble() / static_cast(chunk_size)) * chunk_size; + int end_time = qCeil(this->out().toDouble() / static_cast(chunk_size)) * chunk_size; + + for (int i=start_time; iin(), rational(i)), + qMin(this->out(), rational(i + chunk_size)))); + } + + return split_ranges; +} + void TimeRange::normalize() { // If `out` is earlier than `in`, swap them @@ -81,7 +176,208 @@ void TimeRange::normalize() length_ = out_ - in_; } +void TimeRangeList::insert(const TimeRangeList &list_to_add) +{ + for (auto it=list_to_add.cbegin(); it!=list_to_add.cend(); it++) { + insert(*it); + } +} + +void TimeRangeList::insert(TimeRange range_to_add) +{ + // See if list contains this range + if (contains(range_to_add)) { + return; + } + + // Does not contain range, so we'll almost certainly be adding it in some way + for (int i=0;i= range.out()) { + // No intersect + continue; + } else { + // Crop the time range to the range and add it to the list + TimeRange cropped(qMax(range.in(), compare.in()), + qMin(range.out(), compare.out())); + + intersect_list.insert(cropped); + } + } + + return intersect_list; +} + uint qHash(const TimeRange &r, uint seed) { return qHash(r.in(), seed) ^ qHash(r.out(), seed); } + +TimeRangeListFrameIterator::TimeRangeListFrameIterator() : + TimeRangeListFrameIterator(TimeRangeList(), rational::NaN) +{ +} + +TimeRangeListFrameIterator::TimeRangeListFrameIterator(const TimeRangeList &list, const rational &timebase) : + list_(list), + timebase_(timebase), + range_index_(-1), + size_(-1), + frame_index_(0), + custom_range_(false) +{ + if (!list_.isEmpty() && timebase_.isNull()) { + qCritical() << "TimeRangeListFrameIterator created with null timebase but non-empty list, this will likely lead to infinite loops"; + } + + UpdateIndexIfNecessary(); +} + +bool TimeRangeListFrameIterator::GetNext(rational *out) +{ + if (!HasNext()) { + return false; + } + + // Output current value + *out = current_; + + // Determine next value by adding timebase + current_ += timebase_; + + // If this time is outside the current range, jump to the next one + UpdateIndexIfNecessary(); + + // Increment frame index + frame_index_++; + + return true; +} + +bool TimeRangeListFrameIterator::HasNext() const +{ + return range_index_ < list_.size(); +} + +int TimeRangeListFrameIterator::size() +{ + if (size_ == -1) { + // Size isn't calculated automatically for optimization, so we'll calculate it now + size_ = 0; + + foreach (const TimeRange &range, list_) { + rational start = Timecode::snap_time_to_timebase(range.in(), timebase_, Timecode::kCeil); + rational end = Timecode::snap_time_to_timebase(range.out(), timebase_, Timecode::kFloor); + + if (end == range.out()) { + end -= timebase_; + } + + int64_t start_ts = Timecode::time_to_timestamp(start, timebase_); + int64_t end_ts = Timecode::time_to_timestamp(end, timebase_); + + size_ += 1 + (end_ts - start_ts); + } + } + + return size_; +} + +void TimeRangeListFrameIterator::UpdateIndexIfNecessary() +{ + while (range_index_ < list_.size() && (range_index_ == -1 || current_ >= list_.at(range_index_).out())) { + range_index_++; + + if (range_index_ < list_.size()) { + current_ = Timecode::snap_time_to_timebase(list_.at(range_index_).in(), timebase_, Timecode::kCeil); + } + } +} + +} + +QDebug operator<<(QDebug debug, const olive::TimeRange &r) +{ + debug.nospace() << r.in().toDouble() << " - " << r.out().toDouble(); + return debug.space(); +} + +QDebug operator<<(QDebug debug, const olive::TimeRangeList &r) +{ + debug << r.internal_array(); + return debug.space(); +} diff --git a/app/common/timerange.h b/app/common/timerange.h index fa239baed..abb004853 100644 --- a/app/common/timerange.h +++ b/app/common/timerange.h @@ -1,7 +1,30 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + #ifndef TIMERANGE_H #define TIMERANGE_H #include "rational.h" +#include "timecodefunctions.h" + +namespace olive { class TimeRange { public: @@ -17,12 +40,24 @@ public: void set_range(const rational& in, const rational& out); bool operator==(const TimeRange& r) const; + bool operator!=(const TimeRange& r) const; - bool OverlapsWith(const TimeRange& a) const; - TimeRange CombineWith(const TimeRange& a) const; + bool OverlapsWith(const TimeRange& a, bool in_inclusive = true, bool out_inclusive = true) const; + bool Contains(const TimeRange& a, bool in_inclusive = true, bool out_inclusive = true) const; + bool Contains(const rational& r) const; - static bool Overlap(const TimeRange& a, const TimeRange& b); + TimeRange Combined(const TimeRange& a) const; static TimeRange Combine(const TimeRange &a, const TimeRange &b); + TimeRange Intersected(const TimeRange& a) const; + static TimeRange Intersect(const TimeRange &a, const TimeRange &b); + + TimeRange operator+(const rational& rhs) const; + TimeRange operator-(const rational& rhs) const; + + const TimeRange& operator+=(const rational &rhs); + const TimeRange& operator-=(const rational &rhs); + + std::list Split(const int &chunk_size) const; private: void normalize(); @@ -30,8 +65,210 @@ private: rational in_; rational out_; rational length_; + }; -uint qHash(const TimeRange& r, uint seed); +class TimeRangeList { +public: + TimeRangeList() = default; + + TimeRangeList(std::initializer_list r) : + array_(r) + { + } + + void insert(const TimeRangeList &list_to_add); + void insert(TimeRange range_to_add); + + void remove(const TimeRange& remove); + void remove(const TimeRangeList &list); + + template + static void util_remove(QVector *list, const TimeRange &remove) + { + int sz = list->size(); + + for (int i=0;iremoveAt(i); + i--; + sz--; + } else if (compare.Contains(remove, false, false)) { + // The remove range is within this element, only choice is to split the element into two + T new_range = compare; + new_range.set_in(remove.out()); + compare.set_out(remove.in()); + list->append(new_range); + break; + } else if (compare.in() < remove.in() && compare.out() > remove.in()) { + // This element's out point overlaps the range's in, we'll trim it + compare.set_out(remove.in()); + } else if (compare.in() < remove.out() && compare.out() > remove.out()) { + // This element's in point overlaps the range's out, we'll trim it + compare.set_in(remove.out()); + } + } + } + + bool contains(const TimeRange& range, bool in_inclusive = true, bool out_inclusive = true) const; + + bool isEmpty() const + { + return array_.isEmpty(); + } + + void clear() + { + array_.clear(); + } + + int size() const + { + return array_.size(); + } + + void shift(const rational& diff); + + void trim_in(const rational& diff); + + void trim_out(const rational& diff); + + TimeRangeList Intersects(const TimeRange& range) const; + + using const_iterator = QVector::const_iterator; + + const_iterator begin() const + { + return array_.constBegin(); + } + + const_iterator end() const + { + return array_.constEnd(); + } + + const_iterator cbegin() const + { + return begin(); + } + + const_iterator cend() const + { + return end(); + } + + const TimeRange& first() const + { + return array_.first(); + } + + const TimeRange& last() const + { + return array_.last(); + } + + const TimeRange& at(int index) const + { + return array_.at(index); + } + + const QVector& internal_array() const + { + return array_; + } + + bool operator==(const TimeRangeList &rhs) const + { + return array_ == rhs.array_; + } + +private: + QVector array_; + +}; + +class TimeRangeListFrameIterator +{ +public: + TimeRangeListFrameIterator(); + TimeRangeListFrameIterator(const TimeRangeList &list, const rational &timebase); + + bool GetNext(rational *out); + + bool HasNext() const; + + QVector ToVector() const + { + TimeRangeListFrameIterator copy(list_, timebase_); + QVector times; + rational r; + while (copy.GetNext(&r)) { + times.append(r); + } + return times; + } + + int size(); + + void reset() + { + *this = TimeRangeListFrameIterator(); + } + + void insert(const TimeRange &range) + { + list_.insert(range); + } + + void insert(const TimeRangeList &list) + { + list_.insert(list); + } + + bool IsCustomRange() const + { + return custom_range_; + } + + void SetCustomRange(bool e) + { + custom_range_ = e; + } + + int frame_index() const + { + return frame_index_; + } + +private: + void UpdateIndexIfNecessary(); + + TimeRangeList list_; + + rational timebase_; + + rational current_; + + int range_index_; + + int size_; + + int frame_index_; + + bool custom_range_; + +}; + +uint qHash(const TimeRange& r, uint seed = 0); + +} + +QDebug operator<<(QDebug debug, const olive::TimeRange& r); +QDebug operator<<(QDebug debug, const olive::TimeRangeList& r); + +Q_DECLARE_METATYPE(olive::TimeRange) #endif // TIMERANGE_H diff --git a/app/common/tohex.h b/app/common/tohex.h new file mode 100644 index 000000000..600baf54d --- /dev/null +++ b/app/common/tohex.h @@ -0,0 +1,17 @@ +#ifndef TOHEX_H +#define TOHEX_H + +#include +#include + +#include "common/define.h" + +namespace olive { + +inline QString ToHex(quint64 t) { + return QStringLiteral("%1").arg(t, 0, 16); +} + +} + +#endif // TOHEX_H diff --git a/app/common/xmlutils.cpp b/app/common/xmlutils.cpp new file mode 100644 index 000000000..27057ffd9 --- /dev/null +++ b/app/common/xmlutils.cpp @@ -0,0 +1,46 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "xmlutils.h" + +#include "node/block/block.h" +#include "node/factory.h" +#include "widget/nodeparamview/nodeparamviewundo.h" +#include "widget/nodeview/nodeviewundo.h" + +namespace olive { + +bool XMLReadNextStartElement(QXmlStreamReader *reader) +{ + QXmlStreamReader::TokenType token; + + while ((token = reader->readNext()) != QXmlStreamReader::Invalid + && token != QXmlStreamReader::EndDocument) { + if (reader->isEndElement()) { + return false; + } else if (reader->isStartElement()) { + return true; + } + } + + return false; +} + +} diff --git a/app/common/xmlutils.h b/app/common/xmlutils.h new file mode 100644 index 000000000..5a4735ea5 --- /dev/null +++ b/app/common/xmlutils.h @@ -0,0 +1,52 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef XMLREADLOOP_H +#define XMLREADLOOP_H + +#include + +#include "node/param.h" +#include "undo/undocommand.h" + +namespace olive { + +class Block; +class Node; +class NodeInput; +class NodeGroup; + +#define XMLAttributeLoop(reader, item) \ + foreach (const QXmlStreamAttribute& item, reader->attributes()) + +/** + * @brief Workaround for QXmlStreamReader::readNextStartElement not detecting the end of a document + * + * Since Qt's default function doesn't exit at the end of the document, it ends up consistently + * throwing a "premature end of document" error. We have our own function here that does essentially + * the same thing but fixes that issue. + * + * See also: https://stackoverflow.com/questions/46346450/qt-qxmlstreamreader-always-returns-premature-end-of-document-error + */ +bool XMLReadNextStartElement(QXmlStreamReader* reader); + +} + +#endif // XMLREADLOOP_H diff --git a/app/config/CMakeLists.txt b/app/config/CMakeLists.txt index 755e1d3dd..2674ced82 100644 --- a/app/config/CMakeLists.txt +++ b/app/config/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2019 Olive Team +# Copyright (C) 2021 Olive Team # # 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 diff --git a/app/config/config.cpp b/app/config/config.cpp index 443a4e752..2f88ea1b9 100644 --- a/app/config/config.cpp +++ b/app/config/config.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -24,10 +24,18 @@ #include #include #include +#include #include -#include "core.h" +#include "common/autoscroll.h" #include "common/filefunctions.h" +#include "common/xmlutils.h" +#include "core.h" +#include "ui/colorcoding.h" +#include "ui/style/style.h" +#include "window/mainwindow/mainwindow.h" + +namespace olive { Config Config::current_config_; @@ -36,9 +44,14 @@ Config::Config() SetDefaults(); } +void Config::SetEntryInternal(const QString &key, NodeValue::Type type, const QVariant &data) +{ + config_map_[key] = {type, data}; +} + QString Config::GetConfigFilePath() { - return QDir(GetConfigurationLocation()).filePath("config.xml"); + return QDir(FileFunctions::GetConfigurationLocation()).filePath(QStringLiteral("config.xml")); } Config &Config::Current() @@ -49,11 +62,79 @@ Config &Config::Current() void Config::SetDefaults() { config_map_.clear(); - config_map_["TimecodeDisplay"] = olive::kTimecodeNonDropFrame; - config_map_["DefaultStillLength"] = QVariant::fromValue(rational(2)); - config_map_["HoverFocus"] = false; - config_map_["AudioScrubbing"] = true; - config_map_["AutorecoveryInterval"] = 1; + SetEntryInternal(QStringLiteral("Style"), NodeValue::kText, StyleManager::kDefaultStyle); + SetEntryInternal(QStringLiteral("TimecodeDisplay"), NodeValue::kInt, Timecode::kTimecodeDropFrame); + SetEntryInternal(QStringLiteral("DefaultStillLength"), NodeValue::kRational, QVariant::fromValue(rational(2))); + SetEntryInternal(QStringLiteral("HoverFocus"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("AudioScrubbing"), NodeValue::kBoolean, true); + SetEntryInternal(QStringLiteral("AutorecoveryEnabled"), NodeValue::kBoolean, true); + SetEntryInternal(QStringLiteral("AutorecoveryInterval"), NodeValue::kInt, 1); + SetEntryInternal(QStringLiteral("AutorecoveryMaximum"), NodeValue::kInt, 20); + SetEntryInternal(QStringLiteral("DiskCacheSaveInterval"), NodeValue::kInt, 10000); + SetEntryInternal(QStringLiteral("Language"), NodeValue::kText, QString()); + SetEntryInternal(QStringLiteral("ScrollZooms"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("EnableSeekToImport"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("EditToolAlsoSeeks"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("EditToolSelectsLinks"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("EnableDragFilesToTimeline"), NodeValue::kBoolean, true); + SetEntryInternal(QStringLiteral("InvertTimelineScrollAxes"), NodeValue::kBoolean, true); + SetEntryInternal(QStringLiteral("SelectAlsoSeeks"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("PasteSeeks"), NodeValue::kBoolean, true); + SetEntryInternal(QStringLiteral("SelectAlsoSeeks"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("SetNameWithMarker"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("AutoSeekToBeginning"), NodeValue::kBoolean, true); + SetEntryInternal(QStringLiteral("DropFileOnMediaToReplace"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("AddDefaultEffectsToClips"), NodeValue::kBoolean, true); + SetEntryInternal(QStringLiteral("AutoscaleByDefault"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("Autoscroll"), NodeValue::kInt, AutoScroll::kPage); + SetEntryInternal(QStringLiteral("AutoSelectDivider"), NodeValue::kBoolean, true); + SetEntryInternal(QStringLiteral("SetNameWithMarker"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("RectifiedWaveforms"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("DropWithoutSequenceBehavior"), NodeValue::kInt, ImportTool::kDWSAsk); + SetEntryInternal(QStringLiteral("Loop"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("SplitClipsCopyNodes"), NodeValue::kBoolean, true); + SetEntryInternal(QStringLiteral("UseGradients"), NodeValue::kBoolean, true); + SetEntryInternal(QStringLiteral("AutoMergeTracks"), NodeValue::kBoolean, true); + SetEntryInternal(QStringLiteral("UseSliderLadders"), NodeValue::kBoolean, true); + SetEntryInternal(QStringLiteral("ShowWelcomeDialog"), NodeValue::kBoolean, true); + SetEntryInternal(QStringLiteral("ShowClipWhileDragging"), NodeValue::kBoolean, true); + SetEntryInternal(QStringLiteral("StopPlaybackOnLastFrame"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("UseLegacyColorInInputTab"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("ReassocLinToNonLin"), NodeValue::kBoolean, false); + + SetEntryInternal(QStringLiteral("AutoCacheDelay"), NodeValue::kInt, 1000); + + SetEntryInternal(QStringLiteral("CatColor0"), NodeValue::kInt, ColorCoding::kRed); + SetEntryInternal(QStringLiteral("CatColor1"), NodeValue::kInt, ColorCoding::kMaroon); + SetEntryInternal(QStringLiteral("CatColor2"), NodeValue::kInt, ColorCoding::kOrange); + SetEntryInternal(QStringLiteral("CatColor3"), NodeValue::kInt, ColorCoding::kBrown); + SetEntryInternal(QStringLiteral("CatColor4"), NodeValue::kInt, ColorCoding::kYellow); + SetEntryInternal(QStringLiteral("CatColor5"), NodeValue::kInt, ColorCoding::kOlive); + SetEntryInternal(QStringLiteral("CatColor6"), NodeValue::kInt, ColorCoding::kLime); + SetEntryInternal(QStringLiteral("CatColor7"), NodeValue::kInt, ColorCoding::kGreen); + SetEntryInternal(QStringLiteral("CatColor8"), NodeValue::kInt, ColorCoding::kCyan); + SetEntryInternal(QStringLiteral("CatColor9"), NodeValue::kInt, ColorCoding::kTeal); + SetEntryInternal(QStringLiteral("CatColor10"), NodeValue::kInt, ColorCoding::kBlue); + SetEntryInternal(QStringLiteral("CatColor11"), NodeValue::kInt, ColorCoding::kNavy); + + SetEntryInternal(QStringLiteral("AudioOutput"), NodeValue::kText, QString()); + SetEntryInternal(QStringLiteral("AudioInput"), NodeValue::kText, QString()); + + SetEntryInternal(QStringLiteral("DiskCacheBehind"), NodeValue::kRational, QVariant::fromValue(rational(0))); + SetEntryInternal(QStringLiteral("DiskCacheAhead"), NodeValue::kRational, QVariant::fromValue(rational(60))); + + SetEntryInternal(QStringLiteral("DefaultSequenceWidth"), NodeValue::kInt, 1920); + SetEntryInternal(QStringLiteral("DefaultSequenceHeight"), NodeValue::kInt, 1080); + SetEntryInternal(QStringLiteral("DefaultSequencePixelAspect"), NodeValue::kRational, QVariant::fromValue(rational(1))); + SetEntryInternal(QStringLiteral("DefaultSequenceFrameRate"), NodeValue::kRational, QVariant::fromValue(rational(1001, 30000))); + SetEntryInternal(QStringLiteral("DefaultSequenceInterlacing"), NodeValue::kInt, VideoParams::kInterlaceNone); + SetEntryInternal(QStringLiteral("DefaultSequenceAutoCache"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("DefaultSequenceAudioFrequency"), NodeValue::kInt, 48000); + SetEntryInternal(QStringLiteral("DefaultSequenceAudioLayout"), NodeValue::kInt, QVariant::fromValue(static_cast(AV_CH_LAYOUT_STEREO))); + + // Online/offline settings + SetEntryInternal(QStringLiteral("OnlinePixelFormat"), NodeValue::kInt, VideoParams::kFormatFloat32); + SetEntryInternal(QStringLiteral("OfflinePixelFormat"), NodeValue::kInt, VideoParams::kFormatFloat16); } void Config::Load() @@ -65,8 +146,7 @@ void Config::Load() } if (!config_file.open(QFile::ReadOnly)) { - qWarning() << QCoreApplication::translate("Config", "Failed to load application settings. This session will use " - "defaults."); + qWarning() << "Failed to load application settings. This session will use defaults."; return; } @@ -74,34 +154,61 @@ void Config::Load() current_config_.SetDefaults(); QXmlStreamReader reader(&config_file); - while (!reader.atEnd()) { - reader.readNext(); - if (!reader.isStartElement()) { - continue; - } + QString config_version; - QString key = reader.name().toString(); + while (XMLReadNextStartElement(&reader)) { + if (reader.name() == QStringLiteral("Configuration")) { + while (XMLReadNextStartElement(&reader)) { + QString key = reader.name().toString(); + QString value = reader.readElementText(); - reader.readNext(); - QString value = reader.text().toString(); + if (key == QStringLiteral("Version")) { + config_version = value; - if (key == "Configuration") { - // First element, ignore - } else if (key == "Version") { - if (!value.contains(".")) { - qDebug() << "This is a 0.1.x config file, upconvert"; + if (!value.contains(".")) { + qDebug() << "CONFIG: This is a 0.1.x config file, upconvert"; + } + } else if (key == QStringLiteral("DefaultSequenceFrameRate") && !config_version.contains('.')) { + // 0.1.x stored this value as a float while we now use rationals, we'll use a heuristic to find the closest + // supported rational + qDebug() << " CONFIG: Finding closest match to" << value; + + double config_fr = value.toDouble(); + + const QVector& supported_frame_rates = VideoParams::kSupportedFrameRates; + + rational match = supported_frame_rates.first(); + double match_diff = qAbs(match.toDouble() - config_fr); + + for (int i=1;imain_window(), QCoreApplication::translate("Config", "Error loading settings"), QCoreApplication::translate("Config", "Failed to load application settings. This session will " - "use defaults."), + "use defaults.\n\n%1").arg(reader.errorString()), QMessageBox::Ok); current_config_.SetDefaults(); } @@ -111,18 +218,22 @@ void Config::Load() void Config::Save() { - QFile config_file(GetConfigFilePath()); + QString real_filename = GetConfigFilePath(); + QString temp_filename = FileFunctions::GetSafeTemporaryFilename(real_filename); + + QFile config_file(temp_filename); if (!config_file.open(QFile::WriteOnly)) { - QMessageBox::critical(olive::core.main_window(), + QMessageBox::critical(Core::instance()->main_window(), QCoreApplication::translate("Config", "Error saving settings"), QCoreApplication::translate("Config", "Failed to save application settings. The application " - "may lack write permissions to this location."), + "may lack write permissions for this location."), QMessageBox::Ok); return; } QXmlStreamWriter writer(&config_file); + writer.setAutoFormatting(true); writer.writeStartDocument(); @@ -131,10 +242,17 @@ void Config::Save() // Anything after the hyphen is considered "unimportant" information writer.writeTextElement("Version", QCoreApplication::applicationVersion().split('-').first()); - QMapIterator iterator(current_config_.config_map_); + QMapIterator iterator(current_config_.config_map_); while (iterator.hasNext()) { iterator.next(); - writer.writeTextElement(iterator.key(), iterator.value().toString()); + + QString value = NodeValue::ValueToString(iterator.value().type, iterator.value().data, false); + + if (iterator.value().type == NodeValue::kNone) { + qWarning() << "Config key" << iterator.key() << "had null type and was discarded"; + } else { + writer.writeTextElement(iterator.key(), value); + } } writer.writeEndElement(); // Configuration @@ -142,14 +260,26 @@ void Config::Save() writer.writeEndDocument(); config_file.close(); + + if (!FileFunctions::RenameFileAllowOverwrite(temp_filename, real_filename)) { + qWarning() << QStringLiteral("Failed to overwrite \"%1\". Config has been saved as \"%2\" instead.") + .arg(real_filename, temp_filename); + } } QVariant Config::operator[](const QString &key) const { - return config_map_[key]; + return config_map_[key].data; } QVariant &Config::operator[](const QString &key) { - return config_map_[key]; + return config_map_[key].data; +} + +NodeValue::Type Config::GetConfigEntryType(const QString &key) const +{ + return config_map_[key].type; +} + } diff --git a/app/config/config.h b/app/config/config.h index d7e99eadd..11f466cc3 100644 --- a/app/config/config.h +++ b/app/config/config.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -26,6 +26,9 @@ #include #include "common/timecodefunctions.h" +#include "node/value.h" + +namespace olive { class Config { public: @@ -41,14 +44,25 @@ public: QVariant& operator[](const QString&); + NodeValue::Type GetConfigEntryType(const QString& key) const; + private: Config(); - QMap config_map_; + struct ConfigEntry { + NodeValue::Type type; + QVariant data; + }; + + void SetEntryInternal(const QString& key, NodeValue::Type type, const QVariant& data); + + QMap config_map_; static Config current_config_; static QString GetConfigFilePath(); }; +} + #endif // CONFIG_H diff --git a/app/core.cpp b/app/core.cpp index ab47fd8d3..8696d12b3 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -21,111 +21,230 @@ #include "core.h" #include -#include +#include #include #include #include #include +#include #include #include +#ifdef Q_OS_WINDOWS +#include +#endif #include "audio/audiomanager.h" +#include "cli/clitask/clitaskdialog.h" +#include "codec/conformmanager.h" +#include "common/filefunctions.h" +#include "common/xmlutils.h" #include "config/config.h" #include "dialog/about/about.h" -#include "dialog/sequence/sequence.h" -#include "dialog/preferences/preferences.h" +#include "dialog/autorecovery/autorecoverydialog.h" +#include "dialog/export/export.h" +#include "dialog/footagerelink/footagerelinkdialog.h" +#ifdef USE_OTIO +#include "dialog/otioproperties/otiopropertiesdialog.h" +#endif #include "dialog/projectproperties/projectproperties.h" +#include "dialog/sequence/sequence.h" +#include "dialog/task/task.h" +#include "dialog/preferences/preferences.h" +#include "node/color/colormanager/colormanager.h" +#include "node/factory.h" +#include "node/project/serializer/serializer.h" #include "panel/panelmanager.h" #include "panel/project/project.h" -#include "project/item/footage/footage.h" -#include "project/item/sequence/sequence.h" -#include "render/colormanager.h" -#include "task/import/import.h" +#include "panel/viewer/viewer.h" +#include "render/diskmanager.h" +#include "render/framemanager.h" +#include "render/rendermanager.h" +#ifdef USE_OTIO +#include "task/project/loadotio/loadotio.h" +#include "task/project/saveotio/saveotio.h" +#endif +#include "task/project/import/import.h" +#include "task/project/import/importerrordialog.h" +#include "task/project/load/load.h" +#include "task/project/save/save.h" #include "task/taskmanager.h" #include "ui/style/style.h" #include "undo/undostack.h" #include "widget/menu/menushared.h" #include "widget/taskview/taskviewitem.h" +#include "widget/viewer/viewer.h" +#include "widget/nodeparamview/nodeparamviewundo.h" +#include "window/mainwindow/mainstatusbar.h" +#include "window/mainwindow/mainwindow.h" -Core olive::core; +namespace olive { -Core::Core() : +Core* Core::instance_ = nullptr; + +Core::Core(const CoreParams& params) : main_window_(nullptr), - tool_(olive::tool::kPointer), + tool_(Tool::kPointer), + addable_object_(Tool::kAddableEmpty), snapping_(true), - queue_autorecovery_(false) + core_params_(params), + pixel_sampling_users_(0) { + // Store reference to this object, making the assumption that Core will only ever be made in + // main(). This will obviously break if not. + instance_ = this; + + translator_ = new QTranslator(this); +} + +Core *Core::instance() +{ + return instance_; +} + +void Core::DeclareTypesForQt() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); } void Core::Start() { - // - // Parse command line arguments - // - - QCoreApplication* app = QCoreApplication::instance(); - - QCommandLineParser parser; - parser.addHelpOption(); - parser.addVersionOption(); - - // Project from command line option - // FIXME: What's the correct way to make a visually "optional" positional argument, or is manually adding square - // brackets like this correct? - parser.addPositionalArgument("[project]", tr("Project to open on startup")); - - // Create fullscreen option - QCommandLineOption fullscreen_option({"f", "fullscreen"}, tr("Start in full screen mode")); - parser.addOption(fullscreen_option); - - // Parse options - parser.process(*app); - - QStringList args = parser.positionalArguments(); - - // Detect project to load on startup - if (!args.isEmpty()) { - startup_project_ = args.first(); - } - - // Declare custom types for Qt signal/slot syste - DeclareTypesForQt(); - // Load application config Config::Load(); - // Set up color manager - ColorManager::CreateInstance(); + // Set locale based on either startup arg, config, or auto-detect + SetStartupLocale(); + // Declare custom types for Qt signal/slot system + DeclareTypesForQt(); + + // Set up node factory/library + NodeFactory::Initialize(); + + // Set up color manager's default config + ColorManager::SetUpDefaultConfig(); + + // Initialize task manager + TaskManager::CreateInstance(); + + // Initialize RenderManager + RenderManager::CreateInstance(); + + // Initialize FrameManager + FrameManager::CreateInstance(); + + // Initialize ConformManager + ConformManager::CreateInstance(); + + // Initialize project serializers + ProjectSerializer::Initialize(); // - // Start GUI (FIXME CLI mode) + // Start application // - StartGUI(parser.isSet(fullscreen_option)); + qInfo() << "Using Qt version:" << qVersion(); - // Create new project on startup - // FIXME: Load project from startup_project_ instead if not empty - AddOpenProject(std::make_shared()); + switch (core_params_.run_mode()) { + case CoreParams::kRunNormal: + // Start GUI + StartGUI(core_params_.fullscreen()); + + // If we have a startup + QMetaObject::invokeMethod(this, "OpenStartupProject", Qt::QueuedConnection); + break; + case CoreParams::kHeadlessExport: + qInfo() << "Headless export is not fully implemented yet"; + break; + case CoreParams::kHeadlessPreCache: + qInfo() << "Headless pre-cache is not fully implemented yet"; + break; + } + + // Manual crash triggering + if (core_params_.crash_on_startup()) { + const int interval = 5000; + qInfo() << "Manual crash was triggered. Application will crash in" << interval << "ms"; + QTimer *crash_timer = new QTimer(this); + crash_timer->setInterval(interval); + connect(crash_timer, &QTimer::timeout, this, []{ + // Try to read invalid memory to crash the application + int *invalid_ptr = nullptr; + qDebug() << *invalid_ptr; + }); + crash_timer->start(); + } } void Core::Stop() { + // Assume all projects have closed gracefully and no auto-recovery is necessary + autorecovered_projects_.clear(); + SaveUnrecoveredList(); + // Save Config - //Config::Save(); + Config::Save(); + + // Save recently opened projects + { + QFile recent_projects_file(GetRecentProjectsFilePath()); + if (recent_projects_file.open(QFile::WriteOnly | QFile::Text)) { + recent_projects_file.write(recent_projects_.join('\n').toUtf8()); + recent_projects_file.close(); + } + } + + ProjectSerializer::Destroy(); + + ConformManager::DestroyInstance(); + + FrameManager::DestroyInstance(); + + RenderManager::DestroyInstance(); + + MenuShared::DestroyInstance(); + + TaskManager::DestroyInstance(); + + PanelManager::DestroyInstance(); AudioManager::DestroyInstance(); - ColorManager::DestroyInstance(); + DiskManager::DestroyInstance(); + + NodeFactory::Destroy(); delete main_window_; + main_window_ = nullptr; } -olive::MainWindow *Core::main_window() +MainWindow *Core::main_window() { return main_window_; } +UndoStack *Core::undo_stack() +{ + return &undo_stack_; +} + void Core::ImportFiles(const QStringList &urls, ProjectViewModel* model, Folder* parent) { if (urls.isEmpty()) { @@ -133,40 +252,77 @@ void Core::ImportFiles(const QStringList &urls, ProjectViewModel* model, Folder* return; } - olive::task_manager.AddTask(std::make_shared(model, parent, urls)); + ProjectImportTask* pim = new ProjectImportTask(model, parent, urls); + + if (!pim->GetFileCount()) { + // No files to import + delete pim; + return; + } + + TaskDialog* task_dialog = new TaskDialog(pim, tr("Importing..."), main_window()); + + connect(task_dialog, &TaskDialog::TaskSucceeded, this, &Core::ImportTaskComplete); + + task_dialog->open(); } -const olive::tool::Tool &Core::tool() +const Tool::Item &Core::tool() const { return tool_; } -const bool &Core::snapping() +const Tool::AddableObject &Core::GetSelectedAddableObject() const +{ + return addable_object_; +} + +const QString &Core::GetSelectedTransition() const +{ + return selected_transition_; +} + +void Core::SetSelectedAddableObject(const Tool::AddableObject &obj) +{ + addable_object_ = obj; + emit AddableObjectChanged(addable_object_); +} + +void Core::SetSelectedTransitionObject(const QString &obj) +{ + selected_transition_ = obj; +} + +void Core::ClearOpenRecentList() +{ + recent_projects_.clear(); + emit OpenRecentListChanged(); +} + +void Core::CreateNewProject() +{ + // If we already have an empty/new project, switch to it + foreach (Project* already_open, open_projects_) { + if (already_open->is_new()) { + AddOpenProject(already_open); + return; + } + } + + AddOpenProject(new Project()); +} + +const bool &Core::snapping() const { return snapping_; } -void Core::StartModalTask(Task *t) +const QStringList &Core::GetRecentProjects() const { - QDialog dialog(main_window_); - - QHBoxLayout* layout = new QHBoxLayout(&dialog); - layout->setMargin(0); - - TaskViewItem* task_view = new TaskViewItem(&dialog); - task_view->SetTask(t); - layout->addWidget(task_view); - - connect(t, SIGNAL(Finished()), &dialog, SLOT(accept())); - - // FIXME: Risk of task finishing before dialog execs? - - if (t->Start()) { - dialog.exec(); - } + return recent_projects_; } -void Core::SetTool(const olive::tool::Tool &tool) +void Core::SetTool(const Tool::Item &tool) { tool_ = tool; @@ -182,7 +338,7 @@ void Core::SetSnapping(const bool &b) void Core::DialogAboutShow() { - AboutDialog a(main_window_); + AboutDialog a(false, main_window_); a.exec(); } @@ -196,7 +352,7 @@ void Core::DialogImportShow() if (!files.isEmpty()) { // Locate the most recently focused Project panel (assume that's the panel the user wants to import into) - ProjectPanel* active_project_panel = olive::panel_manager->MostRecentlyFocused(); + ProjectPanel* active_project_panel = PanelManager::instance()->MostRecentlyFocused(); Project* active_project; if (active_project_panel == nullptr // Check that we found a Project panel @@ -220,19 +376,47 @@ void Core::DialogPreferencesShow() void Core::DialogProjectPropertiesShow() { - ProjectPropertiesDialog ppd(main_window_); - ppd.exec(); + Project *proj = GetActiveProject(); + + if (proj) { + ProjectPropertiesDialog ppd(proj, main_window_); + ppd.exec(); + } else { + QMessageBox::critical(main_window_, + tr("No Active Project"), + tr("No project is currently open to set the properties for"), + QMessageBox::Ok); + } } +void Core::DialogExportShow() +{ + ViewerOutput* viewer = GetSequenceToExport(); + + if (viewer) { + ExportDialog* ed = new ExportDialog(viewer, main_window_); + connect(ed, &ExportDialog::finished, ed, &ExportDialog::deleteLater); + ed->open(); + } +} + +#ifdef USE_OTIO +bool Core::DialogImportOTIOShow(const QList& sequences) { + Project* active_project = GetActiveProject(); + OTIOPropertiesDialog opd(sequences, active_project); + return opd.exec() == QDialog::Accepted; +} +#endif + void Core::CreateNewFolder() { // Locate the most recently focused Project panel (assume that's the panel the user wants to import into) - ProjectPanel* active_project_panel = olive::panel_manager->MostRecentlyFocused(); + ProjectPanel* active_project_panel = PanelManager::instance()->MostRecentlyFocused(); Project* active_project; if (active_project_panel == nullptr // Check that we found a Project panel || (active_project = active_project_panel->project()) == nullptr) { // and that we could find an active Project - QMessageBox::critical(main_window_, tr("Failed to create new folder"), tr("Failed to find active Project panel")); + QMessageBox::critical(main_window_, tr("Failed to create new folder"), tr("Failed to find active project")); return; } @@ -240,152 +424,622 @@ void Core::CreateNewFolder() Folder* folder = active_project_panel->GetSelectedFolder(); // Create new folder - ItemPtr new_folder = std::make_shared(); + Folder* new_folder = new Folder(); // Set a default name - new_folder->set_name(tr("New Folder")); + new_folder->SetLabel(tr("New Folder")); // Create an undoable command - ProjectViewModel::AddItemCommand* aic = new ProjectViewModel::AddItemCommand(active_project_panel->model(), - folder, - new_folder); + MultiUndoCommand* command = new MultiUndoCommand(); - olive::undo_stack.push(aic); + command->add_child(new NodeAddCommand(active_project, new_folder)); + command->add_child(new FolderAddChild(folder, new_folder)); + + Core::instance()->undo_stack()->push(command); // Trigger an automatic rename so users can enter the folder name - active_project_panel->Edit(new_folder.get()); + active_project_panel->Edit(new_folder); } void Core::CreateNewSequence() { - // Locate the most recently focused Project panel (assume that's the panel the user wants to import into) - ProjectPanel* active_project_panel = olive::panel_manager->MostRecentlyFocused(); - Project* active_project; + Project* active_project = GetActiveProject(); - if (active_project_panel == nullptr // Check that we found a Project panel - || (active_project = active_project_panel->project()) == nullptr) { // and that we could find an active Project - QMessageBox::critical(main_window_, tr("Failed to create new sequence"), tr("Failed to find active Project panel")); + if (!active_project) { + QMessageBox::critical(main_window_, tr("Failed to create new sequence"), tr("Failed to find active project")); return; } - // Get the selected folder in this panel - Folder* folder = active_project_panel->GetSelectedFolder(); - // Create new sequence - SequencePtr new_sequence = std::make_shared(); + Sequence* new_sequence = CreateNewSequenceForProject(active_project); - // Set all defaults for the sequence - new_sequence->set_default_parameters(); - - // Get default name for this sequence (in the format "Sequence N", the first that doesn't exist) - int sequence_number = 1; - QString sequence_name; - do { - sequence_name = tr("Sequence %1").arg(sequence_number); - sequence_number++; - } while (active_project->root()->ChildExistsWithName(sequence_name)); - new_sequence->set_name(sequence_name); - - SequenceDialog sd(new_sequence.get(), SequenceDialog::kNew, main_window_); + SequenceDialog sd(new_sequence, SequenceDialog::kNew, main_window_); // Make sure SequenceDialog doesn't make an undo command for editing the sequence, since we make an undo command for // adding it later on sd.SetUndoable(false); if (sd.exec() == QDialog::Accepted) { + // Create an undoable command - ProjectViewModel::AddItemCommand* aic = new ProjectViewModel::AddItemCommand(active_project_panel->model(), - folder, - new_sequence); + MultiUndoCommand* command = new MultiUndoCommand(); - new_sequence->add_default_nodes(); + command->add_child(new NodeAddCommand(active_project, new_sequence)); + command->add_child(new FolderAddChild(GetSelectedFolderInActiveProject(), new_sequence)); + command->add_child(new NodeSetPositionCommand(new_sequence, new_sequence, Node::Position())); - olive::undo_stack.push(aic); + // Create and connect default nodes to new sequence + new_sequence->add_default_nodes(command); + + Core::instance()->undo_stack()->push(command); + + Core::instance()->main_window()->OpenSequence(new_sequence); + + } else { + + // If the dialog was accepted, ownership goes to the AddItemCommand. But if we get here, just delete + delete new_sequence; - Sequence::Open(new_sequence); } } -void Core::AddOpenProject(ProjectPtr p) +void Core::AddOpenProject(Project* p) { + // Ensure project is not open at the moment + foreach (Project* already_open, open_projects_) { + if (already_open == p) { + // Signal UI to switch to this project + emit ProjectOpened(p); + return; + } + } + + // If we currently have an empty project, close it first + if (!open_projects_.isEmpty() && open_projects_.last()->is_new()) { + CloseProject(open_projects_.last(), false); + } + + connect(p, &Project::ModifiedChanged, this, &Core::ProjectWasModified); open_projects_.append(p); - emit ProjectOpened(p.get()); + PushRecentlyOpenedProject(p->filename()); + + emit ProjectOpened(p); } -void Core::DeclareTypesForQt() +bool Core::AddOpenProjectFromTask(Task *task) { - qRegisterMetaType("Task::Status"); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); + ProjectLoadBaseTask* load_task = static_cast(task); + + if (!load_task->IsCancelled()) { + Project* project = load_task->GetLoadedProject(); + + if (ValidateFootageInLoadedProject(project, project->GetSavedURL())) { + AddOpenProject(project); + main_window_->LoadLayout(project->GetLayoutInfo()); + + return true; + } else { + delete project; + } + } + + return false; +} + +void Core::ImportTaskComplete(Task* task) +{ + ProjectImportTask* import_task = static_cast(task); + + MultiUndoCommand *command = import_task->GetCommand(); + + if (import_task->HasInvalidFiles()) { + ProjectImportErrorDialog d(import_task->GetInvalidFiles(), main_window_); + d.exec(); + } + + undo_stack_.pushIfHasChildren(command); +} + +bool Core::ConfirmImageSequence(const QString& filename) +{ + QMessageBox mb(main_window_); + + mb.setIcon(QMessageBox::Question); + mb.setWindowTitle(tr("Possible image sequence detected")); + mb.setText(tr("The file '%1' looks like it might be part of an image " + "sequence. Would you like to import it as such?").arg(filename)); + + mb.addButton(QMessageBox::Yes); + mb.addButton(QMessageBox::No); + + return (mb.exec() == QMessageBox::Yes); +} + +void Core::ProjectWasModified(bool e) +{ + //Project* p = static_cast(sender()); + + if (e) { + // If this project is modified, we know for sure the window should show a "modified" flag (the * in the titlebar) + main_window_->setWindowModified(true); + } else { + // If we just set this project to "not modified", see if all projects are not modified in which case we can hide + // the modified flag + foreach (Project* open, open_projects_) { + if (open->is_modified()) { + main_window_->setWindowModified(true); + return; + } + } + + main_window_->setWindowModified(false); + } +} + +bool Core::StartHeadlessExport() +{ + const QString& startup_project = core_params_.startup_project(); + + if (startup_project.isEmpty()) { + qCritical().noquote() << tr("You must specify a project file to export"); + return false; + } + + if (!QFileInfo::exists(startup_project)) { + qCritical().noquote() << tr("Specified project does not exist"); + return false; + } + + // Start a load task and try running it + ProjectLoadTask plm(startup_project); + CLITaskDialog task_dialog(&plm); + + /* + if (task_dialog.Run()) { + std::unique_ptr p = std::unique_ptr(plm.GetLoadedProject()); + QVector items = p->get_items_of_type(Item::kSequence); + + // Check if this project contains sequences + if (items.isEmpty()) { + qCritical().noquote() << tr("Project contains no sequences, nothing to export"); + return false; + } + + Sequence* sequence = nullptr; + + // Check if this project contains multiple sequences + if (items.size() > 1) { + qInfo().noquote() << tr("This project has multiple sequences. Which do you wish to export?"); + for (int i=0;iGetLabel().toStdString(); + } + + QTextStream stream(stdin); + QString sequence_read; + int sequence_index = -1; + QString quit_code = QStringLiteral("q"); + std::string prompt = tr("Enter number (or %1 to cancel): ").arg(quit_code).toStdString(); + forever { + std::cout << prompt; + + stream.readLineInto(&sequence_read); + + if (!QString::compare(sequence_read, quit_code, Qt::CaseInsensitive)) { + return false; + } + + bool ok; + sequence_index = sequence_read.toInt(&ok); + + if (ok && sequence_index >= 0 && sequence_index < items.size()) { + break; + } else { + qCritical().noquote() << tr("Invalid sequence number"); + } + } + + sequence = static_cast(items.at(sequence_index)); + } else { + sequence = static_cast(items.first()); + } + + ExportParams params; + ExportTask export_task(sequence->viewer_output(), p->color_manager(), params); + CLITaskDialog export_dialog(&export_task); + if (export_dialog.Run()) { + qInfo().noquote() << tr("Export succeeded"); + return true; + } else { + qInfo().noquote() << tr("Export failed: %1").arg(export_task.GetError()); + return false; + } + } else { + qCritical().noquote() << tr("Project failed to load: %1").arg(plm.GetError()); + return false; + } + */ + + + + return false; +} + +void Core::OpenStartupProject() +{ + const QString& startup_project = core_params_.startup_project(); + bool startup_project_exists = !startup_project.isEmpty() && QFileInfo::exists(startup_project); + + // Load startup project + if (!startup_project_exists && !startup_project.isEmpty()) { + QMessageBox::warning(main_window_, + tr("Failed to open startup file"), + tr("The project \"%1\" doesn't exist. " + "A new project will be started instead.").arg(startup_project), + QMessageBox::Ok); + } + + if (startup_project_exists) { + // If a startup project was set and exists, open it now + OpenProjectInternal(startup_project); + } else { + // If no load project is set, create a new one on open + CreateNewProject(); + } +} + +void Core::AddRecoveryProjectFromTask(Task *task) +{ + if (AddOpenProjectFromTask(task)) { + ProjectLoadBaseTask* load_task = static_cast(task); + + Project* project = load_task->GetLoadedProject(); + + // Clearing the filename will force the user to re-save it somewhere else + project->set_filename(QString()); + + // Forcing a UUID regeneration will prevent it from saving auto-recoveries in the same place + // the original project did + project->RegenerateUuid(); + + // Setting modified will ensure that the program doesn't close and lose the project without + // prompting the user first + project->set_modified(true); + } } void Core::StartGUI(bool full_screen) { // Set UI style - qApp->setStyle(QStyleFactory::create("Fusion")); - StyleManager::SetStyle(StyleManager::DefaultStyle()); + StyleManager::Init(); // Set up shared menus - olive::menu_shared.Initialize(); + MenuShared::CreateInstance(); // Since we're starting GUI mode, create a PanelFocusManager (auto-deletes with QObject) - olive::panel_manager = new PanelManager(this); + PanelManager::CreateInstance(); + + // Initialize audio service + AudioManager::CreateInstance(); + + // Initialize disk service + DiskManager::CreateInstance(); // Connect the PanelFocusManager to the application's focus change signal connect(qApp, - SIGNAL(focusChanged(QWidget*, QWidget*)), - olive::panel_manager, - SLOT(FocusChanged(QWidget*, QWidget*))); + &QApplication::focusChanged, + PanelManager::instance(), + &PanelManager::FocusChanged); // Create main window and open it - main_window_ = new olive::MainWindow(); + main_window_ = new MainWindow(); + if (full_screen) { main_window_->showFullScreen(); } else { main_window_->showMaximized(); } - // When a new project is opened, update the mainwindow - connect(this, SIGNAL(ProjectOpened(Project*)), main_window_, SLOT(ProjectOpen(Project*))); +#ifdef Q_OS_WINDOWS + // Workaround for Qt bug where menus don't appear in full screen mode + // See: https://doc.qt.io/qt-5/windows-issues.html + QWindowsWindowFunctions::setHasBorderInFullScreen(main_window_->windowHandle(), true); +#endif - // Initialize audio service - AudioManager::CreateInstance(); + // When a new project is opened, update the mainwindow + connect(this, &Core::ProjectOpened, main_window_, &MainWindow::ProjectOpen, Qt::QueuedConnection); + connect(this, &Core::ProjectClosed, main_window_, &MainWindow::ProjectClose); // Start autorecovery timer using the config value as its interval SetAutorecoveryInterval(Config::Current()["AutorecoveryInterval"].toInt()); + connect(&autorecovery_timer_, &QTimer::timeout, this, &Core::SaveAutorecovery); autorecovery_timer_.start(); + + // Load recently opened projects list + { + QFile recent_projects_file(GetRecentProjectsFilePath()); + if (recent_projects_file.open(QFile::ReadOnly | QFile::Text)) { + QString r = QString::fromUtf8(recent_projects_file.readAll()); + if (!r.isEmpty()) { + recent_projects_ = r.split('\n'); + } + recent_projects_file.close(); + } + + emit OpenRecentListChanged(); + } +} + +void Core::SaveProjectInternal(Project* project, const QString& override_filename) +{ + // Create save manager + Task* psm; + + // Put layout into project + project->SetLayoutInfo(main_window_->SaveLayout()); + + if (project->filename().endsWith(QStringLiteral(".otio"), Qt::CaseInsensitive)) { +#ifdef USE_OTIO + psm = new SaveOTIOTask(project); +#else + QMessageBox::critical(main_window_, + tr("Missing OpenTimelineIO Libraries"), + tr("This build was compiled without OpenTimelineIO and therefore " + "cannot open OpenTimelineIO files.")); + return; +#endif + } else { + psm = new ProjectSaveTask(project); + + if (!override_filename.isEmpty()) { + // Set override filename if provided + static_cast(psm)->SetOverrideFilename(override_filename); + } + } + + // We don't use a TaskDialog here because a model save dialog is annoying, particularly when + // saving auto-recoveries that the user can't anticipate. Doing this in the main thread will + // cause a brief (but often unnoticeable) pause in the GUI, which, while not ideal, is not that + // different from what already happened (modal dialog preventing use of the GUI) and in many ways + // less annoying (doesn't disrupt any current actions or pull focus from elsewhere). + // + // Ideally we could do this in a background thread and show progress in the status bar like + // Microsoft Word, but that would be far more complex. If it becomes necessary in the future, + // we will look into an approach like that. + if (psm->Start()) { + if (override_filename.isEmpty()) { + ProjectSaveSucceeded(psm); + } + } + + psm->deleteLater(); +} + +ViewerOutput* Core::GetSequenceToExport() +{ + // First try the most recently focused time based window + TimeBasedPanel* time_panel = PanelManager::instance()->MostRecentlyFocused(); + + // If that fails try defaulting to the first timeline (i.e. if a project has just been loaded). + if (!time_panel->GetConnectedViewer()) { + // Safe to assume there will always be one timeline. + time_panel = PanelManager::instance()->GetPanelsOfType().first(); + } + + if (time_panel && time_panel->GetConnectedViewer()) { + if (time_panel->GetConnectedViewer()->GetLength() == 0) { + QMessageBox::critical(main_window_, + tr("Error"), + tr("This Sequence is empty. There is nothing to export."), + QMessageBox::Ok); + } else { + return time_panel->GetConnectedViewer(); + } + } else { + QMessageBox::critical(main_window_, + tr("Error"), + tr("No valid sequence detected.\n\nMake sure a sequence is loaded and it has a connected Viewer node."), + QMessageBox::Ok); + } + + return nullptr; +} + +QString Core::GetAutoRecoveryIndexFilename() +{ + return QDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath(QStringLiteral("unrecovered")); +} + +void Core::SaveUnrecoveredList() +{ + QFile autorecovery_index(GetAutoRecoveryIndexFilename()); + + if (autorecovered_projects_.isEmpty()) { + // Recovery list is empty, delete file if exists + if (autorecovery_index.exists()) { + autorecovery_index.remove(); + } + } else if (autorecovery_index.open(QFile::WriteOnly)) { + // Overwrite recovery list with current list + QTextStream ts(&autorecovery_index); + + bool first = true; + foreach (const QUuid& uuid, autorecovered_projects_) { + if (first) { + first = false; + } else { + ts << QStringLiteral("\n"); + } + ts << uuid.toString(); + } + + autorecovery_index.close(); + } else { + qWarning() << "Failed to save unrecovered list"; + } +} + +bool Core::RevertProjectInternal(Project *p, bool by_opening_existing) +{ + if (p->filename().isEmpty()) { + QMessageBox::critical(main_window_, tr("Revert"), + tr("This project has not yet been saved, therefore there is no last saved state to revert to.")); + } else { + QString msg; + + if (by_opening_existing) { + msg = tr("The project \"%1\" is already open. By re-opening it, the project will revert to " + "its last saved state. Any unsaved changes will be lost. Do you wish to continue?").arg(p->filename()); + } else { + msg = tr("This will revert the project \"%1\" back to its last saved state. " + "All unsaved changes will be lost. Do you wish to continue?").arg(p->name()); + } + + if (QMessageBox::question(main_window_, tr("Revert"), msg, QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) { + // Copy filename because CloseProject is going to delete `p` + QString filename = p->filename(); + + // Close project without prompting to save it + CloseProjectBehavior b = kCloseProjectDontSave; + CloseProject(p, false, b); + + // NOTE: `p` will be deleted now, so don't try accessing it + + // Re-open project at the filename + OpenProjectInternal(filename); + + return true; + } + } + + return false; } void Core::SaveAutorecovery() { - if (queue_autorecovery_) { - // FIXME: Save autorecovery of projects open + if (Config::Current()[QStringLiteral("AutorecoveryEnabled")].toBool()) { + foreach (Project* p, open_projects_) { + if (!p->has_autorecovery_been_saved()) { + QDir project_autorecovery_dir(QDir(FileFunctions::GetAutoRecoveryRoot()).filePath(p->GetUuid().toString())); + if (project_autorecovery_dir.mkpath(QStringLiteral("."))) { + QString this_autorecovery_path = project_autorecovery_dir.filePath(QStringLiteral("%1.ove").arg(QString::number(QDateTime::currentSecsSinceEpoch()))); - queue_autorecovery_ = false; + SaveProjectInternal(p, this_autorecovery_path); + + p->set_autorecovery_saved(true); + + // Keep track of projects that where the "newest" save is the recovery project + if (!autorecovered_projects_.contains(p->GetUuid())) { + autorecovered_projects_.append(p->GetUuid()); + } + + qDebug() << "Saved auto-recovery to:" << this_autorecovery_path; + + // Write human-readable real name so it's not just a UUID + { + QFile realname_file(project_autorecovery_dir.filePath(QStringLiteral("realname.txt"))); + realname_file.open(QFile::WriteOnly); + realname_file.write(p->pretty_filename().toUtf8()); + realname_file.close(); + } + + int64_t max_recoveries_per_file = Config::Current()[QStringLiteral("AutorecoveryMaximum")].toLongLong(); + + // Since we write an extra file, increment total allowed files by 1 + max_recoveries_per_file++; + + // Delete old entries + QStringList recovery_files = project_autorecovery_dir.entryList(QDir::Files | QDir::NoDotAndDotDot, QDir::Name); + while (recovery_files.size() > max_recoveries_per_file) { + bool deleted = false; + for (int i=0; iMostRecentlyFocused(); + Project* p = static_cast(task)->GetProject(); - // If we couldn't find one, return nullptr - if (active_project_panel == nullptr) { + PushRecentlyOpenedProject(p->filename()); + + p->set_modified(false); + + autorecovered_projects_.removeOne(p->GetUuid()); + SaveUnrecoveredList(); +} + +Project* Core::GetActiveProject() const +{ + ProjectPanel* active_project_panel = PanelManager::instance()->MostRecentlyFocused(); + + if (active_project_panel && active_project_panel->project()) { + return active_project_panel->project(); + } + + return nullptr; +} + +ProjectViewModel *Core::GetActiveProjectModel() const +{ + ProjectPanel* active_project_panel = PanelManager::instance()->MostRecentlyFocused(); + + if (active_project_panel) { + return active_project_panel->model(); + } else { return nullptr; } - - // Otherwise, return the project panel's project (which may be nullptr but in most cases shouldn't be) - return active_project_panel->project(); } -void Core::SetProjectModified() +Folder *Core::GetSelectedFolderInActiveProject() const { - main_window()->setWindowModified(true); - queue_autorecovery_ = true; + ProjectPanel* active_project_panel = PanelManager::instance()->MostRecentlyFocused(); + + if (active_project_panel) { + return active_project_panel->GetSelectedFolder(); + } else { + return nullptr; + } +} + +Timecode::Display Core::GetTimecodeDisplay() const +{ + return static_cast(Config::Current()["TimecodeDisplay"].toInt()); +} + +void Core::SetTimecodeDisplay(Timecode::Display d) +{ + Config::Current()["TimecodeDisplay"] = d; + + emit TimecodeDisplayChanged(d); } void Core::SetAutorecoveryInterval(int minutes) @@ -393,3 +1047,662 @@ void Core::SetAutorecoveryInterval(int minutes) // Convert minutes to milliseconds autorecovery_timer_.setInterval(minutes * 60000); } + +void Core::CopyStringToClipboard(const QString &s) +{ + QGuiApplication::clipboard()->setText(s); +} + +QString Core::PasteStringFromClipboard() +{ + return QGuiApplication::clipboard()->text(); +} + +bool Core::SaveActiveProject() +{ + Project* active_project = GetActiveProject(); + + if (active_project) { + return SaveProject(active_project); + } + + return false; +} + +bool Core::SaveActiveProjectAs() +{ + Project* active_project = GetActiveProject(); + + if (active_project) { + return SaveProjectAs(active_project); + } + + return false; +} + +bool Core::SaveAllProjects() +{ + foreach (Project* p, open_projects_) { + if (!SaveProject(p)) { + return false; + } + } + + return true; +} + +void Core::RevertActiveProject() +{ + Project *p = GetActiveProject(); + + if (p) { + RevertProjectInternal(p, false); + } +} + +bool Core::CloseActiveProject() +{ + return CloseProject(GetActiveProject(), true); +} + +bool Core::CloseAllExceptActiveProject() +{ + Project* active_proj = GetActiveProject(); + QList copy = open_projects_; + + foreach (Project* p, copy) { + if (p != active_proj) { + if (!CloseProject(p, true)) { + return false; + } + } + } + + return true; +} + +QString Core::GetProjectFilter(bool include_any_filter) +{ + QString filters; + +#ifdef USE_OTIO + if (include_any_filter) { + filters.append(QStringLiteral("All Supported Projects (*.ove *.otio);;")); + } +#else + Q_UNUSED(include_any_filter) +#endif + + // Append standard filter + filters.append(QStringLiteral("%1 (*.ove)").arg(tr("Olive Project"))); + +#ifdef USE_OTIO + filters.append(QStringLiteral(";;%2 (*.otio)").arg(tr("OpenTimelineIO"))); +#endif + + return filters; +} + +QString Core::GetRecentProjectsFilePath() +{ + return QDir(FileFunctions::GetConfigurationLocation()).filePath(QStringLiteral("recent")); +} + +void Core::SetStartupLocale() +{ + // Set language + if (!core_params_.startup_language().isEmpty()) { + if (translator_->load(core_params_.startup_language()) && QApplication::installTranslator(translator_)) { + return; + } else { + qWarning() << "Failed to load translation file. Falling back to defaults."; + } + } + + QString use_locale = Config::Current()[QStringLiteral("Language")].toString(); + + if (use_locale.isEmpty()) { + // No configured locale, auto-detect the system's locale + use_locale = QLocale::system().name(); + } + + if (!SetLanguage(use_locale)) { + qWarning() << "Trying to use locale" << use_locale << "but couldn't find a translation for it"; + } +} + +bool Core::SaveProject(Project* p) +{ + if (p->filename().isEmpty()) { + return SaveProjectAs(p); + } else { + SaveProjectInternal(p); + + return true; + } +} + +void Core::ShowStatusBarMessage(const QString &s, int timeout) +{ + main_window_->statusBar()->showMessage(s, timeout); +} + +void Core::ClearStatusBarMessage() +{ + main_window_->statusBar()->clearMessage(); +} + +void Core::OpenRecoveryProject(const QString &filename) +{ + OpenProjectInternal(filename, true); +} + +void Core::OpenNodeInViewer(ViewerOutput *viewer) +{ + main_window_->OpenNodeInViewer(viewer); +} + +void Core::CheckForAutoRecoveries() +{ + QFile autorecovery_index(GetAutoRecoveryIndexFilename()); + if (autorecovery_index.exists()) { + // Uh-oh, we have auto-recoveries to prompt + if (autorecovery_index.open(QFile::ReadOnly)) { + QStringList recovery_filenames = QString::fromUtf8(autorecovery_index.readAll()).split('\n'); + + AutoRecoveryDialog ard(tr("The following projects had unsaved changes when Olive " + "forcefully quit. Would you like to load them?"), + recovery_filenames, true, main_window_); + ard.exec(); + + autorecovery_index.close(); + + // Delete recovery index since we don't need it anymore + QFile::remove(GetAutoRecoveryIndexFilename()); + } else { + QMessageBox::critical(main_window_, tr("Auto-Recovery Error"), + tr("Found auto-recoveries but failed to load the auto-recovery index. " + "Auto-recover projects will have to be opened manually.\n\n" + "Your recoverable projects are still available at: %1").arg(FileFunctions::GetAutoRecoveryRoot())); + } + } +} + +void Core::BrowseAutoRecoveries() +{ + // List all auto-recovery entries + AutoRecoveryDialog ard(tr("The following project versions have been auto-saved:"), + QDir(FileFunctions::GetAutoRecoveryRoot()).entryList(QDir::Dirs | QDir::NoDotAndDotDot), + false, main_window_); + ard.exec(); +} + +void Core::RequestPixelSamplingInViewers(bool e) +{ + if (e) { + if (pixel_sampling_users_ == 0) { + // Signal to start pixel sampling + emit ColorPickerEnabled(true); + } + + pixel_sampling_users_++; + } else { + pixel_sampling_users_--; + + if (pixel_sampling_users_ == 0) { + // Signal to end pixel sampling + emit ColorPickerEnabled(false); + } + } +} + +bool Core::SaveProjectAs(Project* p) +{ + QFileDialog fd(main_window_, tr("Save Project As")); + + fd.setAcceptMode(QFileDialog::AcceptSave); + fd.setNameFilter(GetProjectFilter(false)); + + if (fd.exec() == QDialog::Accepted) { + QString fn = fd.selectedFiles().first(); + + // Somewhat hacky method of extracting the extension from the name filter + const QString& name_filter = fd.selectedNameFilter(); + int ext_index = name_filter.indexOf(QStringLiteral("(*.")) + 3; + QString extension = name_filter.mid(ext_index, name_filter.size() - ext_index - 1); + + fn = FileFunctions::EnsureFilenameExtension(fn, extension); + + p->set_filename(fn); + + SaveProjectInternal(p); + + return true; + } + + return false; +} + +void Core::PushRecentlyOpenedProject(const QString& s) +{ + if (s.isEmpty()) { + return; + } + + int existing_index = recent_projects_.indexOf(s); + + if (existing_index >= 0) { + recent_projects_.move(existing_index, 0); + } else { + recent_projects_.prepend(s); + } + + emit OpenRecentListChanged(); +} + +void Core::OpenProjectInternal(const QString &filename, bool recovery_project) +{ + // See if this project is open already + foreach (Project* p, open_projects_) { + // Comparing QFileInfos will handle case insensitivity and both slash directions on platforms + // where this is necessary (not naming any names *cough* Windows) + if (QFileInfo(p->filename()) == QFileInfo(filename)) { + // This project is already open + bool reverted = RevertProjectInternal(p, true); + + if (!reverted) { + // Calling this will focus attention to the project that the user just tried to re-open + AddOpenProject(p); + } + + // Don't do anything else + return; + } + } + + Task* load_task; + + if (filename.endsWith(QStringLiteral(".otio"), Qt::CaseInsensitive)) { + // Load OpenTimelineIO project +#ifdef USE_OTIO + load_task = new LoadOTIOTask(filename); +#else + QMessageBox::critical(main_window_, + tr("Missing OpenTimelineIO Libraries"), + tr("This build was compiled without OpenTimelineIO and therefore " + "cannot open OpenTimelineIO files.")); + return; +#endif + } else { + // Fallback to regular OVE project + load_task = new ProjectLoadTask(filename); + } + + TaskDialog* task_dialog = new TaskDialog(load_task, tr("Load Project"), main_window()); + + if (recovery_project) { + connect(task_dialog, &TaskDialog::TaskSucceeded, this, &Core::AddRecoveryProjectFromTask); + } else { + connect(task_dialog, &TaskDialog::TaskSucceeded, this, &Core::AddOpenProjectFromTask); + } + + task_dialog->open(); +} + +int Core::CountFilesInFileList(const QFileInfoList &filenames) +{ + int file_count = 0; + + foreach (const QFileInfo& f, filenames) { + // For some reason QDir::NoDotAndDotDot doesn't work with entryInfoList, so we have to check manually + if (f.fileName() == "." || f.fileName() == "..") { + continue; + } else if (f.isDir()) { + QFileInfoList info_list = QDir(f.absoluteFilePath()).entryInfoList(); + + file_count += CountFilesInFileList(info_list); + } else { + file_count++; + } + } + + return file_count; +} + +QString GetRenderModePreferencePrefix(RenderMode::Mode mode, const QString &preference) { + QString key; + + key.append((mode == RenderMode::kOffline) ? QStringLiteral("Offline") : QStringLiteral("Online")); + key.append(preference); + + return key; +} + +QVariant Core::GetPreferenceForRenderMode(RenderMode::Mode mode, const QString &preference) +{ + return Config::Current()[GetRenderModePreferencePrefix(mode, preference)]; +} + +void Core::SetPreferenceForRenderMode(RenderMode::Mode mode, const QString &preference, const QVariant &value) +{ + Config::Current()[GetRenderModePreferencePrefix(mode, preference)] = value; +} + +bool Core::LabelNodes(const QVector &nodes, MultiUndoCommand *parent) +{ + if (nodes.isEmpty()) { + return false; + } + + bool ok; + + QString start_label = nodes.first()->GetLabel(); + + for (int i=1; iGetLabel() != start_label) { + // Not all the nodes share the same name, so we'll start with a blank one + start_label.clear(); + break; + } + } + + QString s = QInputDialog::getText(main_window_, + tr("Label Node"), + tr("Set node label"), + QLineEdit::Normal, + start_label, + &ok); + + if (ok) { + NodeRenameCommand* rename_command = new NodeRenameCommand(); + + foreach (Node* n, nodes) { + rename_command->AddNode(n, s); + } + + if (parent) { + parent->add_child(rename_command); + } else { + undo_stack_.push(rename_command); + } + + return true; + } + + return false; +} + +Sequence *Core::CreateNewSequenceForProject(Project* project) const +{ + Sequence* new_sequence = new Sequence(); + + // Get default name for this sequence (in the format "Sequence N", the first that doesn't exist) + int sequence_number = 1; + QString sequence_name; + do { + sequence_name = tr("Sequence %1").arg(sequence_number); + sequence_number++; + } while (project->root()->ChildExistsWithName(sequence_name)); + new_sequence->SetLabel(sequence_name); + + return new_sequence; +} + +void Core::OpenProjectFromRecentList(int index) +{ + const QString& open_fn = recent_projects_.at(index); + + if (QFileInfo::exists(open_fn)) { + OpenProjectInternal(open_fn); + } else if (QMessageBox::information(main_window(), + tr("Cannot open recent project"), + tr("The project \"%1\" doesn't exist. Would you like to remove this file from the recent list?").arg(open_fn), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + recent_projects_.removeAt(index); + + emit OpenRecentListChanged(); + } +} + +bool Core::CloseProject(Project *p, bool auto_open_new) +{ + CloseProjectBehavior b = kCloseProjectOnlyOne; + return CloseProject(p, auto_open_new, b); +} + +bool Core::CloseProject(Project* p, bool auto_open_new, CloseProjectBehavior &confirm_behavior) +{ + for (int i=0;iis_modified() && confirm_behavior != kCloseProjectDontSave) { + + bool save_this_project; + + if (confirm_behavior == kCloseProjectAsk || confirm_behavior == kCloseProjectOnlyOne) { + QMessageBox mb(main_window_); + + mb.setWindowModality(Qt::WindowModal); + mb.setIcon(QMessageBox::Question); + mb.setWindowTitle(tr("Unsaved Changes")); + mb.setText(tr("The project '%1' has unsaved changes. Would you like to save them?") + .arg(p->name())); + + QPushButton* yes_btn = mb.addButton(tr("Save"), QMessageBox::YesRole); + + QPushButton* yes_to_all_btn; + if (confirm_behavior == kCloseProjectOnlyOne) { + yes_to_all_btn = nullptr; + } else { + yes_to_all_btn = mb.addButton(tr("Save All"), QMessageBox::YesRole); + } + + mb.addButton(tr("Don't Save"), QMessageBox::NoRole); + + QPushButton* no_to_all_btn; + if (confirm_behavior == kCloseProjectOnlyOne) { + no_to_all_btn = nullptr; + } else { + no_to_all_btn = mb.addButton(tr("Don't Save All"), QMessageBox::NoRole); + } + + QPushButton* cancel_btn = mb.addButton(QMessageBox::Cancel); + + mb.exec(); + + if (mb.clickedButton() == cancel_btn) { + // Stop closing projects if the user clicked cancel + return false; + } else if (mb.clickedButton() == yes_to_all_btn) { + // Set flag that other CloseProject commands are going to use + confirm_behavior = kCloseProjectSave; + } else if (mb.clickedButton() == no_to_all_btn) { + // Set flag that other CloseProject commands are going to use + confirm_behavior = kCloseProjectDontSave; + } + + save_this_project = (mb.clickedButton() == yes_btn || mb.clickedButton() == yes_to_all_btn); + + } else { + // We must be saving this project + save_this_project = true; + } + + if (save_this_project && !SaveProject(p)) { + // The save failed, stop closing projects + return false; + } + + } + + // For safety, the undo stack is cleared so no commands try to affect a freed project + undo_stack_.clear(); + + disconnect(p, &Project::ModifiedChanged, this, &Core::ProjectWasModified); + emit ProjectClosed(p); + open_projects_.removeAt(i); + delete p; + break; + } + } + + // Ensure a project is always active + if (auto_open_new && open_projects_.isEmpty()) { + CreateNewProject(); + } + + return true; +} + +bool Core::CloseAllProjects(bool auto_open_new) +{ + QList copy = open_projects_; + + // See how many projects are modified so we can set "behavior" correctly + // (i.e. whether to show "Yes/No To All" buttons or not) + int modified_count = 0; + foreach (Project* p, copy) { + if (p->is_modified()) { + modified_count++; + } + } + + CloseProjectBehavior behavior; + + if (modified_count > 1) { + behavior = kCloseProjectAsk; + } else { + behavior = kCloseProjectOnlyOne; + } + + foreach (Project* p, copy) { + // If this is the only remaining project and the user hasn't chose "yes/no to all", hide those buttons + if (modified_count == 1 && behavior == kCloseProjectAsk) { + behavior = kCloseProjectOnlyOne; + } + + if (!CloseProject(p, auto_open_new, behavior)) { + return false; + } + + modified_count--; + } + + return true; +} + +void Core::CacheActiveSequence(bool in_out_only) +{ + TimeBasedPanel* p = PanelManager::instance()->MostRecentlyFocused(); + + if (p && p->GetConnectedViewer()) { + // Hacky but works for now + + // Find Viewer attached to this TimeBasedPanel + QList all_viewers = PanelManager::instance()->GetPanelsOfType(); + + ViewerPanel* found_panel = nullptr; + + foreach (ViewerPanel* viewer, all_viewers) { + if (viewer->GetConnectedViewer() == p->GetConnectedViewer()) { + found_panel = viewer; + break; + } + } + + if (found_panel) { + if (in_out_only) { + found_panel->CacheSequenceInOut(); + } else { + found_panel->CacheEntireSequence(); + } + } else { + QMessageBox::critical(main_window_, + tr("Failed to cache sequence"), + tr("No active viewer found with this sequence."), + QMessageBox::Ok); + } + } +} + +bool Core::ValidateFootageInLoadedProject(Project* project, const QString& project_saved_url) +{ + QVector project_footage = project->root()->ListChildrenOfType(); + QVector footage_we_couldnt_validate; + + foreach (Footage* footage, project_footage) { + if (!QFileInfo::exists(footage->filename()) && !project_saved_url.isEmpty()) { + // If the footage doesn't exist, it might have moved with the project + const QString& project_current_url = project->filename(); + + if (project_current_url != project_saved_url) { + // Project has definitely moved, try to resolve relative paths + QDir saved_dir(QFileInfo(project_saved_url).dir()); + QDir true_dir(QFileInfo(project_current_url).dir()); + + QString relative_filename = saved_dir.relativeFilePath(footage->filename()); + QString transformed_abs_filename = true_dir.filePath(relative_filename); + + if (QFileInfo::exists(transformed_abs_filename)) { + // Use this file instead + qInfo() << "Resolved" << footage->filename() << "relatively to" << transformed_abs_filename; + footage->set_filename(transformed_abs_filename); + } + } + } + + if (QFileInfo::exists(footage->filename())) { + // Assume valid + footage->SetValid(); + } else { + footage_we_couldnt_validate.append(footage); + } + } + + if (!footage_we_couldnt_validate.isEmpty()) { + FootageRelinkDialog frd(footage_we_couldnt_validate, main_window_); + if (frd.exec() == QDialog::Rejected) { + return false; + } + } + + return true; +} + +bool Core::SetLanguage(const QString &locale) +{ + QApplication::removeTranslator(translator_); + + QString resource_path = QStringLiteral(":/ts/%1").arg(locale); + if (translator_->load(resource_path) && QApplication::installTranslator(translator_)) { + return true; + } + + return false; +} + +bool Core::CloseAllProjects() +{ + return CloseAllProjects(true); +} + +void Core::OpenProject() +{ + QString file = QFileDialog::getOpenFileName(main_window_, + tr("Open Project"), + QString(), + GetProjectFilter(true)); + + if (!file.isEmpty()) { + OpenProjectInternal(file); + } +} + +Core::CoreParams::CoreParams() : + mode_(kRunNormal), + run_fullscreen_(false), + crash_(false) +{ +} + +} diff --git a/app/core.h b/app/core.h index e096752d4..653769af6 100644 --- a/app/core.h +++ b/app/core.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -21,14 +21,24 @@ #ifndef CORE_H #define CORE_H +#include #include #include +#include -#include "project/project.h" -#include "project/projectviewmodel.h" -#include "window/mainwindow/mainwindow.h" +#include "common/rational.h" +#include "common/timecodefunctions.h" +#include "node/project/footage/footage.h" +#include "node/project/project.h" +#include "node/project/projectviewmodel.h" +#include "node/project/sequence/sequence.h" #include "task/task.h" #include "tool/tool.h" +#include "undo/undostack.h" + +namespace olive { + +class MainWindow; /** * @brief The main central Olive application instance @@ -43,12 +53,98 @@ class Core : public QObject { Q_OBJECT public: + class CoreParams + { + public: + CoreParams(); + + enum RunMode { + kRunNormal, + kHeadlessExport, + kHeadlessPreCache + }; + + bool fullscreen() const + { + return run_fullscreen_; + } + + void set_fullscreen(bool e) + { + run_fullscreen_ = e; + } + + RunMode run_mode() const + { + return mode_; + } + + void set_run_mode(RunMode m) + { + mode_ = m; + } + + const QString startup_project() const + { + return startup_project_; + } + + void set_startup_project(const QString& p) + { + startup_project_ = p; + } + + const QString& startup_language() const + { + return startup_language_; + } + + void set_startup_language(const QString& s) + { + startup_language_ = s; + } + + bool crash_on_startup() const + { + return crash_; + } + + void set_crash_on_startup(bool e) + { + crash_ = true; + } + + private: + RunMode mode_; + + QString startup_project_; + + QString startup_language_; + + bool run_fullscreen_; + + bool crash_; + + }; + /** * @brief Core Constructor * * Currently empty */ - Core(); + Core(const CoreParams& params); + + /** + * @brief Core object accessible from anywhere in the code + * + * Use this to access Core functions. + */ + static Core* instance(); + + const CoreParams& core_params() const + { + return core_params_; + } /** * @brief Start Olive Core @@ -71,7 +167,12 @@ public: * * Pointer to the olive::MainWindow object, or nullptr if running in CLI mode. */ - olive::MainWindow* main_window(); + MainWindow* main_window(); + + /** + * @brief Retrieve UndoStack object + */ + UndoStack* undo_stack(); /** * @brief Import a list of files @@ -86,21 +187,27 @@ public: /** * @brief Get the currently active tool */ - const olive::tool::Tool& tool(); + const Tool::Item& tool() const; + + /** + * @brief Get the currently selected object that the add tool should make (if the add tool is active) + */ + const Tool::AddableObject& GetSelectedAddableObject() const; + + /** + * @brief Get the currently selected node that the transition tool should make (if the transition tool is active) + */ + const QString& GetSelectedTransition() const; /** * @brief Get current snapping value */ - const bool& snapping(); + const bool& snapping() const; /** - * @brief Starts a modal task - * - * This function does NOT take ownership of the Task. - * - * @param t + * @brief Returns a list of the most recently opened/saved projects */ - void StartModalTask(Task* t); + const QStringList& GetRecentProjects() const; /** * @brief Get the currently active project @@ -112,28 +219,157 @@ public: * * The active Project file, or nullptr if the heuristic couldn't find one. */ - Project* GetActiveProject(); + Project* GetActiveProject() const; + ProjectViewModel* GetActiveProjectModel() const; + Folder* GetSelectedFolderInActiveProject() const; /** - * @brief Sets state to "modified" so that the GUI will prompt the user to save before closing - * - * Call this function whenever a change is made to a currently active project. Saving the project will automatically - * unset this. + * @brief Gets current timecode display mode */ - void SetProjectModified(); + Timecode::Display GetTimecodeDisplay() const; + + /** + * @brief Sets current timecode display mode + */ + void SetTimecodeDisplay(Timecode::Display d); /** * @brief Set how frequently an autorecovery should be saved (if the project has changed, see SetProjectModified()) */ void SetAutorecoveryInterval(int minutes); + static void CopyStringToClipboard(const QString& s); + + static QString PasteStringFromClipboard(); + + /** + * @brief Recursively count files in a file/directory list + */ + static int CountFilesInFileList(const QFileInfoList &filenames); + + static QVariant GetPreferenceForRenderMode(RenderMode::Mode mode, const QString& preference); + static void SetPreferenceForRenderMode(RenderMode::Mode mode, const QString& preference, const QVariant& value); + + /** + * @brief Show a dialog to the user to rename a set of nodes + */ + bool LabelNodes(const QVector &nodes, MultiUndoCommand *parent = nullptr); + + /** + * @brief Create a new sequence named appropriately for the active project + */ + Sequence* CreateNewSequenceForProject(Project *project) const; + + /** + * @brief Opens a project from the recently opened list + */ + void OpenProjectFromRecentList(int index); + + enum CloseProjectBehavior { + kCloseProjectOnlyOne, + kCloseProjectAsk, + kCloseProjectSave, + kCloseProjectDontSave + }; + + /** + * @brief Closes a project + */ + bool CloseProject(Project* p, bool auto_open_new, CloseProjectBehavior& confirm_behavior); + bool CloseProject(Project* p, bool auto_open_new); + + /** + * @brief Closes all open projects + */ + bool CloseAllProjects(bool auto_open_new); + + /** + * @brief Runs a modal cache task on the currently active sequence + */ + void CacheActiveSequence(bool in_out_only); + + /** + * @brief Check each footage object for whether it still exists or has changed + */ + bool ValidateFootageInLoadedProject(Project* project, const QString &project_saved_url); + + /** + * @brief Changes the current language + */ + bool SetLanguage(const QString& locale); + + /** + * @brief Saves a specific project + */ + bool SaveProject(Project *p); + + /** + * @brief Show message in main window's status bar + * + * Shorthand for Core::instance()->main_window()->statusBar()->showMessage(); + */ + void ShowStatusBarMessage(const QString& s, int timeout = 0); + + void ClearStatusBarMessage(); + + void OpenRecoveryProject(const QString& filename); + + void OpenNodeInViewer(ViewerOutput* viewer); + public slots: + /** + * @brief Starts an open file dialog to load a project from file + */ + void OpenProject(); + + /** + * @brief Save the currently active project + * + * If the project hasn't been saved before, this will be equivalent to calling SaveActiveProjectAs(). + */ + bool SaveActiveProject(); + + /** + * @brief Save the currently active project with a new filename + */ + bool SaveActiveProjectAs(); + + /** + * @brief Save all currently open projects + */ + bool SaveAllProjects(); + + /** + * @brief Revert project to last saved state (basically close and open it) + */ + void RevertActiveProject(); + + /** + * @brief Closes the active project + * + * If no other projects are open, a new one is created automatically. + */ + bool CloseActiveProject(); + + /** + * @brief Closes all projects except the active project + */ + bool CloseAllExceptActiveProject(); + + /** + * @brief Closes all open projects + * + * Equivalent to `CloseAllProjects(true)`, but useful for the signal/slot system where you may not be able to specify + * parameters. + */ + bool CloseAllProjects(); + /** * @brief Set the current application-wide tool * * @param tool */ - void SetTool(const olive::tool::Tool& tool); + void SetTool(const Tool::Item& tool); /** * @brief Set the current snapping setting @@ -160,16 +396,54 @@ public slots: */ void DialogProjectPropertiesShow(); + /** + * @brief Show Export dialog + */ + void DialogExportShow(); + + /** + * @brief Show OTIO import dialog + */ +#ifdef USE_OTIO + bool DialogImportOTIOShow(const QList& sequences); +#endif + /** * @brief Create a new folder in the currently active project */ void CreateNewFolder(); /** - * @brief Createa a new sequence in the currently active project + * @brief Create a new sequence in the currently active project */ void CreateNewSequence(); + /** + * @brief Set the currently selected object that the add tool should make + */ + void SetSelectedAddableObject(const Tool::AddableObject& obj); + + /** + * @brief Set the currently selected object that the add tool should make + */ + void SetSelectedTransitionObject(const QString& obj); + + /** + * @brief Clears the list of recently opened/saved projects + */ + void ClearOpenRecentList(); + + /** + * @brief Creates a new empty project and opens it + */ + void CreateNewProject(); + + void CheckForAutoRecoveries(); + + void BrowseAutoRecoveries(); + + void RequestPixelSamplingInViewers(bool e); + signals: /** * @brief Signal emitted when a project is opened @@ -180,21 +454,73 @@ signals: */ void ProjectOpened(Project* p); + /** + * @brief Signal emitted when a project is closed + */ + void ProjectClosed(Project* p); + /** * @brief Signal emitted when the tool is changed from somewhere */ - void ToolChanged(const olive::tool::Tool& tool); + void ToolChanged(const Tool::Item& tool); + + /** + * @brief Signal emitted when addable object changes through SetSelectedAddableObject + */ + void AddableObjectChanged(Tool::AddableObject o); /** * @brief Signal emitted when the snapping setting is changed */ void SnappingChanged(const bool& b); + /** + * @brief Signal emitted when the default timecode display mode changed + */ + void TimecodeDisplayChanged(Timecode::Display d); + + /** + * @brief Signal emitted when a change is made to the open recent list + */ + void OpenRecentListChanged(); + + /** + * @brief Enable mouse color sampling functionality on all viewers + * + * This can be slow, so we only turn it on when we need it. + */ + void ColorPickerEnabled(bool e); + + /** + * @brief A viewer with color picked enabled has emitted a color + */ + void ColorPickerColorEmitted(const Color &reference, const Color &display); + private: /** - * @brief Creates an empty project and adds it to the "open projects" + * @brief Get the file filter than can be used with QFileDialog to open and save compatible projects */ - void AddOpenProject(ProjectPtr p); + static QString GetProjectFilter(bool include_any_filter); + + /** + * @brief Returns the filename where the recently opened/saved projects should be stored + */ + static QString GetRecentProjectsFilePath(); + + /** + * @brief Called only on startup to set the locale + */ + void SetStartupLocale(); + + /** + * @brief Performs a "save as" on a specific project + */ + bool SaveProjectAs(Project *p); + + /** + * @brief Adds a filename to the top of the recently opened projects list (or moves it if it already exists) + */ + void PushRecentlyOpenedProject(const QString &s); /** * @brief Declare custom types/classes for Qt's signal/slot system @@ -213,58 +539,122 @@ private: void StartGUI(bool full_screen); /** - * @brief Internal main window object + * @brief Internal function for saving a project to a file */ - olive::MainWindow* main_window_; + void SaveProjectInternal(Project *project, const QString &override_filename = QString()); /** - * @brief Internal startup project object - * - * If the user specifies a project file on the command line, the command line parser in Start() will write the - * project URL here to be loaded once Olive has finished initializing. + * @brief Retrieves the currently most active sequence for exporting */ - QString startup_project_; + ViewerOutput *GetSequenceToExport(); + + static QString GetAutoRecoveryIndexFilename(); + + void SaveUnrecoveredList(); + + bool RevertProjectInternal(Project *p, bool by_opening_existing); + + /** + * @brief Internal main window object + */ + MainWindow* main_window_; /** * @brief List of currently open projects */ - QList open_projects_; + QList open_projects_; /** * @brief Currently active tool */ - olive::tool::Tool tool_; + Tool::Item tool_; /** - * @brief Current snapping toggle + * @brief Currently active addable object + */ + Tool::AddableObject addable_object_; + + /** + * @brief Currently selected transition + */ + QString selected_transition_; + + /** + * @brief Current snapping setting */ bool snapping_; - /** - * @brief Internal value for whether to make an autorecovery next interval - * - * True if the project has changed since the last autorecovery and we should save next time. False if the project has - * not changed and saving another autorecovery would be a waste. - */ - bool queue_autorecovery_; - /** * @brief Internal timer for saving autorecovery files */ QTimer autorecovery_timer_; + /** + * @brief Application-wide undo stack instance + */ + UndoStack undo_stack_; + + /** + * @brief List of most recently opened/saved projects + */ + QStringList recent_projects_; + + /** + * @brief Parameters set up in main() determining how the program should run + */ + CoreParams core_params_; + + /** + * @brief Static singleton core instance + */ + static Core* instance_; + + /** + * @brief Internal translator + */ + QTranslator* translator_; + + /** + * @brief List of projects that are unsaved but have autorecovery projects + */ + QVector autorecovered_projects_; + + /** + * @brief How many widgets currently need pixel sampling access + */ + int pixel_sampling_users_; + private slots: void SaveAutorecovery(); + void ProjectSaveSucceeded(Task *task); + + /** + * @brief Adds a project to the "open projects" list + */ + void AddOpenProject(olive::Project* p); + + bool AddOpenProjectFromTask(Task* task); + + void ImportTaskComplete(Task *task); + + bool ConfirmImageSequence(const QString &filename); + + void ProjectWasModified(bool e); + + bool StartHeadlessExport(); + + void OpenStartupProject(); + + void AddRecoveryProjectFromTask(Task* task); + + /** + * @brief Internal project open + */ + void OpenProjectInternal(const QString& filename, bool recovery_project = false); + }; -namespace olive { -/** - * @brief Core object accessible from anywhere in the code - * - * Use this to access Core functions. - */ -extern Core core; } #endif // CORE_H diff --git a/app/crashhandler/CMakeLists.txt b/app/crashhandler/CMakeLists.txt new file mode 100644 index 000000000..e05972a24 --- /dev/null +++ b/app/crashhandler/CMakeLists.txt @@ -0,0 +1,65 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# 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 . + +# Create crash handler executable +add_executable( + olive-crashhandler + crashhandler.cpp + crashhandler.h + $ +) + +# Disable console appearing on crash handler dialog +set_target_properties(olive-crashhandler PROPERTIES + WIN32_EXECUTABLE TRUE +) + +# Set crash handler includes +target_include_directories( + olive-crashhandler + PRIVATE + ${CMAKE_SOURCE_DIR}/app + ${CRASHPAD_INCLUDE_DIRS} +) + +# Set crash handler libs +target_link_libraries( + olive-crashhandler + PRIVATE + Qt5::Core + Qt5::Gui + Qt5::Widgets + Qt5::Network + ${CRASHPAD_LIBRARIES} +) + +set(CRASHPAD_HANDLER "crashpad_handler${CMAKE_EXECUTABLE_SUFFIX}") +set(MINIDUMP_STACKWALK "minidump_stackwalk${CMAKE_EXECUTABLE_SUFFIX}") + +if(APPLE) + # Move crash handler executables inside Mac app bundle + add_custom_command(TARGET olive-crashhandler POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different olive-crashhandler $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CRASHPAD_LIBRARY_DIRS}/${CRASHPAD_HANDLER} $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${BREAKPAD_BIN_DIR}/${MINIDUMP_STACKWALK} $ + ) +elseif(UNIX) + install(TARGETS olive-crashhandler RUNTIME DESTINATION bin) + install(PROGRAMS ${CRASHPAD_LIBRARY_DIRS}/${CRASHPAD_HANDLER} DESTINATION bin) + install(PROGRAMS ${BREAKPAD_BIN_DIR}/${MINIDUMP_STACKWALK} DESTINATION bin) +endif() + +target_compile_definitions(olive-crashhandler PRIVATE ${OLIVE_DEFINITIONS}) diff --git a/app/crashhandler/crashhandler.cpp b/app/crashhandler/crashhandler.cpp new file mode 100644 index 000000000..a56a43c79 --- /dev/null +++ b/app/crashhandler/crashhandler.cpp @@ -0,0 +1,357 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "crashhandler.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/crashpadutils.h" +#include "common/filefunctions.h" +#include "version.h" + +namespace olive { + +CrashHandlerDialog::CrashHandlerDialog(const QString& report_path) +{ + setWindowTitle(tr("Olive")); + setWindowFlags(Qt::WindowStaysOnTopHint); + + report_filename_ = report_path; + waiting_for_upload_ = false; + + QVBoxLayout* layout = new QVBoxLayout(this); + + layout->addWidget(new QLabel(tr("We're sorry, Olive has crashed. Please help us fix it by " + "sending an error report."))); + + QSplitter* splitter = new QSplitter(Qt::Vertical); + splitter->setChildrenCollapsible(false); + layout->addWidget(splitter); + + summary_edit_ = new QTextEdit(); + summary_edit_->setPlaceholderText(tr("Describe what you were doing in as much detail as " + "possible. If you can, provide steps to reproduce this crash.")); + + splitter->addWidget(summary_edit_); + + QWidget* crash_widget = new QWidget(); + QVBoxLayout* crash_widget_layout = new QVBoxLayout(crash_widget); + crash_widget_layout->setMargin(0); + + crash_widget_layout->addWidget(new QLabel(tr("Crash Report:"))); + + crash_report_ = new QTextEdit(); + crash_report_->setReadOnly(true); + crash_report_->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + crash_widget_layout->addWidget(crash_report_); + + splitter->addWidget(crash_widget); + + QHBoxLayout* btn_layout = new QHBoxLayout(); + btn_layout->setMargin(0); + btn_layout->addStretch(); + + send_report_btn_ = new QPushButton(tr("Send Error Report")); + connect(send_report_btn_, &QPushButton::clicked, this, &CrashHandlerDialog::SendErrorReport); + btn_layout->addWidget(send_report_btn_); + + dont_send_btn_ = new QPushButton(tr("Don't Send")); + connect(dont_send_btn_, &QPushButton::clicked, this, &CrashHandlerDialog::reject); + btn_layout->addWidget(dont_send_btn_); + + layout->addLayout(btn_layout); + + crash_report_->setEnabled(false); + send_report_btn_->setEnabled(false); + + crash_report_->setText(tr("Waiting for crash report to be generated...")); + + GenerateReport(); +} + +void CrashHandlerDialog::SetGUIObjectsEnabled(bool e) +{ + summary_edit_->setEnabled(e); + crash_report_->setEnabled(e); + send_report_btn_->setEnabled(e); + dont_send_btn_->setEnabled(e); +} + +QString CrashHandlerDialog::GetSymbolPath() +{ + QDir app_path(qApp->applicationDirPath()); + QString symbols_path; + +#if defined(OS_WIN) + symbols_path = app_path.filePath(QStringLiteral("symbols")); +#elif defined(OS_LINUX) + app_path.cdUp(); + symbols_path = app_path.filePath(QStringLiteral("share/olive-editor/symbols")); +#elif defined(OS_APPLE) + app_path.cdUp(); + symbols_path = app_path.filePath(QStringLiteral("Resources/symbols")); +#endif + + return symbols_path; +} + +void CrashHandlerDialog::GenerateReport() +{ + QProcess* p = new QProcess(); + + connect(p, QOverload::of(&QProcess::finished), + this, &CrashHandlerDialog::ReadProcessFinished); + connect(p, &QProcess::readyReadStandardOutput, this, &CrashHandlerDialog::ReadProcessHasData); + + QString stackwalk_filename = FileFunctions::GetFormattedExecutableForPlatform(QStringLiteral("minidump_stackwalk")); + + QString stackwalk_bin = QDir(qApp->applicationDirPath()).filePath(stackwalk_filename); + p->start(stackwalk_bin, {report_filename_, GetSymbolPath()}); + crash_report_->setText(QStringLiteral("Trying to run: %1").arg(stackwalk_bin)); +} + +void CrashHandlerDialog::ReplyFinished(QNetworkReply* reply) +{ + waiting_for_upload_ = false; + + if (reply->error() == QNetworkReply::NoError) { + // Close dialog + QDialog::accept(); + } else { + QMessageBox b(this); + b.setIcon(QMessageBox::Critical); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("Upload Failed")); + b.setText(tr("Failed to send error report. Please try again later.")); + b.addButton(QMessageBox::Ok); + b.exec(); + + SetGUIObjectsEnabled(true); + } +} + +void CrashHandlerDialog::AttemptToFindReport() +{ + // If we found it, use it, otherwise wait a second and try again + if (report_filename_.isEmpty()) { + // Couldn't find report, try again in one second + QTimer::singleShot(500, this, &CrashHandlerDialog::AttemptToFindReport); + } else { + GenerateReport(); + } +} + +void CrashHandlerDialog::ReadProcessHasData() +{ + report_data_.append(static_cast(sender())->readAllStandardOutput()); +} + +void CrashHandlerDialog::ReadProcessFinished() +{ + SetGUIObjectsEnabled(true); + crash_report_->setText(report_data_); + delete sender(); +} + +void CrashHandlerDialog::SendErrorReport() +{ + if (summary_edit_->document()->isEmpty()) { + QMessageBox b(this); + b.setIcon(QMessageBox::Question); + b.setWindowModality(Qt::WindowModal); + b.setText(tr("You must write a description to submit this crash report.")); + b.addButton(QMessageBox::Ok); + b.exec(); + return; + } + + QNetworkAccessManager* manager = new QNetworkAccessManager(); + connect(manager, &QNetworkAccessManager::finished, this, &CrashHandlerDialog::ReplyFinished); + + QNetworkRequest request; + request.setSslConfiguration(QSslConfiguration::defaultConfiguration()); + request.setUrl(QStringLiteral("https://olivevideoeditor.org/crashpad/report.php")); + + // Create HTTP form + QHttpMultiPart* multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + + // Create description section + QHttpPart desc_part; + desc_part.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/plain; charset=UTF-8")); + desc_part.setHeader(QNetworkRequest::ContentDispositionHeader, QStringLiteral("form-data; name=\"description\"")); + desc_part.setBody(summary_edit_->toPlainText().toUtf8()); + multipart->append(desc_part); + + // Create report section + QHttpPart report_part; + report_part.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/plain; charset=UTF-8")); + report_part.setHeader(QNetworkRequest::ContentDispositionHeader, QStringLiteral("form-data; name=\"report\"")); + report_part.setBody(report_data_); + multipart->append(report_part); + + // Create commit section + QHttpPart commit_part; + commit_part.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/plain; charset=UTF-8")); + commit_part.setHeader(QNetworkRequest::ContentDispositionHeader, QStringLiteral("form-data; name=\"commit\"")); + commit_part.setBody(kAppVersionLong.toUtf8()); + multipart->append(commit_part); + + // Create dump section + QHttpPart dump_part; + dump_part.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/octet-stream")); + dump_part.setHeader(QNetworkRequest::ContentDispositionHeader, QStringLiteral("form-data; name=\"dump\"; filename=\"%1\"") + .arg(QFileInfo(report_filename_).fileName())); + QFile* dump_file = new QFile(report_filename_); + dump_file->open(QFile::ReadOnly); + dump_part.setBodyDevice(dump_file); + dump_file->setParent(multipart); // Delete file with multipart + multipart->append(dump_part); + + // Find symbol file + QDir symbol_dir(GetSymbolPath()); + + QString symbol_bin_name; +#if defined(OS_WIN) + symbol_bin_name = QStringLiteral("olive-editor.pdb"); +#elif defined(OS_APPLE) + symbol_bin_name = QStringLiteral("Olive"); +#else + symbol_bin_name = QStringLiteral("olive-editor"); +#endif + symbol_dir = QDir(symbol_dir.filePath(symbol_bin_name)); + + QStringList folders_in_symbol_path = symbol_dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + + if (folders_in_symbol_path.size() > 0) { + symbol_dir = QDir(symbol_dir.filePath(folders_in_symbol_path.first())); + } else { + QMessageBox b(this); + b.setIcon(QMessageBox::Critical); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("Failed to send report")); + b.setText(tr("Failed to find symbols necessary to send report. " + "This is a packaging issue. Please notify " + "the maintainers of this package.")); + b.addButton(QMessageBox::Ok); + b.exec(); + return; + } + + // Create sym section + QString symbol_filename; +#if defined(OS_APPLE) + symbol_filename = QStringLiteral("Olive.sym"); +#else + symbol_filename = QStringLiteral("olive-editor.sym"); +#endif + QString symbol_full_path = symbol_dir.filePath(symbol_filename); + QHttpPart sym_part; + sym_part.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/octet-stream")); + sym_part.setHeader(QNetworkRequest::ContentDispositionHeader, QStringLiteral("form-data; name=\"sym\"; filename=\"%1\"") + .arg(symbol_filename)); + QFile sym_file(symbol_full_path); + + if (!sym_file.open(QFile::ReadOnly)) { + QMessageBox b(this); + b.setIcon(QMessageBox::Critical); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("Failed to send report")); + b.setText(tr("Failed to open symbol file. You may not have " + "permission to access it.")); + b.addButton(QMessageBox::Ok); + b.exec(); + return; + } + + QByteArray symbol_data = qCompress(sym_file.readAll(), 9); + sym_file.close(); + + sym_part.setBody(symbol_data); + multipart->append(sym_part); + + manager->post(request, multipart); + + SetGUIObjectsEnabled(false); + waiting_for_upload_ = true; +} + +void CrashHandlerDialog::closeEvent(QCloseEvent* e) +{ + QMessageBox b(this); + b.setIcon(QMessageBox::Warning); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("Confirm Close")); + b.setText(tr("Crash report is still uploading. Closing now may result in no " + "report being sent. Are you sure you wish to close?")); + b.addButton(QMessageBox::Ok); + b.addButton(QMessageBox::Cancel); + + if (waiting_for_upload_ && b.exec() == QMessageBox::Cancel) { + e->ignore(); + } else { + e->accept(); + } +} + +} + +int main(int argc, char *argv[]) +{ + QString report; + +#ifdef Q_OS_WINDOWS + int num_args; + LPWSTR *args = CommandLineToArgvW(GetCommandLineW(), &num_args); + if (num_args < 2) { + LocalFree(args); + return 1; + } + + report = QString::fromWCharArray(args[1]); + LocalFree(args); +#else + if (argc < 2) { + return 1; + } + + report = argv[1]; +#endif + + QApplication a(argc, argv); + + olive::CrashHandlerDialog chd(report); + chd.open(); + + return a.exec(); +} diff --git a/app/crashhandler/crashhandler.h b/app/crashhandler/crashhandler.h new file mode 100644 index 000000000..ae1200261 --- /dev/null +++ b/app/crashhandler/crashhandler.h @@ -0,0 +1,80 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef CRASHHANDLERDIALOG_H +#define CRASHHANDLERDIALOG_H + +#include +#include +#include +#include +#include +#include + +#include "common/define.h" + +namespace olive { + +class CrashHandlerDialog : public QDialog +{ + Q_OBJECT +public: + CrashHandlerDialog(const QString& report_path); + +private: + void SetGUIObjectsEnabled(bool e); + + void GenerateReport(); + + static QString GetSymbolPath(); + + QTextEdit* summary_edit_; + + QTextEdit* crash_report_; + + QPushButton* send_report_btn_; + + QPushButton* dont_send_btn_; + + QString report_filename_; + + QByteArray report_data_; + + bool waiting_for_upload_; + +protected: + virtual void closeEvent(QCloseEvent* e) override; + +private slots: + void ReplyFinished(QNetworkReply *reply); + + void AttemptToFindReport(); + + void ReadProcessHasData(); + + void ReadProcessFinished(); + + void SendErrorReport(); + +}; + +} + +#endif // CRASHHANDLERDIALOG_H diff --git a/app/decoder/decoder.cpp b/app/decoder/decoder.cpp deleted file mode 100644 index 1659614d9..000000000 --- a/app/decoder/decoder.cpp +++ /dev/null @@ -1,163 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - 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 . - -***/ - -#include "decoder.h" - -#include -#include -#include - -#include "decoder/ffmpeg/ffmpegdecoder.h" -#include "decoder/oiio/oiiodecoder.h" - -Decoder::Decoder() : - open_(false), - stream_(nullptr) -{ -} - -Decoder::Decoder(Stream *fs) : - open_(false), - stream_(fs) -{ -} - -Decoder::~Decoder() -{ -} - -StreamPtr Decoder::stream() -{ - return stream_; -} - -void Decoder::set_stream(StreamPtr fs) -{ - Close(); - - stream_ = fs; -} - -FramePtr Decoder::RetrieveVideo(const rational &/*timecode*/) -{ - return nullptr; -} - -FramePtr Decoder::RetrieveAudio(const rational &/*timecode*/, const rational &/*length*/, const AudioRenderingParams &/*params*/) -{ - return nullptr; -} - -bool Decoder::SupportsVideo() -{ - return false; -} - -bool Decoder::SupportsAudio() -{ - return false; -} - -/* - * DECODER STATIC PUBLIC MEMBERS - */ - -QVector ReceiveListOfAllDecoders() { - QVector decoders; - - // The order in which these decoders are added is their priority when probing. Hence FFmpeg should usually be last, - // since it supports so many formats and we presumably want to override those formats with a more specific decoder. - - decoders.append(std::make_shared()); - decoders.append(std::make_shared()); - - return decoders; -} - -bool Decoder::ProbeMedia(Footage *f) -{ - // Check for a valid filename - if (f->filename().isEmpty()) { - qWarning() << "Tried to probe media with an empty filename"; - return false; - } - - // Check file exists - if (!QFileInfo::exists(f->filename())) { - qWarning() << "Tried to probe file that doesn't exist:" << f->filename(); - return false; - } - - // Reset Footage state for probing - f->Clear(); - - // Create list to iterate through - QVector decoder_list = ReceiveListOfAllDecoders(); - - // Pass Footage through each Decoder's probe function - for (int i=0;iProbe(f)) { - - // We found a Decoder, so we can set this media as valid - f->set_status(Footage::kReady); - - // Attach the successful Decoder to this Footage object - f->set_decoder(decoder->id()); - - // FIXME: Cache the results so we don't have to probe if this media is added a second time - - return true; - } - } - - // We aren't able to use this Footage - f->set_status(Footage::kInvalid); - f->set_decoder(QString()); - - return false; -} - -DecoderPtr Decoder::CreateFromID(const QString &id) -{ - if (id.isEmpty()) { - return nullptr; - } - - // Create list to iterate through - QVector decoder_list = ReceiveListOfAllDecoders(); - - foreach (DecoderPtr d, decoder_list) { - if (d->id() == id) { - return d; - } - } - - return nullptr; -} - -void Decoder::Conform(const AudioRenderingParams ¶ms) -{ - Q_UNUSED(params) - qCritical() << "Conform called on an audio decoder that does not have a handler for it:" << id(); - abort(); -} diff --git a/app/decoder/decoder.h b/app/decoder/decoder.h deleted file mode 100644 index 3e0d8ef3e..000000000 --- a/app/decoder/decoder.h +++ /dev/null @@ -1,229 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - 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 . - -***/ - -#ifndef DECODER_H -#define DECODER_H - -#include -#include - -#include "common/constructors.h" -#include "common/rational.h" -#include "project/item/footage/footage.h" -#include "decoder/frame.h" - -class Decoder; -using DecoderPtr = std::shared_ptr; - -/** - * @brief A decoder's is the main class for bringing external media into Olive - * - * Its responsibilities are to serve as - * abstraction from codecs/decoders and provide complete frames. These frames can be video or audio data and are - * provided as Frame objects in shared pointers to alleviate the responsibility of memory handling. - * - * The main function in a decoder is Retrieve() which should return complete image/audio data. A decoder should - * alleviate all the complexities of codec compression from the rest of the application (i.e. a decoder should never - * return a partial frame or require other parts of the system to interface directly with the codec). Often this will - * necessitate pre-emptively caching, indexing, or even fully transcoding media before using it which can be implemented - * through the Analyze() function. - * - * A decoder does NOT perform any pixel/sample format conversion. Frames should pass through the PixelFormatConverter - * (olive::pix_fmt_conv) to be utilized in the rest of the rendering pipeline. - */ -class Decoder : public QObject -{ - Q_OBJECT -public: - Decoder(); - - Decoder(Stream* fs); - - // Necessary for subclassing, it's empty - virtual ~Decoder(); - - DISABLE_COPY_MOVE(Decoder) - - virtual QString id() = 0; - - StreamPtr stream(); - void set_stream(StreamPtr fs); - - /** - * @brief Probe a footage file and dump metadata about it - * - * When a Footage file is imported, we'll need to know whether Olive is equipped with a decoder for utilizing it - * and metadata should be retrieved about it if so. For this purpose, the Footage object is passed through all - * Probe() functions of available deocders until one returns TRUE. A FALSE return means the Decoder was unable to - * parse this file and the next should be tried. - * - * Probe() differs from Open() since it focuses on a file as a whole rather than one particular stream. Probe() - * should be able to be run directly without calling Open() or Close() and should free its memory before returning. - * - * Probe() will never be called on an object that is also used for decoding. In other words, it will never be called - * alongside Open() or Close() externally, so Probe() can use variables that would otherwise be used for decoding - * without conflict. - * - * @param f - * - * A Footage object to probe. The Footage object will have a valid filename and will be empty prior to being sent - * to this function (i.e. Footage::Clear() will not have to be called). - * - * @return - * - * TRUE if the Decoder was able to decode this file. FALSE if not. This function should have filled the Footage - * object with metadata if it returns TRUE. Otherwise, the Footage object should be untouched. - */ - virtual bool Probe(Footage* f) = 0; - - /** - * @brief Open media/allocate memory - * - * Any file handles or memory allocation that needs to be done before this instance of a Decoder can return data - * should be done here. - * - * @return - * - * TRUE if successful and ready to return data, FALSE if failed to open and unable to retrieve data. If the function - * fails, any memory allocated should be free'd before returning FALSE, possibly by calling Close(). - */ - virtual bool Open() = 0; - - /** - * @brief Retrieve video frame - * - * The main function for retrieving video data from the Decoder. This function should always provide complete frame - * data (i.e. no partial frames) at the timecode provided. The Decoder should perform any steps required to retrieve - * a complete frame separate from the rest of the program, using any form of caching/indexing to keep this as - * performant as possible. - * - * It's acceptable for this function to check whether the Decoder is open, and call Open() if not. If Open() returns - * false, this function should return nullptr. - * - * @param timecode - * - * The timecode (a rational in seconds) to retrieve the frame at. If there is not a frame at this precise location - * this should be corrected internally to the closest fit for the timecode. - * - * @return - * - * A FramePtr of valid data at this timecode or nullptr if there was nothing to retrieve at the provided timecode or - * the media could not be opened. - */ - virtual FramePtr RetrieveVideo(const rational& timecode); - - /** - * @brief Retrieve video frame - * - * The main function for retrieving audio data from the Decoder. This function should always provide complete frame - * data (i.e. no missing samples) at the timecode and length requested. The Decoder should perform any steps - * required to retrieve a complete frame separate from the rest of the program, using any form of caching/indexing - * to keep this as performant as possible. - * - * It's acceptable for this function to check whether the Decoder is open, and call Open() if not. If Open() returns - * false, this function should return nullptr. - * - * @param timecode - * - * The starting timecode (a rational in seconds) to retrieve the data at. - * - * @param length - * - * The total length of audio data to retrieve (a rational in seconds). - * - * @return - * - * A FramePtr of valid data at this timecode of the requested length or nullptr if there was nothing to retrieve at - * the provided timecode or the media could not be opened. - */ - virtual FramePtr RetrieveAudio(const rational& timecode, const rational& length, const AudioRenderingParams& params); - - virtual bool SupportsVideo(); - virtual bool SupportsAudio(); - - /** - * @brief Close media/deallocate memory - * - * Any file handles or memory allocations opened in Open() should be cleaned up here. - * - * As the main memory freeing function, it's good practice to call this in Open() if there's an error that prevents - * correct function before Open() returns. As such, Close() should be prepared for not all memory/file handles to - * have been opened successfully. - */ - virtual void Close() = 0; - - /** - * @brief Get a media file's internal timestamp - * - * Used to determine which frame will be served at a given time, useful for caching. - */ - virtual int64_t GetTimestampFromTime(const rational& time) = 0; - - /** - * @brief Try to probe a Footage file by passing it through all available Decoders - * - * This is a helper function designed to abstract the process of communicating with several Decoders from the rest of - * the application. This function will take a Footage file and manually pass it through the available Decoders' Probe() - * functions until one indicates that it can decode this file. That Decoder will then dump information about the file - * into the Footage object for use throughout the program. - * - * Probing may be a lengthy process and it's recommended to run this in a separate thread. - * - * @param f - * - * A Footage object with a valid filename. If the Footage does not have a valid filename (e.g. is empty or file doesn't - * exist), this function will return FALSE. - * - * @return - * - * TRUE if a Decoder was successfully able to parse and probe this file. FALSE if not. - */ - static bool ProbeMedia(Footage* f); - - /** - * @brief Create a Decoder instance using a Decoder ID - * - * @return - * - * A Decoder instance or nullptr if a Decoder with this ID does not exist - */ - static DecoderPtr CreateFromID(const QString& id); - - /** - * @brief Conform an audio stream to match certain parameters (audio only) - * - * Resamples and converts the currently open audio to match the params. If the audio doesn't need conforming (e.g. - * audio params already match or a conformed match already exists), this function will return immediately. Otherwise - * it will block the calling thread until the conform is complete. This function should therefore only be called - * from a background render thread. - * - * All audio decoders must override this. It's not pure since video decoders don't need to use this, but default - * behavior will abort since it should never be called. - */ - virtual void Conform(const AudioRenderingParams& params); - -protected: - bool open_; - -private: - StreamPtr stream_; -}; - -#endif // DECODER_H diff --git a/app/decoder/ffmpeg/ffmpegdecoder.cpp b/app/decoder/ffmpeg/ffmpegdecoder.cpp deleted file mode 100644 index d84fad177..000000000 --- a/app/decoder/ffmpeg/ffmpegdecoder.cpp +++ /dev/null @@ -1,1026 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - 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 . - -***/ - -#include "ffmpegdecoder.h" - -extern "C" { -#include -#include -#include -} - -#include -#include -#include -#include -#include - -#include "common/filefunctions.h" -#include "common/timecodefunctions.h" -#include "decoder/waveinput.h" -#include "render/pixelservice.h" - -FFmpegDecoder::FFmpegDecoder() : - fmt_ctx_(nullptr), - codec_ctx_(nullptr), - opts_(nullptr), - scale_ctx_(nullptr) -{ -} - -FFmpegDecoder::~FFmpegDecoder() -{ - Close(); -} - -bool FFmpegDecoder::Open() -{ - if (open_) { - return true; - } - - int error_code; - - // Convert QString to a C strng - QByteArray ba = stream()->footage()->filename().toUtf8(); - const char* filename = ba.constData(); - - // Open file in a format context - error_code = avformat_open_input(&fmt_ctx_, filename, nullptr, nullptr); - - // Handle format context error - if (error_code != 0) { - FFmpegError(error_code); - return false; - } - - // Get stream information from format - error_code = avformat_find_stream_info(fmt_ctx_, nullptr); - - // Handle get stream information error - if (error_code < 0) { - FFmpegError(error_code); - return false; - } - - // Dump format information - av_dump_format(fmt_ctx_, stream()->index(), filename, 0); - - // Get reference to correct AVStream - avstream_ = fmt_ctx_->streams[stream()->index()]; - - // Find decoder - AVCodec* codec = avcodec_find_decoder(avstream_->codecpar->codec_id); - - // Handle failure to find decoder - if (codec == nullptr) { - Error(QStringLiteral("Failed to find appropriate decoder for this codec (%1 :: %2)") - .arg(stream()->footage()->filename(), avstream_->codecpar->codec_id)); - return false; - } - - // Allocate context for the decoder - codec_ctx_ = avcodec_alloc_context3(codec); - if (codec_ctx_ == nullptr) { - Error(QStringLiteral("Failed to allocate codec context (%1 :: %2)").arg(stream()->footage()->filename(), stream()->index())); - return false; - } - - // Copy parameters from the AVStream to the AVCodecContext - error_code = avcodec_parameters_to_context(codec_ctx_, avstream_->codecpar); - - // Handle failure to copy parameters - if (error_code < 0) { - FFmpegError(error_code); - return false; - } - - // enable multithreading on decoding - error_code = av_dict_set(&opts_, "threads", "auto", 0); - - // Handle failure to set multithreaded decoding - if (error_code < 0) { - FFmpegError(error_code); - return false; - } - - // Open codec - error_code = avcodec_open2(codec_ctx_, codec, &opts_); - if (error_code < 0) { - FFmpegError(error_code); - return false; - } - - // Set up - if (codec_ctx_->codec_type == AVMEDIA_TYPE_VIDEO) { - // Set up pixel format conversion for video - AVPixelFormat pix_fmt = static_cast(avstream_->codecpar->format); - - // Get an Olive compatible AVPixelFormat - AVPixelFormat ideal_pix_fmt = GetCompatiblePixelFormat(pix_fmt); - - // Determine which Olive native pixel format we retrieved - // Note that FFmpeg doesn't support float formats - switch (ideal_pix_fmt) { - case AV_PIX_FMT_RGBA: - output_fmt_ = olive::PIX_FMT_RGBA8; - break; - case AV_PIX_FMT_RGBA64: - output_fmt_ = olive::PIX_FMT_RGBA16U; - break; - default: - // We should never get here, but if we do there's nothing we can do with this format - return false; - } - - scale_ctx_ = sws_getContext(avstream_->codecpar->width, - avstream_->codecpar->height, - pix_fmt, - avstream_->codecpar->width, - avstream_->codecpar->height, - ideal_pix_fmt, - 0, - nullptr, - nullptr, - nullptr); - - } else if (codec_ctx_->codec_type == AVMEDIA_TYPE_AUDIO) { - // FIXME: Fill this in - } - - // All allocation succeeded so we set the state to open - open_ = true; - - return true; -} - -FramePtr FFmpegDecoder::RetrieveVideo(const rational &timecode) -{ - if (!open_ && !Open()) { - return nullptr; - } - - if (avstream_->codecpar->codec_type != AVMEDIA_TYPE_VIDEO) { - return nullptr; - } - - // Convert timecode to AVStream timebase - int64_t target_ts = GetTimestampFromTime(timecode); - - if (target_ts < 0) { - Error(QStringLiteral("Index failed to produce a valid timestamp")); - return nullptr; - } - - QFile compressed_frame(GetIndexFilename().append(QString::number(target_ts))); - if (compressed_frame.open(QFile::ReadOnly)) { - QByteArray frame_loader = qUncompress(compressed_frame.readAll()); - AVFrame* frame = av_frame_alloc(); - - if (frame == nullptr) { - qWarning() << "Failed to create AVFrame for swscale"; - return nullptr; - } - - frame->width = avstream_->codecpar->width; - frame->height = avstream_->codecpar->height; - - QDataStream ds(&frame_loader, QIODevice::ReadOnly); - ds >> frame->format; - - if (av_frame_get_buffer(frame, 0) != 0) { - qWarning() << "Failed to get AVFrame buffer"; - av_frame_free(&frame); - return nullptr; - } - - // Read data - size_t pos = sizeof(int); - for (int i=0;i(frame->linesize[i] * CalculatePlaneHeight(frame->height, static_cast(frame->format), i)); - memcpy(frame->data[i], frame_loader.data() + pos, plane_size); - pos += plane_size; - } - - // Frame was valid, now we convert it to a native Olive frame - FramePtr frame_container = Frame::Create(); - frame_container->set_width(frame->width); - frame_container->set_height(frame->height); - frame_container->set_format(static_cast(output_fmt_)); - frame_container->set_timestamp(olive::timestamp_to_time(frame->pts, avstream_->time_base)); - frame_container->allocate(); - - // Convert pixel format/linesize if necessary - uint8_t* dst_data = reinterpret_cast(frame_container->data()); - int dst_linesize = frame_container->width() * PixelService::BytesPerPixel(static_cast(output_fmt_)); - - // Perform pixel conversion - sws_scale(scale_ctx_, - frame->data, - frame->linesize, - 0, - frame->height, - &dst_data, - &dst_linesize); - - av_frame_free(&frame); - - return frame_container; - } - - return nullptr; -} - -FramePtr FFmpegDecoder::RetrieveAudio(const rational &timecode, const rational &length, const AudioRenderingParams ¶ms) -{ - if (!open_ && !Open()) { - return nullptr; - } - - if (avstream_->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) { - return nullptr; - } - - if (!LoadIndex()) { - Index(); - } - - Conform(params); - - WaveInput input(GetConformedFilename(params)); - - if (input.open()) { - const AudioRenderingParams& params = input.params(); - - FramePtr audio_frame = Frame::Create(); - audio_frame->set_audio_params(params); - audio_frame->set_sample_count(params.time_to_samples(length)); - audio_frame->allocate(); - - input.read(params.time_to_bytes(timecode), - audio_frame->data(), - audio_frame->allocated_size()); - - input.close(); - - return audio_frame; - } - - return nullptr; -} - -void FFmpegDecoder::Close() -{ - frame_index_.clear(); - - if (scale_ctx_ != nullptr) { - sws_freeContext(scale_ctx_); - scale_ctx_ = nullptr; - } - - if (opts_ != nullptr) { - av_dict_free(&opts_); - opts_ = nullptr; - } - - if (codec_ctx_ != nullptr) { - avcodec_free_context(&codec_ctx_); - codec_ctx_ = nullptr; - } - - if (fmt_ctx_ != nullptr) { - avformat_close_input(&fmt_ctx_); - fmt_ctx_ = nullptr; - } - - open_ = false; -} - -QString FFmpegDecoder::id() -{ - return "ffmpeg"; -} - -int64_t FFmpegDecoder::GetTimestampFromTime(const rational &time) -{ - if (!open_ && !Open()) { - return -1; - } - - // Convert timecode to AVStream timebase - int64_t target_ts = olive::time_to_timestamp(time, avstream_->time_base); - - // Find closest actual timebase in the file - target_ts = GetClosestTimestampInIndex(target_ts); - - return target_ts; -} - -void FFmpegDecoder::Conform(const AudioRenderingParams ¶ms) -{ - if (avstream_->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) { - // Nothing to be done - return; - } - - if (!LoadIndex()) { - Index(); - } - - // Get indexed WAV file - WaveInput input(GetIndexFilename()); - - if (input.open()) { - // If the parameters are equal, nothing to be done - // FIXME: Technically we only need to conform if the SAMPLE RATE is not equal. Format and channel layout conversion - // could be done on the fly so we could perhaps conform less often at some point. - if (input.params() == params) { - input.close(); - return; - } - - // Otherwise, let's start converting the format - - // Generate destination filename for this conversion to see if it exists - QString conformed_fn = GetConformedFilename(params); - - if (QFileInfo::exists(conformed_fn)) { - // We must have already conformed this format - input.close(); - return; - } - - // Set up resampler - SwrContext* resampler = swr_alloc_set_opts(nullptr, - static_cast(params.channel_layout()), - GetFFmpegSampleFormat(params.format()), - params.sample_rate(), - static_cast(input.params().channel_layout()), - GetFFmpegSampleFormat(input.params().format()), - input.params().sample_rate(), - 0, - nullptr); - - swr_init(resampler); - - WaveOutput conformed_output(conformed_fn, params); - if (!conformed_output.open()) { - qWarning() << "Failed to open conformed output:" << conformed_fn; - input.close(); - return; - } - - // Convert one second of audio at a time - int input_buffer_sz = input.params().time_to_bytes(1); - - while (!input.at_end()) { - // Read up to one second of audio from WAV file - QByteArray read_samples = input.read(input_buffer_sz); - - // Determine how many samples this is - int in_sample_count = input.params().bytes_to_samples(read_samples.size()); - - ConformInternal(resampler, &conformed_output, read_samples.data(), in_sample_count); - } - - // Flush resampler - ConformInternal(resampler, &conformed_output, nullptr, 0); - - // Clean up - swr_free(&resampler); - conformed_output.close(); - input.close(); - } else { - qWarning() << "Failed to conform file:" << stream()->footage()->filename(); - } -} - -bool FFmpegDecoder::SupportsVideo() -{ - return true; -} - -bool FFmpegDecoder::SupportsAudio() -{ - return true; -} - -void FFmpegDecoder::ConformInternal(SwrContext* resampler, WaveOutput* output, const char* in_data, int in_sample_count) -{ - // Determine how many samples the output will be - int out_sample_count = swr_get_out_samples(resampler, in_sample_count); - - // Allocate array for the amount of samples we'll need - QByteArray out_samples; - out_samples.resize(output->params().samples_to_bytes(out_sample_count)); - - char* out_data = out_samples.data(); - - // Convert samples - int convert_count = swr_convert(resampler, - reinterpret_cast(&out_data), - out_sample_count, - reinterpret_cast(&in_data), - in_sample_count); - - if (convert_count != out_sample_count) { - out_samples.resize(output->params().samples_to_bytes(convert_count)); - } - - output->write(out_samples); -} - -bool FFmpegDecoder::Probe(Footage *f) -{ - if (open_) { - qWarning() << "Probe must be called while the Decoder is closed"; - return false; - } - - // Variable for receiving errors from FFmpeg - int error_code; - - // Result to return - bool result = false; - - // Convert QString to a C strng - QByteArray ba = f->filename().toUtf8(); - const char* filename = ba.constData(); - - // Open file in a format context - error_code = avformat_open_input(&fmt_ctx_, filename, nullptr, nullptr); - - bool need_manual_duration = false; - - // Handle format context error - if (error_code == 0) { - - // Retrieve metadata about the media - av_dump_format(fmt_ctx_, 0, filename, 0); - - // Dump it into the Footage object - for (unsigned int i=0;inb_streams;i++) { - - avstream_ = fmt_ctx_->streams[i]; - - StreamPtr str; - - if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { - - // Create a video stream object - VideoStreamPtr video_stream = std::make_shared(); - - video_stream->set_width(avstream_->codecpar->width); - video_stream->set_height(avstream_->codecpar->height); - video_stream->set_frame_rate(av_guess_frame_rate(fmt_ctx_, avstream_, nullptr)); - - str = video_stream; - - } else if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { - - // Create an audio stream object - AudioStreamPtr audio_stream = std::make_shared(); - - audio_stream->set_layout(avstream_->codecpar->channel_layout); - audio_stream->set_channels(avstream_->codecpar->channels); - audio_stream->set_sample_rate(avstream_->codecpar->sample_rate); - - str = audio_stream; - - } else { - - // This is data we can't utilize at the moment, but we make a Stream object anyway to keep parity with the file - str = std::make_shared(); - - // Set the correct codec type based on FFmpeg's result - switch (avstream_->codecpar->codec_type) { - case AVMEDIA_TYPE_UNKNOWN: - str->set_type(Stream::kUnknown); - break; - case AVMEDIA_TYPE_DATA: - str->set_type(Stream::kData); - break; - case AVMEDIA_TYPE_SUBTITLE: - str->set_type(Stream::kSubtitle); - break; - case AVMEDIA_TYPE_ATTACHMENT: - str->set_type(Stream::kAttachment); - break; - default: - // We should never realistically get here, but we make an "invalid" stream just in case - str->set_type(Stream::kUnknown); - break; - } - - } - - str->set_index(avstream_->index); - str->set_timebase(avstream_->time_base); - str->set_duration(avstream_->duration); - - // The container/stream info may not contain a duration, so we'll need to manually retrieve it - if (avstream_->duration == AV_NOPTS_VALUE) { - need_manual_duration = true; - } - - f->add_stream(str); - } - - // As long as we can open the container and retrieve information, this was a successful probe - result = true; - } - - // Free all memory - Close(); - - // If the metadata did not contain a duration, we'll need to loop through the file to retrieve it - if (need_manual_duration) { - // Index the first stream to retrieve the duration - - set_stream(f->stream(0)); - - Open(); - - // Use index to find duration - // FIXME: Does nothing for sound - if (!LoadIndex()) { - Index(); - } - - // Use last frame index as the duration - // FIXME: Does this skip the last frame? - int64_t duration = frame_index_.last(); - - f->stream(0)->set_duration(duration); - - // Assume all durations are the same and set for each - for (int i=1;istream_count();i++) { - int64_t new_dur = av_rescale_q(duration, - f->stream(0)->timebase().toAVRational(), - f->stream(i)->timebase().toAVRational()); - - f->stream(i)->set_duration(new_dur); - } - - Close(); - } - - return result; -} - -void FFmpegDecoder::FFmpegError(int error_code) -{ - char err[1024]; - av_strerror(error_code, err, 1024); - - Error(QStringLiteral("Error decoding %1 - %2 %3").arg(stream()->footage()->filename(), - QString::number(error_code), - err)); -} - -void FFmpegDecoder::Error(const QString &s) -{ - qWarning() << s; - - Close(); -} - -void FFmpegDecoder::Index() -{ - if (!open_) { - qWarning() << "Indexing function tried to run while decoder was closed"; - return; - } - - // Allocate a packet and frame for decoding - AVPacket* pkt = av_packet_alloc(); - AVFrame* frame = av_frame_alloc(); - - if (pkt == nullptr || frame == nullptr) { - // Handle failure to allocate either - Error(QStringLiteral("Failed to allocate resources for indexing")); - } else { - // Reset state - Seek(0); - - if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { - IndexVideo(pkt, frame); - } else if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { - IndexAudio(pkt, frame); - } - - // Reset state - Seek(0); - } - - // Free resources - if (pkt != nullptr) - av_packet_free(&pkt); - - if (frame != nullptr) - av_frame_free(&frame); -} - -QString FFmpegDecoder::GetIndexFilename() -{ - if (!open_) { - qWarning() << "GetIndexFilename tried to run while decoder was closed"; - return QString(); - } - - return GetMediaIndexFilename(GetUniqueFileIdentifier(stream()->footage()->filename())) - .append(QString::number(avstream_->index)); -} - -QString FFmpegDecoder::GetConformedFilename(const AudioRenderingParams ¶ms) -{ - QString index_fn = GetIndexFilename(); - WaveInput input(GetIndexFilename()); - - if (input.open()) { - // If the parameters are equal, nothing to be done - AudioRenderingParams index_params = input.params(); - input.close(); - - if (index_params == params) { - // Source file matches perfectly, no conform required - return index_fn; - } - } - - index_fn.append('.'); - index_fn.append(QString::number(params.sample_rate())); - index_fn.append('.'); - index_fn.append(QString::number(params.format())); - index_fn.append('.'); - index_fn.append(QString::number(params.channel_layout())); - - return index_fn; -} - -bool FFmpegDecoder::LoadIndex() -{ - switch (avstream_->codecpar->codec_type) { - case AVMEDIA_TYPE_VIDEO: - { - // Load index from file - QFile index_file(GetIndexFilename()); - - if (!index_file.exists()) { - return false; - } - - if (index_file.open(QFile::ReadOnly)) { - // Resize based on filesize - frame_index_.resize(static_cast(static_cast(index_file.size()) / sizeof(int64_t))); - - // Read frame index into vector - index_file.read(reinterpret_cast(frame_index_.data()), - index_file.size()); - - index_file.close(); - - return true; - } - break; - } - case AVMEDIA_TYPE_AUDIO: - { - return QFileInfo::exists(GetIndexFilename()); - } - default: - break; - } - - return false; -} - -void FFmpegDecoder::SaveIndex() -{ - // Save index to file - QFile index_file(GetIndexFilename()); - if (index_file.open(QFile::WriteOnly)) { - // Write index in binary - index_file.write(reinterpret_cast(frame_index_.constData()), - frame_index_.size() * static_cast(sizeof(int64_t))); - - index_file.close(); - } else { - qWarning() << QStringLiteral("Failed to save index for %1").arg(stream()->footage()->filename()); - } -} - -void FFmpegDecoder::IndexAudio(AVPacket *pkt, AVFrame *frame) -{ - // Iterate through each audio frame and extract the PCM data - - uint64_t channel_layout = avstream_->codecpar->channel_layout; - if (!channel_layout) { - if (!avstream_->codecpar->channels) { - // No channel data - we can't do anything with this - return; - } - - channel_layout = static_cast(av_get_default_channel_layout(avstream_->codecpar->channels)); - } - - SwrContext* resampler = nullptr; - AVSampleFormat src_sample_fmt = static_cast(avstream_->codecpar->format); - AVSampleFormat dst_sample_fmt; - - // We don't use planar types internally, so if this is a planar format convert it now - if (av_sample_fmt_is_planar(src_sample_fmt)) { - dst_sample_fmt = av_get_packed_sample_fmt(src_sample_fmt); - - resampler = swr_alloc_set_opts(nullptr, - static_cast(avstream_->codecpar->channel_layout), - dst_sample_fmt, - avstream_->codecpar->sample_rate, - static_cast(avstream_->codecpar->channel_layout), - src_sample_fmt, - avstream_->codecpar->sample_rate, - 0, - nullptr); - } else { - dst_sample_fmt = src_sample_fmt; - } - - WaveOutput wave_out(GetIndexFilename(), - AudioRenderingParams(avstream_->codecpar->sample_rate, - channel_layout, - GetNativeSampleFormat(dst_sample_fmt))); - - int ret; - - if (wave_out.open()) { - while (true) { - ret = GetFrame(pkt, frame); - - if (ret < 0) { - break; - } else { - AVFrame* data_frame; - - if (resampler != nullptr) { - // We must need to resample this (mainly just convert from planar to packed if necessary) - data_frame = av_frame_alloc(); - data_frame->sample_rate = frame->sample_rate; - data_frame->channel_layout = frame->channel_layout; - data_frame->channels = frame->channels; - data_frame->format = dst_sample_fmt; - av_frame_make_writable(data_frame); - - int ret = swr_convert_frame(resampler, data_frame, frame); - - if (ret != 0) { - char err_str[50]; - av_strerror(ret, err_str, 50); - qWarning() << "libswresample failed with error:" << ret << err_str; - } - } else { - // No resampling required, we can write directly from te frame buffer - data_frame = frame; - } - - int buffer_sz = av_samples_get_buffer_size(nullptr, - avstream_->codecpar->channels, - data_frame->nb_samples, - dst_sample_fmt, - 0); // FIXME: Documentation unclear - should this be 0 or 1? - - // Write packed WAV data to the disk cache - wave_out.write(reinterpret_cast(data_frame->data[0]), buffer_sz); - - // If we allocated an output for the resampler, delete it here - if (data_frame != frame) { - av_frame_free(&data_frame); - } - } - } - - wave_out.close(); - } else { - qWarning() << "Failed to open WAVE output for indexing"; - } - - if (resampler != nullptr) { - swr_free(&resampler); - } -} - -void FFmpegDecoder::IndexVideo(AVPacket* pkt, AVFrame* frame) -{ - // This should be unnecessary, but just in case... - frame_index_.clear(); - - // Iterate through every single frame and get each timestamp - // NOTE: Expects no frames to have been read so far - - int ret; - - while (true) { - ret = GetFrame(pkt, frame); - - if (ret >= 0) { - // Save frame - QByteArray frame_save; - QDataStream ds(&frame_save, QIODevice::WriteOnly); - - ds << frame->format; - - // Save data - for (int i=0;i(frame->data[i]), - frame->linesize[i] * CalculatePlaneHeight(frame->height, static_cast(frame->format), i)); - } - - QFile compressed_frame(GetIndexFilename().append(QString::number(frame->pts))); - if (compressed_frame.open(QFile::WriteOnly)) { - compressed_frame.write(qCompress(frame_save)); - compressed_frame.close(); - } - - frame_index_.append(frame->pts); - } else { - // Assume we've reached the end of the file - break; - } - } - - // Save index to file - SaveIndex(); -} - -int FFmpegDecoder::GetFrame(AVPacket *pkt, AVFrame *frame) -{ - bool eof = false; - - int ret; - - // Clear any previous frames - av_frame_unref(frame); - - while ((ret = avcodec_receive_frame(codec_ctx_, frame)) == AVERROR(EAGAIN) && !eof) { - - // Find next packet in the correct stream index - do { - // Free buffer in packet if there is one - av_packet_unref(pkt); - - // Read packet from file - ret = av_read_frame(fmt_ctx_, pkt); - } while (pkt->stream_index != avstream_->index && ret >= 0); - - if (ret == AVERROR_EOF) { - // Don't break so that receive gets called again, but don't try to read again - eof = true; - - // Send a null packet to signal end of - avcodec_send_packet(codec_ctx_, nullptr); - } else if (ret < 0) { - // Handle other error - break; - } else { - // Successful read, send the packet - ret = avcodec_send_packet(codec_ctx_, pkt); - - // We don't need the packet anymore, so free it - av_packet_unref(pkt); - - if (ret < 0) { - break; - } - } - } - - return ret; -} - -AVPixelFormat FFmpegDecoder::GetCompatiblePixelFormat(const AVPixelFormat &pix_fmt) -{ - AVPixelFormat possible_pix_fmts[] = { - AV_PIX_FMT_RGBA, - AV_PIX_FMT_RGBA64, - AV_PIX_FMT_NONE - }; - - return avcodec_find_best_pix_fmt_of_list(possible_pix_fmts, - pix_fmt, - 1, - nullptr); -} - -SampleFormat FFmpegDecoder::GetNativeSampleFormat(const AVSampleFormat &smp_fmt) -{ - switch (smp_fmt) { - case AV_SAMPLE_FMT_U8: - return SAMPLE_FMT_U8; - case AV_SAMPLE_FMT_S16: - return SAMPLE_FMT_S16; - case AV_SAMPLE_FMT_S32: - return SAMPLE_FMT_S32; - case AV_SAMPLE_FMT_S64: - return SAMPLE_FMT_S64; - case AV_SAMPLE_FMT_FLT: - return SAMPLE_FMT_FLT; - case AV_SAMPLE_FMT_DBL: - return SAMPLE_FMT_DBL; - case AV_SAMPLE_FMT_U8P : - case AV_SAMPLE_FMT_S16P: - case AV_SAMPLE_FMT_S32P: - case AV_SAMPLE_FMT_S64P: - case AV_SAMPLE_FMT_FLTP: - case AV_SAMPLE_FMT_DBLP: - case AV_SAMPLE_FMT_NONE: - case AV_SAMPLE_FMT_NB: - break; - } - - return SAMPLE_FMT_INVALID; -} - -AVSampleFormat FFmpegDecoder::GetFFmpegSampleFormat(const SampleFormat &smp_fmt) -{ - switch (smp_fmt) { - case SAMPLE_FMT_U8: - return AV_SAMPLE_FMT_U8; - case SAMPLE_FMT_S16: - return AV_SAMPLE_FMT_S16; - case SAMPLE_FMT_S32: - return AV_SAMPLE_FMT_S32; - case SAMPLE_FMT_S64: - return AV_SAMPLE_FMT_S64; - case SAMPLE_FMT_FLT: - return AV_SAMPLE_FMT_FLT; - case SAMPLE_FMT_DBL: - return AV_SAMPLE_FMT_DBL; - case SAMPLE_FMT_INVALID: - case SAMPLE_FMT_COUNT: - break; - } - - return AV_SAMPLE_FMT_NONE; -} - -int FFmpegDecoder::CalculatePlaneHeight(int frame_height, const AVPixelFormat &format, int plane) -{ - // FIXME: This seems dumb, but I can't find any FFmpeg function that returns this information - - if ((plane == 1 || plane == 2) - && format == AV_PIX_FMT_YUV420P) { - return frame_height/2; - } - return frame_height; -} - -int64_t FFmpegDecoder::GetClosestTimestampInIndex(const int64_t &ts) -{ - // Index now if we haven't already - if (frame_index_.isEmpty() && !LoadIndex()) { - Index(); - } - - if (frame_index_.isEmpty()) { - return -1; - } - - if (ts <= 0) { - return frame_index_.first(); - } - - // Use index to find closest frame in file - for (int i=1;i ts) { - return frame_index_.at(i - 1); - } - } - - return frame_index_.last(); -} - -void FFmpegDecoder::Seek(int64_t timestamp) -{ - avcodec_flush_buffers(codec_ctx_); - av_seek_frame(fmt_ctx_, avstream_->index, timestamp, AVSEEK_FLAG_BACKWARD); -} diff --git a/app/decoder/ffmpeg/ffmpegdecoder.h b/app/decoder/ffmpeg/ffmpegdecoder.h deleted file mode 100644 index f3604d97e..000000000 --- a/app/decoder/ffmpeg/ffmpegdecoder.h +++ /dev/null @@ -1,165 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - 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 . - -***/ - -#ifndef FFMPEGDECODER_H -#define FFMPEGDECODER_H - -extern "C" { -#include -#include -#include -} - -#include - -#include "audio/sampleformat.h" -#include "decoder/decoder.h" -#include "decoder/waveoutput.h" - -/** - * @brief A Decoder derivative that wraps FFmpeg functions as on Olive decoder - */ -class FFmpegDecoder : public Decoder -{ -public: - // Constructor - FFmpegDecoder(); - - // Destructor - virtual ~FFmpegDecoder() override; - - virtual bool Probe(Footage *f) override; - - virtual bool Open() override; - virtual FramePtr RetrieveVideo(const rational &timecode) override; - virtual FramePtr RetrieveAudio(const rational &timecode, const rational &length, const AudioRenderingParams& params) override; - virtual void Close() override; - - virtual QString id() override; - - virtual int64_t GetTimestampFromTime(const rational& time) override; - - virtual void Conform(const AudioRenderingParams& params) override; - - virtual bool SupportsVideo() override; - virtual bool SupportsAudio() override; - -private: - void ConformInternal(SwrContext *resampler, WaveOutput *output, const char *in_data, int in_sample_count); - - /** - * @brief Handle an error - * - * Immediately closes the Decoder (freeing memory resources) and sends the string provided to the warning stream. - * As this function closes the Decoder, no further Decoder functions should be performed after this is called - * (unless the Decoder is opened again first). - */ - void Error(const QString& s); - - /** - * @brief Handle an FFmpeg error code - * - * Uses the FFmpeg API to retrieve a descriptive string for this error code and sends it to Error(). As such, this - * function also automatically closes the Decoder. - * - * @param error_code - */ - void FFmpegError(int error_code); - - /** - * @brief Uses the FFmpeg API to retrieve a packet (stored in pkt_) and decode it (stored in frame_) - * - * @return - * - * An FFmpeg error code, or >= 0 on success - */ - int GetFrame(AVPacket* pkt, AVFrame* frame); - - /** - * @brief Create an index for this media - * - * Indexes are used to improve speed and reliability of imported media. Calling Retrieve() will automatically check - * for an index and create one if it doesn't exist. - * - * Indexing is slow so it's recommended to do it in a background thread. Index() must be called while the Decoder is - * open, and does not automatically call Open() and Close() the Decoder. The caller must call thse manually. - * - * FIXME: This should perhaps become a common function for the base Decoder class - */ - void Index(); - - /** - * @brief Returns the filename for the index - * - * Retrieves the absolute filename of the index file for this stream. Decoder must be open for this to work correctly. - */ - QString GetIndexFilename(); - - /** - * @brief Get the destination filename of an audio stream conformed to a set of parameters - */ - QString GetConformedFilename(const AudioRenderingParams ¶ms); - - /** - * @brief Used internally to load a frame index into frame_index_ - * - * @return - * - * TRUE if a frame index was successfully loaded. FALSE usually means the file didn't exist and Index() should be - * run to create it. - */ - bool LoadIndex(); - - /** - * @brief Used in Index() to save the just created frame index to a file that can be loaded later - */ - void SaveIndex(); - - void IndexAudio(AVPacket* pkt, AVFrame* frame); - void IndexVideo(AVPacket* pkt, AVFrame* frame); - - int64_t GetClosestTimestampInIndex(const int64_t& ts); - - void Seek(int64_t timestamp); - - /** - * @brief Returns an AVPixelFormat that can be used in Olive and causes minimal data loss - */ - AVPixelFormat GetCompatiblePixelFormat(const AVPixelFormat& pix_fmt); - - SampleFormat GetNativeSampleFormat(const AVSampleFormat& smp_fmt); - - AVSampleFormat GetFFmpegSampleFormat(const SampleFormat& smp_fmt); - - int CalculatePlaneHeight(int frame_height, const AVPixelFormat& format, int plane); - - AVFormatContext* fmt_ctx_; - AVCodecContext* codec_ctx_; - AVStream* avstream_; - AVDictionary* opts_; - - SwsContext* scale_ctx_; - int output_fmt_; - - QVector frame_index_; - -}; - -#endif // FFMPEGDECODER_H diff --git a/app/decoder/frame.cpp b/app/decoder/frame.cpp deleted file mode 100644 index 6b564183d..000000000 --- a/app/decoder/frame.cpp +++ /dev/null @@ -1,146 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - 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 . - -***/ - -#include "frame.h" - -#include -#include - -#include "render/pixelservice.h" - -Frame::Frame() : - width_(0), - height_(0), - format_(olive::PIX_FMT_INVALID), - sample_count_(0), - timestamp_(0), - native_timestamp_(0) -{ -} - -FramePtr Frame::Create() -{ - return std::make_shared(); -} - -const int &Frame::width() -{ - return width_; -} - -void Frame::set_width(const int &width) -{ - width_ = width; -} - -const int &Frame::height() -{ - return height_; -} - -void Frame::set_height(const int &height) -{ - height_ = height; -} - -const AudioRenderingParams &Frame::audio_params() -{ - return audio_params_; -} - -void Frame::set_audio_params(const AudioRenderingParams ¶ms) -{ - audio_params_ = params; -} - -const rational &Frame::timestamp() -{ - return timestamp_; -} - -void Frame::set_timestamp(const rational ×tamp) -{ - timestamp_ = timestamp; -} - -/*const int64_t &Frame::native_timestamp() -{ - return native_timestamp_; -} - -void Frame::set_native_timestamp(const int64_t ×tamp) -{ - native_timestamp_ = timestamp; -}*/ - -const olive::PixelFormat &Frame::format() -{ - return format_; -} - -void Frame::set_format(const olive::PixelFormat &format) -{ - format_ = format; -} - -QByteArray Frame::ToByteArray() -{ - return data_; -} - -const int &Frame::sample_count() -{ - return sample_count_; -} - -void Frame::set_sample_count(const int &audio_sample_count) -{ - sample_count_ = audio_sample_count; -} - -char *Frame::data() -{ - return data_.data(); -} - -const char *Frame::const_data() -{ - return data_.constData(); -} - -void Frame::allocate() -{ - // Assume this frame is intended to be a video frame - if (width_ > 0 && height_ > 0) { - data_.resize(PixelService::GetBufferSize(static_cast(format_), width_, height_)); - } else if (sample_count_ > 0) { - data_.resize(audio_params_.samples_to_bytes(sample_count_)); - } -} - -void Frame::destroy() -{ - data_.clear(); -} - -int Frame::allocated_size() const -{ - return data_.size(); -} diff --git a/app/decoder/oiio/oiiodecoder.cpp b/app/decoder/oiio/oiiodecoder.cpp deleted file mode 100644 index 7b304b1a2..000000000 --- a/app/decoder/oiio/oiiodecoder.cpp +++ /dev/null @@ -1,153 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - 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 . - -***/ - -#include "oiiodecoder.h" - -#include - -#include "common/define.h" - -OIIODecoder::OIIODecoder() : - image_(nullptr), - frame_(nullptr) -{ -} - -QString OIIODecoder::id() -{ - return "oiio"; -} - -bool OIIODecoder::Probe(Footage *f) -{ - auto in = OIIO::ImageInput::open(f->filename().toStdString()); - - if (!in) { - return false; - } - - if (!strcmp(in->format_name(), "FFmpeg movie")) { - // If this is FFmpeg via OIIO, fall-through to our native FFmpeg decoder - return false; - } - - // Get stats for this image and dump them into the Footage file - const OIIO::ImageSpec& spec = in->spec(); - - ImageStreamPtr image_stream = std::make_shared(); - image_stream->set_width(spec.width); - image_stream->set_height(spec.height); - - f->add_stream(image_stream); - - // If we're here, we have a successful image open - in->close(); - - return true; -} - -bool OIIODecoder::Open() -{ - image_ = OIIO::ImageInput::open(stream()->footage()->filename().toStdString()); - - if (!image_) { - return false; - } - - // Check if we can work with this pixel format - const OIIO::ImageSpec& spec = image_->spec(); - - width_ = spec.width; - height_ = spec.height; - - // Weirdly, switch statement doesn't work correctly here - if (spec.format == OIIO::TypeDesc::UINT8) { - pix_fmt_ = olive::PIX_FMT_RGBA8; - } else if (spec.format == OIIO::TypeDesc::UINT16) { - pix_fmt_ = olive::PIX_FMT_RGBA16U; - } else if (spec.format == OIIO::TypeDesc::HALF) { - pix_fmt_ = olive::PIX_FMT_RGBA16F; - } else if (spec.format == OIIO::TypeDesc::FLOAT) { - pix_fmt_ = olive::PIX_FMT_RGBA32F; - } else { - qWarning() << "Failed to convert OIIO::ImageDesc to native pixel format"; - return false; - } - - // FIXME: Many OIIO pixel formats are not handled here - - is_rgba_ = (spec.nchannels == kRGBAChannels); - - pix_fmt_info_ = PixelService::GetPixelFormatInfo(static_cast(pix_fmt_)); - - return true; -} - -FramePtr OIIODecoder::RetrieveVideo(const rational &timecode) -{ - if (!open_ && !Open()) { - return nullptr; - } - - Q_UNUSED(timecode) - - if (frame_ == nullptr) { - frame_ = Frame::Create(); - - frame_->set_width(width_); - frame_->set_height(height_); - frame_->set_format(pix_fmt_); - frame_->allocate(); - - // Use the native format to determine what format OIIO should return - // FIXME: Behavior of RGB images as opposed to RGBA? - image_->read_image(pix_fmt_info_.oiio_desc, frame_->data()); - - if (!is_rgba_) { - PixelService::ConvertRGBtoRGBA(frame_); - } - } - - return frame_; -} - -void OIIODecoder::Close() -{ - if (image_ != nullptr) { - image_->close(); - image_ = nullptr; - } - - frame_ = nullptr; -} - -int64_t OIIODecoder::GetTimestampFromTime(const rational &time) -{ - Q_UNUSED(time) - - // A still image will always return the same frame - - return 0; -} - -bool OIIODecoder::SupportsVideo() -{ - return true; -} diff --git a/app/decoder/waveinput.cpp b/app/decoder/waveinput.cpp deleted file mode 100644 index fab020ee4..000000000 --- a/app/decoder/waveinput.cpp +++ /dev/null @@ -1,202 +0,0 @@ -#include "waveinput.h" - -extern "C" { -#include -} - -#include - -WaveInput::WaveInput(const QString &f) : - file_(f) -{ -} - -WaveInput::~WaveInput() -{ - close(); -} - -bool WaveInput::open() -{ - if (!file_.open(QFile::ReadOnly)) { - return false; - } - - if (file_.read(4) != "RIFF") { - close(); - qDebug() << "No RIFF found"; - return false; - } - - // Skip filesize bytes - file_.seek(file_.pos() + 4); - - if (file_.read(4) != "WAVE") { - close(); - qDebug() << "No WAVE found"; - return false; - } - - // Find fmt_ section - if (!find_str(&file_, "fmt ")) { - close(); - qDebug() << "No fmt found"; - return false; - } - - // Skip fmt_ section size - file_.seek(file_.pos()+4); - - // Create data stream for reading bytes into types - QDataStream data_stream(&file_); - data_stream.setByteOrder(QDataStream::LittleEndian); - - // Read data type - uint16_t data_type; - data_stream >> data_type; - - bool data_is_float; - switch (data_type) { - case 1: // PCM Integer - data_is_float = false; - break; - case 3: - data_is_float = true; - break; - default: - // If it's neither float nor int, we can't work with this file - close(); - qDebug() << "Invalid WAV type" << data_type; - return false; - } - - // Read number of channels - uint16_t channel_count; - data_stream >> channel_count; - - uint64_t channel_layout = static_cast(av_get_default_channel_layout(channel_count)); - - int32_t sample_rate; - data_stream >> sample_rate; - - // Skip bytes per second value and bytes per sample value - file_.seek(file_.pos() + 6); - - uint16_t bits_per_sample; - data_stream >> bits_per_sample; - - SampleFormat format; - - switch (bits_per_sample) { - case 8: - format = SAMPLE_FMT_U8; - break; - case 16: - format = SAMPLE_FMT_S16; - break; - case 32: - if (data_is_float) { - format = SAMPLE_FMT_FLT; - } else { - format = SAMPLE_FMT_S32; - } - break; - case 64: - if (data_is_float) { - format = SAMPLE_FMT_DBL; - } else { - format = SAMPLE_FMT_S64; - } - break; - default: - // We don't know this format... - close(); - qDebug() << "Invalid format found" << bits_per_sample; - return false; - } - - // We're good to go! - params_ = AudioRenderingParams(sample_rate, channel_layout, format); - - if (!find_str(&file_, "data")) { - close(); - qDebug() << "No data tag found"; - return false; - } - - data_stream >> data_size_; - data_position_ = file_.pos(); - - return true; -} - -bool WaveInput::is_open() const -{ - return file_.isOpen(); -} - -QByteArray WaveInput::read(int length) -{ - if (!is_open()) { - return QByteArray(); - } - - return file_.read(length); -} - -QByteArray WaveInput::read(int offset, int length) -{ - if (!is_open()) { - return QByteArray(); - } - - file_.seek(offset + data_position_); - return file_.read(length); -} - -void WaveInput::read(int offset, char *buffer, int length) -{ - if (!is_open()) { - return; - } - - file_.seek(offset + data_position_); - file_.read(buffer, length); -} - -bool WaveInput::at_end() const -{ - return file_.atEnd(); -} - -const AudioRenderingParams &WaveInput::params() const -{ - return params_; -} - -void WaveInput::close() -{ - if (file_.isOpen()) { - file_.close(); - } -} - -int WaveInput::sample_count() const -{ - return params_.bytes_to_samples(static_cast(data_size_)); -} - -bool WaveInput::find_str(QFile *f, const char *str) -{ - qint64 pos = f->pos(); - while (f->read(4) != str) { - if (f->atEnd()) { - return false; - } - - pos++; - f->seek(pos); - } - - return true; -} diff --git a/app/decoder/waveinput.h b/app/decoder/waveinput.h deleted file mode 100644 index d25271c1c..000000000 --- a/app/decoder/waveinput.h +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef WAVEINPUT_H -#define WAVEINPUT_H - -#include - -#include "common/constructors.h" -#include "render/audioparams.h" - -class WaveInput -{ -public: - WaveInput(const QString& f); - - ~WaveInput(); - - DISABLE_COPY_MOVE(WaveInput) - - bool open(); - - bool is_open() const; - - QByteArray read(int length); - QByteArray read(int offset, int length); - void read(int offset, char *buffer, int length); - - bool at_end() const; - - const AudioRenderingParams& params() const; - - void close(); - - int sample_count() const; - -private: - bool find_str(QFile* f, const char* str); - - AudioRenderingParams params_; - - QFile file_; - - qint64 data_position_; - - quint32 data_size_; -}; - -#endif // WAVEINPUT_H diff --git a/app/decoder/waveoutput.cpp b/app/decoder/waveoutput.cpp deleted file mode 100644 index 34f73407b..000000000 --- a/app/decoder/waveoutput.cpp +++ /dev/null @@ -1,149 +0,0 @@ -#include "waveoutput.h" - -const int16_t kWAVIntegerFormat = 1; -const int16_t kWAVFloatFormat = 3; - -WaveOutput::WaveOutput(const QString &f, - const AudioRenderingParams& params) : - file_(f), - params_(params) -{ - Q_ASSERT(params_.is_valid()); -} - -WaveOutput::~WaveOutput() -{ - close(); -} - -bool WaveOutput::open() -{ - data_length_ = 0; - - if (file_.open(QFile::WriteOnly)) { - // RIFF header - file_.write("RIFF"); - - // Total file size minus RIFF and this integer (minus 8 bytes, filled in later) - write_int(&file_, 0); - - // File type header - file_.write("WAVE"); - - // Begin format descriptor chunk - file_.write("fmt "); - - // Format chunk size - write_int(&file_, 16); - - // Type of format - switch (params_.format()) { - case SAMPLE_FMT_U8: - case SAMPLE_FMT_S16: - case SAMPLE_FMT_S32: - case SAMPLE_FMT_S64: - write_int(&file_, kWAVIntegerFormat); - break; - case SAMPLE_FMT_FLT: - case SAMPLE_FMT_DBL: - write_int(&file_, kWAVFloatFormat); - break; - case SAMPLE_FMT_INVALID: - case SAMPLE_FMT_COUNT: - qWarning() << "Invalid sample format for WAVE audio"; - file_.close(); - return false; - } - - // Number of channels - write_int(&file_, static_cast(params_.channel_count())); - - // Sample rate - write_int(&file_, params_.sample_rate()); - - // Bytes per second - write_int(&file_, params_.samples_to_bytes(params_.sample_rate())); - - // Bytes per sample - write_int(&file_, static_cast(params_.samples_to_bytes(1))); - - // Bits per sample per channel - write_int(&file_, static_cast(params_.bits_per_sample())); - - // Data chunk header - file_.write("data"); - - // Size of data chunk (filled in later) - write_int(&file_, 0); - - return true; - } - - return false; -} - -void WaveOutput::write(const QByteArray &bytes) -{ - if (file_.isOpen()) { - file_.write(bytes); - - data_length_ += bytes.size(); - } -} - -void WaveOutput::write(const char *bytes, int length) -{ - if (file_.isOpen()) { - file_.write(bytes, length); - - data_length_ += length; - } -} - -void WaveOutput::close() -{ - if (file_.isOpen()) { - - // Write file sizes - file_.seek(4); - write_int(&file_, data_length_ + 36); - - file_.seek(40); - write_int(&file_, data_length_); - - file_.close(); - } -} - -const AudioRenderingParams &WaveOutput::params() const -{ - return params_; -} - -void WaveOutput::switch_endianness(QByteArray& array) -{ - int half_sz = array.size()/2; - - for (int i=0;i -void WaveOutput::write_int(QFile *file, T integer) -{ - QByteArray bytes; - bytes.resize(sizeof(T)); - memcpy(bytes.data(), &integer, static_cast(bytes.size())); - - // WAV expects little-endian, so if the integer is big endian we need to switch - if (QSysInfo::ByteOrder == QSysInfo::BigEndian) { - switch_endianness(bytes); - } - - file->write(bytes); -} diff --git a/app/decoder/waveoutput.h b/app/decoder/waveoutput.h deleted file mode 100644 index 43b45c534..000000000 --- a/app/decoder/waveoutput.h +++ /dev/null @@ -1,44 +0,0 @@ -#ifndef WAVEAUDIO_H -#define WAVEAUDIO_H - -#include -#include - -#include "audio/sampleformat.h" -#include "common/constructors.h" -#include "render/audioparams.h" - -class WaveOutput -{ -public: - WaveOutput(const QString& f, - const AudioRenderingParams& params); - - ~WaveOutput(); - - DISABLE_COPY_MOVE(WaveOutput) - - bool open(); - - void write(const QByteArray& bytes); - void write(const char* bytes, int length); - - void close(); - - const AudioRenderingParams& params() const; - -private: - template - void write_int(QFile* file, T integer); - - void switch_endianness(QByteArray &array); - - QFile file_; - - AudioRenderingParams params_; - - int data_length_; - -}; - -#endif // WAVEAUDIO_H diff --git a/app/dialog/CMakeLists.txt b/app/dialog/CMakeLists.txt index 0e8af7660..a7ecf0f78 100644 --- a/app/dialog/CMakeLists.txt +++ b/app/dialog/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2019 Olive Team +# Copyright (C) 2021 Olive Team # # 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 @@ -16,10 +16,25 @@ add_subdirectory(about) add_subdirectory(actionsearch) +add_subdirectory(autorecovery) +add_subdirectory(color) +add_subdirectory(configbase) +add_subdirectory(diskcache) +add_subdirectory(export) add_subdirectory(footageproperties) +add_subdirectory(footagerelink) +add_subdirectory(keyframeproperties) +if(OpenTimelineIO_FOUND) + add_subdirectory(otioproperties) +endif() add_subdirectory(preferences) +add_subdirectory(progress) add_subdirectory(projectproperties) +add_subdirectory(rendercancel) add_subdirectory(sequence) +add_subdirectory(speedduration) +add_subdirectory(task) +add_subdirectory(text) set(OLIVE_SOURCES ${OLIVE_SOURCES} diff --git a/app/dialog/about/CMakeLists.txt b/app/dialog/about/CMakeLists.txt index c4a932e77..707a0bbdc 100644 --- a/app/dialog/about/CMakeLists.txt +++ b/app/dialog/about/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2019 Olive Team +# Copyright (C) 2021 Olive Team # # 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 @@ -16,7 +16,10 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} - dialog/about/about.h dialog/about/about.cpp + dialog/about/about.h + dialog/about/patreon.h + dialog/about/scrollinglabel.cpp + dialog/about/scrollinglabel.h PARENT_SCOPE ) diff --git a/app/dialog/about/about.cpp b/app/dialog/about/about.cpp index 76c6fd941..e3500e217 100644 --- a/app/dialog/about/about.cpp +++ b/app/dialog/about/about.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -21,45 +21,121 @@ #include "about.h" #include -#include #include +#include #include -AboutDialog::AboutDialog(QWidget *parent) : +#include "common/qtutils.h" +#include "config/config.h" +#include "patreon.h" +#include "scrollinglabel.h" + +namespace olive { + +AboutDialog::AboutDialog(bool welcome_dialog, QWidget *parent) : QDialog(parent) { - setWindowTitle(tr("About %1").arg(QApplication::applicationName())); + if (welcome_dialog) { + setWindowTitle(tr("Welcome to %1").arg(QApplication::applicationName())); + } else { + setWindowTitle(tr("About %1").arg(QApplication::applicationName())); + } + + QFontMetrics fm = fontMetrics(); QVBoxLayout* layout = new QVBoxLayout(this); - //layout->setSpacing(20); + layout->setMargin(fm.height()); + + QHBoxLayout *horiz_layout = new QHBoxLayout(); + horiz_layout->setMargin(fm.height()); + horiz_layout->setSpacing(fm.height()*2); + + QLabel* icon = new QLabel(QStringLiteral("")); + icon->setAlignment(Qt::AlignCenter); + horiz_layout->addWidget(icon); // Construct About text QLabel* label = - new QLabel(QString("" - "

" - "

" - "" - "https://www.olivevideoeditor.org/" - "

" - "

%1

" // AppName (version identifier) - "

%2

" // First statement - "

%3

" // Second statement - "").arg(QApplication::applicationName(), - tr("Olive is a non-linear video editor. This software is free and " - "protected by the GNU GPL."), - tr("Olive Team is obliged to inform users that Olive source code is " - "available for download from its website.")),this); + new QLabel(QStringLiteral("" + "

%1 %2

" // AppName (version identifier) + "

" + "https://www.olivevideoeditor.org/" + "

" + "

%3

" // First statement + "").arg(QApplication::applicationName(), + QApplication::applicationVersion(), + tr("Olive is a free open source non-linear video editor. " + "This software is licensed under the GNU GPL Version 3."))); // Set text formatting - label->setAlignment(Qt::AlignCenter); - label->setTextInteractionFlags(Qt::TextSelectableByMouse); - label->setCursor(Qt::IBeamCursor); + label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); label->setWordWrap(true); - layout->addWidget(label); + label->setOpenExternalLinks(true); + label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + horiz_layout->addWidget(label); + + layout->addLayout(horiz_layout); + + // Patrons where possible + layout->addWidget(new QLabel()); + + QString opening_statement; + + if (welcome_dialog || patrons.isEmpty()) { + opening_statement = tr("Olive relies on support from the community to continue its development."); + } else { + opening_statement = tr("Olive wouldn't be possible without the support of gracious donations from the following people."); + } + + QLabel* support_lbl = new QLabel(tr("%1 " + "If you like this project, please consider making a " + "one-time donation or " + "pledging monthly to " + "support its development.").arg(opening_statement)); + support_lbl->setWordWrap(true); + support_lbl->setAlignment(Qt::AlignCenter); + support_lbl->setOpenExternalLinks(true); + layout->addWidget(support_lbl); + + if (!patrons.isEmpty()) { + ScrollingLabel* scroll = new ScrollingLabel(patrons); + scroll->StartAnimating(); + layout->addWidget(scroll); + } + + layout->addWidget(new QLabel()); + + QHBoxLayout *btn_layout = new QHBoxLayout(); + btn_layout->setMargin(0); + btn_layout->setSpacing(0); + + if (welcome_dialog) { + dont_show_again_checkbox_ = new QCheckBox(tr("Don't show this message again")); + btn_layout->addWidget(dont_show_again_checkbox_); + } else { + dont_show_again_checkbox_ = nullptr; + } QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok, this); - buttons->setCenterButtons(true); - layout->addWidget(buttons); + if (!welcome_dialog) { + buttons->setCenterButtons(true); + } + btn_layout->addWidget(buttons); + + layout->addLayout(btn_layout); + + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + + setFixedSize(sizeHint()); +} + +void AboutDialog::accept() +{ + if (dont_show_again_checkbox_ && dont_show_again_checkbox_->isChecked()) { + Config::Current()[QStringLiteral("ShowWelcomeDialog")] = false; + } + + QDialog::accept(); +} - connect(buttons, SIGNAL(accepted()), this, SLOT(accept())); } diff --git a/app/dialog/about/about.h b/app/dialog/about/about.h index 1c4839b56..cc6197f15 100644 --- a/app/dialog/about/about.h +++ b/app/dialog/about/about.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -21,8 +21,13 @@ #ifndef ABOUTDIALOG_H #define ABOUTDIALOG_H +#include #include +#include "common/define.h" + +namespace olive { + /** * @brief The AboutDialog class * @@ -42,7 +47,16 @@ public: * * QWidget parent object. Usually this will be MainWindow. */ - explicit AboutDialog(QWidget *parent = nullptr); + explicit AboutDialog(bool welcome_dialog, QWidget *parent = nullptr); + +public slots: + virtual void accept() override; + +private: + QCheckBox *dont_show_again_checkbox_; + }; +} + #endif // ABOUTDIALOG_H diff --git a/app/dialog/about/patreon.h b/app/dialog/about/patreon.h new file mode 100644 index 000000000..3cbc1495d --- /dev/null +++ b/app/dialog/about/patreon.h @@ -0,0 +1,8 @@ +#ifndef PATREON_H +#define PATREON_H + +#include + +QStringList patrons; + +#endif // PATREON_H diff --git a/app/dialog/about/patreon.py b/app/dialog/about/patreon.py new file mode 100644 index 000000000..411c8d215 --- /dev/null +++ b/app/dialog/about/patreon.py @@ -0,0 +1,32 @@ +import json +import requests +import os + +url = 'https://www.patreon.com/api/oauth2/v2/campaigns/1478705/members?include=currently_entitled_tiers&fields%5Bmember%5D=full_name' +name_list = '' + +while True: + member_data = requests.get(url, headers = {"authorization": "Bearer " + os.environ.get('PATREON_KEY')}) + member_data_decoded = json.loads(member_data.text) + + for member in member_data_decoded["data"]: + if len(member["relationships"]["currently_entitled_tiers"]["data"]) > 0: + if member["relationships"]["currently_entitled_tiers"]["data"][0]["id"] == "3952333": + if len(name_list) > 0: + name_list += ',\n' + name = member["attributes"]["full_name"] + name_list += " QStringLiteral(\"" + name_list += name.translate(str.maketrans({ + "\"": "\\\"", + "\\": "\\\\" + })) + name_list += "\")" + + if "links" in member_data_decoded: + url = member_data_decoded["links"]["next"] + else: + break + +text_file = open("patreon.h", "w", encoding="utf-8") +text_file.write("#ifndef PATREON_H\n#define PATREON_H\n\n#include \n\nQStringList patrons = {\n%s\n};\n\n#endif // PATREON_H\n" % name_list) +text_file.close() diff --git a/app/dialog/about/scrollinglabel.cpp b/app/dialog/about/scrollinglabel.cpp new file mode 100644 index 000000000..c7fb59b9d --- /dev/null +++ b/app/dialog/about/scrollinglabel.cpp @@ -0,0 +1,121 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "scrollinglabel.h" + +#include + +#include "common/qtutils.h" + +namespace olive { + +const int ScrollingLabel::kMinLineHeight = 10; + +ScrollingLabel::ScrollingLabel(QWidget *parent) : + QWidget(parent), + animate_(0) +{ + timer_.setInterval(50); + connect(&timer_, &QTimer::timeout, this, &ScrollingLabel::AnimationUpdate); +} + +ScrollingLabel::ScrollingLabel(const QStringList &text, QWidget *parent) : + ScrollingLabel(parent) +{ + SetText(text); +} + +void ScrollingLabel::SetText(const QStringList &text) +{ + text_ = text; + + QFontMetrics fm = fontMetrics(); + text_height_ = fm.height(); + + int width = 0; + foreach (const QString& s, text_) { + width = qMax(width, QtUtils::QFontMetricsWidth(fm, s)); + } + + setMinimumSize(width, text_height_ * kMinLineHeight); +} + +void ScrollingLabel::paintEvent(QPaintEvent *e) +{ + QImage map(width(), height(), QImage::Format_RGBA8888_Premultiplied); + map.fill(Qt::transparent); + + { + QPainter p(&map); + p.setPen(palette().text().color()); + + QFontMetrics fm = p.fontMetrics(); + int half_width = width(); + + for (int i=0; i= height()) { + continue; + } + + const QString& s = text_.at(i); + + int width = QtUtils::QFontMetricsWidth(fm, s); + p.drawText(half_width/2 - width/2, text_y, s); + } + + for (int y=0; y= height() + text_.size() * text_height_) { + animate_ = 0; + } + + update(); +} + +} diff --git a/app/dialog/about/scrollinglabel.h b/app/dialog/about/scrollinglabel.h new file mode 100644 index 000000000..03394b805 --- /dev/null +++ b/app/dialog/about/scrollinglabel.h @@ -0,0 +1,71 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef SCROLLINGLABEL_H +#define SCROLLINGLABEL_H + +#include +#include + +namespace olive { + +class ScrollingLabel : public QWidget +{ + Q_OBJECT +public: + ScrollingLabel(QWidget* parent = nullptr); + ScrollingLabel(const QStringList& text, QWidget* parent = nullptr); + + void SetText(const QStringList& text); + + void StartAnimating() + { + timer_.start(); + } + + void StopAnimating() + { + timer_.stop(); + } + +protected: + virtual void paintEvent(QPaintEvent* e) override; + +private: + static void SetOpacityOfScanLine(uchar* scan_line, int width, int channels, double mul); + + static const int kMinLineHeight; + + QStringList text_; + + int text_height_; + + QTimer timer_; + + int animate_; + +private slots: + void AnimationUpdate(); + +}; + +} + +#endif // SCROLLINGLABEL_H diff --git a/app/dialog/actionsearch/CMakeLists.txt b/app/dialog/actionsearch/CMakeLists.txt index 16d7e7a68..c16f4288c 100644 --- a/app/dialog/actionsearch/CMakeLists.txt +++ b/app/dialog/actionsearch/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2019 Olive Team +# Copyright (C) 2021 Olive Team # # 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 diff --git a/app/dialog/actionsearch/actionsearch.cpp b/app/dialog/actionsearch/actionsearch.cpp index b5227655a..ac632f415 100644 --- a/app/dialog/actionsearch/actionsearch.cpp +++ b/app/dialog/actionsearch/actionsearch.cpp @@ -25,6 +25,8 @@ #include #include +namespace olive { + ActionSearch::ActionSearch(QWidget *parent) : QDialog(parent), menu_bar_(nullptr) @@ -157,7 +159,7 @@ void ActionSearch::search_update(const QString &s, const QString &p, QMenu *pare if (comp.contains(s, Qt::CaseInsensitive)) { // If so, we add it to the list widget. - QListWidgetItem* item = new QListWidgetItem(QString("%1\n(%2)").arg(comp, menu_text), list_widget); + QListWidgetItem* item = new QListWidgetItem(QStringLiteral("%1\n(%2)").arg(comp, menu_text), list_widget); // Add a pointer to the original QAction in the item's data item->setData(Qt::UserRole+1, reinterpret_cast(a)); @@ -251,3 +253,5 @@ void ActionSearchList::mouseDoubleClickEvent(QMouseEvent *) { emit dbl_click(); } + +} diff --git a/app/dialog/actionsearch/actionsearch.h b/app/dialog/actionsearch/actionsearch.h index ff48717e3..ecb446955 100644 --- a/app/dialog/actionsearch/actionsearch.h +++ b/app/dialog/actionsearch/actionsearch.h @@ -27,6 +27,10 @@ #include #include +#include "common/define.h" + +namespace olive { + class ActionSearchList; /** @@ -178,4 +182,6 @@ signals: void moveSelectionDown(); }; +} + #endif // ACTIONSEARCH_H diff --git a/app/dialog/autorecovery/CMakeLists.txt b/app/dialog/autorecovery/CMakeLists.txt new file mode 100644 index 000000000..e9c85bc7d --- /dev/null +++ b/app/dialog/autorecovery/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + dialog/autorecovery/autorecoverydialog.h + dialog/autorecovery/autorecoverydialog.cpp + PARENT_SCOPE +) diff --git a/app/dialog/autorecovery/autorecoverydialog.cpp b/app/dialog/autorecovery/autorecoverydialog.cpp new file mode 100644 index 000000000..31948e4e7 --- /dev/null +++ b/app/dialog/autorecovery/autorecoverydialog.cpp @@ -0,0 +1,136 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "autorecoverydialog.h" + +#include +#include +#include +#include +#include +#include + +#include "core.h" + +namespace olive { + +#define super QDialog + +AutoRecoveryDialog::AutoRecoveryDialog(const QString &message, const QStringList &recoveries, bool autocheck_latest, QWidget* parent) : + QDialog(parent) +{ + Init(message); + + PopulateTree(recoveries, autocheck_latest); +} + +void AutoRecoveryDialog::accept() +{ + foreach (QTreeWidgetItem* checkable, checkable_items_) { + if (checkable->checkState(0) == Qt::Checked) { + QString filename = checkable->data(0, kFilenameRole).toString(); + Core::instance()->OpenRecoveryProject(filename); + } + } + + super::accept(); +} + +void AutoRecoveryDialog::Init(const QString& header_text) +{ + QVBoxLayout* layout = new QVBoxLayout(this); + + setWindowTitle(tr("Auto-Recovery")); + + layout->addWidget(new QLabel(header_text)); + + tree_widget_ = new QTreeWidget(); + tree_widget_->setHeaderHidden(true); + layout->addWidget(tree_widget_); + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + buttons->button(QDialogButtonBox::Ok)->setText(tr("Load")); + connect(buttons, &QDialogButtonBox::accepted, this, &AutoRecoveryDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &AutoRecoveryDialog::reject); + layout->addWidget(buttons); +} + +void AutoRecoveryDialog::PopulateTree(const QStringList& recoveries, bool autocheck_latest) +{ + // Each entry in `recoveries` is a directory with 1+ recovery projects in it + QDir autorecovery_root(FileFunctions::GetAutoRecoveryRoot()); + + foreach (const QString& recovery_folder, recoveries) { + QDir recovery_dir(autorecovery_root.filePath(recovery_folder)); + + QString pretty_name; + { + // Retrieve pretty name + QFile pretty_name_file(recovery_dir.filePath(QStringLiteral("realname.txt"))); + if (pretty_name_file.open(QFile::ReadOnly)) { + // Read pretty name that we should have written in the autorecovery process + pretty_name = QString::fromUtf8(pretty_name_file.readAll()); + pretty_name_file.close(); + } + + if (pretty_name.isEmpty()) { + // Fallback to just the UUID. While it won't mean much to the user, it's better than nothing. + pretty_name = recovery_dir.dirName(); + } + } + + QTreeWidgetItem* top_level = new QTreeWidgetItem(tree_widget_); + top_level->setText(0, pretty_name); + + { + // Populate with recoveries + QStringList entries = recovery_dir.entryList(QDir::Files | QDir::NoDotAndDotDot, QDir::Name | QDir::Reversed); + for (int i=0; isetText(0, entry_name); + entry_item->setData(0, kFilenameRole, recovery_dir.filePath(entry)); + + // Allow to be checked, auto-checking the first entry + entry_item->setCheckState(0, (autocheck_latest && top_level->childCount() == 1) ? Qt::Checked : Qt::Unchecked); + + checkable_items_.append(entry_item); + } + } + } + } +} + +} diff --git a/app/dialog/autorecovery/autorecoverydialog.h b/app/dialog/autorecovery/autorecoverydialog.h new file mode 100644 index 000000000..0e4792695 --- /dev/null +++ b/app/dialog/autorecovery/autorecoverydialog.h @@ -0,0 +1,57 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef AUTORECOVERYDIALOG_H +#define AUTORECOVERYDIALOG_H + +#include +#include + +#include "common/define.h" + +namespace olive { + +class AutoRecoveryDialog : public QDialog +{ + Q_OBJECT +public: + AutoRecoveryDialog(const QString& message, const QStringList& recoveries, bool autocheck_latest, QWidget* parent); + +public slots: + virtual void accept() override; + +private: + void Init(const QString &header_text); + + void PopulateTree(const QStringList &recoveries, bool autocheck); + + QTreeWidget* tree_widget_; + + QVector checkable_items_; + + enum DataRole { + kFilenameRole = Qt::UserRole + }; + +}; + +} + +#endif // AUTORECOVERYDIALOG_H diff --git a/app/dialog/color/CMakeLists.txt b/app/dialog/color/CMakeLists.txt new file mode 100644 index 000000000..8c055b1f6 --- /dev/null +++ b/app/dialog/color/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + dialog/color/colordialog.h + dialog/color/colordialog.cpp + PARENT_SCOPE +) diff --git a/app/dialog/color/colordialog.cpp b/app/dialog/color/colordialog.cpp new file mode 100644 index 000000000..cd36864e9 --- /dev/null +++ b/app/dialog/color/colordialog.cpp @@ -0,0 +1,164 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "colordialog.h" + +#include +#include +#include + +#include "common/qtutils.h" + +namespace olive { + +ColorDialog::ColorDialog(ColorManager* color_manager, const ManagedColor& start, QWidget *parent) : + QDialog(parent), + color_manager_(color_manager) +{ + setWindowTitle(tr("Select Color")); + + QVBoxLayout* layout = new QVBoxLayout(this); + + QSplitter* splitter = new QSplitter(Qt::Horizontal); + splitter->setChildrenCollapsible(false); + layout->addWidget(splitter); + + QWidget* wheel_area = new QWidget(); + QHBoxLayout* wheel_layout = new QHBoxLayout(wheel_area); + splitter->addWidget(wheel_area); + + color_wheel_ = new ColorWheelWidget(); + wheel_layout->addWidget(color_wheel_); + + hsv_value_gradient_ = new ColorGradientWidget(Qt::Vertical); + hsv_value_gradient_->setFixedWidth(QtUtils::QFontMetricsWidth(fontMetrics(), QStringLiteral("HHH"))); + wheel_layout->addWidget(hsv_value_gradient_); + + QWidget* value_area = new QWidget(); + QVBoxLayout* value_layout = new QVBoxLayout(value_area); + value_layout->setSpacing(0); + splitter->addWidget(value_area); + + color_values_widget_ = new ColorValuesWidget(color_manager_); + color_values_widget_->IgnorePickFrom(this); + value_layout->addWidget(color_values_widget_); + + chooser_ = new ColorSpaceChooser(color_manager_); + chooser_->set_input(start.color_input()); + chooser_->set_output(start.color_output()); + + value_layout->addWidget(chooser_); + + // Split window 50/50 + splitter->setSizes({INT_MAX, INT_MAX}); + + connect(color_wheel_, &ColorWheelWidget::SelectedColorChanged, color_values_widget_, &ColorValuesWidget::SetColor); + connect(color_wheel_, &ColorWheelWidget::SelectedColorChanged, hsv_value_gradient_, &ColorGradientWidget::SetSelectedColor); + connect(hsv_value_gradient_, &ColorGradientWidget::SelectedColorChanged, color_values_widget_, &ColorValuesWidget::SetColor); + connect(hsv_value_gradient_, &ColorGradientWidget::SelectedColorChanged, color_wheel_, &ColorWheelWidget::SetSelectedColor); + connect(color_values_widget_, &ColorValuesWidget::ColorChanged, hsv_value_gradient_, &ColorGradientWidget::SetSelectedColor); + connect(color_values_widget_, &ColorValuesWidget::ColorChanged, color_wheel_, &ColorWheelWidget::SetSelectedColor); + + connect(color_wheel_, &ColorWheelWidget::DiameterChanged, hsv_value_gradient_, &ColorGradientWidget::setFixedHeight); + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + layout->addWidget(buttons); + + Color managed_start; + + if (start.color_input().isEmpty()) { + + managed_start = start; + + } else { + + // Convert reference color to the input space + ColorProcessorPtr linear_to_input = ColorProcessor::Create(color_manager_, + color_manager_->GetReferenceColorSpace(), + start.color_input()); + + managed_start = linear_to_input->ConvertColor(start); + + } + + color_wheel_->SetSelectedColor(managed_start); + hsv_value_gradient_->SetSelectedColor(managed_start); + color_values_widget_->SetColor(managed_start); + + connect(chooser_, &ColorSpaceChooser::ColorSpaceChanged, this, &ColorDialog::ColorSpaceChanged); + ColorSpaceChanged(chooser_->input(), chooser_->output()); + + // Set default size ratio to 2:1 + resize(sizeHint().height() * 2, sizeHint().height()); +} + +ManagedColor ColorDialog::GetSelectedColor() const +{ + ManagedColor selected = color_wheel_->GetSelectedColor(); + + // Convert to linear and return a linear color + if (input_to_ref_processor_) { + selected = input_to_ref_processor_->ConvertColor(selected); + } + + selected.set_color_input(GetColorSpaceInput()); + selected.set_color_output(GetColorSpaceOutput()); + + return selected; +} + +QString ColorDialog::GetColorSpaceInput() const +{ + return chooser_->input(); +} + +ColorTransform ColorDialog::GetColorSpaceOutput() const +{ + return chooser_->output(); +} + +void ColorDialog::ColorSpaceChanged(const QString &input, const ColorTransform &output) +{ + input_to_ref_processor_ = ColorProcessor::Create(color_manager_, input, color_manager_->GetReferenceColorSpace()); + + ColorProcessorPtr ref_to_display = ColorProcessor::Create(color_manager_, + color_manager_->GetReferenceColorSpace(), + output); + + ColorProcessorPtr ref_to_input = ColorProcessor::Create(color_manager_, color_manager_->GetReferenceColorSpace(), input); + + // FIXME: For some reason, using OCIO::TRANSFORM_DIR_INVERSE (wrapped by ColorProcessor::kInverse) causes OCIO to + // crash. We've disabled that functionality for now (also disabling display_tab_ in ColorValuesWidget) + + /*ColorProcessorPtr display_to_ref = ColorProcessor::Create(color_manager_->GetConfig(), + color_manager_->GetReferenceColorSpace(), + display, + view, + look, + ColorProcessor::kInverse);*/ + + color_wheel_->SetColorProcessor(input_to_ref_processor_, ref_to_display); + hsv_value_gradient_->SetColorProcessor(input_to_ref_processor_, ref_to_display); + color_values_widget_->SetColorProcessor(input_to_ref_processor_, ref_to_display, nullptr, ref_to_input); +} + +} diff --git a/app/dialog/color/colordialog.h b/app/dialog/color/colordialog.h new file mode 100644 index 000000000..064129a2c --- /dev/null +++ b/app/dialog/color/colordialog.h @@ -0,0 +1,92 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef COLORDIALOG_H +#define COLORDIALOG_H + +#include + +#include "node/color/colormanager/colormanager.h" +#include "render/color.h" +#include "render/managedcolor.h" +#include "widget/colorwheel/colorgradientwidget.h" +#include "widget/colorwheel/colorspacechooser.h" +#include "widget/colorwheel/colorvalueswidget.h" +#include "widget/colorwheel/colorwheelwidget.h" + +namespace olive { + +class ColorDialog : public QDialog +{ + Q_OBJECT +public: + /** + * @brief ColorDialog Constructor + * + * @param color_manager + * + * The ColorManager to use for color management. This must be valid. + * + * @param start + * + * The color to start with. This must be in the color_manager's reference space + * + * @param input_cs + * + * The input range that the user should see. The start color will be converted to this for UI object. + * + * @param parent + * + * QWidget parent. + */ + ColorDialog(ColorManager* color_manager, const ManagedColor &start = Color(1.0f, 1.0f, 1.0f), QWidget* parent = nullptr); + + /** + * @brief Retrieves the color selected by the user + * + * The color is always returned in the ColorManager's reference space (usually scene linear). + */ + ManagedColor GetSelectedColor() const; + + QString GetColorSpaceInput() const; + + ColorTransform GetColorSpaceOutput() const; + +private: + ColorManager* color_manager_; + + ColorWheelWidget* color_wheel_; + + ColorValuesWidget* color_values_widget_; + + ColorGradientWidget* hsv_value_gradient_; + + ColorProcessorPtr input_to_ref_processor_; + + ColorSpaceChooser* chooser_; + +private slots: + void ColorSpaceChanged(const QString& input, const ColorTransform &output); + +}; + +} + +#endif // COLORDIALOG_H diff --git a/app/dialog/configbase/CMakeLists.txt b/app/dialog/configbase/CMakeLists.txt new file mode 100644 index 000000000..0875e1e21 --- /dev/null +++ b/app/dialog/configbase/CMakeLists.txt @@ -0,0 +1,24 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + dialog/configbase/configdialogbase.cpp + dialog/configbase/configdialogbase.h + dialog/configbase/configdialogbasetab.cpp + dialog/configbase/configdialogbasetab.h + PARENT_SCOPE +) diff --git a/app/dialog/configbase/configdialogbase.cpp b/app/dialog/configbase/configdialogbase.cpp new file mode 100644 index 000000000..9cc9637f8 --- /dev/null +++ b/app/dialog/configbase/configdialogbase.cpp @@ -0,0 +1,93 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "configdialogbase.h" + +#include +#include +#include + +#include "core.h" + +namespace olive { + +ConfigDialogBase::ConfigDialogBase(QWidget* parent) : + QDialog(parent) +{ + QVBoxLayout* layout = new QVBoxLayout(this); + + QSplitter* splitter = new QSplitter(); + splitter->setChildrenCollapsible(false); + layout->addWidget(splitter); + + list_widget_ = new QListWidget(); + + preference_pane_stack_ = new QStackedWidget(this); + + splitter->addWidget(list_widget_); + splitter->addWidget(preference_pane_stack_); + + QDialogButtonBox* button_box = new QDialogButtonBox(this); + button_box->setOrientation(Qt::Horizontal); + button_box->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok); + + layout->addWidget(button_box); + + connect(button_box, &QDialogButtonBox::accepted, this, &ConfigDialogBase::accept); + connect(button_box, &QDialogButtonBox::rejected, this, &ConfigDialogBase::reject); + + connect(list_widget_, + &QListWidget::currentRowChanged, + preference_pane_stack_, + &QStackedWidget::setCurrentIndex); +} + +void ConfigDialogBase::accept() +{ + foreach (ConfigDialogBaseTab* tab, tabs_) { + if (!tab->Validate()) { + return; + } + } + + MultiUndoCommand* command = new MultiUndoCommand(); + + foreach (ConfigDialogBaseTab* tab, tabs_) { + tab->Accept(command); + } + + if (command->child_count() == 0) { + delete command; + } else { + Core::instance()->undo_stack()->push(command); + } + + QDialog::accept(); +} + +void ConfigDialogBase::AddTab(ConfigDialogBaseTab *tab, const QString &title) +{ + list_widget_->addItem(title); + preference_pane_stack_->addWidget(tab); + + tabs_.append(tab); +} + +} diff --git a/app/project/item/footage/videostream.h b/app/dialog/configbase/configdialogbase.h similarity index 54% rename from app/project/item/footage/videostream.h rename to app/dialog/configbase/configdialogbase.h index 334182b01..eccf7af90 100644 --- a/app/project/item/footage/videostream.h +++ b/app/dialog/configbase/configdialogbase.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -18,31 +18,41 @@ ***/ -#ifndef VIDEOSTREAM_H -#define VIDEOSTREAM_H +#ifndef CONFIGBASE_H +#define CONFIGBASE_H -#include "imagestream.h" +#include +#include +#include -class VideoStream : public ImageStream +#include "configdialogbasetab.h" + +namespace olive { + +class ConfigDialogBase : public QDialog { + Q_OBJECT public: - VideoStream(); - - virtual QString description() override; + ConfigDialogBase(QWidget* parent = nullptr); +private slots: /** - * @brief Get this video stream's frame rate - * - * Used purely for metadata, rendering uses the timebase instead. + * @brief Override of accept to save preferences to Config. */ - const rational& frame_rate(); - void set_frame_rate(const rational& frame_rate); + virtual void accept() override; + +protected: + void AddTab(ConfigDialogBaseTab* tab, const QString& title); private: - rational frame_rate_; + QListWidget* list_widget_; + + QStackedWidget* preference_pane_stack_; + + QList tabs_; }; -using VideoStreamPtr = std::shared_ptr; +} -#endif // VIDEOSTREAM_H +#endif // CONFIGBASE_H diff --git a/app/dialog/configbase/configdialogbasetab.cpp b/app/dialog/configbase/configdialogbasetab.cpp new file mode 100644 index 000000000..0a0d1bab7 --- /dev/null +++ b/app/dialog/configbase/configdialogbasetab.cpp @@ -0,0 +1,30 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "configdialogbasetab.h" + +namespace olive { + +bool ConfigDialogBaseTab::Validate() +{ + return true; +} + +} diff --git a/app/dialog/configbase/configdialogbasetab.h b/app/dialog/configbase/configdialogbasetab.h new file mode 100644 index 000000000..4f34c0655 --- /dev/null +++ b/app/dialog/configbase/configdialogbasetab.h @@ -0,0 +1,43 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef PREFERENCESTAB_H +#define PREFERENCESTAB_H + +#include + +#include "config/config.h" +#include "undo/undocommand.h" + +namespace olive { + +class ConfigDialogBaseTab : public QWidget +{ +public: + ConfigDialogBaseTab() = default; + + virtual bool Validate(); + + virtual void Accept(MultiUndoCommand *parent) = 0; +}; + +} + +#endif // PREFERENCESTAB_H diff --git a/app/dialog/diskcache/CMakeLists.txt b/app/dialog/diskcache/CMakeLists.txt new file mode 100644 index 000000000..4a5402d6f --- /dev/null +++ b/app/dialog/diskcache/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + dialog/diskcache/diskcachedialog.h + dialog/diskcache/diskcachedialog.cpp + PARENT_SCOPE +) diff --git a/app/dialog/diskcache/diskcachedialog.cpp b/app/dialog/diskcache/diskcachedialog.cpp new file mode 100644 index 000000000..75c005468 --- /dev/null +++ b/app/dialog/diskcache/diskcachedialog.cpp @@ -0,0 +1,113 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "diskcachedialog.h" + +#include +#include +#include +#include + +#include "config/config.h" +#include "core.h" + +namespace olive { + +DiskCacheDialog::DiskCacheDialog(DiskCacheFolder *folder, QWidget* parent) : + QDialog(parent), + folder_(folder) +{ + QGridLayout* layout = new QGridLayout(this); + + int row = 0; + + layout->addWidget(new QLabel(tr("Disk Cache: %1").arg(folder->GetPath())), row, 0, 1, 2); + setWindowTitle(tr("Disk Cache Settings")); + + row++; + + layout->addWidget(new QLabel(tr("Maximum Disk Cache:")), row, 0); + + maximum_cache_slider_ = new FloatSlider(); + maximum_cache_slider_->SetFormat(tr("%1 GB")); + maximum_cache_slider_->SetMinimum(1.0); + maximum_cache_slider_->SetValue(static_cast(folder->GetLimit()) / static_cast(kBytesInGigabyte)); + layout->addWidget(maximum_cache_slider_, row, 1); + + row++; + + clear_cache_btn_ = new QPushButton(tr("Clear Disk Cache")); + connect(clear_cache_btn_, &QPushButton::clicked, this, static_cast(&DiskCacheDialog::ClearDiskCache)); + layout->addWidget(clear_cache_btn_, row, 1); + + row++; + + clear_disk_cache_ = new QCheckBox(tr("Automatically clear disk cache on close")); + clear_disk_cache_->setChecked(folder->GetClearOnClose()); + layout->addWidget(clear_disk_cache_, row, 1); + + row++; + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(buttons, &QDialogButtonBox::accepted, this, &DiskCacheDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &DiskCacheDialog::reject); + layout->addWidget(buttons, row, 0, 1, 2); +} + +void DiskCacheDialog::accept() +{ + qint64 new_disk_cache_limit = qRound64(maximum_cache_slider_->GetValue() * kBytesInGigabyte); + if (new_disk_cache_limit != folder_->GetLimit()) { + folder_->SetLimit(new_disk_cache_limit); + } + + if (folder_->GetClearOnClose() != clear_disk_cache_->isChecked()) { + folder_->SetClearOnClose(clear_disk_cache_->isChecked()); + } + + QDialog::accept(); +} + +void DiskCacheDialog::ClearDiskCache() +{ + ClearDiskCache(folder_->GetPath(), this, clear_cache_btn_); +} + +void DiskCacheDialog::ClearDiskCache(const QString &path, QWidget *parent, QPushButton *clear_btn) +{ + if (QMessageBox::question(parent, + tr("Clear Disk Cache"), + tr("Are you sure you want to clear the disk cache in '%1'?").arg(path), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + if (clear_btn) clear_btn->setEnabled(false); + + if (DiskManager::instance()->ClearDiskCache(path)) { + if (clear_btn) clear_btn->setText(tr("Disk Cache Cleared")); + } else { + QMessageBox::information(parent, + tr("Clear Disk Cache"), + tr("Disk cache failed to fully clear. You may have to delete the cache files manually."), + QMessageBox::Ok); + if (clear_btn) clear_btn->setText(tr("Disk Cache Partially Cleared")); + } + } +} + +} diff --git a/app/render/backend/opengl/openglframebuffer.h b/app/dialog/diskcache/diskcachedialog.h similarity index 50% rename from app/render/backend/opengl/openglframebuffer.h rename to app/dialog/diskcache/diskcachedialog.h index ad063e48d..87317fc77 100644 --- a/app/render/backend/opengl/openglframebuffer.h +++ b/app/dialog/diskcache/diskcachedialog.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -18,50 +18,43 @@ ***/ -#ifndef OPENGLFRAMEBUFFER_H -#define OPENGLFRAMEBUFFER_H +#ifndef DISKCACHEDIALOG_H +#define DISKCACHEDIALOG_H -#include +#include +#include +#include -#include "common/constructors.h" -#include "opengltexture.h" +#include "render/diskmanager.h" +#include "widget/slider/floatslider.h" -class OpenGLFramebuffer : public QObject +namespace olive { + +class DiskCacheDialog : public QDialog { Q_OBJECT public: - OpenGLFramebuffer(); - virtual ~OpenGLFramebuffer() override; + DiskCacheDialog(DiskCacheFolder* folder, QWidget* parent = nullptr); - DISABLE_COPY_MOVE(OpenGLFramebuffer) - - void Create(QOpenGLContext *ctx); - - bool IsCreated() const; - - void Bind(); - - void Release(); - - void Attach(OpenGLTexturePtr texture); - - void AttachBackBuffer(OpenGLTexturePtr texture); - - void Detach(); - - const GLuint& buffer() const; + static void ClearDiskCache(const QString &path, QWidget *parent, QPushButton *clear_btn = nullptr); public slots: - void Destroy(); + virtual void accept() override; private: - void AttachInternal(GLuint tex, bool clear); + DiskCacheFolder* folder_; - QOpenGLContext* context_; + FloatSlider* maximum_cache_slider_; - GLuint buffer_; + QCheckBox* clear_disk_cache_; + + QPushButton* clear_cache_btn_; + +private slots: + void ClearDiskCache(); - OpenGLTexturePtr texture_; }; -#endif // OPENGLFRAMEBUFFER_H +} + +#endif // DISKCACHEDIALOG_H diff --git a/app/dialog/export/CMakeLists.txt b/app/dialog/export/CMakeLists.txt new file mode 100644 index 000000000..845d4ab91 --- /dev/null +++ b/app/dialog/export/CMakeLists.txt @@ -0,0 +1,32 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# 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 . + +add_subdirectory(codec) + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + dialog/export/export.cpp + dialog/export/export.h + dialog/export/exportadvancedvideodialog.cpp + dialog/export/exportadvancedvideodialog.h + dialog/export/exportaudiotab.cpp + dialog/export/exportaudiotab.h + dialog/export/exportsubtitlestab.cpp + dialog/export/exportsubtitlestab.h + dialog/export/exportvideotab.cpp + dialog/export/exportvideotab.h + PARENT_SCOPE +) diff --git a/app/dialog/export/codec/CMakeLists.txt b/app/dialog/export/codec/CMakeLists.txt new file mode 100644 index 000000000..1669edd70 --- /dev/null +++ b/app/dialog/export/codec/CMakeLists.txt @@ -0,0 +1,30 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + dialog/export/codec/cineformsection.cpp + dialog/export/codec/cineformsection.h + dialog/export/codec/codecsection.cpp + dialog/export/codec/codecsection.h + dialog/export/codec/codecstack.cpp + dialog/export/codec/codecstack.h + dialog/export/codec/h264section.cpp + dialog/export/codec/h264section.h + dialog/export/codec/imagesection.cpp + dialog/export/codec/imagesection.h + PARENT_SCOPE +) diff --git a/app/dialog/export/codec/cineformsection.cpp b/app/dialog/export/codec/cineformsection.cpp new file mode 100644 index 000000000..dedecb32e --- /dev/null +++ b/app/dialog/export/codec/cineformsection.cpp @@ -0,0 +1,85 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "cineformsection.h" + +#include +#include + +namespace olive { + +CineformSection::CineformSection(QWidget *parent) : + CodecSection(parent) +{ + QGridLayout *layout = new QGridLayout(this); + + layout->setMargin(0); + + int row = 0; + + layout->addWidget(new QLabel(tr("Quality:")), row, 0); + + quality_combobox_ = new QComboBox(); + + /* Correspond to the following indexes for FFmpeg + * + * -quality E..V....... set quality (from 0 to 12) (default film3+) + * film3+ 0 E..V....... + * film3 1 E..V....... + * film2+ 2 E..V....... + * film2 3 E..V....... + * film1.5 4 E..V....... + * film1+ 5 E..V....... + * film1 6 E..V....... + * high+ 7 E..V....... + * high 8 E..V....... + * medium+ 9 E..V....... + * medium 10 E..V....... + * low+ 11 E..V....... + * low 12 E..V....... + * + */ + + quality_combobox_->addItem(tr("Film Scan 3+")); + quality_combobox_->addItem(tr("Film Scan 3")); + quality_combobox_->addItem(tr("Film Scan 2+")); + quality_combobox_->addItem(tr("Film Scan 2")); + quality_combobox_->addItem(tr("Film Scan 1.5")); + quality_combobox_->addItem(tr("Film Scan 1+")); + quality_combobox_->addItem(tr("Film Scan 1")); + quality_combobox_->addItem(tr("High+")); + quality_combobox_->addItem(tr("High")); + quality_combobox_->addItem(tr("Medium+")); + quality_combobox_->addItem(tr("Medium")); + quality_combobox_->addItem(tr("Low+")); + quality_combobox_->addItem(tr("Low")); + + // Default to "medium" + quality_combobox_->setCurrentIndex(10); + + layout->addWidget(quality_combobox_, row, 1); +} + +void CineformSection::AddOpts(EncodingParams *params) +{ + params->set_video_option(QStringLiteral("quality"), QString::number(quality_combobox_->currentIndex())); +} + +} diff --git a/app/dialog/export/codec/cineformsection.h b/app/dialog/export/codec/cineformsection.h new file mode 100644 index 000000000..6dc09f2d0 --- /dev/null +++ b/app/dialog/export/codec/cineformsection.h @@ -0,0 +1,45 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef CINEFORMSECTION_H +#define CINEFORMSECTION_H + +#include + +#include "codecsection.h" + +namespace olive { + +class CineformSection : public CodecSection +{ + Q_OBJECT +public: + CineformSection(QWidget *parent = nullptr); + + virtual void AddOpts(EncodingParams* params) override; + +private: + QComboBox *quality_combobox_; + +}; + +} + +#endif // CINEFORMSECTION_H diff --git a/app/dialog/export/codec/codecsection.cpp b/app/dialog/export/codec/codecsection.cpp new file mode 100644 index 000000000..5072c40bb --- /dev/null +++ b/app/dialog/export/codec/codecsection.cpp @@ -0,0 +1,30 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "codecsection.h" + +namespace olive { + +CodecSection::CodecSection(QWidget *parent) : + QWidget(parent) +{ +} + +} diff --git a/app/dialog/export/codec/codecsection.h b/app/dialog/export/codec/codecsection.h new file mode 100644 index 000000000..65c4c10f8 --- /dev/null +++ b/app/dialog/export/codec/codecsection.h @@ -0,0 +1,42 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef CODECSECTION_H +#define CODECSECTION_H + +#include + +#include "codec/encoder.h" + +namespace olive { + +class CodecSection : public QWidget +{ + Q_OBJECT +public: + CodecSection(QWidget* parent = nullptr); + + virtual void AddOpts(EncodingParams* params){Q_UNUSED(params)} + +}; + +} + +#endif // CODECSECTION_H diff --git a/app/project/project.cpp b/app/dialog/export/codec/codecstack.cpp similarity index 51% rename from app/project/project.cpp rename to app/dialog/export/codec/codecstack.cpp index a811acd2b..cdffb12cd 100644 --- a/app/project/project.cpp +++ b/app/dialog/export/codec/codecstack.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -18,44 +18,36 @@ ***/ -#include "project.h" +#include "codecstack.h" -Project::Project() +namespace olive { + +#define super QStackedWidget + +CodecStack::CodecStack(QWidget *parent) + : super{parent} { - name_ = tr("(untitled)"); + connect(this, &CodecStack::currentChanged, this, &CodecStack::OnChange); } -Folder *Project::root() +void CodecStack::addWidget(QWidget *widget) { - return &root_; + super::addWidget(widget); + + OnChange(currentIndex()); } -const QString &Project::name() +void CodecStack::OnChange(int index) { - return name_; + for (int i=0; isetSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + } else { + widget(i)->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + } + widget(i)->adjustSize(); + } + adjustSize(); } -void Project::set_name(const QString &s) -{ - name_ = s; -} - -const QString &Project::ocio_config() -{ - return ocio_config_; -} - -void Project::set_ocio_config(const QString &ocio_config) -{ - ocio_config_ = ocio_config; -} - -const QString &Project::default_input_colorspace() -{ - return default_input_colorspace_; -} - -void Project::set_default_input_colorspace(const QString &colorspace) -{ - default_input_colorspace_ = colorspace; } diff --git a/app/dialog/export/codec/codecstack.h b/app/dialog/export/codec/codecstack.h new file mode 100644 index 000000000..116f8754c --- /dev/null +++ b/app/dialog/export/codec/codecstack.h @@ -0,0 +1,45 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef CODECSTACK_H +#define CODECSTACK_H + +#include + +namespace olive { + +class CodecStack : public QStackedWidget +{ + Q_OBJECT +public: + explicit CodecStack(QWidget *parent = nullptr); + + void addWidget(QWidget *widget); + +signals: + +private slots: + void OnChange(int index); + +}; + +} + +#endif // CODECSTACK_H diff --git a/app/dialog/export/codec/h264section.cpp b/app/dialog/export/codec/h264section.cpp new file mode 100644 index 000000000..6b15be526 --- /dev/null +++ b/app/dialog/export/codec/h264section.cpp @@ -0,0 +1,251 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "h264section.h" + +#include +#include +#include +#include + +#include "common/qtutils.h" +#include "widget/slider/integerslider.h" + +namespace olive { + +H264Section::H264Section(QWidget *parent) : + H264Section(H264CRFSection::kDefaultH264CRF, parent) +{ +} + +H264Section::H264Section(int default_crf, QWidget *parent) : + CodecSection(parent) +{ + QGridLayout* layout = new QGridLayout(this); + layout->setMargin(0); + + int row = 0; + layout->addWidget(new QLabel(tr("Encode Speed:")), row, 0); + + + preset_combobox_ = new QComboBox(); + preset_combobox_->setToolTip(tr("This setting allows you to tweak the ratio of export speed to compression quality. \n\n" + "If using Constant Rate Factor, slower speeds will result in smaller file sizes for the same quality. \n\n" + "If using Target Bit Rate or Target File Size, slower speeds will result in higher quality for the same bitrate/filesize. \n\n" + "This setting is equivalent to the `preset` setting in libx264.")); + + preset_combobox_->addItem(tr("Ultra Fast")); + preset_combobox_->addItem(tr("Super Fast")); + preset_combobox_->addItem(tr("Very Fast")); + preset_combobox_->addItem(tr("Faster")); + preset_combobox_->addItem(tr("Fast")); + preset_combobox_->addItem(tr("Medium")); + preset_combobox_->addItem(tr("Slow")); + preset_combobox_->addItem(tr("Slower")); + preset_combobox_->addItem(tr("Very Slow")); + + //Default to "medium" + preset_combobox_->setCurrentIndex(5); + + layout->addWidget(preset_combobox_, row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Compression Method:")), row, 0); + + QComboBox* compression_box = new QComboBox(); + + // These items must correspond to the CompressionMethod enum + compression_box->addItem(tr("Constant Rate Factor")); + compression_box->addItem(tr("Target Bit Rate")); + compression_box->addItem(tr("Target File Size")); + + layout->addWidget(compression_box, row, 1); + + row++; + + compression_method_stack_ = new QStackedWidget(); + layout->addWidget(compression_method_stack_, row, 0, 1, 2); + + crf_section_ = new H264CRFSection(default_crf); + compression_method_stack_->addWidget(crf_section_); + + bitrate_section_ = new H264BitRateSection(); + compression_method_stack_->addWidget(bitrate_section_); + + filesize_section_ = new H264FileSizeSection(); + compression_method_stack_->addWidget(filesize_section_); + + connect(compression_box, + static_cast(&QComboBox::currentIndexChanged), + compression_method_stack_, + &QStackedWidget::setCurrentIndex); +} + +void H264Section::AddOpts(EncodingParams *params) +{ + // FIXME: Implement two-pass + + CompressionMethod method = static_cast(compression_method_stack_->currentIndex()); + + if (method == kConstantRateFactor) { + + // Simply set CRF value + params->set_video_option(QStringLiteral("crf"), QString::number(crf_section_->GetValue())); + + } else { + + int64_t target_rate, max_rate, min_rate; + + if (method == kTargetBitRate) { + // Use user-supplied values for the bit rate + target_rate = bitrate_section_->GetTargetBitRate(); + min_rate = 0; + max_rate = bitrate_section_->GetMaximumBitRate(); + } else { + // Calculate the bit rate from the file size divided by the sequence length in seconds (bits per second) + target_rate = qRound64(static_cast(filesize_section_->GetFileSize()) / params->GetExportLength().toDouble()); + min_rate = target_rate; + max_rate = target_rate; + } + + // Disable CRF encoding + params->set_video_option(QStringLiteral("crf"), QStringLiteral("-1")); + + params->set_video_bit_rate(target_rate); + params->set_video_min_bit_rate(min_rate); + params->set_video_max_bit_rate(max_rate); + params->set_video_buffer_size(2000000); + + } + + params->set_video_option(QStringLiteral("preset"), QString::number(preset_combobox_->currentIndex())); +} + +H264CRFSection::H264CRFSection(int default_crf, QWidget *parent) : + QWidget(parent) +{ + QHBoxLayout* layout = new QHBoxLayout(this); + layout->setMargin(0); + + crf_slider_ = new QSlider(Qt::Horizontal); + crf_slider_->setMinimum(kMinimumCRF); + crf_slider_->setMaximum(kMaximumCRF); + crf_slider_->setValue(default_crf); + layout->addWidget(crf_slider_); + + IntegerSlider* crf_input = new IntegerSlider(); + crf_input->setMaximumWidth(QtUtils::QFontMetricsWidth(crf_input->fontMetrics(), QStringLiteral("HHHH"))); + crf_input->SetMinimum(kMinimumCRF); + crf_input->SetMaximum(kMaximumCRF); + crf_input->SetValue(default_crf); + crf_input->SetDefaultValue(default_crf); + layout->addWidget(crf_input); + + connect(crf_slider_, &QSlider::valueChanged, crf_input, &IntegerSlider::SetValue); + connect(crf_input, &IntegerSlider::ValueChanged, crf_slider_, &QSlider::setValue); +} + +int H264CRFSection::GetValue() const +{ + return crf_slider_->value(); +} + +H264BitRateSection::H264BitRateSection(QWidget *parent) : + QWidget(parent) +{ + QGridLayout* layout = new QGridLayout(this); + layout->setMargin(0); + + int row = 0; + + layout->addWidget(new QLabel(tr("Target Bit Rate (Mbps):")), row, 0); + + target_rate_ = new FloatSlider(); + target_rate_->SetMinimum(0); + layout->addWidget(target_rate_, row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Maximum Bit Rate (Mbps):")), row, 0); + + max_rate_ = new FloatSlider(); + max_rate_->SetMinimum(0); + layout->addWidget(max_rate_, row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Two-Pass")), row, 0); + + QCheckBox* two_pass_box = new QCheckBox(); + layout->addWidget(two_pass_box, row, 1); + + // Bit rate defaults + target_rate_->SetValue(16.0); + max_rate_->SetValue(32.0); +} + +int64_t H264BitRateSection::GetTargetBitRate() const +{ + return qRound64(target_rate_->GetValue() * 1000000.0); +} + +int64_t H264BitRateSection::GetMaximumBitRate() const +{ + return qRound64(max_rate_->GetValue() * 1000000.0); +} + +H264FileSizeSection::H264FileSizeSection(QWidget *parent) : + QWidget(parent) +{ + QGridLayout* layout = new QGridLayout(this); + layout->setMargin(0); + + int row = 0; + + layout->addWidget(new QLabel(tr("Target File Size (MB):")), row, 0); + + file_size_ = new FloatSlider(); + file_size_->SetMinimum(0); + layout->addWidget(file_size_, row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Two-Pass")), row, 0); + + QCheckBox* two_pass_box = new QCheckBox(); + layout->addWidget(two_pass_box, row, 1); + + // File size defaults + file_size_->SetValue(700.0); +} + +int64_t H264FileSizeSection::GetFileSize() const +{ + // Convert megabytes to BITS + return qRound64(file_size_->GetValue() * 1024.0 * 1024.0 * 8.0); +} + +H265Section::H265Section(QWidget *parent) : + H264Section(H264CRFSection::kDefaultH265CRF, parent) +{ +} + +} diff --git a/app/dialog/export/codec/h264section.h b/app/dialog/export/codec/h264section.h new file mode 100644 index 000000000..78fbef19a --- /dev/null +++ b/app/dialog/export/codec/h264section.h @@ -0,0 +1,127 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef H264SECTION_H +#define H264SECTION_H + +#include +#include +#include + +#include "codecsection.h" +#include "widget/slider/floatslider.h" + +namespace olive { + +class H264CRFSection : public QWidget +{ + Q_OBJECT +public: + H264CRFSection(int default_crf, QWidget* parent = nullptr); + + int GetValue() const; + + static const int kDefaultH264CRF = 23; + static const int kDefaultH265CRF = 28; + +private: + static const int kMinimumCRF = 0; + static const int kMaximumCRF = 51; + + QSlider* crf_slider_; + +}; + +class H264BitRateSection : public QWidget +{ + Q_OBJECT +public: + H264BitRateSection(QWidget* parent = nullptr); + + /** + * @brief Get user-selected target bit rate (returns in BITS) + */ + int64_t GetTargetBitRate() const; + + /** + * @brief Get user-selected maximum bit rate (returns in BITS) + */ + int64_t GetMaximumBitRate() const; + +private: + FloatSlider* target_rate_; + + FloatSlider* max_rate_; + +}; + +class H264FileSizeSection : public QWidget +{ + Q_OBJECT +public: + H264FileSizeSection(QWidget* parent = nullptr); + + /** + * @brief Returns file size in BITS + */ + int64_t GetFileSize() const; + +private: + FloatSlider* file_size_; + +}; + +class H264Section : public CodecSection +{ + Q_OBJECT +public: + enum CompressionMethod { + kConstantRateFactor, + kTargetBitRate, + kTargetFileSize + }; + + H264Section(QWidget* parent = nullptr); + H264Section(int default_crf, QWidget* parent); + + virtual void AddOpts(EncodingParams* params) override; + +private: + QStackedWidget* compression_method_stack_; + + H264CRFSection* crf_section_; + + H264BitRateSection* bitrate_section_; + + H264FileSizeSection* filesize_section_; + + QComboBox *preset_combobox_; +}; + +class H265Section : public H264Section +{ + Q_OBJECT +public: + H265Section(QWidget* parent = nullptr); +}; + +} + +#endif // H264SECTION_H diff --git a/app/dialog/export/codec/imagesection.cpp b/app/dialog/export/codec/imagesection.cpp new file mode 100644 index 000000000..cf38fd698 --- /dev/null +++ b/app/dialog/export/codec/imagesection.cpp @@ -0,0 +1,58 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "imagesection.h" + +#include +#include + +namespace olive { + +ImageSection::ImageSection(QWidget* parent) : + CodecSection(parent) +{ + QGridLayout* layout = new QGridLayout(this); + layout->setMargin(0); + + int row = 0; + + layout->addWidget(new QLabel(tr("Image Sequence:")), row, 0); + + image_sequence_checkbox_ = new QCheckBox(); + connect(image_sequence_checkbox_, &QCheckBox::toggled, this, &ImageSection::ImageSequenceCheckBoxToggled); + layout->addWidget(image_sequence_checkbox_, row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Frame to Export:")), row, 0); + + frame_slider_ = new RationalSlider(); + frame_slider_->SetMinimum(0); + frame_slider_->SetValue(0); + frame_slider_->SetDisplayType(RationalSlider::kTime); + layout->addWidget(frame_slider_, row, 1); +} + +void ImageSection::ImageSequenceCheckBoxToggled(bool e) +{ + frame_slider_->setEnabled(!e); +} + +} diff --git a/app/dialog/export/codec/imagesection.h b/app/dialog/export/codec/imagesection.h new file mode 100644 index 000000000..6e116b466 --- /dev/null +++ b/app/dialog/export/codec/imagesection.h @@ -0,0 +1,69 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef IMAGESECTION_H +#define IMAGESECTION_H + +#include + +#include "codecsection.h" +#include "widget/slider/rationalslider.h" + +namespace olive { + +class ImageSection : public CodecSection +{ + Q_OBJECT +public: + ImageSection(QWidget* parent = nullptr); + + bool IsImageSequenceChecked() const + { + return image_sequence_checkbox_->isChecked(); + } + + void SetTimebase(const rational& r) + { + frame_slider_->SetTimebase(r); + } + + rational GetTime() const + { + return frame_slider_->GetValue(); + } + + void SetTime(const rational &t) + { + frame_slider_->SetValue(t); + } + +private: + QCheckBox* image_sequence_checkbox_; + + RationalSlider* frame_slider_; + +private slots: + void ImageSequenceCheckBoxToggled(bool e); + +}; + +} + +#endif // IMAGESECTION_H diff --git a/app/dialog/export/export.cpp b/app/dialog/export/export.cpp new file mode 100644 index 000000000..c0d9b9ba0 --- /dev/null +++ b/app/dialog/export/export.cpp @@ -0,0 +1,637 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "export.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/digit.h" +#include "common/qtutils.h" +#include "core.h" +#include "dialog/task/task.h" +#include "node/project/project.h" +#include "node/project/sequence/sequence.h" +#include "task/taskmanager.h" +#include "ui/icons/icons.h" + +namespace olive { + +ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : + QDialog(parent), + viewer_node_(viewer_node) +{ + QHBoxLayout* layout = new QHBoxLayout(this); + + QSplitter* splitter = new QSplitter(Qt::Horizontal); + splitter->setChildrenCollapsible(false); + layout->addWidget(splitter); + + preferences_area_ = new QWidget(); + QGridLayout* preferences_layout = new QGridLayout(preferences_area_); + preferences_layout->setMargin(0); + + int row = 0; + + QLabel* fn_lbl = new QLabel(tr("Filename:")); + fn_lbl->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + preferences_layout->addWidget(fn_lbl, row, 0); + + filename_edit_ = new QLineEdit(); + preferences_layout->addWidget(filename_edit_, row, 1, 1, 2); + + QPushButton* file_browse_btn = new QPushButton(); + file_browse_btn->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + file_browse_btn->setIcon(icon::Folder); + file_browse_btn->setToolTip(tr("Browse for exported file filename")); + connect(file_browse_btn, + &QPushButton::clicked, + this, + &ExportDialog::BrowseFilename); + preferences_layout->addWidget(file_browse_btn, row, 3); + + row++; + + QLabel* preset_lbl = new QLabel(tr("Preset:")); + preset_lbl->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + preferences_layout->addWidget(preset_lbl, row, 0); + QComboBox* preset_combobox = new QComboBox(); + preset_combobox->addItem(tr("Same As Source - High Quality")); + preset_combobox->addItem(tr("Same As Source - Medium Quality")); + preset_combobox->addItem(tr("Same As Source - Low Quality")); + preferences_layout->addWidget(preset_combobox, row, 1); + + QPushButton* preset_load_btn = new QPushButton(); + preset_load_btn->setIcon(icon::Open); + preset_load_btn->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + preferences_layout->addWidget(preset_load_btn, row, 2); + + QPushButton* preset_save_btn = new QPushButton(); + preset_save_btn->setIcon(icon::Save); + preset_save_btn->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + preferences_layout->addWidget(preset_save_btn, row, 3); + + row++; + + preferences_layout->addWidget(QtUtils::CreateHorizontalLine(), row, 0, 1, 4); + + row++; + + preferences_layout->addWidget(new QLabel(tr("Range:")), row, 0); + + range_combobox_ = new QComboBox(); + range_combobox_->addItem(tr("Entire Sequence")); + range_combobox_->addItem(tr("In to Out")); + range_combobox_->setEnabled(viewer_node_->GetTimelinePoints()->workarea()->enabled()); + + preferences_layout->addWidget(range_combobox_, row, 1, 1, 3); + + row++; + + preferences_layout->addWidget(QtUtils::CreateHorizontalLine(), row, 0, 1, 4); + + row++; + + preferences_layout->addWidget(new QLabel(tr("Format:")), row, 0); + format_combobox_ = new QComboBox(); + preferences_layout->addWidget(format_combobox_, row, 1, 1, 3); + + row++; + + QHBoxLayout* av_enabled_layout = new QHBoxLayout(); + + video_enabled_ = new QCheckBox(tr("Export Video")); + av_enabled_layout->addWidget(video_enabled_); + + audio_enabled_ = new QCheckBox(tr("Export Audio")); + av_enabled_layout->addWidget(audio_enabled_); + + subtitles_enabled_ = new QCheckBox(tr("Export Subtitle")); + av_enabled_layout->addWidget(subtitles_enabled_); + + preferences_layout->addLayout(av_enabled_layout, row, 0, 1, 4); + + row++; + + preferences_tabs_ = new QTabWidget(); + + color_manager_ = viewer_node_->project()->color_manager(); + video_tab_ = new ExportVideoTab(color_manager_); + AddPreferencesTab(video_tab_, tr("Video")); + + audio_tab_ = new ExportAudioTab(); + AddPreferencesTab(audio_tab_, tr("Audio")); + + subtitle_tab_ = new ExportSubtitlesTab(); + AddPreferencesTab(subtitle_tab_, tr("Subtitles")); + + preferences_layout->addWidget(preferences_tabs_, row, 0, 1, 4); + + row++; + + QHBoxLayout *btn_layout = new QHBoxLayout(); + btn_layout->setMargin(0); + preferences_layout->addLayout(btn_layout, row, 0, 1, 4); + + btn_layout->addStretch(); + + QPushButton *export_btn = new QPushButton(tr("Export")); + btn_layout->addWidget(export_btn); + connect(export_btn, &QPushButton::clicked, this, &ExportDialog::StartExport); + + QPushButton *cancel_btn = new QPushButton(tr("Cancel")); + btn_layout->addWidget(cancel_btn); + connect(cancel_btn, &QPushButton::clicked, this, &ExportDialog::reject); + + export_bkg_box_ = new QCheckBox(tr("Run In Background")); + export_bkg_box_->setToolTip(tr("Exporting in the background allows you to continue using Olive while " + "exporting, but may result in slower export speeds, and may" + "severely impact editing and playback performance.")); + btn_layout->addWidget(export_bkg_box_); + + btn_layout->addStretch(); + + splitter->addWidget(preferences_area_); + + QWidget* preview_area = new QWidget(); + QVBoxLayout* preview_layout = new QVBoxLayout(preview_area); + preview_layout->addWidget(new QLabel(tr("Preview"))); + preview_viewer_ = new ViewerWidget(); + preview_viewer_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + connect(preview_viewer_, &ViewerWidget::TimeChanged, video_tab_, &ExportVideoTab::SetTime); + preview_layout->addWidget(preview_viewer_); + splitter->addWidget(preview_area); + + // Prioritize preview area + splitter->setSizes({1, 99999}); + + // Set default filename + SetDefaultFilename(); + + // Populate combobox formats + for (int i=0; i(i)); + + bool inserted = false; + + for (int j=0; jcount(); j++) { + if (format_combobox_->itemText(j) > format_name) { + format_combobox_->insertItem(j, format_name, i); + inserted = true; + break; + } + } + + if (!inserted) { + format_combobox_->addItem(format_name, i); + } + } + + // Set defaults + previously_selected_format_ = ExportFormat::kFormatMPEG4; + SetCurrentFormat(ExportFormat::kFormatMPEG4); + connect(format_combobox_, + static_cast(&QComboBox::currentIndexChanged), + this, + &ExportDialog::FormatChanged); + FormatChanged(format_combobox_->currentIndex()); + + VideoParams vp = viewer_node_->GetVideoParams(); + AudioParams ap = viewer_node_->GetAudioParams(); + + video_tab_->width_slider()->SetValue(vp.width()); + video_tab_->width_slider()->SetDefaultValue(vp.width()); + video_tab_->height_slider()->SetValue(vp.height()); + video_tab_->height_slider()->SetDefaultValue(vp.height()); + video_tab_->SetSelectedFrameRate(vp.frame_rate()); + video_tab_->pixel_aspect_combobox()->SetPixelAspectRatio(vp.pixel_aspect_ratio()); + video_tab_->pixel_format_field()->SetPixelFormat(static_cast(Config::Current()[QStringLiteral("OnlinePixelFormat")].toInt())); + video_tab_->interlaced_combobox()->SetInterlaceMode(vp.interlacing()); + audio_tab_->sample_rate_combobox()->SetSampleRate(ap.sample_rate()); + audio_tab_->channel_layout_combobox()->SetChannelLayout(ap.channel_layout()); + + video_aspect_ratio_ = static_cast(vp.width()) / static_cast(vp.height()); + + connect(video_tab_->width_slider(), + &IntegerSlider::ValueChanged, + this, + &ExportDialog::ResolutionChanged); + + connect(video_tab_->height_slider(), + &IntegerSlider::ValueChanged, + this, + &ExportDialog::ResolutionChanged); + + connect(video_tab_->scaling_method_combobox(), + static_cast(&QComboBox::currentIndexChanged), + this, + &ExportDialog::UpdateViewerDimensions); + + connect(video_tab_->maintain_aspect_checkbox(), + &QCheckBox::toggled, + this, + &ExportDialog::ResolutionChanged); + + connect(video_tab_, + &ExportVideoTab::ColorSpaceChanged, + preview_viewer_, + static_cast(&ViewerWidget::SetColorTransform)); + connect(video_tab_, + &ExportVideoTab::ImageSequenceCheckBoxChanged, + this, + &ExportDialog::ImageSequenceCheckBoxChanged); + + // Set viewer to view the node + preview_viewer_->ConnectViewerNode(viewer_node_); + preview_viewer_->ruler()->ConnectTimelinePoints(viewer_node_->GetTimelinePoints()); + preview_viewer_->SetColorMenuEnabled(false); + preview_viewer_->SetColorTransform(video_tab_->CurrentOCIOColorSpace()); +} + +ExportFormat::Format ExportDialog::GetSelectedFormat() const +{ + return static_cast(format_combobox_->currentData().toInt()); +} + +rational ExportDialog::GetSelectedTimebase() const +{ + return video_tab_->GetSelectedFrameRate().flipped(); +} + +void ExportDialog::StartExport() +{ + if (!video_enabled_->isChecked() && !audio_enabled_->isChecked() && !subtitles_enabled_->isChecked()) { + QtUtils::MessageBox(this, QMessageBox::Critical, tr("Invalid parameters"), + tr("Video, audio, and subtitles are disabled. There's nothing to export.")); + return; + } + + // Validate if the entered filename contains the correct extension (the extension is necessary + // for both FFmpeg and OIIO to determine the output format) + QString necessary_ext = QStringLiteral(".%1").arg(ExportFormat::GetExtension(GetSelectedFormat())); + QString proposed_filename = filename_edit_->text().trimmed(); + + // If it doesn't, see if the user wants to append it automatically. If not, we don't abort the export. + if (!proposed_filename.endsWith(necessary_ext, Qt::CaseInsensitive)) { + if (QtUtils::MessageBox(this, QMessageBox::Warning, tr("Invalid filename"), + tr("The filename must contain the extension \"%1\". Would you like to append it " + "automatically?").arg(necessary_ext), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + filename_edit_->setText(proposed_filename.append(necessary_ext)); + } else { + return; + } + } + + // Validate the intended path + QFileInfo file_info(proposed_filename); + QFileInfo dir_info(file_info.path()); + + // If the directory does not exist, try to create it + if (!QDir(file_info.path()).mkpath(QStringLiteral("."))) { + QtUtils::MessageBox(this, QMessageBox::Critical, tr("Failed to create output directory"), + tr("The intended output directory doesn't exist and Olive couldn't create it. " + "Please choose a different filename.")); + return; + } + + // Validate if this is an image sequence and if the filename contains enough digits + if (video_tab_->IsImageSequenceSet()) { + // Ensure filename contains digits + if (!Encoder::FilenameContainsDigitPlaceholder(proposed_filename)) { + QtUtils::MessageBox(this, QMessageBox::Critical, tr("Invalid filename"), + tr("Export is set to an image sequence, but the filename does not have a section for digits " + "(formatted as [#####] where the amount of # is the amount of digits).")); + return; + } + + int64_t frame_count = GetExportLengthInTimebaseUnits(); + int64_t needed_digit_count = GetDigitCount(frame_count); + int current_digit_count = Encoder::GetImageSequencePlaceholderDigitCount(proposed_filename); + if (current_digit_count < needed_digit_count) { + QtUtils::MessageBox(this, QMessageBox::Critical, tr("Invalid filename"), + tr("Filename doesn't contain enough digits for the amount of frames " + "this export will need (need %1 for %n frame(s)).", nullptr, frame_count) + .arg(QString::number(needed_digit_count))); + return; + } + } + + // Validate if the file exists and whether the user wishes to overwrite it + if (file_info.exists()) { + if (QtUtils::MessageBox(this, QMessageBox::Warning, tr("Confirm Overwrite"), + tr("The file \"%1\" already exists. Do you want to overwrite it?") + .arg(proposed_filename), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { + return; + } + } + + // Validate video resolution + if (video_enabled_->isChecked() + && (video_tab_->GetSelectedCodec() == ExportCodec::kCodecH264 || video_tab_->GetSelectedCodec() == ExportCodec::kCodecH265) + && (video_tab_->width_slider()->GetValue()%2 != 0 || video_tab_->height_slider()->GetValue()%2 != 0)) { + QtUtils::MessageBox(this, QMessageBox::Critical, tr("Invalid Parameters"), + tr("Width and height must be multiples of 2.")); + return; + } + + ExportTask* task = new ExportTask(viewer_node_, color_manager_, GenerateParams()); + + if (export_bkg_box_->isChecked()) { + // Send to TaskManager to export in background + TaskManager::instance()->AddTask(task); + this->accept(); + } else { + // Use modal dialog box + TaskDialog* td = new TaskDialog(task, tr("Export"), this); + connect(td, &TaskDialog::TaskSucceeded, this, &ExportDialog::ExportFinished); + td->open(); + } +} + +void ExportDialog::ExportFinished() +{ + TaskDialog* td = static_cast(sender()); + + if (td->GetTask()->IsCancelled()) { + // If this task was cancelled, we stay open so the user can potentially queue another export + } else { + // Accept this dialog and close + this->accept(); + } +} + +void ExportDialog::ImageSequenceCheckBoxChanged(bool e) +{ + QFileInfo current_fileinfo(filename_edit_->text()); + + QString basename = current_fileinfo.completeBaseName(); + QString suffix = current_fileinfo.suffix(); + + if (e) { + if (!Encoder::FilenameContainsDigitPlaceholder(basename)) { + basename.append(QStringLiteral("_[#####]")); + } + } else { + basename = Encoder::FilenameRemoveDigitPlaceholder(basename); + } + + // Set filename + if (!suffix.isEmpty()) { + basename.append('.'); + basename.append(suffix); + } + filename_edit_->setText(current_fileinfo.dir().filePath(basename)); +} + +void ExportDialog::closeEvent(QCloseEvent *e) +{ + preview_viewer_->ConnectViewerNode(nullptr); + + QDialog::closeEvent(e); +} + +void ExportDialog::AddPreferencesTab(QWidget *inner_widget, const QString &title) +{ + QScrollArea* scroll_area = new QScrollArea(); + scroll_area->setWidgetResizable(true); + scroll_area->setWidget(inner_widget); + preferences_tabs_->addTab(scroll_area, title); +} + +void ExportDialog::BrowseFilename() +{ + ExportFormat::Format f = GetSelectedFormat(); + + QString browsed_fn = QFileDialog::getSaveFileName(this, + "", + filename_edit_->text().trimmed(), + QStringLiteral("%1 (*.%2)").arg(ExportFormat::GetName(f), ExportFormat::GetExtension(f)), + nullptr, + + // We don't confirm overwrite here because we do it later + QFileDialog::DontConfirmOverwrite); + + if (!browsed_fn.isEmpty()) { + filename_edit_->setText(browsed_fn); + } +} + +void ExportDialog::FormatChanged(int index) +{ + QString current_filename = filename_edit_->text().trimmed(); + QString previously_selected_ext = ExportFormat::GetExtension(previously_selected_format_); + ExportFormat::Format current_format = static_cast(format_combobox_->itemData(index).toInt()); + QString currently_selected_ext = ExportFormat::GetExtension(current_format); + + // If the previous extension was added, remove it + if (current_filename.endsWith(previously_selected_ext, Qt::CaseInsensitive)) { + current_filename.resize(current_filename.size() - previously_selected_ext.size() - 1); + } + + // Add the extension and set it + current_filename.append('.'); + current_filename.append(currently_selected_ext); + filename_edit_->setText(current_filename); + + previously_selected_format_ = current_format; + + // Update video and audio comboboxes + bool has_video_codecs = video_tab_->SetFormat(current_format); + video_enabled_->setChecked(has_video_codecs); + video_enabled_->setEnabled(has_video_codecs); + + bool has_audio_codecs = audio_tab_->SetFormat(current_format); + audio_enabled_->setChecked(has_audio_codecs); + audio_enabled_->setEnabled(has_audio_codecs); + + bool has_subtitle_codecs = subtitle_tab_->SetFormat(current_format); + subtitles_enabled_->setChecked(has_subtitle_codecs); + subtitles_enabled_->setEnabled(has_subtitle_codecs); +} + +void ExportDialog::ResolutionChanged() +{ + if (video_tab_->maintain_aspect_checkbox()->isChecked()) { + // Keep aspect ratio maintained + if (sender() == video_tab_->height_slider()) { + + // Convert height to float + double new_width = video_tab_->height_slider()->GetValue(); + + // Generate width from aspect ratio + new_width *= video_aspect_ratio_; + + // Align to even number and set + video_tab_->width_slider()->SetValue(new_width); + + } else { + + // Convert width to float + double new_height = video_tab_->width_slider()->GetValue(); + + // Generate height from aspect ratio + new_height /= video_aspect_ratio_; + + // Align to even number and set + video_tab_->height_slider()->SetValue(new_height); + + } + } + + UpdateViewerDimensions(); +} + +void ExportDialog::LoadPresets() +{ + +} + +void ExportDialog::SetDefaultFilename() +{ + Project* p = viewer_node_->project(); + + QDir doc_location; + + if (p->filename().isEmpty()) { + doc_location.setPath(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); + } else { + doc_location = QFileInfo(p->filename()).dir(); + } + + QString file_location = doc_location.filePath(viewer_node_->GetLabel()); + filename_edit_->setText(file_location); +} + +ExportParams ExportDialog::GenerateParams() const +{ + VideoParams video_render_params(static_cast(video_tab_->width_slider()->GetValue()), + static_cast(video_tab_->height_slider()->GetValue()), + GetSelectedTimebase(), + video_tab_->pixel_format_field()->GetPixelFormat(), + VideoParams::kInternalChannelCount, + video_tab_->pixel_aspect_combobox()->GetPixelAspectRatio(), + video_tab_->interlaced_combobox()->GetInterlaceMode(), + 1); + + AudioParams audio_render_params(audio_tab_->sample_rate_combobox()->currentData().toInt(), + audio_tab_->channel_layout_combobox()->GetChannelLayout(), + AudioParams::kInternalFormat); + + ExportParams params; + params.set_encoder(Encoder::GetTypeFromFormat(GetSelectedFormat())); + params.SetFilename(filename_edit_->text().trimmed()); + params.SetExportLength(viewer_node_->GetLength()); + + if (ExportCodec::IsCodecAStillImage(video_tab_->GetSelectedCodec()) && !video_tab_->IsImageSequenceSet()) { + // Exporting as image without exporting image sequence, only export one frame + rational export_time = video_tab_->GetStillImageTime(); + params.set_custom_range(TimeRange(export_time, export_time)); + } else if (range_combobox_->currentIndex() == kRangeInToOut) { + // Assume if this combobox is enabled, workarea is enabled - a check that we make in this dialog's constructor + params.set_custom_range(viewer_node_->GetTimelinePoints()->workarea()->range()); + } + + if (video_tab_->scaling_method_combobox()->isEnabled()) { + params.set_video_scaling_method(static_cast(video_tab_->scaling_method_combobox()->currentData().toInt())); + } + + if (video_enabled_->isChecked()) { + ExportCodec::Codec video_codec = video_tab_->GetSelectedCodec(); + params.EnableVideo(video_render_params, video_codec); + + params.set_video_threads(video_tab_->threads()); + + if (video_tab_->isVisible()) { + video_tab_->GetCodecSection()->AddOpts(¶ms); + } + + params.set_color_transform(video_tab_->CurrentOCIOColorSpace()); + + params.set_video_pix_fmt(video_tab_->pix_fmt()); + + params.set_video_is_image_sequence(video_tab_->IsImageSequenceSet()); + } + + if (audio_enabled_->isChecked()) { + ExportCodec::Codec audio_codec = static_cast(audio_tab_->codec_combobox()->currentData().toInt()); + params.EnableAudio(audio_render_params, audio_codec); + + params.set_audio_bit_rate(audio_tab_->bit_rate_slider()->GetValue() * 1000); + } + + if (subtitles_enabled_->isChecked()) { + params.EnableSubtitles(subtitle_tab_->GetSubtitleCodec()); + } + + return params; +} + +void ExportDialog::SetCurrentFormat(ExportFormat::Format format) +{ + for (int i=0; icount(); i++) { + if (format_combobox_->itemData(i).toInt() == format) { + format_combobox_->setCurrentIndex(i); + break; + } + } +} + +rational ExportDialog::GetExportLength() const +{ + if (range_combobox_->currentIndex() == kRangeInToOut) { + return viewer_node_->GetTimelinePoints()->workarea()->range().length(); + } else { + return viewer_node_->GetLength(); + } +} + +int64_t ExportDialog::GetExportLengthInTimebaseUnits() const +{ + return Timecode::time_to_timestamp(GetExportLength(), GetSelectedTimebase()); +} + +void ExportDialog::UpdateViewerDimensions() +{ + preview_viewer_->SetViewerResolution(static_cast(video_tab_->width_slider()->GetValue()), + static_cast(video_tab_->height_slider()->GetValue())); + + VideoParams vp = viewer_node_->GetVideoParams(); + + QMatrix4x4 transform = ExportParams::GenerateMatrix( + static_cast(video_tab_->scaling_method_combobox()->currentData().toInt()), + vp.width(), + vp.height(), + static_cast(video_tab_->width_slider()->GetValue()), + static_cast(video_tab_->height_slider()->GetValue()) + ); + + preview_viewer_->SetMatrix(transform); +} + +} diff --git a/app/dialog/export/export.h b/app/dialog/export/export.h new file mode 100644 index 000000000..ad8056fcc --- /dev/null +++ b/app/dialog/export/export.h @@ -0,0 +1,117 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef EXPORTDIALOG_H +#define EXPORTDIALOG_H + +#include +#include +#include +#include +#include + +#include "codec/exportcodec.h" +#include "codec/exportformat.h" +#include "exportaudiotab.h" +#include "exportsubtitlestab.h" +#include "exportvideotab.h" +#include "task/export/export.h" +#include "widget/viewer/viewer.h" + +namespace olive { + +class ExportDialog : public QDialog +{ + Q_OBJECT +public: + ExportDialog(ViewerOutput* viewer_node, QWidget* parent = nullptr); + + ExportFormat::Format GetSelectedFormat() const; + + rational GetSelectedTimebase() const; + +protected: + virtual void closeEvent(QCloseEvent *e) override; + +private: + void AddPreferencesTab(QWidget *inner_widget, const QString &title); + + void LoadPresets(); + void SetDefaultFilename(); + + ExportParams GenerateParams() const; + + void SetCurrentFormat(ExportFormat::Format format); + + ViewerOutput* viewer_node_; + + ExportFormat::Format previously_selected_format_; + + rational GetExportLength() const; + int64_t GetExportLengthInTimebaseUnits() const; + + enum RangeSelection { + kRangeEntireSequence, + kRangeInToOut + }; + + QTabWidget* preferences_tabs_; + + QComboBox* range_combobox_; + + QCheckBox* video_enabled_; + QCheckBox* audio_enabled_; + QCheckBox* subtitles_enabled_; + + ViewerWidget* preview_viewer_; + QLineEdit* filename_edit_; + QComboBox* format_combobox_; + + ExportVideoTab* video_tab_; + ExportAudioTab* audio_tab_; + ExportSubtitlesTab* subtitle_tab_; + + double video_aspect_ratio_; + + ColorManager* color_manager_; + + QWidget* preferences_area_; + QCheckBox *export_bkg_box_; + +private slots: + void BrowseFilename(); + + void FormatChanged(int index); + + void ResolutionChanged(); + + void UpdateViewerDimensions(); + + void StartExport(); + + void ExportFinished(); + + void ImageSequenceCheckBoxChanged(bool e); + +}; + +} + +#endif // EXPORTDIALOG_H diff --git a/app/dialog/export/exportadvancedvideodialog.cpp b/app/dialog/export/exportadvancedvideodialog.cpp new file mode 100644 index 000000000..71143b50b --- /dev/null +++ b/app/dialog/export/exportadvancedvideodialog.cpp @@ -0,0 +1,63 @@ +#include "exportadvancedvideodialog.h" + +#include +#include +#include +#include + +namespace olive { + +ExportAdvancedVideoDialog::ExportAdvancedVideoDialog(const QList &pix_fmts, QWidget *parent) : + QDialog(parent) +{ + setWindowTitle(tr("Advanced")); + + QVBoxLayout* layout = new QVBoxLayout(this); + + { + // Pixel Settings + QGroupBox* pixel_group = new QGroupBox(); + layout->addWidget(pixel_group); + pixel_group->setTitle(tr("Pixel")); + + QGridLayout* pixel_layout = new QGridLayout(pixel_group); + + int row = 0; + + pixel_layout->addWidget(new QLabel(tr("Pixel Format:")), row, 0); + + pixel_format_combobox_ = new QComboBox(); + pixel_format_combobox_->addItems(pix_fmts); + pixel_layout->addWidget(pixel_format_combobox_, row, 1); + + row++; + } + + { + // Performance Settings + QGroupBox* performance_group = new QGroupBox(); + layout->addWidget(performance_group); + performance_group->setTitle(tr("Performance")); + + QGridLayout* performance_layout = new QGridLayout(performance_group); + + int row = 0; + + performance_layout->addWidget(new QLabel(tr("Threads:")), row, 0); + + thread_slider_ = new IntegerSlider(); + thread_slider_->SetMinimum(0); + thread_slider_->SetDefaultValue(0); + thread_slider_->InsertLabelSubstitution(0, tr("Auto")); + performance_layout->addWidget(thread_slider_, row, 1); + + row++; + } + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(buttons, &QDialogButtonBox::accepted, this, &ExportAdvancedVideoDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &ExportAdvancedVideoDialog::reject); + layout->addWidget(buttons); +} + +} diff --git a/app/dialog/export/exportadvancedvideodialog.h b/app/dialog/export/exportadvancedvideodialog.h new file mode 100644 index 000000000..275ae44f9 --- /dev/null +++ b/app/dialog/export/exportadvancedvideodialog.h @@ -0,0 +1,47 @@ +#ifndef EXPORTADVANCEDVIDEODIALOG_H +#define EXPORTADVANCEDVIDEODIALOG_H + +#include +#include + +#include "widget/slider/integerslider.h" + +namespace olive { + +class ExportAdvancedVideoDialog : public QDialog +{ + Q_OBJECT +public: + ExportAdvancedVideoDialog(const QList& pix_fmts, + QWidget* parent = nullptr); + + int threads() const + { + return static_cast(thread_slider_->GetValue()); + } + + void set_threads(int t) + { + thread_slider_->SetValue(t); + } + + QString pix_fmt() const + { + return pixel_format_combobox_->currentText(); + } + + void set_pix_fmt(const QString& s) + { + pixel_format_combobox_->setCurrentText(s); + } + +private: + IntegerSlider* thread_slider_; + + QComboBox* pixel_format_combobox_; + +}; + +} + +#endif // EXPORTADVANCEDVIDEODIALOG_H diff --git a/app/dialog/export/exportaudiotab.cpp b/app/dialog/export/exportaudiotab.cpp new file mode 100644 index 000000000..0db6dd0cd --- /dev/null +++ b/app/dialog/export/exportaudiotab.cpp @@ -0,0 +1,89 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "exportaudiotab.h" + +#include +#include + +#include "core.h" + +namespace olive { + +ExportAudioTab::ExportAudioTab(QWidget* parent) : + QWidget(parent) +{ + QVBoxLayout* outer_layout = new QVBoxLayout(this); + + QGridLayout* layout = new QGridLayout(); + outer_layout->addLayout(layout); + + int row = 0; + + layout->addWidget(new QLabel(tr("Codec:")), row, 0); + + codec_combobox_ = new QComboBox(); + layout->addWidget(codec_combobox_, row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Sample Rate:")), row, 0); + + sample_rate_combobox_ = new SampleRateComboBox(); + layout->addWidget(sample_rate_combobox_, row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Channel Layout:")), row, 0); + + channel_layout_combobox_ = new ChannelLayoutComboBox(); + layout->addWidget(channel_layout_combobox_, row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Format:")), row, 0); + layout->addWidget(new QComboBox(), row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Bit Rate:")), row, 0); + + bit_rate_slider_ = new IntegerSlider(); + bit_rate_slider_->SetMinimum(32); + bit_rate_slider_->SetMaximum(320); + bit_rate_slider_->SetValue(256); + bit_rate_slider_->SetFormat(tr("%1 kbps")); + layout->addWidget(bit_rate_slider_, row, 1); + + outer_layout->addStretch(); +} + +int ExportAudioTab::SetFormat(ExportFormat::Format format) +{ + QList acodecs = ExportFormat::GetAudioCodecs(format); + setEnabled(!acodecs.isEmpty()); + codec_combobox()->clear(); + foreach (ExportCodec::Codec acodec, acodecs) { + codec_combobox()->addItem(ExportCodec::GetCodecName(acodec), acodec); + } + return acodecs.size(); +} + +} diff --git a/app/dialog/export/exportaudiotab.h b/app/dialog/export/exportaudiotab.h new file mode 100644 index 000000000..ef5155018 --- /dev/null +++ b/app/dialog/export/exportaudiotab.h @@ -0,0 +1,72 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef EXPORTAUDIOTAB_H +#define EXPORTAUDIOTAB_H + +#include +#include + +#include "common/define.h" +#include "codec/exportformat.h" +#include "widget/slider/integerslider.h" +#include "widget/standardcombos/standardcombos.h" + +namespace olive { + +class ExportAudioTab : public QWidget +{ + Q_OBJECT +public: + ExportAudioTab(QWidget* parent = nullptr); + + int SetFormat(ExportFormat::Format format); + + QComboBox* codec_combobox() const + { + return codec_combobox_; + } + + SampleRateComboBox* sample_rate_combobox() const + { + return sample_rate_combobox_; + } + + ChannelLayoutComboBox* channel_layout_combobox() const + { + return channel_layout_combobox_; + } + + IntegerSlider* bit_rate_slider() const + { + return bit_rate_slider_; + } + +private: + QComboBox* codec_combobox_; + SampleRateComboBox* sample_rate_combobox_; + ChannelLayoutComboBox* channel_layout_combobox_; + IntegerSlider* bit_rate_slider_; + +}; + +} + +#endif // EXPORTAUDIOTAB_H diff --git a/app/dialog/export/exportsubtitlestab.cpp b/app/dialog/export/exportsubtitlestab.cpp new file mode 100644 index 000000000..27c40fc16 --- /dev/null +++ b/app/dialog/export/exportsubtitlestab.cpp @@ -0,0 +1,37 @@ +#include "exportsubtitlestab.h" + +#include +#include + +namespace olive { + +ExportSubtitlesTab::ExportSubtitlesTab(QWidget *parent) : + QWidget(parent) +{ + QVBoxLayout* outer_layout = new QVBoxLayout(this); + + QGridLayout* layout = new QGridLayout(); + outer_layout->addLayout(layout); + + int row = 0; + + layout->addWidget(new QLabel(tr("Codec:")), row, 0); + + codec_combobox_ = new QComboBox(); + layout->addWidget(codec_combobox_, row, 1); + + outer_layout->addStretch(); +} + +int ExportSubtitlesTab::SetFormat(ExportFormat::Format format) +{ + auto scodecs = ExportFormat::GetSubtitleCodecs(format); + setEnabled(!scodecs.isEmpty()); + codec_combobox_->clear(); + foreach (ExportCodec::Codec scodec, scodecs) { + codec_combobox_->addItem(ExportCodec::GetCodecName(scodec), scodec); + } + return scodecs.size(); +} + +} diff --git a/app/dialog/export/exportsubtitlestab.h b/app/dialog/export/exportsubtitlestab.h new file mode 100644 index 000000000..fa81df56e --- /dev/null +++ b/app/dialog/export/exportsubtitlestab.h @@ -0,0 +1,51 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef EXPORTSUBTITLESTAB_H +#define EXPORTSUBTITLESTAB_H + +#include + +#include "codec/exportformat.h" +#include "render/subtitleparams.h" + +namespace olive { + +class ExportSubtitlesTab : public QWidget +{ + Q_OBJECT +public: + ExportSubtitlesTab(QWidget *parent = nullptr); + + int SetFormat(ExportFormat::Format format); + + ExportCodec::Codec GetSubtitleCodec() + { + return static_cast(codec_combobox_->currentData().toInt()); + } + +private: + QComboBox *codec_combobox_; + +}; + +} + +#endif // EXPORTSUBTITLESTAB_H diff --git a/app/dialog/export/exportvideotab.cpp b/app/dialog/export/exportvideotab.cpp new file mode 100644 index 000000000..d99cc59b0 --- /dev/null +++ b/app/dialog/export/exportvideotab.cpp @@ -0,0 +1,272 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "exportvideotab.h" + +#include +#include +#include +#include +#include + +#include "core.h" +#include "exportadvancedvideodialog.h" +#include "node/color/colormanager/colormanager.h" +#include "task/export/exportparams.h" + +namespace olive { + +ExportVideoTab::ExportVideoTab(ColorManager* color_manager, QWidget *parent) : + QWidget(parent), + color_manager_(color_manager), + threads_(0) +{ + QVBoxLayout* outer_layout = new QVBoxLayout(this); + + outer_layout->addWidget(SetupResolutionSection()); + + outer_layout->addWidget(SetupCodecSection()); + + outer_layout->addWidget(SetupColorSection()); + + outer_layout->addStretch(); +} + +int ExportVideoTab::SetFormat(ExportFormat::Format format) +{ + format_ = format; + + QList vcodecs = ExportFormat::GetVideoCodecs(format); + setEnabled(!vcodecs.isEmpty()); + codec_combobox()->clear(); + foreach (ExportCodec::Codec vcodec, vcodecs) { + codec_combobox()->addItem(ExportCodec::GetCodecName(vcodec), vcodec); + } + return vcodecs.size(); +} + +bool ExportVideoTab::IsImageSequenceSet() const +{ + ImageSection* img_section = dynamic_cast(codec_stack_->currentWidget()); + + return (img_section && img_section->IsImageSequenceChecked()); +} + +QWidget* ExportVideoTab::SetupResolutionSection() +{ + int row = 0; + + QGroupBox* resolution_group = new QGroupBox(); + resolution_group->setTitle(tr("General")); + + QGridLayout* layout = new QGridLayout(resolution_group); + + layout->addWidget(new QLabel(tr("Width:")), row, 0); + + width_slider_ = new IntegerSlider(); + width_slider_->SetMinimum(1); + layout->addWidget(width_slider_, row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Height:")), row, 0); + + height_slider_ = new IntegerSlider(); + height_slider_->SetMinimum(1); + layout->addWidget(height_slider_, row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Maintain Aspect Ratio:")), row, 0); + + maintain_aspect_checkbox_ = new QCheckBox(); + maintain_aspect_checkbox_->setChecked(true); + layout->addWidget(maintain_aspect_checkbox_, row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Scaling Method:")), row, 0); + + scaling_method_combobox_ = new QComboBox(); + scaling_method_combobox_->setEnabled(false); + scaling_method_combobox_->addItem(tr("Fit"), ExportParams::kFit); + scaling_method_combobox_->addItem(tr("Stretch"), ExportParams::kStretch); + scaling_method_combobox_->addItem(tr("Crop"), ExportParams::kCrop); + layout->addWidget(scaling_method_combobox_, row, 1); + + // Automatically enable/disable the scaling method depending on maintain aspect ratio + connect(maintain_aspect_checkbox_, &QCheckBox::toggled, this, &ExportVideoTab::MaintainAspectRatioChanged); + + row++; + + layout->addWidget(new QLabel(tr("Frame Rate:")), row, 0); + + frame_rate_combobox_ = new FrameRateComboBox(); + connect(frame_rate_combobox_, &FrameRateComboBox::FrameRateChanged, this, &ExportVideoTab::UpdateFrameRate); + layout->addWidget(frame_rate_combobox_, row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Pixel Aspect Ratio:")), row, 0); + + pixel_aspect_combobox_ = new PixelAspectRatioComboBox(); + layout->addWidget(pixel_aspect_combobox_, row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Interlacing:")), row, 0); + + interlaced_combobox_ = new InterlacedComboBox(); + layout->addWidget(interlaced_combobox_, row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Quality:")), row, 0); + + pixel_format_field_ = new PixelFormatComboBox(false); + layout->addWidget(pixel_format_field_, row, 1); + + return resolution_group; +} + +QWidget* ExportVideoTab::SetupColorSection() +{ + color_space_chooser_ = new ColorSpaceChooser(color_manager_, true, false); + connect(color_space_chooser_, &ColorSpaceChooser::InputColorSpaceChanged, this, &ExportVideoTab::ColorSpaceChanged); + return color_space_chooser_; +} + +QWidget *ExportVideoTab::SetupCodecSection() +{ + int row = 0; + + QGroupBox* codec_group = new QGroupBox(); + codec_group->setTitle(tr("Codec")); + + QGridLayout* codec_layout = new QGridLayout(codec_group); + + codec_layout->addWidget(new QLabel(tr("Codec:")), row, 0); + + codec_combobox_ = new QComboBox(); + codec_layout->addWidget(codec_combobox_, row, 1); + connect(codec_combobox_, + static_cast(&QComboBox::currentIndexChanged), + this, + &ExportVideoTab::VideoCodecChanged); + + row++; + + codec_stack_ = new CodecStack(); + codec_layout->addWidget(codec_stack_, row, 0, 1, 2); + + image_section_ = new ImageSection(); + codec_stack_->addWidget(image_section_); + + h264_section_ = new H264Section(); + codec_stack_->addWidget(h264_section_); + + h265_section_ = new H265Section(); + codec_stack_->addWidget(h265_section_); + + cineform_section_ = new CineformSection(); + codec_stack_->addWidget(cineform_section_); + + row++; + + QPushButton* advanced_btn = new QPushButton(tr("Advanced")); + connect(advanced_btn, &QPushButton::clicked, this, &ExportVideoTab::OpenAdvancedDialog); + codec_layout->addWidget(advanced_btn, row, 1); + + return codec_group; +} + +void ExportVideoTab::MaintainAspectRatioChanged(bool val) +{ + scaling_method_combobox_->setEnabled(!val); +} + +void ExportVideoTab::OpenAdvancedDialog() +{ + // Find export formats compatible with this encoder + QStringList pixel_formats = ExportFormat::GetPixelFormatsForCodec(format_, GetSelectedCodec()); + + ExportAdvancedVideoDialog d(pixel_formats, this); + + d.set_threads(threads_); + d.set_pix_fmt(pix_fmt_); + + if (d.exec() == QDialog::Accepted) { + threads_ = d.threads(); + pix_fmt_ = d.pix_fmt(); + } +} + +void ExportVideoTab::UpdateFrameRate(rational r) +{ + // Convert frame rate to timebase + r.flip(); + + for (int i=0; icount(); i++) { + ImageSection* img = dynamic_cast(codec_stack_->widget(i)); + if (img) { + img->SetTimebase(r); + } + } +} + +void ExportVideoTab::VideoCodecChanged() +{ + ExportCodec::Codec codec = GetSelectedCodec(); + + switch (codec) { + case ExportCodec::kCodecH264: + case ExportCodec::kCodecH264rgb: + SetCodecSection(h264_section_); + break; + case ExportCodec::kCodecH265: + SetCodecSection(h265_section_); + break; + case ExportCodec::kCodecCineform: + SetCodecSection(cineform_section_); + break; + default: + SetCodecSection(ExportCodec::IsCodecAStillImage(codec) ? image_section_ : nullptr); + } + + // Set default pixel format + QStringList pix_fmts = ExportFormat::GetPixelFormatsForCodec(format_, codec); + if (!pix_fmts.isEmpty()) { + pix_fmt_ = pix_fmts.first(); + } else { + pix_fmt_.clear(); + } +} + +void ExportVideoTab::SetTime(const rational &time) +{ + for (int i=0; icount(); i++) { + ImageSection* img = dynamic_cast(codec_stack_->widget(i)); + if (img) { + img->SetTime(time); + } + } +} + +} diff --git a/app/dialog/export/exportvideotab.h b/app/dialog/export/exportvideotab.h new file mode 100644 index 000000000..4a952fb15 --- /dev/null +++ b/app/dialog/export/exportvideotab.h @@ -0,0 +1,194 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef EXPORTVIDEOTAB_H +#define EXPORTVIDEOTAB_H + +#include +#include +#include + +#include "common/rational.h" +#include "dialog/export/codec/cineformsection.h" +#include "dialog/export/codec/codecstack.h" +#include "dialog/export/codec/h264section.h" +#include "dialog/export/codec/imagesection.h" +#include "node/color/colormanager/colormanager.h" +#include "widget/colorwheel/colorspacechooser.h" +#include "widget/slider/integerslider.h" +#include "widget/standardcombos/standardcombos.h" + +namespace olive { + +class ExportVideoTab : public QWidget +{ + Q_OBJECT +public: + ExportVideoTab(ColorManager* color_manager, QWidget* parent = nullptr); + + int SetFormat(ExportFormat::Format format); + + bool IsImageSequenceSet() const; + + rational GetStillImageTime() const + { + return image_section_->GetTime(); + } + + ExportCodec::Codec GetSelectedCodec() const + { + return static_cast(codec_combobox()->currentData().toInt()); + } + + QComboBox* codec_combobox() const + { + return codec_combobox_; + } + + IntegerSlider* width_slider() const + { + return width_slider_; + } + + IntegerSlider* height_slider() const + { + return height_slider_; + } + + QCheckBox* maintain_aspect_checkbox() const + { + return maintain_aspect_checkbox_; + } + + QComboBox* scaling_method_combobox() const + { + return scaling_method_combobox_; + } + + rational GetSelectedFrameRate() const + { + return frame_rate_combobox_->GetFrameRate(); + } + + void SetSelectedFrameRate(const rational& fr) + { + frame_rate_combobox_->SetFrameRate(fr); + UpdateFrameRate(fr); + } + + QString CurrentOCIOColorSpace() + { + return color_space_chooser_->input(); + } + + CodecSection* GetCodecSection() const + { + return static_cast(codec_stack_->currentWidget()); + } + + void SetCodecSection(CodecSection* section) + { + if (section) { + codec_stack_->setVisible(true); + codec_stack_->setCurrentWidget(section); + } else { + codec_stack_->setVisible(false); + } + } + + InterlacedComboBox* interlaced_combobox() const + { + return interlaced_combobox_; + } + + PixelAspectRatioComboBox* pixel_aspect_combobox() const + { + return pixel_aspect_combobox_; + } + + PixelFormatComboBox* pixel_format_field() const + { + return pixel_format_field_; + } + + const int& threads() const + { + return threads_; + } + + const QString& pix_fmt() const { + return pix_fmt_; + } + +public slots: + void VideoCodecChanged(); + + void SetTime(const rational &time); + +signals: + void ColorSpaceChanged(const QString& colorspace); + + void ImageSequenceCheckBoxChanged(bool e); + +private: + QWidget* SetupResolutionSection(); + QWidget* SetupColorSection(); + QWidget* SetupCodecSection(); + + QComboBox* codec_combobox_; + FrameRateComboBox* frame_rate_combobox_; + QCheckBox* maintain_aspect_checkbox_; + QComboBox* scaling_method_combobox_; + + CodecStack* codec_stack_; + ImageSection* image_section_; + H264Section* h264_section_; + H264Section* h265_section_; + CineformSection *cineform_section_; + + ColorSpaceChooser* color_space_chooser_; + + IntegerSlider* width_slider_; + IntegerSlider* height_slider_; + + ColorManager* color_manager_; + + InterlacedComboBox* interlaced_combobox_; + PixelAspectRatioComboBox* pixel_aspect_combobox_; + PixelFormatComboBox* pixel_format_field_; + + int threads_; + + QString pix_fmt_; + + ExportFormat::Format format_; + +private slots: + void MaintainAspectRatioChanged(bool val); + + void OpenAdvancedDialog(); + + void UpdateFrameRate(rational r); + +}; + +} + +#endif // EXPORTVIDEOTAB_H diff --git a/app/dialog/footageproperties/CMakeLists.txt b/app/dialog/footageproperties/CMakeLists.txt index 84282a556..25287c233 100644 --- a/app/dialog/footageproperties/CMakeLists.txt +++ b/app/dialog/footageproperties/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2019 Olive Team +# Copyright (C) 2020 Olive Team # # 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 @@ -18,7 +18,7 @@ add_subdirectory(streamproperties) set(OLIVE_SOURCES ${OLIVE_SOURCES} - dialog/footageproperties/footageproperties.h dialog/footageproperties/footageproperties.cpp + dialog/footageproperties/footageproperties.h PARENT_SCOPE ) diff --git a/app/dialog/footageproperties/footageproperties.cpp b/app/dialog/footageproperties/footageproperties.cpp index 94582b53c..7cb56c63a 100644 --- a/app/dialog/footageproperties/footageproperties.cpp +++ b/app/dialog/footageproperties/footageproperties.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2020 Olive Team 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 @@ -31,9 +31,12 @@ #include #include +#include "core.h" #include "streamproperties/audiostreamproperties.h" #include "streamproperties/videostreamproperties.h" -#include "undo/undostack.h" +#include "widget/nodeview/nodeviewundo.h" + +namespace olive { FootagePropertiesDialog::FootagePropertiesDialog(QWidget *parent, Footage *footage) : QDialog(parent), @@ -41,14 +44,14 @@ FootagePropertiesDialog::FootagePropertiesDialog(QWidget *parent, Footage *foota { QGridLayout* layout = new QGridLayout(this); - setWindowTitle(tr("\"%1\" Properties").arg(footage_->name())); + setWindowTitle(tr("\"%1\" Properties").arg(footage_->GetLabelOrName())); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); int row = 0; layout->addWidget(new QLabel(tr("Name:")), row, 0); - footage_name_field_ = new QLineEdit(footage_->name()); + footage_name_field_ = new QLineEdit(footage_->GetLabel()); layout->addWidget(footage_name_field_, row, 1); row++; @@ -63,182 +66,113 @@ FootagePropertiesDialog::FootagePropertiesDialog(QWidget *parent, Footage *foota stacked_widget_ = new QStackedWidget(); layout->addWidget(stacked_widget_, row, 0, 1, 2); - foreach (StreamPtr stream, footage_->streams()) { - QListWidgetItem* item = new QListWidgetItem(stream->description(), track_list); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(stream->enabled() ? Qt::Checked : Qt::Unchecked); - track_list->addItem(item); + int first_usable_stream = -1; - switch (stream->type()) { - case Stream::kVideo: - stacked_widget_->addWidget(new VideoStreamProperties(std::static_pointer_cast(stream))); + for (int i=0; iGetTotalStreamCount(); i++) { + Track::Reference reference = footage_->GetReferenceFromRealIndex(i); + + QString description; + bool is_enabled = false; + + switch (reference.type()) { + case Track::kVideo: + { + stacked_widget_->addWidget(new VideoStreamProperties(footage_, reference.index())); + + VideoParams vp = footage_->GetVideoParams(reference.index()); + is_enabled = vp.enabled(); + description = tr("%1x%2 %3 FPS").arg(QString::number(vp.width()), QString::number(vp.height()), QString::number(vp.frame_rate().toDouble())); break; - case Stream::kAudio: - stacked_widget_->addWidget(new AudioStreamProperties(std::static_pointer_cast(stream))); + } + case Track::kAudio: + { + stacked_widget_->addWidget(new AudioStreamProperties(footage_, reference.index())); + + AudioParams ap = footage_->GetAudioParams(reference.index()); + is_enabled = ap.enabled(); + description = tr("%1 Hz %2 channels").arg(QString::number(ap.sample_rate()), QString::number(ap.channel_count())); break; + } default: stacked_widget_->addWidget(new StreamProperties()); + description = tr("Unknown"); + break; + } + + QListWidgetItem* item = new QListWidgetItem(description, track_list); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(is_enabled ? Qt::Checked : Qt::Unchecked); + track_list->addItem(item); + + if (first_usable_stream == -1 + && (reference.type() == Track::kVideo + || reference.type() == Track::kAudio)) { + first_usable_stream = i; } } row++; - /*if (f->video_tracks.size() > 0) { - // frame conforming - if (!f->video_tracks.at(0).infinite_length) { - layout->addWidget(new QLabel(tr("Conform to Frame Rate:"), this), row, 0); - conform_fr = new QDoubleSpinBox(this); - conform_fr->setMinimum(0.01); - conform_fr->setValue(f->video_tracks.at(0).video_frame_rate * f->speed); - layout->addWidget(conform_fr, row, 1); - } - - row++; - - // premultiplied alpha mode - premultiply_alpha_setting = new QCheckBox(tr("Alpha is Premultiplied"), this); - premultiply_alpha_setting->setChecked(f->alpha_is_associated); - layout->addWidget(premultiply_alpha_setting, row, 0); - - row++; - - // deinterlacing mode - interlacing_box = new QComboBox(this); - interlacing_box->addItem( - tr("Auto (%1)").arg( - Footage::get_interlacing_name(f->video_tracks.at(0).video_auto_interlacing) - ) - ); - interlacing_box->addItem(Footage::get_interlacing_name(VIDEO_PROGRESSIVE)); - interlacing_box->addItem(Footage::get_interlacing_name(VIDEO_TOP_FIELD_FIRST)); - interlacing_box->addItem(Footage::get_interlacing_name(VIDEO_BOTTOM_FIELD_FIRST)); - - interlacing_box->setCurrentIndex( - (f->video_tracks.at(0).video_auto_interlacing == f->video_tracks.at(0).video_interlacing) - ? 0 - : f->video_tracks.at(0).video_interlacing + 1); - - layout->addWidget(new QLabel(tr("Interlacing:"), this), row, 0); - layout->addWidget(interlacing_box, row, 1); - - row++; - - input_color_space = new QComboBox(this); - - OCIO::ConstConfigRcPtr config = OCIO::GetCurrentConfig(); - - QString footage_colorspace = f->Colorspace(); - - for (int i=0;igetNumColorSpaces();i++) { - QString colorspace = config->getColorSpaceNameByIndex(i); - - input_color_space->addItem(colorspace); - - if (colorspace == footage_colorspace) { - input_color_space->setCurrentIndex(i); - } - } - - layout->addWidget(new QLabel(tr("Color Space:")), row, 0); - layout->addWidget(input_color_space, row, 1); - - row++; - - } - */ - - connect(track_list, SIGNAL(currentRowChanged(int)), stacked_widget_, SLOT(setCurrentIndex(int))); - - QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); buttons->setCenterButtons(true); layout->addWidget(buttons, row, 0, 1, 2); - connect(buttons, SIGNAL(accepted()), this, SLOT(accept())); - connect(buttons, SIGNAL(rejected()), this, SLOT(reject())); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + + connect(track_list, &QListWidget::currentRowChanged, stacked_widget_, &QStackedWidget::setCurrentIndex); + + // Auto-select first item that actually has properties + if (first_usable_stream >= 0) { + track_list->setCurrentRow(first_usable_stream); + } + track_list->setFocus(); } -void FootagePropertiesDialog::accept() { - /*Footage* f = item->to_footage(); +void FootagePropertiesDialog::accept() +{ + // Perform sanity check on all pages + for (int i=0;icount();i++) { + if (!static_cast(stacked_widget_->widget(i))->SanityCheck()) { + // Switch to the failed panel in question + stacked_widget_->setCurrentIndex(i); - ComboAction* ca = new ComboAction(); - - // set track enable - for (int i=0;icount();i++) { - QListWidgetItem* item = track_list->item(i); - const QVariant& data = item->data(Qt::UserRole+1); - if (!data.isNull()) { - int index = data.toInt(); - bool found = false; - for (int j=0;jvideo_tracks.size();j++) { - if (f->video_tracks.at(j).file_index == index) { - f->video_tracks[j].enabled = (item->checkState() == Qt::Checked); - found = true; - break; - } - } - if (!found) { - for (int j=0;jaudio_tracks.size();j++) { - if (f->audio_tracks.at(j).file_index == index) { - f->audio_tracks[j].enabled = (item->checkState() == Qt::Checked); - break; - } - } - } + // Do nothing (it's up to the property panel itself to throw the error message) + return; } } - bool refresh_clips = false; + MultiUndoCommand* command = new MultiUndoCommand(); - // set interlacing - if (f->video_tracks.size() > 0) { - if (interlacing_box->currentIndex() > 0) { - ca->append(new SetInt(&f->video_tracks[0].video_interlacing, interlacing_box->currentIndex() - 1)); - } else { - ca->append(new SetInt(&f->video_tracks[0].video_interlacing, f->video_tracks.at(0).video_auto_interlacing)); + if (footage_->GetLabel() != footage_name_field_->text()) { + NodeRenameCommand *nrc = new NodeRenameCommand(); + nrc->AddNode(footage_, footage_name_field_->text()); + command->add_child(nrc); + } + + for (int i=0; iGetTotalStreamCount(); i++) { + Track::Reference reference = footage_->GetReferenceFromRealIndex(i); + bool new_stream_enabled = (track_list->item(i)->checkState() == Qt::Checked); + bool old_stream_enabled = new_stream_enabled; + + switch (reference.type()) { + case Track::kVideo: + old_stream_enabled = footage_->GetVideoParams(reference.index()).enabled(); + break; + case Track::kAudio: + old_stream_enabled = footage_->GetAudioParams(reference.index()).enabled(); + break; + case Track::kSubtitle: + case Track::kNone: + case Track::kCount: + break; } - // set frame rate conform - if (!f->video_tracks.at(0).infinite_length) { - if (!qFuzzyCompare(conform_fr->value(), f->video_tracks.at(0).video_frame_rate)) { - ca->append(new SetDouble(&f->speed, f->speed, conform_fr->value()/f->video_tracks.at(0).video_frame_rate)); - refresh_clips = true; - } - } - - // set premultiplied alpha - f->alpha_is_associated = premultiply_alpha_setting->isChecked(); - } - - f->SetColorspace(input_color_space->currentText()); - - // set name - MediaRename* mr = new MediaRename(item, name_box->text()); - - ca->append(mr); - ca->appendPost(new CloseAllClipsCommand()); - ca->appendPost(new UpdateFootageTooltip(item)); - if (refresh_clips) { - ca->appendPost(new RefreshClips(item)); - } - ca->appendPost(new UpdateViewer()); - - olive::undo_stack.push(ca);*/ - - QUndoCommand* command = new QUndoCommand(); - - if (footage_->name() != footage_name_field_->text()) { - new FootageChangeCommand(footage_, - footage_name_field_->text(), - command); - } - - for (int i=0;istreams().size();i++) { - bool stream_enabled = (track_list->item(i)->checkState() == Qt::Checked); - - if (footage_->stream(i)->enabled() == stream_enabled) { - new StreamEnableChangeCommand(footage_->stream(i), - stream_enabled, - command); + if (old_stream_enabled != new_stream_enabled) { + command->add_child(new StreamEnableChangeCommand(footage_, + reference.type(), + reference.index(), + new_stream_enabled)); } } @@ -246,45 +180,72 @@ void FootagePropertiesDialog::accept() { static_cast(stacked_widget_->widget(i))->Accept(command); } - olive::undo_stack.pushIfHasChildren(command); + Core::instance()->undo_stack()->pushIfHasChildren(command); QDialog::accept(); } -FootagePropertiesDialog::FootageChangeCommand::FootageChangeCommand(Footage *footage, const QString &name, QUndoCommand* command) : - QUndoCommand(command), +FootagePropertiesDialog::StreamEnableChangeCommand::StreamEnableChangeCommand(Footage *footage, Track::Type type, int index_in_type, bool enabled) : footage_(footage), - new_name_(name) -{ -} - -void FootagePropertiesDialog::FootageChangeCommand::redo() -{ - old_name_ = footage_->name(); - - footage_->set_name(new_name_); -} - -void FootagePropertiesDialog::FootageChangeCommand::undo() -{ - footage_->set_name(old_name_); -} - -FootagePropertiesDialog::StreamEnableChangeCommand::StreamEnableChangeCommand(StreamPtr stream, bool enabled, QUndoCommand *command) : - QUndoCommand(command), - stream_(stream), + type_(type), + index_(index_in_type), new_enabled_(enabled) { } +Project *FootagePropertiesDialog::StreamEnableChangeCommand::GetRelevantProject() const +{ + return footage_->project(); +} + void FootagePropertiesDialog::StreamEnableChangeCommand::redo() { - old_enabled_ = stream_->enabled(); - - stream_->set_enabled(new_enabled_); + switch (type_) { + case Track::kVideo: + { + VideoParams vp = footage_->GetVideoParams(index_); + old_enabled_ = vp.enabled(); + vp.set_enabled(new_enabled_); + footage_->SetVideoParams(vp, index_); + break; + } + case Track::kAudio: + { + AudioParams ap = footage_->GetAudioParams(index_); + old_enabled_ = ap.enabled(); + ap.set_enabled(new_enabled_); + footage_->SetAudioParams(ap, index_); + break; + } + case Track::kSubtitle: + case Track::kNone: + case Track::kCount: + break; + } } void FootagePropertiesDialog::StreamEnableChangeCommand::undo() { - stream_->set_enabled(old_enabled_); + switch (type_) { + case Track::kVideo: + { + VideoParams vp = footage_->GetVideoParams(index_); + vp.set_enabled(old_enabled_); + footage_->SetVideoParams(vp, index_); + break; + } + case Track::kAudio: + { + AudioParams ap = footage_->GetAudioParams(index_); + ap.set_enabled(old_enabled_); + footage_->SetAudioParams(ap, index_); + break; + } + case Track::kSubtitle: + case Track::kNone: + case Track::kCount: + break; + } +} + } diff --git a/app/dialog/footageproperties/footageproperties.h b/app/dialog/footageproperties/footageproperties.h index 65af1baf8..aa71fa01e 100644 --- a/app/dialog/footageproperties/footageproperties.h +++ b/app/dialog/footageproperties/footageproperties.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2020 Olive Team 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 @@ -28,9 +28,11 @@ #include #include #include -#include -#include "project/item/footage/footage.h" +#include "node/project/footage/footage.h" +#include "undo/undocommand.h" + +namespace olive { /** * @brief The MediaPropertiesDialog class @@ -54,33 +56,23 @@ public: */ FootagePropertiesDialog(QWidget *parent, Footage* footage); private: - class FootageChangeCommand : public QUndoCommand { + class StreamEnableChangeCommand : public UndoCommand { public: - FootageChangeCommand(Footage* footage, - const QString& name, - QUndoCommand *command = nullptr); + StreamEnableChangeCommand(Footage *footage, + Track::Type type, + int index_in_type, + bool enabled); + virtual Project* GetRelevantProject() const override; + + protected: virtual void redo() override; virtual void undo() override; private: - Footage* footage_; - - QString new_name_; - QString old_name_; - }; - - class StreamEnableChangeCommand : public QUndoCommand { - public: - StreamEnableChangeCommand(StreamPtr stream, - bool enabled, - QUndoCommand* command = nullptr); - - virtual void redo() override; - virtual void undo() override; - - private: - StreamPtr stream_; + Footage *footage_; + Track::Type type_; + int index_; bool old_enabled_; bool new_enabled_; @@ -124,4 +116,6 @@ private slots: }; +} + #endif // MEDIAPROPERTIESDIALOG_H diff --git a/app/dialog/footageproperties/streamproperties/CMakeLists.txt b/app/dialog/footageproperties/streamproperties/CMakeLists.txt index b6952d2aa..3228e9520 100644 --- a/app/dialog/footageproperties/streamproperties/CMakeLists.txt +++ b/app/dialog/footageproperties/streamproperties/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2019 Olive Team +# Copyright (C) 2020 Olive Team # # 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 diff --git a/app/dialog/footageproperties/streamproperties/audiostreamproperties.cpp b/app/dialog/footageproperties/streamproperties/audiostreamproperties.cpp index 24e475515..0359e7577 100644 --- a/app/dialog/footageproperties/streamproperties/audiostreamproperties.cpp +++ b/app/dialog/footageproperties/streamproperties/audiostreamproperties.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2020 Olive Team 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 @@ -20,11 +20,18 @@ #include "audiostreamproperties.h" -AudioStreamProperties::AudioStreamProperties(AudioStreamPtr stream) : - stream_(stream) +namespace olive { + +AudioStreamProperties::AudioStreamProperties(Footage *footage, int audio_index) : + footage_(footage), + audio_index_(audio_index) { } -void AudioStreamProperties::Accept(QUndoCommand*) +void AudioStreamProperties::Accept(MultiUndoCommand*) { + Q_UNUSED(footage_) + Q_UNUSED(audio_index_) +} + } diff --git a/app/dialog/footageproperties/streamproperties/audiostreamproperties.h b/app/dialog/footageproperties/streamproperties/audiostreamproperties.h index 0942797cc..058ff2bfc 100644 --- a/app/dialog/footageproperties/streamproperties/audiostreamproperties.h +++ b/app/dialog/footageproperties/streamproperties/audiostreamproperties.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2020 Olive Team 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 @@ -21,18 +21,25 @@ #ifndef AUDIOSTREAMPROPERTIES_H #define AUDIOSTREAMPROPERTIES_H -#include "project/item/footage/audiostream.h" +#include "node/project/footage/footage.h" #include "streamproperties.h" +namespace olive { + class AudioStreamProperties : public StreamProperties { public: - AudioStreamProperties(AudioStreamPtr stream); + AudioStreamProperties(Footage *footage, int audio_index); - virtual void Accept(QUndoCommand* parent) override; + virtual void Accept(MultiUndoCommand* parent) override; private: - AudioStreamPtr stream_; + Footage *footage_; + + int audio_index_; + }; +} + #endif // AUDIOSTREAMPROPERTIES_H diff --git a/app/dialog/footageproperties/streamproperties/streamproperties.cpp b/app/dialog/footageproperties/streamproperties/streamproperties.cpp index 29d938948..96f3bbd5a 100644 --- a/app/dialog/footageproperties/streamproperties/streamproperties.cpp +++ b/app/dialog/footageproperties/streamproperties/streamproperties.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2020 Olive Team 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 @@ -20,7 +20,11 @@ #include "streamproperties.h" +namespace olive { + StreamProperties::StreamProperties(QWidget *parent) : QWidget(parent) { } + +} diff --git a/app/dialog/footageproperties/streamproperties/streamproperties.h b/app/dialog/footageproperties/streamproperties/streamproperties.h index 72840f010..c8457216b 100644 --- a/app/dialog/footageproperties/streamproperties/streamproperties.h +++ b/app/dialog/footageproperties/streamproperties/streamproperties.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2020 Olive Team 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 @@ -21,15 +21,24 @@ #ifndef STREAMPROPERTIES_H #define STREAMPROPERTIES_H -#include #include +#include "common/define.h" +#include "undo/undocommand.h" + +namespace olive { + class StreamProperties : public QWidget { public: StreamProperties(QWidget* parent = nullptr); - virtual void Accept(QUndoCommand*){} + virtual void Accept(MultiUndoCommand*){} + + virtual bool SanityCheck(){return true;} + }; +} + #endif // STREAMPROPERTIES_H diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp index 26ed33054..6fcbdd914 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2020 Olive Team 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 @@ -21,72 +21,251 @@ #include "videostreamproperties.h" #include +#include +#include #include -#include -namespace OCIO = OCIO_NAMESPACE::v1; +#include +#include "common/ocioutils.h" +#include "core.h" #include "undo/undostack.h" -VideoStreamProperties::VideoStreamProperties(VideoStreamPtr stream) : - stream_(stream) +namespace olive { + +VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index) : + footage_(footage), + video_index_(video_index), + video_premultiply_alpha_(nullptr) { QGridLayout* video_layout = new QGridLayout(this); video_layout->setMargin(0); - video_layout->addWidget(new QLabel(tr("Color Space:")), 0, 0); + int row = 0; + + video_layout->addWidget(new QLabel(tr("Pixel Aspect:")), row, 0); + + VideoParams vp = footage_->GetVideoParams(video_index_); + + pixel_aspect_combo_ = new PixelAspectRatioComboBox(); + pixel_aspect_combo_->SetPixelAspectRatio(vp.pixel_aspect_ratio()); + video_layout->addWidget(pixel_aspect_combo_, row, 1); + + row++; + + video_layout->addWidget(new QLabel(tr("Interlacing:")), row, 0); + + video_interlace_combo_ = new InterlacedComboBox(); + video_interlace_combo_->SetInterlaceMode(vp.interlacing()); + + video_layout->addWidget(video_interlace_combo_, row, 1); + + row++; + + video_layout->addWidget(new QLabel(tr("Color Space:")), row, 0); video_color_space_ = new QComboBox(); - OCIO::ConstConfigRcPtr config = OCIO::GetCurrentConfig(); + OCIO::ConstConfigRcPtr config = footage_->project()->color_manager()->GetConfig(); int number_of_colorspaces = config->getNumColorSpaces(); + video_color_space_->addItem(tr("Default (%1)").arg(footage_->project()->color_manager()->GetDefaultInputColorSpace())); + for (int i=0;igetColorSpaceNameByIndex(i); video_color_space_->addItem(colorspace); } - video_color_space_->setCurrentText(stream_->colorspace()); + video_color_space_->setCurrentText(vp.colorspace()); - video_layout->addWidget(video_color_space_, 0, 1); + video_layout->addWidget(video_color_space_, row, 1); - video_premultiply_alpha_ = new QCheckBox(tr("Premultiplied Alpha")); - video_premultiply_alpha_->setChecked(stream_->premultiplied_alpha()); - video_layout->addWidget(video_premultiply_alpha_, 1, 0, 1, 2); -} + if (vp.channel_count() == VideoParams::kRGBAChannelCount) { + row++; -void VideoStreamProperties::Accept(QUndoCommand *parent) -{ - if (video_premultiply_alpha_->isChecked() != stream_->premultiplied_alpha() - || video_color_space_->currentText() != stream_->colorspace()) { - new VideoStreamChangeCommand(stream_, - video_premultiply_alpha_->isChecked(), - video_color_space_->currentText(), - parent); + video_premultiply_alpha_ = new QCheckBox(tr("Premultiplied Alpha")); + video_premultiply_alpha_->setChecked(vp.premultiplied_alpha()); + video_layout->addWidget(video_premultiply_alpha_, row, 0, 1, 2); + } + + row++; + + if (vp.video_type() == VideoParams::kVideoTypeImageSequence) { + QGroupBox* imgseq_group = new QGroupBox(tr("Image Sequence")); + QGridLayout* imgseq_layout = new QGridLayout(imgseq_group); + + int imgseq_row = 0; + + imgseq_layout->addWidget(new QLabel(tr("Start Index:")), imgseq_row, 0); + + imgseq_start_time_ = new IntegerSlider(); + imgseq_start_time_->SetMinimum(0); + imgseq_start_time_->SetValue(vp.start_time()); + imgseq_layout->addWidget(imgseq_start_time_, imgseq_row, 1); + + imgseq_row++; + + imgseq_layout->addWidget(new QLabel(tr("End Index:")), imgseq_row, 0); + + imgseq_end_time_ = new IntegerSlider(); + imgseq_end_time_->SetMinimum(0); + imgseq_end_time_->SetValue(vp.start_time() + vp.duration() - 1); + imgseq_layout->addWidget(imgseq_end_time_, imgseq_row, 1); + + imgseq_row++; + + imgseq_layout->addWidget(new QLabel(tr("Frame Rate:")), imgseq_row, 0); + + imgseq_frame_rate_ = new FrameRateComboBox(); + imgseq_frame_rate_->SetFrameRate(vp.frame_rate()); + imgseq_layout->addWidget(imgseq_frame_rate_, imgseq_row, 1); + + video_layout->addWidget(imgseq_group, row, 0, 1, 2); } } -VideoStreamProperties::VideoStreamChangeCommand::VideoStreamChangeCommand(VideoStreamPtr stream, +void VideoStreamProperties::Accept(MultiUndoCommand *parent) +{ + QString set_colorspace; + + if (video_color_space_->currentIndex() > 0) { + set_colorspace = video_color_space_->currentText(); + } + + VideoParams vp = footage_->GetVideoParams(video_index_); + + if ((video_premultiply_alpha_ && video_premultiply_alpha_->isChecked() != vp.premultiplied_alpha()) + || set_colorspace != vp.colorspace() + || static_cast(video_interlace_combo_->currentIndex()) != vp.interlacing() + || pixel_aspect_combo_->GetPixelAspectRatio() != vp.pixel_aspect_ratio()) { + + parent->add_child(new VideoStreamChangeCommand(footage_, + video_index_, + video_premultiply_alpha_ ? video_premultiply_alpha_->isChecked() : vp.premultiplied_alpha(), + set_colorspace, + static_cast(video_interlace_combo_->currentIndex()), + pixel_aspect_combo_->GetPixelAspectRatio())); + } + + if (vp.video_type() == VideoParams::kVideoTypeImageSequence) { + int64_t new_dur = imgseq_end_time_->GetValue() - imgseq_start_time_->GetValue() + 1; + + if (vp.start_time() != imgseq_start_time_->GetValue() + || vp.duration() != new_dur + || vp.frame_rate() != imgseq_frame_rate_->GetFrameRate()) { + parent->add_child(new ImageSequenceChangeCommand(footage_, + video_index_, + imgseq_start_time_->GetValue(), + new_dur, + imgseq_frame_rate_->GetFrameRate())); + } + } +} + +bool VideoStreamProperties::SanityCheck() +{ + if (footage_->GetVideoParams(video_index_).video_type() == VideoParams::kVideoTypeImageSequence) { + if (imgseq_start_time_->GetValue() >= imgseq_end_time_->GetValue()) { + QMessageBox::critical(this, + tr("Invalid Configuration"), + tr("Image sequence end index must be a value higher than the start index."), + QMessageBox::Ok); + return false; + } + } + + return true; +} + +VideoStreamProperties::VideoStreamChangeCommand::VideoStreamChangeCommand(Footage *footage, + int video_index, bool premultiplied, QString colorspace, - QUndoCommand *parent) : - QUndoCommand(parent), - stream_(stream), + VideoParams::Interlacing interlacing, + const rational &pixel_ar) : + footage_(footage), + video_index_(video_index), new_premultiplied_(premultiplied), - new_colorspace_(colorspace) + new_colorspace_(colorspace), + new_interlacing_(interlacing), + new_pixel_ar_(pixel_ar) { } +Project *VideoStreamProperties::VideoStreamChangeCommand::GetRelevantProject() const +{ + return footage_->project(); +} + void VideoStreamProperties::VideoStreamChangeCommand::redo() { - old_premultiplied_ = stream_->premultiplied_alpha(); - old_colorspace_ = stream_->colorspace(); + VideoParams vp = footage_->GetVideoParams(video_index_); - stream_->set_premultiplied_alpha(new_premultiplied_); - stream_->set_colorspace(new_colorspace_); + old_premultiplied_ = vp.premultiplied_alpha(); + old_colorspace_ = vp.colorspace(); + old_interlacing_ = vp.interlacing(); + old_pixel_ar_ = vp.pixel_aspect_ratio(); + + vp.set_premultiplied_alpha(new_premultiplied_); + vp.set_colorspace(new_colorspace_); + vp.set_interlacing(new_interlacing_); + vp.set_pixel_aspect_ratio(new_pixel_ar_); + + footage_->SetVideoParams(vp, video_index_); } void VideoStreamProperties::VideoStreamChangeCommand::undo() { - stream_->set_premultiplied_alpha(old_premultiplied_); - stream_->set_colorspace(old_colorspace_); + VideoParams vp = footage_->GetVideoParams(video_index_); + + vp.set_premultiplied_alpha(old_premultiplied_); + vp.set_colorspace(old_colorspace_); + vp.set_interlacing(old_interlacing_); + vp.set_pixel_aspect_ratio(old_pixel_ar_); + + footage_->SetVideoParams(vp, video_index_); +} + +VideoStreamProperties::ImageSequenceChangeCommand::ImageSequenceChangeCommand(Footage *footage, int video_index, int64_t start_index, int64_t duration, const rational &frame_rate) : + footage_(footage), + video_index_(video_index), + new_start_index_(start_index), + new_duration_(duration), + new_frame_rate_(frame_rate) +{ +} + +Project *VideoStreamProperties::ImageSequenceChangeCommand::GetRelevantProject() const +{ + return footage_->project(); +} + +void VideoStreamProperties::ImageSequenceChangeCommand::redo() +{ + VideoParams vp = footage_->GetVideoParams(video_index_); + + old_start_index_ = vp.start_time(); + vp.set_start_time(new_start_index_); + + old_duration_ = vp.duration(); + vp.set_duration(new_duration_); + + old_frame_rate_ = vp.frame_rate(); + vp.set_frame_rate(new_frame_rate_); + vp.set_time_base(new_frame_rate_.flipped()); + + footage_->SetVideoParams(vp, video_index_); +} + +void VideoStreamProperties::ImageSequenceChangeCommand::undo() +{ + VideoParams vp = footage_->GetVideoParams(video_index_); + + vp.set_start_time(old_start_index_); + vp.set_duration(old_duration_); + vp.set_frame_rate(old_frame_rate_); + vp.set_time_base(old_frame_rate_.flipped()); + + footage_->SetVideoParams(vp, video_index_); +} + } diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.h b/app/dialog/footageproperties/streamproperties/videostreamproperties.h index 4536f907b..ea689e2f0 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.h +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2020 Olive Team 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 @@ -23,23 +23,28 @@ #include #include -#include -#include "project/item/footage/videostream.h" +#include "node/project/footage/footage.h" #include "streamproperties.h" +#include "widget/slider/integerslider.h" +#include "widget/standardcombos/standardcombos.h" + +namespace olive { class VideoStreamProperties : public StreamProperties { + Q_OBJECT public: - VideoStreamProperties(VideoStreamPtr stream); + VideoStreamProperties(Footage *footage, int video_index); - virtual void Accept(QUndoCommand* parent) override; + virtual void Accept(MultiUndoCommand *parent) override; + + virtual bool SanityCheck() override; private: - /** - * @brief Attached video stream - */ - VideoStreamPtr stream_; + Footage *footage_; + + int video_index_; /** * @brief Setting for associated/premultiplied alpha @@ -51,25 +56,93 @@ private: */ QComboBox* video_color_space_; - class VideoStreamChangeCommand : public QUndoCommand { + /** + * @brief Setting for video interlacing + */ + InterlacedComboBox* video_interlace_combo_; + + /** + * @brief Sets the start index for image sequences + */ + IntegerSlider* imgseq_start_time_; + + /** + * @brief Sets the end index for image sequences + */ + IntegerSlider* imgseq_end_time_; + + /** + * @brief Sets the frame rate for image sequences + */ + FrameRateComboBox* imgseq_frame_rate_; + + /** + * @brief Sets the pixel aspect ratio of the stream + */ + PixelAspectRatioComboBox* pixel_aspect_combo_; + + class VideoStreamChangeCommand : public UndoCommand { public: - VideoStreamChangeCommand(VideoStreamPtr stream, + VideoStreamChangeCommand(Footage *footage, + int video_index, bool premultiplied, QString colorspace, - QUndoCommand* parent = nullptr); + VideoParams::Interlacing interlacing, + const rational& pixel_ar); + virtual Project* GetRelevantProject() const override; + + protected: virtual void redo() override; virtual void undo() override; private: - VideoStreamPtr stream_; + Footage *footage_; + int video_index_; bool new_premultiplied_; QString new_colorspace_; + VideoParams::Interlacing new_interlacing_; + rational new_pixel_ar_; bool old_premultiplied_; QString old_colorspace_; + VideoParams::Interlacing old_interlacing_; + rational old_pixel_ar_; + }; + + class ImageSequenceChangeCommand : public UndoCommand { + public: + ImageSequenceChangeCommand(Footage *footage, + int video_index, + int64_t start_index, + int64_t duration, + const rational& frame_rate); + + virtual Project* GetRelevantProject() const override; + + protected: + virtual void redo() override; + virtual void undo() override; + + private: + Footage *footage_; + int video_index_; + + int64_t new_start_index_; + int64_t old_start_index_; + + int64_t new_duration_; + int64_t old_duration_; + + rational new_frame_rate_; + rational old_frame_rate_; + + }; + }; +} + #endif // VIDEOSTREAMPROPERTIES_H diff --git a/app/dialog/footagerelink/CMakeLists.txt b/app/dialog/footagerelink/CMakeLists.txt new file mode 100644 index 000000000..9697864b6 --- /dev/null +++ b/app/dialog/footagerelink/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + dialog/footagerelink/footagerelinkdialog.h + dialog/footagerelink/footagerelinkdialog.cpp + PARENT_SCOPE +) diff --git a/app/dialog/footagerelink/footagerelinkdialog.cpp b/app/dialog/footagerelink/footagerelinkdialog.cpp new file mode 100644 index 000000000..2f89de75e --- /dev/null +++ b/app/dialog/footagerelink/footagerelinkdialog.cpp @@ -0,0 +1,175 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + 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 . + +***/ + +#include "footagerelinkdialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace olive { + +FootageRelinkDialog::FootageRelinkDialog(const QVector &footage, QWidget* parent) : + QDialog(parent), + footage_(footage) +{ + QVBoxLayout* layout = new QVBoxLayout(this); + + layout->addWidget(new QLabel("The following files couldn't be found. Clips using them will be " + "unplayable until they're relinked.")); + + table_ = new QTreeWidget(); + + table_->setColumnCount(3); + table_->setHeaderLabels({tr("Footage"), tr("Filename"), tr("Actions")}); + table_->setRootIsDecorated(false); + table_->setSelectionBehavior(QAbstractItemView::SelectRows); + table_->header()->setSectionsMovable(false); + + // Prefer stretching URL column (QHeaderView defaults to stretching the last column, which in + // our case is just a browse button) + table_->header()->setSectionResizeMode(1, QHeaderView::Stretch); + table_->header()->setStretchLastSection(false); + + for (int i=0; isetProperty("index", i); + connect(item_browse_btn, &QPushButton::clicked, this, &FootageRelinkDialog::BrowseForFootage); + item_actions_layout->addWidget(item_browse_btn); + + item->setIcon(0, f->icon()); + item->setText(0, f->GetLabel()); + item->setText(1, f->filename()); + + table_->addTopLevelItem(item); + + table_->setItemWidget(item, 2, item_actions); + } + + layout->addWidget(table_); + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(buttons, &QDialogButtonBox::accepted, this, &FootageRelinkDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &FootageRelinkDialog::reject); + layout->addWidget(buttons); + + setWindowTitle(tr("Relink Footage")); + + +} + +void FootageRelinkDialog::UpdateFootageItem(int index) +{ + Footage* f = footage_.at(index); + QTreeWidgetItem* item = table_->topLevelItem(index); + item->setIcon(0, f->icon()); + item->setText(1, f->filename()); +} + +void FootageRelinkDialog::BrowseForFootage() +{ + int index = sender()->property("index").toInt(); + Footage* f = footage_.at(index); + + QFileInfo info(f->filename()); + + QString new_fn = QFileDialog::getOpenFileName(this, + tr("Relink \"%1\"").arg(f->GetLabel()), + info.absolutePath(), + QStringLiteral("%1;;%2 (**)").arg(info.fileName(), tr("All Files"))); + + // We received a new filename + if (!new_fn.isEmpty()) { + // Store original dir since we might be able to use this to find other files + QDir original_dir = info.dir(); + QDir new_dir = QFileInfo(new_fn).dir(); + + // Set new filename since this was set manually by the user + f->set_filename(new_fn); + + // Assume footage is valid here. We could do some decoder probing to ensure it's a usable file + // but otherwise we assume the user knows what they're doing here. + + // Set footage to valid and update icon + f->SetValid(); + + // Update item visually + UpdateFootageItem(index); + + // Check all other footage files for matches + for (int it=0; itIsValid()) { + // Get footage path relative to original directory + QString relative_to_original = original_dir.relativeFilePath(other_footage->filename()); + QString absolute_to_new = new_dir.filePath(relative_to_original); + + // Second attempt. Try appending the filename to our new filepath + if (!QFileInfo::exists(absolute_to_new)) { + QFileInfo file_info(other_footage->filename()); + absolute_to_new = new_dir.filePath(file_info.fileName()); + } + + // Check if file exists + if (QFileInfo::exists(absolute_to_new)) { + other_footage->set_filename(absolute_to_new); + other_footage->SetValid(); + UpdateFootageItem(it); + } + } + } + } + + // Check where the next invalid footage is. If there is none, accept automatically. Otherwise, + // jump to that footage so the user knows where it is. + int next_invalid = -1; + for (int i=0; iIsValid()) { + next_invalid = i; + break; + } + } + + if (next_invalid == -1) { + // No more invalid footage, just accept + this->accept(); + } else { + // Jump to next invalid footage + QModelIndex idx = table_->model()->index(next_invalid, 0); + table_->selectionModel()->select(idx, + QItemSelectionModel::Select | QItemSelectionModel::Rows); + table_->scrollTo(idx); + + } +} + +} diff --git a/app/dialog/footagerelink/footagerelinkdialog.h b/app/dialog/footagerelink/footagerelinkdialog.h new file mode 100644 index 000000000..251db09b1 --- /dev/null +++ b/app/dialog/footagerelink/footagerelinkdialog.h @@ -0,0 +1,51 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + 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 . + +***/ + +#ifndef FOOTAGERELINKDIALOG_H +#define FOOTAGERELINKDIALOG_H + +#include +#include + +#include "node/project/footage/footage.h" + +namespace olive { + +class FootageRelinkDialog : public QDialog +{ + Q_OBJECT +public: + FootageRelinkDialog(const QVector& footage, QWidget* parent = nullptr); + +private: + void UpdateFootageItem(int index); + + QTreeWidget* table_; + + QVector footage_; + +private slots: + void BrowseForFootage(); + +}; + +} + +#endif // FOOTAGERELINKDIALOG_H diff --git a/app/dialog/keyframeproperties/CMakeLists.txt b/app/dialog/keyframeproperties/CMakeLists.txt new file mode 100644 index 000000000..990386190 --- /dev/null +++ b/app/dialog/keyframeproperties/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + dialog/keyframeproperties/keyframeproperties.h + dialog/keyframeproperties/keyframeproperties.cpp + PARENT_SCOPE +) diff --git a/app/dialog/keyframeproperties/keyframeproperties.cpp b/app/dialog/keyframeproperties/keyframeproperties.cpp new file mode 100644 index 000000000..9637c486f --- /dev/null +++ b/app/dialog/keyframeproperties/keyframeproperties.cpp @@ -0,0 +1,242 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "keyframeproperties.h" + +#include +#include + +#include "core.h" +#include "common/timecodefunctions.h" +#include "widget/keyframeview/keyframeviewundo.h" +#include "widget/nodeparamview/nodeparamviewundo.h" + +namespace olive { + +KeyframePropertiesDialog::KeyframePropertiesDialog(const QVector &keys, const rational &timebase, QWidget *parent) : + QDialog(parent), + keys_(keys), + timebase_(timebase) +{ + setWindowTitle(tr("Keyframe Properties")); + + QGridLayout* layout = new QGridLayout(this); + + int row = 0; + + layout->addWidget(new QLabel("Time:"), row, 0); + + time_slider_ = new RationalSlider(); + time_slider_->SetDisplayType(RationalSlider::kTime); + time_slider_->SetTimebase(timebase_); + layout->addWidget(time_slider_, row, 1); + + row++; + + layout->addWidget(new QLabel("Type:"), row, 0); + + type_select_ = new QComboBox(); + connect(type_select_, SIGNAL(currentIndexChanged(int)), this, SLOT(KeyTypeChanged(int))); + layout->addWidget(type_select_, row, 1); + + row++; + + // Bezier handles + bezier_group_ = new QGroupBox(); + + QGridLayout* bezier_group_layout = new QGridLayout(bezier_group_); + + bezier_group_layout->addWidget(new QLabel(tr("In:")), 0, 0); + + bezier_in_x_slider_ = new FloatSlider(); + bezier_group_layout->addWidget(bezier_in_x_slider_, 0, 1); + + bezier_in_y_slider_ = new FloatSlider(); + bezier_group_layout->addWidget(bezier_in_y_slider_, 0, 2); + + bezier_group_layout->addWidget(new QLabel(tr("Out:")), 1, 0); + + bezier_out_x_slider_ = new FloatSlider(); + bezier_group_layout->addWidget(bezier_out_x_slider_, 1, 1); + + bezier_out_y_slider_ = new FloatSlider(); + bezier_group_layout->addWidget(bezier_out_y_slider_, 1, 2); + + layout->addWidget(bezier_group_, row, 0, 1, 2); + + bool all_same_time = true; + bool can_set_time = true; + + bool all_same_type = true; + + bool all_same_bezier_in_x = true; + bool all_same_bezier_in_y = true; + bool all_same_bezier_out_x = true; + bool all_same_bezier_out_y = true; + + for (int i=0;i 0) { + NodeKeyframe* prev_key = keys_.at(i-1); + NodeKeyframe* this_key = keys_.at(i); + + // Determine if the keyframes are all the same time or not + if (all_same_time) { + all_same_time = (prev_key->time() == this_key->time()); + } + + // Determine if the keyframes are all the same type + if (all_same_type) { + all_same_type = (prev_key->type() == this_key->type()); + } + + // Check all four bezier control points + if (all_same_bezier_in_x) { + all_same_bezier_in_x = (prev_key->bezier_control_in().x() == this_key->bezier_control_in().x()); + } + + if (all_same_bezier_in_y) { + all_same_bezier_in_y = (prev_key->bezier_control_in().y() == this_key->bezier_control_in().y()); + } + + if (all_same_bezier_out_x) { + all_same_bezier_out_x = (prev_key->bezier_control_out().x() == this_key->bezier_control_out().x()); + } + + if (all_same_bezier_out_y) { + all_same_bezier_out_y = (prev_key->bezier_control_out().y() == this_key->bezier_control_out().y()); + } + } + + // Determine if any keyframes are on the same track (in which case we can't set the time) + if (can_set_time) { + for (int j=0;jtrack() == keys_.at(i)->track()) { + can_set_time = false; + break; + } + } + } + + if (!all_same_time + && !all_same_type + && !can_set_time + && !all_same_bezier_in_x + && !all_same_bezier_in_y + && !all_same_bezier_out_x + && !all_same_bezier_out_y) { + break; + } + } + + if (all_same_time) { + time_slider_->SetValue(keys_.first()->time()); + } else { + time_slider_->SetTristate(); + } + + time_slider_->setEnabled(can_set_time); + + if (!all_same_type) { + // If all keyframes aren't the same type, add an empty item + type_select_->addItem(QStringLiteral("--"), -1); + + // Ensure UI updates for the index being 0 + KeyTypeChanged(0); + } + + type_select_->addItem(tr("Linear"), NodeKeyframe::kLinear); + type_select_->addItem(tr("Hold"), NodeKeyframe::kHold); + type_select_->addItem(tr("Bezier"), NodeKeyframe::kBezier); + + if (all_same_type) { + // If all keyframes are the same type, set it here + for (int i=0;icount();i++) { + if (type_select_->itemData(i).toInt() == keys_.first()->type()) { + type_select_->setCurrentIndex(i); + + // Ensure UI updates for this index + KeyTypeChanged(i); + break; + } + } + } + + SetUpBezierSlider(bezier_in_x_slider_, all_same_bezier_in_x, keys_.first()->bezier_control_in().x()); + SetUpBezierSlider(bezier_in_y_slider_, all_same_bezier_in_y, keys_.first()->bezier_control_in().y()); + SetUpBezierSlider(bezier_out_x_slider_, all_same_bezier_out_x, keys_.first()->bezier_control_out().x()); + SetUpBezierSlider(bezier_out_y_slider_, all_same_bezier_out_y, keys_.first()->bezier_control_out().y()); + + row++; + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + buttons->setCenterButtons(true); + layout->addWidget(buttons, row, 0, 1, 2); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +void KeyframePropertiesDialog::accept() +{ + MultiUndoCommand* command = new MultiUndoCommand(); + + rational new_time = time_slider_->GetValue(); + int new_type = type_select_->currentData().toInt(); + + foreach (NodeKeyframe* key, keys_) { + if (time_slider_->isEnabled() && !time_slider_->IsTristate()) { + command->add_child(new NodeParamSetKeyframeTimeCommand(key, new_time)); + } + + if (new_type > -1) { + command->add_child(new KeyframeSetTypeCommand(key, static_cast(new_type))); + } + + if (bezier_group_->isEnabled()) { + command->add_child(new KeyframeSetBezierControlPoint(key, + NodeKeyframe::kInHandle, + QPointF(bezier_in_x_slider_->GetValue(), bezier_in_y_slider_->GetValue()))); + + command->add_child(new KeyframeSetBezierControlPoint(key, + NodeKeyframe::kOutHandle, + QPointF(bezier_out_x_slider_->GetValue(), bezier_out_y_slider_->GetValue()))); + } + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); + + QDialog::accept(); +} + +void KeyframePropertiesDialog::SetUpBezierSlider(FloatSlider *slider, bool all_same, double value) +{ + if (all_same) { + slider->SetValue(value); + } else { + slider->SetTristate(); + } +} + +void KeyframePropertiesDialog::KeyTypeChanged(int index) +{ + bezier_group_->setEnabled(type_select_->itemData(index) == NodeKeyframe::kBezier); +} + +} diff --git a/app/dialog/keyframeproperties/keyframeproperties.h b/app/dialog/keyframeproperties/keyframeproperties.h new file mode 100644 index 000000000..835845943 --- /dev/null +++ b/app/dialog/keyframeproperties/keyframeproperties.h @@ -0,0 +1,71 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef KEYFRAMEPROPERTIESDIALOG_H +#define KEYFRAMEPROPERTIESDIALOG_H + +#include +#include +#include + +#include "node/keyframe.h" +#include "widget/slider/floatslider.h" +#include "widget/slider/rationalslider.h" + +namespace olive { + +class KeyframePropertiesDialog : public QDialog +{ + Q_OBJECT +public: + KeyframePropertiesDialog(const QVector& keys, const rational& timebase, QWidget* parent = nullptr); + +public slots: + virtual void accept() override; + +private: + void SetUpBezierSlider(FloatSlider *slider, bool all_same, double value); + + const QVector& keys_; + + rational timebase_; + + RationalSlider* time_slider_; + + QComboBox* type_select_; + + QGroupBox* bezier_group_; + + FloatSlider* bezier_in_x_slider_; + + FloatSlider* bezier_in_y_slider_; + + FloatSlider* bezier_out_x_slider_; + + FloatSlider* bezier_out_y_slider_; + +private slots: + void KeyTypeChanged(int index); + +}; + +} + +#endif // KEYFRAMEPROPERTIESDIALOG_H diff --git a/app/node/blend/CMakeLists.txt b/app/dialog/otioproperties/CMakeLists.txt similarity index 88% rename from app/node/blend/CMakeLists.txt rename to app/dialog/otioproperties/CMakeLists.txt index 645e6b7a9..8a07ce76a 100644 --- a/app/node/blend/CMakeLists.txt +++ b/app/dialog/otioproperties/CMakeLists.txt @@ -14,11 +14,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -add_subdirectory(alphaover) - set(OLIVE_SOURCES ${OLIVE_SOURCES} - node/blend/blend.h - node/blend/blend.cpp + dialog/otioproperties/otiopropertiesdialog.h + dialog/otioproperties/otiopropertiesdialog.cpp PARENT_SCOPE -) +) diff --git a/app/dialog/otioproperties/otiopropertiesdialog.cpp b/app/dialog/otioproperties/otiopropertiesdialog.cpp new file mode 100644 index 000000000..e153aa47b --- /dev/null +++ b/app/dialog/otioproperties/otiopropertiesdialog.cpp @@ -0,0 +1,89 @@ +/*** + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + 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 . +***/ + +#include "otiopropertiesdialog.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "core.h" +#include "dialog/sequence/sequence.h" + +namespace olive { + +OTIOPropertiesDialog::OTIOPropertiesDialog(const QList& sequences, Project* active_project, QWidget* parent) + : + QDialog(parent), + sequences_(sequences) +{ + QVBoxLayout* layout = new QVBoxLayout(this); + + QLabel *msg = new QLabel(tr("OpenTimelineIO files do not store sequence parameters (resolution, frame rate, etc.)\n\n" + "Please set the correct parameters on the sequences below (they have been set to your default sequence parameters as a starting point).")); + msg->setWordWrap(true); + layout->addWidget(msg); + + table_ = new QTreeWidget(); + table_->setColumnCount(2); + table_->setHeaderLabels({tr("Sequence"), tr("Actions")}); + table_->setRootIsDecorated(false); + + for (int i = 0; i < sequences.size(); i++) { + QTreeWidgetItem* item = new QTreeWidgetItem(); + Sequence* s = sequences.at(i); + + QWidget* item_actions = new QWidget(); + QHBoxLayout* item_actions_layout = new QHBoxLayout(item_actions); + QPushButton* item_settings_btn = new QPushButton(tr("Settings")); + item_settings_btn->setProperty("index", i); + connect(item_settings_btn, &QPushButton::clicked, this, &OTIOPropertiesDialog::SetupSequence); + item_actions_layout->addWidget(item_settings_btn); + + item->setText(0, s->GetLabel()); + + table_->addTopLevelItem(item); + + table_->setItemWidget(item, 1, item_actions); + } + + // Stretch first column to take up as much space as possible, and second column to take as little + table_->header()->setSectionResizeMode(0, QHeaderView::Stretch); + table_->header()->setSectionResizeMode(1, QHeaderView::Fixed); + table_->header()->setStretchLastSection(false); + + layout->addWidget(table_); + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); + connect(buttons, &QDialogButtonBox::accepted, this, &OTIOPropertiesDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &OTIOPropertiesDialog::reject); + layout->addWidget(buttons); + + setWindowTitle(tr("Load OpenTimelineIO Project")); +} + +void OTIOPropertiesDialog::SetupSequence() { + int index = sender()->property("index").toInt(); + Sequence* s = sequences_.at(index); + SequenceDialog sd(s, SequenceDialog::kNew); + sd.SetUndoable(false); + sd.exec(); +} + +} // namespace olive diff --git a/app/dialog/otioproperties/otiopropertiesdialog.h b/app/dialog/otioproperties/otiopropertiesdialog.h new file mode 100644 index 000000000..ad66bbd9f --- /dev/null +++ b/app/dialog/otioproperties/otiopropertiesdialog.h @@ -0,0 +1,39 @@ + +#ifndef OTIOPROPERTIESDIALOG_H +#define OTIOPROPERTIESDIALOG_H + +#include +#include + +#include "common/define.h" +#include "opentimelineio/timeline.h" +#include "node/project/sequence/sequence.h" +#include "node/project/project.h" + +namespace olive { + + /** + * @brief Dialog to load setting for OTIO sequences. + * + * Takes a list of Sequences and allows the setting of options for each. + */ + class OTIOPropertiesDialog : public QDialog { + Q_OBJECT + public: + OTIOPropertiesDialog(const QList& sequences, Project* active_project, QWidget* parent = nullptr); + + private: + QTreeWidget* table_; + + const QList sequences_; + + private slots: + /** + * @brief Brings up the Sequence settings dialog. + */ + void SetupSequence(); + }; + +} //namespace olive + +#endif // OTIOPROPERTIESDIALOG_H diff --git a/app/dialog/preferences/CMakeLists.txt b/app/dialog/preferences/CMakeLists.txt index 82201b179..4088607be 100644 --- a/app/dialog/preferences/CMakeLists.txt +++ b/app/dialog/preferences/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2019 Olive Team +# Copyright (C) 2021 Olive Team # # 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 diff --git a/app/dialog/preferences/keysequenceeditor.cpp b/app/dialog/preferences/keysequenceeditor.cpp index f476abed9..37ab138bb 100644 --- a/app/dialog/preferences/keysequenceeditor.cpp +++ b/app/dialog/preferences/keysequenceeditor.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -21,9 +21,14 @@ #include "keysequenceeditor.h" #include +#include + +namespace olive { + +#define super QKeySequenceEdit KeySequenceEditor::KeySequenceEditor(QWidget* parent, QAction* a) - : QKeySequenceEdit(parent), action(a) { + : super(parent), action(a) { setKeySequence(action->shortcut()); } @@ -40,9 +45,33 @@ QString KeySequenceEditor::action_name() { } QString KeySequenceEditor::export_shortcut() { - QString ks = keySequence().toString(); - if (ks != action->property("keydefault")) { - return action->property("id").toString() + "\t" + ks; + QKeySequence ks = keySequence(); + if (ks != action->property("keydefault").value()) { + return action->property("id").toString() + "\t" + ks.toString(); } return nullptr; } + +void KeySequenceEditor::keyPressEvent(QKeyEvent *e) +{ + if (e->key() == Qt::Key_Backspace) { + clear(); + } else if (e->key() == Qt::Key_Escape) { + e->ignore(); + } else{ + super::keyPressEvent(e); + } +} + +void KeySequenceEditor::keyReleaseEvent(QKeyEvent *e) +{ + if (e->key() == Qt::Key_Backspace) { + // Do nothing + } else if (e->key() == Qt::Key_Escape) { + e->ignore(); + } else { + super::keyReleaseEvent(e); + } +} + +} diff --git a/app/dialog/preferences/keysequenceeditor.h b/app/dialog/preferences/keysequenceeditor.h index bda946137..ea5a2b335 100644 --- a/app/dialog/preferences/keysequenceeditor.h +++ b/app/dialog/preferences/keysequenceeditor.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -23,6 +23,10 @@ #include +#include "common/debug.h" + +namespace olive { + /** * @brief The KeySequenceEditor class * @@ -90,6 +94,12 @@ public: * because a default shortcut does not need to be saved to a file. */ QString export_shortcut(); + +protected: + virtual void keyPressEvent(QKeyEvent *e) override; + + virtual void keyReleaseEvent(QKeyEvent *e) override; + private: /** * @brief Internal reference to the linked QAction @@ -97,4 +107,6 @@ private: QAction* action; }; +} + #endif // KEYSEQUENCEEDITOR_H diff --git a/app/dialog/preferences/preferences.cpp b/app/dialog/preferences/preferences.cpp index f87e35024..3eee5336d 100644 --- a/app/dialog/preferences/preferences.cpp +++ b/app/dialog/preferences/preferences.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -29,318 +29,23 @@ #include "tabs/preferencesgeneraltab.h" #include "tabs/preferencesbehaviortab.h" #include "tabs/preferencesappearancetab.h" -#include "tabs/preferencesplaybacktab.h" +#include "tabs/preferencesdisktab.h" #include "tabs/preferencesaudiotab.h" #include "tabs/preferenceskeyboardtab.h" +namespace olive { + PreferencesDialog::PreferencesDialog(QWidget *parent, QMenuBar* main_menu_bar) : - QDialog(parent) + ConfigDialogBase(parent) { setWindowTitle(tr("Preferences")); - QVBoxLayout* layout = new QVBoxLayout(this); - - QSplitter* splitter = new QSplitter(); - splitter->setChildrenCollapsible(false); - layout->addWidget(splitter); - - list_widget_ = new QListWidget(); - - preference_pane_stack_ = new QStackedWidget(this); - AddTab(new PreferencesGeneralTab(), tr("General")); AddTab(new PreferencesAppearanceTab(), tr("Appearance")); AddTab(new PreferencesBehaviorTab(), tr("Behavior")); - AddTab(new PreferencesPlaybackTab(), tr("Playback")); + AddTab(new PreferencesDiskTab(), tr("Disk")); AddTab(new PreferencesAudioTab(), tr("Audio")); AddTab(new PreferencesKeyboardTab(main_menu_bar), tr("Keyboard")); - - splitter->addWidget(list_widget_); - splitter->addWidget(preference_pane_stack_); - - QDialogButtonBox* buttonBox = new QDialogButtonBox(this); - buttonBox->setOrientation(Qt::Horizontal); - buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok); - - layout->addWidget(buttonBox); - - connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); - connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); - - connect(list_widget_, SIGNAL(currentRowChanged(int)), preference_pane_stack_, SLOT(setCurrentIndex(int))); - - // set up default sequence - /*default_sequence.set_name(tr("Default Sequence")); - default_sequence.set_width(olive::config.default_sequence_width); - default_sequence.set_height(olive::config.default_sequence_height); - default_sequence.set_frame_rate(olive::config.default_sequence_framerate); - default_sequence.set_audio_frequency(olive::config.default_sequence_audio_frequency); - default_sequence.set_audio_layout(olive::config.default_sequence_audio_channel_layout);*/ } -void PreferencesDialog::AddBoolPair(QCheckBox *ui, bool *value, bool restart_required) -{ - bool_ui.append(ui); - bool_value.append(value); - bool_restart_required.append(restart_required); - - ui->setChecked(*value); -} - -void PreferencesDialog::accept() -{ - foreach (PreferencesTab* tab, tabs_) { - tab->Accept(); - } - - QDialog::accept(); - - /* - bool restart_after_saving = false; - bool reinit_audio = false; - bool reload_language = false; - bool reload_effects = false; - bool reset_ocio_shaders = false; - bool reset_render_threads = false; - - // Validate whether the specified CSS file exists - if (!custom_css_fn->text().isEmpty() && !QFileInfo::exists(custom_css_fn->text())) { - QMessageBox::critical( - this, - tr("Invalid CSS File"), - tr("CSS file '%1' does not exist.").arg(custom_css_fn->text()) - ); - return; - } - - // Validate whether the chosen OCIO configuration file - if (enable_color_management->isChecked()) { - - // Check whether the file exists - if (!QFileInfo::exists(ocio_config_file->text())) { - - QString msg_title = tr("Invalid OpenColorIO Configuration File"); - QString msg_body; - - if (ocio_config_file->text().isEmpty()) { - msg_body = tr("You must specify an OpenColorIO configuration file if color management is enabled."); - } else { - msg_body = tr("OpenColorIO configuration file '%1' does not exist.").arg(ocio_config_file->text()); - } - - QMessageBox::critical( - this, - msg_title, - msg_body - ); - return; - - } else if (olive::config.ocio_config_path != ocio_config_file->text()) { - - // Check whether OCIO can load it - OCIO::ConstConfigRcPtr file_config = TestOCIOConfig(ocio_config_file->text().toUtf8()); - - if (!file_config) { - return; - } - - } - } - - // Validate whether one of the bool options requires a restart - bool bool_requires_restart = false; - for (int i=0;iisChecked() != *bool_value.at(i)) { - bool_requires_restart = true; - break; - } - } - - // Check if any settings will require a restart of Olive (including the bool options determined above) - if (bool_requires_restart - || olive::config.thumbnail_resolution != thumbnail_res_spinbox->value() - || olive::config.waveform_resolution != waveform_res_spinbox->value() - || olive::config.css_path != custom_css_fn->text() - || olive::config.style != static_cast(ui_style->currentData().toInt())) { - - // any changes to these settings will require a restart - ask the user if we should do one now or later - - int ret = QMessageBox::question(this, - "Restart Required", - "Some of the changed settings will require a restart of Olive. Would you like " - "to restart now?", - QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); - - if (ret == QMessageBox::Cancel) { - // Return to Preferences dialog without saving any settings - return; - } else if (ret == QMessageBox::Yes) { - - // Check if we can close the current project. If not, we'll treat it as if the user clicked "Cancel". - if (olive::Global->can_close_project()) { - restart_after_saving = true; - } else { - return; - } - } - // Selecting "No" will save the settings and not restart. They will become active next time Olive opens. - - } - - - // Everything checks out, start saving settings from the UI to the backend - olive::config.css_path = custom_css_fn->text(); - olive::config.recording_mode = recordingComboBox->currentIndex() + 1; - olive::config.img_seq_formats = imgSeqFormatEdit->text(); - olive::config.upcoming_queue_size = upcoming_queue_spinbox->value(); - olive::config.upcoming_queue_type = upcoming_queue_type->currentIndex(); - olive::config.previous_queue_size = previous_queue_spinbox->value(); - olive::config.previous_queue_type = previous_queue_type->currentIndex(); - - // Audio settings may require the audio device to be re-initiated. - if (kUsingAudioOutput != audio_output_devices->currentData().toString() - || olive::config.preferred_audio_input != audio_input_devices->currentData().toString() - || olive::config.audio_rate != audio_sample_rate->currentData().toInt()) { - reinit_audio = true; - } - kUsingAudioOutput = audio_output_devices->currentData().toString(); - olive::config.preferred_audio_input = audio_input_devices->currentData().toString(); - olive::config.audio_rate = audio_sample_rate->currentData().toInt(); - - olive::config.effect_textbox_lines = effect_textbox_lines_field->value(); - - // see if the language file should be reloaded (not necessary if the app is restarting anyway) - if (!restart_after_saving - && olive::config.language_file != language_combobox->currentData().toString()) { - reload_language = true; - } - olive::config.language_file = language_combobox->currentData().toString(); - - // Check whether OCIO settings will require a reset of the render threads - if (olive::config.playback_bit_depth != playback_bit_depth->currentIndex() - || olive::config.export_bit_depth != export_bit_depth->currentIndex()) { - reset_render_threads = true; - } - if (olive::config.ocio_config_path != ocio_config_file->text() - || olive::config.ocio_display != ocio_display->currentText() - || olive::config.ocio_view != ocio_view->currentText() - || olive::config.ocio_look != ocio_look->currentData().toString()) { - reset_ocio_shaders = true; - } - if (olive::config.ocio_config_path != ocio_config_file->text()) { - OCIO::SetCurrentConfig(OCIO::Config::CreateFromFile(ocio_config_file->text().toUtf8())); - olive::config.ocio_config_path = ocio_config_file->text(); - } - olive::config.enable_color_management = enable_color_management->isChecked(); - olive::config.playback_bit_depth = playback_bit_depth->currentIndex(); - olive::config.export_bit_depth = export_bit_depth->currentIndex(); - olive::config.ocio_display = ocio_display->currentText(); - olive::config.ocio_default_input_colorspace = ocio_default_input->currentText(); - olive::config.ocio_view = ocio_view->currentText(); - - // We use data here instead of text because there's a "(None)" option with an empty string - olive::config.ocio_look = ocio_look->currentData().toString(); - - - // Set default sequence options - olive::config.default_sequence_width = default_sequence.width(); - olive::config.default_sequence_height = default_sequence.height(); - olive::config.default_sequence_framerate = default_sequence.frame_rate(); - olive::config.default_sequence_audio_frequency = default_sequence.audio_frequency(); - olive::config.default_sequence_audio_channel_layout = default_sequence.audio_layout(); - - // Set all bool options - for (int i=0;iisChecked(); - } - - // Set new style - olive::config.style = static_cast(ui_style->currentData().toInt()); - - // Check if the thumbnail or waveform icon fields have changed, we may need to recreate the previews if so - if (olive::config.thumbnail_resolution != thumbnail_res_spinbox->value() - || olive::config.waveform_resolution != waveform_res_spinbox->value()) { - // we're changing the size of thumbnails and waveforms, so let's delete them and regenerate them next start - - // delete nothing - PreviewDeleteTypes delete_type = DELETE_NONE; - - if (olive::config.thumbnail_resolution != thumbnail_res_spinbox->value()) { - // delete existing thumbnails - olive::config.thumbnail_resolution = thumbnail_res_spinbox->value(); - - // delete only thumbnails - delete_type = DELETE_THUMBNAILS; - } - - if (olive::config.waveform_resolution != waveform_res_spinbox->value()) { - // delete existing waveforms - olive::config.waveform_resolution = waveform_res_spinbox->value(); - - // if we're already deleting thumbnails - if (delete_type == DELETE_THUMBNAILS) { - // delete all - delete_type = DELETE_BOTH; - } else { - // just delete waveforms - delete_type = DELETE_WAVEFORMS; - } - } - - delete_previews(delete_type); - } - - - - QDialog::accept(); - - if (restart_after_saving) { - - // since we already ran can_close_project(), bypass checking again by running set_modified(false) - olive::Global->set_modified(false); - - olive::MainWindow->close(); - - QProcess::startDetached(QApplication::applicationFilePath(), { olive::ActiveProjectFilename }); - } else { - - // Audio settings may require the audio device to be re-initiated. - if (reinit_audio) { - init_audio(); - } - - if (reload_effects) { - panel_effect_controls->Reload(); - } - - // reload language file if it changed - if (reload_language) { - olive::Global->load_translation_from_config(); - } - - if (reset_render_threads) { - if (panel_footage_viewer->seq != nullptr) { - panel_footage_viewer->seq->Close(); - } - panel_footage_viewer->viewer_widget()->get_renderer()->delete_ctx(); - if (panel_sequence_viewer->seq != nullptr) { - panel_sequence_viewer->seq->Close(); - } - panel_sequence_viewer->viewer_widget()->get_renderer()->delete_ctx(); - } else if (reset_ocio_shaders) { - panel_footage_viewer->viewer_widget()->get_renderer()->destroy_ocio(); - panel_sequence_viewer->viewer_widget()->get_renderer()->destroy_ocio(); - } - - } - */ -} - -void PreferencesDialog::AddTab(PreferencesTab *tab, const QString &title) -{ - list_widget_->addItem(title); - preference_pane_stack_->addWidget(tab); - - tabs_.append(tab); } diff --git a/app/dialog/preferences/preferences.h b/app/dialog/preferences/preferences.h index 5e44c43bb..cdf164cd2 100644 --- a/app/dialog/preferences/preferences.h +++ b/app/dialog/preferences/preferences.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -28,7 +28,9 @@ #include #include -#include "tabs/preferencestab.h" +#include "dialog/configbase/configdialogbase.h" + +namespace olive { /** * @brief The PreferencesDialog class @@ -36,7 +38,7 @@ * A dialog for the global application settings. Mostly an interface for Config. Can be loaded from any part of the * application. */ -class PreferencesDialog : public QDialog +class PreferencesDialog : public ConfigDialogBase { Q_OBJECT @@ -48,69 +50,10 @@ public: * * QWidget parent. Usually MainWindow. */ - explicit PreferencesDialog(QWidget *parent, QMenuBar* main_menu_bar); + PreferencesDialog(QWidget *parent, QMenuBar* main_menu_bar); -private slots: - /** - * @brief Override of accept to save preferences to Config. - */ - virtual void accept() override; - -private: - void AddTab(PreferencesTab* tab, const QString& title); - - QListWidget* list_widget_; - - QStackedWidget* preference_pane_stack_; - - QList tabs_; - - /** - * @brief Stored default Sequence object - * - * Default Sequence settings are loaded into an actual Sequence object that can be loaded into NewSequenceDialog - * for the sake of familiarity with the user. - */ - //Sequence default_sequence; - - /** - * @brief Add an automated QCheckBox+boolean value pair - * - * Many preferences are simple true/false (or on/off) options. Rather than adding a QCheckBox for each one and - * manually setting its checked value to the configuration setting (and vice versa when saving), this convenience - * function will add it to an automated set of checkboxes, automatically setting the checked state to the current - * setting, and then saving the new checked state back to the setting when the user accepts the changes (clicks OK). - * - * @param ui - * - * A valid QCheckBox item. This function does not take ownership of the QWidget or place it in a layout anywhere. - * - * @param value - * - * A pointer to the Boolean value this QCheckBox should be shared with. The QCheckBox widget's checked state will be - * set to the value of this pointer. - * - * @param restart_required - * - * Defaults to FALSE, set this to TRUE if changing this setting should prompt the user for a restart of Olive before - * the setting change takes effect. - */ - void AddBoolPair(QCheckBox* ui, bool* value, bool restart_required = false); - - /** - * @brief Internal array managed by AddBoolPair(). Do not access this directly. - */ - QVector bool_ui; - - /** - * @brief Internal array managed by AddBoolPair(). Do not access this directly. - */ - QVector bool_value; - - /** - * @brief Internal array managed by AddBoolPair(). Do not access this directly. - */ - QVector bool_restart_required; }; +} + #endif // PREFERENCESDIALOG_H diff --git a/app/dialog/preferences/tabs/CMakeLists.txt b/app/dialog/preferences/tabs/CMakeLists.txt index 10f978449..2ccb6938b 100644 --- a/app/dialog/preferences/tabs/CMakeLists.txt +++ b/app/dialog/preferences/tabs/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2019 Olive Team +# Copyright (C) 2021 Olive Team # # 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 @@ -20,15 +20,13 @@ set(OLIVE_SOURCES dialog/preferences/tabs/preferencesgeneraltab.cpp dialog/preferences/tabs/preferencesbehaviortab.h dialog/preferences/tabs/preferencesbehaviortab.cpp + dialog/preferences/tabs/preferencesdisktab.h + dialog/preferences/tabs/preferencesdisktab.cpp dialog/preferences/tabs/preferencesappearancetab.h dialog/preferences/tabs/preferencesappearancetab.cpp - dialog/preferences/tabs/preferencesplaybacktab.h - dialog/preferences/tabs/preferencesplaybacktab.cpp dialog/preferences/tabs/preferencesaudiotab.h dialog/preferences/tabs/preferencesaudiotab.cpp dialog/preferences/tabs/preferenceskeyboardtab.h dialog/preferences/tabs/preferenceskeyboardtab.cpp - dialog/preferences/tabs/preferencestab.h - dialog/preferences/tabs/preferencestab.cpp PARENT_SCOPE ) diff --git a/app/dialog/preferences/tabs/preferencesappearancetab.cpp b/app/dialog/preferences/tabs/preferencesappearancetab.cpp index ec9f808d8..bec5431ad 100644 --- a/app/dialog/preferences/tabs/preferencesappearancetab.cpp +++ b/app/dialog/preferences/tabs/preferencesappearancetab.cpp @@ -1,14 +1,41 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + #include "preferencesappearancetab.h" +#include #include #include +#include #include -#include + +#include "node/node.h" +#include "widget/colorbutton/colorbutton.h" +#include "widget/menu/menushared.h" +#include "ui/colorcoding.h" + +namespace olive { PreferencesAppearanceTab::PreferencesAppearanceTab() { QVBoxLayout* layout = new QVBoxLayout(this); - layout->setMargin(0); QGridLayout* appearance_layout = new QGridLayout(); layout->addLayout(appearance_layout); @@ -18,35 +45,60 @@ PreferencesAppearanceTab::PreferencesAppearanceTab() // Appearance -> Theme appearance_layout->addWidget(new QLabel(tr("Theme")), row, 0); - style_ = new QComboBox(); + style_combobox_ = new QComboBox(); - style_list_ = StyleManager::ListInternal(); + { + const QMap& themes = StyleManager::available_themes(); + QMap::const_iterator i; + for (i=themes.cbegin(); i!=themes.cend(); i++) { + style_combobox_->addItem(i.value(), i.key()); - foreach (StyleDescriptor s, style_list_) { - style_->addItem(s.name(), s.path()); - - if (s.path() == Config::Current()["Style"]) { - style_->setCurrentIndex(style_->count()-1); + if (StyleManager::GetStyle() == i.key()) { + style_combobox_->setCurrentIndex(style_combobox_->count()-1); + } } } - appearance_layout->addWidget(style_, row, 1, 1, 2); + appearance_layout->addWidget(style_combobox_, row, 1); row++; + { + QGroupBox* color_group = new QGroupBox(); + color_group->setTitle(tr("Default Node Colors")); + + QGridLayout* color_layout = new QGridLayout(color_group); + + for (int i=0; i(i)); + color_layout->addWidget(new QLabel(cat_name), i, 0); + + ColorCodingComboBox* ccc = new ColorCodingComboBox(); + ccc->SetColor(Config::Current()[QStringLiteral("CatColor%1").arg(i)].toInt()); + color_layout->addWidget(ccc, i, 1); + color_btns_.append(ccc); + } + + appearance_layout->addWidget(color_group, row, 0, 1, 2); + } + layout->addStretch(); } -void PreferencesAppearanceTab::Accept() +void PreferencesAppearanceTab::Accept(MultiUndoCommand *command) { - QString style_path = style_->currentData().toString(); - StyleManager::SetStyle(style_path); - Config::Current()["Style"] = style_path; + Q_UNUSED(command) - if (style_->currentIndex() < style_list_.size()) { - // This is an internal style, set accordingly + QString style_path = style_combobox_->currentData().toString(); - } else { - StyleManager::SetStyle(style_->currentData().toString()); + if (style_path != StyleManager::GetStyle()) { + StyleManager::SetStyle(style_path); + Config::Current()[QStringLiteral("Style")] = style_path; + } + + for (int i=0; iGetSelectedColor(); } } + +} diff --git a/app/dialog/preferences/tabs/preferencesappearancetab.h b/app/dialog/preferences/tabs/preferencesappearancetab.h index a8c46bc6b..3ee33a0ad 100644 --- a/app/dialog/preferences/tabs/preferencesappearancetab.h +++ b/app/dialog/preferences/tabs/preferencesappearancetab.h @@ -1,37 +1,54 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + #ifndef PREFERENCESAPPEARANCETAB_H #define PREFERENCESAPPEARANCETAB_H #include #include +#include -#include "preferencestab.h" +#include "dialog/configbase/configdialogbase.h" #include "ui/style/style.h" +#include "widget/colorlabelmenu/colorcodingcombobox.h" -class PreferencesAppearanceTab : public PreferencesTab +namespace olive { + +class PreferencesAppearanceTab : public ConfigDialogBaseTab { Q_OBJECT public: PreferencesAppearanceTab(); - virtual void Accept() override; + virtual void Accept(MultiUndoCommand* command) override; private: - /** - * @brief Show a file dialog to browse for an external CSS file to load for styling the application. - */ - void BrowseForCSS(); - /** * @brief UI widget for selecting the current UI style */ - QComboBox* style_; + QComboBox* style_combobox_; - /** - * @brief List of internal styles - */ - QList style_list_; + QVector color_btns_; - QString custom_style_path_; }; +} + #endif // PREFERENCESAPPEARANCETAB_H diff --git a/app/dialog/preferences/tabs/preferencesaudiotab.cpp b/app/dialog/preferences/tabs/preferencesaudiotab.cpp index 160feede8..8b16c350b 100644 --- a/app/dialog/preferences/tabs/preferencesaudiotab.cpp +++ b/app/dialog/preferences/tabs/preferencesaudiotab.cpp @@ -1,168 +1,208 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + #include "preferencesaudiotab.h" -#include #include +#include #include -#include #include "audio/audiomanager.h" #include "config/config.h" -PreferencesAudioTab::PreferencesAudioTab() : - has_devices_(false) +namespace olive { + +PreferencesAudioTab::PreferencesAudioTab() { - QGridLayout* audio_tab_layout = new QGridLayout(this); - audio_tab_layout->setMargin(0); + QVBoxLayout* audio_tab_layout = new QVBoxLayout(this); - int row = 0; + { + // Backend Layout + QGridLayout* main_layout = new QGridLayout(); + main_layout->setMargin(0); - // Audio -> Output Device - audio_tab_layout->addWidget(new QLabel(tr("Output Device:")), row, 0); + int row = 0; - audio_output_devices = new QComboBox(); - audio_tab_layout->addWidget(audio_output_devices, row, 1); + main_layout->addWidget(new QLabel(tr("Backend:")), row, 0); - row++; + audio_backend_combobox_ = new QComboBox(); + connect(audio_backend_combobox_, static_cast(&QComboBox::currentIndexChanged), this, &PreferencesAudioTab::RefreshDevices); + main_layout->addWidget(audio_backend_combobox_, row, 1); - // Audio -> Input Device - audio_tab_layout->addWidget(new QLabel(tr("Input Device:")), row, 0); + audio_tab_layout->addLayout(main_layout); + } - audio_input_devices = new QComboBox(); - audio_tab_layout->addWidget(audio_input_devices, row, 1); + { + QGroupBox* groupbox = new QGroupBox(); + audio_tab_layout->addWidget(groupbox); - row++; + QVBoxLayout* layout = new QVBoxLayout(groupbox); - // Audio -> Sample Rate + int row = 0; - audio_tab_layout->addWidget(new QLabel(tr("Sample Rate:")), row, 0); + { + // Output Group + QGroupBox* output_group = new QGroupBox(); + output_group->setTitle(tr("Output")); + layout->addWidget(output_group); - audio_sample_rate = new QComboBox(); - /*combobox_audio_sample_rates(audio_sample_rate); - for (int i=0;icount();i++) { - if (audio_sample_rate->itemData(i).toInt() == olive::config.audio_rate) { - audio_sample_rate->setCurrentIndex(i); - break; + QGridLayout* output_layout = new QGridLayout(output_group); + + output_layout->addWidget(new QLabel(tr("Device:")), row, 0); + + audio_output_devices_ = new QComboBox(); + output_layout->addWidget(audio_output_devices_, row, 1); } - }*/ - audio_tab_layout->addWidget(audio_sample_rate, row, 1); + row = 0; - row++; + { + QGroupBox* input_group = new QGroupBox(); + input_group->setTitle(tr("Input")); + layout->addWidget(input_group); - // Audio -> Audio Recording - audio_tab_layout->addWidget(new QLabel(tr("Audio Recording:"), this), row, 0); + QGridLayout* input_layout = new QGridLayout(input_group); - recordingComboBox = new QComboBox(); - recordingComboBox->addItem(tr("Mono")); - recordingComboBox->addItem(tr("Stereo")); -// recordingComboBox->setCurrentIndex(olive::config.recording_mode - 1); - audio_tab_layout->addWidget(recordingComboBox, row, 1); + input_layout->addWidget(new QLabel(tr("Device:")), row, 0); - row++; + audio_input_devices_ = new QComboBox(); + input_layout->addWidget(audio_input_devices_, row, 1); - QPushButton* refresh_devices = new QPushButton(tr("Refresh Devices")); - audio_tab_layout->addWidget(refresh_devices, row, 1); + row++; - row++; + input_layout->addWidget(new QLabel(tr("Recording Mode:"), this), row, 0); - RetrieveDeviceLists(); + recording_combobox_ = new QComboBox(); + recording_combobox_->addItem(tr("Mono")); + recording_combobox_->addItem(tr("Stereo")); + input_layout->addWidget(recording_combobox_, row, 1); + } - connect(refresh_devices, SIGNAL(clicked(bool)), this, SLOT(RefreshDevices())); - connect(AudioManager::instance(), SIGNAL(DeviceListReady()), this, SLOT(RetrieveDeviceLists())); + QHBoxLayout* refresh_layout = new QHBoxLayout(); + layout->addLayout(refresh_layout); + refresh_layout->addStretch(); + + refresh_devices_btn_ = new QPushButton(tr("Refresh Devices")); + refresh_layout->addWidget(refresh_devices_btn_); + + connect(refresh_devices_btn_, &QPushButton::clicked, this, &PreferencesAudioTab::HardRefreshBackends); + } + + audio_tab_layout->addStretch(); + + // Populate lists + RefreshBackends(); } -void PreferencesAudioTab::Accept() +void PreferencesAudioTab::Accept(MultiUndoCommand *command) { - // If we don't have the device list, we can't set it - if (!has_devices_) { - return; + Q_UNUSED(command) + + // Get device indexes + PaDeviceIndex output_device = audio_output_devices_->currentData().value(); + PaDeviceIndex input_device = audio_input_devices_->currentData().value(); + + // Get device names, which seem to be the closest thing we have to a "unique identifier" for them + Config::Current()[QStringLiteral("AudioOutput")] = audio_output_devices_->currentText(); + Config::Current()[QStringLiteral("AudioInput")] = audio_input_devices_->currentText(); + + // Set devices to be used from now on + AudioManager::instance()->SetOutputDevice(output_device); + AudioManager::instance()->SetInputDevice(input_device); +} + +void PreferencesAudioTab::RefreshBackends() +{ + audio_backend_combobox_->clear(); + for (PaHostApiIndex i=0, end=Pa_GetHostApiCount(); iaddItem(info->name); } - // Get device info - QAudioDeviceInfo selected_output; - QAudioDeviceInfo selected_input; + RefreshDevices(); - QString selected_output_name; - QString selected_input_name; - - // Index 0 is always the default device - if (audio_output_devices->currentIndex() == 0) { - selected_output = QAudioDeviceInfo::defaultOutputDevice(); - } else { - selected_output = AudioManager::instance()->ListOutputDevices().at(audio_output_devices->currentData().toInt()); - selected_output_name = selected_output.deviceName(); - } - - // Index 0 is always the default device - if (audio_input_devices->currentIndex() == 0) { - selected_input = QAudioDeviceInfo::defaultInputDevice(); - } else { - selected_input = AudioManager::instance()->ListInputDevices().at(audio_input_devices->currentData().toInt()); - selected_input_name = selected_input.deviceName(); - } - - // Save it in the global application preferences - // FIXME: Qt documentation states that QAudioDeviceInfo::deviceName() is a "unique identifiers", which would make them - // ideal for saving in preferences, but in practice they don't actually appear to be unique. - // See: https://bugreports.qt.io/browse/QTBUG-16841 - Config::Current()["AudioOutput"] = selected_output_name; - Config::Current()["AudioInput"] = selected_input_name; - - // Finally, set these as the current device - AudioManager::instance()->SetOutputDevice(selected_output); - AudioManager::instance()->SetInputDevice(selected_input); + AttemptToSetDevicesFromConfig(); } void PreferencesAudioTab::RefreshDevices() { - AudioManager::instance()->RefreshDevices(); - - RetrieveDeviceLists(); -} - -void PreferencesAudioTab::RetrieveDeviceLists() -{ - has_devices_ = false; - - audio_input_devices->clear(); - audio_output_devices->clear(); - - if (AudioManager::instance()->IsRefreshing()) { - audio_output_devices->addItem(tr("Please wait...")); - audio_input_devices->addItem(tr("Please wait...")); - audio_output_devices->setEnabled(false); - audio_input_devices->setEnabled(false); + if (audio_backend_combobox_->count() == 0) { return; } - audio_output_devices->setEnabled(true); - audio_input_devices->setEnabled(true); + PaHostApiIndex host_index = audio_backend_combobox_->currentIndex(); + const PaHostApiInfo *host = Pa_GetHostApiInfo(host_index); - // list all available audio output devices - bool found_preferred_device = false; - audio_output_devices->addItem(tr("Default"), ""); - for (int i=0;iListOutputDevices().size();i++) { - audio_output_devices->addItem(AudioManager::instance()->ListOutputDevices().at(i).deviceName(), - i); - if (!found_preferred_device - && AudioManager::instance()->ListOutputDevices().at(i).deviceName() == Config::Current()["AudioOutput"]) { - audio_output_devices->setCurrentIndex(audio_output_devices->count()-1); - found_preferred_device = true; + audio_output_devices_->clear(); + audio_input_devices_->clear(); + + for (int i=0; ideviceCount; i++) { + PaDeviceIndex device_index = Pa_HostApiDeviceIndexToDeviceIndex(host_index, i); + const PaDeviceInfo *device = Pa_GetDeviceInfo(device_index); + + if (device->maxOutputChannels) { + audio_output_devices_->addItem(device->name, device_index); + } + + if (device->maxInputChannels) { + audio_input_devices_->addItem(device->name, device_index); } } +} - // list all available audio input devices - found_preferred_device = false; - audio_input_devices->addItem(tr("Default"), ""); - for (int i=0;iListInputDevices().size();i++) { - audio_input_devices->addItem(AudioManager::instance()->ListInputDevices().at(i).deviceName(), - i); - if (!found_preferred_device - && AudioManager::instance()->ListInputDevices().at(i).deviceName() == Config::Current()["AudioInput"]) { - audio_input_devices->setCurrentIndex(audio_input_devices->count()-1); - found_preferred_device = true; +void PreferencesAudioTab::HardRefreshBackends() +{ + AudioManager::instance()->HardReset(); + RefreshBackends(); +} + +void PreferencesAudioTab::AttemptToSetDevicesFromConfig() +{ + // Load with currently active devices + PaDeviceIndex current_output_index = AudioManager::instance()->GetOutputDevice(); + PaDeviceIndex current_input_index = AudioManager::instance()->GetInputDevice(); + + const PaDeviceInfo *current_output = nullptr, *current_input = nullptr; + if (current_output_index != paNoDevice) { + current_output = Pa_GetDeviceInfo(current_output_index); + } + if (current_input_index != paNoDevice) { + current_input = Pa_GetDeviceInfo(current_input_index); + } + + if (current_output || current_input) { + PaHostApiIndex host = current_output ? current_output->hostApi : current_input->hostApi; + + // Set backend accordingly + audio_backend_combobox_->setCurrentIndex(host); + + // Device comboboxes should be populated correctly now + if (current_output) { + audio_output_devices_->setCurrentText(current_output->name); + } + + if (current_input) { + audio_input_devices_->setCurrentText(current_input->name); } } +} - has_devices_ = true; } diff --git a/app/dialog/preferences/tabs/preferencesaudiotab.h b/app/dialog/preferences/tabs/preferencesaudiotab.h index 8b3bcde32..38b6138e3 100644 --- a/app/dialog/preferences/tabs/preferencesaudiotab.h +++ b/app/dialog/preferences/tabs/preferencesaudiotab.h @@ -1,47 +1,75 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + #ifndef PREFERENCESAUDIOTAB_H #define PREFERENCESAUDIOTAB_H #include +#include -#include "preferencestab.h" +#include "dialog/configbase/configdialogbase.h" -class PreferencesAudioTab : public PreferencesTab +namespace olive { + +class PreferencesAudioTab : public ConfigDialogBaseTab { Q_OBJECT public: PreferencesAudioTab(); - virtual void Accept() override; + virtual void Accept(MultiUndoCommand* command) override; private: + QComboBox* audio_backend_combobox_; + /** * @brief UI widget for selecting the output audio device */ - QComboBox* audio_output_devices; + QComboBox* audio_output_devices_; /** * @brief UI widget for selecting the input audio device */ - QComboBox* audio_input_devices; - - /** - * @brief UI widget for selecting the audio sampling rates - */ - QComboBox* audio_sample_rate; + QComboBox* audio_input_devices_; /** * @brief UI widget for editing the recording channels */ - QComboBox* recordingComboBox; + QComboBox* recording_combobox_; + + /** + * @brief Button that triggers a refresh of the available audio devices + */ + QPushButton* refresh_devices_btn_; private slots: + void RefreshBackends(); + void RefreshDevices(); - void RetrieveDeviceLists(); + void HardRefreshBackends(); -private: - bool has_devices_; + void AttemptToSetDevicesFromConfig(); }; +} + #endif // PREFERENCESAUDIOTAB_H diff --git a/app/dialog/preferences/tabs/preferencesbehaviortab.cpp b/app/dialog/preferences/tabs/preferencesbehaviortab.cpp index 32aa493ab..8d71b3df5 100644 --- a/app/dialog/preferences/tabs/preferencesbehaviortab.cpp +++ b/app/dialog/preferences/tabs/preferencesbehaviortab.cpp @@ -1,48 +1,159 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + #include "preferencesbehaviortab.h" #include #include +#include "config/config.h" + +namespace olive { + PreferencesBehaviorTab::PreferencesBehaviorTab() { QVBoxLayout* layout = new QVBoxLayout(this); - layout->setMargin(0); behavior_tree_ = new QTreeWidget(); layout->addWidget(behavior_tree_); behavior_tree_->setHeaderLabel(tr("Behavior")); - behavior_tree_->setRootIsDecorated(false); - AddItem(tr("Add Default Effects to New Clips")); - AddItem(tr("Automatically Seek to the Beginning When Playing at the End of a Sequence")); - AddItem(tr("Selecting Also Seeks")); - AddItem(tr("Edit Tool Also Seeks")); - AddItem(tr("Edit Tool Selects Links")); - AddItem(tr("Seek Also Selects")); - AddItem(tr("Seek to the End of Pastes")); - AddItem(tr("Scroll Wheel Zooms")); - AddItem(tr("Invert Timeline Scroll Axes")); - AddItem(tr("Enable Drag Files to Timeline")); - AddItem(tr("Auto-Scale By Default")); - AddItem(tr("Auto-Seek to Imported Clips")); - AddItem(tr("Audio Scrubbing")); - AddItem(tr("Drop Files on Media to Replace")); - AddItem(tr("Enable Hover Focus")); - AddItem(tr("Ask For Name When Setting Marker")); + QTreeWidgetItem* general_group = AddParent(tr("General")); + AddItem(tr("Enable hover focus"), + QStringLiteral("HoverFocus"), + tr("Panels will be considered focused when the mouse cursor is over them without having to click them."), + general_group); + AddItem(tr("Enable slider ladder"), + QStringLiteral("UseSliderLadders"), + general_group); - //scroll_wheel_zooms->setToolTip(tr("Hold CTRL to toggle this setting")); + QTreeWidgetItem* audio_group = AddParent(tr("Audio")); + AddItem(tr("Enable audio scrubbing"), + QStringLiteral("AudioScrubbing"), + audio_group); + + QTreeWidgetItem* timeline_group = AddParent(tr("Timeline")); + AddItem(tr("Auto-Seek to Imported Clips"), + QStringLiteral("EnableSeekToImport"), + timeline_group); + AddItem(tr("Edit Tool Also Seeks"), + QStringLiteral("EditToolAlsoSeeks"), + timeline_group); + AddItem(tr("Edit Tool Selects Links"), + QStringLiteral("EditToolSelectsLinks"), + timeline_group); + AddItem(tr("Enable Drag Files to Timeline"), + QStringLiteral("EnableDragFilesToTimeline"), + timeline_group); + AddItem(tr("Invert Timeline Scroll Axes"), + QStringLiteral("InvertTimelineScrollAxes"), + tr("Hold ALT on any UI element to switch scrolling axes"), + timeline_group); + AddItem(tr("Seek Also Selects"), + QStringLiteral("SelectAlsoSeeks"), + timeline_group); + AddItem(tr("Seek to the End of Pastes"), + QStringLiteral("PasteSeeks"), + timeline_group); + AddItem(tr("Selecting Also Seeks"), + QStringLiteral("SelectAlsoSeeks"), + timeline_group); + + QTreeWidgetItem* playback_group = AddParent(tr("Playback")); + AddItem(tr("Ask For Name When Setting Marker"), + QStringLiteral("SetNameWithMarker"), + playback_group); + AddItem(tr("Automatically rewind at the end of a sequence"), + QStringLiteral("AutoSeekToBeginning"), + playback_group); + + QTreeWidgetItem* project_group = AddParent(tr("Project")); + AddItem(tr("Drop Files on Media to Replace"), + QStringLiteral("DropFileOnMediaToReplace"), + project_group); + + QTreeWidgetItem* node_group = AddParent(tr("Nodes")); + AddItem(tr("Add Default Effects to New Clips"), + QStringLiteral("AddDefaultEffectsToClips"), + node_group); + AddItem(tr("Auto-Scale By Default"), + QStringLiteral("AutoscaleByDefault"), + node_group); + AddItem(tr("Splitting Clips Copies Dependencies"), + QStringLiteral("SplitClipsCopyNodes"), + tr("Multiple clips can share the same nodes. Disable this to automatically share node " + "dependencies among clips when copying or splitting them."), + node_group); } -void PreferencesBehaviorTab::Accept() +void PreferencesBehaviorTab::Accept(MultiUndoCommand *command) { + Q_UNUSED(command) + QMap::const_iterator iterator; + + for (iterator=config_map_.begin();iterator!=config_map_.end();iterator++) { + Config::Current()[iterator.value()] = (iterator.key()->checkState(0) == Qt::Checked); + } } -void PreferencesBehaviorTab::AddItem(const QString &text, const QString& tooltip) +QTreeWidgetItem* PreferencesBehaviorTab::AddItem(const QString &text, const QString &config_key, const QString& tooltip, QTreeWidgetItem* parent) { QTreeWidgetItem* item = new QTreeWidgetItem({text}); item->setToolTip(0, tooltip); - item->setCheckState(0, Qt::Unchecked); - behavior_tree_->addTopLevelItem(item); + item->setCheckState(0, Config::Current()[config_key].toBool() ? Qt::Checked : Qt::Unchecked); + + config_map_.insert(item, config_key); + + if (parent) { + parent->addChild(item); + } else { + behavior_tree_->addTopLevelItem(item); + } + + return item; +} + +QTreeWidgetItem *PreferencesBehaviorTab::AddItem(const QString &text, const QString &config_key, QTreeWidgetItem *parent) +{ + return AddItem(text, config_key, QString(), parent); +} + +QTreeWidgetItem *PreferencesBehaviorTab::AddParent(const QString &text, const QString &tooltip, QTreeWidgetItem *parent) +{ + QTreeWidgetItem* item = new QTreeWidgetItem({text}); + item->setToolTip(0, tooltip); + + if (parent) { + parent->addChild(item); + } else { + behavior_tree_->addTopLevelItem(item); + } + + return item; +} + +QTreeWidgetItem *PreferencesBehaviorTab::AddParent(const QString &text, QTreeWidgetItem *parent) +{ + return AddParent(text, QString(), parent); +} + } diff --git a/app/dialog/preferences/tabs/preferencesbehaviortab.h b/app/dialog/preferences/tabs/preferencesbehaviortab.h index f0590e4a5..0367475ea 100644 --- a/app/dialog/preferences/tabs/preferencesbehaviortab.h +++ b/app/dialog/preferences/tabs/preferencesbehaviortab.h @@ -1,22 +1,53 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + #ifndef PREFERENCESBEHAVIORTAB_H #define PREFERENCESBEHAVIORTAB_H #include -#include "preferencestab.h" +#include "dialog/configbase/configdialogbase.h" -class PreferencesBehaviorTab : public PreferencesTab +namespace olive { + +class PreferencesBehaviorTab : public ConfigDialogBaseTab { Q_OBJECT public: PreferencesBehaviorTab(); - virtual void Accept() override; + virtual void Accept(MultiUndoCommand* command) override; private: - void AddItem(const QString& text, const QString &tooltip = QString()); + QTreeWidgetItem *AddParent(const QString& text, const QString &tooltip, QTreeWidgetItem *parent = nullptr); + QTreeWidgetItem *AddParent(const QString& text, QTreeWidgetItem *parent = nullptr); + + QTreeWidgetItem *AddItem(const QString& text, const QString& config_key, const QString &tooltip, QTreeWidgetItem *parent ); + QTreeWidgetItem *AddItem(const QString& text, const QString& config_key, QTreeWidgetItem *parent); + + QMap config_map_; QTreeWidget* behavior_tree_; + }; +} + #endif // PREFERENCESBEHAVIORTAB_H diff --git a/app/dialog/preferences/tabs/preferencesdisktab.cpp b/app/dialog/preferences/tabs/preferencesdisktab.cpp new file mode 100644 index 000000000..fc6b43d9c --- /dev/null +++ b/app/dialog/preferences/tabs/preferencesdisktab.cpp @@ -0,0 +1,122 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "preferencesdisktab.h" + +#include +#include +#include +#include +#include +#include + +#include "common/filefunctions.h" + +namespace olive { + +PreferencesDiskTab::PreferencesDiskTab() +{ + // Get default disk cache folder + default_disk_cache_folder_ = DiskManager::instance()->GetDefaultCacheFolder(); + + QVBoxLayout* outer_layout = new QVBoxLayout(this); + + QGroupBox* disk_management_group = new QGroupBox(tr("Disk Management")); + outer_layout->addWidget(disk_management_group); + + QGridLayout* disk_management_layout = new QGridLayout(disk_management_group); + + int row = 0; + + disk_management_layout->addWidget(new QLabel(tr("Disk Cache Location:")), row, 0); + + disk_cache_location_ = new PathWidget(default_disk_cache_folder_->GetPath()); + disk_management_layout->addWidget(disk_cache_location_, row, 1); + + row++; + + QPushButton* disk_cache_settings_btn = new QPushButton(tr("Disk Cache Settings")); + connect(disk_cache_settings_btn, &QPushButton::clicked, this, [this](){ + DiskManager::instance()->ShowDiskCacheSettingsDialog(disk_cache_location_->text(), this); + }); + disk_management_layout->addWidget(disk_cache_settings_btn, row, 1); + + row++; + + QGroupBox* cache_behavior = new QGroupBox(tr("Cache Behavior")); + outer_layout->addWidget(cache_behavior); + QGridLayout* cache_behavior_layout = new QGridLayout(cache_behavior); + + row = 0; + + cache_behavior_layout->addWidget(new QLabel(tr("Cache Ahead:")), row, 0); + + cache_ahead_slider_ = new FloatSlider(); + cache_ahead_slider_->SetFormat(tr("%1 seconds")); + cache_ahead_slider_->SetMinimum(0); + cache_ahead_slider_->SetValue(Config::Current()["DiskCacheAhead"].value().toDouble()); + cache_behavior_layout->addWidget(cache_ahead_slider_, row, 1); + + cache_behavior_layout->addWidget(new QLabel(tr("Cache Behind:")), row, 2); + + cache_behind_slider_ = new FloatSlider(); + cache_behind_slider_->SetMinimum(0); + cache_behind_slider_->SetFormat(tr("%1 seconds")); + cache_behind_slider_->SetValue(Config::Current()["DiskCacheBehind"].value().toDouble()); + cache_behavior_layout->addWidget(cache_behind_slider_, row, 3); + + outer_layout->addStretch(); +} + +bool PreferencesDiskTab::Validate() +{ + if (disk_cache_location_->text() != default_disk_cache_folder_->GetPath()) { + // Disk cache location is changing + + // Check if the user is okay with invalidating the current cache + if (!DiskManager::ShowDiskCacheChangeConfirmationDialog(this)) { + return false; + } + + // Check validity of the new path + if (!FileFunctions::DirectoryIsValid(disk_cache_location_->text(), true)) { + QMessageBox::critical(this, + tr("Disk Cache"), + tr("Failed to set disk cache location. Access was denied.")); + return false; + } + } + + return true; +} + +void PreferencesDiskTab::Accept(MultiUndoCommand *command) +{ + Q_UNUSED(command) + + if (disk_cache_location_->text() != default_disk_cache_folder_->GetPath()) { + default_disk_cache_folder_->SetPath(disk_cache_location_->text()); + } + + Config::Current()["DiskCacheBehind"] = QVariant::fromValue(rational::fromDouble(cache_behind_slider_->GetValue())); + Config::Current()["DiskCacheAhead"] = QVariant::fromValue(rational::fromDouble(cache_ahead_slider_->GetValue())); +} + +} diff --git a/app/dialog/preferences/tabs/preferencesdisktab.h b/app/dialog/preferences/tabs/preferencesdisktab.h new file mode 100644 index 000000000..abd68dfc0 --- /dev/null +++ b/app/dialog/preferences/tabs/preferencesdisktab.h @@ -0,0 +1,58 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef PREFERENCESDISKTAB_H +#define PREFERENCESDISKTAB_H + +#include +#include +#include + +#include "dialog/configbase/configdialogbase.h" +#include "render/diskmanager.h" +#include "widget/slider/floatslider.h" +#include "widget/path/pathwidget.h" + +namespace olive { + +class PreferencesDiskTab : public ConfigDialogBaseTab +{ + Q_OBJECT +public: + PreferencesDiskTab(); + + virtual bool Validate() override; + + virtual void Accept(MultiUndoCommand* command) override; + +private: + PathWidget* disk_cache_location_; + + FloatSlider* cache_ahead_slider_; + + FloatSlider* cache_behind_slider_; + + DiskCacheFolder* default_disk_cache_folder_; + +}; + +} + +#endif // PREFERENCESDISKTAB_H diff --git a/app/dialog/preferences/tabs/preferencesgeneraltab.cpp b/app/dialog/preferences/tabs/preferencesgeneraltab.cpp index 0039efa09..3c869bb73 100644 --- a/app/dialog/preferences/tabs/preferencesgeneraltab.cpp +++ b/app/dialog/preferences/tabs/preferencesgeneraltab.cpp @@ -1,106 +1,191 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + #include "preferencesgeneraltab.h" #include +#include #include -#include -#include #include +#include "common/autoscroll.h" +#include "core.h" +#include "dialog/sequence/sequence.h" +#include "node/project/sequence/sequence.h" + +namespace olive { + PreferencesGeneralTab::PreferencesGeneralTab() { QVBoxLayout* layout = new QVBoxLayout(this); - layout->setMargin(0); - QGridLayout* general_layout = new QGridLayout(); - layout->addLayout(general_layout); + { + QGroupBox* global_groupbox = new QGroupBox(tr("Locale")); + QGridLayout* global_layout = new QGridLayout(global_groupbox); + layout->addWidget(global_groupbox); - int row = 0; + int row = 0; - // General -> Language - general_layout->addWidget(new QLabel(tr("Language:")), row, 0); + // General -> Language + global_layout->addWidget(new QLabel(tr("Language:")), row, 0); - language_combobox = new QComboBox(); + language_combobox_ = new QComboBox(); - // add default language (en-US) - language_combobox->addItem(QLocale::languageToString(QLocale("en-US").language())); + // Add default language (en-US) + QDir language_dir(QStringLiteral(":/ts")); + QStringList languages = language_dir.entryList(); + foreach (const QString& l, languages) { + AddLanguage(l); + } - /* - // add languages from file - QList translation_paths = get_language_paths(); + QString current_language = Config::Current()[QStringLiteral("Language")].toString(); + if (current_language.isEmpty()) { + // No configured language, use system language + current_language = QLocale::system().name(); - // iterate through all language search paths - for (int j=0;jaddItem(QLocale(locale_str).nativeLanguageName(), locale_relative_path); - - if (olive::config.language_file == locale_relative_path) { - language_combobox->setCurrentIndex(language_combobox->count() - 1); - } - } + // If we don't have a language for this, default to en_US + if (!languages.contains(current_language)) { + current_language = QStringLiteral("en_US"); } } - */ + language_combobox_->setCurrentIndex(languages.indexOf(current_language)); - general_layout->addWidget(language_combobox, row, 1); + global_layout->addWidget(language_combobox_, row, 1); + } - row++; + { + QGroupBox* timeline_groupbox = new QGroupBox(tr("Timeline")); + QGridLayout* timeline_layout = new QGridLayout(timeline_groupbox); + layout->addWidget(timeline_groupbox); - // General -> Thumbnail and Waveform Resolution - general_layout->addWidget(new QLabel(tr("Thumbnail Resolution:"), this), row, 0); + int row = 0; - thumbnail_res_spinbox = new QSpinBox(this); - thumbnail_res_spinbox->setMinimum(0); - thumbnail_res_spinbox->setMaximum(INT_MAX); - general_layout->addWidget(thumbnail_res_spinbox, row, 1); + QLabel* autoscroll_lbl = new QLabel(tr("Auto-Scroll Method:")); + autoscroll_lbl->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + timeline_layout->addWidget(autoscroll_lbl, row, 0); - row++; + // ComboBox indices match enum indices + autoscroll_method_ = new QComboBox(); + autoscroll_method_->addItem(tr("None"), AutoScroll::kNone); + autoscroll_method_->addItem(tr("Page Scrolling"), AutoScroll::kPage); + autoscroll_method_->addItem(tr("Smooth Scrolling"), AutoScroll::kSmooth); + autoscroll_method_->setCurrentIndex(Config::Current()["Autoscroll"].toInt()); + timeline_layout->addWidget(autoscroll_method_, row, 1); - general_layout->addWidget(new QLabel(tr("Waveform Resolution:"), this), row, 0); + row++; - waveform_res_spinbox = new QSpinBox(this); - waveform_res_spinbox->setMinimum(0); - waveform_res_spinbox->setMaximum(INT_MAX); - general_layout->addWidget(waveform_res_spinbox, row, 1); + timeline_layout->addWidget(new QLabel(tr("Rectified Waveforms:")), row, 0); - row++; + rectified_waveforms_ = new QCheckBox(); + rectified_waveforms_->setChecked(Config::Current()["RectifiedWaveforms"].toBool()); + timeline_layout->addWidget(rectified_waveforms_, row, 1); - QPushButton* delete_preview_btn = new QPushButton(tr("Delete Previews")); - general_layout->addWidget(delete_preview_btn, row, 1); - //connect(delete_preview_btn, SIGNAL(clicked(bool)), this, SLOT(delete_all_previews())); + row++; - row++; + timeline_layout->addWidget(new QLabel(tr("Default Still Image Length:")), row, 0); - QHBoxLayout* misc_general = new QHBoxLayout(); + default_still_length_ = new RationalSlider(); + default_still_length_->SetMinimum(rational(100, 1000)); + default_still_length_->SetTimebase(rational(100, 1000)); + default_still_length_->SetFormat(tr("%1 seconds")); + default_still_length_->SetValue(Config::Current()["DefaultStillLength"].value()); + timeline_layout->addWidget(default_still_length_); + } - // General -> Default Sequence Settings - QPushButton* default_sequence_settings = new QPushButton(tr("Default Sequence Settings")); - default_sequence_settings->setEnabled(false); - connect(default_sequence_settings, SIGNAL(clicked(bool)), this, SLOT(edit_default_sequence_settings())); - misc_general->addWidget(default_sequence_settings); + { + QGroupBox* autorecovery_groupbox = new QGroupBox(tr("Auto-Recovery")); + QGridLayout* autorecovery_layout = new QGridLayout(autorecovery_groupbox); + layout->addWidget(autorecovery_groupbox); - general_layout->addLayout(misc_general, row, 0); + int row = 0; + + autorecovery_layout->addWidget(new QLabel(tr("Enable Auto-Recovery:")), row, 0); + + autorecovery_enabled_ = new QCheckBox(); + autorecovery_enabled_->setChecked(Config::Current()[QStringLiteral("AutorecoveryEnabled")].toBool()); + autorecovery_layout->addWidget(autorecovery_enabled_, row, 1); + + row++; + + autorecovery_layout->addWidget(new QLabel(tr("Auto-Recovery Interval:")), row, 0); + + autorecovery_interval_ = new IntegerSlider(); + autorecovery_interval_->SetMinimum(1); + autorecovery_interval_->SetMaximum(60); + autorecovery_interval_->SetFormat(QT_TRANSLATE_N_NOOP("olive::SliderBase", "%n minute(s)"), true); + autorecovery_interval_->SetValue(Config::Current()[QStringLiteral("AutorecoveryInterval")].toLongLong()); + autorecovery_layout->addWidget(autorecovery_interval_, row, 1); + + row++; + + autorecovery_layout->addWidget(new QLabel(tr("Maximum Versions Per Project:")), row, 0); + + autorecovery_maximum_ = new IntegerSlider(); + autorecovery_maximum_->SetMinimum(1); + autorecovery_maximum_->SetMaximum(1000); + autorecovery_maximum_->SetValue(Config::Current()[QStringLiteral("AutorecoveryMaximum")].toLongLong()); + autorecovery_layout->addWidget(autorecovery_maximum_, row, 1); + + row++; + + QPushButton* browse_autorecoveries = new QPushButton(tr("Browse Auto-Recoveries")); + connect(browse_autorecoveries, &QPushButton::clicked, Core::instance(), &Core::BrowseAutoRecoveries); + autorecovery_layout->addWidget(browse_autorecoveries, row, 1); + } layout->addStretch(); } -void PreferencesGeneralTab::Accept() +void PreferencesGeneralTab::Accept(MultiUndoCommand *command) { + Q_UNUSED(command) + Config::Current()[QStringLiteral("RectifiedWaveforms")] = rectified_waveforms_->isChecked(); + + Config::Current()[QStringLiteral("Autoscroll")] = autoscroll_method_->currentData(); + + Config::Current()[QStringLiteral("DefaultStillLength")] = QVariant::fromValue(default_still_length_->GetValue()); + + QString set_language = language_combobox_->currentData().toString(); + if (QLocale::system().name() == set_language) { + // Language is set to the system, assume this is effectively "auto" + set_language = QString(); + } + + // If the language has changed, set it now + if (Config::Current()[QStringLiteral("Language")].toString() != set_language) { + Config::Current()[QStringLiteral("Language")] = set_language; + Core::instance()->SetLanguage(set_language.isEmpty() ? QLocale::system().name() : set_language); + } + + Config::Current()[QStringLiteral("AutorecoveryEnabled")] = autorecovery_enabled_->isChecked(); + Config::Current()[QStringLiteral("AutorecoveryInterval")] = QVariant::fromValue(autorecovery_interval_->GetValue()); + Config::Current()[QStringLiteral("AutorecoveryMaximum")] = QVariant::fromValue(autorecovery_maximum_->GetValue()); + Core::instance()->SetAutorecoveryInterval(autorecovery_interval_->GetValue()); } -void PreferencesGeneralTab::edit_default_sequence_settings() +void PreferencesGeneralTab::AddLanguage(const QString &locale_name) { - /*NewSequenceDialog nsd(this, nullptr, &default_sequence); - nsd.SetNameEditable(false); - nsd.exec();*/ + language_combobox_->addItem(tr("%1 (%2)").arg(QLocale(locale_name).nativeLanguageName(), + locale_name));; + language_combobox_->setItemData(language_combobox_->count() - 1, locale_name); +} + } diff --git a/app/dialog/preferences/tabs/preferencesgeneraltab.h b/app/dialog/preferences/tabs/preferencesgeneraltab.h index c8364f863..4bb90e0ec 100644 --- a/app/dialog/preferences/tabs/preferencesgeneraltab.h +++ b/app/dialog/preferences/tabs/preferencesgeneraltab.h @@ -1,41 +1,64 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + #ifndef PREFERENCESGENERALTAB_H #define PREFERENCESGENERALTAB_H +#include #include #include -#include "preferencestab.h" +#include "dialog/configbase/configdialogbase.h" +#include "node/project/sequence/sequence.h" +#include "widget/slider/rationalslider.h" +#include "widget/slider/integerslider.h" -class PreferencesGeneralTab : public PreferencesTab +namespace olive { + +class PreferencesGeneralTab : public ConfigDialogBaseTab { Q_OBJECT public: PreferencesGeneralTab(); - virtual void Accept() override; - -private slots: - /** - * @brief Shows a NewSequenceDialog attached to default_sequence - */ - void edit_default_sequence_settings(); + virtual void Accept(MultiUndoCommand* command) override; private: - /** - * @brief UI widget for selecting the UI language - */ - QComboBox* language_combobox; + void AddLanguage(const QString& locale_name); - /** - * @brief UI widget for selecting the resolution of the thumbnails to generate - */ - QSpinBox* thumbnail_res_spinbox; + QComboBox* language_combobox_; - /** - * @brief UI widget for selecting the resolution of the waveforms to generate - */ - QSpinBox* waveform_res_spinbox; + QComboBox* autoscroll_method_; + + QCheckBox* rectified_waveforms_; + + RationalSlider* default_still_length_; + + QCheckBox* autorecovery_enabled_; + + IntegerSlider* autorecovery_interval_; + + IntegerSlider* autorecovery_maximum_; }; +} + #endif // PREFERENCESGENERALTAB_H diff --git a/app/dialog/preferences/tabs/preferenceskeyboardtab.cpp b/app/dialog/preferences/tabs/preferenceskeyboardtab.cpp index d5d8143d9..a7e906372 100644 --- a/app/dialog/preferences/tabs/preferenceskeyboardtab.cpp +++ b/app/dialog/preferences/tabs/preferenceskeyboardtab.cpp @@ -1,3 +1,23 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + #include "preferenceskeyboardtab.h" #include @@ -7,10 +27,11 @@ #include #include +namespace olive { + PreferencesKeyboardTab::PreferencesKeyboardTab(QMenuBar *menubar) { QVBoxLayout* shortcut_layout = new QVBoxLayout(this); - shortcut_layout->setMargin(0); QLineEdit* key_search_line = new QLineEdit(); key_search_line->setPlaceholderText(tr("Search for action or shortcut")); @@ -49,8 +70,10 @@ PreferencesKeyboardTab::PreferencesKeyboardTab(QMenuBar *menubar) setup_kbd_shortcuts(menubar); } -void PreferencesKeyboardTab::Accept() +void PreferencesKeyboardTab::Accept(MultiUndoCommand *command) { + Q_UNUSED(command) + // Save keyboard shortcuts for (int i=0;iset_action_shortcut(); @@ -127,6 +150,9 @@ bool PreferencesKeyboardTab::refine_shortcut_list(const QString &s, QTreeWidgetI for (int i=0;itopLevelItemCount();i++) { refine_shortcut_list(s, keyboard_tree_->topLevelItem(i)); } + + // Return value is `all_children_are_hidden` which doesn't matter at the top level + return false; } else { parent->setExpanded(!s.isEmpty()); @@ -135,7 +161,9 @@ bool PreferencesKeyboardTab::refine_shortcut_list(const QString &s, QTreeWidgetI for (int i=0;ichildCount();i++) { QTreeWidgetItem* item = parent->child(i); if (item->childCount() > 0) { - all_children_are_hidden = refine_shortcut_list(s, item); + if (!refine_shortcut_list(s, item)) { + all_children_are_hidden = false; + } } else { item->setHidden(false); if (s.isEmpty()) { @@ -160,7 +188,6 @@ bool PreferencesKeyboardTab::refine_shortcut_list(const QString &s, QTreeWidgetI return all_children_are_hidden; } - return true; } void PreferencesKeyboardTab::load_shortcut_file() { @@ -168,7 +195,7 @@ void PreferencesKeyboardTab::load_shortcut_file() { if (!fn.isEmpty()) { QFile f(fn); if (f.exists() && f.open(QFile::ReadOnly)) { - QByteArray ba = f.readAll(); + QString ba = f.readAll(); f.close(); for (int i=0;iaction_name()); @@ -216,3 +243,5 @@ void PreferencesKeyboardTab::save_shortcut_file() { } } } + +} diff --git a/app/dialog/preferences/tabs/preferenceskeyboardtab.h b/app/dialog/preferences/tabs/preferenceskeyboardtab.h index fd332253a..6e8f22077 100644 --- a/app/dialog/preferences/tabs/preferenceskeyboardtab.h +++ b/app/dialog/preferences/tabs/preferenceskeyboardtab.h @@ -1,19 +1,41 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + #ifndef PREFERENCESKEYBOARDTAB_H #define PREFERENCESKEYBOARDTAB_H #include #include -#include "preferencestab.h" +#include "dialog/configbase/configdialogbase.h" #include "../keysequenceeditor.h" -class PreferencesKeyboardTab : public PreferencesTab +namespace olive { + +class PreferencesKeyboardTab : public ConfigDialogBaseTab { Q_OBJECT public: PreferencesKeyboardTab(QMenuBar* menubar); - virtual void Accept() override; + virtual void Accept(MultiUndoCommand* command) override; private slots: /** @@ -112,4 +134,6 @@ private: QVector key_shortcut_fields_; }; +} + #endif // PREFERENCESKEYBOARDTAB_H diff --git a/app/dialog/preferences/tabs/preferencesplaybacktab.cpp b/app/dialog/preferences/tabs/preferencesplaybacktab.cpp deleted file mode 100644 index 5d21e773e..000000000 --- a/app/dialog/preferences/tabs/preferencesplaybacktab.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "preferencesplaybacktab.h" - -#include -#include -#include - -PreferencesPlaybackTab::PreferencesPlaybackTab() -{ -} - -void PreferencesPlaybackTab::Accept() -{ - -} diff --git a/app/dialog/preferences/tabs/preferencesplaybacktab.h b/app/dialog/preferences/tabs/preferencesplaybacktab.h deleted file mode 100644 index 00321ec0f..000000000 --- a/app/dialog/preferences/tabs/preferencesplaybacktab.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef PREFERENCESPLAYBACKTAB_H -#define PREFERENCESPLAYBACKTAB_H - -#include -#include - -#include "preferencestab.h" - -class PreferencesPlaybackTab : public PreferencesTab -{ - Q_OBJECT -public: - PreferencesPlaybackTab(); - - virtual void Accept() override; - -private: -}; - -#endif // PREFERENCESPLAYBACKTAB_H diff --git a/app/dialog/preferences/tabs/preferencestab.cpp b/app/dialog/preferences/tabs/preferencestab.cpp deleted file mode 100644 index 1fad11b7e..000000000 --- a/app/dialog/preferences/tabs/preferencestab.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include "preferencestab.h" - -PreferencesTab::PreferencesTab() -{ - -} diff --git a/app/dialog/preferences/tabs/preferencestab.h b/app/dialog/preferences/tabs/preferencestab.h deleted file mode 100644 index 9dcfc6f2a..000000000 --- a/app/dialog/preferences/tabs/preferencestab.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef PREFERENCESTAB_H -#define PREFERENCESTAB_H - -#include - -#include "config/config.h" - -class PreferencesTab : public QWidget -{ -public: - PreferencesTab(); - - virtual void Accept() = 0; -}; - -#endif // PREFERENCESTAB_H diff --git a/app/dialog/progress/CMakeLists.txt b/app/dialog/progress/CMakeLists.txt new file mode 100644 index 000000000..c8fe86b27 --- /dev/null +++ b/app/dialog/progress/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + dialog/progress/progress.h + dialog/progress/progress.cpp + PARENT_SCOPE +) diff --git a/app/dialog/progress/progress.cpp b/app/dialog/progress/progress.cpp new file mode 100644 index 000000000..32f47e6e5 --- /dev/null +++ b/app/dialog/progress/progress.cpp @@ -0,0 +1,142 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "progress.h" + +#include +#include +#include +#include + +#include "window/mainwindow/mainwindow.h" + +namespace olive { + +#define super QDialog + +ProgressDialog::ProgressDialog(const QString& message, const QString& title, QWidget *parent) : + super(parent), + show_progress_(true), + first_show_(true) +{ + if (!title.isEmpty()) { + setWindowTitle(title); + } + + QVBoxLayout* layout = new QVBoxLayout(this); + + layout->addWidget(new QLabel(message)); + + bar_ = new QProgressBar(); + bar_->setMinimum(0); + bar_->setValue(0); + bar_->setMaximum(100); + layout->addWidget(bar_); + + elapsed_timer_lbl_ = new ElapsedCounterWidget(); + layout->addWidget(elapsed_timer_lbl_); + + QHBoxLayout* cancel_layout = new QHBoxLayout(); + layout->addLayout(cancel_layout); + cancel_layout->setMargin(0); + cancel_layout->setSpacing(0); + cancel_layout->addStretch(); + + QPushButton* cancel_btn = new QPushButton(tr("Cancel")); + + // Signal that derivatives can connect to + connect(cancel_btn, &QPushButton::clicked, this, &ProgressDialog::Cancelled, Qt::DirectConnection); + + // Stop updating the elapsed/remaining timers + connect(cancel_btn, &QPushButton::clicked, elapsed_timer_lbl_, &ElapsedCounterWidget::Stop); + + // Disable the button so that users know they don't need to keep clicking it + connect(cancel_btn, &QPushButton::clicked, this, &ProgressDialog::DisableSenderWidget); + + // Prevent the progress bar from continuing to move + connect(cancel_btn, &QPushButton::clicked, this, &ProgressDialog::DisableProgressWidgets); + + cancel_layout->addWidget(cancel_btn); + + cancel_layout->addStretch(); +} + +void ProgressDialog::showEvent(QShowEvent *e) +{ + super::showEvent(e); + + if (first_show_) { + elapsed_timer_lbl_->Start(); + + Core::instance()->main_window()->SetApplicationProgressStatus(MainWindow::kProgressShow); + + first_show_ = false; + } +} + +void ProgressDialog::closeEvent(QCloseEvent *e) +{ + super::closeEvent(e); + + Core::instance()->main_window()->SetApplicationProgressStatus(MainWindow::kProgressNone); + + elapsed_timer_lbl_->Stop(); + + first_show_ = true; +} + +void ProgressDialog::SetProgress(double value) +{ + if (!show_progress_) { + return; + } + + int percent = qRound(100.0 * value); + + bar_->setValue(percent); + elapsed_timer_lbl_->SetProgress(value); + + Core::instance()->main_window()->SetApplicationProgressValue(percent); +} + +void ProgressDialog::ShowErrorMessage(const QString &title, const QString &message) +{ + Core::instance()->main_window()->SetApplicationProgressStatus(MainWindow::kProgressError); + + QMessageBox b(this); + b.setIcon(QMessageBox::Critical); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(title); + b.setText(message); + b.addButton(QMessageBox::Ok); + b.exec(); +} + +void ProgressDialog::DisableSenderWidget() +{ + static_cast(sender())->setEnabled(false); +} + +void ProgressDialog::DisableProgressWidgets() +{ + show_progress_ = false; +} + +} diff --git a/app/dialog/progress/progress.h b/app/dialog/progress/progress.h new file mode 100644 index 000000000..123417762 --- /dev/null +++ b/app/dialog/progress/progress.h @@ -0,0 +1,70 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef PROGRESSDIALOG_H +#define PROGRESSDIALOG_H + +#include +#include + +#include "common/debug.h" +#include "widget/taskview/elapsedcounterwidget.h" + +namespace olive { + +class ProgressDialog : public QDialog +{ + Q_OBJECT +public: + ProgressDialog(const QString &message, const QString &title, QWidget* parent = nullptr); + +protected: + virtual void showEvent(QShowEvent* e) override; + + virtual void closeEvent(QCloseEvent *) override; + +public slots: + void SetProgress(double value); + +signals: + void Cancelled(); + +protected: + void ShowErrorMessage(const QString& title, const QString& message); + +private: + QProgressBar* bar_; + + ElapsedCounterWidget* elapsed_timer_lbl_; + + bool show_progress_; + + bool first_show_; + +private slots: + void DisableSenderWidget(); + + void DisableProgressWidgets(); + +}; + +} + +#endif // PROGRESSDIALOG_H diff --git a/app/dialog/projectproperties/CMakeLists.txt b/app/dialog/projectproperties/CMakeLists.txt index 7763f8c34..b45092bf6 100644 --- a/app/dialog/projectproperties/CMakeLists.txt +++ b/app/dialog/projectproperties/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2019 Olive Team +# Copyright (C) 2020 Olive Team # # 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 diff --git a/app/dialog/projectproperties/projectproperties.cpp b/app/dialog/projectproperties/projectproperties.cpp index ddd90a88e..2c75edc9e 100644 --- a/app/dialog/projectproperties/projectproperties.cpp +++ b/app/dialog/projectproperties/projectproperties.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2020 Olive Team 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 @@ -20,136 +20,236 @@ #include "projectproperties.h" +#include #include #include -#include #include #include #include #include -#include -namespace OCIO = OCIO_NAMESPACE::v1; +#include "common/filefunctions.h" +#include "common/ocioutils.h" #include "config/config.h" #include "core.h" -#include "render/colormanager.h" +#include "node/color/colormanager/colormanager.h" +#include "render/diskmanager.h" -ProjectPropertiesDialog::ProjectPropertiesDialog(QWidget *parent) : - QDialog(parent), - working_project_(olive::core.GetActiveProject()) +namespace olive { + +#define super QDialog + +ProjectPropertiesDialog::ProjectPropertiesDialog(Project* p, QWidget *parent) : + super(parent), + working_project_(p), + ocio_config_is_valid_(true) { QVBoxLayout* layout = new QVBoxLayout(this); - setWindowTitle(tr("Project Properties")); + setWindowTitle(tr("Project Properties for '%1'").arg(working_project_->name())); - QGroupBox* color_group = new QGroupBox(); - color_group->setTitle(tr("Color Management")); + QTabWidget* tabs = new QTabWidget; + layout->addWidget(tabs); - QGridLayout* color_layout = new QGridLayout(color_group); + { + // Color management group + QWidget* color_group = new QWidget(); - int row = 0; + QVBoxLayout* color_outer_layout = new QVBoxLayout(color_group); - color_layout->addWidget(new QLabel(tr("OpenColorIO Configuration:")), row, 0); + QGridLayout* color_layout = new QGridLayout(); + color_outer_layout->addLayout(color_layout); - ocio_filename_ = new QLineEdit(); - color_layout->addWidget(ocio_filename_, row, 1); + int row = 0; - row++; + color_layout->addWidget(new QLabel(tr("OpenColorIO Configuration:")), row, 0); - color_layout->addWidget(new QLabel(tr("Default Input Color Space:")), row, 0); + ocio_filename_ = new QLineEdit(); + ocio_filename_->setPlaceholderText(tr("(default)")); + color_layout->addWidget(ocio_filename_, row, 1); - default_input_colorspace_ = new QComboBox(); - color_layout->addWidget(default_input_colorspace_, row, 1, 1, 2); + row++; - row++; + color_layout->addWidget(new QLabel(tr("Default Input Color Space:")), row, 0); - QPushButton* browse_btn = new QPushButton(tr("Browse")); - color_layout->addWidget(browse_btn, 0, 2); - connect(browse_btn, SIGNAL(clicked(bool)), this, SLOT(BrowseForOCIOConfig())); + default_input_colorspace_ = new QComboBox(); + color_layout->addWidget(default_input_colorspace_, row, 1, 1, 2); - layout->addWidget(color_group); + row++; - QDialogButtonBox* dialog_btns = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal); - layout->addWidget(dialog_btns); - connect(dialog_btns, SIGNAL(accepted()), this, SLOT(accept())); - connect(dialog_btns, SIGNAL(rejected()), this, SLOT(reject())); + QPushButton* browse_btn = new QPushButton(tr("Browse")); + color_layout->addWidget(browse_btn, 0, 2); + connect(browse_btn, &QPushButton::clicked, this, &ProjectPropertiesDialog::BrowseForOCIOConfig); - if (working_project_ == nullptr) { - QMessageBox::critical(this, - tr("No Active Project"), - tr("No project is currently open to set the properties for"), - QMessageBox::Ok); - reject(); - return; + ocio_filename_->setText(working_project_->color_manager()->GetConfigFilename()); + + connect(ocio_filename_, &QLineEdit::textChanged, this, &ProjectPropertiesDialog::OCIOFilenameUpdated); + OCIOFilenameUpdated(); + + tabs->addTab(color_group, tr("Color Management")); + + color_outer_layout->addStretch(); } - ocio_filename_->setText(working_project_->ocio_config()); - ListPossibleInputSpaces(working_project_->ocio_config()); + { + // Cache group + QWidget* cache_group = new QWidget(); + + QVBoxLayout* cache_layout = new QVBoxLayout(cache_group); + + QButtonGroup* disk_cache_btn_group = new QButtonGroup(); + + // Create radio buttons and add to widget and button group + disk_cache_radios_[ProjectSettingsNode::kCacheUseDefaultLocation] = new QRadioButton(tr("Use Default Location")); + disk_cache_radios_[ProjectSettingsNode::kCacheStoreAlongsideProject] = new QRadioButton(tr("Store Alongside Project")); + disk_cache_radios_[ProjectSettingsNode::kCacheCustomPath] = new QRadioButton(tr("Use Custom Location:")); + for (int i=0; iaddButton(disk_cache_radios_[i]); + cache_layout->addWidget(disk_cache_radios_[i]); + } + + // Create custom cache path widget + custom_cache_path_ = new PathWidget(working_project_->settings()->GetCustomCachePath(), this); + custom_cache_path_->setEnabled(false); + cache_layout->addWidget(custom_cache_path_); + + // Ensure custom cache path "enabled" is tied to the radio button being checked + connect(disk_cache_radios_[ProjectSettingsNode::kCacheCustomPath], &QRadioButton::toggled, custom_cache_path_, &PathWidget::setEnabled); + + // Check the radio button that should currently be active + disk_cache_radios_[working_project_->settings()->GetCacheSetting()]->setChecked(true); + + // Add disk cache settings button + QPushButton* disk_cache_settings_btn = new QPushButton(tr("Disk Cache Settings")); + connect(disk_cache_settings_btn, &QPushButton::clicked, this, &ProjectPropertiesDialog::OpenDiskCacheSettings); + cache_layout->addWidget(disk_cache_settings_btn); + + tabs->addTab(cache_group, tr("Disk Cache")); + } + + QDialogButtonBox* dialog_btns = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, + Qt::Horizontal); + layout->addWidget(dialog_btns); + connect(dialog_btns, &QDialogButtonBox::accepted, this, &ProjectPropertiesDialog::accept); + connect(dialog_btns, &QDialogButtonBox::rejected, this, &ProjectPropertiesDialog::reject); } void ProjectPropertiesDialog::accept() { - try { - OCIO::ConstConfigRcPtr config = OCIO::Config::CreateFromFile(ocio_filename_->text().toUtf8()); - - working_project_->set_default_input_colorspace(default_input_colorspace_->currentText()); - - working_project_->set_ocio_config(ocio_filename_->text()); - - // This should ripple changes throughout the program that the color config has changed, therefore must be done last - ColorManager::instance()->SetConfig(config); - - QDialog::accept(); - } catch (OCIO::Exception& e) { - QMessageBox::critical(this, - tr("OpenColorIO Config Error"), - tr("Failed to set OpenColorIO configuration: %1").arg(e.what()), - QMessageBox::Ok); + if (!ocio_config_is_valid_) { + QMessageBox mb(this); + mb.setWindowModality(Qt::WindowModal); + mb.setIcon(QMessageBox::Critical); + mb.setWindowTitle(tr("OpenColorIO Config Error")); + mb.setText(tr("Failed to set OpenColorIO configuration: %1").arg(ocio_config_error_)); + mb.addButton(QMessageBox::Ok); + mb.exec(); + return; } + + if (disk_cache_radios_[ProjectSettingsNode::kCacheUseDefaultLocation]->isChecked()) { + // Keep new cache path empty, which means default + } else if (disk_cache_radios_[ProjectSettingsNode::kCacheStoreAlongsideProject]->isChecked()) { + // Ensure alongside project path is valid + if (!VerifyPathAndWarnIfBad(working_project_->get_cache_alongside_project_path())) { + return; + } + } else { + // Ensure custom path is valid + if (!VerifyPathAndWarnIfBad(custom_cache_path_->text())) { + return; + } + } + + if (custom_cache_path_->text() != working_project_->settings()->GetCustomCachePath()) { + // Check if the user is okay with invalidating the current cache + if (!DiskManager::ShowDiskCacheChangeConfirmationDialog(this)) { + return; + } + + working_project_->settings()->SetCustomCachePath(custom_cache_path_->text()); + + emit DiskManager::instance()->InvalidateProject(working_project_); + } + + // This should ripple changes throughout the graph/cache that the color config has changed, and + // therefore should be done after the cache path is changed + if (working_project_->color_manager()->GetConfigFilename() != ocio_filename_->text()) { + working_project_->color_manager()->SetConfigFilename(ocio_filename_->text()); + } + if (working_project_->color_manager()->GetDefaultInputColorSpace() != default_input_colorspace_->currentText()) { + working_project_->color_manager()->SetDefaultInputColorSpace(default_input_colorspace_->currentText()); + } + + super::accept(); } -bool ProjectPropertiesDialog::VerifyOCIOConfig(const QString &fn) +bool ProjectPropertiesDialog::VerifyPathAndWarnIfBad(const QString &path) { - try { - OCIO::Config::CreateFromFile(fn.toUtf8()); - - return true; - } catch (OCIO::Exception& e) { - QMessageBox::critical(this, - tr("OpenColorIO Config Error"), - tr("Failed to set OpenColorIO configuration: %1").arg(e.what()), - QMessageBox::Ok); - + if (!FileFunctions::DirectoryIsValid(path, true)) { + QMessageBox mb(this); + mb.setWindowModality(Qt::WindowModal); + mb.setIcon(QMessageBox::Critical); + mb.setWindowTitle(tr("Invalid path")); + mb.setText(tr("The custom cache path is invalid. Please check it and try again.")); + mb.addButton(QMessageBox::Ok); + mb.exec(); return false; } -} -void ProjectPropertiesDialog::ListPossibleInputSpaces(const QString& fn) -{ - try { - default_input_colorspace_->clear(); - - QStringList input_cs = ColorManager::ListAvailableInputColorspaces(OCIO::Config::CreateFromFile(fn.toUtf8())); - - foreach (QString cs, input_cs) { - default_input_colorspace_->addItem(cs); - - if (cs == working_project_->default_input_colorspace()) { - default_input_colorspace_->setCurrentIndex(default_input_colorspace_->count()-1); - } - } - } catch (OCIO::Exception&) { - } + return true; } void ProjectPropertiesDialog::BrowseForOCIOConfig() { QString fn = QFileDialog::getOpenFileName(this, tr("Browse for OpenColorIO configuration")); if (!fn.isEmpty()) { - if (VerifyOCIOConfig(fn)) { - ocio_filename_->setText(fn); - - ListPossibleInputSpaces(fn); - } + ocio_filename_->setText(fn); } } + +void ProjectPropertiesDialog::OCIOFilenameUpdated() +{ + default_input_colorspace_->clear(); + + try { + OCIO::ConstConfigRcPtr c; + + if (ocio_filename_->text().isEmpty()) { + c = ColorManager::GetDefaultConfig(); + } else { + c = ColorManager::CreateConfigFromFile(ocio_filename_->text()); + } + + ocio_filename_->setStyleSheet(QString()); + ocio_config_is_valid_ = true; + + // List input color spaces + QStringList input_cs = ColorManager::ListAvailableColorspaces(c); + + foreach (QString cs, input_cs) { + default_input_colorspace_->addItem(cs); + + if (cs == working_project_->color_manager()->GetDefaultInputColorSpace()) { + default_input_colorspace_->setCurrentIndex(default_input_colorspace_->count()-1); + } + } + } catch (OCIO::Exception& e) { + ocio_config_is_valid_ = false; + ocio_filename_->setStyleSheet(QStringLiteral("QLineEdit {color: red;}")); + ocio_config_error_ = e.what(); + } +} + +void ProjectPropertiesDialog::OpenDiskCacheSettings() +{ + if (disk_cache_radios_[ProjectSettingsNode::kCacheUseDefaultLocation]->isChecked()) { + DiskManager::instance()->ShowDiskCacheSettingsDialog(DiskManager::instance()->GetDefaultCacheFolder(), this); + } else if (disk_cache_radios_[ProjectSettingsNode::kCacheStoreAlongsideProject]->isChecked()) { + DiskManager::instance()->ShowDiskCacheSettingsDialog(working_project_->get_cache_alongside_project_path(), this); + } else { + DiskManager::instance()->ShowDiskCacheSettingsDialog(custom_cache_path_->text(), this); + } +} + +} diff --git a/app/dialog/projectproperties/projectproperties.h b/app/dialog/projectproperties/projectproperties.h index 7573d30b8..e6963e541 100644 --- a/app/dialog/projectproperties/projectproperties.h +++ b/app/dialog/projectproperties/projectproperties.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2020 Olive Team 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 @@ -21,25 +21,29 @@ #ifndef PROJECTPROPERTIESDIALOG_H #define PROJECTPROPERTIESDIALOG_H +#include #include #include +#include #include +#include -#include "project/project.h" +#include "node/project/project.h" +#include "widget/path/pathwidget.h" + +namespace olive { class ProjectPropertiesDialog : public QDialog { Q_OBJECT public: - ProjectPropertiesDialog(QWidget* parent); + ProjectPropertiesDialog(Project *p, QWidget* parent); public slots: virtual void accept() override; private: - bool VerifyOCIOConfig(const QString& fn); - - void ListPossibleInputSpaces(const QString &fn); + bool VerifyPathAndWarnIfBad(const QString &path); Project* working_project_; @@ -47,9 +51,24 @@ private: QComboBox* default_input_colorspace_; + bool ocio_config_is_valid_; + + QString ocio_config_error_; + + PathWidget* custom_cache_path_; + + static const int kDiskCacheRadioCount = 3; + QRadioButton *disk_cache_radios_[kDiskCacheRadioCount]; + private slots: void BrowseForOCIOConfig(); + void OCIOFilenameUpdated(); + + void OpenDiskCacheSettings(); + }; +} + #endif // PROJECTPROPERTIESDIALOG_H diff --git a/app/dialog/rendercancel/CMakeLists.txt b/app/dialog/rendercancel/CMakeLists.txt new file mode 100644 index 000000000..244d0ad8d --- /dev/null +++ b/app/dialog/rendercancel/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + dialog/rendercancel/rendercancel.h + dialog/rendercancel/rendercancel.cpp + PARENT_SCOPE +) diff --git a/app/dialog/rendercancel/rendercancel.cpp b/app/dialog/rendercancel/rendercancel.cpp new file mode 100644 index 000000000..cb172913f --- /dev/null +++ b/app/dialog/rendercancel/rendercancel.cpp @@ -0,0 +1,82 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "rendercancel.h" + +namespace olive { + +RenderCancelDialog::RenderCancelDialog(QWidget *parent) : + ProgressDialog(tr("Waiting for workers to finish..."), tr("Renderer"), parent), + busy_workers_(0), + total_workers_(0) +{ +} + +void RenderCancelDialog::RunIfWorkersAreBusy() +{ + if (busy_workers_ > 0) { + waiting_workers_ = busy_workers_; + + exec(); + } +} + +void RenderCancelDialog::SetWorkerCount(int count) +{ + total_workers_ = count; + + UpdateProgress(); +} + +void RenderCancelDialog::WorkerStarted() +{ + busy_workers_++; + + UpdateProgress(); +} + +void RenderCancelDialog::WorkerDone() +{ + busy_workers_--; + + UpdateProgress(); +} + +void RenderCancelDialog::showEvent(QShowEvent *event) +{ + QDialog::showEvent(event); + + UpdateProgress(); +} + +void RenderCancelDialog::UpdateProgress() +{ + if (!total_workers_ || !isVisible()) { + return; + } + + SetProgress(qRound(100.0 * static_cast(waiting_workers_ - busy_workers_) / static_cast(waiting_workers_))); + + if (busy_workers_ == 0) { + accept(); + } +} + +} diff --git a/app/dialog/rendercancel/rendercancel.h b/app/dialog/rendercancel/rendercancel.h new file mode 100644 index 000000000..c07cf5c27 --- /dev/null +++ b/app/dialog/rendercancel/rendercancel.h @@ -0,0 +1,59 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef RENDERCANCELDIALOG_H +#define RENDERCANCELDIALOG_H + +#include "dialog/progress/progress.h" + +namespace olive { + +class RenderCancelDialog : public ProgressDialog +{ + Q_OBJECT +public: + RenderCancelDialog(QWidget* parent = nullptr); + + void RunIfWorkersAreBusy(); + + void SetWorkerCount(int count); + + void WorkerStarted(); + +public slots: + void WorkerDone(); + +protected: + virtual void showEvent(QShowEvent* event) override; + +private: + void UpdateProgress(); + + int busy_workers_; + + int total_workers_; + + int waiting_workers_; + +}; + +} + +#endif // RENDERCANCELDIALOG_H diff --git a/app/dialog/sequence/CMakeLists.txt b/app/dialog/sequence/CMakeLists.txt index 150197b77..f2128cfe5 100644 --- a/app/dialog/sequence/CMakeLists.txt +++ b/app/dialog/sequence/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2019 Olive Team +# Copyright (C) 2021 Olive Team # # 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 @@ -16,7 +16,13 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} + dialog/sequence/presetmanager.h dialog/sequence/sequence.h dialog/sequence/sequence.cpp + dialog/sequence/sequencedialogparametertab.h + dialog/sequence/sequencedialogparametertab.cpp + dialog/sequence/sequencedialogpresettab.h + dialog/sequence/sequencedialogpresettab.cpp + dialog/sequence/sequencepreset.h PARENT_SCOPE ) diff --git a/app/dialog/sequence/presetmanager.h b/app/dialog/sequence/presetmanager.h new file mode 100644 index 000000000..3ed913026 --- /dev/null +++ b/app/dialog/sequence/presetmanager.h @@ -0,0 +1,246 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef PRESETMANAGER_H +#define PRESETMANAGER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/define.h" +#include "common/filefunctions.h" +#include "common/xmlutils.h" + +namespace olive { + +class Preset +{ +public: + Preset() = default; + + virtual ~Preset(){} + + const QString& GetName() const + { + return name_; + } + + void SetName(const QString& s) + { + name_ = s; + } + + virtual void Load(QXmlStreamReader* reader) = 0; + + virtual void Save(QXmlStreamWriter* writer) const = 0; + +private: + QString name_; + +}; + +using PresetPtr = std::shared_ptr; + +template +class PresetManager +{ +public: + PresetManager(QWidget* parent, const QString& preset_name) : + preset_name_(preset_name), + parent_(parent) + { + // Load custom preset data from file + QFile preset_file(GetCustomPresetFilename()); + if (preset_file.open(QFile::ReadOnly)) { + QXmlStreamReader reader(&preset_file); + + while (XMLReadNextStartElement(&reader)) { + if (reader.name() == QStringLiteral("presets")) { + while (XMLReadNextStartElement(&reader)) { + if (reader.name() == QStringLiteral("preset")) { + PresetPtr p = std::make_unique(); + + p->Load(&reader); + + custom_preset_data_.append(p); + } else { + reader.skipCurrentElement(); + } + } + } else { + reader.skipCurrentElement(); + } + } + + preset_file.close(); + } + } + + ~PresetManager() + { + // Save custom presets to disk + QFile preset_file(GetCustomPresetFilename()); + if (preset_file.open(QFile::WriteOnly)) { + QXmlStreamWriter writer(&preset_file); + writer.setAutoFormatting(true); + + writer.writeStartDocument(); + + writer.writeStartElement(QStringLiteral("presets")); + + foreach (PresetPtr p, custom_preset_data_) { + writer.writeStartElement(QStringLiteral("preset")); + + p->Save(&writer); + + writer.writeEndElement(); // preset + } + + writer.writeEndElement(); // presets + + writer.writeEndDocument(); + + preset_file.close(); + } + } + + QString GetPresetName(QString start) const + { + bool ok; + + forever { + start = QInputDialog::getText(parent_, + QCoreApplication::translate("PresetManager", "Save Preset"), + QCoreApplication::translate("PresetManager", "Set preset name:"), + QLineEdit::Normal, + start, + &ok); + + if (!ok) { + // Dialog cancelled - leave function entirely + return QString(); + } + + if (start.isEmpty()) { + // No preset name entered, start loop over + QMessageBox::critical(parent_, + QCoreApplication::translate("PresetManager", "Invalid preset name"), + QCoreApplication::translate("PresetManager", "You must enter a preset name"), + QMessageBox::Ok); + } else { + break; + } + } + + return start; + } + + enum SaveStatus { + kAppended, + kReplaced, + kNotSaved + }; + + SaveStatus SavePreset(PresetPtr preset) + { + QString preset_name; + int existing_preset; + + forever { + preset_name = GetPresetName(preset_name); + + if (preset_name.isEmpty()) { + // Dialog cancelled - leave function entirely + return kNotSaved; + } + + existing_preset = -1; + for (int i=0; iGetName() == preset_name) { + existing_preset = i; + break; + } + } + + if (existing_preset == -1 + || QMessageBox::question(parent_, + QCoreApplication::translate("PresetManager", "Preset exists"), + QCoreApplication::translate("PresetManager", + "A preset with this name already exists. " + "Would you like to replace it?")) == QMessageBox::Yes) { + break; + } + } + + preset->SetName(preset_name); + + if (existing_preset >= 0) { + custom_preset_data_.replace(existing_preset, preset); + return kReplaced; + } else { + custom_preset_data_.append(preset); + return kAppended; + } + } + + QString GetCustomPresetFilename() const + { + return QDir(FileFunctions::GetConfigurationLocation()).filePath(preset_name_); + } + + PresetPtr GetPreset(int index) + { + return custom_preset_data_.at(index); + } + + void DeletePreset(int index) + { + custom_preset_data_.removeAt(index); + } + + int GetNumberOfPresets() const + { + return custom_preset_data_.size(); + } + + const QVector& GetPresetData() const + { + return custom_preset_data_; + } + +private: + QVector custom_preset_data_; + + QString preset_name_; + + QWidget* parent_; + +}; + +} + +#endif // PRESETMANAGER_H diff --git a/app/dialog/sequence/sequence.cpp b/app/dialog/sequence/sequence.cpp index 29a227203..e7daa93e5 100644 --- a/app/dialog/sequence/sequence.cpp +++ b/app/dialog/sequence/sequence.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -25,12 +25,19 @@ #include #include #include +#include +#include #include +#include "config/config.h" +#include "core.h" #include "common/channellayout.h" +#include "common/qtutils.h" #include "common/rational.h" #include "undo/undostack.h" +namespace olive { + SequenceDialog::SequenceDialog(Sequence* s, Type t, QWidget* parent) : QDialog(parent), sequence_(s), @@ -38,43 +45,21 @@ SequenceDialog::SequenceDialog(Sequence* s, Type t, QWidget* parent) : { QVBoxLayout* layout = new QVBoxLayout(this); - // Set up preset section - QHBoxLayout* preset_layout = new QHBoxLayout(); - preset_layout->addWidget(new QLabel(tr("Preset:"))); - QComboBox* preset_combobox = new QComboBox(); - preset_layout->addWidget(preset_combobox); - layout->addLayout(preset_layout); + QSplitter* splitter = new QSplitter(); + layout->addWidget(splitter); - // Set up video section - QGroupBox* video_group = new QGroupBox(); - video_group->setTitle(tr("Video")); - QGridLayout* video_layout = new QGridLayout(video_group); - video_layout->addWidget(new QLabel(tr("Width:")), 0, 0); - video_width_field_ = new QSpinBox(); - video_width_field_->setMaximum(99999); - video_layout->addWidget(video_width_field_, 0, 1); - video_layout->addWidget(new QLabel(tr("Height:")), 1, 0); - video_height_field_ = new QSpinBox(); - video_height_field_->setMaximum(99999); - video_layout->addWidget(video_height_field_, 1, 1); - video_layout->addWidget(new QLabel(tr("Frame Rate:")), 2, 0); - video_frame_rate_field_ = new QComboBox(); - video_layout->addWidget(video_frame_rate_field_, 2, 1); - layout->addWidget(video_group); + preset_tab_ = new SequenceDialogPresetTab(); + splitter->addWidget(preset_tab_); - // Set up audio section - QGroupBox* audio_group = new QGroupBox(); - audio_group->setTitle(tr("Audio")); - QGridLayout* audio_layout = new QGridLayout(audio_group); - audio_layout->addWidget(new QLabel(tr("Sample Rate:")), 0, 0); - audio_sample_rate_field_ = new QComboBox(); - // FIXME: No sample rate made - audio_layout->addWidget(audio_sample_rate_field_, 0, 1); - audio_layout->addWidget(new QLabel(tr("Channels:")), 1, 0); - audio_channels_field_ = new QComboBox(); - // FIXME: No channels made - audio_layout->addWidget(audio_channels_field_, 1, 1); - layout->addWidget(audio_group); + parameter_tab_ = new SequenceDialogParameterTab(sequence_); + splitter->addWidget(parameter_tab_); + + connect(preset_tab_, &SequenceDialogPresetTab::PresetChanged, + parameter_tab_, &SequenceDialogParameterTab::PresetChanged); + connect(preset_tab_, &SequenceDialogPresetTab::PresetAccepted, + this, &SequenceDialog::accept); + connect(parameter_tab_, &SequenceDialogParameterTab::SaveParametersAsPreset, + preset_tab_, &SequenceDialogPresetTab::SaveParametersAsPreset); // Set up name section QHBoxLayout* name_layout = new QHBoxLayout(); @@ -85,71 +70,23 @@ SequenceDialog::SequenceDialog(Sequence* s, Type t, QWidget* parent) : // Set up dialog buttons QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - buttons->setCenterButtons(true); - connect(buttons, SIGNAL(accepted()), this, SLOT(accept())); - connect(buttons, SIGNAL(rejected()), this, SLOT(reject())); + QPushButton *default_btn = buttons->addButton(tr("Set As Default"), QDialogButtonBox::ActionRole); + connect(buttons, &QDialogButtonBox::accepted, this, &SequenceDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &SequenceDialog::reject); + connect(default_btn, &QPushButton::clicked, this, &SequenceDialog::SetAsDefaultClicked); layout->addWidget(buttons); // Set window title based on type switch (t) { case kNew: - setWindowTitle("New Sequence"); + setWindowTitle(tr("New Sequence")); break; case kExisting: - setWindowTitle(tr("Editing \"%1\"").arg(sequence_->name())); + setWindowTitle(tr("Editing \"%1\"").arg(sequence_->GetLabel())); break; } - // Set up available frame rates - AddFrameRate(rational(10, 1)); // 10 FPS - AddFrameRate(rational(15, 1)); // 15 FPS - AddFrameRate(rational(24000, 1001)); // 23.976 FPS - AddFrameRate(rational(24, 1)); // 24 FPS - AddFrameRate(rational(25, 1)); // 25 FPS - AddFrameRate(rational(30000, 1001)); // 29.97 FPS - AddFrameRate(rational(30, 1)); // 30 FPS - AddFrameRate(rational(48000, 1001)); // 47.952 FPS - AddFrameRate(rational(48, 1)); // 48 FPS - AddFrameRate(rational(50, 1)); // 50 FPS - AddFrameRate(rational(60000, 1001)); // 59.94 FPS - AddFrameRate(rational(60, 1)); // 60 FPS - - // Set up available sample rates - AddSampleRate(8000); // 8000 Hz - AddSampleRate(11025); // 11025 Hz - AddSampleRate(16000); // 16000 Hz - AddSampleRate(22050); // 22050 Hz - AddSampleRate(24000); // 24000 Hz - AddSampleRate(32000); // 32000 Hz - AddSampleRate(44100); // 44100 Hz - AddSampleRate(48000); // 48000 Hz - AddSampleRate(88200); // 88200 Hz - AddSampleRate(96000); // 96000 Hz - - // Set up available channel layouts - AddChannelLayout(AV_CH_LAYOUT_MONO); - AddChannelLayout(AV_CH_LAYOUT_STEREO); - AddChannelLayout(AV_CH_LAYOUT_5POINT1); - AddChannelLayout(AV_CH_LAYOUT_7POINT1); - - // Set values based on input sequence - video_width_field_->setValue(sequence_->video_params().width()); - video_height_field_->setValue(sequence_->video_params().height()); - - int frame_rate_index = frame_rate_list_.indexOf(sequence_->video_params().time_base().flipped()); - video_frame_rate_field_->setCurrentIndex(frame_rate_index); - - int sample_rate_index = sample_rate_list_.indexOf(sequence_->audio_params().sample_rate()); - audio_sample_rate_field_->setCurrentIndex(sample_rate_index); - - for (int i=0;icount();i++) { - if (audio_channels_field_->itemData(i).toULongLong() == sequence_->audio_params().channel_layout()) { - audio_channels_field_->setCurrentIndex(i); - break; - } - } - - name_field_->setText(sequence_->name()); + name_field_->setText(sequence_->GetLabel()); } void SequenceDialog::SetUndoable(bool u) @@ -157,30 +94,31 @@ void SequenceDialog::SetUndoable(bool u) make_undoable_ = u; } +void SequenceDialog::SetNameIsEditable(bool e) +{ + name_field_->setEnabled(e); +} + void SequenceDialog::accept() { - if (name_field_->text().isEmpty()) { - QMessageBox::critical(this, tr("Error editing Sequence"), tr("Please enter a name for this Sequence.")); + if (name_field_->isEnabled() && name_field_->text().isEmpty()) { + QtUtils::MessageBox(this, QMessageBox::Critical, tr("Error editing Sequence"), tr("Please enter a name for this Sequence.")); return; } - // Get the rational at the combobox's index (which will be correct provided AddFrameRate() was used at all time) - rational video_time_base = frame_rate_list_.at(video_frame_rate_field_->currentIndex()).flipped(); - - - // Get the rational at the combobox's index (which will be correct provided AddFrameRate() was used at all time) - int audio_sample_rate = sample_rate_list_.at(audio_sample_rate_field_->currentIndex()); - - // Get the audio channel layout value - uint64_t channels = audio_channels_field_->currentData().toULongLong(); - // Generate video and audio parameter structs from data - VideoParams video_params = VideoParams(video_width_field_->value(), - video_height_field_->value(), - video_time_base); + VideoParams video_params = VideoParams(parameter_tab_->GetSelectedVideoWidth(), + parameter_tab_->GetSelectedVideoHeight(), + parameter_tab_->GetSelectedVideoFrameRate().flipped(), + parameter_tab_->GetSelectedPreviewFormat(), + VideoParams::kInternalChannelCount, + parameter_tab_->GetSelectedVideoPixelAspect(), + parameter_tab_->GetSelectedVideoInterlacingMode(), + parameter_tab_->GetSelectedPreviewResolution()); - AudioParams audio_params = AudioParams(audio_sample_rate, - channels); + AudioParams audio_params = AudioParams(parameter_tab_->GetSelectedAudioSampleRate(), + parameter_tab_->GetSelectedAudioChannelLayout(), + AudioParams::kInternalFormat); if (make_undoable_) { @@ -188,84 +126,82 @@ void SequenceDialog::accept() SequenceParamCommand* param_command = new SequenceParamCommand(sequence_, video_params, audio_params, - name_field_->text()); + name_field_->text(), + parameter_tab_->GetSelectedPreviewAutoCache()); - olive::undo_stack.push(param_command); + Core::instance()->undo_stack()->push(param_command); } else { // Set sequence values directly with no undo command - sequence_->set_video_params(video_params); - sequence_->set_audio_params(audio_params); - sequence_->set_name(name_field_->text()); + sequence_->SetVideoParams(video_params); + sequence_->SetAudioParams(audio_params); + sequence_->SetLabel(name_field_->text()); + sequence_->SetVideoAutoCacheEnabled(parameter_tab_->GetSelectedPreviewAutoCache()); } QDialog::accept(); } -void SequenceDialog::AddFrameRate(const rational &r) +void SequenceDialog::SetAsDefaultClicked() { - frame_rate_list_.append(r); - - video_frame_rate_field_->addItem(tr("%1 FPS").arg(r.toDouble())); -} - -void SequenceDialog::AddSampleRate(const int &rate) -{ - sample_rate_list_.append(rate); - - audio_sample_rate_field_->addItem(tr("%1 Hz").arg(rate)); -} - -void SequenceDialog::AddChannelLayout(int layout) -{ - QString layout_name; - - switch (layout) { - case AV_CH_LAYOUT_MONO: - layout_name = tr("Mono"); - break; - case AV_CH_LAYOUT_STEREO: - layout_name = tr("Stereo"); - break; - case AV_CH_LAYOUT_5POINT1: - layout_name = tr("5.1"); - break; - case AV_CH_LAYOUT_7POINT1: - layout_name = tr("7.1"); - break; - default: - layout_name = tr("Unknown (0x%1)").arg(layout, 1, 16); + if (QtUtils::MessageBox(this, QMessageBox::Question, tr("Confirm Set As Default"), + tr("Are you sure you want to set the current parameters as defaults?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + // Maybe replace with Preset system + Config::Current()[QStringLiteral("DefaultSequenceWidth")] = parameter_tab_->GetSelectedVideoWidth(); + Config::Current()[QStringLiteral("DefaultSequenceHeight")] = parameter_tab_->GetSelectedVideoHeight(); + Config::Current()[QStringLiteral("DefaultSequencePixelAspect")] = QVariant::fromValue(parameter_tab_->GetSelectedVideoPixelAspect()); + Config::Current()[QStringLiteral("DefaultSequenceFrameRate")] = QVariant::fromValue(parameter_tab_->GetSelectedVideoFrameRate().flipped()); + Config::Current()[QStringLiteral("DefaultSequenceInterlacing")] = parameter_tab_->GetSelectedVideoInterlacingMode(); + Config::Current()[QStringLiteral("DefaultSequenceAudioFrequency")] = parameter_tab_->GetSelectedAudioSampleRate(); + Config::Current()[QStringLiteral("DefaultSequenceAudioLayout")] = QVariant::fromValue(parameter_tab_->GetSelectedAudioChannelLayout()); + Config::Current()[QStringLiteral("DefaultSequenceAutoCache")] = QVariant::fromValue(parameter_tab_->GetSelectedPreviewAutoCache()); } - - audio_channels_field_->addItem(layout_name, layout); } SequenceDialog::SequenceParamCommand::SequenceParamCommand(Sequence* s, const VideoParams& video_params, - const AudioParams& audio_params, - const QString& name, - QUndoCommand* parent) : - QUndoCommand(parent), + const AudioParams &audio_params, + const QString& name, bool autocache) : sequence_(s), new_video_params_(video_params), new_audio_params_(audio_params), new_name_(name), - old_video_params_(s->video_params()), - old_audio_params_(s->audio_params()), - old_name_(s->name()) + new_autocache_(autocache), + old_video_params_(s->GetVideoParams()), + old_audio_params_(s->GetAudioParams()), + old_name_(s->GetLabel()), + old_autocache_(s->GetVideoAutoCacheEnabled()) { } +Project *SequenceDialog::SequenceParamCommand::GetRelevantProject() const +{ + return sequence_->project(); +} + void SequenceDialog::SequenceParamCommand::redo() { - sequence_->set_video_params(new_video_params_); - sequence_->set_audio_params(new_audio_params_); - sequence_->set_name(new_name_); + if (sequence_->GetVideoParams() != new_video_params_) { + sequence_->SetVideoParams(new_video_params_); + } + if (sequence_->GetAudioParams() != new_audio_params_) { + sequence_->SetAudioParams(new_audio_params_); + } + sequence_->SetLabel(new_name_); + sequence_->SetVideoAutoCacheEnabled(new_autocache_); } void SequenceDialog::SequenceParamCommand::undo() { - sequence_->set_video_params(old_video_params_); - sequence_->set_audio_params(old_audio_params_); - sequence_->set_name(old_name_); + if (sequence_->GetVideoParams() != old_video_params_) { + sequence_->SetVideoParams(old_video_params_); + } + if (sequence_->GetAudioParams() != old_audio_params_) { + sequence_->SetAudioParams(old_audio_params_); + } + sequence_->SetLabel(old_name_); + sequence_->SetVideoAutoCacheEnabled(old_autocache_); +} + } diff --git a/app/dialog/sequence/sequence.h b/app/dialog/sequence/sequence.h index c4e53206f..b12d1fcb3 100644 --- a/app/dialog/sequence/sequence.h +++ b/app/dialog/sequence/sequence.h @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -24,9 +24,13 @@ #include #include #include -#include -#include "project/item/sequence/sequence.h" +#include "node/project/sequence/sequence.h" +#include "sequencedialogparametertab.h" +#include "sequencedialogpresettab.h" +#include "undo/undocommand.h" + +namespace olive { /** * @brief A dialog for editing Sequence parameters @@ -34,7 +38,7 @@ * This dialog exposes all the parameters of a Sequence to users allowing them to set up a Sequence however they wish. * A Sequence can be sent to this dialog through the constructor. All fields will be filled using that Sequence's * parameters, allowing the user to view and edit them. Accepting the dialog will apply them back to that Sequence, - * either directly or using a QUndoCommand (see SetUndoable()). + * either directly or using an UndoCommand (see SetUndoable()). * * If creating a new Sequence, the Sequence must still be constructed first before sending it to SequenceDialog. * SequenceDialog does not create any new objects. In most cases when creating a new Sequence, editing its parameters @@ -71,10 +75,17 @@ public: /** * @brief Set whether the parameter changes should be made into an undo command or not * - * @param u + * Defaults to true. */ void SetUndoable(bool u); + /** + * @brief Set whether the name of this Sequence can be edited with this dialog + * + * Defaults to true. + */ + void SetNameIsEditable(bool e); + public slots: /** * @brief Function called when the user presses OK @@ -82,65 +93,52 @@ public slots: virtual void accept() override; private: - /** - * @brief Internal function for adding a selectable frame rate - */ - void AddFrameRate(const rational& r); - - /** - * @brief Internal function for adding a selectable sample rate - */ - void AddSampleRate(const int &rate); - - /** - * @brief Internal function for adding a selectable channel layout - */ - void AddChannelLayout(int layout); - Sequence* sequence_; + SequenceDialogPresetTab* preset_tab_; + + SequenceDialogParameterTab* parameter_tab_; + bool make_undoable_; - QSpinBox* video_width_field_; - - QSpinBox* video_height_field_; - - QComboBox* video_frame_rate_field_; - - QComboBox* audio_sample_rate_field_; - - QComboBox* audio_channels_field_; - QLineEdit* name_field_; - QVector frame_rate_list_; - - QVector sample_rate_list_; - /** - * @brief A QUndoCommand for setting the parameters on a sequence + * @brief An UndoCommand for setting the parameters on a sequence */ - class SequenceParamCommand : public QUndoCommand { + class SequenceParamCommand : public UndoCommand { public: SequenceParamCommand(Sequence* s, const VideoParams& video_params, const AudioParams& audio_params, const QString& name, - QUndoCommand* parent = nullptr); + bool autocache); + virtual Project* GetRelevantProject() const override; + + protected: virtual void redo() override; virtual void undo() override; + private: Sequence* sequence_; VideoParams new_video_params_; AudioParams new_audio_params_; QString new_name_; + bool new_autocache_; VideoParams old_video_params_; AudioParams old_audio_params_; QString old_name_; + bool old_autocache_; }; + +private slots: + void SetAsDefaultClicked(); + }; +} + #endif // SEQUENCEDIALOG_H diff --git a/app/dialog/sequence/sequencedialogparametertab.cpp b/app/dialog/sequence/sequencedialogparametertab.cpp new file mode 100644 index 000000000..00f89e002 --- /dev/null +++ b/app/dialog/sequence/sequencedialogparametertab.cpp @@ -0,0 +1,151 @@ +#include "sequencedialogparametertab.h" + +#include +#include +#include +#include + +#include "core.h" + +namespace olive { + +SequenceDialogParameterTab::SequenceDialogParameterTab(Sequence* sequence, QWidget* parent) : + QWidget(parent) +{ + QVBoxLayout* layout = new QVBoxLayout(this); + + int row = 0; + + // Set up video section + QGroupBox* video_group = new QGroupBox(); + video_group->setTitle(tr("Video")); + QGridLayout *video_layout = new QGridLayout(video_group); + video_layout->addWidget(new QLabel(tr("Width:")), row, 0); + width_slider_ = new IntegerSlider(); + width_slider_->SetMinimum(0); + video_layout->addWidget(width_slider_, row, 1); + row++; + video_layout->addWidget(new QLabel(tr("Height:")), row, 0); + height_slider_ = new IntegerSlider(); + height_slider_->SetMinimum(0); + video_layout->addWidget(height_slider_, row, 1); + row++; + video_layout->addWidget(new QLabel(tr("Frame Rate:")), row, 0); + framerate_combo_ = new FrameRateComboBox(); + video_layout->addWidget(framerate_combo_, row, 1); + row++; + video_layout->addWidget(new QLabel(tr("Pixel Aspect Ratio:")), row, 0); + pixelaspect_combo_ = new PixelAspectRatioComboBox(); + video_layout->addWidget(pixelaspect_combo_, row, 1); + row++; + video_layout->addWidget(new QLabel(tr("Interlacing:")), row, 0); + interlacing_combo_ = new InterlacedComboBox(); + video_layout->addWidget(interlacing_combo_, row, 1); + layout->addWidget(video_group); + + row = 0; + + // Set up audio section + QGroupBox* audio_group = new QGroupBox(); + audio_group->setTitle(tr("Audio")); + QGridLayout* audio_layout = new QGridLayout(audio_group); + audio_layout->addWidget(new QLabel(tr("Sample Rate:")), row, 0); + audio_sample_rate_field_ = new SampleRateComboBox(); + audio_layout->addWidget(audio_sample_rate_field_, row, 1); + row++; + audio_layout->addWidget(new QLabel(tr("Channels:")), row, 0); + audio_channels_field_ = new ChannelLayoutComboBox(); + audio_layout->addWidget(audio_channels_field_, row, 1); + layout->addWidget(audio_group); + + row = 0; + + // Set up preview section + QGroupBox* preview_group = new QGroupBox(); + preview_group->setTitle(tr("Preview")); + QGridLayout* preview_layout = new QGridLayout(preview_group); + preview_layout->addWidget(new QLabel(tr("Resolution:")), row, 0); + preview_resolution_field_ = new VideoDividerComboBox(); + preview_layout->addWidget(preview_resolution_field_, row, 1); + preview_resolution_label_ = new QLabel(); + preview_layout->addWidget(preview_resolution_label_, row, 2); + row++; + preview_layout->addWidget(new QLabel(tr("Quality:")), row, 0); + preview_format_field_ = new PixelFormatComboBox(true); + preview_layout->addWidget(preview_format_field_, row, 1, 1, 2); + row++; + preview_layout->addWidget(new QLabel(tr("Auto-Cache:")), row, 0); + preview_autocache_field_ = new QCheckBox(); + preview_layout->addWidget(preview_autocache_field_, row, 1); + layout->addWidget(preview_group); + + // Set values based on input sequence + VideoParams vp = sequence->GetVideoParams(); + AudioParams ap = sequence->GetAudioParams(); + width_slider_->SetValue(vp.width()); + height_slider_->SetValue(vp.height()); + framerate_combo_->SetFrameRate(vp.time_base().flipped()); + pixelaspect_combo_->SetPixelAspectRatio(vp.pixel_aspect_ratio()); + interlacing_combo_->SetInterlaceMode(vp.interlacing()); + preview_resolution_field_->SetDivider(vp.divider()); + preview_format_field_->SetPixelFormat(vp.format()); + preview_autocache_field_->setChecked(sequence->GetVideoAutoCacheEnabled()); + audio_sample_rate_field_->SetSampleRate(ap.sample_rate()); + audio_channels_field_->SetChannelLayout(ap.channel_layout()); + + connect(preview_resolution_field_, static_cast(&QComboBox::currentIndexChanged), + this, &SequenceDialogParameterTab::UpdatePreviewResolutionLabel); + + layout->addStretch(); + + QPushButton* save_preset_btn = new QPushButton(tr("Save Preset")); + connect(save_preset_btn, &QPushButton::clicked, this, &SequenceDialogParameterTab::SavePresetClicked); + layout->addWidget(save_preset_btn); + + UpdatePreviewResolutionLabel(); +} + +void SequenceDialogParameterTab::PresetChanged(const SequencePreset &preset) +{ + width_slider_->SetValue(preset.width()); + height_slider_->SetValue(preset.height()); + framerate_combo_->SetFrameRate(preset.frame_rate()); + pixelaspect_combo_->SetPixelAspectRatio(preset.pixel_aspect()); + interlacing_combo_->SetInterlaceMode(preset.interlacing()); + audio_sample_rate_field_->SetSampleRate(preset.sample_rate()); + audio_channels_field_->SetChannelLayout(preset.channel_layout()); + preview_resolution_field_->SetDivider(preset.preview_divider()); + preview_format_field_->SetPixelFormat(preset.preview_format()); + preview_autocache_field_->setChecked(preset.preview_autocache()); +} + +void SequenceDialogParameterTab::SavePresetClicked() +{ + emit SaveParametersAsPreset({QString(), + GetSelectedVideoWidth(), + GetSelectedVideoHeight(), + GetSelectedVideoFrameRate(), + GetSelectedVideoPixelAspect(), + GetSelectedVideoInterlacingMode(), + GetSelectedAudioSampleRate(), + GetSelectedAudioChannelLayout(), + GetSelectedPreviewResolution(), + GetSelectedPreviewFormat(), + GetSelectedPreviewAutoCache()}); +} + +void SequenceDialogParameterTab::UpdatePreviewResolutionLabel() +{ + VideoParams test_param(GetSelectedVideoWidth(), + GetSelectedVideoHeight(), + VideoParams::kFormatInvalid, + VideoParams::kInternalChannelCount, + rational(1), + VideoParams::kInterlaceNone, + preview_resolution_field_->currentData().toInt()); + + preview_resolution_label_->setText(tr("(%1x%2)").arg(QString::number(test_param.effective_width()), + QString::number(test_param.effective_height()))); +} + +} diff --git a/app/dialog/sequence/sequencedialogparametertab.h b/app/dialog/sequence/sequencedialogparametertab.h new file mode 100644 index 000000000..e11561b2f --- /dev/null +++ b/app/dialog/sequence/sequencedialogparametertab.h @@ -0,0 +1,110 @@ +#ifndef SEQUENCEDIALOGPARAMETERTAB_H +#define SEQUENCEDIALOGPARAMETERTAB_H + +#include +#include +#include +#include + +#include "node/project/sequence/sequence.h" +#include "sequencepreset.h" +#include "widget/slider/integerslider.h" +#include "widget/standardcombos/standardcombos.h" + +namespace olive { + +class SequenceDialogParameterTab : public QWidget +{ + Q_OBJECT +public: + SequenceDialogParameterTab(Sequence* sequence, QWidget* parent = nullptr); + + int GetSelectedVideoWidth() const + { + return width_slider_->GetValue(); + } + + int GetSelectedVideoHeight() const + { + return height_slider_->GetValue(); + } + + rational GetSelectedVideoFrameRate() const + { + return framerate_combo_->GetFrameRate(); + } + + rational GetSelectedVideoPixelAspect() const + { + return pixelaspect_combo_->GetPixelAspectRatio(); + } + + VideoParams::Interlacing GetSelectedVideoInterlacingMode() const + { + return interlacing_combo_->GetInterlaceMode(); + } + + int GetSelectedAudioSampleRate() const + { + return audio_sample_rate_field_->GetSampleRate(); + } + + uint64_t GetSelectedAudioChannelLayout() const + { + return audio_channels_field_->GetChannelLayout(); + } + + int GetSelectedPreviewResolution() const + { + return preview_resolution_field_->GetDivider(); + } + + VideoParams::Format GetSelectedPreviewFormat() const + { + return preview_format_field_->GetPixelFormat(); + } + + bool GetSelectedPreviewAutoCache() const + { + return preview_autocache_field_->isChecked(); + } + +public slots: + void PresetChanged(const SequencePreset& preset); + +signals: + void SaveParametersAsPreset(const SequencePreset& preset); + +private: + IntegerSlider *width_slider_; + + IntegerSlider *height_slider_; + + FrameRateComboBox *framerate_combo_; + + PixelAspectRatioComboBox *pixelaspect_combo_; + + InterlacedComboBox *interlacing_combo_; + + SampleRateComboBox* audio_sample_rate_field_; + + ChannelLayoutComboBox* audio_channels_field_; + + VideoDividerComboBox* preview_resolution_field_; + + QLabel* preview_resolution_label_; + + PixelFormatComboBox* preview_format_field_; + + QCheckBox *preview_autocache_field_; + +private slots: + void SavePresetClicked(); + + void UpdatePreviewResolutionLabel(); + +}; + +} + +#endif // SEQUENCEDIALOGPARAMETERTAB_H diff --git a/app/dialog/sequence/sequencedialogpresettab.cpp b/app/dialog/sequence/sequencedialogpresettab.cpp new file mode 100644 index 000000000..a52258fa0 --- /dev/null +++ b/app/dialog/sequence/sequencedialogpresettab.cpp @@ -0,0 +1,307 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "sequencedialogpresettab.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/filefunctions.h" +#include "config/config.h" +#include "render/videoparams.h" +#include "ui/icons/icons.h" +#include "widget/menu/menu.h" + +namespace olive { + +const int kDataIsPreset = Qt::UserRole; +const int kDataPresetIsCustomRole = Qt::UserRole + 1; +const int kDataPresetDataRole = Qt::UserRole + 2; + +SequenceDialogPresetTab::SequenceDialogPresetTab(QWidget* parent) : + QWidget(parent), + PresetManager(this, QStringLiteral("sequencepresets")) +{ + QVBoxLayout* outer_layout = new QVBoxLayout(this); + outer_layout->setMargin(0); + + preset_tree_ = new QTreeWidget(); + preset_tree_->setColumnCount(1); + preset_tree_->setHeaderLabel(tr("Preset")); + preset_tree_->setContextMenuPolicy(Qt::CustomContextMenu); + connect(preset_tree_, &QTreeWidget::customContextMenuRequested, this, &SequenceDialogPresetTab::ShowContextMenu); + outer_layout->addWidget(preset_tree_); + connect(preset_tree_, &QTreeWidget::currentItemChanged, this, &SequenceDialogPresetTab::SelectedItemChanged); + connect(preset_tree_, &QTreeWidget::itemDoubleClicked, this, &SequenceDialogPresetTab::ItemDoubleClicked); + + // Add "my presets" folder + my_presets_folder_ = CreateFolder(tr("My Presets")); + preset_tree_->addTopLevelItem(my_presets_folder_); + + // Add presets + preset_tree_->addTopLevelItem(CreateHDPresetFolder(tr("4K UHD"), 3840, 2160, 6)); + preset_tree_->addTopLevelItem(CreateHDPresetFolder(tr("1080p"), 1920, 1080, 3)); + preset_tree_->addTopLevelItem(CreateHDPresetFolder(tr("720p"), 1280, 720, 2)); + + preset_tree_->addTopLevelItem(CreateSDPresetFolder(tr("NTSC"), 720, 480, rational(30000, 1001), + VideoParams::kPixelAspectNTSCStandard, + VideoParams::kPixelAspectNTSCWidescreen, 1)); + preset_tree_->addTopLevelItem(CreateSDPresetFolder(tr("PAL"), 720, 576, rational(25, 1), + VideoParams::kPixelAspectPALStandard, + VideoParams::kPixelAspectPALWidescreen, 1)); + + // Load custom presets + for (int i=0;i(preset); + + // If replaced, no need to make another item. If not saved, shared ptr will delete itself + if (SavePreset(preset_ptr) == kAppended) { + AddCustomItem(my_presets_folder_, preset_ptr, GetNumberOfPresets() - 1); + } +} + +QTreeWidgetItem* SequenceDialogPresetTab::CreateFolder(const QString &name) +{ + QTreeWidgetItem* folder = new QTreeWidgetItem(); + folder->setText(0, name); + folder->setIcon(0, icon::Folder); + return folder; +} + +QTreeWidgetItem *SequenceDialogPresetTab::CreateHDPresetFolder(const QString &name, int width, int height, int divider) +{ + const VideoParams::Format default_format = static_cast(Config::Current()["OfflinePixelFormat"].toInt()); + const bool default_autocache = Config::Current()[QStringLiteral("DefaultSequenceAutoCache")].toBool(); + QTreeWidgetItem* parent = CreateFolder(name); + AddStandardItem(parent, std::make_shared(tr("%1 23.976 FPS").arg(name), + width, + height, + rational(24000, 1001), + VideoParams::kPixelAspectSquare, + VideoParams::kInterlaceNone, + 48000, + AV_CH_LAYOUT_STEREO, + divider, + default_format, + default_autocache)); + AddStandardItem(parent, std::make_shared(tr("%1 25 FPS").arg(name), + width, + height, + rational(25, 1), + VideoParams::kPixelAspectSquare, + VideoParams::kInterlaceNone, + 48000, + AV_CH_LAYOUT_STEREO, + divider, + default_format, + default_autocache)); + AddStandardItem(parent, std::make_shared(tr("%1 29.97 FPS").arg(name), + width, + height, + rational(30000, 1001), + VideoParams::kPixelAspectSquare, + VideoParams::kInterlaceNone, + 48000, + AV_CH_LAYOUT_STEREO, + divider, + default_format, + default_autocache)); + AddStandardItem(parent, std::make_shared(tr("%1 50 FPS").arg(name), + width, + height, + rational(50, 1), + VideoParams::kPixelAspectSquare, + VideoParams::kInterlaceNone, + 48000, + AV_CH_LAYOUT_STEREO, + divider, + default_format, + default_autocache)); + AddStandardItem(parent, std::make_shared(tr("%1 59.94 FPS").arg(name), + width, + height, + rational(60000, 1001), + VideoParams::kPixelAspectSquare, + VideoParams::kInterlaceNone, + 48000, + AV_CH_LAYOUT_STEREO, + divider, + default_format, + default_autocache)); + return parent; +} + +QTreeWidgetItem *SequenceDialogPresetTab::CreateSDPresetFolder(const QString &name, int width, int height, const rational& frame_rate, const rational &standard_par, const rational &wide_par, int divider) +{ + const VideoParams::Format default_format = static_cast(Config::Current()["OfflinePixelFormat"].toInt()); + const bool default_autocache = Config::Current()[QStringLiteral("DefaultSequenceAutoCache")].toBool(); + QTreeWidgetItem* parent = CreateFolder(name); + preset_tree_->addTopLevelItem(parent); + AddStandardItem(parent, std::make_shared(tr("%1 Standard").arg(name), + width, + height, + frame_rate, + standard_par, + VideoParams::kInterlacedBottomFirst, + 48000, + AV_CH_LAYOUT_STEREO, + divider, + default_format, + default_autocache)); + AddStandardItem(parent, std::make_shared(tr("%1 Widescreen").arg(name), + width, + height, + frame_rate, + wide_par, + VideoParams::kInterlacedBottomFirst, + 48000, + AV_CH_LAYOUT_STEREO, + divider, + default_format, + default_autocache)); + return parent; +} + +QTreeWidgetItem *SequenceDialogPresetTab::GetSelectedItem() +{ + QList selected_items = preset_tree_->selectedItems(); + + if (selected_items.isEmpty()) { + return nullptr; + } else { + return selected_items.first(); + } +} + +QTreeWidgetItem *SequenceDialogPresetTab::GetSelectedCustomPreset() +{ + QTreeWidgetItem* sel = GetSelectedItem(); + + if (sel + && sel->data(0, kDataIsPreset).toBool() + && sel->data(0, kDataPresetIsCustomRole).toBool()) { + return sel; + } + + return nullptr; +} + +void SequenceDialogPresetTab::AddStandardItem(QTreeWidgetItem *folder, PresetPtr preset, const QString& description) +{ + int index = default_preset_data_.size(); + default_preset_data_.append(preset); + AddItemInternal(folder, preset, false, index, description); +} + +void SequenceDialogPresetTab::AddCustomItem(QTreeWidgetItem *folder, PresetPtr preset, int index, const QString &description) +{ + AddItemInternal(folder, preset, true, index, description); +} + +void SequenceDialogPresetTab::AddItemInternal(QTreeWidgetItem *folder, PresetPtr preset, bool is_custom, int index, const QString &description) +{ + QTreeWidgetItem* item = new QTreeWidgetItem(); + + item->setText(0, preset->GetName()); + item->setIcon(0, icon::Video); + item->setToolTip(0, description); + item->setData(0, kDataIsPreset, true); + item->setData(0, kDataPresetIsCustomRole, is_custom); + item->setData(0, kDataPresetDataRole, index); + + folder->addChild(item); +} + +void SequenceDialogPresetTab::SelectedItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous) +{ + Q_UNUSED(previous) + + if (current->data(0, kDataIsPreset).toBool()) { + int preset_index = current->data(0, kDataPresetDataRole).toInt(); + + PresetPtr preset_data = (current->data(0, kDataPresetIsCustomRole).toBool()) + ? GetPreset(preset_index) + : default_preset_data_.at(preset_index); + + emit PresetChanged(*static_cast(preset_data.get())); + } +} + +void SequenceDialogPresetTab::ItemDoubleClicked(QTreeWidgetItem *item, int column) +{ + Q_UNUSED(column) + + if (item->data(0, kDataIsPreset).toBool()) { + emit PresetAccepted(); + } +} + +void SequenceDialogPresetTab::ShowContextMenu() +{ + QTreeWidgetItem* sel = GetSelectedCustomPreset(); + + if (sel) { + Menu m(this); + + QAction* delete_action = m.addAction(tr("Delete Preset")); + connect(delete_action, &QAction::triggered, this, &SequenceDialogPresetTab::DeleteSelectedPreset); + + m.exec(QCursor::pos()); + } +} + +void SequenceDialogPresetTab::DeleteSelectedPreset() +{ + QTreeWidgetItem* sel = GetSelectedCustomPreset(); + + if (sel) { + int preset_index = sel->data(0, kDataPresetDataRole).toInt(); + + // Shift all items whose index was after this preset forward + for (int i=0; ichildCount(); i++) { + QTreeWidgetItem* custom_item = my_presets_folder_->child(i); + int this_item_index = custom_item->data(0, kDataPresetDataRole).toInt(); + + if (this_item_index > preset_index) { + custom_item->setData(0, kDataPresetDataRole, this_item_index - 1); + } + } + + // Remove the preset + DeletePreset(preset_index); + + // Delete the item + delete sel; + } +} + +} diff --git a/app/dialog/sequence/sequencedialogpresettab.h b/app/dialog/sequence/sequencedialogpresettab.h new file mode 100644 index 000000000..bd2e334a3 --- /dev/null +++ b/app/dialog/sequence/sequencedialogpresettab.h @@ -0,0 +1,82 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef SEQUENCEDIALOGPRESETTAB_H +#define SEQUENCEDIALOGPRESETTAB_H + +#include +#include +#include + +#include "presetmanager.h" +#include "sequencepreset.h" + +namespace olive { + +class SequenceDialogPresetTab : public QWidget, public PresetManager +{ + Q_OBJECT +public: + SequenceDialogPresetTab(QWidget* parent = nullptr); + +public slots: + void SaveParametersAsPreset(SequencePreset preset); + +signals: + void PresetChanged(const SequencePreset& preset); + + void PresetAccepted(); + +private: + QTreeWidgetItem *CreateFolder(const QString& name); + + QTreeWidgetItem *CreateHDPresetFolder(const QString& name, int width, int height, int divider); + + QTreeWidgetItem *CreateSDPresetFolder(const QString& name, int width, int height, const rational &frame_rate, const rational& standard_par, const rational& wide_par, int divider); + + QTreeWidgetItem* GetSelectedItem(); + QTreeWidgetItem* GetSelectedCustomPreset(); + + void AddStandardItem(QTreeWidgetItem* folder, PresetPtr preset, const QString &description = QString()); + + void AddCustomItem(QTreeWidgetItem* folder, PresetPtr preset, int index, const QString& description = QString()); + + void AddItemInternal(QTreeWidgetItem* folder, PresetPtr preset, bool is_custom, int index, const QString& description = QString()); + + QTreeWidget* preset_tree_; + + QTreeWidgetItem* my_presets_folder_; + + QVector default_preset_data_; + +private slots: + void SelectedItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous); + + void ItemDoubleClicked(QTreeWidgetItem *item, int column); + + void ShowContextMenu(); + + void DeleteSelectedPreset(); + +}; + +} + +#endif // SEQUENCEDIALOGPRESETTAB_H diff --git a/app/dialog/sequence/sequencepreset.h b/app/dialog/sequence/sequencepreset.h new file mode 100644 index 000000000..0d2a12fa2 --- /dev/null +++ b/app/dialog/sequence/sequencepreset.h @@ -0,0 +1,174 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef SEQUENCEPARAM_H +#define SEQUENCEPARAM_H + +#include + +#include "common/rational.h" +#include "common/xmlutils.h" +#include "dialog/sequence/presetmanager.h" +#include "render/videoparams.h" + +namespace olive { + +class SequencePreset : public Preset { +public: + SequencePreset() = default; + + SequencePreset(const QString& name, + int width, + int height, + const rational& frame_rate, + const rational& pixel_aspect, + VideoParams::Interlacing interlacing, + int sample_rate, + uint64_t channel_layout, + int preview_divider, + VideoParams::Format preview_format, + bool preview_autocache) : + width_(width), + height_(height), + frame_rate_(frame_rate), + pixel_aspect_(pixel_aspect), + interlacing_(interlacing), + sample_rate_(sample_rate), + channel_layout_(channel_layout), + preview_divider_(preview_divider), + preview_format_(preview_format), + preview_autocache_(preview_autocache) + { + SetName(name); + } + + virtual void Load(QXmlStreamReader* reader) override + { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("name")) { + SetName(reader->readElementText()); + } else if (reader->name() == QStringLiteral("width")) { + width_ = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("height")) { + height_ = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("framerate")) { + frame_rate_ = rational::fromString(reader->readElementText()); + } else if (reader->name() == QStringLiteral("pixelaspect")) { + pixel_aspect_ = rational::fromString(reader->readElementText()); + } else if (reader->name() == QStringLiteral("interlacing")) { + interlacing_ = static_cast(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("samplerate")) { + sample_rate_ = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("chlayout")) { + channel_layout_ = reader->readElementText().toULongLong(); + } else if (reader->name() == QStringLiteral("divider")) { + preview_divider_ = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("format")) { + preview_format_ = static_cast(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("autocache")) { + preview_autocache_ = reader->readElementText().toInt(); + } else { + reader->skipCurrentElement(); + } + } + } + + virtual void Save(QXmlStreamWriter* writer) const override + { + writer->writeTextElement(QStringLiteral("name"), GetName()); + writer->writeTextElement(QStringLiteral("width"), QString::number(width_)); + writer->writeTextElement(QStringLiteral("height"), QString::number(height_)); + writer->writeTextElement(QStringLiteral("framerate"), frame_rate_.toString()); + writer->writeTextElement(QStringLiteral("pixelaspect"), pixel_aspect_.toString()); + writer->writeTextElement(QStringLiteral("interlacing_"), QString::number(interlacing_)); + writer->writeTextElement(QStringLiteral("samplerate"), QString::number(sample_rate_)); + writer->writeTextElement(QStringLiteral("chlayout"), QString::number(channel_layout_)); + writer->writeTextElement(QStringLiteral("divider"), QString::number(preview_divider_)); + writer->writeTextElement(QStringLiteral("format"), QString::number(preview_format_)); + writer->writeTextElement(QStringLiteral("autocache"), QString::number(preview_autocache_)); + } + + int width() const + { + return width_; + } + + int height() const + { + return height_; + } + + const rational& frame_rate() const + { + return frame_rate_; + } + + const rational& pixel_aspect() const + { + return pixel_aspect_; + } + + VideoParams::Interlacing interlacing() const + { + return interlacing_; + } + + int sample_rate() const + { + return sample_rate_; + } + + uint64_t channel_layout() const + { + return channel_layout_; + } + + int preview_divider() const + { + return preview_divider_; + } + + VideoParams::Format preview_format() const + { + return preview_format_; + } + + bool preview_autocache() const + { + return preview_autocache_; + } + +private: + int width_; + int height_; + rational frame_rate_; + rational pixel_aspect_; + VideoParams::Interlacing interlacing_; + int sample_rate_; + uint64_t channel_layout_; + int preview_divider_; + VideoParams::Format preview_format_; + bool preview_autocache_; + +}; + +} + +#endif // SEQUENCEPARAM_H diff --git a/app/dialog/speedduration/CMakeLists.txt b/app/dialog/speedduration/CMakeLists.txt new file mode 100644 index 000000000..42471bd32 --- /dev/null +++ b/app/dialog/speedduration/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + dialog/speedduration/speeddurationdialog.cpp + dialog/speedduration/speeddurationdialog.h + PARENT_SCOPE +) diff --git a/app/dialog/speedduration/speeddurationdialog.cpp b/app/dialog/speedduration/speeddurationdialog.cpp new file mode 100644 index 000000000..cf6af022a --- /dev/null +++ b/app/dialog/speedduration/speeddurationdialog.cpp @@ -0,0 +1,256 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "speeddurationdialog.h" + +#include +#include +#include +#include + +#include "core.h" +#include "widget/nodeparamview/nodeparamviewundo.h" +#include "widget/timelinewidget/undo/timelineundopointer.h" + +namespace olive { + +#define super QDialog + +SpeedDurationDialog::SpeedDurationDialog(const QVector &clips, const rational &timebase, QWidget *parent) : + super(parent), + clips_(clips), + timebase_(timebase) +{ + setWindowTitle(tr("Speed/Duration")); + + QVBoxLayout *layout = new QVBoxLayout(this); + + { + QGroupBox *speed_group = new QGroupBox(); + layout->addWidget(speed_group); + + QGridLayout *speed_layout = new QGridLayout(speed_group); + + int row = 0; + + speed_layout->addWidget(new QLabel(tr("Speed:")), row, 0); + + speed_slider_ = new FloatSlider(); + speed_slider_->SetDisplayType(FloatSlider::kPercentage); + connect(speed_slider_, &FloatSlider::ValueChanged, this, &SpeedDurationDialog::SpeedChanged); + speed_layout->addWidget(speed_slider_, row, 1); + + row++; + + speed_layout->addWidget(new QLabel(tr("Duration:")), row, 0); + + dur_slider_ = new RationalSlider(); + dur_slider_->SetTimebase(timebase); + dur_slider_->SetDisplayType(RationalSlider::kTime); + connect(dur_slider_, &RationalSlider::ValueChanged, this, &SpeedDurationDialog::DurationChanged); + speed_layout->addWidget(dur_slider_, row, 1); + + row++; + + link_box_ = new QCheckBox(tr("Link Speed and Duration")); + link_box_->setChecked(true); + speed_layout->addWidget(link_box_, row, 0, 1, 2); + } + + reverse_box_ = new QCheckBox(tr("Reverse")); + layout->addWidget(reverse_box_); + + maintain_audio_pitch_box_ = new QCheckBox(tr("Maintain Audio Pitch")); + layout->addWidget(maintain_audio_pitch_box_); + + ripple_box_ = new QCheckBox(tr("Ripple Trailing Clips")); + layout->addWidget(ripple_box_); + + QDialogButtonBox *btns = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + btns->setCenterButtons(true); + connect(btns, &QDialogButtonBox::accepted, this, &SpeedDurationDialog::accept); + connect(btns, &QDialogButtonBox::rejected, this, &SpeedDurationDialog::reject); + layout->addWidget(btns); + + // Determine which speed value to use + start_speed_ = clips.first()->speed(); + start_duration_ = clips.first()->length(); + start_reverse_ = clips.first()->reverse(); + start_maintain_audio_pitch_ = clips.first()->maintain_audio_pitch(); + for (int i=1; ispeed())) { + // Speed differs per clip + start_speed_ = qSNaN(); + } + + if (start_duration_ != -1 && c->length() != start_duration_) { + start_duration_ = -1; + } + + // Yes, in theory a bool should only ever be 0 or 1 anyway, but MSVC complained and it is + // *possible* that a bool could be something else, so this code is safer + int clip_reverse = c->reverse() ? 1 : 0; + int clip_maintain_pitch = c->maintain_audio_pitch() ? 1 : 0; + if (start_reverse_ != -1 && clip_reverse != start_reverse_) { + start_reverse_ = -1; + } + if (start_maintain_audio_pitch_ != -1 && clip_maintain_pitch != start_maintain_audio_pitch_) { + start_maintain_audio_pitch_ = -1; + } + } + + if (qIsNaN(start_speed_)) { + speed_slider_->SetTristate(); + } else { + speed_slider_->SetValue(start_speed_); + } + + if (start_duration_ == -1) { + dur_slider_->SetTristate(); + } else { + dur_slider_->SetValue(start_duration_); + } + + if (start_reverse_ == -1) { + reverse_box_->setTristate(); + } else { + reverse_box_->setChecked(start_reverse_); + } + + if (start_maintain_audio_pitch_ == -1) { + maintain_audio_pitch_box_->setTristate(); + } else { + maintain_audio_pitch_box_->setChecked(start_maintain_audio_pitch_); + } +} + +void SpeedDurationDialog::accept() +{ + // We haven't implemented rippling yet, so warn the user + if (ripple_box_->isChecked()) { + // FIXME: Stub + if (QMessageBox::information(this, QString(), tr("Rippling is a stub and will not do anything. Do you wish to continue?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { + return; + } + } + + MultiUndoCommand *command = new MultiUndoCommand(); + + // Set speed values + if (speed_slider_->IsTristate()) { + if (link_box_->isChecked() && !dur_slider_->IsTristate()) { + // Automatically determine speed from duration + foreach (ClipBlock *c, clips_) { + command->add_child(new NodeParamSetStandardValueCommand(NodeKeyframeTrackReference(NodeInput(c, ClipBlock::kSpeedInput)), GetSpeedAdjustment(c->speed(), c->length(), dur_slider_->GetValue()))); + } + } + } else { + // Set speeds to value of slider + foreach (ClipBlock *c, clips_) { + command->add_child(new NodeParamSetStandardValueCommand(NodeKeyframeTrackReference(NodeInput(c, ClipBlock::kSpeedInput)), speed_slider_->GetValue())); + } + } + + // Set reverse values + if (!reverse_box_->isTristate()) { + foreach (ClipBlock *c, clips_) { + command->add_child(new NodeParamSetStandardValueCommand(NodeKeyframeTrackReference(NodeInput(c, ClipBlock::kReverseInput)), reverse_box_->isChecked())); + } + } + + // Set reverse values + if (!maintain_audio_pitch_box_->isTristate()) { + foreach (ClipBlock *c, clips_) { + command->add_child(new NodeParamSetStandardValueCommand(NodeKeyframeTrackReference(NodeInput(c, ClipBlock::kMaintainAudioPitchInput)), maintain_audio_pitch_box_->isChecked())); + } + } + + // Set duration values + foreach (ClipBlock *c, clips_) { + rational proposed_length = c->length(); + + if (dur_slider_->IsTristate()) { + if (link_box_->isChecked() && !speed_slider_->IsTristate()) { + proposed_length = GetLengthAdjustment(c->length(), c->speed(), speed_slider_->GetValue(), timebase_); + } + } else { + proposed_length = dur_slider_->GetValue(); + } + + if (proposed_length != c->length()) { + // Clip length should ideally change, but check if there's "room" to do so + if (proposed_length > c->length() && c->next()) { + if (GapBlock *gap = dynamic_cast(c->next())) { + proposed_length = qMin(proposed_length, gap->out() - c->in()); + } else { + proposed_length = c->length(); + } + } + + if (proposed_length != c->length()) { + command->add_child(new BlockTrimCommand(c->track(), c, proposed_length, Timeline::kTrimOut)); + } + } + } + + Core::instance()->undo_stack()->push(command); + + super::accept(); +} + +rational SpeedDurationDialog::GetLengthAdjustment(const rational &original_length, double original_speed, double new_speed, const rational &timebase) +{ + return Timecode::snap_time_to_timebase(rational::fromDouble(original_length.toDouble() / new_speed * original_speed), timebase); +} + +double SpeedDurationDialog::GetSpeedAdjustment(double original_speed, const rational &original_length, const rational &new_length) +{ + return original_speed / new_length.toDouble() * original_length.toDouble(); +} + +void SpeedDurationDialog::SpeedChanged(double s) +{ + if (!link_box_->isChecked()) { + return; + } + + if (start_duration_ == -1) { + dur_slider_->SetTristate(); + } else { + dur_slider_->SetValue(GetLengthAdjustment(start_duration_, start_speed_, s, timebase_)); + } +} + +void SpeedDurationDialog::DurationChanged(const rational &r) +{ + if (!link_box_->isChecked()) { + return; + } + + if (qIsNaN(start_speed_)) { + speed_slider_->SetTristate(); + } else { + speed_slider_->SetValue(GetSpeedAdjustment(start_speed_, start_duration_, r)); + } +} + +} diff --git a/app/dialog/speedduration/speeddurationdialog.h b/app/dialog/speedduration/speeddurationdialog.h new file mode 100644 index 000000000..be24ec9d6 --- /dev/null +++ b/app/dialog/speedduration/speeddurationdialog.h @@ -0,0 +1,84 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef SPEEDDURATIONDIALOG_H +#define SPEEDDURATIONDIALOG_H + +#include +#include + +#include "node/block/clip/clip.h" +#include "node/block/gap/gap.h" +#include "undo/undocommand.h" +#include "widget/slider/floatslider.h" +#include "widget/slider/rationalslider.h" + +namespace olive { + +class SpeedDurationDialog : public QDialog +{ + Q_OBJECT +public: + explicit SpeedDurationDialog(const QVector &clips, const rational &timebase, QWidget *parent = nullptr); + +public slots: + virtual void accept() override; + +signals: + +private: + static rational GetLengthAdjustment(const rational &original_length, double original_speed, double new_speed, const rational &timebase); + + static double GetSpeedAdjustment(double original_speed, const rational &original_length, const rational &new_length); + + QVector clips_; + + FloatSlider *speed_slider_; + + RationalSlider *dur_slider_; + + QCheckBox *link_box_; + + QCheckBox *reverse_box_; + + QCheckBox *maintain_audio_pitch_box_; + + QCheckBox *ripple_box_; + + int start_reverse_; + + int start_maintain_audio_pitch_; + + double start_speed_; + + rational start_duration_; + + rational timebase_; + +private slots: + void SpeedChanged(double s); + + void DurationChanged(const rational &r); + +}; + +} + +#endif // SPEEDDURATIONDIALOG_H diff --git a/app/task/probe/CMakeLists.txt b/app/dialog/task/CMakeLists.txt similarity index 90% rename from app/task/probe/CMakeLists.txt rename to app/dialog/task/CMakeLists.txt index 55d195d96..276cdc588 100644 --- a/app/task/probe/CMakeLists.txt +++ b/app/dialog/task/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2019 Olive Team +# Copyright (C) 2021 Olive Team # # 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 @@ -16,7 +16,7 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} - task/probe/probe.h - task/probe/probe.cpp + dialog/task/task.h + dialog/task/task.cpp PARENT_SCOPE ) diff --git a/app/dialog/task/task.cpp b/app/dialog/task/task.cpp new file mode 100644 index 000000000..8b16bd944 --- /dev/null +++ b/app/dialog/task/task.cpp @@ -0,0 +1,98 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "task.h" + +#include + +namespace olive { + +#define super ProgressDialog + +TaskDialog::TaskDialog(Task* task, const QString& title, QWidget *parent) : + super(task->GetTitle(), title, parent), + task_(task), + destroy_on_close_(true), + already_shown_(false) +{ + // Clear task when this dialog is destroyed + task_->setParent(this); + + // Connect the save manager progress signal to the progress bar update on the dialog + connect(task_, &Task::ProgressChanged, this, &TaskDialog::SetProgress, Qt::QueuedConnection); + + // Connect cancel signal (must be a direct connection or it'll be queued after the task has + // already finished) + connect(this, &TaskDialog::Cancelled, task_, &Task::Cancel, Qt::DirectConnection); +} + +void TaskDialog::showEvent(QShowEvent *e) +{ + super::showEvent(e); + + if (!already_shown_) { + // Create watcher for when the task finishes + QFutureWatcher* task_watcher = new QFutureWatcher(); + + // Listen for when the task finishes + connect(task_watcher, &QFutureWatcher::finished, + this, &TaskDialog::TaskFinished, Qt::QueuedConnection); + + // Run task in another thread with QtConcurrent + task_watcher->setFuture(QtConcurrent::run(task_, &Task::Start)); + + already_shown_ = true; + } +} + +void TaskDialog::closeEvent(QCloseEvent *e) +{ + // Cancel task if it is running + task_->Cancel(); + + // Standard close function + super::closeEvent(e); + + // Reset shown + already_shown_ = false; + + // Clean up this task and dialog + if (destroy_on_close_) { + deleteLater(); + } +} + +void TaskDialog::TaskFinished() +{ + QFutureWatcher* task_watcher = static_cast*>(sender()); + + if (task_watcher->result()) { + emit TaskSucceeded(task_); + } else { + ShowErrorMessage(tr("Task Failed"), task_->GetError()); + emit TaskFailed(task_); + } + + task_watcher->deleteLater(); + + close(); +} + +} diff --git a/app/dialog/task/task.h b/app/dialog/task/task.h new file mode 100644 index 000000000..8abb4a060 --- /dev/null +++ b/app/dialog/task/task.h @@ -0,0 +1,84 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef TASKDIALOG_H +#define TASKDIALOG_H + +#include "dialog/progress/progress.h" +#include "task/task.h" + +namespace olive { + +class TaskDialog : public ProgressDialog +{ + Q_OBJECT +public: + /** + * @brief TaskDialog Constructor + * + * Creates a TaskDialog. The TaskDialog takes ownership of the Task and will destroy it on close. + * Connect to the Task::Succeeded() if you want to retrieve information from the task before it + * gets destroyed. + */ + TaskDialog(Task *task, const QString &title, QWidget* parent = nullptr); + + /** + * @brief Set whether TaskDialog should destroy itself (and the task) when it's closed + * + * This is TRUE by default. + */ + void SetDestroyOnClose(bool e) + { + destroy_on_close_ = e; + } + + /** + * @brief Returns this dialog's task + */ + Task* GetTask() const + { + return task_; + } + +protected: + virtual void showEvent(QShowEvent* e) override; + + virtual void closeEvent(QCloseEvent* e) override; + +signals: + void TaskSucceeded(Task* task); + + void TaskFailed(Task* task); + +private: + Task* task_; + + bool destroy_on_close_; + + bool already_shown_; + +private slots: + void TaskFinished(); + +}; + +} + +#endif // TASKDIALOG_H diff --git a/app/task/import/CMakeLists.txt b/app/dialog/text/CMakeLists.txt similarity index 90% rename from app/task/import/CMakeLists.txt rename to app/dialog/text/CMakeLists.txt index ed0fd9842..6a96dce13 100644 --- a/app/task/import/CMakeLists.txt +++ b/app/dialog/text/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2019 Olive Team +# Copyright (C) 2021 Olive Team # # 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 @@ -16,7 +16,7 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} - task/import/import.h - task/import/import.cpp + dialog/text/text.h + dialog/text/text.cpp PARENT_SCOPE ) diff --git a/app/dialog/text/text.cpp b/app/dialog/text/text.cpp new file mode 100644 index 000000000..264d6624b --- /dev/null +++ b/app/dialog/text/text.cpp @@ -0,0 +1,50 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#include "text.h" + +#include +#include +#include +#include +#include + +#include "ui/icons/icons.h" + +namespace olive { + +TextDialog::TextDialog(const QString &start, QWidget* parent) : + QDialog(parent) +{ + QVBoxLayout* layout = new QVBoxLayout(this); + + // Create text edit widget + text_edit_ = new QPlainTextEdit(); + text_edit_->document()->setPlainText(start); + layout->addWidget(text_edit_); + + // Create buttons + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + layout->addWidget(buttons); + connect(buttons, &QDialogButtonBox::accepted, this, &TextDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &TextDialog::reject); +} + +} diff --git a/app/dialog/text/text.h b/app/dialog/text/text.h new file mode 100644 index 000000000..5916b0101 --- /dev/null +++ b/app/dialog/text/text.h @@ -0,0 +1,51 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + 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 . + +***/ + +#ifndef RICHTEXTDIALOG_H +#define RICHTEXTDIALOG_H + +#include +#include +#include + +#include "common/define.h" +#include "widget/slider/floatslider.h" + +namespace olive { + +class TextDialog : public QDialog +{ + Q_OBJECT +public: + TextDialog(const QString &start, QWidget* parent = nullptr); + + QString text() const + { + return text_edit_->toPlainText(); + } + +private: + QPlainTextEdit* text_edit_; + +}; + +} + +#endif // RICHTEXTDIALOG_H diff --git a/app/main.cpp b/app/main.cpp index 6b2e7579f..7e729216f 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -1,7 +1,7 @@ /*** Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team + Copyright (C) 2021 Olive Team 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 @@ -31,48 +31,228 @@ extern "C" { #include } +#include + #include +#include +#include #include #include "core.h" +#include "common/commandlineparser.h" #include "common/debug.h" +#include "version.h" -int main(int argc, char *argv[]) { - QApplication::setAttribute(Qt::AA_ShareOpenGLContexts); +#ifdef _WIN32 +#include +#include +#include +#include +#endif - // Set OpenGL display profile (3.2 Core) - QSurfaceFormat format; - format.setVersion(3, 2); - format.setDepthBufferSize(24); - format.setProfile(QSurfaceFormat::CoreProfile); - QSurfaceFormat::setDefaultFormat(format); +#ifdef USE_CRASHPAD +#include "common/crashpadinterface.h" +#endif // USE_CRASHPAD - // Create application instance - QApplication a(argc, argv); +int main(int argc, char *argv[]) +{ + // Set up debug handler + qInstallMessageHandler(olive::DebugHandler); // Set application metadata QCoreApplication::setOrganizationName("olivevideoeditor.org"); QCoreApplication::setOrganizationDomain("olivevideoeditor.org"); QCoreApplication::setApplicationName("Olive"); - - QString app_version = "0.2.0"; -#ifdef GITHASH - // Anything after the hyphen is considered "unimportant" information. Text BEFORE the hyphen is used in version - // checking project files and config files - app_version.append("-"); - app_version.append(GITHASH); -#endif - - qInfo() << "Using Qt version:" << qVersion(); - - QCoreApplication::setApplicationVersion(app_version); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)) QGuiApplication::setDesktopFileName("org.olivevideoeditor.Olive"); + QCoreApplication::setApplicationVersion(olive::kAppVersionLong); + + + // + // Parse command line arguments + // + + QVector args; +#if defined(_WIN32) && defined(UNICODE) + int wargc; + LPWSTR *wargv = CommandLineToArgvW(GetCommandLineW(), &wargc); + args.resize(wargc); + for (int i=0; i