avioflow 0.7.3

High-performance audio decoding, encoding and resampling powered by FFmpeg
cmake_minimum_required(VERSION 3.20)
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/version.txt" AVIOFLOW_VERSION_RAW)
string(STRIP "${AVIOFLOW_VERSION_RAW}" AVIOFLOW_VERSION)
project(avioflow VERSION ${AVIOFLOW_VERSION} LANGUAGES CXX)
include(CMakePackageConfigHelpers)
include(CTest)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

option(BUILD_SHARED_LIBS "Whether to build shared libraries" OFF)
option(BUILD_TESTS "Build tests and examples" ON)
option(ENABLE_WASAPI "Enable WASAPI loopback capture support" OFF)
option(ENABLE_PYTHON "Enable Python bindings" OFF)
option(ENABLE_NODE_JS "Enable Node.js bindings" OFF)
option(ENABLE_JAVA "Enable Java JNI bindings" OFF)
option(ENABLE_BINARY "Whether to build binaries" OFF)
set(AVIOFLOW_GLIBCXX_USE_CXX11_ABI "1" CACHE STRING "Set _GLIBCXX_USE_CXX11_ABI for Linux libstdc++ builds")
if(UNIX AND NOT APPLE)
    if(NOT AVIOFLOW_GLIBCXX_USE_CXX11_ABI MATCHES "^[01]$")
        message(FATAL_ERROR "AVIOFLOW_GLIBCXX_USE_CXX11_ABI must be 0 or 1")
    endif()
    add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=${AVIOFLOW_GLIBCXX_USE_CXX11_ABI})
endif()

# Logic to force static build if Node.js is enabled
if(CMAKE_JS_VERSION)
    set(ENABLE_NODE_JS ON CACHE BOOL "Auto-enable Node.js for cmake-js" FORCE)
endif()

if(ENABLE_NODE_JS)
    message(STATUS "Node.js bindings enabled, forcing static build for core.")
    set(BUILD_SHARED_LIBS "OFF" CACHE BOOL "Force static build for Node.js" FORCE)
endif()


# Reduce verbose output - disable detailed makefile output
set(CMAKE_VERBOSE_MAKEFILE OFF)

# Add cmake directory to module path for easy includes
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)

# Configure MSVC
if(MSVC)
    add_compile_options(
        /W4 /utf-8 
        /GL     # Whole Program Optimization
        /Gw     # Optimize Global Data
        /Gy     # Function-Level Linking
        /experimental:deterministic      # Enable determinism for /pathmap
        "/pathmap:${CMAKE_SOURCE_DIR}=." # Strip absolute paths from __FILE__
    )
    add_link_options(
        /LTCG   # Link Time Code Generation
        /OPT:REF # Remove unreferenced data/functions
        /OPT:ICF # Identical COMDAT Folding
    )
    set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS OFF)
    # Use dynamic CRT (/MD or /MDd) to match FFmpeg built with /MD
    set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
    add_definitions(-D_CRT_SECURE_NO_WARNINGS)
elseif(APPLE)
    # macOS/Clang optimizations
    add_compile_options(
        -O3
        -fPIC
        -fvisibility=hidden
        -ffunction-sections
        -fdata-sections
        "-fdebug-prefix-map=${CMAKE_SOURCE_DIR}=."
    )

    add_link_options(
        -Wl,-dead_strip
    )
else()
    # Linux/GCC/Clang optimizations
    add_compile_options(
        -O3
        -fPIC                   # Required for linking static lib into shared object (Python bindings)
        -fvisibility=hidden     # Hide internal symbols by default
        -ffunction-sections
        -fdata-sections
        "-fdebug-prefix-map=${CMAKE_SOURCE_DIR}=." # Strip absolute paths in debug info
    )

    # Emscripten (WASM) uses wasm-ld which doesn't support --exclude-libs
    if(NOT EMSCRIPTEN)
        add_link_options(
            LINKER:--as-needed      # Only link libraries that satisfy unresolved symbols
            -Wl,--gc-sections       # Remove unused sections
            -Wl,--exclude-libs,ALL  # Prevent library symbols from being exported
        )
    else()
        add_link_options(
            -Wl,--gc-sections       # Remove unused sections
        )
    endif()

    # Strip symbols in Release builds for Linux
    if(CMAKE_BUILD_TYPE STREQUAL "Release")
        add_link_options(-s)    # Strip all symbols
    endif()
endif()

# Set unified output directory for all executables and libraries
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)

# RPATH configuration for Linux and macOS
if(APPLE)
    set(CMAKE_INSTALL_RPATH "@loader_path;@loader_path/../lib")
    set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
