1 /**********************************************************************
2
3 Audacity: A Digital Audio Editor
4
5 ExportMultiple.cpp
6
7 Dominic Mazzoni
8
9 *******************************************************************//**
10
11 \class ExportMultipleDialog
12 \brief Presents a dialog box allowing the user to export multiple files
13 either by exporting each track as a separate file, or by
14 exporting each label as a separate file.
15
16 *//********************************************************************/
17
18
19 #include "ExportMultiple.h"
20
21 #include <wx/defs.h>
22 #include <wx/button.h>
23 #include <wx/checkbox.h>
24 #include <wx/choice.h>
25 #include <wx/dialog.h>
26 #include <wx/dirdlg.h>
27 #include <wx/event.h>
28 #include <wx/listbase.h>
29 #include <wx/filefn.h>
30 #include <wx/filename.h>
31 #include <wx/intl.h>
32 #include <wx/log.h>
33 #include <wx/radiobut.h>
34 #include <wx/simplebook.h>
35 #include <wx/sizer.h>
36 #include <wx/statbox.h>
37 #include <wx/stattext.h>
38 #include <wx/textctrl.h>
39 #include <wx/textdlg.h>
40
41 #include "FileNames.h"
42 #include "LabelTrack.h"
43 #include "Project.h"
44 #include "ProjectSettings.h"
45 #include "ProjectWindow.h"
46 #include "ProjectWindows.h"
47 #include "Prefs.h"
48 #include "../SelectionState.h"
49 #include "../ShuttleGui.h"
50 #include "../Tags.h"
51 #include "../WaveTrack.h"
52 #include "../widgets/HelpSystem.h"
53 #include "../widgets/AudacityMessageBox.h"
54 #include "../widgets/AudacityTextEntryDialog.h"
55 #include "../widgets/ProgressDialog.h"
56
57
58 namespace {
59 /** \brief A private class used to store the information needed to do an
60 * export.
61 *
62 * We create a set of these during the interactive phase of the export
63 * cycle, then use them when the actual exports are done. */
64 class ExportKit
65 {
66 public:
67 Tags filetags; /**< The set of metadata to use for the export */
68 wxFileNameWrapper destfile; /**< The file to export to */
69 double t0; /**< Start time for the export */
70 double t1; /**< End time for the export */
71 unsigned channels; /**< Number of channels for ExportMultipleByTrack */
72 }; // end of ExportKit declaration
73 /* we are going to want an set of these kits, and don't know how many until
74 * runtime. I would dearly like to use a std::vector, but it seems that
75 * this isn't done anywhere else in Audacity, presumably for a reason?, so
76 * I'm stuck with wxArrays, which are much harder, as well as non-standard.
77 */
78 }
79
80 /* define our dynamic array of export settings */
81
82 enum {
83 FormatID = 10001,
84 OptionsID,
85 DirID,
86 CreateID,
87 ChooseID,
88 LabelID,
89 FirstID,
90 FirstFileNameID,
91 TrackID,
92 ByNameAndNumberID,
93 ByNameID,
94 ByNumberID,
95 PrefixID,
96 OverwriteID
97 };
98
99 //
100 // ExportMultipleDialog methods
101 //
102
BEGIN_EVENT_TABLE(ExportMultipleDialog,wxDialogWrapper)103 BEGIN_EVENT_TABLE(ExportMultipleDialog, wxDialogWrapper)
104 EVT_CHOICE(FormatID, ExportMultipleDialog::OnFormat)
105 // EVT_BUTTON(OptionsID, ExportMultipleDialog::OnOptions)
106 EVT_BUTTON(CreateID, ExportMultipleDialog::OnCreate)
107 EVT_BUTTON(ChooseID, ExportMultipleDialog::OnChoose)
108 EVT_BUTTON(wxID_OK, ExportMultipleDialog::OnExport)
109 EVT_BUTTON(wxID_CANCEL, ExportMultipleDialog::OnCancel)
110 EVT_BUTTON(wxID_HELP, ExportMultipleDialog::OnHelp)
111 EVT_RADIOBUTTON(LabelID, ExportMultipleDialog::OnLabel)
112 EVT_RADIOBUTTON(TrackID, ExportMultipleDialog::OnTrack)
113 EVT_RADIOBUTTON(ByNameAndNumberID, ExportMultipleDialog::OnByName)
114 EVT_RADIOBUTTON(ByNameID, ExportMultipleDialog::OnByName)
115 EVT_RADIOBUTTON(ByNumberID, ExportMultipleDialog::OnByNumber)
116 EVT_CHECKBOX(FirstID, ExportMultipleDialog::OnFirst)
117 EVT_TEXT(FirstFileNameID, ExportMultipleDialog::OnFirstFileName)
118 EVT_TEXT(PrefixID, ExportMultipleDialog::OnPrefix)
119 END_EVENT_TABLE()
120
121 BEGIN_EVENT_TABLE(SuccessDialog, wxDialogWrapper)
122 EVT_LIST_KEY_DOWN(wxID_ANY, SuccessDialog::OnKeyDown)
123 EVT_LIST_ITEM_ACTIVATED(wxID_ANY, SuccessDialog::OnItemActivated) // happens when <enter> is pressed with list item having focus
124 END_EVENT_TABLE()
125
126 BEGIN_EVENT_TABLE(MouseEvtHandler, wxEvtHandler)
127 EVT_LEFT_DCLICK(MouseEvtHandler::OnMouse)
128 END_EVENT_TABLE()
129
130 ExportMultipleDialog::ExportMultipleDialog(AudacityProject *project)
131 : wxDialogWrapper( &GetProjectFrame( *project ),
132 wxID_ANY, XO("Export Multiple") )
133 , mExporter{ *project }
134 , mSelectionState{ SelectionState::Get( *project ) }
135 {
136 SetName();
137
138 mProject = project;
139 mTracks = &TrackList::Get( *project );
140 // Construct an array of non-owning pointers
141 for (const auto &plugin : mExporter.GetPlugins())
142 mPlugins.push_back(plugin.get());
143
144 this->CountTracksAndLabels();
145
146 mBook = NULL;
147
148 ShuttleGui S(this, eIsCreatingFromPrefs);
149
150 // Creating some of the widgets cause events to fire
151 // and we don't want that until after we're completely
152 // created. (Observed on Windows)
153 mInitialized = false;
154 PopulateOrExchange(S);
155 mInitialized = true;
156
157 Layout();
158 Fit();
159 SetMinSize(GetSize());
160 Center();
161
162 EnableControls();
163 }
164
~ExportMultipleDialog()165 ExportMultipleDialog::~ExportMultipleDialog()
166 {
167 }
168
CountTracksAndLabels()169 void ExportMultipleDialog::CountTracksAndLabels()
170 {
171 bool anySolo = !(( mTracks->Any<const WaveTrack>() + &WaveTrack::GetSolo ).empty());
172
173 mNumWaveTracks =
174 (mTracks->Leaders< const WaveTrack >() -
175 (anySolo ? &WaveTrack::GetNotSolo : &WaveTrack::GetMute)).size();
176
177 // only the first label track
178 mLabels = *mTracks->Any< const LabelTrack >().begin();
179 mNumLabels = mLabels ? mLabels->GetNumLabels() : 0;
180 }
181
ShowModal()182 int ExportMultipleDialog::ShowModal()
183 {
184 // Cannot export if all audio tracks are muted.
185 if (mNumWaveTracks == 0)
186 {
187 ::AudacityMessageBox(
188 XO("All audio is muted."),
189 XO("Cannot Export Multiple"),
190 wxOK | wxCENTRE,
191 this);
192 return wxID_CANCEL;
193 }
194
195 if ((mNumWaveTracks < 1) && (mNumLabels < 1))
196 {
197 ::AudacityMessageBox(
198 XO(
199 "You have no unmuted Audio Tracks and no applicable \
200 \nlabels, so you cannot export to separate audio files."),
201 XO("Cannot Export Multiple"),
202 wxOK | wxCENTRE,
203 this);
204 return wxID_CANCEL;
205 }
206
207 bool bHasLabels = (mNumLabels > 0);
208 bool bHasTracks = (mNumWaveTracks > 0);
209
210 mLabel->Enable(bHasLabels && bHasTracks);
211 mTrack->Enable(bHasTracks);
212
213 // If you have 2 or more tracks, then it is export by tracks.
214 // If you have no labels, then it is export by tracks.
215 // Otherwise it is export by labels, by default.
216 bool bPreferByLabels = bHasLabels && (mNumWaveTracks < 2);
217 mLabel->SetValue(bPreferByLabels);
218 mTrack->SetValue(!bPreferByLabels);
219
220 EnableControls();
221
222 return wxDialogWrapper::ShowModal();
223 }
224
PopulateOrExchange(ShuttleGui & S)225 void ExportMultipleDialog::PopulateOrExchange(ShuttleGui& S)
226 {
227 wxString name = mProject->GetProjectName();
228 wxString defaultFormat = gPrefs->Read(wxT("/Export/Format"), wxT("WAV"));
229
230 TranslatableStrings visibleFormats;
231 wxArrayStringEx formats;
232 mPluginIndex = -1;
233 mFilterIndex = 0;
234
235 {
236 int i = -1;
237 for (const auto &pPlugin : mPlugins)
238 {
239 ++i;
240 for (int j = 0; j < pPlugin->GetFormatCount(); j++)
241 {
242 auto format = mPlugins[i]->GetDescription(j);
243 visibleFormats.push_back( format );
244 // use MSGID of description as a value too, written into config file
245 // This is questionable. A change in the msgid can make the
246 // preference stored in old config files inapplicable
247 formats.push_back( format.MSGID().GET() );
248 if (mPlugins[i]->GetFormat(j) == defaultFormat) {
249 mPluginIndex = i;
250 mSubFormatIndex = j;
251 }
252 if (mPluginIndex == -1) mFilterIndex++;
253 }
254 }
255 }
256
257
258 // Bug 1304: Set the default file path. It's used if none stored in config.
259 auto DefaultPath = FileNames::FindDefaultPath(FileNames::Operation::Export);
260
261 if (mPluginIndex == -1)
262 {
263 mPluginIndex = 0;
264 mFilterIndex = 0;
265 mSubFormatIndex = 0;
266 }
267
268 S.SetBorder(5);
269 S.StartHorizontalLay(wxEXPAND, true);
270 {
271 S.SetBorder(5);
272 S.StartStatic(XO("Export files to:"), true);
273 {
274 S.StartMultiColumn(4, true);
275 {
276 mDir = S.Id(DirID)
277 .AddTextBox(XXO("Folder:"),
278 DefaultPath,
279 64);
280 S.Id(ChooseID).AddButton(XXO("Choose..."));
281 S.Id(CreateID).AddButton(XXO("Create"));
282
283 mFormat = S.Id(FormatID)
284 .TieChoice( XXO("Format:"),
285 {
286 wxT("/Export/MultipleFormat"),
287 {
288 ByColumns,
289 visibleFormats,
290 formats
291 },
292 mFilterIndex
293 }
294 );
295 S.AddVariableText( {}, false);
296 S.AddVariableText( {}, false);
297
298 S.AddPrompt(XXO("Options:"));
299
300 mBook = S.Id(OptionsID)
301 .Style(wxBORDER_STATIC)
302 .StartSimplebook();
303 if (S.GetMode() == eIsCreating)
304 {
305 for (const auto &pPlugin : mPlugins)
306 {
307 for (int j = 0; j < pPlugin->GetFormatCount(); j++)
308 {
309 // Name of simple book page is not displayed
310 S.StartNotebookPage( {} );
311 pPlugin->OptionsCreate(S, j);
312 S.EndNotebookPage();
313 }
314 }
315 mBook->ChangeSelection(mFormat->GetSelection());
316 }
317 S.EndSimplebook();
318 S.AddVariableText( {}, false);
319 S.AddVariableText( {}, false);
320 }
321 S.EndMultiColumn();
322 }
323 S.EndStatic();
324 }
325 S.EndHorizontalLay();
326
327 S.StartHorizontalLay(wxEXPAND, false);
328 {
329 S.SetBorder(5);
330 S.StartStatic(XO("Split files based on:"), 1);
331 {
332 // Row 1
333 S.SetBorder(1);
334
335 // Bug 2692: Place button group in panel so tabbing will work and,
336 // on the Mac, VoiceOver will announce as radio buttons.
337 S.StartPanel();
338 {
339 mTrack = S.Id(TrackID)
340 .AddRadioButton(XXO("Tracks"));
341
342 // Row 2
343 S.SetBorder(1);
344 mLabel = S.Id(LabelID)
345 .AddRadioButtonToGroup(XXO("Labels"));
346 }
347 S.EndPanel();
348
349 S.SetBorder(3);
350 S.StartMultiColumn(2, wxEXPAND);
351 S.SetStretchyCol(1);
352 {
353 // Row 3 (indented)
354 S.AddVariableText(Verbatim(" "), false);
355 mFirst = S.Id(FirstID)
356 .AddCheckBox(XXO("Include audio before first label"), false);
357
358 // Row 4
359 S.AddVariableText( {}, false);
360 S.StartMultiColumn(2, wxEXPAND);
361 S.SetStretchyCol(1);
362 {
363 mFirstFileLabel =
364 S.AddVariableText(XO("First file name:"), false);
365 mFirstFileName = S.Id(FirstFileNameID)
366 .Prop(1)
367 .Name(XO("First file name"))
368 .TieTextBox( {},
369 name,
370 30);
371 }
372 S.EndMultiColumn();
373 }
374 S.EndMultiColumn();
375
376 S.SetBorder(3);
377 }
378 S.EndStatic();
379
380 S.SetBorder(5);
381 S.StartStatic(XO("Name files:"), 1);
382 {
383 S.SetBorder(2);
384
385 // Bug 2692: Place button group in panel so tabbing will work and,
386 // on the Mac, VoiceOver will announce as radio buttons.
387 S.StartPanel();
388 {
389 S.StartRadioButtonGroup({
390 wxT("/Export/TrackNameWithOrWithoutNumbers"),
391 {
392 { wxT("labelTrack"), XXO("Using Label/Track Name") },
393 { wxT("numberBefore"), XXO("Numbering before Label/Track Name") },
394 { wxT("numberAfter"), XXO("Numbering after File name prefix") },
395 },
396 0 // labelTrack
397 });
398 {
399 mByName = S.Id(ByNameID).TieRadioButton();
400
401 mByNumberAndName = S.Id(ByNameAndNumberID).TieRadioButton();
402
403 mByNumber = S.Id(ByNumberID).TieRadioButton();
404 }
405 S.EndRadioButtonGroup();
406 }
407 S.EndPanel();
408
409 S.StartMultiColumn(3, wxEXPAND);
410 S.SetStretchyCol(2);
411 {
412 // Row 3 (indented)
413 S.AddVariableText(Verbatim(" "), false);
414 mPrefixLabel = S.AddVariableText(XO("File name prefix:"), false);
415 mPrefix = S.Id(PrefixID)
416 .Name(XO("File name prefix"))
417 .TieTextBox( {},
418 name,
419 30);
420 }
421 S.EndMultiColumn();
422 }
423 S.EndStatic();
424 }
425 S.EndHorizontalLay();
426
427 S.SetBorder(5);
428 S.StartHorizontalLay(wxEXPAND, false);
429 {
430 mOverwrite = S.Id(OverwriteID).TieCheckBox(XXO("Overwrite existing files"),
431 {wxT("/Export/OverwriteExisting"),
432 false});
433 }
434 S.EndHorizontalLay();
435
436 S.AddStandardButtons(eOkButton | eCancelButton | eHelpButton);
437 mExport = (wxButton *)wxWindow::FindWindowById(wxID_OK, this);
438 mExport->SetLabel(_("Export"));
439
440 }
441
EnableControls()442 void ExportMultipleDialog::EnableControls()
443 {
444 bool enable;
445
446 if (!mInitialized) {
447 return;
448 }
449
450 mFirst->Enable(mLabel->GetValue());
451
452 enable = mLabel->GetValue() &&
453 (mByName->GetValue() || mByNumberAndName->GetValue()) &&
454 mFirst->GetValue();
455 mFirstFileLabel->Enable(enable);
456 mFirstFileName->Enable(enable);
457
458 enable = mByNumber->GetValue();
459 mPrefixLabel->Enable(enable);
460 mPrefix->Enable(enable);
461
462 bool ok = true;
463
464 if (mLabel->GetValue() && mFirst->GetValue() &&
465 mFirstFileName->GetValue().empty() &&
466 mPrefix->GetValue().empty())
467 ok = false;
468
469 if (mByNumber->GetValue() &&
470 mPrefix->GetValue().empty())
471 ok = false;
472
473 mExport->Enable(ok);
474 }
475
OnFormat(wxCommandEvent & WXUNUSED (event))476 void ExportMultipleDialog::OnFormat(wxCommandEvent& WXUNUSED(event))
477 {
478 mBook->ChangeSelection(mFormat->GetSelection());
479
480 EnableControls();
481 }
482
OnOptions(wxCommandEvent & WXUNUSED (event))483 void ExportMultipleDialog::OnOptions(wxCommandEvent& WXUNUSED(event))
484 {
485 const int sel = mFormat->GetSelection();
486 if (sel != wxNOT_FOUND)
487 {
488 size_t c = 0;
489 int i = -1;
490 for (const auto &pPlugin : mPlugins)
491 {
492 ++i;
493 for (int j = 0; j < pPlugin->GetFormatCount(); j++)
494 {
495 if ((size_t)sel == c)
496 {
497 mPluginIndex = i;
498 mSubFormatIndex = j;
499 }
500 c++;
501 }
502 }
503 }
504 mPlugins[mPluginIndex]->DisplayOptions(this,mSubFormatIndex);
505 }
506
OnCreate(wxCommandEvent & WXUNUSED (event))507 void ExportMultipleDialog::OnCreate(wxCommandEvent& WXUNUSED(event))
508 {
509 wxFileName fn;
510
511 fn.AssignDir(mDir->GetValue());
512
513 bool ok = fn.Mkdir(0777, wxPATH_MKDIR_FULL);
514
515 if (!ok) {
516 // Mkdir will produce an error dialog
517 return;
518 }
519
520 ::AudacityMessageBox(
521 XO("\"%s\" successfully created.").Format( fn.GetPath() ),
522 XO("Export Multiple"),
523 wxOK | wxCENTRE,
524 this);
525 }
526
OnChoose(wxCommandEvent & WXUNUSED (event))527 void ExportMultipleDialog::OnChoose(wxCommandEvent& WXUNUSED(event))
528 {
529 wxDirDialogWrapper dlog(this,
530 XO("Choose a location to save the exported files"),
531 mDir->GetValue());
532 dlog.ShowModal();
533 if (!dlog.GetPath().empty())
534 mDir->SetValue(dlog.GetPath());
535 }
536
OnLabel(wxCommandEvent & WXUNUSED (event))537 void ExportMultipleDialog::OnLabel(wxCommandEvent& WXUNUSED(event))
538 {
539 EnableControls();
540 }
541
OnFirst(wxCommandEvent & WXUNUSED (event))542 void ExportMultipleDialog::OnFirst(wxCommandEvent& WXUNUSED(event))
543 {
544 EnableControls();
545 }
546
OnFirstFileName(wxCommandEvent & WXUNUSED (event))547 void ExportMultipleDialog::OnFirstFileName(wxCommandEvent& WXUNUSED(event))
548 {
549 EnableControls();
550 }
551
OnTrack(wxCommandEvent & WXUNUSED (event))552 void ExportMultipleDialog::OnTrack(wxCommandEvent& WXUNUSED(event))
553 {
554 EnableControls();
555 }
556
OnByName(wxCommandEvent & WXUNUSED (event))557 void ExportMultipleDialog::OnByName(wxCommandEvent& WXUNUSED(event))
558 {
559 EnableControls();
560 }
561
OnByNumber(wxCommandEvent & WXUNUSED (event))562 void ExportMultipleDialog::OnByNumber(wxCommandEvent& WXUNUSED(event))
563 {
564 EnableControls();
565 }
566
OnPrefix(wxCommandEvent & WXUNUSED (event))567 void ExportMultipleDialog::OnPrefix(wxCommandEvent& WXUNUSED(event))
568 {
569 EnableControls();
570 }
571
OnCancel(wxCommandEvent & WXUNUSED (event))572 void ExportMultipleDialog::OnCancel(wxCommandEvent& WXUNUSED(event))
573 {
574 EndModal(0);
575 }
576
OnHelp(wxCommandEvent & WXUNUSED (event))577 void ExportMultipleDialog::OnHelp(wxCommandEvent& WXUNUSED(event))
578 {
579 HelpSystem::ShowHelp(this, L"Export_Multiple", true);
580 }
581
OnExport(wxCommandEvent & WXUNUSED (event))582 void ExportMultipleDialog::OnExport(wxCommandEvent& WXUNUSED(event))
583 {
584 ShuttleGui S(this, eIsSavingToPrefs);
585 PopulateOrExchange(S);
586
587 gPrefs->Flush();
588
589 FileNames::UpdateDefaultPath(FileNames::Operation::Export, mDir->GetValue());
590
591 // Make sure the output directory is in good shape
592 if (!DirOk()) {
593 return;
594 }
595
596 mFilterIndex = mFormat->GetSelection();
597 if (mFilterIndex != wxNOT_FOUND)
598 {
599 size_t c = 0;
600 int i = -1;
601 for (const auto &pPlugin : mPlugins)
602 {
603 ++i;
604 for (int j = 0; j < pPlugin->GetFormatCount(); j++, c++)
605 {
606 if ((size_t)mFilterIndex == c)
607 { // this is the selected format. Store the plug-in and sub-format
608 // needed to achieve it.
609 mPluginIndex = i;
610 mSubFormatIndex = j;
611 mBook->GetPage(mFilterIndex)->TransferDataFromWindow();
612 }
613 }
614 }
615 }
616
617 // bool overwrite = mOverwrite->GetValue();
618 ProgressResult ok = ProgressResult::Failed;
619 mExported.clear();
620
621 // Give 'em the result
622 auto cleanup = finally( [&]
623 {
624 auto msg = (ok == ProgressResult::Success
625 ? XO("Successfully exported the following %lld file(s).")
626 : ok == ProgressResult::Failed
627 ? XO("Something went wrong after exporting the following %lld file(s).")
628 : ok == ProgressResult::Cancelled
629 ? XO("Export canceled after exporting the following %lld file(s).")
630 : ok == ProgressResult::Stopped
631 ? XO("Export stopped after exporting the following %lld file(s).")
632 : XO("Something went really wrong after exporting the following %lld file(s).")
633 ).Format((long long) mExported.size());
634
635 wxString FileList;
636 for (size_t i = 0; i < mExported.size(); i++) {
637 FileList += mExported[i];
638 FileList += '\n';
639 }
640
641 // TODO: give some warning dialog first, when only some files exported
642 // successfully.
643
644 GuardedCall( [&] {
645 // This results dialog is a child of this dialog.
646 HelpSystem::ShowInfoDialog( this,
647 XO("Export Multiple"),
648 msg,
649 FileList,
650 450,400);
651 } );
652 } );
653
654 if (mLabel->GetValue()) {
655 ok = ExportMultipleByLabel(mByName->GetValue() || mByNumberAndName->GetValue(),
656 mPrefix->GetValue(),
657 mByNumberAndName->GetValue());
658 }
659 else {
660 ok = ExportMultipleByTrack(mByName->GetValue() || mByNumberAndName->GetValue(),
661 mPrefix->GetValue(),
662 mByNumberAndName->GetValue());
663 }
664
665 if (ok == ProgressResult::Success || ok == ProgressResult::Stopped) {
666 EndModal(1);
667 }
668 }
669
DirOk()670 bool ExportMultipleDialog::DirOk()
671 {
672 wxFileName fn;
673
674 fn.AssignDir(mDir->GetValue());
675
676 if (fn.DirExists()) {
677 return true;
678 }
679
680 auto prompt = XO("\"%s\" doesn't exist.\n\nWould you like to create it?")
681 .Format( fn.GetFullPath() );
682
683 int action = AudacityMessageBox(
684 prompt,
685 XO("Warning"),
686 wxYES_NO | wxICON_EXCLAMATION);
687 if (action != wxYES) {
688 return false;
689 }
690
691 return fn.Mkdir(0777, wxPATH_MKDIR_FULL);
692 }
693
GetNumExportChannels(const TrackList & tracks)694 static unsigned GetNumExportChannels( const TrackList &tracks )
695 {
696 /* counters for tracks panned different places */
697 int numLeft = 0;
698 int numRight = 0;
699 //int numMono = 0;
700 /* track iteration kit */
701
702 bool anySolo = !(( tracks.Any<const WaveTrack>() + &WaveTrack::GetSolo ).empty());
703
704 // Want only unmuted wave tracks.
705 for (auto tr :
706 tracks.Any< const WaveTrack >() -
707 (anySolo ? &WaveTrack::GetNotSolo : &WaveTrack::GetMute)
708 ) {
709 // Found a left channel
710 if (tr->GetChannel() == Track::LeftChannel) {
711 numLeft++;
712 }
713
714 // Found a right channel
715 else if (tr->GetChannel() == Track::RightChannel) {
716 numRight++;
717 }
718
719 // Found a mono channel, but it may be panned
720 else if (tr->GetChannel() == Track::MonoChannel) {
721 float pan = tr->GetPan();
722
723 // Figure out what kind of channel it should be
724 if (pan == -1.0) { // panned hard left
725 numLeft++;
726 }
727 else if (pan == 1.0) { // panned hard right
728 numRight++;
729 }
730 else if (pan == 0) { // panned dead center
731 // numMono++;
732 }
733 else { // panned somewhere else
734 numLeft++;
735 numRight++;
736 }
737 }
738 }
739
740 // if there is stereo content, report 2, else report 1
741 if (numRight > 0 || numLeft > 0) {
742 return 2;
743 }
744
745 return 1;
746 }
747
748 // TODO: JKC July2016: Merge labels/tracks duplicated export code.
749 // TODO: JKC Apr2019: Doubly so merge these! Too much duplication.
ExportMultipleByLabel(bool byName,const wxString & prefix,bool addNumber)750 ProgressResult ExportMultipleDialog::ExportMultipleByLabel(bool byName,
751 const wxString &prefix, bool addNumber)
752 {
753 wxASSERT(mProject);
754 int numFiles = mNumLabels;
755 int l = 0; // counter for files done
756 std::vector<ExportKit> exportSettings; // dynamic array for settings.
757 exportSettings.reserve(numFiles); // Allocate some guessed space to use.
758
759 // Account for exporting before first label
760 if( mFirst->GetValue() ) {
761 l--;
762 numFiles++;
763 }
764
765 // Figure out how many channels we should export.
766 auto channels = GetNumExportChannels( *mTracks );
767
768 FilePaths otherNames; // keep track of file names we will use, so we
769 // don't duplicate them
770 ExportKit setting; // the current batch of settings
771 setting.destfile.SetPath(mDir->GetValue());
772 setting.destfile.SetExt(mPlugins[mPluginIndex]->GetExtension(mSubFormatIndex));
773 wxLogDebug(wxT("Plug-in index = %d, Sub-format = %d"), mPluginIndex, mSubFormatIndex);
774 wxLogDebug(wxT("File extension is %s"), setting.destfile.GetExt());
775 wxString name; // used to hold file name whilst we mess with it
776 wxString title; // un-messed-with title of file for tagging with
777
778 const LabelStruct *info = NULL;
779 /* Examine all labels a first time, sort out all data but don't do any
780 * exporting yet (so this run is quick but interactive) */
781 while( l < mNumLabels ) {
782
783 // Get file name and starting time
784 if( l < 0 ) {
785 // create wxFileName for output file
786 name = (mFirstFileName->GetValue());
787 setting.t0 = 0.0;
788 } else {
789 info = mLabels->GetLabel(l);
790 name = (info->title);
791 setting.t0 = info->selectedRegion.t0();
792 }
793
794 // Figure out the ending time
795 if( info && !info->selectedRegion.isPoint() ) {
796 setting.t1 = info->selectedRegion.t1();
797 } else if( l < mNumLabels-1 ) {
798 // Use start of next label as end
799 const LabelStruct *info1 = mLabels->GetLabel(l+1);
800 setting.t1 = info1->selectedRegion.t0();
801 } else {
802 setting.t1 = mTracks->GetEndTime();
803 }
804
805 if( name.empty() )
806 name = _("untitled");
807
808 // store title of label to use in tags
809 title = name;
810
811 // Numbering files...
812 if( !byName ) {
813 name.Printf(wxT("%s-%02d"), prefix, l+1);
814 } else if( addNumber ) {
815 // Following discussion with GA, always have 2 digits
816 // for easy file-name sorting (on Windows)
817 name.Prepend(wxString::Format(wxT("%02d-"), l+1));
818 }
819
820 // store sanitised and user checked name in object
821 setting.destfile.SetName(MakeFileName(name));
822 if( setting.destfile.GetName().empty() )
823 { // user cancelled dialogue, or deleted everything in field.
824 // or maybe the label was empty??
825 // So we ignore this one and keep going.
826 }
827 else
828 {
829 // FIXME: TRAP_ERR User could have given an illegal filename prefix.
830 // in that case we should tell them, not fail silently.
831 wxASSERT(setting.destfile.IsOk()); // burp if file name is broke
832
833 // Make sure the (final) file name is unique within the set of exports
834 FileNames::MakeNameUnique(otherNames, setting.destfile);
835
836 /* do the metadata for this file */
837 // copy project metadata to start with
838 setting.filetags = Tags::Get( *mProject );
839 setting.filetags.LoadDefaults();
840 if (exportSettings.size()) {
841 setting.filetags = exportSettings.back().filetags;
842 }
843 // over-ride with values
844 setting.filetags.SetTag(TAG_TITLE, title);
845 setting.filetags.SetTag(TAG_TRACK, l+1);
846 // let the user have a crack at editing it, exit if cancelled
847 auto &settings = ProjectSettings::Get( *mProject );
848 bool bShowTagsDialog = settings.GetShowId3Dialog();
849
850 bShowTagsDialog = bShowTagsDialog && mPlugins[mPluginIndex]->GetCanMetaData(mSubFormatIndex);
851
852 if( bShowTagsDialog ){
853 bool bCancelled = !setting.filetags.ShowEditDialog(
854 ProjectWindow::Find( mProject ),
855 XO("Edit Metadata Tags"), bShowTagsDialog);
856 gPrefs->Read(wxT("/AudioFiles/ShowId3Dialog"), &bShowTagsDialog, true);
857 settings.SetShowId3Dialog( bShowTagsDialog );
858 if( bCancelled )
859 return ProgressResult::Cancelled;
860 }
861 }
862
863 /* add the settings to the array of settings to be used for export */
864 exportSettings.push_back(setting);
865
866 l++; // next label, count up one
867 }
868
869 auto ok = ProgressResult::Success; // did it work?
870 int count = 0; // count the number of successful runs
871 ExportKit activeSetting; // pointer to the settings in use for this export
872 /* Go round again and do the exporting (so this run is slow but
873 * non-interactive) */
874 std::unique_ptr<ProgressDialog> pDialog;
875 for (count = 0; count < numFiles; count++) {
876 /* get the settings to use for the export from the array */
877 activeSetting = exportSettings[count];
878 // Bug 1440 fix.
879 if( activeSetting.destfile.GetName().empty() )
880 continue;
881
882 // Export it
883 ok = DoExport(pDialog, channels, activeSetting.destfile, false,
884 activeSetting.t0, activeSetting.t1, activeSetting.filetags);
885 if (ok == ProgressResult::Stopped) {
886 AudacityMessageDialog dlgMessage(
887 nullptr,
888 XO("Continue to export remaining files?"),
889 XO("Export"),
890 wxYES_NO | wxNO_DEFAULT | wxICON_WARNING);
891 if (dlgMessage.ShowModal() != wxID_YES ) {
892 // User decided not to continue - bail out!
893 break;
894 }
895 }
896 else if (ok != ProgressResult::Success) {
897 break;
898 }
899 }
900
901 return ok;
902 }
903
ExportMultipleByTrack(bool byName,const wxString & prefix,bool addNumber)904 ProgressResult ExportMultipleDialog::ExportMultipleByTrack(bool byName,
905 const wxString &prefix, bool addNumber)
906 {
907 wxASSERT(mProject);
908 int l = 0; // track counter
909 auto ok = ProgressResult::Success;
910 FilePaths otherNames;
911 std::vector<ExportKit> exportSettings; // dynamic array we will use to store the
912 // settings needed to do the exports with in
913 exportSettings.reserve(mNumWaveTracks); // Allocate some guessed space to use.
914 ExportKit setting; // the current batch of settings
915 setting.destfile.SetPath(mDir->GetValue());
916 setting.destfile.SetExt(mPlugins[mPluginIndex]->GetExtension(mSubFormatIndex));
917
918 wxString name; // used to hold file name whilst we mess with it
919 wxString title; // un-messed-with title of file for tagging with
920
921 /* Remember which tracks were selected, and set them to deselected */
922 SelectionStateChanger changer{ mSelectionState, *mTracks };
923 for (auto tr : mTracks->Selected<WaveTrack>())
924 tr->SetSelected(false);
925
926 bool anySolo = !(( mTracks->Any<const WaveTrack>() + &WaveTrack::GetSolo ).empty());
927
928 bool skipSilenceAtBeginning;
929 gPrefs->Read(wxT("/AudioFiles/SkipSilenceAtBeginning"), &skipSilenceAtBeginning, false);
930
931 /* Examine all tracks in turn, collecting export information */
932 for (auto tr : mTracks->Leaders<WaveTrack>() -
933 (anySolo ? &WaveTrack::GetNotSolo : &WaveTrack::GetMute)) {
934
935 // Get the times for the track
936 auto channels = TrackList::Channels(tr);
937 setting.t0 = skipSilenceAtBeginning ? channels.min(&Track::GetStartTime) : 0;
938 setting.t1 = channels.max( &Track::GetEndTime );
939
940 // number of export channels?
941 setting.channels = channels.size();
942 if (setting.channels == 1 &&
943 !(tr->GetChannel() == WaveTrack::MonoChannel &&
944 tr->GetPan() == 0.0))
945 setting.channels = 2;
946
947 // Get name and title
948 title = tr->GetName();
949 if( title.empty() )
950 title = _("untitled");
951
952 if (byName) {
953 name = title;
954 if (addNumber) {
955 name.Prepend(
956 wxString::Format(wxT("%02d-"), l+1));
957 }
958 }
959 else {
960 name = (wxString::Format(wxT("%s-%02d"), prefix, l+1));
961 }
962
963 // store sanitised and user checked name in object
964 setting.destfile.SetName(MakeFileName(name));
965
966 if (setting.destfile.GetName().empty())
967 { // user cancelled dialogue, or deleted everything in field.
968 // So we ignore this one and keep going.
969 }
970 else
971 {
972
973 // FIXME: TRAP_ERR User could have given an illegal track name.
974 // in that case we should tell them, not fail silently.
975 wxASSERT(setting.destfile.IsOk()); // burp if file name is broke
976
977 // Make sure the (final) file name is unique within the set of exports
978 FileNames::MakeNameUnique(otherNames, setting.destfile);
979
980 /* do the metadata for this file */
981 // copy project metadata to start with
982 setting.filetags = Tags::Get( *mProject );
983 setting.filetags.LoadDefaults();
984 if (exportSettings.size()) {
985 setting.filetags = exportSettings.back().filetags;
986 }
987 // over-ride with values
988 setting.filetags.SetTag(TAG_TITLE, title);
989 setting.filetags.SetTag(TAG_TRACK, l+1);
990 // let the user have a crack at editing it, exit if cancelled
991 auto &settings = ProjectSettings::Get( *mProject );
992 bool bShowTagsDialog = settings.GetShowId3Dialog();
993
994 bShowTagsDialog = bShowTagsDialog && mPlugins[mPluginIndex]->GetCanMetaData(mSubFormatIndex);
995
996 if( bShowTagsDialog ){
997 bool bCancelled = !setting.filetags.ShowEditDialog(
998 ProjectWindow::Find( mProject ),
999 XO("Edit Metadata Tags"), bShowTagsDialog);
1000 gPrefs->Read(wxT("/AudioFiles/ShowId3Dialog"), &bShowTagsDialog, true);
1001 settings.SetShowId3Dialog( bShowTagsDialog );
1002 if( bCancelled )
1003 return ProgressResult::Cancelled;
1004 }
1005 }
1006 /* add the settings to the array of settings to be used for export */
1007 exportSettings.push_back(setting);
1008
1009 l++; // next track, count up one
1010 }
1011 // end of user-interactive data gathering loop, start of export processing
1012 // loop
1013 int count = 0; // count the number of successful runs
1014 ExportKit activeSetting; // pointer to the settings in use for this export
1015 std::unique_ptr<ProgressDialog> pDialog;
1016
1017 for (auto tr : mTracks->Leaders<WaveTrack>() -
1018 (anySolo ? &WaveTrack::GetNotSolo : &WaveTrack::GetMute)) {
1019
1020 wxLogDebug( "Get setting %i", count );
1021 /* get the settings to use for the export from the array */
1022 activeSetting = exportSettings[count];
1023 if( activeSetting.destfile.GetName().empty() ){
1024 count++;
1025 continue;
1026 }
1027
1028 /* Select the track */
1029 SelectionStateChanger changer2{ mSelectionState, *mTracks };
1030 const auto range = TrackList::Channels(tr);
1031 for (auto channel : range)
1032 channel->SetSelected(true);
1033
1034 // Export the data. "channels" are per track.
1035 ok = DoExport(pDialog,
1036 activeSetting.channels, activeSetting.destfile, true,
1037 activeSetting.t0, activeSetting.t1, activeSetting.filetags);
1038 if (ok == ProgressResult::Stopped) {
1039 AudacityMessageDialog dlgMessage(
1040 nullptr,
1041 XO("Continue to export remaining files?"),
1042 XO("Export"),
1043 wxYES_NO | wxNO_DEFAULT | wxICON_WARNING);
1044 if (dlgMessage.ShowModal() != wxID_YES ) {
1045 // User decided not to continue - bail out!
1046 break;
1047 }
1048 }
1049 else if (ok != ProgressResult::Success) {
1050 break;
1051 }
1052 // increment export counter
1053 count++;
1054
1055 }
1056
1057 return ok ;
1058 }
1059
DoExport(std::unique_ptr<ProgressDialog> & pDialog,unsigned channels,const wxFileName & inName,bool selectedOnly,double t0,double t1,const Tags & tags)1060 ProgressResult ExportMultipleDialog::DoExport(std::unique_ptr<ProgressDialog> &pDialog,
1061 unsigned channels,
1062 const wxFileName &inName,
1063 bool selectedOnly,
1064 double t0,
1065 double t1,
1066 const Tags &tags)
1067 {
1068 wxFileName name;
1069
1070 wxLogDebug(wxT("Doing multiple Export: File name \"%s\""), (inName.GetFullName()));
1071 wxLogDebug(wxT("Channels: %i, Start: %lf, End: %lf "), channels, t0, t1);
1072 if (selectedOnly)
1073 wxLogDebug(wxT("Selected Region Only"));
1074 else
1075 wxLogDebug(wxT("Whole Project"));
1076
1077 wxFileName backup;
1078 if (mOverwrite->GetValue()) {
1079 name = inName;
1080 backup.Assign(name);
1081
1082 int suffix = 0;
1083 do {
1084 backup.SetName(name.GetName() +
1085 wxString::Format(wxT("%d"), suffix));
1086 ++suffix;
1087 }
1088 while (backup.FileExists());
1089 ::wxRenameFile(inName.GetFullPath(), backup.GetFullPath());
1090 }
1091 else {
1092 name = inName;
1093 int i = 2;
1094 wxString base(name.GetName());
1095 while (name.FileExists()) {
1096 name.SetName(wxString::Format(wxT("%s-%d"), base, i++));
1097 }
1098 }
1099
1100 ProgressResult success = ProgressResult::Cancelled;
1101 const wxString fullPath{name.GetFullPath()};
1102
1103 auto cleanup = finally( [&] {
1104 bool ok =
1105 success == ProgressResult::Stopped ||
1106 success == ProgressResult::Success;
1107 if (backup.IsOk()) {
1108 if ( ok )
1109 // Remove backup
1110 ::wxRemoveFile(backup.GetFullPath());
1111 else {
1112 // Restore original
1113 ::wxRemoveFile(fullPath);
1114 ::wxRenameFile(backup.GetFullPath(), fullPath);
1115 }
1116 }
1117 else {
1118 if ( ! ok )
1119 // Remove any new, and only partially written, file.
1120 ::wxRemoveFile(fullPath);
1121 }
1122 } );
1123
1124 // Call the format export routine
1125 success = mPlugins[mPluginIndex]->Export(mProject,
1126 pDialog,
1127 channels,
1128 fullPath,
1129 selectedOnly,
1130 t0,
1131 t1,
1132 NULL,
1133 &tags,
1134 mSubFormatIndex);
1135
1136 if (success == ProgressResult::Success || success == ProgressResult::Stopped) {
1137 mExported.push_back(fullPath);
1138 }
1139
1140 Refresh();
1141 Update();
1142
1143 return success;
1144 }
1145
MakeFileName(const wxString & input)1146 wxString ExportMultipleDialog::MakeFileName(const wxString &input)
1147 {
1148 wxString newname = input; // name we are generating
1149
1150 // strip out anything that isn't allowed in file names on this platform
1151 auto changed = Internat::SanitiseFilename(newname, wxT("_"));
1152
1153 if(changed)
1154 { // need to get user to fix file name
1155 // build the dialog
1156 TranslatableString msg;
1157 wxString excluded = ::wxJoin( Internat::GetExcludedCharacters(), wxT(' '), wxT('\0') );
1158 // TODO: For Russian language we should have separate cases for 2 and more than 2 letters.
1159 if( excluded.length() > 1 ){
1160 msg = XO(
1161 // i18n-hint: The second %s gives some letters that can't be used.
1162 "Label or track \"%s\" is not a legal file name.\nYou cannot use any of these characters:\n\n%s\n\nSuggested replacement:")
1163 .Format( input, excluded );
1164 } else {
1165 msg = XO(
1166 // i18n-hint: The second %s gives a letter that can't be used.
1167 "Label or track \"%s\" is not a legal file name. You cannot use \"%s\".\n\nSuggested replacement:")
1168 .Format( input, excluded );
1169 }
1170
1171 AudacityTextEntryDialog dlg( this, msg, XO("Save As..."), newname );
1172
1173
1174 // And tell the validator about excluded chars
1175 dlg.SetTextValidator( wxFILTER_EXCLUDE_CHAR_LIST );
1176 wxTextValidator *tv = dlg.GetTextValidator();
1177 tv->SetExcludes(Internat::GetExcludedCharacters());
1178
1179 // Show the dialog and bail if the user cancels
1180 if( dlg.ShowModal() == wxID_CANCEL )
1181 {
1182 return wxEmptyString;
1183 }
1184 // Extract the name from the dialog
1185 newname = dlg.GetValue();
1186 } // phew - end of file name sanitisation procedure
1187 return newname;
1188 }
1189
OnKeyDown(wxListEvent & event)1190 void SuccessDialog::OnKeyDown(wxListEvent& event)
1191 {
1192 if (event.GetKeyCode() == WXK_RETURN)
1193 EndModal(1);
1194 else
1195 event.Skip(); // allow standard behaviour
1196 }
1197
OnItemActivated(wxListEvent & WXUNUSED (event))1198 void SuccessDialog::OnItemActivated(wxListEvent& WXUNUSED(event))
1199 {
1200 EndModal(1);
1201 }
1202
OnMouse(wxMouseEvent & event)1203 void MouseEvtHandler::OnMouse(wxMouseEvent& event)
1204 {
1205 event.Skip(false);
1206 }
1207