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