mirror of
https://github.com/Lime3DS/Lime3DS.git
synced 2024-12-18 05:51:49 +01:00
Merge pull request #4508 from B3n30/dsp_aac
CoreAudio::HLE: Add FFmpeg/WMF AAC decoder
This commit is contained in:
commit
1f38c53d8f
@ -5,7 +5,7 @@ cd /citra
|
|||||||
echo 'max_size = 3.0G' > "$HOME/.ccache/ccache.conf"
|
echo 'max_size = 3.0G' > "$HOME/.ccache/ccache.conf"
|
||||||
|
|
||||||
mkdir build && cd build
|
mkdir build && cd build
|
||||||
cmake .. -DCMAKE_TOOLCHAIN_FILE="$(pwd)/../CMakeModules/MinGWCross.cmake" -DUSE_CCACHE=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON
|
cmake .. -DCMAKE_TOOLCHAIN_FILE="$(pwd)/../CMakeModules/MinGWCross.cmake" -DUSE_CCACHE=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON -DENABLE_MF=ON
|
||||||
make -j4
|
make -j4
|
||||||
|
|
||||||
echo "Tests skipped"
|
echo "Tests skipped"
|
||||||
|
@ -7,7 +7,7 @@ export Qt5_DIR=$(brew --prefix)/opt/qt5
|
|||||||
export PATH="/usr/local/opt/ccache/libexec:$PATH"
|
export PATH="/usr/local/opt/ccache/libexec:$PATH"
|
||||||
|
|
||||||
mkdir build && cd build
|
mkdir build && cd build
|
||||||
cmake .. -DCMAKE_OSX_ARCHITECTURES="x86_64;x86_64h" -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON
|
cmake .. -DCMAKE_OSX_ARCHITECTURES="x86_64;x86_64h" -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON -DENABLE_FFMPEG=ON
|
||||||
make -j4
|
make -j4
|
||||||
|
|
||||||
ctest -VV -C Release
|
ctest -VV -C Release
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh -ex
|
#!/bin/sh -ex
|
||||||
|
|
||||||
brew update
|
brew update
|
||||||
brew install qt5 sdl2 dylibbundler p7zip ccache
|
brew install qt5 sdl2 dylibbundler p7zip ccache ffmpeg
|
||||||
|
@ -21,10 +21,10 @@ $(brew --prefix)/opt/qt5/bin/macdeployqt "${REV_NAME}/citra-qt.app" -executable=
|
|||||||
dylibbundler -b -x "${REV_NAME}/citra" -cd -d "${REV_NAME}/libs" -p "@executable_path/libs/"
|
dylibbundler -b -x "${REV_NAME}/citra" -cd -d "${REV_NAME}/libs" -p "@executable_path/libs/"
|
||||||
|
|
||||||
# TODO(merry): Figure out why these libraries are not automatically processed
|
# TODO(merry): Figure out why these libraries are not automatically processed
|
||||||
install_name_tool -change /usr/local/Cellar/ffmpeg/4.1_1/lib/libavutil.56.dylib @executable_path/../Frameworks/libavutil.56.dylib "${REV_NAME}/citra-qt.app/Contents/Frameworks/libavcodec.58.dylib"
|
install_name_tool -change /usr/local/Cellar/ffmpeg/4.1_6/lib/libavutil.56.dylib @executable_path/../Frameworks/libavutil.56.dylib "${REV_NAME}/citra-qt.app/Contents/Frameworks/libavcodec.58.dylib"
|
||||||
install_name_tool -change /usr/local/Cellar/ffmpeg/4.1_1/lib/libavutil.56.dylib @executable_path/../Frameworks/libavutil.56.dylib "${REV_NAME}/citra-qt.app/Contents/Frameworks/libswresample.3.dylib"
|
install_name_tool -change /usr/local/Cellar/ffmpeg/4.1_6/lib/libavutil.56.dylib @executable_path/../Frameworks/libavutil.56.dylib "${REV_NAME}/citra-qt.app/Contents/Frameworks/libswresample.3.dylib"
|
||||||
install_name_tool -change /usr/local/Cellar/ffmpeg/4.1_1/lib/libavutil.56.dylib @executable_path/libs/libavutil.56.dylib "${REV_NAME}/libs/libavcodec.58.dylib"
|
install_name_tool -change /usr/local/Cellar/ffmpeg/4.1_6/lib/libavutil.56.dylib @executable_path/libs/libavutil.56.dylib "${REV_NAME}/libs/libavcodec.58.dylib"
|
||||||
install_name_tool -change /usr/local/Cellar/ffmpeg/4.1_1/lib/libavutil.56.dylib @executable_path/libs/libavutil.56.dylib "${REV_NAME}/libs/libswresample.3.dylib"
|
install_name_tool -change /usr/local/Cellar/ffmpeg/4.1_6/lib/libavutil.56.dylib @executable_path/libs/libavutil.56.dylib "${REV_NAME}/libs/libswresample.3.dylib"
|
||||||
install_name_tool -change /usr/local/Cellar/libvorbis/1.3.6/lib/libvorbis.0.dylib @executable_path/libs/libavutil.56.dylib "${REV_NAME}/libs/libvorbisenc.2.dylib"
|
install_name_tool -change /usr/local/Cellar/libvorbis/1.3.6/lib/libvorbis.0.dylib @executable_path/libs/libavutil.56.dylib "${REV_NAME}/libs/libvorbisenc.2.dylib"
|
||||||
|
|
||||||
# Make the citra-qt.app application launch a debugging terminal.
|
# Make the citra-qt.app application launch a debugging terminal.
|
||||||
|
@ -20,8 +20,12 @@ option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" ON)
|
|||||||
|
|
||||||
option(ENABLE_CUBEB "Enables the cubeb audio backend" ON)
|
option(ENABLE_CUBEB "Enables the cubeb audio backend" ON)
|
||||||
|
|
||||||
|
option(ENABLE_FFMPEG "Enable FFmpeg decoder/encoder" OFF)
|
||||||
|
|
||||||
option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
|
option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
|
||||||
|
|
||||||
|
CMAKE_DEPENDENT_OPTION(ENABLE_MF "Use Media Foundation decoder" ON "WIN32;NOT ENABLE_FFMPEG" OFF)
|
||||||
|
|
||||||
if(NOT EXISTS ${PROJECT_SOURCE_DIR}/.git/hooks/pre-commit)
|
if(NOT EXISTS ${PROJECT_SOURCE_DIR}/.git/hooks/pre-commit)
|
||||||
message(STATUS "Copying pre-commit hook")
|
message(STATUS "Copying pre-commit hook")
|
||||||
file(COPY hooks/pre-commit
|
file(COPY hooks/pre-commit
|
||||||
@ -251,6 +255,31 @@ if (ENABLE_QT)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (ENABLE_FFMPEG)
|
||||||
|
if (CITRA_USE_BUNDLED_FFMPEG)
|
||||||
|
if ((MSVC_VERSION GREATER_EQUAL 1910 AND MSVC_VERSION LESS 1920) AND ARCHITECTURE_x86_64)
|
||||||
|
set(FFmpeg_VER "ffmpeg-4.0.2-msvc")
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "No bundled FFmpeg binaries for your toolchain. Disable CITRA_USE_BUNDLED_FFMPEG and provide your own.")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (DEFINED FFmpeg_VER)
|
||||||
|
download_bundled_external("ffmpeg/" ${FFmpeg_VER} FFmpeg_PREFIX)
|
||||||
|
set(FFMPEG_DIR "${FFmpeg_PREFIX}/../")
|
||||||
|
set(FFMPEG_FOUND YES)
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
find_package(FFmpeg REQUIRED COMPONENTS avcodec)
|
||||||
|
if ("${FFmpeg_avcodec_VERSION}" VERSION_LESS "57.48.101")
|
||||||
|
message(FATAL_ERROR "Found version for libavcodec is too low. The required version is at least 57.48.101 (included in FFmpeg 3.1 and later).")
|
||||||
|
else()
|
||||||
|
set(FFMPEG_FOUND YES)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
set(FFMPEG_FOUND NO)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Platform-specific library requirements
|
# Platform-specific library requirements
|
||||||
# ======================================
|
# ======================================
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ install:
|
|||||||
- git submodule update --init --recursive
|
- git submodule update --init --recursive
|
||||||
- ps: |
|
- ps: |
|
||||||
if ($env:BUILD_TYPE -eq 'mingw') {
|
if ($env:BUILD_TYPE -eq 'mingw') {
|
||||||
$dependencies = "mingw64/mingw-w64-x86_64-qt5"
|
$dependencies = "mingw64/mingw-w64-x86_64-qt5 mingw64/mingw-w64-x86_64-ffmpeg"
|
||||||
# redirect err to null to prevent warnings from becoming errors
|
# redirect err to null to prevent warnings from becoming errors
|
||||||
# workaround to prevent pacman from failing due to cyclical dependencies
|
# workaround to prevent pacman from failing due to cyclical dependencies
|
||||||
C:\msys64\usr\bin\bash -lc "pacman --noconfirm -S mingw64/mingw-w64-x86_64-freetype mingw64/mingw-w64-x86_64-fontconfig" 2> $null
|
C:\msys64\usr\bin\bash -lc "pacman --noconfirm -S mingw64/mingw-w64-x86_64-freetype mingw64/mingw-w64-x86_64-fontconfig" 2> $null
|
||||||
@ -43,9 +43,9 @@ before_build:
|
|||||||
$COMPAT = if ($env:ENABLE_COMPATIBILITY_REPORTING -eq $null) {0} else {$env:ENABLE_COMPATIBILITY_REPORTING}
|
$COMPAT = if ($env:ENABLE_COMPATIBILITY_REPORTING -eq $null) {0} else {$env:ENABLE_COMPATIBILITY_REPORTING}
|
||||||
if ($env:BUILD_TYPE -eq 'msvc') {
|
if ($env:BUILD_TYPE -eq 'msvc') {
|
||||||
# redirect stderr and change the exit code to prevent powershell from cancelling the build if cmake prints a warning
|
# redirect stderr and change the exit code to prevent powershell from cancelling the build if cmake prints a warning
|
||||||
cmd /C 'cmake -G "Visual Studio 15 2017 Win64" -DCITRA_USE_BUNDLED_QT=1 -DCITRA_USE_BUNDLED_SDL2=1 -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON .. 2>&1 && exit 0'
|
cmd /C 'cmake -G "Visual Studio 15 2017 Win64" -DCITRA_USE_BUNDLED_QT=1 -DCITRA_USE_BUNDLED_SDL2=1 -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON -DENABLE_MF=ON .. 2>&1 && exit 0'
|
||||||
} else {
|
} else {
|
||||||
C:\msys64\usr\bin\bash.exe -lc "cmake -G 'MSYS Makefiles' -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON .. 2>&1"
|
C:\msys64\usr\bin\bash.exe -lc "cmake -G 'MSYS Makefiles' -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON -DENABLE_MF=ON .. 2>&1"
|
||||||
}
|
}
|
||||||
- cd ..
|
- cd ..
|
||||||
|
|
||||||
|
183
externals/cmake-modules/FindFFmpeg.cmake
vendored
Normal file
183
externals/cmake-modules/FindFFmpeg.cmake
vendored
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
# FindFFmpeg
|
||||||
|
# ----------
|
||||||
|
#
|
||||||
|
# Find the native FFmpeg includes and libraries
|
||||||
|
#
|
||||||
|
# This module defines the following variables:
|
||||||
|
#
|
||||||
|
# FFmpeg_INCLUDE_<component>: where to find <component>.h
|
||||||
|
# FFmpeg_LIBRARY_<component>: where to find the <component> library
|
||||||
|
# FFmpeg_INCLUDES: aggregate all the include paths
|
||||||
|
# FFmpeg_LIBRARIES: aggregate all the paths to the libraries
|
||||||
|
# FFmpeg_FOUND: True if all components have been found
|
||||||
|
#
|
||||||
|
# This module defines the following targets, which are prefered over variables:
|
||||||
|
#
|
||||||
|
# FFmpeg::<component>: Target to use <component> directly, with include path,
|
||||||
|
# library and dependencies set up. If you are using a static build, you are
|
||||||
|
# responsible for adding any external dependencies (such as zlib, bzlib...).
|
||||||
|
#
|
||||||
|
# <component> can be one of:
|
||||||
|
# avcodec
|
||||||
|
# avdevice
|
||||||
|
# avfilter
|
||||||
|
# avformat
|
||||||
|
# postproc
|
||||||
|
# swresample
|
||||||
|
# swscale
|
||||||
|
#
|
||||||
|
|
||||||
|
set(_FFmpeg_ALL_COMPONENTS
|
||||||
|
avcodec
|
||||||
|
avdevice
|
||||||
|
avfilter
|
||||||
|
avformat
|
||||||
|
avutil
|
||||||
|
postproc
|
||||||
|
swresample
|
||||||
|
swscale
|
||||||
|
)
|
||||||
|
|
||||||
|
set(_FFmpeg_DEPS_avcodec avutil)
|
||||||
|
set(_FFmpeg_DEPS_avdevice avcodec avformat avutil)
|
||||||
|
set(_FFmpeg_DEPS_avfilter avutil)
|
||||||
|
set(_FFmpeg_DEPS_avformat avcodec avutil)
|
||||||
|
set(_FFmpeg_DEPS_postproc avutil)
|
||||||
|
set(_FFmpeg_DEPS_swresample avutil)
|
||||||
|
set(_FFmpeg_DEPS_swscale avutil)
|
||||||
|
|
||||||
|
function(find_ffmpeg LIBNAME)
|
||||||
|
if(DEFINED ENV{FFMPEG_DIR})
|
||||||
|
set(FFMPEG_DIR $ENV{FFMPEG_DIR})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(FFMPEG_DIR)
|
||||||
|
list(APPEND INCLUDE_PATHS
|
||||||
|
${FFMPEG_DIR}
|
||||||
|
${FFMPEG_DIR}/ffmpeg
|
||||||
|
${FFMPEG_DIR}/lib${LIBNAME}
|
||||||
|
${FFMPEG_DIR}/include/lib${LIBNAME}
|
||||||
|
${FFMPEG_DIR}/include/ffmpeg
|
||||||
|
${FFMPEG_DIR}/include
|
||||||
|
NO_DEFAULT_PATH
|
||||||
|
NO_CMAKE_FIND_ROOT_PATH
|
||||||
|
)
|
||||||
|
list(APPEND LIB_PATHS
|
||||||
|
${FFMPEG_DIR}
|
||||||
|
${FFMPEG_DIR}/lib
|
||||||
|
${FFMPEG_DIR}/lib${LIBNAME}
|
||||||
|
NO_DEFAULT_PATH
|
||||||
|
NO_CMAKE_FIND_ROOT_PATH
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
list(APPEND INCLUDE_PATHS
|
||||||
|
/usr/local/include/ffmpeg
|
||||||
|
/usr/local/include/lib${LIBNAME}
|
||||||
|
/usr/include/ffmpeg
|
||||||
|
/usr/include/lib${LIBNAME}
|
||||||
|
/usr/include/ffmpeg/lib${LIBNAME}
|
||||||
|
)
|
||||||
|
|
||||||
|
list(APPEND LIB_PATHS
|
||||||
|
/usr/local/lib
|
||||||
|
/usr/lib
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
find_path(FFmpeg_INCLUDE_${LIBNAME} lib${LIBNAME}/${LIBNAME}.h
|
||||||
|
HINTS ${INCLUDE_PATHS}
|
||||||
|
)
|
||||||
|
|
||||||
|
find_library(FFmpeg_LIBRARY_${LIBNAME} ${LIBNAME}
|
||||||
|
HINTS ${LIB_PATHS}
|
||||||
|
)
|
||||||
|
|
||||||
|
if(NOT FFMPEG_DIR AND (NOT FFmpeg_LIBRARY_${LIBNAME} OR NOT FFmpeg_INCLUDE_${LIBNAME}))
|
||||||
|
# Didn't find it in the usual paths, try pkg-config
|
||||||
|
find_package(PkgConfig QUIET)
|
||||||
|
pkg_check_modules(FFmpeg_PKGCONFIG_${LIBNAME} QUIET lib${LIBNAME})
|
||||||
|
|
||||||
|
find_path(FFmpeg_INCLUDE_${LIBNAME} lib${LIBNAME}/${LIBNAME}.h
|
||||||
|
${FFmpeg_PKGCONFIG_${LIBNAME}_INCLUDE_DIRS}
|
||||||
|
)
|
||||||
|
|
||||||
|
find_library(FFmpeg_LIBRARY_${LIBNAME} ${LIBNAME}
|
||||||
|
${FFmpeg_PKGCONFIG_${LIBNAME}_LIBRARY_DIRS}
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(FFmpeg_INCLUDE_${LIBNAME} AND FFmpeg_LIBRARY_${LIBNAME})
|
||||||
|
set(FFmpeg_INCLUDE_${LIBNAME} "${FFmpeg_INCLUDE_${LIBNAME}}" PARENT_SCOPE)
|
||||||
|
set(FFmpeg_LIBRARY_${LIBNAME} "${FFmpeg_LIBRARY_${LIBNAME}}" PARENT_SCOPE)
|
||||||
|
|
||||||
|
# Extract FFmpeg version from version.h
|
||||||
|
foreach(v MAJOR MINOR MICRO)
|
||||||
|
set(FFmpeg_${LIBNAME}_VERSION_${v} 0)
|
||||||
|
endforeach()
|
||||||
|
string(TOUPPER ${LIBNAME} LIBNAME_UPPER)
|
||||||
|
file(STRINGS "${FFmpeg_INCLUDE_${LIBNAME}}/lib${LIBNAME}/version.h" _FFmpeg_VERSION_H_CONTENTS REGEX "#define LIB${LIBNAME_UPPER}_VERSION_(MAJOR|MINOR|MICRO) ")
|
||||||
|
set(_FFmpeg_VERSION_REGEX "([0-9]+)")
|
||||||
|
foreach(v MAJOR MINOR MICRO)
|
||||||
|
if("${_FFmpeg_VERSION_H_CONTENTS}" MATCHES "#define LIB${LIBNAME_UPPER}_VERSION_${v}[\\t ]+${_FFmpeg_VERSION_REGEX}")
|
||||||
|
set(FFmpeg_${LIBNAME}_VERSION_${v} "${CMAKE_MATCH_1}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
set(FFmpeg_${LIBNAME}_VERSION "${FFmpeg_${LIBNAME}_VERSION_MAJOR}.${FFmpeg_${LIBNAME}_VERSION_MINOR}.${FFmpeg_${LIBNAME}_VERSION_MICRO}")
|
||||||
|
set(FFmpeg_${c}_VERSION "${FFmpeg_${LIBNAME}_VERSION}" PARENT_SCOPE)
|
||||||
|
unset(_FFmpeg_VERSION_REGEX)
|
||||||
|
unset(_FFmpeg_VERSION_H_CONTENTS)
|
||||||
|
|
||||||
|
set(FFmpeg_${c}_FOUND TRUE PARENT_SCOPE)
|
||||||
|
if(NOT FFmpeg_FIND_QUIETLY)
|
||||||
|
message("-- Found ${LIBNAME}: ${FFmpeg_INCLUDE_${LIBNAME}} ${FFmpeg_LIBRARY_${LIBNAME}} (version: ${FFmpeg_${LIBNAME}_VERSION})")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
foreach(c ${_FFmpeg_ALL_COMPONENTS})
|
||||||
|
find_ffmpeg(${c})
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
foreach(c ${_FFmpeg_ALL_COMPONENTS})
|
||||||
|
if(FFmpeg_${c}_FOUND)
|
||||||
|
list(APPEND FFmpeg_INCLUDES ${FFmpeg_INCLUDE_${c}})
|
||||||
|
list(APPEND FFmpeg_LIBRARIES ${FFmpeg_LIBRARY_${c}})
|
||||||
|
|
||||||
|
add_library(FFmpeg::${c} IMPORTED UNKNOWN)
|
||||||
|
set_target_properties(FFmpeg::${c} PROPERTIES
|
||||||
|
IMPORTED_LOCATION ${FFmpeg_LIBRARY_${c}}
|
||||||
|
INTERFACE_INCLUDE_DIRECTORIES ${FFmpeg_INCLUDE_${c}}
|
||||||
|
)
|
||||||
|
if(_FFmpeg_DEPS_${c})
|
||||||
|
set(deps)
|
||||||
|
foreach(dep ${_FFmpeg_DEPS_${c}})
|
||||||
|
list(APPEND deps FFmpeg::${dep})
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
set_target_properties(FFmpeg::${c} PROPERTIES
|
||||||
|
INTERFACE_LINK_LIBRARIES "${deps}"
|
||||||
|
)
|
||||||
|
unset(deps)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
if(FFmpeg_INCLUDES)
|
||||||
|
list(REMOVE_DUPLICATES FFmpeg_INCLUDES)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
foreach(c ${FFmpeg_FIND_COMPONENTS})
|
||||||
|
list(APPEND _FFmpeg_REQUIRED_VARS FFmpeg_INCLUDE_${c} FFmpeg_LIBRARY_${c})
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
include(FindPackageHandleStandardArgs)
|
||||||
|
find_package_handle_standard_args(FFmpeg
|
||||||
|
REQUIRED_VARS ${_FFmpeg_REQUIRED_VARS}
|
||||||
|
HANDLE_COMPONENTS
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach(c ${_FFmpeg_ALL_COMPONENTS})
|
||||||
|
unset(_FFmpeg_DEPS_${c})
|
||||||
|
endforeach()
|
||||||
|
unset(_FFmpeg_ALL_COMPONENTS)
|
||||||
|
unset(_FFmpeg_REQUIRED_VARS)
|
@ -4,7 +4,11 @@ add_library(audio_core STATIC
|
|||||||
codec.h
|
codec.h
|
||||||
dsp_interface.cpp
|
dsp_interface.cpp
|
||||||
dsp_interface.h
|
dsp_interface.h
|
||||||
|
hle/adts.h
|
||||||
|
hle/adts_reader.cpp
|
||||||
hle/common.h
|
hle/common.h
|
||||||
|
hle/decoder.cpp
|
||||||
|
hle/decoder.h
|
||||||
hle/filter.cpp
|
hle/filter.cpp
|
||||||
hle/filter.h
|
hle/filter.h
|
||||||
hle/hle.cpp
|
hle/hle.cpp
|
||||||
@ -27,6 +31,8 @@ add_library(audio_core STATIC
|
|||||||
|
|
||||||
$<$<BOOL:${SDL2_FOUND}>:sdl2_sink.cpp sdl2_sink.h>
|
$<$<BOOL:${SDL2_FOUND}>:sdl2_sink.cpp sdl2_sink.h>
|
||||||
$<$<BOOL:${ENABLE_CUBEB}>:cubeb_sink.cpp cubeb_sink.h>
|
$<$<BOOL:${ENABLE_CUBEB}>:cubeb_sink.cpp cubeb_sink.h>
|
||||||
|
$<$<BOOL:${FFMPEG_FOUND}>:hle/ffmpeg_decoder.cpp hle/ffmpeg_decoder.h hle/ffmpeg_dl.cpp hle/ffmpeg_dl.h>
|
||||||
|
$<$<BOOL:${ENABLE_MF}>:hle/wmf_decoder.cpp hle/wmf_decoder.h hle/wmf_decoder_utils.cpp hle/wmf_decoder_utils.h>
|
||||||
)
|
)
|
||||||
|
|
||||||
create_target_directory_groups(audio_core)
|
create_target_directory_groups(audio_core)
|
||||||
@ -34,6 +40,20 @@ create_target_directory_groups(audio_core)
|
|||||||
target_link_libraries(audio_core PUBLIC common core)
|
target_link_libraries(audio_core PUBLIC common core)
|
||||||
target_link_libraries(audio_core PRIVATE SoundTouch teakra)
|
target_link_libraries(audio_core PRIVATE SoundTouch teakra)
|
||||||
|
|
||||||
|
if(FFMPEG_FOUND)
|
||||||
|
if(UNIX)
|
||||||
|
target_link_libraries(audio_core PRIVATE FFmpeg::avcodec)
|
||||||
|
else()
|
||||||
|
target_include_directories(audio_core PRIVATE ${FFMPEG_DIR}/include)
|
||||||
|
endif()
|
||||||
|
target_compile_definitions(audio_core PUBLIC HAVE_FFMPEG)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(ENABLE_MF)
|
||||||
|
target_link_libraries(audio_core PRIVATE mf.lib mfplat.lib mfuuid.lib)
|
||||||
|
target_compile_definitions(audio_core PUBLIC HAVE_MF)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(SDL2_FOUND)
|
if(SDL2_FOUND)
|
||||||
target_link_libraries(audio_core PRIVATE SDL2)
|
target_link_libraries(audio_core PRIVATE SDL2)
|
||||||
target_compile_definitions(audio_core PRIVATE HAVE_SDL2)
|
target_compile_definitions(audio_core PRIVATE HAVE_SDL2)
|
||||||
@ -43,3 +63,4 @@ if(ENABLE_CUBEB)
|
|||||||
target_link_libraries(audio_core PRIVATE cubeb)
|
target_link_libraries(audio_core PRIVATE cubeb)
|
||||||
add_definitions(-DHAVE_CUBEB=1)
|
add_definitions(-DHAVE_CUBEB=1)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
23
src/audio_core/hle/adts.h
Normal file
23
src/audio_core/hle/adts.h
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// Copyright 2019 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
struct ADTSData {
|
||||||
|
bool MPEG2;
|
||||||
|
u8 profile;
|
||||||
|
u8 channels;
|
||||||
|
u8 channel_idx;
|
||||||
|
u8 framecount;
|
||||||
|
u8 samplerate_idx;
|
||||||
|
u32 length;
|
||||||
|
u32 samplerate;
|
||||||
|
};
|
||||||
|
|
||||||
|
ADTSData ParseADTS(const char* buffer);
|
||||||
|
|
||||||
|
// last two bytes of MF AAC decoder user data
|
||||||
|
// see https://docs.microsoft.com/en-us/windows/desktop/medfound/aac-decoder#example-media-types
|
||||||
|
u16 MFGetAACTag(const ADTSData& input);
|
61
src/audio_core/hle/adts_reader.cpp
Normal file
61
src/audio_core/hle/adts_reader.cpp
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// Copyright 2019 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
#include <array>
|
||||||
|
#include "adts.h"
|
||||||
|
|
||||||
|
constexpr std::array<u32, 16> freq_table = {96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050,
|
||||||
|
16000, 12000, 11025, 8000, 7350, 0, 0, 0};
|
||||||
|
constexpr std::array<u8, 8> channel_table = {0, 1, 2, 3, 4, 5, 6, 8};
|
||||||
|
|
||||||
|
ADTSData ParseADTS(const char* buffer) {
|
||||||
|
u32 tmp = 0;
|
||||||
|
ADTSData out;
|
||||||
|
|
||||||
|
// sync word 0xfff
|
||||||
|
tmp = (buffer[0] << 8) | (buffer[1] & 0xf0);
|
||||||
|
if ((tmp & 0xffff) != 0xfff0) {
|
||||||
|
out.length = 0;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
out.MPEG2 = (buffer[1] >> 3) & 0x1;
|
||||||
|
// bit 17 to 18
|
||||||
|
out.profile = (buffer[2] >> 6) + 1;
|
||||||
|
// bit 19 to 22
|
||||||
|
tmp = (buffer[2] >> 2) & 0xf;
|
||||||
|
out.samplerate_idx = tmp;
|
||||||
|
out.samplerate = (tmp > 15) ? 0 : freq_table[tmp];
|
||||||
|
// bit 24 to 26
|
||||||
|
tmp = ((buffer[2] & 0x1) << 2) | ((buffer[3] >> 6) & 0x3);
|
||||||
|
out.channel_idx = tmp;
|
||||||
|
out.channels = (tmp > 7) ? 0 : channel_table[tmp];
|
||||||
|
|
||||||
|
// bit 55 to 56
|
||||||
|
out.framecount = (buffer[6] & 0x3) + 1;
|
||||||
|
|
||||||
|
// bit 31 to 43
|
||||||
|
tmp = (buffer[3] & 0x3) << 11;
|
||||||
|
tmp |= (buffer[4] << 3) & 0x7f8;
|
||||||
|
tmp |= (buffer[5] >> 5) & 0x7;
|
||||||
|
|
||||||
|
out.length = tmp;
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// last two bytes of MF AAC decoder user data
|
||||||
|
// Audio object type (5 bits)
|
||||||
|
// Sample rate profile (4 bits)
|
||||||
|
// Channel configuration profile (4 bits)
|
||||||
|
// Frame length flag (1 bit)
|
||||||
|
// Depends on core coder (1 bit)
|
||||||
|
// Extension flag (1 bit)
|
||||||
|
u16 MFGetAACTag(const ADTSData& input) {
|
||||||
|
u16 tag = 0;
|
||||||
|
|
||||||
|
tag |= input.profile << 11;
|
||||||
|
tag |= input.samplerate_idx << 7;
|
||||||
|
tag |= input.channel_idx << 3;
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
}
|
35
src/audio_core/hle/decoder.cpp
Normal file
35
src/audio_core/hle/decoder.cpp
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include "audio_core/hle/decoder.h"
|
||||||
|
|
||||||
|
namespace AudioCore::HLE {
|
||||||
|
|
||||||
|
DecoderBase::~DecoderBase(){};
|
||||||
|
|
||||||
|
NullDecoder::NullDecoder() = default;
|
||||||
|
|
||||||
|
NullDecoder::~NullDecoder() = default;
|
||||||
|
|
||||||
|
std::optional<BinaryResponse> NullDecoder::ProcessRequest(const BinaryRequest& request) {
|
||||||
|
BinaryResponse response;
|
||||||
|
switch (request.cmd) {
|
||||||
|
case DecoderCommand::Init:
|
||||||
|
case DecoderCommand::Unknown:
|
||||||
|
std::memcpy(&response, &request, sizeof(response));
|
||||||
|
response.unknown1 = 0x0;
|
||||||
|
return response;
|
||||||
|
case DecoderCommand::Decode:
|
||||||
|
response.codec = request.codec;
|
||||||
|
response.cmd = DecoderCommand::Decode;
|
||||||
|
response.num_channels = 2; // Just assume stereo here
|
||||||
|
response.size = request.size;
|
||||||
|
response.num_samples = 1024; // Just assume 1024 here
|
||||||
|
return response;
|
||||||
|
default:
|
||||||
|
LOG_ERROR(Audio_DSP, "Got unknown binary request: {}", static_cast<u16>(request.cmd));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace AudioCore::HLE
|
68
src/audio_core/hle/decoder.h
Normal file
68
src/audio_core/hle/decoder.h
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/swap.h"
|
||||||
|
#include "core/core.h"
|
||||||
|
|
||||||
|
namespace AudioCore::HLE {
|
||||||
|
|
||||||
|
enum class DecoderCommand : u16 {
|
||||||
|
Init,
|
||||||
|
Decode,
|
||||||
|
Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class DecoderCodec : u16 {
|
||||||
|
None,
|
||||||
|
AAC,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BinaryRequest {
|
||||||
|
enum_le<DecoderCodec> codec =
|
||||||
|
DecoderCodec::None; // this is a guess. until now only 0x1 was observed here
|
||||||
|
enum_le<DecoderCommand> cmd = DecoderCommand::Init;
|
||||||
|
u32_le fixed = 0;
|
||||||
|
u32_le src_addr = 0;
|
||||||
|
u32_le size = 0;
|
||||||
|
u32_le dst_addr_ch0 = 0;
|
||||||
|
u32_le dst_addr_ch1 = 0;
|
||||||
|
u32_le unknown1 = 0;
|
||||||
|
u32_le unknown2 = 0;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(BinaryRequest) == 32, "Unexpected struct size for BinaryRequest");
|
||||||
|
|
||||||
|
struct BinaryResponse {
|
||||||
|
enum_le<DecoderCodec> codec =
|
||||||
|
DecoderCodec::None; // this could be something else. until now only 0x1 was observed here
|
||||||
|
enum_le<DecoderCommand> cmd = DecoderCommand::Init;
|
||||||
|
u32_le unknown1 = 0;
|
||||||
|
u32_le unknown2 = 0;
|
||||||
|
u32_le num_channels = 0; // this is a guess, so far I only observed 2 here
|
||||||
|
u32_le size = 0;
|
||||||
|
u32_le unknown3 = 0;
|
||||||
|
u32_le unknown4 = 0;
|
||||||
|
u32_le num_samples = 0; // this is a guess, so far I only observed 1024 here
|
||||||
|
};
|
||||||
|
static_assert(sizeof(BinaryResponse) == 32, "Unexpected struct size for BinaryResponse");
|
||||||
|
|
||||||
|
class DecoderBase {
|
||||||
|
public:
|
||||||
|
virtual ~DecoderBase();
|
||||||
|
virtual std::optional<BinaryResponse> ProcessRequest(const BinaryRequest& request) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class NullDecoder final : public DecoderBase {
|
||||||
|
public:
|
||||||
|
NullDecoder();
|
||||||
|
~NullDecoder() override;
|
||||||
|
std::optional<BinaryResponse> ProcessRequest(const BinaryRequest& request) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace AudioCore::HLE
|
263
src/audio_core/hle/ffmpeg_decoder.cpp
Normal file
263
src/audio_core/hle/ffmpeg_decoder.cpp
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include "audio_core/hle/ffmpeg_decoder.h"
|
||||||
|
#include "audio_core/hle/ffmpeg_dl.h"
|
||||||
|
|
||||||
|
namespace AudioCore::HLE {
|
||||||
|
|
||||||
|
class FFMPEGDecoder::Impl {
|
||||||
|
public:
|
||||||
|
explicit Impl(Memory::MemorySystem& memory);
|
||||||
|
~Impl();
|
||||||
|
std::optional<BinaryResponse> ProcessRequest(const BinaryRequest& request);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::optional<BinaryResponse> Initalize(const BinaryRequest& request);
|
||||||
|
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
std::optional<BinaryResponse> Decode(const BinaryRequest& request);
|
||||||
|
|
||||||
|
struct AVPacketDeleter {
|
||||||
|
void operator()(AVPacket* packet) const {
|
||||||
|
av_packet_free_dl(&packet);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AVCodecContextDeleter {
|
||||||
|
void operator()(AVCodecContext* context) const {
|
||||||
|
avcodec_free_context_dl(&context);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AVCodecParserContextDeleter {
|
||||||
|
void operator()(AVCodecParserContext* parser) const {
|
||||||
|
av_parser_close_dl(parser);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AVFrameDeleter {
|
||||||
|
void operator()(AVFrame* frame) const {
|
||||||
|
av_frame_free_dl(&frame);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
bool initalized = false;
|
||||||
|
bool have_ffmpeg_dl;
|
||||||
|
|
||||||
|
Memory::MemorySystem& memory;
|
||||||
|
|
||||||
|
AVCodec* codec;
|
||||||
|
std::unique_ptr<AVCodecContext, AVCodecContextDeleter> av_context;
|
||||||
|
std::unique_ptr<AVCodecParserContext, AVCodecParserContextDeleter> parser;
|
||||||
|
std::unique_ptr<AVPacket, AVPacketDeleter> av_packet;
|
||||||
|
std::unique_ptr<AVFrame, AVFrameDeleter> decoded_frame;
|
||||||
|
};
|
||||||
|
|
||||||
|
FFMPEGDecoder::Impl::Impl(Memory::MemorySystem& memory) : memory(memory) {
|
||||||
|
have_ffmpeg_dl = InitFFmpegDL();
|
||||||
|
}
|
||||||
|
|
||||||
|
FFMPEGDecoder::Impl::~Impl() = default;
|
||||||
|
|
||||||
|
std::optional<BinaryResponse> FFMPEGDecoder::Impl::ProcessRequest(const BinaryRequest& request) {
|
||||||
|
if (request.codec != DecoderCodec::AAC) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Got wrong codec {}", static_cast<u16>(request.codec));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (request.cmd) {
|
||||||
|
case DecoderCommand::Init: {
|
||||||
|
return Initalize(request);
|
||||||
|
}
|
||||||
|
case DecoderCommand::Decode: {
|
||||||
|
return Decode(request);
|
||||||
|
}
|
||||||
|
case DecoderCommand::Unknown: {
|
||||||
|
BinaryResponse response;
|
||||||
|
std::memcpy(&response, &request, sizeof(response));
|
||||||
|
response.unknown1 = 0x0;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
LOG_ERROR(Audio_DSP, "Got unknown binary request: {}", static_cast<u16>(request.cmd));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<BinaryResponse> FFMPEGDecoder::Impl::Initalize(const BinaryRequest& request) {
|
||||||
|
if (initalized) {
|
||||||
|
Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
BinaryResponse response;
|
||||||
|
std::memcpy(&response, &request, sizeof(response));
|
||||||
|
response.unknown1 = 0x0;
|
||||||
|
|
||||||
|
if (!have_ffmpeg_dl) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
av_packet.reset(av_packet_alloc_dl());
|
||||||
|
|
||||||
|
codec = avcodec_find_decoder_dl(AV_CODEC_ID_AAC);
|
||||||
|
if (!codec) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Codec not found\n");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.reset(av_parser_init_dl(codec->id));
|
||||||
|
if (!parser) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Parser not found\n");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
av_context.reset(avcodec_alloc_context3_dl(codec));
|
||||||
|
if (!av_context) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Could not allocate audio codec context\n");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avcodec_open2_dl(av_context.get(), codec, nullptr) < 0) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Could not open codec\n");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
initalized = true;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FFMPEGDecoder::Impl::Clear() {
|
||||||
|
if (!have_ffmpeg_dl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
av_context.reset();
|
||||||
|
parser.reset();
|
||||||
|
decoded_frame.reset();
|
||||||
|
av_packet.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<BinaryResponse> FFMPEGDecoder::Impl::Decode(const BinaryRequest& request) {
|
||||||
|
BinaryResponse response;
|
||||||
|
response.codec = request.codec;
|
||||||
|
response.cmd = request.cmd;
|
||||||
|
response.size = request.size;
|
||||||
|
|
||||||
|
if (!initalized) {
|
||||||
|
LOG_DEBUG(Audio_DSP, "Decoder not initalized");
|
||||||
|
// This is a hack to continue games that are not compiled with the aac codec
|
||||||
|
response.num_channels = 2;
|
||||||
|
response.num_samples = 1024;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.src_addr < Memory::FCRAM_PADDR ||
|
||||||
|
request.src_addr + request.size > Memory::FCRAM_PADDR + Memory::FCRAM_SIZE) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Got out of bounds src_addr {:08x}", request.src_addr);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
u8* data = memory.GetFCRAMPointer(request.src_addr - Memory::FCRAM_PADDR);
|
||||||
|
|
||||||
|
std::array<std::vector<u8>, 2> out_streams;
|
||||||
|
|
||||||
|
std::size_t data_size = request.size;
|
||||||
|
while (data_size > 0) {
|
||||||
|
if (!decoded_frame) {
|
||||||
|
decoded_frame.reset(av_frame_alloc_dl());
|
||||||
|
if (!decoded_frame) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Could not allocate audio frame");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret =
|
||||||
|
av_parser_parse2_dl(parser.get(), av_context.get(), &av_packet->data, &av_packet->size,
|
||||||
|
data, data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
|
||||||
|
if (ret < 0) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Error while parsing");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
data += ret;
|
||||||
|
data_size -= ret;
|
||||||
|
|
||||||
|
ret = avcodec_send_packet_dl(av_context.get(), av_packet.get());
|
||||||
|
if (ret < 0) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Error submitting the packet to the decoder");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (av_packet->size) {
|
||||||
|
while (ret >= 0) {
|
||||||
|
ret = avcodec_receive_frame_dl(av_context.get(), decoded_frame.get());
|
||||||
|
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
|
||||||
|
break;
|
||||||
|
else if (ret < 0) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Error during decoding");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
int bytes_per_sample = av_get_bytes_per_sample_dl(av_context->sample_fmt);
|
||||||
|
if (bytes_per_sample < 0) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Failed to calculate data size");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT(decoded_frame->channels <= out_streams.size());
|
||||||
|
|
||||||
|
std::size_t size = bytes_per_sample * (decoded_frame->nb_samples);
|
||||||
|
|
||||||
|
response.num_channels = decoded_frame->channels;
|
||||||
|
response.num_samples += decoded_frame->nb_samples;
|
||||||
|
|
||||||
|
// FFmpeg converts to 32 signed floating point PCM, we need s16 PCM so we need to
|
||||||
|
// convert it
|
||||||
|
f32 val_float;
|
||||||
|
for (std::size_t current_pos(0); current_pos < size;) {
|
||||||
|
for (std::size_t channel(0); channel < decoded_frame->channels; channel++) {
|
||||||
|
std::memcpy(&val_float, decoded_frame->data[channel] + current_pos,
|
||||||
|
sizeof(val_float));
|
||||||
|
s16 val = static_cast<s16>(0x7FFF * val_float);
|
||||||
|
out_streams[channel].push_back(val & 0xFF);
|
||||||
|
out_streams[channel].push_back(val >> 8);
|
||||||
|
}
|
||||||
|
current_pos += sizeof(val_float);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out_streams[0].size() != 0) {
|
||||||
|
if (request.dst_addr_ch0 < Memory::FCRAM_PADDR ||
|
||||||
|
request.dst_addr_ch0 + out_streams[0].size() >
|
||||||
|
Memory::FCRAM_PADDR + Memory::FCRAM_SIZE) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Got out of bounds dst_addr_ch0 {:08x}", request.dst_addr_ch0);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
std::memcpy(memory.GetFCRAMPointer(request.dst_addr_ch0 - Memory::FCRAM_PADDR),
|
||||||
|
out_streams[0].data(), out_streams[0].size());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out_streams[1].size() != 0) {
|
||||||
|
if (request.dst_addr_ch1 < Memory::FCRAM_PADDR ||
|
||||||
|
request.dst_addr_ch1 + out_streams[1].size() >
|
||||||
|
Memory::FCRAM_PADDR + Memory::FCRAM_SIZE) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Got out of bounds dst_addr_ch1 {:08x}", request.dst_addr_ch1);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
std::memcpy(memory.GetFCRAMPointer(request.dst_addr_ch1 - Memory::FCRAM_PADDR),
|
||||||
|
out_streams[1].data(), out_streams[1].size());
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
FFMPEGDecoder::FFMPEGDecoder(Memory::MemorySystem& memory) : impl(std::make_unique<Impl>(memory)) {}
|
||||||
|
|
||||||
|
FFMPEGDecoder::~FFMPEGDecoder() = default;
|
||||||
|
|
||||||
|
std::optional<BinaryResponse> FFMPEGDecoder::ProcessRequest(const BinaryRequest& request) {
|
||||||
|
return impl->ProcessRequest(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace AudioCore::HLE
|
22
src/audio_core/hle/ffmpeg_decoder.h
Normal file
22
src/audio_core/hle/ffmpeg_decoder.h
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "audio_core/hle/decoder.h"
|
||||||
|
|
||||||
|
namespace AudioCore::HLE {
|
||||||
|
|
||||||
|
class FFMPEGDecoder final : public DecoderBase {
|
||||||
|
public:
|
||||||
|
explicit FFMPEGDecoder(Memory::MemorySystem& memory);
|
||||||
|
~FFMPEGDecoder() override;
|
||||||
|
std::optional<BinaryResponse> ProcessRequest(const BinaryRequest& request) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
class Impl;
|
||||||
|
std::unique_ptr<Impl> impl;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace AudioCore::HLE
|
178
src/audio_core/hle/ffmpeg_dl.cpp
Normal file
178
src/audio_core/hle/ffmpeg_dl.cpp
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include "audio_core/hle/ffmpeg_dl.h"
|
||||||
|
#include "common/file_util.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "common/string_util.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct LibraryDeleter {
|
||||||
|
using pointer = HMODULE;
|
||||||
|
void operator()(HMODULE h) const {
|
||||||
|
if (h != nullptr)
|
||||||
|
FreeLibrary(h);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unique_ptr<HMODULE, LibraryDeleter> dll_util{nullptr};
|
||||||
|
std::unique_ptr<HMODULE, LibraryDeleter> dll_codec{nullptr};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
FuncDL<int(AVSampleFormat)> av_get_bytes_per_sample_dl;
|
||||||
|
FuncDL<AVFrame*(void)> av_frame_alloc_dl;
|
||||||
|
FuncDL<void(AVFrame**)> av_frame_free_dl;
|
||||||
|
FuncDL<AVCodecContext*(const AVCodec*)> avcodec_alloc_context3_dl;
|
||||||
|
FuncDL<void(AVCodecContext**)> avcodec_free_context_dl;
|
||||||
|
FuncDL<int(AVCodecContext*, const AVCodec*, AVDictionary**)> avcodec_open2_dl;
|
||||||
|
FuncDL<AVPacket*(void)> av_packet_alloc_dl;
|
||||||
|
FuncDL<void(AVPacket**)> av_packet_free_dl;
|
||||||
|
FuncDL<AVCodec*(AVCodecID)> avcodec_find_decoder_dl;
|
||||||
|
FuncDL<int(AVCodecContext*, const AVPacket*)> avcodec_send_packet_dl;
|
||||||
|
FuncDL<int(AVCodecContext*, AVFrame*)> avcodec_receive_frame_dl;
|
||||||
|
FuncDL<AVCodecParserContext*(int)> av_parser_init_dl;
|
||||||
|
FuncDL<int(AVCodecParserContext*, AVCodecContext*, uint8_t**, int*, const uint8_t*, int, int64_t,
|
||||||
|
int64_t, int64_t)>
|
||||||
|
av_parser_parse2_dl;
|
||||||
|
FuncDL<void(AVCodecParserContext*)> av_parser_close_dl;
|
||||||
|
|
||||||
|
bool InitFFmpegDL() {
|
||||||
|
std::string dll_path = FileUtil::GetUserPath(FileUtil::UserPath::DLLDir);
|
||||||
|
FileUtil::CreateDir(dll_path);
|
||||||
|
std::wstring w_dll_path = Common::UTF8ToUTF16W(dll_path);
|
||||||
|
SetDllDirectoryW(w_dll_path.c_str());
|
||||||
|
|
||||||
|
dll_util.reset(LoadLibrary("avutil-56.dll"));
|
||||||
|
if (!dll_util) {
|
||||||
|
DWORD error_message_id = GetLastError();
|
||||||
|
LPSTR message_buffer = nullptr;
|
||||||
|
size_t size =
|
||||||
|
FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
|
||||||
|
FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||||
|
nullptr, error_message_id, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
||||||
|
reinterpret_cast<LPSTR>(&message_buffer), 0, nullptr);
|
||||||
|
|
||||||
|
std::string message(message_buffer, size);
|
||||||
|
|
||||||
|
LocalFree(message_buffer);
|
||||||
|
LOG_ERROR(Audio_DSP, "Could not load avutil-56.dll: {}", message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dll_codec.reset(LoadLibrary("avcodec-58.dll"));
|
||||||
|
if (!dll_codec) {
|
||||||
|
DWORD error_message_id = GetLastError();
|
||||||
|
LPSTR message_buffer = nullptr;
|
||||||
|
size_t size =
|
||||||
|
FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
|
||||||
|
FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||||
|
nullptr, error_message_id, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
||||||
|
reinterpret_cast<LPSTR>(&message_buffer), 0, nullptr);
|
||||||
|
|
||||||
|
std::string message(message_buffer, size);
|
||||||
|
|
||||||
|
LocalFree(message_buffer);
|
||||||
|
LOG_ERROR(Audio_DSP, "Could not load avcodec-58.dll: {}", message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
av_get_bytes_per_sample_dl =
|
||||||
|
FuncDL<int(AVSampleFormat)>(dll_util.get(), "av_get_bytes_per_sample");
|
||||||
|
if (!av_get_bytes_per_sample_dl) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Can not load function av_get_bytes_per_sample");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
av_frame_alloc_dl = FuncDL<AVFrame*()>(dll_util.get(), "av_frame_alloc");
|
||||||
|
if (!av_frame_alloc_dl) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Can not load function av_frame_alloc");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
av_frame_free_dl = FuncDL<void(AVFrame**)>(dll_util.get(), "av_frame_free");
|
||||||
|
if (!av_frame_free_dl) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Can not load function av_frame_free");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
avcodec_alloc_context3_dl =
|
||||||
|
FuncDL<AVCodecContext*(const AVCodec*)>(dll_codec.get(), "avcodec_alloc_context3");
|
||||||
|
if (!avcodec_alloc_context3_dl) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Can not load function avcodec_alloc_context3");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
avcodec_free_context_dl =
|
||||||
|
FuncDL<void(AVCodecContext**)>(dll_codec.get(), "avcodec_free_context");
|
||||||
|
if (!av_get_bytes_per_sample_dl) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Can not load function avcodec_free_context");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
avcodec_open2_dl = FuncDL<int(AVCodecContext*, const AVCodec*, AVDictionary**)>(
|
||||||
|
dll_codec.get(), "avcodec_open2");
|
||||||
|
if (!avcodec_open2_dl) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Can not load function avcodec_open2");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
av_packet_alloc_dl = FuncDL<AVPacket*(void)>(dll_codec.get(), "av_packet_alloc");
|
||||||
|
if (!av_packet_alloc_dl) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Can not load function av_packet_alloc");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
av_packet_free_dl = FuncDL<void(AVPacket**)>(dll_codec.get(), "av_packet_free");
|
||||||
|
if (!av_packet_free_dl) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Can not load function av_packet_free");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
avcodec_find_decoder_dl = FuncDL<AVCodec*(AVCodecID)>(dll_codec.get(), "avcodec_find_decoder");
|
||||||
|
if (!avcodec_find_decoder_dl) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Can not load function avcodec_find_decoder");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
avcodec_send_packet_dl =
|
||||||
|
FuncDL<int(AVCodecContext*, const AVPacket*)>(dll_codec.get(), "avcodec_send_packet");
|
||||||
|
if (!avcodec_send_packet_dl) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Can not load function avcodec_send_packet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
avcodec_receive_frame_dl =
|
||||||
|
FuncDL<int(AVCodecContext*, AVFrame*)>(dll_codec.get(), "avcodec_receive_frame");
|
||||||
|
if (!avcodec_receive_frame_dl) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Can not load function avcodec_receive_frame");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
av_parser_init_dl = FuncDL<AVCodecParserContext*(int)>(dll_codec.get(), "av_parser_init");
|
||||||
|
if (!av_parser_init_dl) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Can not load function av_parser_init");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
av_parser_parse2_dl =
|
||||||
|
FuncDL<int(AVCodecParserContext*, AVCodecContext*, uint8_t**, int*, const uint8_t*, int,
|
||||||
|
int64_t, int64_t, int64_t)>(dll_codec.get(), "av_parser_parse2");
|
||||||
|
if (!av_parser_parse2_dl) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Can not load function av_parser_parse2");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
av_parser_close_dl = FuncDL<void(AVCodecParserContext*)>(dll_codec.get(), "av_parser_close");
|
||||||
|
if (!av_parser_close_dl) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Can not load function av_parser_close");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // _Win32
|
79
src/audio_core/hle/ffmpeg_dl.h
Normal file
79
src/audio_core/hle/ffmpeg_dl.h
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#endif // _WIN32
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <libavcodec/avcodec.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct FuncDL {
|
||||||
|
FuncDL() = default;
|
||||||
|
FuncDL(HMODULE dll, const char* name) {
|
||||||
|
if (dll) {
|
||||||
|
ptr_function = reinterpret_cast<T*>(GetProcAddress(dll, name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operator T*() const {
|
||||||
|
return ptr_function;
|
||||||
|
}
|
||||||
|
|
||||||
|
explicit operator bool() const {
|
||||||
|
return ptr_function != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
T* ptr_function = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern FuncDL<int(AVSampleFormat)> av_get_bytes_per_sample_dl;
|
||||||
|
extern FuncDL<AVFrame*(void)> av_frame_alloc_dl;
|
||||||
|
extern FuncDL<void(AVFrame**)> av_frame_free_dl;
|
||||||
|
extern FuncDL<AVCodecContext*(const AVCodec*)> avcodec_alloc_context3_dl;
|
||||||
|
extern FuncDL<void(AVCodecContext**)> avcodec_free_context_dl;
|
||||||
|
extern FuncDL<int(AVCodecContext*, const AVCodec*, AVDictionary**)> avcodec_open2_dl;
|
||||||
|
extern FuncDL<AVPacket*(void)> av_packet_alloc_dl;
|
||||||
|
extern FuncDL<void(AVPacket**)> av_packet_free_dl;
|
||||||
|
extern FuncDL<AVCodec*(AVCodecID)> avcodec_find_decoder_dl;
|
||||||
|
extern FuncDL<int(AVCodecContext*, const AVPacket*)> avcodec_send_packet_dl;
|
||||||
|
extern FuncDL<int(AVCodecContext*, AVFrame*)> avcodec_receive_frame_dl;
|
||||||
|
extern FuncDL<AVCodecParserContext*(int)> av_parser_init_dl;
|
||||||
|
extern FuncDL<int(AVCodecParserContext*, AVCodecContext*, uint8_t**, int*, const uint8_t*, int,
|
||||||
|
int64_t, int64_t, int64_t)>
|
||||||
|
av_parser_parse2_dl;
|
||||||
|
extern FuncDL<void(AVCodecParserContext*)> av_parser_close_dl;
|
||||||
|
|
||||||
|
bool InitFFmpegDL();
|
||||||
|
|
||||||
|
#else // _Win32
|
||||||
|
|
||||||
|
// No dynamic loading for Unix and Apple
|
||||||
|
|
||||||
|
const auto av_get_bytes_per_sample_dl = &av_get_bytes_per_sample;
|
||||||
|
const auto av_frame_alloc_dl = &av_frame_alloc;
|
||||||
|
const auto av_frame_free_dl = &av_frame_free;
|
||||||
|
const auto avcodec_alloc_context3_dl = &avcodec_alloc_context3;
|
||||||
|
const auto avcodec_free_context_dl = &avcodec_free_context;
|
||||||
|
const auto avcodec_open2_dl = &avcodec_open2;
|
||||||
|
const auto av_packet_alloc_dl = &av_packet_alloc;
|
||||||
|
const auto av_packet_free_dl = &av_packet_free;
|
||||||
|
const auto avcodec_find_decoder_dl = &avcodec_find_decoder;
|
||||||
|
const auto avcodec_send_packet_dl = &avcodec_send_packet;
|
||||||
|
const auto avcodec_receive_frame_dl = &avcodec_receive_frame;
|
||||||
|
const auto av_parser_init_dl = &av_parser_init;
|
||||||
|
const auto av_parser_parse2_dl = &av_parser_parse2;
|
||||||
|
const auto av_parser_close_dl = &av_parser_close;
|
||||||
|
|
||||||
|
bool InitFFmpegDL() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // _Win32
|
@ -3,7 +3,13 @@
|
|||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
#include "audio_core/audio_types.h"
|
#include "audio_core/audio_types.h"
|
||||||
|
#ifdef HAVE_MF
|
||||||
|
#include "audio_core/hle/wmf_decoder.h"
|
||||||
|
#elif HAVE_FFMPEG
|
||||||
|
#include "audio_core/hle/ffmpeg_decoder.h"
|
||||||
|
#endif
|
||||||
#include "audio_core/hle/common.h"
|
#include "audio_core/hle/common.h"
|
||||||
|
#include "audio_core/hle/decoder.h"
|
||||||
#include "audio_core/hle/hle.h"
|
#include "audio_core/hle/hle.h"
|
||||||
#include "audio_core/hle/mixers.h"
|
#include "audio_core/hle/mixers.h"
|
||||||
#include "audio_core/hle/shared_memory.h"
|
#include "audio_core/hle/shared_memory.h"
|
||||||
@ -69,6 +75,8 @@ private:
|
|||||||
DspHle& parent;
|
DspHle& parent;
|
||||||
Core::TimingEventType* tick_event;
|
Core::TimingEventType* tick_event;
|
||||||
|
|
||||||
|
std::unique_ptr<HLE::DecoderBase> decoder;
|
||||||
|
|
||||||
std::weak_ptr<DSP_DSP> dsp_dsp;
|
std::weak_ptr<DSP_DSP> dsp_dsp;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -79,6 +87,15 @@ DspHle::Impl::Impl(DspHle& parent_, Memory::MemorySystem& memory) : parent(paren
|
|||||||
source.SetMemory(memory);
|
source.SetMemory(memory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_MF
|
||||||
|
decoder = std::make_unique<HLE::WMFDecoder>(memory);
|
||||||
|
#elif HAVE_FFMPEG
|
||||||
|
decoder = std::make_unique<HLE::FFMPEGDecoder>(memory);
|
||||||
|
#else
|
||||||
|
LOG_WARNING(Audio_DSP, "No decoder found, this could lead to missing audio");
|
||||||
|
decoder = std::make_unique<HLE::NullDecoder>();
|
||||||
|
#endif // HAVE_MF
|
||||||
|
|
||||||
Core::Timing& timing = Core::System::GetInstance().CoreTiming();
|
Core::Timing& timing = Core::System::GetInstance().CoreTiming();
|
||||||
tick_event =
|
tick_event =
|
||||||
timing.RegisterEvent("AudioCore::DspHle::tick_event", [this](u64, s64 cycles_late) {
|
timing.RegisterEvent("AudioCore::DspHle::tick_event", [this](u64, s64 cycles_late) {
|
||||||
@ -215,6 +232,28 @@ void DspHle::Impl::PipeWrite(DspPipe pipe_number, const std::vector<u8>& buffer)
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case DspPipe::Binary: {
|
||||||
|
// TODO(B3N30): Make this async, and signal the interrupt
|
||||||
|
HLE::BinaryRequest request;
|
||||||
|
if (sizeof(request) != buffer.size()) {
|
||||||
|
LOG_CRITICAL(Audio_DSP, "got binary pipe with wrong size {}", buffer.size());
|
||||||
|
UNIMPLEMENTED();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::memcpy(&request, buffer.data(), buffer.size());
|
||||||
|
if (request.codec != HLE::DecoderCodec::AAC) {
|
||||||
|
LOG_CRITICAL(Audio_DSP, "got unknown codec {}", static_cast<u16>(request.codec));
|
||||||
|
UNIMPLEMENTED();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::optional<HLE::BinaryResponse> response = decoder->ProcessRequest(request);
|
||||||
|
if (response) {
|
||||||
|
const HLE::BinaryResponse& value = *response;
|
||||||
|
pipe_data[static_cast<u32>(pipe_number)].resize(sizeof(value));
|
||||||
|
std::memcpy(pipe_data[static_cast<u32>(pipe_number)].data(), &value, sizeof(value));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
LOG_CRITICAL(Audio_DSP, "pipe_number = {} unimplemented",
|
LOG_CRITICAL(Audio_DSP, "pipe_number = {} unimplemented",
|
||||||
static_cast<std::size_t>(pipe_number));
|
static_cast<std::size_t>(pipe_number));
|
||||||
|
274
src/audio_core/hle/wmf_decoder.cpp
Normal file
274
src/audio_core/hle/wmf_decoder.cpp
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include "audio_core/hle/wmf_decoder.h"
|
||||||
|
#include "audio_core/hle/wmf_decoder_utils.h"
|
||||||
|
|
||||||
|
namespace AudioCore::HLE {
|
||||||
|
|
||||||
|
class WMFDecoder::Impl {
|
||||||
|
public:
|
||||||
|
explicit Impl(Memory::MemorySystem& memory);
|
||||||
|
~Impl();
|
||||||
|
std::optional<BinaryResponse> ProcessRequest(const BinaryRequest& request);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::optional<BinaryResponse> Initalize(const BinaryRequest& request);
|
||||||
|
|
||||||
|
std::optional<BinaryResponse> Decode(const BinaryRequest& request);
|
||||||
|
|
||||||
|
MFOutputState DecodingLoop(ADTSData adts_header, std::array<std::vector<u8>, 2>& out_streams);
|
||||||
|
|
||||||
|
bool transform_initialized = false;
|
||||||
|
bool format_selected = false;
|
||||||
|
|
||||||
|
Memory::MemorySystem& memory;
|
||||||
|
|
||||||
|
unique_mfptr<IMFTransform> transform;
|
||||||
|
DWORD in_stream_id = 0;
|
||||||
|
DWORD out_stream_id = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
WMFDecoder::Impl::Impl(Memory::MemorySystem& memory) : memory(memory) {
|
||||||
|
HRESULT hr = S_OK;
|
||||||
|
hr = CoInitialize(NULL);
|
||||||
|
// S_FALSE will be returned when COM has already been initialized
|
||||||
|
if (hr != S_OK && hr != S_FALSE) {
|
||||||
|
ReportError("Failed to start COM components", hr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// lite startup is faster and all what we need is included
|
||||||
|
hr = MFStartup(MF_VERSION, MFSTARTUP_LITE);
|
||||||
|
if (hr != S_OK) {
|
||||||
|
// Do you know you can't initialize MF in test mode or safe mode?
|
||||||
|
ReportError("Failed to initialize Media Foundation", hr);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO(Audio_DSP, "Media Foundation activated");
|
||||||
|
|
||||||
|
// initialize transform
|
||||||
|
transform = MFDecoderInit();
|
||||||
|
if (transform == nullptr) {
|
||||||
|
LOG_CRITICAL(Audio_DSP, "Can't initialize decoder");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = transform->GetStreamIDs(1, &in_stream_id, 1, &out_stream_id);
|
||||||
|
if (hr == E_NOTIMPL) {
|
||||||
|
// if not implemented, it means this MFT does not assign stream ID for you
|
||||||
|
in_stream_id = 0;
|
||||||
|
out_stream_id = 0;
|
||||||
|
} else if (FAILED(hr)) {
|
||||||
|
ReportError("Decoder failed to initialize the stream ID", hr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
transform_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
WMFDecoder::Impl::~Impl() {
|
||||||
|
if (transform_initialized) {
|
||||||
|
MFFlush(transform.get());
|
||||||
|
// delete the transform object before shutting down MF
|
||||||
|
// otherwise access violation will occur
|
||||||
|
transform.reset();
|
||||||
|
}
|
||||||
|
MFShutdown();
|
||||||
|
CoUninitialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<BinaryResponse> WMFDecoder::Impl::ProcessRequest(const BinaryRequest& request) {
|
||||||
|
if (request.codec != DecoderCodec::AAC) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Got unknown codec {}", static_cast<u16>(request.codec));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (request.cmd) {
|
||||||
|
case DecoderCommand::Init: {
|
||||||
|
LOG_INFO(Audio_DSP, "WMFDecoder initializing");
|
||||||
|
return Initalize(request);
|
||||||
|
}
|
||||||
|
case DecoderCommand::Decode: {
|
||||||
|
return Decode(request);
|
||||||
|
}
|
||||||
|
case DecoderCommand::Unknown: {
|
||||||
|
BinaryResponse response;
|
||||||
|
std::memcpy(&response, &request, sizeof(response));
|
||||||
|
response.unknown1 = 0x0;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
LOG_ERROR(Audio_DSP, "Got unknown binary request: {}", static_cast<u16>(request.cmd));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<BinaryResponse> WMFDecoder::Impl::Initalize(const BinaryRequest& request) {
|
||||||
|
BinaryResponse response;
|
||||||
|
std::memcpy(&response, &request, sizeof(response));
|
||||||
|
response.unknown1 = 0x0;
|
||||||
|
|
||||||
|
format_selected = false; // select format again if application request initialize the DSP
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
MFOutputState WMFDecoder::Impl::DecodingLoop(ADTSData adts_header,
|
||||||
|
std::array<std::vector<u8>, 2>& out_streams) {
|
||||||
|
MFOutputState output_status = MFOutputState::OK;
|
||||||
|
std::optional<std::vector<f32>> output_buffer;
|
||||||
|
unique_mfptr<IMFSample> output;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
auto [output_status, output] = ReceiveSample(transform.get(), out_stream_id);
|
||||||
|
|
||||||
|
// 0 -> okay; 3 -> okay but more data available (buffer too small)
|
||||||
|
if (output_status == MFOutputState::OK || output_status == MFOutputState::HaveMoreData) {
|
||||||
|
output_buffer = CopySampleToBuffer(output.get());
|
||||||
|
|
||||||
|
// the following was taken from ffmpeg version of the decoder
|
||||||
|
f32 val_f32;
|
||||||
|
for (std::size_t i = 0; i < output_buffer->size();) {
|
||||||
|
for (std::size_t channel = 0; channel < adts_header.channels; channel++) {
|
||||||
|
val_f32 = output_buffer->at(i);
|
||||||
|
s16 val = static_cast<s16>(0x7FFF * val_f32);
|
||||||
|
out_streams[channel].push_back(val & 0xFF);
|
||||||
|
out_streams[channel].push_back(val >> 8);
|
||||||
|
// i is incremented on per channel basis
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// in case of "ok" only, just return quickly
|
||||||
|
if (output_status == MFOutputState::OK)
|
||||||
|
return MFOutputState::OK;
|
||||||
|
|
||||||
|
// for status = 2, reset MF
|
||||||
|
if (output_status == MFOutputState::NeedReconfig) {
|
||||||
|
format_selected = false;
|
||||||
|
return MFOutputState::NeedReconfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// for status = 3, try again with new buffer
|
||||||
|
if (output_status == MFOutputState::HaveMoreData)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// according to MS document, this is not an error (?!)
|
||||||
|
if (output_status == MFOutputState::NeedMoreInput)
|
||||||
|
return MFOutputState::NeedMoreInput;
|
||||||
|
|
||||||
|
return MFOutputState::FatalError; // return on other status
|
||||||
|
}
|
||||||
|
|
||||||
|
return MFOutputState::FatalError;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<BinaryResponse> WMFDecoder::Impl::Decode(const BinaryRequest& request) {
|
||||||
|
BinaryResponse response;
|
||||||
|
response.codec = request.codec;
|
||||||
|
response.cmd = request.cmd;
|
||||||
|
response.size = request.size;
|
||||||
|
response.num_channels = 2;
|
||||||
|
response.num_samples = 1024;
|
||||||
|
|
||||||
|
if (!transform_initialized) {
|
||||||
|
LOG_DEBUG(Audio_DSP, "Decoder not initialized");
|
||||||
|
// This is a hack to continue games when decoder failed to initialize
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.src_addr < Memory::FCRAM_PADDR ||
|
||||||
|
request.src_addr + request.size > Memory::FCRAM_PADDR + Memory::FCRAM_SIZE) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Got out of bounds src_addr {:08x}", request.src_addr);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
u8* data = memory.GetFCRAMPointer(request.src_addr - Memory::FCRAM_PADDR);
|
||||||
|
|
||||||
|
std::array<std::vector<u8>, 2> out_streams;
|
||||||
|
unique_mfptr<IMFSample> sample;
|
||||||
|
MFInputState input_status = MFInputState::OK;
|
||||||
|
MFOutputState output_status = MFOutputState::OK;
|
||||||
|
std::optional<ADTSMeta> adts_meta = DetectMediaType((char*)data, request.size);
|
||||||
|
|
||||||
|
if (!adts_meta) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Unable to deduce decoding parameters from ADTS stream");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.num_channels = adts_meta->ADTSHeader.channels;
|
||||||
|
|
||||||
|
if (!format_selected) {
|
||||||
|
LOG_DEBUG(Audio_DSP, "New ADTS stream: channels = {}, sample rate = {}",
|
||||||
|
adts_meta->ADTSHeader.channels, adts_meta->ADTSHeader.samplerate);
|
||||||
|
SelectInputMediaType(transform.get(), in_stream_id, adts_meta->ADTSHeader,
|
||||||
|
adts_meta->AACTag, 14);
|
||||||
|
SelectOutputMediaType(transform.get(), out_stream_id);
|
||||||
|
SendSample(transform.get(), in_stream_id, nullptr);
|
||||||
|
// cache the result from detect_mediatype and call select_*_mediatype only once
|
||||||
|
// This could increase performance very slightly
|
||||||
|
transform->ProcessMessage(MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, 0);
|
||||||
|
format_selected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sample = CreateSample((void*)data, request.size, 1, 0);
|
||||||
|
sample->SetUINT32(MFSampleExtension_CleanPoint, 1);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
input_status = SendSample(transform.get(), in_stream_id, sample.get());
|
||||||
|
output_status = DecodingLoop(adts_meta->ADTSHeader, out_streams);
|
||||||
|
|
||||||
|
if (output_status == MFOutputState::FatalError) {
|
||||||
|
// if the decode issues are caused by MFT not accepting new samples, try again
|
||||||
|
// NOTICE: you are required to check the output even if you already knew/guessed
|
||||||
|
// MFT didn't accept the input sample
|
||||||
|
if (input_status == MFInputState::NotAccepted) {
|
||||||
|
// try again
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_ERROR(Audio_DSP, "Errors occurred when receiving output");
|
||||||
|
return response;
|
||||||
|
} else if (output_status == MFOutputState::NeedReconfig) {
|
||||||
|
// flush the transform
|
||||||
|
MFFlush(transform.get());
|
||||||
|
// decode again
|
||||||
|
return this->Decode(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
break; // jump out of the loop if at least we don't have obvious issues
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out_streams[0].size() != 0) {
|
||||||
|
if (request.dst_addr_ch0 < Memory::FCRAM_PADDR ||
|
||||||
|
request.dst_addr_ch0 + out_streams[0].size() >
|
||||||
|
Memory::FCRAM_PADDR + Memory::FCRAM_SIZE) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Got out of bounds dst_addr_ch0 {:08x}", request.dst_addr_ch0);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
std::memcpy(memory.GetFCRAMPointer(request.dst_addr_ch0 - Memory::FCRAM_PADDR),
|
||||||
|
out_streams[0].data(), out_streams[0].size());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out_streams[1].size() != 0) {
|
||||||
|
if (request.dst_addr_ch1 < Memory::FCRAM_PADDR ||
|
||||||
|
request.dst_addr_ch1 + out_streams[1].size() >
|
||||||
|
Memory::FCRAM_PADDR + Memory::FCRAM_SIZE) {
|
||||||
|
LOG_ERROR(Audio_DSP, "Got out of bounds dst_addr_ch1 {:08x}", request.dst_addr_ch1);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
std::memcpy(memory.GetFCRAMPointer(request.dst_addr_ch1 - Memory::FCRAM_PADDR),
|
||||||
|
out_streams[1].data(), out_streams[1].size());
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
WMFDecoder::WMFDecoder(Memory::MemorySystem& memory) : impl(std::make_unique<Impl>(memory)) {}
|
||||||
|
|
||||||
|
WMFDecoder::~WMFDecoder() = default;
|
||||||
|
|
||||||
|
std::optional<BinaryResponse> WMFDecoder::ProcessRequest(const BinaryRequest& request) {
|
||||||
|
return impl->ProcessRequest(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace AudioCore::HLE
|
22
src/audio_core/hle/wmf_decoder.h
Normal file
22
src/audio_core/hle/wmf_decoder.h
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "audio_core/hle/decoder.h"
|
||||||
|
|
||||||
|
namespace AudioCore::HLE {
|
||||||
|
|
||||||
|
class WMFDecoder final : public DecoderBase {
|
||||||
|
public:
|
||||||
|
explicit WMFDecoder(Memory::MemorySystem& memory);
|
||||||
|
~WMFDecoder() override;
|
||||||
|
std::optional<BinaryResponse> ProcessRequest(const BinaryRequest& request) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
class Impl;
|
||||||
|
std::unique_ptr<Impl> impl;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace AudioCore::HLE
|
349
src/audio_core/hle/wmf_decoder_utils.cpp
Normal file
349
src/audio_core/hle/wmf_decoder_utils.cpp
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
// Copyright 2019 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "common/string_util.h"
|
||||||
|
#include "wmf_decoder_utils.h"
|
||||||
|
|
||||||
|
// utility functions
|
||||||
|
void ReportError(std::string msg, HRESULT hr) {
|
||||||
|
if (SUCCEEDED(hr)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LPWSTR err;
|
||||||
|
FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER |
|
||||||
|
FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||||
|
nullptr, hr,
|
||||||
|
// hardcode to use en_US because if any user had problems with this
|
||||||
|
// we can help them w/o translating anything
|
||||||
|
// default is to use the language currently active on the operating system
|
||||||
|
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), (LPWSTR)&err, 0, nullptr);
|
||||||
|
if (err != nullptr) {
|
||||||
|
LOG_CRITICAL(Audio_DSP, "{}: {}", msg, Common::UTF16ToUTF8(err));
|
||||||
|
LocalFree(err);
|
||||||
|
}
|
||||||
|
LOG_CRITICAL(Audio_DSP, "{}: {:08x}", msg, hr);
|
||||||
|
}
|
||||||
|
|
||||||
|
unique_mfptr<IMFTransform> MFDecoderInit(GUID audio_format) {
|
||||||
|
HRESULT hr = S_OK;
|
||||||
|
MFT_REGISTER_TYPE_INFO reg = {0};
|
||||||
|
GUID category = MFT_CATEGORY_AUDIO_DECODER;
|
||||||
|
IMFActivate** activate;
|
||||||
|
unique_mfptr<IMFTransform> transform;
|
||||||
|
UINT32 num_activate;
|
||||||
|
|
||||||
|
reg.guidMajorType = MFMediaType_Audio;
|
||||||
|
reg.guidSubtype = audio_format;
|
||||||
|
|
||||||
|
hr = MFTEnumEx(category,
|
||||||
|
MFT_ENUM_FLAG_SYNCMFT | MFT_ENUM_FLAG_LOCALMFT | MFT_ENUM_FLAG_SORTANDFILTER,
|
||||||
|
®, nullptr, &activate, &num_activate);
|
||||||
|
if (FAILED(hr) || num_activate < 1) {
|
||||||
|
ReportError("Failed to enumerate decoders", hr);
|
||||||
|
CoTaskMemFree(activate);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
LOG_INFO(Audio_DSP, "Windows(R) Media Foundation found {} suitable decoder(s)", num_activate);
|
||||||
|
for (unsigned int n = 0; n < num_activate; n++) {
|
||||||
|
hr = activate[n]->ActivateObject(
|
||||||
|
IID_IMFTransform,
|
||||||
|
reinterpret_cast<void**>(static_cast<IMFTransform**>(Amp(transform))));
|
||||||
|
if (FAILED(hr))
|
||||||
|
transform = nullptr;
|
||||||
|
activate[n]->Release();
|
||||||
|
if (SUCCEEDED(hr))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (transform == nullptr) {
|
||||||
|
ReportError("Failed to initialize MFT", hr);
|
||||||
|
CoTaskMemFree(activate);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
CoTaskMemFree(activate);
|
||||||
|
return transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
unique_mfptr<IMFSample> CreateSample(const void* data, DWORD len, DWORD alignment,
|
||||||
|
LONGLONG duration) {
|
||||||
|
HRESULT hr = S_OK;
|
||||||
|
unique_mfptr<IMFMediaBuffer> buf;
|
||||||
|
unique_mfptr<IMFSample> sample;
|
||||||
|
|
||||||
|
hr = MFCreateSample(Amp(sample));
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
ReportError("Unable to allocate a sample", hr);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
// Yes, the argument for alignment is the actual alignment - 1
|
||||||
|
hr = MFCreateAlignedMemoryBuffer(len, alignment - 1, Amp(buf));
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
ReportError("Unable to allocate a memory buffer for sample", hr);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (data) {
|
||||||
|
BYTE* buffer;
|
||||||
|
// lock the MediaBuffer
|
||||||
|
// this is actually not a thread-safe lock
|
||||||
|
hr = buf->Lock(&buffer, nullptr, nullptr);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
ReportError("Unable to lock down MediaBuffer", hr);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::memcpy(buffer, data, len);
|
||||||
|
|
||||||
|
buf->SetCurrentLength(len);
|
||||||
|
buf->Unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
sample->AddBuffer(buf.get());
|
||||||
|
hr = sample->SetSampleDuration(duration);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
// MFT will take a guess for you in this case
|
||||||
|
ReportError("Unable to set sample duration, but continuing anyway", hr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sample;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SelectInputMediaType(IMFTransform* transform, int in_stream_id, const ADTSData& adts,
|
||||||
|
const UINT8* user_data, UINT32 user_data_len, GUID audio_format) {
|
||||||
|
HRESULT hr = S_OK;
|
||||||
|
unique_mfptr<IMFMediaType> t;
|
||||||
|
|
||||||
|
// actually you can get rid of the whole block of searching and filtering mess
|
||||||
|
// if you know the exact parameters of your media stream
|
||||||
|
hr = MFCreateMediaType(Amp(t));
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
ReportError("Unable to create an empty MediaType", hr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// basic definition
|
||||||
|
t->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
|
||||||
|
t->SetGUID(MF_MT_SUBTYPE, audio_format);
|
||||||
|
|
||||||
|
t->SetUINT32(MF_MT_AAC_PAYLOAD_TYPE, 1);
|
||||||
|
t->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, adts.channels);
|
||||||
|
t->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, adts.samplerate);
|
||||||
|
// 0xfe = 254 = "unspecified"
|
||||||
|
t->SetUINT32(MF_MT_AAC_AUDIO_PROFILE_LEVEL_INDICATION, 254);
|
||||||
|
t->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, 1);
|
||||||
|
t->SetBlob(MF_MT_USER_DATA, user_data, user_data_len);
|
||||||
|
hr = transform->SetInputType(in_stream_id, t.get(), 0);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
ReportError("failed to select input types for MFT", hr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SelectOutputMediaType(IMFTransform* transform, int out_stream_id, GUID audio_format) {
|
||||||
|
HRESULT hr = S_OK;
|
||||||
|
UINT32 tmp;
|
||||||
|
unique_mfptr<IMFMediaType> type;
|
||||||
|
|
||||||
|
// If you know what you need and what you are doing, you can specify the conditions instead of
|
||||||
|
// searching but it's better to use search since MFT may or may not support your output
|
||||||
|
// parameters
|
||||||
|
for (DWORD i = 0;; i++) {
|
||||||
|
hr = transform->GetOutputAvailableType(out_stream_id, i, Amp(type));
|
||||||
|
if (hr == MF_E_NO_MORE_TYPES || hr == E_NOTIMPL) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
ReportError("failed to get output types for MFT", hr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = type->GetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, &tmp);
|
||||||
|
|
||||||
|
if (FAILED(hr))
|
||||||
|
continue;
|
||||||
|
// select PCM-16 format
|
||||||
|
if (tmp == 32) {
|
||||||
|
hr = type->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, 1);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
ReportError("failed to set MF_MT_AUDIO_BLOCK_ALIGNMENT for MFT on output stream",
|
||||||
|
hr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
hr = transform->SetOutputType(out_stream_id, type.get(), 0);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
ReportError("failed to select output types for MFT", hr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReportError("MFT: Unable to find preferred output format", E_NOTIMPL);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<ADTSMeta> DetectMediaType(char* buffer, std::size_t len) {
|
||||||
|
if (len < 7) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
ADTSData tmp;
|
||||||
|
ADTSMeta result;
|
||||||
|
// see https://docs.microsoft.com/en-us/windows/desktop/api/mmreg/ns-mmreg-heaacwaveinfo_tag
|
||||||
|
// for the meaning of the byte array below
|
||||||
|
|
||||||
|
// it might be a good idea to wrap the parameters into a struct
|
||||||
|
// and pass that struct into the function but doing that will lead to messier code
|
||||||
|
// const UINT8 aac_data[] = { 0x01, 0x00, 0xfe, 00, 00, 00, 00, 00, 00, 00, 00, 00, 0x11, 0x90
|
||||||
|
// }; first byte: 0: raw aac 1: adts 2: adif 3: latm/laos
|
||||||
|
UINT8 aac_tmp[] = {0x01, 0x00, 0xfe, 00, 00, 00, 00, 00, 00, 00, 00, 00, 0x00, 0x00};
|
||||||
|
uint16_t tag = 0;
|
||||||
|
|
||||||
|
tmp = ParseADTS(buffer);
|
||||||
|
if (tmp.length == 0) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
tag = MFGetAACTag(tmp);
|
||||||
|
aac_tmp[12] |= (tag & 0xff00) >> 8;
|
||||||
|
aac_tmp[13] |= (tag & 0x00ff);
|
||||||
|
std::memcpy(&(result.ADTSHeader), &tmp, sizeof(ADTSData));
|
||||||
|
std::memcpy(&(result.AACTag), aac_tmp, 14);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MFFlush(IMFTransform* transform) {
|
||||||
|
HRESULT hr = transform->ProcessMessage(MFT_MESSAGE_COMMAND_FLUSH, 0);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
ReportError("MFT: Flush command failed", hr);
|
||||||
|
}
|
||||||
|
hr = transform->ProcessMessage(MFT_MESSAGE_NOTIFY_END_OF_STREAM, 0);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
ReportError("Failed to end streaming for MFT", hr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MFInputState SendSample(IMFTransform* transform, DWORD in_stream_id, IMFSample* in_sample) {
|
||||||
|
HRESULT hr = S_OK;
|
||||||
|
|
||||||
|
if (in_sample) {
|
||||||
|
hr = transform->ProcessInput(in_stream_id, in_sample, 0);
|
||||||
|
if (hr == MF_E_NOTACCEPTING) {
|
||||||
|
return MFInputState::NotAccepted; // try again
|
||||||
|
} else if (FAILED(hr)) {
|
||||||
|
ReportError("MFT: Failed to process input", hr);
|
||||||
|
return MFInputState::FatalError;
|
||||||
|
} // FAILED(hr)
|
||||||
|
} else {
|
||||||
|
hr = transform->ProcessMessage(MFT_MESSAGE_COMMAND_DRAIN, 0);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
ReportError("MFT: Failed to drain when processing input", hr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MFInputState::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::tuple<MFOutputState, unique_mfptr<IMFSample>> ReceiveSample(IMFTransform* transform,
|
||||||
|
DWORD out_stream_id) {
|
||||||
|
HRESULT hr;
|
||||||
|
MFT_OUTPUT_DATA_BUFFER out_buffers;
|
||||||
|
MFT_OUTPUT_STREAM_INFO out_info;
|
||||||
|
DWORD status = 0;
|
||||||
|
unique_mfptr<IMFSample> sample;
|
||||||
|
bool mft_create_sample = false;
|
||||||
|
|
||||||
|
hr = transform->GetOutputStreamInfo(out_stream_id, &out_info);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
ReportError("MFT: Failed to get stream info", hr);
|
||||||
|
return std::make_tuple(MFOutputState::FatalError, std::move(sample));
|
||||||
|
}
|
||||||
|
mft_create_sample = (out_info.dwFlags & MFT_OUTPUT_STREAM_PROVIDES_SAMPLES) ||
|
||||||
|
(out_info.dwFlags & MFT_OUTPUT_STREAM_CAN_PROVIDE_SAMPLES);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
status = 0;
|
||||||
|
|
||||||
|
if (!mft_create_sample) {
|
||||||
|
sample = CreateSample(nullptr, out_info.cbSize, out_info.cbAlignment);
|
||||||
|
if (!sample.get()) {
|
||||||
|
ReportError("MFT: Unable to allocate memory for samples", hr);
|
||||||
|
return std::make_tuple(MFOutputState::FatalError, std::move(sample));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out_buffers.dwStreamID = out_stream_id;
|
||||||
|
out_buffers.pSample = sample.get();
|
||||||
|
|
||||||
|
hr = transform->ProcessOutput(0, 1, &out_buffers, &status);
|
||||||
|
|
||||||
|
if (!FAILED(hr)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) {
|
||||||
|
// Most likely reasons: data corrupted; your actions not expected by MFT
|
||||||
|
return std::make_tuple(MFOutputState::NeedMoreInput, std::move(sample));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hr == MF_E_TRANSFORM_STREAM_CHANGE) {
|
||||||
|
ReportError("MFT: stream format changed, re-configuration required", hr);
|
||||||
|
return std::make_tuple(MFOutputState::NeedReconfig, std::move(sample));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out_buffers.dwStatus & MFT_OUTPUT_DATA_BUFFER_INCOMPLETE) {
|
||||||
|
// this status is also unreliable but whatever
|
||||||
|
return std::make_tuple(MFOutputState::HaveMoreData, std::move(sample));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out_buffers.pSample == nullptr) {
|
||||||
|
ReportError("MFT: decoding failure", hr);
|
||||||
|
return std::make_tuple(MFOutputState::FatalError, std::move(sample));
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_tuple(MFOutputState::OK, std::move(sample));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::vector<f32>> CopySampleToBuffer(IMFSample* sample) {
|
||||||
|
unique_mfptr<IMFMediaBuffer> buffer;
|
||||||
|
HRESULT hr = S_OK;
|
||||||
|
std::optional<std::vector<f32>> output;
|
||||||
|
std::vector<f32> output_buffer;
|
||||||
|
BYTE* data;
|
||||||
|
DWORD len = 0;
|
||||||
|
|
||||||
|
hr = sample->GetTotalLength(&len);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
ReportError("Failed to get the length of sample buffer", hr);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = sample->ConvertToContiguousBuffer(Amp(buffer));
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
ReportError("Failed to get sample buffer", hr);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = buffer->Lock(&data, nullptr, nullptr);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
ReportError("Failed to lock the buffer", hr);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
output_buffer.resize(len / sizeof(f32));
|
||||||
|
std::memcpy(output_buffer.data(), data, len);
|
||||||
|
output = output_buffer;
|
||||||
|
|
||||||
|
// if buffer unlock fails, then... whatever, we have already got data
|
||||||
|
buffer->Unlock();
|
||||||
|
return output;
|
||||||
|
}
|
89
src/audio_core/hle/wmf_decoder_utils.h
Normal file
89
src/audio_core/hle/wmf_decoder_utils.h
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// Copyright 2019 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// AAC decoder related APIs are only available with WIN7+
|
||||||
|
#define WINVER _WIN32_WINNT_WIN7
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <tuple>
|
||||||
|
#include <vector>
|
||||||
|
#include <comdef.h>
|
||||||
|
#include <mfapi.h>
|
||||||
|
#include <mferror.h>
|
||||||
|
#include <mfidl.h>
|
||||||
|
#include <mftransform.h>
|
||||||
|
|
||||||
|
#include "adts.h"
|
||||||
|
|
||||||
|
enum class MFOutputState { FatalError, OK, NeedMoreInput, NeedReconfig, HaveMoreData };
|
||||||
|
enum class MFInputState { FatalError, OK, NotAccepted };
|
||||||
|
|
||||||
|
// utility functions / templates
|
||||||
|
template <class T>
|
||||||
|
struct MFRelease {
|
||||||
|
void operator()(T* pointer) const {
|
||||||
|
pointer->Release();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct MFRelease<IMFTransform> {
|
||||||
|
void operator()(IMFTransform* pointer) const {
|
||||||
|
MFShutdownObject(pointer);
|
||||||
|
pointer->Release();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// wrapper facilities for dealing with pointers
|
||||||
|
template <typename T>
|
||||||
|
using unique_mfptr = std::unique_ptr<T, MFRelease<T>>;
|
||||||
|
|
||||||
|
template <typename SmartPtr, typename RawPtr>
|
||||||
|
class AmpImpl {
|
||||||
|
public:
|
||||||
|
AmpImpl(SmartPtr& smart_ptr) : smart_ptr(smart_ptr) {}
|
||||||
|
~AmpImpl() {
|
||||||
|
smart_ptr.reset(raw_ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
operator RawPtr*() {
|
||||||
|
return &raw_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
SmartPtr& smart_ptr;
|
||||||
|
RawPtr raw_ptr = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename SmartPtr>
|
||||||
|
auto Amp(SmartPtr& smart_ptr) {
|
||||||
|
return AmpImpl<SmartPtr, decltype(smart_ptr.get())>(smart_ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// convient function for formatting error messages
|
||||||
|
void ReportError(std::string msg, HRESULT hr);
|
||||||
|
|
||||||
|
// data type for transferring ADTS metadata between functions
|
||||||
|
struct ADTSMeta {
|
||||||
|
ADTSData ADTSHeader;
|
||||||
|
u8 AACTag[14];
|
||||||
|
};
|
||||||
|
|
||||||
|
// exported functions
|
||||||
|
unique_mfptr<IMFTransform> MFDecoderInit(GUID audio_format = MFAudioFormat_AAC);
|
||||||
|
unique_mfptr<IMFSample> CreateSample(const void* data, DWORD len, DWORD alignment = 1,
|
||||||
|
LONGLONG duration = 0);
|
||||||
|
bool SelectInputMediaType(IMFTransform* transform, int in_stream_id, const ADTSData& adts,
|
||||||
|
const UINT8* user_data, UINT32 user_data_len,
|
||||||
|
GUID audio_format = MFAudioFormat_AAC);
|
||||||
|
std::optional<ADTSMeta> DetectMediaType(char* buffer, std::size_t len);
|
||||||
|
bool SelectOutputMediaType(IMFTransform* transform, int out_stream_id,
|
||||||
|
GUID audio_format = MFAudioFormat_PCM);
|
||||||
|
void MFFlush(IMFTransform* transform);
|
||||||
|
MFInputState SendSample(IMFTransform* transform, DWORD in_stream_id, IMFSample* in_sample);
|
||||||
|
std::tuple<MFOutputState, unique_mfptr<IMFSample>> ReceiveSample(IMFTransform* transform,
|
||||||
|
DWORD out_stream_id);
|
||||||
|
std::optional<std::vector<f32>> CopySampleToBuffer(IMFSample* sample);
|
@ -38,6 +38,7 @@
|
|||||||
#define SYSDATA_DIR "sysdata"
|
#define SYSDATA_DIR "sysdata"
|
||||||
#define LOG_DIR "log"
|
#define LOG_DIR "log"
|
||||||
#define CHEATS_DIR "cheats"
|
#define CHEATS_DIR "cheats"
|
||||||
|
#define DLL_DIR "external_dlls"
|
||||||
|
|
||||||
// Filenames
|
// Filenames
|
||||||
// Files in the directory returned by GetUserPath(UserPath::LogDir)
|
// Files in the directory returned by GetUserPath(UserPath::LogDir)
|
||||||
|
@ -711,6 +711,7 @@ const std::string& GetUserPath(UserPath path, const std::string& new_path) {
|
|||||||
// TODO: Put the logs in a better location for each OS
|
// TODO: Put the logs in a better location for each OS
|
||||||
paths.emplace(UserPath::LogDir, user_path + LOG_DIR DIR_SEP);
|
paths.emplace(UserPath::LogDir, user_path + LOG_DIR DIR_SEP);
|
||||||
paths.emplace(UserPath::CheatsDir, user_path + CHEATS_DIR DIR_SEP);
|
paths.emplace(UserPath::CheatsDir, user_path + CHEATS_DIR DIR_SEP);
|
||||||
|
paths.emplace(UserPath::DLLDir, user_path + DLL_DIR DIR_SEP);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!new_path.empty()) {
|
if (!new_path.empty()) {
|
||||||
|
@ -24,6 +24,7 @@ enum class UserPath {
|
|||||||
CacheDir,
|
CacheDir,
|
||||||
CheatsDir,
|
CheatsDir,
|
||||||
ConfigDir,
|
ConfigDir,
|
||||||
|
DLLDir,
|
||||||
LogDir,
|
LogDir,
|
||||||
NANDDir,
|
NANDDir,
|
||||||
RootDir,
|
RootDir,
|
||||||
|
@ -9,6 +9,8 @@ add_executable(tests
|
|||||||
core/hle/kernel/hle_ipc.cpp
|
core/hle/kernel/hle_ipc.cpp
|
||||||
core/memory/memory.cpp
|
core/memory/memory.cpp
|
||||||
core/memory/vm_manager.cpp
|
core/memory/vm_manager.cpp
|
||||||
|
audio_core/audio_fixures.h
|
||||||
|
audio_core/decoder_tests.cpp
|
||||||
tests.cpp
|
tests.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,7 +23,7 @@ endif()
|
|||||||
|
|
||||||
create_target_directory_groups(tests)
|
create_target_directory_groups(tests)
|
||||||
|
|
||||||
target_link_libraries(tests PRIVATE common core video_core)
|
target_link_libraries(tests PRIVATE common core video_core audio_core)
|
||||||
target_link_libraries(tests PRIVATE ${PLATFORM_LIBRARIES} catch-single-include nihstro-headers Threads::Threads)
|
target_link_libraries(tests PRIVATE ${PLATFORM_LIBRARIES} catch-single-include nihstro-headers Threads::Threads)
|
||||||
|
|
||||||
add_test(NAME tests COMMAND tests)
|
add_test(NAME tests COMMAND tests)
|
||||||
|
12
src/tests/audio_core/audio_fixures.h
Normal file
12
src/tests/audio_core/audio_fixures.h
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// Copyright 2019 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
constexpr int fixure_buffer_size = 41;
|
||||||
|
constexpr std::array<u8, 41> fixure_buffer[41] = {
|
||||||
|
0xff, 0xf1, 0x4c, 0x80, 0x05, 0x3f, 0xfc, 0x21, 0x1a, 0x4e, 0xb0, 0x00, 0x00, 0x00,
|
||||||
|
0x05, 0xfc, 0x4e, 0x1f, 0x08, 0x88, 0x00, 0x00, 0x00, 0xc4, 0x1a, 0x03, 0xfc, 0x9c,
|
||||||
|
0x3e, 0x1d, 0x08, 0x84, 0x03, 0xd8, 0x3f, 0xe4, 0xe1, 0x20, 0x00, 0x0b, 0x38};
|
58
src/tests/audio_core/decoder_tests.cpp
Normal file
58
src/tests/audio_core/decoder_tests.cpp
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// Copyright 2019 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
#if defined(HAVE_MF) || defined(HAVE_FFMPEG)
|
||||||
|
|
||||||
|
#include <catch2/catch.hpp>
|
||||||
|
#include "core/core.h"
|
||||||
|
#include "core/core_timing.h"
|
||||||
|
#include "core/hle/kernel/memory.h"
|
||||||
|
#include "core/hle/kernel/process.h"
|
||||||
|
#include "core/hle/kernel/shared_page.h"
|
||||||
|
#include "core/memory.h"
|
||||||
|
|
||||||
|
#include "audio_core/hle/decoder.h"
|
||||||
|
#ifdef HAVE_MF
|
||||||
|
#include "audio_core/hle/wmf_decoder.h"
|
||||||
|
#elif HAVE_FFMPEG
|
||||||
|
#include "audio_core/hle/ffmpeg_decoder.h"
|
||||||
|
#endif
|
||||||
|
#include "audio_fixures.h"
|
||||||
|
|
||||||
|
TEST_CASE("DSP HLE Audio Decoder", "[audio_core]") {
|
||||||
|
// HACK: see comments of member timing
|
||||||
|
Core::System::GetInstance().timing = std::make_unique<Core::Timing>();
|
||||||
|
Core::System::GetInstance().memory = std::make_unique<Memory::MemorySystem>();
|
||||||
|
Kernel::KernelSystem kernel(*Core::System::GetInstance().memory, 0);
|
||||||
|
SECTION("decoder should produce correct samples") {
|
||||||
|
auto process = kernel.CreateProcess(kernel.CreateCodeSet("", 0));
|
||||||
|
auto decoder =
|
||||||
|
#ifdef HAVE_MF
|
||||||
|
std::make_unique<AudioCore::HLE::WMFDecoder>(*Core::System::GetInstance().memory);
|
||||||
|
#elif HAVE_FFMPEG
|
||||||
|
std::make_unique<AudioCore::HLE::FFMPEGDecoder>(*Core::System::GetInstance().memory);
|
||||||
|
#endif
|
||||||
|
AudioCore::HLE::BinaryRequest request;
|
||||||
|
|
||||||
|
request.codec = AudioCore::HLE::DecoderCodec::AAC;
|
||||||
|
request.cmd = AudioCore::HLE::DecoderCommand::Init;
|
||||||
|
// initialize decoder
|
||||||
|
std::optional<AudioCore::HLE::BinaryResponse> response = decoder->ProcessRequest(request);
|
||||||
|
|
||||||
|
request.cmd = AudioCore::HLE::DecoderCommand::Decode;
|
||||||
|
u8* fcram = Core::System::GetInstance().memory->GetFCRAMPointer(0);
|
||||||
|
|
||||||
|
memcpy(fcram, fixure_buffer, fixure_buffer_size);
|
||||||
|
request.src_addr = Memory::FCRAM_PADDR;
|
||||||
|
request.dst_addr_ch0 = Memory::FCRAM_PADDR + 1024;
|
||||||
|
request.dst_addr_ch1 = Memory::FCRAM_PADDR + 1048576; // 1 MB
|
||||||
|
request.size = fixure_buffer_size;
|
||||||
|
|
||||||
|
response = decoder->ProcessRequest(request);
|
||||||
|
response = decoder->ProcessRequest(request);
|
||||||
|
// remove this line
|
||||||
|
request.src_addr = Memory::FCRAM_PADDR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
Loading…
Reference in New Issue
Block a user