Merge branch 'vapoursynth' into feature
This commit is contained in:
commit
24ac0647ac
27 changed files with 1266 additions and 2 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -19,6 +19,7 @@ tools/repack-thes-dict.dSYM
|
||||||
# Meson
|
# Meson
|
||||||
build*/
|
build*/
|
||||||
subprojects/avisynth
|
subprojects/avisynth
|
||||||
|
subprojects/bestsource/
|
||||||
subprojects/boost*/
|
subprojects/boost*/
|
||||||
subprojects/cairo*
|
subprojects/cairo*
|
||||||
subprojects/ffmpeg
|
subprojects/ffmpeg
|
||||||
|
@ -30,6 +31,7 @@ subprojects/glib*
|
||||||
subprojects/googletest-*
|
subprojects/googletest-*
|
||||||
subprojects/harfbuzz
|
subprojects/harfbuzz
|
||||||
subprojects/icu
|
subprojects/icu
|
||||||
|
subprojects/jansson
|
||||||
subprojects/libass
|
subprojects/libass
|
||||||
subprojects/libffi*
|
subprojects/libffi*
|
||||||
subprojects/libpng-*
|
subprojects/libpng-*
|
||||||
|
@ -42,3 +44,4 @@ subprojects/zlib-*
|
||||||
subprojects/dirent-*
|
subprojects/dirent-*
|
||||||
subprojects/hunspell-*
|
subprojects/hunspell-*
|
||||||
subprojects/uchardet-*
|
subprojects/uchardet-*
|
||||||
|
subprojects/vapoursynth
|
||||||
|
|
25
meson.build
25
meson.build
|
@ -224,6 +224,24 @@ foreach dep: [
|
||||||
endif
|
endif
|
||||||
endforeach
|
endforeach
|
||||||
|
|
||||||
|
needs_ffmpeg = false
|
||||||
|
|
||||||
|
if get_option('bestsource').enabled()
|
||||||
|
conf.set('WITH_BESTSOURCE', 1)
|
||||||
|
bs = subproject('bestsource')
|
||||||
|
deps += bs.get_variable('bestsource_dep')
|
||||||
|
dep_avail += 'BestSource'
|
||||||
|
needs_ffmpeg = true
|
||||||
|
endif
|
||||||
|
|
||||||
|
if needs_ffmpeg
|
||||||
|
conf.set('WITH_FFMPEG', 1)
|
||||||
|
deps += [
|
||||||
|
dependency('libavutil', default_options: ['tests=disabled']),
|
||||||
|
dependency('libswscale', default_options: ['tests=disabled']),
|
||||||
|
]
|
||||||
|
endif
|
||||||
|
|
||||||
if get_option('avisynth').enabled()
|
if get_option('avisynth').enabled()
|
||||||
conf.set('WITH_AVISYNTH', 1) # bundled separately with installer
|
conf.set('WITH_AVISYNTH', 1) # bundled separately with installer
|
||||||
dep_avail += 'AviSynth'
|
dep_avail += 'AviSynth'
|
||||||
|
@ -241,6 +259,13 @@ if get_option('avisynth').enabled()
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
if get_option('vapoursynth').enabled()
|
||||||
|
conf.set('WITH_VAPOURSYNTH', 1)
|
||||||
|
vs_sub = subproject('vapoursynth')
|
||||||
|
deps_inc += vs_sub.get_variable('vs_inc')
|
||||||
|
dep_avail += 'VapourSynth'
|
||||||
|
endif
|
||||||
|
|
||||||
if host_machine.system() == 'windows' and not get_option('directsound').disabled()
|
if host_machine.system() == 'windows' and not get_option('directsound').disabled()
|
||||||
dsound_dep = cc.find_library('dsound', required: get_option('directsound'))
|
dsound_dep = cc.find_library('dsound', required: get_option('directsound'))
|
||||||
winmm_dep = cc.find_library('winmm', required: get_option('directsound'))
|
winmm_dep = cc.find_library('winmm', required: get_option('directsound'))
|
||||||
|
|
|
@ -7,6 +7,8 @@ option('default_audio_output', type: 'combo', choices: ['auto', 'ALSA', 'OpenAL'
|
||||||
|
|
||||||
option('ffms2', type: 'feature', description: 'FFMS2 video source')
|
option('ffms2', type: 'feature', description: 'FFMS2 video source')
|
||||||
option('avisynth', type: 'feature', description: 'AviSynth video source')
|
option('avisynth', type: 'feature', description: 'AviSynth video source')
|
||||||
|
option('bestsource', type: 'feature', description: 'BestSource video source')
|
||||||
|
option('vapoursynth', type: 'feature', description: 'VapourSynth video source')
|
||||||
|
|
||||||
option('fftw3', type: 'feature', description: 'FFTW3 support')
|
option('fftw3', type: 'feature', description: 'FFTW3 support')
|
||||||
option('hunspell', type: 'feature', description: 'Hunspell spell checker')
|
option('hunspell', type: 'feature', description: 'Hunspell spell checker')
|
||||||
|
|
|
@ -29,6 +29,7 @@ Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes";
|
||||||
Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".wav"; ValueData: ""; Flags: uninsdeletekey
|
Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".wav"; ValueData: ""; Flags: uninsdeletekey
|
||||||
Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".ogg"; ValueData: ""; Flags: uninsdeletekey
|
Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".ogg"; ValueData: ""; Flags: uninsdeletekey
|
||||||
Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".avs"; ValueData: ""; Flags: uninsdeletekey
|
Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".avs"; ValueData: ""; Flags: uninsdeletekey
|
||||||
|
Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".vpy"; ValueData: ""; Flags: uninsdeletekey
|
||||||
Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".opus"; ValueData: ""; Flags: uninsdeletekey
|
Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".opus"; ValueData: ""; Flags: uninsdeletekey
|
||||||
Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".h264"; ValueData: ""; Flags: uninsdeletekey
|
Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".h264"; ValueData: ""; Flags: uninsdeletekey
|
||||||
Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".hevc"; ValueData: ""; Flags: uninsdeletekey
|
Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".hevc"; ValueData: ""; Flags: uninsdeletekey
|
||||||
|
@ -165,6 +166,7 @@ Root: HKLM; Subkey: "SOFTWARE\Classes\.m4a\OpenWithProgids"; ValueType: string;
|
||||||
Root: HKLM; Subkey: "SOFTWARE\Classes\.wav\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Audio.1"; Flags: uninsdeletevalue
|
Root: HKLM; Subkey: "SOFTWARE\Classes\.wav\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Audio.1"; Flags: uninsdeletevalue
|
||||||
Root: HKLM; Subkey: "SOFTWARE\Classes\.ogg\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Media.1"; Flags: uninsdeletevalue
|
Root: HKLM; Subkey: "SOFTWARE\Classes\.ogg\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Media.1"; Flags: uninsdeletevalue
|
||||||
Root: HKLM; Subkey: "SOFTWARE\Classes\.avs\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Video.1"; Flags: uninsdeletevalue
|
Root: HKLM; Subkey: "SOFTWARE\Classes\.avs\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Video.1"; Flags: uninsdeletevalue
|
||||||
|
Root: HKLM; Subkey: "SOFTWARE\Classes\.vpy\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Media.1"; Flags: uninsdeletevalue
|
||||||
Root: HKLM; Subkey: "SOFTWARE\Classes\.opus\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Audio.1"; Flags: uninsdeletevalue
|
Root: HKLM; Subkey: "SOFTWARE\Classes\.opus\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Audio.1"; Flags: uninsdeletevalue
|
||||||
Root: HKLM; Subkey: "SOFTWARE\Classes\.h264\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Video.1"; Flags: uninsdeletevalue
|
Root: HKLM; Subkey: "SOFTWARE\Classes\.h264\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Video.1"; Flags: uninsdeletevalue
|
||||||
Root: HKLM; Subkey: "SOFTWARE\Classes\.hevc\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Video.1"; Flags: uninsdeletevalue
|
Root: HKLM; Subkey: "SOFTWARE\Classes\.hevc\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Video.1"; Flags: uninsdeletevalue
|
||||||
|
|
89
src/audio_provider_bestsource.cpp
Normal file
89
src/audio_provider_bestsource.cpp
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
// Copyright (c) 2022, arch1t3cht <arch1t3cht@gmail.com>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
//
|
||||||
|
// Aegisub Project http://www.aegisub.org/
|
||||||
|
|
||||||
|
/// @file audio_provider_bestsource.cpp
|
||||||
|
/// @brief BS-based audio provider
|
||||||
|
/// @ingroup audio_input bestsource
|
||||||
|
///
|
||||||
|
|
||||||
|
#ifdef WITH_BESTSOURCE
|
||||||
|
#include <libaegisub/audio/provider.h>
|
||||||
|
|
||||||
|
#include "audiosource.h"
|
||||||
|
|
||||||
|
#include "bestsource_common.h"
|
||||||
|
#include "compat.h"
|
||||||
|
#include "options.h"
|
||||||
|
|
||||||
|
#include <libaegisub/fs.h>
|
||||||
|
#include <libaegisub/make_unique.h>
|
||||||
|
#include <libaegisub/background_runner.h>
|
||||||
|
#include <libaegisub/log.h>
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
class BSAudioProvider final : public agi::AudioProvider {
|
||||||
|
std::map<std::string, std::string> bsopts;
|
||||||
|
BestAudioSource bs;
|
||||||
|
AudioProperties properties;
|
||||||
|
|
||||||
|
void FillBuffer(void *Buf, int64_t Start, int64_t Count) const override;
|
||||||
|
public:
|
||||||
|
BSAudioProvider(agi::fs::path const& filename, agi::BackgroundRunner *br);
|
||||||
|
|
||||||
|
bool NeedsCache() const override { return OPT_GET("Provider/Audio/BestSource/Aegisub Cache")->GetBool(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Constructor
|
||||||
|
/// @param filename The filename to open
|
||||||
|
BSAudioProvider::BSAudioProvider(agi::fs::path const& filename, agi::BackgroundRunner *br) try
|
||||||
|
: bsopts()
|
||||||
|
, bs(filename.string(), -1, -1, GetBSCacheFile(filename), &bsopts)
|
||||||
|
{
|
||||||
|
bs.SetMaxCacheSize(OPT_GET("Provider/Audio/BestSource/Max Cache Size")->GetInt() << 20);
|
||||||
|
br->Run([&](agi::ProgressSink *ps) {
|
||||||
|
ps->SetTitle(from_wx(_("Exacting")));
|
||||||
|
ps->SetMessage(from_wx(_("Creating cache... This can take a while!")));
|
||||||
|
ps->SetIndeterminate();
|
||||||
|
if (bs.GetExactDuration()) {
|
||||||
|
LOG_D("bs") << "File cached and has exact samples.";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
properties = bs.GetAudioProperties();
|
||||||
|
float_samples = properties.IsFloat;
|
||||||
|
bytes_per_sample = properties.BytesPerSample;
|
||||||
|
sample_rate = properties.SampleRate;
|
||||||
|
channels = properties.Channels;
|
||||||
|
num_samples = properties.NumSamples;
|
||||||
|
decoded_samples = OPT_GET("Provider/Audio/BestSource/Aegisub Cache")->GetBool() ? 0 : num_samples;
|
||||||
|
}
|
||||||
|
catch (AudioException const& err) {
|
||||||
|
throw agi::AudioProviderError("Failed to create BestAudioSource");
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSAudioProvider::FillBuffer(void *Buf, int64_t Start, int64_t Count) const {
|
||||||
|
const_cast<BestAudioSource &>(bs).GetPackedAudio(reinterpret_cast<uint8_t *>(Buf), Start, Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<agi::AudioProvider> CreateBSAudioProvider(agi::fs::path const& file, agi::BackgroundRunner *br) {
|
||||||
|
return agi::make_unique<BSAudioProvider>(file, br);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* WITH_BESTSOURCE */
|
||||||
|
|
|
@ -31,6 +31,8 @@ using namespace agi;
|
||||||
|
|
||||||
std::unique_ptr<AudioProvider> CreateAvisynthAudioProvider(fs::path const& filename, BackgroundRunner *);
|
std::unique_ptr<AudioProvider> CreateAvisynthAudioProvider(fs::path const& filename, BackgroundRunner *);
|
||||||
std::unique_ptr<AudioProvider> CreateFFmpegSourceAudioProvider(fs::path const& filename, BackgroundRunner *);
|
std::unique_ptr<AudioProvider> CreateFFmpegSourceAudioProvider(fs::path const& filename, BackgroundRunner *);
|
||||||
|
std::unique_ptr<AudioProvider> CreateBSAudioProvider(fs::path const& filename, BackgroundRunner *);
|
||||||
|
std::unique_ptr<AudioProvider> CreateVapoursynthAudioProvider(fs::path const& filename, BackgroundRunner *);
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
struct factory {
|
struct factory {
|
||||||
|
@ -48,6 +50,12 @@ const factory providers[] = {
|
||||||
#ifdef WITH_AVISYNTH
|
#ifdef WITH_AVISYNTH
|
||||||
{"Avisynth", CreateAvisynthAudioProvider, false},
|
{"Avisynth", CreateAvisynthAudioProvider, false},
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef WITH_BESTSOURCE
|
||||||
|
{"BestSource", CreateBSAudioProvider, false},
|
||||||
|
#endif
|
||||||
|
#ifdef WITH_VAPOURSYNTH
|
||||||
|
{"Vapoursynth", CreateVapoursynthAudioProvider, false},
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
167
src/audio_provider_vs.cpp
Normal file
167
src/audio_provider_vs.cpp
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
// Copyright (c) 2022, arch1t3cht <arch1t3cht@gmail.com>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
//
|
||||||
|
// Aegisub Project http://www.aegisub.org/
|
||||||
|
|
||||||
|
/// @file audio_provider_vs.cpp
|
||||||
|
/// @brief Vapoursynth-based audio provider
|
||||||
|
/// @ingroup audio_input
|
||||||
|
///
|
||||||
|
|
||||||
|
#ifdef WITH_VAPOURSYNTH
|
||||||
|
#include <libaegisub/audio/provider.h>
|
||||||
|
|
||||||
|
#include "audio_controller.h"
|
||||||
|
#include "options.h"
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
#include <libaegisub/access.h>
|
||||||
|
#include <libaegisub/format.h>
|
||||||
|
#include <libaegisub/fs.h>
|
||||||
|
#include <libaegisub/path.h>
|
||||||
|
#include <libaegisub/make_unique.h>
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include "vapoursynth_wrap.h"
|
||||||
|
#include "VSScript4.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
class VapoursynthAudioProvider final : public agi::AudioProvider {
|
||||||
|
VapourSynthWrapper vs;
|
||||||
|
VSScript *script = nullptr;
|
||||||
|
VSNode *node = nullptr;
|
||||||
|
const VSAudioInfo *vi = nullptr;
|
||||||
|
|
||||||
|
void FillBufferWithFrame(void *buf, int frame, int64_t start, int64_t count) const;
|
||||||
|
void FillBuffer(void *buf, int64_t start, int64_t count) const override;
|
||||||
|
public:
|
||||||
|
VapoursynthAudioProvider(agi::fs::path const& filename);
|
||||||
|
~VapoursynthAudioProvider();
|
||||||
|
|
||||||
|
bool NeedsCache() const override { return true; }
|
||||||
|
};
|
||||||
|
|
||||||
|
VapoursynthAudioProvider::VapoursynthAudioProvider(agi::fs::path const& filename) try {
|
||||||
|
agi::acs::CheckFileRead(filename);
|
||||||
|
std::lock_guard<std::mutex> lock(vs.GetMutex());
|
||||||
|
|
||||||
|
script = vs.GetScriptAPI()->createScript(nullptr);
|
||||||
|
if (script == nullptr) {
|
||||||
|
throw VapoursynthError("Error creating script API");
|
||||||
|
}
|
||||||
|
vs.GetScriptAPI()->evalSetWorkingDir(script, 1);
|
||||||
|
if (vs.GetScriptAPI()->evaluateFile(script, filename.string().c_str())) {
|
||||||
|
std::string msg = agi::format("Error executing VapourSynth script: %s", vs.GetScriptAPI()->getError(script));
|
||||||
|
vs.GetScriptAPI()->freeScript(script);
|
||||||
|
throw VapoursynthError(msg);
|
||||||
|
}
|
||||||
|
node = vs.GetScriptAPI()->getOutputNode(script, 0);
|
||||||
|
if (node == nullptr) {
|
||||||
|
vs.GetScriptAPI()->freeScript(script);
|
||||||
|
throw VapoursynthError("No output node set");
|
||||||
|
}
|
||||||
|
if (vs.GetAPI()->getNodeType(node) != mtAudio) {
|
||||||
|
vs.GetAPI()->freeNode(node);
|
||||||
|
vs.GetScriptAPI()->freeScript(script);
|
||||||
|
throw VapoursynthError("Output node isn't an audio node");
|
||||||
|
}
|
||||||
|
vi = vs.GetAPI()->getAudioInfo(node);
|
||||||
|
float_samples = vi->format.sampleType == stFloat;
|
||||||
|
bytes_per_sample = vi->format.bytesPerSample;
|
||||||
|
sample_rate = vi->sampleRate;
|
||||||
|
channels = vi->format.numChannels;
|
||||||
|
num_samples = vi->numSamples;
|
||||||
|
}
|
||||||
|
catch (VapoursynthError const& err) {
|
||||||
|
throw agi::AudioProviderError(agi::format("Vapoursynth error: %s", err.GetMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
static void PackChannels(const uint8_t **Src, void *Dst, size_t Length, size_t Channels) {
|
||||||
|
T *D = reinterpret_cast<T *>(Dst);
|
||||||
|
for (size_t c = 0; c < Channels; c++) {
|
||||||
|
const T *S = reinterpret_cast<const T *>(Src[c]);
|
||||||
|
for (size_t i = 0; i < Length; i++) {
|
||||||
|
D[Channels * i + c] = S[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VapoursynthAudioProvider::FillBufferWithFrame(void *buf, int n, int64_t start, int64_t count) const {
|
||||||
|
char errorMsg[1024];
|
||||||
|
const VSFrame *frame = vs.GetAPI()->getFrame(n, node, errorMsg, sizeof(errorMsg));
|
||||||
|
if (frame == nullptr) {
|
||||||
|
throw VapoursynthError(agi::format("Error getting frame: %s", errorMsg));
|
||||||
|
}
|
||||||
|
if (vs.GetAPI()->getFrameLength(frame) < count) {
|
||||||
|
vs.GetAPI()->freeFrame(frame);
|
||||||
|
throw VapoursynthError("Audio frame too short");
|
||||||
|
}
|
||||||
|
if (vs.GetAPI()->getAudioFrameFormat(frame)->numChannels != channels || vs.GetAPI()->getAudioFrameFormat(frame)->bytesPerSample != bytes_per_sample) {
|
||||||
|
vs.GetAPI()->freeFrame(frame);
|
||||||
|
throw VapoursynthError("Audio format is not constant");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<const uint8_t *> planes(channels);
|
||||||
|
for (int c = 0; c < channels; c++) {
|
||||||
|
planes[c] = vs.GetAPI()->getReadPtr(frame, c);
|
||||||
|
if (planes[c] == nullptr) {
|
||||||
|
vs.GetAPI()->freeFrame(frame);
|
||||||
|
throw VapoursynthError("Failed to read audio channel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes_per_sample == 1)
|
||||||
|
PackChannels<uint8_t>(planes.data(), buf, count, channels);
|
||||||
|
else if (bytes_per_sample == 2)
|
||||||
|
PackChannels<uint16_t>(planes.data(), buf, count, channels);
|
||||||
|
else if (bytes_per_sample == 4)
|
||||||
|
PackChannels<uint32_t>(planes.data(), buf, count, channels);
|
||||||
|
else if (bytes_per_sample == 8)
|
||||||
|
PackChannels<uint64_t>(planes.data(), buf, count, channels);
|
||||||
|
|
||||||
|
vs.GetAPI()->freeFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VapoursynthAudioProvider::FillBuffer(void *buf, int64_t start, int64_t count) const {
|
||||||
|
int end = start + count; // exclusive
|
||||||
|
int startframe = start / VS_AUDIO_FRAME_SAMPLES;
|
||||||
|
int endframe = (end - 1) / VS_AUDIO_FRAME_SAMPLES;
|
||||||
|
int offset = start - (VS_AUDIO_FRAME_SAMPLES * startframe);
|
||||||
|
|
||||||
|
for (int frame = startframe; frame <= endframe; frame++) {
|
||||||
|
int framestart = frame * VS_AUDIO_FRAME_SAMPLES;
|
||||||
|
int frameend = (frame + 1) * VS_AUDIO_FRAME_SAMPLES;
|
||||||
|
int fstart = framestart < start ? start - framestart : 0;
|
||||||
|
int fcount = VS_AUDIO_FRAME_SAMPLES - fstart - (frameend > end ? frameend - end : 0);
|
||||||
|
int bufstart = frame == startframe ? 0 : (frame - startframe) * VS_AUDIO_FRAME_SAMPLES - offset;
|
||||||
|
FillBufferWithFrame(reinterpret_cast<uint8_t *>(buf) + channels * bytes_per_sample * bufstart, frame, fstart, fcount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VapoursynthAudioProvider::~VapoursynthAudioProvider() {
|
||||||
|
if (node != nullptr) {
|
||||||
|
vs.GetAPI()->freeNode(node);
|
||||||
|
}
|
||||||
|
if (script != nullptr) {
|
||||||
|
vs.GetScriptAPI()->freeScript(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<agi::AudioProvider> CreateVapoursynthAudioProvider(agi::fs::path const& file, agi::BackgroundRunner *) {
|
||||||
|
return agi::make_unique<VapoursynthAudioProvider>(file);
|
||||||
|
}
|
||||||
|
#endif
|
49
src/bestsource_common.cpp
Normal file
49
src/bestsource_common.cpp
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright (c) 2022, arch1t3cht <arch1t3cht@gmail.com>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
//
|
||||||
|
// Aegisub Project http://www.aegisub.org/
|
||||||
|
|
||||||
|
/// @file ffmpegsource_common.cpp
|
||||||
|
/// @brief Shared code for ffms video and audio providers
|
||||||
|
/// @ingroup video_input audio_input ffms
|
||||||
|
///
|
||||||
|
|
||||||
|
#ifdef WITH_BESTSOURCE
|
||||||
|
#include "bestsource_common.h"
|
||||||
|
|
||||||
|
#include "options.h"
|
||||||
|
|
||||||
|
#include <libaegisub/fs.h>
|
||||||
|
#include <libaegisub/path.h>
|
||||||
|
|
||||||
|
#include <boost/crc.hpp>
|
||||||
|
#include <boost/filesystem/path.hpp>
|
||||||
|
|
||||||
|
|
||||||
|
std::string GetBSCacheFile(agi::fs::path const& filename) {
|
||||||
|
// BS can store all its index data in a single file, but we make a separate index file
|
||||||
|
// for each video file to ensure that the old index is invalidated if the file is modified.
|
||||||
|
// While BS does check the filesize of the files, it doesn't check the modification time.
|
||||||
|
uintmax_t len = agi::fs::Size(filename);
|
||||||
|
boost::crc_32_type hash;
|
||||||
|
hash.process_bytes(filename.string().c_str(), filename.string().size());
|
||||||
|
|
||||||
|
auto result = config::path->Decode("?local/bsindex/" + filename.filename().string() + "_" + std::to_string(hash.checksum()) + "_" + std::to_string(len) + "_" + std::to_string(agi::fs::ModifiedTime(filename)) + ".json");
|
||||||
|
agi::fs::CreateDirectory(result.parent_path());
|
||||||
|
|
||||||
|
return result.string();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endif // WITH_BESTSOURCE
|
28
src/bestsource_common.h
Normal file
28
src/bestsource_common.h
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// Copyright (c) 2022, arch1t3cht <arch1t3cht@gmail.com>>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
//
|
||||||
|
// Aegisub Project http://www.aegisub.org/
|
||||||
|
|
||||||
|
/// @file ffmpegsource_common.h
|
||||||
|
/// @see ffmpegsource_common.cpp
|
||||||
|
/// @ingroup video_input audio_input ffms
|
||||||
|
///
|
||||||
|
|
||||||
|
#ifdef WITH_BESTSOURCE
|
||||||
|
|
||||||
|
#include <libaegisub/fs_fwd.h>
|
||||||
|
|
||||||
|
std::string GetBSCacheFile(agi::fs::path const& filename);
|
||||||
|
|
||||||
|
#endif /* WITH_BESTSOURCE */
|
|
@ -80,7 +80,7 @@ struct audio_open final : public Command {
|
||||||
STR_HELP("Open an audio file")
|
STR_HELP("Open an audio file")
|
||||||
|
|
||||||
void operator()(agi::Context *c) override {
|
void operator()(agi::Context *c) override {
|
||||||
auto str = from_wx(_("Audio Formats") + " (*.aac,*.ac3,*.ape,*.dts,*.eac3,*.flac,*.m4a,*.mka,*.mp3,*.mp4,*.ogg,*.opus,*.w64,*.wav,*.wma)|*.aac;*.ac3;*.ape;*.dts;*.eac3;*.flac;*.m4a;*.mka;*.mp3;*.mp4;*.ogg;*.opus;*.w64;*.wav;*.wma|"
|
auto str = from_wx(_("Audio Formats") + " (*.aac,*.ac3,*.ape,*.avs,*.dts,*.eac3,*.flac,*.m4a,*.mka,*.mp3,*.mp4,*.ogg,*.opus,*.vpy,*.w64,*.wav,*.wma)|*.aac;*.ac3;*.ape;*.avs;*.dts;*.eac3;*.flac;*.m4a;*.mka;*.mp3;*.mp4;*.ogg;*.opus;*.vpy;*.w64;*.wav;*.wma|"
|
||||||
+ _("Video Formats") + " (*.asf,*.avi,*.avs,*.d2v,*.m2ts,*.m4v,*.mkv,*.mov,*.mp4,*.mpeg,*.mpg,*.ogm,*.webm,*.wmv,*.ts)|*.asf;*.avi;*.avs;*.d2v;*.m2ts;*.m4v;*.mkv;*.mov;*.mp4;*.mpeg;*.mpg;*.ogm;*.webm;*.wmv;*.ts|"
|
+ _("Video Formats") + " (*.asf,*.avi,*.avs,*.d2v,*.m2ts,*.m4v,*.mkv,*.mov,*.mp4,*.mpeg,*.mpg,*.ogm,*.webm,*.wmv,*.ts)|*.asf;*.avi;*.avs;*.d2v;*.m2ts;*.m4v;*.mkv;*.mov;*.mp4;*.mpeg;*.mpg;*.ogm;*.webm;*.wmv;*.ts|"
|
||||||
+ _("All Files") + " (*.*)|*.*");
|
+ _("All Files") + " (*.*)|*.*");
|
||||||
auto filename = OpenFileSelector(_("Open Audio File"), "Path/Last/Audio", "", "", str, c->parent);
|
auto filename = OpenFileSelector(_("Open Audio File"), "Path/Last/Audio", "", "", str, c->parent);
|
||||||
|
|
|
@ -581,7 +581,7 @@ struct video_open final : public Command {
|
||||||
STR_HELP("Open a video file")
|
STR_HELP("Open a video file")
|
||||||
|
|
||||||
void operator()(agi::Context *c) override {
|
void operator()(agi::Context *c) override {
|
||||||
auto str = from_wx(_("Video Formats") + " (*.asf,*.avi,*.avs,*.d2v,*.h264,*.hevc,*.m2ts,*.m4v,*.mkv,*.mov,*.mp4,*.mpeg,*.mpg,*.ogm,*.webm,*.wmv,*.ts,*.y4m,*.yuv)|*.asf;*.avi;*.avs;*.d2v;*.h264;*.hevc;*.m2ts;*.m4v;*.mkv;*.mov;*.mp4;*.mpeg;*.mpg;*.ogm;*.webm;*.wmv;*.ts;*.y4m;*.yuv|"
|
auto str = from_wx(_("Video Formats") + " (*.asf,*.avi,*.avs,*.d2v,*.h264,*.hevc,*.m2ts,*.m4v,*.mkv,*.mov,*.mp4,*.mpeg,*.mpg,*.ogm,*.webm,*.wmv,*.ts,*.vpy,*.y4m,*.yuv)|*.asf;*.avi;*.avs;*.d2v;*.h264;*.hevc;*.m2ts;*.m4v;*.mkv;*.mov;*.mp4;*.mpeg;*.mpg;*.ogm;*.webm;*.wmv;*.ts;*.vpy;*.y4m;*.yuv|"
|
||||||
+ _("All Files") + " (*.*)|*.*");
|
+ _("All Files") + " (*.*)|*.*");
|
||||||
auto filename = OpenFileSelector(_("Open video file"), "Path/Last/Video", "", "", str, c->parent);
|
auto filename = OpenFileSelector(_("Open video file"), "Path/Last/Video", "", "", str, c->parent);
|
||||||
if (!filename.empty())
|
if (!filename.empty())
|
||||||
|
|
|
@ -336,6 +336,10 @@
|
||||||
"FFmpegSource" : {
|
"FFmpegSource" : {
|
||||||
"Decode Error Handling" : "ignore",
|
"Decode Error Handling" : "ignore",
|
||||||
"Downmix" : false
|
"Downmix" : false
|
||||||
|
},
|
||||||
|
"BestSource": {
|
||||||
|
"Max Cache Size" : 100,
|
||||||
|
"Aegisub Cache" : true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Avisynth" : {
|
"Avisynth" : {
|
||||||
|
@ -356,6 +360,11 @@
|
||||||
"FFmpegSource" : {
|
"FFmpegSource" : {
|
||||||
"Decoding Threads" : -1,
|
"Decoding Threads" : -1,
|
||||||
"Unsafe Seeking" : false
|
"Unsafe Seeking" : false
|
||||||
|
},
|
||||||
|
"BestSource" : {
|
||||||
|
"Max Cache Size" : 1024,
|
||||||
|
"Threads" : 0,
|
||||||
|
"Seek Preroll" : 12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -335,6 +335,10 @@
|
||||||
},
|
},
|
||||||
"FFmpegSource" : {
|
"FFmpegSource" : {
|
||||||
"Decode Error Handling" : "ignore"
|
"Decode Error Handling" : "ignore"
|
||||||
|
},
|
||||||
|
"BestSource": {
|
||||||
|
"Max Cache Size" : 100,
|
||||||
|
"Aegisub Cache" : true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Avisynth" : {
|
"Avisynth" : {
|
||||||
|
@ -355,6 +359,11 @@
|
||||||
"FFmpegSource" : {
|
"FFmpegSource" : {
|
||||||
"Decoding Threads" : -1,
|
"Decoding Threads" : -1,
|
||||||
"Unsafe Seeking" : false
|
"Unsafe Seeking" : false
|
||||||
|
},
|
||||||
|
"BestSource" : {
|
||||||
|
"Max Cache Size" : 1024,
|
||||||
|
"Threads" : 0,
|
||||||
|
"Seek Preroll" : 12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -237,6 +237,12 @@ opt_src = [
|
||||||
['FFMS2', ['audio_provider_ffmpegsource.cpp',
|
['FFMS2', ['audio_provider_ffmpegsource.cpp',
|
||||||
'video_provider_ffmpegsource.cpp',
|
'video_provider_ffmpegsource.cpp',
|
||||||
'ffmpegsource_common.cpp']],
|
'ffmpegsource_common.cpp']],
|
||||||
|
['BestSource', ['audio_provider_bestsource.cpp',
|
||||||
|
'video_provider_bestsource.cpp',
|
||||||
|
'bestsource_common.cpp']],
|
||||||
|
['VapourSynth', ['vapoursynth_wrap.cpp',
|
||||||
|
'audio_provider_vs.cpp',
|
||||||
|
'video_provider_vs.cpp']],
|
||||||
|
|
||||||
['AviSynth', ['avisynth_wrap.cpp',
|
['AviSynth', ['avisynth_wrap.cpp',
|
||||||
'audio_provider_avs.cpp',
|
'audio_provider_avs.cpp',
|
||||||
|
|
|
@ -417,6 +417,13 @@ void Advanced_Audio(wxTreebook *book, Preferences *parent) {
|
||||||
stereo->SetToolTip("Reduces memory usage on surround audio, but may cause audio tracks to sound blank in specific circumstances. This will not affect audio with two channels or less.");
|
stereo->SetToolTip("Reduces memory usage on surround audio, but may cause audio tracks to sound blank in specific circumstances. This will not affect audio with two channels or less.");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef WITH_BESTSOURCE
|
||||||
|
auto bs = p->PageSizer("BestSource");
|
||||||
|
p->OptionAdd(bs, _("Max BS cache size (MB)"), "Provider/Audio/BestSource/Max Cache Size");
|
||||||
|
p->OptionAdd(bs, _("Use Aegisub's Cache"), "Provider/Audio/BestSource/Aegisub Cache");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
#ifdef WITH_PORTAUDIO
|
#ifdef WITH_PORTAUDIO
|
||||||
auto portaudio = p->PageSizer("Portaudio");
|
auto portaudio = p->PageSizer("Portaudio");
|
||||||
p->OptionChoice(portaudio, _("Portaudio device"), PortAudioPlayer::GetOutputDevices(), "Player/Audio/PortAudio/Device Name");
|
p->OptionChoice(portaudio, _("Portaudio device"), PortAudioPlayer::GetOutputDevices(), "Player/Audio/PortAudio/Device Name");
|
||||||
|
@ -469,6 +476,13 @@ void Advanced_Video(wxTreebook *book, Preferences *parent) {
|
||||||
p->OptionAdd(ffms, _("Enable unsafe seeking"), "Provider/Video/FFmpegSource/Unsafe Seeking");
|
p->OptionAdd(ffms, _("Enable unsafe seeking"), "Provider/Video/FFmpegSource/Unsafe Seeking");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef WITH_BESTSOURCE
|
||||||
|
auto bs = p->PageSizer("BestSource");
|
||||||
|
p->OptionAdd(bs, _("Max cache size (MB)"), "Provider/Video/BestSource/Max Cache Size");
|
||||||
|
p->OptionAdd(bs, _("Decoder Threads (0 to autodetect)"), "Provider/Video/BestSource/Threads");
|
||||||
|
p->OptionAdd(bs, _("Seek preroll (Frames)"), "Provider/Video/BestSource/Seek Preroll");
|
||||||
|
#endif
|
||||||
|
|
||||||
p->SetSizerAndFit(p->sizer);
|
p->SetSizerAndFit(p->sizer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
105
src/vapoursynth_wrap.cpp
Normal file
105
src/vapoursynth_wrap.cpp
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
// Copyright (c) 2022, arch1t3cht <arch1t3cht@gmail.com>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
//
|
||||||
|
// Aegisub Project http://www.aegisub.org/
|
||||||
|
|
||||||
|
/// @file vapoursynth_wrap.cpp
|
||||||
|
/// @brief Wrapper-layer for Vapoursynth
|
||||||
|
/// @ingroup video_input audio_input
|
||||||
|
///
|
||||||
|
|
||||||
|
#ifdef WITH_VAPOURSYNTH
|
||||||
|
#include "vapoursynth_wrap.h"
|
||||||
|
|
||||||
|
#include "VSScript4.h"
|
||||||
|
|
||||||
|
#include "options.h"
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#define VSSCRIPT_SO "vsscript.dll"
|
||||||
|
#else
|
||||||
|
#define VSSCRIPT_SO "libvapoursynth-script.so"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Allocate storage for and initialise static members
|
||||||
|
namespace {
|
||||||
|
bool vs_loaded = false;
|
||||||
|
#ifdef _WIN32
|
||||||
|
HINSTANCE hLib = nullptr;
|
||||||
|
#else
|
||||||
|
void* hLib = nullptr;
|
||||||
|
#endif
|
||||||
|
const VSAPI *api = nullptr;
|
||||||
|
VSSCRIPTAPI *scriptapi = nullptr;
|
||||||
|
std::mutex VapourSynthMutex;
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef VSSCRIPTAPI* VS_CC FUNC(int);
|
||||||
|
|
||||||
|
VapourSynthWrapper::VapourSynthWrapper() {
|
||||||
|
// VSScript assumes it's only loaded once, so unlike AVS we can't unload it when the refcount reaches zero
|
||||||
|
if (!vs_loaded) {
|
||||||
|
vs_loaded = true;
|
||||||
|
#ifdef _WIN32
|
||||||
|
#define CONCATENATE(x, y) x ## y
|
||||||
|
#define _Lstr(x) CONCATENATE(L, x)
|
||||||
|
hLib = LoadLibraryW(_Lstr(VSSCRIPT_SO));
|
||||||
|
#undef _Lstr
|
||||||
|
#undef CONCATENATE
|
||||||
|
#else
|
||||||
|
hLib = dlopen(VSSCRIPT_SO, RTLD_LAZY | RTLD_GLOBAL | RTLD_DEEPBIND);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!hLib)
|
||||||
|
throw VapoursynthError("Could not load " VSSCRIPT_SO);
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
FUNC* getVSScriptAPI = (FUNC*)GetProcAddress(hLib, "getVSScriptAPI");
|
||||||
|
#else
|
||||||
|
FUNC* getVSScriptAPI = (FUNC*)dlsym(hLib, "getVSScriptAPI");
|
||||||
|
#endif
|
||||||
|
if (!getVSScriptAPI)
|
||||||
|
throw VapoursynthError("Failed to get address of getVSScriptAPI from " VSSCRIPT_SO);
|
||||||
|
|
||||||
|
scriptapi = getVSScriptAPI(VSSCRIPT_API_VERSION);
|
||||||
|
|
||||||
|
if (!scriptapi)
|
||||||
|
throw VapoursynthError("Failed to get Vapoursynth ScriptAPI");
|
||||||
|
|
||||||
|
api = scriptapi->getVSAPI(VAPOURSYNTH_API_VERSION);
|
||||||
|
|
||||||
|
if (!api)
|
||||||
|
throw VapoursynthError("Failed to get Vapoursynth API");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::mutex& VapourSynthWrapper::GetMutex() const {
|
||||||
|
return VapourSynthMutex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VSAPI *VapourSynthWrapper::GetAPI() const {
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VSSCRIPTAPI *VapourSynthWrapper::GetScriptAPI() const {
|
||||||
|
return scriptapi;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
42
src/vapoursynth_wrap.h
Normal file
42
src/vapoursynth_wrap.h
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
// Copyright (c) 2022, arch1t3cht <arch1t3cht@gmail.com>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
//
|
||||||
|
// Aegisub Project http://www.aegisub.org/
|
||||||
|
|
||||||
|
/// @file vapoursynth_wrap.h
|
||||||
|
/// @see vapoursynth_wrap.cpp
|
||||||
|
/// @ingroup video_input audio_input
|
||||||
|
///
|
||||||
|
|
||||||
|
#ifdef WITH_VAPOURSYNTH
|
||||||
|
|
||||||
|
#include <libaegisub/exception.h>
|
||||||
|
|
||||||
|
DEFINE_EXCEPTION(VapoursynthError, agi::Exception);
|
||||||
|
|
||||||
|
struct VSAPI;
|
||||||
|
struct VSSCRIPTAPI;
|
||||||
|
namespace std { class mutex; }
|
||||||
|
|
||||||
|
class VapourSynthWrapper {
|
||||||
|
VapourSynthWrapper(VapourSynthWrapper const&);
|
||||||
|
public:
|
||||||
|
std::mutex& GetMutex() const;
|
||||||
|
const VSAPI *GetAPI() const;
|
||||||
|
const VSSCRIPTAPI *GetScriptAPI() const;
|
||||||
|
|
||||||
|
VapourSynthWrapper();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
|
@ -14,6 +14,8 @@
|
||||||
//
|
//
|
||||||
// Aegisub Project http://www.aegisub.org/
|
// Aegisub Project http://www.aegisub.org/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
class wxImage;
|
class wxImage;
|
||||||
|
|
204
src/video_provider_bestsource.cpp
Normal file
204
src/video_provider_bestsource.cpp
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
// Copyright (c) 2022, arch1t3cht <arch1t3cht@gmail.com>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
//
|
||||||
|
// Aegisub Project http://www.aegisub.org/
|
||||||
|
|
||||||
|
/// @file video_provider_bestsource.cpp
|
||||||
|
/// @brief BestSource-based video provider
|
||||||
|
/// @ingroup video_input bestsource
|
||||||
|
///
|
||||||
|
|
||||||
|
#ifdef WITH_BESTSOURCE
|
||||||
|
#include "include/aegisub/video_provider.h"
|
||||||
|
|
||||||
|
#include "videosource.h"
|
||||||
|
#include "audiosource.h"
|
||||||
|
#include "BSRational.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <libavutil/frame.h>
|
||||||
|
#include <libavutil/pixfmt.h>
|
||||||
|
#include <libswscale/swscale.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "bestsource_common.h"
|
||||||
|
#include "options.h"
|
||||||
|
#include "compat.h"
|
||||||
|
#include "video_frame.h"
|
||||||
|
namespace agi { class BackgroundRunner; }
|
||||||
|
|
||||||
|
#include <libaegisub/fs.h>
|
||||||
|
#include <libaegisub/path.h>
|
||||||
|
#include <libaegisub/make_unique.h>
|
||||||
|
#include <libaegisub/background_runner.h>
|
||||||
|
#include <libaegisub/log.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/// @class BSVideoProvider
|
||||||
|
/// @brief Implements video loading through BestSource.
|
||||||
|
class BSVideoProvider final : public VideoProvider {
|
||||||
|
std::map<std::string, std::string> bsopts;
|
||||||
|
BestVideoSource bs;
|
||||||
|
VideoProperties properties;
|
||||||
|
|
||||||
|
std::vector<int> Keyframes;
|
||||||
|
agi::vfr::Framerate Timecodes;
|
||||||
|
std::string colorspace;
|
||||||
|
bool has_audio = false;
|
||||||
|
|
||||||
|
public:
|
||||||
|
BSVideoProvider(agi::fs::path const& filename, std::string const& colormatrix, agi::BackgroundRunner *br);
|
||||||
|
|
||||||
|
void GetFrame(int n, VideoFrame &out) override;
|
||||||
|
|
||||||
|
void SetColorSpace(std::string const& matrix) override { } // TODO Follow Aegisub's colorspace forcing?
|
||||||
|
|
||||||
|
int GetFrameCount() const override { return properties.NumFrames; };
|
||||||
|
|
||||||
|
int GetWidth() const override { return properties.Width; };
|
||||||
|
int GetHeight() const override { return properties.Height; };
|
||||||
|
double GetDAR() const override { return ((double) properties.Width * properties.SAR.Num) / (properties.Height * properties.SAR.Den); };
|
||||||
|
|
||||||
|
agi::vfr::Framerate GetFPS() const override { return Timecodes; };
|
||||||
|
std::string GetColorSpace() const override { return colorspace; };
|
||||||
|
std::string GetRealColorSpace() const override { return colorspace; };
|
||||||
|
std::vector<int> GetKeyFrames() const override { return Keyframes; };
|
||||||
|
std::string GetDecoderName() const override { return "BestSource"; };
|
||||||
|
bool WantsCaching() const override { return false; };
|
||||||
|
bool HasAudio() const override { return has_audio; };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Match the logic from the ffms2 provider, but directly use libavutil's constants and don't abort when encountering an unknown color space
|
||||||
|
std::string colormatrix_description(const AVFrame *frame) {
|
||||||
|
// Assuming TV for unspecified
|
||||||
|
std::string str = frame->color_range == AVCOL_RANGE_JPEG ? "PC" : "TV";
|
||||||
|
LOG_D("bestsource") << frame->colorspace;
|
||||||
|
|
||||||
|
switch (frame->colorspace) {
|
||||||
|
case AVCOL_SPC_BT709:
|
||||||
|
return str + ".709";
|
||||||
|
case AVCOL_SPC_FCC:
|
||||||
|
return str + ".FCC";
|
||||||
|
case AVCOL_SPC_BT470BG:
|
||||||
|
case AVCOL_SPC_SMPTE170M:
|
||||||
|
return str + ".601";
|
||||||
|
case AVCOL_SPC_SMPTE240M:
|
||||||
|
return str + ".240M";
|
||||||
|
default:
|
||||||
|
return "None";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BSVideoProvider::BSVideoProvider(agi::fs::path const& filename, std::string const& colormatrix, agi::BackgroundRunner *br) try
|
||||||
|
: bsopts()
|
||||||
|
, bs(filename.string(), "", -1, false, OPT_GET("Provider/Video/BestSource/Threads")->GetInt(), GetBSCacheFile(filename), &bsopts)
|
||||||
|
{
|
||||||
|
bs.SetMaxCacheSize(OPT_GET("Provider/Video/BestSource/Max Cache Size")->GetInt() << 20);
|
||||||
|
bs.SetSeekPreRoll(OPT_GET("Provider/Video/BestSource/Seek Preroll")->GetInt());
|
||||||
|
try {
|
||||||
|
BestAudioSource dummysource(filename.string(), -1, 0, "");
|
||||||
|
has_audio = true;
|
||||||
|
} catch (AudioException const& err) {
|
||||||
|
has_audio = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
properties = bs.GetVideoProperties();
|
||||||
|
|
||||||
|
if (properties.NumFrames == -1) {
|
||||||
|
LOG_D("bs") << "File not cached or varying samples, creating cache.";
|
||||||
|
br->Run([&](agi::ProgressSink *ps) {
|
||||||
|
ps->SetTitle(from_wx(_("Exacting")));
|
||||||
|
ps->SetMessage(from_wx(_("Creating cache... This can take a while!")));
|
||||||
|
ps->SetIndeterminate();
|
||||||
|
if (bs.GetExactDuration()) {
|
||||||
|
LOG_D("bs") << "File cached and has exact samples.";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
properties = bs.GetVideoProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
br->Run([&](agi::ProgressSink *ps) {
|
||||||
|
ps->SetTitle(from_wx(_("Scanning")));
|
||||||
|
ps->SetMessage(from_wx(_("Finding Keyframes and Timecodes...")));
|
||||||
|
|
||||||
|
std::vector<int> TimecodesVector;
|
||||||
|
for (int n = 0; n < properties.NumFrames; n++) {
|
||||||
|
if (ps->IsCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::unique_ptr<BestVideoFrame> frame(bs.GetFrame(n));
|
||||||
|
if (frame == nullptr) {
|
||||||
|
throw VideoOpenError("Couldn't read frame!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame->GetAVFrame()->key_frame) {
|
||||||
|
Keyframes.push_back(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
TimecodesVector.push_back((int) frame->GetAVFrame()->pts);
|
||||||
|
ps->SetProgress(n, properties.NumFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TimecodesVector.size() < 2 || TimecodesVector.front() == TimecodesVector.back()) {
|
||||||
|
Timecodes = (double) properties.FPS.Num / properties.FPS.Den;
|
||||||
|
} else {
|
||||||
|
Timecodes = agi::vfr::Framerate(TimecodesVector);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Decode the first frame to get the color space
|
||||||
|
std::unique_ptr<BestVideoFrame> frame(bs.GetFrame(0));
|
||||||
|
colorspace = colormatrix_description(frame->GetAVFrame());
|
||||||
|
}
|
||||||
|
catch (VideoException const& err) {
|
||||||
|
throw VideoOpenError("Failed to create BestVideoSource");
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSVideoProvider::GetFrame(int n, VideoFrame &out) {
|
||||||
|
std::unique_ptr<BestVideoFrame> bsframe(bs.GetFrame(n));
|
||||||
|
if (bsframe == nullptr) {
|
||||||
|
throw VideoDecodeError("Couldn't read frame!");
|
||||||
|
}
|
||||||
|
const AVFrame *frame = bsframe->GetAVFrame();
|
||||||
|
|
||||||
|
SwsContext *context = sws_getContext(
|
||||||
|
frame->width, frame->height, (AVPixelFormat) frame->format, // TODO figure out aegi's color space forcing.
|
||||||
|
frame->width, frame->height, AV_PIX_FMT_BGR0,
|
||||||
|
SWS_BICUBIC, nullptr, nullptr, nullptr);
|
||||||
|
|
||||||
|
if (context == nullptr) {
|
||||||
|
throw VideoDecodeError("Couldn't convert frame!");
|
||||||
|
}
|
||||||
|
|
||||||
|
out.data.resize(frame->width * frame->height * 4);
|
||||||
|
uint8_t *data[1] = {&out.data[0]};
|
||||||
|
int stride[1] = {frame->width * 4};
|
||||||
|
sws_scale(context, frame->data, frame->linesize, 0, frame->height, data, stride);
|
||||||
|
|
||||||
|
out.width = frame->width;
|
||||||
|
out.height = frame->height;
|
||||||
|
out.pitch = stride[0];
|
||||||
|
out.flipped = false; // TODO figure out flipped
|
||||||
|
|
||||||
|
sws_freeContext(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<VideoProvider> CreateBSVideoProvider(agi::fs::path const& path, std::string const& colormatrix, agi::BackgroundRunner *br) {
|
||||||
|
return agi::make_unique<BSVideoProvider>(path, colormatrix, br);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* WITH_BESTSOURCE */
|
|
@ -29,6 +29,8 @@ std::unique_ptr<VideoProvider> CreateDummyVideoProvider(agi::fs::path const&, st
|
||||||
std::unique_ptr<VideoProvider> CreateYUV4MPEGVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *);
|
std::unique_ptr<VideoProvider> CreateYUV4MPEGVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *);
|
||||||
std::unique_ptr<VideoProvider> CreateFFmpegSourceVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *);
|
std::unique_ptr<VideoProvider> CreateFFmpegSourceVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *);
|
||||||
std::unique_ptr<VideoProvider> CreateAvisynthVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *);
|
std::unique_ptr<VideoProvider> CreateAvisynthVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *);
|
||||||
|
std::unique_ptr<VideoProvider> CreateBSVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *);
|
||||||
|
std::unique_ptr<VideoProvider> CreateVapoursynthVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *);
|
||||||
|
|
||||||
std::unique_ptr<VideoProvider> CreateCacheVideoProvider(std::unique_ptr<VideoProvider>);
|
std::unique_ptr<VideoProvider> CreateCacheVideoProvider(std::unique_ptr<VideoProvider>);
|
||||||
|
|
||||||
|
@ -47,6 +49,12 @@ namespace {
|
||||||
#endif
|
#endif
|
||||||
#ifdef WITH_AVISYNTH
|
#ifdef WITH_AVISYNTH
|
||||||
{"Avisynth", CreateAvisynthVideoProvider, false},
|
{"Avisynth", CreateAvisynthVideoProvider, false},
|
||||||
|
#endif
|
||||||
|
#ifdef WITH_BESTSOURCE
|
||||||
|
{"BestSource", CreateBSVideoProvider, false},
|
||||||
|
#endif
|
||||||
|
#ifdef WITH_VAPOURSYNTH
|
||||||
|
{"Vapoursynth", CreateVapoursynthVideoProvider, false},
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
282
src/video_provider_vs.cpp
Normal file
282
src/video_provider_vs.cpp
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
// Copyright (c) 2022, arch1t3cht <arch1t3cht@gmail.com>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
//
|
||||||
|
// Aegisub Project http://www.aegisub.org/
|
||||||
|
|
||||||
|
#ifdef WITH_VAPOURSYNTH
|
||||||
|
#include "include/aegisub/video_provider.h"
|
||||||
|
|
||||||
|
#include "options.h"
|
||||||
|
#include "video_frame.h"
|
||||||
|
|
||||||
|
#include <libaegisub/access.h>
|
||||||
|
#include <libaegisub/format.h>
|
||||||
|
#include <libaegisub/fs.h>
|
||||||
|
#include <libaegisub/path.h>
|
||||||
|
#include <libaegisub/make_unique.h>
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include "vapoursynth_wrap.h"
|
||||||
|
#include "VSScript4.h"
|
||||||
|
#include "VSHelper4.h"
|
||||||
|
#include "VSConstants4.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
class VapoursynthVideoProvider: public VideoProvider {
|
||||||
|
VapourSynthWrapper vs;
|
||||||
|
VSScript *script = nullptr;
|
||||||
|
VSNode *node = nullptr;
|
||||||
|
const VSVideoInfo *vi = nullptr;
|
||||||
|
|
||||||
|
double dar = 0;
|
||||||
|
agi::vfr::Framerate fps;
|
||||||
|
std::vector<int> keyframes;
|
||||||
|
std::string colorspace;
|
||||||
|
std::string real_colorspace;
|
||||||
|
|
||||||
|
const VSFrame *GetVSFrame(int n);
|
||||||
|
void SetResizeArg(VSMap *args, const VSMap *props, const char *arg_name, const char *prop_name, int deflt, int unspecified = -1);
|
||||||
|
|
||||||
|
public:
|
||||||
|
VapoursynthVideoProvider(agi::fs::path const& filename, std::string const& colormatrix);
|
||||||
|
~VapoursynthVideoProvider();
|
||||||
|
|
||||||
|
void GetFrame(int n, VideoFrame &frame) override;
|
||||||
|
|
||||||
|
void SetColorSpace(std::string const& matrix) override { }
|
||||||
|
|
||||||
|
int GetFrameCount() const override { return vi->numFrames; }
|
||||||
|
agi::vfr::Framerate GetFPS() const override { return fps; }
|
||||||
|
int GetWidth() const override { return vi->width; }
|
||||||
|
int GetHeight() const override { return vi->height; }
|
||||||
|
double GetDAR() const override { return dar; }
|
||||||
|
std::vector<int> GetKeyFrames() const override { return keyframes; }
|
||||||
|
std::string GetColorSpace() const override { return colorspace; }
|
||||||
|
std::string GetRealColorSpace() const override { return colorspace; }
|
||||||
|
bool HasAudio() const override { return false; }
|
||||||
|
virtual bool WantsCaching() const override { return true; }
|
||||||
|
virtual std::string GetDecoderName() const override { return "VapourSynth"; }
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string colormatrix_description(int colorFamily, int colorRange, int matrix) {
|
||||||
|
if (colorFamily != cfYUV) {
|
||||||
|
return "None";
|
||||||
|
}
|
||||||
|
// Assuming TV for unspecified
|
||||||
|
std::string str = colorRange == VSC_RANGE_FULL ? "PC" : "TV";
|
||||||
|
|
||||||
|
switch (matrix) {
|
||||||
|
case VSC_MATRIX_RGB:
|
||||||
|
return "None";
|
||||||
|
case VSC_MATRIX_BT709:
|
||||||
|
return str + ".709";
|
||||||
|
case VSC_MATRIX_FCC:
|
||||||
|
return str + ".FCC";
|
||||||
|
case VSC_MATRIX_BT470_BG:
|
||||||
|
case VSC_MATRIX_ST170_M:
|
||||||
|
return str + ".601";
|
||||||
|
case VSC_MATRIX_ST240_M:
|
||||||
|
return str + ".240M";
|
||||||
|
default:
|
||||||
|
return "None";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds an argument to the rescaler if the corresponding frameprop does not exist or is set as unspecified
|
||||||
|
void VapoursynthVideoProvider::SetResizeArg(VSMap *args, const VSMap *props, const char *arg_name, const char *prop_name, int deflt, int unspecified) {
|
||||||
|
int err;
|
||||||
|
int result = vs.GetAPI()->mapGetInt(props, prop_name, 0, &err);
|
||||||
|
if (err != 0 || result == unspecified) {
|
||||||
|
result = deflt;
|
||||||
|
vs.GetAPI()->mapSetInt(args, arg_name, result, maAppend);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename, std::string const& colormatrix) try {
|
||||||
|
agi::acs::CheckFileRead(filename);
|
||||||
|
std::lock_guard<std::mutex> lock(vs.GetMutex());
|
||||||
|
|
||||||
|
script = vs.GetScriptAPI()->createScript(nullptr);
|
||||||
|
if (script == nullptr) {
|
||||||
|
throw VapoursynthError("Error creating script API");
|
||||||
|
}
|
||||||
|
vs.GetScriptAPI()->evalSetWorkingDir(script, 1);
|
||||||
|
if (vs.GetScriptAPI()->evaluateFile(script, filename.string().c_str())) {
|
||||||
|
std::string msg = agi::format("Error executing VapourSynth script: %s", vs.GetScriptAPI()->getError(script));
|
||||||
|
vs.GetScriptAPI()->freeScript(script);
|
||||||
|
throw VapoursynthError(msg);
|
||||||
|
}
|
||||||
|
node = vs.GetScriptAPI()->getOutputNode(script, 0);
|
||||||
|
if (node == nullptr) {
|
||||||
|
vs.GetScriptAPI()->freeScript(script);
|
||||||
|
throw VapoursynthError("No output node set");
|
||||||
|
}
|
||||||
|
if (vs.GetAPI()->getNodeType(node) != mtVideo) {
|
||||||
|
vs.GetAPI()->freeNode(node);
|
||||||
|
vs.GetScriptAPI()->freeScript(script);
|
||||||
|
throw VapoursynthError("Output node isn't a video node");
|
||||||
|
}
|
||||||
|
vi = vs.GetAPI()->getVideoInfo(node);
|
||||||
|
if (!vsh::isConstantVideoFormat(vi)) {
|
||||||
|
vs.GetAPI()->freeNode(node);
|
||||||
|
vs.GetScriptAPI()->freeScript(script);
|
||||||
|
throw VapoursynthError("Video doesn't have constant format");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume constant frame rate, since handling VFR would require going through all frames when loading.
|
||||||
|
// Users can load custom timecodes files to deal with VFR.
|
||||||
|
// Alternatively (TODO) the provider could read timecodes and keyframes from a second output node.
|
||||||
|
fps = (double) vi->fpsNum / vi->fpsDen;
|
||||||
|
|
||||||
|
// Find the first frame to get some info
|
||||||
|
const VSFrame *frame;
|
||||||
|
try {
|
||||||
|
frame = GetVSFrame(0);
|
||||||
|
} catch (VapoursynthError const& err) {
|
||||||
|
vs.GetAPI()->freeNode(node);
|
||||||
|
vs.GetScriptAPI()->freeScript(script);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
int err1, err2;
|
||||||
|
const VSMap *props = vs.GetAPI()->getFramePropertiesRO(frame);
|
||||||
|
int sarn = vs.GetAPI()->mapGetInt(props, "_SARNum", 0, &err1);
|
||||||
|
int sard = vs.GetAPI()->mapGetInt(props, "_SARDen", 0, &err2);
|
||||||
|
if (!err1 && !err2) {
|
||||||
|
dar = ((double) vi->width * sarn) / (vi->height * sard);
|
||||||
|
}
|
||||||
|
|
||||||
|
int range = vs.GetAPI()->mapGetInt(props, "_ColorRange", 0, &err1);
|
||||||
|
int matrix = vs.GetAPI()->mapGetInt(props, "_Matrix", 0, &err2);
|
||||||
|
colorspace = colormatrix_description(vi->format.colorFamily, err1 == 0 ? range : -1, err2 == 0 ? matrix : -1);
|
||||||
|
|
||||||
|
vs.GetAPI()->freeFrame(frame);
|
||||||
|
|
||||||
|
if (vi->format.colorFamily != cfRGB || vi->format.bitsPerSample != 8) {
|
||||||
|
// Convert to RGB24 format
|
||||||
|
VSPlugin *resize = vs.GetAPI()->getPluginByID(VSH_RESIZE_PLUGIN_ID, vs.GetScriptAPI()->getCore(script));
|
||||||
|
if (resize == nullptr) {
|
||||||
|
throw VapoursynthError("Couldn't find resize plugin");
|
||||||
|
}
|
||||||
|
VSMap *args = vs.GetAPI()->createMap();
|
||||||
|
if (args == nullptr) {
|
||||||
|
throw VapoursynthError("Failed to create argument map");
|
||||||
|
}
|
||||||
|
|
||||||
|
vs.GetAPI()->mapSetNode(args, "clip", node, maAppend);
|
||||||
|
vs.GetAPI()->mapSetInt(args, "format", pfRGB24, maAppend);
|
||||||
|
if (vi->format.colorFamily != cfGray)
|
||||||
|
SetResizeArg(args, props, "matrix_in", "_Matrix", VSC_MATRIX_BT709, VSC_MATRIX_UNSPECIFIED);
|
||||||
|
SetResizeArg(args, props, "transfer_in", "_Transfer", VSC_TRANSFER_BT709, VSC_TRANSFER_UNSPECIFIED);
|
||||||
|
SetResizeArg(args, props, "primaries_in", "_Primaries", VSC_PRIMARIES_BT709, VSC_PRIMARIES_UNSPECIFIED);
|
||||||
|
SetResizeArg(args, props, "range_in", "_ColorRange", VSC_RANGE_LIMITED);
|
||||||
|
SetResizeArg(args, props, "chromaloc_in", "_ChromaLocation", VSC_CHROMA_LEFT);
|
||||||
|
|
||||||
|
VSMap *result = vs.GetAPI()->invoke(resize, "Bicubic", args);
|
||||||
|
vs.GetAPI()->freeMap(args);
|
||||||
|
const char *error = vs.GetAPI()->mapGetError(result);
|
||||||
|
if (error) {
|
||||||
|
vs.GetAPI()->freeMap(result);
|
||||||
|
vs.GetAPI()->freeNode(node);
|
||||||
|
vs.GetScriptAPI()->freeScript(script);
|
||||||
|
throw VideoProviderError(agi::format("Failed to convert to RGB24: %s", error));
|
||||||
|
}
|
||||||
|
int err;
|
||||||
|
vs.GetAPI()->freeNode(node);
|
||||||
|
node = vs.GetAPI()->mapGetNode(result, "clip", 0, &err);
|
||||||
|
vs.GetAPI()->freeMap(result);
|
||||||
|
if (err) {
|
||||||
|
vs.GetScriptAPI()->freeScript(script);
|
||||||
|
throw VideoProviderError("Failed to get resize output node");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, try to get the first frame again, so if the filter does crash, it happens before loading finishes
|
||||||
|
const VSFrame *rgbframe;
|
||||||
|
try {
|
||||||
|
rgbframe = GetVSFrame(0);
|
||||||
|
} catch (VapoursynthError const& err) {
|
||||||
|
vs.GetAPI()->freeNode(node);
|
||||||
|
vs.GetScriptAPI()->freeScript(script);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
vs.GetAPI()->freeFrame(rgbframe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (VapoursynthError const& err) {
|
||||||
|
throw VideoProviderError(agi::format("Vapoursynth error: %s", err.GetMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const VSFrame *VapoursynthVideoProvider::GetVSFrame(int n) {
|
||||||
|
char errorMsg[1024];
|
||||||
|
const VSFrame *frame = vs.GetAPI()->getFrame(n, node, errorMsg, sizeof(errorMsg));
|
||||||
|
if (frame == nullptr) {
|
||||||
|
throw VapoursynthError(agi::format("Error getting frame: %s", errorMsg));
|
||||||
|
}
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VapoursynthVideoProvider::GetFrame(int n, VideoFrame &out) {
|
||||||
|
std::lock_guard<std::mutex> lock(vs.GetMutex());
|
||||||
|
|
||||||
|
const VSFrame *frame = GetVSFrame(n);
|
||||||
|
|
||||||
|
const VSVideoFormat *format = vs.GetAPI()->getVideoFrameFormat(frame);
|
||||||
|
if (format->colorFamily != cfRGB || format->numPlanes != 3 || format->bitsPerSample != 8 || format->subSamplingH != 0 || format->subSamplingW != 0) {
|
||||||
|
throw VapoursynthError("Frame not in RGB24 format");
|
||||||
|
}
|
||||||
|
|
||||||
|
out.width = vs.GetAPI()->getFrameWidth(frame, 0);
|
||||||
|
out.height = vs.GetAPI()->getFrameHeight(frame, 0);
|
||||||
|
out.pitch = out.width * 4;
|
||||||
|
out.flipped = false;
|
||||||
|
|
||||||
|
out.data.resize(out.pitch * out.height);
|
||||||
|
|
||||||
|
for (int p = 0; p < format->numPlanes; p++) {
|
||||||
|
ptrdiff_t stride = vs.GetAPI()->getStride(frame, p);
|
||||||
|
const uint8_t *readPtr = vs.GetAPI()->getReadPtr(frame, p);
|
||||||
|
uint8_t *writePtr = &out.data[2 - p];
|
||||||
|
int rows = vs.GetAPI()->getFrameHeight(frame, p);
|
||||||
|
int cols = vs.GetAPI()->getFrameWidth(frame, p);
|
||||||
|
|
||||||
|
for (int row = 0; row < rows; row++) {
|
||||||
|
const uint8_t *rowPtr = readPtr;
|
||||||
|
uint8_t *rowWritePtr = writePtr;
|
||||||
|
for (int col = 0; col < cols; col++) {
|
||||||
|
*rowWritePtr = *rowPtr++;
|
||||||
|
rowWritePtr += 4;
|
||||||
|
}
|
||||||
|
readPtr += stride;
|
||||||
|
writePtr += out.pitch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vs.GetAPI()->freeFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
VapoursynthVideoProvider::~VapoursynthVideoProvider() {
|
||||||
|
if (node != nullptr) {
|
||||||
|
vs.GetAPI()->freeNode(node);
|
||||||
|
}
|
||||||
|
if (script != nullptr) {
|
||||||
|
vs.GetScriptAPI()->freeScript(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace agi { class BackgroundRunner; }
|
||||||
|
std::unique_ptr<VideoProvider> CreateVapoursynthVideoProvider(agi::fs::path const& path, std::string const& colormatrix, agi::BackgroundRunner *) {
|
||||||
|
return agi::make_unique<VapoursynthVideoProvider>(path, colormatrix);
|
||||||
|
}
|
||||||
|
#endif // HAVE_VAPOURSYNTH
|
7
subprojects/bestsource.wrap
Normal file
7
subprojects/bestsource.wrap
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
[wrap-git]
|
||||||
|
url = https://github.com/vapoursynth/bestsource
|
||||||
|
revision = head
|
||||||
|
patch_directory = bestsource
|
||||||
|
|
||||||
|
[provide]
|
||||||
|
bestsource = bestsource_dep
|
4
subprojects/jansson.wrap
Normal file
4
subprojects/jansson.wrap
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[wrap-git]
|
||||||
|
directory = jansson
|
||||||
|
url = https://github.com/akheron/jansson.git
|
||||||
|
revision = v2.14
|
156
subprojects/packagefiles/bestsource/libp2p/p2p_api.h
Normal file
156
subprojects/packagefiles/bestsource/libp2p/p2p_api.h
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
// Since we don't use ExportAsPlanar, we don't actually need libp2p.
|
||||||
|
// So instead of adding another wrap and meson build file, and *also*
|
||||||
|
// patching the include statement in videosource.cpp, we throw a dummy
|
||||||
|
// header file in the folder that should contain the checkout of libp2p.
|
||||||
|
|
||||||
|
#ifndef P2P_API_H_
|
||||||
|
#define P2P_API_H_
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notation: [Xa-Ya-Za]
|
||||||
|
*
|
||||||
|
* [] denotes a machine word of the specified endianness. Xa-Ya-Za denote
|
||||||
|
* component X, Y, and Z packed in the word, with bit depths a, b, c, in order
|
||||||
|
* from MSB to LSB. Padding bits are represented by the component '!'.
|
||||||
|
*/
|
||||||
|
enum p2p_packing {
|
||||||
|
/** [R8-G8-B8] */
|
||||||
|
p2p_rgb24_be, /* RGB */
|
||||||
|
p2p_rgb24_le, /* BGR */
|
||||||
|
p2p_rgb24,
|
||||||
|
/** [A8-R8-G8-B8] */
|
||||||
|
p2p_argb32_be, /* ARGB */
|
||||||
|
p2p_argb32_le, /* BGRA */
|
||||||
|
p2p_argb32,
|
||||||
|
/** [A8-Y8-U8-V8] */
|
||||||
|
p2p_ayuv_be, /* AYUV */
|
||||||
|
p2p_ayuv_le, /* VUYA */
|
||||||
|
p2p_ayuv,
|
||||||
|
/** [R16-G16-B16] */
|
||||||
|
p2p_rgb48_be, /* RGB, big-endian components */
|
||||||
|
p2p_rgb48_le, /* BGR, little-endian components */
|
||||||
|
p2p_rgb48,
|
||||||
|
/** [A16-R16-G16-B16] */
|
||||||
|
p2p_argb64_be, /* ARGB big-endian components */
|
||||||
|
p2p_argb64_le, /* BGRA little-endian components */
|
||||||
|
p2p_argb64,
|
||||||
|
/** [A2-R10-G10-B10] */
|
||||||
|
p2p_rgb30_be, /* ARGB packed in big-endian DWORD */
|
||||||
|
p2p_rgb30_le, /* ARGB packed in little-endian DWORD */
|
||||||
|
p2p_rgb30,
|
||||||
|
/** [A2-V10-Y10-U10] */
|
||||||
|
p2p_y410_be, /* AVYU packed in big-endian DWORD */
|
||||||
|
p2p_y410_le, /* AVYU packed in little-endian DWORD */
|
||||||
|
p2p_y410,
|
||||||
|
/** [A16-V16-Y16-U16] */
|
||||||
|
p2p_y416_be, /* AVYU, big-endian components */
|
||||||
|
p2p_y416_le, /* UYVA, little-endian components */
|
||||||
|
p2p_y416,
|
||||||
|
/** [Y8] [U8] [Y8] [V8] */
|
||||||
|
p2p_yuy2,
|
||||||
|
/** [U8] [Y8] [V8] [Y8] */
|
||||||
|
p2p_uyvy,
|
||||||
|
/** [Y10-!6] [U10-!6] [Y10-!6] [V10-!6] */
|
||||||
|
p2p_y210_be, /* YUYV, big-endian components, lower 6 bits zero */
|
||||||
|
p2p_y210_le, /* YUYV, little-endian components, lower 6 bits zero. Microsoft Y210. */
|
||||||
|
p2p_y210,
|
||||||
|
/** [Y16] [U16] [Y16] [V16] */
|
||||||
|
p2p_y216_be, /* YUYV, big-endian components */
|
||||||
|
p2p_y216_le, /* YUYV, little-endian components. Microsoft Y216. */
|
||||||
|
p2p_y216,
|
||||||
|
/** [!2-V10-Y10-U10] [!2-Y10-U10-Y10] [!2-U10-Y10-V10] [!2-Y10-V10-Y10] */
|
||||||
|
p2p_v210_be, /* v210 with big-endian DWORDs */
|
||||||
|
p2p_v210_le, /* Apple/QuickTime v210 */
|
||||||
|
p2p_v210,
|
||||||
|
/** [U16] [Y16] [V16] [Y16] */
|
||||||
|
p2p_v216_be, /* UYVY, big-endian components */
|
||||||
|
p2p_v216_le, /* UYVY, little-endian components. Apple/QuickTime v216. */
|
||||||
|
p2p_v216,
|
||||||
|
/** [U8-V8] */
|
||||||
|
p2p_nv12_be, /* aka NV21, V first */
|
||||||
|
p2p_nv12_le, /* NV12 */
|
||||||
|
p2p_nv12,
|
||||||
|
/** [U10-!6-V10-!6] */
|
||||||
|
p2p_p010_be, /* NV21, big-endian components, lower 6 bits zero */
|
||||||
|
p2p_p010_le, /* NV12, little-endian components, lower 6 bits zero. Microsoft P010. */
|
||||||
|
p2p_p010,
|
||||||
|
/** [U16-V16] */
|
||||||
|
p2p_p016_be, /* NV21, big-endian components */
|
||||||
|
p2p_p016_le, /* NV12, little-endian components. Microsoft P016. */
|
||||||
|
p2p_p016,
|
||||||
|
/** [U10-!6-V10-!6] */
|
||||||
|
p2p_p210_be, /* NV21, big-endian components, lower 6 bits zero */
|
||||||
|
p2p_p210_le, /* NV12, little-endian components, lower 6 bits zero. Microsoft P210. */
|
||||||
|
p2p_p210,
|
||||||
|
/** [U16-V16] */
|
||||||
|
p2p_p216_be, /* NV21, big-endian components */
|
||||||
|
p2p_p216_le, /* NV12, little-endian components. Microsoft P216. */
|
||||||
|
p2p_p216,
|
||||||
|
/** [R8-G8-B8-A8] */
|
||||||
|
p2p_rgba32_be, /* RGBA */
|
||||||
|
p2p_rgba32_le, /* ABGR */
|
||||||
|
p2p_rgba32,
|
||||||
|
/** [R16-G16-B16-A16] */
|
||||||
|
p2p_rgba64_be, /* RGBA, big-endian components */
|
||||||
|
p2p_rgba64_le, /* ABGR, little-endian components */
|
||||||
|
p2p_rgba64,
|
||||||
|
/** [A16-B16-G16-R16] */
|
||||||
|
p2p_abgr64_be, /* ABGR, big-endian components */
|
||||||
|
p2p_abgr64_le, /* RGBA, little-endian components */
|
||||||
|
p2p_abgr64,
|
||||||
|
/** [B16-G16-R16] */
|
||||||
|
p2p_bgr48_be, /* BGR, big-endian components */
|
||||||
|
p2p_bgr48_le, /* RGB, little-endian components */
|
||||||
|
p2p_bgr48,
|
||||||
|
/** [B16-G16-R16-A16] */
|
||||||
|
p2p_bgra64_be, /* BGRA, big-endian components */
|
||||||
|
p2p_bgra64_le, /* ARGB, little-endian components */
|
||||||
|
p2p_bgra64,
|
||||||
|
|
||||||
|
p2p_packing_max,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct p2p_buffer_param {
|
||||||
|
/**
|
||||||
|
* Planar order: R-G-B-A or Y-U-V-A. Alpha is optional.
|
||||||
|
* Packed order: Y-UV if NV12/21, else single plane. Y optional for NV12/21.
|
||||||
|
*/
|
||||||
|
const void *src[4];
|
||||||
|
void *dst[4];
|
||||||
|
ptrdiff_t src_stride[4];
|
||||||
|
ptrdiff_t dst_stride[4];
|
||||||
|
unsigned width;
|
||||||
|
unsigned height;
|
||||||
|
enum p2p_packing packing;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Pack/unpack a range of pixels from a scanline. */
|
||||||
|
typedef void (*p2p_unpack_func)(const void *src, void * const dst[4], unsigned left, unsigned right);
|
||||||
|
typedef void (*p2p_pack_func)(const void * const src[4], void *dst, unsigned left, unsigned right);
|
||||||
|
|
||||||
|
/** Select a line pack/unpack function. */
|
||||||
|
p2p_unpack_func p2p_select_unpack_func(enum p2p_packing packing);
|
||||||
|
p2p_pack_func p2p_select_pack_func(enum p2p_packing packing);
|
||||||
|
p2p_pack_func p2p_select_pack_func_ex(enum p2p_packing packing, int alpha_one_fill);
|
||||||
|
|
||||||
|
|
||||||
|
/** When processing formats like NV12, ignore the unpacked plane. */
|
||||||
|
#define P2P_SKIP_UNPACKED_PLANES (1UL << 0)
|
||||||
|
/** When packing, store a bit pattern of all ones in the alpha channel instead of all zeros. */
|
||||||
|
#define P2P_ALPHA_SET_ONE (1UL << 1)
|
||||||
|
|
||||||
|
/** Helper function to pack/unpack between memory locations. */
|
||||||
|
void p2p_unpack_frame(const struct p2p_buffer_param *param, unsigned long flags) {};
|
||||||
|
void p2p_pack_frame(const struct p2p_buffer_param *param, unsigned long flags) {};
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
} /* extern "C" */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* P2P_API_H_ */
|
36
subprojects/packagefiles/bestsource/meson.build
Normal file
36
subprojects/packagefiles/bestsource/meson.build
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
project('BestSource', 'cpp',
|
||||||
|
default_options: ['buildtype=release', 'b_ndebug=if-release', 'cpp_std=c++14'],
|
||||||
|
meson_version: '>=0.48.0'
|
||||||
|
)
|
||||||
|
|
||||||
|
cmake = import('cmake')
|
||||||
|
|
||||||
|
sources = [
|
||||||
|
'src/audiosource.cpp',
|
||||||
|
'src/videosource.cpp',
|
||||||
|
'src/SrcAttribCache.cpp',
|
||||||
|
'src/BSRational.cpp',
|
||||||
|
]
|
||||||
|
|
||||||
|
deps = [
|
||||||
|
dependency('libavcodec'),
|
||||||
|
dependency('libavformat'),
|
||||||
|
dependency('libavutil'),
|
||||||
|
]
|
||||||
|
|
||||||
|
jansson_dep = dependency('jansson', version: '>= 2.7', required: false)
|
||||||
|
|
||||||
|
if jansson_dep.found()
|
||||||
|
deps += jansson_dep
|
||||||
|
else
|
||||||
|
jansson = cmake.subproject('jansson')
|
||||||
|
deps += jansson.dependency('jansson')
|
||||||
|
endif
|
||||||
|
|
||||||
|
bs_lib = static_library('bestsource', sources,
|
||||||
|
dependencies: deps,
|
||||||
|
gnu_symbol_visibility: 'hidden'
|
||||||
|
)
|
||||||
|
|
||||||
|
bestsource_dep = declare_dependency(link_with: bs_lib, include_directories: include_directories('src'))
|
||||||
|
|
3
subprojects/packagefiles/vapoursynth/meson.build
Normal file
3
subprojects/packagefiles/vapoursynth/meson.build
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
project('vapoursynth', 'cpp')
|
||||||
|
|
||||||
|
vs_inc = include_directories('include')
|
4
subprojects/vapoursynth.wrap
Normal file
4
subprojects/vapoursynth.wrap
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[wrap-git]
|
||||||
|
url = https://github.com/Vapoursynth/vapoursynth.git
|
||||||
|
revision = R59
|
||||||
|
patch_directory = vapoursynth
|
Loading…
Reference in a new issue