diff --git a/.github/workflows/build-puredata-plugins-with-pd-lib-builder.yml b/.github/workflows/build-puredata-plugins-with-pd-lib-builder.yml index 98872ac..30507cc 100644 --- a/.github/workflows/build-puredata-plugins-with-pd-lib-builder.yml +++ b/.github/workflows/build-puredata-plugins-with-pd-lib-builder.yml @@ -56,13 +56,15 @@ jobs: - name: Set up Conan run: | - pip install conan + pip install "conan>=2.13,<3" conan profile detect + # Resolved at the repo root so this wrapper and zerr_core share one profile; + # puredata/Makefile reads ../build/conandeps.mk by default. - name: Configure Conan - working-directory: puredata run: | - conan install . --output-folder=build --build=missing + conan install . --output-folder=build --build=missing \ + -pr:h=profiles/macos -pr:b=profiles/macos - name: Build & Install working-directory: puredata @@ -108,13 +110,13 @@ jobs: - name: Set up Conan run: | - pip install conan + pip install "conan>=2.13,<3" conan profile detect - name: Configure Conan - working-directory: puredata run: | - conan install . --output-folder=build --build=missing + conan install . --output-folder=build --build=missing \ + -pr:h=profiles/linux -pr:b=profiles/linux - name: Build & Install working-directory: puredata @@ -220,14 +222,13 @@ jobs: - name: Set up Conan run: | - pip install conan + pip install "conan>=2.13,<3" conan profile detect --force + # mingw-profile.txt moved to profiles/mingw alongside the other profiles. - name: Install Dependencies - working-directory: puredata run: | - mkdir build - conan install . -pr:h=./mingw-profile.txt -pr:b=./mingw-profile.txt --output-folder=build --build=missing + conan install . -pr:h=profiles/mingw -pr:b=profiles/mingw --output-folder=build --build=missing - name: Build External shell: bash diff --git a/.github/workflows/build-zerr-core-static-library.yml b/.github/workflows/build-zerr-core-static-library.yml index 2ea9ba5..24aae1f 100644 --- a/.github/workflows/build-zerr-core-static-library.yml +++ b/.github/workflows/build-zerr-core-static-library.yml @@ -7,6 +7,13 @@ on: - main paths: - "core/**" + - "conanfile.txt" + - "profiles/**" + +# Dependencies are resolved once at the repo root (see /conanfile.txt) using a +# committed profile, so CI and local builds share one dependency configuration. +# conan is pinned below the next major: a bare `pip install conan` would float to +# conan 3.x and break these jobs with no change on our side. jobs: macOS: @@ -17,21 +24,21 @@ jobs: - name: Set up Conan run: | - pip install conan + pip install "conan>=2.13,<3" conan profile detect - name: Install Dependencies - working-directory: core run: | - conan install . --output-folder=build --build=missing + conan install . --output-folder=build --build=missing \ + -pr:h=profiles/macos -pr:b=profiles/macos - name: Build Static Library - working-directory: core run: | - cd build/ - cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release - cmake --build . - make install + cmake -S core -B core/build \ + -DCMAKE_TOOLCHAIN_FILE="$GITHUB_WORKSPACE/build/conan_toolchain.cmake" \ + -DCMAKE_BUILD_TYPE=Release + cmake --build core/build + cmake --install core/build - name: Upload zerr-core library uses: actions/upload-artifact@v4 @@ -48,22 +55,22 @@ jobs: - name: Set up Conan run: | - pip install conan + pip install "conan>=2.13,<3" conan profile detect - name: Install Dependencies - working-directory: core run: | - conan install . --output-folder=build --build=missing + conan install . --output-folder=build --build=missing \ + -pr:h=profiles/linux -pr:b=profiles/linux - name: Build Static Library - working-directory: core run: | export CXXFLAGS="-fPIC" - cd build/ - cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release - cmake --build . - make install + cmake -S core -B core/build \ + -DCMAKE_TOOLCHAIN_FILE="$GITHUB_WORKSPACE/build/conan_toolchain.cmake" \ + -DCMAKE_BUILD_TYPE=Release + cmake --build core/build + cmake --install core/build - name: Upload zerr-core library uses: actions/upload-artifact@v4 @@ -85,13 +92,15 @@ jobs: - name: Set up Conan run: | - pip install conan + pip install "conan>=2.13,<3" conan profile detect --force + # Deliberately keeps its inline settings rather than -pr:h=profiles/mingw: + # this job builds natively with "MinGW Makefiles" on windows-latest, whereas + # profiles/mingw additionally pins compiler_executables for the + # cross-compiling PureData job. Switching it is untested — see docs/design. - name: Install Dependencies - working-directory: core run: | - mkdir build conan install . --output-folder=build --build=missing ` -s compiler=gcc ` -s compiler.version=13 ` @@ -99,15 +108,14 @@ jobs: -s build_type=Release - name: Build Static Library - working-directory: core run: | - cmake -S . -B build ` + cmake -S core -B core/build ` -G "MinGW Makefiles" ` - -DCMAKE_TOOLCHAIN_FILE="build/conan_toolchain.cmake" ` + -DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}/build/conan_toolchain.cmake" ` -DCMAKE_BUILD_TYPE=Release ` -DCMAKE_CXX_STANDARD=17 - cmake --build build --config Release - cmake --install build + cmake --build core/build --config Release + cmake --install core/build - name: Upload zerr-core library uses: actions/upload-artifact@v4 diff --git a/.github/workflows/deploy-doxygen.yml b/.github/workflows/deploy-doxygen.yml new file mode 100644 index 0000000..3b170ca --- /dev/null +++ b/.github/workflows/deploy-doxygen.yml @@ -0,0 +1,53 @@ +name: Deploy Doxygen documentation + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - "core/include/**" + - "docs/**" + +permissions: + # Specifying any permissions sets every unlisted scope to `none`, so + # actions/checkout needs contents: read stated explicitly -- without it the + # checkout has no repo read scope and the deploy breaks the moment the repo is + # private or default token permissions tighten. + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + submodules: true + + - name: Install Doxygen + run: sudo apt-get install -y doxygen + + - name: Generate documentation + run: doxygen docs/Doxyfile + + - name: Configure Pages + uses: actions/configure-pages@v4 + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/build/html + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index e87545b..6735cfe 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ fftw3/ yaml-cpp/ # Documentation output +docs/build/ html/ latex/ diff --git a/.gitmodules b/.gitmodules index 526e939..f8ee30a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -9,3 +9,6 @@ [submodule "maxmsp/source/min-lib"] path = maxmsp/source/min-lib url = https://github.com/Cycling74/min-lib.git +[submodule "doxygen-awesome-css"] + path = docs/doxygen-awesome-css + url = https://github.com/jothepro/doxygen-awesome-css.git diff --git a/CLAUDE.md b/CLAUDE.md index 836fe7d..d8786fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,25 +10,43 @@ Reference paper: "Autogenous Spatialization for Arbitrary Loudspeaker Setups" (I ## Build Commands +### Dependencies (resolved once, at the repo root) + +Declared in the root `conanfile.txt` and installed into a single shared `build/` folder that every +target consumes: core and Max/MSP via `build/conan_toolchain.cmake`, PureData via +`build/conandeps.mk`. Committed profiles in `profiles/` (`macos`, `linux`, `mingw`) pin the settings +that must be reproducible across machines. Recipe revisions are pinned in `conanfile.txt` because both +libraries need a ConanCenter revision that supports CMake 4. The macOS deployment target is set in +`core/CMakeLists.txt` and `maxmsp/CMakeLists.txt`, not in the profile. + +```bash +conan install . --output-folder=build --build=missing \ + -pr:h=profiles/macos -pr:b=profiles/macos # or profiles/linux, profiles/mingw +``` + +`build.sh` does this automatically on first use. Conan is only needed when dependencies actually +need resolving — a tree with a populated `build/` still builds without it. + ### Core Library (must be built first — other targets depend on `libzerr_core.a`) ```bash -cd core -conan install . --output-folder=build --build=missing -cd build -cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release -cmake --build . -make install # Installs to core/lib +cmake -S core -B core/build \ + -DCMAKE_TOOLCHAIN_FILE="$PWD/build/conan_toolchain.cmake" -DCMAKE_BUILD_TYPE=Release +cmake --build core/build +cmake --install core/build # Installs to core/lib ``` ### Using the build script (recommended) ```bash +./build.sh deps # Resolve dependencies only +./build.sh -c deps # Discard resolved dependencies +./build.sh core # Build core library ./build.sh puredata # Build PureData externals (auto-builds core if needed) ./build.sh -i puredata # Build and install ./build.sh maxmsp # Build Max/MSP externals ./build.sh -i maxmsp # Build and install -./build.sh jack # JACK client (in development) +./build.sh jack # JACK client — currently broken, see docs/design/repo-audit-2026-07-30.md ``` ### Code Formatting @@ -78,10 +96,17 @@ YAML files defining speaker positions in Cartesian coordinates (x, y, z). Exampl - **Namespace**: `zerr`, features in `zerr::feature` - **Custom types** (`types.h`): `Sample` = double, `Param` = float, `Index` = int - **Naming**: PascalCase classes, camelCase methods, `_prefixed` private methods -- **Dependencies**: Conan 2.x manages fftw3 and yaml-cpp +- **Dependencies**: Conan 2.x manages fftw3 and yaml-cpp, declared once in the root `conanfile.txt` ## Platform Notes - **macOS**: Supports Intel + Apple Silicon (fat binaries with Xcode 12+) - **Linux**: Requires `-fPIC` for static library builds -- **Windows**: MinGW-w64 gcc 13 for PureData; uses `puredata/mingw-profile.txt` Conan profile +- **Windows**: MinGW-w64 gcc 13 for PureData; uses the `profiles/mingw` Conan profile. `build.sh` + runs from an MSYS2 MINGW64 / Git Bash shell — it pins the `MinGW Makefiles` generator and prefers + `mingw32-make`. Max/MSP is not buildable there (`.mxe64` needs MSVC, which cannot link a + MinGW-built core); `build.sh maxmsp` refuses with a message + +Before changing any `find_package` call, the `CONAN_*` variables in `puredata/Makefile`, or +`conanfile.txt`, read `docs/design/dependency-fallbacks.md` — it records which non-conan resolve +routes are viable and what each one breaks (target names, static/shared, deployment target, CMake 4). diff --git a/README.md b/README.md index 459863d..3f333ac 100644 --- a/README.md +++ b/README.md @@ -42,13 +42,19 @@ Otherwise, you can also build it for your own machine. #### Dependencies - Zerr* uses [Conan](https://docs.conan.io/2/tutorial.html) for dependency management, please refer to its documentation for details -- The core library of Zerr* depends only on [yaml-cpp](https://github.com/jbeder/yaml-cpp) and [fftw3](https://www.fftw.org/). Use the following Conan command to install them +- The core library of Zerr* depends only on [yaml-cpp](https://github.com/jbeder/yaml-cpp) and [fftw3](https://www.fftw.org/). Both are declared once in the root `conanfile.txt` and resolved into a single shared `build/` folder that every target consumes — the core and Max/MSP via `build/conan_toolchain.cmake`, PureData via `build/conandeps.mk` ```bash -conan install . --output-folder=build --build=missing +# from the repo root; pick the profile matching your host +conan install . --output-folder=build --build=missing \ + -pr:h=profiles/macos -pr:b=profiles/macos ``` -- You can also edit the paths in MakefIle/CMakeLists.txt to point to your own +- Committed profiles live in `profiles/` (`macos`, `linux`, `mingw`), so dependency resolution is reproducible instead of depending on each machine's `~/.conan2` default. Recipe revisions are pinned in `conanfile.txt` because both libraries need a ConanCenter revision that supports CMake 4. Read `profiles/macos` before changing either — it records which settings are deliberately *not* pinned, and why +- The macOS deployment target is set in `core/CMakeLists.txt` (and `maxmsp/CMakeLists.txt`), not via the Conan profile — see the comment in `profiles/macos` for the reasoning +- `./build.sh` resolves dependencies automatically on first use, so the command above is only needed for a manual or non-default build. `./build.sh deps` re-runs just the resolve step, and `./build.sh -c deps` discards it +- Conan is only required when dependencies actually need resolving; a tree with a populated `build/` folder still builds without it +- You can also edit the paths in Makefile/CMakeLists.txt to point to your own #### For Puredata @@ -64,6 +70,29 @@ conan install . --output-folder=build --build=missing ./build.sh -i puredata ``` +#### On Windows + +`build.sh` runs on Windows as well, from an **MSYS2 MINGW64** or Git Bash shell. It detects the host +and selects `profiles/mingw`, pins CMake to the `MinGW Makefiles` generator — without that, CMake +picks Visual Studio whenever one is installed and compiles with MSVC against a MinGW toolchain — and +uses `mingw32-make` in preference to the MSYS `make`. There is no `.bat`/PowerShell equivalent; +native `cmd.exe` is not supported. + +```bash +# in an MSYS2 MINGW64 shell, with MinGW-w64 gcc 13 on PATH +./build.sh deps +./build.sh core +./build.sh puredata +``` + +pd-lib-builder needs to find your Pd installation for `m_pd.h`; set `PDDIR` if it does not locate it +on its own. + +Max/MSP cannot be built on Windows from this script — a `.mxe64` requires an MSVC toolchain, whose +runtime and C++ ABI cannot link against the MinGW-built `libzerr_core.a` that `profiles/mingw` +produces. `./build.sh maxmsp` reports this and exits rather than failing inside Min-DevKit. See +[`docs/design/dependency-fallbacks.md`](docs/design/dependency-fallbacks.md) §3.7. + #### For Jack ```bash diff --git a/build.sh b/build.sh index d3dd560..07add70 100755 --- a/build.sh +++ b/build.sh @@ -2,12 +2,16 @@ set -e install=false +clean=false -while getopts ":i" opt; do +while getopts ":ic" opt; do case ${opt} in i ) install=true ;; + c ) + clean=true + ;; \? ) echo "Invalid Option: -$OPTARG" 1>&2 exit 1 @@ -16,6 +20,14 @@ while getopts ":i" opt; do done shift $((OPTIND -1)) +# ----------------------------------------------------------------------------- +# Paths. Dependencies are resolved once at the repo root (see /conanfile.txt) and +# shared by every target, so all of them link against one identical profile. +# ----------------------------------------------------------------------------- +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONAN_DIR="$ROOT/build" +TOOLCHAIN="$CONAN_DIR/conan_toolchain.cmake" + # ----------------------------------------------------------------------------- # Pre-flight checks: verify required tools are available # ----------------------------------------------------------------------------- @@ -27,79 +39,273 @@ check_tool() { fi } -check_tool conan check_tool cmake -check_tool make # ----------------------------------------------------------------------------- -build_core() { - echo "Building zerr core library..." - cd core || { echo "Failed to enter 'core' directory"; exit 1; } +# Host platform +# ----------------------------------------------------------------------------- +# "Windows" here always means an MSYS2 / MinGW64 / Git Bash shell -- that is the +# only way a bash script runs on Windows, and it is what profiles/mingw targets. +is_windows() { + case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*) return 0 ;; + *) return 1 ;; + esac +} - conan install . --output-folder=build --build=missing - cd build || { echo "Failed to enter 'core/build' directory"; exit 1; } +# Pick the committed profile matching the host. Each pins the settings that must +# agree between zerr_core and the wrappers; see profiles/ for the rationale. +detect_profile() { + case "$(uname -s)" in + Darwin) echo "profiles/macos" ;; + Linux) echo "profiles/linux" ;; + MINGW*|MSYS*|CYGWIN*) echo "profiles/mingw" ;; + *) echo "default" ;; + esac +} - cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release - cmake --build . +# The MinGW-w64 toolchain ships its make as `mingw32-make`. MSYS2 additionally +# provides a plain `make` built against the MSYS runtime, which is *not* the one +# that pairs with the mingw compilers. The PureData Windows CI job runs +# mingw32-make; prefer the same binary, and fall back to `make` where it is all +# that exists. +detect_make() { + if is_windows && command -v mingw32-make &> /dev/null; then + echo "mingw32-make" + else + echo "make" + fi +} - echo "Installing zerr_core library to local lib folder..." - make install +MAKE="$(detect_make)" +check_tool "$MAKE" + +# CMake on Windows defaults to a Visual Studio generator whenever one is installed, +# which would compile with MSVC against the gcc/libstdc++ toolchain profiles/mingw +# resolves -- objects that cannot link together. The core CI job pins the generator +# for exactly this reason; match it rather than depending on what happens to be +# installed on the machine. +CMAKE_GEN_ARGS=() +CMAKE_GENERATOR_NAME="" +if is_windows; then + CMAKE_GENERATOR_NAME="MinGW Makefiles" + CMAKE_GEN_ARGS=(-G "$CMAKE_GENERATOR_NAME") +fi - cd ../.. || exit 1 +# What CMake records in CMakeCache.txt is not the string we handed it. Under +# MSYS2 / Git Bash, $ROOT comes out of `pwd` as /c/Users/..., and the MSYS layer +# rewrites the argument to the native C:/Users/... on its way to a native CMake -- +# so comparing the cached CMAKE_TOOLCHAIN_FILE against $TOOLCHAIN verbatim can +# never match, and every single run would discard the build tree and rebuild the +# core. Keep $TOOLCHAIN in POSIX form (bash still has to stat it) and compare +# against the native spelling instead. +# +# If cygpath is somehow absent this degrades to the over-eager discard rather than +# to a wrong build, which is the right way round to fail. +TOOLCHAIN_CACHE="$TOOLCHAIN" +if is_windows && command -v cygpath &> /dev/null; then + TOOLCHAIN_CACHE="$(cygpath -m "$TOOLCHAIN")" +fi + +# A Max external on Windows is a .mxe64, which Max loads with the MSVC runtime. +# profiles/mingw resolves gcc/libstdc++11 packages and builds a libzerr_core.a to +# match, and the two ABIs cannot be linked together -- so there is no way to +# produce a working .mxe64 from this script's single conan resolve. +# +# Checked up front, over the whole target list, rather than inside build_maxmsp: +# the dispatcher builds the core library before it calls the per-target function, +# so a guard down there would resolve dependencies and compile the entire core +# before announcing that it cannot proceed. +assert_target_supported() { + if [ "$1" = "maxmsp" ] && is_windows; then + echo "Max/MSP is not buildable on Windows from this script." + echo " A .mxe64 requires an MSVC toolchain and a matching conan profile;" + echo " profiles/mingw produces gcc/libstdc++ objects that cannot link into one." + echo " See docs/design/dependency-fallbacks.md section 3.7." + exit 1 + fi } -check_core_built() { - if [ -d "core/lib" ]; then - return 0 +# ----------------------------------------------------------------------------- +# Dependencies (conan, once, at the root) +# ----------------------------------------------------------------------------- + +# Existence is not freshness. A git pull that bumps the pinned recipe revisions in +# conanfile.txt -- the whole reason those revisions are pinned there -- leaves a +# perfectly well-formed but outdated build/ behind, and every target would keep +# linking the previously resolved packages without a word. Re-resolve whenever an +# input is newer than the output conan generated from it. +deps_installed() { + [ -f "$TOOLCHAIN" ] && [ -f "$CONAN_DIR/conandeps.mk" ] || return 1 + + local input + for input in "$ROOT/conanfile.txt" "$ROOT/$(detect_profile)"; do + if [ -f "$input" ] && [ "$input" -nt "$TOOLCHAIN" ]; then + echo "$input is newer than the resolved dependencies." + return 1 + fi + done + + return 0 +} + +install_deps() { + # conan is only required when dependencies actually need resolving, so a tree + # with a populated build/ folder still builds on a machine without conan. + check_tool conan + + local profile + profile="$(detect_profile)" + + echo "Resolving dependencies with conan (profile: $profile)..." + cd "$ROOT" || exit 1 + conan install . --output-folder=build --build=missing \ + -pr:h="$profile" -pr:b="$profile" +} + +ensure_deps() { + if deps_installed; then + echo "Conan dependencies already present in $CONAN_DIR. Skipping resolve." else + install_deps + fi +} + +# A CMakeCache left over from an earlier layout silently ignores a newly passed +# CMAKE_TOOLCHAIN_FILE -- CMake only warns "Manually-specified variables were not +# used by the project" and keeps using the cached one. The shared root toolchain, +# and the deployment target it pins, would then never take effect. Drop the cache +# whenever it disagrees with the toolchain we intend to build against. +stale_cache_reason() { + local build_dir="$1" + + if [ -f "$build_dir/conan_toolchain.cmake" ]; then + # Pre-consolidation tree: conan used to generate into each target's own + # build folder, so this cache has that older toolchain baked in. + echo "it holds a pre-consolidation conan output" + elif [ -f "$build_dir/CMakeCache.txt" ]; then + local cached generator + cached="$(sed -n 's/^CMAKE_TOOLCHAIN_FILE:[^=]*=//p' "$build_dir/CMakeCache.txt" | head -1)" + generator="$(sed -n 's/^CMAKE_GENERATOR:[^=]*=//p' "$build_dir/CMakeCache.txt" | head -1)" + if [ "$cached" != "$TOOLCHAIN_CACHE" ]; then + echo "it was configured with ${cached:-}" + elif [ -n "$CMAKE_GENERATOR_NAME" ] && [ "$generator" != "$CMAKE_GENERATOR_NAME" ]; then + # Passing -G to a build directory whose cache names a different generator + # is a hard CMake error, not a warning. The case that matters: a cache + # written before this script pinned "MinGW Makefiles", when CMake picked + # Visual Studio on its own. Discard it like any other disagreement rather + # than making the user delete it by hand. + echo "it was configured with the ${generator:-} generator" + elif [ -f "$TOOLCHAIN" ] && [ "$TOOLCHAIN" -nt "$build_dir/CMakeCache.txt" ]; then + # The toolchain's *contents* changed since this cache was written -- a + # re-resolve with different profile settings, for instance. The toolchain + # assigns with set(... CACHE ...), which never overrides an existing + # entry, so an older cache silently wins: that is how a newly pinned + # CMAKE_OSX_DEPLOYMENT_TARGET stayed empty and left objects at the + # host SDK. Path equality is not enough; compare freshness too. + echo "the toolchain is newer than this cache" + fi + fi +} + +reconfigure_if_stale() { + local build_dir="$1" + [ -d "$build_dir" ] || return 0 + + local reason + reason="$(stale_cache_reason "$build_dir")" + [ -n "$reason" ] || return 0 + + echo "Discarding $build_dir: $reason." + echo " (generated output only — reconfiguring against $TOOLCHAIN)" + rm -rf "$build_dir" +} + +# ----------------------------------------------------------------------------- +build_core() { + ensure_deps + reconfigure_if_stale "$ROOT/core/build" + + echo "Building zerr core library..." + cmake -S "$ROOT/core" -B "$ROOT/core/build" "${CMAKE_GEN_ARGS[@]}" \ + -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN" \ + -DCMAKE_BUILD_TYPE=Release + cmake --build "$ROOT/core/build" + + echo "Installing zerr_core library to local lib folder..." + cmake --install "$ROOT/core/build" +} + +# What a wrapper actually links is core/lib/libzerr_core.a, so "core is already +# built" has to mean "built against the toolchain this wrapper is about to be +# configured with". Existence alone is not enough: after a re-resolve, +# reconfigure_if_stale drops the wrapper's cache so the wrapper picks up the new +# toolchain, while a surviving archive still carries the old settings. That is the +# very core/wrapper mismatch (deployment target, libcxx, arch) the shared toolchain +# exists to prevent -- it would just move from configure time to link time. +check_core_built() { + local lib="$ROOT/core/lib/libzerr_core.a" + [ -f "$lib" ] || return 1 + + # Same freshness test reconfigure_if_stale applies to a CMakeCache. + if [ -f "$TOOLCHAIN" ] && [ "$TOOLCHAIN" -nt "$lib" ]; then + echo "Installed core library predates $TOOLCHAIN." + return 1 + fi + + # The build tree that produced the archive would itself be discarded now, so + # whatever it installed was configured against something we no longer accept. + local reason + reason="$(stale_cache_reason "$ROOT/core/build")" + if [ -n "$reason" ]; then + echo "Installed core library came from a build tree to be discarded: $reason." return 1 fi + + return 0 } # ----------------------------------------------------------------------------- build_puredata() { - echo "Building Zerr* for Pure Data..." - cd puredata || { echo "Failed to enter 'puredata' directory"; exit 1; } + ensure_deps - conan install . --output-folder=build --build=missing + echo "Building Zerr* for Pure Data..." + cd "$ROOT/puredata" || { echo "Failed to enter 'puredata' directory"; exit 1; } - make + "$MAKE" if [ "$install" = true ]; then echo "Installing Pure Data build..." - make install + "$MAKE" install fi - cd .. || exit 1 + cd "$ROOT" || exit 1 } # ----------------------------------------------------------------------------- build_maxmsp() { - echo "Building Zerr* for Max/MSP..." - cd maxmsp || { echo "Failed to enter 'maxmsp' directory"; exit 1; } - - conan install . --output-folder=build --build=missing + # Windows is rejected by assert_target_supported before any build starts. + ensure_deps + reconfigure_if_stale "$ROOT/maxmsp/build" - cd build || { echo "Failed to enter 'maxmsp/build' directory"; exit 1; } - - cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release - cmake --build . + echo "Building Zerr* for Max/MSP..." + cmake -S "$ROOT/maxmsp" -B "$ROOT/maxmsp/build" "${CMAKE_GEN_ARGS[@]}" \ + -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN" \ + -DCMAKE_BUILD_TYPE=Release + cmake --build "$ROOT/maxmsp/build" if [ "$install" = true ]; then echo "Installing Max/MSP build..." - make install + cmake --build "$ROOT/maxmsp/build" --target install fi - - cd ../.. || exit 1 - } # ----------------------------------------------------------------------------- build_jack() { echo "Building Zerr* for JACK..." - cd jack || { echo "Failed to enter 'jack' directory"; exit 1; } + cd "$ROOT/jack" || { echo "Failed to enter 'jack' directory"; exit 1; } meson setup builddir --wipe 2>/dev/null || meson setup builddir meson compile -C builddir @@ -109,38 +315,95 @@ build_jack() { meson install -C builddir fi - cd .. || exit 1 + cd "$ROOT" || exit 1 } +# ----------------------------------------------------------------------------- +clean_deps() { + echo "Cleaning resolved dependencies..." + rm -rf "$CONAN_DIR" + echo "Dependencies cleaned." +} + +clean_core() { + echo "Cleaning core build artifacts..." + rm -rf "$ROOT/core/build" "$ROOT/core/lib" + echo "Core cleaned." +} + +clean_puredata() { + echo "Cleaning PureData build artifacts..." + # puredata/build is now only an install destination (PDLIBDIR); the conan + # output it used to hold lives at the repo root -- see clean_deps. + rm -rf "$ROOT/puredata/build" + # Subshell so the cd cannot leak, and `|| true` because `set -e` would abort the + # whole script -- skipping any remaining targets -- if `make clean` failed (it + # needs the pd-lib-builder submodule checked out). + ( cd "$ROOT/puredata" && "$MAKE" clean >/dev/null 2>&1 ) || true + echo "PureData cleaned." +} + +clean_maxmsp() { + echo "Cleaning Max/MSP build artifacts..." + rm -rf "$ROOT/maxmsp/build" + echo "Max/MSP cleaned." +} + +clean_jack() { + echo "Cleaning JACK build artifacts..." + rm -rf "$ROOT/jack/builddir" + echo "JACK cleaned." +} + # ----------------------------------------------------------------------------- if [ $# -eq 0 ]; then - echo "No targets provided. Usage: $0 [-i] " + echo "No targets provided. Usage: $0 [-i] [-c] " exit 1 fi -# Build core if needed -if check_core_built; then - echo "Core already built. Skipping core build." -else - build_core -fi - -# Build the requested targets for target in "$@"; do case $target in - puredata) - build_puredata - ;; - maxmsp) - build_maxmsp - ;; - jack) - build_jack - ;; + deps|core|puredata|maxmsp|jack) ;; *) - echo "Invalid target: $target. Valid options are: puredata, maxmsp, jack" + echo "Invalid target: $target. Valid options are: deps, core, puredata, maxmsp, jack" exit 1 ;; esac + # Cleaning an unsupported target is still fine -- it is only ever an rm -rf. + [ "$clean" = true ] || assert_target_supported "$target" +done + +# Clean or build the requested targets +for target in "$@"; do + if [ "$clean" = true ]; then + case $target in + deps) clean_deps ;; + core) clean_core ;; + puredata) clean_puredata ;; + maxmsp) clean_maxmsp ;; + jack) clean_jack ;; + esac + else + case $target in + deps) + install_deps + ;; + core) + build_core + ;; + puredata|maxmsp|jack) + if check_core_built; then + echo "Core already built. Skipping core build." + else + build_core + fi + case $target in + puredata) build_puredata ;; + maxmsp) build_maxmsp ;; + jack) build_jack ;; + esac + ;; + esac + fi done diff --git a/conanfile.txt b/conanfile.txt new file mode 100644 index 0000000..0640fab --- /dev/null +++ b/conanfile.txt @@ -0,0 +1,34 @@ +# Single source of truth for Zerr*'s external dependencies. +# +# Previously core/, puredata/ and maxmsp/ each carried an identical [requires] +# block and ran their own `conan install`. Three independent installs meant three +# independent profile resolutions, which is how the core came to be compiled for a +# different macOS deployment target than the wrappers linked against. +# +# One install at the repo root, consumed by all three targets: +# core, maxmsp -> build/conan_toolchain.cmake (CMakeToolchain + CMakeDeps) +# puredata -> build/conandeps.mk (MakeDeps, for pd-lib-builder) +# +# conan install . --output-folder=build --build=missing -pr:h=profiles/macos -pr:b=profiles/macos + +# Recipe revisions are pinned deliberately. Both libraries ship a bundled +# CMakeLists.txt with `cmake_minimum_required` below 3.5 (fftw 3.0, yaml-cpp 3.4), +# and CMake 4.x removed that compatibility -- so building either from source with +# CMake 4 fails at configure time with "Compatibility with CMake < 3.5 has been +# removed". The revisions below are the ConanCenter ones that set +# `CMAKE_POLICY_VERSION_MINIMUM=3.5` as a cache variable, which is the only +# placement that works (line 1 runs before the toolchain file is loaded). +# +# Pinned rather than floating so a `conan install` cannot silently pick up a +# revision that regresses this. To move them, check the candidate revision with: +# conan list "/#*" -r=conancenter +# grep POLICY_VERSION_MINIMUM "$(conan cache path /#)/conanfile.py" + +[requires] +fftw/3.3.10#3b4cceb8283a56cac30946c1b6f57bf8 +yaml-cpp/0.8.0#1aa371213f0307605d88ad5445581aff + +[generators] +CMakeDeps +CMakeToolchain +MakeDeps diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 62a2bb8..8a48ed5 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -7,6 +7,32 @@ add_definitions(-DYAML_CPP_STATIC_DEFINE) set(CMAKE_CXX_STANDARD 17) set(CMAKE_POSITION_INDEPENDENT_CODE ON) +# Pin the macOS deployment target, matching maxmsp/CMakeLists.txt's approach. +# +# Without this the core compiles against the host SDK while the wrappers link at +# a much older floor -- measured before the fix: libzerr_core.a at `minos 15.0` +# inside a zerr_features~.pd_darwin declaring `minos 11.0`. Nothing in the core +# uses availability-gated libc++ today (no , , ), +# so that was latent rather than broken -- but at a 15.0 target the compiler would +# silently accept a newer-only API and ship an external that fails on older macs. +# +# 10.13 is the project's single declared floor: it is what maxmsp/CMakeLists.txt +# pins and what the PureData macOS CI job passes as -mmacosx-version-min. Do not +# raise it here in isolation -- on arm64 clang clamps any lower value up to 11.0 +# (arm64's own floor), so 10.13 and 11.0 are indistinguishable on Apple Silicon, +# but on an Intel or fat build a core pinned to 11.0 against wrappers at 10.13 +# reintroduces the very object-version mismatch this is meant to remove, just in +# the opposite direction. One floor, stated in each place that needs it. +# +# Deliberately set here rather than via `os.version` in the Conan profile: +# os.version is part of the package_id, so pinning it there means no ConanCenter +# prebuilt ever matches and every dependency is rebuilt from source, in CI too. +# See profiles/macos and docs/design/repo-audit-2026-07-30.md. +if(APPLE AND NOT CMAKE_OSX_DEPLOYMENT_TARGET) + set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "macOS deployment target" FORCE) + message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET set to ${CMAKE_OSX_DEPLOYMENT_TARGET}") +endif() + # find packages find_package(FFTW3 REQUIRED) find_package(yaml-cpp REQUIRED) diff --git a/core/conanfile.txt b/core/conanfile.txt deleted file mode 100644 index f82b7b7..0000000 --- a/core/conanfile.txt +++ /dev/null @@ -1,7 +0,0 @@ -[requires] -fftw/3.3.10 -yaml-cpp/0.8.0 - -[generators] -CMakeDeps -CMakeToolchain \ No newline at end of file diff --git a/core/include/features/centroid.h b/core/include/features/centroid.h index e1f1431..1d64448 100644 --- a/core/include/features/centroid.h +++ b/core/include/features/centroid.h @@ -6,85 +6,65 @@ #include "linearinterpolator.h" #include "utils.h" -namespace zerr -{ - namespace feature - { - /** - * @brief Spectral Centroid algorithm - Calculates the center of mass of the spectrum - * - * The spectral centroid represents the "brightness" of a sound by indicating where - * the center of mass of the spectrum is located. Higher centroid values generally - * correspond to brighter sounds with more high frequency content. - */ - class Centroid : public FeatureExtractor - { - public: - static const std::string name; ///< Name identifier for this feature - static const std::string category; ///< Category this feature belongs to - static const std::string description; ///< Description of what this feature measures +namespace zerr { +namespace feature { +/** + * @brief Spectral Centroid algorithm - Calculates the center of mass of the spectrum + * + * Category: Frequency-Domain + * + * The spectral centroid is a measure used in digital signal processing to characterise + * a spectrum. It represents the "brightness" of a sound by indicating where the center + * of mass of the spectrum is located. Higher centroid values generally correspond to + * brighter sounds with more high frequency content. + */ +class Centroid : public FeatureExtractor { + public: + Centroid(); - /** - * @brief Get the name identifier of this feature - * @return std::string The feature name - */ - std::string get_name() { return name; } + /** + * @brief Initialize the centroid extractor with system configurations + * @param sys_cfg System configuration parameters + */ + void initialize(SystemConfigs sys_cfg); - /** - * @brief Get the category this feature belongs to - * @return std::string The feature category - */ - std::string get_category() { return category; } + /** + * @brief Extract the spectral centroid from the current audio frame + */ + void extract(); - /** - * @brief Get the description of what this feature measures - * @return std::string The feature description - */ - std::string get_description() { return description; } + /** + * @brief Reset the centroid extractor state + */ + void reset(); - /** - * @brief Initialize the centroid extractor with system configurations - * @param sys_cfg System configuration parameters - */ - void initialize(SystemConfigs sys_cfg); + /** + * @brief Load new audio input data for processing + * @param in Audio input data + */ + void fetch(const AudioInputs& in); - /** - * @brief Extract the spectral centroid from the current audio frame - */ - void extract(); + /** + * @brief Get the calculated centroid values + * @return FeatureVals The extracted centroid values + */ + FeatureVals send(); + // FeatureVals perform(AudioInputs x); - /** - * @brief Reset the centroid extractor state - */ - void reset(); + private: + /** + * @brief Reset internal parameters to initial state + */ + void _reset_param(); - /** - * @brief Load new audio input data for processing - * @param in Audio input data - */ - void fetch(AudioInputs in); + FeatureVal prv_y; ///< Previous centroid value + FeatureVal crr_y; ///< Current centroid value - /** - * @brief Get the calculated centroid values - * @return FeatureVals The extracted centroid values - */ - FeatureVals send(); - // FeatureVals perform(AudioInputs x); + double freq_max; ///< Maximum frequency considered for centroid calculation - private: - /** - * @brief Reset internal parameters to initial state - */ - void _reset_param(); + LinearInterpolator linear_interpolator; ///< Interpolator for smoothing centroid values +}; - FeatureVal prv_y; ///< Previous centroid value - FeatureVal crr_y; ///< Current centroid value - - double freq_max; ///< Maximum frequency considered for centroid calculation - - LinearInterpolator linear_interpolator; ///< Interpolator for smoothing centroid values - }; - - } // namespace feature +} // namespace feature } // namespace zerr #endif // CENTROID_H \ No newline at end of file diff --git a/core/include/features/crestfactor.h b/core/include/features/crestfactor.h index b5b4f24..ef88b43 100644 --- a/core/include/features/crestfactor.h +++ b/core/include/features/crestfactor.h @@ -6,84 +6,64 @@ #include "linearinterpolator.h" #include "utils.h" -namespace zerr -{ - namespace feature - { - /** - * @brief Crest Factor algorithm - Calculates the ratio of peak amplitude to RMS value - * - * The crest factor, also known as peak-to-average ratio (PAR), measures the waveform's - * dynamic range by comparing its peak values to the RMS level. Higher crest factors - * indicate more peaky signals with greater dynamic range, while lower values suggest - * more consistent amplitude levels. - */ - class CrestFactor : public FeatureExtractor - { - public: - static const std::string name; ///< Name identifier for this feature - static const std::string category; ///< Category this feature belongs to - static const std::string description; ///< Description of what this feature measures +namespace zerr { +namespace feature { +/** + * @brief Crest Factor algorithm - Calculates the ratio of peak amplitude to RMS value + * + * Category: Time-Domain + * + * Crest Factor is a parameter used in signal processing and audio engineering to describe + * the characteristics of a waveform. It is defined as the ratio of the peak value of a + * waveform to its RMS (Root Mean Square) value. Higher crest factors indicate more peaky + * signals with greater dynamic range, while lower values suggest more consistent amplitude + * levels. Also known as peak-to-average ratio (PAR). + */ +class CrestFactor : public FeatureExtractor { + public: + CrestFactor(); - /** - * @brief Get the name identifier of this feature - * @return std::string The feature name - */ - std::string get_name() { return name; } + /** + * @brief Initialize the crest factor extractor with system configurations + * @param sys_cfg System configuration parameters + */ + void initialize(SystemConfigs sys_cfg); - /** - * @brief Get the category this feature belongs to - * @return std::string The feature category - */ - std::string get_category() { return category; } + /** + * @brief Extract the crest factor from the current audio frame + */ + void extract(); - /** - * @brief Get the description of what this feature measures - * @return std::string The feature description - */ - std::string get_description() { return description; } + /** + * @brief Reset the crest factor extractor state + */ + void reset(); - /** - * @brief Initialize the crest factor extractor with system configurations - * @param sys_cfg System configuration parameters - */ - void initialize(SystemConfigs sys_cfg); + /** + * @brief Load new audio input data for processing + * @param in Audio input data + */ + void fetch(const AudioInputs& in); - /** - * @brief Extract the crest factor from the current audio frame - */ - void extract(); + /** + * @brief Get the calculated crest factor values + * @return FeatureVals The extracted crest factor values + */ + FeatureVals send(); + // FeatureVals perform(AudioInputs x); - /** - * @brief Reset the crest factor extractor state - */ - void reset(); + private: + /** + * @brief Reset internal parameters to initial state + */ + void _reset_param(); - /** - * @brief Load new audio input data for processing - * @param in Audio input data - */ - void fetch(AudioInputs in); + FeatureVal prv_y; ///< Previous crest factor value + FeatureVal crr_y; ///< Current crest factor value - /** - * @brief Get the calculated crest factor values - * @return FeatureVals The extracted crest factor values - */ - FeatureVals send(); - // FeatureVals perform(AudioInputs x); + LinearInterpolator linear_interpolator; ///< Interpolator for smoothing crest factor values +}; - private: - /** - * @brief Reset internal parameters to initial state - */ - void _reset_param(); - - FeatureVal prv_y; ///< Previous crest factor value - FeatureVal crr_y; ///< Current crest factor value - - LinearInterpolator linear_interpolator; ///< Interpolator for smoothing crest factor values - }; - - } // namespace feature +} // namespace feature } // namespace zerr #endif // CREST_H diff --git a/core/include/features/flatness.h b/core/include/features/flatness.h index 6c7e7fa..c619a8c 100644 --- a/core/include/features/flatness.h +++ b/core/include/features/flatness.h @@ -6,84 +6,66 @@ #include "linearinterpolator.h" #include "utils.h" -namespace zerr -{ - namespace feature - { - /** - * @brief Spectral Flatness algorithm - Calculates the ratio of geometric to arithmetic mean of spectrum - * - * The spectral flatness measures how noise-like or tone-like a sound is by comparing the - * geometric mean to the arithmetic mean of the spectrum. Values closer to 1 indicate more - * noise-like signals with energy spread across frequencies, while values closer to 0 - * suggest more tonal sounds with energy concentrated at specific frequencies. - */ - class Flatness : public FeatureExtractor - { - public: - static const std::string name; ///< Name identifier for this feature - static const std::string category; ///< Category this feature belongs to - static const std::string description; ///< Description of what this feature measures +namespace zerr { +namespace feature { +/** + * @brief Spectral Flatness algorithm - Calculates the ratio of geometric to arithmetic mean of + * spectrum + * + * Category: Frequency-Domain + * + * Spectral flatness, also known as Wiener entropy, is a measure used in digital signal + * processing to characterize an audio spectrum. It is typically used to quantify how + * noise-like a signal is, as opposed to being tonal. A higher value of spectral flatness + * indicates a more noise-like signal, whereas a lower value indicates a more tonal signal. + * Values closer to 1 indicate energy spread across frequencies, while values closer to 0 + * suggest energy concentrated at specific frequencies. + */ +class Flatness : public FeatureExtractor { + public: + Flatness(); - /** - * @brief Get the name identifier of this feature - * @return std::string The feature name - */ - std::string get_name() { return name; } + /** + * @brief Initialize the flatness extractor with system configurations + * @param sys_cfg System configuration parameters + */ + void initialize(SystemConfigs sys_cfg); - /** - * @brief Get the category this feature belongs to - * @return std::string The feature category - */ - std::string get_category() { return category; } + /** + * @brief Extract the spectral flatness from the current audio frame + */ + void extract(); - /** - * @brief Get the description of what this feature measures - * @return std::string The feature description - */ - std::string get_description() { return description; } + /** + * @brief Reset the flatness extractor state + */ + void reset(); - /** - * @brief Initialize the flatness extractor with system configurations - * @param sys_cfg System configuration parameters - */ - void initialize(SystemConfigs sys_cfg); + /** + * @brief Load new audio input data for processing + * @param in Audio input data + */ + void fetch(const AudioInputs& in); - /** - * @brief Extract the spectral flatness from the current audio frame - */ - void extract(); + /** + * @brief Get the calculated flatness values + * @return FeatureVals The extracted flatness values + */ + FeatureVals send(); + // FeatureVals perform(AudioInputs x); - /** - * @brief Reset the flatness extractor state - */ - void reset(); + private: + /** + * @brief Reset internal parameters to initial state + */ + void _reset_param(); - /** - * @brief Load new audio input data for processing - * @param in Audio input data - */ - void fetch(AudioInputs in); + FeatureVal prv_y; ///< Previous flatness value + FeatureVal crr_y; ///< Current flatness value - /** - * @brief Get the calculated flatness values - * @return FeatureVals The extracted flatness values - */ - FeatureVals send(); - // FeatureVals perform(AudioInputs x); + LinearInterpolator linear_interpolator; ///< Interpolator for smoothing flatness values +}; - private: - /** - * @brief Reset internal parameters to initial state - */ - void _reset_param(); - - FeatureVal prv_y; ///< Previous flatness value - FeatureVal crr_y; ///< Current flatness value - - LinearInterpolator linear_interpolator; ///< Interpolator for smoothing flatness values - }; - - } // namespace feature +} // namespace feature } // namespace zerr #endif // FLATNESS_H diff --git a/core/include/features/flux.h b/core/include/features/flux.h index 3c95e81..fca500f 100644 --- a/core/include/features/flux.h +++ b/core/include/features/flux.h @@ -1,44 +1,27 @@ #ifndef FLUX_H #define FLUX_H -#include "utils.h" #include "configs.h" #include "featureextractor.h" #include "linearinterpolator.h" +#include "utils.h" namespace zerr { -namespace feature{ +namespace feature { /** * @brief Spectral Flux algorithm - Calculates frame-to-frame changes in the spectrum * - * The spectral flux measures how quickly the spectrum is changing between successive frames - * by calculating the difference between consecutive spectral frames. Higher flux values - * indicate more rapid spectral changes, while lower values suggest more steady spectral - * content. This can help detect note onsets and transitions in audio signals. + * Category: Frequency-Domain + * + * Spectral flux is a measure used in digital signal processing that quantifies how quickly + * the power spectrum of a signal changes. It is calculated as the difference between + * consecutive spectral frames. Higher flux values indicate more rapid spectral changes, + * while lower values suggest more steady spectral content. It is often used in audio + * analysis for onset detection and other applications. */ class Flux : public FeatureExtractor { -public: - static const std::string name; ///< Name identifier for this feature - static const std::string category; ///< Category this feature belongs to - static const std::string description; ///< Description of what this feature measures - - /** - * @brief Get the name identifier of this feature - * @return std::string The feature name - */ - std::string get_name(){return name;} - - /** - * @brief Get the category this feature belongs to - * @return std::string The feature category - */ - std::string get_category(){return category;} - - /** - * @brief Get the description of what this feature measures - * @return std::string The feature description - */ - std::string get_description(){return description;} + public: + Flux(); /** * @brief Initialize the flux extractor with system configurations @@ -60,7 +43,7 @@ class Flux : public FeatureExtractor { * @brief Load new audio input data for processing * @param in Audio input data */ - void fetch(AudioInputs in); + void fetch(const AudioInputs& in); /** * @brief Get the calculated flux values @@ -69,19 +52,19 @@ class Flux : public FeatureExtractor { FeatureVals send(); // FeatureVals perform(AudioInputs x); -private: + private: /** * @brief Reset internal parameters to initial state */ void _reset_param(); - Samples prv_x; ///< Previous spectral frame - FeatureVal prv_y; ///< Previous flux value - FeatureVal crr_y; ///< Current flux value + Samples prv_x; ///< Previous spectral frame + FeatureVal prv_y; ///< Previous flux value + FeatureVal crr_y; ///< Current flux value LinearInterpolator linear_interpolator; ///< Interpolator for smoothing flux values }; -} //namespace feature -} //namespace zerr +} // namespace feature +} // namespace zerr #endif // FLUX_H \ No newline at end of file diff --git a/core/include/features/rolloff.h b/core/include/features/rolloff.h index 2378429..e4a9f2d 100644 --- a/core/include/features/rolloff.h +++ b/core/include/features/rolloff.h @@ -15,88 +15,69 @@ #include "linearinterpolator.h" #include "utils.h" -namespace zerr -{ - namespace feature - { - /** - * @brief Spectral Rolloff algorithm - Calculates frequency threshold containing N% of spectral energy - * - * The spectral rolloff is a measure used in signal processing to determine the - * frequency below which a specified percentage of the total spectral energy - * lies. It is often used to distinguish between harmonic and non-harmonic - * content in an audio signal. Higher rolloff frequencies indicate more high-frequency - * content, while lower values suggest more energy concentrated in lower frequencies. - */ - class Rolloff : public FeatureExtractor - { - public: - static const std::string name; ///< Name identifier for this feature - static const std::string category; ///< Category this feature belongs to - static const std::string description; ///< Description of what this feature measures - - /** - * @brief Get the name identifier of this feature - * @return std::string The feature name - */ - std::string get_name() { return name; } - - /** - * @brief Get the category this feature belongs to - * @return std::string The feature category - */ - std::string get_category() { return category; } - - /** - * @brief Get the description of what this feature measures - * @return std::string The feature description - */ - std::string get_description() { return description; } - - /** - * @brief Initialize the rolloff extractor with system configurations - * @param sys_cfg System configuration parameters - */ - void initialize(SystemConfigs sys_cfg); - - /** - * @brief Extract the spectral rolloff from the current audio frame - */ - void extract(); - - /** - * @brief Reset the rolloff extractor state - */ - void reset(); - - /** - * @brief Load new audio input data for processing - * @param in Audio input data - */ - void fetch(AudioInputs in); - - /** - * @brief Get the calculated rolloff values - * @return FeatureVals The extracted rolloff values - */ - FeatureVals send(); - // FeatureVals perform(AudioInputs x); - - private: - /** - * @brief Reset internal parameters to initial state - */ - void _reset_param(); - - FeatureVal prv_y; ///< Previous rolloff value - FeatureVal crr_y; ///< Current rolloff value - - double freq_max; ///< Maximum frequency considered for rolloff calculation - double rolloffPercent = 0.85; ///< Percentage threshold for spectral energy accumulation - - LinearInterpolator linear_interpolator; ///< Interpolator for smoothing rolloff values - }; - - } // namespace feature +namespace zerr { +namespace feature { +/** + * @brief Spectral Rolloff algorithm - Calculates frequency threshold containing N% of spectral + * energy + * + * Category: Frequency-Domain + * + * The spectral rolloff is a measure used in signal processing to determine the frequency + * below which a specified percentage of the total spectral energy lies. It is often used + * to distinguish between harmonic and non-harmonic content in an audio signal. Higher + * rolloff frequencies indicate more high-frequency content, while lower values suggest + * more energy concentrated in lower frequencies. + */ +class Rolloff : public FeatureExtractor { + public: + Rolloff(); + + /** + * @brief Initialize the rolloff extractor with system configurations + * @param sys_cfg System configuration parameters + */ + void initialize(SystemConfigs sys_cfg); + + /** + * @brief Extract the spectral rolloff from the current audio frame + */ + void extract(); + + /** + * @brief Reset the rolloff extractor state + */ + void reset(); + + /** + * @brief Load new audio input data for processing + * @param in Audio input data + */ + void fetch(const AudioInputs& in); + + /** + * @brief Get the calculated rolloff values + * @return FeatureVals The extracted rolloff values + */ + FeatureVals send(); + // FeatureVals perform(AudioInputs x); + + private: + /** + * @brief Reset internal parameters to initial state + */ + void _reset_param(); + + FeatureVal prv_y; ///< Previous rolloff value + FeatureVal crr_y; ///< Current rolloff value + + double freq_max; ///< Maximum frequency considered for rolloff calculation + double rolloffPercent = + DEFAULT_ROLLOFF_PERCENT; ///< Percentage threshold for spectral energy accumulation + + LinearInterpolator linear_interpolator; ///< Interpolator for smoothing rolloff values +}; + +} // namespace feature } // namespace zerr #endif // ROLLOFF_H diff --git a/core/include/features/rootmeansquare.h b/core/include/features/rootmeansquare.h index 5bfa9e4..14f18f2 100644 --- a/core/include/features/rootmeansquare.h +++ b/core/include/features/rootmeansquare.h @@ -15,89 +15,66 @@ #include "linearinterpolator.h" #include "utils.h" -namespace zerr -{ - namespace feature - { - /** - * @brief Root Mean Square algorithm - Calculates the quadratic mean amplitude of a signal - * - * The RMS value of a set of values (or a continuous-time waveform) is - * the square root of the arithmetic mean of the squares of the values, - * or the square of the function that defines the continuous waveform. - * RMS is commonly used as a measure of signal power or amplitude level, - * providing a more meaningful average than simple arithmetic mean for - * oscillating signals. - * - * https://en.wikipedia.org/wiki/Root_mean_square - */ - class RootMeanSquare : public FeatureExtractor - { - public: - static const std::string name; ///< Name identifier for this feature - static const std::string category; ///< Category this feature belongs to - static const std::string description; ///< Description of what this feature measures - - /** - * @brief Get the name identifier of this feature - * @return std::string The feature name - */ - std::string get_name() { return name; } - - /** - * @brief Get the category this feature belongs to - * @return std::string The feature category - */ - std::string get_category() { return category; } - - /** - * @brief Get the description of what this feature measures - * @return std::string The feature description - */ - std::string get_description() { return description; } - - /** - * @brief Initialize the RMS extractor with system configurations - * @param sys_cfg System configuration parameters - */ - void initialize(SystemConfigs sys_cfg); - - /** - * @brief Extract the RMS value from the current audio frame - */ - void extract(); - - /** - * @brief Reset the RMS extractor state - */ - void reset(); - - /** - * @brief Load new audio input data for processing - * @param in Audio input data - */ - void fetch(AudioInputs in); - - /** - * @brief Get the calculated RMS values - * @return FeatureVals The extracted RMS values - */ - FeatureVals send(); - // FeatureVals perform(AudioInputs x); - - private: - /** - * @brief Reset internal parameters to initial state - */ - void _reset_param(); - - FeatureVal prv_y; ///< Previous RMS value - FeatureVal crr_y; ///< Current RMS value - - LinearInterpolator linear_interpolator; ///< Interpolator for smoothing RMS values - }; - - } // namespace feature +namespace zerr { +namespace feature { +/** + * @brief Root Mean Square algorithm - Calculates the quadratic mean amplitude of a signal + * + * Category: Time-Domain + * + * The RMS is defined as the square root of the mean over time of the square of the + * vertical distance of the graph from the rest state. RMS is commonly used as a measure + * of signal power or amplitude level, providing a more meaningful average than simple + * arithmetic mean for oscillating signals. + * + * https://en.wikipedia.org/wiki/Root_mean_square + */ +class RootMeanSquare : public FeatureExtractor { + public: + RootMeanSquare(); + + /** + * @brief Initialize the RMS extractor with system configurations + * @param sys_cfg System configuration parameters + */ + void initialize(SystemConfigs sys_cfg); + + /** + * @brief Extract the RMS value from the current audio frame + */ + void extract(); + + /** + * @brief Reset the RMS extractor state + */ + void reset(); + + /** + * @brief Load new audio input data for processing + * @param in Audio input data + */ + void fetch(const AudioInputs& in); + + /** + * @brief Get the calculated RMS values + * @return FeatureVals The extracted RMS values + */ + FeatureVals send(); + // FeatureVals perform(AudioInputs x); + + private: + /** + * @brief Reset internal parameters to initial state + */ + void _reset_param(); + + FeatureVal prv_y; ///< Previous RMS value + FeatureVal crr_y; ///< Current RMS value + + LinearInterpolator linear_interpolator; ///< Interpolator for smoothing RMS values +}; + +} // namespace feature } // namespace zerr #endif // ROOT_MEAN_SQUARE \ No newline at end of file diff --git a/core/include/features/zerocrossingrate.h b/core/include/features/zerocrossingrate.h index 72450fa..a0f012a 100644 --- a/core/include/features/zerocrossingrate.h +++ b/core/include/features/zerocrossingrate.h @@ -6,87 +6,68 @@ #include "linearinterpolator.h" #include "utils.h" -namespace zerr -{ - namespace feature - { - /** - * @brief Zero Crossing Rate algorithm - Calculates how often a signal crosses the zero level - * - * The zero crossing rate measures the number of times an audio signal changes sign - * (crosses zero amplitude) within a given frame, divided by the frame length. Higher - * rates indicate more high frequency content or noise-like signals, while lower rates - * suggest more low frequency or tonal content. This feature is useful for distinguishing - * between voiced and unvoiced speech segments or detecting percussive sounds. - */ - class ZeroCrossingRate : public FeatureExtractor - { - public: - static const std::string name; ///< Name identifier for this feature - static const std::string category; ///< Category this feature belongs to - static const std::string description; ///< Description of what this feature measures +namespace zerr { +namespace feature { +/** + * @brief Zero Crossing Rate algorithm - Calculates how often a signal crosses the zero level + * + * Category: Time-Domain + * + * The zero crossing rate (ZCR) is a measure of how frequently a signal changes its sign. + * It represents the rate at which the signal crosses the zero amplitude level over a given + * time period, normalized by the frame length. Higher rates indicate more high frequency + * content or noise-like signals, while lower rates suggest more low frequency or tonal + * content. This feature is useful for distinguishing between voiced and unvoiced speech + * segments or detecting percussive sounds. + */ +class ZeroCrossingRate : public FeatureExtractor { + public: + ZeroCrossingRate(); - /** - * @brief Get the name identifier of this feature - * @return std::string The feature name - */ - std::string get_name() { return name; } + /** + * @brief Initialize the zero crossing rate extractor with system configurations + * @param sys_cfg System configuration parameters + */ + void initialize(SystemConfigs sys_cfg); - /** - * @brief Get the category this feature belongs to - * @return std::string The feature category - */ - std::string get_category() { return category; } + /** + * @brief Extract the zero crossing rate from the current audio frame + */ + void extract(); - /** - * @brief Get the description of what this feature measures - * @return std::string The feature description - */ - std::string get_description() { return description; } + /** + * @brief Reset the zero crossing rate extractor state + */ + void reset(); - /** - * @brief Initialize the zero crossing rate extractor with system configurations - * @param sys_cfg System configuration parameters - */ - void initialize(SystemConfigs sys_cfg); + /** + * @brief Load new audio input data for processing + * @param in Audio input data + */ + void fetch(const AudioInputs& in); - /** - * @brief Extract the zero crossing rate from the current audio frame - */ - void extract(); + /** + * @brief Get the calculated zero crossing rate values + * @return FeatureVals The extracted zero crossing rate values + */ + FeatureVals send(); + // FeatureVals perform(AudioInputs x); - /** - * @brief Reset the zero crossing rate extractor state - */ - void reset(); + private: + /** + * @brief Reset internal parameters to initial state + */ + void _reset_param(); - /** - * @brief Load new audio input data for processing - * @param in Audio input data - */ - void fetch(AudioInputs in); + FeatureVal prv_y; ///< Previous zero crossing rate value + FeatureVal crr_y; ///< Current zero crossing rate value + // params + // int zero_crossings; - /** - * @brief Get the calculated zero crossing rate values - * @return FeatureVals The extracted zero crossing rate values - */ - FeatureVals send(); - // FeatureVals perform(AudioInputs x); + LinearInterpolator + linear_interpolator; ///< Interpolator for smoothing zero crossing rate values +}; - private: - /** - * @brief Reset internal parameters to initial state - */ - void _reset_param(); - - FeatureVal prv_y; ///< Previous zero crossing rate value - FeatureVal crr_y; ///< Current zero crossing rate value - // params - // int zero_crossings; - - LinearInterpolator linear_interpolator; ///< Interpolator for smoothing zero crossing rate values - }; - - } // namespace feature +} // namespace feature } // namespace zerr #endif // ZEROCROSSINGRATE_H \ No newline at end of file diff --git a/core/include/features/zerocrossings.h b/core/include/features/zerocrossings.h index 02501d4..6c9352d 100644 --- a/core/include/features/zerocrossings.h +++ b/core/include/features/zerocrossings.h @@ -5,91 +5,70 @@ #include "featureextractor.h" #include "utils.h" -namespace zerr -{ - namespace feature - { - /** - * @brief Zero Crossings algorithm - Counts the number of times a signal crosses the zero level - * - * The zero crossings feature counts the total number of times an audio signal crosses - * the zero amplitude level within a frame. Unlike zero crossing rate, this provides - * the absolute count rather than a rate normalized by frame length. Higher counts - * indicate more high frequency content or noise-like signals, while lower counts - * suggest more low frequency or tonal content. This can help characterize signal - * periodicity and frequency content. - */ - class ZeroCrossings : public FeatureExtractor - { - public: - static const std::string name; ///< Name identifier for this feature - static const std::string category; ///< Category this feature belongs to - static const std::string description; ///< Description of what this feature measures +namespace zerr { +namespace feature { +/** + * @brief Zero Crossings algorithm - Counts the number of times a signal crosses the zero level + * + * Category: Sample-Level + * + * Zero crossing is used to describe the point at which a signal changes its sign from + * positive to negative or from negative to positive. Unlike zero crossing rate, this + * provides the absolute count rather than a rate normalized by frame length. Higher counts + * indicate more high frequency content or noise-like signals, while lower counts suggest + * more low frequency or tonal content. This can help characterize signal periodicity and + * frequency content. + */ +class ZeroCrossings : public FeatureExtractor { + public: + ZeroCrossings(); - /** - * @brief Get the name identifier of this feature - * @return std::string The feature name - */ - std::string get_name() { return name; } + /** + * @brief Initialize the zero crossings extractor with system configurations + * @param sys_cfg System configuration parameters + */ + void initialize(SystemConfigs sys_cfg); - /** - * @brief Get the category this feature belongs to - * @return std::string The feature category - */ - std::string get_category() { return category; } + /** + * @brief Extract the zero crossings count from the current audio frame + */ + void extract(); - /** - * @brief Get the description of what this feature measures - * @return std::string The feature description - */ - std::string get_description() { return description; } + /** + * @brief Reset the zero crossings extractor state + */ + void reset(); - /** - * @brief Initialize the zero crossings extractor with system configurations - * @param sys_cfg System configuration parameters - */ - void initialize(SystemConfigs sys_cfg); + /** + * @brief Load new audio input data for processing + * @param in Audio input data + */ + void fetch(const AudioInputs& in); - /** - * @brief Extract the zero crossings count from the current audio frame - */ - void extract(); + /** + * @brief Get the calculated zero crossings values + * @return FeatureVals The extracted zero crossings counts + */ + FeatureVals send(); + // FeatureVals perform(AudioInputs x); - /** - * @brief Reset the zero crossings extractor state - */ - void reset(); + private: + /** + * @brief Reset internal parameters to initial state + */ + void _reset_param(); - /** - * @brief Load new audio input data for processing - * @param in Audio input data - */ - void fetch(AudioInputs in); + /** + * @brief Check if there is a zero crossing between two samples + * @param first First sample value + * @param second Second sample value + * @return bool True if there is a zero crossing, false otherwise + */ + bool _isZeroCrossing(Sample first, Sample second); - /** - * @brief Get the calculated zero crossings values - * @return FeatureVals The extracted zero crossings counts - */ - FeatureVals send(); - // FeatureVals perform(AudioInputs x); + Sample last_sample; ///< Last sample from previous frame for zero crossing detection +}; - private: - /** - * @brief Reset internal parameters to initial state - */ - void _reset_param(); - - /** - * @brief Check if there is a zero crossing between two samples - * @param first First sample value - * @param second Second sample value - * @return bool True if there is a zero crossing, false otherwise - */ - bool _isZeroCrossing(Sample first, Sample second); - - Sample last_sample; ///< Last sample from previous frame for zero crossing detection - }; - - } // namespace feature +} // namespace feature } // namespace zerr #endif // ZEROCROSSINGS_H diff --git a/core/include/modules/audiodisperser.h b/core/include/modules/audiodisperser.h index ff1e167..42a52f6 100644 --- a/core/include/modules/audiodisperser.h +++ b/core/include/modules/audiodisperser.h @@ -9,9 +9,9 @@ #ifndef AUDIODISPERSER_H #define AUDIODISPERSER_H -#include #include "logger.h" #include "types.h" +#include namespace zerr { /** @@ -43,13 +43,13 @@ class AudioDisperser { * @brief Get the block size used by the audio disperser * @return The block size from system configuration */ - int get_block_size() { return systemCfgs.block_size; } + int get_block_size() const noexcept { return systemCfgs.block_size; } private: int numChannel; /**< Number of audio channels for dispersal */ SystemConfigs systemCfgs; /**< system configuration: sample_rate, block_size */ std::string combinationMode; /**< Mode for combining audio signals */ - Logger* logger; /**< Logger instance for debug/error messages */ + Logger logger; /**< Logger instance for debug/error messages */ Blocks inputBuffer; /**< Buffer for storing input audio blocks */ Blocks outputBuffer; /**< Buffer for storing processed output blocks */ }; diff --git a/core/include/modules/envelopecombinator.h b/core/include/modules/envelopecombinator.h index 4d6ed1a..7d04cdb 100644 --- a/core/include/modules/envelopecombinator.h +++ b/core/include/modules/envelopecombinator.h @@ -9,10 +9,10 @@ #ifndef ENVELOPECOMBINATOR_H #define ENVELOPECOMBINATOR_H -#include #include "logger.h" #include "types.h" #include "utils.h" +#include namespace zerr { /** @@ -41,20 +41,17 @@ class EnvelopeCombinator { * @brief Get the block size used by the envelope combinator * @return The block size from system configuration */ - int get_block_size() { return systemCfgs.block_size; } + int get_block_size() const noexcept { return systemCfgs.block_size; } /** * @brief Process input envelope blocks and combine using selected mode * @param in Input envelope blocks to process * @return Combined output envelope blocks */ Blocks perform(Blocks in); - /** - * @brief Destructor for the Envelope Combinator - */ - ~EnvelopeCombinator(); + ~EnvelopeCombinator() = default; private: - typedef void (EnvelopeCombinator::*ProcessFunction)(); + using ProcessFunction = void (EnvelopeCombinator::*)(); ProcessFunction processFunc; int numSource; /**< Number of envelope sources to combine */ @@ -62,7 +59,7 @@ class EnvelopeCombinator { zerr::SystemConfigs systemCfgs; /**< system configuration: sample_rate, block_size */ std::string combMode; /**< Mode for combining envelopes */ - Logger* logger; /**< Logger instance for debug/error messages */ + Logger logger; /**< Logger instance for debug/error messages */ Blocks inputBuffer; /**< Buffer for storing input envelope blocks */ Blocks outputBuffer; /**< Buffer for storing combined output blocks */ diff --git a/core/include/modules/envelopegenerator.h b/core/include/modules/envelopegenerator.h index f7cea8d..c3d5857 100644 --- a/core/include/modules/envelopegenerator.h +++ b/core/include/modules/envelopegenerator.h @@ -13,11 +13,12 @@ #ifndef CORE_ENVELOPEGENERATOR_H #define CORE_ENVELOPEGENERATOR_H -#include #include "logger.h" #include "onsetdetector.h" #include "speakermanager.h" #include "types.h" +#include +#include namespace zerr { @@ -32,10 +33,10 @@ namespace zerr { * envelope shaping. */ class EnvelopeGenerator { - public: + public: const int numInlet = 3; ///< Number of inlets: main(1), spread(2), volume(3). - int numOutlet; ///< Number of outlets, assigned according to the speaker - ///< configuration. + int numOutlet; ///< Number of outlets, assigned according to the speaker + ///< configuration. /** * @brief Constructor of EnvelopeGenerator setups the parameters for * initializing this Class. @@ -46,7 +47,7 @@ class EnvelopeGenerator { * @param selectionMode The strategy for generating envelope: trigger | * trajectory */ - EnvelopeGenerator(SystemConfigs systemCfgs, ConfigPath speakerCfgs, Mode genMode); + EnvelopeGenerator(SystemConfigs systemCfgs, ConfigPath speakerCfgs, GenMode genMode); /** * @brief Initialize the EnvelopeGenerator. * @@ -103,41 +104,36 @@ class EnvelopeGenerator { * @brief Resets all parameters to default values and reinitializes the generator */ void reset(); - /** - * @brief Destructor that cleans up allocated resources - */ - ~EnvelopeGenerator(); + ~EnvelopeGenerator() = default; /** * @brief xxxxx */ void setPrinter(Logger::PrintStrategy newPrinter); - - private: - Logger* logger; /**< Logger object for printing logs to all kinds of console */ - - SpeakerManager* speakerManager; /**< SpeakerManger object to access the speaker array information */ - - typedef void (EnvelopeGenerator::*ProcessFunction)(); - ProcessFunction processFunc; - + private: + // NOTE: declaration order matters — config members must precede objects that depend on them SystemConfigs systemCfgs; /**< System configurations: sample rate, block size etc. */ + ConfigPath speakerCfgs; /**< Path to the speaker array setup configuration file */ + GenMode genMode; /**< The strategy for generating envelope: trigger | + trajectory */ - ConfigPath speakerCfgs; /**< Path to the speaker array setup configuration file */ - Mode genMode; /**< The strategy for generating envelope: trigger | - trajectory */ - Mode triggerMode; /**< The strategy for choosing the next speaker to jump to - using trigger with topology */ + Logger logger; /**< Logger object for printing logs to all kinds of console */ + std::unique_ptr + speakerManager; /**< SpeakerManger object to access the speaker array information */ + std::unique_ptr + onsetDetector; /**< Detector for identifying onset triggers in the input signal */ - AudioBuffers inputBuffers; /**< multi-channel input buffer in the shape of - input channel number x block size */ - AudioBuffers outputBuffers; /**< multi-channel output buffer in the shape of - output channel number x block size */ + using ProcessFunction = void (EnvelopeGenerator::*)(); + ProcessFunction processFunc; - // SpeakerManager* speakerManager; /**< SpeakerManger object to access the speaker array information */ + TriggerMode triggerMode; /**< The strategy for choosing the next speaker to jump to + using trigger with topology */ - OnsetDetector* onsetDetector; /**< Detector for identifying onset triggers in the input signal */ + Blocks inputBuffers; /**< multi-channel input buffer in the shape of + input channel number x block size */ + Blocks outputBuffers; /**< multi-channel output buffer in the shape of + output channel number x block size */ std::map indexChannelLookup; /**< index to channel reverse lookup table */ /** diff --git a/core/include/modules/featurebank.h b/core/include/modules/featurebank.h index 9ac126e..73bde5a 100644 --- a/core/include/modules/featurebank.h +++ b/core/include/modules/featurebank.h @@ -21,10 +21,11 @@ namespace zerr { * then distributes the results to all activated feature extraction algorithms. */ class FeatureBank { - public: - using CreateFunc = std::unique_ptr (*)(); /**< Function pointer type for creating - FeatureExtractor objects */ - typedef std::unique_ptr fe_ptr; /**< The unique_ptr of type virtual class FeatureExtractor */ + public: + using CreateFunc = std::unique_ptr (*)(); /**< Function pointer type for + creating FeatureExtractor objects */ + using fe_ptr = std::unique_ptr; /**< The unique_ptr of type virtual class + FeatureExtractor */ /** * @brief FeatureBank Constructor * @@ -55,18 +56,21 @@ class FeatureBank { * @param in Input audio block to analyze * @return Map of feature names to extracted feature values */ - FeaturesVals perform(Block in); + FeaturesVals perform(Samples in); /** * @brief Reset the feature bank parameters and load a new set of features * @param feature_names New list of feature names to activate */ void reset(FeatureNames feature_names); - private: - std::map registed_features; /**< Map between feature names and their constructor functions */ + private: + std::map + registed_features; /**< Map between feature names and their constructor functions */ std::vector activated_features; /**< Vector of pointers to activated feature objects */ + FeatureNames active_feature_names; /**< Names of currently activated features */ + RingBuffer ring_buffer; /**< Ring buffer to hold previous audio samples for analysis */ FrequencyTransformer freq_transformer; /**< FFT wrapper to perform frequency analysis diff --git a/core/include/modules/featureextractor.h b/core/include/modules/featureextractor.h index 5059360..43a6bb3 100644 --- a/core/include/modules/featureextractor.h +++ b/core/include/modules/featureextractor.h @@ -3,82 +3,65 @@ #include "utils.h" -namespace zerr -{ +namespace zerr { +/** + * @brief Virtual base class that defines the interface for audio feature extractors + * + * This class provides a common interface that all feature extraction algorithms must implement. + * It handles initialization, processing, and data management for extracting audio features. + */ +class FeatureExtractor { + public: /** - * @brief Virtual base class that defines the interface for audio feature extractors + * @brief Virtual destructor for proper cleanup of derived classes + */ + virtual ~FeatureExtractor() {}; + /** + * @brief Initialize the feature extraction algorithm with system configuration + * @param sys_cfg System configuration containing sample rate, block size etc. + */ + virtual void initialize(SystemConfigs sys_cfg) = 0; + /** + * @brief Process input data and calculate feature values * - * This class provides a common interface that all feature extraction algorithms must implement. - * It handles initialization, processing, and data management for extracting audio features. + * Runs the feature extraction algorithm on the data in the input buffer + * and stores results in the output buffer + */ + virtual void extract() = 0; + /** + * @brief Reset the feature extractor to initial state + * + * Clears buffers and resets any accumulated state + */ + virtual void reset() = 0; + /** + * @brief Load new audio data into the input buffer + * @param x Input audio data structure containing time/frequency domain signals + */ + virtual void fetch(const AudioInputs& x) = 0; + /** + * @brief Retrieve the calculated feature values + * @return Map of feature names to their computed values + */ + virtual FeatureVals send() = 0; + /** + * @brief Check if the feature extractor is properly initialized + * @return True if initialized, false otherwise + */ + bool is_initialized() const noexcept { return initialized; } + /** + * @brief Set the initialization status + * @param s New initialization status */ - class FeatureExtractor - { - public: - /** - * @brief Virtual destructor for proper cleanup of derived classes - */ - virtual ~FeatureExtractor() {}; - /** - * @brief Get basic information about the feature extractor - * @return Name of the feature extractor - */ - virtual std::string get_name() = 0; - /** - * @brief Get the category this feature belongs to - * @return Category name (e.g. "spectral", "temporal", etc.) - */ - virtual std::string get_category() = 0; - /** - * @brief Get detailed description of what this feature measures - * @return Description string - */ - virtual std::string get_description() = 0; - /** - * @brief Initialize the feature extraction algorithm with system configuration - * @param sys_cfg System configuration containing sample rate, block size etc. - */ - virtual void initialize(SystemConfigs sys_cfg) = 0; - /** - * @brief Process input data and calculate feature values - * - * Runs the feature extraction algorithm on the data in the input buffer - * and stores results in the output buffer - */ - virtual void extract() = 0; - /** - * @brief Reset the feature extractor to initial state - * - * Clears buffers and resets any accumulated state - */ - virtual void reset() = 0; - /** - * @brief Load new audio data into the input buffer - * @param x Input audio data structure containing time/frequency domain signals - */ - virtual void fetch(AudioInputs x) = 0; - /** - * @brief Retrieve the calculated feature values - * @return Map of feature names to their computed values - */ - virtual FeatureVals send() = 0; - /** - * @brief Check if the feature extractor is properly initialized - * @return True if initialized, false otherwise - */ - bool is_initialized() { return initialized; } - /** - * @brief Set the initialization status - * @param s New initialization status - */ - void set_initialize_statue(bool s) { initialized = s; } + void set_initialize_status(bool s) noexcept { initialized = s; } - protected: - Samples x; /**< Input data buffer containing time or frequency domain samples */ - FeatureVals y; /**< Output buffer containing extracted feature values */ + protected: + Samples x; /**< Input data buffer containing time or frequency domain samples */ + FeatureVals y; /**< Output buffer containing extracted feature values */ - SystemConfigs system_configs; /**< System configuration parameters */ - bool initialized = false; /**< Tracks whether the extractor is initialized */ - }; // Class FeatureExtractor + SystemConfigs system_configs; /**< System configuration parameters */ + bool initialized = false; /**< Tracks whether the extractor is initialized */ +}; // Class FeatureExtractor } // Namespace zerr #endif // FEATUREEXTRACTOR_H diff --git a/core/include/modules/speakermanager.h b/core/include/modules/speakermanager.h index f2724e4..43031c0 100644 --- a/core/include/modules/speakermanager.h +++ b/core/include/modules/speakermanager.h @@ -1,7 +1,8 @@ /** * @file speakermanager.h * @author Zeyu Yang (zeyuuyang42@gmail.com) - * @brief Speaker & SpeakerManager Classes Header - Contains class definitions for managing speaker configurations and behaviors + * @brief Speaker & SpeakerManager Classes Header - Contains class definitions for managing speaker + * configurations and behaviors * @date 2024-02-18 * * @copyright Copyright (c) 2023-2025 @@ -20,9 +21,9 @@ #define SPEAKERMANAGER_H #include +#include #include #include -#include #include "configs.h" #include "logger.h" @@ -43,7 +44,7 @@ namespace zerr { * accessing and displaying these properties. */ class Speaker { - public: + public: /** * @brief Construct a new Speaker object with specified configurations. * @param index Unique identification number of the speaker. @@ -57,55 +58,57 @@ class Speaker { * @brief Get the identification index of this speaker instance. * @return Index The unique identification number of the speaker. */ - Index getIndex() { return index; }; + Index getIndex() noexcept { return index; } /** * @brief Get the x coordinate in Cartesian system of this speaker instance. * @return Param The x coordinate value. */ - Param getX() { return position.cartesian.x; }; + Param getX() noexcept { return position.cartesian.x; } /** * @brief Get the y coordinate in Cartesian system of this speaker instance. * @return Param The y coordinate value. */ - Param getY() { return position.cartesian.y; }; + Param getY() noexcept { return position.cartesian.y; } /** * @brief Get the z coordinate in Cartesian system of this speaker instance. * @return Param The z coordinate value. */ - Param getZ() { return position.cartesian.z; }; + Param getZ() noexcept { return position.cartesian.z; } /** * @brief Get the azimuth in spherical system of this speaker instance. - * @return Param The azimuth value in degrees, where 0° is front center and positive values rotate clockwise. + * @return Param The azimuth value in degrees, where 0° is front center and positive values + * rotate clockwise. */ - Param getAzimuth() { return position.spherical.azimuth; }; + Param getAzimuth() noexcept { return position.spherical.azimuth; } /** * @brief Get the elevation in spherical system of this speaker instance. - * @return Param The elevation value in degrees, where 0° is horizontal and positive values point upward. + * @return Param The elevation value in degrees, where 0° is horizontal and positive values + * point upward. */ - Param getElevation() { return position.spherical.elevation; }; + Param getElevation() noexcept { return position.spherical.elevation; } /** * @brief Get the distance in spherical system of this speaker instance. * @return Param The distance value in meters from the origin to the speaker. */ - Param getDistance() { return position.spherical.distance; }; + Param getDistance() noexcept { return position.spherical.distance; } /** * @brief Get the yaw orientation of this speaker instance. * @return Param The yaw value in degrees, representing horizontal rotation of the speaker. */ - Param getYaw() { return orientation.yaw; }; + Param getYaw() noexcept { return orientation.yaw; } /** * @brief Get the pitch orientation of this speaker instance. * @return Param The pitch value in degrees, representing vertical tilt of the speaker. */ - Param getPitch() { return orientation.pitch; }; + Param getPitch() noexcept { return orientation.pitch; } /** * @brief Print all configurations of the speaker, including index, @@ -113,15 +116,15 @@ class Speaker { */ void printAll(); - private: - Logger* logger; ///< Logger object for logging messages and errors. - Index index; ///< Unique identification number of the speaker. - Position position; ///< Position of the speaker in both Cartesian and - ///< spherical coordinates. + private: + Logger logger; ///< Logger object for logging messages and errors. + Index index; ///< Unique identification number of the speaker. + Position position; ///< Position of the speaker in both Cartesian and + ///< spherical coordinates. Orientation orientation; ///< Orientation of the speaker in terms of yaw and pitch. - void _print_index(); ///< Internal method to print the speaker's index to console. - void _print_position(); ///< Internal method to print the speaker's position coordinates. + void _print_index(); ///< Internal method to print the speaker's index to console. + void _print_position(); ///< Internal method to print the speaker's position coordinates. void _print_orientation(); ///< Internal method to print the speaker's ///< orientation angles. }; @@ -139,7 +142,7 @@ class Speaker { * user-defined patterns. */ class SpeakerManager { - public: + public: /** * @brief Construct a new SpeakerManager object and set the configuration * file path. @@ -188,10 +191,10 @@ class SpeakerManager { * @brief Get a pair of speaker indexes based on the trajectory input value. * @param trajVal The trajectory input value between 0.0 and 1.0 used to determine speaker * selection along the defined trajectory. - * @return Pair A pair of adjacent speaker indexes selected based on the trajectory + * @return SpeakerPair A pair of adjacent speaker indexes selected based on the trajectory * value. */ - Pair getIndexesByTrajectory(Param trajVal); + SpeakerPair getIndexesByTrajectory(Param trajVal); /** * @brief Calculate the panning ratio based on the trajectory input value. @@ -209,10 +212,10 @@ class SpeakerManager { * @param pos The target position coordinates. * @param mask A boolean vector to include (true) or exclude (false) specific speakers. * @param coordinate The coordinate system to use ("Cartesian" or "spherical"). - * @return Pair A pair of speaker indexes closest to the given position. + * @return SpeakerPair A pair of speaker indexes closest to the given position. */ - Pair get_indexs_by_geometry(std::vector pos, std::vector mask, - std::string coordinate); + SpeakerPair get_indexs_by_geometry(std::vector pos, std::vector mask, + std::string coordinate); /** * @brief Select a speaker index based on a trigger signal and the current @@ -221,7 +224,7 @@ class SpeakerManager { * @param mode The operation mode that determines the selection algorithm. * @return Index The selected speaker index based on the trigger and mode. */ - Index getIndexesByTrigger(Param trigger, Mode mode); + Index getIndexesByTrigger(Param trigger, TriggerMode mode); /** * @brief Get a vector of distances from a specific speaker index to all @@ -234,7 +237,8 @@ class SpeakerManager { /** * @brief Activate or deactivate speakers based on the given action and * speaker indexes. - * @param action The action to perform: "set" (replace current), "add" (activate), or "del" (deactivate). + * @param action The action to perform: "set" (replace current), "add" (activate), or "del" + * (deactivate). * @param spkrIdxes The speaker indexes to modify. */ void setActiveSpeakers(std::string action, Indexes spkrIdxes); @@ -250,7 +254,8 @@ class SpeakerManager { * @brief Set or update the topology matrix based on specified actions and * speaker indexes. The topology matrix is used to manage spatial * relationships between speakers. - * @param action The action to perform: "set" (replace), "add" (create connection), or "del" (remove connection). + * @param action The action to perform: "set" (replace), "add" (create connection), or "del" + * (remove connection). * @param spkrIdxes The speaker indexes to modify in the topology. */ void setTopoMatrix(std::string action, Indexes spkrIdxes); @@ -287,25 +292,26 @@ class SpeakerManager { void setPrinter(Logger::PrintStrategy newPrinter) { // The logger of SpeakerManger - logger->setPrinter(newPrinter); + logger.setPrinter(newPrinter); } - Logger* logger; ///< Pointer to a Logger object for logging messages and errors - ///< in different environments. + Logger logger; ///< Logger object for logging messages and errors + ///< in different environments. - private: + private: ConfigPath speakerArrayPath; ///< Path to the YAML speaker array configuration file. YAML::Node speakerArrayNode; ///< YAML Node containing the parsed speaker ///< array configuration data. - std::map speakers; ///< Map associating speaker indexes with their Speaker objects. + std::map + speakers; ///< Map associating speaker indexes with their Speaker objects. std::map distanceMatrix; ///< Matrix storing pre-calculated ///< distances between all speaker pairs. - Index currIdx; ///< Index of the currently selected speaker. - Indexes actvSpkIdx; ///< Vector storing indexes of all currently active speakers. - Indexes trajVector; ///< Ordered vector of speaker indexes defining the spatial - ///< trajectory for playback. + Index currIdx; ///< Index of the currently selected speaker. + Indexes actvSpkIdx; ///< Vector storing indexes of all currently active speakers. + Indexes trajVector; ///< Ordered vector of speaker indexes defining the spatial + ///< trajectory for playback. TopoMatrix topoMatrix; ///< Matrix defining the connectivity and spatial ///< relationships between speakers. @@ -399,8 +405,8 @@ class SpeakerManager { }; #ifdef PUREDATA // explicit instantiation required for PD -template bool isInKey(int element, std::map map); -template std::string formatVector(std::vector vector); +template bool isInKey(int element, const std::map& map); +template std::string formatVector(const std::vector& vector); #endif // PUREDATA } // namespace zerr diff --git a/core/include/utils/configs.h b/core/include/utils/configs.h index 3f37bf8..25d7d36 100644 --- a/core/include/utils/configs.h +++ b/core/include/utils/configs.h @@ -1,16 +1,36 @@ #ifndef CONFIGS_H #define CONFIGS_H +#include + // #define TESTMODE /**< Print log when TESTMODE Macro defined */ -#define TRIGGER_THRESHOLD 1e-4 /**< Threshold value for triggering feature extraction */ +namespace zerr { + +/// Threshold value for triggering feature extraction +inline constexpr double TRIGGER_THRESHOLD = 1e-4; + +/// Pi constant (lowercase to avoid macro clash with Max/MSP SDK) +inline constexpr double pi = 3.14159265; + +/// Size of audio processing buffer in samples +inline constexpr std::size_t AUDIO_BUFFER_SIZE = 2048; + +/// Minimum volume threshold for audio processing +inline constexpr double VOLUME_THRESHOLD = 1e-4; + +/// Scaling factor for distance calculations in speaker positioning +inline constexpr double DISTANCE_SCALE = 1e-1; -#define PI 3.14159265 /**< Macro definition for Pi */ +/// Default debounce threshold for onset detection (in samples) +inline constexpr int DEFAULT_ONSET_DEBOUNCE = 50; -#define AUDIO_BUFFER_SIZE 2048 /**< Size of audio processing buffer in samples */ +/// Small constant to avoid log(0) in spectral calculations +inline constexpr double LOG_FLOOR = 1e-10; -#define VOLUME_THRESHOLD 1e-4 /**< Minimum volume threshold for audio processing */ +/// Default rolloff percentage for spectral energy accumulation +inline constexpr double DEFAULT_ROLLOFF_PERCENT = 0.85; -#define DISTANCE_SCALE 1e-1 /**< Scaling factor for distance calculations in speaker positioning */ +} // namespace zerr -#endif // CONFIGS_H +#endif // CONFIGS_H diff --git a/core/include/utils/frequencytransformer.h b/core/include/utils/frequencytransformer.h index 6692e20..eeb0b6a 100644 --- a/core/include/utils/frequencytransformer.h +++ b/core/include/utils/frequencytransformer.h @@ -24,21 +24,21 @@ class FrequencyTransformer { FrequencyTransformer(int L); /** * @brief Perform Fast Fourier Transform - * + * * Run FFT on data in fft_in buffer and save the result to fft_out buffer. * The input should be real-valued time domain signal. */ void fft(); /** * @brief Perform Inverse Fast Fourier Transform - * + * * Run IFFT on fft_out buffer and save the result to fft_in buffer. * Converts frequency domain data back to time domain. */ void ifft(); /** * @brief Calculate power spectrum - * + * * Computes the power spectrum using the complex FFT data in fft_out buffer. * Power spectrum represents the signal's energy at each frequency. */ @@ -55,13 +55,13 @@ class FrequencyTransformer { fftw_complex* fft_output(); /** * @brief Get the computed power spectrum - * @return AudioBuffer Buffer containing the power spectrum values + * @return Samples Buffer containing the power spectrum values */ - AudioBuffer get_power_spectrum(); + Samples get_power_spectrum(); /** * @brief Apply window function to input data - * + * * Applies windowing to reduce spectral leakage in FFT analysis */ void windowing(); @@ -70,19 +70,19 @@ class FrequencyTransformer { * @brief Get the frame size used for analysis * @return int The frame size in samples */ - int get_frame_size() { return frame_size; } + int get_frame_size() const noexcept { return frame_size; } private: - int frame_size; ///< Size of the analysis frame in samples - int fft_size; ///< Size of the FFT (typically frame_size/2 + 1) + int frame_size; ///< Size of the analysis frame in samples + int fft_size; ///< Size of the FFT (typically frame_size/2 + 1) - AudioBuffer power_spec; ///< Buffer to store power spectrum results + Samples power_spec; ///< Buffer to store power spectrum results - double* fft_in; ///< Input buffer for FFT - fftw_complex* fft_out; ///< Output buffer for FFT results + double* fft_in; ///< Input buffer for FFT + fftw_complex* fft_out; ///< Output buffer for FFT results - fftw_plan p_fft, p_ifft; ///< FFTW plan objects for forward and inverse transforms + fftw_plan p_fft, p_ifft; ///< FFTW plan objects for forward and inverse transforms }; -} // namespace zerr -#endif // FREQUENCYTRANSFORMER_H \ No newline at end of file +} // namespace zerr +#endif // FREQUENCYTRANSFORMER_H \ No newline at end of file diff --git a/core/include/utils/onsetdetector.h b/core/include/utils/onsetdetector.h index c868c8b..2b1fbc1 100644 --- a/core/include/utils/onsetdetector.h +++ b/core/include/utils/onsetdetector.h @@ -53,7 +53,7 @@ class OnsetDetector { * Analyzes a block of audio samples to detect onset events by comparing * consecutive samples and identifying significant amplitude increases. */ - void detectOnsetInBlock(Block& block); + void detectOnsetInBlock(Samples& block); private: int lastSample; ///< Previous sample value for amplitude comparison diff --git a/core/include/utils/ringbuffer.h b/core/include/utils/ringbuffer.h index 2a6255b..69e94fe 100644 --- a/core/include/utils/ringbuffer.h +++ b/core/include/utils/ringbuffer.h @@ -37,19 +37,19 @@ class RingBuffer { * @brief Get the current number of samples in the buffer * @return size_t Current number of samples stored */ - size_t get_size() const; + size_t get_size() const noexcept; /** * @brief Get the maximum capacity of the buffer * @return size_t Maximum number of samples the buffer can hold */ - size_t get_capacity() const; + size_t get_capacity() const noexcept; /** * @brief Add a block of samples to the buffer * @param block Block of samples to enqueue */ - void enqueue(const Block& block); + void enqueue(const Samples& block); /** * @brief Retrieve samples from the buffer diff --git a/core/include/utils/types.h b/core/include/utils/types.h index 0a2424d..4c54043 100644 --- a/core/include/utils/types.h +++ b/core/include/utils/types.h @@ -10,6 +10,7 @@ #ifndef TYPES_H #define TYPES_H #include +#include #include #include @@ -21,41 +22,46 @@ using Sample = double; /**< Base type for audio sample values */ using Param = float; /**< Base type for parameter values used in audio processing */ using Index = int; /**< Base type for indexing and counting */ -struct Complex { - Sample real; /**< Real component of complex number */ - Sample img; /**< Imaginary component of complex number */ -}; /**< Complex number representation for frequency domain calculations */ - using Samples = std::vector; /**< Vector container for audio samples */ +using Blocks = + std::vector; /**< Collection of sample vectors for multi-channel processing */ -using Block = Samples; /**< Single block of audio samples */ -using Blocks = std::vector; /**< Collection of audio blocks for multi-channel processing */ +struct AudioInputs { + Samples block; /**< Single block of audio samples for processing */ + Samples wave; /**< Buffered audio frame for temporal analysis */ + std::vector spec; /**< Spectral power data for frequency analysis */ +}; /**< Consolidated structure for different types of audio input data */ -using AudioBuffer = Samples; /**< Buffer for storing audio frames */ -using AudioBuffers = - std::vector; /**< Collection of audio buffers for multi-channel storage */ +using FeatureNames = std::vector; /**< List of feature names to be processed */ -using FFTBuffer = std::vector; /**< Buffer for storing FFT results as complex numbers */ -using SpecBuffer = std::vector; /**< Buffer for storing spectral power values */ +/**< Envelope generation strategy */ +enum class GenMode { + Trigger, /**< Jump to a new speaker on each trigger event */ + Trajectory, /**< Continuously move along a trajectory path */ +}; -struct AudioInputs { - Block block; /**< Single block of audio samples for processing */ - AudioBuffer wave; /**< Buffered audio frame for temporal analysis */ - SpecBuffer spec; /**< Spectral power data for frequency analysis */ -}; /**< Consolidated structure for different types of audio input data */ +/**< Speaker selection strategy within trigger mode */ +enum class TriggerMode { + Random, /**< Randomly select from connected speakers */ +}; -using FeatureName = std::string; /**< String identifier for audio features */ -using FeatureNames = std::vector; /**< List of feature names to be processed */ +/** + * @brief Parse a string into a GenMode enum value + * @param s The string to parse ("trigger" or "trajectory") + * @return GenMode The corresponding enum value + * @throws std::invalid_argument if the string is not a valid mode + */ +inline GenMode parseGenMode(const std::string& s) +{ + if (s == "trigger") + return GenMode::Trigger; + if (s == "trajectory") + return GenMode::Trajectory; + throw std::invalid_argument("Unknown GenMode: " + s); +} -using Mode = std::string; /**< String identifier for processing modes */ using ConfigPath = std::string; /**< Path string for configuration files */ -// TODO(Zeyu Yang): If this only use in PD wrapper, move it out -typedef struct { - char** names; /**< Array of feature name strings */ - int num; /**< Number of feature names in the array */ -} t_featureNames; - using Params = std::vector; /**< Vector container for parameter values */ using FeatureVal = Param; /**< Single feature value computed from an audio block */ @@ -89,14 +95,15 @@ struct Orientation { // specific configs using Indexes = std::vector; /**< Collection of index values */ using TopoMatrix = - std::map; /**< Topology matrix mapping indices to their connections */ -using Pair = std::pair; /**< Pair of indices for representing connections */ + std::map; /**< Topology matrix mapping indices to their connections */ +using SpeakerPair = + std::pair; /**< Pair of speaker indices for representing connections */ // system config -typedef struct { +struct SystemConfigs { size_t sample_rate; /**< Audio sampling rate in Hz */ size_t block_size; /**< Size of processing blocks in samples */ -} SystemConfigs; +}; } // namespace zerr #endif // TYPES_H diff --git a/core/include/utils/utils.h b/core/include/utils/utils.h index 346f1a3..c58a598 100644 --- a/core/include/utils/utils.h +++ b/core/include/utils/utils.h @@ -11,11 +11,11 @@ #include #include +#include #include #include #include #include -#include #include #include #include @@ -37,7 +37,7 @@ std::string formatString(const char* format, ...); */ // template // std::string formatVector(std::vector vector); -template std::string formatVector(std::vector vector) +template std::string formatVector(const std::vector& vector) { std::string formated = ""; for (size_t i = 0; i < vector.size(); ++i) { @@ -67,7 +67,7 @@ bool isEqualTo0(Param value, Param epsilon = std::numeric_limits::epsilon */ inline float get_hann_sample(int pos, int L) { - float val = 0.5 * (1.0 - cos((2.0 * PI * (float)pos) / (float)L)); + float val = 0.5 * (1.0 - cos((2.0 * pi * static_cast(pos)) / static_cast(L))); return val; } /** @@ -76,14 +76,14 @@ inline float get_hann_sample(int pos, int L) * @param vector The vector to search within * @return bool True if the element is found in the vector, false otherwise */ -template bool isInVec(T element, std::vector vector); +template bool isInVec(T element, const std::vector& vector); /** * @brief Check if a key exists in a map * @param element The key to search for * @param map The map to search within * @return bool True if the key exists in the map, false otherwise */ -template bool isInKey(T element, std::map map) +template bool isInKey(T element, const std::map& map) { auto it = map.find(element); return it == map.end() ? false : true; diff --git a/core/src/features/centroid.cpp b/core/src/features/centroid.cpp index b230d00..4e813af 100644 --- a/core/src/features/centroid.cpp +++ b/core/src/features/centroid.cpp @@ -4,11 +4,7 @@ using namespace zerr; using namespace feature; -const std::string Centroid::name = "Spectral Centroid"; -const std::string Centroid::category = "Frequency-Domain"; -const std::string Centroid::description = - "The spectral centroid is a measure used in digital signal processing to " - "characterise a spectrum."; +Centroid::Centroid() {} void Centroid::initialize(SystemConfigs sys_cfg) { @@ -18,7 +14,7 @@ void Centroid::initialize(SystemConfigs sys_cfg) _reset_param(); if (is_initialized() == false) { - set_initialize_statue(true); + set_initialize_status(true); } } @@ -42,7 +38,7 @@ void Centroid::extract() void Centroid::reset() { _reset_param(); } -void Centroid::fetch(AudioInputs in) +void Centroid::fetch(const AudioInputs& in) { x = in.spec; prv_y = crr_y; diff --git a/core/src/features/crestfactor.cpp b/core/src/features/crestfactor.cpp index 3295bd8..48a9d00 100644 --- a/core/src/features/crestfactor.cpp +++ b/core/src/features/crestfactor.cpp @@ -4,13 +4,7 @@ using namespace zerr; using namespace feature; -const std::string CrestFactor::name = "Crest Factor"; -const std::string CrestFactor::category = "Time-Domain"; -const std::string CrestFactor::description = - "Crest Factor is a parameter used in signal processing and audio " - "engineering to describe the characteristics of a waveform. It is defined " - "as the ratio of the peak value of a waveform to its RMS (Root Mean " - "Square) value. "; +CrestFactor::CrestFactor() {} void CrestFactor::initialize(SystemConfigs sys_cfg) { @@ -18,29 +12,27 @@ void CrestFactor::initialize(SystemConfigs sys_cfg) _reset_param(); - if (is_initialized() == false) - { - set_initialize_statue(true); + if (is_initialized() == false) { + set_initialize_status(true); } } void CrestFactor::extract() { - double square_sum = 0.0; + double square_sum = 0.0; double square_root = 0.0; - double peak_max = 0.0; - double peak_tmp = 0.0; - int x_size = x.size(); + double peak_max = 0.0; + double peak_tmp = 0.0; + int x_size = x.size(); - for (int i = 0; i < x_size; ++i) - { + for (int i = 0; i < x_size; ++i) { square_sum += x[i] * x[i]; peak_tmp = abs(x[i]); peak_max = peak_tmp > peak_max ? peak_tmp : peak_max; } - square_sum = square_sum / x_size; + square_sum = square_sum / x_size; square_root = std::sqrt(square_sum); crr_y = peak_max / square_root; @@ -48,9 +40,9 @@ void CrestFactor::extract() void CrestFactor::reset() { _reset_param(); } -void CrestFactor::fetch(AudioInputs in) +void CrestFactor::fetch(const AudioInputs& in) { - x = in.wave; + x = in.wave; prv_y = crr_y; } @@ -58,8 +50,7 @@ FeatureVals CrestFactor::send() { linear_interpolator.set_value(prv_y, crr_y, system_configs.block_size); - for (size_t i = 0; i < system_configs.block_size; ++i) - { + for (size_t i = 0; i < system_configs.block_size; ++i) { y[i] = linear_interpolator.get_value(); linear_interpolator.next_step(); } diff --git a/core/src/features/flatness.cpp b/core/src/features/flatness.cpp index 7794273..cf99d7c 100644 --- a/core/src/features/flatness.cpp +++ b/core/src/features/flatness.cpp @@ -6,39 +6,31 @@ using namespace zerr; using namespace feature; -const std::string Flatness::name = "Spectral Flatness"; -const std::string Flatness::category = "Frequency-Domain"; -const std::string Flatness::description = - "Spectral flatness, also known as Wiener entropy, is a measure used in " - "digital signal processing to characterize an audio spectrum. Spectral " - "flatness is typically used to quantify how noise-like a signal is, as " - "opposed to being tonal. A higher value of spectral flatness indicates a " - "more noise-like signal, whereas a lower value indicates a more tonal " - "signal."; - -void Flatness::initialize(SystemConfigs sys_cfg) { +Flatness::Flatness() {} + +void Flatness::initialize(SystemConfigs sys_cfg) +{ system_configs = sys_cfg; _reset_param(); if (is_initialized() == false) { - set_initialize_statue(true); + set_initialize_status(true); } } -void Flatness::extract() { +void Flatness::extract() +{ // Calculate the sum of logarithms double logSum = 0.0; - const double smallConstant = - 1e-10; // Small constant to avoid taking log of 0 for (double sample : x) { - logSum += std::log(sample + smallConstant); + logSum += std::log(sample + LOG_FLOOR); } // Calculate the geometric mean using exponentiation double geometricMean = std::exp(logSum / x.size()); // Calculate the arithmetic mean - double sum = std::accumulate(x.begin(), x.end(), 0.0); + double sum = std::accumulate(x.begin(), x.end(), 0.0); double arithmeticMean = sum / x.size(); // Calculate the spectral flatness @@ -52,12 +44,14 @@ void Flatness::extract() { void Flatness::reset() { _reset_param(); } -void Flatness::fetch(AudioInputs in) { - x = in.spec; +void Flatness::fetch(const AudioInputs& in) +{ + x = in.spec; prv_y = crr_y; } -FeatureVals Flatness::send() { +FeatureVals Flatness::send() +{ linear_interpolator.set_value(prv_y, crr_y, system_configs.block_size); for (size_t i = 0; i < system_configs.block_size; ++i) { @@ -68,7 +62,8 @@ FeatureVals Flatness::send() { return y; } -void Flatness::_reset_param() { +void Flatness::_reset_param() +{ x.resize(AUDIO_BUFFER_SIZE, 0.0f); prv_y = 0.0; diff --git a/core/src/features/flux.cpp b/core/src/features/flux.cpp index a975b6a..12fd77f 100644 --- a/core/src/features/flux.cpp +++ b/core/src/features/flux.cpp @@ -3,24 +3,21 @@ using namespace zerr; using namespace feature; -const std::string Flux::name = "Spectral Flux"; -const std::string Flux::category = "Frequency-Domain"; -const std::string Flux::description = - "Spectral flux is a measure used in digital signal processing that " - "quantifies how quickly the power spectrum of a signal changes. It is " - "often used in audio analysis for onset detection and other applications."; - -void Flux::initialize(SystemConfigs sys_cfg) { +Flux::Flux() {} + +void Flux::initialize(SystemConfigs sys_cfg) +{ system_configs = sys_cfg; _reset_param(); if (is_initialized() == false) { - set_initialize_statue(true); + set_initialize_status(true); } } -void Flux::extract() { +void Flux::extract() +{ Param flux = 0.0; Param diff = 0.0; @@ -34,14 +31,16 @@ void Flux::extract() { void Flux::reset() { _reset_param(); } -void Flux::fetch(AudioInputs in) { +void Flux::fetch(const AudioInputs& in) +{ prv_x = x; - x = in.spec; + x = in.spec; prv_y = crr_y; } -FeatureVals Flux::send() { +FeatureVals Flux::send() +{ linear_interpolator.set_value(prv_y, crr_y, system_configs.block_size); for (size_t i = 0; i < system_configs.block_size; ++i) { @@ -53,7 +52,8 @@ FeatureVals Flux::send() { return y; } -void Flux::_reset_param() { +void Flux::_reset_param() +{ prv_x.resize(AUDIO_BUFFER_SIZE, 0.0f); x.resize(AUDIO_BUFFER_SIZE, 0.0f); diff --git a/core/src/features/rolloff.cpp b/core/src/features/rolloff.cpp index dfbebcd..83d67cf 100644 --- a/core/src/features/rolloff.cpp +++ b/core/src/features/rolloff.cpp @@ -6,25 +6,21 @@ using namespace zerr; using namespace feature; -const std::string Rolloff::name = "Spectral Rolloff"; -const std::string Rolloff::category = "Frequency-Domain"; -const std::string Rolloff::description = - "The spectral rolloff is a measure used in signal processing to determine " - "the frequency below which a specified percentage of the total spectral " - "energy lies. It is often used to distinguish between harmonic and " - "non-harmonic content in an audio signal."; - -void Rolloff::initialize(SystemConfigs sys_cfg) { +Rolloff::Rolloff() {} + +void Rolloff::initialize(SystemConfigs sys_cfg) +{ system_configs = sys_cfg; - freq_max = static_cast(system_configs.sample_rate) / 2.0; + freq_max = static_cast(system_configs.sample_rate) / 2.0; _reset_param(); if (is_initialized() == false) { - set_initialize_statue(true); + set_initialize_status(true); } } -void Rolloff::extract() { +void Rolloff::extract() +{ // Calculate the total energy in the spectrum double totalEnergy = std::accumulate(x.begin(), x.end(), 0.0); @@ -37,7 +33,7 @@ void Rolloff::extract() { sumEnergy += x[i]; if (sumEnergy >= rolloffThreshold) { // Calculate the frequency corresponding to the bin index - crr_y = (double)i * freq_max / (double)x.size(); + crr_y = static_cast(i) * freq_max / static_cast(x.size()); return; } } @@ -47,12 +43,14 @@ void Rolloff::extract() { void Rolloff::reset() { _reset_param(); } -void Rolloff::fetch(AudioInputs in) { - x = in.spec; +void Rolloff::fetch(const AudioInputs& in) +{ + x = in.spec; prv_y = crr_y; } -FeatureVals Rolloff::send() { +FeatureVals Rolloff::send() +{ linear_interpolator.set_value(prv_y, crr_y, system_configs.block_size); for (size_t i = 0; i < system_configs.block_size; ++i) { @@ -63,7 +61,8 @@ FeatureVals Rolloff::send() { return y; } -void Rolloff::_reset_param() { +void Rolloff::_reset_param() +{ x.resize(AUDIO_BUFFER_SIZE, 0.0f); prv_y = 0.0; diff --git a/core/src/features/rootmeansquare.cpp b/core/src/features/rootmeansquare.cpp index fb055d9..9f8fbd2 100644 --- a/core/src/features/rootmeansquare.cpp +++ b/core/src/features/rootmeansquare.cpp @@ -4,31 +4,29 @@ using namespace zerr; using namespace feature; -const std::string RootMeanSquare::name = "Root-Mean-Squre Amplitude"; -const std::string RootMeanSquare::category = "Time-Domain"; -const std::string RootMeanSquare::description = - "The RMS is defined as the square root of the mean over time of the square " - "of the vertical distance of the graph from the rest state"; +RootMeanSquare::RootMeanSquare() {} -void RootMeanSquare::initialize(SystemConfigs sys_cfg) { +void RootMeanSquare::initialize(SystemConfigs sys_cfg) +{ system_configs = sys_cfg; _reset_param(); if (is_initialized() == false) { - set_initialize_statue(true); + set_initialize_status(true); } } -void RootMeanSquare::extract() { - double square_sum = 0; +void RootMeanSquare::extract() +{ + double square_sum = 0; double square_root = 0; - int x_size = x.size(); + int x_size = x.size(); for (int i = 0; i < x_size; ++i) { square_sum += x[i] * x[i]; } - square_sum = square_sum / x_size; + square_sum = square_sum / x_size; square_root = std::sqrt(square_sum); crr_y = square_root; @@ -36,12 +34,14 @@ void RootMeanSquare::extract() { void RootMeanSquare::reset() { _reset_param(); } -void RootMeanSquare::fetch(AudioInputs in) { - x = in.wave; +void RootMeanSquare::fetch(const AudioInputs& in) +{ + x = in.wave; prv_y = crr_y; } -FeatureVals RootMeanSquare::send() { +FeatureVals RootMeanSquare::send() +{ linear_interpolator.set_value(prv_y, crr_y, system_configs.block_size); for (size_t i = 0; i < system_configs.block_size; ++i) { @@ -52,7 +52,8 @@ FeatureVals RootMeanSquare::send() { return y; } -void RootMeanSquare::_reset_param() { +void RootMeanSquare::_reset_param() +{ x.resize(AUDIO_BUFFER_SIZE, 0.0f); prv_y = 0.0; diff --git a/core/src/features/zerocrossingrate.cpp b/core/src/features/zerocrossingrate.cpp index 93c5e03..95a6a48 100644 --- a/core/src/features/zerocrossingrate.cpp +++ b/core/src/features/zerocrossingrate.cpp @@ -1,32 +1,29 @@ -#include "utils.h" #include "zerocrossingrate.h" +#include "utils.h" using namespace zerr; using namespace feature; -const std::string ZeroCrossingRate::name = "Zero crossing rate"; -const std::string ZeroCrossingRate::category = "Time-Domain"; -const std::string ZeroCrossingRate::description = - "The zero crossing rate (ZCR) is a measure of how frequently a signal " - "changes its sign. It represents the rate at which the signal crosses the " - "zero amplitude level over a given time period."; +ZeroCrossingRate::ZeroCrossingRate() {} -void ZeroCrossingRate::initialize(SystemConfigs sys_cfg) { +void ZeroCrossingRate::initialize(SystemConfigs sys_cfg) +{ system_configs = sys_cfg; _reset_param(); // zero_crossings = 0; if (is_initialized() == false) { - set_initialize_statue(true); + set_initialize_status(true); } } -void ZeroCrossingRate::extract() { +void ZeroCrossingRate::extract() +{ assert(is_initialized()); int zero_crossings = 0; - int x_size = x.size(); + int x_size = x.size(); for (int i = 1; i < x_size; ++i) { if ((x[i] >= 0 && x[i - 1] < 0) || (x[i] < 0 && x[i - 1] >= 0)) { @@ -39,12 +36,14 @@ void ZeroCrossingRate::extract() { void ZeroCrossingRate::reset() { _reset_param(); } -void ZeroCrossingRate::fetch(AudioInputs in) { - x = in.wave; +void ZeroCrossingRate::fetch(const AudioInputs& in) +{ + x = in.wave; prv_y = crr_y; } -FeatureVals ZeroCrossingRate::send() { +FeatureVals ZeroCrossingRate::send() +{ linear_interpolator.set_value(prv_y, crr_y, system_configs.block_size); for (size_t i = 0; i < system_configs.block_size; ++i) { @@ -55,7 +54,8 @@ FeatureVals ZeroCrossingRate::send() { return y; } -void ZeroCrossingRate::_reset_param() { +void ZeroCrossingRate::_reset_param() +{ x.resize(AUDIO_BUFFER_SIZE, 0.0f); prv_y = 0.0; diff --git a/core/src/features/zerocrossings.cpp b/core/src/features/zerocrossings.cpp index 64458fb..e851967 100644 --- a/core/src/features/zerocrossings.cpp +++ b/core/src/features/zerocrossings.cpp @@ -1,29 +1,29 @@ -#include "utils.h" #include "zerocrossings.h" +#include "utils.h" using namespace zerr; using namespace feature; -const std::string ZeroCrossings::name = "ZeroCrossings"; -const std::string ZeroCrossings::category = "Sample-Level"; -const std::string ZeroCrossings::description = - "Zero crossing is used to describe the point at which a signal changes its " - "sign from positive to negative or from negative to positive."; +ZeroCrossings::ZeroCrossings() {} -void ZeroCrossings::initialize(SystemConfigs sys_cfg) { +void ZeroCrossings::initialize(SystemConfigs sys_cfg) +{ system_configs = sys_cfg; _reset_param(); if (is_initialized() == false) { - set_initialize_statue(true); + set_initialize_status(true); } } -void ZeroCrossings::extract() { - if (_isZeroCrossing(last_sample, x[0])) y[0] = 1; +void ZeroCrossings::extract() +{ + if (_isZeroCrossing(last_sample, x[0])) + y[0] = 1; for (size_t i = 1; i < x.size(); ++i) { - if (_isZeroCrossing(x[i - 1], x[i])) y[i] = 1; + if (_isZeroCrossing(x[i - 1], x[i])) + y[i] = 1; } last_sample = x[x.size() - 1]; @@ -31,7 +31,8 @@ void ZeroCrossings::extract() { void ZeroCrossings::reset() { _reset_param(); } -void ZeroCrossings::fetch(AudioInputs in) { +void ZeroCrossings::fetch(const AudioInputs& in) +{ x = in.block; y.clear(); y.resize(x.size(), 0.0f); @@ -39,12 +40,14 @@ void ZeroCrossings::fetch(AudioInputs in) { FeatureVals ZeroCrossings::send() { return y; } -void ZeroCrossings::_reset_param() { +void ZeroCrossings::_reset_param() +{ x.resize(system_configs.block_size, 0.0f); y.resize(system_configs.block_size, 0.0f); last_sample = 0.0; } -bool ZeroCrossings::_isZeroCrossing(Sample first, Sample second) { +bool ZeroCrossings::_isZeroCrossing(Sample first, Sample second) +{ return ((first >= 0 && second < 0) || (first < 0 && second >= 0)); } diff --git a/core/src/modules/audiodisperser.cpp b/core/src/modules/audiodisperser.cpp index 43b751f..9b7054b 100644 --- a/core/src/modules/audiodisperser.cpp +++ b/core/src/modules/audiodisperser.cpp @@ -10,27 +10,27 @@ #include "audiodisperser.h" using namespace zerr; -AudioDisperser::AudioDisperser(int numChannel, zerr::SystemConfigs systemCfgs) { - this->numChannel = numChannel; - this->systemCfgs = systemCfgs; - - numInlet = numChannel + 1; +AudioDisperser::AudioDisperser(int numChannel, zerr::SystemConfigs systemCfgs) + : numChannel(numChannel), systemCfgs(systemCfgs) +{ + numInlet = numChannel + 1; numOutlet = numChannel; - logger = new Logger(); #ifdef TESTMODE - logger->setLogLevel(LogLevel::INFO); -#endif // TESTMODE + logger.setLogLevel(LogLevel::INFO); +#endif // TESTMODE } -bool AudioDisperser::initialize() { +bool AudioDisperser::initialize() +{ inputBuffer.resize(numInlet, Samples(systemCfgs.block_size, 0.0f)); outputBuffer.resize(numOutlet, Samples(systemCfgs.block_size, 0.0f)); return true; } -Blocks AudioDisperser::perfrom(Blocks in) { +Blocks AudioDisperser::perfrom(Blocks in) +{ inputBuffer = in; // clean the output buffer diff --git a/core/src/modules/envelopecombinator.cpp b/core/src/modules/envelopecombinator.cpp index 4ecb67a..679eef4 100644 --- a/core/src/modules/envelopecombinator.cpp +++ b/core/src/modules/envelopecombinator.cpp @@ -20,9 +20,8 @@ EnvelopeCombinator::EnvelopeCombinator(int numSource, int numChannel, SystemConf numInlet = numSource * numChannel; numOutlet = numChannel; - logger = new Logger(); #ifdef TESTMODE - logger->setLogLevel(LogLevel::INFO); + logger.setLogLevel(LogLevel::INFO); #endif // TESTMODE } @@ -41,7 +40,7 @@ bool EnvelopeCombinator::initialize() processFunc = &EnvelopeCombinator::_process_max; } else { - logger->logError("EnvelopeCombinator::initialize Unknown combination mode: " + combMode); + logger.logError("EnvelopeCombinator::initialize Unknown combination mode: " + combMode); return false; } @@ -82,7 +81,7 @@ void EnvelopeCombinator::_process_root() buffer.assign(buffer.size(), 0.0f); } - double exponent = 1.0 / (double)numSource; + double exponent = 1.0 / static_cast(numSource); Sample multi_tmp; for (int i = 0; i < numChannel; ++i) { // TODO: use systemcfg.block_size could cause bug(sometimes smaller) @@ -116,5 +115,3 @@ void EnvelopeCombinator::_process_max() } } } - -EnvelopeCombinator::~EnvelopeCombinator() { delete logger; } diff --git a/core/src/modules/envelopegenerator.cpp b/core/src/modules/envelopegenerator.cpp index 83e3a03..d0db5b3 100644 --- a/core/src/modules/envelopegenerator.cpp +++ b/core/src/modules/envelopegenerator.cpp @@ -9,24 +9,21 @@ #include "envelopegenerator.h" using zerr::Blocks; +using zerr::DEFAULT_ONSET_DEBOUNCE; +using zerr::DISTANCE_SCALE; using zerr::EnvelopeGenerator; using zerr::Param; +using zerr::pi; +using zerr::VOLUME_THRESHOLD; EnvelopeGenerator::EnvelopeGenerator(SystemConfigs systemCfgs, std::string speakerCfgs, - Mode genMode) + GenMode genMode) + : systemCfgs(systemCfgs), speakerCfgs(speakerCfgs), genMode(genMode), + speakerManager(std::make_unique(speakerCfgs)), + onsetDetector(std::make_unique(DEFAULT_ONSET_DEBOUNCE)) { - this->systemCfgs = systemCfgs; - this->speakerCfgs = speakerCfgs; - this->genMode = genMode; - - speakerManager = new SpeakerManager(this->speakerCfgs); - - logger = new Logger(); - - onsetDetector = new OnsetDetector(50); - #ifdef TESTMODE - logger->setLogLevel(LogLevel::INFO); + logger.setLogLevel(LogLevel::INFO); #endif // TESTMODE } @@ -37,15 +34,13 @@ bool EnvelopeGenerator::initialize() return false; // check the generator mode and bind process func - if (genMode == "trigger") { + switch (genMode) { + case GenMode::Trigger: processFunc = &EnvelopeGenerator::_processTrigger; - } - else if (genMode == "trajectory") { + break; + case GenMode::Trajectory: processFunc = &EnvelopeGenerator::_processTrajectory; - } - else { - logger->logError("EnvelopeGenerator::initialize Unknown selection mode: " + genMode); - return false; + break; } // get the number of speakers @@ -63,9 +58,9 @@ bool EnvelopeGenerator::initialize() } // initialize trigger mode specified parameters - if (genMode == "trigger") { + if (genMode == GenMode::Trigger) { speakerManager->setCurrentSpeaker(speakerManager->getRandomIndex()); - triggerMode = "random"; + triggerMode = TriggerMode::Random; } // initialized @@ -111,23 +106,16 @@ void EnvelopeGenerator::setTopoMatrix(std::string action, Indexes idxs) void EnvelopeGenerator::setTriggerInterval(Param newInterval) { newInterval = newInterval < 0 ? 0 : newInterval; - int newThreshold = (int)(newInterval / 1000.0 * systemCfgs.sample_rate); + int newThreshold = static_cast(newInterval / 1000.0 * systemCfgs.sample_rate); onsetDetector->setDebounceThreshold(newThreshold); } void EnvelopeGenerator::printParameters() { speakerManager->printParameters(); } -EnvelopeGenerator::~EnvelopeGenerator() -{ - delete speakerManager; - delete logger; - delete onsetDetector; -} - void EnvelopeGenerator::setPrinter(Logger::PrintStrategy newPrinter) { // The logger of EnvelopeGenerator - logger->setPrinter(newPrinter); + logger.setPrinter(newPrinter); // The logger of SpeakerManger speakerManager->setPrinter(newPrinter); } @@ -182,8 +170,8 @@ void EnvelopeGenerator::_processTrigger() void EnvelopeGenerator::_processTrajectory() { - Pair speakerPair; - Pair channelPair; + SpeakerPair speakerPair; + SpeakerPair channelPair; Param panRatio; @@ -223,9 +211,9 @@ Param EnvelopeGenerator::_calculateGain(Param x, Param theta) theta = theta < 0.0 ? 0 : theta; theta = theta > 1.0 ? 1 : theta; - Param tmp = tan(theta * PI / 2.0); + Param tmp = tan(theta * pi / 2.0); - Param gain = isEqualTo0(tmp, VOLUME_THRESHOLD) ? 0.0 : 1.0 - x / tan(theta * PI / 2.0); + Param gain = isEqualTo0(tmp, VOLUME_THRESHOLD) ? 0.0 : 1.0 - x / tan(theta * pi / 2.0); // clip gain gain = gain < 0.0 ? 0 : gain; diff --git a/core/src/modules/featurebank.cpp b/core/src/modules/featurebank.cpp index f034148..abac666 100644 --- a/core/src/modules/featurebank.cpp +++ b/core/src/modules/featurebank.cpp @@ -23,38 +23,35 @@ void FeatureBank::print_all_features() void FeatureBank::print_active_features() { std::cout << "All activated features: " << std::endl; - for (size_t i = 0; i < activated_features.size(); ++i) { - std::cout << " -Name: " << activated_features[i]->get_name() << std::endl; - std::cout << " -Category: " << activated_features[i]->get_category() << std::endl; - std::cout << " -Description: " << activated_features[i]->get_description() << std::endl; - std::cout << std::endl; + for (const auto& name : active_feature_names) { + std::cout << " -Name: " << name << std::endl; } } void FeatureBank::initialize(FeatureNames feature_names, SystemConfigs system_configs) { - for (auto name : feature_names) { + active_feature_names = feature_names; + for (const auto& name : feature_names) { activated_features.push_back(_create(name)); } n_features = activated_features.size(); - for (int i = 0; i < n_features; ++i) { - activated_features[i]->initialize(system_configs); + for (auto& feature : activated_features) { + feature->initialize(system_configs); } y.resize(activated_features.size()); x.wave.resize(AUDIO_BUFFER_SIZE); } -FeaturesVals FeatureBank::perform(Block in) +FeaturesVals FeatureBank::perform(Samples in) { // fetch Sample* buf_ptr = nullptr; size_t buf_len; ring_buffer.enqueue(in); - x.block.clear(); - x.block = in; + x.block = std::move(in); buf_ptr = x.wave.data(); buf_len = x.wave.size(); @@ -69,7 +66,6 @@ FeaturesVals FeatureBank::perform(Block in) x.spec = freq_transformer.get_power_spectrum(); // process: - // TODO: use multi-thread for (size_t i = 0; i < activated_features.size(); ++i) { activated_features[i]->fetch(x); activated_features[i]->extract(); diff --git a/core/src/modules/speakermanager.cpp b/core/src/modules/speakermanager.cpp index 4275a16..e087621 100644 --- a/core/src/modules/speakermanager.cpp +++ b/core/src/modules/speakermanager.cpp @@ -11,57 +11,48 @@ using namespace zerr; Speaker::Speaker(Index index, Position position, Orientation orientation) + : index(index), position(position), orientation(orientation) { - logger = new Logger(); - #ifdef TESTMODE - logger->setLogLevel(LogLevel::INFO); + logger.setLogLevel(LogLevel::INFO); #endif // TESTMODE - - this->index = index; - this->position = position; - this->orientation = orientation; } void Speaker::printAll() { - logger->logInfo("-----------------------"); + logger.logInfo("-----------------------"); _print_index(); _print_position(); _print_orientation(); } -void Speaker::_print_index() { logger->logInfo(formatString("Speaker ID: %d", index)); } +void Speaker::_print_index() { logger.logInfo(formatString("Speaker ID: %d", index)); } void Speaker::_print_position() { - logger->logInfo("Cartesian Position: "); - logger->logInfo(formatString(" x: %.2f", position.cartesian.x)); - logger->logInfo(formatString(" y: %.2f", position.cartesian.y)); - logger->logInfo(formatString(" z: %.2f", position.cartesian.z)); - logger->logInfo("Spherical Position: "); - logger->logInfo(formatString(" azimuth: : %.2f", position.spherical.azimuth)); - logger->logInfo(formatString(" elevation: : %.2f", position.spherical.elevation)); - logger->logInfo(formatString(" distance: : %.2f", position.spherical.distance)); + logger.logInfo("Cartesian Position: "); + logger.logInfo(formatString(" x: %.2f", position.cartesian.x)); + logger.logInfo(formatString(" y: %.2f", position.cartesian.y)); + logger.logInfo(formatString(" z: %.2f", position.cartesian.z)); + logger.logInfo("Spherical Position: "); + logger.logInfo(formatString(" azimuth: : %.2f", position.spherical.azimuth)); + logger.logInfo(formatString(" elevation: : %.2f", position.spherical.elevation)); + logger.logInfo(formatString(" distance: : %.2f", position.spherical.distance)); } void Speaker::_print_orientation() { - logger->logInfo("Orientation: "); - logger->logInfo(formatString(" yaw: : %.2f", orientation.yaw)); - logger->logInfo(formatString(" pitch: : %.2f", orientation.pitch)); + logger.logInfo("Orientation: "); + logger.logInfo(formatString(" yaw: : %.2f", orientation.yaw)); + logger.logInfo(formatString(" pitch: : %.2f", orientation.pitch)); } -SpeakerManager::SpeakerManager(ConfigPath spkrArry) +SpeakerManager::SpeakerManager(ConfigPath spkrArry) : speakerArrayPath(spkrArry) { - this->speakerArrayPath = spkrArry; - - logger = new Logger(); - #ifdef TESTMODE - logger->setLogLevel(LogLevel::INFO); + logger.setLogLevel(LogLevel::INFO); #endif // TESTMODE - logger->logInfo("SpeakerManager::SpeakerManager " + speakerArrayPath); + logger.logInfo("SpeakerManager::SpeakerManager " + speakerArrayPath); } bool SpeakerManager::initialize() @@ -72,8 +63,8 @@ bool SpeakerManager::initialize() speakerArrayNode = YAML::LoadFile(speakerArrayPath); } catch (...) { - logger->logError("SpeakerManager::initialize: Load speaker configuration " + - speakerArrayPath + " failed"); + logger.logError("SpeakerManager::initialize: Load speaker configuration " + + speakerArrayPath + " failed"); return false; } // analysis speaker configuration file @@ -146,15 +137,14 @@ bool SpeakerManager::initialize() // initialize the specific configs // every speaker is actvSpkIdx when at initialize point - for (size_t i = 0; i < actvSpkIdx.size(); ++i) { - trajVector.push_back(actvSpkIdx[i]); + for (auto idx : actvSpkIdx) { + trajVector.push_back(idx); } std::sort(trajVector.begin(), trajVector.end()); // topoMatrix every speaker is connected - for (size_t i = 0; i < actvSpkIdx.size(); ++i) { - Indexes tmp_idx = actvSpkIdx; // deepcopy - topoMatrix[actvSpkIdx[i]] = tmp_idx; + for (auto idx : actvSpkIdx) { + topoMatrix[idx] = actvSpkIdx; // copy } return true; @@ -181,7 +171,7 @@ Speaker SpeakerManager::getSpeakerByIndex(Index spkrIdx) return it->second; } -Pair SpeakerManager::getIndexesByTrajectory(Param trajVal) +SpeakerPair SpeakerManager::getIndexesByTrajectory(Param trajVal) { trajVal = trajVal < 0.0 ? 0.0 : trajVal; @@ -210,8 +200,8 @@ Param SpeakerManager::getPanningRatio(Param trajVal) return (scaled - std::floor(scaled)); } -Pair SpeakerManager::get_indexs_by_geometry(std::vector pos, std::vector mask, - std::string coordinate) +SpeakerPair SpeakerManager::get_indexs_by_geometry(std::vector pos, std::vector mask, + std::string coordinate) { std::vector distance; std::vector indexs; @@ -219,10 +209,7 @@ Pair SpeakerManager::get_indexs_by_geometry(std::vector pos, std::vector< assert(mask.size() == 3 && "ERROR: The mask vector for geometry selection muss be in size 3."); Param tmp_distan; - for (const auto& maps : speakers) { - Index key = maps.first; - Speaker spkr = maps.second; - + for (auto& [key, spkr] : speakers) { if (coordinate == "cartesian") { tmp_distan = abs(spkr.getX() - pos[0]) * mask[0] + abs(spkr.getY() - pos[1]) * mask[1] + abs(spkr.getZ() - pos[2]) * mask[2]; @@ -255,7 +242,7 @@ Pair SpeakerManager::get_indexs_by_geometry(std::vector pos, std::vector< return std::make_pair(smallest, second_small); } -Index SpeakerManager::getIndexesByTrigger(Param trigger, Mode mode) +Index SpeakerManager::getIndexesByTrigger(Param trigger, TriggerMode mode) { // just return the original one when trigger doesn't close to 1.0 if (!isEqualTo1(trigger, TRIGGER_THRESHOLD)) @@ -304,7 +291,7 @@ void SpeakerManager::setActiveSpeakers(std::string action, Indexes spkrIdxes) _delActiveSpeakerIndexs(spkrIdxes); } else { - logger->logWarning("SpeakerManager::setActiveSpeakers unknown action " + action); + logger.logWarning("SpeakerManager::setActiveSpeakers unknown action " + action); } #ifdef TESTMODE @@ -317,7 +304,7 @@ void SpeakerManager::setTrajectoryVector(Indexes spkrIdxes) Indexes tmpTrajVector; for (size_t i = 0; i < spkrIdxes.size(); ++i) { if (!isInVec(spkrIdxes[i], actvSpkIdx)) { - logger->logError( + logger.logError( formatString("SpeakerManager: speaker %d is not activated!", spkrIdxes[i])); return; } @@ -343,7 +330,7 @@ void SpeakerManager::setTopoMatrix(std::string action, Indexes spkrIdxes) _delTopoMatrixIndexes(spkrIdxes); } else { - logger->logWarning("SpeakerManager::setTopoMatrix unknown action " + action); + logger.logWarning("SpeakerManager::setTopoMatrix unknown action " + action); } #ifdef TESTMODE printTopoMatrix(); @@ -353,7 +340,7 @@ void SpeakerManager::setTopoMatrix(std::string action, Indexes spkrIdxes) void SpeakerManager::setCurrentSpeaker(Index newIdx) { if (!isInVec(newIdx, actvSpkIdx)) { - logger->logError(formatString("SpeakerManager: speaker %d is not activated!", newIdx)); + logger.logError(formatString("SpeakerManager: speaker %d is not activated!", newIdx)); return; } else { @@ -361,14 +348,14 @@ void SpeakerManager::setCurrentSpeaker(Index newIdx) } #ifdef TESTMODE - logger->logDebug(formatString("EnvelopeGenerator::initialize currIdx %d", currIdx)); + logger.logDebug(formatString("EnvelopeGenerator::initialize currIdx %d", currIdx)); #endif // TESTMODE } bool SpeakerManager::_isActivated(Index idx) { if (!isInVec(idx, actvSpkIdx)) { - logger->logError(formatString("Speaker %d is not activated!", idx)); + logger.logError(formatString("Speaker %d is not activated!", idx)); return false; } return true; @@ -431,23 +418,22 @@ void SpeakerManager::printParameters() void SpeakerManager::printActiveSpeakerIndexs() { - logger->logInfo("Active Speakers: "); - logger->logInfo(" " + formatVector(actvSpkIdx)); + logger.logInfo("Active Speakers: "); + logger.logInfo(" " + formatVector(actvSpkIdx)); } void SpeakerManager::printTopoMatrix() { - logger->logInfo("Topological Matrix: "); - for (auto it = topoMatrix.begin(); it != topoMatrix.end(); ++it) { - logger->logInfo(" " + std::to_string(it->first) + " | " + - formatVector(it->second)); + logger.logInfo("Topological Matrix: "); + for (const auto& [idx, connections] : topoMatrix) { + logger.logInfo(" " + std::to_string(idx) + " | " + formatVector(connections)); } } void SpeakerManager::printTrajectoryVector() { - logger->logInfo("Trajectory Vector: "); - logger->logInfo(" " + formatVector(trajVector)); + logger.logInfo("Trajectory Vector: "); + logger.logInfo(" " + formatVector(trajVector)); } void SpeakerManager::_initDistanceMatrix() @@ -512,11 +498,11 @@ Cartesian SpeakerManager::_spherical2cartesian(Spherical spherical) { // test Cartesian cartesian; - cartesian.x = spherical.distance * cos(spherical.elevation / 180.0 * PI) * - cos(spherical.azimuth / 180.0 * PI); - cartesian.y = spherical.distance * cos(spherical.elevation / 180.0 * PI) * - sin(spherical.azimuth / 180.0 * PI); - cartesian.z = spherical.distance * sin(spherical.elevation / 180.0 * PI); + cartesian.x = spherical.distance * cos(spherical.elevation / 180.0 * pi) * + cos(spherical.azimuth / 180.0 * pi); + cartesian.y = spherical.distance * cos(spherical.elevation / 180.0 * pi) * + sin(spherical.azimuth / 180.0 * pi); + cartesian.z = spherical.distance * sin(spherical.elevation / 180.0 * pi); return cartesian; } @@ -526,8 +512,8 @@ Spherical SpeakerManager::_cartesian2spherical(Cartesian cartesian) Spherical spherical; spherical.distance = sqrt(cartesian.x * cartesian.x + cartesian.y * cartesian.y + cartesian.z * cartesian.z); - spherical.azimuth = atan2(cartesian.y, cartesian.x) / PI * 180.0; - spherical.elevation = asin(cartesian.z / spherical.distance) / PI * 180.0; + spherical.azimuth = atan2(cartesian.y, cartesian.x) / pi * 180.0; + spherical.elevation = asin(cartesian.z / spherical.distance) / pi * 180.0; return spherical; } @@ -538,14 +524,14 @@ void SpeakerManager::_setActiveSpeakerIndexs(Indexes spkrIdxes) for (size_t i = 0; i < spkrIdxes.size(); ++i) { auto it = speakers.find(spkrIdxes[i]); if (it == speakers.end()) { - logger->logError(formatString("SpeakerManager::_set_actvSpkIdx_indexs unknow " - "speaker index %d!", - spkrIdxes[i])); + logger.logError(formatString("SpeakerManager::_set_actvSpkIdx_indexs unknow " + "speaker index %d!", + spkrIdxes[i])); return; } // add to actvSpkIdx if (isInVec(spkrIdxes[i], actvSpkIdx)) { - logger->logWarning( + logger.logWarning( formatString("SpeakerManager: index %d already added, ignored", spkrIdxes[i])); } else { @@ -557,15 +543,14 @@ void SpeakerManager::_setActiveSpeakerIndexs(Indexes spkrIdxes) _initDistanceMatrix(); trajVector.clear(); - for (size_t i = 0; i < actvSpkIdx.size(); ++i) { - trajVector.push_back(actvSpkIdx[i]); + for (auto idx : actvSpkIdx) { + trajVector.push_back(idx); } std::sort(trajVector.begin(), trajVector.end()); topoMatrix.clear(); - for (size_t i = 0; i < actvSpkIdx.size(); ++i) { - Indexes tmp_idx = actvSpkIdx; - topoMatrix[actvSpkIdx[i]] = tmp_idx; + for (auto idx : actvSpkIdx) { + topoMatrix[idx] = actvSpkIdx; // copy } } @@ -574,12 +559,12 @@ void SpeakerManager::_addActiveSpeakerIndexs(Indexes spkrIdxes) for (size_t i = 0; i < spkrIdxes.size(); ++i) { // check if the input index is valid if (!isInKey(spkrIdxes[i], speakers)) { - logger->logError("SpeakerManager unknow speaker index!"); + logger.logError("SpeakerManager unknow speaker index!"); return; } // add to actvSpkIdx if (isInVec(spkrIdxes[i], actvSpkIdx)) { - logger->logWarning( + logger.logWarning( formatString("SpeakerManager: index %d already added, ignored", spkrIdxes[i])); } else { @@ -593,12 +578,12 @@ void SpeakerManager::_delActiveSpeakerIndexs(Indexes spkrIdxes) for (size_t i = 0; i < spkrIdxes.size(); ++i) { // check if the input index is valid if (!isInKey(spkrIdxes[i], speakers)) { - logger->logError("SpeakerManager unknow speaker index!"); + logger.logError("SpeakerManager unknow speaker index!"); return; } // remove from actvSpkIdx if (!isInVec(spkrIdxes[i], actvSpkIdx)) { - logger->logWarning( + logger.logWarning( formatString("SpeakerManager index %d already removed, ignored", spkrIdxes[i])); } else { @@ -607,7 +592,7 @@ void SpeakerManager::_delActiveSpeakerIndexs(Indexes spkrIdxes) } // remove from trajVector if (!isInVec(spkrIdxes[i], trajVector)) { - logger->logWarning( + logger.logWarning( formatString("SpeakerManager index %d already removed, ignored", spkrIdxes[i])); } else { @@ -618,10 +603,10 @@ void SpeakerManager::_delActiveSpeakerIndexs(Indexes spkrIdxes) if (isInKey(spkrIdxes[i], topoMatrix)) { topoMatrix.erase(spkrIdxes[i]); } - for (auto it = topoMatrix.begin(); it != topoMatrix.end(); ++it) { - if (isInVec(spkrIdxes[i], it->second)) { - it->second.erase(std::remove(it->second.begin(), it->second.end(), spkrIdxes[i]), - it->second.end()); + for (auto& [idx, connections] : topoMatrix) { + if (isInVec(spkrIdxes[i], connections)) { + connections.erase(std::remove(connections.begin(), connections.end(), spkrIdxes[i]), + connections.end()); } } } diff --git a/core/src/utils/frequencytransformer.cpp b/core/src/utils/frequencytransformer.cpp index e7c431a..ef94013 100644 --- a/core/src/utils/frequencytransformer.cpp +++ b/core/src/utils/frequencytransformer.cpp @@ -1,9 +1,10 @@ #include "frequencytransformer.h" using namespace zerr; -FrequencyTransformer::FrequencyTransformer(int L) { +FrequencyTransformer::FrequencyTransformer(int L) +{ frame_size = L; - fft_size = (L / 2 + 1); + fft_size = (L / 2 + 1); fft_in = new double[frame_size]; for (int i = 0; i < frame_size; i++) { @@ -14,7 +15,7 @@ FrequencyTransformer::FrequencyTransformer(int L) { power_spec.resize(fft_size); - p_fft = fftw_plan_dft_r2c_1d(frame_size, fft_in, fft_out, FFTW_ESTIMATE); + p_fft = fftw_plan_dft_r2c_1d(frame_size, fft_in, fft_out, FFTW_ESTIMATE); p_ifft = fftw_plan_dft_c2r_1d(frame_size, fft_out, fft_in, FFTW_ESTIMATE); } @@ -22,15 +23,16 @@ void FrequencyTransformer::fft() { fftw_execute(p_fft); } void FrequencyTransformer::ifft() { fftw_execute(p_ifft); } -void FrequencyTransformer::power_spectrum() { +void FrequencyTransformer::power_spectrum() +{ for (int i = 0; i < fft_size; i++) { - power_spec[i] = - (1.0 / (2.0 * (float)fft_size)) * - (fft_out[i][0] * fft_out[i][0] + fft_out[i][1] * fft_out[i][1]); + power_spec[i] = (1.0 / (2.0 * static_cast(fft_size))) * + (fft_out[i][0] * fft_out[i][0] + fft_out[i][1] * fft_out[i][1]); } } -void FrequencyTransformer::windowing() { +void FrequencyTransformer::windowing() +{ for (int i = 0; i < frame_size; i++) fft_in[i] = fft_in[i] * get_hann_sample(i, frame_size); } @@ -39,4 +41,4 @@ double* FrequencyTransformer::fft_input() { return fft_in; } fftw_complex* FrequencyTransformer::fft_output() { return fft_out; } -AudioBuffer FrequencyTransformer::get_power_spectrum() { return power_spec; } \ No newline at end of file +Samples FrequencyTransformer::get_power_spectrum() { return power_spec; } diff --git a/core/src/utils/linearinterpolator.cpp b/core/src/utils/linearinterpolator.cpp index 6bed610..956d2c6 100644 --- a/core/src/utils/linearinterpolator.cpp +++ b/core/src/utils/linearinterpolator.cpp @@ -1,21 +1,25 @@ #include "linearinterpolator.h" using namespace zerr; -void LinearInterpolator::set_value(Param start, Param stop, int len) { +void LinearInterpolator::set_value(Param start, Param stop, int len) +{ start_val = start; - stop_val = stop; + stop_val = stop; - n_steps = len; + n_steps = len; position = 0; } -Param LinearInterpolator::get_value() { - inter_val = start_val + ((Param)position * - ((stop_val - start_val) / (Param)(n_steps - 1))); +Param LinearInterpolator::get_value() +{ + inter_val = start_val + (static_cast(position) * + ((stop_val - start_val) / static_cast(n_steps - 1))); return inter_val; } -void LinearInterpolator::next_step() { - if (position < n_steps) position += 1; +void LinearInterpolator::next_step() +{ + if (position < n_steps) + position += 1; } diff --git a/core/src/utils/onsetdetector.cpp b/core/src/utils/onsetdetector.cpp index 4334712..b1ee357 100644 --- a/core/src/utils/onsetdetector.cpp +++ b/core/src/utils/onsetdetector.cpp @@ -8,9 +8,9 @@ */ #include "onsetdetector.h" -using zerr::Block; using zerr::Index; using zerr::OnsetDetector; +using zerr::Samples; void OnsetDetector::setDebounceThreshold(int newThreshold) { @@ -18,7 +18,7 @@ void OnsetDetector::setDebounceThreshold(int newThreshold) lastOnsetPosition = -debounceThreshold; // Reset lastOnsetPosition based on new threshold } -void OnsetDetector::detectOnsetInBlock(Block& block) +void OnsetDetector::detectOnsetInBlock(Samples& block) { if (block.empty()) return; diff --git a/core/src/utils/ringbuffer.cpp b/core/src/utils/ringbuffer.cpp index 7be9e43..c7c4e6e 100644 --- a/core/src/utils/ringbuffer.cpp +++ b/core/src/utils/ringbuffer.cpp @@ -3,11 +3,11 @@ using namespace zerr; RingBuffer::RingBuffer(size_t capacity) : buffer(capacity, 0.0f), head(0), tail(0), size(0) {} -size_t RingBuffer::get_size() const { return size; } +size_t RingBuffer::get_size() const noexcept { return size; } -size_t RingBuffer::get_capacity() const { return buffer.size(); } +size_t RingBuffer::get_capacity() const noexcept { return buffer.size(); } -void RingBuffer::enqueue(const Block& block) +void RingBuffer::enqueue(const Samples& block) { assert(block.size() <= buffer.size() && "Block size must be smaller than buffer size."); diff --git a/core/src/utils/utils.cpp b/core/src/utils/utils.cpp index 5b15335..3535432 100644 --- a/core/src/utils/utils.cpp +++ b/core/src/utils/utils.cpp @@ -38,13 +38,13 @@ bool isEqualTo1(Param value, Param epsilon) { return std::abs(value - 1.0) < eps bool isEqualTo0(Param value, Param epsilon) { return std::abs(value) < epsilon; } -template bool isInVec(T element, std::vector vector) +template bool isInVec(T element, const std::vector& vector) { auto it = std::find(vector.begin(), vector.end(), element); return it == vector.end() ? false : true; } // explicit instantiation required for PD -template bool isInVec(int element, std::vector vector); +template bool isInVec(int element, const std::vector& vector); Samples applyMovingAverage(const Samples& segment, int windowSize) { diff --git a/docs/Doxyfile b/docs/Doxyfile new file mode 100644 index 0000000..7c470d4 --- /dev/null +++ b/docs/Doxyfile @@ -0,0 +1,51 @@ +# Doxyfile for Zerr* core library documentation +# Run from project root: doxygen docs/Doxyfile + +PROJECT_NAME = "Zerr*" +PROJECT_BRIEF = "Channel-based audio spatialization system" +PROJECT_NUMBER = + +OUTPUT_DIRECTORY = docs/build/ +CREATE_SUBDIRS = NO + +# Input sources (relative to project root) +INPUT = core/include/ README.md +RECURSIVE = YES +FILE_PATTERNS = *.h +USE_MDFILE_AS_MAINPAGE = README.md + +# Extraction settings +EXTRACT_ALL = NO +EXTRACT_PRIVATE = NO +EXTRACT_STATIC = YES + +# Build settings +JAVADOC_AUTOBRIEF = YES +BUILTIN_STL_SUPPORT = YES +SORT_MEMBER_DOCS = YES + +# Output formats +GENERATE_HTML = YES +GENERATE_LATEX = NO + +# Doxygen Awesome theme +GENERATE_TREEVIEW = YES +DISABLE_INDEX = NO +FULL_SIDEBAR = NO +HTML_COLORSTYLE = LIGHT +HTML_HEADER = docs/header.html +HTML_EXTRA_STYLESHEET = docs/doxygen-awesome-css/doxygen-awesome.css \ + docs/doxygen-awesome-css/doxygen-awesome-sidebar-only.css \ + docs/doxygen-awesome-css/doxygen-awesome-sidebar-only-darkmode-toggle.css +HTML_EXTRA_FILES = docs/doxygen-awesome-css/doxygen-awesome-darkmode-toggle.js \ + docs/doxygen-awesome-css/doxygen-awesome-fragment-copy-button.js \ + docs/doxygen-awesome-css/doxygen-awesome-paragraph-link.js \ + docs/doxygen-awesome-css/doxygen-awesome-interactive-toc.js +HTML_COPY_CLIPBOARD = NO + +# Graph settings +HAVE_DOT = NO + +# Warnings +WARN_IF_UNDOCUMENTED = YES +WARN_NO_PARAMDOC = NO diff --git a/docs/design/core-modernization-changes.md b/docs/design/core-modernization-changes.md new file mode 100644 index 0000000..8a5b61f --- /dev/null +++ b/docs/design/core-modernization-changes.md @@ -0,0 +1,138 @@ +# `core_modernization` vs `main` — Change Comparison Report + +_Branch: `core_modernization` · compared against `origin/main` · hand-authored doc under `docs/design/`_ + +## Overview + +**22 commits** transforming the codebase, spanning **52 files** (`+1,443 / −1,272`). The `core/` +library alone: **38 files, `+884 / −1,071`** — a **net reduction of ~187 lines** despite adding +documentation. + +**Nature of the change:** this is a **modernization / refactor branch, not a feature branch.** No +audio algorithm was rewritten. When whitespace/reformatting is excluded, the actual logic churn in +`core/src` is modest (`+220 / −263`) — the bulk is C++ modernization, boilerplate removal, and +tooling. There are, however, a few **real semantic changes** (memory ownership, mode typing, a Max +macro-clash fix) flagged in the risk section. + +--- + +## 1. Type System Overhaul (`types.h`) + +The type vocabulary was consolidated and hardened: + +| Removed | Replaced with | +|---|---| +| `Complex`, `FFTBuffer`, `SpecBuffer` | (FFT now uses `fftw_complex` / `Samples` directly) | +| `Block`, `AudioBuffer`, `AudioBuffers` | unified into `Samples` / `Blocks` | +| `FeatureName` (alias) | `std::string` directly | +| `Mode = std::string` | strong enums `GenMode` / `TriggerMode` | +| `t_featureNames` (C-style PD struct) | removed (was outdated) | +| `Pair` | renamed `SpeakerPair` (clarity) | +| `typedef struct {...} SystemConfigs` | `struct SystemConfigs {...}` | + +**New:** `enum class GenMode { Trigger, Trajectory }`, `enum class TriggerMode { Random }`, and a +validating `parseGenMode(const std::string&)` helper that **throws `std::invalid_argument`** on bad +input — replacing the previous unchecked string comparison. + +## 2. Compile-Time Constants (`configs.h`) + +All preprocessor macros → **namespaced `inline constexpr`** inside `namespace zerr`: + +```cpp +#define PI 3.14159265 → inline constexpr double pi = 3.14159265; +#define AUDIO_BUFFER_SIZE... → inline constexpr std::size_t AUDIO_BUFFER_SIZE = 2048; +``` + +- **`PI` → `pi`**: deliberately lowercased to **avoid a macro clash with the Max/MSP SDK** (a + genuine Windows/Max build fix, not cosmetic). +- **New extracted constants** replacing magic numbers previously buried in `.cpp` files: + `DEFAULT_ONSET_DEBOUNCE = 50`, `LOG_FLOOR = 1e-10`, `DEFAULT_ROLLOFF_PERCENT = 0.85`. + +## 3. Metadata Boilerplate Removal (all 8 features + base class) + +The `FeatureExtractor` base dropped three pure-virtual getters — `get_name()`, `get_category()`, +`get_description()` — and every feature (Centroid, Rolloff, RMS, …) dropped its +`static const std::string name/category/description` members and their accessors. Descriptions were +**moved into Doxygen comments** instead. This is the single largest source of the line reduction +(each feature header shrank ~30–50%). + +Also in the base class: the typo **`set_initialize_statue` → `set_initialize_status`** was fixed. + +## 4. Memory Ownership: Raw Pointers → RAII ⚠️ *(semantic change)* + +The most meaningful behavioral change. `EnvelopeGenerator` and `SpeakerManager`/`Speaker`: + +```cpp +// before +Logger* logger; logger = new Logger(); +SpeakerManager* speakerManager; +OnsetDetector* onsetDetector; +~EnvelopeGenerator(); // manual cleanup + +// after +Logger logger; // value member +std::unique_ptr speakerManager; // owned +std::unique_ptr onsetDetector; +~EnvelopeGenerator() = default; // RAII +``` + +This eliminates manual `new`/`delete` and closes potential leaks. A code comment now flags that +**member declaration order matters** (config members must precede the objects that depend on them). + +## 5. Const-Correctness, `noexcept`, Pass-by-Reference + +- Getters marked `const noexcept` (e.g. `is_initialized()`, `get_frame_size()`). +- Hot-path signatures changed `fetch(AudioInputs)` → **`fetch(const AudioInputs&)`** across the base + class and all features — removes a per-block copy of the audio input struct. +- `EnvelopeGenerator(..., Mode)` → `(..., GenMode)`; `getIndexesByTrigger(Param, Mode)` → + `(Param, TriggerMode)`. + +## 6. Modern C++ Idioms (mechanical, behavior-preserving) + +- **Raw index loops → range-for**: `for (size_t i...) actvSpkIdx[i]` → `for (auto idx : actvSpkIdx)`. +- **Structured bindings**: `for (auto& [key, spkr] : speakers)` replacing `.first`/`.second`. +- **`typedef` → `using`**; **`#define` → `constexpr`**; **C-cast/raw ptr → `static_cast`**. +- Member-initializer lists instead of assignment in constructor bodies + (`: index(index), position(position)`). +- `clear()+assign` → `move`. + +## 7. Tooling & Infrastructure + +| Area | Change | +|---|---| +| **Doxygen** | New `docs/Doxyfile`, `docs/header.html`, `doxygen-awesome-css` submodule; docs reorganized under `docs/` | +| **CI** | New `.github/workflows/deploy-doxygen.yml` — auto-builds & deploys API docs to **GitHub Pages** on push to `main` touching `core/include/**` or `docs/**` | +| **build.sh** | Added **`-c` clean flag** with per-target `clean_*` functions, added `core` as an explicit target, and target-name validation | +| **.gitignore** | Ignore `docs/build/` (generated docs) | +| **docs/design** | The project status report added previously | + +## 8. Wrapper Adaptations + +The PureData and Max/MSP wrappers were updated to match the new core API (enum modes instead of +strings, removed metadata getters, const-ref signatures): `puredata/src/zerr_features~.cpp` +(105 lines), `zerr_envelopes.cpp`, `zerr_features.cpp/.h`, +`maxmsp/.../zerr_envelopes.hpp`, and the `zerr_envelopes~-help.pd` help patch. + +--- + +## Behavioral Impact & Risk Assessment + +| Change | Behavior risk | +|---|---| +| Pointer → `unique_ptr`/value (RAII) | **Low-positive** — fixes lifetime/leak risk; declaration order now load-bearing | +| `Mode` string → strong enums + `parseGenMode` throwing | **Low** — adds validation (new throw path where invalid mode strings were previously silently accepted) | +| `PI` → `pi`, `#define` → `constexpr` | **Positive** — resolves Max SDK macro clash | +| `fetch` by const-ref | **None** — pure optimization | +| Metadata getter removal | **API change** — anything calling `get_name()`/etc. no longer compiles (wrappers already updated) | +| Range-for / structured bindings / formatting | **None** — behavior-preserving | + +**No formal test suite exists**, so correctness rests on manual help-patch testing. Already verified +during the build fix: **core compiles cleanly** against this branch and **all 4 PureData externals +link and build**. + +--- + +**Bottom line:** a disciplined, net-negative-LOC modernization pass — stronger types, RAII memory +safety, const-correctness, constants over macros, and a full Doxygen/CI documentation pipeline — +with three small but real semantic improvements (RAII ownership, validated enum modes, Max +macro-clash fix) and no intended algorithmic changes. diff --git a/docs/design/dependency-fallbacks.md b/docs/design/dependency-fallbacks.md new file mode 100644 index 0000000..ced0000 --- /dev/null +++ b/docs/design/dependency-fallbacks.md @@ -0,0 +1,178 @@ +# Dependency Fallbacks — What Is Valid, and What Breaks + +_Reference. Not a plan of record — nothing here is scheduled work._ + +**Conan is the only supported way to resolve `fftw` and `yaml-cpp` in this repo.** This document +exists for whoever is later asked to add a non-conan path. Read it before touching any +`find_package` call, the `CONAN_*` variables in `puredata/Makefile`, or `conanfile.txt`. + +There is no `USE_SYSTEM_DEPS`-style fallback in the tree by design: an untested second resolve path +silently reintroduces the ABI split that [`repo-audit-2026-07-30.md`](repo-audit-2026-07-30.md) §4 +was written to close. + +## 1. What conan supplies today + +One `conan install` at the repo root produces three consumption surfaces: + +| Surface | Generator | Consumed by | +| --- | --- | --- | +| `build/conan_toolchain.cmake` | `CMakeToolchain` | `core/CMakeLists.txt`, `maxmsp/CMakeLists.txt` | +| `build/*-config.cmake` | `CMakeDeps` | the `find_package(FFTW3)` / `find_package(yaml-cpp)` calls in both | +| `build/conandeps.mk` | `MakeDeps` | `puredata/Makefile` | + +`puredata/Makefile` consumes exactly five variables from `conandeps.mk`: + +``` +CONAN_INCLUDE_DIRS_YAML_CPP CONAN_LIB_DIRS_YAML_CPP CONAN_LIBS_YAML_CPP +CONAN_INCLUDE_DIRS_FFTW CONAN_LIB_DIRS_FFTW +``` + +**A fallback must cover all three surfaces.** The Pd wrapper is the hard one: pd-lib-builder has no +`find_package`, so it needs literal `-I` / `-L` / `-l` strings. Any proposal that only handles the +CMake side leaves the Pd externals resolving from a different provider than the core they link — +which is the exact failure mode the root `conanfile.txt` exists to prevent (§3.8). + +## 2. Viable fallbacks, ranked + +| Option | CMake surface | Makefile surface | Verdict | +| --- | --- | --- | --- | +| **vcpkg toolchain file** | drop-in replacement for `conan_toolchain.cmake` + config files | none — hand-write the `-I/-L/-l` | Best CMake story, still needs Makefile work | +| **pkg-config** | `pkg_check_modules` | `$(shell pkg-config --cflags yaml-cpp)` | Only option that maps *cleanly* onto pd-lib-builder. `yaml-cpp.pc` and `fftw3.pc` both ship | +| **System packages + `find_package(CONFIG)`** | homebrew / apt / vcpkg configs | hardcoded paths or pkg-config | Workable on Linux; on macOS collides with §3.3 and §3.4 | +| **`FetchContent` / `add_subdirectory` from source** | builds in-tree | in-tree paths | **Avoid** — hits the CMake 4 wall (§3.5) | + +## 3. What breaks + +### 3.1 Target names are not stable across providers + +Verified against the generated files in `build/` for the pinned revisions: + +| Provider | yaml-cpp targets | FFTW targets | +| --- | --- | --- | +| conan `yaml-cpp/0.8.0` (CMakeDeps) | `yaml-cpp` **and** `yaml-cpp::yaml-cpp` | — | +| upstream yaml-cpp 0.8 config | `yaml-cpp::yaml-cpp` only | — | +| conan `fftw/3.3.10` (CMakeDeps) | — | package aggregate `fftw::fftw`; components `FFTW3::fftw3`, `FFTW3::fftw3f`, `FFTW3::fftw3l` | +| various `FindFFTW` modules | — | `FFTW3::fftw3` or `fftw::fftw`, inconsistently | + +Note the shape difference: conan's fftw recipe exposes the *package* as `fftw::fftw` but the usable +libraries as `FFTW3::*` components — the namespaces do not match, which is the trap. Reproduce the +list on any resolve with: + +```bash +grep -rhoE "add_library\([A-Za-z0-9_:.-]+ INTERFACE IMPORTED\)" build/*.cmake | grep -v _DEPS_TARGET +grep -rhoE "FFTW3::[A-Za-z0-9]+" build/*.cmake | sort -u +``` + +Current call sites, both of which must be audited together when the provider changes: + +- `core/CMakeLists.txt` — `target_link_libraries(zerr_core_static PUBLIC yaml-cpp FFTW3::fftw3)` +- `maxmsp/source/projects/*/CMakeLists.txt` — `target_link_libraries(… ${ZERR_LIBRARY} yaml-cpp FFTW3::fftw3)` + +> **`fftw3` vs `fftw3f`.** `Sample` is `double` (`core/include/utils/types.h`), so this must be the +> double-precision library. Conan exposes `FFTW3::fftw3f` (single) and `FFTW3::fftw3l` (long double) +> as siblings of the one we want, so a one-character slip links cleanly and then produces silent +> garbage in the spectral features rather than an error. + +### 3.2 `YAML_CPP_STATIC_DEFINE` + +Set in **two separate places** — `core/CMakeLists.txt` (`add_definitions`) and `puredata/Makefile` +(`cflags`). It is required when linking yaml-cpp **statically**; without it the headers declare +`dllimport` on Windows. + +If a fallback provides a **shared** yaml-cpp, this define must be removed from both. Change one and +not the other and you get a link failure in whichever target you forgot. + +### 3.3 Static vs shared + +The build products are loadable bundles (`.pd_darwin`, `.mxo`, `.dll`). A shared `fftw` or +`yaml-cpp` becomes a runtime dependency the end user must install, and on macOS an `@rpath` that +will not resolve inside Pd or Max. **Keep both static.** Homebrew ships shared by default — this is +the most likely way a well-meaning fallback breaks end users rather than the build. + +Check for accidental shared linkage on a built external: + +```bash +otool -L puredata/zerr_features~.pd_darwin | grep -Ei "yaml|fftw" # expect no output +``` + +### 3.4 macOS deployment target + +`core/CMakeLists.txt` pins `CMAKE_OSX_DEPLOYMENT_TARGET` to 10.13 — read the comment there, it +explains why the value must not be raised in isolation. Homebrew binaries are built for the host's +macOS version, so linking them reintroduces the exact `minos` mismatch that made this a problem in +the first place. + +Verify after any dependency change: + +```bash +otool -l core/lib/libzerr_core.a | grep -A4 LC_BUILD_VERSION | grep minos | sort -u +``` + +Expect `minos 11.0` on Apple Silicon — clang clamps 10.13 up to arm64's own floor. `15.0` means the +pin is not reaching the compile. + +> Do **not** pin `os.version` in the conan profile instead. It is part of the `package_id`, so no +> ConanCenter prebuilt would ever match and every dependency would rebuild from source, in CI too. + +Note that the prebuilt yaml-cpp in the conan cache is itself built at a newer floor, so the link step +emits `object file … was built for newer 'macOS' version (15.0) than being linked (11.0)` warnings. +Those are expected and come from upstream's binary, not from this project's settings. + +### 3.5 CMake 4 rejects the bundled `CMakeLists.txt` + +`conanfile.txt` pins recipe revisions (`fftw#3b4cceb8…`, `yaml-cpp#1aa37121…`) because both +libraries' own `CMakeLists.txt` declare `cmake_minimum_required` below 3.5, which CMake 4.x removed +support for. The pinned ConanCenter revisions set `CMAKE_POLICY_VERSION_MINIMUM=3.5` as a *cache* +variable — the only placement that works, since line 1 runs before the toolchain file loads. + +Any from-source fallback — `FetchContent`, vcpkg building from source, an unpatched distro package — +hits the same wall. This is the main reason option 4 in §2 is not viable. + +### 3.6 Architecture + +`maxmsp/CMakeLists.txt` sets `CMAKE_OSX_ARCHITECTURES` to `x86_64;arm64` when `C74_BUILD_FAT` is on +(Xcode 12+). Homebrew and vcpkg provide single-arch libraries, so a fat build fails to link the +missing slice. Conan resolves per-arch; a fat build needs both resolved. + +### 3.7 Windows has two incompatible toolchains + +- **PureData** builds with MinGW-w64 gcc 13 / `libstdc++11` via `profiles/mingw`, driven by + `mingw32-make`. +- **Max/MSP** needs MSVC to produce `.mxe64` — a different runtime and C++ ABI. + +These cannot share one conan resolve or one CMake configure. `build.sh` refuses `maxmsp` outright on +Windows for this reason, and it is also why there is no CMake superbuild in this repo: one configure +means one toolchain. + +Note that `profiles/mingw` pins `tools.build:compiler_executables` to the `x86_64-w64-mingw32-*` +names, and the core CI job deliberately bypasses the profile for that reason — see the comment in +`.github/workflows/build-zerr-core-static-library.yml`. + +### 3.8 Core and wrappers must come from one resolve + +This is the invariant everything else serves. `core/`, `puredata/` and `maxmsp/` each used to run +their own `conan install`; three independent resolutions meant three profile evaluations, which is +how the core came to be compiled for a different macOS deployment target than the wrappers linked +against. + +A fallback that supplies the CMake targets from vcpkg while leaving `puredata/Makefile` on conan +recreates that split exactly. + +## 4. Checklist before shipping a fallback + +- [ ] All three surfaces in §1 covered, Makefile included +- [ ] Target names verified against the new provider (§3.1), double-precision FFTW confirmed +- [ ] Static vs shared decided; `YAML_CPP_STATIC_DEFINE` consistent in **both** places (§3.2) +- [ ] No shared `yaml-cpp`/`fftw` in `otool -L` on a built external (§3.3) +- [ ] `minos` check passes on macOS (§3.4) +- [ ] Architecture matches the wrappers' expectation (§3.6) +- [ ] Core and every wrapper resolve from the same provider (§3.8) +- [ ] `./build.sh -c deps core puredata maxmsp` then a full rebuild succeeds +- [ ] Pd and Max help patches load the rebuilt externals + +## 5. Why conan, and why it stays + +See [`repo-audit-2026-07-30.md`](repo-audit-2026-07-30.md) §2.2 for the duplicated-conanfile problem +and §4 for the fix. The short version: conan is the only mechanism currently giving all three +consumption surfaces from a single pinned, committed profile — and the committed profiles in +`profiles/` are what make the core/wrapper ABI agreement reproducible across machines and CI. diff --git a/docs/design/project-status-report.md b/docs/design/project-status-report.md new file mode 100644 index 0000000..ff27f8c --- /dev/null +++ b/docs/design/project-status-report.md @@ -0,0 +1,142 @@ +# Zerr\* — Project Status & Design Report + +_Generated: 2026-07-06 · Branch analysed: `dependency_fix` (see §1 for rename)_ + +> Hand-authored design/status docs live under `docs/design/`. +> Auto-generated API docs (Doxygen) are emitted to `docs/build/` and are **not** part of this tree. + +--- + +## 1. Current Working Status + +**Active branch:** `dependency_fix` — working tree clean, **21 commits ahead of `origin/main`** +(unmerged). It is the most advanced local line of work: `dev`, `improve_deps_manage`, +`windows_compile`, and `max_doc` are all strictly *behind* it (by 51, 51, 34, 60 commits). + +**What the branch actually contains** — despite the name, the commits are a **code-quality / +modernization sweep**, not dependency changes: + +- Strong-typed enums (`Mode` split into `GenMode` / `TriggerMode`) +- `const` / `noexcept` correctness, removed magic numbers, `const AudioInputs&` to remove copies, + `move` instead of clear+assign +- Type simplification (`Pair` → `SpeakerPair`, removed unused types) +- Doxygen documentation added (`docs/Doxyfile`, `doxygen-awesome-css` submodule), metadata + boilerplate stripped from headers + +⚠️ **Branch-name vs. content mismatch.** The branch has been re-scoped/renamed to reflect its +actual modernization + docs content (see the branch rename performed alongside this report). + +**Branch sprawl:** 6 local + 12 remote branches. `jack` is heavily diverged (**289 ahead / +13 behind main**). Several look abandoned or parallel (`PD_modules_test_Makefiles`, `core_to_lib`, +`add_cmake`). Consolidation recommended. + +--- + +## 2. Build Status — ✅ Fixed (2026-07-06) + +**Problem found:** the repo had been relocated (from `…/Documents/workspace/zerr_workspace/Zerr` +to `…/Workspace/zerr-060823/Zerr`). The stale `core/build/CMakeCache.txt` still referenced the old +absolute source path, so any incremental `cmake --build .` failed with +`The source directory "…/zerr_workspace/Zerr/core" does not exist`. Build outputs +(`core/build`, `core/lib`, `libzerr_core.a`) are gitignored, so nothing stale was committed — the +breakage was local build state only. + +**Fix applied & verified:** + +1. Wiped `core/build` and `core/lib`. +2. `conan install` (conan 2.13.0; deps `fftw/3.3.10`, `yaml-cpp/0.8.0` served from cache). +3. `cmake` configure + build → `libzerr_core.a` compiles **cleanly** against current source. +4. `make install` → reinstalled to `core/lib`. +5. **Downstream check:** rebuilt all 4 PureData externals + (`zerr_features~`, `zerr_envelopes~`, `zerr_combinator~`, `zerr_disperser~`) — they link + against the fresh core (arm64, `.pd_darwin`). + +**Minor build-hygiene note:** the core objects are compiled for a newer macOS deployment target +(15.0) than pd-lib-builder links against (11.0), producing harmless `ld` warnings. Consider pinning +a common `MACOSX_DEPLOYMENT_TARGET` (or `CMAKE_OSX_DEPLOYMENT_TARGET`) across core and wrappers to +silence them and keep release binaries portable. + +--- + +## 3. Design Overview + +Platform-independent C++17 static core (`zerr_core`, namespace `zerr`) wrapped per host environment. + +``` +Audio In → FeatureExtractor → FeatureBank → FeatureProcessor + ↓ +Speakers ← AudioDisperser ← EnvelopeCombinator ← EnvelopeGenerator +``` + +Audio features drive spatial distribution — no manual positioning. + +**Core subsystems (`core/`):** + +- **Features (8):** spectral — Centroid, Rolloff, Flatness, Flux; temporal — RMS, + ZeroCrossingRate, CrestFactor, ZeroCrossings. All inherit `FeatureExtractor`. +- **Modules (7):** SpeakerManager, FeatureBank, FeatureProcessor, EnvelopeGenerator, + EnvelopeCombinator, AudioDisperser. +- **Utils:** `types.h`, `configs.h`, FrequencyTransformer (FFTW3 wrapper), RingBuffer, + OnsetDetector, LinearInterpolator, Logger. + +**Type model (`types.h`):** `Sample=double`, `Param=float`, `Index=int`; +`AudioInputs{block, wave, spec}`; strong enums `GenMode{Trigger, Trajectory}` / +`TriggerMode{Random}`; `Position` carries both `Cartesian` and `Spherical`; +`TopoMatrix = map`. + +**Dependencies:** Conan 2.x → `fftw/3.3.10`, `yaml-cpp/0.8.0`. Speaker layouts are YAML in +`configs/` (quad_4, ring_8, line_16, ambisonic_21, …). + +**Platform wrappers & support (from README):** + +| Env | Linux | macOS | Windows | Build system | +| -------------------------- | :---: | :---: | :-----: | ----------------------- | +| **PureData** (4 externals) | ✅ | ✅ | ✅ | pd-lib-builder + Make | +| **Max/MSP** (5 `mc.zerr.*`)| ➖ | ✅ | 🛠️ | Min-DevKit + CMake | +| **JACK** (skeleton) | 🛠️ | 🛠️ | 🛠️ | Meson | +| **SuperCollider** | ⏳ | ⏳ | ⏳ | planned | + +--- + +## 4. Open WIP Issues & Functional Gaps + +**Core algorithm TODOs (functional, not cosmetic):** + +- `speakermanager.cpp:454` — `// TODO: seems incorrect` → **flagged likely bug**; needs + investigation. +- `speakermanager.cpp` — coordinate handling incomplete: `:102` verify Cartesian/Spherical + consistency, `:120` orientation structure, `:217` equal weighting for spherical. +- `envelopegenerator.cpp` — `:144` envelope interpolator missing, `:197` linear panning should + become a parameterized crossfade, `:207` a hardcoded `DISTANCE_SCALE` to remove/parameterize. +- `envelopecombinator.cpp:87` — possible bug: using `systemcfg.block_size` "could cause bug + (sometimes smaller)". + +**Feature completeness:** + +- **Trajectory mode** envelope interpolation is unimplemented (TODO only). +- **TriggerMode** enum has a single value (`Random`) — selection strategies not yet expanded. +- **PD message interface** for runtime control (set trajectory vector, topological matrix, + enable/disable speakers) is marked *"still in development"* in `zerr_envelopes~-help.pd`. +- **JACK client** is a 4-file skeleton (`zerr.h/.cpp`, `main.cpp`, `meson.build`), no README — + earliest-stage wrapper. + +**Testing:** no formal test suite; verification is manual via PD/Max help patches. Given the scale +of the current refactor (~1,300 lines changed across 51 files vs. `main`), this is a real +regression risk. + +**Dead code:** PD `get_port_count()` in `zerr_combinator.h`/`zerr_disperser.h` marked "remove if +not needed"; empty doc stub at `zerr_envelopes~.cpp:252`. + +--- + +## 5. Recommended Next Steps (priority order) + +1. ~~**Fix the build**~~ — ✅ done (§2). +2. ~~**Rename/re-scope `dependency_fix`**~~ — ✅ done; still needs review + merge to `main` before + it drifts further (and a matching remote-branch update). +3. **Triage `speakermanager.cpp:454`** ("seems incorrect") and the `envelopecombinator.cpp:87` + block-size concern — these are correctness risks, not polish. +4. **Prune branches** — decide the fate of `jack` (289 ahead) and the stale `origin/PD*`, + `add_cmake`, `core_to_lib` branches. +5. **Add smoke tests** for the core pipeline to protect the refactor (even a minimal harness given + "no formal test suite"). diff --git a/docs/design/repo-audit-2026-07-30.md b/docs/design/repo-audit-2026-07-30.md new file mode 100644 index 0000000..a85498a --- /dev/null +++ b/docs/design/repo-audit-2026-07-30.md @@ -0,0 +1,409 @@ +# Zerr\* — Repository Audit: Git State, Dependencies, Outstanding Work + +_Generated: 2026-07-30 · Branch analysed: `core_modernization` @ `786c098`_ + +> Supersedes the branch/commit counts in [`project-status-report.md`](project-status-report.md), +> which still refers to this branch by its former name `dependency_fix` and reports a branch +> inventory that no longer matches the repository (see §1, note). + +--- + +## 1. Git Working Status + +| | | +| --------------------------------- | --------------------------------------------- | +| Branch | `core_modernization` — working tree **clean** | +| vs `origin/core_modernization` | **1 commit ahead** (`786c098`) — unpushed | +| vs `main` | **23 ahead / 0 behind** → clean fast-forward available, never merged | + +**Branch inventory** (4 local, 4 remote): + +| Branch | Last commit | vs `core_modernization` | Verdict | +| --------------------- | -------------- | ----------------------- | -------------------------------- | +| `main` | 2026-02-14 | 23 behind | stale trunk | +| `core_modernization` | 2026-07-07 | — | active | +| `improve_deps_manage` | 2026-01-18 | 7 ahead / 53 behind | **stranded dependency work** (§2.3) | +| `max_doc` | 2025-11-11 | 1 ahead / 62 behind, remote **[gone]** | deletable | +| `origin/jack` | **2023-06-14** | 13 ahead / 312 behind | archival, pre-refactor tree | + +**Submodules** — all initialized, all on tags. No issues. + +| Submodule | Version | +| -------------------------- | -------------- | +| `puredata/pd-lib-builder` | v0.7.0 | +| `maxmsp/source/min-api` | v0.6.9-12 | +| `maxmsp/source/min-lib` | v0.6.0 | +| `docs/doxygen-awesome-css` | v2.4.1 | + +**Hooks** — `core.hooksPath=.githooks` is set, so the clang-format `pre-commit` hook is live; +`clang-format` is installed (`/opt/homebrew/opt/llvm/bin/clang-format`). + +> **Note — doc drift.** `project-status-report.md` §1 states branch `dependency_fix`, 21 commits +> ahead, "6 local + 12 remote branches", and `jack` at 289 ahead / 13 behind. Measured today: +> `core_modernization`, 23 commits ahead, 4 local + 4 remote, `jack` at 13 ahead / 312 behind. + +--- + +## 2. Dependency Issues + +### 2.1 `conan` is not installed — the live blocker + +`conan` is absent from `PATH` and is not present via pip or pipx. `build.sh` aborts immediately at +`check_tool conan`, so **no target can be configured from scratch**. + +The tree only still builds because of leftover generated state: + +- `core/build/` and `puredata/build/` contain prior conan output + (`conan_toolchain.cmake`, `conandeps.mk`, `FFTW3Config.cmake`, …) +- `~/.conan2/p` still caches the `fftw` and `yaml-cpp` packages + +Verified working in that state: + +- `cmake --build core/build` → `[100%] Built target zerr_core_static` +- `make` in `puredata/` → up to date; all 4 `.pd_darwin` externals present +- `core/lib/libzerr_core.a` present (311 KB) + +Wiping any `build/` directory, or invoking `./build.sh` at all, breaks until conan is reinstalled. + +> **Status: partly mitigated** (§4). `build.sh` now checks for `conan` only inside `install_deps`, +> so a tree with a populated root `build/` builds without it — verified. Resolving dependencies +> still requires conan, which remains uninstalled by choice; the recommended command is +> `uv tool install conan==2.13.0`. + +### 2.2 Three duplicated conanfiles + +`core/conanfile.txt`, `puredata/conanfile.txt`, and `maxmsp/conanfile.txt` each declare an identical +`[requires]` — `fftw/3.3.10`, `yaml-cpp/0.8.0` — and differ only in generator: + +| File | Generators | +| ------------------------ | ------------------------------ | +| `core/conanfile.txt` | `CMakeDeps`, `CMakeToolchain` | +| `maxmsp/conanfile.txt` | `CMakeDeps`, `CMakeToolchain` | +| `puredata/conanfile.txt` | `MakeDeps` | + +Three independent `conan install` runs, none pinning a profile → settings drift between wrappers. + +That drift is measurable, not theoretical. Via `otool -l`: + +| Artifact | `minos` | +| ---------------------------- | ------- | +| `core/lib/libzerr_core.a` | 15.0 | +| `zerr_features~.pd_darwin` | 11.0 | + +So the external advertises macOS 11.0 while carrying code compiled against a 15.0 SDK — a real +portability defect. (pd-lib-builder actually passes `-mmacosx-version-min=10.6`; clang clamps that up +to 11.0, arm64's floor.) + +A second, separate cause of the same `ld` warning class: the **prebuilt ConanCenter binaries** were +themselves built for newer targets — ~357 warnings from `libfftw3.a`, more from `libyaml-cpp.a` (13.3). + +`os.version` *is* part of the package_id — it was absent from the cached packages' `conaninfo.txt` +only because it was unset in the profile that built them, not because the recipes exclude it. So +pinning a deployment target in the Conan profile changes the package_id, no ConanCenter prebuilt +matches, and every dependency is rebuilt from source. That does eliminate this second warning class, +but at a cost that was judged not worth paying; §4 has the measured comparison. These warnings +therefore remain, by choice: 376 of them, concerning dependency objects rather than our own code. + +Note also that the core's own 15.0-vs-11.0 gap was **latent, not broken**: the core includes only +long-stable headers (``, ``, ``, ``, ``, …) — no +``, `` or `` — and calls no macOS APIs. The value of pinning the +target is preventing a *future* newer-only libc++ call from compiling silently and failing on an +older mac. + +> **Status: addressed** — see §4. + +### 2.3 The fix for §2.2 already exists and is stranded + +`improve_deps_manage` (7 commits) contains exactly this consolidation: + +- single root `conanfile.txt` (moved from `core/`), `maxmsp/` and `puredata/` conanfiles deleted +- new root `CMakeLists.txt` and `puredata/CMakeLists.txt` +- hardcoded conan paths in `puredata/Makefile` replaced with generic, assignable paths +- `core/CMakeLists.txt` extended to resolve system dependencies when conan is absent +- `build.sh` and `puredata/README.md` updated to match + +It is **53 commits behind** and touches precisely the files `core_modernization` has since rewritten +— `core/CMakeLists.txt`, `build.sh`, `maxmsp/CMakeLists.txt`, `puredata/Makefile`. A merge will +conflict throughout. This needs **re-application, not a merge**, and it is the work most likely to +be silently lost. + +> **Status: partly re-applied by hand, remainder declined.** +> +> - The conanfile consolidation and the generic `puredata/Makefile` paths were re-applied — see §4. +> - The **root `CMakeLists.txt` + `puredata/CMakeLists.txt` superbuild** was assessed and +> **declined**. It was considered as a way to widen platform coverage and cannot do that: the Pd +> path is a Makefile needing `mingw32-make`, which breaks under an MSVC generator, and a Windows +> Max external needs MSVC, which cannot share one CMake configure with a MinGW-built core. One +> configure means one toolchain. Its remaining value was IDE integration, which did not justify a +> second build entry point beside `build.sh`. +> - The **`USE_SYSTEM_DEPS` / `find_package_with_help` fallback** was **declined** and replaced by +> [`dependency-fallbacks.md`](dependency-fallbacks.md), which records which non-conan routes are +> viable and what each one breaks. An untested second resolve path reintroduces exactly the ABI +> split §4 closes. +> - Note the branch also carries two regressions against `core_modernization`: unpinned recipe +> revisions in `conanfile.txt` (breaks under CMake 4, see §4) and +> `CMAKE_OSX_DEPLOYMENT_TARGET "13.3"` in `maxmsp/CMakeLists.txt` against 10.13 everywhere else. +> +> Nothing further is owed to the branch; it can be deleted. + +### 2.4 The JACK target is dead code but still advertised + +`jack/` contains only 4 files (`zerr.h`, `zerr.cpp`, `main.cpp`, `meson.build`), yet +`jack/meson.build` lists ~15 sources that do not exist (`src/zerr.cpp`, `src/modules/mapper.cpp`, +`src/features/rms_amplitude.cpp`, `src/modules/trajectorygenerator.cpp`, …). + +`jack/zerr.h` includes `audiorouter.h`, `mapper.h`, `trajectorygenerator.h` — module names deleted +from the core. Current `core/include/modules/`: `audiodisperser.h`, `envelopecombinator.h`, +`envelopegenerator.h`, `featurebank.h`, `featureextractor.h`, `featureprocessor.h`, +`speakermanager.h`. + +Additional mismatches: never links `zerr_core`; pins `cpp_std=c++11` against a C++17 core; resolves +deps via pkg-config rather than conan. `./build.sh jack` — documented in `README.md` — cannot +succeed. The toolchain is not the problem: meson 1.2.0 and the `jack` pkg-config file are both +present locally. + +### 2.5 Max/MSP is unverified against the new core + +The 23-commit sweep touched exactly **one** maxmsp file +(`mc.zerr.envelopes_tilde/zerr_envelopes.hpp`) across 5 externals, and there is no Max CI workflow. + +A grep of all Max sources for removed/renamed symbols (`t_featureNames`, `Pair`, `Mode::`, bare +`PI`, old `t_*` typedefs) returned **no hits**, so there is no obvious breakage. But +`core-modernization-changes.md` notes that metadata-getter removal means "anything calling +`get_name()` no longer compiles," and nothing had actually compiled the Max side against this core. + +> **Status: resolved — it builds.** A clean-slate `./build.sh maxmsp` (2026-07-31, macOS/arm64, +> CMake 4.3.1) builds **all 5 externals with 0 errors** and no compiler warnings from our own +> sources: `mc.zerr.combinator~`, `mc.zerr.disperser~`, `mc.zerr.envelopes~`, `mc.zerr.features~`, +> `zerr.features~`. All five `.mxo` binaries are fresh, arm64, `minos 11.0`. So the modernization +> sweep did not break the Max wrappers despite touching only one file there. +> +> Two caveats found while verifying: +> +> - **`ctest` registers 0 tests.** `min-object-unittest.cmake` is included by every project, but +> none ship a `*_test.cpp`, so it contributes nothing. The Max side has no automated coverage +> either — consistent with §3's "no test suite". +> - **The Max externals are never fat binaries.** `maxmsp/CMakeLists.txt:9` gates `C74_BUILD_FAT` +> on `CMAKE_GENERATOR MATCHES "Xcode"`, and falls back to `CMAKE_OSX_ARCHITECTURES = +> CMAKE_SYSTEM_PROCESSOR` otherwise. `build.sh` uses the default Unix Makefiles generator, so +> every build is single-architecture — `lipo -archs` confirms `arm64` only on all five. README.md +> and CLAUDE.md advertise "fat binaries with Xcode 12+", which is true of the CMake logic but not +> of anything `build.sh` produces. Distributing universal externals needs either +> `-G Xcode` or an explicit `-DCMAKE_OSX_ARCHITECTURES="x86_64;arm64"`. + +### 2.6 CI never sees this branch + +- `build-zerr-core-static-library.yml` — triggers only on `push` to `main`, paths `core/**` +- `build-puredata-plugins-with-pd-lib-builder.yml` — chains off the above via `workflow_run` + (its own `push` trigger is commented out) +- `deploy-doxygen.yml` — triggers only on `push` to `main`, paths `core/include/**`, `docs/**` + +Net effect: 23 commits rewriting the core have **never been built on Linux or Windows** — only +locally on arm64 macOS. No workflow covers Max/MSP or JACK at all. The Windows job in the PureData +workflow also carries two "Set up Conan" blocks, one commented out — leftover experimentation. + +### 2.7 Minor + +- **`doxygen` not installed locally**, so the docs pipeline this branch added cannot be exercised + outside CI (which only runs on `main`). `docs/build/html` exists from an earlier run, gitignored. +- **CMake is 4.3.1**; all 5 maxmsp project `CMakeLists.txt` declare + `cmake_minimum_required(VERSION 3.5)`, `min-lib` declares 3.10. Tested: 3.5 still configures under + 4.3.1 with a deprecation warning only — not broken, but on CMake's removal path. +- **Legacy duplicate** `maxmsp/source/projects/zerr.features_tilde/` alongside + `mc.zerr.features_tilde/`, with its own copy of `zerr_features.hpp`. It is also the only project + linking `FFTW3::fftw3` explicitly; its siblings rely on transitive propagation from `zerr_core`. + +--- + +## 3. Outstanding Work + +### Correctness risks flagged in code, untriaged + +- `core/src/modules/speakermanager.cpp:454` — `// TODO: seems incorrect` +- `core/src/modules/envelopecombinator.cpp:87` — `systemcfg.block_size` "could cause bug (sometimes + smaller)" + +### Incomplete functionality + +- `core/src/modules/speakermanager.cpp:102` — Cartesian↔Spherical consistency unchecked; + `:120` orientation structure unfinished; `:217` spherical weighting not equalized +- `core/src/modules/envelopegenerator.cpp:144` — Trajectory-mode interpolator missing; + `:197` linear panning should become a parameterized crossfade; `:207` hardcoded `DISTANCE_SCALE` +- `core/src/modules/featurebank.cpp:79` — should be an external function +- `TriggerMode` has a single value (`Random`) — selection strategies not expanded +- PD runtime message interface still marked "in development" in `zerr_envelopes~-help.pd` + +### Dead code + +- `puredata/include/zerr_combinator.h:54` and `zerr_disperser.h:49` — `get_port_count()`, + both marked "remove if not needed" +- `puredata/src/zerr_envelopes~.cpp:252` — empty doc stub +- `maxmsp/.../mc.zerr.combinator_tilde.cpp:138` — "TODO: Initialize the ZerrCombinator based on the + mode" + +### No test suite + +~1,580 insertions across 53 files, including semantic changes (RAII ownership, throwing enum +parsing, const-ref signatures), validated solely by "core compiles + 4 PureData externals link." +This is the largest unmitigated risk on the branch. + +--- + +## 4. Dependency Handling — Changes Applied (2026-07-31) + +Conan was **kept**, deliberately. Three things make it load-bearing and no CMake-native alternative +(FetchContent/CPM) covers them: `puredata/Makefile` consumes a `MakeDeps` `conandeps.mk` and +pd-lib-builder is not CMake; the Windows build cross-compiles via a conan profile; and distributable +externals need static, fat libraries. + +Homebrew `fftw` and `yaml-cpp` are installed on the dev machine, which makes a system-dependency +fallback tempting — but brew's `yaml-cpp` ships **dylib only, no `.a`** (and 0.9.0 vs the pinned +0.8.0), and both are arm64-only. Linking them would embed a runtime dylib dependency in every +`.pd_darwin`/`.mxo` and preclude the `x86_64;arm64` fat build. System deps are a local dev escape +hatch, never the release path. + +**What changed:** + +| Change | Effect | +| --- | --- | +| Root `conanfile.txt`, with `CMakeDeps` + `CMakeToolchain` + `MakeDeps`; per-target conanfiles deleted | One dependency declaration, one install, one profile resolution — removes the drift's root cause | +| `profiles/macos`, `profiles/linux` (both `include(default)`), `puredata/mingw-profile.txt` → `profiles/mingw` | Committed, reviewable settings instead of an ambient per-machine `~/.conan2` default | +| `CMAKE_OSX_DEPLOYMENT_TARGET "11.0"` in `core/CMakeLists.txt` | Our objects are built at the floor the externals advertise, so the compiler rejects a newer-only libc++ API instead of silently shipping an external that fails on older macs. Set in CMake rather than as `os.version` in the profile — see below | +| Pinned recipe revisions in `conanfile.txt` (`fftw#3b4cceb8…`, `yaml-cpp#1aa37121…`) | Both libraries' bundled `CMakeLists.txt` declare `cmake_minimum_required` below 3.5 (fftw 3.0, yaml-cpp 3.4), which **CMake 4.x rejects outright**. These are the ConanCenter revisions that set `CMAKE_POLICY_VERSION_MINIMUM=3.5` as a *cache* variable — the only placement that works, since line 1 executes before the toolchain file loads. Pinned so a resolve cannot regress it | +| `puredata/Makefile`: `ZERR_CONAN_DIR ?= ../build` | Reads the shared root output; overridable, with an actionable error when absent | +| `build.sh`: `install_deps`/`ensure_deps`, new `deps` target, `-c deps` | One root resolve, shared by all targets; `conan` now only required when deps actually need resolving, so a populated `build/` builds without it | +| `build.sh`: `reconfigure_if_stale` | Discards a `core/build`/`maxmsp/build` that would otherwise **silently ignore** the toolchain. Two distinct failure modes, both observed: a pre-consolidation cache ignores a newly passed `CMAKE_TOOLCHAIN_FILE` (CMake only warns "Manually-specified variables were not used"), and a cache older than the toolchain keeps stale values because the toolchain assigns via `set(... CACHE ...)`, which never overrides an existing entry — that one kept `CMAKE_OSX_DEPLOYMENT_TARGET` empty and left objects at the host SDK despite the pin | +| CI: `pip install "conan>=2.13,<3"` | A bare `pip install conan` floated to latest; conan 3.x would have broken CI with no change on our side | +| CI: root install + `-pr:h=profiles/`, toolchain from `$GITHUB_WORKSPACE/build` | CI and local builds resolve identically | +| `puredata/Makefile`: dropped duplicate `-lyaml-cpp` | Removes "ignoring duplicate libraries" from the link | + +**Why the deployment target is set in CMake, not in the Conan profile.** Both work; they differ in +cost. `os.version=11.0` in `profiles/macos` additionally rebuilds the dependencies at that target, +which silences all 376 `ld` version-mismatch warnings — but `os.version` is part of the package_id +and ConanCenter builds its binaries with it unset, so no prebuilt would ever match and *every* +dependency would compile from source on every machine and every CI run without a `~/.conan2` cache. +It would also raise the x86_64 floor to 11.0, contradicting the 10.13 `maxmsp/CMakeLists.txt` asks +for in a fat build. + +Measured both ways on macOS/arm64: + +| | `os.version` in profile | `CMAKE_OSX_DEPLOYMENT_TARGET` in CMake (chosen) | +| --- | --- | --- | +| `libzerr_core.a` | `minos 11.0` | `minos 11.0` | +| Externals | `minos 11.0` | `minos 11.0` | +| Dependency resolution | built from source | all `Cache` / prebuilt | +| `ld` version-mismatch warnings | 0 | 376 (357 `libfftw3.a`, 19 `libyaml-cpp.a`) | +| Errors | 0 | 0 | + +The warnings are about *dependency* objects being built for a newer target than the link floor; they +do not affect our code's correctness. For a genuinely warning-free link, do it as an explicit one-off +rather than by changing the profile: +`conan install . --output-folder=build --build=missing -pr:h=profiles/macos -pr:b=profiles/macos -s os.version=11.0` + +**Other profile choices worth knowing:** `compiler.cppstd` is inherited, not pinned — the detected +`gnu17` is part of yaml-cpp's package_id, so forcing `17` would invalidate the cached binaries for no +behavioural gain. `compiler.version` is likewise inherited even though the detected value (apple-clang +14) trails the active toolchain (AppleClang 17), because it *is* part of the package_id and +re-detecting discards the cache. + +**Verified end-to-end on macOS/arm64, with conan 2.13.0 present:** + +- `./build.sh deps` → `conan install` resolves and builds both dependencies from source under + CMake 4.3.1, all three generators emitted (`conan_toolchain.cmake`, CMakeDeps configs, + `conandeps.mk`), `Install finished successfully` +- both dependencies resolve from `Cache` (fftw back to its original package_id `aedbe8f5…`) +- `core/lib/libzerr_core.a` → `minos 11.0` (was 15.0), from `core/CMakeLists.txt` alone — the + toolchain sets no deployment target at all now +- all 4 PureData externals rebuild from scratch → `minos 11.0`, **0** duplicate-library warnings, + **0** errors, and the 376 dependency version-mismatch warnings documented above +- `reconfigure_if_stale` fires on both failure modes and is idempotent (second run: 0 discards) +- `build.sh deps` fails with an actionable message when `conan` is absent; the `puredata/Makefile` + guard likewise + +- `./build.sh maxmsp` from a clean tree → **all 5 Max externals build, 0 errors**, binaries arm64 / + `minos 11.0` (see §2.5, which this closes) + +**Still unverified:** Linux and Windows/MinGW. + +**Note for CI:** prebuilt binaries are reused, so no dependency is compiled from source on the macOS +jobs. One exception is inherent to the pinned recipe revisions: ConanCenter may not publish a binary +for a given revision against `compiler.version=14`, in which case that package is built once and then +cached. A `~/.conan2` cache step would make this a non-issue. + +--- + +## 5. Code Review Findings — Fixed (2026-07-31) + +A review of `main...HEAD` plus the uncommitted dependency work raised five findings. All are fixed +and verified; the first was a genuine host-crash bug introduced by the modernization sweep. + +**1. Uncaught exception aborted the host — critical.** `Mode` (a validated string) became `GenMode` +plus a throwing `parseGenMode()` (`core/include/utils/types.h:60`). Both wrappers call it inside a +constructor with no handler — `puredata/src/zerr_envelopes.cpp:19` and +`maxmsp/.../zerr_envelopes.hpp:39` — so a typo'd creation argument escaped into the host's C call +stack, reached `std::terminate` and killed the process. Reproduced: +`[zerr_envelopes~ triggerz circulation_8]` → +`libc++abi: terminating due to uncaught exception of type std::invalid_argument` / `Pd: signal 6`, +losing unsaved patches. Before the refactor the same patch merely logged an error. + +Fixed by catching at both wrapper boundaries (the pattern `puredata/src/zerr_features.cpp:26` +already used) and failing the way each host expects. `parseGenMode` keeps throwing, which is the +right contract for a library. Verified after the fix: +`error: zerr_envelopes~: Unknown GenMode: triggerz` / `error: ... couldn't create`, host alive; +valid modes (`trigger`, `trajectory`) still create normally. + +**2. The new profiles were untracked — high.** `profiles/macos` and `profiles/linux` were written but +never `git add`ed (only `profiles/mingw` was tracked, via `git mv`). Since `build.sh` and both +rewritten workflows pass `-pr:h=profiles/`, any fresh clone or CI run would have failed with +"Profile not found". Now tracked, along with this document. + +**3. `clean` aborted mid-run — medium.** Two compounding causes. `clean_puredata` used +`cd … && make clean; cd …`, whose failure status trips `set -e` and exits before the remaining +targets. And the `$(error …)` guard in `puredata/Makefile` fires at *parse* time for every goal, so +`make clean` failed exactly when dependencies were absent — the state `-c deps` creates. Fixed both: +the guard is skipped for clean-only invocations, and the `cd` runs in a subshell with `|| true`. +Verified: `./build.sh -c deps puredata maxmsp` now cleans all three (exit 0), `make clean` works +with no dependencies present, and `make` still errors clearly. + +**4. `deploy-doxygen.yml` had no `contents: read` — medium.** Declaring any `permissions` sets every +unlisted scope to `none`, leaving `actions/checkout` without repo read scope. Added. + +**5. Deployment target disagreed with the wrappers — low.** The pin was 11.0 while +`maxmsp/CMakeLists.txt` pins 10.13 and the PureData macOS CI job passes +`-mmacosx-version-min=10.13`. Invisible on arm64 (clang clamps everything up to 11.0) but on an Intel +or fat build it reintroduced the same object-version mismatch in the opposite direction. Core now +pins **10.13**, the project's single declared floor; the built archive is still `minos 11.0` on +arm64, so nothing changes on Apple Silicon. + +Also fixed: a new `-Wreorder-ctor` warning at `puredata/src/zerr_features.cpp` (mem-init list not in +declaration order — harmless, now silent). + +**Re-verified after all fixes, from a fully clean tree:** `./build.sh puredata` and +`./build.sh maxmsp` both succeed from zero — 4 PureData externals, 5 Max externals, 0 errors, 0 +warnings from our own sources. + +--- + +## 6. Recommended Order + +1. ~~**Restore conan**~~ — ✅ done: `uv tool install conan==2.13.0`, matching the cache version. +2. ~~**Re-apply `improve_deps_manage`'s consolidation**~~ — ✅ done by hand (§4). That branch and + `max_doc` can now be deleted. +3. ~~**Build the Max externals once**~~ — ✅ done: all 5 build cleanly (§2.5). +4. **Push the branch** — `786c098` plus the dependency work in §4 is still local. +5. **Merge `core_modernization` → `main`** (clean fast-forward) so CI finally builds it on **Linux and + Windows**, the two platforms still unverified — or temporarily widen the workflow triggers to run + on the branch first. +6. **Decide `jack`'s fate** — fix it against the current core, or drop it from `build.sh` and + `README.md` rather than shipping a target that cannot build (§2.4). +7. **Triage** `speakermanager.cpp:454` and `envelopecombinator.cpp:87` (§3); add a minimal smoke + harness — `ctest` currently registers 0 tests on every platform. +8. **Audit the remaining exception escapes at wrapper boundaries.** §5's finding 1 was one instance of + a general hazard: any exception crossing into Pd's or Max's C call stack aborts the host. The other + throw in the core is `SpeakerManager::getSpeakerByIndex` (`speakermanager.cpp:169`, + `std::out_of_range`). All four of its call sites are internal and pass indices derived from the + manager's own maps, and the Pd wrapper validates user-supplied indices, so it is not obviously + reachable — but if it ever fires during `perform` it kills the host with no error message. Worth + confirming, and worth a rule that wrapper entry points never let an exception through. +8. **Decide on fat Max binaries** — `build.sh maxmsp` produces arm64-only, contradicting what + README.md advertises (§2.5). diff --git a/docs/doxygen-awesome-css b/docs/doxygen-awesome-css new file mode 160000 index 0000000..1f36200 --- /dev/null +++ b/docs/doxygen-awesome-css @@ -0,0 +1 @@ +Subproject commit 1f3620084ff75734ed192101acf40e9dff01d848 diff --git a/docs/header.html b/docs/header.html new file mode 100644 index 0000000..6e305c7 --- /dev/null +++ b/docs/header.html @@ -0,0 +1,87 @@ + + + + + + + + +$projectname: $title +$title + + + + + + + + + + + + +$treeview +$search +$mathjax +$darkmode + +$extrastylesheet + + + + + + + + + +
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
$projectname $projectnumber +
+
$projectbrief
+
+
$projectbrief
+
$searchbox
$searchbox
+
+ + diff --git a/maxmsp/conanfile.txt b/maxmsp/conanfile.txt deleted file mode 100644 index f82b7b7..0000000 --- a/maxmsp/conanfile.txt +++ /dev/null @@ -1,7 +0,0 @@ -[requires] -fftw/3.3.10 -yaml-cpp/0.8.0 - -[generators] -CMakeDeps -CMakeToolchain \ No newline at end of file diff --git a/maxmsp/source/projects/mc.zerr.envelopes_tilde/mc.zerr.envelopes_tilde.cpp b/maxmsp/source/projects/mc.zerr.envelopes_tilde/mc.zerr.envelopes_tilde.cpp index 50b320e..68db765 100644 --- a/maxmsp/source/projects/mc.zerr.envelopes_tilde/mc.zerr.envelopes_tilde.cpp +++ b/maxmsp/source/projects/mc.zerr.envelopes_tilde/mc.zerr.envelopes_tilde.cpp @@ -24,9 +24,9 @@ * @brief Main data structure for the mc.zerr.envelopes~ object */ typedef struct _zerr_envelopes { - t_pxobject x_obj; ///< DSP object header (must be first) + t_pxobject x_obj; ///< DSP object header (must be first) long channel_count; ///< Channel count of multichannel signal - ZerrEnvelopes* ze; ///< Pointer to the zerr_envelopes implementation + ZerrEnvelopes* ze; ///< Pointer to the zerr_envelopes implementation } t_zerr_envelopes; //------------------------------------------------------------------------------ @@ -36,8 +36,11 @@ typedef struct _zerr_envelopes { void* zerr_envelopes_new(t_symbol* s, long argc, t_atom* argv); void zerr_envelopes_free(t_zerr_envelopes* x); void zerr_envelopes_assist(t_zerr_envelopes* x, void* b, long m, long a, char* s); -void zerr_envelopes_dsp64(t_zerr_envelopes* x, t_object* dsp64, short* count, double samplerate, long maxvectorsize, long flags); -void zerr_envelopes_perform64(t_zerr_envelopes* x, t_object* dsp64, double** ins, long numins, double** outs, long numouts, long sampleframes, long flags, void* userparam); +void zerr_envelopes_dsp64(t_zerr_envelopes* x, t_object* dsp64, short* count, double samplerate, + long maxvectorsize, long flags); +void zerr_envelopes_perform64(t_zerr_envelopes* x, t_object* dsp64, double** ins, long numins, + double** outs, long numouts, long sampleframes, long flags, + void* userparam); long zerr_envelopes_multichanneloutputs(t_zerr_envelopes* x, long outletindex); //------------------------------------------------------------------------------ void zerr_envelopes_bang(t_zerr_envelopes* x); @@ -59,18 +62,14 @@ C74_EXPORT void ext_main(void* r) { t_class* c; - c = class_new("mc.zerr.envelopes~", - (method)zerr_envelopes_new, - (method)zerr_envelopes_free, - sizeof(t_zerr_envelopes), - 0L, - A_GIMME, - 0); + c = class_new("mc.zerr.envelopes~", (method)zerr_envelopes_new, (method)zerr_envelopes_free, + sizeof(t_zerr_envelopes), 0L, A_GIMME, 0); // Register methods class_addmethod(c, (method)zerr_envelopes_dsp64, "dsp64", A_CANT, 0); class_addmethod(c, (method)zerr_envelopes_assist, "assist", A_CANT, 0); - class_addmethod(c, (method)zerr_envelopes_multichanneloutputs, "multichanneloutputs", A_CANT, 0); + class_addmethod(c, (method)zerr_envelopes_multichanneloutputs, "multichanneloutputs", A_CANT, + 0); class_addmethod(c, (method)zerr_envelopes_bang, "bang", 0); class_addmethod(c, (method)zerr_envelopes_active, "active", A_GIMME, 0); class_addmethod(c, (method)zerr_envelopes_curr, "curr", A_GIMME, 0); @@ -105,7 +104,7 @@ void* zerr_envelopes_new(t_symbol* s, long argc, t_atom* argv) return NULL; // Initialize default values ----------------------------------------------- - x->ze = NULL; + x->ze = NULL; x->channel_count = 1; // Default 1 channel output for the multichannel outlet // Parsing arguments ------------------------------------------------------- @@ -114,7 +113,7 @@ void* zerr_envelopes_new(t_symbol* s, long argc, t_atom* argv) } // Process argument 1: mode - t_symbol* arg1 = atom_getsym(argv); + t_symbol* arg1 = atom_getsym(argv); const char* mode = arg1->s_name; // Process argument 2: relative path of the speaker configuration file @@ -125,10 +124,10 @@ void* zerr_envelopes_new(t_symbol* s, long argc, t_atom* argv) // Find the absolute path of configuration file ---------------------------- bool yaml_found = false; - size_t len = strlen(file_path->s_name); + size_t len = strlen(file_path->s_name); t_symbol* absolute_path = NULL; - t_fourcc file_types[] = { 'TEXT', 'YAML' }; + t_fourcc file_types[] = {'TEXT', 'YAML'}; t_max_err err; char* unix_path = NULL; @@ -144,7 +143,7 @@ void* zerr_envelopes_new(t_symbol* s, long argc, t_atom* argv) // Check if path already has .yaml or .yml extension bool has_yaml_ext = (len > 5 && strcmp(file_path->s_name + len - 5, ".yaml") == 0); - bool has_yml_ext = (len > 4 && strcmp(file_path->s_name + len - 4, ".yml") == 0); + bool has_yml_ext = (len > 4 && strcmp(file_path->s_name + len - 4, ".yml") == 0); if (has_yaml_ext || has_yml_ext) { err = path_absolutepath(&absolute_path, file_path, file_types, 2); @@ -196,18 +195,37 @@ void* zerr_envelopes_new(t_symbol* s, long argc, t_atom* argv) } if (!yaml_found) { - object_error((t_object*)x, "Cannot find YAML file: %s (tried as-is, with .yaml, and with .yml)", file_path->s_name); - object_free(x); // temporary solution + object_error((t_object*)x, + "Cannot find YAML file: %s (tried as-is, with .yaml, and with .yml)", + file_path->s_name); return NULL; } // create & initialize ZerrEnvelopes instance ------------------------------ - x->ze = new ZerrEnvelopes(sys_getsr(), sys_getblksize(), mode, unix_path); - if (!x->ze) - return NULL; - - if (!x->ze->initialize()) { + // The constructor calls zerr::parseGenMode(), which throws on an unknown mode + // argument. An exception must never escape into Max's C call stack: it would + // reach std::terminate and abort the host. Report and return NULL instead. + // + // Return NULL rather than object_free(x): object_free reaches zerr_envelopes_free, + // which calls dsp_free() -- but dsp_setup() does not run until further down, and + // MSP requires those two to be paired. This path is one typo in the mode argument + // away for any user, so it has to be the safe kind of failure. Leaving x for Max + // to reclaim is what every other early return in this function already does. + // x->ze is NULL until the assignment below, so the delete in the catch is correct + // whether the throw came from the constructor or from initialize(). + try { + x->ze = new ZerrEnvelopes(sys_getsr(), sys_getblksize(), mode, unix_path); + + if (!x->ze->initialize()) { + delete x->ze; + x->ze = NULL; + return NULL; + } + } + catch (const std::exception& e) { + object_error((t_object*)x, "mc.zerr.envelopes~: %s", e.what()); delete x->ze; + x->ze = NULL; return NULL; } @@ -241,7 +259,8 @@ void zerr_envelopes_assist(t_zerr_envelopes* x, void* b, long m, long a, char* s { if (m == ASSIST_INLET) { strcpy(s, "(signal) Input source signal"); - } else if (m == ASSIST_OUTLET) { + } + else if (m == ASSIST_OUTLET) { strcpy(s, "(multichannel signal) Output envelopes"); } } @@ -259,12 +278,15 @@ long zerr_envelopes_multichanneloutputs(t_zerr_envelopes* x, long outletindex) // DSP Methods //------------------------------------------------------------------------------ -void zerr_envelopes_dsp64(t_zerr_envelopes* x, t_object* dsp64, short* count, double samplerate, long maxvectorsize, long flags) +void zerr_envelopes_dsp64(t_zerr_envelopes* x, t_object* dsp64, short* count, double samplerate, + long maxvectorsize, long flags) { dsp_add64(dsp64, (t_object*)x, (t_perfroutine64)zerr_envelopes_perform64, 0, NULL); } -void zerr_envelopes_perform64(t_zerr_envelopes* x, t_object* dsp64, double** ins, long numins, double** outs, long numouts, long sampleframes, long flags, void* userparam) +void zerr_envelopes_perform64(t_zerr_envelopes* x, t_object* dsp64, double** ins, long numins, + double** outs, long numouts, long sampleframes, long flags, + void* userparam) { x->ze->perform(ins, numins, outs, numouts, sampleframes); } @@ -294,7 +316,7 @@ void zerr_envelopes_active(t_zerr_envelopes* x, t_symbol* msg, long argc, t_atom return; } - action = atom_getsym(argv); + action = atom_getsym(argv); index_count = argc - 1; indices = (int*)sysmem_newptr(index_count * sizeof(int)); @@ -306,9 +328,11 @@ void zerr_envelopes_active(t_zerr_envelopes* x, t_symbol* msg, long argc, t_atom for (i = 0; i < index_count; i++) { if (atom_gettype(argv + i + 1) == A_LONG) { indices[i] = (int)atom_getlong(argv + i + 1); - } else if (atom_gettype(argv + i + 1) == A_FLOAT) { + } + else if (atom_gettype(argv + i + 1) == A_FLOAT) { indices[i] = (int)atom_getfloat(argv + i + 1); - } else { + } + else { object_error((t_object*)x, "active: index %ld must be a number", i + 1); sysmem_freeptr(indices); return; @@ -338,9 +362,11 @@ void zerr_envelopes_curr(t_zerr_envelopes* x, t_symbol* msg, long argc, t_atom* if (atom_gettype(argv) == A_LONG) { speaker_index = atom_getlong(argv); - } else if (atom_gettype(argv) == A_FLOAT) { + } + else if (atom_gettype(argv) == A_FLOAT) { speaker_index = (long)atom_getfloat(argv); - } else { + } + else { object_error((t_object*)x, "current_speaker: argument must be a number"); return; } @@ -371,7 +397,7 @@ void zerr_envelopes_topo(t_zerr_envelopes* x, t_symbol* msg, long argc, t_atom* return; } - action = atom_getsym(argv); + action = atom_getsym(argv); index_count = argc - 1; indices = (int*)sysmem_newptr(index_count * sizeof(int)); @@ -383,9 +409,11 @@ void zerr_envelopes_topo(t_zerr_envelopes* x, t_symbol* msg, long argc, t_atom* for (i = 0; i < index_count; i++) { if (atom_gettype(argv + i + 1) == A_LONG) { indices[i] = (int)atom_getlong(argv + i + 1); - } else if (atom_gettype(argv + i + 1) == A_FLOAT) { + } + else if (atom_gettype(argv + i + 1) == A_FLOAT) { indices[i] = (int)atom_getfloat(argv + i + 1); - } else { + } + else { object_error((t_object*)x, "topomatrix: index %ld must be a number", i + 1); sysmem_freeptr(indices); return; @@ -423,9 +451,11 @@ void zerr_envelopes_traj(t_zerr_envelopes* x, t_symbol* msg, long argc, t_atom* for (i = 0; i < argc; i++) { if (atom_gettype(argv + i) == A_LONG) { indices[i] = (int)atom_getlong(argv + i); - } else if (atom_gettype(argv + i) == A_FLOAT) { + } + else if (atom_gettype(argv + i) == A_FLOAT) { indices[i] = (int)atom_getfloat(argv + i); - } else { + } + else { object_error((t_object*)x, "trajectory: index %ld must be a number", i); sysmem_freeptr(indices); return; @@ -455,9 +485,11 @@ void zerr_envelopes_interval(t_zerr_envelopes* x, t_symbol* msg, long argc, t_at if (atom_gettype(argv) == A_FLOAT) { interval = atom_getfloat(argv); - } else if (atom_gettype(argv) == A_LONG) { + } + else if (atom_gettype(argv) == A_LONG) { interval = (double)atom_getlong(argv); - } else { + } + else { object_error((t_object*)x, "trigger_interval: argument must be a number"); return; } @@ -470,7 +502,4 @@ void zerr_envelopes_interval(t_zerr_envelopes* x, t_symbol* msg, long argc, t_at * @param x Pointer to the t_zerr_envelopes object. * @param s Unused symbol parameter. */ -void zerr_envelopes_print(t_zerr_envelopes* x) -{ - x->ze->printParameters(); -} \ No newline at end of file +void zerr_envelopes_print(t_zerr_envelopes* x) { x->ze->printParameters(); } \ No newline at end of file diff --git a/maxmsp/source/projects/mc.zerr.envelopes_tilde/zerr_envelopes.hpp b/maxmsp/source/projects/mc.zerr.envelopes_tilde/zerr_envelopes.hpp index 90c63a8..436e52a 100644 --- a/maxmsp/source/projects/mc.zerr.envelopes_tilde/zerr_envelopes.hpp +++ b/maxmsp/source/projects/mc.zerr.envelopes_tilde/zerr_envelopes.hpp @@ -13,13 +13,15 @@ /** * @class ZerrEnvelopes - * @brief Main wrapper class that interfaces between Max/MSP and the core multi-channel envelope generation functionality + * @brief Main wrapper class that interfaces between Max/MSP and the core multi-channel envelope + * generation functionality * - * This class handles the initialization, audio processing, and cleanup of audio feature extraction operations. - * It manages the data flow between Max/MSP's audio system and the internal feature processing chain. + * This class handles the initialization, audio processing, and cleanup of audio feature extraction + * operations. It manages the data flow between Max/MSP's audio system and the internal feature + * processing chain. */ class ZerrEnvelopes { - public: + public: /** * @brief Creates a new ZerrEnvelopes instance * @param sampleRate The audio sample rate in Hz @@ -34,15 +36,15 @@ class ZerrEnvelopes { } , selectionMode { selectionMode } , spkrCfgFile { spkrCfgFile } - , generator { std::make_unique(systemConfigs, spkrCfgFile, selectionMode) } + , generator { std::make_unique(systemConfigs, spkrCfgFile, zerr::parseGenMode(selectionMode)) } { } // Disable copying but allow moving - ZerrEnvelopes(const ZerrEnvelopes&) = delete; + ZerrEnvelopes(const ZerrEnvelopes&) = delete; ZerrEnvelopes& operator=(const ZerrEnvelopes&) = delete; - ZerrEnvelopes(ZerrEnvelopes&&) = default; - ZerrEnvelopes& operator=(ZerrEnvelopes&&) = default; + ZerrEnvelopes(ZerrEnvelopes&&) = default; + ZerrEnvelopes& operator=(ZerrEnvelopes&&) = default; /** * @brief Initializes all internal components and prepares the object for processing @@ -58,9 +60,7 @@ class ZerrEnvelopes { // assgin Max/MSP print method the the logger function // Because we want to see logs in the PD log window - auto printFunc = [](const std::string& msg) { - post(msg.c_str()); - }; + auto printFunc = [](const std::string& msg) { post(msg.c_str()); }; generator->setPrinter(printFunc); outputCount = generator->getNumSpeakers(); @@ -76,7 +76,7 @@ class ZerrEnvelopes { * @brief Main DSP callback function that processes audio buffers * @param ins Array of pointers to input audio buffers * @param numins Number of input channels - * @param outs Array of pointers to output audio buffers + * @param outs Array of pointers to output audio buffers * @param numouts Number of output channels * @param sampleframes Number of samples to process * @throws std::invalid_argument if buffer pointers or sizes are invalid @@ -176,22 +176,21 @@ class ZerrEnvelopes { /** * @brief Prints current parameters to the console */ - void printParameters() - { - generator->printParameters(); - } + void printParameters() { generator->printParameters(); } - private: - static constexpr int inputCount = 3; /**< Number of signal inlets: main(0), spread(1), volume(2) */ + private: + static constexpr int inputCount = + 3; /**< Number of signal inlets: main(0), spread(1), volume(2) */ int outputCount = 0; /**< Number of signal outlets based on the loudspeaker setup */ zerr::SystemConfigs systemConfigs; /**< System configuration: sample rate and block size */ - zerr::Blocks inputBuffer; /**< Multi-channel buffer for storing incoming audio samples */ + zerr::Blocks inputBuffer; /**< Multi-channel buffer for storing incoming audio samples */ zerr::Blocks outputBuffer; /**< Multi-channel buffer for storing envelope outputs */ - std::unique_ptr generator; /**< Core component that implements envelope generation logic */ + std::unique_ptr + generator; /**< Core component that implements envelope generation logic */ - std::string spkrCfgFile; /**< Path to speaker configuration YAML file */ + std::string spkrCfgFile; /**< Path to speaker configuration YAML file */ std::string selectionMode; /**< Envelope generation mode: "trajectory" or "trigger" */ }; \ No newline at end of file diff --git a/profiles/linux b/profiles/linux new file mode 100644 index 0000000..289a495 --- /dev/null +++ b/profiles/linux @@ -0,0 +1,17 @@ +# Zerr* Linux build profile. +# +# Inherits the machine-detected `default` profile and pins only the build type, +# so CI and local builds resolve one consistent set of dependency binaries. +# +# fPIC is not set here: ConanCenter's fftw and yaml-cpp recipes already default +# to fPIC=True for static builds (confirmed in the cached packages' conaninfo), +# and core/CMakeLists.txt sets CMAKE_POSITION_INDEPENDENT_CODE for zerr_core +# itself, which is what the shared-object wrappers require. +# +# See profiles/macos for why compiler.cppstd and compiler.version are inherited +# rather than pinned. + +include(default) + +[settings] +build_type=Release diff --git a/profiles/macos b/profiles/macos new file mode 100644 index 0000000..4e7fe08 --- /dev/null +++ b/profiles/macos @@ -0,0 +1,42 @@ +# Zerr* macOS build profile. +# +# Inherits the machine-detected `default` profile, so compiler and architecture +# stay local, then pins only the build type. +# +# Deliberately NOT pinned here: os.version. The macOS deployment target is set in +# core/CMakeLists.txt (and already in maxmsp/CMakeLists.txt) instead. +# +# Setting os.version here does work -- Conan then emits CMAKE_OSX_DEPLOYMENT_TARGET +# and additionally rebuilds the dependencies at that target, which silences the +# ~357 `ld` "built for newer macOS version" warnings the prebuilt fftw/yaml-cpp +# binaries produce. But os.version is part of the package_id, and ConanCenter +# builds its binaries with it unset, so no prebuilt would ever match: every +# dependency would be compiled from source on every machine and on every CI run +# without a ~/.conan2 cache. It would also raise the x86_64 floor to 11.0, which +# contradicts the 10.13 that maxmsp/CMakeLists.txt asks for in a fat build. +# +# Setting it in CMake gets the part that matters -- our own objects built at the +# floor we advertise, so the compiler rejects a newer-only libc++ API instead of +# silently shipping an external that fails on older macs -- at no build cost. The +# residual dependency warnings are cosmetic and documented in +# docs/design/repo-audit-2026-07-30.md. +# +# If you do want bit-consistent targets across dependencies too, prefer an explicit +# one-off over changing this file: +# conan install . --output-folder=build --build=missing \ +# -pr:h=profiles/macos -pr:b=profiles/macos -s os.version=11.0 +# +# Also deliberately NOT pinned: compiler.cppstd. The detected default's `gnu17` is +# part of yaml-cpp's package_id, so forcing `17` would invalidate the cached +# binaries for no behavioural gain -- core sets CMAKE_CXX_STANDARD 17 itself. +# +# Note: `conan profile detect` may record a compiler.version older than the active +# toolchain (e.g. apple-clang 14 against AppleClang 17). That is inherited here on +# purpose: compiler.version *is* part of the package_id, so re-detecting invalidates +# the dependency cache. Re-run `conan profile detect --force` only when you actually +# intend to rebuild the dependencies. + +include(default) + +[settings] +build_type=Release diff --git a/profiles/mingw b/profiles/mingw new file mode 100644 index 0000000..f80a59c --- /dev/null +++ b/profiles/mingw @@ -0,0 +1,20 @@ +# Zerr* Windows/MinGW-w64 profile (moved here from puredata/mingw-profile.txt so +# that all profiles live in one place alongside the root conanfile.txt). +# +# Unlike profiles/macos and profiles/linux this is a full standalone profile: it +# describes a cross-compilation target, so it cannot `include(default)` -- every +# setting has to be stated explicitly rather than inherited from the host machine. + +[settings] +os=Windows +arch=x86_64 +build_type=Release +compiler=gcc +compiler.version=13 +compiler.libcxx=libstdc++11 +compiler.threads=posix +compiler.exception=seh +compiler.cppstd=17 + +[conf] +tools.build:compiler_executables={"c":"x86_64-w64-mingw32-gcc","cpp":"x86_64-w64-mingw32-g++"} \ No newline at end of file diff --git a/puredata/Makefile b/puredata/Makefile index 8e33c49..5d08d08 100644 --- a/puredata/Makefile +++ b/puredata/Makefile @@ -16,12 +16,32 @@ ZERR_INCLUDES := -I$(ZERR_CORE_DIR)/include/utils \ ############################################################# # CONAN DEPENDENCIES ############################################################# -# Guard: ensure conan dependencies are available -ifeq ($(wildcard build/conandeps.mk),) - $(error build/conandeps.mk not found — run 'conan install . --output-folder=build --build=missing' first) -endif +# Dependencies are resolved once at the repo root (see /conanfile.txt) so that +# this wrapper and zerr_core are built against one identical profile. Override +# ZERR_CONAN_DIR if the conan output folder lives elsewhere. +ZERR_CONAN_DIR ?= ../build + +# Guard: ensure conan dependencies are available before building. +# +# $(error) fires while the makefile is being parsed, i.e. for *every* goal, so an +# unconditional guard makes `make clean` fail too whenever the dependencies are +# absent -- which is exactly the state you are in after removing them. Cleaning +# must not require them, so the guard is skipped for clean-only invocations. +# +# "Clean-only" means every named goal needs no dependencies -- not merely that one +# of them does. A plain `$(filter clean,...)` also matches `make clean all`, which +# would then build with the CONAN_* variables empty: `-I$(CONAN_INCLUDE_DIRS_YAML_CPP)` +# collapses to a bare `-I` that swallows the flag behind it, and `-l$(CONAN_LIBS_YAML_CPP)` +# to a bare `-l`, trading this message for a cryptic compiler error. An empty goal +# list means the default target, which does need them. +NODEPS_GOALS := clean +CLEAN_ONLY := $(if $(MAKECMDGOALS),$(if $(filter-out $(NODEPS_GOALS),$(MAKECMDGOALS)),,yes)) -include build/conandeps.mk +ifneq ($(wildcard $(ZERR_CONAN_DIR)/conandeps.mk),) + include $(ZERR_CONAN_DIR)/conandeps.mk +else ifeq ($(CLEAN_ONLY),) + $(error $(ZERR_CONAN_DIR)/conandeps.mk not found — from the repo root run: conan install . --output-folder=build --build=missing -pr:h=profiles/ -pr:b=profiles/) +endif ############################################################# # COMPILER & LINKER FLAGS @@ -52,7 +72,8 @@ zerr_features~.class.sources += src/zerr_features.cpp # class specific dependencies zerr_features~.class.ldflags += -L$(CONAN_LIB_DIRS_FFTW) zerr_features~.class.ldlibs += -lfftw3 -zerr_envelopes~.class.ldlibs += -lyaml-cpp +# yaml-cpp is already in the library-wide ldlibs above; repeating it here made the +# linker report "ignoring duplicate libraries: '-lyaml-cpp'". # add library data files datafiles += LICENSE diff --git a/puredata/conanfile.txt b/puredata/conanfile.txt deleted file mode 100644 index e8bfe5b..0000000 --- a/puredata/conanfile.txt +++ /dev/null @@ -1,6 +0,0 @@ -[requires] -fftw/3.3.10 -yaml-cpp/0.8.0 - -[generators] -MakeDeps \ No newline at end of file diff --git a/puredata/help/zerr_envelopes~-help.pd b/puredata/help/zerr_envelopes~-help.pd index 3032c81..77a7b73 100644 --- a/puredata/help/zerr_envelopes~-help.pd +++ b/puredata/help/zerr_envelopes~-help.pd @@ -1,12 +1,12 @@ -#N canvas 2846 -433 936 1323 12; -#X floatatom 331 809 5 0 100 0 - - - 0; -#X obj 331 831 / 100; -#X floatatom 192 803 5 0 100 0 - - - 0; -#X obj 192 829 / 100; -#X obj 30 809 snapshot~; -#X obj 30 782 metro 100; -#X obj 30 758 tgl 19 0 empty empty empty 0 -10 0 12 #fcfcfc #000000 #000000 0 1; -#X obj 30 842 vsl 19 162 0 1 0 0 empty empty empty 0 -9 0 12 #fcfcfc #000000 #000000 0 1; +#N canvas 2025 -430 936 1323 12; +#X floatatom 331 462 5 0 100 0 - - - 0; +#X obj 331 484 / 100; +#X floatatom 192 456 5 0 100 0 - - - 0; +#X obj 192 482 / 100; +#X obj 30 462 snapshot~; +#X obj 30 435 metro 100; +#X obj 30 411 tgl 19 0 empty empty empty 0 -10 0 12 #fcfcfc #000000 #000000 0 1; +#X obj 30 495 vsl 19 162 0 1 0 0 empty empty empty 0 -9 0 12 #fcfcfc #000000 #000000 0 1; #N canvas 1709 -138 685 329 reference 0; #X obj 18 52 cnv 5 550 5 empty empty INLET: 8 18 0 13 #202020 #000000 0; #X obj 18 130 cnv 2 550 2 empty empty OUTLET: 8 12 0 13 #202020 #000000 0; @@ -21,12 +21,12 @@ #X text 89 61 signal - trigger or trajectory mapping; #X text 89 81 signal - spread parameter for sound width; #X text 89 100 signal - overall envelopes gain; -#X restore 10 1409 pd reference; -#X text 104 1409 <= click; -#X obj 14 408 cnv 1 520 1 empty empty empty 8 12 0 13 #000000 #000000 0; -#X obj 9 1349 cnv 1 520 1 empty empty empty 8 12 0 13 #000000 #000000 0; -#X msg 422 800 \; pd dsp \$1; -#X text 460 770 DSP on/off; +#X restore 10 1062 pd reference; +#X text 104 1062 <= click; +#X obj 14 61 cnv 1 520 1 empty empty empty 8 12 0 13 #000000 #000000 0; +#X obj 9 1002 cnv 1 520 1 empty empty empty 8 12 0 13 #000000 #000000 0; +#X msg 422 453 \; pd dsp \$1; +#X text 460 423 DSP on/off; #N canvas 2566 -231 499 459 set-dsp-tgl 0; #X msg 241 138 set 0; #X msg 182 109 set 1; @@ -55,7 +55,7 @@ #X connect 9 0 0 0; #X connect 11 0 7 0; #X coords 0 -1 1 1 30 30 2 100 100; -#X restore 422 762 pd set-dsp-tgl; +#X restore 422 415 pd set-dsp-tgl; #N canvas 504 119 1266 805 view 0; #X obj 42 55 snapshot~; #X obj 252 4 metro 100; @@ -109,10 +109,10 @@ #X connect 24 0 8 0; #X connect 25 0 6 0; #X coords 0 -1 1 1 284 196 2 100 100; -#X restore 54 1197 pd view; -#X text 307 378 - generate envelopes through different strategies; -#X obj 331 762 loadbang; -#X obj 192 776 loadbang; +#X restore 54 850 pd view; +#X text 307 31 - generate envelopes through different strategies; +#X obj 331 415 loadbang; +#X obj 192 429 loadbang; #N canvas 157 38 1355 830 source 0; #X obj 50 191 phasor~ 0.1; #X msg 213 159 0; @@ -147,30 +147,30 @@ #X connect 12 0 1 0; #X connect 13 0 15 0; #X connect 15 0 11 0; -#X restore 54 758 pd source signal; -#X msg 331 786 100; -#X text 12 592 Messages to set the trajectory vector \, set topological matrix \, active or disable speakers are available. (ps. the message functions are still in development.), f 74; -#X text 12 493 First argument chooses the envelope generation strategy: "trajectory" and "trigger". The second parameter is the path to the speaker array configuration file. It accepts .yaml file as input., f 74; -#X text 12 537 The number of outlets is automatically assigned by the speaker array configuration. It's equal to the number of speakers. The order of outlets is as same as the order of speakers. The outlets send envelope signals in audio rate., f 74; -#X text 12 412 The [zerr_envelopes~] generate envelopes according to input signals and the speaker array setups. All the input signals and output signals are in audio rate. The first inlet receives main input signal as trigger[0 or 1] or trajectory mapping[0.0 - 1.0]. The second inlet controls the spread parameter. How width the the sound distributed to. The third inlet controls the overall envelope gain., f 74; -#X text 12 633 In the help patch \, a source signal pendeling between 0 and 1 is generated. The [zerr_envelopes~] is in trajectory mode and uses a typical 8 speaker ring configuration. That connected in serial order in default. 1->2->3->4->5->6->7->8. For simplicity \, the spread and gain are linked to the float number. The envelope center moves from 1 to 8 when the source signal from 0 to 1.0. You can click on the set trajectory messages to change the mapping oder or use the active-speaker message to add/del speakers(buggy), f 74; -#X obj 54 1156 zerr_envelopes~ trajectory circulation_8; -#X obj 15 379 zerr_envelopes~ trajectory circulation_8; -#X msg 105 982 print; -#X obj 291 927 loadbang; -#X msg 218 893 curr 4; -#X msg 291 957 interval 0; -#X text 105 957 Print current status; -#X text 218 867 Hard set the current active speaker; -#X text 291 898 The FadeIn/Out time in ms; -#X msg 404 967 topo set 1 2 \, topo set 2 4 \, topo set 3 5 7 \, topo set 4 1 \, topo set 5 2 \, topo set 6 4 \, topo set 7 8 \, topo set 8 1; -#X text 404 938 Set the topological connection between speakers; -#X msg 382 1031 active del 3; -#X msg 482 1031 active add 3; -#X text 382 1006 active/deactive speakers; -#X msg 379 1108 traj 1 8 2 7 3 6 4 5; -#X text 379 1082 Set the trajectory connection; -#X msg 290 985 interval 50; +#X restore 54 411 pd source signal; +#X msg 331 439 100; +#X text 12 245 Messages to set the trajectory vector \, set topological matrix \, active or disable speakers are available. (ps. the message functions are still in development.), f 74; +#X text 12 146 First argument chooses the envelope generation strategy: "trajectory" and "trigger". The second parameter is the path to the speaker array configuration file. It accepts .yaml file as input., f 74; +#X text 12 190 The number of outlets is automatically assigned by the speaker array configuration. It's equal to the number of speakers. The order of outlets is as same as the order of speakers. The outlets send envelope signals in audio rate., f 74; +#X text 12 65 The [zerr_envelopes~] generate envelopes according to input signals and the speaker array setups. All the input signals and output signals are in audio rate. The first inlet receives main input signal as trigger[0 or 1] or trajectory mapping[0.0 - 1.0]. The second inlet controls the spread parameter. How width the the sound distributed to. The third inlet controls the overall envelope gain., f 74; +#X text 12 286 In the help patch \, a source signal pendeling between 0 and 1 is generated. The [zerr_envelopes~] is in trajectory mode and uses a typical 8 speaker ring configuration. That connected in serial order in default. 1->2->3->4->5->6->7->8. For simplicity \, the spread and gain are linked to the float number. The envelope center moves from 1 to 8 when the source signal from 0 to 1.0. You can click on the set trajectory messages to change the mapping oder or use the active-speaker message to add/del speakers(buggy), f 74; +#X obj 54 809 zerr_envelopes~ trajectory circulation_8; +#X obj 15 32 zerr_envelopes~ trajectory circulation_8; +#X msg 105 635 print; +#X obj 291 580 loadbang; +#X msg 218 546 curr 4; +#X msg 291 610 interval 0; +#X text 105 610 Print current status; +#X text 218 520 Hard set the current active speaker; +#X text 291 551 The FadeIn/Out time in ms; +#X msg 404 620 topo set 1 2 \, topo set 2 4 \, topo set 3 5 7 \, topo set 4 1 \, topo set 5 2 \, topo set 6 4 \, topo set 7 8 \, topo set 8 1; +#X text 404 591 Set the topological connection between speakers; +#X msg 382 684 active del 3; +#X msg 482 684 active add 3; +#X text 382 659 active/deactive speakers; +#X msg 379 761 traj 1 8 2 7 3 6 4 5; +#X text 379 735 Set the trajectory connection; +#X msg 290 638 interval 50; #X connect 0 0 1 0; #X connect 1 0 26 2; #X connect 2 0 3 0; diff --git a/puredata/include/zerr_features.h b/puredata/include/zerr_features.h index 0c16610..de8cfe6 100644 --- a/puredata/include/zerr_features.h +++ b/puredata/include/zerr_features.h @@ -1,7 +1,8 @@ /** * @file zerr_features.h * @author Zeyu Yang (zeyuuyang42@gmail.com) - * @brief AudioFeatures Class Puredata Wrapper - Provides interface between Pure Data and the core audio feature extraction functionality + * @brief AudioFeatures Class Puredata Wrapper - Provides interface between Pure Data and the core + * audio feature extraction functionality * @date 2024-01-30 * * @copyright Copyright (c) 2023-2024 @@ -20,10 +21,12 @@ /** * @class ZerrFeatures - * @brief Main wrapper class that interfaces between Pure Data and the core audio feature extraction functionality - * - * This class handles the initialization, audio processing, and cleanup of audio feature extraction operations. - * It manages the data flow between Pure Data's audio system and the internal feature processing chain. + * @brief Main wrapper class that interfaces between Pure Data and the core audio feature extraction + * functionality + * + * This class handles the initialization, audio processing, and cleanup of audio feature extraction + * operations. It manages the data flow between Pure Data's audio system and the internal feature + * processing chain. */ class ZerrFeatures { public: @@ -34,7 +37,7 @@ class ZerrFeatures { * @param sys_cnfg Pure Data system configuration containing sample rate and block size settings * @param ft_names List of audio features to extract from the input signal */ - ZerrFeatures(zerr::SystemConfigs sys_cnfg, zerr::t_featureNames ft_names); + ZerrFeatures(zerr::SystemConfigs sys_cnfg, zerr::FeatureNames ft_names); /** * @brief Initializes all internal components and prepares the object for processing * @return 1 if initialization was successful, 0 otherwise @@ -43,9 +46,10 @@ class ZerrFeatures { /** * @brief Main DSP callback function that processes audio buffers * @param ports Array of pointers to input/output audio buffers (shared memory between in/out) - * @param n_vec The actual size of audio vectors to process (may be smaller than system block size) + * @param n_vec The actual size of audio vectors to process (may be smaller than system block + * size) */ - void perform(float **ports, int n_vec); + void perform(float** ports, int n_vec); /** * @brief Gets the total number of ports (inlets + outlets) * @return Total count of all audio ports @@ -60,15 +64,16 @@ class ZerrFeatures { zerr::SystemConfigs systemConfigs; /**< Pure Data system configuration settings */ zerr::FeatureNames featureNames; /**< List of enabled audio feature extractors */ - zerr::Blocks input_buffer; /**< Buffer for storing incoming audio samples */ - zerr::FeaturesVals output_buffer; /**< Buffer for storing extracted feature values */ + zerr::Blocks input_buffer; /**< Buffer for storing incoming audio samples */ + zerr::FeaturesVals output_buffer; /**< Buffer for storing extracted feature values */ - float **in_ptr; /**< Array of pointers to Pure Data input signal vectors */ - float **out_ptr; /**< Array of pointers to Pure Data output signal vectors */ + float** in_ptr; /**< Array of pointers to Pure Data input signal vectors */ + float** out_ptr; /**< Array of pointers to Pure Data output signal vectors */ - std::vector in_tmp; /**< Temporary buffer for input signal processing */ + std::vector in_tmp; /**< Temporary buffer for input signal processing */ - std::string zerr_cfg; /**< Path to configuration file */ + std::string zerr_cfg; /**< Path to configuration file */ - zerr::FeatureBank *bank; /**< Core component that implements the feature extraction algorithms */ + zerr::FeatureBank* + bank; /**< Core component that implements the feature extraction algorithms */ }; diff --git a/puredata/mingw-profile.txt b/puredata/mingw-profile.txt deleted file mode 100644 index 79e8607..0000000 --- a/puredata/mingw-profile.txt +++ /dev/null @@ -1,13 +0,0 @@ -[settings] -os=Windows -arch=x86_64 -build_type=Release -compiler=gcc -compiler.version=13 -compiler.libcxx=libstdc++11 -compiler.threads=posix -compiler.exception=seh -compiler.cppstd=17 - -[conf] -tools.build:compiler_executables={"c":"x86_64-w64-mingw32-gcc","cpp":"x86_64-w64-mingw32-g++"} \ No newline at end of file diff --git a/puredata/src/zerr_envelopes.cpp b/puredata/src/zerr_envelopes.cpp index b564c65..b05344e 100644 --- a/puredata/src/zerr_envelopes.cpp +++ b/puredata/src/zerr_envelopes.cpp @@ -8,13 +8,15 @@ */ #include "zerr_envelopes.h" -ZerrEnvelopes::ZerrEnvelopes(zerr::SystemConfigs systemCfgs, std::string selectionMode, std::string spkrCfgFile) +ZerrEnvelopes::ZerrEnvelopes(zerr::SystemConfigs systemCfgs, std::string selectionMode, + std::string spkrCfgFile) { - this->systemCfgs = systemCfgs; - this->spkrCfgFile = spkrCfgFile; + this->systemCfgs = systemCfgs; + this->spkrCfgFile = spkrCfgFile; this->selectionMode = selectionMode; - envelopeGenerator = new zerr::EnvelopeGenerator(systemCfgs, spkrCfgFile, selectionMode); + envelopeGenerator = + new zerr::EnvelopeGenerator(systemCfgs, spkrCfgFile, zerr::parseGenMode(selectionMode)); } bool ZerrEnvelopes::initialize() @@ -26,9 +28,7 @@ bool ZerrEnvelopes::initialize() // assgin Max/MSP post method the the logger function // Because we want to see logs in the PD log window - auto printFunc = [](const std::string& msg) { - post(msg.c_str()); - }; + auto printFunc = [](const std::string& msg) { post(msg.c_str()); }; envelopeGenerator->setPrinter(printFunc); numOutlet = envelopeGenerator->getNumSpeakers(); @@ -36,7 +36,7 @@ bool ZerrEnvelopes::initialize() inputBuffer.resize(numInlet, zerr::Samples(systemCfgs.block_size, 0.0f)); outputBuffer.resize(numOutlet, zerr::Samples(systemCfgs.block_size, 0.0f)); - inPtr = (float**)getbytes(numInlet * sizeof(float**)); + inPtr = (float**)getbytes(numInlet * sizeof(float**)); outPtr = (float**)getbytes(numOutlet * sizeof(float**)); return true; @@ -44,7 +44,7 @@ bool ZerrEnvelopes::initialize() void ZerrEnvelopes::perform(float** ports, int blockSize) { - inPtr = (float**)&ports[0]; + inPtr = (float**)&ports[0]; outPtr = (float**)&ports[numInlet]; for (int i = 0; i < numInlet; i++) { @@ -62,10 +62,7 @@ void ZerrEnvelopes::perform(float** ports, int blockSize) } } -int ZerrEnvelopes::get_port_count() -{ - return numInlet + numOutlet; -} +int ZerrEnvelopes::get_port_count() { return numInlet + numOutlet; } void ZerrEnvelopes::setActiveSpeakerIndexs(char* action, int* idxs, size_t size) { @@ -97,12 +94,6 @@ void ZerrEnvelopes::setTriggerInterval(float interval) envelopeGenerator->setTriggerInterval(newInterval); } -void ZerrEnvelopes::printParameters() -{ - envelopeGenerator->printParameters(); -} +void ZerrEnvelopes::printParameters() { envelopeGenerator->printParameters(); } -ZerrEnvelopes::~ZerrEnvelopes() -{ - delete envelopeGenerator; -} +ZerrEnvelopes::~ZerrEnvelopes() { delete envelopeGenerator; } diff --git a/puredata/src/zerr_envelopes~.cpp b/puredata/src/zerr_envelopes~.cpp index 1674af8..5964192 100644 --- a/puredata/src/zerr_envelopes~.cpp +++ b/puredata/src/zerr_envelopes~.cpp @@ -42,10 +42,14 @@ void* zerr_envelopes_tilde_new(__attribute__((unused)) t_symbol* s, int argc, t_ return NULL; // no enough args to initialize object if (argv[0].a_type != A_SYMBOL) return NULL; - char* selectionMode = strdup(atom_getsymbol(argv)->s_name); + // Borrowed, not copied: a t_symbol's name lives as long as the Pd process, and + // both strings are consumed before this function returns (selectionMode into a + // std::string parameter, spkrCfgName into canvas_open). strdup here only added + // an allocation that every early return below would leak. + const char* selectionMode = atom_getsymbol(argv)->s_name; if (argv[1].a_type != A_SYMBOL) return NULL; - char* spkrCfgName = strdup(atom_getsymbol(argv + 1)->s_name); + const char* spkrCfgName = atom_getsymbol(argv + 1)->s_name; // find the absolute path of config file t_canvas* canvas = canvas_getcurrent(); @@ -69,12 +73,33 @@ void* zerr_envelopes_tilde_new(__attribute__((unused)) t_symbol* s, int argc, t_ strcat(spkrCfgFile, nameResult); post(spkrCfgFile); - // create & initialize ZerrEnvelopes object - x->z = new ZerrEnvelopes(systemCfgs, selectionMode, spkrCfgFile); - if (!x->z) - return NULL; - if (!x->z->initialize()) + // create & initialize ZerrEnvelopes object. + // The constructor calls zerr::parseGenMode(), which throws on an unknown mode + // argument. An exception must never escape into Pd's C call stack: it would + // reach std::terminate and abort the whole host, losing unsaved patches. Fail + // the way Pd expects instead -- report and return NULL, so the object simply + // "couldn't create". + // + // Both failure paths destroy the instance: Pd does not call the free method when + // the new method returns NULL, so anything constructed here is ours to release. + // x->z is NULL on entry (pd_new zeroes the object), which is what makes the + // delete in the catch correct whether the throw came from the constructor -- + // nothing to destroy -- or from initialize(). + x->z = NULL; + try { + x->z = new ZerrEnvelopes(systemCfgs, selectionMode, spkrCfgFile); + if (!x->z->initialize()) { + delete x->z; + x->z = NULL; + return NULL; + } + } + catch (const std::exception& e) { + pd_error(x, "zerr_envelopes~: %s", e.what()); + delete x->z; + x->z = NULL; return NULL; + } // create inlets x->spread_inlet = inlet_new(&x->x_obj, &x->x_obj.ob_pd, &s_signal, &s_signal); diff --git a/puredata/src/zerr_features.cpp b/puredata/src/zerr_features.cpp index 234ad21..ea4f2ec 100644 --- a/puredata/src/zerr_features.cpp +++ b/puredata/src/zerr_features.cpp @@ -3,7 +3,7 @@ * @author Zeyu Yang (zeyuuyang42@gmail.com) * @brief AudioFeatures Class Puredata Wrapper * @date 2024-01-30 - * + * * @copyright Copyright (c) 2023-2025 */ @@ -11,23 +11,25 @@ #include -ZerrFeatures::ZerrFeatures(zerr::SystemConfigs sys_cnfg, zerr::t_featureNames ft_names): - input_buffer(n_inlet, std::vector(sys_cnfg.block_size, 0.0f)) { +ZerrFeatures::ZerrFeatures(zerr::SystemConfigs sys_cnfg, zerr::FeatureNames ft_names) + // Order matches the declaration order in zerr_features.h (featureNames precedes + // input_buffer); initializers run in declaration order regardless, so listing + // them out of order only produces a -Wreorder-ctor warning. + : featureNames(std::move(ft_names)), + input_buffer(n_inlet, std::vector(sys_cnfg.block_size, 0.0f)) +{ bank = new zerr::FeatureBank(); systemConfigs.sample_rate = sys_cnfg.sample_rate; systemConfigs.block_size = sys_cnfg.block_size; - - for (int i = 0; i < ft_names.num; ++i) { - featureNames.push_back(ft_names.names[i]); - } } - -int ZerrFeatures::initialize() { +int ZerrFeatures::initialize() +{ try { bank->initialize(featureNames, systemConfigs); - } catch (...) { + } + catch (...) { // send bank initialize failed return 0; } @@ -37,16 +39,16 @@ int ZerrFeatures::initialize() { input_buffer.resize(n_inlet, std::vector(systemConfigs.block_size, 0.0f)); output_buffer.resize(n_outlet); - in_ptr = (float **) malloc(n_inlet * sizeof(float **)); - out_ptr = (float **) malloc(n_outlet * sizeof(float **)); + in_ptr = (float**)malloc(n_inlet * sizeof(float**)); + out_ptr = (float**)malloc(n_outlet * sizeof(float**)); return 1; } - -void ZerrFeatures::perform(float **ports, int n_vec) { - in_ptr = (float **) &ports[0]; - out_ptr = (float **) &ports[n_inlet]; +void ZerrFeatures::perform(float** ports, int n_vec) +{ + in_ptr = (float**)&ports[0]; + out_ptr = (float**)&ports[n_inlet]; for (int i = 0; i < n_inlet; i++) { for (int j = 0; j < n_vec; j++) { @@ -63,12 +65,6 @@ void ZerrFeatures::perform(float **ports, int n_vec) { } } +int ZerrFeatures::get_port_count() { return n_inlet + n_outlet; } -int ZerrFeatures::get_port_count() { - return n_inlet+n_outlet; -} - - -ZerrFeatures::~ZerrFeatures() { - delete bank; -} +ZerrFeatures::~ZerrFeatures() { delete bank; } diff --git a/puredata/src/zerr_features~.cpp b/puredata/src/zerr_features~.cpp index cba8190..2124205 100644 --- a/puredata/src/zerr_features~.cpp +++ b/puredata/src/zerr_features~.cpp @@ -3,7 +3,7 @@ * @author Zeyu Yang (zeyuuyang42@gmail.com) * @brief zerr_features~ Pure Data External * @date 2024-01-30 - * + * * @copyright Copyright (c) 2023-2025 */ #include "./zerr_features_tilde.h" @@ -12,106 +12,102 @@ extern "C" { #endif -static t_class *zerr_features_tilde_class; +static t_class* zerr_features_tilde_class; -void *zerr_features_tilde_new(t_symbol *s, int argc, t_atom *argv) { - zerr_features_tilde *x = (zerr_features_tilde *) - pd_new(zerr_features_tilde_class); - if (!x) return NULL; +void* zerr_features_tilde_new(t_symbol* s, int argc, t_atom* argv) +{ + zerr_features_tilde* x = (zerr_features_tilde*)pd_new(zerr_features_tilde_class); + if (!x) + return NULL; // at least one feature name should be given - if (argc < 1) return NULL; + if (argc < 1) + return NULL; - // zerr data structure for saving feature names - zerr::t_featureNames ft_names; - ft_names.names = (char **)malloc(argc * sizeof(char *)); - ft_names.num = argc; - - // copy arguments to ft_names structure + // parse feature names from arguments + zerr::FeatureNames ft_names; for (int i = 0; i < argc; i++) { if (argv[i].a_type == A_SYMBOL) { - ft_names.names[i] = strdup(atom_getsymbol(argv+i)->s_name); - } else { + ft_names.push_back(atom_getsymbol(argv + i)->s_name); + } + else { return NULL; } } // system config to initialize zerr: sample rate, block size zerr::SystemConfigs sys_cnfg; - sys_cnfg.sample_rate = (size_t) sys_getsr(); - sys_cnfg.block_size = (size_t) sys_getblksize(); + sys_cnfg.sample_rate = (size_t)sys_getsr(); + sys_cnfg.block_size = (size_t)sys_getblksize(); // create & initialize ZerrFeatures instance x->z = new ZerrFeatures(sys_cnfg, ft_names); - if (!x->z) return NULL; - if (!x->z->initialize()) return NULL; + if (!x->z) + return NULL; + if (!x->z->initialize()) + return NULL; // create the same number of outlets as features x->n_outlet = argc; - x->x_vec = (t_zerrout *)getbytes(x->n_outlet * sizeof(*x->x_vec)); + x->x_vec = (t_zerrout*)getbytes(x->n_outlet * sizeof(*x->x_vec)); - t_zerrout *u; + t_zerrout* u; int i; for (i = 0, u = x->x_vec; i < x->n_outlet; u++, i++) { u->u_outlet = outlet_new(&x->x_obj, &s_signal); } - return (void *) x; + return (void*)x; } - -void zerr_features_tilde_free(zerr_features_tilde *x) { +void zerr_features_tilde_free(zerr_features_tilde* x) +{ freebytes(x->x_vec, x->n_outlet * sizeof(*x->x_vec)); delete x->z; } +static t_int* zerr_features_tilde_perform(t_int* w) +{ + zerr_features_tilde* x = (zerr_features_tilde*)w[1]; + int n_vec = (int)w[2]; + int n_args = (int)w[3]; -static t_int *zerr_features_tilde_perform(t_int *w) { - zerr_features_tilde *x = (zerr_features_tilde *) w[1]; - int n_vec = (int) w[2]; - int n_args = (int) w[3]; - - t_sample **ports = (t_sample **) &w[4]; + t_sample** ports = (t_sample**)&w[4]; x->z->perform(ports, n_vec); - return &w[n_args+1]; + return &w[n_args + 1]; } +void zerr_features_tilde_dsp(zerr_features_tilde* x, t_signal** sp) +{ + int n_rest = 3; // size of [x, n_vec, n_args] -void zerr_features_tilde_dsp(zerr_features_tilde *x, t_signal **sp) { - int n_rest = 3; // size of [x, n_vec, n_args] - - int n_vec = sp[0]->s_n; + int n_vec = sp[0]->s_n; int n_port = x->z->get_port_count(); int n_args = n_port + n_rest; - t_int *vec = (t_int *) getbytes(n_args * sizeof(t_int *)); + t_int* vec = (t_int*)getbytes(n_args * sizeof(t_int*)); - vec[0] = (t_int) x; - vec[1] = (t_int) n_vec; - vec[2] = (t_int) n_args; + vec[0] = (t_int)x; + vec[1] = (t_int)n_vec; + vec[2] = (t_int)n_args; for (int i = 0; i < n_port; ++i) { - vec[i+n_rest] = (t_int) sp[i]->s_vec; + vec[i + n_rest] = (t_int)sp[i]->s_vec; } dsp_addv(zerr_features_tilde_perform, n_args, vec); } +void zerr_features_tilde_setup(void) +{ + zerr_features_tilde_class = + class_new(gensym("zerr_features~"), (t_newmethod)zerr_features_tilde_new, + (t_method)zerr_features_tilde_free, (size_t)sizeof(zerr_features_tilde), + CLASS_DEFAULT, A_GIMME, 0); -void zerr_features_tilde_setup(void) { - zerr_features_tilde_class = class_new(gensym("zerr_features~"), - (t_newmethod) zerr_features_tilde_new, - (t_method) zerr_features_tilde_free, - (size_t) sizeof(zerr_features_tilde), - CLASS_DEFAULT, - A_GIMME, 0); - - class_addmethod(zerr_features_tilde_class, - (t_method) zerr_features_tilde_dsp, - gensym("dsp"), - A_CANT, - A_NULL); + class_addmethod(zerr_features_tilde_class, (t_method)zerr_features_tilde_dsp, gensym("dsp"), + A_CANT, A_NULL); class_sethelpsymbol(zerr_features_tilde_class, gensym("zerr_features~")); CLASS_MAINSIGNALIN(zerr_features_tilde_class, zerr_features_tilde, f); @@ -120,6 +116,3 @@ void zerr_features_tilde_setup(void) { #ifdef __cplusplus } #endif - - -