elseif(UNIX)
    # Ensure that installed binaries can find libraries in the relative ../lib directory
    # $ORIGIN is for libraries in lib/ to find each other
    # $ORIGIN/../lib is for binaries in bin/ to find libraries in lib/
    set(CMAKE_INSTALL_RPATH "$ORIGIN:$ORIGIN/../lib")
    set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
endif()

# FFmpeg Configuration
include(cmake/ffmpeg.cmake)

# miniaudio Configuration
if(ENABLE_WASAPI AND WIN32)
    include(cmake/miniaudio.cmake)
endif()

# Source files directory
set(FFMPEG_CORE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/avioflow/core/ffmpeg")

# Library sources
set(AVIOFLOW_SOURCES
    "${FFMPEG_CORE_DIR}/audio-resampler.cpp"
    "${FFMPEG_CORE_DIR}/avio-context-handler.cpp"
    "${FFMPEG_CORE_DIR}/device-handler.cpp"
    "${FFMPEG_CORE_DIR}/single-stream-decoder.cpp"
    "${FFMPEG_CORE_DIR}/single-stream-encoder.cpp"
    "${FFMPEG_CORE_DIR}/ffmpeg-common.cpp"
    "${CMAKE_CURRENT_SOURCE_DIR}/avioflow/include/avioflow-cxx-api.cpp"
    "${CMAKE_CURRENT_SOURCE_DIR}/avioflow/include/avioflow-c-api.cpp"
)

if(ENABLE_WASAPI AND WIN32)
    list(APPEND AVIOFLOW_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/avioflow/core/wasapi/wasapi-handler.cpp")
endif()

# Build library
if(BUILD_SHARED_LIBS STREQUAL "ON")
    set(AVIOFLOW_TYPE SHARED)
else()
    set(AVIOFLOW_TYPE STATIC)
endif()

add_library(avioflow ${AVIOFLOW_TYPE} ${AVIOFLOW_SOURCES})
target_compile_features(avioflow PUBLIC cxx_std_17)
target_compile_definitions(avioflow PRIVATE AVIOFLOW_EXPORTS)
if(NOT BUILD_SHARED_LIBS STREQUAL "ON")
    target_compile_definitions(avioflow PUBLIC AVIOFLOW_STATIC)
endif()
if(ENABLE_WASAPI AND WIN32)
    target_compile_definitions(avioflow PRIVATE AVIOFLOW_HAS_WASAPI)
endif()
if(UNIX AND NOT APPLE)
    target_compile_definitions(avioflow PUBLIC _GLIBCXX_USE_CXX11_ABI=${AVIOFLOW_GLIBCXX_USE_CXX11_ABI})
endif()

target_include_directories(avioflow PUBLIC 
    $<BUILD_INTERFACE:${FFMPEG_CORE_DIR}>
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/avioflow/include>
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/avioflow/core/wasapi>
    $<INSTALL_INTERFACE:include>
)

if(ENABLE_WASAPI AND WIN32)
    target_include_directories(avioflow PRIVATE "${MINIAUDIO_INCLUDE_DIR}")
endif()

target_link_libraries(avioflow PUBLIC ffmpeg::ffmpeg)

if(WIN32)
    target_link_libraries(avioflow PRIVATE Winmm)
endif()

# FFmpeg DLLs copy (for convenient local running)
if(WIN32)
    if(EXISTS "${FFMPEG_BIN_DIR}")
        file(GLOB FFMPEG_DLL_FILES "${FFMPEG_BIN_DIR}/*.dll")
        if(FFMPEG_DLL_FILES)
            add_custom_command(TARGET avioflow POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E copy_directory
                    "${FFMPEG_BIN_DIR}"
                    "$<TARGET_FILE_DIR:avioflow>"
                COMMENT "Copying FFmpeg DLLs to output directory"
            )
        endif()
    endif()
endif()

# Binaries
if(ENABLE_BINARY OR BUILD_TESTS)
    add_subdirectory(avioflow/bin)
endif()

# --- Python Bindings ---
if(ENABLE_PYTHON)
    add_subdirectory(python/csrc)
endif()

# --- Node.js Bindings ---
if(ENABLE_NODE_JS)
    add_subdirectory(nodejs/csrc)
endif()

# --- Java JNI Bindings ---
if(ENABLE_JAVA)
    add_subdirectory(java/src/main/cpp)
endif()

# --- glibc math symbol compatibility ---
# The prebuilt static FFmpeg bundles a libvorbis that still refers to the
# __*_finite math symbols glibc 2.31 removed. Any target that links those
# archives into a binary must resolve them, so this is needed whenever FFmpeg is
# static on Linux, not just for the Rust bindings. See the file's header comment.
if(UNIX AND NOT APPLE AND NOT BUILD_SHARED_LIBS)
    enable_language(C)
    target_sources(avioflow PRIVATE
        "${CMAKE_CURRENT_SOURCE_DIR}/avioflow/core/compat/glibc-finite-compat.c")
endif()

# --- WebAssembly Bindings ---
option(ENABLE_WASM "Enable WebAssembly bindings" OFF)
if(ENABLE_WASM)
    add_subdirectory(wasm/csrc)
endif()


# --- Installation & Packaging ---
# Only perform regular C++ installation if NOT building a Python wheel
if(NOT SKBUILD)
    # Install public headers
    install(FILES 
        "avioflow/include/avioflow-cxx-api.h"
        "avioflow/include/avioflow-c-api.h"
        "avioflow/include/metadata.h"
        DESTINATION include
    )

    # Install library
    install(TARGETS avioflow
        EXPORT avioflowTargets
        RUNTIME DESTINATION bin
        LIBRARY DESTINATION lib
        ARCHIVE DESTINATION lib
        INCLUDES DESTINATION include
    )

    set(AVIOFLOW_CMAKE_INSTALL_DIR "lib/cmake/avioflow")

    configure_package_config_file(
        "${CMAKE_CURRENT_SOURCE_DIR}/cmake/avioflowConfig.cmake.in"
        "${CMAKE_CURRENT_BINARY_DIR}/avioflowConfig.cmake"
        INSTALL_DESTINATION "${AVIOFLOW_CMAKE_INSTALL_DIR}"
    )

    write_basic_package_version_file(
        "${CMAKE_CURRENT_BINARY_DIR}/avioflowConfigVersion.cmake"
        VERSION "${PROJECT_VERSION}"
        COMPATIBILITY SameMajorVersion
    )

    install(EXPORT avioflowTargets
        FILE avioflowTargets.cmake
        NAMESPACE avioflow::
        DESTINATION "${AVIOFLOW_CMAKE_INSTALL_DIR}"
    )

    install(FILES
        "${CMAKE_CURRENT_BINARY_DIR}/avioflowConfig.cmake"
        "${CMAKE_CURRENT_BINARY_DIR}/avioflowConfigVersion.cmake"
        DESTINATION "${AVIOFLOW_CMAKE_INSTALL_DIR}"
    )

    if(EXISTS "${FFMPEG_ROOT}/lib/cmake/FFmpeg")
        install(DIRECTORY "${FFMPEG_ROOT}/lib/cmake/FFmpeg"
            DESTINATION lib/cmake
        )
    endif()

    # Install bundled FFmpeg runtime or static dependency payload.
    if(BUILD_SHARED_LIBS)
        if(WIN32)
            # Collect all DLLs from FFMPEG_BIN_DIR
            file(GLOB FFMPEG_DLLS "${FFMPEG_BIN_DIR}/*.dll")
            install(FILES ${FFMPEG_DLLS} DESTINATION bin)
            file(GLOB FFMPEG_IMPLIBS
                "${FFMPEG_LIB_DIR}/*.dll.a"
                "${FFMPEG_LIB_DIR}/*.lib"
            )
            if(FFMPEG_IMPLIBS)
                install(FILES ${FFMPEG_IMPLIBS} DESTINATION lib)
            endif()
        elseif(APPLE)
            # Collect all shared libraries from FFMPEG_LIB_DIR
            file(GLOB FFMPEG_DYLIBS "${FFMPEG_LIB_DIR}/lib*.dylib")
            install(FILES ${FFMPEG_DYLIBS} DESTINATION lib)
        elseif(UNIX)
            # Collect all shared libraries from FFMPEG_LIB_DIR
            file(GLOB FFMPEG_SOS "${FFMPEG_LIB_DIR}/lib*.so*")
            install(FILES ${FFMPEG_SOS} DESTINATION lib)
        endif()
    else()
        install(DIRECTORY "${FFMPEG_ROOT}/lib/"
            DESTINATION lib
            FILES_MATCHING
                PATTERN "*.a"
                PATTERN "*.lib"
                PATTERN "*.pc"
                PATTERN "*.cmake"
        )
    endif()

    # (Optional) Install CLI tools if ENABLE_BINARY is on
    if(ENABLE_BINARY)
        set(INSTALL_TARGETS 
            avioflow-offline-load-audio
            avioflow-online-load-audio
            avioflow-microphone-capture
        )

        if(TARGET avioflow-wasapi-capture)
            list(APPEND INSTALL_TARGETS avioflow-wasapi-capture)
        endif()

        install(TARGETS ${INSTALL_TARGETS}
            DESTINATION bin
        )
    endif()
endif()