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 [](https://travis-ci.org/olive-editor/olive) [](https://ci.appveyor.com/project/itsmattkc/olive)
+# Olive Video Editor [](https://github.com/olive-editor/olive/actions?query=branch%3Amaster)
Olive is a free non-linear video editor for Windows, macOS, and Linux.
-
+
-**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:
[](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