1 /**********************************************************************
2
3 Audacity: A Digital Audio Editor
4
5 ExportCL.cpp
6
7 Joshua Haberman
8
9 This code allows Audacity to export data by piping it to an external
10 program.
11
12 **********************************************************************/
13
14 #include "ProjectRate.h"
15
16 #include <wx/app.h>
17 #include <wx/button.h>
18 #include <wx/cmdline.h>
19 #include <wx/combobox.h>
20 #include <wx/log.h>
21 #include <wx/process.h>
22 #include <wx/sizer.h>
23 #include <wx/textctrl.h>
24 #if defined(__WXMSW__)
25 #include <wx/msw/registry.h> // for wxRegKey
26 #endif
27
28 #include "FileNames.h"
29 #include "Export.h"
30
31 #include "../Mix.h"
32 #include "Prefs.h"
33 #include "../SelectFile.h"
34 #include "../ShuttleGui.h"
35 #include "../Tags.h"
36 #include "../Track.h"
37 #include "float_cast.h"
38 #include "../widgets/FileHistory.h"
39 #include "../widgets/AudacityMessageBox.h"
40 #include "../widgets/ProgressDialog.h"
41 #include "../widgets/Warning.h"
42 #include "wxFileNameWrapper.h"
43
44 #ifdef USE_LIBID3TAG
45 #include <id3tag.h>
46 extern "C" {
47 struct id3_frame *id3_frame_new(char const *);
48 }
49 #endif
50
51 //----------------------------------------------------------------------------
52 // ExportCLOptions
53 //----------------------------------------------------------------------------
54
55 class ExportCLOptions final : public wxPanelWrapper
56 {
57 public:
58 ExportCLOptions(wxWindow *parent, int format);
59 virtual ~ExportCLOptions();
60
61 void PopulateOrExchange(ShuttleGui & S);
62 bool TransferDataToWindow() override;
63 bool TransferDataFromWindow() override;
64
65 void OnBrowse(wxCommandEvent & event);
66
67 private:
68 wxComboBox *mCmd;
69 FileHistory mHistory;
70
71 DECLARE_EVENT_TABLE()
72 };
73
74 #define ID_BROWSE 5000
75
BEGIN_EVENT_TABLE(ExportCLOptions,wxPanelWrapper)76 BEGIN_EVENT_TABLE(ExportCLOptions, wxPanelWrapper)
77 EVT_BUTTON(ID_BROWSE, ExportCLOptions::OnBrowse)
78 END_EVENT_TABLE()
79
80 ///
81 ///
82 ExportCLOptions::ExportCLOptions(wxWindow *parent, int WXUNUSED(format))
83 : wxPanelWrapper(parent, wxID_ANY)
84 {
85 mHistory.Load(*gPrefs, wxT("/FileFormats/ExternalProgramHistory"));
86
87 if (mHistory.empty()) {
88 mHistory.Append(wxT("ffmpeg -i - \"%f.opus\""));
89 mHistory.Append(wxT("ffmpeg -i - \"%f.wav\""));
90 mHistory.Append(wxT("ffmpeg -i - \"%f\""));
91 mHistory.Append(wxT("lame - \"%f\""));
92 }
93
94 mHistory.Append(gPrefs->Read(wxT("/FileFormats/ExternalProgramExportCommand"),
95 mHistory[ 0 ]));
96
97 ShuttleGui S(this, eIsCreatingFromPrefs);
98 PopulateOrExchange(S);
99
100 TransferDataToWindow();
101
102 parent->Layout();
103 }
104
~ExportCLOptions()105 ExportCLOptions::~ExportCLOptions()
106 {
107 TransferDataFromWindow();
108 }
109
110 ///
111 ///
PopulateOrExchange(ShuttleGui & S)112 void ExportCLOptions::PopulateOrExchange(ShuttleGui & S)
113 {
114 wxArrayStringEx cmds( mHistory.begin(), mHistory.end() );
115 auto cmd = cmds[0];
116
117 S.StartVerticalLay();
118 {
119 S.StartHorizontalLay(wxEXPAND);
120 {
121 S.SetSizerProportion(1);
122 S.StartMultiColumn(3, wxEXPAND);
123 {
124 S.SetStretchyCol(1);
125 mCmd = S.AddCombo(XXO("Command:"),
126 cmd,
127 cmds);
128 S.Id(ID_BROWSE).AddButton(XXO("Browse..."),
129 wxALIGN_CENTER_VERTICAL);
130 S.AddFixedText( {} );
131 S.TieCheckBox(XXO("Show output"),
132 {wxT("/FileFormats/ExternalProgramShowOutput"),
133 false});
134 }
135 S.EndMultiColumn();
136 }
137 S.EndHorizontalLay();
138
139 S.AddTitle(XO(
140 /* i18n-hint: Some programmer-oriented terminology here:
141 "Data" refers to the sound to be exported, "piped" means sent,
142 and "standard in" means the default input stream that the external program,
143 named by %f, will read. And yes, it's %f, not %s -- this isn't actually used
144 in the program as a format string. Keep %f unchanged. */
145 "Data will be piped to standard in. \"%f\" uses the file name in the export window."), 250);
146 }
147 S.EndVerticalLay();
148 }
149
150 ///
151 ///
TransferDataToWindow()152 bool ExportCLOptions::TransferDataToWindow()
153 {
154 return true;
155 }
156
157 ///
158 ///
TransferDataFromWindow()159 bool ExportCLOptions::TransferDataFromWindow()
160 {
161 ShuttleGui S(this, eIsSavingToPrefs);
162 PopulateOrExchange(S);
163
164 wxString cmd = mCmd->GetValue();
165
166 mHistory.Append(cmd);
167 mHistory.Save(*gPrefs);
168
169 gPrefs->Write(wxT("/FileFormats/ExternalProgramExportCommand"), cmd);
170 gPrefs->Flush();
171
172 return true;
173 }
174
175 ///
176 ///
OnBrowse(wxCommandEvent & WXUNUSED (event))177 void ExportCLOptions::OnBrowse(wxCommandEvent& WXUNUSED(event))
178 {
179 wxString path;
180 FileExtension ext;
181 FileNames::FileType type = FileNames::AllFiles;
182
183 #if defined(__WXMSW__)
184 ext = wxT("exe");
185 /* i18n-hint files that can be run as programs */
186 type = { XO("Executables"), { ext } };
187 #endif
188
189 path = SelectFile(FileNames::Operation::Open,
190 XO("Find path to command"),
191 wxEmptyString,
192 wxEmptyString,
193 ext,
194 { type },
195 wxFD_OPEN | wxRESIZE_BORDER,
196 this);
197 if (path.empty()) {
198 return;
199 }
200
201 if (path.Find(wxT(' ')) == wxNOT_FOUND) {
202 mCmd->SetValue(path);
203 }
204 else {
205 mCmd->SetValue(wxT('"') + path + wxT('"'));
206 }
207
208 mCmd->SetInsertionPointEnd();
209
210 return;
211 }
212
213 //----------------------------------------------------------------------------
214 // ExportCLProcess
215 //----------------------------------------------------------------------------
216
Drain(wxInputStream * s,wxString * o)217 static void Drain(wxInputStream *s, wxString *o)
218 {
219 while (s->CanRead()) {
220 char buffer[4096];
221
222 s->Read(buffer, WXSIZEOF(buffer) - 1);
223 buffer[s->LastRead()] = wxT('\0');
224 *o += LAT1CTOWX(buffer);
225 }
226 }
227
228 class ExportCLProcess final : public wxProcess
229 {
230 public:
ExportCLProcess(wxString * output)231 ExportCLProcess(wxString *output)
232 {
233 #if defined(__WXMAC__)
234 // Don't want to crash on broken pipe
235 signal(SIGPIPE, SIG_IGN);
236 #endif
237
238 mOutput = output;
239 mActive = true;
240 mStatus = -555;
241 Redirect();
242 }
243
IsActive()244 bool IsActive()
245 {
246 return mActive;
247 }
248
OnTerminate(int WXUNUSED (pid),int status)249 void OnTerminate(int WXUNUSED( pid ), int status)
250 {
251 Drain(GetInputStream(), mOutput);
252 Drain(GetErrorStream(), mOutput);
253
254 mStatus = status;
255 mActive = false;
256 }
257
GetStatus()258 int GetStatus()
259 {
260 return mStatus;
261 }
262
263 private:
264 wxString *mOutput;
265 bool mActive;
266 int mStatus;
267 };
268
269 //----------------------------------------------------------------------------
270 // ExportCL
271 //----------------------------------------------------------------------------
272
273 class ExportCL final : public ExportPlugin
274 {
275 public:
276
277 ExportCL();
278
279 // Required
280 void OptionsCreate(ShuttleGui &S, int format) override;
281
282 ProgressResult Export(AudacityProject *project,
283 std::unique_ptr<ProgressDialog> &pDialog,
284 unsigned channels,
285 const wxFileNameWrapper &fName,
286 bool selectedOnly,
287 double t0,
288 double t1,
289 MixerSpec *mixerSpec = NULL,
290 const Tags *metadata = NULL,
291 int subformat = 0) override;
292
293 // Optional
294 bool CheckFileName(wxFileName &filename, int format = 0) override;
295
296 private:
297 void GetSettings();
298
299 std::vector<char> GetMetaChunk(const Tags *metadata);
300 wxString mCmd;
301 bool mShow;
302
303 struct ExtendPath
304 {
305 #if defined(__WXMSW__)
306 wxString opath;
307
ExtendPathExportCL::ExtendPath308 ExtendPath()
309 {
310 // Give Windows a chance at finding lame command in the default location.
311 wxString paths[] = {wxT("HKEY_LOCAL_MACHINE\\Software\\Lame for Audacity"),
312 wxT("HKEY_LOCAL_MACHINE\\Software\\FFmpeg for Audacity")};
313 wxString npath;
314 wxRegKey reg;
315
316 wxGetEnv(wxT("PATH"), &opath);
317 npath = opath;
318
319 for (int i = 0; i < WXSIZEOF(paths); i++) {
320 reg.SetName(paths[i]);
321
322 if (reg.Exists()) {
323 wxString ipath;
324 reg.QueryValue(wxT("InstallPath"), ipath);
325 if (!ipath.empty()) {
326 npath += wxPATH_SEP + ipath;
327 }
328 }
329 }
330
331 wxSetEnv(wxT("PATH"),npath);
332 };
333
~ExtendPathExportCL::ExtendPath334 ~ExtendPath()
335 {
336 if (!opath.empty())
337 {
338 wxSetEnv(wxT("PATH"),opath);
339 }
340 }
341 #endif
342 };
343 };
344
ExportCL()345 ExportCL::ExportCL()
346 : ExportPlugin()
347 {
348 AddFormat();
349 SetFormat(wxT("CL"),0);
350 AddExtension(wxT(""),0);
351 SetMaxChannels(255,0);
352 SetCanMetaData(false,0);
353 SetDescription(XO("(external program)"),0);
354 }
355
Export(AudacityProject * project,std::unique_ptr<ProgressDialog> & pDialog,unsigned channels,const wxFileNameWrapper & fName,bool selectionOnly,double t0,double t1,MixerSpec * mixerSpec,const Tags * metadata,int WXUNUSED (subformat))356 ProgressResult ExportCL::Export(AudacityProject *project,
357 std::unique_ptr<ProgressDialog> &pDialog,
358 unsigned channels,
359 const wxFileNameWrapper &fName,
360 bool selectionOnly,
361 double t0,
362 double t1,
363 MixerSpec *mixerSpec,
364 const Tags *metadata,
365 int WXUNUSED(subformat))
366 {
367 ExtendPath ep;
368 wxString output;
369 long rc;
370
371 const auto path = fName.GetFullPath();
372
373 GetSettings();
374
375 // Bug 2178 - users who don't know what they are doing will
376 // now get a file extension of .wav appended to their ffmpeg filename
377 // and therefore ffmpeg will be able to choose a file type.
378 if( mCmd == wxT("ffmpeg -i - \"%f\"") && !fName.HasExt())
379 mCmd.Replace( "%f", "%f.wav" );
380 mCmd.Replace(wxT("%f"), path);
381
382 // Kick off the command
383 ExportCLProcess process(&output);
384
385 rc = wxExecute(mCmd, wxEXEC_ASYNC, &process);
386 if (!rc) {
387 AudacityMessageBox( XO("Cannot export audio to %s").Format( path ) );
388 process.Detach();
389 process.CloseOutput();
390
391 return ProgressResult::Cancelled;
392 }
393
394 // Turn off logging to prevent broken pipe messages
395 wxLogNull nolog;
396
397 // establish parameters
398 int rate = lrint( ProjectRate::Get( *project ).GetRate());
399 const size_t maxBlockLen = 44100 * 5;
400 unsigned long totalSamples = lrint((t1 - t0) * rate);
401 unsigned long sampleBytes = totalSamples * channels * SAMPLE_SIZE(floatSample);
402
403 wxOutputStream *os = process.GetOutputStream();
404
405 // RIFF header
406 struct {
407 char riffID[4]; // "RIFF"
408 wxUint32 riffLen; // basically the file len - 8
409 char riffType[4]; // "WAVE"
410 } riff;
411
412 // format chunk */
413 struct {
414 char fmtID[4]; // "fmt " */
415 wxUint32 formatChunkLen; // (format chunk len - first two fields) 16 in our case
416 wxUint16 formatTag; // 1 for PCM
417 wxUint16 channels;
418 wxUint32 sampleRate;
419 wxUint32 avgBytesPerSec; // sampleRate * blockAlign
420 wxUint16 blockAlign; // bitsPerSample * channels (assume bps % 8 = 0)
421 wxUint16 bitsPerSample;
422 } fmt;
423
424 // id3 chunk header
425 struct {
426 char id3ID[4]; // "id3 "
427 wxUint32 id3Len; // length of metadata in bytes
428 } id3;
429
430 // data chunk header
431 struct {
432 char dataID[4]; // "data"
433 wxUint32 dataLen; // length of all samples in bytes
434 } data;
435
436 riff.riffID[0] = 'R';
437 riff.riffID[1] = 'I';
438 riff.riffID[2] = 'F';
439 riff.riffID[3] = 'F';
440 riff.riffLen = wxUINT32_SWAP_ON_BE(sizeof(riff) +
441 sizeof(fmt) +
442 sizeof(data) +
443 sampleBytes -
444 8);
445 riff.riffType[0] = 'W';
446 riff.riffType[1] = 'A';
447 riff.riffType[2] = 'V';
448 riff.riffType[3] = 'E';
449
450 fmt.fmtID[0] = 'f';
451 fmt.fmtID[1] = 'm';
452 fmt.fmtID[2] = 't';
453 fmt.fmtID[3] = ' ';
454 fmt.formatChunkLen = wxUINT32_SWAP_ON_BE(16);
455 fmt.formatTag = wxUINT16_SWAP_ON_BE(3);
456 fmt.channels = wxUINT16_SWAP_ON_BE(channels);
457 fmt.sampleRate = wxUINT32_SWAP_ON_BE(rate);
458 fmt.bitsPerSample = wxUINT16_SWAP_ON_BE(SAMPLE_SIZE(floatSample) * 8);
459 fmt.blockAlign = wxUINT16_SWAP_ON_BE(fmt.bitsPerSample * fmt.channels / 8);
460 fmt.avgBytesPerSec = wxUINT32_SWAP_ON_BE(fmt.sampleRate * fmt.blockAlign);
461
462 // Retrieve tags if not given a set
463 if (metadata == NULL) {
464 metadata = &Tags::Get(*project);
465 }
466 auto metachunk = GetMetaChunk(metadata);
467
468 if (metachunk.size()) {
469
470 id3.id3ID[0] = 'i';
471 id3.id3ID[1] = 'd';
472 id3.id3ID[2] = '3';
473 id3.id3ID[3] = ' ';
474 id3.id3Len = wxUINT32_SWAP_ON_BE(metachunk.size());
475 riff.riffLen += sizeof(id3) + metachunk.size();
476 }
477
478 data.dataID[0] = 'd';
479 data.dataID[1] = 'a';
480 data.dataID[2] = 't';
481 data.dataID[3] = 'a';
482 data.dataLen = wxUINT32_SWAP_ON_BE(sampleBytes);
483
484 // write the headers and metadata
485 os->Write(&riff, sizeof(riff));
486 os->Write(&fmt, sizeof(fmt));
487 if (metachunk.size()) {
488 os->Write(&id3, sizeof(id3));
489 os->Write(metachunk.data(), metachunk.size());
490 }
491 os->Write(&data, sizeof(data));
492
493 // Mix 'em up
494 const auto &tracks = TrackList::Get( *project );
495 auto mixer = CreateMixer(
496 tracks,
497 selectionOnly,
498 t0,
499 t1,
500 channels,
501 maxBlockLen,
502 true,
503 rate,
504 floatSample,
505 mixerSpec);
506
507 size_t numBytes = 0;
508 constSamplePtr mixed = NULL;
509 auto updateResult = ProgressResult::Success;
510
511 {
512 auto closeIt = finally ( [&] {
513 // Should make the process die, before propagating any exception
514 process.CloseOutput();
515 } );
516
517 // Prepare the progress display
518 InitProgress( pDialog, XO("Export"),
519 selectionOnly
520 ? XO("Exporting the selected audio using command-line encoder")
521 : XO("Exporting the audio using command-line encoder") );
522 auto &progress = *pDialog;
523
524 // Start piping the mixed data to the command
525 while (updateResult == ProgressResult::Success && process.IsActive() && os->IsOk()) {
526 // Capture any stdout and stderr from the command
527 Drain(process.GetInputStream(), &output);
528 Drain(process.GetErrorStream(), &output);
529
530 // Need to mix another block
531 if (numBytes == 0) {
532 auto numSamples = mixer->Process(maxBlockLen);
533 if (numSamples == 0) {
534 break;
535 }
536
537 mixed = mixer->GetBuffer();
538 numBytes = numSamples * channels;
539
540 // Byte-swapping is necessary on big-endian machines, since
541 // WAV files are little-endian
542 #if wxBYTE_ORDER == wxBIG_ENDIAN
543 auto buffer = (const float *) mixed;
544 for (int i = 0; i < numBytes; i++) {
545 buffer[i] = wxUINT32_SWAP_ON_BE(buffer[i]);
546 }
547 #endif
548 numBytes *= SAMPLE_SIZE(floatSample);
549 }
550
551 // Don't write too much at once...pipes may not be able to handle it
552 size_t bytes = wxMin(numBytes, 4096);
553 numBytes -= bytes;
554
555 while (bytes > 0) {
556 os->Write(mixed, bytes);
557 if (!os->IsOk()) {
558 updateResult = ProgressResult::Cancelled;
559 break;
560 }
561 bytes -= os->LastWrite();
562 mixed += os->LastWrite();
563 }
564
565 // Update the progress display
566 updateResult = progress.Update(mixer->MixGetCurrentTime() - t0, t1 - t0);
567 }
568 // Done with the progress display
569 }
570
571 // Wait for process to terminate
572 while (process.IsActive()) {
573 wxMilliSleep(10);
574 wxTheApp->Yield();
575 }
576
577 // Display output on error or if the user wants to see it
578 if (process.GetStatus() != 0 || mShow) {
579 // TODO use ShowInfoDialog() instead.
580 wxDialogWrapper dlg(nullptr,
581 wxID_ANY,
582 XO("Command Output"),
583 wxDefaultPosition,
584 wxSize(600, 400),
585 wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER);
586 dlg.SetName();
587
588 ShuttleGui S(&dlg, eIsCreating);
589 S
590 .Style( wxTE_MULTILINE | wxTE_READONLY | wxTE_RICH )
591 .AddTextWindow(mCmd + wxT("\n\n") + output);
592 S.StartHorizontalLay(wxALIGN_CENTER, false);
593 {
594 S.Id(wxID_OK).AddButton(XXO("&OK"), wxALIGN_CENTER, true);
595 }
596 dlg.GetSizer()->AddSpacer(5);
597 dlg.Layout();
598 dlg.SetMinSize(dlg.GetSize());
599 dlg.Center();
600
601 dlg.ShowModal();
602
603 if (process.GetStatus() != 0)
604 updateResult = ProgressResult::Failed;
605 }
606
607 return updateResult;
608 }
609
GetMetaChunk(const Tags * tags)610 std::vector<char> ExportCL::GetMetaChunk(const Tags *tags)
611 {
612 std::vector<char> buffer;
613
614 #ifdef USE_LIBID3TAG
615 struct id3_tag_deleter {
616 void operator () (id3_tag *p) const { if (p) id3_tag_delete(p); }
617 };
618
619 std::unique_ptr<id3_tag, id3_tag_deleter> tp { id3_tag_new() };
620
621 for (const auto &pair : tags->GetRange()) {
622 const auto &n = pair.first;
623 const auto &v = pair.second;
624 const char *name = "TXXX";
625
626 if (n.CmpNoCase(TAG_TITLE) == 0) {
627 name = ID3_FRAME_TITLE;
628 }
629 else if (n.CmpNoCase(TAG_ARTIST) == 0) {
630 name = ID3_FRAME_ARTIST;
631 }
632 else if (n.CmpNoCase(TAG_ALBUM) == 0) {
633 name = ID3_FRAME_ALBUM;
634 }
635 else if (n.CmpNoCase(TAG_YEAR) == 0) {
636 name = ID3_FRAME_YEAR;
637 }
638 else if (n.CmpNoCase(TAG_GENRE) == 0) {
639 name = ID3_FRAME_GENRE;
640 }
641 else if (n.CmpNoCase(TAG_COMMENTS) == 0) {
642 name = ID3_FRAME_COMMENT;
643 }
644 else if (n.CmpNoCase(TAG_TRACK) == 0) {
645 name = ID3_FRAME_TRACK;
646 }
647 else if (n.CmpNoCase(wxT("composer")) == 0) {
648 name = "TCOM";
649 }
650
651 struct id3_frame *frame = id3_frame_new(name);
652
653 if (!n.IsAscii() || !v.IsAscii()) {
654 id3_field_settextencoding(id3_frame_field(frame, 0), ID3_FIELD_TEXTENCODING_UTF_16);
655 }
656 else {
657 id3_field_settextencoding(id3_frame_field(frame, 0), ID3_FIELD_TEXTENCODING_ISO_8859_1);
658 }
659
660 MallocString<id3_ucs4_t> ucs4{
661 id3_utf8_ucs4duplicate((id3_utf8_t *) (const char *) v.mb_str(wxConvUTF8)) };
662
663 if (strcmp(name, ID3_FRAME_COMMENT) == 0) {
664 // A hack to get around iTunes not recognizing the comment. The
665 // language defaults to XXX and, since it's not a valid language,
666 // iTunes just ignores the tag. So, either set it to a valid language
667 // (which one???) or just clear it. Unfortunately, there's no supported
668 // way of clearing the field, so do it directly.
669 id3_field *f = id3_frame_field(frame, 1);
670 memset(f->immediate.value, 0, sizeof(f->immediate.value));
671 id3_field_setfullstring(id3_frame_field(frame, 3), ucs4.get());
672 }
673 else if (strcmp(name, "TXXX") == 0) {
674 id3_field_setstring(id3_frame_field(frame, 2), ucs4.get());
675
676 ucs4.reset(id3_utf8_ucs4duplicate((id3_utf8_t *) (const char *) n.mb_str(wxConvUTF8)));
677
678 id3_field_setstring(id3_frame_field(frame, 1), ucs4.get());
679 }
680 else {
681 auto addr = ucs4.get();
682 id3_field_setstrings(id3_frame_field(frame, 1), 1, &addr);
683 }
684
685 id3_tag_attachframe(tp.get(), frame);
686 }
687
688 tp->options &= (~ID3_TAG_OPTION_COMPRESSION); // No compression
689
690 // If this version of libid3tag supports it, use v2.3 ID3
691 // tags instead of the newer, but less well supported, v2.4
692 // that libid3tag uses by default.
693 #ifdef ID3_TAG_HAS_TAG_OPTION_ID3V2_3
694 tp->options |= ID3_TAG_OPTION_ID3V2_3;
695 #endif
696
697 id3_length_t len;
698
699 len = id3_tag_render(tp.get(), 0);
700 if ((len % 2) != 0) {
701 len++; // Length must be even.
702 }
703
704 if (len > 0) {
705 buffer.resize(len);
706 id3_tag_render(tp.get(), (id3_byte_t *) buffer.data());
707 }
708 #endif
709
710 return buffer;
711 }
712
OptionsCreate(ShuttleGui & S,int format)713 void ExportCL::OptionsCreate(ShuttleGui &S, int format)
714 {
715 S.AddWindow( safenew ExportCLOptions{ S.GetParent(), format } );
716 }
717
CheckFileName(wxFileName & filename,int WXUNUSED (format))718 bool ExportCL::CheckFileName(wxFileName &filename, int WXUNUSED(format))
719 {
720 ExtendPath ep;
721
722 if (filename.GetExt().empty()) {
723 if (ShowWarningDialog(NULL,
724 wxT("MissingExtension"),
725 XO("You've specified a file name without an extension. Are you sure?"),
726 true) == wxID_CANCEL) {
727 return false;
728 }
729 }
730
731 GetSettings();
732
733 wxArrayString argv = wxCmdLineParser::ConvertStringToArgs(mCmd,
734 #if defined(__WXMSW__)
735 wxCMD_LINE_SPLIT_DOS
736 #else
737 wxCMD_LINE_SPLIT_UNIX
738 #endif
739 );
740
741 if (argv.size() == 0) {
742 ShowExportErrorDialog(
743 ":745",
744 XO("Program name appears to be missing."));
745 return false;
746 }
747
748 // Normalize the path (makes absolute and resolves variables)
749 wxFileName cmd(argv[0]);
750 cmd.Normalize(wxPATH_NORM_ALL & ~wxPATH_NORM_ABSOLUTE);
751
752 // Just verify the given path exists if it is absolute.
753 if (cmd.IsAbsolute()) {
754 if (!cmd.Exists()) {
755 AudacityMessageBox(
756 XO("\"%s\" couldn't be found.").Format(cmd.GetFullPath()),
757 XO("Warning"),
758 wxOK | wxICON_EXCLAMATION);
759
760 return false;
761 }
762
763 return true;
764 }
765
766 // Search for the command in the PATH list
767 wxPathList pathlist;
768 pathlist.AddEnvList(wxT("PATH"));
769 wxString path = pathlist.FindAbsoluteValidPath(argv[0]);
770
771 #if defined(__WXMSW__)
772 if (path.empty()) {
773 path = pathlist.FindAbsoluteValidPath(argv[0] + wxT(".exe"));
774 }
775 #endif
776
777 if (path.empty()) {
778 int action = AudacityMessageBox(
779 XO("Unable to locate \"%s\" in your path.").Format(cmd.GetFullPath()),
780 XO("Warning"),
781 wxOK | wxICON_EXCLAMATION);
782
783 return false;
784 }
785
786 return true;
787 }
788
GetSettings()789 void ExportCL::GetSettings()
790 {
791 // Retrieve settings
792 gPrefs->Read(wxT("/FileFormats/ExternalProgramShowOutput"), &mShow, false);
793 mCmd = gPrefs->Read(wxT("/FileFormats/ExternalProgramExportCommand"), wxT("lame - \"%f.mp3\""));
794 }
795
796 static Exporter::RegisteredExportPlugin sRegisteredPlugin{ "CommandLine",
__anonca18cd7c0602null797 []{ return std::make_unique< ExportCL >(); }
798 };
799