diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c9e7f1f7..a2d154e95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,8 @@ jobs: -Dfribidi:docs=false -Dlibass:fontconfig=disabled -Davisynth=enabled + -Dbestsource=enabled + -Dvapoursynth=enabled #- { # name: Windows MinGW, # os: windows-latest, @@ -56,7 +58,7 @@ jobs: 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 -Davisynth=enabled -Dbestsource=enabled -Dvapoursynth=enabled } steps: diff --git a/.gitignore b/.gitignore index 7d31eeed4..41d7bf6cf 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ tools/repack-thes-dict.dSYM # Meson build*/ subprojects/avisynth +subprojects/bestsource/ subprojects/boost*/ subprojects/cairo* subprojects/ffmpeg @@ -30,6 +31,7 @@ subprojects/glib* subprojects/googletest-* subprojects/harfbuzz subprojects/icu +subprojects/jansson subprojects/libass subprojects/libffi* subprojects/libpng-* @@ -42,3 +44,4 @@ subprojects/zlib-* subprojects/dirent-* subprojects/hunspell-* subprojects/uchardet-* +subprojects/vapoursynth diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 4647a9147..000000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,541 +0,0 @@ -cmake_minimum_required(VERSION 3.14) -cmake_policy(SET CMP0074 NEW) - -project(Aegisub) - -set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) - -include_directories("build") -include_directories("libaegisub/include") -include_directories("vendor/luajit/include") - -add_library(libaegisub STATIC - libaegisub/common/parser.cpp - libaegisub/ass/dialogue_parser.cpp - libaegisub/ass/time.cpp - libaegisub/ass/uuencode.cpp - libaegisub/audio/provider.cpp - libaegisub/audio/provider_convert.cpp - libaegisub/audio/provider_dummy.cpp - libaegisub/audio/provider_hd.cpp - libaegisub/audio/provider_lock.cpp - libaegisub/audio/provider_pcm.cpp - libaegisub/audio/provider_ram.cpp - libaegisub/common/cajun/elements.cpp - libaegisub/common/cajun/reader.cpp - libaegisub/common/cajun/writer.cpp - libaegisub/lua/modules/lfs.cpp - libaegisub/lua/modules/re.cpp - libaegisub/lua/modules/unicode.cpp - libaegisub/lua/modules/lpeg.c - libaegisub/lua/modules.cpp - libaegisub/lua/script_reader.cpp - libaegisub/lua/utils.cpp - libaegisub/common/calltip_provider.cpp - libaegisub/common/character_count.cpp - libaegisub/common/charset.cpp - libaegisub/common/charset_6937.cpp - libaegisub/common/charset_conv.cpp - libaegisub/common/color.cpp - libaegisub/common/file_mapping.cpp - libaegisub/common/format.cpp - libaegisub/common/fs.cpp - libaegisub/common/hotkey.cpp - libaegisub/common/io.cpp - libaegisub/common/json.cpp - libaegisub/common/kana_table.cpp - libaegisub/common/karaoke_matcher.cpp - libaegisub/common/keyframe.cpp - libaegisub/common/line_iterator.cpp - libaegisub/common/log.cpp - libaegisub/common/mru.cpp - libaegisub/common/option.cpp - libaegisub/common/option_value.cpp - libaegisub/common/path.cpp - libaegisub/common/thesaurus.cpp - libaegisub/common/util.cpp - libaegisub/common/vfr.cpp - libaegisub/common/ycbcr_conv.cpp - libaegisub/common/dispatch.cpp -) -if (UNIX) - target_sources(libaegisub PRIVATE - libaegisub/unix/access.cpp - libaegisub/unix/fs.cpp - libaegisub/unix/log.cpp - libaegisub/unix/path.cpp - libaegisub/unix/util.cpp - ) -elseif(WIN32) - target_sources(libaegisub PRIVATE - libaegisub/windows/access.cpp - libaegisub/windows/charset_conv_win.cpp - libaegisub/windows/fs.cpp - libaegisub/windows/lagi_pre.cpp - libaegisub/windows/log_win.cpp - libaegisub/windows/path_win.cpp - libaegisub/windows/util_win.cpp - ) -endif(UNIX) -SET_TARGET_PROPERTIES(libaegisub PROPERTIES PREFIX "") - -add_library(luabins STATIC - vendor/luabins/src/fwrite.c - vendor/luabins/src/load.c - vendor/luabins/src/luabins.c - vendor/luabins/src/luainternals.c - vendor/luabins/src/save.c - vendor/luabins/src/savebuffer.c - vendor/luabins/src/write.c -) - -add_executable(luajit-minilua vendor/luajit/src/host/minilua.c) -if (NOT MSVC) -target_link_libraries(luajit-minilua m) -endif(NOT MSVC) -add_custom_command(TARGET luajit-minilua POST_BUILD - COMMAND ${CMAKE_COMMAND} -E make_directory ${PROJECT_SOURCE_DIR}/vendor/luajit/src/gen - COMMAND luajit-minilua ../dynasm/dynasm.lua -D P64 -D JIT -D FFI -D FPU -D HFABI -D VER= -o gen/buildvm_arch.h vm_x86.dasc - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/vendor/luajit/src -) -add_custom_command(TARGET luajit-minilua POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_SOURCE_DIR}/src/libresrc/default_config_win.json ${PROJECT_SOURCE_DIR}/src/libresrc/default_config_platform.json - COMMAND luajit-minilua ../../tools/respack.lua manifest.respack default_config.cpp default_config.h - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/src/libresrc - BYPRODUCTS ${PROJECT_SOURCE_DIR}/src/libresrc/default_config.cpp ${PROJECT_SOURCE_DIR}/src/libresrc/default_config.h -) -add_custom_command(TARGET luajit-minilua POST_BUILD - COMMAND luajit-minilua ../../tools/respack.lua manifest.respack ../libresrc/bitmap.cpp ../libresrc/bitmap.h - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/src/bitmaps - BYPRODUCTS ${PROJECT_SOURCE_DIR}/src/libresrc/bitmap.cpp ${PROJECT_SOURCE_DIR}/src/libresrc/bitmap.h -) - -add_executable(luajit-buildvm - vendor/luajit/src/host/buildvm.c - vendor/luajit/src/host/buildvm_asm.c - vendor/luajit/src/host/buildvm_peobj.c - vendor/luajit/src/host/buildvm_lib.c - vendor/luajit/src/host/buildvm_fold.c -) -target_include_directories(luajit-buildvm PRIVATE vendor/luajit/src vendor/luajit/src/gen) -add_dependencies(luajit-buildvm luajit-minilua) -if(UNIX) - add_custom_command(TARGET luajit-buildvm POST_BUILD - COMMAND luajit-buildvm -m elfasm -o lj_vm.s - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/vendor/luajit/src - BYPRODUCTS ${PROJECT_SOURCE_DIR}/vendor/luajit/src/lj_vm.s - ) - set_property(SOURCE vendor/luajit/src/lj_vm.s PROPERTY LANGUAGE C) -elseif(MSVC) - add_custom_command(TARGET luajit-buildvm POST_BUILD - COMMAND luajit-buildvm -m peobj -o lj_vm.obj - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/vendor/luajit/src - BYPRODUCTS ${PROJECT_SOURCE_DIR}/vendor/luajit/src/lj_vm.obj - ) -endif(UNIX) -add_custom_command(TARGET luajit-buildvm POST_BUILD - COMMAND luajit-buildvm -m ffdef -o gen/lj_ffdef.h lib_base.c lib_math.c lib_bit.c lib_string.c lib_table.c lib_io.c lib_os.c lib_package.c lib_debug.c lib_jit.c lib_ffi.c - COMMAND luajit-buildvm -m bcdef -o gen/lj_bcdef.h lib_base.c lib_math.c lib_bit.c lib_string.c lib_table.c lib_io.c lib_os.c lib_package.c lib_debug.c lib_jit.c lib_ffi.c - COMMAND luajit-buildvm -m folddef -o gen/lj_folddef.h lj_opt_fold.c - COMMAND luajit-buildvm -m recdef -o gen/lj_recdef.h lib_base.c lib_math.c lib_bit.c lib_string.c lib_table.c lib_io.c lib_os.c lib_package.c lib_debug.c lib_jit.c lib_ffi.c - COMMAND luajit-buildvm -m libdef -o gen/lj_libdef.h lib_base.c lib_math.c lib_bit.c lib_string.c lib_table.c lib_io.c lib_os.c lib_package.c lib_debug.c lib_jit.c lib_ffi.c - COMMAND luajit-buildvm -m vmdef -o jit/vmdef.lua lib_base.c lib_math.c lib_bit.c lib_string.c lib_table.c lib_io.c lib_os.c lib_package.c lib_debug.c lib_jit.c lib_ffi.c - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/vendor/luajit/src -) - - -add_library(luajit STATIC - vendor/luajit/src/lj_gc.c - vendor/luajit/src/lj_err.c - vendor/luajit/src/lj_char.c - vendor/luajit/src/lj_bc.c - vendor/luajit/src/lj_obj.c - vendor/luajit/src/lj_str.c - vendor/luajit/src/lj_tab.c - vendor/luajit/src/lj_func.c - vendor/luajit/src/lj_udata.c - vendor/luajit/src/lj_meta.c - vendor/luajit/src/lj_debug.c - vendor/luajit/src/lj_state.c - vendor/luajit/src/lj_dispatch.c - vendor/luajit/src/lj_vmevent.c - vendor/luajit/src/lj_vmmath.c - vendor/luajit/src/lj_strscan.c - vendor/luajit/src/lj_api.c - vendor/luajit/src/lj_lex.c - vendor/luajit/src/lj_parse.c - vendor/luajit/src/lj_bcread.c - vendor/luajit/src/lj_bcwrite.c - vendor/luajit/src/lj_load.c - vendor/luajit/src/lj_ir.c - vendor/luajit/src/lj_opt_mem.c - vendor/luajit/src/lj_opt_fold.c - vendor/luajit/src/lj_opt_narrow.c - vendor/luajit/src/lj_opt_dce.c - vendor/luajit/src/lj_opt_loop.c - vendor/luajit/src/lj_opt_split.c - vendor/luajit/src/lj_opt_sink.c - vendor/luajit/src/lj_mcode.c - vendor/luajit/src/lj_snap.c - vendor/luajit/src/lj_record.c - vendor/luajit/src/lj_crecord.c - vendor/luajit/src/lj_ffrecord.c - vendor/luajit/src/lj_asm.c - vendor/luajit/src/lj_trace.c - vendor/luajit/src/lj_gdbjit.c - vendor/luajit/src/lj_ctype.c - vendor/luajit/src/lj_cdata.c - vendor/luajit/src/lj_cconv.c - vendor/luajit/src/lj_ccall.c - vendor/luajit/src/lj_ccallback.c - vendor/luajit/src/lj_carith.c - vendor/luajit/src/lj_clib.c - vendor/luajit/src/lj_cparse.c - vendor/luajit/src/lj_lib.c - vendor/luajit/src/lj_alloc.c - vendor/luajit/src/lib_aux.c - vendor/luajit/src/lib_base.c - vendor/luajit/src/lib_math.c - vendor/luajit/src/lib_bit.c - vendor/luajit/src/lib_string.c - vendor/luajit/src/lib_table.c - vendor/luajit/src/lib_io.c - vendor/luajit/src/lib_os.c - vendor/luajit/src/lib_package.c - vendor/luajit/src/lib_debug.c - vendor/luajit/src/lib_jit.c - vendor/luajit/src/lib_ffi.c - vendor/luajit/src/lib_init.c -) -if(MSVC) - target_sources(luajit PRIVATE vendor/luajit/src/lj_vm.obj) -else(MSVC) - target_sources(luajit PRIVATE vendor/luajit/src/lj_vm.s) - set_property(SOURCE vendor/luajit/src/lj_vm.s PROPERTY LANGUAGE C) - target_link_libraries(luajit dl) -endif(MSVC) -target_include_directories(luajit PRIVATE vendor/luajit/src/gen) -add_dependencies(luajit luajit-buildvm) -target_compile_definitions(luajit PRIVATE LUAJIT_ENABLE_LUA52COMPAT) - -add_library(resrc STATIC - src/libresrc/bitmap.cpp - src/libresrc/default_config.cpp - src/libresrc/libresrc.cpp -) -add_dependencies(resrc luajit-minilua) - -add_library(csri STATIC - vendor/csri/lib/list.c - vendor/csri/lib/wrap.c - vendor/csri/subhelp/logging.c -) -target_include_directories(csri PRIVATE "vendor/csri/include") -IF (WIN32) - target_include_directories(csri PRIVATE "vendor/csri/lib/win32") - target_sources(csri PRIVATE vendor/csri/lib/win32/enumerate.c) -ELSE() - target_include_directories(csri PRIVATE "vendor/csri/lib/posix") - target_sources(csri PRIVATE vendor/csri/lib/posix/enumerate.c) -ENDIF() - -add_executable(Aegisub WIN32 - src/command/app.cpp - src/command/audio.cpp - src/command/automation.cpp - src/command/command.cpp - src/command/edit.cpp - src/command/grid.cpp - src/command/help.cpp - src/command/keyframe.cpp - src/command/recent.cpp - src/command/subtitle.cpp - src/command/time.cpp - src/command/timecode.cpp - src/command/tool.cpp - src/command/video.cpp - src/command/vis_tool.cpp - src/dialog_about.cpp - src/dialog_align.cpp - src/dialog_attachments.cpp - src/dialog_automation.cpp - src/dialog_autosave.cpp - src/dialog_colorpicker.cpp - src/dialog_detached_video.cpp - src/dialog_dummy_video.cpp - src/dialog_export.cpp - src/dialog_export_ebu3264.cpp - src/dialog_fonts_collector.cpp - src/dialog_jumpto.cpp - src/dialog_kara_timing_copy.cpp - src/dialog_log.cpp - src/dialog_paste_over.cpp - src/dialog_progress.cpp - src/dialog_properties.cpp - src/dialog_resample.cpp - src/dialog_search_replace.cpp - src/dialog_selected_choices.cpp - src/dialog_selection.cpp - src/dialog_shift_times.cpp - src/dialog_spellchecker.cpp - src/dialog_style_editor.cpp - src/dialog_style_manager.cpp - src/dialog_styling_assistant.cpp - src/dialog_text_import.cpp - src/dialog_timing_processor.cpp - src/dialog_translation.cpp - src/dialog_version_check.cpp - src/dialog_video_details.cpp - src/dialog_video_properties.cpp - src/subtitle_format.cpp - src/subtitle_format_ass.cpp - src/subtitle_format_ebu3264.cpp - src/subtitle_format_encore.cpp - src/subtitle_format_microdvd.cpp - src/subtitle_format_mkv.cpp - src/subtitle_format_srt.cpp - src/subtitle_format_ssa.cpp - src/subtitle_format_transtation.cpp - src/subtitle_format_ttxt.cpp - src/subtitle_format_txt.cpp - src/visual_tool.cpp - src/visual_tool_clip.cpp - src/visual_tool_cross.cpp - src/visual_tool_drag.cpp - src/visual_tool_rotatexy.cpp - src/visual_tool_rotatez.cpp - src/visual_tool_scale.cpp - src/visual_tool_vector_clip.cpp - src/MatroskaParser.c - src/aegisublocale.cpp - src/ass_attachment.cpp - src/ass_dialogue.cpp - src/ass_entry.cpp - src/ass_export_filter.cpp - src/ass_exporter.cpp - src/ass_file.cpp - src/ass_karaoke.cpp - src/ass_override.cpp - src/ass_parser.cpp - src/ass_style.cpp - src/ass_style_storage.cpp - src/async_video_provider.cpp - src/audio_box.cpp - src/audio_colorscheme.cpp - src/audio_controller.cpp - src/audio_display.cpp - src/audio_karaoke.cpp - src/audio_marker.cpp - src/audio_player.cpp - src/audio_provider_factory.cpp - src/audio_renderer.cpp - src/audio_renderer_spectrum.cpp - src/audio_renderer_waveform.cpp - src/audio_timing_dialogue.cpp - src/audio_timing_karaoke.cpp - src/auto4_base.cpp - src/auto4_lua.cpp - src/auto4_lua_assfile.cpp - src/auto4_lua_dialog.cpp - src/auto4_lua_progresssink.cpp - src/base_grid.cpp - src/charset_detect.cpp - src/colorspace.cpp - src/colour_button.cpp - src/compat.cpp - src/context.cpp - src/export_fixstyle.cpp - src/export_framerate.cpp - src/fft.cpp - src/font_file_lister.cpp - src/frame_main.cpp - src/gl_text.cpp - src/gl_wrap.cpp - src/grid_column.cpp - src/help_button.cpp - src/hotkey.cpp - src/hotkey_data_view_model.cpp - src/image_position_picker.cpp - src/initial_line_state.cpp - src/main.cpp - src/menu.cpp - src/mkv_wrap.cpp - src/pen.cpp - src/persist_location.cpp - src/preferences.cpp - src/preferences_base.cpp - src/project.cpp - src/resolution_resampler.cpp - src/search_replace_engine.cpp - src/selection_controller.cpp - src/spellchecker.cpp - src/spline.cpp - src/spline_curve.cpp - src/string_codec.cpp - src/subs_controller.cpp - src/subs_edit_box.cpp - src/subs_edit_ctrl.cpp - src/subs_preview.cpp - src/subtitles_provider.cpp - src/subtitles_provider_libass.cpp - src/text_file_reader.cpp - src/text_file_writer.cpp - src/text_selection_controller.cpp - src/thesaurus.cpp - src/timeedit_ctrl.cpp - src/toggle_bitmap.cpp - src/toolbar.cpp - src/tooltip_manager.cpp - src/utils.cpp - src/validators.cpp - src/vector2d.cpp - src/version.cpp - src/video_box.cpp - src/video_controller.cpp - src/video_display.cpp - src/video_frame.cpp - src/video_out_gl.cpp - src/video_provider_cache.cpp - src/video_provider_dummy.cpp - src/video_provider_manager.cpp - src/video_provider_yuv4mpeg.cpp - src/video_slider.cpp - src/visual_feature.cpp -) -target_link_libraries(Aegisub ${CMAKE_DL_LIBS} libaegisub luabins luajit resrc csri) - -if (MSVC) - set_target_properties(libaegisub PROPERTIES COMPILE_FLAGS "/Yu${PROJECT_SOURCE_DIR}/libaegisub/lagi_pre.h" COMPILE_FLAGS "/FI${PROJECT_SOURCE_DIR}/libaegisub/lagi_pre.h") -else(MSVC) - target_compile_options(libaegisub PRIVATE -include "${PROJECT_SOURCE_DIR}/libaegisub/lagi_pre.h") -endif(MSVC) - -set_property( - SOURCE libaegisub/unix/path.cpp - PROPERTY COMPILE_DEFINITIONS - P_DATA="${CMAKE_INSTALL_PREFIX}/share/aegisub/" -) - -if (MSVC) - add_definitions("-DNOMINMAX -MP -DINITGUID") - set_target_properties(Aegisub PROPERTIES COMPILE_FLAGS "/Yu${PROJECT_SOURCE_DIR}/src/agi_pre.h" COMPILE_FLAGS "/FI${PROJECT_SOURCE_DIR}/src/agi_pre.h") - target_link_libraries (Aegisub Usp10) - #target_sources(Aegisub PRIVATE src/res/res.rc src/res/strings.rc src/crash_writer_minidump.cpp) - target_sources(Aegisub PRIVATE src/res/res.rc src/res/strings.rc src/crash_writer.cpp src/dpi_aware.manifest) - set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT Aegisub) -else(MSVC) - target_sources(Aegisub PRIVATE src/crash_writer.cpp) - target_compile_options(Aegisub PRIVATE -include "${PROJECT_SOURCE_DIR}/src/agi_pre.h") -endif(MSVC) - -if (WIN32) - target_sources(Aegisub PRIVATE src/font_file_lister_gdi.cpp) -else (WIN32) - find_package(Fontconfig REQUIRED) - target_link_libraries (Aegisub ${Fontconfig_LIBRARIES}) - target_sources(Aegisub PRIVATE src/font_file_lister_fontconfig.cpp) - set_property(SOURCE src/font_file_lister_fontconfig.cpp PROPERTY INCLUDE_DIRECTORIES "${Fontconfig_INCLUDE_DIRS}") -endif (WIN32) - -find_package(ass REQUIRED) -include_directories(${ass_INCLUDE_DIRS}) -target_link_libraries (Aegisub ${ass_LIBRARIES}) - -find_package(Boost REQUIRED chrono filesystem locale regex system thread) -include_directories(${Boost_INCLUDE_DIRS}) -target_link_directories(Aegisub PRIVATE ${Boost_LIBRARY_DIRS}) -target_link_libraries(Aegisub ${Boost_LIBRARIES}) - -find_package(OpenGL REQUIRED) -include_directories(${OPENGL_INCLUDE_DIR}) -target_link_libraries (Aegisub ${OPENGL_LIBRARIES}) - -find_package(Hunspell REQUIRED) -include_directories(${HUNSPELL_INCLUDE_DIR}) -target_link_libraries (Aegisub ${HUNSPELL_LIBRARIES}) -add_definitions("-DWITH_HUNSPELL") -target_sources(Aegisub PRIVATE src/spellchecker_hunspell.cpp) - -find_package(Iconv REQUIRED) -include_directories(${Iconv_INCLUDE_DIRS}) -target_link_libraries (Aegisub ${Iconv_LIBRARIES}) -add_definitions("-DHAVE_ICONV") -if (NOT Iconv_IS_BUILT_IN) -set_property( - SOURCE libaegisub/common/charset_conv.cpp - PROPERTY COMPILE_DEFINITIONS AGI_ICONV_CONST -) -endif (NOT Iconv_IS_BUILT_IN) - -find_package(ICU REQUIRED uc dt in) -include_directories(${ICU_INCLUDE_DIRS}) -target_link_libraries (Aegisub ${ICU_LIBRARIES}) - -find_package(wxWidgets REQUIRED adv base core gl stc xml) -include(${wxWidgets_USE_FILE}) -target_link_libraries(Aegisub ${wxWidgets_LIBRARIES}) - -find_package(ZLIB REQUIRED) -include_directories(${ZLIB_INCLUDE_DIRS}) -target_link_libraries (Aegisub ${ZLIB_LIBRARIES}) - -find_package(ALSA) -if (ALSA_FOUND) - include_directories(${ALSA_INCLUDE_DIRS}) - target_link_libraries (Aegisub ${ALSA_LIBRARIES}) - add_definitions("-DWITH_ALSA") - target_sources(Aegisub PRIVATE src/audio_player_alsa.cpp) -endif(ALSA_FOUND) - -# target_compile_definitions(Aegisub PRIVATE "WITH_AVISYNTH") -# target_sources(Aegisub PRIVATE src/audio_provider_avs.cpp src/avisynth_wrap.cpp src/video_provider_avs.cpp) - -target_compile_definitions(Aegisub PRIVATE "WITH_CSRI") -target_sources(Aegisub PRIVATE src/subtitles_provider_csri.cpp) -set_property(SOURCE src/subtitles_provider_csri.cpp PROPERTY INCLUDE_DIRECTORIES "${PROJECT_SOURCE_DIR}/vendor/csri/include") - -if(MSVC) - target_link_libraries (Aegisub dsound) - add_definitions("-DWITH_DIRECTSOUND") - target_sources(Aegisub PRIVATE src/audio_player_dsound.cpp src/audio_player_dsound2.cpp) -endif(MSVC) - -find_package(FFMS2) -if (FFMS2_FOUND) - include_directories(${FFMS2_INCLUDE_DIRS}) - target_link_libraries (Aegisub ${FFMS2_LIBRARIES}) - add_definitions("-DWITH_FFMS2") - target_sources(Aegisub PRIVATE src/audio_provider_ffmpegsource.cpp src/ffmpegsource_common.cpp src/video_provider_ffmpegsource.cpp) -endif(FFMS2_FOUND) - -find_package(FFTW) -if (FFTW_FOUND) - include_directories(${FFTW_INCLUDES}) - target_link_libraries (Aegisub ${FFTW_LIBRARIES}) - add_definitions("-DWITH_FFTW3") -endif(FFTW_FOUND) - -#ifdef WITH_LIBPULSE -#add_definitions("-DWITH_LIBPULSE") -#target_sources(Aegisub PRIVATE src/audio_player_pulse.cpp) - -find_package(OpenAL) -if (OPENAL_FOUND) - include_directories(${OPENAL_INCLUDE_DIR}) - target_link_libraries (Aegisub ${OPENAL_LIBRARY}) - add_definitions("-DWITH_OPENAL") - target_sources(Aegisub PRIVATE src/audio_player_openal.cpp) -endif(OPENAL_FOUND) - -#ifdef WITH_OSS -#ifdef WITH_PORTAUDIO -#ifdef WITH_STARTUPLOG - -find_package(uchardet) -if (uchardet_FOUND) - include_directories(${uchardet_INCLUDE_DIRS}) - target_link_libraries (Aegisub ${uchardet_LIBRARIES}) - add_definitions("-DWITH_UCHARDET") -endif(uchardet_FOUND) - -#ifdef WITH_UPDATE_CHECKER diff --git a/README.md b/README.md index 478ee14b8..95a453820 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,13 @@ The `cibuilds` branch makes some CI builds of snapshots of `feature` at relevant - [`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 (Still occasionally crashes) -- [`bugfixes`](https://github.com/arch1t3cht/Aegisub/tree/bugfixes): Various fixes, mostly relevant for compilation +- [`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/vapourssynth): 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. +- [`fixes`](https://github.com/arch1t3cht/Aegisub/tree/fixes): Miscellaneous bugfixes - [`misc_dc`](https://github.com/arch1t3cht/Aegisub/tree/misc_dc): Miscellaneous changes taken from AegisubDC +- [`xa-ds2`](https://github.com/arch1t3cht/Aegisub/tree/xa-ds2): Add XAudio2 backend and allow stereo playback for some other backends, by wangqr and Shinon. - [`video_panning_feature`](https://github.com/arch1t3cht/Aegisub/tree/video_panning_feature): Merge [moex3's video zoom and panning](https://github.com/TypesettingTools/Aegisub/pull/150), with an OSX fix 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 @@ -43,7 +47,9 @@ The exact way of switching depends on your Linux distribution, but essentially y The changes to `default_config.json` or similar files weren't detected by meson due to missing regen dependencies. You can either merge the `bugfixes` branch or rebuild from scratch. #### 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 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. +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. diff --git a/libaegisub/audio/provider.cpp b/libaegisub/audio/provider.cpp index 961e5bf89..0abb67eeb 100644 --- a/libaegisub/audio/provider.cpp +++ b/libaegisub/audio/provider.cpp @@ -21,13 +21,107 @@ #include "libaegisub/log.h" #include "libaegisub/util.h" -namespace agi { -void AudioProvider::GetAudioWithVolume(void *buf, int64_t start, int64_t count, double volume) const { - GetAudio(buf, start, count); +namespace { +template +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(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(reinterpret_cast(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 +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(reinterpret_cast(buff))[i]; + else if (bytes_per_sample == sizeof(double)) + for (int64_t i = 0; i < count; ++i) + buf[i] = ConvertFloatToInt16(reinterpret_cast(buff))[i]; + } + else { + if (bytes_per_sample == sizeof(uint8_t)) + for (int64_t i = 0; i < count; ++i) + buf[i] = ConvertUInt8ToInt16(reinterpret_cast(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(reinterpret_cast(buff)), channels)[i]; + else if (bytes_per_sample == sizeof(double)) + for (int64_t i = 0; i < count; ++i) + buf[i] = DownmixToMono >(ConvertFloatToInt16(reinterpret_cast(buff)), channels)[i]; + } + else { + if (bytes_per_sample == sizeof(uint8_t)) + for (int64_t i = 0; i < count; ++i) + buf[i] = DownmixToMono(ConvertUInt8ToInt16(reinterpret_cast(buff)), channels)[i]; + else + for (int64_t i = 0; i < count; ++i) + buf[i] = DownmixToMono(ConvertIntToInt16(buff, bytes_per_sample), channels)[i]; + } + } + free(buff); +} + +void AudioProvider::GetInt16MonoAudioWithVolume(int16_t *buf, int64_t start, int64_t count, double volume) const { + GetInt16MonoAudio(buf, start, count); if (volume == 1.0) return; - if (bytes_per_sample != 2) - throw agi::InternalError("GetAudioWithVolume called on unconverted audio stream"); auto buffer = static_cast(buf); for (size_t i = 0; i < (size_t)count; ++i) @@ -75,6 +169,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 +241,7 @@ void SaveAudioClip(AudioProvider const& provider, fs::path const& path, int star out.write("WAVEfmt "); out.write(16); // Size of chunk - out.write(1); // compression format (PCM) + out.write(provider.AreSamplesFloat() ? 3 : 1); // compression format (1: WAVE_FORMAT_PCM, 3: WAVE_FORMAT_IEEE_FLOAT) out.write(provider.GetChannels()); out.write(provider.GetSampleRate()); out.write(provider.GetSampleRate() * provider.GetChannels() * provider.GetBytesPerSample()); @@ -134,4 +261,4 @@ void SaveAudioClip(AudioProvider const& provider, fs::path const& path, int star out.write(buf); } } -} +} \ No newline at end of file diff --git a/libaegisub/audio/provider_convert.cpp b/libaegisub/audio/provider_convert.cpp index b45d8a852..21603c28a 100644 --- a/libaegisub/audio/provider_convert.cpp +++ b/libaegisub/audio/provider_convert.cpp @@ -22,119 +22,19 @@ #include using namespace agi; - -/// Anything integral -> 16 bit signed machine-endian audio converter namespace { -template -class BitdepthConvertAudioProvider final : public AudioProviderWrapper { - int src_bytes_per_sample; - mutable std::vector src_buf; - +class ConvertAudioProvider final : public AudioProviderWrapper { public: - BitdepthConvertAudioProvider(std::unique_ptr 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(count64); - assert(count == count64); - - src_buf.resize(count * src_bytes_per_sample * channels); - source->GetAudio(src_buf.data(), start, count); - - auto dest = static_cast(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(src_bytes_per_sample) > sizeof(Target)) - sample /= 1LL << (src_bytes_per_sample - sizeof(Target)) * 8; - else if (static_cast(src_bytes_per_sample) < sizeof(Target)) - sample *= 1LL << (sizeof(Target) - src_bytes_per_sample ) * 8; - - dest[i] = static_cast(sample); - } - } -}; - -/// Floating point -> 16 bit signed machine-endian audio converter -template -class FloatConvertAudioProvider final : public AudioProviderWrapper { - mutable std::vector src_buf; - -public: - FloatConvertAudioProvider(std::unique_ptr src) : AudioProviderWrapper(std::move(src)) { - bytes_per_sample = sizeof(Target); + ConvertAudioProvider(std::unique_ptr src) : AudioProviderWrapper(std::move(src)) { float_samples = false; - } - - void FillBuffer(void *buf, int64_t start, int64_t count64) const override { - auto count = static_cast(count64); - assert(count == count64); - - src_buf.resize(count * channels); - source->GetAudio(&src_buf[0], start, count); - - auto dest = static_cast(buf); - - for (size_t i = 0; i < static_cast(count * channels); ++i) { - Source expanded; - if (src_buf[i] < 0) - expanded = static_cast(-src_buf[i] * std::numeric_limits::min()); - else - expanded = static_cast(src_buf[i] * std::numeric_limits::max()); - - dest[i] = expanded < std::numeric_limits::min() ? std::numeric_limits::min() : - expanded > std::numeric_limits::max() ? std::numeric_limits::max() : - static_cast(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 src_buf; - -public: - DownmixAudioProvider(std::unique_ptr 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(count64); - assert(count == count64); - - src_buf.resize(count * src_channels); - source->GetAudio(&src_buf[0], start, count); - - auto dst = static_cast(buf); - // Just average the channels together - while (count-- > 0) { - int sum = 0; - for (int c = 0; c < src_channels; ++c) - sum += src_buf[count * src_channels + c]; - dst[count] = static_cast(sum / src_channels); - } + void FillBuffer(void *buf, int64_t start, int64_t count) const override { + source->GetInt16MonoAudio(reinterpret_cast(buf), start, count); } }; - /// Sample doubler with linear interpolation for the samples provider /// Requires 16-bit mono input class SampleDoublingAudioProvider final : public AudioProviderWrapper { @@ -177,26 +77,23 @@ std::unique_ptr CreateConvertAudioProvider(std::unique_ptrAreSamplesFloat()) { LOG_D("audio_provider") << "Converting float to S16"; - if (provider->GetBytesPerSample() == sizeof(float)) - provider = agi::make_unique>(std::move(provider)); - else - provider = agi::make_unique>(std::move(provider)); } if (provider->GetBytesPerSample() != 2) { - LOG_D("audio_provider") << "Converting " << provider->GetBytesPerSample() << " bytes per sample or wrong endian to S16"; - provider = agi::make_unique>(std::move(provider)); + LOG_D("audio_provider") << "Converting " << provider->GetBytesPerSample() << " bytes per sample to S16"; } // We currently only support mono audio if (provider->GetChannels() != 1) { LOG_D("audio_provider") << "Downmixing to mono from " << provider->GetChannels() << " channels"; - provider = agi::make_unique(std::move(provider)); } // Some players don't like low sample rate audio - while (provider->GetSampleRate() < 32000) { - LOG_D("audio_provider") << "Doubling sample rate"; - provider = agi::make_unique(std::move(provider)); + if (provider->GetSampleRate() < 32000) { + provider = agi::make_unique(std::move(provider)); + while (provider->GetSampleRate() < 32000) { + LOG_D("audio_provider") << "Doubling sample rate"; + provider = agi::make_unique(std::move(provider)); + } } return provider; diff --git a/libaegisub/audio/provider_hd.cpp b/libaegisub/audio/provider_hd.cpp index 19e33eeed..2969b244a 100644 --- a/libaegisub/audio/provider_hd.cpp +++ b/libaegisub/audio/provider_hd.cpp @@ -43,15 +43,15 @@ class HDAudioProvider final : public AudioProviderWrapper { } if (count > 0) { - start *= bytes_per_sample; - count *= bytes_per_sample; + start *= bytes_per_sample * channels; + count *= bytes_per_sample * channels; memcpy(buf, file.read(start, count), count); } } fs::path CacheFilename(fs::path const& dir) { // Check free space - if ((uint64_t)num_samples * bytes_per_sample > fs::FreeSpace(dir)) + if ((uint64_t)num_samples * bytes_per_sample * channels > fs::FreeSpace(dir)) throw AudioProviderError("Not enough free disk space in " + dir.string() + " to cache the audio"); return format("audio-%lld-%lld", time(nullptr), @@ -61,7 +61,7 @@ class HDAudioProvider final : public AudioProviderWrapper { public: HDAudioProvider(std::unique_ptr src, agi::fs::path const& dir) : AudioProviderWrapper(std::move(src)) - , file(dir / CacheFilename(dir), num_samples * bytes_per_sample) + , file(dir / CacheFilename(dir), num_samples * bytes_per_sample * channels) { decoded_samples = 0; decoder = std::thread([&] { diff --git a/libaegisub/audio/provider_lock.cpp b/libaegisub/audio/provider_lock.cpp index eb397e410..e405487a1 100644 --- a/libaegisub/audio/provider_lock.cpp +++ b/libaegisub/audio/provider_lock.cpp @@ -29,6 +29,11 @@ class LockAudioProvider final : public agi::AudioProviderWrapper { source->GetAudio(buf, start, count); } + void FillBufferInt16Mono(int16_t *buf, int64_t start, int64_t count) const override { + std::unique_lock lock(mutex); + source->GetInt16MonoAudio(buf, start, count); + } + public: LockAudioProvider(std::unique_ptr src) : AudioProviderWrapper(std::move(src)) diff --git a/libaegisub/audio/provider_ram.cpp b/libaegisub/audio/provider_ram.cpp index 0c1da546c..e708b0112 100644 --- a/libaegisub/audio/provider_ram.cpp +++ b/libaegisub/audio/provider_ram.cpp @@ -46,14 +46,14 @@ public: decoded_samples = 0; try { - blockcache.resize((source->GetNumSamples() * source->GetBytesPerSample() + CacheBlockSize - 1) >> CacheBits); + blockcache.resize((num_samples * bytes_per_sample * channels + CacheBlockSize - 1) >> CacheBits); } catch (std::bad_alloc const&) { throw AudioProviderError("Not enough memory available to cache in RAM"); } decoder = std::thread([&] { - int64_t readsize = CacheBlockSize / source->GetBytesPerSample(); + int64_t readsize = CacheBlockSize / bytes_per_sample / channels; for (size_t i = 0; i < blockcache.size(); i++) { if (cancelled) break; auto actual_read = std::min(readsize, num_samples - i * readsize); @@ -71,20 +71,22 @@ public: void RAMAudioProvider::FillBuffer(void *buf, int64_t start, int64_t count) const { auto charbuf = static_cast(buf); - for (int64_t bytes_remaining = count * bytes_per_sample; bytes_remaining; ) { + for (int64_t bytes_remaining = count * bytes_per_sample * channels; bytes_remaining; ) { if (start >= decoded_samples) { memset(charbuf, 0, bytes_remaining); break; } - const int i = (start * bytes_per_sample) >> CacheBits; - const int start_offset = (start * bytes_per_sample) & (CacheBlockSize-1); - const int read_size = std::min(bytes_remaining, CacheBlockSize - start_offset); + const int64_t samples_per_block = CacheBlockSize / bytes_per_sample / channels; + + const size_t i = start / samples_per_block; + const int start_offset = (start % samples_per_block) * bytes_per_sample * channels; + const int read_size = std::min(bytes_remaining, samples_per_block * bytes_per_sample * channels - start_offset); memcpy(charbuf, &blockcache[i][start_offset], read_size); charbuf += read_size; bytes_remaining -= read_size; - start += read_size / bytes_per_sample; + start += read_size / bytes_per_sample / channels; } } } @@ -93,4 +95,4 @@ namespace agi { std::unique_ptr CreateRAMAudioProvider(std::unique_ptr src) { return agi::make_unique(std::move(src)); } -} +} \ No newline at end of file diff --git a/libaegisub/include/libaegisub/audio/provider.h b/libaegisub/include/libaegisub/audio/provider.h index 70460a723..741979b12 100644 --- a/libaegisub/include/libaegisub/audio/provider.h +++ b/libaegisub/include/libaegisub/audio/provider.h @@ -20,8 +20,8 @@ #include #include -#include #include +#include namespace agi { class AudioProvider { @@ -37,6 +37,7 @@ protected: bool float_samples = false; virtual void FillBuffer(void *buf, int64_t start, int64_t count) const = 0; + virtual void FillBufferInt16Mono(int16_t* buf, int64_t start, int64_t count) const; void ZeroFill(void *buf, int64_t count) const; @@ -44,7 +45,8 @@ public: virtual ~AudioProvider() = default; void GetAudio(void *buf, int64_t start, int64_t count) const; - void GetAudioWithVolume(void *buf, int64_t start, int64_t count, double volume) const; + void GetInt16MonoAudio(int16_t* buf, int64_t start, int64_t count) const; + void GetInt16MonoAudioWithVolume(int16_t *buf, int64_t start, int64_t count, double volume) const; int64_t GetNumSamples() const { return num_samples; } int64_t GetDecodedSamples() const { return decoded_samples; } diff --git a/meson.build b/meson.build index 7ef4edb52..b0595359f 100644 --- a/meson.build +++ b/meson.build @@ -7,7 +7,7 @@ project('Aegisub', ['c', 'cpp'], cmake = import('cmake') if host_machine.system() == 'windows' - add_project_arguments('-DNOMINMAX', '-D_WIN32_WINNT=0x0601', language: 'cpp') + add_project_arguments('-DNOMINMAX', language: 'cpp') if not get_option('csri').disabled() add_global_arguments('-DCSRI_NO_EXPORT', language: 'c') @@ -224,6 +224,24 @@ foreach dep: [ endif endforeach +needs_ffmpeg = false + +if get_option('bestsource').enabled() + conf.set('WITH_BESTSOURCE', 1) + bs = subproject('bestsource') + deps += bs.get_variable('bestsource_dep') + dep_avail += 'BestSource' + needs_ffmpeg = true +endif + +if needs_ffmpeg + conf.set('WITH_FFMPEG', 1) + deps += [ + dependency('libavutil', default_options: ['tests=disabled']), + dependency('libswscale', default_options: ['tests=disabled']), + ] +endif + if get_option('avisynth').enabled() conf.set('WITH_AVISYNTH', 1) # bundled separately with installer dep_avail += 'AviSynth' @@ -241,20 +259,51 @@ if get_option('avisynth').enabled() endif endif -if host_machine.system() == 'windows' and not get_option('directsound').disabled() - dsound_dep = cc.find_library('dsound', required: get_option('directsound')) - winmm_dep = cc.find_library('winmm', required: get_option('directsound')) - ole32_dep = cc.find_library('ole32', required: get_option('directsound')) - have_dsound_h = cc.has_header('dsound.h') - if not have_dsound_h and get_option('directsound').enabled() - error('DirectSound enabled but dsound.h not found') +if get_option('vapoursynth').enabled() + conf.set('WITH_VAPOURSYNTH', 1) + vs_sub = subproject('vapoursynth') + deps_inc += vs_sub.get_variable('vs_inc') + dep_avail += 'VapourSynth' +endif + +if host_machine.system() == 'windows' + if not get_option('directsound').disabled() + dsound_dep = cc.find_library('dsound', required: get_option('directsound')) + winmm_dep = cc.find_library('winmm', required: get_option('directsound')) + ole32_dep = cc.find_library('ole32', required: get_option('directsound')) + have_dsound_h = cc.has_header('dsound.h') + if not have_dsound_h and get_option('directsound').enabled() + error('DirectSound enabled but dsound.h not found') + endif + + dxguid_dep = cc.find_library('dxguid', required: true) + if dsound_dep.found() and winmm_dep.found() and ole32_dep.found() and dxguid_dep.found() and have_dsound_h + deps += [dsound_dep, winmm_dep, ole32_dep, dxguid_dep] + conf.set('WITH_DIRECTSOUND', 1) + dep_avail += 'DirectSound' + endif endif - dxguid_dep = cc.find_library('dxguid', required: true) - if dsound_dep.found() and winmm_dep.found() and ole32_dep.found() and dxguid_dep.found() and have_dsound_h - deps += [dsound_dep, winmm_dep, ole32_dep, dxguid_dep] - conf.set('WITH_DIRECTSOUND', 1) - dep_avail += 'DirectSound' + if not get_option('xaudio2').disabled() + have_xaudio_h = cc.has_header('xaudio2.h') + xaudio2_dep = cc.find_library('xaudio2', required: true) + if have_xaudio_h and xaudio2_dep.found() + deps += [xaudio2_dep] + conf.set('WITH_XAUDIO2', 1) + dep_avail += 'XAudio2' + # XAudio2 needs Windows 8 or newer, so we tell meson not to define an older windows or else it can break things. + add_project_arguments('-D_WIN32_WINNT=0x0602', language: 'cpp') + else + # Windows 8 not required if XAudio2 fails to be found. revert for compat. + add_project_arguments('-D_WIN32_WINNT=0x0601', language: 'cpp') + endif + + if not have_dsound_h and get_option('xaudio2').enabled() + error('xaudio2 enabled but xaudio2.h not found') + endif + else + # Windows 8 not required if XAudio2 is disabled. revert for compat. + add_project_arguments('-D_WIN32_WINNT=0x0601', language: 'cpp') endif endif diff --git a/meson_options.txt b/meson_options.txt index 3bc0461cd..5fde15e8a 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,10 +3,13 @@ option('openal', type: 'feature', description: 'OpenAL audio output') option('libpulse', type: 'feature', description: 'PulseAudio audio output') option('portaudio', type: 'feature', description: 'PortAudio audio output') option('directsound', type: 'feature', description: 'DirectSound audio output') -option('default_audio_output', type: 'combo', choices: ['auto', 'ALSA', 'OpenAL', 'PulseAudio', 'PortAudio', 'DirectSound'], description: 'Default audio output') +option('xaudio2', type: 'feature', description: 'XAudio2 audio output') +option('default_audio_output', type: 'combo', choices: ['auto', 'ALSA', 'OpenAL', 'PulseAudio', 'PortAudio', 'DirectSound', 'XAudio2'], description: 'Default audio output') option('ffms2', type: 'feature', description: 'FFMS2 video source') option('avisynth', type: 'feature', description: 'AviSynth video source') +option('bestsource', type: 'feature', description: 'BestSource video source') +option('vapoursynth', type: 'feature', description: 'VapourSynth video source') option('fftw3', type: 'feature', description: 'FFTW3 support') option('hunspell', type: 'feature', description: 'Hunspell spell checker') diff --git a/packages/win_installer/fragment_associations.iss b/packages/win_installer/fragment_associations.iss index c766011da..ec33599b9 100644 --- a/packages/win_installer/fragment_associations.iss +++ b/packages/win_installer/fragment_associations.iss @@ -29,6 +29,7 @@ Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".wav"; ValueData: ""; Flags: uninsdeletekey Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".ogg"; ValueData: ""; Flags: uninsdeletekey Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".avs"; ValueData: ""; Flags: uninsdeletekey +Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".vpy"; ValueData: ""; Flags: uninsdeletekey Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".opus"; ValueData: ""; Flags: uninsdeletekey Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".h264"; ValueData: ""; Flags: uninsdeletekey Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".hevc"; ValueData: ""; Flags: uninsdeletekey @@ -165,6 +166,7 @@ Root: HKLM; Subkey: "SOFTWARE\Classes\.m4a\OpenWithProgids"; ValueType: string; Root: HKLM; Subkey: "SOFTWARE\Classes\.wav\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Audio.1"; Flags: uninsdeletevalue Root: HKLM; Subkey: "SOFTWARE\Classes\.ogg\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Media.1"; Flags: uninsdeletevalue Root: HKLM; Subkey: "SOFTWARE\Classes\.avs\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Video.1"; Flags: uninsdeletevalue +Root: HKLM; Subkey: "SOFTWARE\Classes\.vpy\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Media.1"; Flags: uninsdeletevalue Root: HKLM; Subkey: "SOFTWARE\Classes\.opus\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Audio.1"; Flags: uninsdeletevalue Root: HKLM; Subkey: "SOFTWARE\Classes\.h264\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Video.1"; Flags: uninsdeletevalue Root: HKLM; Subkey: "SOFTWARE\Classes\.hevc\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Video.1"; Flags: uninsdeletevalue diff --git a/src/audio_player.cpp b/src/audio_player.cpp index f5a8327ca..c46ba0a40 100644 --- a/src/audio_player.cpp +++ b/src/audio_player.cpp @@ -43,6 +43,7 @@ std::unique_ptr CreateAlsaPlayer(agi::AudioProvider *providers, wxWindow *window); std::unique_ptr CreateDirectSoundPlayer(agi::AudioProvider *providers, wxWindow *window); std::unique_ptr CreateDirectSound2Player(agi::AudioProvider *providers, wxWindow *window); +std::unique_ptr CreateXAudio2Player(agi::AudioProvider *providers, wxWindow *window); std::unique_ptr CreateOpenALPlayer(agi::AudioProvider *providers, wxWindow *window); std::unique_ptr CreatePortAudioPlayer(agi::AudioProvider *providers, wxWindow *window); std::unique_ptr CreatePulseAudioPlayer(agi::AudioProvider *providers, wxWindow *window); @@ -63,6 +64,9 @@ namespace { {"DirectSound-old", CreateDirectSoundPlayer, false}, {"DirectSound", CreateDirectSound2Player, false}, #endif +#ifdef WITH_XAUDIO2 + {"Xaudio2", CreateXAudio2Player, false}, +#endif #ifdef WITH_OPENAL {"OpenAL", CreateOpenALPlayer, false}, #endif diff --git a/src/audio_player_alsa.cpp b/src/audio_player_alsa.cpp index 5a1705622..5a98d9ed7 100644 --- a/src/audio_player_alsa.cpp +++ b/src/audio_player_alsa.cpp @@ -127,7 +127,7 @@ void AlsaPlayer::PlaybackThread() do_setup: snd_pcm_format_t pcm_format; - switch (provider->GetBytesPerSample()) + switch (/*provider->GetBytesPerSample()*/ sizeof(int16_t)) { case 1: LOG_D("audio/player/alsa") << "format U8"; @@ -143,7 +143,7 @@ do_setup: if (snd_pcm_set_params(pcm, pcm_format, SND_PCM_ACCESS_RW_INTERLEAVED, - provider->GetChannels(), + /*provider->GetChannels()*/ 1, provider->GetSampleRate(), 1, // allow resample 100*1000 // 100 milliseconds latency @@ -151,7 +151,8 @@ do_setup: return; LOG_D("audio/player/alsa") << "set pcm params"; - size_t framesize = provider->GetChannels() * provider->GetBytesPerSample(); + //size_t framesize = provider->GetChannels() * provider->GetBytesPerSample(); + size_t framesize = sizeof(int16_t); while (true) { @@ -175,7 +176,7 @@ do_setup: { auto avail = std::min(snd_pcm_avail(pcm), (snd_pcm_sframes_t)(end_position-position)); decode_buffer.resize(avail * framesize); - provider->GetAudioWithVolume(decode_buffer.data(), position, avail, volume); + provider->GetInt16MonoAudioWithVolume(reinterpret_cast(decode_buffer.data()), position, avail, volume); snd_pcm_sframes_t written = 0; while (written <= 0) @@ -235,7 +236,7 @@ do_setup: { decode_buffer.resize(avail * framesize); - provider->GetAudioWithVolume(decode_buffer.data(), position, avail, volume); + provider->GetInt16MonoAudioWithVolume(reinterpret_cast(decode_buffer.data()), position, avail, volume); snd_pcm_sframes_t written = 0; while (written <= 0) { @@ -352,4 +353,4 @@ std::unique_ptr CreateAlsaPlayer(agi::AudioProvider *provider, wxWi return agi::make_unique(provider); } -#endif // WITH_ALSA +#endif // WITH_ALSA \ No newline at end of file diff --git a/src/audio_player_dsound.cpp b/src/audio_player_dsound.cpp index 01b47b354..8b50492a0 100644 --- a/src/audio_player_dsound.cpp +++ b/src/audio_player_dsound.cpp @@ -45,6 +45,7 @@ #include #include +#include namespace { class DirectSoundPlayer; @@ -111,8 +112,10 @@ DirectSoundPlayer::DirectSoundPlayer(agi::AudioProvider *provider, wxWindow *par WAVEFORMATEX waveFormat; waveFormat.wFormatTag = WAVE_FORMAT_PCM; waveFormat.nSamplesPerSec = provider->GetSampleRate(); - waveFormat.nChannels = provider->GetChannels(); - waveFormat.wBitsPerSample = provider->GetBytesPerSample() * 8; + //waveFormat.nChannels = provider->GetChannels(); + //waveFormat.wBitsPerSample = provider->GetBytesPerSample() * 8; + waveFormat.nChannels = 1; + waveFormat.wBitsPerSample = sizeof(int16_t) * 8; waveFormat.nBlockAlign = waveFormat.nChannels * waveFormat.wBitsPerSample / 8; waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign; waveFormat.cbSize = sizeof(waveFormat); @@ -160,7 +163,7 @@ bool DirectSoundPlayer::FillBuffer(bool fill) { HRESULT res; void *ptr1, *ptr2; unsigned long int size1, size2; - int bytesps = provider->GetBytesPerSample(); + int bytesps = /*provider->GetBytesPerSample()*/ sizeof(int16_t); // To write length int toWrite = 0; @@ -223,8 +226,8 @@ RetryLock: LOG_D_IF(!count1 && !count2, "audio/player/dsound1") << "DS fill: nothing"; // Get source wave - if (count1) provider->GetAudioWithVolume(ptr1, playPos, count1, volume); - if (count2) provider->GetAudioWithVolume(ptr2, playPos+count1, count2, volume); + if (count1) provider->GetInt16MonoAudioWithVolume(reinterpret_cast(ptr1), playPos, count1, volume); + if (count2) provider->GetInt16MonoAudioWithVolume(reinterpret_cast(ptr2), playPos+count1, count2, volume); playPos += count1+count2; buffer->Unlock(ptr1,count1*bytesps,ptr2,count2*bytesps); @@ -254,7 +257,7 @@ void DirectSoundPlayer::Play(int64_t start,int64_t count) { FillBuffer(true); DWORD play_flag = 0; - if (count*provider->GetBytesPerSample() > bufSize) { + if (count*/*provider->GetBytesPerSample()*/sizeof(int16_t) > bufSize) { // Start thread thread = new DirectSoundPlayerThread(this); thread->Create(); @@ -371,4 +374,4 @@ std::unique_ptr CreateDirectSoundPlayer(agi::AudioProvider *provide return agi::make_unique(provider, parent); } -#endif // WITH_DIRECTSOUND +#endif // WITH_DIRECTSOUND \ No newline at end of file diff --git a/src/audio_player_dsound2.cpp b/src/audio_player_dsound2.cpp index dd7bf8680..ad9149803 100644 --- a/src/audio_player_dsound2.cpp +++ b/src/audio_player_dsound2.cpp @@ -317,13 +317,14 @@ void DirectSoundPlayer2Thread::Run() // Describe the wave format WAVEFORMATEX waveFormat; - waveFormat.wFormatTag = WAVE_FORMAT_PCM; waveFormat.nSamplesPerSec = provider->GetSampleRate(); + waveFormat.cbSize = 0; + waveFormat.wFormatTag = provider->AreSamplesFloat() ? 3 : WAVE_FORMAT_PCM; // Eh fuck it. waveFormat.nChannels = provider->GetChannels(); waveFormat.wBitsPerSample = provider->GetBytesPerSample() * 8; waveFormat.nBlockAlign = waveFormat.nChannels * waveFormat.wBitsPerSample / 8; waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign; - waveFormat.cbSize = sizeof(waveFormat); + //waveFormat.cbSize = sizeof(waveFormat); // And the buffer itself int aim = waveFormat.nAvgBytesPerSec * (wanted_latency*buffer_length)/1000; @@ -332,7 +333,7 @@ void DirectSoundPlayer2Thread::Run() DWORD bufSize = mid(min,aim,max); // size of entire playback buffer DSBUFFERDESC desc; desc.dwSize = sizeof(DSBUFFERDESC); - desc.dwFlags = DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_GLOBALFOCUS; + desc.dwFlags = DSBCAPS_CTRLVOLUME | DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_GLOBALFOCUS; desc.dwBufferBytes = bufSize; desc.dwReserved = 0; desc.lpwfxFormat = &waveFormat; @@ -461,6 +462,15 @@ stop_playback: goto do_fill_buffer; case WAIT_OBJECT_0+3: + { + LONG invert_volume = (LONG)((this->volume - 1.0) * 5000.0); // Hrmm weirdly it's half? + // Look, I would have used a min max but it just errored out for me lol. + if (invert_volume > DSBVOLUME_MAX) + invert_volume = DSBVOLUME_MAX; + else if (invert_volume < DSBVOLUME_MIN / 2) + invert_volume = DSBVOLUME_MIN / 2; + bfr->SetVolume(invert_volume); + } // Change volume // We aren't thread safe right now, filling the buffers grabs volume directly // from the field set by the controlling thread, but it shouldn't be a major @@ -608,7 +618,7 @@ DWORD DirectSoundPlayer2Thread::FillAndUnlockBuffers(void *buf1, DWORD buf1sz, v buf2sz = 0; } - provider->GetAudioWithVolume(buf1, input_frame, buf1szf, volume); + provider->GetAudio(buf1, input_frame, buf1szf); input_frame += buf1szf; } @@ -621,7 +631,7 @@ DWORD DirectSoundPlayer2Thread::FillAndUnlockBuffers(void *buf1, DWORD buf1sz, v buf2sz = buf2szf * bytes_per_frame; } - provider->GetAudioWithVolume(buf2, input_frame, buf2szf, volume); + provider->GetAudio(buf2, input_frame, buf2szf); input_frame += buf2szf; } @@ -932,4 +942,4 @@ std::unique_ptr CreateDirectSound2Player(agi::AudioProvider *provid return agi::make_unique(provider, parent); } -#endif // WITH_DIRECTSOUND +#endif // WITH_DIRECTSOUND \ No newline at end of file diff --git a/src/audio_player_openal.cpp b/src/audio_player_openal.cpp index b0f8372bd..d95d804ce 100644 --- a/src/audio_player_openal.cpp +++ b/src/audio_player_openal.cpp @@ -125,7 +125,7 @@ public: OpenALPlayer::OpenALPlayer(agi::AudioProvider *provider) : AudioPlayer(provider) , samplerate(provider->GetSampleRate()) -, bpf(provider->GetChannels() * provider->GetBytesPerSample()) +, bpf(/*provider->GetChannels() * provider->GetBytesPerSample()*/sizeof(int16_t)) { device = alcOpenDevice(nullptr); if (!device) throw AudioPlayerOpenError("Failed opening default OpenAL device"); @@ -241,7 +241,7 @@ void OpenALPlayer::FillBuffers(ALsizei count) if (fill_len > 0) // Get fill_len frames of audio - provider->GetAudioWithVolume(&decode_buffer[0], cur_frame, fill_len, volume); + provider->GetInt16MonoAudioWithVolume(reinterpret_cast(decode_buffer.data()), cur_frame, fill_len, volume); if ((size_t)fill_len * bpf < decode_buffer.size()) // And zerofill the rest memset(&decode_buffer[fill_len * bpf], 0, decode_buffer.size() - fill_len * bpf); @@ -308,4 +308,4 @@ std::unique_ptr CreateOpenALPlayer(agi::AudioProvider *provider, wx return agi::make_unique(provider); } -#endif // WITH_OPENAL +#endif // WITH_OPENAL \ No newline at end of file diff --git a/src/audio_player_oss.cpp b/src/audio_player_oss.cpp index 93950baef..0934ce197 100644 --- a/src/audio_player_oss.cpp +++ b/src/audio_player_oss.cpp @@ -131,7 +131,7 @@ public: while (!TestDestroy() && parent->cur_frame < parent->end_frame) { int rsize = std::min(wsize, parent->end_frame - parent->cur_frame); - parent->provider->GetAudioWithVolume(buf, parent->cur_frame, + parent->provider->GetInt16MonoAudioWithVolume(reinterpret_cast(buf), parent->cur_frame, rsize, parent->volume); int written = ::write(parent->dspdev, buf, rsize * parent->bpf); parent->cur_frame += written / parent->bpf; @@ -146,7 +146,7 @@ public: void OSSPlayer::OpenStream() { - bpf = provider->GetChannels() * provider->GetBytesPerSample(); + bpf = /*provider->GetChannels() * provider->GetBytesPerSample()*/sizeof(int16_t); // Open device wxString device = to_wx(OPT_GET("Player/Audio/OSS/Device")->GetString()); @@ -162,14 +162,14 @@ void OSSPlayer::OpenStream() #endif // Set number of channels - int channels = provider->GetChannels(); + int channels = /*provider->GetChannels()*/1; if (ioctl(dspdev, SNDCTL_DSP_CHANNELS, &channels) < 0) { throw AudioPlayerOpenError("OSS player: setting channels failed"); } // Set sample format int sample_format; - switch (provider->GetBytesPerSample()) { + switch (/*provider->GetBytesPerSample()*/sizeof(int16_t)) { case 1: sample_format = AFMT_S8; break; @@ -283,4 +283,4 @@ std::unique_ptr CreateOSSPlayer(agi::AudioProvider *provider, wxWin return agi::make_unique(provider); } -#endif // WITH_OSS +#endif // WITH_OSS \ No newline at end of file diff --git a/src/audio_player_portaudio.cpp b/src/audio_player_portaudio.cpp index 7a5babcdc..513b8b28b 100644 --- a/src/audio_player_portaudio.cpp +++ b/src/audio_player_portaudio.cpp @@ -140,7 +140,7 @@ void PortAudioPlayer::OpenStream() { const PaDeviceInfo *device_info = Pa_GetDeviceInfo((*device_ids)[i]); PaStreamParameters pa_output_p; pa_output_p.device = (*device_ids)[i]; - pa_output_p.channelCount = provider->GetChannels(); + pa_output_p.channelCount = /*provider->GetChannels()*/ 1; pa_output_p.sampleFormat = paInt16; pa_output_p.suggestedLatency = device_info->defaultLowOutputLatency; pa_output_p.hostApiSpecificStreamInfo = nullptr; @@ -222,7 +222,7 @@ int PortAudioPlayer::paCallback(const void *inputBuffer, void *outputBuffer, // Play something if (lenAvailable > 0) { - player->provider->GetAudioWithVolume(outputBuffer, player->current, lenAvailable, player->GetVolume()); + player->provider->GetInt16MonoAudioWithVolume(reinterpret_cast(outputBuffer), player->current, lenAvailable, player->GetVolume()); // Set play position player->current += lenAvailable; @@ -283,4 +283,4 @@ std::unique_ptr CreatePortAudioPlayer(agi::AudioProvider *provider, return agi::make_unique(provider); } -#endif // WITH_PORTAUDIO +#endif // WITH_PORTAUDIO \ No newline at end of file diff --git a/src/audio_player_pulse.cpp b/src/audio_player_pulse.cpp index 7174356bd..09882cb74 100644 --- a/src/audio_player_pulse.cpp +++ b/src/audio_player_pulse.cpp @@ -133,11 +133,11 @@ PulseAudioPlayer::PulseAudioPlayer(agi::AudioProvider *provider) : AudioPlayer(p } // Set up stream - bpf = provider->GetChannels() * provider->GetBytesPerSample(); + bpf = /*provider->GetChannels() * provider->GetBytesPerSample()*/sizeof(int16_t); pa_sample_spec ss; ss.format = PA_SAMPLE_S16LE; // FIXME ss.rate = provider->GetSampleRate(); - ss.channels = provider->GetChannels(); + ss.channels = /*provider->GetChannels()*/1; pa_channel_map map; pa_channel_map_init_auto(&map, ss.channels, PA_CHANNEL_MAP_DEFAULT); @@ -308,7 +308,7 @@ void PulseAudioPlayer::pa_stream_write(pa_stream *p, size_t length, PulseAudioPl unsigned long maxframes = thread->end_frame - thread->cur_frame; if (frames > maxframes) frames = maxframes; void *buf = malloc(frames * bpf); - thread->provider->GetAudioWithVolume(buf, thread->cur_frame, frames, thread->volume); + thread->provider->GetInt16MonoAudioWithVolume(reinterpret_cast(buf), thread->cur_frame, frames, thread->volume); ::pa_stream_write(p, buf, frames*bpf, free, 0, PA_SEEK_RELATIVE); thread->cur_frame += frames; } @@ -324,4 +324,4 @@ void PulseAudioPlayer::pa_stream_notify(pa_stream *p, PulseAudioPlayer *thread) std::unique_ptr CreatePulseAudioPlayer(agi::AudioProvider *provider, wxWindow *) { return agi::make_unique(provider); } -#endif // WITH_LIBPULSE +#endif // WITH_LIBPULSE \ No newline at end of file diff --git a/src/audio_player_xaudio2.cpp b/src/audio_player_xaudio2.cpp new file mode 100644 index 000000000..a7cbf0f28 --- /dev/null +++ b/src/audio_player_xaudio2.cpp @@ -0,0 +1,694 @@ +// Copyright (c) 2019, Qirui Wang +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of the Aegisub Group nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// Aegisub Project http://www.aegisub.org/ + +#ifdef WITH_XAUDIO2 +#include "include/aegisub/audio_player.h" + +#include "options.h" + +#include +#include +#include +#include + +#ifndef XAUDIO2_REDIST +#include +#else +#include +#endif + +namespace { +class XAudio2Thread; + +/// @class XAudio2Player +/// @brief XAudio2-based audio player +/// +/// The core design idea is to have a playback thread that performs all playback operations, and use the player object as a proxy to send commands to the playback thread. +class XAudio2Player final : public AudioPlayer { + /// The playback thread + std::unique_ptr thread; + + /// Desired length in milliseconds to write ahead of the playback cursor + int WantedLatency; + + /// Multiplier for WantedLatency to get total buffer length + int BufferLength; + + /// @brief Tell whether playback thread is alive + /// @return True if there is a playback thread and it's ready + bool IsThreadAlive(); + +public: + /// @brief Constructor + XAudio2Player(agi::AudioProvider* provider); + /// @brief Destructor + ~XAudio2Player() = default; + + /// @brief Start playback + /// @param start First audio frame to play + /// @param count Number of audio frames to play + void Play(int64_t start, int64_t count); + + /// @brief Stop audio playback + /// @param timerToo Whether to also stop the playback update timer + void Stop(); + + /// @brief Tell whether playback is active + /// @return True if audio is playing back + bool IsPlaying(); + + /// @brief Get playback end position + /// @return Audio frame index + /// + /// Returns 0 if playback is stopped or there is no playback thread + int64_t GetEndPosition(); + /// @brief Get approximate playback position + /// @return Index of audio frame user is currently hearing + /// + /// Returns 0 if playback is stopped or there is no playback thread + int64_t GetCurrentPosition(); + + /// @brief Change playback end position + /// @param pos New end position + void SetEndPosition(int64_t pos); + + /// @brief Change playback volume + /// @param vol Amplification factor + void SetVolume(double vol); +}; + +/// @brief RAII support class to init and de-init the COM library +struct COMInitialization { + + /// Flag set if an inited COM library is managed + bool inited = false; + + /// @brief Destructor, de-inits COM if it is inited + ~COMInitialization() { + if (inited) CoUninitialize(); + } + + /// @brief Initialise the COM library as single-threaded apartment if isn't already inited by us + bool Init() { + if (!inited && SUCCEEDED(CoInitialize(nullptr))) + inited = true; + return inited; + } +}; + +struct ReleaseCOMObject { + void operator()(IUnknown* obj) { + if (obj) obj->Release(); + } +}; + +/// @brief RAII wrapper around Win32 HANDLE type +struct Win32KernelHandle final : public agi::scoped_holder { + /// @brief Create with a managed handle + /// @param handle Win32 handle to manage + Win32KernelHandle(HANDLE handle = 0) :scoped_holder(handle, CloseHandle) {} + + Win32KernelHandle& operator=(HANDLE new_handle) { + scoped_holder::operator=(new_handle); + return *this; + } +}; + +/// @class XAudio2Thread +/// @brief Playback thread class for XAudio2Player +/// +/// Not based on wxThread, but uses Win32 threads directly +class XAudio2Thread :public IXAudio2VoiceCallback { + /// @brief Win32 thread entry point + /// @param parameter Pointer to our thread object + /// @return Thread return value, always 0 here + static unsigned int __stdcall ThreadProc(void* parameter); + /// @brief Thread entry point + void Run(); + + /// @brief Check for error state and throw exception if one occurred + void CheckError(); + + /// Win32 handle to the thread + Win32KernelHandle thread_handle; + + /// Event object, world to thread, set to start playback + Win32KernelHandle event_start_playback; + + /// Event object, world to thread, set to stop playback + Win32KernelHandle event_stop_playback; + + /// Event object, world to thread, set if playback end time was updated + Win32KernelHandle event_update_end_time; + + /// Event object, world to thread, set if the volume was changed + Win32KernelHandle event_set_volume; + + /// Event object, world to thread, set if the thread should end as soon as possible + Win32KernelHandle event_buffer_end; + + /// Event object, world to thread, set if the thread should end as soon as possible + Win32KernelHandle event_kill_self; + + /// Event object, thread to world, set when the thread has entered its main loop + Win32KernelHandle thread_running; + + /// Event object, thread to world, set when playback is ongoing + Win32KernelHandle is_playing; + + /// Event object, thread to world, set if an error state has occurred (implies thread is dying) + Win32KernelHandle error_happened; + + /// Statically allocated error message text describing reason for error_happened being set + const char* error_message = nullptr; + + /// Playback volume, 1.0 is "unchanged" + double volume = 1.0; + + /// Audio frame to start playback at + int64_t start_frame = 0; + + /// Audio frame to end playback at + int64_t end_frame = 0; + + /// Desired length in milliseconds to write ahead of the playback cursor + int wanted_latency; + + /// Multiplier for WantedLatency to get total buffer length + int buffer_length; + + /// System millisecond timestamp of last playback start, used to calculate playback position + ULONGLONG last_playback_restart; + + /// Audio provider to take sample data from + agi::AudioProvider* provider; + + /// Buffer occupied indicator + std::vector buffer_occupied; + +public: + /// @brief Constructor, creates and starts playback thread + /// @param provider Audio provider to take sample data from + /// @param WantedLatency Desired length in milliseconds to write ahead of the playback cursor + /// @param BufferLength Multiplier for WantedLatency to get total buffer length + XAudio2Thread(agi::AudioProvider* provider, int WantedLatency, int BufferLength); + /// @brief Destructor, waits for thread to have died + ~XAudio2Thread(); + + // IXAudio2VoiceCallback + void STDMETHODCALLTYPE OnVoiceProcessingPassStart(UINT32 BytesRequired) override {} + void STDMETHODCALLTYPE OnVoiceProcessingPassEnd() override {} + void STDMETHODCALLTYPE OnStreamEnd() override {} + void STDMETHODCALLTYPE OnBufferStart(void* pBufferContext) override {} + void STDMETHODCALLTYPE OnBufferEnd(void* pBufferContext) override { + intptr_t i = reinterpret_cast(pBufferContext); + buffer_occupied[i] = false; + SetEvent(event_buffer_end); + } + void STDMETHODCALLTYPE OnLoopEnd(void* pBufferContext) override {} + void STDMETHODCALLTYPE OnVoiceError(void* pBufferContext, HRESULT Error) override {} + + /// @brief Start audio playback + /// @param start Audio frame to start playback at + /// @param count Number of audio frames to play + void Play(int64_t start, int64_t count); + + /// @brief Stop audio playback + void Stop(); + + /// @brief Change audio playback end point + /// @param new_end_frame New last audio frame to play + /// + /// Playback stops instantly if new_end_frame is before the current playback position + void SetEndFrame(int64_t new_end_frame); + + /// @brief Change audio playback volume + /// @param new_volume New playback amplification factor, 1.0 is "unchanged" + void SetVolume(double new_volume); + + /// @brief Tell whether audio playback is active + /// @return True if audio is being played back, false if it is not + bool IsPlaying(); + + /// @brief Get approximate current audio frame being heard by the user + /// @return Audio frame index + /// + /// Returns 0 if not playing + int64_t GetCurrentFrame(); + + /// @brief Get audio playback end point + /// @return Audio frame index + int64_t GetEndFrame(); + + /// @brief Tell whether playback thread has died + /// @return True if thread is no longer running + bool IsDead(); +}; + +unsigned int __stdcall XAudio2Thread::ThreadProc(void* parameter) { + static_cast(parameter)->Run(); + return 0; +} + +/// Macro used to set error_message, error_happened and end the thread +#define REPORT_ERROR(msg) \ +{ \ + ResetEvent(is_playing); \ + error_message = "XAudio2Thread: " msg; \ + SetEvent(error_happened); \ + return; \ +} + +void XAudio2Thread::Run() { + COMInitialization COM_library; + if (!COM_library.Init()) { + REPORT_ERROR("Could not initialise COM") + } + IXAudio2* pXAudio2; + IXAudio2SourceVoice* pSourceVoice; + HRESULT hr; + if (FAILED(hr = XAudio2Create(&pXAudio2, 0, XAUDIO2_DEFAULT_PROCESSOR))) { + REPORT_ERROR("Failed initializing XAudio2") + } + IXAudio2MasteringVoice* pMasterVoice = NULL; + if (FAILED(hr = pXAudio2->CreateMasteringVoice(&pMasterVoice))) { + REPORT_ERROR("Failed initializing XAudio2 MasteringVoice") + } + + // Describe the wave format + WAVEFORMATEX wfx; + wfx.nSamplesPerSec = provider->GetSampleRate(); + wfx.cbSize = 0; + bool original = true; + wfx.wFormatTag = provider->AreSamplesFloat() ? WAVE_FORMAT_IEEE_FLOAT : WAVE_FORMAT_PCM; + wfx.nChannels = provider->GetChannels(); + wfx.wBitsPerSample = provider->GetBytesPerSample() * 8; + wfx.nBlockAlign = wfx.nChannels * wfx.wBitsPerSample / 8; + wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign; + + if (FAILED(hr = pXAudio2->CreateSourceVoice(&pSourceVoice, &wfx, 0, 2, this))) { + if (hr == XAUDIO2_E_INVALID_CALL) { + // Retry with 16bit mono + original = false; + wfx.wFormatTag = WAVE_FORMAT_PCM; + wfx.nChannels = 1; + wfx.wBitsPerSample = sizeof(int16_t) * 8; + wfx.nBlockAlign = wfx.nChannels * wfx.wBitsPerSample / 8; + wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign; + if (FAILED(hr = pXAudio2->CreateSourceVoice(&pSourceVoice, &wfx, 0, 2, this))) { + REPORT_ERROR("Failed initializing XAudio2 SourceVoice") + } + } + else { + REPORT_ERROR("Failed initializing XAudio2 SourceVoice") + } + } + + // Now we're ready to roll! + SetEvent(thread_running); + bool running = true; + + HANDLE events_to_wait[] = { + event_start_playback, + event_stop_playback, + event_update_end_time, + event_set_volume, + event_buffer_end, + event_kill_self + }; + + int64_t next_input_frame = 0; + DWORD buffer_offset = 0; + bool playback_should_be_running = false; + int current_latency = wanted_latency; + const int wanted_frames = wanted_latency * wfx.nSamplesPerSec / 1000; + const DWORD wanted_latency_bytes = wanted_frames * wfx.nBlockAlign; + std::vector > buff(buffer_length); + for (auto& i : buff) + i.resize(wanted_latency_bytes); + + while (running) { + DWORD wait_result = WaitForMultipleObjects(sizeof(events_to_wait) / sizeof(HANDLE), events_to_wait, FALSE, INFINITE); + + switch (wait_result) { + case WAIT_OBJECT_0 + 0: + // Start or restart playback + pSourceVoice->Stop(); + pSourceVoice->FlushSourceBuffers(); + + next_input_frame = start_frame; + playback_should_be_running = true; + pSourceVoice->Start(); + SetEvent(is_playing); + goto do_fill_buffer; + + case WAIT_OBJECT_0 + 1: + stop_playback: + // Stop playing + ResetEvent(is_playing); + pSourceVoice->Stop(); + pSourceVoice->FlushSourceBuffers(); + playback_should_be_running = false; + break; + + case WAIT_OBJECT_0 + 2: + // Set end frame + if (end_frame <= next_input_frame) + goto stop_playback; + goto do_fill_buffer; + + case WAIT_OBJECT_0 + 3: + // Change volume + pSourceVoice->SetVolume(volume); + break; + + case WAIT_OBJECT_0 + 4: + // Buffer end + do_fill_buffer: + // Time to fill more into buffer + if (!playback_should_be_running) + break; + + for (int i = 0; i < buffer_length; ++i) { + if (!buffer_occupied[i]) { + int fill_len = std::min(end_frame - next_input_frame, wanted_frames); + if (fill_len <= 0) + break; + buffer_occupied[i] = true; + if (original) + provider->GetAudio(buff[i].data(), next_input_frame, fill_len); + else + provider->GetInt16MonoAudio(reinterpret_cast(buff[i].data()), next_input_frame, fill_len); + next_input_frame += fill_len; + XAUDIO2_BUFFER xbf; + xbf.Flags = fill_len + next_input_frame == end_frame ? XAUDIO2_END_OF_STREAM : 0; + xbf.AudioBytes = fill_len * wfx.nBlockAlign; + xbf.pAudioData = buff[i].data(); + xbf.PlayBegin = 0; + xbf.PlayLength = 0; + xbf.LoopBegin = 0; + xbf.LoopLength = 0; + xbf.LoopCount = 0; + xbf.pContext = reinterpret_cast(static_cast(i)); + if (FAILED(hr = pSourceVoice->SubmitSourceBuffer(&xbf))) { + REPORT_ERROR("Failed initializing Submit Buffer") + } + } + } + break; + + case WAIT_OBJECT_0 + 5: + // Perform suicide + running = false; + pXAudio2->Release(); + ResetEvent(is_playing); + playback_should_be_running = false; + break; + + default: + REPORT_ERROR("Something bad happened while waiting on events in playback loop, either the wait failed or an event object was abandoned.") + break; + } + } +} + +#undef REPORT_ERROR + +void XAudio2Thread::CheckError() +{ + try { + switch (WaitForSingleObject(error_happened, 0)) + { + case WAIT_OBJECT_0: + throw error_message; + + case WAIT_ABANDONED: + throw "The XAudio2Thread error signal event was abandoned, somehow. This should not happen."; + + case WAIT_FAILED: + throw "Failed checking state of XAudio2Thread error signal event."; + + case WAIT_TIMEOUT: + default: + return; + } + } + catch (...) { + ResetEvent(is_playing); + ResetEvent(thread_running); + throw; + } +} + +XAudio2Thread::XAudio2Thread(agi::AudioProvider* provider, int WantedLatency, int BufferLength) + : event_start_playback(CreateEvent(0, FALSE, FALSE, 0)) + , event_stop_playback(CreateEvent(0, FALSE, FALSE, 0)) + , event_update_end_time(CreateEvent(0, FALSE, FALSE, 0)) + , event_set_volume(CreateEvent(0, FALSE, FALSE, 0)) + , event_buffer_end(CreateEvent(0, FALSE, FALSE, 0)) + , event_kill_self(CreateEvent(0, FALSE, FALSE, 0)) + , thread_running(CreateEvent(0, TRUE, FALSE, 0)) + , is_playing(CreateEvent(0, TRUE, FALSE, 0)) + , error_happened(CreateEvent(0, FALSE, FALSE, 0)) + , wanted_latency(WantedLatency) + , buffer_length(BufferLength < XAUDIO2_MAX_QUEUED_BUFFERS ? BufferLength : XAUDIO2_MAX_QUEUED_BUFFERS) + , provider(provider) + , buffer_occupied(BufferLength) +{ + if (!(thread_handle = (HANDLE)_beginthreadex(0, 0, ThreadProc, this, 0, 0))) { + throw AudioPlayerOpenError("Failed creating playback thread in XAudio2Player. This is bad."); + } + + HANDLE running_or_error[] = { thread_running, error_happened }; + switch (WaitForMultipleObjects(2, running_or_error, FALSE, INFINITE)) { + case WAIT_OBJECT_0: + // running, all good + return; + + case WAIT_OBJECT_0 + 1: + // error happened, we fail + throw AudioPlayerOpenError(error_message ? error_message : "Failed wait for thread start or thread error in XAudio2Player. This is bad."); + + default: + throw AudioPlayerOpenError("Failed wait for thread start or thread error in XAudio2Player. This is bad."); + } +} + +XAudio2Thread::~XAudio2Thread() { + SetEvent(event_kill_self); + WaitForSingleObject(thread_handle, INFINITE); +} + +void XAudio2Thread::Play(int64_t start, int64_t count) +{ + CheckError(); + + start_frame = start; + end_frame = start + count; + SetEvent(event_start_playback); + + last_playback_restart = GetTickCount64(); + + // Block until playback actually begins to avoid race conditions with + // checking if playback is in progress + HANDLE events_to_wait[] = { is_playing, error_happened }; + switch (WaitForMultipleObjects(2, events_to_wait, FALSE, INFINITE)) { + case WAIT_OBJECT_0 + 0: // Playing + LOG_D("audio/player/xaudio2") << "Playback begun"; + break; + case WAIT_OBJECT_0 + 1: // Error + throw error_message; + default: + throw agi::InternalError("Unexpected result from WaitForMultipleObjects in XAudio2Thread::Play"); + } +} + +void XAudio2Thread::Stop() { + CheckError(); + + SetEvent(event_stop_playback); +} + +void XAudio2Thread::SetEndFrame(int64_t new_end_frame) { + CheckError(); + + end_frame = new_end_frame; + SetEvent(event_update_end_time); +} + +void XAudio2Thread::SetVolume(double new_volume) { + CheckError(); + + volume = new_volume; + SetEvent(event_set_volume); +} + +bool XAudio2Thread::IsPlaying() { + CheckError(); + + switch (WaitForSingleObject(is_playing, 0)) + { + case WAIT_ABANDONED: + throw "The XAudio2Thread playback state event was abandoned, somehow. This should not happen."; + + case WAIT_FAILED: + throw "Failed checking state of XAudio2Thread playback state event."; + + case WAIT_OBJECT_0: + return true; + + case WAIT_TIMEOUT: + default: + return false; + } +} + +int64_t XAudio2Thread::GetCurrentFrame() { + CheckError(); + if (!IsPlaying()) return 0; + ULONGLONG milliseconds_elapsed = GetTickCount64() - last_playback_restart; + return start_frame + milliseconds_elapsed * provider->GetSampleRate() / 1000; +} + +int64_t XAudio2Thread::GetEndFrame() { + CheckError(); + return end_frame; +} + +bool XAudio2Thread::IsDead() { + switch (WaitForSingleObject(thread_running, 0)) + { + case WAIT_OBJECT_0: + return false; + default: + return true; + } +} + +XAudio2Player::XAudio2Player(agi::AudioProvider* provider) :AudioPlayer(provider) { + // The buffer will hold BufferLength times WantedLatency milliseconds of audio + WantedLatency = OPT_GET("Player/Audio/DirectSound/Buffer Latency")->GetInt(); + BufferLength = OPT_GET("Player/Audio/DirectSound/Buffer Length")->GetInt(); + + // sanity checking + if (WantedLatency <= 0) + WantedLatency = 100; + if (BufferLength <= 0) + BufferLength = 5; + + try { + thread = agi::make_unique(provider, WantedLatency, BufferLength); + } + catch (const char* msg) { + LOG_E("audio/player/xaudio2") << msg; + throw AudioPlayerOpenError(msg); + } +} + +bool XAudio2Player::IsThreadAlive() { + if (thread && thread->IsDead()) + thread.reset(); + return static_cast(thread); +} + +void XAudio2Player::Play(int64_t start, int64_t count) { + try { + thread->Play(start, count); + } + catch (const char* msg) { + LOG_E("audio/player/xaudio2") << msg; + } +} + +void XAudio2Player::Stop() { + try { + if (IsThreadAlive()) thread->Stop(); + } + catch (const char* msg) { + LOG_E("audio/player/xaudio2") << msg; + } +} + +bool XAudio2Player::IsPlaying() { + try { + if (!IsThreadAlive()) return false; + return thread->IsPlaying(); + } + catch (const char* msg) { + LOG_E("audio/player/xaudio2") << msg; + return false; + } +} + +int64_t XAudio2Player::GetEndPosition() { + try { + if (!IsThreadAlive()) return 0; + return thread->GetEndFrame(); + } + catch (const char* msg) { + LOG_E("audio/player/xaudio2") << msg; + return 0; + } +} + +int64_t XAudio2Player::GetCurrentPosition() { + try { + if (!IsThreadAlive()) return 0; + return thread->GetCurrentFrame(); + } + catch (const char* msg) { + LOG_E("audio/player/xaudio2") << msg; + return 0; + } +} + +void XAudio2Player::SetEndPosition(int64_t pos) { + try { + if (IsThreadAlive()) thread->SetEndFrame(pos); + } + catch (const char* msg) { + LOG_E("audio/player/xaudio2") << msg; + } +} + +void XAudio2Player::SetVolume(double vol) { + try { + if (IsThreadAlive()) thread->SetVolume(vol); + } + catch (const char* msg) { + LOG_E("audio/player/xaudio2") << msg; + } +} +} + +std::unique_ptr CreateXAudio2Player(agi::AudioProvider* provider, wxWindow*) { + return agi::make_unique(provider); +} + +#endif // WITH_XAUDIO2 diff --git a/src/audio_provider_bestsource.cpp b/src/audio_provider_bestsource.cpp new file mode 100644 index 000000000..5390788dc --- /dev/null +++ b/src/audio_provider_bestsource.cpp @@ -0,0 +1,89 @@ +// Copyright (c) 2022, arch1t3cht +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +/// @file audio_provider_bestsource.cpp +/// @brief BS-based audio provider +/// @ingroup audio_input bestsource +/// + +#ifdef WITH_BESTSOURCE +#include + +#include "audiosource.h" + +#include "bestsource_common.h" +#include "compat.h" +#include "options.h" + +#include +#include +#include +#include + +#include + +namespace { +class BSAudioProvider final : public agi::AudioProvider { + std::map bsopts; + BestAudioSource bs; + AudioProperties properties; + + void FillBuffer(void *Buf, int64_t Start, int64_t Count) const override; +public: + BSAudioProvider(agi::fs::path const& filename, agi::BackgroundRunner *br); + + bool NeedsCache() const override { return OPT_GET("Provider/Audio/BestSource/Aegisub Cache")->GetBool(); } +}; + +/// @brief Constructor +/// @param filename The filename to open +BSAudioProvider::BSAudioProvider(agi::fs::path const& filename, agi::BackgroundRunner *br) try +: bsopts() +, bs(filename.string(), -1, -1, GetBSCacheFile(filename), &bsopts) +{ + bs.SetMaxCacheSize(OPT_GET("Provider/Audio/BestSource/Max Cache Size")->GetInt() << 20); + br->Run([&](agi::ProgressSink *ps) { + ps->SetTitle(from_wx(_("Exacting"))); + ps->SetMessage(from_wx(_("Creating cache... This can take a while!"))); + ps->SetIndeterminate(); + if (bs.GetExactDuration()) { + LOG_D("bs") << "File cached and has exact samples."; + } + }); + properties = bs.GetAudioProperties(); + float_samples = properties.IsFloat; + bytes_per_sample = properties.BytesPerSample; + sample_rate = properties.SampleRate; + channels = properties.Channels; + num_samples = properties.NumSamples; + decoded_samples = OPT_GET("Provider/Audio/BestSource/Aegisub Cache")->GetBool() ? 0 : num_samples; +} +catch (AudioException const& err) { + throw agi::AudioProviderError("Failed to create BestAudioSource"); +} + +void BSAudioProvider::FillBuffer(void *Buf, int64_t Start, int64_t Count) const { + const_cast(bs).GetPackedAudio(reinterpret_cast(Buf), Start, Count); +} + +} + +std::unique_ptr CreateBSAudioProvider(agi::fs::path const& file, agi::BackgroundRunner *br) { + return agi::make_unique(file, br); +} + +#endif /* WITH_BESTSOURCE */ + diff --git a/src/audio_provider_factory.cpp b/src/audio_provider_factory.cpp index 887783644..71c34f66e 100644 --- a/src/audio_provider_factory.cpp +++ b/src/audio_provider_factory.cpp @@ -31,6 +31,8 @@ using namespace agi; std::unique_ptr CreateAvisynthAudioProvider(fs::path const& filename, BackgroundRunner *); std::unique_ptr CreateFFmpegSourceAudioProvider(fs::path const& filename, BackgroundRunner *); +std::unique_ptr CreateBSAudioProvider(fs::path const& filename, BackgroundRunner *); +std::unique_ptr CreateVapoursynthAudioProvider(fs::path const& filename, BackgroundRunner *); namespace { struct factory { @@ -48,6 +50,12 @@ const factory providers[] = { #ifdef WITH_AVISYNTH {"Avisynth", CreateAvisynthAudioProvider, false}, #endif +#ifdef WITH_BESTSOURCE + {"BestSource", CreateBSAudioProvider, false}, +#endif +#ifdef WITH_VAPOURSYNTH + {"Vapoursynth", CreateVapoursynthAudioProvider, false}, +#endif }; } diff --git a/src/audio_provider_vs.cpp b/src/audio_provider_vs.cpp new file mode 100644 index 000000000..02d469b51 --- /dev/null +++ b/src/audio_provider_vs.cpp @@ -0,0 +1,167 @@ +// Copyright (c) 2022, arch1t3cht +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +/// @file audio_provider_vs.cpp +/// @brief Vapoursynth-based audio provider +/// @ingroup audio_input +/// + +#ifdef WITH_VAPOURSYNTH +#include + +#include "audio_controller.h" +#include "options.h" +#include "utils.h" + +#include +#include +#include +#include +#include + +#include + +#include "vapoursynth_wrap.h" +#include "VSScript4.h" + +namespace { +class VapoursynthAudioProvider final : public agi::AudioProvider { + VapourSynthWrapper vs; + VSScript *script = nullptr; + VSNode *node = nullptr; + const VSAudioInfo *vi = nullptr; + + void FillBufferWithFrame(void *buf, int frame, int64_t start, int64_t count) const; + void FillBuffer(void *buf, int64_t start, int64_t count) const override; +public: + VapoursynthAudioProvider(agi::fs::path const& filename); + ~VapoursynthAudioProvider(); + + bool NeedsCache() const override { return true; } +}; + +VapoursynthAudioProvider::VapoursynthAudioProvider(agi::fs::path const& filename) try { + agi::acs::CheckFileRead(filename); + std::lock_guard lock(vs.GetMutex()); + + script = vs.GetScriptAPI()->createScript(nullptr); + if (script == nullptr) { + throw VapoursynthError("Error creating script API"); + } + vs.GetScriptAPI()->evalSetWorkingDir(script, 1); + if (vs.GetScriptAPI()->evaluateFile(script, filename.string().c_str())) { + std::string msg = agi::format("Error executing VapourSynth script: %s", vs.GetScriptAPI()->getError(script)); + vs.GetScriptAPI()->freeScript(script); + throw VapoursynthError(msg); + } + node = vs.GetScriptAPI()->getOutputNode(script, 0); + if (node == nullptr) { + vs.GetScriptAPI()->freeScript(script); + throw VapoursynthError("No output node set"); + } + if (vs.GetAPI()->getNodeType(node) != mtAudio) { + vs.GetAPI()->freeNode(node); + vs.GetScriptAPI()->freeScript(script); + throw VapoursynthError("Output node isn't an audio node"); + } + vi = vs.GetAPI()->getAudioInfo(node); + float_samples = vi->format.sampleType == stFloat; + bytes_per_sample = vi->format.bytesPerSample; + sample_rate = vi->sampleRate; + channels = vi->format.numChannels; + num_samples = vi->numSamples; +} +catch (VapoursynthError const& err) { + throw agi::AudioProviderError(agi::format("Vapoursynth error: %s", err.GetMessage())); +} + +template +static void PackChannels(const uint8_t **Src, void *Dst, size_t Length, size_t Channels) { + T *D = reinterpret_cast(Dst); + for (size_t c = 0; c < Channels; c++) { + const T *S = reinterpret_cast(Src[c]); + for (size_t i = 0; i < Length; i++) { + D[Channels * i + c] = S[i]; + } + } +} + +void VapoursynthAudioProvider::FillBufferWithFrame(void *buf, int n, int64_t start, int64_t count) const { + char errorMsg[1024]; + const VSFrame *frame = vs.GetAPI()->getFrame(n, node, errorMsg, sizeof(errorMsg)); + if (frame == nullptr) { + throw VapoursynthError(agi::format("Error getting frame: %s", errorMsg)); + } + if (vs.GetAPI()->getFrameLength(frame) < count) { + vs.GetAPI()->freeFrame(frame); + throw VapoursynthError("Audio frame too short"); + } + if (vs.GetAPI()->getAudioFrameFormat(frame)->numChannels != channels || vs.GetAPI()->getAudioFrameFormat(frame)->bytesPerSample != bytes_per_sample) { + vs.GetAPI()->freeFrame(frame); + throw VapoursynthError("Audio format is not constant"); + } + + std::vector planes(channels); + for (int c = 0; c < channels; c++) { + planes[c] = vs.GetAPI()->getReadPtr(frame, c); + if (planes[c] == nullptr) { + vs.GetAPI()->freeFrame(frame); + throw VapoursynthError("Failed to read audio channel"); + } + } + + if (bytes_per_sample == 1) + PackChannels(planes.data(), buf, count, channels); + else if (bytes_per_sample == 2) + PackChannels(planes.data(), buf, count, channels); + else if (bytes_per_sample == 4) + PackChannels(planes.data(), buf, count, channels); + else if (bytes_per_sample == 8) + PackChannels(planes.data(), buf, count, channels); + + vs.GetAPI()->freeFrame(frame); +} + +void VapoursynthAudioProvider::FillBuffer(void *buf, int64_t start, int64_t count) const { + int end = start + count; // exclusive + int startframe = start / VS_AUDIO_FRAME_SAMPLES; + int endframe = (end - 1) / VS_AUDIO_FRAME_SAMPLES; + int offset = start - (VS_AUDIO_FRAME_SAMPLES * startframe); + + for (int frame = startframe; frame <= endframe; frame++) { + int framestart = frame * VS_AUDIO_FRAME_SAMPLES; + int frameend = (frame + 1) * VS_AUDIO_FRAME_SAMPLES; + int fstart = framestart < start ? start - framestart : 0; + int fcount = VS_AUDIO_FRAME_SAMPLES - fstart - (frameend > end ? frameend - end : 0); + int bufstart = frame == startframe ? 0 : (frame - startframe) * VS_AUDIO_FRAME_SAMPLES - offset; + FillBufferWithFrame(reinterpret_cast(buf) + channels * bytes_per_sample * bufstart, frame, fstart, fcount); + } +} + +VapoursynthAudioProvider::~VapoursynthAudioProvider() { + if (node != nullptr) { + vs.GetAPI()->freeNode(node); + } + if (script != nullptr) { + vs.GetScriptAPI()->freeScript(script); + } +} +} + +std::unique_ptr CreateVapoursynthAudioProvider(agi::fs::path const& file, agi::BackgroundRunner *) { + return agi::make_unique(file); +} +#endif diff --git a/src/audio_renderer_spectrum.cpp b/src/audio_renderer_spectrum.cpp index 217090a6f..7190c448a 100644 --- a/src/audio_renderer_spectrum.cpp +++ b/src/audio_renderer_spectrum.cpp @@ -208,8 +208,8 @@ void AudioSpectrumRenderer::FillBlock(size_t block_index, float *block) assert(block); int64_t first_sample = (((int64_t)block_index) << derivation_dist) - ((int64_t)1 << derivation_size); - provider->GetAudio(&audio_scratch[0], first_sample, 2 << derivation_size); - + provider->GetInt16MonoAudio(audio_scratch.data(), first_sample, 2 << derivation_size); + // Because the FFTs used here are unnormalized DFTs, we have to compensate // the possible length difference between derivation_size used in the // calculations and its user-provided counterpart. Thus, the display is diff --git a/src/audio_renderer_waveform.cpp b/src/audio_renderer_waveform.cpp index d5bb802fb..789dca024 100644 --- a/src/audio_renderer_waveform.cpp +++ b/src/audio_renderer_waveform.cpp @@ -88,7 +88,7 @@ void AudioWaveformRenderer::Render(wxBitmap &bmp, int start, AudioRenderingStyle for (int x = 0; x < rect.width; ++x) { - provider->GetAudio(audio_buffer.get(), (int64_t)cur_sample, (int64_t)pixel_samples); + provider->GetInt16MonoAudio(reinterpret_cast(audio_buffer.get()), (int64_t)cur_sample, (int64_t)pixel_samples); cur_sample += pixel_samples; int peak_min = 0, peak_max = 0; diff --git a/src/auto4_lua_dialog.cpp b/src/auto4_lua_dialog.cpp index 7985c49b9..7b4b5fa0c 100644 --- a/src/auto4_lua_dialog.cpp +++ b/src/auto4_lua_dialog.cpp @@ -284,6 +284,10 @@ namespace Automation4 { max = DBL_MAX; min = -DBL_MAX; } + if (step != 0.0) { + min = min == -DBL_MAX ? 0.0 : min; + max = max == DBL_MAX ? 100.0 : max; + } } bool CanSerialiseValue() const override { return true; } diff --git a/src/bestsource_common.cpp b/src/bestsource_common.cpp new file mode 100644 index 000000000..69d627015 --- /dev/null +++ b/src/bestsource_common.cpp @@ -0,0 +1,49 @@ +// Copyright (c) 2022, arch1t3cht +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +/// @file ffmpegsource_common.cpp +/// @brief Shared code for ffms video and audio providers +/// @ingroup video_input audio_input ffms +/// + +#ifdef WITH_BESTSOURCE +#include "bestsource_common.h" + +#include "options.h" + +#include +#include + +#include +#include + + +std::string GetBSCacheFile(agi::fs::path const& filename) { + // BS can store all its index data in a single file, but we make a separate index file + // for each video file to ensure that the old index is invalidated if the file is modified. + // While BS does check the filesize of the files, it doesn't check the modification time. + uintmax_t len = agi::fs::Size(filename); + boost::crc_32_type hash; + hash.process_bytes(filename.string().c_str(), filename.string().size()); + + auto result = config::path->Decode("?local/bsindex/" + filename.filename().string() + "_" + std::to_string(hash.checksum()) + "_" + std::to_string(len) + "_" + std::to_string(agi::fs::ModifiedTime(filename)) + ".json"); + agi::fs::CreateDirectory(result.parent_path()); + + return result.string(); +} + + +#endif // WITH_BESTSOURCE diff --git a/src/bestsource_common.h b/src/bestsource_common.h new file mode 100644 index 000000000..b93ccc815 --- /dev/null +++ b/src/bestsource_common.h @@ -0,0 +1,28 @@ +// Copyright (c) 2022, arch1t3cht > +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +/// @file ffmpegsource_common.h +/// @see ffmpegsource_common.cpp +/// @ingroup video_input audio_input ffms +/// + +#ifdef WITH_BESTSOURCE + +#include + +std::string GetBSCacheFile(agi::fs::path const& filename); + +#endif /* WITH_BESTSOURCE */ diff --git a/src/command/audio.cpp b/src/command/audio.cpp index 386866775..a160e8eee 100644 --- a/src/command/audio.cpp +++ b/src/command/audio.cpp @@ -80,7 +80,7 @@ struct audio_open final : public Command { STR_HELP("Open an audio file") void operator()(agi::Context *c) override { - auto str = from_wx(_("Audio Formats") + " (*.aac,*.ac3,*.ape,*.dts,*.eac3,*.flac,*.m4a,*.mka,*.mp3,*.mp4,*.ogg,*.opus,*.w64,*.wav,*.wma)|*.aac;*.ac3;*.ape;*.dts;*.eac3;*.flac;*.m4a;*.mka;*.mp3;*.mp4;*.ogg;*.opus;*.w64;*.wav;*.wma|" + auto str = from_wx(_("Audio Formats") + " (*.aac,*.ac3,*.ape,*.avs,*.dts,*.eac3,*.flac,*.m4a,*.mka,*.mp3,*.mp4,*.ogg,*.opus,*.vpy,*.w64,*.wav,*.wma)|*.aac;*.ac3;*.ape;*.avs;*.dts;*.eac3;*.flac;*.m4a;*.mka;*.mp3;*.mp4;*.ogg;*.opus;*.vpy;*.w64;*.wav;*.wma|" + _("Video Formats") + " (*.asf,*.avi,*.avs,*.d2v,*.m2ts,*.m4v,*.mkv,*.mov,*.mp4,*.mpeg,*.mpg,*.ogm,*.webm,*.wmv,*.ts)|*.asf;*.avi;*.avs;*.d2v;*.m2ts;*.m4v;*.mkv;*.mov;*.mp4;*.mpeg;*.mpg;*.ogm;*.webm;*.wmv;*.ts|" + _("All Files") + " (*.*)|*.*"); auto filename = OpenFileSelector(_("Open Audio File"), "Path/Last/Audio", "", "", str, c->parent); diff --git a/src/command/video.cpp b/src/command/video.cpp index ae0dfe2ad..3dd58547a 100644 --- a/src/command/video.cpp +++ b/src/command/video.cpp @@ -581,7 +581,7 @@ struct video_open final : public Command { STR_HELP("Open a video file") void operator()(agi::Context *c) override { - auto str = from_wx(_("Video Formats") + " (*.asf,*.avi,*.avs,*.d2v,*.h264,*.hevc,*.m2ts,*.m4v,*.mkv,*.mov,*.mp4,*.mpeg,*.mpg,*.ogm,*.webm,*.wmv,*.ts,*.y4m,*.yuv)|*.asf;*.avi;*.avs;*.d2v;*.h264;*.hevc;*.m2ts;*.m4v;*.mkv;*.mov;*.mp4;*.mpeg;*.mpg;*.ogm;*.webm;*.wmv;*.ts;*.y4m;*.yuv|" + auto str = from_wx(_("Video Formats") + " (*.asf,*.avi,*.avs,*.d2v,*.h264,*.hevc,*.m2ts,*.m4v,*.mkv,*.mov,*.mp4,*.mpeg,*.mpg,*.ogm,*.webm,*.wmv,*.ts,*.vpy,*.y4m,*.yuv)|*.asf;*.avi;*.avs;*.d2v;*.h264;*.hevc;*.m2ts;*.m4v;*.mkv;*.mov;*.mp4;*.mpeg;*.mpg;*.ogm;*.webm;*.wmv;*.ts;*.vpy;*.y4m;*.yuv|" + _("All Files") + " (*.*)|*.*"); auto filename = OpenFileSelector(_("Open video file"), "Path/Last/Video", "", "", str, c->parent); if (!filename.empty()) diff --git a/src/dialog_colorpicker.cpp b/src/dialog_colorpicker.cpp index a9f20afef..378bbb534 100644 --- a/src/dialog_colorpicker.cpp +++ b/src/dialog_colorpicker.cpp @@ -388,7 +388,7 @@ void ColorPickerScreenDropper::DropFromScreenXY(int x, int y) { #ifndef __WXMAC__ std::unique_ptr screen; - if (!OPT_GET("Tool/Color Picker/Restrict to Window")->GetBool()) { + if (!OPT_GET("Tool/Colour Picker/Restrict to Window")->GetBool()) { screen = agi::make_unique(); } else { wxWindow *superparent = GetParent(); @@ -397,7 +397,7 @@ void ColorPickerScreenDropper::DropFromScreenXY(int x, int y) { } superparent->ScreenToClient(&x, &y); - screen = agi::make_unique(superparent); + screen = agi::make_unique(superparent); } capdc.StretchBlit(0, 0, resx * magnification, resy * magnification, screen.get(), x - resx / 2, y - resy / 2, resx, resy); diff --git a/src/libresrc/default_config.json b/src/libresrc/default_config.json index fb6ea30b8..31786e75b 100644 --- a/src/libresrc/default_config.json +++ b/src/libresrc/default_config.json @@ -336,6 +336,10 @@ "FFmpegSource" : { "Decode Error Handling" : "ignore", "Downmix" : false + }, + "BestSource": { + "Max Cache Size" : 100, + "Aegisub Cache" : true } }, "Avisynth" : { @@ -356,6 +360,11 @@ "FFmpegSource" : { "Decoding Threads" : -1, "Unsafe Seeking" : false + }, + "BestSource" : { + "Max Cache Size" : 1024, + "Threads" : 0, + "Seek Preroll" : 12 } } }, @@ -451,6 +460,7 @@ "X" : -1, "Y" : -1 }, + "Restrict to Window" : false, "Maximized" : false }, "Fonts Collector" : { @@ -576,9 +586,6 @@ "Maximized" : false, "Skip Whitespace" : true }, - "Color Picker" : { - "Restrict to Window" : false - }, "Visual" : { "Autohide": false }, diff --git a/src/libresrc/osx/default_config.json b/src/libresrc/osx/default_config.json index 05ae560ba..a7737093f 100644 --- a/src/libresrc/osx/default_config.json +++ b/src/libresrc/osx/default_config.json @@ -334,7 +334,12 @@ "Sample Rate" : 0 }, "FFmpegSource" : { - "Decode Error Handling" : "ignore" + "Decode Error Handling" : "ignore", + "Downmix" : false + }, + "BestSource": { + "Max Cache Size" : 100, + "Aegisub Cache" : true } }, "Avisynth" : { @@ -355,6 +360,11 @@ "FFmpegSource" : { "Decoding Threads" : -1, "Unsafe Seeking" : false + }, + "BestSource" : { + "Max Cache Size" : 1024, + "Threads" : 0, + "Seek Preroll" : 12 } } }, @@ -450,6 +460,7 @@ "X" : -1, "Y" : -1 }, + "Restrict to Window" : false, "Maximized" : false }, "Fonts Collector" : { @@ -575,9 +586,6 @@ "Maximized" : false, "Skip Whitespace" : true }, - "Color Picker" : { - "Restrict to Window" : false - }, "Visual" : { "Autohide": false }, diff --git a/src/meson.build b/src/meson.build index 8e7e562c7..c4ca4fa1d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -233,10 +233,16 @@ opt_src = [ ['OSS', 'audio_player_oss.cpp'], ['DirectSound', ['audio_player_dsound.cpp', 'audio_player_dsound2.cpp']], - + ['XAudio2', 'audio_player_xaudio2.cpp'], ['FFMS2', ['audio_provider_ffmpegsource.cpp', 'video_provider_ffmpegsource.cpp', 'ffmpegsource_common.cpp']], + ['BestSource', ['audio_provider_bestsource.cpp', + 'video_provider_bestsource.cpp', + 'bestsource_common.cpp']], + ['VapourSynth', ['vapoursynth_wrap.cpp', + 'audio_provider_vs.cpp', + 'video_provider_vs.cpp']], ['AviSynth', ['avisynth_wrap.cpp', 'audio_provider_avs.cpp', diff --git a/src/preferences.cpp b/src/preferences.cpp index 6b445dc0a..cf337dc2a 100644 --- a/src/preferences.cpp +++ b/src/preferences.cpp @@ -231,8 +231,8 @@ void Interface(wxTreebook *book, Preferences *parent) { auto tl_assistant = p->PageSizer(_("Translation Assistant")); p->OptionAdd(tl_assistant, _("Skip over whitespace"), "Tool/Translation Assistant/Skip Whitespace"); - auto color_picker = p->PageSizer(_("Color Picker")); - p->OptionAdd(color_picker, _("Restrict Screen Picker to Window"), "Tool/Color Picker/Restrict to Window"); + auto color_picker = p->PageSizer(_("Colour Picker")); + p->OptionAdd(color_picker, _("Restrict Screen Picker to Window"), "Tool/Colour Picker/Restrict to Window"); p->SetSizerAndFit(p->sizer); } @@ -417,6 +417,13 @@ void Advanced_Audio(wxTreebook *book, Preferences *parent) { stereo->SetToolTip("Reduces memory usage on surround audio, but may cause audio tracks to sound blank in specific circumstances. This will not affect audio with two channels or less."); #endif +#ifdef WITH_BESTSOURCE + auto bs = p->PageSizer("BestSource"); + p->OptionAdd(bs, _("Max BS cache size (MB)"), "Provider/Audio/BestSource/Max Cache Size"); + p->OptionAdd(bs, _("Use Aegisub's Cache"), "Provider/Audio/BestSource/Aegisub Cache"); +#endif + + #ifdef WITH_PORTAUDIO auto portaudio = p->PageSizer("Portaudio"); p->OptionChoice(portaudio, _("Portaudio device"), PortAudioPlayer::GetOutputDevices(), "Player/Audio/PortAudio/Device Name"); @@ -469,6 +476,13 @@ void Advanced_Video(wxTreebook *book, Preferences *parent) { p->OptionAdd(ffms, _("Enable unsafe seeking"), "Provider/Video/FFmpegSource/Unsafe Seeking"); #endif +#ifdef WITH_BESTSOURCE + auto bs = p->PageSizer("BestSource"); + p->OptionAdd(bs, _("Max cache size (MB)"), "Provider/Video/BestSource/Max Cache Size"); + p->OptionAdd(bs, _("Decoder Threads (0 to autodetect)"), "Provider/Video/BestSource/Threads"); + p->OptionAdd(bs, _("Seek preroll (Frames)"), "Provider/Video/BestSource/Seek Preroll"); +#endif + p->SetSizerAndFit(p->sizer); } diff --git a/src/res/res.rc b/src/res/res.rc index 26f268165..0fdf8cb21 100644 --- a/src/res/res.rc +++ b/src/res/res.rc @@ -39,8 +39,13 @@ eyedropper_cursor CURSOR "../bitmaps/windows/eyedropper.cur" #endif VS_VERSION_INFO VERSIONINFO +#ifdef TAGGED_RELEASE FILEVERSION RESOURCE_BASE_VERSION, BUILD_GIT_VERSION_NUMBER PRODUCTVERSION RESOURCE_BASE_VERSION, 0 +#else +FILEVERSION BUILD_GIT_VERSION_NUMBER, BUILD_GIT_VERSION_NUMBER +PRODUCTVERSION BUILD_GIT_VERSION_NUMBER, 0 +#endif FILEFLAGSMASK VS_FFI_FILEFLAGSMASK FILEFLAGS (AGI_RC_FLAG_DEBUG|AGI_RC_FLAG_PRERELEASE) FILEOS VOS__WINDOWS32 diff --git a/src/vapoursynth_wrap.cpp b/src/vapoursynth_wrap.cpp new file mode 100644 index 000000000..d0d0fbd8b --- /dev/null +++ b/src/vapoursynth_wrap.cpp @@ -0,0 +1,105 @@ +// Copyright (c) 2022, arch1t3cht +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +/// @file vapoursynth_wrap.cpp +/// @brief Wrapper-layer for Vapoursynth +/// @ingroup video_input audio_input +/// + +#ifdef WITH_VAPOURSYNTH +#include "vapoursynth_wrap.h" + +#include "VSScript4.h" + +#include "options.h" + +#include + +#ifndef _WIN32 +#include +#endif + +#ifdef _WIN32 +#define VSSCRIPT_SO "vsscript.dll" +#else +#define VSSCRIPT_SO "libvapoursynth-script.so" +#endif + +// Allocate storage for and initialise static members +namespace { + bool vs_loaded = false; +#ifdef _WIN32 + HINSTANCE hLib = nullptr; +#else + void* hLib = nullptr; +#endif + const VSAPI *api = nullptr; + VSSCRIPTAPI *scriptapi = nullptr; + std::mutex VapourSynthMutex; +} + +typedef VSSCRIPTAPI* VS_CC FUNC(int); + +VapourSynthWrapper::VapourSynthWrapper() { + // VSScript assumes it's only loaded once, so unlike AVS we can't unload it when the refcount reaches zero + if (!vs_loaded) { + vs_loaded = true; +#ifdef _WIN32 +#define CONCATENATE(x, y) x ## y +#define _Lstr(x) CONCATENATE(L, x) + hLib = LoadLibraryW(_Lstr(VSSCRIPT_SO)); +#undef _Lstr +#undef CONCATENATE +#else + hLib = dlopen(VSSCRIPT_SO, RTLD_LAZY | RTLD_GLOBAL | RTLD_DEEPBIND); +#endif + + if (!hLib) + throw VapoursynthError("Could not load " VSSCRIPT_SO); + +#ifdef _WIN32 + FUNC* getVSScriptAPI = (FUNC*)GetProcAddress(hLib, "getVSScriptAPI"); +#else + FUNC* getVSScriptAPI = (FUNC*)dlsym(hLib, "getVSScriptAPI"); +#endif + if (!getVSScriptAPI) + throw VapoursynthError("Failed to get address of getVSScriptAPI from " VSSCRIPT_SO); + + scriptapi = getVSScriptAPI(VSSCRIPT_API_VERSION); + + if (!scriptapi) + throw VapoursynthError("Failed to get Vapoursynth ScriptAPI"); + + api = scriptapi->getVSAPI(VAPOURSYNTH_API_VERSION); + + if (!api) + throw VapoursynthError("Failed to get Vapoursynth API"); + } +} + +std::mutex& VapourSynthWrapper::GetMutex() const { + return VapourSynthMutex; +} + +const VSAPI *VapourSynthWrapper::GetAPI() const { + return api; +} + +const VSSCRIPTAPI *VapourSynthWrapper::GetScriptAPI() const { + return scriptapi; +} + +#endif diff --git a/src/vapoursynth_wrap.h b/src/vapoursynth_wrap.h new file mode 100644 index 000000000..802cb3d58 --- /dev/null +++ b/src/vapoursynth_wrap.h @@ -0,0 +1,42 @@ +// Copyright (c) 2022, arch1t3cht +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +/// @file vapoursynth_wrap.h +/// @see vapoursynth_wrap.cpp +/// @ingroup video_input audio_input +/// + +#ifdef WITH_VAPOURSYNTH + +#include + +DEFINE_EXCEPTION(VapoursynthError, agi::Exception); + +struct VSAPI; +struct VSSCRIPTAPI; +namespace std { class mutex; } + +class VapourSynthWrapper { + VapourSynthWrapper(VapourSynthWrapper const&); +public: + std::mutex& GetMutex() const; + const VSAPI *GetAPI() const; + const VSSCRIPTAPI *GetScriptAPI() const; + + VapourSynthWrapper(); +}; + +#endif diff --git a/src/video_frame.h b/src/video_frame.h index 2a47ed69c..2f4f21e90 100644 --- a/src/video_frame.h +++ b/src/video_frame.h @@ -14,6 +14,8 @@ // // Aegisub Project http://www.aegisub.org/ +#pragma once + #include class wxImage; diff --git a/src/video_provider_bestsource.cpp b/src/video_provider_bestsource.cpp new file mode 100644 index 000000000..3b5e0834c --- /dev/null +++ b/src/video_provider_bestsource.cpp @@ -0,0 +1,204 @@ +// Copyright (c) 2022, arch1t3cht +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +/// @file video_provider_bestsource.cpp +/// @brief BestSource-based video provider +/// @ingroup video_input bestsource +/// + +#ifdef WITH_BESTSOURCE +#include "include/aegisub/video_provider.h" + +#include "videosource.h" +#include "audiosource.h" +#include "BSRational.h" + +extern "C" { +#include +#include +#include +} + +#include "bestsource_common.h" +#include "options.h" +#include "compat.h" +#include "video_frame.h" +namespace agi { class BackgroundRunner; } + +#include +#include +#include +#include +#include + +namespace { + +/// @class BSVideoProvider +/// @brief Implements video loading through BestSource. +class BSVideoProvider final : public VideoProvider { + std::map bsopts; + BestVideoSource bs; + VideoProperties properties; + + std::vector Keyframes; + agi::vfr::Framerate Timecodes; + std::string colorspace; + bool has_audio = false; + +public: + BSVideoProvider(agi::fs::path const& filename, std::string const& colormatrix, agi::BackgroundRunner *br); + + void GetFrame(int n, VideoFrame &out) override; + + void SetColorSpace(std::string const& matrix) override { } // TODO Follow Aegisub's colorspace forcing? + + int GetFrameCount() const override { return properties.NumFrames; }; + + int GetWidth() const override { return properties.Width; }; + int GetHeight() const override { return properties.Height; }; + double GetDAR() const override { return ((double) properties.Width * properties.SAR.Num) / (properties.Height * properties.SAR.Den); }; + + agi::vfr::Framerate GetFPS() const override { return Timecodes; }; + std::string GetColorSpace() const override { return colorspace; }; + std::string GetRealColorSpace() const override { return colorspace; }; + std::vector GetKeyFrames() const override { return Keyframes; }; + std::string GetDecoderName() const override { return "BestSource"; }; + bool WantsCaching() const override { return false; }; + bool HasAudio() const override { return has_audio; }; +}; + +// Match the logic from the ffms2 provider, but directly use libavutil's constants and don't abort when encountering an unknown color space +std::string colormatrix_description(const AVFrame *frame) { + // Assuming TV for unspecified + std::string str = frame->color_range == AVCOL_RANGE_JPEG ? "PC" : "TV"; + LOG_D("bestsource") << frame->colorspace; + + switch (frame->colorspace) { + case AVCOL_SPC_BT709: + return str + ".709"; + case AVCOL_SPC_FCC: + return str + ".FCC"; + case AVCOL_SPC_BT470BG: + case AVCOL_SPC_SMPTE170M: + return str + ".601"; + case AVCOL_SPC_SMPTE240M: + return str + ".240M"; + default: + return "None"; + } +} + +BSVideoProvider::BSVideoProvider(agi::fs::path const& filename, std::string const& colormatrix, agi::BackgroundRunner *br) try +: bsopts() +, bs(filename.string(), "", -1, false, OPT_GET("Provider/Video/BestSource/Threads")->GetInt(), GetBSCacheFile(filename), &bsopts) +{ + bs.SetMaxCacheSize(OPT_GET("Provider/Video/BestSource/Max Cache Size")->GetInt() << 20); + bs.SetSeekPreRoll(OPT_GET("Provider/Video/BestSource/Seek Preroll")->GetInt()); + try { + BestAudioSource dummysource(filename.string(), -1, 0, ""); + has_audio = true; + } catch (AudioException const& err) { + has_audio = false; + } + + properties = bs.GetVideoProperties(); + + if (properties.NumFrames == -1) { + LOG_D("bs") << "File not cached or varying samples, creating cache."; + br->Run([&](agi::ProgressSink *ps) { + ps->SetTitle(from_wx(_("Exacting"))); + ps->SetMessage(from_wx(_("Creating cache... This can take a while!"))); + ps->SetIndeterminate(); + if (bs.GetExactDuration()) { + LOG_D("bs") << "File cached and has exact samples."; + } + }); + properties = bs.GetVideoProperties(); + } + + br->Run([&](agi::ProgressSink *ps) { + ps->SetTitle(from_wx(_("Scanning"))); + ps->SetMessage(from_wx(_("Finding Keyframes and Timecodes..."))); + + std::vector TimecodesVector; + for (int n = 0; n < properties.NumFrames; n++) { + if (ps->IsCancelled()) { + return; + } + std::unique_ptr frame(bs.GetFrame(n)); + if (frame == nullptr) { + throw VideoOpenError("Couldn't read frame!"); + } + + if (frame->GetAVFrame()->key_frame) { + Keyframes.push_back(n); + } + + TimecodesVector.push_back((int) frame->GetAVFrame()->pts); + ps->SetProgress(n, properties.NumFrames); + } + + if (TimecodesVector.size() < 2 || TimecodesVector.front() == TimecodesVector.back()) { + Timecodes = (double) properties.FPS.Num / properties.FPS.Den; + } else { + Timecodes = agi::vfr::Framerate(TimecodesVector); + } + }); + + // Decode the first frame to get the color space + std::unique_ptr frame(bs.GetFrame(0)); + colorspace = colormatrix_description(frame->GetAVFrame()); +} +catch (VideoException const& err) { + throw VideoOpenError("Failed to create BestVideoSource"); +} + +void BSVideoProvider::GetFrame(int n, VideoFrame &out) { + std::unique_ptr bsframe(bs.GetFrame(n)); + if (bsframe == nullptr) { + throw VideoDecodeError("Couldn't read frame!"); + } + const AVFrame *frame = bsframe->GetAVFrame(); + + SwsContext *context = sws_getContext( + frame->width, frame->height, (AVPixelFormat) frame->format, // TODO figure out aegi's color space forcing. + frame->width, frame->height, AV_PIX_FMT_BGR0, + SWS_BICUBIC, nullptr, nullptr, nullptr); + + if (context == nullptr) { + throw VideoDecodeError("Couldn't convert frame!"); + } + + out.data.resize(frame->width * frame->height * 4); + uint8_t *data[1] = {&out.data[0]}; + int stride[1] = {frame->width * 4}; + sws_scale(context, frame->data, frame->linesize, 0, frame->height, data, stride); + + out.width = frame->width; + out.height = frame->height; + out.pitch = stride[0]; + out.flipped = false; // TODO figure out flipped + + sws_freeContext(context); +} + +} + +std::unique_ptr CreateBSVideoProvider(agi::fs::path const& path, std::string const& colormatrix, agi::BackgroundRunner *br) { + return agi::make_unique(path, colormatrix, br); +} + +#endif /* WITH_BESTSOURCE */ diff --git a/src/video_provider_manager.cpp b/src/video_provider_manager.cpp index 3739a7293..c239163b8 100644 --- a/src/video_provider_manager.cpp +++ b/src/video_provider_manager.cpp @@ -29,6 +29,8 @@ std::unique_ptr CreateDummyVideoProvider(agi::fs::path const&, st std::unique_ptr CreateYUV4MPEGVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); std::unique_ptr CreateFFmpegSourceVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); std::unique_ptr CreateAvisynthVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); +std::unique_ptr CreateBSVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); +std::unique_ptr CreateVapoursynthVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); std::unique_ptr CreateCacheVideoProvider(std::unique_ptr); @@ -47,6 +49,12 @@ namespace { #endif #ifdef WITH_AVISYNTH {"Avisynth", CreateAvisynthVideoProvider, false}, +#endif +#ifdef WITH_BESTSOURCE + {"BestSource", CreateBSVideoProvider, false}, +#endif +#ifdef WITH_VAPOURSYNTH + {"Vapoursynth", CreateVapoursynthVideoProvider, false}, #endif }; } diff --git a/src/video_provider_vs.cpp b/src/video_provider_vs.cpp new file mode 100644 index 000000000..3f9839e38 --- /dev/null +++ b/src/video_provider_vs.cpp @@ -0,0 +1,282 @@ +// Copyright (c) 2022, arch1t3cht +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +#ifdef WITH_VAPOURSYNTH +#include "include/aegisub/video_provider.h" + +#include "options.h" +#include "video_frame.h" + +#include +#include +#include +#include +#include + +#include + +#include "vapoursynth_wrap.h" +#include "VSScript4.h" +#include "VSHelper4.h" +#include "VSConstants4.h" + +namespace { +class VapoursynthVideoProvider: public VideoProvider { + VapourSynthWrapper vs; + VSScript *script = nullptr; + VSNode *node = nullptr; + const VSVideoInfo *vi = nullptr; + + double dar = 0; + agi::vfr::Framerate fps; + std::vector keyframes; + std::string colorspace; + std::string real_colorspace; + + const VSFrame *GetVSFrame(int n); + void SetResizeArg(VSMap *args, const VSMap *props, const char *arg_name, const char *prop_name, int deflt, int unspecified = -1); + +public: + VapoursynthVideoProvider(agi::fs::path const& filename, std::string const& colormatrix); + ~VapoursynthVideoProvider(); + + void GetFrame(int n, VideoFrame &frame) override; + + void SetColorSpace(std::string const& matrix) override { } + + int GetFrameCount() const override { return vi->numFrames; } + agi::vfr::Framerate GetFPS() const override { return fps; } + int GetWidth() const override { return vi->width; } + int GetHeight() const override { return vi->height; } + double GetDAR() const override { return dar; } + std::vector GetKeyFrames() const override { return keyframes; } + std::string GetColorSpace() const override { return colorspace; } + std::string GetRealColorSpace() const override { return colorspace; } + bool HasAudio() const override { return false; } + virtual bool WantsCaching() const override { return true; } + virtual std::string GetDecoderName() const override { return "VapourSynth"; } +}; + +std::string colormatrix_description(int colorFamily, int colorRange, int matrix) { + if (colorFamily != cfYUV) { + return "None"; + } + // Assuming TV for unspecified + std::string str = colorRange == VSC_RANGE_FULL ? "PC" : "TV"; + + switch (matrix) { + case VSC_MATRIX_RGB: + return "None"; + case VSC_MATRIX_BT709: + return str + ".709"; + case VSC_MATRIX_FCC: + return str + ".FCC"; + case VSC_MATRIX_BT470_BG: + case VSC_MATRIX_ST170_M: + return str + ".601"; + case VSC_MATRIX_ST240_M: + return str + ".240M"; + default: + return "None"; + } +} + +// Adds an argument to the rescaler if the corresponding frameprop does not exist or is set as unspecified +void VapoursynthVideoProvider::SetResizeArg(VSMap *args, const VSMap *props, const char *arg_name, const char *prop_name, int deflt, int unspecified) { + int err; + int result = vs.GetAPI()->mapGetInt(props, prop_name, 0, &err); + if (err != 0 || result == unspecified) { + result = deflt; + vs.GetAPI()->mapSetInt(args, arg_name, result, maAppend); + } +} + +VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename, std::string const& colormatrix) try { + agi::acs::CheckFileRead(filename); + std::lock_guard lock(vs.GetMutex()); + + script = vs.GetScriptAPI()->createScript(nullptr); + if (script == nullptr) { + throw VapoursynthError("Error creating script API"); + } + vs.GetScriptAPI()->evalSetWorkingDir(script, 1); + if (vs.GetScriptAPI()->evaluateFile(script, filename.string().c_str())) { + std::string msg = agi::format("Error executing VapourSynth script: %s", vs.GetScriptAPI()->getError(script)); + vs.GetScriptAPI()->freeScript(script); + throw VapoursynthError(msg); + } + node = vs.GetScriptAPI()->getOutputNode(script, 0); + if (node == nullptr) { + vs.GetScriptAPI()->freeScript(script); + throw VapoursynthError("No output node set"); + } + if (vs.GetAPI()->getNodeType(node) != mtVideo) { + vs.GetAPI()->freeNode(node); + vs.GetScriptAPI()->freeScript(script); + throw VapoursynthError("Output node isn't a video node"); + } + vi = vs.GetAPI()->getVideoInfo(node); + if (!vsh::isConstantVideoFormat(vi)) { + vs.GetAPI()->freeNode(node); + vs.GetScriptAPI()->freeScript(script); + throw VapoursynthError("Video doesn't have constant format"); + } + + // Assume constant frame rate, since handling VFR would require going through all frames when loading. + // Users can load custom timecodes files to deal with VFR. + // Alternatively (TODO) the provider could read timecodes and keyframes from a second output node. + fps = (double) vi->fpsNum / vi->fpsDen; + + // Find the first frame to get some info + const VSFrame *frame; + try { + frame = GetVSFrame(0); + } catch (VapoursynthError const& err) { + vs.GetAPI()->freeNode(node); + vs.GetScriptAPI()->freeScript(script); + throw err; + } + int err1, err2; + const VSMap *props = vs.GetAPI()->getFramePropertiesRO(frame); + int sarn = vs.GetAPI()->mapGetInt(props, "_SARNum", 0, &err1); + int sard = vs.GetAPI()->mapGetInt(props, "_SARDen", 0, &err2); + if (!err1 && !err2) { + dar = ((double) vi->width * sarn) / (vi->height * sard); + } + + int range = vs.GetAPI()->mapGetInt(props, "_ColorRange", 0, &err1); + int matrix = vs.GetAPI()->mapGetInt(props, "_Matrix", 0, &err2); + colorspace = colormatrix_description(vi->format.colorFamily, err1 == 0 ? range : -1, err2 == 0 ? matrix : -1); + + vs.GetAPI()->freeFrame(frame); + + if (vi->format.colorFamily != cfRGB || vi->format.bitsPerSample != 8) { + // Convert to RGB24 format + VSPlugin *resize = vs.GetAPI()->getPluginByID(VSH_RESIZE_PLUGIN_ID, vs.GetScriptAPI()->getCore(script)); + if (resize == nullptr) { + throw VapoursynthError("Couldn't find resize plugin"); + } + VSMap *args = vs.GetAPI()->createMap(); + if (args == nullptr) { + throw VapoursynthError("Failed to create argument map"); + } + + vs.GetAPI()->mapSetNode(args, "clip", node, maAppend); + vs.GetAPI()->mapSetInt(args, "format", pfRGB24, maAppend); + if (vi->format.colorFamily != cfGray) + SetResizeArg(args, props, "matrix_in", "_Matrix", VSC_MATRIX_BT709, VSC_MATRIX_UNSPECIFIED); + SetResizeArg(args, props, "transfer_in", "_Transfer", VSC_TRANSFER_BT709, VSC_TRANSFER_UNSPECIFIED); + SetResizeArg(args, props, "primaries_in", "_Primaries", VSC_PRIMARIES_BT709, VSC_PRIMARIES_UNSPECIFIED); + SetResizeArg(args, props, "range_in", "_ColorRange", VSC_RANGE_LIMITED); + SetResizeArg(args, props, "chromaloc_in", "_ChromaLocation", VSC_CHROMA_LEFT); + + VSMap *result = vs.GetAPI()->invoke(resize, "Bicubic", args); + vs.GetAPI()->freeMap(args); + const char *error = vs.GetAPI()->mapGetError(result); + if (error) { + vs.GetAPI()->freeMap(result); + vs.GetAPI()->freeNode(node); + vs.GetScriptAPI()->freeScript(script); + throw VideoProviderError(agi::format("Failed to convert to RGB24: %s", error)); + } + int err; + vs.GetAPI()->freeNode(node); + node = vs.GetAPI()->mapGetNode(result, "clip", 0, &err); + vs.GetAPI()->freeMap(result); + if (err) { + vs.GetScriptAPI()->freeScript(script); + throw VideoProviderError("Failed to get resize output node"); + } + + // Finally, try to get the first frame again, so if the filter does crash, it happens before loading finishes + const VSFrame *rgbframe; + try { + rgbframe = GetVSFrame(0); + } catch (VapoursynthError const& err) { + vs.GetAPI()->freeNode(node); + vs.GetScriptAPI()->freeScript(script); + throw err; + } + vs.GetAPI()->freeFrame(rgbframe); + } +} +catch (VapoursynthError const& err) { + throw VideoProviderError(agi::format("Vapoursynth error: %s", err.GetMessage())); +} + +const VSFrame *VapoursynthVideoProvider::GetVSFrame(int n) { + char errorMsg[1024]; + const VSFrame *frame = vs.GetAPI()->getFrame(n, node, errorMsg, sizeof(errorMsg)); + if (frame == nullptr) { + throw VapoursynthError(agi::format("Error getting frame: %s", errorMsg)); + } + return frame; +} + +void VapoursynthVideoProvider::GetFrame(int n, VideoFrame &out) { + std::lock_guard lock(vs.GetMutex()); + + const VSFrame *frame = GetVSFrame(n); + + const VSVideoFormat *format = vs.GetAPI()->getVideoFrameFormat(frame); + if (format->colorFamily != cfRGB || format->numPlanes != 3 || format->bitsPerSample != 8 || format->subSamplingH != 0 || format->subSamplingW != 0) { + throw VapoursynthError("Frame not in RGB24 format"); + } + + out.width = vs.GetAPI()->getFrameWidth(frame, 0); + out.height = vs.GetAPI()->getFrameHeight(frame, 0); + out.pitch = out.width * 4; + out.flipped = false; + + out.data.resize(out.pitch * out.height); + + for (int p = 0; p < format->numPlanes; p++) { + ptrdiff_t stride = vs.GetAPI()->getStride(frame, p); + const uint8_t *readPtr = vs.GetAPI()->getReadPtr(frame, p); + uint8_t *writePtr = &out.data[2 - p]; + int rows = vs.GetAPI()->getFrameHeight(frame, p); + int cols = vs.GetAPI()->getFrameWidth(frame, p); + + for (int row = 0; row < rows; row++) { + const uint8_t *rowPtr = readPtr; + uint8_t *rowWritePtr = writePtr; + for (int col = 0; col < cols; col++) { + *rowWritePtr = *rowPtr++; + rowWritePtr += 4; + } + readPtr += stride; + writePtr += out.pitch; + } + } + + vs.GetAPI()->freeFrame(frame); +} + +VapoursynthVideoProvider::~VapoursynthVideoProvider() { + if (node != nullptr) { + vs.GetAPI()->freeNode(node); + } + if (script != nullptr) { + vs.GetScriptAPI()->freeScript(script); + } +} +} + +namespace agi { class BackgroundRunner; } +std::unique_ptr CreateVapoursynthVideoProvider(agi::fs::path const& path, std::string const& colormatrix, agi::BackgroundRunner *) { + return agi::make_unique(path, colormatrix); +} +#endif // HAVE_VAPOURSYNTH diff --git a/subprojects/bestsource.wrap b/subprojects/bestsource.wrap new file mode 100644 index 000000000..fa0e5ef45 --- /dev/null +++ b/subprojects/bestsource.wrap @@ -0,0 +1,7 @@ +[wrap-git] +url = https://github.com/vapoursynth/bestsource +revision = head +patch_directory = bestsource + +[provide] +bestsource = bestsource_dep diff --git a/subprojects/jansson.wrap b/subprojects/jansson.wrap new file mode 100644 index 000000000..51ff9a82b --- /dev/null +++ b/subprojects/jansson.wrap @@ -0,0 +1,4 @@ +[wrap-git] +directory = jansson +url = https://github.com/akheron/jansson.git +revision = v2.14 diff --git a/subprojects/packagefiles/bestsource/libp2p/p2p_api.h b/subprojects/packagefiles/bestsource/libp2p/p2p_api.h new file mode 100644 index 000000000..f66166538 --- /dev/null +++ b/subprojects/packagefiles/bestsource/libp2p/p2p_api.h @@ -0,0 +1,156 @@ +// Since we don't use ExportAsPlanar, we don't actually need libp2p. +// So instead of adding another wrap and meson build file, and *also* +// patching the include statement in videosource.cpp, we throw a dummy +// header file in the folder that should contain the checkout of libp2p. + +#ifndef P2P_API_H_ +#define P2P_API_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Notation: [Xa-Ya-Za] + * + * [] denotes a machine word of the specified endianness. Xa-Ya-Za denote + * component X, Y, and Z packed in the word, with bit depths a, b, c, in order + * from MSB to LSB. Padding bits are represented by the component '!'. + */ +enum p2p_packing { + /** [R8-G8-B8] */ + p2p_rgb24_be, /* RGB */ + p2p_rgb24_le, /* BGR */ + p2p_rgb24, + /** [A8-R8-G8-B8] */ + p2p_argb32_be, /* ARGB */ + p2p_argb32_le, /* BGRA */ + p2p_argb32, + /** [A8-Y8-U8-V8] */ + p2p_ayuv_be, /* AYUV */ + p2p_ayuv_le, /* VUYA */ + p2p_ayuv, + /** [R16-G16-B16] */ + p2p_rgb48_be, /* RGB, big-endian components */ + p2p_rgb48_le, /* BGR, little-endian components */ + p2p_rgb48, + /** [A16-R16-G16-B16] */ + p2p_argb64_be, /* ARGB big-endian components */ + p2p_argb64_le, /* BGRA little-endian components */ + p2p_argb64, + /** [A2-R10-G10-B10] */ + p2p_rgb30_be, /* ARGB packed in big-endian DWORD */ + p2p_rgb30_le, /* ARGB packed in little-endian DWORD */ + p2p_rgb30, + /** [A2-V10-Y10-U10] */ + p2p_y410_be, /* AVYU packed in big-endian DWORD */ + p2p_y410_le, /* AVYU packed in little-endian DWORD */ + p2p_y410, + /** [A16-V16-Y16-U16] */ + p2p_y416_be, /* AVYU, big-endian components */ + p2p_y416_le, /* UYVA, little-endian components */ + p2p_y416, + /** [Y8] [U8] [Y8] [V8] */ + p2p_yuy2, + /** [U8] [Y8] [V8] [Y8] */ + p2p_uyvy, + /** [Y10-!6] [U10-!6] [Y10-!6] [V10-!6] */ + p2p_y210_be, /* YUYV, big-endian components, lower 6 bits zero */ + p2p_y210_le, /* YUYV, little-endian components, lower 6 bits zero. Microsoft Y210. */ + p2p_y210, + /** [Y16] [U16] [Y16] [V16] */ + p2p_y216_be, /* YUYV, big-endian components */ + p2p_y216_le, /* YUYV, little-endian components. Microsoft Y216. */ + p2p_y216, + /** [!2-V10-Y10-U10] [!2-Y10-U10-Y10] [!2-U10-Y10-V10] [!2-Y10-V10-Y10] */ + p2p_v210_be, /* v210 with big-endian DWORDs */ + p2p_v210_le, /* Apple/QuickTime v210 */ + p2p_v210, + /** [U16] [Y16] [V16] [Y16] */ + p2p_v216_be, /* UYVY, big-endian components */ + p2p_v216_le, /* UYVY, little-endian components. Apple/QuickTime v216. */ + p2p_v216, + /** [U8-V8] */ + p2p_nv12_be, /* aka NV21, V first */ + p2p_nv12_le, /* NV12 */ + p2p_nv12, + /** [U10-!6-V10-!6] */ + p2p_p010_be, /* NV21, big-endian components, lower 6 bits zero */ + p2p_p010_le, /* NV12, little-endian components, lower 6 bits zero. Microsoft P010. */ + p2p_p010, + /** [U16-V16] */ + p2p_p016_be, /* NV21, big-endian components */ + p2p_p016_le, /* NV12, little-endian components. Microsoft P016. */ + p2p_p016, + /** [U10-!6-V10-!6] */ + p2p_p210_be, /* NV21, big-endian components, lower 6 bits zero */ + p2p_p210_le, /* NV12, little-endian components, lower 6 bits zero. Microsoft P210. */ + p2p_p210, + /** [U16-V16] */ + p2p_p216_be, /* NV21, big-endian components */ + p2p_p216_le, /* NV12, little-endian components. Microsoft P216. */ + p2p_p216, + /** [R8-G8-B8-A8] */ + p2p_rgba32_be, /* RGBA */ + p2p_rgba32_le, /* ABGR */ + p2p_rgba32, + /** [R16-G16-B16-A16] */ + p2p_rgba64_be, /* RGBA, big-endian components */ + p2p_rgba64_le, /* ABGR, little-endian components */ + p2p_rgba64, + /** [A16-B16-G16-R16] */ + p2p_abgr64_be, /* ABGR, big-endian components */ + p2p_abgr64_le, /* RGBA, little-endian components */ + p2p_abgr64, + /** [B16-G16-R16] */ + p2p_bgr48_be, /* BGR, big-endian components */ + p2p_bgr48_le, /* RGB, little-endian components */ + p2p_bgr48, + /** [B16-G16-R16-A16] */ + p2p_bgra64_be, /* BGRA, big-endian components */ + p2p_bgra64_le, /* ARGB, little-endian components */ + p2p_bgra64, + + p2p_packing_max, +}; + +struct p2p_buffer_param { + /** + * Planar order: R-G-B-A or Y-U-V-A. Alpha is optional. + * Packed order: Y-UV if NV12/21, else single plane. Y optional for NV12/21. + */ + const void *src[4]; + void *dst[4]; + ptrdiff_t src_stride[4]; + ptrdiff_t dst_stride[4]; + unsigned width; + unsigned height; + enum p2p_packing packing; +}; + +/** Pack/unpack a range of pixels from a scanline. */ +typedef void (*p2p_unpack_func)(const void *src, void * const dst[4], unsigned left, unsigned right); +typedef void (*p2p_pack_func)(const void * const src[4], void *dst, unsigned left, unsigned right); + +/** Select a line pack/unpack function. */ +p2p_unpack_func p2p_select_unpack_func(enum p2p_packing packing); +p2p_pack_func p2p_select_pack_func(enum p2p_packing packing); +p2p_pack_func p2p_select_pack_func_ex(enum p2p_packing packing, int alpha_one_fill); + + +/** When processing formats like NV12, ignore the unpacked plane. */ +#define P2P_SKIP_UNPACKED_PLANES (1UL << 0) +/** When packing, store a bit pattern of all ones in the alpha channel instead of all zeros. */ +#define P2P_ALPHA_SET_ONE (1UL << 1) + +/** Helper function to pack/unpack between memory locations. */ +void p2p_unpack_frame(const struct p2p_buffer_param *param, unsigned long flags) {}; +void p2p_pack_frame(const struct p2p_buffer_param *param, unsigned long flags) {}; + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* P2P_API_H_ */ diff --git a/subprojects/packagefiles/bestsource/meson.build b/subprojects/packagefiles/bestsource/meson.build new file mode 100644 index 000000000..b4f77c93f --- /dev/null +++ b/subprojects/packagefiles/bestsource/meson.build @@ -0,0 +1,36 @@ +project('BestSource', 'cpp', + default_options: ['buildtype=release', 'b_ndebug=if-release', 'cpp_std=c++14'], + meson_version: '>=0.48.0' +) + +cmake = import('cmake') + +sources = [ + 'src/audiosource.cpp', + 'src/videosource.cpp', + 'src/SrcAttribCache.cpp', + 'src/BSRational.cpp', +] + +deps = [ + dependency('libavcodec'), + dependency('libavformat'), + dependency('libavutil'), +] + +jansson_dep = dependency('jansson', version: '>= 2.7', required: false) + +if jansson_dep.found() + deps += jansson_dep +else + jansson = cmake.subproject('jansson') + deps += jansson.dependency('jansson') +endif + +bs_lib = static_library('bestsource', sources, + dependencies: deps, + gnu_symbol_visibility: 'hidden' +) + +bestsource_dep = declare_dependency(link_with: bs_lib, include_directories: include_directories('src')) + diff --git a/subprojects/packagefiles/vapoursynth/meson.build b/subprojects/packagefiles/vapoursynth/meson.build new file mode 100644 index 000000000..5dfef44f7 --- /dev/null +++ b/subprojects/packagefiles/vapoursynth/meson.build @@ -0,0 +1,3 @@ +project('vapoursynth', 'cpp') + +vs_inc = include_directories('include') diff --git a/subprojects/vapoursynth.wrap b/subprojects/vapoursynth.wrap new file mode 100644 index 000000000..592446ccf --- /dev/null +++ b/subprojects/vapoursynth.wrap @@ -0,0 +1,4 @@ +[wrap-git] +url = https://github.com/Vapoursynth/vapoursynth.git +revision = R59 +patch_directory = vapoursynth diff --git a/tests/tests/audio.cpp b/tests/tests/audio.cpp index 23255c042..300834e5a 100644 --- a/tests/tests/audio.cpp +++ b/tests/tests/audio.cpp @@ -172,21 +172,21 @@ TEST(lagi_audio, save_audio_clip_out_of_audio_range) { TEST(lagi_audio, get_with_volume) { TestAudioProvider<> provider; - uint16_t buff[4]; + int16_t buff[4]; - provider.GetAudioWithVolume(buff, 0, 4, 1.0); + provider.GetInt16MonoAudioWithVolume(buff, 0, 4, 1.0); EXPECT_EQ(0, buff[0]); EXPECT_EQ(1, buff[1]); EXPECT_EQ(2, buff[2]); EXPECT_EQ(3, buff[3]); - provider.GetAudioWithVolume(buff, 0, 4, 0.0); + provider.GetInt16MonoAudioWithVolume(buff, 0, 4, 0.0); EXPECT_EQ(0, buff[0]); EXPECT_EQ(0, buff[1]); EXPECT_EQ(0, buff[2]); EXPECT_EQ(0, buff[3]); - provider.GetAudioWithVolume(buff, 0, 4, 2.0); + provider.GetInt16MonoAudioWithVolume(buff, 0, 4, 2.0); EXPECT_EQ(0, buff[0]); EXPECT_EQ(2, buff[1]); EXPECT_EQ(4, buff[2]); @@ -195,8 +195,8 @@ TEST(lagi_audio, get_with_volume) { TEST(lagi_audio, volume_should_clamp_rather_than_wrap) { TestAudioProvider<> provider; - uint16_t buff[1]; - provider.GetAudioWithVolume(buff, 30000, 1, 2.0); + int16_t buff[1]; + provider.GetInt16MonoAudioWithVolume(buff, 30000, 1, 2.0); EXPECT_EQ(SHRT_MAX, buff[0]); } @@ -232,7 +232,7 @@ TEST(lagi_audio, convert_8bit) { auto provider = agi::CreateConvertAudioProvider(agi::make_unique>()); int16_t data[256]; - provider->GetAudio(data, 0, 256); + provider->GetInt16MonoAudio(data, 0, 256); for (int i = 0; i < 256; ++i) ASSERT_EQ((i - 128) * 256, data[i]); } @@ -243,13 +243,13 @@ TEST(lagi_audio, convert_32bit) { auto provider = agi::CreateConvertAudioProvider(std::move(src)); int16_t sample; - provider->GetAudio(&sample, 0, 1); + provider->GetInt16MonoAudio(&sample, 0, 1); EXPECT_EQ(SHRT_MIN, sample); - provider->GetAudio(&sample, 1LL << 31, 1); + provider->GetInt16MonoAudio(&sample, 1LL << 31, 1); EXPECT_EQ(0, sample); - provider->GetAudio(&sample, (1LL << 32) - 1, 1); + provider->GetInt16MonoAudio(&sample, (1LL << 32) - 1, 1); EXPECT_EQ(SHRT_MAX, sample); } @@ -310,10 +310,10 @@ TEST(lagi_audio, stereo_downmix) { }; auto provider = agi::CreateConvertAudioProvider(agi::make_unique()); - EXPECT_EQ(1, provider->GetChannels()); + EXPECT_EQ(2, provider->GetChannels()); int16_t samples[100]; - provider->GetAudio(samples, 0, 100); + provider->GetInt16MonoAudio(samples, 0, 100); for (int i = 0; i < 100; ++i) EXPECT_EQ(i, samples[i]); } @@ -333,27 +333,27 @@ struct FloatAudioProvider : agi::AudioProvider { auto out = static_cast(buf); for (int64_t end = start + count; start < end; ++start) { auto shifted = start + SHRT_MIN; - *out++ = (Float)(1.0 * shifted / (shifted < 0 ? -SHRT_MIN : SHRT_MAX)); + *out++ = (Float)(shifted) / (-SHRT_MIN); } } }; TEST(lagi_audio, float_conversion) { auto provider = agi::CreateConvertAudioProvider(agi::make_unique>()); - EXPECT_FALSE(provider->AreSamplesFloat()); + EXPECT_TRUE(provider->AreSamplesFloat()); int16_t samples[1 << 16]; - provider->GetAudio(samples, 0, 1 << 16); + provider->GetInt16MonoAudio(samples, 0, 1 << 16); for (int i = 0; i < (1 << 16); ++i) ASSERT_EQ(i + SHRT_MIN, samples[i]); } TEST(lagi_audio, double_conversion) { auto provider = agi::CreateConvertAudioProvider(agi::make_unique>()); - EXPECT_FALSE(provider->AreSamplesFloat()); + EXPECT_TRUE(provider->AreSamplesFloat()); int16_t samples[1 << 16]; - provider->GetAudio(samples, 0, 1 << 16); + provider->GetInt16MonoAudio(samples, 0, 1 << 16); for (int i = 0; i < (1 << 16); ++i) ASSERT_EQ(i + SHRT_MIN, samples[i]); } @@ -551,4 +551,4 @@ TEST(lagi_audio, wave64_truncated) { } agi::fs::Remove(path); -} +} \ No newline at end of file