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"
12#include "AudioIO.h"
13#include "Project.h"
14#include "ProjectAudioIO.h"
15#include "ProjectHistory.h"
16#include "ProjectWindow.h"
17#include "ProjectWindows.h"
19#include "TimeStretching.h"
20#include "TrackPanel.h"
22#include "UndoManager.h"
23#include "ViewInfo.h"
24#include "WaveClip.h"
25#include "WaveClipUIUtilities.h"
26#include "WaveTrackUtilities.h"
27#include "WindowAccessible.h"
28
29#include <wx/button.h>
30#include <wx/checkbox.h>
31#include <wx/layout.h>
32#include <wx/spinctrl.h>
33#include <wx/textctrl.h>
34
35#include "ShuttleGui.h"
37#include "SpinControl.h"
38#include "WaveClip.h"
40
41#include <regex>
42
43namespace
44{
45constexpr auto semitoneCtrlId = wxID_HIGHEST + 1;
46constexpr auto speedCtrlId = wxID_HIGHEST + 3;
47
48struct HitClip
49{
50 std::shared_ptr<WaveTrack> track;
51 std::shared_ptr<WaveTrack::Interval> clip;
52};
53
54std::optional<HitClip>
56{
57 const auto pos = event.event.GetPosition();
58 const auto& viewInfo = ViewInfo::Get(project);
59 const auto t = viewInfo.PositionToTime(pos.x, event.rect.GetX());
60 auto& trackPanel = TrackPanel::Get(project);
61 for (auto leader : TrackList::Get(project).Any<WaveTrack>())
62 {
63 const auto trackRect = trackPanel.FindTrackRect(leader);
64 if (!trackRect.Contains(pos))
65 continue;
66 auto [begin, end] = leader->Intervals();
67 while (begin != end)
68 {
69 auto clip = *begin++;
70 if (clip->WithinPlayRegion(t))
71 return HitClip { std::static_pointer_cast<WaveTrack>(
72 leader->SharedPointer()),
73 clip };
74 }
75 }
76 return {};
77}
78
80{
81 auto& viewInfo = ViewInfo::Get(project);
82 return clip.GetPlayStartTime() == viewInfo.selectedRegion.t0() &&
83 clip.GetPlayEndTime() == viewInfo.selectedRegion.t1();
84}
85
87{
88 // Rules:
89 // 1. cents must be in the range [-100, 100]
90 // 2. on semitone updates, keep semitone and cent sign consistent
91
92 PitchAndSpeedDialog::PitchShift shift { 0, newCents };
93 while (shift.cents <= -100)
94 {
95 --shift.semis;
96 shift.cents += 100;
97 }
98 while (shift.cents >= 100)
99 {
100 ++shift.semis;
101 shift.cents -= 100;
102 }
103
104 const auto onlySemitonesChanged = [](int oldCents, int newCents) {
105 while (oldCents < 0)
106 oldCents += 100;
107 while (newCents < 0)
108 newCents += 100;
109 return oldCents / 100 != newCents / 100;
110 }(oldCents, newCents);
111
112 if (onlySemitonesChanged && shift.cents > 0 && shift.semis < 0)
113 return { shift.semis + 1, shift.cents - 100 };
114 else if (onlySemitonesChanged && shift.cents < 0 && shift.semis > 0)
115 return { shift.semis - 1, shift.cents + 100 };
116 else
117 return shift;
118}
119
121{
122 static_assert(TimeAndPitchInterface::MaxCents % 100 == 0);
123 static_assert(TimeAndPitchInterface::MinCents % 100 == 0);
124 const auto cents = shift.semis * 100 + shift.cents;
126 shift = { TimeAndPitchInterface::MaxCents / 100, 0 };
127 else if (cents < TimeAndPitchInterface::MinCents)
128 shift = { TimeAndPitchInterface::MinCents / 100, 0 };
129}
130
132{
133 const auto totalShift = clip.GetCentShift();
134 return { totalShift / 100, totalShift % 100 };
135}
136
137static const AttachedWindows::RegisteredFactory key {
138 [](AudacityProject& project) -> wxWeakRef<wxWindow> {
140 }
141};
142} // namespace
143
145{
147}
148
151{
152 return Get(const_cast<AudacityProject&>(project));
153}
154
156{
157 auto& attachedWindows = GetAttachedWindows(project);
158 auto* pPanel = attachedWindows.Find(key);
159 if (pPanel)
160 {
161 pPanel->wxWindow::Destroy();
162 attachedWindows.Assign(key, nullptr);
163 }
164}
165
168 FindProjectFrame(&project), wxID_ANY, XO("Pitch and Speed"), wxDefaultPosition,
169 { 480, 250 }, wxDEFAULT_DIALOG_STYLE)
170 , mProject { project }
171 , mProjectCloseSubscription { ProjectWindow::Get(mProject).Subscribe(
172 [this](ProjectWindowDestroyedMessage) { Destroy(mProject); }) }
173 , mTitle { GetTitle() }
174{
175 Bind(wxEVT_CLOSE_WINDOW, [this](const auto&) { Show(false); });
176
177 Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) {
178 if (event.GetKeyCode() == WXK_ESCAPE)
179 Show(false);
180 else
181 event.Skip();
182 });
183
184 if (const auto audioIo = AudioIO::Get())
185 {
186 mAudioIOSubscription =
187 audioIo->Subscribe([this](const AudioIOEvent& event) {
188 if (event.pProject != &mProject)
189 return;
190 switch (event.type)
191 {
192 case AudioIOEvent::CAPTURE:
193 case AudioIOEvent::PLAYBACK:
194 if (const auto child = wxDialog::FindWindowById(speedCtrlId))
195 child->Enable(!event.on);
196 break;
197 case AudioIOEvent::MONITOR:
198 break;
199 default:
200 // Unknown event type
201 assert(false);
202 }
203 });
204 }
205}
206
208{
209 const auto target = GetHitClip(mProject, event);
210 if (!target.has_value() || target->clip == mLeftClip.lock())
211 return;
212 Retarget(target->track, target->clip);
213}
214
216 const std::shared_ptr<WaveTrack>& track,
217 const WaveTrack::IntervalHolder& clip)
218{
219 mConsolidateHistory = false;
220 wxDialog::SetTitle(mTitle + " - " + clip->GetName());
221 const auto leftClip = clip;
223 leftClip->Observer::Publisher<WaveClipDtorCalled>::Subscribe(
224 [this](WaveClipDtorCalled) { Show(false); });
226 leftClip->Observer::Publisher<CentShiftChange>::Subscribe(
227 [this](const CentShiftChange& cents) {
229 mShift.semis * 100 + mShift.cents, cents.newValue);
230 UpdateDialog();
231 });
233 leftClip->Observer::Publisher<StretchRatioChange>::Subscribe(
234 [this](const StretchRatioChange& stretchRatio) {
235 mClipSpeed = 100.0 / stretchRatio.newValue;
236 UpdateDialog();
237 });
238
239 mTrack = track;
240 mLeftClip = leftClip;
241 mClipSpeed = 100.0 / leftClip->GetStretchRatio();
243 mShift = GetClipShift(*leftClip);
245 mFormantPreservation = leftClip->GetPitchAndSpeedPreset() ==
248
250
251 {
252 ScopedVerticalLay v { s };
254 }
255
256 if (mFirst)
257 {
258 Layout();
259 Fit();
260 Centre();
261 mFirst = false;
262 }
263
264 return *this;
265}
266
268 const std::optional<PitchAndSpeedDialogGroup>& group)
269{
270
271 const auto item =
272 group.has_value() ?
273 wxWindow::FindWindowById(
276 this) :
277 nullptr;
278 if (item)
279 item->SetFocus();
280 wxDialog::Show(true);
281 wxDialog::Raise();
283 return *this;
284}
285
287{
288 {
289 ScopedInvisiblePanel panel { s, 15 };
290 s.SetBorder(0);
291 {
292 ScopedStatic scopedStatic { s, XO("Clip Pitch") };
293 {
294 ScopedHorizontalLay h { s, wxLeft };
295 s.SetBorder(2);
296 // Use `TieSpinCtrl` rather than `AddSpinCtrl`, too see updates
297 // instantly when `UpdateDialog` is called.
298 const auto semiSpin = s.Id(semitoneCtrlId)
300 XO("se&mitones:"), mShift.semis,
303 semiSpin->Bind(wxEVT_SPINCTRL, [this, semiSpin](const auto&) {
304 // The widget's value isn't updated yet on macos, so we need
305 // to asynchronously query it later.
306 CallAfter([this, semiSpin] {
307 const auto prevSemis = mShift.semis;
308 mShift.semis = semiSpin->GetValue();
309 // If we have e.g. -3 semi, -1 cents, and the user
310 // changes the sign of the semitones, the logic in
311 // `SetSemitoneShift` would result in 2 semi, 99
312 // cents. If the user changes sign again, we would now
313 // get 1 semi, -1 cents. Mirrorring (e.g. -3 semi, -1
314 // cents -> 3 semi, 1 cents) is not a good idea because
315 // that would ruin the work of users painstakingly
316 // adjusting the cents of an instrument. So instead, we
317 // map -3 semi, -1 cents to 3 semi, 99 cents.
318 if (mShift.cents != 0)
319 {
320 if (prevSemis < 0 && mShift.semis > 0)
321 ++mShift.semis;
322 else if (prevSemis > 0 && mShift.semis < 0)
323 --mShift.semis;
324 }
326 });
327 });
328 const auto centSpin =
329 s.TieSpinCtrl(XO("&cents:"), mShift.cents, 100, -100);
330 centSpin->Bind(wxEVT_SPINCTRL, [this, centSpin](const auto&) {
331 CallAfter([this, centSpin] {
332 mShift.cents = centSpin->GetValue();
334 });
335 });
336 }
337 }
338
339 s.AddSpace(0, 12);
340 s.SetBorder(0);
341
342 {
343 ScopedStatic scopedStatic { s, XO("Clip Speed") };
344 {
345 ScopedHorizontalLay h { s, wxLeft };
346 const auto txtCtrl =
347 s.Id(speedCtrlId)
348 .Name(XO("Clip Speed"))
350 wxSize(60, -1), XO("&speed %: "), mClipSpeed, 1000.0, 1.0);
351#if wxUSE_ACCESSIBILITY
352 txtCtrl->SetAccessible(safenew WindowAccessible(txtCtrl));
353#endif
354 const auto playbackOngoing =
356 txtCtrl->Enable(!playbackOngoing);
357 txtCtrl->Bind(
358 wxEVT_SPINCTRL, [this, txtCtrl](wxCommandEvent& event) {
359 mClipSpeed = txtCtrl->GetValue();
360 if (!SetClipSpeed())
361 if (auto target = LockTarget())
362 {
364 *target->track, *target->clip);
365 UpdateDialog();
366 }
367 });
368 }
369 }
370
371 s.AddSpace(0, 12);
372 s.SetBorder(0);
373
374 {
375 ScopedStatic scopedStatic { s, XO("General") };
376 {
377 ScopedHorizontalLay h { s, wxLeft };
378 s.SetBorder(2);
379 s.TieCheckBox(XO("&Optimize for Voice"), mFormantPreservation)
380 ->Bind(wxEVT_CHECKBOX, [this](auto&) {
382 if (auto target = LockTarget())
383 target->clip->SetPitchAndSpeedPreset(
387 });
388 }
389 }
390 }
391}
392
394{
395 auto target = LockTarget();
396 if (!target)
397 return false;
398
399 const auto wasExactlySelected =
401
403 *target->track, *target->clip, 100 / mClipSpeed))
404 return false;
405
406 if (wasExactlySelected)
408
409 UpdateHistory(XO("Changed Speed"));
410
411 return true;
412}
413
415{
418}
419
421{
424 mConsolidateHistory = true;
425}
426
427std::optional<PitchAndSpeedDialog::StrongTarget>
429{
430 if (const auto track = mTrack.lock())
431 if (const auto leftClip = mLeftClip.lock())
432 return StrongTarget {
433 track, leftClip
434 };
435 return {};
436}
437
439{
440 auto target = LockTarget();
441 if (!target)
442 return;
444 const auto success =
445 target->clip->SetCentShift(mShift.semis * 100 + mShift.cents);
446 assert(success);
447 TrackPanel::Get(mProject).RefreshTrack(target->track.get());
448 UpdateHistory(XO("Changed Pitch"));
449}
XO("Cut/Copy/Paste")
#define safenew
Definition: MemoryX.h:10
wxFrame * FindProjectFrame(AudacityProject *project)
Get a pointer to the window associated with a project, or null if the given pointer is null,...
AUDACITY_DLL_API AttachedWindows & GetAttachedWindows(AudacityProject &project)
accessors for certain important windows associated with each project
@ eIsSettingToDialog
Definition: ShuttleGui.h:39
@ eIsCreating
Definition: ShuttleGui.h:37
const auto project
#define S(N)
Definition: ToChars.cpp:64
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:90
static AudioIO * Get()
Definition: AudioIO.cpp:126
Subclass & Get(const RegisteredFactory &key)
Get reference to an attachment, creating on demand if not present, down-cast it to Subclass.
Definition: ClientData.h:318
virtual double GetPlayEndTime() const =0
virtual double GetPlayStartTime() const =0
Subscription Subscribe(Callback callback)
Connect a callback to the Publisher; later-connected are called earlier.
Definition: Observer.h:199
void SetFocus() override
Observer::Subscription mClipSpeedChangeSubscription
static PitchAndSpeedDialog & Get(AudacityProject &project)
void TryRetarget(const TrackPanelMouseEvent &event)
std::optional< StrongTarget > LockTarget()
Observer::Subscription mClipDeletedSubscription
void UpdateHistory(const TranslatableString &desc)
static void Destroy(AudacityProject &project)
bool Show(bool show) override
std::weak_ptr< WaveClip > mLeftClip
AudacityProject & mProject
PitchAndSpeedDialog & Retarget(const std::shared_ptr< WaveTrack > &track, const WaveTrack::IntervalHolder &wideClip)
std::weak_ptr< WaveTrack > mTrack
PitchAndSpeedDialog(AudacityProject &project)
void PopulateOrExchange(ShuttleGui &s)
Observer::Subscription mClipCentShiftChangeSubscription
bool IsAudioActive() const
static ProjectAudioIO & Get(AudacityProject &project)
void PushState(const TranslatableString &desc, const TranslatableString &shortDesc)
static ProjectHistory & Get(AudacityProject &project)
static ProjectWindow & Get(AudacityProject &project)
void SetBorder(int Border)
Definition: ShuttleGui.h:495
wxCheckBox * TieCheckBox(const TranslatableString &Prompt, bool &Var)
wxSpinCtrl * TieSpinCtrl(const TranslatableString &Prompt, int &Value, const int max, const int min=0)
SpinControl * TieSpinControl(const wxSize &size, const TranslatableString &Prompt, double &Value, const double max, const double min=0)
Derived from ShuttleGuiBase, an Audacity specific class for shuttling data to and from GUI.
Definition: ShuttleGui.h:640
ShuttleGui & Id(int id)
wxSizerItem * AddSpace(int width, int height, int prop=0)
ShuttleGui & Name(const TranslatableString &name)
Definition: ShuttleGui.h:672
static constexpr auto MaxCents
static constexpr auto MinCents
static TrackList & Get(AudacityProject &project)
Definition: Track.cpp:314
static TrackPanel & Get(AudacityProject &project)
Definition: TrackPanel.cpp:234
void RefreshTrack(Track *trk, bool refreshbacking=true)
Definition: TrackPanel.cpp:768
Holds a msgid for the translation catalog; may also bind format arguments.
static ViewInfo & Get(AudacityProject &project)
Definition: ViewInfo.cpp:235
This allows multiple clips to be a part of one WaveTrack.
Definition: WaveClip.h:238
int GetCentShift() const override
Definition: WaveClip.cpp:634
std::shared_ptr< Interval > IntervalHolder
Definition: WaveTrack.h:209
An alternative to using wxWindowAccessible, which in wxWidgets 3.1.1 contained GetParent() which was ...
void SetFocus(const WindowPlacement &focus)
Set the window that accepts keyboard input.
Definition: BasicUI.h:384
void CallAfter(Action action)
Schedule an action to be done later, and in the main thread.
Definition: BasicUI.cpp:213
IMPORT_EXPORT_API ExportResult Show(ExportTask exportTask)
WAVE_TRACK_API bool SetClipStretchRatio(const WaveTrack &track, WaveTrack::Interval &interval, double stretchRatio)
void SelectClip(AudacityProject &project, const WaveTrack::Interval &clip)
WAVE_TRACK_API void ExpandClipTillNextOne(const WaveTrack &track, WaveTrack::Interval &interval)
const TranslatableString desc
Definition: ExportPCM.cpp:51
void ClampPitchShift(PitchAndSpeedDialog::PitchShift &shift)
static const AttachedWindows::RegisteredFactory key
std::optional< HitClip > GetHitClip(AudacityProject &project, const TrackPanelMouseEvent &event)
bool IsExactlySelected(AudacityProject &project, const ClipTimes &clip)
PitchAndSpeedDialog::PitchShift GetClipShift(const WaveClip &clip)
PitchAndSpeedDialog::PitchShift ToSemitonesAndCents(int oldCents, int newCents)
const char * end(const char *str) noexcept
Definition: StringUtils.h:106
const char * begin(const char *str) noexcept
Definition: StringUtils.h:101
enum AudioIOEvent::Type type
AudacityProject * pProject
Definition: AudioIO.h:60
const int newValue
Definition: WaveClip.h:203
Message sent when the project window is closed.
Definition: ProjectWindow.h:29
const double newValue
Definition: WaveClip.h:221