6#include <catch2/catch.hpp>
12 const std::vector<std::pair<std::string, std::optional<double>>> testCases {
16 {
"120 BPM.opus", 120 },
20 {
"C:/my\\path/to\\120 BPM", 120 },
23 {
"1 BPM", std::nullopt },
24 {
"29 BPM", std::nullopt },
27 {
"301 BPM", std::nullopt },
28 {
"1000 BPM", std::nullopt },
31 {
"000120 BPM", 120 },
34 {
"anything 120 BPM", 120 },
36 {
"anything120 BPM", std::nullopt },
38 {
"120 BPM anything", 120 },
40 {
"120 BPManything", std::nullopt },
43 {
"anything-120-BPM", 120 },
44 {
"anything_120_BPM", 120 },
45 {
"anything.120.BPM", 120 },
48 {
"120/BPM", std::nullopt },
49 {
"120\\BPM", std::nullopt },
50 {
"120:BPM", std::nullopt },
51 {
"120;BPM", std::nullopt },
52 {
"120'BPM", std::nullopt },
56 {
"anything_120-BPM", 120 },
59 {
"anything.120BPM", 120 },
62 {
"Cymatics - Cyclone Top Drum Loop 3 - 174 BPM", 174 },
63 {
"Fantasie Impromptu Op. 66.mp3", std::nullopt },
65 std::vector<bool> success(testCases.size());
67 testCases.begin(), testCases.end(), success.begin(),
68 [](
const auto& testCase) {
69 return GetBpmFromFilename(testCase.first) == testCase.second;
72 std::all_of(success.begin(), success.end(), [](
bool b) { return b; }));
85 SECTION(
"operator bool")
87 SECTION(
"returns false if ACID tag says one-shot")
94 SECTION(
"returns true if ACID tag says non-one-shot")
101 SECTION(
"BPM is invalid")
103 SECTION(
"returns true if filename has BPM")
110 SECTION(
"returns false if filename has no BPM")
113 input.
filename =
"filenameWithoutBpm";
119 SECTION(
"GetProjectSyncInfo")
121 SECTION(
"prioritizes ACID tags over filename")
128 REQUIRE(info->rawAudioTempo == 120);
131 SECTION(
"falls back on filename if tag bpm is invalid")
138 REQUIRE(info->rawAudioTempo == 100);
141 SECTION(
"stretchMinimizingPowOfTwo is as expected")
146 input.projectTempo = 100.;
152 input.projectTempo = 200;
156 input.projectTempo = 400;
158 input.projectTempo = 50;
160 input.projectTempo = 25;
164 input.projectTempo = 100 * std::pow(2, .51);
166 input.projectTempo = 100 * std::pow(2, .49);
168 input.projectTempo = 100 * std::pow(2, -.49);
170 input.projectTempo = 100 * std::pow(2, -.51);
178 constexpr auto initialProjectTempo = 100.;
181 SECTION(
"single-file import")
189 project.isBeatsAndMeasures = GENERATE(
false,
true);
190 project.shouldBeReconfigured = GENERATE(
false,
true);
191 const auto projectWasEmpty = GENERATE(
false,
true);
192 const auto clipsHaveTempo = GENERATE(
false,
true);
194 const std::vector<std::shared_ptr<AnalyzedAudioClip>> clips {
195 std::make_shared<FakeAnalyzedAudioClip>(
196 clipsHaveTempo ? std::make_optional(clipParams) : std::nullopt)
199 const auto projectWasReconfigured = [&](
bool yes) {
200 const auto reconfigurationCheck = yes ==
project.wasReconfigured;
201 const auto projectTempoCheck =
203 (yes ? clipParams.tempo : initialProjectTempo);
204 REQUIRE(reconfigurationCheck);
205 REQUIRE(projectTempoCheck);
208 const auto clipsWereSynchronized = [&](
bool yes) {
209 const auto check = yes ==
project.clipsWereSynchronized;
213 SECTION(
"nothing happens if")
215 SECTION(
"no clip has tempo")
219 projectWasReconfigured(
false);
220 clipsWereSynchronized(
false);
223 "user doesn't want reconfiguration and view is minutes and seconds")
227 projectWasReconfigured(
false);
228 clipsWereSynchronized(
false);
231 "user wants reconfiguration but view is minutes and seconds and project is not empty")
237 projectWasReconfigured(
false);
238 clipsWereSynchronized(
false);
243 "project gets reconfigured only if clips have tempo, user wants to and project is empty")
246 projectWasReconfigured(
247 clipsHaveTempo &&
project.shouldBeReconfigured && projectWasEmpty);
250 SECTION(
"project does not get reconfigured if")
252 SECTION(
"user doesn't want to")
253 if (!
project.shouldBeReconfigured)
256 projectWasReconfigured(
false);
259 SECTION(
"project was not empty")
260 if (!projectWasEmpty)
263 projectWasReconfigured(
false);
267 SECTION(
"clips don't get synchronized if view is minutes and seconds and")
268 if (!
project.isBeatsAndMeasures)
270 SECTION(
"user says no to reconfiguration")
271 if (!
project.shouldBeReconfigured)
274 clipsWereSynchronized(
false);
276 SECTION(
"project was not empty")
277 if (!projectWasEmpty)
280 clipsWereSynchronized(
false);
284 SECTION(
"clips get synchronized if some clip has tempo and")
288 "user doesn't want reconfiguration but view is beats and measures")
292 clipsWereSynchronized(
true);
295 "user wants reconfiguration, view is beats and measures and project is not empty")
301 clipsWereSynchronized(
true);
306 SECTION(
"multiple-file import")
308 project.shouldBeReconfigured =
true;
309 constexpr auto projectWasEmpty =
true;
312 "for clips of different tempi, precedence is header-based, then title-based, then signal-based")
316 std::make_shared<FakeAnalyzedAudioClip>(
319 std::make_shared<FakeAnalyzedAudioClip>(
322 std::make_shared<FakeAnalyzedAudioClip>(
327 REQUIRE(
project.projectTempo == 456.);
331 std::make_shared<FakeAnalyzedAudioClip>(
334 std::make_shared<FakeAnalyzedAudioClip>(
339 REQUIRE(
project.projectTempo == 123.);
343 std::make_shared<FakeAnalyzedAudioClip>(
348 REQUIRE(
project.projectTempo == 789.);
351 SECTION(
"raw audio tempo of one-shot clips is set to project tempo")
353 const auto oneShotClip =
354 std::make_shared<FakeAnalyzedAudioClip>(std::nullopt);
358 std::make_shared<FakeAnalyzedAudioClip>(
363 REQUIRE(
project.projectTempo == 123);
364 REQUIRE(oneShotClip->rawAudioTempo == 123);
TEST_CASE("GetBpmFromFilename")
std::optional< ProjectSyncInfo > GetProjectSyncInfo(const ProjectSyncInfoInput &in)
void SynchronizeProject(const std::vector< std::shared_ptr< AnalyzedAudioClip > > &clips, ProjectInterface &project, bool projectWasEmpty)