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