Audacity 3.2.0
MusicInformationRetrieval.cpp
Go to the documentation of this file.
1/* SPDX-License-Identifier: GPL-2.0-or-later */
2/*!********************************************************************
3
4 Audacity: A Digital Audio Editor
5
6 MusicInformationRetrieval.cpp
7
8 Matthieu Hodgkinson
9
10**********************************************************************/
14#include "MirProjectInterface.h"
15#include "MirTypes.h"
16#include "MirUtils.h"
17#include "StftFrameProvider.h"
18
19#include "MemoryX.h"
20
21#include <array>
22#include <cassert>
23#include <cmath>
24#include <numeric>
25#include <regex>
26
27namespace MIR
28{
29namespace
30{
31// Normal distribution parameters obtained by fitting a gaussian in the GTZAN
32// dataset tempo values.
33static constexpr auto bpmExpectedValue = 126.3333;
34
35constexpr auto numTimeSignatures = static_cast<int>(TimeSignature::_count);
36
37auto RemovePathPrefix(const std::string& filename)
38{
39 return filename.substr(filename.find_last_of("/\\") + 1);
40}
41
42// When we get time-signature estimate, we may need a map for that, since 6/8
43// has 1.5 quarter notes per beat.
44constexpr std::array<double, numTimeSignatures> quarternotesPerBeat { 2., 1.,
45 1., 1.5 };
46} // namespace
47
48std::optional<ProjectSyncInfo>
50{
51 if (in.tags.has_value() && in.tags->isOneShot)
52 // That's a one-shot file, we don't want to sync it.
53 return {};
54
55 std::optional<double> bpm;
56 std::optional<TimeSignature> timeSignature;
57 std::optional<TempoObtainedFrom> usedMethod;
58
59 if (in.tags.has_value() && in.tags->bpm.has_value() && *in.tags->bpm > 30.)
60 {
61 bpm = in.tags->bpm;
62 usedMethod = TempoObtainedFrom::Header;
63 }
64 else if ((bpm = GetBpmFromFilename(in.filename)))
65 usedMethod = TempoObtainedFrom::Title;
66 else if (
67 const auto meter = GetMusicalMeterFromSignal(
68 in.source,
72 {
73 bpm = meter->bpm;
74 timeSignature = meter->timeSignature;
75 usedMethod = TempoObtainedFrom::Signal;
76 }
77 else
78 return {};
79
80 const auto qpm = *bpm * quarternotesPerBeat[static_cast<int>(
81 timeSignature.value_or(TimeSignature::FourFour))];
82
83 auto recommendedStretch = 1.0;
84 if (!in.projectWasEmpty)
85 // There already is content in this project, meaning that its tempo won't
86 // be changed. Change speed by some power of two to minimize stretching.
87 recommendedStretch =
88 std::pow(2., std::round(std::log2(in.projectTempo / qpm)));
89
90 auto excessDurationInQuarternotes = 0.;
91 auto numQuarters = in.source.GetDuration() * qpm / 60.;
92 const auto roundedNumQuarters = std::round(numQuarters);
93 const auto delta = numQuarters - roundedNumQuarters;
94 // If there is an excess less than a 32nd, we treat it as an edit error.
95 if (0 < delta && delta < 1. / 8)
96 excessDurationInQuarternotes = delta;
97
98 return ProjectSyncInfo {
99 qpm,
100 *usedMethod,
101 timeSignature,
102 recommendedStretch,
103 excessDurationInQuarternotes,
104 };
105}
106
107std::optional<double> GetBpmFromFilename(const std::string& filename)
108{
109 // regex matching a forward or backward slash:
110
111 // Regex: <(anything + (directory) separator) or nothing> <2 or 3 digits>
112 // <optional separator> <bpm (case-insensitive)> <separator or nothing>
113 const std::regex bpmRegex {
114 R"((?:.*(?:_|-|\s|\.|/|\\))?(\d+)(?:_|-|\s|\.)?bpm(?:(?:_|-|\s|\.).*)?)",
115 std::regex::icase
116 };
117 std::smatch matches;
118 if (std::regex_match(filename, matches, bpmRegex))
119 try
120 {
121 const auto value = std::stoi(matches[1]);
122 return 30 <= value && value <= 300 ? std::optional<double> { value } :
123 std::nullopt;
124 }
125 catch (const std::invalid_argument& e)
126 {
127 assert(false);
128 }
129 return {};
130}
131
132std::optional<MusicalMeter> GetMusicalMeterFromSignal(
134 const std::function<void(double)>& progressCallback,
135 QuantizationFitDebugOutput* debugOutput)
136{
137 if (audio.GetSampleRate() <= 0)
138 return {};
139 const auto duration = 1. * audio.GetNumSamples() / audio.GetSampleRate();
140 if (duration > 60)
141 // A file longer than 1 minute is most likely not a loop, and processing
142 // it would be costly.
143 return {};
144 DecimatingMirAudioReader decimatedAudio { audio };
146 decimatedAudio, tolerance, progressCallback, debugOutput);
147}
148
150 const std::vector<std::shared_ptr<AnalyzedAudioClip>>& clips,
151 ProjectInterface& project, bool projectWasEmpty)
152{
153 const auto isBeatsAndMeasures = project.ViewIsBeatsAndMeasures();
154
155 if (!projectWasEmpty && !isBeatsAndMeasures)
156 return;
157
158 const auto projectTempo =
159 !projectWasEmpty ? std::make_optional(project.GetTempo()) : std::nullopt;
160
161 if (!std::any_of(
162 clips.begin(), clips.end(),
163 [](const std::shared_ptr<AnalyzedAudioClip>& clip) {
164 return clip->GetSyncInfo().has_value();
165 }))
166 return;
167
168 Finally Do = [&] {
169 // Re-evaluate if we are in B&M view - we might have convinced the user to
170 // switch:
171 if (!project.ViewIsBeatsAndMeasures())
172 return;
173 std::for_each(
174 clips.begin(), clips.end(),
175 [&](const std::shared_ptr<AnalyzedAudioClip>& clip) {
176 clip->Synchronize();
177 });
178 project.OnClipsSynchronized();
179 };
180
181 if (!projectWasEmpty && isBeatsAndMeasures)
182 return;
183
184 const auto [loopIndices, oneshotIndices] = [&] {
185 std::vector<size_t> loopIndices;
186 std::vector<size_t> oneshotIndices;
187 for (size_t i = 0; i < clips.size(); ++i)
188 if (clips[i]->GetSyncInfo().has_value())
189 loopIndices.push_back(i);
190 else
191 oneshotIndices.push_back(i);
192 return std::make_pair(loopIndices, oneshotIndices);
193 }();
194
195 // Favor results based on reliability. We assume that header info is most
196 // reliable, followed by title, followed by DSP.
197 std::unordered_map<TempoObtainedFrom, size_t> indexMap;
198 std::for_each(loopIndices.begin(), loopIndices.end(), [&](size_t i) {
199 const auto usedMethod = clips[i]->GetSyncInfo()->usedMethod;
200 if (!indexMap.count(usedMethod))
201 indexMap[usedMethod] = i;
202 });
203
204 const auto chosenIndex = indexMap.count(TempoObtainedFrom::Header) ?
205 indexMap.at(TempoObtainedFrom::Header) :
206 indexMap.count(TempoObtainedFrom::Title) ?
207 indexMap.at(TempoObtainedFrom::Title) :
208 indexMap.at(TempoObtainedFrom::Signal);
209
210 const auto& chosenSyncInfo = *clips[chosenIndex]->GetSyncInfo();
211 const auto isSingleFileImport = clips.size() == 1;
212 if (!project.ShouldBeReconfigured(
213 chosenSyncInfo.rawAudioTempo, isSingleFileImport))
214 return;
215
216 project.ReconfigureMusicGrid(
217 chosenSyncInfo.rawAudioTempo, chosenSyncInfo.timeSignature);
218
219 // Reset tempo of one-shots to this new project tempo, so that they don't
220 // get stretched:
221 std::for_each(oneshotIndices.begin(), oneshotIndices.end(), [&](size_t i) {
222 clips[i]->SetRawAudioTempo(chosenSyncInfo.rawAudioTempo);
223 });
224}
225} // namespace MIR
MockedAudio audio
const auto project
Our MIR operations do not need the full 44.1 or 48kHz resolution typical of audio files....
double GetDuration() const
Definition: MirTypes.h:120
constexpr std::array< double, numTimeSignatures > quarternotesPerBeat
std::optional< MusicalMeter > GetMusicalMeterFromSignal(const MirAudioReader &audio, FalsePositiveTolerance tolerance, const std::function< void(double)> &progressCallback, QuantizationFitDebugOutput *debugOutput)
std::optional< double > GetBpmFromFilename(const std::string &filename)
FalsePositiveTolerance
Definition: MirTypes.h:25
std::optional< MusicalMeter > GetMeterUsingTatumQuantizationFit(const MirAudioReader &audio, FalsePositiveTolerance tolerance, const std::function< void(double)> &progressCallback, QuantizationFitDebugOutput *debugOutput)
Get the BPM of the given audio file, using the Tatum Quantization Fit method.
std::optional< ProjectSyncInfo > GetProjectSyncInfo(const ProjectSyncInfoInput &in)
void SynchronizeProject(const std::vector< std::shared_ptr< AnalyzedAudioClip > > &clips, ProjectInterface &project, bool projectWasEmpty)
fastfloat_really_inline void round(adjusted_mantissa &am, callback cb) noexcept
Definition: fast_float.h:2512
"finally" as in The C++ Programming Language, 4th ed., p. 358 Useful for defining ad-hoc RAII actions...
Definition: MemoryX.h:175
std::function< void(double progress)> progressCallback
std::optional< LibFileFormats::AcidizerTags > tags