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