AviSynth / AviSynthPlus

AviSynth with improvements
http://avs-plus.net
972 stars 75 forks source link

AVISource and DirectShowSource both changing A/V type - where's the flaw? #286

Open FredMSloniker opened 2 years ago

FredMSloniker commented 2 years ago

I have a file, t1.avs, that's simply ConvertAudioToFloat(BlankClip()). Running ffprobe on it gives me this:

Stream #0:1: Audio: pcm_f32le, 44100 Hz, 1 channels, flt, 1411 kb/s

I have a second file, t2.avs, that's simply DirectShowSource("t1.avs"). Running ffprobe on it gives me this:

Stream #0:1: Audio: pcm_s16le, 44100 Hz, 1 channels, s16, 705 kb/s

Changing DirectShowSource() to AVISource() doesn't help. I'm combining the output of several .avs files into a master .avs file, and I don't know if this counts as a bug or if I'm doing something wrong...

qyot27 commented 2 years ago

Two things could be happening, depending on how the graph is being constructed: A) DirectShowSource opens LAVFilters to open the script with AviSynth, LAVFilters is set to output pcm_s16le. B) DirectShowSource is directly re-accessing AviSynth to chainload itself, and some of the particular global options meant for working with VfW or DirectShow are getting in the way before it realizes it's talking to ffprobe and not something like VirtualDub.

That said, for the task of

I'm combining the output of several .avs files into a master .avs file

that would probably be better handled by Import, not one of the source filters.

For one, Import is portable across OSes, AVISource and DirectShowSource are Windows-only (and quite possibly x86(-64) Windows-only at that, either because Windows on ARM doesn't include VfW or DirectShow or because even if it does, no one will bother porting the relevant VfW/DS codecs to WoA).

There's also the risk that trying to essentially chainload AviSynth.dll through itself on multiple invocations of DirectShowSource could interact strangely with anything like DLL reentrancy or memory and cache operations or who-knows-what. It may not show itself with only a few, but if the master file is supposed to end up doing this dozens or hundreds of times...

FredMSloniker commented 2 years ago

Thanks for the tip. Switching to Import() caused some glitches, though, which I narrowed down to the .avs files I'm importing defining variables and changing last and so on. What kind of insulation do I have to wrap around Import() to only return the clip I'd get if I opened it directly?

qyot27 commented 2 years ago

Ah, yeah, the documentation seems to mention something like that.

I don't know if it'd work, but if you wrap the contents of the child script into a function, I would think it would preserve the state of the child script when imported and then invoked by calling the function in the master script. It'd just be woefully inefficient if most of the child scripts use the same or very similar filtering.

To illustrate (and somewhat for the benefit of anyone stumbling onto this through Google), test.avs

Version()

And test2.avs

Version().Invert()

And import.avs

a=Import("test.avs")
b=Import("test2.avs")
a+b

import.avs plays back the regular Version info for 10 seconds, then a negative of it for 10 seconds. But we can do something like this in test2.avs instead (the indentation is just for readability):

function loadsomething()
{
    a=Version()
    b=a.Invert()
    return b
}

And then in import.avs

a=Import("test.avs")
b=Import("test2.avs")
a+loadsomething()

And it outputs exactly the same thing as the first import.avs example, except that test2.avs was changed into a function, and you call the function.

FredMSloniker commented 2 years ago

So would this function do what I think it'll do, i.e. isolate the imported .avs and supply only its video output?

function Wrap(string name)
{
    return Import(name)
}
qyot27 commented 2 years ago

If you're defining it as a function, it should be done in the child script, not the master script, and so Import shouldn't be happening there. The master script Imports the child script, and then loads it in using the function name.

If you can provide a more concrete example where the variable(s) in the scripts aren't honored when using Import, it'd be easier to troubleshoot this.

FredMSloniker commented 2 years ago

The issue is rather the opposite. I'm trying to keep the .avs I'm importing from, for instance, overwriting last; I want adding it to the master .avs to be just like adding a clip with AVISource or DirectShowSource, i.e. only returning a video clip and not making any other changes to the master .avs's environment.

e: here's a test case. I created t1.avs:

DirectShowSource("test.mkv")
z = Import("t2.avs")
last

And t2.avs:

BlankClip()
Info()

I would expect, on loading t1.avs, to see the contents of test.mkv. Instead, bizarrely, I see a blank clip without the info, meaning I'm getting neither the expected result nor the glitch I expected to get, i.e. the contents of t2.avs! However, if I change t1.avs to:

DirectShowSource("test.mkv")
function Wrap(string name)
{
    return Import(name)
}
z = Wrap("t2.avs")
last

...then I get test.mkv as intended.

qyot27 commented 2 years ago

Okay, I see more of what's going on now. Let me see.

Import("t2.avs")
Version()
last

You get Version, same as you would if you were using the implicit last.

Version()
Import("t2.avs")
last

You get the contents of t2.avs, same as if you were using the implicit last.

z=Import("t2.avs")
Version()
last

You get Version(). Same with the implicit last.

Version()
z=Import("t2.avs")
last

You get t2.avs. But the implicit last,

Version()
z=Import("t2.avs")

is now broken. This seems like it might be a bug, because (as far as I know) variables other than last should not be considered implicit, and last shouldn't be equivocating itself to a declared variable (that part may be up for debate, but if the user explicitly calls last you might be able to make an argument for why it should equivocate; I'd argue that it would make sense for implicit last to ignore declared variables, though). But the more concerning thing is that it breaks at all. Basically, the skip does seem to be functioning, in that it refuses to load z into last without the user having invoked either z or last explicitly, but under other circumstances it should have fallen back to the non-variable-invoked Version() instead.

