Audacity 3.2.0
PitchAndSpeedDialog.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 PitchAndSpeedDialog.cpp
7
8 Dmitry Vedenko
9
10 **********************************************************************/
11#include "PitchAndSpeedDialog.h"
13
14#include <wx/button.h>
15#include <wx/layout.h>
16#include <wx/spinctrl.h>
17#include <wx/textctrl.h>
18
19#include "ShuttleGui.h"
20#include "SpinControl.h"
22
23#include <regex>
24
25namespace
26{
27template <typename ReturnType, typename... Args> class ScopedSizer
28{
29public:
31 ShuttleGui& s, ReturnType (ShuttleGui::*startFunc)(Args...),
32 void (ShuttleGui::*endFunc)(), Args... args)
33 : mShuttleGui(s)
34 , mStartFunction(startFunc)
35 , mEndFunction(endFunc)
36 {
37 (mShuttleGui.*mStartFunction)(std::forward<Args>(args)...);
38 }
39
41 {
42 (mShuttleGui.*mEndFunction)();
43 }
44
45private:
47 ReturnType (ShuttleGui::*mStartFunction)(Args...);
48 void (ShuttleGui::*mEndFunction)();
49};
50
51class ScopedHorizontalLay : public ScopedSizer<void, int, int>
52{
53public:
55 ShuttleGui& s, int PositionFlags = wxALIGN_CENTRE, int iProp = 1)
56 : ScopedSizer<void, int, int>(
57 s, &ShuttleGui::StartHorizontalLay, &ShuttleGui::EndHorizontalLay,
58 PositionFlags, iProp)
59 {
60 }
61};
62
63class ScopedInvisiblePanel : public ScopedSizer<wxPanel*, int>
64{
65public:
66 ScopedInvisiblePanel(ShuttleGui& s, int border = 0)
67 : ScopedSizer<wxPanel*, int>(
68 s, &ShuttleGui::StartInvisiblePanel, &ShuttleGui::EndInvisiblePanel,
69 border)
70 {
71 }
72};
73
74class ScopedVerticalLay : public ScopedSizer<void, int>
75{
76public:
77 ScopedVerticalLay(ShuttleGui& s, int iProp = 1)
78 : ScopedSizer<void, int>(
79 s, &ShuttleGui::StartVerticalLay, &ShuttleGui::EndVerticalLay,
80 iProp)
81 {
82 }
83};
84
86 public ScopedSizer<wxStaticBox*, const TranslatableString&, int>
87{
88public:
90 : ScopedSizer<wxStaticBox*, const TranslatableString&, int>(
91 s, &ShuttleGui::StartStatic, &ShuttleGui::EndStatic, label, iProp)
92 {
93 }
94};
95
97auto GetInt(const wxCommandEvent& event, int& output)
98{
99 try
100 {
101 const auto str = event.GetString().ToStdString();
102 if (str.empty() || str == "-")
103 {
104 output = 0;
105 return true;
106 }
107 // Exact integer match
108 if (!std::regex_match(str, std::regex { "^-?[0-9]+$" }))
109 return false;
110 output = std::stoi(str);
111 return true;
112 }
113 catch (const std::exception&)
114 {
115 return false;
116 }
117}
118} // namespace
119
121 bool playbackOngoing, WaveTrack& waveTrack, WaveTrack::Interval& interval,
122 wxWindow* parent, const std::optional<PitchAndSpeedDialogFocus>& focus)
124 parent, wxID_ANY, XO("Pitch and Speed"), wxDefaultPosition,
125 { 480, 250 }, wxDEFAULT_DIALOG_STYLE)
126 , mPlaybackOngoing { playbackOngoing }
127 , mTrack { waveTrack }
128 , mTrackInterval { interval }
129 , mClipSpeed { 100.0 / interval.GetStretchRatio() }
130 , mOldClipSpeed { mClipSpeed }
131{
132 const auto totalShift = mTrackInterval.GetCentShift();
133 mShift.cents = totalShift % 100;
134 mShift.semis = (totalShift - mShift.cents) / 100;
135 mOldShift = mShift;
136
137 ShuttleGui s(this, eIsCreating);
138
139 {
140 ScopedVerticalLay v { s };
141 PopulateOrExchange(s, focus);
142 }
143
144 // TODO: Tolerance?
145 // Stretch ratio of
146 assert(mOldClipSpeed > 0.0);
147
148 Layout();
149 Fit();
150 Centre();
151
152 Bind(wxEVT_CHAR_HOOK, [this](auto& evt) {
153 if (!IsEscapeKey(evt))
154 {
155 evt.Skip();
156 return;
157 }
158
159 OnCancel();
160 });
161}
162
164
166 ShuttleGui& s, const std::optional<PitchAndSpeedDialogFocus>& focus)
167{
168 {
169 ScopedInvisiblePanel panel { s, 15 };
170 s.SetBorder(0);
171 {
172 ScopedStatic scopedStatic { s, XO("Clip Pitch") };
173 {
174 ScopedHorizontalLay h { s, wxLeft };
175 s.SetBorder(2);
176 // Use `TieSpinCtrl` rather than `AddSpinCtrl`, too see updates
177 // instantly when `UpdateDialog` is called.
178 s.TieSpinCtrl(
179 XO("semitones:"), mShift.semis,
182 ->Bind(wxEVT_TEXT, [&](wxCommandEvent& event) {
183 const auto prevSemis = mShift.semis;
184 if (GetInt(event, mShift.semis))
185 {
186 // If we have e.g. -3 semi, -1 cents, and the user changes
187 // the sign of the semitones, the logic in
188 // `OnPitchShiftChange` would result in 2 semi, 99 cents.
189 // If the user changes sign again, we would now get 1 semi,
190 // -1 cents. Mirrorring (e.g. -3 semi, -1 cents -> 3 semi,
191 // 1 cents) is not a good idea because that would ruin the
192 // work of users painstakingly adjusting the cents of an
193 // instrument. So instead, we map -3 semi, -1 cents to 3
194 // semi, 99 cents.
195 if (mShift.cents != 0)
196 {
197 if (prevSemis < 0 && mShift.semis > 0)
198 ++mShift.semis;
199 else if (prevSemis > 0 && mShift.semis < 0)
200 --mShift.semis;
201 }
202 OnPitchShiftChange(true);
203 }
204 else
205 // Something silly was entered; reset dialog
206 UpdateDialog();
207 });
208 s.TieSpinCtrl(XO("cents:"), mShift.cents, 100, -100)
209 ->Bind(wxEVT_TEXT, [&](wxCommandEvent& event) {
210 if (GetInt(event, mShift.cents))
211 OnPitchShiftChange(false);
212 else
213 // Something silly was entered; reset dialog
214 UpdateDialog();
215 });
216 }
217 }
218
219 s.AddSpace(0, 12);
220 s.SetBorder(0);
221
222 {
223 ScopedStatic scopedStatic { s, XO("Clip Speed") };
224 {
225 ScopedHorizontalLay h { s, wxLeft };
226 auto txtCtrl = s.Name(XO("Clip Speed"))
227 .NameSuffix(Verbatim("%"))
228 .TieNumericTextBox({}, mClipSpeed, 14, true);
229 txtCtrl->Enable(!mPlaybackOngoing);
230 if (focus.has_value() && *focus == PitchAndSpeedDialogFocus::Speed)
231 txtCtrl->SetFocus();
232 if (!mPlaybackOngoing)
233 {
234 txtCtrl->Bind(wxEVT_TEXT_ENTER, [this](auto&) { OnOk(); });
235 txtCtrl->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
236 try
237 {
238 mClipSpeed = std::stod(event.GetString().ToStdString());
239 }
240 catch (const std::exception&)
241 {
242 // Something silly was entered; reset dialog
243 UpdateDialog();
244 }
245 });
246 }
247 s.AddFixedText(Verbatim("%"));
248 }
249 }
250 }
251
252 ScopedHorizontalLay h { s, wxEXPAND, 0 };
253 {
254 ScopedInvisiblePanel panel { s, 10 };
255 s.SetBorder(2);
256 {
257 ScopedHorizontalLay h { s, wxALIGN_RIGHT, 0 };
258 s.AddSpace(270, 0, 0);
259 s.AddButton(XXO("&Cancel"))->Bind(wxEVT_BUTTON, [this](auto&) {
260 OnCancel();
261 });
262 auto okBtn = s.AddButton(XXO("&Ok"));
263 okBtn->Bind(wxEVT_BUTTON, [this](auto&) { OnOk(); });
264 okBtn->SetDefault();
265 }
266 }
267}
268
270{
273 EndModal(wxID_OK);
274}
275
277{
280 EndModal(wxID_CANCEL);
281}
282
284{
285 {
288 }
289
290 if (mClipSpeed <= 0.0)
291 {
293 /* i18n-hint: Title of an error message shown, when invalid clip speed
294 is set */
295 wxWidgetsWindowPlacement { this }, XO("Invalid clip speed"),
296 XO("Clip speed must be a positive value"), {});
297
298 return false;
299 }
300
301 const auto nextClip =
303 const auto maxEndTime = nextClip != nullptr ?
304 nextClip->Start() :
305 std::numeric_limits<double>::infinity();
306
307 const auto start = mTrackInterval.Start();
308 const auto end = mTrackInterval.End();
309
310 const auto expectedEndTime =
311 start + (end - start) * mOldClipSpeed / mClipSpeed;
312
313 if (expectedEndTime >= maxEndTime)
314 {
316 wxWidgetsWindowPlacement { this }, XO("Invalid clip speed"),
317 XO("There is not enough space to stretch the clip to the selected speed"),
318 {});
319
320 return false;
321 }
322
323 mTrackInterval.StretchRightTo(expectedEndTime);
324
325 {
328 }
329
330 return true;
331}
332
334{
335 // Rules:
336 // 1. total shift is clipped to [minSemis, maxSemis]
337 // 2. cents must be in the range [-100, 100]
338 // 3. on semitone updates, keep semitone and cent sign consistent
339 const auto newCentShift = mShift.semis * 100 + mShift.cents;
340 static_assert(TimeAndPitchInterface::MaxCents % 100 == 0);
341 static_assert(TimeAndPitchInterface::MinCents % 100 == 0);
342 std::optional<PitchShift> correctedShift;
343 if (newCentShift > TimeAndPitchInterface::MaxCents)
344 correctedShift = PitchShift { TimeAndPitchInterface::MaxCents / 100, 0 };
345 else if (newCentShift < TimeAndPitchInterface::MinCents)
346 correctedShift = PitchShift { TimeAndPitchInterface::MinCents / 100, 0 };
347 else if (mShift.cents == 100)
348 correctedShift = PitchShift { mShift.semis + 1, 0 };
349 else if (mShift.cents == -100)
350 correctedShift = PitchShift { mShift.semis - 1, 0 };
351 else if (semitonesChanged && mShift.cents > 0 && mShift.semis < 0)
352 correctedShift = PitchShift { mShift.semis + 1, mShift.cents - 100 };
353 else if (semitonesChanged && mShift.cents < 0 && mShift.semis > 0)
354 correctedShift = PitchShift { mShift.semis - 1, mShift.cents + 100 };
355
356 if (correctedShift.has_value())
357 mShift = *correctedShift;
359 if (correctedShift.has_value())
360 UpdateDialog();
361}
362
364{
367}
368
370{
371 const auto success =
373 assert(success);
374}
#define str(a)
XO("Cut/Copy/Paste")
XXO("&Cut/Copy/Paste Toolbar")
@ eIsSettingToDialog
Definition: ShuttleGui.h:39
@ eIsCreating
Definition: ShuttleGui.h:37
@ eIsGettingFromDialog
Definition: ShuttleGui.h:38
TranslatableString label
Definition: TagsEditor.cpp:165
#define S(N)
Definition: ToChars.cpp:64
TranslatableString Verbatim(wxString str)
Require calls to the one-argument constructor to go through this distinct global function name.
double End() const
Definition: Channel.h:42
double Start() const
Definition: Channel.h:41
PitchAndSpeedDialog(bool playbackOngoing, WaveTrack &track, WaveTrack::Interval &interval, wxWindow *parent, const std::optional< PitchAndSpeedDialogFocus > &focus={})
WaveTrack::Interval & mTrackInterval
void OnPitchShiftChange(bool semitonesChanged)
void PopulateOrExchange(ShuttleGui &s, const std::optional< PitchAndSpeedDialogFocus > &focus={})
~PitchAndSpeedDialog() override
void SetBorder(int Border)
Definition: ShuttleGui.h:488
wxButton * AddButton(const TranslatableString &Text, int PositionFlags=wxALIGN_CENTRE, bool setDefault=false)
Definition: ShuttleGui.cpp:361
wxTextCtrl * TieNumericTextBox(const TranslatableString &Prompt, int &Value, const int nChars=0, bool acceptEnter=false)
wxSpinCtrl * TieSpinCtrl(const TranslatableString &Prompt, int &Value, const int max, const int min=0)
void AddFixedText(const TranslatableString &Str, bool bCenter=false, int wrapWidth=0)
Definition: ShuttleGui.cpp:441
Derived from ShuttleGuiBase, an Audacity specific class for shuttling data to and from GUI.
Definition: ShuttleGui.h:630
ShuttleGui & NameSuffix(const TranslatableString &suffix)
Definition: ShuttleGui.h:670
wxSizerItem * AddSpace(int width, int height, int prop=0)
ShuttleGui & Name(const TranslatableString &name)
Definition: ShuttleGui.h:662
static constexpr auto MaxCents
static constexpr auto MinCents
Holds a msgid for the translation catalog; may also bind format arguments.
bool SetCentShift(int cents)
Definition: WaveTrack.cpp:279
void StretchRightTo(double t)
Definition: WaveTrack.cpp:267
A Track that contains audio waveform data.
Definition: WaveTrack.h:227
IntervalConstHolder GetNextInterval(const Interval &interval, PlaybackDirection searchDirection) const
Definition: WaveTrack.cpp:534
ScopedHorizontalLay(ShuttleGui &s, int PositionFlags=wxALIGN_CENTRE, int iProp=1)
ScopedSizer(ShuttleGui &s, ReturnType(ShuttleGui::*startFunc)(Args...), void(ShuttleGui::*endFunc)(), Args... args)
ScopedStatic(ShuttleGui &s, const TranslatableString &label, int iProp=0)
void ShowErrorDialog(const WindowPlacement &placement, const TranslatableString &dlogTitle, const TranslatableString &message, const ManualPageID &helpPage, const ErrorDialogOptions &options={})
Show an error dialog with a link to the manual for further help.
Definition: BasicUI.h:262
auto end(const Ptr< Type, BaseDeleter > &p)
Enables range-for.
Definition: PackedArray.h:159
auto GetInt(const wxCommandEvent &event, int &output)
Returns true if and only if output was updated.
Window placement information for wxWidgetsBasicUI can be constructed from a wxWindow pointer.