diff --git a/README.md b/README.md index e819285..e7284ac 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# BeatIt - [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![Build](https://img.shields.io/badge/build-CMake-blue.svg)](#build) [![Tests](https://img.shields.io/badge/tests-ctest-orange.svg)](#tests) +

BeatIt

+ Machine Learning assisted Beat and Downbeat Tracking Framework for macOS. ## Core Values @@ -44,6 +44,30 @@ Several canonical files are currently exact or perceptually exact in tempo, drif phase after BeatIt postprocessing, while the same material was visibly worse with the model output alone. +### Flexibility + +BeatIt is intentionally not locked to one inference stack. The project currently supports two +entirely different backend families: + +- CoreML for the native macOS path +- Torch for the PyTorch model path + +Those backends are loaded through plugins rather than hard-linked into one monolithic binary. +That matters in practice: + +- the default CoreML path stays lightweight +- the heavy Torch runtime closure, easily well above `200 MB`, remains optional +- the project can ship one product while still tracking multiple model ecosystems + +This is not just a packaging detail. It is a deliberate architectural choice: + +- BeatIt can stay fast and native on the default macOS path +- while still being able to adopt new model exports, new research code, or new runtime stacks + when machine-learning approaches to tempo, beat, and downbeat detection move forward + +That flexibility is what lets BeatIt combine a strongly optimized native path with the ability +to follow the latest model trends instead of freezing the project around one runtime forever. + ## Features ### Sparse Selection @@ -214,16 +238,6 @@ available as an optional backend. ### CLI -#### Defaults - -If you run the CLI without model switches, BeatIt uses: - -- backend: CoreML -- preset: `beatthis` -- BPM range clamp: `70..180` -- DBN: enabled (calmdad mode) -- sparse probe mode: enabled - Basic: ```bash @@ -250,6 +264,7 @@ Important options: - `--preset ` - `--device ` - `--model ` +- `--dump-events` - `--min-bpm ` / `--max-bpm ` (validated in `[70,180]`) - `--dbn` / `--no-dbn` - `--log-level ` diff --git a/scripts/package_macos_pkg.sh b/scripts/package_macos_pkg.sh index bcdb506..44cf2e8 100755 --- a/scripts/package_macos_pkg.sh +++ b/scripts/package_macos_pkg.sh @@ -20,42 +20,70 @@ INSTALLER_SIGN_KEYCHAIN="${INSTALLER_SIGN_KEYCHAIN:-}" UNSIGNED_PKG_PATH="${DIST_DIR}/unsigned-${PKG_NAME}" RUNTIME_PAYLOAD_ROOT="$DIST_DIR/pkgroot-runtime" +COREML_PAYLOAD_ROOT="$DIST_DIR/pkgroot-coreml" +TORCH_PAYLOAD_ROOT="$DIST_DIR/pkgroot-torch" COMPONENT_DIR="$DIST_DIR/pkg-components" -RUNTIME_PKG_PATH="$COMPONENT_DIR/beatit-runtime.pkg" +RUNTIME_PKG_PATH="$COMPONENT_DIR/beatit-cli.pkg" +COREML_PKG_PATH="$COMPONENT_DIR/beatit-coreml-runtime.pkg" +TORCH_PKG_PATH="$COMPONENT_DIR/beatit-torch-runtime.pkg" FRAMEWORK_PKG_PATH="$COMPONENT_DIR/beatit-framework.pkg" DISTRIBUTION_XML="$DIST_DIR/distribution.xml" RESOURCE_DIR="$DIST_DIR/pkg-resources" mkdir -p "$DIST_DIR" -rm -rf "$RUNTIME_PAYLOAD_ROOT" "$COMPONENT_DIR" "$RESOURCE_DIR" +rm -rf "$RUNTIME_PAYLOAD_ROOT" "$COREML_PAYLOAD_ROOT" "$TORCH_PAYLOAD_ROOT" "$COMPONENT_DIR" "$RESOURCE_DIR" rm -f "$PKG_PATH" "$UNSIGNED_PKG_PATH" "$DISTRIBUTION_XML" mkdir -p "$COMPONENT_DIR" "$RESOURCE_DIR" mkdir -p "$RUNTIME_PAYLOAD_ROOT/usr/local/bin" -mkdir -p "$RUNTIME_PAYLOAD_ROOT/usr/local/lib/beatit" mkdir -p "$RUNTIME_PAYLOAD_ROOT/usr/local/share/beatit/models" +mkdir -p "$COREML_PAYLOAD_ROOT/usr/local/lib/beatit" +mkdir -p "$COREML_PAYLOAD_ROOT/usr/local/share/beatit/models" +mkdir -p "$TORCH_PAYLOAD_ROOT/usr/local/lib/beatit" +mkdir -p "$TORCH_PAYLOAD_ROOT/usr/local/share/beatit/models" cp "$BUILD_DIR/beatit" "$RUNTIME_PAYLOAD_ROOT/usr/local/bin/beatit" cp "$ROOT/README.md" "$RUNTIME_PAYLOAD_ROOT/usr/local/share/beatit/README.md" cp "$ROOT/LICENSE" "$RUNTIME_PAYLOAD_ROOT/usr/local/share/beatit/LICENSE" -if [[ -d "$BUILD_DIR/plugins" ]]; then - find "$BUILD_DIR/plugins" -maxdepth 1 -type f -name '*.dylib' -print0 | while IFS= read -r -d '' dylib; do - cp "$dylib" "$RUNTIME_PAYLOAD_ROOT/usr/local/lib/beatit/" - done +COREML_PLUGIN_PATH="$BUILD_DIR/plugins/libbeatit_backend_coreml.dylib" +TORCH_PLUGIN_PATH="$BUILD_DIR/plugins/libbeatit_backend_torch.dylib" + +if [[ -f "$COREML_PLUGIN_PATH" ]]; then + cp "$COREML_PLUGIN_PATH" "$COREML_PAYLOAD_ROOT/usr/local/lib/beatit/" fi cp -R "$ROOT/models/BeatThis_small0.mlpackage" \ - "$RUNTIME_PAYLOAD_ROOT/usr/local/share/beatit/models/BeatThis_small0.mlpackage" + "$COREML_PAYLOAD_ROOT/usr/local/share/beatit/models/BeatThis_small0.mlpackage" + +if [[ -f "$TORCH_PLUGIN_PATH" ]]; then + find "$BUILD_DIR/plugins" -maxdepth 1 -type f -name '*.dylib' ! -name 'libbeatit_backend_coreml.dylib' -print0 | while IFS= read -r -d '' dylib; do + cp "$dylib" "$TORCH_PAYLOAD_ROOT/usr/local/lib/beatit/" + done +fi if [[ -f "$ROOT/models/BeatThis_small0.pt" ]]; then cp "$ROOT/models/BeatThis_small0.pt" \ - "$RUNTIME_PAYLOAD_ROOT/usr/local/share/beatit/models/BeatThis_small0.pt" + "$TORCH_PAYLOAD_ROOT/usr/local/share/beatit/models/BeatThis_small0.pt" fi RUNTIME_PKGBUILD_ARGS=( --root "$RUNTIME_PAYLOAD_ROOT" - --identifier "com.tilltoenshoff.beatit.runtime" + --identifier "com.tilltoenshoff.beatit.cli" + --version "$VERSION" + --install-location "/" +) + +COREML_PKGBUILD_ARGS=( + --root "$COREML_PAYLOAD_ROOT" + --identifier "com.tilltoenshoff.beatit.coreml" + --version "$VERSION" + --install-location "/" +) + +TORCH_PKGBUILD_ARGS=( + --root "$TORCH_PAYLOAD_ROOT" + --identifier "com.tilltoenshoff.beatit.torch" --version "$VERSION" --install-location "/" ) @@ -67,38 +95,102 @@ FRAMEWORK_PKGBUILD_ARGS=( --install-location "/Library/Frameworks" ) -echo "Building runtime component package: $RUNTIME_PKG_PATH" +echo "Building CLI component package: $RUNTIME_PKG_PATH" pkgbuild "${RUNTIME_PKGBUILD_ARGS[@]}" "$RUNTIME_PKG_PATH" +echo "Building CoreML runtime package: $COREML_PKG_PATH" +pkgbuild "${COREML_PKGBUILD_ARGS[@]}" "$COREML_PKG_PATH" + +HAVE_TORCH_RUNTIME=0 +if [[ -f "$TORCH_PLUGIN_PATH" && -f "$ROOT/models/BeatThis_small0.pt" ]]; then + echo "Building Torch runtime package: $TORCH_PKG_PATH" + pkgbuild "${TORCH_PKGBUILD_ARGS[@]}" "$TORCH_PKG_PATH" + HAVE_TORCH_RUNTIME=1 +fi + echo "Building framework component package: $FRAMEWORK_PKG_PATH" pkgbuild "${FRAMEWORK_PKGBUILD_ARGS[@]}" "$FRAMEWORK_PKG_PATH" -cp "$ROOT/README.md" "$RESOURCE_DIR/README.txt" cp "$ROOT/LICENSE" "$RESOURCE_DIR/LICENSE.txt" +cat > "$RESOURCE_DIR/WELCOME.txt" <<'TEXT' +Installs the BeatIt command-line tool, optional CoreML and Torch runtime +support for the CLI, and the BeatIt framework. + +Install locations: +- /usr/local/bin/beatit +- /usr/local/lib/beatit +- /usr/local/share/beatit +- /Library/Frameworks/BeatIt.framework + +The overall space requirement of the installation is around a crazy 500mb due to bundled model assets and runtime. + +Use the installer to deploy the runtime and framework on this Mac. +TEXT +cat > "$RESOURCE_DIR/README.txt" <<'TEXT' +BeatIt + +After installation, you can run BeatIt from Terminal: + + beatit -i /path/to/song.mp3 -cat > "$DISTRIBUTION_XML" <<'XML' +Installer choices: + +- BeatIt CLI +- BeatIt CoreML Runtime Support +- BeatIt Torch Runtime Support +- BeatIt Framework + +The framework is self-contained. The CLI needs at least one runtime support +package to run actual model inference. + +The full project documentation and license are installed here: + +- /usr/local/share/beatit/README.md +- /usr/local/share/beatit/LICENSE +TEXT + +cat > "$DISTRIBUTION_XML" < BeatIt - + - + - + + +$(if [[ $HAVE_TORCH_RUNTIME -eq 1 ]]; then cat <<'CHOICE' + +CHOICE +fi) - - - + + + + + + + +$(if [[ $HAVE_TORCH_RUNTIME -eq 1 ]]; then cat <<'CHOICE' + + - +CHOICE +fi) + - beatit-runtime.pkg + beatit-cli.pkg + beatit-coreml-runtime.pkg +$(if [[ $HAVE_TORCH_RUNTIME -eq 1 ]]; then cat <<'CHOICE' + beatit-torch-runtime.pkg +CHOICE +fi) beatit-framework.pkg XML diff --git a/site/images/BeatIt_Logo.png b/site/images/BeatIt_Logo.png new file mode 100644 index 0000000..bc6e0a9 Binary files /dev/null and b/site/images/BeatIt_Logo.png differ diff --git a/src/cli/main.mm b/src/cli/main.mm index a36f81d..8dd6df3 100644 --- a/src/cli/main.mm +++ b/src/cli/main.mm @@ -921,6 +921,15 @@ int main(int argc, char** argv) { stream << std::fixed << std::setprecision(precision) << value; return stream.str(); }; + const auto append_event_position = [&](std::ostream& stream, + unsigned long long sample_frame) { + double seconds = 0.0; + if (audio.sample_rate > 0.0) { + seconds = static_cast(sample_frame) / audio.sample_rate; + } + stream << "time=" << format_fixed(seconds, 3) + << " sec sample_frame=" << sample_frame; + }; if (!std::isfinite(result.estimated_bpm) || result.estimated_bpm <= 0.0 || @@ -997,20 +1006,26 @@ int main(int argc, char** argv) { } if (options.dump_events) { - std::cout << "Events (first 64):\n"; - const std::size_t max_beats = std::min(64, beat_feature_frames.size()); - for (std::size_t i = 0; i < max_beats; ++i) { + std::cout << "type,time_sec,sample_frame,activation\n"; + for (std::size_t i = 0; i < beat_feature_frames.size(); ++i) { const bool is_downbeat = std::find(downbeat_feature_frames.begin(), downbeat_feature_frames.end(), beat_feature_frames[i]) != downbeat_feature_frames.end(); - std::cout << (is_downbeat ? "* " : " ") - << "feature_frame=" << beat_feature_frames[i]; + std::cout << (is_downbeat ? "downbeat" : "beat") << ","; if (i < beat_sample_frames.size()) { - std::cout << " sample_frame=" << beat_sample_frames[i]; + double seconds = 0.0; + if (audio.sample_rate > 0.0) { + seconds = static_cast(beat_sample_frames[i]) / audio.sample_rate; + } + std::cout << format_fixed(seconds, 3) << "," << beat_sample_frames[i]; + } else { + std::cout << "n/a,n/a"; } if (i < result.coreml_beat_strengths.size()) { - std::cout << " strength=" << result.coreml_beat_strengths[i]; + std::cout << "," << result.coreml_beat_strengths[i]; + } else { + std::cout << ","; } std::cout << "\n"; } @@ -1198,11 +1213,9 @@ int main(int argc, char** argv) { std::find(refined.downbeat_feature_frames.begin(), refined.downbeat_feature_frames.end(), refined.beat_feature_frames[i]) != refined.downbeat_feature_frames.end(); - std::cout << (is_downbeat ? "* " : " ") - << "feature_frame=" << refined.beat_feature_frames[i] - << " sample_frame=" << refined.beat_sample_frames[i] - << " strength=" << refined.beat_strengths[i] - << "\n"; + std::cout << (is_downbeat ? "* " : " "); + append_event_position(std::cout, refined.beat_sample_frames[i]); + std::cout << " strength=" << refined.beat_strengths[i] << "\n"; } if (refined.beat_sample_frames.size() > 1) { print_bpm_stats(refined.beat_sample_frames, "Constant");