diff --git a/.gitignore b/.gitignore index 7d31eeed4..41d7bf6cf 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ tools/repack-thes-dict.dSYM # Meson build*/ subprojects/avisynth +subprojects/bestsource/ subprojects/boost*/ subprojects/cairo* subprojects/ffmpeg @@ -30,6 +31,7 @@ subprojects/glib* subprojects/googletest-* subprojects/harfbuzz subprojects/icu +subprojects/jansson subprojects/libass subprojects/libffi* subprojects/libpng-* @@ -42,3 +44,4 @@ subprojects/zlib-* subprojects/dirent-* subprojects/hunspell-* subprojects/uchardet-* +subprojects/vapoursynth diff --git a/meson.build b/meson.build index 7ef4edb52..2e3aabcfe 100644 --- a/meson.build +++ b/meson.build @@ -224,6 +224,24 @@ foreach dep: [ endif 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() conf.set('WITH_AVISYNTH', 1) # bundled separately with installer dep_avail += 'AviSynth' @@ -241,6 +259,13 @@ if get_option('avisynth').enabled() 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() dsound_dep = cc.find_library('dsound', required: get_option('directsound')) winmm_dep = cc.find_library('winmm', required: get_option('directsound')) diff --git a/meson_options.txt b/meson_options.txt index 3bc0461cd..8be9ca584 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -7,6 +7,8 @@ option('default_audio_output', type: 'combo', choices: ['auto', 'ALSA', 'OpenAL' option('ffms2', type: 'feature', description: 'FFMS2 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('hunspell', type: 'feature', description: 'Hunspell spell checker') diff --git a/packages/win_installer/fragment_associations.iss b/packages/win_installer/fragment_associations.iss index c766011da..ec33599b9 100644 --- a/packages/win_installer/fragment_associations.iss +++ b/packages/win_installer/fragment_associations.iss @@ -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: ".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: ".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: ".h264"; 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\.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\.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\.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 diff --git a/src/audio_provider_bestsource.cpp b/src/audio_provider_bestsource.cpp new file mode 100644 index 000000000..5390788dc --- /dev/null +++ b/src/audio_provider_bestsource.cpp @@ -0,0 +1,89 @@ +// Copyright (c) 2022, arch1t3cht +// +// 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 + +#include "audiosource.h" + +#include "bestsource_common.h" +#include "compat.h" +#include "options.h" + +#include +#include +#include +#include + +#include + +namespace { +class BSAudioProvider final : public agi::AudioProvider { + std::map 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(bs).GetPackedAudio(reinterpret_cast(Buf), Start, Count); +} + +} + +std::unique_ptr CreateBSAudioProvider(agi::fs::path const& file, agi::BackgroundRunner *br) { + return agi::make_unique(file, br); +} + +#endif /* WITH_BESTSOURCE */ + diff --git a/src/audio_provider_factory.cpp b/src/audio_provider_factory.cpp index 887783644..71c34f66e 100644 --- a/src/audio_provider_factory.cpp +++ b/src/audio_provider_factory.cpp @@ -31,6 +31,8 @@ using namespace agi; std::unique_ptr CreateAvisynthAudioProvider(fs::path const& filename, BackgroundRunner *); std::unique_ptr CreateFFmpegSourceAudioProvider(fs::path const& filename, BackgroundRunner *); +std::unique_ptr CreateBSAudioProvider(fs::path const& filename, BackgroundRunner *); +std::unique_ptr CreateVapoursynthAudioProvider(fs::path const& filename, BackgroundRunner *); namespace { struct factory { @@ -48,6 +50,12 @@ const factory providers[] = { #ifdef WITH_AVISYNTH {"Avisynth", CreateAvisynthAudioProvider, false}, #endif +#ifdef WITH_BESTSOURCE + {"BestSource", CreateBSAudioProvider, false}, +#endif +#ifdef WITH_VAPOURSYNTH + {"Vapoursynth", CreateVapoursynthAudioProvider, false}, +#endif }; } diff --git a/src/audio_provider_vs.cpp b/src/audio_provider_vs.cpp new file mode 100644 index 000000000..02d469b51 --- /dev/null +++ b/src/audio_provider_vs.cpp @@ -0,0 +1,167 @@ +// Copyright (c) 2022, arch1t3cht +// +// 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 + +#include "audio_controller.h" +#include "options.h" +#include "utils.h" + +#include +#include +#include +#include +#include + +#include + +#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 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 +static void PackChannels(const uint8_t **Src, void *Dst, size_t Length, size_t Channels) { + T *D = reinterpret_cast(Dst); + for (size_t c = 0; c < Channels; c++) { + const T *S = reinterpret_cast(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 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(planes.data(), buf, count, channels); + else if (bytes_per_sample == 2) + PackChannels(planes.data(), buf, count, channels); + else if (bytes_per_sample == 4) + PackChannels(planes.data(), buf, count, channels); + else if (bytes_per_sample == 8) + PackChannels(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(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 CreateVapoursynthAudioProvider(agi::fs::path const& file, agi::BackgroundRunner *) { + return agi::make_unique(file); +} +#endif diff --git a/src/bestsource_common.cpp b/src/bestsource_common.cpp new file mode 100644 index 000000000..69d627015 --- /dev/null +++ b/src/bestsource_common.cpp @@ -0,0 +1,49 @@ +// Copyright (c) 2022, arch1t3cht +// +// 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 +#include + +#include +#include + + +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 diff --git a/src/bestsource_common.h b/src/bestsource_common.h new file mode 100644 index 000000000..b93ccc815 --- /dev/null +++ b/src/bestsource_common.h @@ -0,0 +1,28 @@ +// Copyright (c) 2022, arch1t3cht > +// +// 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 + +std::string GetBSCacheFile(agi::fs::path const& filename); + +#endif /* WITH_BESTSOURCE */ diff --git a/src/command/audio.cpp b/src/command/audio.cpp index 386866775..a160e8eee 100644 --- a/src/command/audio.cpp +++ b/src/command/audio.cpp @@ -80,7 +80,7 @@ struct audio_open final : public Command { STR_HELP("Open an audio file") 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|" + _("All Files") + " (*.*)|*.*"); auto filename = OpenFileSelector(_("Open Audio File"), "Path/Last/Audio", "", "", str, c->parent); diff --git a/src/command/video.cpp b/src/command/video.cpp index ae0dfe2ad..3dd58547a 100644 --- a/src/command/video.cpp +++ b/src/command/video.cpp @@ -581,7 +581,7 @@ struct video_open final : public Command { STR_HELP("Open a video file") 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") + " (*.*)|*.*"); auto filename = OpenFileSelector(_("Open video file"), "Path/Last/Video", "", "", str, c->parent); if (!filename.empty()) diff --git a/src/libresrc/default_config.json b/src/libresrc/default_config.json index b4dbe5952..31786e75b 100644 --- a/src/libresrc/default_config.json +++ b/src/libresrc/default_config.json @@ -336,6 +336,10 @@ "FFmpegSource" : { "Decode Error Handling" : "ignore", "Downmix" : false + }, + "BestSource": { + "Max Cache Size" : 100, + "Aegisub Cache" : true } }, "Avisynth" : { @@ -356,6 +360,11 @@ "FFmpegSource" : { "Decoding Threads" : -1, "Unsafe Seeking" : false + }, + "BestSource" : { + "Max Cache Size" : 1024, + "Threads" : 0, + "Seek Preroll" : 12 } } }, diff --git a/src/libresrc/osx/default_config.json b/src/libresrc/osx/default_config.json index 0160bb354..2eff38637 100644 --- a/src/libresrc/osx/default_config.json +++ b/src/libresrc/osx/default_config.json @@ -335,6 +335,10 @@ }, "FFmpegSource" : { "Decode Error Handling" : "ignore" + }, + "BestSource": { + "Max Cache Size" : 100, + "Aegisub Cache" : true } }, "Avisynth" : { @@ -355,6 +359,11 @@ "FFmpegSource" : { "Decoding Threads" : -1, "Unsafe Seeking" : false + }, + "BestSource" : { + "Max Cache Size" : 1024, + "Threads" : 0, + "Seek Preroll" : 12 } } }, diff --git a/src/meson.build b/src/meson.build index 8e7e562c7..22999a2c3 100644 --- a/src/meson.build +++ b/src/meson.build @@ -237,6 +237,12 @@ opt_src = [ ['FFMS2', ['audio_provider_ffmpegsource.cpp', 'video_provider_ffmpegsource.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', 'audio_provider_avs.cpp', diff --git a/src/preferences.cpp b/src/preferences.cpp index 4f9532ee6..cf337dc2a 100644 --- a/src/preferences.cpp +++ b/src/preferences.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."); #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 auto portaudio = p->PageSizer("Portaudio"); 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"); #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); } diff --git a/src/vapoursynth_wrap.cpp b/src/vapoursynth_wrap.cpp new file mode 100644 index 000000000..d0d0fbd8b --- /dev/null +++ b/src/vapoursynth_wrap.cpp @@ -0,0 +1,105 @@ +// Copyright (c) 2022, arch1t3cht +// +// 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 + +#ifndef _WIN32 +#include +#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 diff --git a/src/vapoursynth_wrap.h b/src/vapoursynth_wrap.h new file mode 100644 index 000000000..802cb3d58 --- /dev/null +++ b/src/vapoursynth_wrap.h @@ -0,0 +1,42 @@ +// Copyright (c) 2022, arch1t3cht +// +// 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 + +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 diff --git a/src/video_frame.h b/src/video_frame.h index 2a47ed69c..2f4f21e90 100644 --- a/src/video_frame.h +++ b/src/video_frame.h @@ -14,6 +14,8 @@ // // Aegisub Project http://www.aegisub.org/ +#pragma once + #include class wxImage; diff --git a/src/video_provider_bestsource.cpp b/src/video_provider_bestsource.cpp new file mode 100644 index 000000000..3b5e0834c --- /dev/null +++ b/src/video_provider_bestsource.cpp @@ -0,0 +1,204 @@ +// Copyright (c) 2022, arch1t3cht +// +// 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 +#include +#include +} + +#include "bestsource_common.h" +#include "options.h" +#include "compat.h" +#include "video_frame.h" +namespace agi { class BackgroundRunner; } + +#include +#include +#include +#include +#include + +namespace { + +/// @class BSVideoProvider +/// @brief Implements video loading through BestSource. +class BSVideoProvider final : public VideoProvider { + std::map bsopts; + BestVideoSource bs; + VideoProperties properties; + + std::vector 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 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 TimecodesVector; + for (int n = 0; n < properties.NumFrames; n++) { + if (ps->IsCancelled()) { + return; + } + std::unique_ptr 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 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 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 CreateBSVideoProvider(agi::fs::path const& path, std::string const& colormatrix, agi::BackgroundRunner *br) { + return agi::make_unique(path, colormatrix, br); +} + +#endif /* WITH_BESTSOURCE */ diff --git a/src/video_provider_manager.cpp b/src/video_provider_manager.cpp index 3739a7293..c239163b8 100644 --- a/src/video_provider_manager.cpp +++ b/src/video_provider_manager.cpp @@ -29,6 +29,8 @@ std::unique_ptr CreateDummyVideoProvider(agi::fs::path const&, st std::unique_ptr CreateYUV4MPEGVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); std::unique_ptr CreateFFmpegSourceVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); std::unique_ptr CreateAvisynthVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); +std::unique_ptr CreateBSVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); +std::unique_ptr CreateVapoursynthVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); std::unique_ptr CreateCacheVideoProvider(std::unique_ptr); @@ -47,6 +49,12 @@ namespace { #endif #ifdef WITH_AVISYNTH {"Avisynth", CreateAvisynthVideoProvider, false}, +#endif +#ifdef WITH_BESTSOURCE + {"BestSource", CreateBSVideoProvider, false}, +#endif +#ifdef WITH_VAPOURSYNTH + {"Vapoursynth", CreateVapoursynthVideoProvider, false}, #endif }; } diff --git a/src/video_provider_vs.cpp b/src/video_provider_vs.cpp new file mode 100644 index 000000000..3f9839e38 --- /dev/null +++ b/src/video_provider_vs.cpp @@ -0,0 +1,282 @@ +// Copyright (c) 2022, arch1t3cht +// +// 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 +#include +#include +#include +#include + +#include + +#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 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 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 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 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 CreateVapoursynthVideoProvider(agi::fs::path const& path, std::string const& colormatrix, agi::BackgroundRunner *) { + return agi::make_unique(path, colormatrix); +} +#endif // HAVE_VAPOURSYNTH diff --git a/subprojects/bestsource.wrap b/subprojects/bestsource.wrap new file mode 100644 index 000000000..fa0e5ef45 --- /dev/null +++ b/subprojects/bestsource.wrap @@ -0,0 +1,7 @@ +[wrap-git] +url = https://github.com/vapoursynth/bestsource +revision = head +patch_directory = bestsource + +[provide] +bestsource = bestsource_dep diff --git a/subprojects/jansson.wrap b/subprojects/jansson.wrap new file mode 100644 index 000000000..51ff9a82b --- /dev/null +++ b/subprojects/jansson.wrap @@ -0,0 +1,4 @@ +[wrap-git] +directory = jansson +url = https://github.com/akheron/jansson.git +revision = v2.14 diff --git a/subprojects/packagefiles/bestsource/libp2p/p2p_api.h b/subprojects/packagefiles/bestsource/libp2p/p2p_api.h new file mode 100644 index 000000000..f66166538 --- /dev/null +++ b/subprojects/packagefiles/bestsource/libp2p/p2p_api.h @@ -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 + +#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_ */ diff --git a/subprojects/packagefiles/bestsource/meson.build b/subprojects/packagefiles/bestsource/meson.build new file mode 100644 index 000000000..b4f77c93f --- /dev/null +++ b/subprojects/packagefiles/bestsource/meson.build @@ -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')) + diff --git a/subprojects/packagefiles/vapoursynth/meson.build b/subprojects/packagefiles/vapoursynth/meson.build new file mode 100644 index 000000000..5dfef44f7 --- /dev/null +++ b/subprojects/packagefiles/vapoursynth/meson.build @@ -0,0 +1,3 @@ +project('vapoursynth', 'cpp') + +vs_inc = include_directories('include') diff --git a/subprojects/vapoursynth.wrap b/subprojects/vapoursynth.wrap new file mode 100644 index 000000000..592446ccf --- /dev/null +++ b/subprojects/vapoursynth.wrap @@ -0,0 +1,4 @@ +[wrap-git] +url = https://github.com/Vapoursynth/vapoursynth.git +revision = R59 +patch_directory = vapoursynth