Audacity 3.2.0
BeatsNumericConverterFormatter.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 @file BeatsNumericConverterFormatter.cpp
7
8 Dmitry Vedenko
9
10 **********************************************************************/
12
13#include <algorithm>
14#include <array>
15#include <cmath>
16
19
20#include "SampleCount.h"
21
22#include "Project.h"
24
25namespace
26{
27// This function will return 10^pow
28// No overflow checks are performed, it is assumed that 10^pow
29// does not overflow
30constexpr size_t Get10Pow (size_t pow)
31{
32 return pow > 0 ? 10 * Get10Pow(pow - 1) : 1;
33}
34
35/* i18n-hint: The music theory "bar" */
36const auto BarString = XO("bar");
37/* i18n-hint: The music theory "beat" */
38const auto BeatString = XO("beat");
39
40class BeatsFormatter final :
42 public PrefsListener
43{
44public:
45 static constexpr std::array<size_t, 3> MIN_DIGITS { 3, 2, 2 };
46 static constexpr std::array<size_t, 3> UPPER_BOUNDS {
47 Get10Pow(MIN_DIGITS[0] - 1) + 1, Get10Pow(MIN_DIGITS[1] - 1) + 1,
48 Get10Pow(MIN_DIGITS[2] - 1) + 1
49 };
50
51 BeatsFormatter(const FormatterContext& context, int fracPart, bool timeFormat)
52 : mContext { context }
53 , mFracPart { fracPart }
54 , mFieldValueOffset { timeFormat ? 1 : 0 }
55 {
56 auto project = mContext.GetProject();
57
58 if (!project)
59 return;
60
61 mBarString = BarString.Translation();
62 mBeatString = BeatString.Translation();
63
64 UpdateFormat(*project);
65
66 // Subscribing requires non-const reference
67 mTimeSignatureChangedSubscription =
69 .Subscribe(
70 [this](const auto&)
71 {
72 // Receiving this message means that project is
73 // alive and well
74 UpdateFormat(*mContext.GetProject());
75 Publish({});
76 });
77 }
78
80 bool CheckField(size_t fieldIndex, int value) const noexcept
81 {
82 if (fieldIndex >= mFields.size())
83 return false;
84
85 const auto digitsCount = mFields[fieldIndex].digits;
86
87 // Format always allows at least two digits
88 const auto lowerRange =
89 digitsCount > MIN_DIGITS[fieldIndex] ? Get10Pow(digitsCount - 1) : 0;
90
91 const auto upperRange = Get10Pow(digitsCount);
92
93 return value >= int(lowerRange) && value < int(upperRange);
94 }
95
96 bool CheckFracField (int newLts) const noexcept
97 {
98 if (mFracPart > newLts)
99 return CheckField(2, mFracPart / mLowerTimeSignature);
100 else
101 return mFields.size() == 2;
102 }
103
104 void UpdateFields (size_t barsDigits)
105 {
106 mFields.clear();
107 mDigits.clear();
108
109 // Range is assumed to allow 999 bars.
110 auto& barsField =
111 mFields.emplace_back(NumericField::WithDigits(barsDigits));
112
113 barsField.label = L" " + mBarString + L" ";
114
115 // Beats format is 1 based. For the time point "0" the expected output is
116 // "1 bar 1 beat [1]" For this reason we use (uts + 1) as the "range". On
117 // top of that, we want at least two digits to be shown. NumericField
118 // accepts range as in [0, range), so add 1.
119
120 auto& beatsField = mFields.emplace_back(NumericField::ForRange(
121 std::max<size_t>(UPPER_BOUNDS[1], mUpperTimeSignature + 1)));
122
123 beatsField.label = L" " + mBeatString;
124
125 const auto hasFracPart = mFracPart > mLowerTimeSignature;
126
127 if (hasFracPart)
128 {
129 beatsField.label += L" ";
130 // See the reasoning above about the range
131 auto& fracField = mFields.emplace_back(NumericField::ForRange(
132 std::max(11, mFracPart / mLowerTimeSignature + 1)));
133 }
134
135 // Fill the aux mDigits structure
136 size_t pos = 0;
137 for (size_t i = 0; i < mFields.size(); i++)
138 {
139 mFields[i].pos = pos;
140
141 for (size_t j = 0; j < mFields[i].digits; j++)
142 {
143 mDigits.push_back(DigitInfo { i, j, pos });
144 pos++;
145 }
146
147 pos += mFields[i].label.length();
148 }
149 }
150
152 {
153 auto& timeSignature = ProjectTimeSignature::Get(project);
154
155 const double newTempo = timeSignature.GetTempo();
156 const int newUts = timeSignature.GetUpperTimeSignature();
157 const int newLts = timeSignature.GetLowerTimeSignature();
158
159 if (newTempo == mTempo && newUts == mUpperTimeSignature && newLts == mLowerTimeSignature)
160 return ;
161
162 const bool formatOk = CheckField(1, newUts) && CheckFracField(newLts);
163
164 mTempo = newTempo;
165 mUpperTimeSignature = newUts;
166 mLowerTimeSignature = newLts;
167
168 // 1/4 = BPM is used for now
169 const auto quarterLength = 60.0 / mTempo;
170 const auto beatLength = quarterLength * 4.0 / mLowerTimeSignature;
171 const auto barLength = mUpperTimeSignature * beatLength;
172
173 mFieldLengths[0] = barLength;
174 mFieldLengths[1] = beatLength;
175
176 const auto hasFracPart = mFracPart > mLowerTimeSignature;
177
178 if (hasFracPart)
179 {
180 const auto fracLength = beatLength * mLowerTimeSignature / mFracPart;
181 mFieldLengths[2] = fracLength;
182 }
183
184 if (formatOk)
185 return ;
186
187 UpdateFields(MIN_DIGITS[0]);
188 }
189
190 void UpdateFormatForValue(double value, bool canShrink) override
191 {
192 // Beats formatter does not support negative values
193 value = std::max(0.0, value);
194
195 // ForRange has a preserved weird behavior
196 const auto barsCount =
197 // Range is not inclusive
198 1 +
199 // Bars can start from 1
200 mFieldValueOffset +
201 static_cast<int>(std::floor(value / mFieldLengths[0]));
202
203 const auto barsField = NumericField::ForRange(
204 barsCount, true, MIN_DIGITS[0]);
205
206 const auto oldDigits = mFields[0].digits;
207
208 const bool updateNeeded = canShrink ? oldDigits != barsField.digits :
209 oldDigits < barsField.digits;
210
211 if (!updateNeeded)
212 return;
213
214 UpdateFields(barsField.digits);
215 Publish({ value, oldDigits > mFields[0].digits });
216 }
217
219 {
220 for (size_t fieldIndex = 0; fieldIndex < mFields.size(); ++fieldIndex)
221 {
222 result.valueString +=
223 result.fieldValueStrings[fieldIndex] + mFields[fieldIndex].label;
224 }
225 }
226
227 ConversionResult ValueToString(double value, bool) const override
228 {
229 ConversionResult result;
230 result.fieldValueStrings.resize(mFields.size());
231
232 if (value < 0)
233 {
234 for (size_t fieldIndex = 0; fieldIndex < mFields.size (); ++fieldIndex)
235 {
236 const auto digitsCount = mFields[fieldIndex].digits;
237 auto& fieldValue = result.fieldValueStrings[fieldIndex];
238 for (int digitIndex = 0; digitIndex < digitsCount; ++digitIndex)
239 fieldValue += L"-";
240 }
241
242 UpdateResultString(result);
243
244 return result;
245 }
246
247 // Calculate the epsilon only once, so the total loss of precision is addressed.
248 // This is a "multiplicative" epsilon, so there is no need to calculate 1 + eps every time.
249 const auto eps =
250 1.0 + std::max(1.0, value) * std::numeric_limits<double>::epsilon();
251
252 for (size_t fieldIndex = 0; fieldIndex < mFields.size(); ++fieldIndex)
253 {
254 const auto fieldLength = mFieldLengths[fieldIndex];
255 const auto fieldValue = std::max(
256 0, static_cast<int>(std::floor(value * eps / fieldLength)));
257
258 result.fieldValueStrings[fieldIndex] = wxString::Format(
259 mFields[fieldIndex].formatStr, fieldValue + mFieldValueOffset);
260
261 value = value - fieldValue * fieldLength;
262 }
263
264 UpdateResultString(result);
265 return result;
266 }
267
268 std::optional<double> StringToValue(const wxString& valueString) const override
269 {
270 if (
271 mFields.size() > 0 &&
272 valueString.Mid(mFields[0].pos, 1) == wxChar('-'))
273 return std::nullopt;
274
275 double t = 0.0;
276 size_t lastIndex = 0;
277
278 for (size_t i = 0; i < mFields.size(); i++)
279 {
280 const auto& field = mFields[i];
281
282 const size_t labelIndex = field.label.empty() ?
284 valueString.find(field.label, lastIndex);
285
286 long val;
287
288 const auto fieldStringValue = valueString.Mid(
289 lastIndex,
290 labelIndex == wxString::npos ? labelIndex : labelIndex - lastIndex);
291
292 if (!fieldStringValue.ToLong(&val))
293 return std::nullopt;
294
295 t += (val - mFieldValueOffset) * mFieldLengths[i];
296
297 lastIndex = labelIndex + field.label.Length();
298 }
299
300 return t;
301 }
302
303 double SingleStep(double value, int digitIndex, bool upwards) const override
304 {
305 if (digitIndex < 0 || size_t(digitIndex) >= mDigits.size())
306 return value;
307
308 const auto& digit = mDigits[digitIndex];
309 const auto& fieldIndex = digit.field;
310 const auto& field = mFields[fieldIndex];
311
312 const auto stepSize = mFieldLengths[fieldIndex] *
313 std::pow(10, field.digits - digit.index - 1);
314
315 return upwards ? value + stepSize : value - stepSize;
316 }
317
318 void UpdatePrefs() override
319 {
320 auto project = mContext.GetProject();
321
322 if (!project)
323 return;
324
325 auto barString = BarString.Translation();
326 auto beatString = BeatString.Translation();
327
328 if (barString == mBarString && beatString == mBeatString)
329 return;
330
331 mBarString = barString;
332 mBeatString = beatString;
333
334 UpdateFormat(*project);
335 }
336
337private:
339
341
342 double mTempo { 0.0 };
343
344 int mUpperTimeSignature { 0 };
345 int mLowerTimeSignature { 0 };
346
347 const int mFracPart;
348
350
351 std::array<double, 3> mFieldLengths {};
352
353 wxString mBarString;
354 wxString mBeatString;
355};
356
359{
360public:
361 BeatsNumericConverterFormatterFactory (int fracPart, bool timeFormat)
362 : mFracPart { fracPart }
363 , mTimeFormat { timeFormat }
364 {
365 }
366
367 std::unique_ptr<NumericConverterFormatter>
368 Create(const FormatterContext& context) const override
369 {
370 if (!IsAcceptableInContext(context))
371 return {};
372
373 return std::make_unique<BeatsFormatter>(context, mFracPart, mTimeFormat);
374 }
375
376 bool IsAcceptableInContext(const FormatterContext& context) const override
377 {
378 return context.HasProject();
379 }
380
381private:
382 const int mFracPart;
383 const bool mTimeFormat;
384};
385
386auto BuildBeatsGroup(bool timeFormat)
387{
389 timeFormat ? "beatsTime" : "beatsDuration",
392 /* i18n-hint: "bar" and "beat" are musical notation elements. */
393 "beats", XO("bar:beat"),
394 std::make_unique<BeatsNumericConverterFormatterFactory>(0, timeFormat)),
396 /* i18n-hint: "bar" and "beat" are musical notation elements. "tick"
397 corresponds to a 16th note. */
398 "beats16", XO("bar:beat:tick"),
399 std::make_unique<BeatsNumericConverterFormatterFactory>(16, timeFormat)));
400}
401
403 BuildBeatsGroup(true),
404 Registry::Placement { "parsed", { Registry::OrderingHint::After, L"parsedTime" } }
405};
406
408 BuildBeatsGroup(false),
409 Registry::Placement { "parsed", { Registry::OrderingHint::After, L"parsedDuration" } }
410};
411} // namespace
412
413std::unique_ptr<NumericConverterFormatter> CreateBeatsNumericConverterFormatter(
414 const FormatterContext& context, int fracPart /*= 0*/,
415 bool timeFormat /*= true*/)
416{
417 return std::make_unique<BeatsFormatter>(context, fracPart, timeFormat);
418}
std::unique_ptr< NumericConverterFormatter > CreateBeatsNumericConverterFormatter(const FormatterContext &context, int fracPart, bool timeFormat)
XO("Cut/Copy/Paste")
#define field(n, t)
Definition: ImportAUP.cpp:165
constexpr auto NumericConverterFormatterGroup
constexpr auto NumericConverterFormatterItem
const NumericConverterType & NumericConverterType_DURATION()
const NumericConverterType & NumericConverterType_TIME()
const auto project
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:90
A context in which formatter operates.
bool HasProject() const
Returns true if the reference to the project is valid at this moment.
A move-only handle representing a connection to a Publisher.
Definition: Observer.h:70
A listener notified of changes in preferences.
Definition: Prefs.h:652
static ProjectTimeSignature & Get(AudacityProject &project)
Generates classes whose instances register items at construction.
Definition: Registry.h:388
bool CheckField(size_t fieldIndex, int value) const noexcept
Check that field exists and has enough digits to fit the value.
double SingleStep(double value, int digitIndex, bool upwards) const override
void UpdateFormatForValue(double value, bool canShrink) override
Potentially updates the format so it can fit the value. Default implementation is empty.
BeatsFormatter(const FormatterContext &context, int fracPart, bool timeFormat)
std::optional< double > StringToValue(const wxString &valueString) const override
std::unique_ptr< NumericConverterFormatter > Create(const FormatterContext &context) const override
constexpr size_t npos(-1)
static NumericField WithDigits(size_t digits, bool zeropad=true)
static NumericField ForRange(size_t range, bool zeropad=true, size_t minDigits=0)