From 9fdf61553e0f0115b6a122bcd562d0ee57516dee Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sat, 25 Jul 2026 22:37:36 +0000 Subject: [PATCH] backend(audio-cpp): add the native build scaffold Links 0xShug0/audio.cpp engine_runtime through its public framework headers and serves Health/Status. Model loading and the audio RPCs follow. Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto --- backend/cpp/audio-cpp/.gitignore | 8 ++ backend/cpp/audio-cpp/CMakeLists.txt | 105 ++++++++++++++++++++++++++ backend/cpp/audio-cpp/Makefile | 78 +++++++++++++++++++ backend/cpp/audio-cpp/grpc-server.cpp | 102 +++++++++++++++++++++++++ 4 files changed, 293 insertions(+) create mode 100644 backend/cpp/audio-cpp/.gitignore create mode 100644 backend/cpp/audio-cpp/CMakeLists.txt create mode 100644 backend/cpp/audio-cpp/Makefile create mode 100644 backend/cpp/audio-cpp/grpc-server.cpp diff --git a/backend/cpp/audio-cpp/.gitignore b/backend/cpp/audio-cpp/.gitignore new file mode 100644 index 000000000..2b848d6d8 --- /dev/null +++ b/backend/cpp/audio-cpp/.gitignore @@ -0,0 +1,8 @@ +audio.cpp/ +build/ +package/ +grpc-server +backend.pb.cc +backend.pb.h +backend.grpc.pb.cc +backend.grpc.pb.h diff --git a/backend/cpp/audio-cpp/CMakeLists.txt b/backend/cpp/audio-cpp/CMakeLists.txt new file mode 100644 index 000000000..1e784cde8 --- /dev/null +++ b/backend/cpp/audio-cpp/CMakeLists.txt @@ -0,0 +1,105 @@ +cmake_minimum_required(VERSION 3.20) +project(audio-cpp-grpc-server LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(TARGET grpc-server) + +set(AUDIO_CPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/audio.cpp" + CACHE PATH "Path to the pinned audio.cpp checkout") +option(AUDIO_CPP_GRPC_BUILD_TESTS "Build engine-linked ctest binaries" OFF) + +if(NOT EXISTS "${AUDIO_CPP_DIR}/CMakeLists.txt") + message(FATAL_ERROR + "AUDIO_CPP_DIR does not contain an audio.cpp checkout: ${AUDIO_CPP_DIR}. " + "Run 'make audio.cpp' first.") +endif() + +if(APPLE) + # Homebrew installs protobuf/grpc under a non-default prefix. + if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "arm64") + set(HOMEBREW_DEFAULT_PREFIX "/opt/homebrew") + else() + set(HOMEBREW_DEFAULT_PREFIX "/usr/local") + endif() + link_directories("${HOMEBREW_DEFAULT_PREFIX}/lib") + include_directories("${HOMEBREW_DEFAULT_PREFIX}/include") +endif() + +find_package(Threads REQUIRED) +find_package(Protobuf CONFIG QUIET) +if(NOT Protobuf_FOUND) + find_package(Protobuf REQUIRED) +endif() +find_package(gRPC CONFIG QUIET) +if(NOT gRPC_FOUND) + # Ubuntu's apt-installed grpc++ ships no CMake config. Fall back. + find_library(GRPCPP_LIB grpc++ REQUIRED) + find_library(GRPCPP_REFLECTION_LIB grpc++_reflection REQUIRED) + add_library(gRPC::grpc++ INTERFACE IMPORTED) + set_target_properties(gRPC::grpc++ PROPERTIES + INTERFACE_LINK_LIBRARIES "${GRPCPP_LIB}") + add_library(gRPC::grpc++_reflection INTERFACE IMPORTED) + set_target_properties(gRPC::grpc++_reflection PROPERTIES + INTERFACE_LINK_LIBRARIES "${GRPCPP_REFLECTION_LIB}") +endif() + +find_program(_PROTOC NAMES protoc REQUIRED) +find_program(_GRPC_CPP_PLUGIN NAMES grpc_cpp_plugin REQUIRED) + +get_filename_component(HW_PROTO "${CMAKE_CURRENT_SOURCE_DIR}/../../backend.proto" ABSOLUTE) +get_filename_component(HW_PROTO_PATH "${HW_PROTO}" PATH) + +set(HW_PROTO_SRCS "${CMAKE_CURRENT_BINARY_DIR}/backend.pb.cc") +set(HW_PROTO_HDRS "${CMAKE_CURRENT_BINARY_DIR}/backend.pb.h") +set(HW_GRPC_SRCS "${CMAKE_CURRENT_BINARY_DIR}/backend.grpc.pb.cc") +set(HW_GRPC_HDRS "${CMAKE_CURRENT_BINARY_DIR}/backend.grpc.pb.h") + +add_custom_command( + OUTPUT "${HW_PROTO_SRCS}" "${HW_PROTO_HDRS}" "${HW_GRPC_SRCS}" "${HW_GRPC_HDRS}" + COMMAND ${_PROTOC} + ARGS --grpc_out "${CMAKE_CURRENT_BINARY_DIR}" + --cpp_out "${CMAKE_CURRENT_BINARY_DIR}" + -I "${HW_PROTO_PATH}" + --plugin=protoc-gen-grpc="${_GRPC_CPP_PLUGIN}" + "${HW_PROTO}" + DEPENDS "${HW_PROTO}") + +add_library(hw_grpc_proto STATIC + ${HW_GRPC_SRCS} ${HW_GRPC_HDRS} + ${HW_PROTO_SRCS} ${HW_PROTO_HDRS}) +target_include_directories(hw_grpc_proto PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) +# Required on macOS: without these the Homebrew protobuf/grpc include dirs never +# reach this target and google/protobuf/runtime_version.h is not found. +target_link_libraries(hw_grpc_proto PUBLIC protobuf::libprotobuf gRPC::grpc++) + +# Upstream's global add_compile_options(-Wall -Wextra -Wpedantic -pedantic-errors) +# is a directory property of the subdirectory and does not reach our targets. +add_subdirectory("${AUDIO_CPP_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/audio-cpp" EXCLUDE_FROM_ALL) + +add_executable(${TARGET} + grpc-server.cpp +) + +target_include_directories(${TARGET} PRIVATE + "${AUDIO_CPP_DIR}/include" + "${CMAKE_CURRENT_SOURCE_DIR}") + +target_link_libraries(${TARGET} PRIVATE + hw_grpc_proto + engine_runtime + ggml + gRPC::grpc++ + gRPC::grpc++_reflection + protobuf::libprotobuf + Threads::Threads) + +# ENGINE_ENABLE_CPU_ALL_VARIANTS builds ggml backends as shared objects that sit +# next to the binary in the package, so the binary must search its own directory. +set_target_properties(${TARGET} PROPERTIES + BUILD_RPATH "$ORIGIN" + INSTALL_RPATH "$ORIGIN") + +if(AUDIO_CPP_GRPC_BUILD_TESTS) + enable_testing() +endif() diff --git a/backend/cpp/audio-cpp/Makefile b/backend/cpp/audio-cpp/Makefile new file mode 100644 index 000000000..4adf0228f --- /dev/null +++ b/backend/cpp/audio-cpp/Makefile @@ -0,0 +1,78 @@ +# audio.cpp backend Makefile. +# +# Upstream pin lives below as AUDIO_CPP_VERSION?= so .github/bump_deps.sh +# can find and update it, matching the llama-cpp / ds4 convention. The clone +# recipe is a make target (not a prepare.sh) so 'make purge && make' is a clean +# rebuild and so the bump bot can see the pin. + +AUDIO_CPP_VERSION?=e800d435d130dc776baf6f3e6129bb62b1495c89 +AUDIO_CPP_REPO?=https://github.com/0xShug0/audio.cpp + +CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +BUILD_DIR := build + +BUILD_TYPE ?= +NATIVE ?= false +JOBS ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) + +UNAME_S := $(shell uname -s) + +# AUDIOCPP_DEPLOYMENT_BUILD compiles the model_specs/*.json catalog into +# engine_runtime, so the shipped package needs no model_specs directory and a +# safetensors model tree still resolves its family spec. +CMAKE_ARGS ?= -DCMAKE_BUILD_TYPE=Release -DAUDIOCPP_DEPLOYMENT_BUILD=ON + +ifeq ($(BUILD_TYPE),cublas) + CMAKE_ARGS += -DENGINE_ENABLE_CUDA=ON +else ifeq ($(BUILD_TYPE),vulkan) + CMAKE_ARGS += -DENGINE_ENABLE_VULKAN=ON +else ifeq ($(UNAME_S),Darwin) + CMAKE_ARGS += -DENGINE_ENABLE_METAL=ON +else + # Portable Linux CPU. Upstream wires this to GGML_BACKEND_DL + + # GGML_CPU_ALL_VARIANTS + $ORIGIN rpath, so one build serves every CPU + # tier instead of an AVX-tier image fan-out. + CMAKE_ARGS += -DENGINE_ENABLE_CPU_ALL_VARIANTS=ON +endif + +ifneq ($(NATIVE),true) + CMAKE_ARGS += -DENGINE_ENABLE_NATIVE_CPU=OFF +endif + +.PHONY: all grpc-server package test test-engine clean purge +all: grpc-server + +# Clone the upstream source at the pinned commit. The directory is the target +# so make only re-clones when it is missing. After bumping AUDIO_CPP_VERSION, +# run 'make purge && make' to refetch. +audio.cpp: + mkdir -p audio.cpp + cd audio.cpp && \ + git init -q && \ + git remote add origin $(AUDIO_CPP_REPO) && \ + git fetch --depth 1 origin $(AUDIO_CPP_VERSION) && \ + git checkout FETCH_HEAD + +grpc-server: audio.cpp + mkdir -p $(BUILD_DIR) + cd $(BUILD_DIR) && cmake $(CMAKE_ARGS) $(CURRENT_MAKEFILE_DIR) && \ + cmake --build . --config Release -j $(JOBS) + cp $(BUILD_DIR)/grpc-server grpc-server + +package: grpc-server + bash package.sh + +test: + @echo "audio-cpp: standalone unit tests run from the repo root via 'make test-backend-cpp'" + +# Engine-linked tests. Needs the upstream checkout and a full engine build. +test-engine: audio.cpp + mkdir -p $(BUILD_DIR) + cd $(BUILD_DIR) && cmake $(CMAKE_ARGS) -DAUDIO_CPP_GRPC_BUILD_TESTS=ON $(CURRENT_MAKEFILE_DIR) && \ + cmake --build . --config Release -j $(JOBS) && ctest --output-on-failure + +clean: + rm -rf $(BUILD_DIR) grpc-server package + +purge: clean + rm -rf audio.cpp diff --git a/backend/cpp/audio-cpp/grpc-server.cpp b/backend/cpp/audio-cpp/grpc-server.cpp new file mode 100644 index 000000000..bc07e2cb2 --- /dev/null +++ b/backend/cpp/audio-cpp/grpc-server.cpp @@ -0,0 +1,102 @@ +// audio.cpp LocalAI gRPC backend. +// +// Links 0xShug0/audio.cpp's engine_runtime through its public +// include/engine/framework/** headers only. Nothing under upstream's app/, +// src/ or tests/ is used: those are application internals, they are where +// upstream expects churn, and upstream is Apache-2.0 while LocalAI is MIT. +// +// This commit is the bind/listen/Health/Status skeleton. Model loading and the +// audio RPCs land in later commits. + +#include "backend.pb.h" +#include "backend.grpc.pb.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using grpc::Server; +using grpc::ServerBuilder; +using grpc::ServerContext; +// Do NOT alias grpc::Status as Status: the Status RPC method would shadow the +// type and break every other method that names it as a return type. +using GStatus = ::grpc::Status; + +namespace { + +std::atomic g_server{nullptr}; + +class AudioCppBackend final : public backend::Backend::Service { +public: + GStatus Health(ServerContext *, const backend::HealthMessage *, + backend::Reply *reply) override { + reply->set_message("OK"); + return GStatus::OK; + } + + GStatus Status(ServerContext *, const backend::HealthMessage *, + backend::StatusResponse *response) override { + response->set_state(backend::StatusResponse::UNINITIALIZED); + return GStatus::OK; + } +}; + +void RunServer(const std::string &addr) { + AudioCppBackend service; + grpc::EnableDefaultHealthCheckService(true); + grpc::reflection::InitProtoReflectionServerBuilderPlugin(); + + ServerBuilder builder; + builder.AddListeningPort(addr, grpc::InsecureServerCredentials()); + builder.RegisterService(&service); + // Audio payloads (PCM buffers, encoded frames) are far larger than the + // 4 MiB gRPC default. + builder.SetMaxReceiveMessageSize(256 * 1024 * 1024); + builder.SetMaxSendMessageSize(256 * 1024 * 1024); + + std::unique_ptr server(builder.BuildAndStart()); + if (!server) { + std::cerr << "audio-cpp grpc-server: failed to bind " << addr << "\n"; + std::exit(1); + } + g_server = server.get(); + std::cerr << "audio-cpp grpc-server listening on " << addr << "\n"; + server->Wait(); +} + +void signal_handler(int) { + if (auto *srv = g_server.load()) { + srv->Shutdown(std::chrono::system_clock::now() + std::chrono::seconds(3)); + } +} + +} // namespace + +int main(int argc, char *argv[]) { + std::string addr = "127.0.0.1:50051"; + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + const std::string addr_flag = "--addr="; + if (a.rfind(addr_flag, 0) == 0) { + addr = a.substr(addr_flag.size()); + } else if (a == "--addr" && i + 1 < argc) { + addr = argv[++i]; + } else if (a == "--help" || a == "-h") { + std::cout << "Usage: grpc-server --addr=HOST:PORT\n"; + return 0; + } + } + std::signal(SIGINT, signal_handler); + std::signal(SIGTERM, signal_handler); + RunServer(addr); + return 0; +}