DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
OfflineBeatEngine.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
10#include "../../Core/OfflineProcessing.h"
11#if DSPARK_HAS_OFFLINE
12#include "../BeatTracker.h"
13#include <cmath>
14#include <cstddef>
15#include <cstdint>
16#include <limits>
17#include <memory_resource>
18
19namespace dspark::detail
20{
21
22// Keep the existing tempo-worker growth policy in the shared Core pool.
24
25template <FloatType T> struct OfflineBeatEngine final
26{
27 struct Result
28 {
30 std::unique_ptr<std::int64_t[]> positions;
31 std::size_t count = 0;
32 [[nodiscard]] std::span<const std::int64_t> beats() const noexcept
33 {
34 return positions ? std::span<const std::int64_t>(positions.get(), count)
35 : std::span<const std::int64_t>();
36 }
37 };
38 // FrameAt returns the shared frontend's OdfFrame with an explicit source
39 // reference. No FFT, audio copy or second tempo algorithm is constructed.
40 template <typename FrameAt>
41 [[nodiscard]] static Result
42 analyze(double sampleRate, int hop, std::size_t count, FrameAt frameAt,
43 double minimumBpm, double maximumBpm, double tightness, OfflineSession &job,
44 std::pmr::memory_resource *workspace = nullptr)
45 {
46 if (!std::isfinite(sampleRate) || sampleRate < 8000 || sampleRate > 384000 || hop < 1 ||
47 hop > sampleRate || !std::isfinite(minimumBpm) || !std::isfinite(maximumBpm) ||
48 minimumBpm < 20 || maximumBpm > 480 || minimumBpm > maximumBpm ||
49 !std::isfinite(tightness) || tightness < 0.001 ||
50 tightness > static_cast<double>(std::numeric_limits<T>::max()))
52 // The shared DP uses int envelope indices; source references stay int64.
53 // Reserve arithmetic headroom for its bounded lag/window additions.
54 if (count > static_cast<std::size_t>(std::numeric_limits<int>::max() / 2))
56 job.checkpoint(OfflinePhase::Analyze, 0, static_cast<std::int64_t>(count));
57 if (count == 0)
58 return {};
59
60 BeatTracker<T> tracker;
61 tracker.allocationAccount_ = {&job, [](void *context, std::size_t bytes) {
62 static_cast<OfflineSession *>(context)->charge(bytes);
63 }};
64 if (workspace)
65 tracker.allocationAccount_ = {
66 workspace, nullptr,
67 [](void *context, std::size_t bytes, std::size_t alignment) {
68 return static_cast<std::pmr::memory_resource *>(context)->allocate(bytes, alignment);
69 },
70 [](void *context, void *p, std::size_t bytes, std::size_t alignment) {
71 static_cast<std::pmr::memory_resource *>(context)->deallocate(p, bytes, alignment);
72 }};
73 tracker.offlineCheckpointContext_ = &job;
74 tracker.offlineCheckpoint_ = [](void *context) {
75 static_cast<OfflineSession *>(context)->checkpoint(OfflinePhase::Analyze, 0, 0);
76 };
77 tracker.sampleRate_ = sampleRate;
78 tracker.hop_ = hop;
79 tracker.frameRate_ = sampleRate / hop;
80 tracker.buildBank();
81 tracker.setTempoRange(static_cast<T>(minimumBpm), static_cast<T>(maximumBpm));
82 tracker.setTightness(static_cast<T>(tightness));
83 tracker.env_.reserve(count);
84 tracker.envRef_.reserve(count);
85 for (auto &registers : tracker.envReg_)
86 registers.reserve(count);
87 std::int64_t previous = 0;
88 for (std::size_t i = 0; i < count; ++i)
89 {
90 if ((i & 4095) == 0)
91 job.checkpoint(OfflinePhase::Analyze, static_cast<std::int64_t>(i),
92 static_cast<std::int64_t>(count));
93 const auto frame = frameAt(i);
94 if (!std::isfinite(frame.value) || frame.value < T(0))
96 if (i && (previous > std::numeric_limits<std::int64_t>::max() - hop ||
97 frame.referenceSample != previous + hop))
99 // beatSample() interpolates by at most half a hop on either side.
100 if ((frame.referenceSample < std::numeric_limits<std::int64_t>::min() + hop) ||
101 (frame.referenceSample > std::numeric_limits<std::int64_t>::max() - hop))
103 tracker.env_.push_back(static_cast<double>(frame.value));
104 tracker.envRef_.push_back(frame.referenceSample);
105 for (std::size_t r = 0; r < frame.registers.size(); ++r)
106 {
107 const auto value = frame.registers[r];
108 if (!std::isfinite(value) || value < T(0))
110 tracker.envReg_[r].push_back(static_cast<double>(value));
111 }
112 previous = frame.referenceSample;
113 }
114 Result result;
115 struct Output
116 {
117 Result &result;
118 OfflineSession &job;
119 } output{result, job};
120 const auto summary = tracker.finishEnvelope(
121 &output, [](void *context, std::span<const std::int64_t> beats) {
122 auto &out = *static_cast<Output *>(context);
123 if (!beats.empty())
124 {
125 out.result.positions = out.job.template allocate<std::int64_t>(beats.size());
126 std::copy(beats.begin(), beats.end(), out.result.positions.get());
127 out.result.count = beats.size();
128 }
129 });
130 result.tempoBpm = summary.tempoBpm;
131 result.secondaryTempoBpm = summary.secondaryTempoBpm;
132 result.confidence = summary.confidence;
133 if (!std::isfinite(result.tempoBpm) || !std::isfinite(result.secondaryTempoBpm) ||
134 !std::isfinite(result.confidence))
136 job.checkpoint(OfflinePhase::Analyze, static_cast<std::int64_t>(count),
137 static_cast<std::int64_t>(count));
138 return result;
139 }
140};
141
142} // namespace dspark::detail
143#endif
Tempo and beat tracking with an offline grid and a causal readout.
void setTempoRange(T minBpm, T maxBpm) noexcept
Restricts the searched tempo range, in BPM. Lock free, no alloc.
void setTightness(T alpha) noexcept
Sets the dynamic-programming tightness (Ellis alpha).
void checkpoint(OfflinePhase phase, std::int64_t completed, std::int64_t total) const
void offlineFail(OfflineStatus status)
std::span< const std::int64_t > beats() const noexcept
std::unique_ptr< std::int64_t[]> positions
static Result analyze(double sampleRate, int hop, std::size_t count, FrameAt frameAt, double minimumBpm, double maximumBpm, double tightness, OfflineSession &job, std::pmr::memory_resource *workspace=nullptr)