| 397 | }; |
| 398 | |
| 399 | static BeatTestResult runPeriodicBeatTest( |
| 400 | int totalBeats, int framesPerBeat, float amplitude = 20000.0f, |
| 401 | float threshold = 1.3f, float sensitivity = 1.0f) |
| 402 | { |
| 403 | audio::detector::Beat detector; |
| 404 | detector.setThreshold(threshold); |
| 405 | detector.setSensitivity(sensitivity); |
| 406 | |
| 407 | BeatTestResult result; |
| 408 | result.beatCount = 0; |
| 409 | |
| 410 | detector.onBeat.add([&result]() { result.beatCount++; }); |
| 411 | |
| 412 | const u32 frameMs = 23; // ~43 fps |
| 413 | fl::vector<fl::i16> silenceData(512, 0); |
| 414 | auto ctx = fl::make_shared<audio::Context>(audio::Sample(silenceData, 0)); |
| 415 | ctx->setSampleRate(44100); |
| 416 | ctx->setFFTHistoryDepth(4); |
| 417 | |
| 418 | // Warmup: 43 frames of silence to fill MovingAverage window |
| 419 | u32 timestamp = 0; |
| 420 | for (int i = 0; i < 43; ++i) { |
| 421 | timestamp += frameMs; |
| 422 | ctx->setSample(audio::Sample(silenceData, timestamp)); |
| 423 | detector.update(ctx); |
| 424 | detector.fireCallbacks(); |
| 425 | } |
| 426 | |
| 427 | // Run periodic pattern |
| 428 | for (int beat = 0; beat < totalBeats; ++beat) { |
| 429 | for (int frame = 0; frame < framesPerBeat; ++frame) { |
| 430 | timestamp += frameMs; |
| 431 | |
| 432 | if (frame == 0) { |
| 433 | ctx->setSample(makeBassSync(timestamp, amplitude)); |
| 434 | } else { |
| 435 | ctx->setSample(audio::Sample(silenceData, timestamp)); |
| 436 | } |
| 437 | |
| 438 | detector.update(ctx); |
| 439 | |
| 440 | if (detector.isBeat()) { |
| 441 | result.beatTimestamps.push_back(timestamp); |
| 442 | } |
| 443 | |
| 444 | detector.fireCallbacks(); |
| 445 | } |
| 446 | } |
| 447 | |
| 448 | result.finalBPM = detector.getBPM(); |
| 449 | result.finalConfidence = detector.getConfidence(); |
| 450 | return result; |
| 451 | } |
| 452 | |
| 453 | } // namespace test_beat_synthetic |
| 454 | using test_beat_synthetic::makeBassSync; |
no test coverage detected