[ffmpeg/demuxer] avisynth: AviSynth script did not return a clip
[lavf] avformat_open_input() failed
Failed to recognize file format.

Which implies that it's mangling the script being imported.

Changing t2.avs to

function here() {
a=BlankClip()
a.Info
}

t1.avs's implicit last is still broken, but explicit last returns Version(), and

Version()
z=Import("test2.avs")
here

returns t2.avs. Although to be fair, the z= declaration is meaningless here, because Import is importing the function, not the source (per ce), and something like z=here fails the exact same way the implicit last does above. To make that work, you actually need to do something like:

Version()
Import("test2.avs")
z=here
z

Honestly, my gut feeling is that a lot of this might simply be undocumented edge case behaviors with an actual explanation, it just rarely occurs because script writing tutorials try to avoid writing scripts with unclear variable usage that end up conflicting this way.

For instance, I would never try to Import a script containing a source filter mid-way through the script, after the parent script has already declared its source. All Import usage should ideally be done at the top of the script, along with whatever global options and other script control functions (SetMemoryMax, SetFilterMTMode, etc.) you might be using. And if I declare more than one source in a script, no source is going to be under an ambiguous last: every source is getting its own distinct variable. And when I go to actually output in that circumstance, unless I'm using a filter that takes variables (AudioDub and Overlay come to mind here), I'll be explicitly declaring the source's variable that I want to output instead of relying on the implicit or explicit last behavior and just hoping it understands what I want.

At this point I'll say that maybe @pinterf or @magiblot or one of the other contributors might have a much better explanation/take on what it's actually doing here, and so I'll just hand it off to them.

So the other part of the t2.avs weirdness:

Instead, bizarrely, I see a blank clip without the info, meaning I'm getting neither the expected result nor the glitch I expected to get, i.e. the contents of t2.avs!

ffplay displays the alpha channel as a black screen. mpv displays it as the typical transparency checkerboard. The output of Info() is there, but because BlankClip creates an RGBA clip by default, it's being obscured by how libavformat-based players display the alpha channel. If you open it in VirtualDub, it'd display the Info() readout. Similarly, if you either use RemoveAlphaPlane() after BlankClip(), or use BlankClip's pixel_type= parameter to select a pixel format that lacks an alpha channel (there's ton of them in AviSynth+), then ffplay and mpv show the Info() readout.