Merge branches 'workarounds' and 'video_provider_rework' into feature

This commit is contained in:
arch1t3cht 2023-07-16 17:57:09 +02:00
commit f9be4a854f
264 changed files with 222704 additions and 170565 deletions

View File

@ -2,9 +2,14 @@ name: Meson CI
on:
push:
branches: [ master ]
branches:
- master
- feature
- cibuilds
pull_request:
branches: [ master ]
branches:
- master
- feature
jobs:
build:
@ -33,6 +38,9 @@ jobs:
-Dfribidi:tests=false
-Dfribidi:docs=false
-Dlibass:fontconfig=disabled
-Davisynth=enabled
-Dbestsource=enabled
-Dvapoursynth=enabled
#- {
# name: Windows MinGW,
# os: windows-latest,
@ -50,25 +58,32 @@ jobs:
buildtype: release,
args: ''
}
- {
name: macOS Debug,
os: macos-latest,
buildtype: debugoptimized,
args: -Ddefault_library=static -Dbuild_osx_bundle=true -Dlocal_boost=true
}
- name: Ubuntu AppImage
os: ubuntu-22.04
buildtype: release
appimage: true
# distro ffms is currently broken
args: >-
--prefix=/usr
-Dbuild_appimage=true
-Ddefault_library=static
--force-fallback-for=ffms2
-Davisynth=enabled
-Dbestsource=enabled
-Dvapoursynth=enabled
- {
name: macOS Release,
os: macos-latest,
buildtype: release,
args: -Ddefault_library=static -Dbuild_osx_bundle=true -Dlocal_boost=true
args: -Ddefault_library=static -Dbuild_osx_bundle=true -Dlocal_boost=true -Dvapoursynth=enabled --force-fallback-for=ffms2
}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: '0'
- uses: actions/setup-python@v2
- uses: actions/setup-python@v4
with:
python-version: '3.x'
@ -103,13 +118,13 @@ jobs:
brew install pulseaudio # NO OpenAL in github CI
- name: Install dependencies (Linux)
if: matrix.config.os == 'ubuntu-latest'
if: startsWith(matrix.config.os, 'ubuntu-')
run: |
sudo apt-get update
sudo apt-get install ninja-build build-essential libx11-dev libwxgtk3.0-gtk3-dev libfreetype6-dev pkg-config libfontconfig1-dev libass-dev libasound2-dev libffms2-dev intltool libboost-all-dev
sudo apt-get install ninja-build build-essential libx11-dev libwxgtk3.0-gtk3-dev libfreetype6-dev pkg-config libfontconfig1-dev libass-dev libasound2-dev libffms2-dev intltool libboost-all-dev libhunspell-dev libuchardet-dev libpulse-dev libopenal-dev libjansson-dev
- name: Configure
run: meson build ${{ matrix.config.args }} -Dbuildtype=${{ matrix.config.buildtype }}
run: meson setup build ${{ matrix.config.args }} -Dbuildtype=${{ matrix.config.buildtype }}
- name: Build
run: meson compile -C build
@ -120,21 +135,25 @@ jobs:
# Windows artifacts
- name: Generate Windows installer
if: matrix.config.os == 'windows-latest'
run: meson compile win-installer -C build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run:
meson compile win-installer -C build
- name: Generate Windows portable installer
if: matrix.config.os == 'windows-latest'
run: cd build && ninja win-portable
- name: Upload artifacts - win_installer
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: matrix.config.os == 'windows-latest'
with:
name: ${{ matrix.config.name }} - installer
path: build/Aegisub-*.exe
if-no-files-found: error
- name: Upload artifacts - portable.zip
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: matrix.config.os == 'windows-latest'
with:
name: ${{ matrix.config.name }} - portable
@ -148,8 +167,33 @@ jobs:
meson compile osx-build-dmg -C build
- name: Upload artifacts - macOS dmg
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: matrix.config.os == 'macos-latest'
with:
name: ${{ matrix.config.name }} - installer
path: build/Aegisub-*.dmg
if-no-files-found: error
# Linux artifacts (AppImage)
- name: Generate AppImage
if: matrix.config.appimage
run: |
mkdir -p appimage/appdir
meson install -C build --destdir=../appimage/appdir
cd appimage
sudo apt-get install libfuse2
curl -L "https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20220822-1/linuxdeploy-x86_64.AppImage" -o linuxdeploy
curl -L "https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage" -o appimagetool
chmod +x linuxdeploy appimagetool
./linuxdeploy --appdir appdir --desktop-file=appdir/aegisub.desktop
./appimagetool appdir
- name: Upload artifacts - Linux AppImage
uses: actions/upload-artifact@v3
if: matrix.config.appimage
with:
name: ${{ matrix.config.name }}
path: appimage/*.AppImage
if-no-files-found: error

5
.gitignore vendored
View File

@ -1,5 +1,6 @@
/tests/data
automation/vapoursynth/__pycache__
packages/desktop/aegisub.desktop
packages/desktop/aegisub.desktop.template
src/libresrc/bitmap.cpp
@ -18,6 +19,8 @@ tools/repack-thes-dict.dSYM
# Meson
build*/
subprojects/avisynth
subprojects/bestsource/
subprojects/boost*/
subprojects/cairo*
subprojects/dav1d
@ -30,6 +33,7 @@ subprojects/glib*
subprojects/googletest-*
subprojects/harfbuzz
subprojects/icu
subprojects/jansson
subprojects/libass
subprojects/libffi*
subprojects/libpng-*
@ -42,3 +46,4 @@ subprojects/zlib-*
subprojects/dirent-*
subprojects/hunspell-*
subprojects/uchardet-*
subprojects/vapoursynth

105
README.md
View File

