Skip to content

Commit 1de8d04

Browse files
committed
Merge branches 'bugfixes' and 'vapoursynth' into feature
3 parents 798262f + 883a1b2 + 84d67c7 commit 1de8d04

18 files changed

Lines changed: 222 additions & 88 deletions

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ jobs:
136136
with:
137137
name: ${{ matrix.config.name }} - installer
138138
path: build/Aegisub-*.exe
139+
if-no-files-found: error
139140

140141
- name: Upload artifacts - portable.zip
141142
uses: actions/upload-artifact@v3
@@ -157,3 +158,4 @@ jobs:
157158
with:
158159
name: ${{ matrix.config.name }} - installer
159160
path: build/Aegisub-*.dmg
161+
if-no-files-found: error

automation/vapoursynth/aegisub_vs.py

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
"""
2-
Utility functions for loading video files into Aegisub using the Vapoursynth
2+
Utility functions for loading video files into Aegisub using the VapourSynth
33
video provider.
44
55
When encountering a file whose file extension is not .py or .vpy, the
6-
Vapoursynth audio and video providers will execute the respective default
6+
VapourSynth audio and video providers will execute the respective default
77
script set in Aegisub's configuration, with the following string variables set:
88
- filename: The path to the file that's being opened.
99
- __aegi_data, __aegi_dictionary, __aegi_local, __aegi_script, __aegi_temp, __aegi_user:
1010
The values of ?data, ?dictionary, etc. respectively.
11-
- __aegi_vscache: The path to a directory where the Vapoursynth script can
11+
- __aegi_vscache: The path to a directory where the VapourSynth script can
1212
store cache files. This directory is cleaned by Aegisub when it gets too
1313
large (as defined by Aegisub's configuration).
1414
@@ -34,6 +34,43 @@
3434
import vapoursynth as vs
3535
core = vs.core
3636

37+
aegi_vscache: str = ""
38+
aegi_vsplugins: str = ""
39+
40+
plugin_extension = ".dll" if os.name == "nt" else ".so"
41+
42+
def set_paths(vars: dict):
43+
"""
44+
Initialize the wrapper library with the given configuration directories.
45+
Should usually be called at the start of the default script as
46+
set_paths(globals())
47+
"""
48+
global aegi_vscache
49+
global aegi_vsplugins
50+
aegi_vscache = vars["__aegi_vscache"]
51+
aegi_vsplugins = vars["__aegi_vsplugins"]
52+
53+
54+
def ensure_plugin(name: str, loadname: str, errormsg: str):
55+
"""
56+
Ensures that the VapourSynth plugin with the given name exists.
57+
If it doesn't, it tries to load it from `loadname`.
58+
If that fails, it raises an error with the given error message.
59+
"""
60+
if hasattr(core, name):
61+
return
62+
63+
if aegi_vsplugins and loadname:
64+
try:
65+
core.std.LoadPlugin(os.path.join(aegi_vsplugins, loadname + plugin_extension))
66+
if hasattr(core, name):
67+
return
68+
except vs.Error:
69+
pass
70+
71+
raise vs.Error(errormsg)
72+
73+
3774
def make_lwi_cache_filename(filename: str) -> str:
3875
"""
3976
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]]:
103140
}
104141

105142

106-
def wrap_lwlibavsource(filename: str, cachedir: str, **kwargs: Any) -> Tuple[vs.VideoNode, Dict[str, List[int]]]:
143+
def wrap_lwlibavsource(filename: str, cachedir: str | None = None, **kwargs: Any) -> Tuple[vs.VideoNode, Dict[str, List[int]]]:
107144
"""
108145
Given a path to a video file and a directory to store index files in
109146
(usually __aegi_vscache), will open the video with LWLibavSource and read
110147
the generated .lwi file to obtain the timecodes and keyframes.
111148
Additional keyword arguments are passed on to LWLibavSource.
112149
"""
150+
if cachedir is None:
151+
cachedir = aegi_vscache
152+
113153
try:
114154
os.mkdir(cachedir)
115155
except FileExistsError:
116156
pass
117157
cachefile = os.path.join(cachedir, make_lwi_cache_filename(filename))
118158

