Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 27 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)

<p align="center"><img src="site/images/BeatIt_Logo.png" width="30%" /><h1 align="center">BeatIt</h1></p>

Machine Learning assisted Beat and Downbeat Tracking Framework for macOS.

## Core Values
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -250,6 +264,7 @@ Important options:
- `--preset <beattrack|beatthis>`
- `--device <auto|cpu|gpu|neural>`
- `--model <path>`
- `--dump-events`
- `--min-bpm <bpm>` / `--max-bpm <bpm>` (validated in `[70,180]`)
- `--dbn` / `--no-dbn`
- `--log-level <error|warn|info|debug>`
Expand Down
134 changes: 113 additions & 21 deletions scripts/package_macos_pkg.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 "/"
)
Expand All @@ -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" <<XML
<?xml version="1.0" encoding="utf-8"?>
<installer-gui-script minSpecVersion="1">
<title>BeatIt</title>
<welcome file="README.txt"/>
<welcome file="WELCOME.txt"/>
<readme file="README.txt"/>
<license file="LICENSE.txt"/>
<options customize="never" require-scripts="false"/>
<options customize="always" require-scripts="false"/>
<domains enable_anywhere="false" enable_currentUserHome="false" enable_localSystem="true"/>
<choices-outline>
<line choice="default">
<line choice="com.tilltoenshoff.beatit.runtime.choice"/>
<line choice="com.tilltoenshoff.beatit.cli.choice"/>
<line choice="com.tilltoenshoff.beatit.coreml.choice"/>
$(if [[ $HAVE_TORCH_RUNTIME -eq 1 ]]; then cat <<'CHOICE'
<line choice="com.tilltoenshoff.beatit.torch.choice"/>
CHOICE
fi)
<line choice="com.tilltoenshoff.beatit.framework.choice"/>
</line>
</choices-outline>
<choice id="default"/>
<choice id="com.tilltoenshoff.beatit.runtime.choice" visible="false">
<pkg-ref id="com.tilltoenshoff.beatit.runtime"/>
<choice id="default" title="BeatIt" description="Install BeatIt CLI and framework."/>
<choice id="com.tilltoenshoff.beatit.cli.choice" title="BeatIt CLI" description="Install the BeatIt command line tool and shared documentation under /usr/local.">
<pkg-ref id="com.tilltoenshoff.beatit.cli"/>
</choice>
<choice id="com.tilltoenshoff.beatit.coreml.choice" title="BeatIt CoreML Runtime Support" description="Install the CoreML backend plugin and BeatThis CoreML model assets for the CLI under /usr/local.">
<pkg-ref id="com.tilltoenshoff.beatit.coreml"/>
</choice>
$(if [[ $HAVE_TORCH_RUNTIME -eq 1 ]]; then cat <<'CHOICE'
<choice id="com.tilltoenshoff.beatit.torch.choice" title="BeatIt Torch Runtime Support" description="Install the Torch backend plugin, Torch runtime dylibs, and BeatThis Torch model assets for the CLI under /usr/local.">
<pkg-ref id="com.tilltoenshoff.beatit.torch"/>
</choice>
<choice id="com.tilltoenshoff.beatit.framework.choice" visible="false">
CHOICE
fi)
<choice id="com.tilltoenshoff.beatit.framework.choice" title="BeatIt Framework" description="Install BeatIt.framework including backend plugins, and bundled model assets under /Library/Frameworks.">
<pkg-ref id="com.tilltoenshoff.beatit.framework"/>
</choice>
<pkg-ref id="com.tilltoenshoff.beatit.runtime">beatit-runtime.pkg</pkg-ref>
<pkg-ref id="com.tilltoenshoff.beatit.cli">beatit-cli.pkg</pkg-ref>
<pkg-ref id="com.tilltoenshoff.beatit.coreml">beatit-coreml-runtime.pkg</pkg-ref>
$(if [[ $HAVE_TORCH_RUNTIME -eq 1 ]]; then cat <<'CHOICE'
<pkg-ref id="com.tilltoenshoff.beatit.torch">beatit-torch-runtime.pkg</pkg-ref>
CHOICE
fi)
<pkg-ref id="com.tilltoenshoff.beatit.framework">beatit-framework.pkg</pkg-ref>
</installer-gui-script>
XML
Expand Down
Binary file added site/images/BeatIt_Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 25 additions & 12 deletions src/cli/main.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>(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 ||
Expand Down Expand Up @@ -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<std::size_t>(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<double>(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";
}
Expand Down Expand Up @@ -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");
Expand Down