@ -1,3 +1,106 @@
## arch1t3cht's Aegisub "fork"
Download release builds [here](https://github.com/arch1t3cht/Aegisub/releases), or the latest CI builds [here](https://github.com/arch1t3cht/Aegisub/actions).
The release page also has detailed list of all changes and new features. If you're interested in the technical details or want to compile yourself, read on.
### Don't we have enough Aegisub forks already??
We absolutely do, and I'm aware that adding another one [doesn't sound like](https://xkcd.com/927/) a [good idea on paper](https://cdn.discordapp.com/attachments/425357202963038208/1007103606421459004/unknown.png). However,
- None of the existing forks are completely satisfying at the moment:
- [wangqr's fork](https://github.com/wangqr/Aegisub) is actively maintained, but focussing more on stability. It's missing most of the modern features.
- [AegisubDC](https://github.com/Ristellise/AegisubDC) has the most modern features (in particular video-panning), but is Windows-only and not actively maintained anymore.
- [The TypesettingTools fork](https://github.com/TypesettingTools/Aegisub) is the one that will one day become the upstream version and builds relatively effortlessly on all operating systems, but at the moment it's not moving much. It's the base for this fork, and I hope to one day merge most of these additions into it.
- Only PR'ing the changes in here to various forks would cause even more chaos
- ~~I try to convince myself that this isn't really a "fork" in the traditional sense - one which aims to provide extended support and stability fixes. It's a collection of new feature additions which I built myself, together with some of the most important new features floating around other forks.~~ At this point it's probably too late to still be saying this. Still, the general mission hasn't changed. This fork collects new features and critical bugfixes, but won't be putting extra time into maintenance aspects like cleanup and refactors. Partly, this is also because any big refactors would make it harder to pull these changes into upstream repositories or future forks.
While this is usually also the version of Aegisub I'm currently using, I make no promises on stability. **Don't** use this version if you're just looking for any version of Aegisub - this is mostly intended for typesetting and other advanced usage.
### Organization
Being a collection of different feature additions, this repository consists of a set of branches for different features, so that they can easily be merged into other repositories. The [`feature`](https://github.com/arch1t3cht/Aegisub/tree/feature) branch merges together all the features I deem as currently usable. Due to the structure of the repository, I will be force-pushing to this branch and some of the individual branches very frequently, so they're not ideal for basing further branches on.
The `cibuilds` branch makes some CI builds of snapshots of `feature` at relevant points in time.
### Branch/Feature list
This list is for navigating the repository. Go to the [release page](https://github.com/arch1t3cht/Aegisub/releases) for a more structured changelog.
- [`folding`](https://github.com/arch1t3cht/Aegisub/tree/folding): Add the ability to visually group and collapse lines in the subtitle grid
- [`lua_api`](https://github.com/arch1t3cht/Aegisub/tree/lua_api): Add new functions to the Lua automation API, like controlling the selection or cursor in the text edit box
- [`vector_clip_actions`](https://github.com/arch1t3cht/Aegisub/tree/vector_clip_actions): Make the different modes of the vector clip tool (lines, bezier curves, adding points, etc) bindable to hotkeys
- [`color_picker_fix2`](https://github.com/arch1t3cht/Aegisub/tree/color_picker_fix2): Add an option (under "Interface") to restrict the color picker to the window, which fixes the color picker on Linux in a lot of cases.
- [`avisynth`](https://github.com/arch1t3cht/Aegisub/tree/avisynth): Reenable Avisynth support on Windows and enable Avisynth on Linux
- [`bestsource`](https://github.com/arch1t3cht/Aegisub/tree/bestsource): Add BestSource audio and video source. This source is slower than others by multiple orders of magnitude, but in exchange it can guarantee exact seeking.
- [`vapoursynth`](https://github.com/arch1t3cht/Aegisub/tree/vapoursynth): Add Vapoursynth audio and video source
- [`bugfixes`](https://github.com/arch1t3cht/Aegisub/tree/bugfixes): Various fixes necessary for compilation. Most branches are based on this.
- [`workarounds`](https://github.com/arch1t3cht/Aegisub/tree/workarounds): Same as `bugfixes`, but these are hacky fixes that probably shouldn't be pulled without more work.
- [`fixes`](https://github.com/arch1t3cht/Aegisub/tree/fixes): Miscellaneous bugfixes
- [`misc`](https://github.com/arch1t3cht/Aegisub/tree/misc): Other miscellaneous additions
- [`wangqr_gui`](https://github.com/arch1t3cht/Aegisub/tree/wangqr_gui): Merge wangqr's changes regarding the GUI. In particular, add high-DPI compatibility.
- [`misc_dc`](https://github.com/arch1t3cht/Aegisub/tree/misc_dc): Miscellaneous changes taken from AegisubDC
- [`xa2-ds`](https://github.com/arch1t3cht/Aegisub/tree/xa2-ds): Add XAudio2 backend and allow stereo playback for some other backends, by wangqr and Shinon.
- [`stereo`](https://github.com/arch1t3cht/Aegisub/tree/stereo): Add multi-channel support for the other audio backends where possible.
- [`video_panning_option`](https://github.com/arch1t3cht/Aegisub/tree/video_panning_option): Merge [moex3's video zoom and panning](https://github.com/TypesettingTools/Aegisub/pull/150), with several bugfixes and more options to control zoom behavior
- [`spectrum-frequency-mapping`](https://github.com/arch1t3cht/Aegisub/tree/spectrum-frequency-mapping): Merge EleonoreMizo's [spectrum display improvements](https://github.com/TypesettingTools/Aegisub/pull/94), and also make Shift+Scroll vertically zoom the audio display
- [`wangqr_time_video`](https://github.com/arch1t3cht/Aegisub/tree/wangqr_time_video): Merge wangqr's feature adding a tool for timing subtitles to changes in the video
### Troubleshooting
I'll gladly take any bug reports, but if you encounter an issue, please check first if it occurs only on my fork, or also on [earlier TSTools builds](https://github.com/TypesettingTools/Aegisub/actions).
If it wasn't introduced by my fork, I can still take a look, but I can't promise anything.
You can find me for support on various servers, including the cave and the TSTools server linked below.
#### Aegisub on Linux doesn't recognize my GTK theme
This is probably because you're building with wxgtk2. Building with wxgtk3 fixes this, but causes some problems of its own (notably the broken color picker, occasional crashes when opening file dialogs from automation scripts, and general layouting issues).
The exact way of switching depends on your Linux distribution, but essentially you need to ensure that `wx-config` or the next best variant of it points to wxgtk3. If it points to wxgtk2 by default and deinstalling wxgtk2 isn't an option, you can also temporarily move it out of the path or use a `native-file` in your meson project. Then, fully reconfigure meson using `meson configure --clearcache` and `meson setup --reconfigure`.
#### The video is desynced / Frames don't appear at the right time
This is probably due to the ffms2 seeking bug ([#394](https://github.com/FFMS/ffms2/issues/394)). On Windows, this specific regression shouldn't happen anymore. On Linux, you need to install the latest git version of ffms2 - for example the [`ffms2-git`](https://aur.archlinux.org/packages/ffms2-git) AUR package on Arch linux, or just compile it yourself.
If it's not because of this particular bug, you can also try an alternative video source like LSMASHSource via Avisynth or Vapoursynth, or BestSource.
#### On Windows: Aegisub crashes whenever I open a video
If you're compiling yourself, try adding `--force-fallback-for=zlib` to the meson options.
### Compilation
If you're just looking to install Aegisub, you might want to check out the [releases page](https://github.com/arch1t3cht/Aegisub/releases) or the [CI builds](https://github.com/arch1t3cht/Aegisub/actions) first.
For compilation on Windows, see the TSTools documentation below. Also check the [GitHub workflow](https://github.com/arch1t3cht/Aegisub/blob/cibuilds/.github/workflows/ci.yml) for the project arguments.
On Arch Linux, there is an AUR package called [aegisub-arch1t3cht-git](https://aur.archlinux.org/packages/aegisub-arch1t3cht-git). It's not maintained by me but seems to work.
On other Linux distributions or for manual compilation you can use this package or the [TSTools PKGBUILD](https://aur.archlinux.org/packages/aegisub-ttools-meson-git) as a reference, in particular for installing the necessary dependencies if you don't want to compile them yourself.
If all dependencies are installed:
- Install Meson
- Clone the repository
- In the repository, run `meson setup build --buildtype=release` for the default configuration. See below for further options.
- `cd` to the `build` directory and run `ninja`
- You'll get an `aegisub` binary in the `build` folder. To install it to a system-wide location, run `ninja install`. To install to `/usr` instead of `/usr/local`, pass `--prefix=/usr` when configuring or reconfiguring meson.
- When recompiling after pulling new commits, skip the `meson setup` setup and just immediately run `ninja` from the build directory - even when the build configuration changed.
#### Compilation flags
Some features are not enabled by default. To enable them, pass `-D<feature>=enabled` with the `meson setup` command:
- `-Davisynth=enabled`: Avisynth support
- `-Dbestsource=enabled`: BestSource
- `-Dvapoursynth=enabled`: Vapoursynth support
You can also disable options that are active by default in the same way. Check the file `meson_options.txt` for all options.
To change the options of an existing build directory, run `meson setup --reconfigure <new arguments>` from inside the `build` directory.
### Dependencies
Apart from the dependencies for the TSTools version, there are some additional dependencies. These are cloned and compiled from scratch if not found, but you might want to install binaries instead:
- `jansson`: For BestSource
- `ffmpeg`: Becomes a direct dependency when compiling with BestSource
- `avisynth` (or `avisynthplus`): Optional run-time dependency for the Avisynth source
- `vapoursynth`: Optional run-time dependency for the VapourSynth source
The following VapourSynth plugins are used by the default scripts set in the default configuration:
- [`lsmas`](https://github.com/AkarinVS/L-SMASH-Works): For LWLibavSource
- [`bas`](https://github.com/vapoursynth/bestaudiosource): For BestAudioSource
- [`wwxd`](https://github.com/dubhater/vapoursynth-wwxd) and [`scxvid`](https://github.com/dubhater/vapoursynth-scxvid) (depending on settings): For keyframe generation
# Aegisub
For binaries and general information [see the homepage](http://www.aegisub.org).
@ -30,7 +133,7 @@ All other dependencies are either stored in the repository or are included as su
Building:
1. Clone Aegisub's repository: `git clone https://github.com/TypesettingTools/Aegisub.git`
1. Clone Aegisub's repository: `git clone https://github.com/arch1t3cht/Aegisub.git`
2. From the Visual Studio "x64 Native Tools Command Prompt", generate the build directory: `meson build -Ddefault_library=static` (if building for release, add `--buildtype=release`)
3. Build with `cd build` and `ninja`

View File

@ -41,3 +41,7 @@ install_data(
'include/aegisub/unicode.moon',
'include/aegisub/util.moon',
install_dir: automation_dir / 'include' / 'aegisub')
install_data(
'vapoursynth/aegisub_vs.py',
install_dir: automation_dir / 'vapoursynth')

View File

@ -0,0 +1,69 @@
Video Frame functions in Automation 4
This file describes the interface used for reading frames from loaded videos.
---
Get a specific frame from the currently loaded video on which multiple other
functions are defined.
function aegisub.get_frame(frame_number, withSubtitles)
@frame_number (number)
Number of frame to retrieve.
@withSubtitles (boolean)
Optional. Whether to load with subtitles drawn on to the frame.
Returns: frame (userdata)
The frame object defines multiple other functions. See below.
---
Get width of frame object.
function frame:width()
Returns: number
Width in pixels.
---
Get height of frame object.
function frame:height()
Returns: number
Height in pixels.
---
Get RGB pixel value at a certain position of frame object.
function frame:frame:getPixel(x, y)
@x (number)
Pixel to retrieve on the x-axis
@y (number)
Pixel to retrieve on the y-axis
Returns: number
Integer value representing the RGB pixel value.
---
Get ASS formated pixel value at a certain position of frame object.
function frame:getPixelFormatted(x, y)
@x (number)
Pixel to retrieve on the x-axis
@y (number)
Pixel to retrieve on the y-axis
Returns: string
String in ASS format representing the pixel value. e.g. "&H0073FF&"
---

View File

@ -0,0 +1,66 @@
Automation 4 Gui Functions
This document describes the available Automation 4 functions for
controlling the editor's graphical interface. These all reside in the
table aegisub.gui .
---
Getting and setting the selection and cursor in the text edit box
This set of functions controls the selection in the text edit box.
All indices are counted starting from 1, following Lua conventions.
The setter functions are applied after all subtitle changes have been
applied. Only the latest update is applied.
The getter functions return the state after the latest update by
the setter functions, or the original state if there were none.
function aegisub.gui.get_cursor()
Returns: 1 number
1. The position of the cursor in the text edit field.
---
function aegisub.gui.get_selection()
Returns: 2 values, all numbers.
1. Starting position of the selection.
2. Ending position of the selection, always larger or equal
than the stating position.
---
function aegisub.gui.set_cursor(position)
@position (number)
The new position of the cursor.
Returns: 0 values
---
function aegisub.gui.set_selection(start, end)
@start (number)
The new start of the selection.
@end (number)
The new end of the selection, i.e. where the cursor will be.
Can be smaller than the start, in which case the cursor will
be on the left side of the selection.
Returns: 0 values
---
Determining whether there are unsaved changes
function aegisub.gui.is_modified()
Returns: 1 boolean
1. Whether the current file has unsaved changes.
---

View File

@ -0,0 +1,273 @@
"""
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
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
store cache files. This directory is cleaned by Aegisub when it gets too
large (as defined by Aegisub's configuration).
The provider reads the video from the script's 0-th output node. By default,
the video is assumed to be CFR. The script can pass further information to
Aegisub using the following variables:
- __aegi_timecodes: List[int] | str: The timecodes for the video, or the
path to a timecodes file.
- __aegi_keyframes: List[int] | str: List of frame numbers to load as
keyframes, or the path to a keyframes file.
- __aegi_hasaudio: int: If nonzero, Aegisub will try to load an audio track
from the same file.
This module provides some utility functions to obtain timecodes, keyframes, and
other data.
"""
import os
import os.path
import re
from enum import Enum
from tkinter.messagebox import askyesno
from collections import deque
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
would use for a .lwi file.
"""
max_len = 254
extension = ".lwi"
if len(filename) + len(extension) > max_len:
filename = filename[-(max_len + len(extension)):]
return "".join(("_" if c in "/\\:" else c) for c in filename) + extension
def make_keyframes_filename(filename: str) -> str:
"""
Given a path `path/to/file.mkv`, will return the path
`path/to/file_keyframes.txt`.
"""
extlen = filename[::-1].find(".") + 1
return filename[:len(filename) - extlen] + "_keyframes.txt"
lwindex_re1 = re.compile(r"Index=(?P<Index>-?[0-9]+),POS=(?P<POS>-?[0-9]+),PTS=(?P<PTS>-?[0-9]+),DTS=(?P<DTS>-?[0-9]+),EDI=(?P<EDI>-?[0-9]+)")
lwindex_re2 = re.compile(r"Key=(?P<Key>-?[0-9]+),Pic=(?P<Pic>-?[0-9]+),POC=(?P<POC>-?[0-9]+),Repeat=(?P<Repeat>-?[0-9]+),Field=(?P<Field>-?[0-9]+)")
streaminfo_re = re.compile(r"Codec=(?P<Codec>[0-9]+),TimeBase=(?P<TimeBase>[0-9\/]+),Width=(?P<Width>[0-9]+),Height=(?P<Height>[0-9]+),Format=(?P<Format>[0-9a-zA-Z]+),ColorSpace=(?P<ColorSpace>[0-9]+)")
class LWIndexFrame:
pts: int
key: int
def __init__(self, raw: list[str]):
match1 = lwindex_re1.match(raw[0])
match2 = lwindex_re2.match(raw[1])
if not match1 or not match2:
raise ValueError("Invalid lwindex format")
self.pts = int(match1.group("PTS"))
self.key = int(match2.group("Key"))
def __int__(self) -> int:
return self.pts
def info_from_lwindex(indexfile: str) -> Dict[str, List[int]]:
"""
Given a path to an .lwi file, will return a dictionary containing
information about the video, with the keys
- timcodes: The timecodes.
- keyframes: Array of frame numbers of keyframes.
"""
with open(indexfile, encoding="latin1") as f:
index = f.read().splitlines()
indexstart, indexend = index.index("</StreamInfo>") + 1, index.index("</LibavReaderIndex>")
frames = [LWIndexFrame(index[i:i+2]) for i in range(indexstart, indexend, 2)]
frames.sort(key=int)
streaminfo = streaminfo_re.match(index[indexstart - 2])
if not streaminfo:
raise ValueError("Invalid lwindex format")
timebase_num, timebase_den = [int(i) for i in streaminfo.group("TimeBase").split("/")]
return {
"timecodes": [(f.pts * 1000 * timebase_num) // timebase_den for f in frames],
"keyframes": [i for i, f in enumerate(frames) if f.key],
}
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))
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.")
clip = core.lsmas.LWLibavSource(source=filename, cachefile=cachefile, **kwargs)
return clip, info_from_lwindex(cachefile)
def make_keyframes(clip: vs.VideoNode, use_scxvid: bool = False,
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.
Will be slightly more efficient with the `akarin` plugin installed.
:param clip: Clip to process.
:param use_scxvid: Whether to use Scxvid. If False, the function uses WWXD.
:param resize_h: Height to resize the clip to before processing.
:param resize_format: Format to convert the clip to before processing.
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)
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
def _cb(n: int, f: vs.VideoFrame) -> vs.VideoFrame:
nonlocal done
keyframes[n] = f.props._SceneChangePrev if use_scxvid else f.props.Scenechange # type: ignore
done += 1
if done % (clip.num_frames // 25) == 0:
vs.core.log_message(vs.MESSAGE_TYPE_INFORMATION, "Detecting keyframes... {}% done.\n".format(100 * done // clip.num_frames))
return f
deque(clip.std.ModifyFrame(clip, _cb).frames(close=True), 0)
vs.core.log_message(vs.MESSAGE_TYPE_INFORMATION, "Done detecting keyframes.\n")
return [n for n in range(clip.num_frames) if keyframes[n]]
def save_keyframes(filename: str, keyframes: List[int]):
"""
Saves a list of keyframes in Aegisub's keyframe format v1 to a file with
the given filename.
"""
with open(filename, "w") as f:
f.write("# keyframe format v1\n")
f.write("fps 0\n")
f.write("".join(f"{n}\n" for n in keyframes))
class GenKeyframesMode(Enum):
NEVER = 0
ALWAYS = 1
ASK = 2
def get_keyframes(filename: str, clip: vs.VideoNode, fallback: str | List[int],
generate: GenKeyframesMode = GenKeyframesMode.ASK, **kwargs: Any) -> str | List[int]:
"""
Looks for a keyframes file for the given filename.
If no file was found, this function can generate a keyframe file for the given clip next
to the given filename using WWXD or Scxvid (see the make_keyframes docstring).
Whether or not keyframes are generated depends on the `generate` argument.
Depending on the `generate` argument, the function will
- always generate keyframes when no file was found
- never generate keyframes when no file was found
(and return the fallback keyframes instead)
- show a dialog to ask the user whether keyframes should be
generated or not
Additional keyword arguments are passed on to make_keyframes.
"""
kffilename = make_keyframes_filename(filename)
if not os.path.exists(kffilename):
if generate == GenKeyframesMode.NEVER:
return fallback
if generate == GenKeyframesMode.ASK and not askyesno("Generate Keyframes", \
"No keyframes file was found for this video file.\nShould Aegisub detect keyframes from the video?\nThis will take a while.", default="no"):
return fallback
vs.core.log_message(vs.MESSAGE_TYPE_INFORMATION, "No keyframes file found, detecting keyframes...\n")
keyframes = make_keyframes(clip, **kwargs)
save_keyframes(kffilename, keyframes)
return kffilename
def check_audio(filename: str, **kwargs: Any) -> bool:
"""
Checks whether the given file has an audio track by trying to open it with
BestAudioSource. Requires the `bas` plugin to return correct results, but
won't crash if it's not installed.
Additional keyword arguments are passed on to BestAudioSource.
"""
try:
ensure_plugin("bas", "BestAudioSource", "")
vs.core.bas.Source(source=filename, **kwargs)
return True
except AttributeError:
pass
except vs.Error:
pass
return False

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.0"
width="64"
height="64"
id="svg2385"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2387" />
<path
d="M 21.534604,6.1472094 51.814439,14.337199 58.897111,53.851308 5.1215609,57.175696 Z"
id="rect2387"
style="display:inline;fill:none;stroke:#80b3ff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
<path
d="m 26.619476,19.563308 17.90644,3.127764 1.752386,21.835103 -25.106058,0.109689 z"
id="rect2387-8"
style="display:inline;fill:#ff0000;stroke:#803300;stroke-width:2.32035;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
</svg>

After

Width:  |  Height:  |  Size: 836 B

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.0"
width="64"
height="64"
id="svg2385"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2387" />
<path
d="M 32,4 V 60 M 48,4 V 60 M 16,4 v 56"
id="rect2387"
style="display:inline;fill:none;stroke:#803300;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
d="M 60,32 H 4 M 60,48 H 4 M 60,16 H 4"
id="rect2387-5"
style="display:inline;fill:none;stroke:#803300;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 791 B

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.0"
width="64"
height="64"
id="svg2385"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2387" />
<path
d="M 21.534604,6.1472094 51.814439,14.337199 58.897111,53.851308 5.1215609,57.175696 Z"
id="rect2387"
style="display:inline;fill:none;stroke:#ff0000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:6, 9;stroke-dashoffset:0;stroke-opacity:1" />
<g
id="g4605"
transform="matrix(1.2858376,0,0,1.2858376,-8.5433316,-7.7280751)">
<path
d="M 41.542973,29.070062 V 43.827625 L 25.457027,43.82747 V 29.070062 Z"
id="rect2387-8"
style="display:inline;fill:#80b3ff;fill-opacity:1;stroke:#002060;stroke-width:2.2344;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
<path
d="m 28.77085,28.13212 c 0,-5.027712 1.267775,-8.633444 5.083463,-8.633444 3.815688,0 4.57289,3.490135 4.57289,8.763711"
style="display:inline;fill:none;fill-opacity:1;stroke:#002060;stroke-width:3.11081;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path2399" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.0"
width="64"
height="64"
id="svg2385"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2387" />
<path
d="M 21.534604,6.1472094 51.814439,14.337199 58.897111,53.851308 5.1215609,57.175696 Z"
id="rect2387"
style="display:inline;fill:none;stroke:#ff0000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:6, 9;stroke-dashoffset:0;stroke-opacity:1" />
<path
d="M 30.272664,27.88368 H 42.773336 L 36.523,40.384351 v 0 z"
id="rect3294"
style="fill:#ff0000;stroke:#803300;stroke-width:2.50013;stroke-linecap:round;stroke-linejoin:round" />
</svg>

After

Width:  |  Height:  |  Size: 794 B

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.0"
width="64"
height="64"
id="svg2385"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2387" />
<path
d="M 21.534604,6.1472094 51.814439,14.337199 58.897111,53.851308 5.1215609,57.175696 Z"
id="rect2387"
style="display:inline;fill:none;stroke:#ff0000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:6, 9;stroke-dashoffset:0;stroke-opacity:1" />
<path
d="m 37.858975,14.096991 h 12.50005 L 44.109,26.59704 v 0 z"
id="rect3294"
style="fill:#ff0000;stroke:#803300;stroke-width:2.50001;stroke-linecap:round;stroke-linejoin:round" />
</svg>

After

Width:  |  Height:  |  Size: 793 B

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.0"
width="64"
height="64"
id="svg2385"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2387" />
<path
d="M 8,8 H 56 V 56 H 8 Z"
id="rect2391"
style="fill:none;stroke:#ff0000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:6, 9;stroke-dashoffset:0;stroke-opacity:1" />
<path
d="m 37.858519,14.096401 h 12.500872 l -6.250436,12.500871 v 0 z"
id="rect3294"
style="fill:#ff0000;stroke:#803300;stroke-width:2.50017;stroke-linecap:round;stroke-linejoin:round" />
</svg>

After

Width:  |  Height:  |  Size: 721 B

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.0"
width="64"
height="64"
id="svg2385"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2387" />
<path
d="M 21.534604,6.1472094 51.814439,14.337199 58.897111,53.851308 5.1215609,57.175696 Z"
id="rect2387"
style="display:inline;fill:none;stroke:#ff0000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:6, 9;stroke-dashoffset:0;stroke-opacity:1" />
<path
d="m 26.619476,19.563308 17.90644,3.127764 1.752386,21.835103 -25.106058,0.109689 z"
id="rect2387-8"
style="display:inline;fill:#80b3ff;fill-opacity:1;stroke:#002060;stroke-width:2.32035;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 888 B

View File

@ -60,7 +60,11 @@ public:
case dt::ERROR: SetStyling(tok.length, ss::ERROR); break;
case dt::ARG: SetStyling(tok.length, ss::PARAMETER); break;
case dt::COMMENT: SetStyling(tok.length, ss::COMMENT); break;
case dt::DRAWING: SetStyling(tok.length, ss::DRAWING); break;
case dt::DRAWING_CMD:SetStyling(tok.length, ss::DRAWING_CMD);break;
case dt::DRAWING_X: SetStyling(tok.length, ss::DRAWING_X); break;
case dt::DRAWING_Y: SetStyling(tok.length, ss::DRAWING_Y); break;
case dt::DRAWING_ENDPOINT_X: SetStyling(tok.length, ss::DRAWING_ENDPOINT_X); break;
case dt::DRAWING_ENDPOINT_Y: SetStyling(tok.length, ss::DRAWING_ENDPOINT_Y); break;
case dt::TEXT: SetStyling(tok.length, ss::NORMAL); break;
case dt::TAG_NAME: SetStyling(tok.length, ss::TAG); break;
case dt::OPEN_PAREN: case dt::CLOSE_PAREN: case dt::ARG_SEP: case dt::TAG_START:
@ -72,6 +76,8 @@ public:
case dt::WHITESPACE:
if (ranges.size() && ranges.back().type == ss::PARAMETER)
SetStyling(tok.length, ss::PARAMETER);
else if (ranges.size() && ranges.back().type == ss::DRAWING_ENDPOINT_X)
SetStyling(tok.length, ss::DRAWING_ENDPOINT_X); // connect the underline between x and y of endpoints
else
SetStyling(tok.length, ss::NORMAL);
break;
@ -118,6 +124,64 @@ class WordSplitter {
}
}
void SplitDrawing(size_t &i) {
size_t starti = i;
// First, split into words
size_t dpos = pos;
size_t tlen = 0;
bool tokentype = text[pos] == ' ' || text[pos] == '\t';
while (tlen < tokens[i].length) {
bool newtype = text[dpos] == ' ' || text[dpos] == '\t';
if (newtype != tokentype) {
tokentype = newtype;
SwitchTo(i, tokentype ? dt::DRAWING_FULL : dt::WHITESPACE, tlen);
tokens[i].type = tokentype ? dt::WHITESPACE : dt::DRAWING_FULL;
tlen = 0;
}
++tlen;
++dpos;
}
// Then, label all the tokens
dpos = pos;
int num_coord = 0;
char lastcmd = ' ';
for (size_t j = starti; j <= i; j++) {
char c = text[dpos];
if (tokens[j].type == dt::WHITESPACE) {
} else if (c == 'm' || c == 'n' || c == 'l' || c == 's' || c == 'b' || c == 'p' || c == 'c') {
tokens[j].type = dt::DRAWING_CMD;
if (tokens[j].length != 1)
tokens[j].type = dt::ERROR;
if (num_coord % 2 != 0)
tokens[j].type = dt::ERROR;
lastcmd = c;
num_coord = 0;
} else {
bool valid = true;
for (size_t k = 0; k < tokens[j].length; k++) {
char c = text[dpos + k];
if (!((c >= '0' && c <= '9') || c == '.' || c == '+' || c == '-' || c == 'e' || c == 'E')) {
valid = false;
}
}
if (!valid)
tokens[j].type = dt::ERROR;
else if (lastcmd == 'b' && num_coord % 6 >= 4)
tokens[j].type = num_coord % 2 == 0 ? dt::DRAWING_ENDPOINT_X : dt::DRAWING_ENDPOINT_Y;
else
tokens[j].type = num_coord % 2 == 0 ? dt::DRAWING_X : dt::DRAWING_Y;
++num_coord;
}
dpos += tokens[j].length;
}
}
public:
WordSplitter(std::string const& text, std::vector<DialogueToken> &tokens)
: text(text)
@ -131,6 +195,9 @@ public:
size_t len = tokens[i].length;
if (tokens[i].type == dt::TEXT)
SplitText(i);
else if (tokens[i].type == dt::DRAWING_FULL) {
SplitDrawing(i);
}
pos += len;
}
}
@ -163,9 +230,51 @@ void MarkDrawings(std::string const& str, std::vector<DialogueToken> &tokens) {
switch (tokens[i].type) {
case dt::TEXT:
if (in_drawing)
tokens[i].type = dt::DRAWING;
tokens[i].type = dt::DRAWING_FULL;
break;
case dt::TAG_NAME:
if (i + 3 < tokens.size() && (len == 4 || len == 5) && !strncmp(str.c_str() + pos + len - 4, "clip", 4)) {
if (tokens[i + 1].type != dt::OPEN_PAREN)
goto tag_p;
size_t drawing_start = 0;
size_t drawing_end = 0;
// Try to find a vector clip
for (size_t j = i + 2; j < tokens.size(); j++) {
if (tokens[j].type == dt::ARG_SEP) {
if (drawing_start) {
break; // More than two arguents - this is a rectangular clip
}
drawing_start = j + 1;
} else if (tokens[j].type == dt::CLOSE_PAREN) {
drawing_end = j;
break;
} else if (tokens[j].type != dt::WHITESPACE && tokens[j].type != dt::ARG) {
break;
}
}
if (!drawing_end)
goto tag_p;
if (!drawing_start)
drawing_start = i + 2;
if (drawing_end == drawing_start)
goto tag_p;
// We found a clip between drawing_start and drawing_end. Now, join
// all the tokens into one and label it as a drawing.
size_t tokenlen = 0;
for (size_t j = drawing_start; j < drawing_end; j++) {
tokenlen += tokens[j].length;
}
tokens[drawing_start].length = tokenlen;
tokens[drawing_start].type = dt::DRAWING_FULL;
tokens.erase(tokens.begin() + drawing_start + 1, tokens.begin() + drawing_end);
last_ovr_end -= drawing_end - drawing_start - 1;
}
tag_p:
if (len != 1 || i + 1 >= tokens.size() || str[pos] != 'p')
break;
@ -199,7 +308,7 @@ void MarkDrawings(std::string const& str, std::vector<DialogueToken> &tokens) {
case dt::KARAOKE_VARIABLE: break;
case dt::LINE_BREAK: break;
default:
tokens[i].type = in_drawing ? dt::DRAWING : dt::TEXT;
tokens[i].type = in_drawing ? dt::DRAWING_FULL : dt::TEXT;
if (i > 0 && tokens[i - 1].type == tokens[i].type) {
tokens[i - 1].length += tokens[i].length;
tokens.erase(tokens.begin() + i);

View File

@ -21,13 +21,148 @@
#include "libaegisub/log.h"
#include "libaegisub/util.h"
namespace {
template<typename Source>
class ConvertFloatToInt16 {
Source* src;
public:
ConvertFloatToInt16(Source* src) :src(src) {}
int16_t operator[](size_t idx) const {
Source expanded = src[idx] * 32768;
return expanded < -32768 ? -32768 :
expanded > 32767 ? 32767 :
static_cast<int16_t>(expanded);
}
};
// 8 bits per sample is assumed to be unsigned with a bias of 128,
// while everything else is assumed to be signed with zero bias
class ConvertIntToInt16 {
void* src;
int bytes_per_sample;
public:
ConvertIntToInt16(void* src, int bytes_per_sample) :src(src), bytes_per_sample(bytes_per_sample) {}
const int16_t& operator[](size_t idx) const {
return *reinterpret_cast<int16_t*>(reinterpret_cast<char*>(src) + (idx + 1) * bytes_per_sample - sizeof(int16_t));
}
};
class ConvertUInt8ToInt16 {
uint8_t* src;
public:
ConvertUInt8ToInt16(uint8_t* src) :src(src) {}
int16_t operator[](size_t idx) const {
return int16_t(src[idx]-128) << 8;
}
};
template<typename Source>
class DownmixToMono {
Source src;
int channels;
public:
DownmixToMono(Source src, int channels) :src(src), channels(channels) {}
int16_t operator[](size_t idx) const {
int ret = 0;
// Just average the channels together
for (int i = 0; i < channels; ++i)
ret += src[idx * channels + i];
return ret / channels;
}
};
}
namespace agi {
void AudioProvider::FillBufferInt16Mono(int16_t* buf, int64_t start, int64_t count) const {
if (!float_samples && bytes_per_sample == 2 && channels == 1) {
FillBuffer(buf, start, count);
return;
}
void* buff = malloc(bytes_per_sample * count * channels);
FillBuffer(buff, start, count);
if (channels == 1) {
if (float_samples) {
if (bytes_per_sample == sizeof(float))
for (int64_t i = 0; i < count; ++i)
buf[i] = ConvertFloatToInt16<float>(reinterpret_cast<float*>(buff))[i];
else if (bytes_per_sample == sizeof(double))
for (int64_t i = 0; i < count; ++i)
buf[i] = ConvertFloatToInt16<double>(reinterpret_cast<double*>(buff))[i];
}
else {
if (bytes_per_sample == sizeof(uint8_t))
for (int64_t i = 0; i < count; ++i)
buf[i] = ConvertUInt8ToInt16(reinterpret_cast<uint8_t*>(buff))[i];
else
for (int64_t i = 0; i < count; ++i)
buf[i] = ConvertIntToInt16(buff, bytes_per_sample)[i];
}
}
else {
if (float_samples) {
if (bytes_per_sample == sizeof(float))
for (int64_t i = 0; i < count; ++i)
buf[i] = DownmixToMono<ConvertFloatToInt16<float> >(ConvertFloatToInt16<float>(reinterpret_cast<float*>(buff)), channels)[i];
else if (bytes_per_sample == sizeof(double))
for (int64_t i = 0; i < count; ++i)
buf[i] = DownmixToMono<ConvertFloatToInt16<double> >(ConvertFloatToInt16<double>(reinterpret_cast<double*>(buff)), channels)[i];
}
else {
if (bytes_per_sample == sizeof(uint8_t))
for (int64_t i = 0; i < count; ++i)
buf[i] = DownmixToMono<ConvertUInt8ToInt16>(ConvertUInt8ToInt16(reinterpret_cast<uint8_t*>(buff)), channels)[i];
else
for (int64_t i = 0; i < count; ++i)
buf[i] = DownmixToMono<ConvertIntToInt16>(ConvertIntToInt16(buff, bytes_per_sample), channels)[i];
}
}
free(buff);
}
// This entire file has turned into a mess. For now I'm just following the pattern of the wangqr code, but
// this should really be restructured entirely again. The original type constructor-based system worked very well - it could
// just give downmix/conversion control to the players instead.
void AudioProvider::GetAudioWithVolume(void *buf, int64_t start, int64_t count, double volume) const {
GetAudio(buf, start, count);
if (volume == 1.0) return;
if (bytes_per_sample != 2)
throw agi::InternalError("GetAudioWithVolume called on unconverted audio stream");
int64_t n = count * GetChannels();
if (float_samples) {
if (bytes_per_sample == sizeof(float)) {
float *buff = reinterpret_cast<float *>(buf);
for (int64_t i = 0; i < n; ++i)
buff[i] = static_cast<float>(buff[i] * volume);
} else if (bytes_per_sample == sizeof(double)) {
double *buff = reinterpret_cast<double *>(buf);
for (int64_t i = 0; i < n; ++i)
buff[i] = buff[i] * volume;
}
}
else {
if (bytes_per_sample == sizeof(uint8_t)) {
uint8_t *buff = reinterpret_cast<uint8_t *>(buf);
for (int64_t i = 0; i < n; ++i)
buff[i] = util::mid(0, static_cast<int>(((int) buff[i] - 128) * volume + 128), 0xFF);
} else if (bytes_per_sample == sizeof(int16_t)) {
int16_t *buff = reinterpret_cast<int16_t *>(buf);
for (int64_t i = 0; i < n; ++i)
buff[i] = util::mid(-0x8000, static_cast<int>(buff[i] * volume), 0x7FFF);
} else if (bytes_per_sample == sizeof(int32_t)) {
int32_t *buff = reinterpret_cast<int32_t *>(buf);
for (int64_t i = 0; i < n; ++i)
buff[i] = static_cast<int32_t>(buff[i] * volume);
} else if (bytes_per_sample == sizeof(int64_t)) {
int64_t *buff = reinterpret_cast<int64_t *>(buf);
for (int64_t i = 0; i < n; ++i)
buff[i] = static_cast<int64_t>(buff[i] * volume);
}
}
}
void AudioProvider::GetInt16MonoAudioWithVolume(int16_t *buf, int64_t start, int64_t count, double volume) const {
GetInt16MonoAudio(buf, start, count);
if (volume == 1.0) return;
auto buffer = static_cast<int16_t *>(buf);
for (size_t i = 0; i < (size_t)count; ++i)
@ -75,6 +210,39 @@ void AudioProvider::GetAudio(void *buf, int64_t start, int64_t count) const {
}
}
void AudioProvider::GetInt16MonoAudio(int16_t* buf, int64_t start, int64_t count) const {
if (start < 0) {
memset(buf, 0, sizeof(int16_t) * std::min(-start, count));
buf -= start;
count += start;
start = 0;
}
if (start + count > num_samples) {
int64_t zero_count = std::min(count, start + count - num_samples);
count -= zero_count;
memset(buf + count, 0, sizeof(int16_t) * zero_count);
}
if (count <= 0) return;
try {
FillBufferInt16Mono(buf, start, count);
}
catch (AudioDecodeError const& e) {
// We don't have any good way to report errors here, so just log the
// failure and return silence
LOG_E("audio_provider") << e.GetMessage();
memset(buf, 0, sizeof(int16_t) * count);
return;
}
catch (...) {
LOG_E("audio_provider") << "Unknown audio decoding error";
memset(buf, 0, sizeof(int16_t) * count);
return;
}
}
namespace {
class writer {
io::Save outfile;
@ -114,7 +282,7 @@ void SaveAudioClip(AudioProvider const& provider, fs::path const& path, int star
out.write("WAVEfmt ");
out.write<int32_t>(16); // Size of chunk
out.write<int16_t>(1); // compression format (PCM)
out.write<int16_t>(provider.AreSamplesFloat() ? 3 : 1); // compression format (1: WAVE_FORMAT_PCM, 3: WAVE_FORMAT_IEEE_FLOAT)
out.write<int16_t>(provider.GetChannels());
out.write<int32_t>(provider.GetSampleRate());
out.write<int32_t>(provider.GetSampleRate() * provider.GetChannels() * provider.GetBytesPerSample());

View File

@ -22,119 +22,19 @@
#include <limits>
using namespace agi;
/// Anything integral -> 16 bit signed machine-endian audio converter
namespace {
template<class Target>
class BitdepthConvertAudioProvider final : public AudioProviderWrapper {
int src_bytes_per_sample;
mutable std::vector<uint8_t> src_buf;
class ConvertAudioProvider final : public AudioProviderWrapper {
public:
BitdepthConvertAudioProvider(std::unique_ptr<AudioProvider> src) : AudioProviderWrapper(std::move(src)) {
if (bytes_per_sample > 8)
throw AudioProviderError("Audio format converter: audio with bitdepths greater than 64 bits/sample is currently unsupported");
src_bytes_per_sample = bytes_per_sample;
bytes_per_sample = sizeof(Target);
}
void FillBuffer(void *buf, int64_t start, int64_t count64) const override {
auto count = static_cast<size_t>(count64);
assert(count == count64);
src_buf.resize(count * src_bytes_per_sample * channels);
source->GetAudio(src_buf.data(), start, count);
auto dest = static_cast<int16_t*>(buf);
for (int64_t i = 0; i < count * channels; ++i) {
int64_t sample = 0;
// 8 bits per sample is assumed to be unsigned with a bias of 127,
// while everything else is assumed to be signed with zero bias
if (src_bytes_per_sample == 1)
sample = src_buf[i] - 128;
else {
for (int j = src_bytes_per_sample; j > 0; --j) {
sample <<= 8;
sample += src_buf[i * src_bytes_per_sample + j - 1];
}
}
if (static_cast<size_t>(src_bytes_per_sample) > sizeof(Target))
sample /= 1LL << (src_bytes_per_sample - sizeof(Target)) * 8;
else if (static_cast<size_t>(src_bytes_per_sample) < sizeof(Target))
sample *= 1LL << (sizeof(Target) - src_bytes_per_sample ) * 8;
dest[i] = static_cast<Target>(sample);
}
}
};
/// Floating point -> 16 bit signed machine-endian audio converter
template<class Source, class Target>
class FloatConvertAudioProvider final : public AudioProviderWrapper {
mutable std::vector<Source> src_buf;
public:
FloatConvertAudioProvider(std::unique_ptr<AudioProvider> src) : AudioProviderWrapper(std::move(src)) {
bytes_per_sample = sizeof(Target);
ConvertAudioProvider(std::unique_ptr<AudioProvider> src) : AudioProviderWrapper(std::move(src)) {
float_samples = false;
}
void FillBuffer(void *buf, int64_t start, int64_t count64) const override {
auto count = static_cast<size_t>(count64);
assert(count == count64);
src_buf.resize(count * channels);
source->GetAudio(&src_buf[0], start, count);
auto dest = static_cast<Target*>(buf);
for (size_t i = 0; i < static_cast<size_t>(count * channels); ++i) {
Source expanded;
if (src_buf[i] < 0)
expanded = static_cast<Target>(-src_buf[i] * std::numeric_limits<Target>::min());
else
expanded = static_cast<Target>(src_buf[i] * std::numeric_limits<Target>::max());
dest[i] = expanded < std::numeric_limits<Target>::min() ? std::numeric_limits<Target>::min() :
expanded > std::numeric_limits<Target>::max() ? std::numeric_limits<Target>::max() :
static_cast<Target>(expanded);
}
}
};
/// Non-mono 16-bit signed machine-endian -> mono 16-bit signed machine endian converter
class DownmixAudioProvider final : public AudioProviderWrapper {
int src_channels;
mutable std::vector<int16_t> src_buf;
public:
DownmixAudioProvider(std::unique_ptr<AudioProvider> src) : AudioProviderWrapper(std::move(src)) {
src_channels = channels;
channels = 1;
bytes_per_sample = sizeof(int16_t);
}
void FillBuffer(void *buf, int64_t start, int64_t count64) const override {
auto count = static_cast<size_t>(count64);
assert(count == count64);
src_buf.resize(count * src_channels);
source->GetAudio(&src_buf[0], start, count);
auto dst = static_cast<int16_t*>(buf);
// Just average the channels together
while (count-- > 0) {