66 [[nodiscard]]
bool isValid() const noexcept {
return beats_ !=
nullptr; }
71 [[nodiscard]] std::span<const Beat>
beats() const noexcept
73 return isValid() ? std::span<const Beat>(beats_.get(), count_)
74 : std::span<const Beat>();
77 [[nodiscard]]
double tempoAt(std::int64_t frame)
const noexcept
79 const auto map =
beats();
80 if (map.size() < 2 || frame < map.front().frame || frame >= map.back().frame)
82 auto next = std::upper_bound(map.begin(), map.end(), frame,
83 [](std::int64_t p,
const Beat &b) { return p < b.frame; });
84 const auto &beat = *(next - 1);
85 return beat.reliable ? beat.nextIntervalBpm : 0;
89 return isValid() ? std::max(std::size_t(1), count_) *
sizeof(
Beat) : 0;
98 std::unique_ptr<Beat[]> beats_;
99 std::size_t count_ = 0;
118 detail::OfflineSession job(jobOptions);
125 candidate.options_ = options;
126 const auto frames = features.
features();
128 candidate.spec_.sampleRate, features.
hopFrames(), frames.size(),
130 typename OnsetDetector<T>::OdfFrame frame;
131 frame.value = frames[i].attack;
132 frame.registers = frames[i].registers;
133 frame.referenceSample = features.featureCenter(i);
136 options.minimumBpm, options.maximumBpm, 25, job);
137 candidate.report_.tempoBpm =
static_cast<double>(tracked.tempoBpm);
138 candidate.report_.secondaryTempoBpm =
static_cast<double>(tracked.secondaryTempoBpm);
139 candidate.report_.confidence =
static_cast<double>(tracked.confidence);
140 for (
const auto frame : tracked.beats())
141 if (frame >= 0 && frame < candidate.spec_.frames)
143 candidate.beats_ = job.template allocate<Beat>(std::max(std::size_t(1), candidate.count_));
144 std::size_t index = 0;
145 for (
const auto frame : tracked.beats())
146 if (frame >= 0 && frame < candidate.spec_.frames)
147 candidate.beats_[index++].frame = frame;
148 for (std::size_t i = 0; i + 1 < candidate.count_; ++i)
150 auto &beat = candidate.beats_[i];
151 const auto next = candidate.beats_[i + 1].frame;
152 if (next <= beat.frame)
154 const double interval =
static_cast<double>(next - beat.frame);
155 std::array<double, 3> local{interval, interval, interval};
157 local[0] =
static_cast<double>(beat.frame - candidate.beats_[i - 1].frame);
158 if (i + 2 < candidate.count_)
159 local[2] =
static_cast<double>(candidate.beats_[i + 2].frame - next);
160 std::sort(local.begin(), local.end());
161 const double quarter = local[1];
162 beat.nextIntervalBpm = 60 * candidate.spec_.sampleRate / quarter;
163 const double tolerance = std::min(0.05 * candidate.spec_.sampleRate, 0.125 * interval);
168 beat.reliable = candidate.report_.confidence >= options.minimumConfidence &&
169 std::abs(interval - quarter) <= 2 * tolerance &&
170 anchored(features.
pulses(), beat.frame, tolerance) &&
171 anchored(features.
pulses(), next, tolerance);
172 candidate.report_.reliableIntervals += beat.reliable ? 1 : 0;
174 confirmLocalIntervals(features, candidate, job);
175 candidate.report_.reason =
180 result.memoryBytes = job.bytes();
181 result.analysis = std::move(candidate);
199 auto features = OfflineTransientAnalyzer<T>().analyze(source, {
false,
true,
true},
201 if (!features.succeeded())
203 result =
analyze(features.analysis, options,
205 if (!result.succeeded())
207 if (source.
getSpec() != result.analysis.getSpec())
209 result.memoryBytes += features.memoryBytes;
220 static void confirmLocalIntervals(
221 const typename OfflineTransientAnalyzer<T>::Analysis &features, Analysis &candidate,
222 detail::OfflineSession &job)
224 if (candidate.count_ < 2 || candidate.report_.confidence >= candidate.options_.minimumConfidence)
226 const auto frames = features.features();
227 const auto window =
static_cast<std::size_t
>(std::ceil(
228 std::max(8., 240 / candidate.options_.minimumBpm) * candidate.spec_.sampleRate /
229 features.hopFrames()));
230 if (frames.size() <= window)
233 for (std::size_t begin = 0;;)
236 candidate.spec_.sampleRate, features.hopFrames(), window,
238 typename OnsetDetector<T>::OdfFrame frame;
239 frame.value = frames[begin + i].attack;
240 frame.registers = frames[begin + i].registers;
241 frame.referenceSample = features.featureCenter(begin + i);
243 }, candidate.options_.minimumBpm, candidate.options_.maximumBpm, 25, job, &workspace);
244 ++candidate.report_.localWindows;
245 const auto grid = local.beats();
246 if (local.confidence >= candidate.options_.minimumConfidence && grid.size() >= 2)
248 const auto first = std::lower_bound(candidate.beats_.get(),
249 candidate.beats_.get() + candidate.count_, grid.front(),
250 [](
const Beat &beat,
auto p) { return beat.frame < p; });
251 auto index =
static_cast<std::size_t
>(first - candidate.beats_.get());
254 for (; index + 1 < candidate.count_ && candidate.beats_[index].frame <= grid.back(); ++index)
256 auto &beat = candidate.beats_[index];
259 const auto end = candidate.beats_[index + 1].frame;
260 const double interval =
static_cast<double>(end - beat.frame);
261 const double quarter = 60 * candidate.spec_.sampleRate / beat.nextIntervalBpm;
262 const double tolerance = std::min(.05 * candidate.spec_.sampleRate, .125 * interval);
263 if (std::abs(interval - quarter) > 2 * tolerance ||
264 !anchored(features.pulses(), beat.frame, tolerance) ||
265 !anchored(features.pulses(), end, tolerance))
267 auto position = std::lower_bound(grid.begin(), grid.end(), beat.frame);
268 if (position == grid.end() || (position != grid.begin() &&
269 beat.frame - *(position - 1) < *position - beat.frame))
271 if (position + 1 == grid.end() ||
272 std::abs(
static_cast<double>(*position - beat.frame)) > tolerance ||
273 std::abs(
static_cast<double>(*(position + 1) - end)) > tolerance)
275 beat.reliable =
true;
276 ++candidate.report_.reliableIntervals;
277 ++candidate.report_.locallyConfirmedIntervals;
280 if (begin + window == frames.size())
282 begin = std::min(begin + window / 2, frames.size() - window);
285 static void validate(Options options)
287 if (!std::isfinite(options.minimumBpm) || !std::isfinite(options.maximumBpm) ||
288 (options.minimumBpm < 20) || (options.maximumBpm > 480) ||
289 options.minimumBpm > options.maximumBpm || !std::isfinite(options.minimumConfidence) ||
290 (options.minimumConfidence < 0) || (options.minimumConfidence > 1))
293 [[nodiscard]]
static bool
294 anchored(std::span<
const typename OfflineTransientAnalyzer<T>::Event> pulses,
295 std::int64_t frame,
double tolerance)
noexcept
297 const auto next = std::lower_bound(pulses.begin(), pulses.end(), frame,
298 [](
const auto &event,
auto p) { return event.begin < p; });
299 return (next != pulses.end() &&
static_cast<double>(next->begin - frame) <= tolerance) ||
300 (next != pulses.begin() &&
static_cast<double>(frame - (next - 1)->begin) <= tolerance);