diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7bdb3e32..0e4400f30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,6 +136,7 @@ jobs: with: name: ${{ matrix.config.name }} - installer path: build/Aegisub-*.exe + if-no-files-found: error - name: Upload artifacts - portable.zip uses: actions/upload-artifact@v3 @@ -157,3 +158,4 @@ jobs: with: name: ${{ matrix.config.name }} - installer path: build/Aegisub-*.dmg + if-no-files-found: error diff --git a/automation/vapoursynth/aegisub_vs.py b/automation/vapoursynth/aegisub_vs.py index c3cc5876c..1eca5f5d3 100644 --- a/automation/vapoursynth/aegisub_vs.py +++ b/automation/vapoursynth/aegisub_vs.py @@ -1,14 +1,14 @@ """ -Utility functions for loading video files into Aegisub using the Vapoursynth +Utility functions for loading video files into Aegisub using the VapourSynth video provider. When encountering a file whose file extension is not .py or .vpy, the -Vapoursynth audio and video providers will execute the respective default +VapourSynth audio and video providers will execute the respective default script set in Aegisub's configuration, with the following string variables set: - filename: The path to the file that's being opened. - __aegi_data, __aegi_dictionary, __aegi_local, __aegi_script, __aegi_temp, __aegi_user: The values of ?data, ?dictionary, etc. respectively. -- __aegi_vscache: The path to a directory where the Vapoursynth script can +- __aegi_vscache: The path to a directory where the VapourSynth script can store cache files. This directory is cleaned by Aegisub when it gets too large (as defined by Aegisub's configuration). @@ -34,6 +34,43 @@ from typing import Any, Dict, List, Tuple import vapoursynth as vs core = vs.core +aegi_vscache: str = "" +aegi_vsplugins: str = "" + +plugin_extension = ".dll" if os.name == "nt" else ".so" + +def set_paths(vars: dict): + """ + Initialize the wrapper library with the given configuration directories. + Should usually be called at the start of the default script as + set_paths(globals()) + """ + global aegi_vscache + global aegi_vsplugins + aegi_vscache = vars["__aegi_vscache"] + aegi_vsplugins = vars["__aegi_vsplugins"] + + +def ensure_plugin(name: str, loadname: str, errormsg: str): + """ + Ensures that the VapourSynth plugin with the given name exists. + If it doesn't, it tries to load it from `loadname`. + If that fails, it raises an error with the given error message. + """ + if hasattr(core, name): + return + + if aegi_vsplugins and loadname: + try: + core.std.LoadPlugin(os.path.join(aegi_vsplugins, loadname + plugin_extension)) + if hasattr(core, name): + return + except vs.Error: + pass + + raise vs.Error(errormsg) + + def make_lwi_cache_filename(filename: str) -> str: """ Given a path to a video, will return a file name like the one LWLibavSource @@ -103,21 +140,23 @@ def info_from_lwindex(indexfile: str) -> Dict[str, List[int]]: } -def wrap_lwlibavsource(filename: str, cachedir: str, **kwargs: Any) -> Tuple[vs.VideoNode, Dict[str, List[int]]]: +def wrap_lwlibavsource(filename: str, cachedir: str | None = None, **kwargs: Any) -> Tuple[vs.VideoNode, Dict[str, List[int]]]: """ Given a path to a video file and a directory to store index files in (usually __aegi_vscache), will open the video with LWLibavSource and read the generated .lwi file to obtain the timecodes and keyframes. Additional keyword arguments are passed on to LWLibavSource. """ + if cachedir is None: + cachedir = aegi_vscache + try: os.mkdir(cachedir) except FileExistsError: pass cachefile = os.path.join(cachedir, make_lwi_cache_filename(filename)) - if not hasattr(core, "lsmas"): - raise vs.Error("To use Aegisub's LWLibavSource wrapper, the `lsmas` plugin for VapourSynth must be installed") + ensure_plugin("lsmas", "libvslsmashsource", "To use Aegisub's LWLibavSource wrapper, the `lsmas` plugin for VapourSynth must be installed") if b"-Dcachedir" not in core.lsmas.Version()["config"]: # type: ignore raise vs.Error("To use Aegisub's LWLibavSource wrapper, the `lsmas` plugin must support the `cachedir` option for LWLibavSource.") @@ -128,7 +167,7 @@ def wrap_lwlibavsource(filename: str, cachedir: str, **kwargs: Any) -> Tuple[vs. def make_keyframes(clip: vs.VideoNode, use_scxvid: bool = False, - resize_h: int = 360, resize_format: int = vs.YUV420P8, + resize_h: int = 360, resize_format: int = vs.GRAY8, **kwargs: Any) -> List[int]: """ Generates a list of keyframes from a clip, using either WWXD or Scxvid. @@ -142,12 +181,14 @@ def make_keyframes(clip: vs.VideoNode, use_scxvid: bool = False, The remaining keyword arguments are passed on to the respective filter. """ - clip = core.resize.Bilinear(clip, width=resize_h * clip.width // clip.height, height=resize_h, format=resize_format); - try: - clip = core.scxvid.Scxvid(clip, **kwargs) if use_scxvid else core.wwxd.WWXD(clip, **kwargs) - except AttributeError: - raise vs.Error("To use the keyframe generation, the `{}` plugin for VapourSynth must be installed" - .format("scxvid" if use_scxvid else "wwxd")) + clip = core.resize.Bilinear(clip, width=resize_h * clip.width // clip.height, height=resize_h, format=resize_format) + + if use_scxvid: + ensure_plugin("scxvid", "libscxvid", "To use the keyframe generation, the scxvid plugin for VapourSynth must be installed") + clip = core.scxvid.Scxvid(clip, **kwargs) + else: + ensure_plugin("wwxd", "libwwxd64", "To use the keyframe generation, the wwxdplugin for VapourSynth must be installed") + clip = core.wwxd.WWXD(clip, **kwargs) keyframes = {} done = 0 @@ -175,6 +216,16 @@ def save_keyframes(filename: str, keyframes: List[int]): f.write("".join(f"{n}\n" for n in keyframes)) +def try_get_keyframes(filename: str, default: str | List[int]) -> str | List[int]: + """ + Checks if a keyframes file for the given filename is present and, if so, + returns it. Otherwise, returns the given list of keyframes. + """ + kffilename = make_keyframes_filename(filename) + + return kffilename if os.path.exists(kffilename) else default + + def get_keyframes(filename: str, clip: vs.VideoNode, **kwargs: Any) -> str: """ When not already present, creates a keyframe file for the given clip next @@ -199,6 +250,7 @@ def check_audio(filename: str, **kwargs: Any) -> bool: Additional keyword arguments are passed on to BestAudioSource. """ try: + ensure_plugin("bas", "BestAudioSource", "") vs.core.bas.Source(source=filename, **kwargs) return True except AttributeError: diff --git a/packages/win_installer/fragment_codecs.iss b/packages/win_installer/fragment_codecs.iss index 06e73b037..f71f74b31 100644 --- a/packages/win_installer/fragment_codecs.iss +++ b/packages/win_installer/fragment_codecs.iss @@ -5,3 +5,8 @@ DestDir: {app}; Source: {#DEPS_DIR}\AvisynthPlus64\x64\AviSynth.dll; Flags: igno DestDir: {app}; Source: {#DEPS_DIR}\AvisynthPlus64\x64\plugins\DirectShowSource.dll; Flags: ignoreversion; Components: main ; VSFilter DestDir: {app}\csri; Source: {#DEPS_DIR}\VSFilter\x64\VSFilter.dll; Flags: ignoreversion; Components: main +; VapourSynth +DestDir: {app}\vapoursynth; Source: {#DEPS_DIR}\L-SMASH-Works\libvslsmashsource.dll; Flags: ignoreversion; Components: vapoursynth +DestDir: {app}\vapoursynth; Source: {#DEPS_DIR}\bestaudiosource\win64\BestAudioSource.dll; Flags: ignoreversion; Components: vapoursynth +DestDir: {app}\vapoursynth; Source: {#DEPS_DIR}\SCXVid\libscxvid.dll; Flags: ignoreversion; Components: vapoursynth +DestDir: {app}\vapoursynth; Source: {#DEPS_DIR}\WWXD\libwwxd64.dll; Flags: ignoreversion; Components: vapoursynth diff --git a/packages/win_installer/fragment_mainprogram.iss b/packages/win_installer/fragment_mainprogram.iss index 7feeaf740..4337261b7 100644 --- a/packages/win_installer/fragment_mainprogram.iss +++ b/packages/win_installer/fragment_mainprogram.iss @@ -1,5 +1,6 @@ [Components] Name: "main"; Description: "Main Files"; Types: full compact custom; Flags: fixed +Name: "vapoursynth"; Description: "Bundled VapourSynth Plugins"; Types: full Name: "macros"; Description: "Automation Scripts"; Types: full Name: "macros\bundled"; Description: "Bundled macros"; Types: full Name: "macros\demos"; Description: "Example macros/Demos"; Types: full diff --git a/packages/win_installer/fragment_spelling.iss b/packages/win_installer/fragment_spelling.iss index 331f3eb29..8e4168727 100644 --- a/packages/win_installer/fragment_spelling.iss +++ b/packages/win_installer/fragment_spelling.iss @@ -1,5 +1,5 @@ ; This file declares all installables related to spell checking and thesaurii in Aegisub [Files] -Source: {#DEPS_DIR}\dictionaries\en_US.aff; DestDir: {app}\dictionaries; Flags: skipifsourcedoesntexist ignoreversion -Source: {#DEPS_DIR}\dictionaries\en_US.dic; DestDir: {app}\dictionaries; Flags: skipifsourcedoesntexist ignoreversion +Source: {#DEPS_DIR}\dictionaries\en_US.aff; DestDir: {app}\dictionaries; Flags: ignoreversion; Components: dictionaries/en_US +Source: {#DEPS_DIR}\dictionaries\en_US.dic; DestDir: {app}\dictionaries; Flags: ignoreversion; Components: dictionaries/en_US diff --git a/packages/win_installer/portable/create-portable.ps1 b/packages/win_installer/portable/create-portable.ps1 index 4f4717bfe..22dc88b15 100644 --- a/packages/win_installer/portable/create-portable.ps1 +++ b/packages/win_installer/portable/create-portable.ps1 @@ -51,11 +51,19 @@ Copy-New-Item $InstallerDir\bin\aegisub.exe $PortableOutputDir Write-Output 'Copying - translations' Copy-New-Items "$InstallerDir\share\locale\*" "$PortableOutputDir\locale" -Recurse +Write-Output 'Copying - dictionaries' +Copy-New-Item $InstallerDepsDir\dictionaries\en_US.aff $PortableOutputDir\dictionaries +Copy-New-Item $InstallerDepsDir\dictionaries\en_US.dic $PortableOutputDir\dictionaries Write-Output 'Copying - codecs' Write-Output 'Copying - codecs\Avisynth' Copy-New-Item $InstallerDepsDir\AvisynthPlus64\x64\system\DevIL.dll $PortableOutputDir Copy-New-Item $InstallerDepsDir\AvisynthPlus64\x64\AviSynth.dll $PortableOutputDir Copy-New-Item $InstallerDepsDir\AvisynthPlus64\x64\plugins\DirectShowSource.dll $PortableOutputDir +Write-Output 'Copying - codecs\VapourSynth' +Copy-New-Item $InstallerDepsDir\L-SMASH-Works\libvslsmashsource.dll $PortableOutputDir\vapoursynth +Copy-New-Item $InstallerDepsDir\bestaudiosource\win64\BestAudioSource.dll $PortableOutputDir\vapoursynth +Copy-New-Item $InstallerDepsDir\SCXVid\libscxvid.dll $PortableOutputDir\vapoursynth +Copy-New-Item $InstallerDepsDir\WWXD\libwwxd64.dll $PortableOutputDir\vapoursynth Write-Output 'Copying - codecs\VSFilter' Copy-New-Item $InstallerDepsDir\VSFilter\x64\VSFilter.dll $PortableOutputDir\csri Write-Output 'Copying - runtimes\MS-CRT' diff --git a/src/audio_provider_factory.cpp b/src/audio_provider_factory.cpp index 71c34f66e..d2597cee4 100644 --- a/src/audio_provider_factory.cpp +++ b/src/audio_provider_factory.cpp @@ -32,7 +32,7 @@ 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 *); +std::unique_ptr CreateVapourSynthAudioProvider(fs::path const& filename, BackgroundRunner *); namespace { struct factory { @@ -54,7 +54,7 @@ const factory providers[] = { {"BestSource", CreateBSAudioProvider, false}, #endif #ifdef WITH_VAPOURSYNTH - {"Vapoursynth", CreateVapoursynthAudioProvider, false}, + {"VapourSynth", CreateVapourSynthAudioProvider, false}, #endif }; } diff --git a/src/audio_provider_vs.cpp b/src/audio_provider_vs.cpp index 1f42803be..2d81f06bb 100644 --- a/src/audio_provider_vs.cpp +++ b/src/audio_provider_vs.cpp @@ -15,7 +15,7 @@ // Aegisub Project http://www.aegisub.org/ /// @file audio_provider_vs.cpp -/// @brief Vapoursynth-based audio provider +/// @brief VapourSynth-based audio provider /// @ingroup audio_input /// @@ -38,7 +38,7 @@ #include "VSScript4.h" namespace { -class VapoursynthAudioProvider final : public agi::AudioProvider { +class VapourSynthAudioProvider final : public agi::AudioProvider { VapourSynthWrapper vs; VSScript *script = nullptr; VSNode *node = nullptr; @@ -47,36 +47,36 @@ class VapoursynthAudioProvider final : public agi::AudioProvider { 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(); + VapourSynthAudioProvider(agi::fs::path const& filename); + ~VapourSynthAudioProvider(); bool NeedsCache() const override { return true; } }; -VapoursynthAudioProvider::VapoursynthAudioProvider(agi::fs::path const& filename) try { +VapourSynthAudioProvider::VapourSynthAudioProvider(agi::fs::path const& filename) try { std::lock_guard lock(vs.GetMutex()); VSCleanCache(); script = vs.GetScriptAPI()->createScript(nullptr); if (script == nullptr) { - throw VapoursynthError("Error creating script API"); + throw VapourSynthError("Error creating script API"); } vs.GetScriptAPI()->evalSetWorkingDir(script, 1); if (OpenScriptOrVideo(vs.GetAPI(), vs.GetScriptAPI(), script, filename, OPT_GET("Provider/Audio/VapourSynth/Default Script")->GetString())) { std::string msg = agi::format("Error executing VapourSynth script: %s", vs.GetScriptAPI()->getError(script)); vs.GetScriptAPI()->freeScript(script); - throw VapoursynthError(msg); + throw VapourSynthError(msg); } node = vs.GetScriptAPI()->getOutputNode(script, 0); if (node == nullptr) { vs.GetScriptAPI()->freeScript(script); - throw VapoursynthError("No output node set"); + 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"); + throw VapourSynthError("Output node isn't an audio node"); } vi = vs.GetAPI()->getAudioInfo(node); float_samples = vi->format.sampleType == stFloat; @@ -85,10 +85,10 @@ VapoursynthAudioProvider::VapoursynthAudioProvider(agi::fs::path const& filename channels = vi->format.numChannels; num_samples = vi->numSamples; } -catch (VapoursynthError const& err) { +catch (VapourSynthError const& err) { // Unlike the video provider manager, the audio provider factory catches AudioProviderErrors and picks whichever source doesn't throw one. // So just rethrow the Error here with an extra label so the user will see the error message and know the audio wasn't loaded with VS - throw VapoursynthError(agi::format("Vapoursynth error: %s", err.GetMessage())); + throw VapourSynthError(agi::format("VapourSynth error: %s", err.GetMessage())); } template @@ -102,19 +102,19 @@ static void PackChannels(const uint8_t **Src, void *Dst, size_t Length, size_t C } } -void VapoursynthAudioProvider::FillBufferWithFrame(void *buf, int n, int64_t start, int64_t count) const { +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)); + 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"); + 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"); + throw VapourSynthError("Audio format is not constant"); } std::vector planes(channels); @@ -122,7 +122,7 @@ void VapoursynthAudioProvider::FillBufferWithFrame(void *buf, int n, int64_t sta planes[c] = vs.GetAPI()->getReadPtr(frame, c) + bytes_per_sample * start; if (planes[c] == nullptr) { vs.GetAPI()->freeFrame(frame); - throw VapoursynthError("Failed to read audio channel"); + throw VapourSynthError("Failed to read audio channel"); } } @@ -138,7 +138,7 @@ void VapoursynthAudioProvider::FillBufferWithFrame(void *buf, int n, int64_t sta vs.GetAPI()->freeFrame(frame); } -void VapoursynthAudioProvider::FillBuffer(void *buf, int64_t start, int64_t count) const { +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; @@ -154,7 +154,7 @@ void VapoursynthAudioProvider::FillBuffer(void *buf, int64_t start, int64_t coun } } -VapoursynthAudioProvider::~VapoursynthAudioProvider() { +VapourSynthAudioProvider::~VapourSynthAudioProvider() { if (node != nullptr) { vs.GetAPI()->freeNode(node); } @@ -164,7 +164,7 @@ VapoursynthAudioProvider::~VapoursynthAudioProvider() { } } -std::unique_ptr CreateVapoursynthAudioProvider(agi::fs::path const& file, agi::BackgroundRunner *) { - return agi::make_unique(file); +std::unique_ptr CreateVapourSynthAudioProvider(agi::fs::path const& file, agi::BackgroundRunner *) { + return agi::make_unique(file); } #endif diff --git a/src/libresrc/default_config.json b/src/libresrc/default_config.json index e6c8d088d..c83002c31 100644 --- a/src/libresrc/default_config.json +++ b/src/libresrc/default_config.json @@ -351,7 +351,7 @@ "Aegisub Cache" : true }, "VapourSynth" : { - "Default Script" : "# This default script will load an audio file using BestAudioSource.\n# It requires the `bas` plugin.\n\nimport vapoursynth as vs\ntry:\n vs.core.bas.Source(source=filename).set_output()\nexcept AttributeError:\n raise vs.Error(\"To use Aegisub's default audio loader, the `bas` plugin for VapourSynth must be installed\")" + "Default Script" : "# This default script will load an audio file using BestAudioSource.\n# It requires the `bas` plugin.\n\nimport vapoursynth as vs\nimport aegisub_vs as a\na.set_paths(locals())\n\na.ensure_plugin(\"bas\", \"BestAudioSource\", \"To use Aegisub's default audio loader, the `bas` plugin for VapourSynth must be installed\")\nvs.core.bas.Source(source=filename).set_output()" } }, "Avisynth" : { @@ -392,7 +392,7 @@ }, "VapourSynth" : { "Log Level": "Information", - "Default Script" : "# This default script will load a video file using LWLibavSource.\n# It requires the `lsmas` plugin.\n# See ?data/automation/vapoursynth/aegisub_vs.py for more information.\n\nimport aegisub_vs as a\nimport vapoursynth as vs\n\nclip, videoinfo = a.wrap_lwlibavsource(filename, __aegi_vscache)\nclip.set_output()\n__aegi_timecodes = videoinfo[\"timecodes\"]\n__aegi_keyframes = videoinfo[\"keyframes\"]\n# Uncomment this to automatically generate keyframes at scene changes.\n#__aegi_keyframes = a.get_keyframes(filename, clip)\n\n# Check if the file has an audio track. This requires the `bas` plugin.\n__aegi_hasaudio = 1 if a.check_audio(filename) else 0" + "Default Script" : "# This default script will load a video file using LWLibavSource.\n# It requires the `lsmas` plugin.\n# See ?data/automation/vapoursynth/aegisub_vs.py for more information.\n\nimport vapoursynth as vs\nimport time\nimport aegisub_vs as a\na.set_paths(locals())\n\nclip, videoinfo = a.wrap_lwlibavsource(filename)\nclip.set_output()\n__aegi_timecodes = videoinfo[\"timecodes\"]\n__aegi_keyframes = videoinfo[\"keyframes\"]\n\n# Uncomment the first following line to read keyframes from a file when present.\n#__aegi_keyframes = a.try_get_keyframes(filename, __aegi_keyframes)\n\n# Uncomment the following line to automatically generate keyframes at scene changes. This will take some time when first loading a video.\n__aegi_keyframes = a.get_keyframes(filename, clip)\n\n# Check if the file has an audio track. This requires the `bas` plugin.\n__aegi_hasaudio = 1 if a.check_audio(filename) else 0" } } }, diff --git a/src/libresrc/osx/default_config.json b/src/libresrc/osx/default_config.json index 38e58a404..8bb115f54 100644 --- a/src/libresrc/osx/default_config.json +++ b/src/libresrc/osx/default_config.json @@ -351,7 +351,7 @@ "Aegisub Cache" : true }, "VapourSynth" : { - "Default Script" : "# This default script will load an audio file using BestAudioSource.\n# It requires the `bas` plugin.\n\nimport vapoursynth as vs\ntry:\n vs.core.bas.Source(source=filename).set_output()\nexcept AttributeError:\n raise vs.Error(\"To use Aegisub's default audio loader, the `bas` plugin for VapourSynth must be installed\")" + "Default Script" : "# This default script will load an audio file using BestAudioSource.\n# It requires the `bas` plugin.\n\nimport vapoursynth as vs\nimport aegisub_vs as a\na.set_paths(globals())\n\na.ensure_plugin(\"bas\", \"BestAudioSource\", \"To use Aegisub's default audio loader, the `bas` plugin for VapourSynth must be installed\")\nvs.core.bas.Source(source=filename).set_output()" } }, "Avisynth" : { @@ -392,7 +392,7 @@ }, "VapourSynth" : { "Log Level": "Information", - "Default Script" : "# This default script will load a video file using LWLibavSource.\n# It requires the `lsmas` plugin.\n# See ?data/automation/vapoursynth/aegisub_vs.py for more information.\n\nimport aegisub_vs as a\nimport vapoursynth as vs\n\nclip, videoinfo = a.wrap_lwlibavsource(filename, __aegi_vscache)\nclip.set_output()\n__aegi_timecodes = videoinfo[\"timecodes\"]\n__aegi_keyframes = videoinfo[\"keyframes\"]\n# Uncomment this to automatically generate keyframes at scene changes.\n#__aegi_keyframes = a.get_keyframes(filename, clip)\n\n# Check if the file has an audio track. This requires the `bas` plugin.\n__aegi_hasaudio = 1 if a.check_audio(filename) else 0" + "Default Script" : "# This default script will load a video file using LWLibavSource.\n# It requires the `lsmas` plugin.\n# See ?data/automation/vapoursynth/aegisub_vs.py for more information.\n\nimport vapoursynth as vs\nimport time\nimport aegisub_vs as a\na.set_paths(locals())\n\nclip, videoinfo = a.wrap_lwlibavsource(filename)\nclip.set_output()\n__aegi_timecodes = videoinfo[\"timecodes\"]\n__aegi_keyframes = videoinfo[\"keyframes\"]\n\n# Uncomment the first following line to read keyframes from a file when present.\n#__aegi_keyframes = a.try_get_keyframes(filename, __aegi_keyframes)\n\n# Uncomment the following line to automatically generate keyframes at scene changes. This will take some time when first loading a video.\n__aegi_keyframes = a.get_keyframes(filename, clip)\n\n# Check if the file has an audio track. This requires the `bas` plugin.\n__aegi_hasaudio = 1 if a.check_audio(filename) else 0" } } }, diff --git a/src/vapoursynth_common.cpp b/src/vapoursynth_common.cpp index b360c6144..e5346f6bc 100644 --- a/src/vapoursynth_common.cpp +++ b/src/vapoursynth_common.cpp @@ -28,7 +28,7 @@ void SetStringVar(const VSAPI *api, VSMap *map, std::string variable, std::string value) { if (api->mapSetData(map, variable.c_str(), value.c_str(), -1, dtUtf8, 1)) - throw VapoursynthError("Failed to set VSMap entry"); + throw VapourSynthError("Failed to set VSMap entry"); } int OpenScriptOrVideo(const VSAPI *api, const VSSCRIPTAPI *sapi, VSScript *script, agi::fs::path const& filename, std::string default_script) { @@ -38,16 +38,21 @@ int OpenScriptOrVideo(const VSAPI *api, const VSSCRIPTAPI *sapi, VSScript *scrip } else { VSMap *map = api->createMap(); if (map == nullptr) - throw VapoursynthError("Failed to create VSMap for script info"); + throw VapourSynthError("Failed to create VSMap for script info"); SetStringVar(api, map, "filename", filename.string()); SetStringVar(api, map, "__aegi_vscache", config::path->Decode("?local/vscache").string()); +#ifdef WIN32 + SetStringVar(api, map, "__aegi_vsplugins", config::path->Decode("?data/vapoursynth").string()); +#else + SetStringVar(api, map, "__aegi_vsplugins", ""); +#endif for (std::string dir : { "data", "dictionary", "local", "script", "temp", "user", }) // Don't include ?audio and ?video in here since these only hold the paths to the previous audio/video files. SetStringVar(api, map, "__aegi_" + dir, config::path->Decode("?" + dir).string()); if (sapi->setVariables(script, map)) - throw VapoursynthError("Failed to set script info variables"); + throw VapourSynthError("Failed to set script info variables"); api->freeMap(map); diff --git a/src/vapoursynth_wrap.cpp b/src/vapoursynth_wrap.cpp index 33c7de659..f9f64add9 100644 --- a/src/vapoursynth_wrap.cpp +++ b/src/vapoursynth_wrap.cpp @@ -15,7 +15,7 @@ // Aegisub Project http://www.aegisub.org/ /// @file vapoursynth_wrap.cpp -/// @brief Wrapper-layer for Vapoursynth +/// @brief Wrapper-layer for VapourSynth /// @ingroup video_input audio_input /// @@ -67,7 +67,7 @@ VapourSynthWrapper::VapourSynthWrapper() { #endif if (!hLib) - throw VapoursynthError("Could not load " VSSCRIPT_SO); + throw VapourSynthError("Could not load " VSSCRIPT_SO); #ifdef _WIN32 FUNC* getVSScriptAPI = (FUNC*)GetProcAddress(hLib, "getVSScriptAPI"); @@ -75,7 +75,7 @@ VapourSynthWrapper::VapourSynthWrapper() { FUNC* getVSScriptAPI = (FUNC*)dlsym(hLib, "getVSScriptAPI"); #endif if (!getVSScriptAPI) - throw VapoursynthError("Failed to get address of getVSScriptAPI from " VSSCRIPT_SO); + throw VapourSynthError("Failed to get address of getVSScriptAPI from " VSSCRIPT_SO); // Python will set the program's locale to the user's default locale, which will break // half of wxwidgets on some operating systems due to locale mismatches. There's not really anything @@ -85,12 +85,12 @@ VapourSynthWrapper::VapourSynthWrapper() { setlocale(LC_ALL, oldlocale.c_str()); if (!scriptapi) - throw VapoursynthError("Failed to get Vapoursynth ScriptAPI"); + throw VapourSynthError("Failed to get VapourSynth ScriptAPI"); api = scriptapi->getVSAPI(VAPOURSYNTH_API_VERSION); if (!api) - throw VapoursynthError("Failed to get Vapoursynth API"); + throw VapourSynthError("Failed to get VapourSynth API"); vs_loaded = true; } diff --git a/src/vapoursynth_wrap.h b/src/vapoursynth_wrap.h index 802cb3d58..ee07235ff 100644 --- a/src/vapoursynth_wrap.h +++ b/src/vapoursynth_wrap.h @@ -23,7 +23,7 @@ #include -DEFINE_EXCEPTION(VapoursynthError, agi::Exception); +DEFINE_EXCEPTION(VapourSynthError, agi::Exception); struct VSAPI; struct VSSCRIPTAPI; diff --git a/src/video_provider_manager.cpp b/src/video_provider_manager.cpp index a15df8b17..9374a7c84 100644 --- a/src/video_provider_manager.cpp +++ b/src/video_provider_manager.cpp @@ -30,7 +30,7 @@ std::unique_ptr CreateYUV4MPEGVideoProvider(agi::fs::path const&, 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 CreateVapourSynthVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); std::unique_ptr CreateCacheVideoProvider(std::unique_ptr); @@ -54,7 +54,7 @@ namespace { {"BestSource (SLOW)", CreateBSVideoProvider, false}, #endif #ifdef WITH_VAPOURSYNTH - {"Vapoursynth", CreateVapoursynthVideoProvider, false}, + {"VapourSynth", CreateVapourSynthVideoProvider, false}, #endif }; } diff --git a/src/video_provider_vs.cpp b/src/video_provider_vs.cpp index b08dd65a6..da4a3d1a5 100644 --- a/src/video_provider_vs.cpp +++ b/src/video_provider_vs.cpp @@ -42,7 +42,7 @@ static const char *tc_key = "__aegi_timecodes"; static const char *audio_key = "__aegi_hasaudio"; namespace { -class VapoursynthVideoProvider: public VideoProvider { +class VapourSynthVideoProvider: public VideoProvider { VapourSynthWrapper vs; VSScript *script = nullptr; VSNode *node = nullptr; @@ -59,8 +59,8 @@ class VapoursynthVideoProvider: public VideoProvider { void SetResizeArg(VSMap *args, const VSMap *props, const char *arg_name, const char *prop_name, int64_t deflt, int64_t unspecified = -1); public: - VapoursynthVideoProvider(agi::fs::path const& filename, std::string const& colormatrix, agi::BackgroundRunner *br); - ~VapoursynthVideoProvider(); + VapourSynthVideoProvider(agi::fs::path const& filename, std::string const& colormatrix, agi::BackgroundRunner *br); + ~VapourSynthVideoProvider(); void GetFrame(int n, VideoFrame &frame) override; @@ -105,7 +105,7 @@ std::string colormatrix_description(int colorFamily, int colorRange, int matrix) } // 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, int64_t deflt, int64_t unspecified) { +void VapourSynthVideoProvider::SetResizeArg(VSMap *args, const VSMap *props, const char *arg_name, const char *prop_name, int64_t deflt, int64_t unspecified) { int err; int64_t result = vs.GetAPI()->mapGetInt(props, prop_name, 0, &err); if (err != 0 || result == unspecified) { @@ -117,7 +117,7 @@ void VapoursynthVideoProvider::SetResizeArg(VSMap *args, const VSMap *props, con } } -VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename, std::string const& colormatrix, agi::BackgroundRunner *br) try { try { +VapourSynthVideoProvider::VapourSynthVideoProvider(agi::fs::path const& filename, std::string const& colormatrix, agi::BackgroundRunner *br) try { try { std::lock_guard lock(vs.GetMutex()); VSCleanCache(); @@ -125,16 +125,16 @@ VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename int err1, err2; VSCore *core = vs.GetAPI()->createCore(0); if (core == nullptr) { - throw VapoursynthError("Error creating core"); + throw VapourSynthError("Error creating core"); } script = vs.GetScriptAPI()->createScript(core); if (script == nullptr) { vs.GetAPI()->freeCore(core); - throw VapoursynthError("Error creating script API"); + throw VapourSynthError("Error creating script API"); } vs.GetScriptAPI()->evalSetWorkingDir(script, 1); br->Run([&](agi::ProgressSink *ps) { - ps->SetTitle(from_wx(_("Executing Vapoursynth Script"))); + ps->SetTitle(from_wx(_("Executing VapourSynth Script"))); ps->SetMessage(""); ps->SetIndeterminate(); @@ -148,20 +148,20 @@ VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename }); if (err1) { std::string msg = agi::format("Error executing VapourSynth script: %s", vs.GetScriptAPI()->getError(script)); - throw VapoursynthError(msg); + throw VapourSynthError(msg); } node = vs.GetScriptAPI()->getOutputNode(script, 0); if (node == nullptr) - throw VapoursynthError("No output node set"); + throw VapourSynthError("No output node set"); if (vs.GetAPI()->getNodeType(node) != mtVideo) { - throw VapoursynthError("Output node isn't a video node"); + throw VapourSynthError("Output node isn't a video node"); } vi = vs.GetAPI()->getVideoInfo(node); if (vi == nullptr) - throw VapoursynthError("Couldn't get video info"); + throw VapourSynthError("Couldn't get video info"); if (!vsh::isConstantVideoFormat(vi)) - throw VapoursynthError("Video doesn't have constant format"); + throw VapourSynthError("Video doesn't have constant format"); int fpsNum = vi->fpsNum; int fpsDen = vi->fpsDen; @@ -174,7 +174,7 @@ VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename // Get timecodes and/or keyframes if provided VSMap *clipinfo = vs.GetAPI()->createMap(); if (clipinfo == nullptr) - throw VapoursynthError("Couldn't create map"); + throw VapourSynthError("Couldn't create map"); vs.GetScriptAPI()->getVariable(script, kf_key, clipinfo); vs.GetScriptAPI()->getVariable(script, tc_key, clipinfo); vs.GetScriptAPI()->getVariable(script, audio_key, clipinfo); @@ -190,7 +190,7 @@ VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename const int64_t *kfs = vs.GetAPI()->mapGetIntArray(clipinfo, kf_key, &err1); const char *kfs_path = vs.GetAPI()->mapGetData(clipinfo, kf_key, 0, &err2); if (err1 && err2) - throw VapoursynthError("Error getting keyframes from returned VSMap"); + throw VapourSynthError("Error getting keyframes from returned VSMap"); if (!err1) { keyframes.reserve(numkf); @@ -199,7 +199,7 @@ VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename } else { int kfs_path_size = vs.GetAPI()->mapGetDataSize(clipinfo, kf_key, 0, &err1); if (err1) - throw VapoursynthError("Error getting size of keyframes path"); + throw VapourSynthError("Error getting size of keyframes path"); try { keyframes = agi::keyframe::Load(config::path->Decode(std::string(kfs_path, size_t(kfs_path_size)))); @@ -213,11 +213,11 @@ VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename const int64_t *tcs = vs.GetAPI()->mapGetIntArray(clipinfo, tc_key, &err1); const char *tcs_path = vs.GetAPI()->mapGetData(clipinfo, tc_key, 0, &err2); if (err1 && err2) - throw VapoursynthError("Error getting timecodes from returned map"); + throw VapourSynthError("Error getting timecodes from returned map"); if (!err1) { if (numtc != vi->numFrames) - throw VapoursynthError("Number of returned timecodes does not match number of frames"); + throw VapourSynthError("Number of returned timecodes does not match number of frames"); std::vector timecodes; timecodes.reserve(numtc); @@ -228,13 +228,13 @@ VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename } else { int tcs_path_size = vs.GetAPI()->mapGetDataSize(clipinfo, tc_key, 0, &err1); if (err1) - throw VapoursynthError("Error getting size of keyframes path"); + throw VapourSynthError("Error getting size of keyframes path"); try { fps = agi::vfr::Framerate(config::path->Decode(std::string(tcs_path, size_t(tcs_path_size)))); } catch (agi::Exception const& e) { // Throw an error here unlike with keyframes since the timecodes not being loaded might not be immediately noticeable - throw VapoursynthError("Failed to open timecodes file specified by script: " + e.GetMessage()); + throw VapourSynthError("Failed to open timecodes file specified by script: " + e.GetMessage()); } } } @@ -245,7 +245,7 @@ VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename const VSMap *props = vs.GetAPI()->getFramePropertiesRO(frame); if (props == nullptr) - throw VapoursynthError("Couldn't get frame properties"); + throw VapourSynthError("Couldn't get frame properties"); int64_t sarn = vs.GetAPI()->mapGetInt(props, "_SARNum", 0, &err1); int64_t sard = vs.GetAPI()->mapGetInt(props, "_SARDen", 0, &err2); if (!err1 && !err2) { @@ -262,11 +262,11 @@ VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename // 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"); + throw VapourSynthError("Couldn't find resize plugin"); VSMap *args = vs.GetAPI()->createMap(); if (args == nullptr) - throw VapoursynthError("Failed to create argument map"); + throw VapourSynthError("Failed to create argument map"); vs.GetAPI()->mapSetNode(args, "clip", node, maAppend); vs.GetAPI()->mapSetInt(args, "format", pfRGB24, maAppend); @@ -299,7 +299,7 @@ VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename const VSFrame *rgbframe = GetVSFrame(0); vs.GetAPI()->freeFrame(rgbframe); } -} catch (VapoursynthError const& err) { // for try inside of function. We need both here since we need to catch errors from the VapoursynthWrap constructor. +} catch (VapourSynthError const& err) { // for try inside of function. We need both here since we need to catch errors from the VapourSynthWrap constructor. if (node != nullptr) vs.GetAPI()->freeNode(node); if (script != nullptr) @@ -307,27 +307,27 @@ VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename throw err; } } -catch (VapoursynthError const& err) { // for the entire constructor - throw VideoProviderError(agi::format("Vapoursynth error: %s", err.GetMessage())); +catch (VapourSynthError const& err) { // for the entire constructor + throw VideoProviderError(agi::format("VapourSynth error: %s", err.GetMessage())); } -const VSFrame *VapoursynthVideoProvider::GetVSFrame(int n) { +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)); + throw VapourSynthError(agi::format("Error getting frame: %s", errorMsg)); } return frame; } -void VapoursynthVideoProvider::GetFrame(int n, VideoFrame &out) { +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"); + throw VapourSynthError("Frame not in RGB24 format"); } out.width = vs.GetAPI()->getFrameWidth(frame, 0); @@ -359,7 +359,7 @@ void VapoursynthVideoProvider::GetFrame(int n, VideoFrame &out) { vs.GetAPI()->freeFrame(frame); } -VapoursynthVideoProvider::~VapoursynthVideoProvider() { +VapourSynthVideoProvider::~VapourSynthVideoProvider() { if (node != nullptr) { vs.GetAPI()->freeNode(node); } @@ -370,7 +370,7 @@ VapoursynthVideoProvider::~VapoursynthVideoProvider() { } namespace agi { class BackgroundRunner; } -std::unique_ptr CreateVapoursynthVideoProvider(agi::fs::path const& path, std::string const& colormatrix, agi::BackgroundRunner *br) { - return agi::make_unique(path, colormatrix, br); +std::unique_ptr CreateVapourSynthVideoProvider(agi::fs::path const& path, std::string const& colormatrix, agi::BackgroundRunner *br) { + return agi::make_unique(path, colormatrix, br); } #endif // WITH_VAPOURSYNTH diff --git a/subprojects/vapoursynth.wrap b/subprojects/vapoursynth.wrap index 592446ccf..1db0de4a5 100644 --- a/subprojects/vapoursynth.wrap +++ b/subprojects/vapoursynth.wrap @@ -1,4 +1,4 @@ [wrap-git] -url = https://github.com/Vapoursynth/vapoursynth.git +url = https://github.com/vapoursynth/vapoursynth.git revision = R59 patch_directory = vapoursynth diff --git a/tools/osx-bundle.sh b/tools/osx-bundle.sh index f8d4a682e..a1563a481 100755 --- a/tools/osx-bundle.sh +++ b/tools/osx-bundle.sh @@ -61,7 +61,12 @@ echo "---- Copying dictionaries ----" if test -f "${DICT_DIR}"; then cp -v "${DICT_DIR}/*" "${PKG_DIR}/Contents/SharedSupport/dictionaries" else - echo "Specified dictionary directory ${DICT_DIR} not found!" + echo "Specified dictionary directory ${DICT_DIR} not found. Downloading dictionaries:" + mkdir "${BUILD_DIR}/dictionaries" + curl -L "https://downloads.sourceforge.net/project/openofficeorg.mirror/contrib/dictionaries/en_US.zip" -o "${BUILD_DIR}/dictionaries/en_US.zip" + unzip "${BUILD_DIR}/dictionaries/en_US.zip" -d "${BUILD_DIR}/dictionaries" + cp -v "${BUILD_DIR}/dictionaries/en_US.aff" "${PKG_DIR}/Contents/SharedSupport/dictionaries" + cp -v "${BUILD_DIR}/dictionaries/en_US.dic" "${PKG_DIR}/Contents/SharedSupport/dictionaries" fi echo diff --git a/tools/win-installer-setup.ps1 b/tools/win-installer-setup.ps1 index 37bc974ed..b5a20b19e 100644 --- a/tools/win-installer-setup.ps1 +++ b/tools/win-installer-setup.ps1 @@ -63,13 +63,60 @@ if (!(Test-Path VSFilter)) { Set-Location $DepsDir } +### VapourSynth plugins + +# L-SMASH-Works +if (!(Test-Path L-SMASH-Works)) { + New-Item -ItemType Directory L-SMASH-Works + $lsmasReleases = Invoke-WebRequest "https://api.github.com/repos/AkarinVS/L-SMASH-Works/releases/latest" -Headers $GitHeaders -UseBasicParsing | ConvertFrom-Json + $lsmasUrl = "https://github.com/AkarinVS/L-SMASH-Works/releases/download/" + $lsmasReleases.tag_name + "/release-x86_64-cachedir-cwd.zip" + Invoke-WebRequest $lsmasUrl -OutFile release-x86_64-cachedir-cwd.zip -UseBasicParsing + Expand-Archive -LiteralPath release-x86_64-cachedir-cwd.zip -DestinationPath L-SMASH-Works + Remove-Item release-x86_64-cachedir-cwd.zip +} + +# bestaudiosource +if (!(Test-Path bestaudiosource)) { + $basDir = New-Item -ItemType Directory bestaudiosource + Set-Location $basDir + $basReleases = Invoke-WebRequest "https://api.github.com/repos/vapoursynth/bestaudiosource/releases/latest" -Headers $GitHeaders -UseBasicParsing | ConvertFrom-Json + $basUrl = $basReleases.assets[0].browser_download_url + Invoke-WebRequest $basUrl -OutFile bas-r1.7z -UseBasicParsing + 7z x bas-r1.7z + Remove-Item bas-r1.7z + Set-Location $DepsDir +} + +# SCXVid +if (!(Test-Path SCXVid)) { + $scxDir = New-Item -ItemType Directory SCXVid + Set-Location $scxDir + $scxReleases = Invoke-WebRequest "https://api.github.com/repos/dubhater/vapoursynth-scxvid/releases/latest" -Headers $GitHeaders -UseBasicParsing | ConvertFrom-Json + $scxUrl = "https://github.com/dubhater/vapoursynth-scxvid/releases/download/" + $scxReleases.tag_name + "/vapoursynth-scxvid-v1-win64.7z" + Invoke-WebRequest $scxUrl -OutFile vapoursynth-scxvid-v1-win64.7z -UseBasicParsing + 7z x vapoursynth-scxvid-v1-win64.7z + Remove-Item vapoursynth-scxvid-v1-win64.7z + Set-Location $DepsDir +} + +# WWXD +if (!(Test-Path WWXD)) { + New-Item -ItemType Directory WWXD + $wwxdReleases = Invoke-WebRequest "https://api.github.com/repos/dubhater/vapoursynth-wwxd/releases/latest" -Headers $GitHeaders -UseBasicParsing | ConvertFrom-Json + $wwxdUrl = "https://github.com/dubhater/vapoursynth-wwxd/releases/download/" + $wwxdReleases.tag_name + "/libwwxd64.dll" + Invoke-WebRequest $wwxdUrl -OutFile WWXD/libwwxd64.dll -UseBasicParsing +} + + # ffi-experiments if (!(Test-Path ffi-experiments)) { Get-Command "moonc" # check to ensure Moonscript is present git clone https://github.com/arch1t3cht/ffi-experiments.git Set-Location ffi-experiments meson build -Ddefault_library=static + if(!$?) { Exit $LASTEXITCODE } meson compile -C build + if(!$?) { Exit $LASTEXITCODE } Set-Location $DepsDir } @@ -79,12 +126,21 @@ if (!(Test-Path VC_redist)) { Invoke-WebRequest https://aka.ms/vs/17/release/VC_redist.x64.exe -OutFile "$redistDir\VC_redist.x64.exe" -UseBasicParsing } -# TODO dictionaries +# dictionaries +if (!(Test-Path dictionaries)) { + New-Item -ItemType Directory dictionaries + [Net.ServicePointManager]::SecurityProtocol = "Tls12" # Needed since otherwise downloading fails in some places like on the GitHub CI: https://stackoverflow.com/a/66614041/4730656 + Invoke-WebRequest https://downloads.sourceforge.net/project/openofficeorg.mirror/contrib/dictionaries/en_US.zip -UserAgent "Wget" -OutFile en_US.zip -UseBasicParsing + Expand-Archive -LiteralPath en_US.zip -DestinationPath dictionaries + Remove-Item en_US.zip +} # localization Set-Location $BuildRoot meson compile aegisub-gmo +if(!$?) { Exit $LASTEXITCODE } # Invoke InnoSetup $IssUrl = Join-Path $InstallerDir "aegisub_depctrl.iss" iscc $IssUrl +if(!$?) { Exit $LASTEXITCODE } \ No newline at end of file