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 case AudioIOEvent::PAUSE:
199 break;
200 default:
201 // Unknown event type
202 assert(false);
203 }
204 });
205 }
206}
207
209{
210 const auto target = GetHitClip(mProject, event);
211 if (!target.has_value() || target->clip == mLeftClip.lock())
212 return;
213 Retarget(target->track, target->clip);
214}
215
217 const std::shared_ptr<WaveTrack>& track,
218 const WaveTrack::IntervalHolder& clip)
219{
220 mConsolidateHistory = false;
221 wxDialog::SetTitle(mTitle + " - " + clip->GetName());
222 const auto leftClip = clip;
224 leftClip->Observer::Publisher<WaveClipDtorCalled>::Subscribe(
225 [this](WaveClipDtorCalled) { Show(false); });
227 leftClip->Observer::Publisher<CentShiftChange>::Subscribe(
228 [this](const CentShiftChange& cents) {
230 mShift.semis * 100 + mShift.cents, cents.newValue);
231 UpdateDialog();
232 });
234 leftClip->Observer::Publisher<StretchRatioChange>::Subscribe(
235 [this](const StretchRatioChange& stretchRatio) {
236 mClipSpeed = 100.0 / stretchRatio.newValue;
237 UpdateDialog();
238 });
239
240 mTrack = track;
241 mLeftClip = leftClip;
242 mClipSpeed = 100.0 / leftClip->GetStretchRatio();
244 mShift = GetClipShift(*leftClip);
246 mFormantPreservation = leftClip->GetPitchAndSpeedPreset() ==
249
251
252 {
253 ScopedVerticalLay v { s };
255 }
256
257 if (mFirst)
258 {
259 Layout();
260 Fit();
261 Centre();
262 mFirst = false;
263 }
264
265 return *this;
266}
267
269 const std::optional<PitchAndSpeedDialogGroup>& group)
270{
271
272 const auto item =
273 group.has_value() ?
274 wxWindow::FindWindowById(
277 this) :
278 nullptr;
279 if (item)
280 item->SetFocus();
281 wxDialog::Show(true);
282 wxDialog::Raise();
284 return *this;
285}
286
288{
289 {
290 ScopedInvisiblePanel panel { s, 15 };
291 s.SetBorder(0);
292 {
293 ScopedStatic scopedStatic { s, XO("Clip Pitch") };
294 {
295 ScopedHorizontalLay h { s, wxLeft };
296 s.SetBorder(2);
297 // Use `TieSpinCtrl` rather than `AddSpinCtrl`, too see updates
298 // instantly when `UpdateDialog` is called.
299 const auto semiSpin = s.Id(semitoneCtrlId)
301 XO("se&mitones:"), mShift.semis,
304 semiSpin->Bind(wxEVT_SPINCTRL, [this, semiSpin](const auto&) {
305 // The widget's value isn't updated yet on macos, so we need
306 // to asynchronously query it later.
307 CallAfter([this, semiSpin] {
308 const auto prevSemis = mShift.semis;
309 mShift.semis = semiSpin->GetValue();
310 // If we have e.g. -3 semi, -1 cents, and the user
311 // changes the sign of the semitones, the logic in
312 // `SetSemitoneShift` would result in 2 semi, 99
313 // cents. If the user changes sign again, we would now
314 // get 1 semi, -1 cents. Mirrorring (e.g. -3 semi, -1
315 // cents -> 3 semi, 1 cents) is not a good idea because
316 // that would ruin the work of users painstakingly
317 // adjusting the cents of an instrument. So instead, we
318 // map -3 semi, -1 cents to 3 semi, 99 cents.
319 if (mShift.cents != 0)
320 {
321 if (prevSemis < 0 && mShift.semis > 0)
322 ++mShift.semis;
323 else if (prevSemis > 0 && mShift.semis < 0)
324 --mShift.semis;
325 }
327 });
328 });
329 const auto centSpin =
330 s.TieSpinCtrl(XO("&cents:"), mShift.cents, 100, -100);
331 centSpin->Bind(wxEVT_SPINCTRL, [this, centSpin](const auto&) {
332 CallAfter([this, centSpin] {
333 mShift.cents = centSpin->GetValue();
335 });
336 });
337 }
338 }
339
340 s.AddSpace(0, 12);
341 s.SetBorder(0);
342
343 {
344 ScopedStatic scopedStatic { s, XO("Clip Speed") };
345 {
346 ScopedHorizontalLay h { s, wxLeft };
347 const auto txtCtrl =
348 s.Id(speedCtrlId)
349 .Name(XO("Clip Speed"))
351 wxSize(60, -1), XO("&speed %: "), mClipSpeed, 1000.0, 1.0);
352#if wxUSE_ACCESSIBILITY
353 txtCtrl->SetAccessible(safenew WindowAccessible(txtCtrl));
354#endif
355 const auto playbackOngoing =
357 txtCtrl->Enable(!playbackOngoing);
358 txtCtrl->Bind(
359 wxEVT_SPINCTRL, [this, txtCtrl](wxCommandEvent& event) {
360 mClipSpeed = txtCtrl->GetValue();
361 if (!SetClipSpeed())
362 if (auto target = LockTarget())
363 {
365 *target->track, *target->clip);
366 UpdateDialog();
367 }
368 });
369 }
370 }
371
372 s.AddSpace(0, 12);
373 s.SetBorder(0);
374
375 {
376 ScopedStatic scopedStatic { s, XO("General") };
377 {
378 ScopedHorizontalLay h { s, wxLeft };
379 s.SetBorder(2);
380 s.TieCheckBox(XO("&Optimize for Voice"), mFormantPreservation)
381 ->Bind(wxEVT_CHECKBOX, [this](auto&) {
383 if (auto target = LockTarget())
384 target->clip->SetPitchAndSpeedPreset(
388 });
389 }
390 }
391 }
392}
393
395{
396 auto target = LockTarget();
397 if (!target)
398 return false;
399
400 const auto wasExactlySelected =
402
404 *target->track, *target->clip, 100 / mClipSpeed))
405 return false;
406
407 if (wasExactlySelected)
409
410 UpdateHistory(XO("Changed Speed"));
411
412 return true;
413}
414
416{
419}
420
422{
425 mConsolidateHistory = true;
426}
427
428std::optional<PitchAndSpeedDialog::StrongTarget>
430{
431 if (const auto track = mTrack.lock())
432 if (const auto leftClip = mLeftClip.lock())
433 return StrongTarget {
434 track, leftClip
435 };
436 return {};
437}
438
440{
441 auto target = LockTarget();
442 if (!target)
443 return;
445 const auto success =
446 target->clip->SetCentShift(mShift.semis * 100 + mShift.cents);
447 assert(success);
448 TrackPanel::Get(mProject).RefreshTrack(target->track.get());
449 UpdateHistory(XO("Changed Pitch"));
450}
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:392
void CallAfter(Action action)
Schedule an action to be done later, and in the main thread.
Definition: BasicUI.cpp:214
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:61
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