1 /**********************************************************************
2 
3   Audacity: A Digital Audio Editor
4 
5   Export.cpp
6 
7   Dominic Mazzoni
8 
9 *******************************************************************//**
10 
11 \class Export
12 \brief Main class to control the export function.
13 
14 *//****************************************************************//**
15 
16 \class ExportType
17 \brief Container for information about supported export types.
18 
19 *//****************************************************************//**
20 
21 \class ExportMixerDialog
22 \brief Dialog for advanced mixing.
23 
24 *//****************************************************************//**
25 
26 \class ExportMixerPanel
27 \brief Panel that displays mixing for advanced mixing option.
28 
29 *//********************************************************************/
30 
31 
32 #include "Export.h"
33 
34 #include <wx/bmpbuttn.h>
35 #include <wx/dcclient.h>
36 #include <wx/file.h>
37 #include <wx/filectrl.h>
38 #include <wx/filename.h>
39 #include <wx/simplebook.h>
40 #include <wx/sizer.h>
41 #include <wx/slider.h>
42 #include <wx/statbox.h>
43 #include <wx/stattext.h>
44 #include <wx/string.h>
45 #include <wx/textctrl.h>
46 #include <wx/timer.h>
47 #include <wx/dcmemory.h>
48 #include <wx/window.h>
49 
50 #include "sndfile.h"
51 
52 #include "widgets/FileDialog/FileDialog.h"
53 
54 #include "AllThemeResources.h"
55 #include "BasicUI.h"
56 #include "Mix.h"
57 #include "Prefs.h"
58 #include "../prefs/ImportExportPrefs.h"
59 #include "Project.h"
60 #include "../ProjectHistory.h"
61 #include "../ProjectSettings.h"
62 #include "../ProjectWindow.h"
63 #include "../ProjectWindows.h"
64 #include "../ShuttleGui.h"
65 #include "../Tags.h"
66 #include "Theme.h"
67 #include "../WaveTrack.h"
68 #include "../widgets/AudacityMessageBox.h"
69 #include "../widgets/Warning.h"
70 #include "../widgets/HelpSystem.h"
71 #include "AColor.h"
72 #include "FileNames.h"
73 #include "widgets/HelpSystem.h"
74 #include "widgets/ProgressDialog.h"
75 #include "wxFileNameWrapper.h"
76 
77 //----------------------------------------------------------------------------
78 // ExportPlugin
79 //----------------------------------------------------------------------------
80 
ExportPlugin()81 ExportPlugin::ExportPlugin()
82 {
83 }
84 
~ExportPlugin()85 ExportPlugin::~ExportPlugin()
86 {
87 }
88 
CheckFileName(wxFileName & WXUNUSED (filename),int WXUNUSED (format))89 bool ExportPlugin::CheckFileName(wxFileName & WXUNUSED(filename), int WXUNUSED(format))
90 {
91   return true;
92 }
93 
94 /** \brief Add a NEW entry to the list of formats this plug-in can export
95  *
96  * To configure the format use SetFormat, SetCanMetaData etc with the index of
97  * the format.
98  * @return The number of formats currently set up. This is one more than the
99  * index of the newly added format.
100  */
AddFormat()101 int ExportPlugin::AddFormat()
102 {
103    FormatInfo nf;
104    mFormatInfos.push_back(nf);
105    return mFormatInfos.size();
106 }
107 
GetFormatCount()108 int ExportPlugin::GetFormatCount()
109 {
110    return mFormatInfos.size();
111 }
112 
113 /**
114  * @param index The plugin to set the format for (range 0 to one less than the
115  * count of formats)
116  */
SetFormat(const wxString & format,int index)117 void ExportPlugin::SetFormat(const wxString & format, int index)
118 {
119    mFormatInfos[index].mFormat = format;
120 }
121 
SetDescription(const TranslatableString & description,int index)122 void ExportPlugin::SetDescription(const TranslatableString & description, int index)
123 {
124    mFormatInfos[index].mDescription = description;
125 }
126 
AddExtension(const FileExtension & extension,int index)127 void ExportPlugin::AddExtension(const FileExtension &extension, int index)
128 {
129    mFormatInfos[index].mExtensions.push_back(extension);
130 }
131 
SetExtensions(FileExtensions extensions,int index)132 void ExportPlugin::SetExtensions(FileExtensions extensions, int index)
133 {
134    mFormatInfos[index].mExtensions = std::move(extensions);
135 }
136 
SetMask(FileNames::FileTypes mask,int index)137 void ExportPlugin::SetMask(FileNames::FileTypes mask, int index)
138 {
139    mFormatInfos[index].mMask = std::move( mask );
140 }
141 
SetMaxChannels(unsigned maxchannels,unsigned index)142 void ExportPlugin::SetMaxChannels(unsigned maxchannels, unsigned index)
143 {
144    mFormatInfos[index].mMaxChannels = maxchannels;
145 }
146 
SetCanMetaData(bool canmetadata,int index)147 void ExportPlugin::SetCanMetaData(bool canmetadata, int index)
148 {
149    mFormatInfos[index].mCanMetaData = canmetadata;
150 }
151 
GetFormat(int index)152 wxString ExportPlugin::GetFormat(int index)
153 {
154    return mFormatInfos[index].mFormat;
155 }
156 
GetDescription(int index)157 TranslatableString ExportPlugin::GetDescription(int index)
158 {
159    return mFormatInfos[index].mDescription;
160 }
161 
GetExtension(int index)162 FileExtension ExportPlugin::GetExtension(int index)
163 {
164    return mFormatInfos[index].mExtensions[0];
165 }
166 
GetExtensions(int index)167 FileExtensions ExportPlugin::GetExtensions(int index)
168 {
169    return mFormatInfos[index].mExtensions;
170 }
171 
GetMask(int index)172 FileNames::FileTypes ExportPlugin::GetMask(int index)
173 {
174    if (!mFormatInfos[index].mMask.empty())
175       return mFormatInfos[index].mMask;
176 
177    return { { GetDescription(index), GetExtensions(index) } };
178 }
179 
GetMaxChannels(int index)180 unsigned ExportPlugin::GetMaxChannels(int index)
181 {
182    return mFormatInfos[index].mMaxChannels;
183 }
184 
GetCanMetaData(int index)185 bool ExportPlugin::GetCanMetaData(int index)
186 {
187    return mFormatInfos[index].mCanMetaData;
188 }
189 
IsExtension(const FileExtension & ext,int index)190 bool ExportPlugin::IsExtension(const FileExtension & ext, int index)
191 {
192    bool isext = false;
193    for (int i = index; i < GetFormatCount(); i = GetFormatCount())
194    {
195       const auto &defext = GetExtension(i);
196       const auto &defexts = GetExtensions(i);
197       int indofext = defexts.Index(ext, false);
198       if (defext.empty() || (indofext != wxNOT_FOUND))
199          isext = true;
200    }
201    return isext;
202 }
203 
DisplayOptions(wxWindow * WXUNUSED (parent),int WXUNUSED (format))204 bool ExportPlugin::DisplayOptions(wxWindow * WXUNUSED(parent), int WXUNUSED(format))
205 {
206    return false;
207 }
208 
OptionsCreate(ShuttleGui & S,int WXUNUSED (format))209 void ExportPlugin::OptionsCreate(ShuttleGui &S, int WXUNUSED(format))
210 {
211    S.StartHorizontalLay(wxCENTER);
212    {
213       S.StartHorizontalLay(wxCENTER, 0);
214       {
215          S.Prop(1).AddTitle(XO("No format specific options"));
216       }
217       S.EndHorizontalLay();
218    }
219    S.EndHorizontalLay();
220 }
221 
222 //Create a mixer by computing the time warp factor
CreateMixer(const TrackList & tracks,bool selectionOnly,double startTime,double stopTime,unsigned numOutChannels,size_t outBufferSize,bool outInterleaved,double outRate,sampleFormat outFormat,MixerSpec * mixerSpec)223 std::unique_ptr<Mixer> ExportPlugin::CreateMixer(const TrackList &tracks,
224          bool selectionOnly,
225          double startTime, double stopTime,
226          unsigned numOutChannels, size_t outBufferSize, bool outInterleaved,
227          double outRate, sampleFormat outFormat,
228          MixerSpec *mixerSpec)
229 {
230    WaveTrackConstArray inputTracks;
231 
232    bool anySolo = !(( tracks.Any<const WaveTrack>() + &WaveTrack::GetSolo ).empty());
233 
234    auto range = tracks.Any< const WaveTrack >()
235       + (selectionOnly ? &Track::IsSelected : &Track::Any )
236       - ( anySolo ? &WaveTrack::GetNotSolo : &WaveTrack::GetMute);
237    for (auto pTrack: range)
238       inputTracks.push_back(
239          pTrack->SharedPointer< const WaveTrack >() );
240    // MB: the stop time should not be warped, this was a bug.
241    return std::make_unique<Mixer>(inputTracks,
242                   // Throw, to stop exporting, if read fails:
243                   true,
244                   Mixer::WarpOptions{tracks},
245                   startTime, stopTime,
246                   numOutChannels, outBufferSize, outInterleaved,
247                   outRate, outFormat,
248                   true, mixerSpec);
249 }
250 
InitProgress(std::unique_ptr<ProgressDialog> & pDialog,const TranslatableString & title,const TranslatableString & message)251 void ExportPlugin::InitProgress(std::unique_ptr<ProgressDialog> &pDialog,
252    const TranslatableString &title, const TranslatableString &message)
253 {
254    if (!pDialog)
255       pDialog = std::make_unique<ProgressDialog>( title, message );
256    else {
257       pDialog->SetTitle( title );
258       pDialog->SetMessage( message );
259       pDialog->Reinit();
260    }
261 }
262 
InitProgress(std::unique_ptr<ProgressDialog> & pDialog,const wxFileNameWrapper & title,const TranslatableString & message)263 void ExportPlugin::InitProgress(std::unique_ptr<ProgressDialog> &pDialog,
264    const wxFileNameWrapper &title, const TranslatableString &message)
265 {
266    return InitProgress(
267       pDialog, Verbatim( title.GetName() ), message );
268 }
269 
270 //----------------------------------------------------------------------------
271 // Export
272 //----------------------------------------------------------------------------
273 
274 
275 wxDEFINE_EVENT(AUDACITY_FILE_SUFFIX_EVENT, wxCommandEvent);
276 
277 BEGIN_EVENT_TABLE(Exporter, wxEvtHandler)
278    EVT_FILECTRL_FILTERCHANGED(wxID_ANY, Exporter::OnFilterChanged)
279    EVT_BUTTON(wxID_HELP, Exporter::OnHelp)
280    EVT_COMMAND(wxID_ANY, AUDACITY_FILE_SUFFIX_EVENT, Exporter::OnExtensionChanged)
281 END_EVENT_TABLE()
282 
283 namespace {
284 const auto PathStart = wxT("Exporters");
285 
sRegistry()286 static Registry::GroupItem &sRegistry()
287 {
288    static Registry::TransparentGroupItem<> registry{ PathStart };
289    return registry;
290 }
291 
292 struct ExporterItem final : Registry::SingleItem {
ExporterItem__anon30554e0d0111::ExporterItem293    ExporterItem(
294       const Identifier &id, const Exporter::ExportPluginFactory &factory )
295       : SingleItem{ id }
296       , mFactory{ factory }
297    {}
298 
299    Exporter::ExportPluginFactory mFactory;
300 };
301 
302    using ExportPluginFactories = std::vector< Exporter::ExportPluginFactory >;
sFactories()303    ExportPluginFactories &sFactories()
304    {
305       static ExportPluginFactories theList;
306       return theList;
307    }
308 }
309 
RegisteredExportPlugin(const Identifier & id,const ExportPluginFactory & factory,const Registry::Placement & placement)310 Exporter::RegisteredExportPlugin::RegisteredExportPlugin(
311    const Identifier &id,
312    const ExportPluginFactory &factory,
313    const Registry::Placement &placement )
314 {
315    if ( factory )
316       Registry::RegisterItem( sRegistry(), placement,
317          std::make_unique< ExporterItem >( id, factory ) );
318 }
319 
Exporter(AudacityProject & project)320 Exporter::Exporter( AudacityProject &project )
321 : mProject{ &project }
322 {
323    using namespace Registry;
324    static OrderingPreferenceInitializer init{
325       PathStart,
326       { {wxT(""), wxT("PCM,MP3,OGG,FLAC,MP2,CommandLine,FFmpeg") } },
327    };
328 
329    mMixerSpec = NULL;
330    mBook = NULL;
331 
332    // build the list of export plugins.
333    for ( const auto &factory : sFactories() )
334       mPlugins.emplace_back( factory() );
335 
336    struct MyVisitor final : Visitor {
MyVisitorExporter::Exporter::MyVisitor337       MyVisitor()
338       {
339          // visit the registry to collect the plug-ins properly
340          // sorted
341          TransparentGroupItem<> top{ PathStart };
342          Registry::Visit( *this, &top, &sRegistry() );
343       }
344 
VisitExporter::Exporter::MyVisitor345       void Visit( SingleItem &item, const Path &path ) override
346       {
347          mPlugins.emplace_back(
348             static_cast<ExporterItem&>( item ).mFactory() );
349       }
350 
351       ExportPluginArray mPlugins;
352    } visitor;
353 
354    mPlugins.swap( visitor.mPlugins );
355 
356    SetFileDialogTitle( XO("Export Audio") );
357 }
358 
~Exporter()359 Exporter::~Exporter()
360 {
361 }
362 
OnExtensionChanged(wxCommandEvent & evt)363 void Exporter::OnExtensionChanged(wxCommandEvent &evt)
364 {
365    mDialog->SetFileExtension(evt.GetString().BeforeFirst(' ').Lower());
366 }
367 
OnHelp(wxCommandEvent & WXUNUSED (evt))368 void Exporter::OnHelp(wxCommandEvent& WXUNUSED(evt))
369 {
370    wxWindow * pWin = FindProjectFrame( mProject );
371    HelpSystem::ShowHelp(pWin, L"File_Export_Dialog", true);
372 }
373 
SetFileDialogTitle(const TranslatableString & DialogTitle)374 void Exporter::SetFileDialogTitle( const TranslatableString & DialogTitle )
375 {
376    // The default title is "Export File"
377    mFileDialogTitle = DialogTitle;
378 }
379 
FindFormatIndex(int exportindex)380 int Exporter::FindFormatIndex(int exportindex)
381 {
382    int c = 0;
383    for (const auto &pPlugin : mPlugins)
384    {
385       for (int j = 0; j < pPlugin->GetFormatCount(); j++)
386       {
387          if (exportindex == c) return j;
388          c++;
389       }
390    }
391    return 0;
392 }
393 
GetPlugins()394 const ExportPluginArray &Exporter::GetPlugins()
395 {
396    return mPlugins;
397 }
398 
DoEditMetadata(AudacityProject & project,const TranslatableString & title,const TranslatableString & shortUndoDescription,bool force)399 bool Exporter::DoEditMetadata(AudacityProject &project,
400    const TranslatableString &title,
401    const TranslatableString &shortUndoDescription, bool force)
402 {
403    auto &settings = ProjectSettings::Get( project );
404    auto &tags = Tags::Get( project );
405 
406    // Back up my tags
407    // Tags (artist name, song properties, MP3 ID3 info, etc.)
408    // The structure may be shared with undo history entries
409    // To keep undo working correctly, always replace this with a NEW duplicate
410    // BEFORE doing any editing of it!
411    auto newTags = tags.Duplicate();
412 
413    if (newTags->ShowEditDialog(&GetProjectFrame( project ), title, force)) {
414       if (tags != *newTags) {
415          // Commit the change to project state only now.
416          Tags::Set( project, newTags );
417          ProjectHistory::Get( project ).PushState( title, shortUndoDescription);
418       }
419       bool bShowInFuture;
420       gPrefs->Read(wxT("/AudioFiles/ShowId3Dialog"), &bShowInFuture, true);
421       settings.SetShowId3Dialog( bShowInFuture );
422       return true;
423    }
424 
425    return false;
426 }
427 
Process(bool selectedOnly,double t0,double t1)428 bool Exporter::Process(bool selectedOnly, double t0, double t1)
429 {
430    // Save parms
431    mSelectedOnly = selectedOnly;
432    mT0 = t0;
433    mT1 = t1;
434 
435    // Gather track information
436    if (!ExamineTracks()) {
437       return false;
438    }
439 
440    // Ask user for file name
441    if (!GetFilename()) {
442       return false;
443    }
444 
445    // Check for down mixing
446    if (!CheckMix()) {
447       return false;
448    }
449 
450    // Let user edit MetaData
451    if (mPlugins[mFormat]->GetCanMetaData(mSubFormat)) {
452       if (!DoEditMetadata( *mProject,
453          XO("Edit Metadata Tags"), XO("Exported Tags"),
454          ProjectSettings::Get( *mProject ).GetShowId3Dialog())) {
455          return false;
456       }
457    }
458 
459    // Ensure filename doesn't interfere with project files.
460    if (!CheckFilename()) {
461       return false;
462    }
463 
464    // Export the tracks
465    bool success = ExportTracks();
466 
467    // Get rid of mixerspec
468    mMixerSpec.reset();
469 
470    if (success) {
471       if (mFormatName.empty()) {
472          gPrefs->Write(wxT("/Export/Format"), mPlugins[mFormat]->GetFormat(mSubFormat));
473       }
474 
475       FileNames::UpdateDefaultPath(FileNames::Operation::Export, mFilename.GetPath());
476    }
477 
478    return success;
479 }
480 
Process(unsigned numChannels,const FileExtension & type,const wxString & filename,bool selectedOnly,double t0,double t1)481 bool Exporter::Process(unsigned numChannels,
482                        const FileExtension &type, const wxString & filename,
483                        bool selectedOnly, double t0, double t1)
484 {
485    // Save parms
486    mChannels = numChannels;
487    mFilename = filename;
488    mSelectedOnly = selectedOnly;
489    mT0 = t0;
490    mT1 = t1;
491    mActualName = mFilename;
492 
493    int i = -1;
494    for (const auto &pPlugin : mPlugins) {
495       ++i;
496       for (int j = 0; j < pPlugin->GetFormatCount(); j++)
497       {
498          if (pPlugin->GetFormat(j).IsSameAs(type, false))
499          {
500             mFormat = i;
501             mSubFormat = j;
502             return CheckFilename() && ExportTracks();
503          }
504       }
505    }
506 
507    return false;
508 }
509 
ExamineTracks()510 bool Exporter::ExamineTracks()
511 {
512    // Init
513    mNumSelected = 0;
514    mNumLeft = 0;
515    mNumRight = 0;
516    mNumMono = 0;
517 
518    // First analyze the selected audio, perform sanity checks, and provide
519    // information as appropriate.
520 
521    // Tally how many are right, left, mono, and make sure at
522    // least one track is selected (if selectedOnly==true)
523 
524    double earliestBegin = mT1;
525    double latestEnd = mT0;
526 
527    auto &tracks = TrackList::Get( *mProject );
528 
529    bool anySolo = !(( tracks.Any<const WaveTrack>() + &WaveTrack::GetSolo ).empty());
530 
531    for (auto tr :
532          tracks.Any< const WaveTrack >()
533             + ( mSelectedOnly ? &Track::IsSelected : &Track::Any )
534             - ( anySolo ? &WaveTrack::GetNotSolo : &WaveTrack::GetMute)
535    ) {
536       mNumSelected++;
537 
538       if (tr->GetChannel() == Track::LeftChannel) {
539          mNumLeft++;
540       }
541       else if (tr->GetChannel() == Track::RightChannel) {
542          mNumRight++;
543       }
544       else if (tr->GetChannel() == Track::MonoChannel) {
545          // It's a mono channel, but it may be panned
546          float pan = tr->GetPan();
547 
548          if (pan == -1.0)
549             mNumLeft++;
550          else if (pan == 1.0)
551             mNumRight++;
552          else if (pan == 0)
553             mNumMono++;
554          else {
555             // Panned partially off-center. Mix as stereo.
556             mNumLeft++;
557             mNumRight++;
558          }
559       }
560 
561       if (tr->GetOffset() < earliestBegin) {
562          earliestBegin = tr->GetOffset();
563       }
564 
565       if (tr->GetEndTime() > latestEnd) {
566          latestEnd = tr->GetEndTime();
567       }
568    }
569 
570    if (mNumSelected == 0) {
571       TranslatableString message;
572       if(mSelectedOnly)
573          message = XO("All selected audio is muted.");
574       else
575          message = XO("All audio is muted.");
576       ShowExportErrorDialog(
577          ":576",
578          message, AudacityExportCaptionStr(), false);
579       return false;
580    }
581 
582    // The skipping of silent space could be cleverer and take
583    // into account clips.
584    // As implemented now, it can only skip initial silent space that
585    // has no clip before it, and terminal silent space that has no clip
586    // after it.
587    if (mT0 < earliestBegin){
588       // Bug 1904
589       // Previously we always skipped initial silent space.
590       // Now skipping it is an opt-in option.
591       bool skipSilenceAtBeginning;
592       gPrefs->Read(wxT("/AudioFiles/SkipSilenceAtBeginning"),
593                                       &skipSilenceAtBeginning, false);
594       if (skipSilenceAtBeginning)
595          mT0 = earliestBegin;
596    }
597 
598    // We still skip silent space at the end
599    if (mT1 > latestEnd)
600       mT1 = latestEnd;
601 
602    return true;
603 }
604 
GetFilename()605 bool Exporter::GetFilename()
606 {
607    mFormat = -1;
608 
609    FileNames::FileTypes fileTypes;
610    auto defaultFormat = mFormatName;
611    if( defaultFormat.empty() )
612       defaultFormat = gPrefs->Read(wxT("/Export/Format"),
613                                          wxT("WAV"));
614 
615    mFilterIndex = 0;
616 
617    {
618       int i = -1;
619       for (const auto &pPlugin : mPlugins) {
620          ++i;
621          for (int j = 0; j < pPlugin->GetFormatCount(); j++)
622          {
623             auto mask = pPlugin->GetMask(j);
624             fileTypes.insert( fileTypes.end(), mask.begin(), mask.end() );
625             if (mPlugins[i]->GetFormat(j) == defaultFormat) {
626                mFormat = i;
627                mSubFormat = j;
628             }
629             if (mFormat == -1) mFilterIndex++;
630          }
631       }
632    }
633    if (mFormat == -1)
634    {
635       mFormat = 0;
636       mFilterIndex = 0;
637       mSubFormat = 0;
638    }
639    wxString defext = mPlugins[mFormat]->GetExtension(mSubFormat).Lower();
640 
641    //Bug 1304: Set a default path if none was given.  For Export.
642    mFilename.SetPath(FileNames::FindDefaultPath(FileNames::Operation::Export));
643    mFilename.SetName(mProject->GetProjectName());
644    if (mFilename.GetName().empty())
645       mFilename.SetName(_("untitled"));
646    while (true) {
647       // Must reset each iteration
648       mBook = NULL;
649 
650       {
651          auto useFileName = mFilename;
652          if (!useFileName.HasExt())
653             useFileName.SetExt(defext);
654          FileDialogWrapper fd( ProjectWindow::Find( mProject ),
655                        mFileDialogTitle,
656                        mFilename.GetPath(),
657                        useFileName.GetFullName(),
658                        fileTypes,
659                        wxFD_SAVE | wxRESIZE_BORDER);
660          mDialog = &fd;
661          mDialog->PushEventHandler(this);
662 
663          fd.SetUserPaneCreator(CreateUserPaneCallback, (wxUIntPtr) this);
664          fd.SetFilterIndex(mFilterIndex);
665 
666          int result = fd.ShowModal();
667 
668          mDialog->PopEventHandler();
669 
670          if (result == wxID_CANCEL) {
671             return false;
672          }
673 
674          mFilename = fd.GetPath();
675          if (mFilename == wxT("")) {
676             return false;
677          }
678 
679          mFormat = fd.GetFilterIndex();
680          mFilterIndex = fd.GetFilterIndex();
681       }
682 
683       {
684          int c = 0;
685          int i = -1;
686          for (const auto &pPlugin : mPlugins)
687          {
688             ++i;
689             for (int j = 0; j < pPlugin->GetFormatCount(); j++)
690             {
691                if (mFilterIndex == c)
692                {
693                   mFormat = i;
694                   mSubFormat = j;
695                }
696                c++;
697             }
698          }
699       }
700 
701       const auto ext = mFilename.GetExt();
702       defext = mPlugins[mFormat]->GetExtension(mSubFormat).Lower();
703 
704       //
705       // Check the extension - add the default if it's not there,
706       // and warn user if it's abnormal.
707       //
708       if (ext.empty()) {
709          //
710          // Make sure the user doesn't accidentally save the file
711          // as an extension with no name, like just plain ".wav".
712          //
713          if (mFilename.GetName().Left(1) == wxT(".")) {
714             auto prompt =
715                XO("Are you sure you want to export the file as \"%s\"?\n")
716                   .Format( mFilename.GetFullName() );
717 
718             int action = AudacityMessageBox(
719                prompt,
720                XO("Warning"),
721                wxYES_NO | wxICON_EXCLAMATION);
722             if (action != wxYES) {
723                continue;
724             }
725          }
726 
727          mFilename.SetExt(defext);
728       }
729 
730       if (!mPlugins[mFormat]->CheckFileName(mFilename, mSubFormat))
731       {
732          continue;
733       }
734       else if (!ext.empty() && !mPlugins[mFormat]->IsExtension(ext,mSubFormat) && ext.CmpNoCase(defext)) {
735          auto prompt = XO("You are about to export a %s file with the name \"%s\".\n\nNormally these files end in \".%s\", and some programs will not open files with nonstandard extensions.\n\nAre you sure you want to export the file under this name?")
736                .Format(mPlugins[mFormat]->GetFormat(mSubFormat),
737                        mFilename.GetFullName(),
738                        defext);
739 
740          int action = AudacityMessageBox(
741             prompt,
742             XO("Warning"),
743             wxYES_NO | wxICON_EXCLAMATION);
744          if (action != wxYES) {
745             continue;
746          }
747       }
748 
749       if (mFilename.GetFullPath().length() >= 256) {
750          AudacityMessageBox(
751             XO( "Sorry, pathnames longer than 256 characters not supported.") );
752          continue;
753       }
754 
755 // For Mac, it's handled by the FileDialog
756 #if !defined(__WXMAC__)
757       if (mFilename.FileExists()) {
758          auto prompt = XO("A file named \"%s\" already exists. Replace?")
759             .Format( mFilename.GetFullPath() );
760 
761          int action = AudacityMessageBox(
762             prompt,
763             XO("Warning"),
764             wxYES_NO | wxICON_EXCLAMATION);
765          if (action != wxYES) {
766             continue;
767          }
768       }
769 #endif
770 
771       break;
772    }
773 
774    return true;
775 }
776 
777 //
778 // For safety, if the file already exists it stores the filename
779 // the user wants in actualName, and returns a temporary file name.
780 // The calling function should rename the file when it's successfully
781 // exported.
782 //
CheckFilename()783 bool Exporter::CheckFilename()
784 {
785    //
786    // To be even safer, return a temporary file name based
787    // on this one...
788    //
789 
790    mActualName = mFilename;
791 
792    int suffix = 0;
793    while (mFilename.FileExists()) {
794       mFilename.SetName(mActualName.GetName() +
795                         wxString::Format(wxT("%d"), suffix));
796       suffix++;
797    }
798 
799    return true;
800 }
801 
DisplayOptions(int index)802 void Exporter::DisplayOptions(int index)
803 {
804    int c = 0;
805    int mf = -1, msf = -1;
806    int i = -1;
807    for (const auto &pPlugin : mPlugins)
808    {
809       ++i;
810       for (int j = 0; j < pPlugin->GetFormatCount(); j++)
811       {
812          if (index == c)
813          {
814             mf = i;
815             msf = j;
816          }
817          c++;
818       }
819    }
820    // This shouldn't happen...
821    if (index >= c) {
822       return;
823    }
824 
825 #if defined(__WXMSW__)
826    mPlugins[mf]->DisplayOptions( FindProjectFrame( mProject ), msf);
827 #else
828    mPlugins[mf]->DisplayOptions(mDialog, msf);
829 #endif
830 }
831 
CheckMix(bool prompt)832 bool Exporter::CheckMix(bool prompt /*= true*/ )
833 {
834    // Clean up ... should never happen
835    mMixerSpec.reset();
836 
837    // Determine if exported file will be stereo or mono or multichannel,
838    // and if mixing will occur.
839 
840    auto downMix = ImportExportPrefs::ExportDownMixSetting.ReadEnum();
841    int exportedChannels = mPlugins[mFormat]->SetNumExportChannels();
842 
843    if (downMix) {
844       if (mNumRight > 0 || mNumLeft > 0) {
845          mChannels = 2;
846       }
847       else {
848          mChannels = 1;
849       }
850       mChannels = std::min(mChannels,
851                            mPlugins[mFormat]->GetMaxChannels(mSubFormat));
852 
853       auto numLeft =  mNumLeft + mNumMono;
854       auto numRight = mNumRight + mNumMono;
855 
856       if (numLeft > 1 || numRight > 1 || mNumLeft + mNumRight + mNumMono > mChannels) {
857          wxString exportFormat = mPlugins[mFormat]->GetFormat(mSubFormat);
858          if (exportFormat != wxT("CL") && exportFormat != wxT("FFMPEG") && exportedChannels == -1)
859             exportedChannels = mChannels;
860 
861          if (prompt) {
862             auto pWindow = ProjectWindow::Find(mProject);
863             if (exportedChannels == 1) {
864                if (ShowWarningDialog(pWindow,
865                   wxT("MixMono"),
866                   XO("Your tracks will be mixed down and exported as one mono file."),
867                   true) == wxID_CANCEL)
868                   return false;
869             }
870             else if (exportedChannels == 2) {
871                if (ShowWarningDialog(pWindow,
872                   wxT("MixStereo"),
873                   XO("Your tracks will be mixed down and exported as one stereo file."),
874                   true) == wxID_CANCEL)
875                   return false;
876             }
877             else {
878                if (ShowWarningDialog(pWindow,
879                   wxT("MixUnknownChannels"),
880                   XO("Your tracks will be mixed down to one exported file according to the encoder settings."),
881                   true) == wxID_CANCEL)
882                   return false;
883             }
884          }
885       }
886    }
887    else
888    {
889       if (exportedChannels < 0)
890          exportedChannels = mPlugins[mFormat]->GetMaxChannels(mSubFormat);
891 
892       ExportMixerDialog md(&TrackList::Get( *mProject ),
893                            mSelectedOnly,
894                            exportedChannels,
895                            NULL,
896                            1,
897                            XO("Advanced Mixing Options"));
898       if (prompt) {
899          if (md.ShowModal() != wxID_OK) {
900             return false;
901          }
902       }
903 
904       mMixerSpec = std::make_unique<MixerSpec>(*(md.GetMixerSpec()));
905       mChannels = mMixerSpec->GetNumChannels();
906    }
907 
908    return true;
909 }
910 
ExportTracks()911 bool Exporter::ExportTracks()
912 {
913    // Keep original in case of failure
914    if (mActualName != mFilename) {
915       ::wxRenameFile(mActualName.GetFullPath(), mFilename.GetFullPath());
916    }
917 
918    bool success = false;
919 
920    auto cleanup = finally( [&] {
921       if (mActualName != mFilename) {
922          // Remove backup
923          if ( success )
924             ::wxRemoveFile(mFilename.GetFullPath());
925          else {
926             // Restore original, if needed
927             ::wxRemoveFile(mActualName.GetFullPath());
928             ::wxRenameFile(mFilename.GetFullPath(), mActualName.GetFullPath());
929          }
930          // Restore filename
931          mFilename = mActualName;
932       }
933       else {
934          if ( ! success )
935             // Remove any new, and only partially written, file.
936             ::wxRemoveFile(mFilename.GetFullPath());
937       }
938    } );
939 
940    std::unique_ptr<ProgressDialog> pDialog;
941    auto result = mPlugins[mFormat]->Export(mProject,
942                                        pDialog,
943                                        mChannels,
944                                        mActualName.GetFullPath(),
945                                        mSelectedOnly,
946                                        mT0,
947                                        mT1,
948                                        mMixerSpec.get(),
949                                        NULL,
950                                        mSubFormat);
951 
952    success =
953       result == ProgressResult::Success || result == ProgressResult::Stopped;
954 
955    return success;
956 }
957 
CreateUserPaneCallback(wxWindow * parent,wxUIntPtr userdata)958 void Exporter::CreateUserPaneCallback(wxWindow *parent, wxUIntPtr userdata)
959 {
960    Exporter *self = (Exporter *) userdata;
961    if (self)
962    {
963       self->CreateUserPane(parent);
964    }
965 }
966 
CreateUserPane(wxWindow * parent)967 void Exporter::CreateUserPane(wxWindow *parent)
968 {
969    ShuttleGui S(parent, eIsCreating);
970 
971    S.StartStatic(XO("Format Options"), 1);
972    {
973       S.StartHorizontalLay(wxEXPAND);
974       {
975          mBook = S.Position(wxEXPAND).StartSimplebook();
976          {
977             for (const auto &pPlugin : mPlugins)
978             {
979                for (int j = 0; j < pPlugin->GetFormatCount(); j++)
980                {
981                   // Name of simple book page is not displayed
982                   S.StartNotebookPage( {} );
983                   {
984                      pPlugin->OptionsCreate(S, j);
985                   }
986                   S.EndNotebookPage();
987                }
988             }
989          }
990          S.EndSimplebook();
991 
992          auto b = safenew wxBitmapButton(S.GetParent(), wxID_HELP, theTheme.Bitmap( bmpHelpIcon ));
993          b->SetToolTip( XO("Help").Translation() );
994          b->SetLabel(XO("Help").Translation());       // for screen readers
995          S.Position(wxALIGN_BOTTOM | wxRIGHT | wxBOTTOM).AddWindow(b);
996       }
997       S.EndHorizontalLay();
998    }
999    S.EndStatic();
1000 
1001    return;
1002 }
1003 
OnFilterChanged(wxFileCtrlEvent & evt)1004 void Exporter::OnFilterChanged(wxFileCtrlEvent & evt)
1005 {
1006    int index = evt.GetFilterIndex();
1007 
1008    // On GTK, this event can fire before the userpane is created
1009    if (mBook == NULL || index < 0 || index >= (int) mBook->GetPageCount())
1010    {
1011       return;
1012    }
1013 
1014 #if defined(__WXGTK__)
1015    // On Windows and MacOS, changing the filter in the dialog
1016    // automatically changes the extension of the current file
1017    // name. GTK doesn't, so do it here.
1018    {
1019       FileNames::FileTypes fileTypes;
1020 
1021       int i = -1;
1022       for (const auto &pPlugin : mPlugins)
1023       {
1024          ++i;
1025          for (int j = 0; j < pPlugin->GetFormatCount(); j++)
1026          {
1027             auto mask = pPlugin->GetMask(j);
1028             fileTypes.insert( fileTypes.end(), mask.begin(), mask.end() );
1029          }
1030       }
1031 
1032       if (index < fileTypes.size())
1033       {
1034          mDialog->SetFileExtension(fileTypes[index].extensions[0].Lower());
1035       }
1036    }
1037 #endif
1038 
1039    mBook->ChangeSelection(index);
1040 }
1041 
ProcessFromTimerRecording(bool selectedOnly,double t0,double t1,wxFileName fnFile,int iFormat,int iSubFormat,int iFilterIndex)1042 bool Exporter::ProcessFromTimerRecording(bool selectedOnly,
1043                                          double t0,
1044                                          double t1,
1045                                          wxFileName fnFile,
1046                                          int iFormat,
1047                                          int iSubFormat,
1048                                          int iFilterIndex)
1049 {
1050    // Save parms
1051    mSelectedOnly = selectedOnly;
1052    mT0 = t0;
1053    mT1 = t1;
1054 
1055    // Auto Export Parameters
1056    mFilename = fnFile;
1057    mFormat = iFormat;
1058    mSubFormat = iSubFormat;
1059    mFilterIndex = iFilterIndex;
1060 
1061    // Gather track information
1062    if (!ExamineTracks()) {
1063       return false;
1064    }
1065 
1066    // Check for down mixing
1067    if (!CheckMix(false)) {
1068       return false;
1069    }
1070 
1071    // Ensure filename doesn't interfere with project files.
1072    if (!CheckFilename()) {
1073       return false;
1074    }
1075 
1076    // Export the tracks
1077    bool success = ExportTracks();
1078 
1079    // Get rid of mixerspec
1080    mMixerSpec.reset();
1081 
1082    return success;
1083 }
1084 
GetAutoExportFormat()1085 int Exporter::GetAutoExportFormat() {
1086    return mFormat;
1087 }
1088 
GetAutoExportSubFormat()1089 int Exporter::GetAutoExportSubFormat() {
1090    return mSubFormat;
1091 }
1092 
GetAutoExportFilterIndex()1093 int Exporter::GetAutoExportFilterIndex() {
1094    return mFormat;
1095 }
1096 
GetAutoExportFileName()1097 wxFileName Exporter::GetAutoExportFileName() {
1098    return mFilename;
1099 }
1100 
SetAutoExportOptions()1101 bool Exporter::SetAutoExportOptions() {
1102    mFormat = -1;
1103 
1104    if( GetFilename()==false )
1105         return false;
1106 
1107    // Let user edit MetaData
1108    if (mPlugins[mFormat]->GetCanMetaData(mSubFormat)) {
1109       if (!DoEditMetadata( *mProject,
1110          XO("Edit Metadata Tags"),
1111          XO("Exported Tags"),
1112          ProjectSettings::Get(*mProject).GetShowId3Dialog())) {
1113          return false;
1114       }
1115    }
1116 
1117    return true;
1118 }
1119 
1120 //----------------------------------------------------------------------------
1121 // ExportMixerPanel
1122 //----------------------------------------------------------------------------
1123 
BEGIN_EVENT_TABLE(ExportMixerPanel,wxPanelWrapper)1124 BEGIN_EVENT_TABLE(ExportMixerPanel, wxPanelWrapper)
1125     EVT_PAINT(ExportMixerPanel::OnPaint)
1126     EVT_MOUSE_EVENTS(ExportMixerPanel::OnMouseEvent)
1127 END_EVENT_TABLE()
1128 
1129 ExportMixerPanel::ExportMixerPanel( wxWindow *parent, wxWindowID id,
1130       MixerSpec *mixerSpec,
1131       wxArrayString trackNames,
1132       const wxPoint& pos, const wxSize& size):
1133    wxPanelWrapper(parent, id, pos, size)
1134    , mMixerSpec{mixerSpec}
1135    , mChannelRects{ mMixerSpec->GetMaxNumChannels() }
1136    , mTrackRects{ mMixerSpec->GetNumTracks() }
1137 {
1138    mBitmap = NULL;
1139    mWidth = 0;
1140    mHeight = 0;
1141    mSelectedTrack = mSelectedChannel = -1;
1142 
1143    mTrackNames = trackNames;
1144 }
1145 
~ExportMixerPanel()1146 ExportMixerPanel::~ExportMixerPanel()
1147 {
1148 }
1149 
1150 //set the font on memDC such that text can fit in specified width and height
SetFont(wxMemoryDC & memDC,const wxString & text,int width,int height)1151 void ExportMixerPanel::SetFont(wxMemoryDC &memDC, const wxString &text, int width,
1152       int height )
1153 {
1154    int l = 0, u = 13, m, w, h;
1155    wxFont font = memDC.GetFont();
1156    while( l < u - 1 )
1157    {
1158       m = ( l + u ) / 2;
1159       font.SetPointSize( m );
1160       memDC.SetFont( font );
1161       memDC.GetTextExtent( text, &w, &h );
1162 
1163       if( w < width && h < height )
1164          l = m;
1165       else
1166          u = m;
1167    }
1168    font.SetPointSize( l );
1169    memDC.SetFont( font );
1170 }
1171 
OnPaint(wxPaintEvent & WXUNUSED (event))1172 void ExportMixerPanel::OnPaint(wxPaintEvent & WXUNUSED(event))
1173 {
1174    wxPaintDC dc( this );
1175 
1176    int width, height;
1177    GetSize( &width, &height );
1178 
1179    if( !mBitmap || mWidth != width || mHeight != height )
1180    {
1181       mWidth = width;
1182       mHeight = height;
1183       mBitmap = std::make_unique<wxBitmap>( mWidth, mHeight,24 );
1184    }
1185 
1186    wxColour bkgnd = GetBackgroundColour();
1187    wxBrush bkgndBrush( bkgnd, wxBRUSHSTYLE_SOLID );
1188 
1189    wxMemoryDC memDC;
1190    memDC.SelectObject( *mBitmap );
1191 
1192    //draw background
1193    wxRect bkgndRect;
1194    bkgndRect.x = 0;
1195    bkgndRect.y = 0;
1196    bkgndRect.width = mWidth;
1197    bkgndRect.height = mHeight;
1198 
1199    memDC.SetBrush( *wxWHITE_BRUSH );
1200    memDC.SetPen( *wxBLACK_PEN );
1201    memDC.DrawRectangle( bkgndRect );
1202 
1203    //box dimensions
1204    mBoxWidth = mWidth / 6;
1205 
1206    mTrackHeight = ( mHeight * 3 ) / ( mMixerSpec->GetNumTracks() * 4 );
1207    if( mTrackHeight > 30 )
1208       mTrackHeight = 30;
1209 
1210    mChannelHeight = ( mHeight * 3 ) / ( mMixerSpec->GetNumChannels() * 4 );
1211    if( mChannelHeight > 30 )
1212       mChannelHeight = 30;
1213 
1214    static double PI = 2 * acos( 0.0 );
1215    double angle = atan( ( 3.0 * mHeight ) / mWidth );
1216    double radius = mHeight / ( 2.0 * sin( PI - 2.0 * angle ) );
1217    double totAngle = ( asin( mHeight / ( 2.0 * radius ) ) * 2.0 );
1218 
1219    //draw tracks
1220    memDC.SetBrush( AColor::envelopeBrush );
1221    angle = totAngle / ( mMixerSpec->GetNumTracks() + 1 );
1222 
1223    int max = 0, w, h;
1224    for( unsigned int i = 1; i < mMixerSpec->GetNumTracks(); i++ )
1225       if( mTrackNames[ i ].length() > mTrackNames[ max ].length() )
1226          max = i;
1227 
1228    SetFont( memDC, mTrackNames[ max ], mBoxWidth, mTrackHeight );
1229 
1230    for( unsigned int i = 0; i < mMixerSpec->GetNumTracks(); i++ )
1231    {
1232       mTrackRects[ i ].x = (int)( mBoxWidth * 2 + radius - radius *
1233          cos( totAngle / 2.0 - angle * ( i + 1 ) ) - mBoxWidth + 0.5 );
1234       mTrackRects[ i ].y = (int)( mHeight * 0.5 - radius *
1235             sin( totAngle * 0.5 - angle * ( i + 1.0 ) ) -
1236             0.5 * mTrackHeight + 0.5 );
1237 
1238       mTrackRects[ i ].width = mBoxWidth;
1239       mTrackRects[ i ].height = mTrackHeight;
1240 
1241       memDC.SetPen( mSelectedTrack == (int)i ? *wxRED_PEN : *wxBLACK_PEN );
1242       memDC.DrawRectangle( mTrackRects[ i ] );
1243 
1244       memDC.GetTextExtent( mTrackNames[ i ], &w, &h );
1245       memDC.DrawText( mTrackNames[ i ],
1246             mTrackRects[ i ].x + ( mBoxWidth - w ) / 2,
1247             mTrackRects[ i ].y + ( mTrackHeight - h ) / 2 );
1248    }
1249 
1250    //draw channels
1251    memDC.SetBrush( AColor::playRegionBrush[ 0 ] );
1252    angle = ( asin( mHeight / ( 2.0 * radius ) ) * 2.0 ) /
1253       ( mMixerSpec->GetNumChannels() + 1 );
1254 
1255    SetFont( memDC, wxT( "Channel: XX" ), mBoxWidth, mChannelHeight );
1256    memDC.GetTextExtent( wxT( "Channel: XX" ), &w, &h );
1257 
1258    for( unsigned int i = 0; i < mMixerSpec->GetNumChannels(); i++ )
1259    {
1260       mChannelRects[ i ].x = (int)( mBoxWidth * 4 - radius  + radius *
1261          cos( totAngle * 0.5 - angle * ( i + 1 ) ) + 0.5 );
1262       mChannelRects[ i ].y = (int)( mHeight * 0.5 - radius *
1263             sin( totAngle * 0.5 - angle * ( i + 1 ) ) -
1264             0.5 * mChannelHeight + 0.5 );
1265 
1266       mChannelRects[ i ].width = mBoxWidth;
1267       mChannelRects[ i ].height = mChannelHeight;
1268 
1269       memDC.SetPen( mSelectedChannel == (int)i ? *wxRED_PEN : *wxBLACK_PEN );
1270       memDC.DrawRectangle( mChannelRects[ i ] );
1271 
1272       memDC.DrawText( wxString::Format( _( "Channel: %2d" ), i + 1 ),
1273             mChannelRects[ i ].x + ( mBoxWidth - w ) / 2,
1274             mChannelRects[ i ].y + ( mChannelHeight - h ) / 2 );
1275    }
1276 
1277    //draw links
1278    memDC.SetPen( wxPen( *wxBLACK, mHeight / 200 ) );
1279    for( unsigned int i = 0; i < mMixerSpec->GetNumTracks(); i++ )
1280       for( unsigned int j = 0; j < mMixerSpec->GetNumChannels(); j++ )
1281          if( mMixerSpec->mMap[ i ][ j ] )
1282             AColor::Line(memDC, mTrackRects[ i ].x + mBoxWidth,
1283                   mTrackRects[ i ].y + mTrackHeight / 2, mChannelRects[ j ].x,
1284                   mChannelRects[ j ].y + mChannelHeight / 2 );
1285 
1286    dc.Blit( 0, 0, mWidth, mHeight, &memDC, 0, 0, wxCOPY, FALSE );
1287 }
1288 
Distance(wxPoint & a,wxPoint & b)1289 double ExportMixerPanel::Distance( wxPoint &a, wxPoint &b )
1290 {
1291    return sqrt( pow( a.x - b.x, 2.0 ) + pow( a.y - b.y, 2.0 ) );
1292 }
1293 
1294 //checks if p is on the line connecting la, lb with tolerance
IsOnLine(wxPoint p,wxPoint la,wxPoint lb)1295 bool ExportMixerPanel::IsOnLine( wxPoint p, wxPoint la, wxPoint lb )
1296 {
1297    return Distance( p, la ) + Distance( p, lb ) - Distance( la, lb ) < 0.1;
1298 }
1299 
OnMouseEvent(wxMouseEvent & event)1300 void ExportMixerPanel::OnMouseEvent(wxMouseEvent & event)
1301 {
1302    if( event.ButtonDown() )
1303    {
1304       bool reset = true;
1305       //check tracks
1306       for( unsigned int i = 0; i < mMixerSpec->GetNumTracks(); i++ )
1307          if( mTrackRects[ i ].Contains( event.m_x, event.m_y ) )
1308          {
1309             reset = false;
1310             if( mSelectedTrack == (int)i )
1311                mSelectedTrack = -1;
1312             else
1313             {
1314                mSelectedTrack = i;
1315                if( mSelectedChannel != -1 )
1316                   mMixerSpec->mMap[ mSelectedTrack ][ mSelectedChannel ] =
1317                      !mMixerSpec->mMap[ mSelectedTrack ][ mSelectedChannel ];
1318             }
1319             goto found;
1320          }
1321 
1322       //check channels
1323       for( unsigned int i = 0; i < mMixerSpec->GetNumChannels(); i++ )
1324          if( mChannelRects[ i ].Contains( event.m_x, event.m_y ) )
1325          {
1326             reset = false;
1327             if( mSelectedChannel == (int)i )
1328                mSelectedChannel = -1;
1329             else
1330             {
1331                mSelectedChannel = i;
1332                if( mSelectedTrack != -1 )
1333                   mMixerSpec->mMap[ mSelectedTrack ][ mSelectedChannel ] =
1334                      !mMixerSpec->mMap[ mSelectedTrack ][ mSelectedChannel ];
1335             }
1336             goto found;
1337          }
1338 
1339       //check links
1340       for( unsigned int i = 0; i < mMixerSpec->GetNumTracks(); i++ )
1341          for( unsigned int j = 0; j < mMixerSpec->GetNumChannels(); j++ )
1342             if( mMixerSpec->mMap[ i ][ j ]  && IsOnLine( wxPoint( event.m_x,
1343                         event.m_y ), wxPoint( mTrackRects[ i ].x + mBoxWidth,
1344                            mTrackRects[ i ].y + mTrackHeight / 2 ),
1345                      wxPoint( mChannelRects[ j ].x, mChannelRects[ j ].y +
1346                      mChannelHeight / 2 ) ) )
1347                mMixerSpec->mMap[ i ][ j ] = false;
1348 
1349 found:
1350       if( reset )
1351          mSelectedTrack = mSelectedChannel = -1;
1352       Refresh( false );
1353    }
1354 }
1355 
1356 //----------------------------------------------------------------------------
1357 // ExportMixerDialog
1358 //----------------------------------------------------------------------------
1359 
1360 enum
1361 {
1362    ID_MIXERPANEL = 10001,
1363    ID_SLIDER_CHANNEL
1364 };
1365 
BEGIN_EVENT_TABLE(ExportMixerDialog,wxDialogWrapper)1366 BEGIN_EVENT_TABLE( ExportMixerDialog, wxDialogWrapper )
1367    EVT_BUTTON( wxID_OK, ExportMixerDialog::OnOk )
1368    EVT_BUTTON( wxID_CANCEL, ExportMixerDialog::OnCancel )
1369    EVT_BUTTON( wxID_HELP, ExportMixerDialog::OnMixerPanelHelp )
1370    EVT_SIZE( ExportMixerDialog::OnSize )
1371    EVT_SLIDER( ID_SLIDER_CHANNEL, ExportMixerDialog::OnSlider )
1372 END_EVENT_TABLE()
1373 
1374 ExportMixerDialog::ExportMixerDialog( const TrackList *tracks, bool selectedOnly,
1375       unsigned maxNumChannels, wxWindow *parent, wxWindowID id, const TranslatableString &title,
1376       const wxPoint &position, const wxSize& size, long style ) :
1377    wxDialogWrapper( parent, id, title, position, size, style | wxRESIZE_BORDER )
1378 {
1379    SetName();
1380 
1381    unsigned numTracks = 0;
1382 
1383    bool anySolo = !(( tracks->Any<const WaveTrack>() + &WaveTrack::GetSolo ).empty());
1384 
1385    for (auto t :
1386          tracks->Any< const WaveTrack >()
1387             + ( selectedOnly ? &Track::IsSelected : &Track::Any  )
1388             - ( anySolo ? &WaveTrack::GetNotSolo :  &WaveTrack::GetMute)
1389    ) {
1390       numTracks++;
1391       const wxString sTrackName = (t->GetName()).Left(20);
1392       if( t->GetChannel() == Track::LeftChannel )
1393       /* i18n-hint: track name and L abbreviating Left channel */
1394          mTrackNames.push_back( wxString::Format( _( "%s - L" ), sTrackName ) );
1395       else if( t->GetChannel() == Track::RightChannel )
1396       /* i18n-hint: track name and R abbreviating Right channel */
1397          mTrackNames.push_back( wxString::Format( _( "%s - R" ), sTrackName ) );
1398       else
1399          mTrackNames.push_back(sTrackName);
1400    }
1401 
1402    // JKC: This is an attempt to fix a 'watching brief' issue, where the slider is
1403    // sometimes not slidable.  My suspicion is that a mixer may incorrectly
1404    // state the number of channels - so we assume there are always at least two.
1405    // The downside is that if someone is exporting to a mono device, the dialog
1406    // will allow them to output to two channels. Hmm.  We may need to revisit this.
1407 
1408    if (maxNumChannels < 2 )
1409       // STF (April 2016): AMR (narrowband) and MP3 may export 1 channel.
1410       // maxNumChannels = 2;
1411       maxNumChannels = 1;
1412    if (maxNumChannels > 32)
1413       maxNumChannels = 32;
1414 
1415    mMixerSpec = std::make_unique<MixerSpec>(numTracks, maxNumChannels);
1416 
1417    auto label = XO("Output Channels: %2d")
1418       .Format( mMixerSpec->GetNumChannels() );
1419 
1420    ShuttleGui S{ this, eIsCreating };
1421    {
1422       S.SetBorder( 5 );
1423 
1424       auto mixerPanel = safenew ExportMixerPanel(
1425          S.GetParent(), ID_MIXERPANEL, mMixerSpec.get(),
1426          mTrackNames, wxDefaultPosition, wxSize(400, -1));
1427       S.Prop(1)
1428          .Name(XO("Mixer Panel"))
1429          .Position(wxEXPAND | wxALL)
1430          .AddWindow(mixerPanel);
1431 
1432       S.StartHorizontalLay(wxALIGN_CENTER | wxALL, 0);
1433       {
1434          mChannelsText = S.AddVariableText(
1435             label,
1436             false, wxALIGN_LEFT | wxALL );
1437 
1438          S
1439             .Id(ID_SLIDER_CHANNEL)
1440             .Name(label)
1441             .Size({300, -1})
1442             .Style(wxSL_HORIZONTAL)
1443             .Position(wxEXPAND | wxALL)
1444             .AddSlider( {},
1445                mMixerSpec->GetNumChannels(),
1446                mMixerSpec->GetMaxNumChannels(), 1 );
1447       }
1448       S.EndHorizontalLay();
1449 
1450       S.AddStandardButtons( eCancelButton | eOkButton | eHelpButton );
1451    }
1452 
1453    SetAutoLayout(true);
1454    GetSizer()->Fit( this );
1455    GetSizer()->SetSizeHints( this );
1456 
1457    SetSizeHints( 640, 480, 20000, 20000 );
1458 
1459    SetSize( 640, 480 );
1460    Center();
1461 }
1462 
~ExportMixerDialog()1463 ExportMixerDialog::~ExportMixerDialog()
1464 {
1465 }
1466 
OnSize(wxSizeEvent & event)1467 void ExportMixerDialog::OnSize(wxSizeEvent &event)
1468 {
1469    ExportMixerPanel *pnl = ( ( ExportMixerPanel* ) FindWindow( ID_MIXERPANEL ) );
1470    pnl->Refresh( false );
1471    event.Skip();
1472 }
1473 
OnSlider(wxCommandEvent & WXUNUSED (event))1474 void ExportMixerDialog::OnSlider( wxCommandEvent & WXUNUSED(event))
1475 {
1476    wxSlider *channels = ( wxSlider* )FindWindow( ID_SLIDER_CHANNEL );
1477    ExportMixerPanel *pnl = ( ( ExportMixerPanel* ) FindWindow( ID_MIXERPANEL ) );
1478    mMixerSpec->SetNumChannels( channels->GetValue() );
1479    pnl->Refresh( false );
1480    wxString label;
1481    label.Printf( _( "Output Channels: %2d" ), mMixerSpec->GetNumChannels() );
1482    mChannelsText->SetLabel( label );
1483    channels->SetName( label );
1484 }
1485 
OnOk(wxCommandEvent & WXUNUSED (event))1486 void ExportMixerDialog::OnOk(wxCommandEvent & WXUNUSED(event))
1487 {
1488    EndModal( wxID_OK );
1489 }
1490 
OnCancel(wxCommandEvent & WXUNUSED (event))1491 void ExportMixerDialog::OnCancel(wxCommandEvent & WXUNUSED(event))
1492 {
1493    EndModal( wxID_CANCEL );
1494 }
1495 
OnMixerPanelHelp(wxCommandEvent & WXUNUSED (event))1496 void ExportMixerDialog::OnMixerPanelHelp(wxCommandEvent & WXUNUSED(event))
1497 {
1498    HelpSystem::ShowHelp(this, L"Advanced_Mixing_Options", true);
1499 }
1500 
1501 
AudacityExportCaptionStr()1502 TranslatableString AudacityExportCaptionStr()
1503 {
1504    return XO("Warning");
1505 }
AudacityExportMessageStr()1506 TranslatableString AudacityExportMessageStr()
1507 {
1508    return XO("Unable to export.\nError %s");
1509 }
1510 
1511 
1512 // This creates a generic export error dialog
1513 // Untranslated ErrorCodes like "MP3:1882" are used since we don't yet have
1514 // a good user facing error message.  They allow us to
1515 // distinguish where the error occurred, and we can update the landing
1516 // page as we learn more about when (if ever) these errors actually happen.
1517 // The number happens to at one time have been a line number, but all
1518 // we need from them is that they be distinct.
ShowExportErrorDialog(wxString ErrorCode,TranslatableString message,const TranslatableString & caption,bool allowReporting)1519 void ShowExportErrorDialog(wxString ErrorCode,
1520    TranslatableString message,
1521    const TranslatableString& caption,
1522    bool allowReporting)
1523 {
1524    using namespace BasicUI;
1525    ShowErrorDialog( {},
1526       caption,
1527       message.Format( ErrorCode ),
1528       "Error:_Unable_to_export", // URL.
1529       ErrorDialogOptions { allowReporting ? ErrorDialogType::ModalErrorReport : ErrorDialogType::ModalError });
1530 }
1531 
ShowDiskFullExportErrorDialog(const wxFileNameWrapper & fileName)1532 void ShowDiskFullExportErrorDialog(const wxFileNameWrapper &fileName)
1533 {
1534    BasicUI::ShowErrorDialog( {},
1535       XO("Warning"),
1536       FileException::WriteFailureMessage(fileName),
1537       "Error:_Disk_full_or_not_writable"
1538    );
1539 }
1540 
1541 
1542