119-
if not hasattr(core, "lsmas"):
120-
raise vs.Error("To use Aegisub's LWLibavSource wrapper, the `lsmas` plugin for VapourSynth must be installed")
159+
ensure_plugin("lsmas", "libvslsmashsource", "To use Aegisub's LWLibavSource wrapper, the `lsmas` plugin for VapourSynth must be installed")
121160

122161
if b"-Dcachedir" not in core.lsmas.Version()["config"]: # type: ignore
123162
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.
128167

129168

130169
def make_keyframes(clip: vs.VideoNode, use_scxvid: bool = False,
131-
resize_h: int = 360, resize_format: int = vs.YUV420P8,
170+
resize_h: int = 360, resize_format: int = vs.GRAY8,
132171
**kwargs: Any) -> List[int]:
133172
"""
134173
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,
142181
The remaining keyword arguments are passed on to the respective filter.
143182
"""
144183

145-
clip = core.resize.Bilinear(clip, width=resize_h * clip.width // clip.height, height=resize_h, format=resize_format);
146-
try:
147-
clip = core.scxvid.Scxvid(clip, **kwargs) if use_scxvid else core.wwxd.WWXD(clip, **kwargs)
148-
except AttributeError:
149-
raise vs.Error("To use the keyframe generation, the `{}` plugin for VapourSynth must be installed"
150-
.format("scxvid" if use_scxvid else "wwxd"))
184+
clip = core.resize.Bilinear(clip, width=resize_h * clip.width // clip.height, height=resize_h, format=resize_format)
185+
186+
if use_scxvid:
187+
ensure_plugin("scxvid", "libscxvid", "To use the keyframe generation, the scxvid plugin for VapourSynth must be installed")
188+
clip = core.scxvid.Scxvid(clip, **kwargs)
189+
else:
190+
ensure_plugin("wwxd", "libwwxd64", "To use the keyframe generation, the wwxdplugin for VapourSynth must be installed")
191+
clip = core.wwxd.WWXD(clip, **kwargs)
151192

152193
keyframes = {}
153194
done = 0
@@ -175,6 +216,16 @@ def save_keyframes(filename: str, keyframes: List[int]):
175216
f.write("".join(f"{n}\n" for n in keyframes))
176217

177218

219+
def try_get_keyframes(filename: str, default: str | List[int]) -> str | List[int]:
220+
"""
221+
Checks if a keyframes file for the given filename is present and, if so,
222+
returns it. Otherwise, returns the given list of keyframes.
223+
"""
224+
kffilename = make_keyframes_filename(filename)
225+
226+
return kffilename if os.path.exists(kffilename) else default
227+
228+
178229
def get_keyframes(filename: str, clip: vs.VideoNode, **kwargs: Any) -> str:
179230
"""
180231
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:
199250
Additional keyword arguments are passed on to BestAudioSource.
200251
"""
201252
try:
253+
ensure_plugin("bas", "BestAudioSource", "")
202254
vs.core.bas.Source(source=filename, **kwargs)
203255
return True
204256
except AttributeError:

packages/win_installer/fragment_codecs.iss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,8 @@ DestDir: {app}; Source: {#DEPS_DIR}\AvisynthPlus64\x64\AviSynth.dll; Flags: igno
55
DestDir: {app}; Source: {#DEPS_DIR}\AvisynthPlus64\x64\plugins\DirectShowSource.dll; Flags: ignoreversion; Components: main
66
; VSFilter
77
DestDir: {app}\csri; Source: {#DEPS_DIR}\VSFilter\x64\VSFilter.dll; Flags: ignoreversion; Components: main
8+
; VapourSynth
9+
DestDir: {app}\vapoursynth; Source: {#DEPS_DIR}\L-SMASH-Works\libvslsmashsource.dll; Flags: ignoreversion; Components: vapoursynth
10+
DestDir: {app}\vapoursynth; Source: {#DEPS_DIR}\bestaudiosource\win64\BestAudioSource.dll; Flags: ignoreversion; Components: vapoursynth
11+
DestDir: {app}\vapoursynth; Source: {#DEPS_DIR}\SCXVid\libscxvid.dll; Flags: ignoreversion; Components: vapoursynth
12+
DestDir: {app}\vapoursynth; Source: {#DEPS_DIR}\WWXD\libwwxd64.dll; Flags: ignoreversion; Components: vapoursynth

packages/win_installer/fragment_mainprogram.iss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[Components]
22
Name: "main"; Description: "Main Files"; Types: full compact custom; Flags: fixed
3+
Name: "vapoursynth"; Description: "Bundled VapourSynth Plugins"; Types: full
34
Name: "macros"; Description: "Automation Scripts"; Types: full
45
Name: "macros\bundled"; Description: "Bundled macros"; Types: full
56
Name: "macros\demos"; Description: "Example macros/Demos"; Types: full
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
; This file declares all installables related to spell checking and thesaurii in Aegisub
22

33
[Files]
4-
Source: {#DEPS_DIR}\dictionaries\en_US.aff; DestDir: {app}\dictionaries; Flags: skipifsourcedoesntexist ignoreversion
5-
Source: {#DEPS_DIR}\dictionaries\en_US.dic; DestDir: {app}\dictionaries; Flags: skipifsourcedoesntexist ignoreversion
4+
Source: {#DEPS_DIR}\dictionaries\en_US.aff; DestDir: {app}\dictionaries; Flags: ignoreversion; Components: dictionaries/en_US
5+
Source: {#DEPS_DIR}\dictionaries\en_US.dic; DestDir: {app}\dictionaries; Flags: ignoreversion; Components: dictionaries/en_US

packages/win_installer/portable/create-portable.ps1

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,19 @@ Copy-New-Item $InstallerDir\bin\aegisub.exe $PortableOutputDir
5151

5252
Write-Output 'Copying - translations'
5353
Copy-New-Items "$InstallerDir\share\locale\*" "$PortableOutputDir\locale" -Recurse
54+
Write-Output 'Copying - dictionaries'
55+
Copy-New-Item $InstallerDepsDir\dictionaries\en_US.aff $PortableOutputDir\dictionaries
56+
Copy-New-Item $InstallerDepsDir\dictionaries\en_US.dic $PortableOutputDir\dictionaries
5457
Write-Output 'Copying - codecs'
5558
Write-Output 'Copying - codecs\Avisynth'
5659
Copy-New-Item $InstallerDepsDir\AvisynthPlus64\x64\system\DevIL.dll $PortableOutputDir
5760
Copy-New-Item $InstallerDepsDir\AvisynthPlus64\x64\AviSynth.dll $PortableOutputDir
5861
Copy-New-Item $InstallerDepsDir\AvisynthPlus64\x64\plugins\DirectShowSource.dll $PortableOutputDir
62+
Write-Output 'Copying - codecs\VapourSynth'
63+
Copy-New-Item $InstallerDepsDir\L-SMASH-Works\libvslsmashsource.dll $PortableOutputDir\vapoursynth
64+
Copy-New-Item $InstallerDepsDir\bestaudiosource\win64\BestAudioSource.dll $PortableOutputDir\vapoursynth
65+
Copy-New-Item $InstallerDepsDir\SCXVid\libscxvid.dll $PortableOutputDir\vapoursynth
66+
Copy-New-Item $InstallerDepsDir\WWXD\libwwxd64.dll $PortableOutputDir\vapoursynth
5967
Write-Output 'Copying - codecs\VSFilter'
6068
Copy-New-Item $InstallerDepsDir\VSFilter\x64\VSFilter.dll $PortableOutputDir\csri
6169
Write-Output 'Copying - runtimes\MS-CRT'

src/audio_provider_factory.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ using namespace agi;
3232
std::unique_ptr<AudioProvider> CreateAvisynthAudioProvider(fs::path const& filename, BackgroundRunner *);
3333
std::unique_ptr<AudioProvider> CreateFFmpegSourceAudioProvider(fs::path const& filename, BackgroundRunner *);
3434
std::unique_ptr<AudioProvider> CreateBSAudioProvider(fs::path const& filename, BackgroundRunner *);
35-
std::unique_ptr<AudioProvider> CreateVapoursynthAudioProvider(fs::path const& filename, BackgroundRunner *);
35+
std::unique_ptr<AudioProvider> CreateVapourSynthAudioProvider(fs::path const& filename, BackgroundRunner *);
3636

3737
namespace {
3838
struct factory {
@@ -54,7 +54,7 @@ const factory providers[] = {
5454
{"BestSource", CreateBSAudioProvider, false},
5555
#endif
5656
#ifdef WITH_VAPOURSYNTH
57-
{"Vapoursynth", CreateVapoursynthAudioProvider, false},
57+
{"VapourSynth", CreateVapourSynthAudioProvider, false},
5858
#endif
5959
};
6060
}

src/audio_provider_vs.cpp

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// Aegisub Project http://www.aegisub.org/
1616

1717
/// @file audio_provider_vs.cpp
18-
/// @brief Vapoursynth-based audio provider
18+
/// @brief VapourSynth-based audio provider
1919
/// @ingroup audio_input
2020
///
2121

@@ -38,7 +38,7 @@
3838
#include "VSScript4.h"
3939

4040
namespace {
41-
class VapoursynthAudioProvider final : public agi::AudioProvider {
41+
class VapourSynthAudioProvider final : public agi::AudioProvider {
4242
VapourSynthWrapper vs;
4343
VSScript *script = nullptr;
4444
VSNode *node = nullptr;
@@ -47,36 +47,36 @@ class VapoursynthAudioProvider final : public agi::AudioProvider {
4747
void FillBufferWithFrame(void *buf, int frame, int64_t start, int64_t count) const;
4848
void FillBuffer(void *buf, int64_t start, int64_t count) const override;
4949
public:
50-
VapoursynthAudioProvider(agi::fs::path const& filename);
51-
~VapoursynthAudioProvider();
50+
VapourSynthAudioProvider(agi::fs::path const& filename);
51+
~VapourSynthAudioProvider();
5252

5353
bool NeedsCache() const override { return true; }
5454
};
5555

56-
VapoursynthAudioProvider::VapoursynthAudioProvider(agi::fs::path const& filename) try {
56+
VapourSynthAudioProvider::VapourSynthAudioProvider(agi::fs::path const& filename) try {
5757
std::lock_guard<std::mutex> lock(vs.GetMutex());
5858

5959
VSCleanCache();
6060

6161
script = vs.GetScriptAPI()->createScript(nullptr);
6262
if (script == nullptr) {
63-
throw VapoursynthError("Error creating script API");
63+
throw VapourSynthError("Error creating script API");
6464
}
6565
vs.GetScriptAPI()->evalSetWorkingDir(script, 1);
6666
if (OpenScriptOrVideo(vs.GetAPI(), vs.GetScriptAPI(), script, filename, OPT_GET("Provider/Audio/VapourSynth/Default Script")->GetString())) {
6767
std::string msg = agi::format("Error executing VapourSynth script: %s", vs.GetScriptAPI()->getError(script));
6868
vs.GetScriptAPI()->freeScript(script);
69-
throw VapoursynthError(msg);
69+
throw VapourSynthError(msg);
7070
}
7171
node = vs.GetScriptAPI()->getOutputNode(script, 0);
7272
if (node == nullptr) {
7373
vs.GetScriptAPI()->freeScript(script);
74-
throw VapoursynthError("No output node set");
74+
throw VapourSynthError("No output node set");
7575
}
7676
if (vs.GetAPI()->getNodeType(node) != mtAudio) {
7777
vs.GetAPI()->freeNode(node);
7878
vs.GetScriptAPI()->freeScript(script);
79-
throw VapoursynthError("Output node isn't an audio node");
79+
throw VapourSynthError("Output node isn't an audio node");
8080
}
8181
vi = vs.GetAPI()->getAudioInfo(node);
8282
float_samples = vi->format.sampleType == stFloat;
@@ -85,10 +85,10 @@ VapoursynthAudioProvider::VapoursynthAudioProvider(agi::fs::path const& filename
8585
channels = vi->format.numChannels;
8686
num_samples = vi->numSamples;
8787
}
88-
catch (VapoursynthError const& err) {
88+
catch (VapourSynthError const& err) {
8989
// Unlike the video provider manager, the audio provider factory catches AudioProviderErrors and picks whichever source doesn't throw one.
9090
// 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
91-
throw VapoursynthError(agi::format("Vapoursynth error: %s", err.GetMessage()));
91+
throw VapourSynthError(agi::format("VapourSynth error: %s", err.GetMessage()));
9292
}
9393

9494
template<typename T>
@@ -102,27 +102,27 @@ static void PackChannels(const uint8_t **Src, void *Dst, size_t Length, size_t C
102102
}
103103
}
104104

105-
void VapoursynthAudioProvider::FillBufferWithFrame(void *buf, int n, int64_t start, int64_t count) const {
105+
void VapourSynthAudioProvider::FillBufferWithFrame(void *buf, int n, int64_t start, int64_t count) const {
106106
char errorMsg[1024];
107107
const VSFrame *frame = vs.GetAPI()->getFrame(n, node, errorMsg, sizeof(errorMsg));
108108
if (frame == nullptr) {
109-
throw VapoursynthError(agi::format("Error getting frame: %s", errorMsg));
109+
throw VapourSynthError(agi::format("Error getting frame: %s", errorMsg));
110110
}
111111
if (vs.GetAPI()->getFrameLength(frame) < count) {
112112
vs.GetAPI()->freeFrame(frame);
113-
throw VapoursynthError("Audio frame too short");
113+
throw VapourSynthError("Audio frame too short");
114114
}
115115
if (vs.GetAPI()->getAudioFrameFormat(frame)->numChannels != channels || vs.GetAPI()->getAudioFrameFormat(frame)->bytesPerSample != bytes_per_sample) {
116116
vs.GetAPI()->freeFrame(frame);
117-
throw VapoursynthError("Audio format is not constant");
117+
throw VapourSynthError("Audio format is not constant");
118118
}
119119

120120
std::vector<const uint8_t *> planes(channels);
121121
for (int c = 0; c < channels; c++) {
122122
planes[c] = vs.GetAPI()->getReadPtr(frame, c) + bytes_per_sample * start;
123123
if (planes[c] == nullptr) {
124124
vs.GetAPI()->freeFrame(frame);
125-
throw VapoursynthError("Failed to read audio channel");
125+
throw VapourSynthError("Failed to read audio channel");
126126
}
127127
}
128128

@@ -138,7 +138,7 @@ void VapoursynthAudioProvider::FillBufferWithFrame(void *buf, int n, int64_t sta
138138
vs.GetAPI()->freeFrame(frame);
139139
}
140140

141-
void VapoursynthAudioProvider::FillBuffer(void *buf, int64_t start, int64_t count) const {
141+
void VapourSynthAudioProvider::FillBuffer(void *buf, int64_t start, int64_t count) const {
142142
int end = start + count; // exclusive
143143
int startframe = start / VS_AUDIO_FRAME_SAMPLES;
144144
int endframe = (end - 1) / VS_AUDIO_FRAME_SAMPLES;
@@ -154,7 +154,7 @@ void VapoursynthAudioProvider::FillBuffer(void *buf, int64_t start, int64_t coun
154154
}
155155
}
156156

157-
VapoursynthAudioProvider::~VapoursynthAudioProvider() {
157+
VapourSynthAudioProvider::~VapourSynthAudioProvider() {
158158
if (node != nullptr) {
159159
vs.GetAPI()->freeNode(node);
160160
}
@@ -164,7 +164,7 @@ VapoursynthAudioProvider::~VapoursynthAudioProvider() {
164164
}
165165
}
166166

167-
std::unique_ptr<agi::AudioProvider> CreateVapoursynthAudioProvider(agi::fs::path const& file, agi::BackgroundRunner *) {
168-
return agi::make_unique<VapoursynthAudioProvider>(file);
167+
std::unique_ptr<agi::AudioProvider> CreateVapourSynthAudioProvider(agi::fs::path const& file, agi::BackgroundRunner *) {
168+
return agi::make_unique<VapourSynthAudioProvider>(file);
169169
}
170170
#endif

src/libresrc/default_config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@
351351
"Aegisub Cache" : true
352352
},
353353
"VapourSynth" : {
354-
"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\")"
354+
"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()"
355355
}
356356
},
357357
"Avisynth" : {
@@ -392,7 +392,7 @@
392392
},
393393
"VapourSynth" : {
394394
"Log Level": "Information",
395-
"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"
395+
"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"
396396
}
397397
}
398398
},

0 commit comments

Comments
 (0)