Audacity 3.2.0
MusicInformationRetrievalTests.cpp
Go to the documentation of this file.
1#include "MirFakes.h"
4#include "WavMirAudioReader.h"
5
6#include <catch2/catch.hpp>
7
8namespace MIR
9{
10TEST_CASE("GetBpmFromFilename")
11{
12 const std::vector<std::pair<std::string, std::optional<double>>> testCases {
13 { "120 BPM", 120 },
14
15 // there may be an extension
16 { "120 BPM.opus", 120 },
17 { "120 BPM", 120 },
18
19 // it may be preceeded by a path
20 { "C:/my\\path/to\\120 BPM", 120 },
21
22 // value must be between 30 and 300 inclusive
23 { "1 BPM", std::nullopt },
24 { "29 BPM", std::nullopt },
25 { "30 BPM", 30 },
26 { "300 BPM", 300 },
27 { "301 BPM", std::nullopt },
28 { "1000 BPM", std::nullopt },
29
30 // it may be preceeded by zeros
31 { "000120 BPM", 120 },
32
33 // there may be something before the value
34 { "anything 120 BPM", 120 },
35 // but then there must be a separator
36 { "anything120 BPM", std::nullopt },
37 // there may be something after the value
38 { "120 BPM anything", 120 },
39 // but then there must also be a separator
40 { "120 BPManything", std::nullopt },
41
42 // what separator is used doesn't matter
43 { "anything-120-BPM", 120 },
44 { "anything_120_BPM", 120 },
45 { "anything.120.BPM", 120 },
46
47 // but of course that can't be an illegal filename character
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 },
53 // ... and so on.
54
55 // separators before and after don't have to match
56 { "anything_120-BPM", 120 },
57
58 // no separator between value and "bpm" is ok
59 { "anything.120BPM", 120 },
60
61 // a few real file names found out there
62 { "Cymatics - Cyclone Top Drum Loop 3 - 174 BPM", 174 },
63 { "Fantasie Impromptu Op. 66.mp3", std::nullopt },
64 };
65 std::vector<bool> success(testCases.size());
66 std::transform(
67 testCases.begin(), testCases.end(), success.begin(),
68 [](const auto& testCase) {
69 return GetBpmFromFilename(testCase.first) == testCase.second;
70 });
71 REQUIRE(
72 std::all_of(success.begin(), success.end(), [](bool b) { return b; }));
73}
74
75namespace
76{
77using namespace LibFileFormats;
78constexpr auto filename100bpm = "my/path\\foo_-_100BPM_Sticks_-_foo.wav";
81} // namespace
82
83TEST_CASE("GetProjectSyncInfo")
84{
85 SECTION("operator bool")
86 {
87 SECTION("returns false if ACID tag says one-shot")
88 {
89 auto input = arbitaryInput;
90 input.tags.emplace(AcidizerTags::OneShot {});
91 REQUIRE(!GetProjectSyncInfo(input).has_value());
92 }
93
94 SECTION("returns true if ACID tag says non-one-shot")
95 {
96 auto input = arbitaryInput;
97 input.tags.emplace(AcidizerTags::Loop { 120.0 });
98 REQUIRE(GetProjectSyncInfo(input).has_value());
99 }
100
101 SECTION("BPM is invalid")
102 {
103 SECTION("returns true if filename has BPM")
104 {
105 auto input = arbitaryInput;
106 input.filename = filename100bpm;
107 REQUIRE(GetProjectSyncInfo(input).has_value());
108 }
109
110 SECTION("returns false if filename has no BPM")
111 {
112 auto input = arbitaryInput;
113 input.filename = "filenameWithoutBpm";
114 REQUIRE(!GetProjectSyncInfo(input).has_value());
115 }
116 }
117 }
118
119 SECTION("GetProjectSyncInfo")
120 {
121 SECTION("prioritizes ACID tags over filename")
122 {
123 auto input = arbitaryInput;
124 input.filename = filename100bpm;
125 input.tags.emplace(AcidizerTags::Loop { 120. });
126 const auto info = GetProjectSyncInfo(input);
127 REQUIRE(info);
128 REQUIRE(info->rawAudioTempo == 120);
129 }
130
131 SECTION("falls back on filename if tag bpm is invalid")
132 {
133 auto input = arbitaryInput;
134 input.filename = filename100bpm;
135 input.tags.emplace(AcidizerTags::Loop { -1. });
136 const auto info = GetProjectSyncInfo(input);
137 REQUIRE(info);
138 REQUIRE(info->rawAudioTempo == 100);
139 }
140
141 SECTION("stretchMinimizingPowOfTwo is as expected")
142 {
143 auto input = arbitaryInput;
144 input.filename = filename100bpm;
145
146 input.projectTempo = 100.;
147 REQUIRE(GetProjectSyncInfo(input)->stretchMinimizingPowOfTwo == 1.);
148
149 // Project tempo twice as fast. Without compensation, the audio would
150 // be stretched to 0.5 its length. Not stretching it at all may still
151 // yield musically interesting results.
152 input.projectTempo = 200;
153 REQUIRE(GetProjectSyncInfo(input)->stretchMinimizingPowOfTwo == 2.);
154
155 // Same principle applies in the following:
156 input.projectTempo = 400;
157 REQUIRE(GetProjectSyncInfo(input)->stretchMinimizingPowOfTwo == 4.);
158 input.projectTempo = 50;
159 REQUIRE(GetProjectSyncInfo(input)->stretchMinimizingPowOfTwo == .5);
160 input.projectTempo = 25;
161 REQUIRE(GetProjectSyncInfo(input)->stretchMinimizingPowOfTwo == .25);
162
163 // Now testing edge cases:
164 input.projectTempo = 100 * std::pow(2, .51);
165 REQUIRE(GetProjectSyncInfo(input)->stretchMinimizingPowOfTwo == 2.);
166 input.projectTempo = 100 * std::pow(2, .49);
167 REQUIRE(GetProjectSyncInfo(input)->stretchMinimizingPowOfTwo == 1.);
168 input.projectTempo = 100 * std::pow(2, -.49);
169 REQUIRE(GetProjectSyncInfo(input)->stretchMinimizingPowOfTwo == 1.);
170 input.projectTempo = 100 * std::pow(2, -.51);
171 REQUIRE(GetProjectSyncInfo(input)->stretchMinimizingPowOfTwo == .5);
172 }
173 }
174}
175
176TEST_CASE("SynchronizeProject")
177{
178 constexpr auto initialProjectTempo = 100.;
179 FakeProjectInterface project { initialProjectTempo };
180
181 SECTION("single-file import")
182 {
183 constexpr FakeAnalyzedAudioClip::Params clipParams {
185 };
186
187 // Generate all possible situations, and in the sections filter for the
188 // conditions we want to check.
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);
193
194 const std::vector<std::shared_ptr<AnalyzedAudioClip>> clips {
195 std::make_shared<FakeAnalyzedAudioClip>(
196 clipsHaveTempo ? std::make_optional(clipParams) : std::nullopt)
197 };
198
199 const auto projectWasReconfigured = [&](bool yes) {
200 const auto reconfigurationCheck = yes == project.wasReconfigured;
201 const auto projectTempoCheck =
202 project.projectTempo ==
203 (yes ? clipParams.tempo : initialProjectTempo);
204 REQUIRE(reconfigurationCheck);
205 REQUIRE(projectTempoCheck);
206 };
207
208 const auto clipsWereSynchronized = [&](bool yes) {
209 const auto check = yes == project.clipsWereSynchronized;
210 REQUIRE(check);
211 };
212
213 SECTION("nothing happens if")
214 {
215 SECTION("no clip has tempo")
216 if (!clipsHaveTempo)
217 {
218 SynchronizeProject(clips, project, projectWasEmpty);
219 projectWasReconfigured(false);
220 clipsWereSynchronized(false);
221 }
222 SECTION(
223 "user doesn't want reconfiguration and view is minutes and seconds")
224 if (!project.shouldBeReconfigured && !project.isBeatsAndMeasures)
225 {
226 SynchronizeProject(clips, project, projectWasEmpty);
227 projectWasReconfigured(false);
228 clipsWereSynchronized(false);
229 }
230 SECTION(
231 "user wants reconfiguration but view is minutes and seconds and project is not empty")
232 if (
233 project.shouldBeReconfigured && !project.isBeatsAndMeasures &&
234 !projectWasEmpty)
235 {
236 SynchronizeProject(clips, project, projectWasEmpty);
237 projectWasReconfigured(false);
238 clipsWereSynchronized(false);
239 }
240 }
241
242 SECTION(
243 "project gets reconfigured only if clips have tempo, user wants to and project is empty")
244 {
245 SynchronizeProject(clips, project, projectWasEmpty);
246 projectWasReconfigured(
247 clipsHaveTempo && project.shouldBeReconfigured && projectWasEmpty);
248 }
249
250 SECTION("project does not get reconfigured if")
251 {
252 SECTION("user doesn't want to")
253 if (!project.shouldBeReconfigured)
254 {
255 SynchronizeProject(clips, project, projectWasEmpty);
256 projectWasReconfigured(false);
257 }
258
259 SECTION("project was not empty")
260 if (!projectWasEmpty)
261 {
262 SynchronizeProject(clips, project, projectWasEmpty);
263 projectWasReconfigured(false);
264 }
265 }
266
267 SECTION("clips don't get synchronized if view is minutes and seconds and")
268 if (!project.isBeatsAndMeasures)
269 {
270 SECTION("user says no to reconfiguration")
271 if (!project.shouldBeReconfigured)
272 {
273 SynchronizeProject(clips, project, projectWasEmpty);
274 clipsWereSynchronized(false);
275 }
276 SECTION("project was not empty")
277 if (!projectWasEmpty)
278 {
279 SynchronizeProject(clips, project, projectWasEmpty);
280 clipsWereSynchronized(false);
281 }
282 }
283
284 SECTION("clips get synchronized if some clip has tempo and")
285 if (clipsHaveTempo)
286 {
287 SECTION(
288 "user doesn't want reconfiguration but view is beats and measures")
289 if (!project.shouldBeReconfigured && project.isBeatsAndMeasures)
290 {
291 SynchronizeProject(clips, project, projectWasEmpty);
292 clipsWereSynchronized(true);
293 }
294 SECTION(
295 "user wants reconfiguration, view is beats and measures and project is not empty")
296 if (
297 project.shouldBeReconfigured && project.isBeatsAndMeasures &&
298 !projectWasEmpty)
299 {
300 SynchronizeProject(clips, project, projectWasEmpty);
301 clipsWereSynchronized(true);
302 }
303 }
304 }
305
306 SECTION("multiple-file import")
307 {
308 project.shouldBeReconfigured = true;
309 constexpr auto projectWasEmpty = true;
310
311 SECTION(
312 "for clips of different tempi, precedence is header-based, then title-based, then signal-based")
313 {
315 {
316 std::make_shared<FakeAnalyzedAudioClip>(
319 std::make_shared<FakeAnalyzedAudioClip>(
322 std::make_shared<FakeAnalyzedAudioClip>(
325 },
326 project, projectWasEmpty);
327 REQUIRE(project.projectTempo == 456.);
328
330 {
331 std::make_shared<FakeAnalyzedAudioClip>(
334 std::make_shared<FakeAnalyzedAudioClip>(
337 },
338 project, projectWasEmpty);
339 REQUIRE(project.projectTempo == 123.);
340
342 {
343 std::make_shared<FakeAnalyzedAudioClip>(
346 },
347 project, projectWasEmpty);
348 REQUIRE(project.projectTempo == 789.);
349 }
350
351 SECTION("raw audio tempo of one-shot clips is set to project tempo")
352 {
353 const auto oneShotClip =
354 std::make_shared<FakeAnalyzedAudioClip>(std::nullopt);
355 constexpr auto whicheverMethod = TempoObtainedFrom::Signal;
357 {
358 std::make_shared<FakeAnalyzedAudioClip>(
359 FakeAnalyzedAudioClip::Params { 123., whicheverMethod }),
360 oneShotClip,
361 },
362 project, projectWasEmpty);
363 REQUIRE(project.projectTempo == 123);
364 REQUIRE(oneShotClip->rawAudioTempo == 123);
365 }
366 }
367}
368} // namespace MIR
const auto project
TEST_CASE("GetBpmFromFilename")
std::optional< ProjectSyncInfo > GetProjectSyncInfo(const ProjectSyncInfoInput &in)
void SynchronizeProject(const std::vector< std::shared_ptr< AnalyzedAudioClip > > &clips, ProjectInterface &project, bool projectWasEmpty)
std::optional< LibFileFormats::AcidizerTags > tags