Audacity 3.2.0
ExportFilePanel.cpp
Go to the documentation of this file.
1#include "ExportFilePanel.h"
2
3#include <numeric>
4#include <wx/stattext.h>
5#include <wx/button.h>
6#include <wx/choice.h>
7#include <wx/textctrl.h>
8#include <wx/radiobut.h>
9#include <wx/regex.h>
10#include <wx/wupdlock.h>
11
12#include "Export.h"
13#include "ExportMixerDialog.h"
14#include "ProjectRate.h"
15#include "Mix.h"
16#include "WaveTrack.h"
17
18#include "ShuttleGui.h"
21#include "ExportUtils.h"
22#include "WindowAccessible.h"
23
24#if wxUSE_ACCESSIBILITY
25#include "WindowAccessible.h"
26#endif
27
28wxDEFINE_EVENT(AUDACITY_EXPORT_FORMAT_CHANGE_EVENT, wxCommandEvent);
29
30namespace
31{
32
35 8000,
36 11025,
37 16000,
38 22050,
39 32000,
40 44100,
41 48000,
42 88200,
43 96000,
44 176400,
45 192000,
46 352800,
47 384000
48};
49
50enum
51{
52 FolderBrowseID = wxID_HIGHEST,
53
55
59
61
63};
64
66{
67 enum {
68 CustomSampleRateID = wxID_HIGHEST
69 };
70public:
71 CustomSampleRateDialog(wxWindow* parent, int defaultSampleRate = 44100)
72 : wxDialogWrapper(parent, wxID_ANY, XO("Custom Sample Rate"), wxDefaultPosition, {-1, 160})
73 , mSampleRate(defaultSampleRate)
74 {
76 S.SetBorder(5);
77 S.StartHorizontalLay(wxEXPAND);
78 {
79 S.StartMultiColumn(2, wxALIGN_CENTER_VERTICAL);
80 {
81 S.Id(CustomSampleRateID).AddNumericTextBox(XO("New sample rate (Hz):"), wxString::Format("%d", mSampleRate), 0);
82 }
83 S.EndMultiColumn();
84 }
85 S.EndHorizontalLay();
86
87 S.AddStandardButtons();
88 }
89
90 int GetSampleRate() const noexcept
91 {
92 return mSampleRate;
93 }
94
95private:
96
97 void OnSampleRateChange(wxCommandEvent& event)
98 {
99 long rate;
100 if(event.GetString().ToLong(&rate))
101 mSampleRate = static_cast<int>(rate);
102 }
103
105
107};
108
109
110
111}
112
113BEGIN_EVENT_TABLE(CustomSampleRateDialog, wxDialogWrapper)
114 EVT_TEXT(CustomSampleRateID, CustomSampleRateDialog::OnSampleRateChange)
116
117BEGIN_EVENT_TABLE(ExportFilePanel, wxPanelWrapper)
119
120 EVT_CHOICE(FormatID, ExportFilePanel::OnFormatChange)
121
122 EVT_RADIOBUTTON(AudioMixModeMonoID, ExportFilePanel::OnChannelsChange)
123 EVT_RADIOBUTTON(AudioMixModeStereoID, ExportFilePanel::OnChannelsChange)
124 EVT_RADIOBUTTON(AudioMixModeCustomID, ExportFilePanel::OnChannelsChange)
125
127
128 EVT_CHOICE(SampleRateID, ExportFilePanel::OnSampleRateChange)
130
131
133 bool monoStereoMode,
134 wxWindow* parent,
135 wxWindowID winid)
136 : wxPanelWrapper(parent, winid)
137 , mMonoStereoMode(monoStereoMode)
138 , mProject(project)
139{
140 ShuttleGui S(this, eIsCreating);
141 PopulateOrExchange(S);
142}
143
145
147{
148 TranslatableStrings formats;
149 if(S.GetMode() == eIsCreating)
150 {
151 for(auto [plugin, formatIndex] : ExportPluginRegistry::Get())
152 {
153 auto formatInfo = plugin->GetFormatInfo(formatIndex);
154 formats.push_back(formatInfo.description);
155 }
156 }
157
158 S.SetBorder(5);
159 S.StartMultiColumn(3, wxEXPAND);
160 {
161 S.SetStretchyCol(1);
162
163 mFullName = S.AddTextBox(XO("File &Name:"), {}, 0);
164 mFullName->Bind(wxEVT_KILL_FOCUS, &ExportFilePanel::OnFullNameFocusKill, this);
165 S.AddSpace(1);
166
167 mFolder = S.AddTextBox(XO("Fo&lder:"), {}, 0);
168 S.Id(FolderBrowseID).AddButton(XO("&Browse..."));
169
170 mFormat = S.Id(FormatID).AddChoice(XO("&Format:"), formats);
171 S.AddSpace(1);
172 }
173 S.EndMultiColumn();
174
175 S.SetBorder(5);
176 S.StartStatic(XO("Audio options"));
177 {
178 S.StartTwoColumn();
179 {
180 if(auto prompt = S.AddPrompt(XO("Channels")))
181 prompt->SetMinSize({140, -1});
182
183 S.StartHorizontalLay(wxALIGN_LEFT);
184 {
185 S.SetBorder(2);
186
187 const int channels = 2;
188
189 mMono = S.Id(AudioMixModeMonoID).AddRadioButton(XO("M&ono"), 1, channels);
190 mStereo = S.Id(AudioMixModeStereoID).AddRadioButtonToGroup(XO("&Stereo"), 2, channels);
191 if(!mMonoStereoMode)
192 {
193 //i18n-hint refers to custom channel mapping configuration
194 mCustomMapping = S.Id(AudioMixModeCustomID).AddRadioButtonToGroup(XO("Custom mappin&g"), 0, true);
196 //i18n-hint accessibility hint, refers to export channel configuration
197 .Name(XO("Configure custom mapping"))
198 .AddButton(XO("Configure"));
199#if wxUSE_ACCESSIBILITY
201#endif
202 }
203 }
204 S.EndHorizontalLay();
205
206 S.SetBorder(5);
207
208 if(auto prompt = S.AddPrompt(XO("Sample &Rate")))
209 prompt->SetMinSize({140, -1});
210
211 S.StartHorizontalLay(wxALIGN_LEFT);
212 {
213 mRates = S.Id(SampleRateID).AddChoice({}, {});
214 }
215 S.EndHorizontalLay();
216 }
217 S.EndTwoColumn();
218
219 mAudioOptionsPanel = S.StartPanel();
220 {
221
222 }
223 S.EndPanel();
224 }
225 S.EndStatic();
226}
227
228void ExportFilePanel::Init(const wxFileName& filename,
229 int sampleRate,
230 const wxString& format,
231 int channels,
232 const ExportProcessor::Parameters& parameters,
233 const MixerOptions::Downmix* mixerSpec)
234{
235 mFolder->SetValue(filename.GetPath());
236 mFullName->SetValue(filename.GetFullName());
238
239 auto selectedFormatIndex = 0;
240 if(!format.empty())
241 {
242 auto counter = 0;
243 for(auto [plugin, formatIndex] : ExportPluginRegistry::Get())
244 {
245 if(plugin->GetFormatInfo(formatIndex).format.IsSameAs(format))
246 {
247 selectedFormatIndex = counter;
248 break;
249 }
250 ++counter;
251 }
252 }
253
254 if(mixerSpec != nullptr)
255 {
256 assert(!mMonoStereoMode);
257 *mMixerSpec = *mixerSpec;
258 mCustomMapping->SetValue(true);
259 }
260 else
261 {
262 int numChannels = channels;
263 if(numChannels == 0)
264 {
265 numChannels = 1;
266 const auto waveTracks =
269 false);
270 for(const auto track : waveTracks)
271 {
272 if(track->NChannels() >= 2 || track->GetPan() != .0f)
273 {
274 numChannels = 2;
275 break;
276 }
277 }
278 }
279 if(numChannels == 1)
280 mMono->SetValue(true);
281 else
282 mStereo->SetValue(true);
283 }
284
285 mFormat->SetSelection(selectedFormatIndex);
286
287 ChangeFormat(selectedFormatIndex);
288
289 if(!parameters.empty())
290 mOptionsHandler->SetParameters(parameters);
291
292 if(mCustomizeChannels != nullptr)
293 mCustomizeChannels->Enable(mCustomMapping->GetValue());
294}
295
296// Used as part of fix for issue #4960
298{
299 mFullName->SetFocus();
300 mFullName->SelectAll();
301}
302
304{
306 return;
307
308 if(!enabled && mCustomMapping->GetValue())
309 {
310 if(mStereo->IsEnabled())
311 mStereo->SetValue(true);
312 else
313 mMono->SetValue(true);
314 }
315 mCustomMapping->Enable(enabled);
316 mCustomizeChannels->Enable(enabled);
317}
318
320{
321 return mFolder->GetValue();
322}
323
325{
327 return mFullName->GetValue();
328}
329
331{
332 return mSelectedPlugin;
333}
334
336{
338}
339
341{
342 return mSampleRate;
343}
344
345std::optional<ExportProcessor::Parameters> ExportFilePanel::GetParameters() const
346{
347 if(mOptionsHandler->TransferDataFromEditor())
348 return { mOptionsHandler->GetParameters() };
349 return std::nullopt;
350}
351
353{
354 if(mCustomMapping != nullptr && mCustomMapping->GetValue())
355 return 0;
356 return mMono->GetValue() ? 1 : 2;
357}
358
360{
361 return mMixerSpec.get();
362}
363
365{
366 if(mSelectedPlugin == nullptr)
367 return;
368
369 const auto formatInfo = mSelectedPlugin->GetFormatInfo(mSelectedFormatIndex);
370 if(formatInfo.extensions.empty())
371 return;
372
373 wxFileName filename;
374 filename.SetFullName(mFullName->GetValue());
375 const auto desiredExt = filename.GetExt().Trim();
376
377 //See https://github.com/audacity/audacity/issues/5823
378 //check if extension is valid, i.e. does not contain whitespace characters.
379 //Otherwise everything after '.'(if present) is considered to be a part of name.
380 if(wxRegEx{R"(^[^ ]+$)"}.Matches(desiredExt))
381 {
382 auto it = std::find_if(
383 formatInfo.extensions.begin(),
384 formatInfo.extensions.end(),
385 // if typed extension uses different case (e.g. MP3 instead of mp3)
386 // we'll reset the file extension to one provided by FormatInfo
387 [&](const auto& ext) { return desiredExt.IsSameAs(ext, false); });
388
389 if(it == formatInfo.extensions.end())
390 it = formatInfo.extensions.begin();
391
392 if(!it->empty() && !it->IsSameAs(filename.GetExt()))
393 {
394 filename.SetExt(*it);
395 mFullName->SetValue(filename.GetFullName());
396 }
397 }
398 else if(!formatInfo.extensions.front().empty())
399 {
400 auto fullname = filename.GetFullName();
401 if(!fullname.EndsWith("."))
402 fullname.Append(".");
403 fullname.Append(formatInfo.extensions.front());
404 filename.SetFullName(fullname);
405 mFullName->SetValue(filename.GetFullName());
406 }
407}
408
410{
411 //When user has finished typing make sure that file extension
412 //is one of extensions supplied by FormatInfo
413
414 event.Skip();
415
417}
418
419void ExportFilePanel::OnFormatChange(wxCommandEvent &event)
420{
421 ChangeFormat(event.GetInt());
422 event.Skip();
423}
424
425void ExportFilePanel::OnSampleRateChange(wxCommandEvent &event)
426{
427 const auto clientData = event.GetClientData();
428 if(clientData == nullptr)
429 {
430 CustomSampleRateDialog dialog(this, mSampleRate);
431 if(dialog.ShowModal() == wxID_OK &&
432 dialog.GetSampleRate() > 0)
433 {
434 mSampleRate = dialog.GetSampleRate();
435 }
437 }
438 else
439 mSampleRate = *reinterpret_cast<const int*>(&clientData);
440}
441
442void ExportFilePanel::OnFolderBrowse(wxCommandEvent &event)
443{
444 FileNames::FileTypes fileTypes;
445
446 for(auto [plugin, formatIndex] : ExportPluginRegistry::Get())
447 {
448 const auto formatInfo = plugin->GetFormatInfo(formatIndex);
449 fileTypes.emplace_back(formatInfo.description, formatInfo.extensions);
450 }
451 wxFileDialog fd(this, _("Choose a location to save the exported files"),
452 mFolder->GetValue(),
453 mFullName->GetValue(),
454 FileNames::FormatWildcard(fileTypes),
455 wxFD_SAVE);
456 fd.SetFilterIndex(mFormat->GetSelection());
457
458 if(fd.ShowModal() == wxID_OK)
459 {
460 wxFileName filepath (fd.GetPath());
461 mFolder->SetValue(filepath.GetPath());
462 mFullName->SetValue(filepath.GetFullName());
463 const auto selectedFormat = fd.GetFilterIndex();
464 if(selectedFormat != mFormat->GetSelection())
465 {
466 mFormat->SetSelection(selectedFormat);
467 ChangeFormat(selectedFormat);
468 }
469 }
470}
471
472void ExportFilePanel::OnChannelsChange(wxCommandEvent& event)
473{
474 if(mCustomizeChannels != nullptr)
475 mCustomizeChannels->Enable(event.GetId() == AudioMixModeCustomID);
476}
477
478void ExportFilePanel::OnChannelsConfigure(wxCommandEvent &event)
479{
480 //Configure for all tracks, but some channels may turn out to be silent
481 //if exported region does not contain audio samples
482 auto waveTracks = TrackList::Get(mProject).Any<const WaveTrack>();
483
484 auto mixerSpec = std::make_unique<MixerOptions::Downmix>(*mMixerSpec);
485
486 ExportMixerDialog md(waveTracks,
487 mixerSpec.get(),
488 nullptr,
489 1,
490 XO("Advanced Mixing Options"));
491 if(md.ShowModal() == wxID_OK)
492 mMixerSpec.swap(mixerSpec);
493}
494
495
497{
498 mSelectedPlugin = nullptr;
499
500 wxWindowUpdateLocker wndupdlck(mAudioOptionsPanel);
501
502 auto formatCounter = 0;
503
504 for(auto [plugin, formatIndex] : ExportPluginRegistry::Get())
505 {
506 if(formatCounter != index)
507 {
508 ++formatCounter;
509 continue;
510 }
511
513
514 mSelectedPlugin = plugin;
515 mSelectedFormatIndex = formatIndex;
516
518
519 mAudioOptionsPanel->SetSizer(nullptr);
520 mAudioOptionsPanel->DestroyChildren();
521
523 mOptionsHandler = std::make_unique<ExportOptionsHandler>(S, *plugin, formatIndex);
525
526 const auto formatInfo = plugin->GetFormatInfo(formatIndex);
527 UpdateMaxChannels(formatInfo.maxChannels);
528
530
531 mAudioOptionsPanel->Layout();
532
533 wxPostEvent(GetParent(), wxCommandEvent { AUDACITY_EXPORT_FORMAT_CHANGE_EVENT, GetId() });
534
535 return;
536 }
537}
538
540{
541 switch(e.type)
542 {
545 break;
547 {
548 const auto formatInfo = mSelectedPlugin->GetFormatInfo(mSelectedFormatIndex);
550 UpdateMaxChannels(formatInfo.maxChannels);
551 } break;
552 }
553
554}
555
556void ExportFilePanel::UpdateMaxChannels(unsigned maxChannels)
557{
558 if(maxChannels < 2 && mStereo->GetValue())
559 mMono->SetValue(true);
560 mStereo->Enable(maxChannels > 1);
561 if(!mMonoStereoMode)
562 {
563 const auto mixerMaxChannels = std::clamp(
564 maxChannels,
565 // JKC: This is an attempt to fix a 'watching brief' issue, where the slider is
566 // sometimes not slidable. My suspicion is that a mixer may incorrectly
567 // state the number of channels - so we assume there are always at least two.
568 // The downside is that if someone is exporting to a mono device, the dialog
569 // will allow them to output to two channels. Hmm. We may need to revisit this.
570 // STF (April 2016): AMR (narrowband) and MP3 may export 1 channel.
571 1u,
573 if(!mMixerSpec || mMixerSpec->GetMaxNumChannels() != mixerMaxChannels)
574 {
575 auto waveTracks = TrackList::Get(mProject).Any<const WaveTrack>();
576 mMixerSpec = std::make_unique<MixerOptions::Downmix>(
577 waveTracks.sum([](const auto track) { return track->NChannels(); }),
578 mixerMaxChannels);
579 }
580 }
581}
582
584{
585 auto availableRates = mOptionsHandler->GetSampleRateList();
586 std::sort(availableRates.begin(), availableRates.end());
587
588 const auto* rates = availableRates.empty() ? &DefaultRates : &availableRates;
589
590 mRates->Clear();
591
592 void* clientData;
593 int customRate = mSampleRate;
594 int selectedItemIndex = 0;
595 //Prefer lowest possible sample rate that is not less than mSampleRate.
596 //Initialize with highest value, so that if all available rates are less
597 //than mSampleRate then we will choose highest rate
598 int preferredRate = rates->back();
599 int preferredItemIndex = rates->size() - 1;
600 for(auto rate : *rates)
601 {
602 *reinterpret_cast<int*>(&clientData) = rate;
603 const auto itemIndex =
604 mRates->Append(
605 XO("%d Hz").Format(rate).Translation(),
606 clientData);
607 if(rate == mSampleRate)
608 {
609 customRate = 0;
610 selectedItemIndex = itemIndex;
611 }
612 if(rate >= mSampleRate && rate < preferredRate)
613 {
614 preferredItemIndex = itemIndex;
615 preferredRate = rate;
616 }
617 }
618
619 if(rates == &DefaultRates)
620 {
621 if(customRate != 0)
622 {
623 *reinterpret_cast<int*>(&clientData) = customRate;
624 selectedItemIndex =
625 mRates->Append(
626 XO("%d Hz (custom)").Format(customRate).Translation(),
627 clientData);
628 }
629 mRates->Append(_("Other..."));
630 }
631 else if(customRate != 0)//sample rate not in the list
632 {
633 auto selectedRate = (*rates)[preferredItemIndex];
634 mSampleRate = selectedRate;
635 selectedItemIndex = preferredItemIndex;
636 }
637 mRates->SetSelection(selectedItemIndex);
638}
END_EVENT_TABLE()
EVT_BUTTON(wxID_NO, DependencyDialog::OnNo) EVT_BUTTON(wxID_YES
wxDEFINE_EVENT(AUDACITY_EXPORT_FORMAT_CHANGE_EVENT, wxCommandEvent)
XO("Cut/Copy/Paste")
#define _(s)
Definition: Internat.h:73
#define safenew
Definition: MemoryX.h:10
an object holding per-project preferred sample rate
@ eIsCreating
Definition: ShuttleGui.h:37
const auto project
#define S(N)
Definition: ToChars.cpp:64
std::vector< TranslatableString > TranslatableStrings
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:90
const ExportPlugin * GetPlugin() const
wxChoice * mFormat
std::unique_ptr< MixerOptions::Downmix > mMixerSpec
void OnFullNameFocusKill(wxFocusEvent &event)
std::unique_ptr< ExportOptionsHandler > mOptionsHandler
void OnSampleRateChange(wxCommandEvent &event)
const ExportPlugin * mSelectedPlugin
int GetSampleRate() const
wxString GetPath() const
~ExportFilePanel() override
wxTextCtrl * mFolder
AudacityProject & mProject
void SetCustomMappingEnabled(bool enabled)
static constexpr auto MaxExportChannels
wxString GetFullName()
MixerOptions::Downmix * GetMixerSpec() const
void ChangeFormat(int index)
wxTextCtrl * mFullName
void OnFormatChange(wxCommandEvent &event)
wxRadioButton * mStereo
int GetFormat() const
wxWindow * mAudioOptionsPanel
int GetChannels() const
wxRadioButton * mCustomMapping
void OnFolderBrowse(wxCommandEvent &event)
wxRadioButton * mMono
void OnChannelsConfigure(wxCommandEvent &event)
std::optional< ExportProcessor::Parameters > GetParameters() const
void OnOptionsHandlerEvent(const ExportOptionsHandlerEvent &e)
wxChoice * mRates
Observer::Subscription mOptionsChangeSubscription
void UpdateMaxChannels(unsigned maxChannels)
void Init(const wxFileName &filename, int sampleRate, const wxString &format=wxEmptyString, int channels=0, const ExportProcessor::Parameters &parameters={}, const MixerOptions::Downmix *mixerSpec=nullptr)
Initializes panel with export settings provided as arguments. Call is required.
void PopulateOrExchange(ShuttleGui &S)
void OnChannelsChange(wxCommandEvent &event)
wxButton * mCustomizeChannels
Dialog for advanced mixing.
std::vector< int > SampleRateList
virtual FormatInfo GetFormatInfo(int index) const =0
Returns FormatInfo structure for given index if it's valid, or a default one. FormatInfo::format isn'...
static ExportPluginRegistry & Get()
std::vector< std::tuple< ExportOptionID, ExportValue > > Parameters
Definition: ExportPlugin.h:93
static TrackIterRange< const WaveTrack > FindExportWaveTracks(const TrackList &tracks, bool selectedOnly)
Definition: ExportUtils.cpp:22
std::vector< FileType > FileTypes
Definition: FileNames.h:75
A matrix of booleans, one row per input channel, column per output.
Definition: MixerOptions.h:32
void Reset() noexcept
Breaks the connection (constant time)
Definition: Observer.cpp:101
Derived from ShuttleGuiBase, an Audacity specific class for shuttling data to and from GUI.
Definition: ShuttleGui.h:640
auto Any() -> TrackIterRange< TrackType >
Definition: Track.h:950
static TrackList & Get(AudacityProject &project)
Definition: Track.cpp:314
A Track that contains audio waveform data.
Definition: WaveTrack.h:203
An alternative to using wxWindowAccessible, which in wxWidgets 3.1.1 contained GetParent() which was ...
CustomSampleRateDialog(wxWindow *parent, int defaultSampleRate=44100)
FILES_API wxString FormatWildcard(const FileTypes &fileTypes)
const ExportOptionsEditor::SampleRateList DefaultRates
enum ExportOptionsHandlerEvent::@41 type