1 /**********************************************************************
2
3 Audacity: A Digital Audio Editor
4
5 ExportPCM.cpp
6
7 Dominic Mazzoni
8
9 **********************************************************************/
10
11
12
13 #include <wx/defs.h>
14
15 #include <wx/app.h>
16 #include <wx/choice.h>
17 #include <wx/dynlib.h>
18 #include <wx/filename.h>
19 #include <wx/intl.h>
20 #include <wx/timer.h>
21 #include <wx/string.h>
22 #include <wx/textctrl.h>
23 #include <wx/window.h>
24
25 #include "sndfile.h"
26
27 #include "Dither.h"
28 #include "../FileFormats.h"
29 #include "../Mix.h"
30 #include "Prefs.h"
31 #include "ProjectRate.h"
32 #include "../ShuttleGui.h"
33 #include "../Tags.h"
34 #include "../Track.h"
35 #include "../widgets/AudacityMessageBox.h"
36 #include "../widgets/ProgressDialog.h"
37 #include "../widgets/wxWidgetsWindowPlacement.h"
38 #include "wxFileNameWrapper.h"
39
40 #include "Export.h"
41
42 #ifdef USE_LIBID3TAG
43 #include <id3tag.h>
44 // DM: the following functions were supposed to have been
45 // included in id3tag.h - should be fixed in the next release
46 // of mad.
47 extern "C" {
48 struct id3_frame *id3_frame_new(char const *);
49 id3_length_t id3_latin1_length(id3_latin1_t const *);
50 void id3_latin1_decode(id3_latin1_t const *, id3_ucs4_t *);
51 }
52 #endif
53
54 struct
55 {
56 int format;
57 const wxChar *name;
58 const TranslatableString desc;
59 }
60 static const kFormats[] =
61 {
62 #if defined(__WXMAC__)
63 {SF_FORMAT_AIFF | SF_FORMAT_PCM_16, wxT("AIFF"), XO("AIFF (Apple/SGI)")},
64 #endif
65 {SF_FORMAT_WAV | SF_FORMAT_PCM_16, wxT("WAV"), XO("WAV (Microsoft)")},
66 };
67
68 enum
69 {
70 #if defined(__WXMAC__)
71 FMT_AIFF,
72 #endif
73 FMT_WAV,
74 FMT_OTHER
75 };
76
77 //----------------------------------------------------------------------------
78 // Statics
79 //----------------------------------------------------------------------------
80
LoadOtherFormat(int def=0)81 static int LoadOtherFormat(int def = 0)
82 {
83 return gPrefs->Read(wxT("/FileFormats/ExportFormat_SF1"),
84 kFormats[0].format & SF_FORMAT_TYPEMASK);
85 }
86
SaveOtherFormat(int val)87 static void SaveOtherFormat(int val)
88 {
89 gPrefs->Write(wxT("/FileFormats/ExportFormat_SF1"), val);
90 gPrefs->Flush();
91 }
92
LoadEncoding(int type)93 static int LoadEncoding(int type)
94 {
95 return gPrefs->Read(wxString::Format(wxT("/FileFormats/ExportFormat_SF1_Type/%s_%x"),
96 sf_header_shortname(type), type), (long int) 0);
97 }
98
SaveEncoding(int type,int val)99 static void SaveEncoding(int type, int val)
100 {
101 gPrefs->Write(wxString::Format(wxT("/FileFormats/ExportFormat_SF1_Type/%s_%x"),
102 sf_header_shortname(type), type), val);
103 gPrefs->Flush();
104 }
105
106 //----------------------------------------------------------------------------
107 // ExportPCMOptions Class
108 //----------------------------------------------------------------------------
109
110 #define ID_HEADER_CHOICE 7102
111 #define ID_ENCODING_CHOICE 7103
112
113 class ExportPCMOptions final : public wxPanelWrapper
114 {
115 public:
116
117 ExportPCMOptions(wxWindow *parent, int format);
118 virtual ~ExportPCMOptions();
119
120 void PopulateOrExchange(ShuttleGui & S);
121
122 void OnShow(wxShowEvent & evt);
123 void OnHeaderChoice(wxCommandEvent & evt);
124 void OnEncodingChoice(wxCommandEvent & evt);
125
126 private:
127
128 void GetTypes();
129 void GetEncodings(int enc = 0);
130 void SendSuffixEvent();
131
132 private:
133
134 std::vector<int> mHeaderIndexes;
135 TranslatableStrings mHeaderNames;
136 wxChoice *mHeaderChoice;
137 int mHeaderFromChoice;
138
139 std::vector<int> mEncodingIndexes;
140 TranslatableStrings mEncodingNames;
141 wxChoice *mEncodingChoice;
142 int mEncodingFromChoice;
143
144 int mSelFormat;
145 int mType;
146
147 DECLARE_EVENT_TABLE()
148 };
149
BEGIN_EVENT_TABLE(ExportPCMOptions,wxPanelWrapper)150 BEGIN_EVENT_TABLE(ExportPCMOptions, wxPanelWrapper)
151 EVT_CHOICE(ID_HEADER_CHOICE, ExportPCMOptions::OnHeaderChoice)
152 EVT_CHOICE(ID_ENCODING_CHOICE, ExportPCMOptions::OnEncodingChoice)
153 END_EVENT_TABLE()
154
155 ExportPCMOptions::ExportPCMOptions(wxWindow *parent, int selformat)
156 : wxPanelWrapper(parent, wxID_ANY)
157 {
158 // Remember the selection format
159 mSelFormat = selformat;
160
161 // Init choices
162 mHeaderFromChoice = 0;
163 mEncodingFromChoice = 0;
164
165 if (mSelFormat < FMT_OTHER)
166 {
167 mType = kFormats[selformat].format & SF_FORMAT_TYPEMASK;
168 GetEncodings(mType & SF_FORMAT_SUBMASK);
169 }
170 else
171 {
172 GetTypes();
173 GetEncodings();
174 }
175
176 ShuttleGui S(this, eIsCreatingFromPrefs);
177 PopulateOrExchange(S);
178
179 parent->Bind(wxEVT_SHOW, &ExportPCMOptions::OnShow, this);
180 }
181
~ExportPCMOptions()182 ExportPCMOptions::~ExportPCMOptions()
183 {
184 // Save the encoding
185 SaveOtherFormat(mType);
186 SaveEncoding(mType, sf_encoding_index_to_subtype(mEncodingIndexes[mEncodingFromChoice]));
187 }
188
PopulateOrExchange(ShuttleGui & S)189 void ExportPCMOptions::PopulateOrExchange(ShuttleGui & S)
190 {
191 S.StartVerticalLay();
192 {
193 S.StartHorizontalLay(wxCENTER);
194 {
195 S.StartMultiColumn(2, wxCENTER);
196 {
197 S.SetStretchyCol(1);
198 if (mSelFormat == FMT_OTHER)
199 {
200 mHeaderChoice = S.Id(ID_HEADER_CHOICE)
201 .AddChoice(XXO("Header:"),
202 mHeaderNames,
203 mHeaderFromChoice);
204 }
205 mEncodingChoice = S.Id(ID_ENCODING_CHOICE)
206 .AddChoice(XXO("Encoding:"),
207 mEncodingNames,
208 mEncodingFromChoice);
209 }
210 S.EndMultiColumn();
211 }
212 S.EndHorizontalLay();
213 }
214 S.EndVerticalLay();
215
216 return;
217 }
218
OnShow(wxShowEvent & evt)219 void ExportPCMOptions::OnShow(wxShowEvent & evt)
220 {
221 evt.Skip();
222
223 // Since the initial file name may not have the "correct" extension,
224 // send off an event to have it changed. Note that this will be
225 // done every time a user changes filters in the dialog.
226 if (evt.IsShown())
227 {
228 SendSuffixEvent();
229 }
230 }
231
OnHeaderChoice(wxCommandEvent & evt)232 void ExportPCMOptions::OnHeaderChoice(wxCommandEvent & evt)
233 {
234 evt.Skip();
235
236 // Remember new selection
237 mHeaderFromChoice = evt.GetInt();
238
239 // Get the type for this selection
240 mType = sf_header_index_to_type(mHeaderIndexes[mHeaderFromChoice]);
241
242 // Save the newly selected type
243 SaveOtherFormat(mType);
244
245 // Reload the encodings valid for this new type
246 GetEncodings();
247
248 // Repopulate the encoding choices
249 mEncodingChoice->Clear();
250 for (int i = 0, num = mEncodingNames.size(); i < num; ++i)
251 {
252 mEncodingChoice->AppendString(mEncodingNames[i].StrippedTranslation());
253 }
254
255 // Select the desired encoding
256 mEncodingChoice->SetSelection(mEncodingFromChoice);
257
258 // Send the event indicating a file suffix change.
259 SendSuffixEvent();
260 }
261
OnEncodingChoice(wxCommandEvent & evt)262 void ExportPCMOptions::OnEncodingChoice(wxCommandEvent & evt)
263 {
264 evt.Skip();
265
266 // Remember new selection
267 mEncodingFromChoice = evt.GetInt();
268
269 // And save it
270 SaveEncoding(mType, sf_encoding_index_to_subtype(mEncodingIndexes[mEncodingFromChoice]));
271 }
272
GetTypes()273 void ExportPCMOptions::GetTypes()
274 {
275 // Reset arrays
276 mHeaderIndexes.clear();
277 mHeaderNames.clear();
278
279 // Get the previously saved type. Note that this is ONLY used for
280 // the FMT_OTHER ("Other uncompressed files") types.
281 int typ = LoadOtherFormat() & SF_FORMAT_TYPEMASK;
282
283 // Rebuild the arrays
284 mHeaderFromChoice = 0;
285 for (int i = 0, num = sf_num_headers(); i < num; ++i)
286 {
287 int type = sf_header_index_to_type(i);
288
289 switch (type)
290 {
291 // On the Mac, do not include in header list
292 #if defined(__WXMAC__)
293 case SF_FORMAT_AIFF:
294 break;
295 #endif
296
297 // Do not include in header list
298 case SF_FORMAT_WAV:
299 break;
300
301 default:
302 // Remember the index if this is the desired type
303 if (type == typ)
304 {
305 mHeaderFromChoice = mHeaderIndexes.size();
306 }
307
308 // Store index and name
309 mHeaderIndexes.push_back(i);
310 mHeaderNames.push_back(Verbatim(sf_header_index_name(i)));
311 break;
312 }
313 }
314
315 // Refresh the current type
316 mType = sf_header_index_to_type(mHeaderIndexes[mHeaderFromChoice]);
317 }
318
GetEncodings(int enc)319 void ExportPCMOptions::GetEncodings(int enc)
320 {
321 // Setup for queries
322 SF_INFO info = {};
323 info.samplerate = 44100;
324 info.channels = 1;
325 info.sections = 1;
326
327 // Reset arrays
328 mEncodingIndexes.clear();
329 mEncodingNames.clear();
330
331 // If the encoding wasn't supplied, look it up
332 if (!(enc & SF_FORMAT_SUBMASK))
333 {
334 enc = LoadEncoding(mType);
335 }
336 enc &= SF_FORMAT_SUBMASK;
337
338 // Fix for Bug 1218 - AIFF with no encoding should default to 16 bit.
339 if (mType == SF_FORMAT_AIFF && enc == 0)
340 {
341 enc = SF_FORMAT_PCM_16;
342 }
343
344 // Rebuild the arrays
345 mEncodingFromChoice = 0;
346 for (int i = 0, num = sf_num_encodings(); i < num; ++i)
347 {
348 int sub = sf_encoding_index_to_subtype(i);
349
350 // Since we're traversing the subtypes linearly, we have to
351 // make sure it can be paired with our current type.
352 info.format = mType | sub;
353 if (sf_format_check(&info))
354 {
355 // If this subtype matches our last saved encoding, remember
356 // its index so we can set it in the dialog.
357 if (sub == enc)
358 {
359 mEncodingFromChoice = mEncodingIndexes.size();
360 }
361
362 // Store index and name
363 mEncodingIndexes.push_back(i);
364 mEncodingNames.push_back(Verbatim(sf_encoding_index_name(i)));
365 }
366 }
367 }
368
SendSuffixEvent()369 void ExportPCMOptions::SendSuffixEvent()
370 {
371 // Synchronously process a change in suffix.
372 wxCommandEvent evt(AUDACITY_FILE_SUFFIX_EVENT, GetId());
373 evt.SetEventObject(this);
374 evt.SetString(sf_header_extension(mType));
375 ProcessWindowEvent(evt);
376 }
377
378 //----------------------------------------------------------------------------
379 // ExportPCM Class
380 //----------------------------------------------------------------------------
381
382 class ExportPCM final : public ExportPlugin
383 {
384 public:
385
386 ExportPCM();
387
388 // Required
389
390 void OptionsCreate(ShuttleGui &S, int format) override;
391 ProgressResult Export(AudacityProject *project,
392 std::unique_ptr<ProgressDialog> &pDialog,
393 unsigned channels,
394 const wxFileNameWrapper &fName,
395 bool selectedOnly,
396 double t0,
397 double t1,
398 MixerSpec *mixerSpec = NULL,
399 const Tags *metadata = NULL,
400 int subformat = 0) override;
401 // optional
402 wxString GetFormat(int index) override;
403 FileExtension GetExtension(int index) override;
404 unsigned GetMaxChannels(int index) override;
405
406 private:
407 void ReportTooBigError(wxWindow * pParent);
408 ArrayOf<char> AdjustString(const wxString & wxStr, int sf_format);
409 bool AddStrings(AudacityProject *project, SNDFILE *sf, const Tags *tags, int sf_format);
410 bool AddID3Chunk(
411 const wxFileNameWrapper &fName, const Tags *tags, int sf_format);
412
413 };
414
ExportPCM()415 ExportPCM::ExportPCM()
416 : ExportPlugin()
417 {
418 int selformat; // the index of the format we are setting up at the moment
419
420 // Add the "special" formats first
421 for (size_t i = 0; i < WXSIZEOF(kFormats); ++i)
422 {
423 selformat = AddFormat() - 1;
424 AddExtension(sf_header_extension(kFormats[i].format), selformat);
425 SetFormat(kFormats[i].name, selformat);
426 SetDescription(kFormats[i].desc, selformat);
427 SetCanMetaData(true, selformat);
428 SetMaxChannels(255, selformat);
429 }
430
431 // Then add the generic libsndfile "format"
432 selformat = AddFormat() - 1; // Matches FMT_OTHER
433 SetExtensions(sf_get_all_extensions(), selformat);
434 SetFormat(wxT("LIBSNDFILE"), selformat);
435 SetDescription(XO("Other uncompressed files"), selformat);
436 SetCanMetaData(true, selformat);
437 SetMaxChannels(255, selformat);
438 }
439
ReportTooBigError(wxWindow * pParent)440 void ExportPCM::ReportTooBigError(wxWindow * pParent)
441 {
442 //Temporary translation hack, to say 'WAV or AIFF' rather than 'WAV'
443 auto message =
444 XO("You have attempted to Export a WAV or AIFF file which would be greater than 4GB.\n"
445 "Audacity cannot do this, the Export was abandoned.");
446
447 BasicUI::ShowErrorDialog( wxWidgetsWindowPlacement{ pParent },
448 XO("Error Exporting"), message,
449 wxT("Size_limits_for_WAV_and_AIFF_files"));
450
451 // This alternative error dialog was to cover the possibility we could not
452 // compute the size in advance.
453 #if 0
454 BasicUI::ShowErrorDialog( wxWidgetsWindowPlacement{ pParent },
455 XO("Error Exporting"),
456 XO("Your exported WAV file has been truncated as Audacity cannot export WAV\n"
457 "files bigger than 4GB."),
458 wxT("Size_limits_for_WAV_files"));
459 #endif
460 }
461
462 /**
463 *
464 * @param subformat Control whether we are doing a "preset" export to a popular
465 * file type, or giving the user full control over libsndfile.
466 */
Export(AudacityProject * project,std::unique_ptr<ProgressDialog> & pDialog,unsigned numChannels,const wxFileNameWrapper & fName,bool selectionOnly,double t0,double t1,MixerSpec * mixerSpec,const Tags * metadata,int subformat)467 ProgressResult ExportPCM::Export(AudacityProject *project,
468 std::unique_ptr<ProgressDialog> &pDialog,
469 unsigned numChannels,
470 const wxFileNameWrapper &fName,
471 bool selectionOnly,
472 double t0,
473 double t1,
474 MixerSpec *mixerSpec,
475 const Tags *metadata,
476 int subformat)
477 {
478 double rate = ProjectRate::Get( *project ).GetRate();
479 const auto &tracks = TrackList::Get( *project );
480
481 // Set a default in case the settings aren't found
482 int sf_format;
483
484 switch (subformat)
485 {
486 #if defined(__WXMAC__)
487 case FMT_AIFF:
488 sf_format = SF_FORMAT_AIFF;
489 break;
490 #endif
491
492 case FMT_WAV:
493 sf_format = SF_FORMAT_WAV;
494 break;
495
496 default:
497 // Retrieve the current format.
498 sf_format = LoadOtherFormat();
499 break;
500 }
501
502 // Prior to v2.4.0, sf_format will include the subtype. If not present,
503 // check for the format specific preference.
504 if (!(sf_format & SF_FORMAT_SUBMASK))
505 {
506 sf_format |= LoadEncoding(sf_format);
507 }
508
509 // If subtype is still not specified, supply a default.
510 if (!(sf_format & SF_FORMAT_SUBMASK))
511 {
512 sf_format |= SF_FORMAT_PCM_16;
513 }
514
515 int fileFormat = sf_format & SF_FORMAT_TYPEMASK;
516
517 auto updateResult = ProgressResult::Success;
518 {
519 wxFile f; // will be closed when it goes out of scope
520 SFFile sf; // wraps f
521
522 wxString formatStr;
523 SF_INFO info;
524 //int err;
525
526 //This whole operation should not occur while a file is being loaded on OD,
527 //(we are worried about reading from a file being written to,) so we block.
528 //Furthermore, we need to do this because libsndfile is not threadsafe.
529 formatStr = SFCall<wxString>(sf_header_name, fileFormat);
530
531 // Use libsndfile to export file
532
533 info.samplerate = (unsigned int)(rate + 0.5);
534 info.frames = (unsigned int)((t1 - t0)*rate + 0.5);
535 info.channels = numChannels;
536 info.format = sf_format;
537 info.sections = 1;
538 info.seekable = 0;
539
540 // Bug 46. Trap here, as sndfile.c does not trap it properly.
541 if( (numChannels != 1) && ((sf_format & SF_FORMAT_SUBMASK) == SF_FORMAT_GSM610) )
542 {
543 AudacityMessageBox( XO("GSM 6.10 requires mono") );
544 return ProgressResult::Cancelled;
545 }
546
547 if (sf_format == SF_FORMAT_WAVEX + SF_FORMAT_GSM610) {
548 AudacityMessageBox(
549 XO("WAVEX and GSM 6.10 formats are not compatible") );
550 return ProgressResult::Cancelled;
551 }
552
553 // If we can't export exactly the format they requested,
554 // try the default format for that header type...
555 //
556 // LLL: I don't think this is valid since libsndfile checks
557 // for all allowed subtypes explicitly and doesn't provide
558 // for an unspecified subtype.
559 if (!sf_format_check(&info))
560 info.format = (info.format & SF_FORMAT_TYPEMASK);
561 if (!sf_format_check(&info)) {
562 AudacityMessageBox( XO("Cannot export audio in this format.") );
563 return ProgressResult::Cancelled;
564 }
565 const auto path = fName.GetFullPath();
566 if (f.Open(path, wxFile::write)) {
567 // Even though there is an sf_open() that takes a filename, use the one that
568 // takes a file descriptor since wxWidgets can open a file with a Unicode name and
569 // libsndfile can't (under Windows).
570 sf.reset(SFCall<SNDFILE*>(sf_open_fd, f.fd(), SFM_WRITE, &info, FALSE));
571 //add clipping for integer formats. We allow floats to clip.
572 sf_command(sf.get(), SFC_SET_CLIPPING, NULL, sf_subtype_is_integer(sf_format)?SF_TRUE:SF_FALSE) ;
573 }
574
575 if (!sf) {
576 AudacityMessageBox( XO("Cannot export audio to %s").Format( path ) );
577 return ProgressResult::Cancelled;
578 }
579 // Retrieve tags if not given a set
580 if (metadata == NULL)
581 metadata = &Tags::Get( *project );
582
583 // Install the meta data at the beginning of the file (except for
584 // WAV and WAVEX formats)
585 if (fileFormat != SF_FORMAT_WAV &&
586 fileFormat != SF_FORMAT_WAVEX) {
587 if (!AddStrings(project, sf.get(), metadata, sf_format)) {
588 return ProgressResult::Cancelled;
589 }
590 }
591
592 sampleFormat format;
593 if (sf_subtype_more_than_16_bits(info.format))
594 format = floatSample;
595 else
596 format = int16Sample;
597
598 // Bug 2200
599 // Only trap size limit for file types we know have an upper size limit.
600 // The error message mentions aiff and wav.
601 if( (fileFormat == SF_FORMAT_WAV) ||
602 (fileFormat == SF_FORMAT_WAVEX) ||
603 (fileFormat == SF_FORMAT_AIFF ))
604 {
605 float sampleCount = (float)(t1-t0)*rate*info.channels;
606 float byteCount = sampleCount * sf_subtype_bytes_per_sample( info.format);
607 // Test for 4 Gibibytes, rather than 4 Gigabytes
608 if( byteCount > 4.295e9)
609 {
610 ReportTooBigError( wxTheApp->GetTopWindow() );
611 return ProgressResult::Failed;
612 }
613 }
614 size_t maxBlockLen = 44100 * 5;
615
616 {
617 std::vector<char> dither;
618 if ((info.format & SF_FORMAT_SUBMASK) == SF_FORMAT_PCM_24) {
619 dither.reserve(maxBlockLen * info.channels * SAMPLE_SIZE(int24Sample));
620 }
621
622 wxASSERT(info.channels >= 0);
623 auto mixer = CreateMixer(tracks, selectionOnly,
624 t0, t1,
625 info.channels, maxBlockLen, true,
626 rate, format, mixerSpec);
627
628 InitProgress( pDialog, fName,
629 (selectionOnly
630 ? XO("Exporting the selected audio as %s")
631 : XO("Exporting the audio as %s"))
632 .Format( formatStr ) );
633 auto &progress = *pDialog;
634
635 while (updateResult == ProgressResult::Success) {
636 sf_count_t samplesWritten;
637 size_t numSamples = mixer->Process(maxBlockLen);
638
639 if (numSamples == 0)
640 break;
641
642 auto mixed = mixer->GetBuffer();
643
644 // Bug 1572: Not ideal, but it does add the desired dither
645 if ((info.format & SF_FORMAT_SUBMASK) == SF_FORMAT_PCM_24) {
646 for (int c = 0; c < info.channels; ++c) {
647 CopySamples(
648 mixed + (c * SAMPLE_SIZE(format)), format,
649 dither.data() + (c * SAMPLE_SIZE(int24Sample)), int24Sample,
650 numSamples, gHighQualityDither, info.channels, info.channels
651 );
652 // Copy back without dither
653 CopySamples(
654 dither.data() + (c * SAMPLE_SIZE(int24Sample)), int24Sample,
655 const_cast<samplePtr>(mixed) // PRL fix this!
656 + (c * SAMPLE_SIZE(format)), format,
657 numSamples, DitherType::none, info.channels, info.channels);
658 }
659 }
660
661 if (format == int16Sample)
662 samplesWritten = SFCall<sf_count_t>(sf_writef_short, sf.get(), (const short *)mixed, numSamples);
663 else
664 samplesWritten = SFCall<sf_count_t>(sf_writef_float, sf.get(), (const float *)mixed, numSamples);
665
666 if (static_cast<size_t>(samplesWritten) != numSamples) {
667 char buffer2[1000];
668 sf_error_str(sf.get(), buffer2, 1000);
669 //Used to give this error message
670 #if 0
671 AudacityMessageBox(
672 XO(
673 /* i18n-hint: %s will be the error message from libsndfile, which
674 * is usually something unhelpful (and untranslated) like "system
675 * error" */
676 "Error while writing %s file (disk full?).\nLibsndfile says \"%s\"")
677 .Format( formatStr, wxString::FromAscii(buffer2) ));
678 #else
679 // But better to give the same error message as for
680 // other cases of disk exhaustion.
681 // The thrown exception doesn't escape but GuardedCall
682 // will enqueue a message.
683 GuardedCall([&fName]{
684 throw FileException{
685 FileException::Cause::Write, fName }; });
686 #endif
687 updateResult = ProgressResult::Cancelled;
688 break;
689 }
690
691 updateResult = progress.Update(mixer->MixGetCurrentTime() - t0, t1 - t0);
692 }
693 }
694
695 // Install the WAV metata in a "LIST" chunk at the end of the file
696 if (updateResult == ProgressResult::Success ||
697 updateResult == ProgressResult::Stopped) {
698 if (fileFormat == SF_FORMAT_WAV ||
699 fileFormat == SF_FORMAT_WAVEX) {
700 if (!AddStrings(project, sf.get(), metadata, sf_format)) {
701 // TODO: more precise message
702 ShowExportErrorDialog("PCM:675");
703 return ProgressResult::Cancelled;
704 }
705 }
706 if (0 != sf.close()) {
707 // TODO: more precise message
708 ShowExportErrorDialog("PCM:681");
709 return ProgressResult::Cancelled;
710 }
711 }
712 }
713
714 if (updateResult == ProgressResult::Success ||
715 updateResult == ProgressResult::Stopped)
716 if ((fileFormat == SF_FORMAT_AIFF) ||
717 (fileFormat == SF_FORMAT_WAV))
718 // Note: file has closed, and gets reopened and closed again here:
719 if (!AddID3Chunk(fName, metadata, sf_format) ) {
720 // TODO: more precise message
721 ShowExportErrorDialog("PCM:694");
722 return ProgressResult::Cancelled;
723 }
724
725 return updateResult;
726 }
727
AdjustString(const wxString & wxStr,int sf_format)728 ArrayOf<char> ExportPCM::AdjustString(const wxString & wxStr, int sf_format)
729 {
730 bool b_aiff = false;
731 if ((sf_format & SF_FORMAT_TYPEMASK) == SF_FORMAT_AIFF)
732 b_aiff = true; // Apple AIFF file
733
734 // We must convert the string to 7 bit ASCII
735 size_t sz = wxStr.length();
736 if(sz == 0)
737 return {};
738 // Size for secure allocation in case of local wide char usage
739 size_t sr = (sz+4) * 2;
740
741 ArrayOf<char> pDest{ sr, true };
742 if (!pDest)
743 return {};
744 ArrayOf<char> pSrc{ sr, true };
745 if (!pSrc)
746 return {};
747
748 if(wxStr.mb_str(wxConvISO8859_1))
749 strncpy(pSrc.get(), wxStr.mb_str(wxConvISO8859_1), sz);
750 else if(wxStr.mb_str())
751 strncpy(pSrc.get(), wxStr.mb_str(), sz);
752 else
753 return {};
754
755 char *pD = pDest.get();
756 char *pS = pSrc.get();
757 unsigned char c;
758
759 // ISO Latin to 7 bit ascii conversion table (best approximation)
760 static char aASCII7Table[256] = {
761 0x00, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f,
762 0x5f, 0x09, 0x0a, 0x5f, 0x0d, 0x5f, 0x5f, 0x5f,
763 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f,
764 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f,
765 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
766 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
767 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
768 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
769 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
770 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
771 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,
772 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
773 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67,
774 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,
775 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77,
776 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f,
777 0x45, 0x20, 0x2c, 0x53, 0x22, 0x2e, 0x2b, 0x2b,
778 0x5e, 0x25, 0x53, 0x28, 0x4f, 0x20, 0x5a, 0x20,
779 0x20, 0x27, 0x27, 0x22, 0x22, 0x2e, 0x2d, 0x5f,
780 0x22, 0x54, 0x73, 0x29, 0x6f, 0x20, 0x7a, 0x59,
781 0x20, 0x21, 0x63, 0x4c, 0x6f, 0x59, 0x7c, 0x53,
782 0x22, 0x43, 0x61, 0x22, 0x5f, 0x2d, 0x43, 0x2d,
783 0x6f, 0x7e, 0x32, 0x33, 0x27, 0x75, 0x50, 0x27,
784 0x2c, 0x31, 0x6f, 0x22, 0x5f, 0x5f, 0x5f, 0x3f,
785 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x43,
786 0x45, 0x45, 0x45, 0x45, 0x49, 0x49, 0x49, 0x49,
787 0x44, 0x4e, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x78,
788 0x4f, 0x55, 0x55, 0x55, 0x55, 0x59, 0x70, 0x53,
789 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x63,
790 0x65, 0x65, 0x65, 0x65, 0x69, 0x69, 0x69, 0x69,
791 0x64, 0x6e, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x2f,
792 0x6f, 0x75, 0x75, 0x75, 0x75, 0x79, 0x70, 0x79
793 };
794
795 size_t i;
796 for(i = 0; i < sr; i++) {
797 c = (unsigned char) *pS++;
798 *pD++ = aASCII7Table[c];
799 if(c == 0)
800 break;
801 }
802 *pD = '\0';
803
804 if(b_aiff) {
805 int len = (int)strlen(pDest.get());
806 if((len % 2) != 0) {
807 // In case of an odd length string, add a space char
808 strcat(pDest.get(), " ");
809 }
810 }
811
812 return pDest;
813 }
814
AddStrings(AudacityProject * WXUNUSED (project),SNDFILE * sf,const Tags * tags,int sf_format)815 bool ExportPCM::AddStrings(AudacityProject * WXUNUSED(project), SNDFILE *sf, const Tags *tags, int sf_format)
816 {
817 if (tags->HasTag(TAG_TITLE)) {
818 auto ascii7Str = AdjustString(tags->GetTag(TAG_TITLE), sf_format);
819 if (ascii7Str) {
820 sf_set_string(sf, SF_STR_TITLE, ascii7Str.get());
821 }
822 }
823
824 if (tags->HasTag(TAG_ALBUM)) {
825 auto ascii7Str = AdjustString(tags->GetTag(TAG_ALBUM), sf_format);
826 if (ascii7Str) {
827 sf_set_string(sf, SF_STR_ALBUM, ascii7Str.get());
828 }
829 }
830
831 if (tags->HasTag(TAG_ARTIST)) {
832 auto ascii7Str = AdjustString(tags->GetTag(TAG_ARTIST), sf_format);
833 if (ascii7Str) {
834 sf_set_string(sf, SF_STR_ARTIST, ascii7Str.get());
835 }
836 }
837
838 if (tags->HasTag(TAG_COMMENTS)) {
839 auto ascii7Str = AdjustString(tags->GetTag(TAG_COMMENTS), sf_format);
840 if (ascii7Str) {
841 sf_set_string(sf, SF_STR_COMMENT, ascii7Str.get());
842 }
843 }
844
845 if (tags->HasTag(TAG_YEAR)) {
846 auto ascii7Str = AdjustString(tags->GetTag(TAG_YEAR), sf_format);
847 if (ascii7Str) {
848 sf_set_string(sf, SF_STR_DATE, ascii7Str.get());
849 }
850 }
851
852 if (tags->HasTag(TAG_GENRE)) {
853 auto ascii7Str = AdjustString(tags->GetTag(TAG_GENRE), sf_format);
854 if (ascii7Str) {
855 sf_set_string(sf, SF_STR_GENRE, ascii7Str.get());
856 }
857 }
858
859 if (tags->HasTag(TAG_COPYRIGHT)) {
860 auto ascii7Str = AdjustString(tags->GetTag(TAG_COPYRIGHT), sf_format);
861 if (ascii7Str) {
862 sf_set_string(sf, SF_STR_COPYRIGHT, ascii7Str.get());
863 }
864 }
865
866 if (tags->HasTag(TAG_SOFTWARE)) {
867 auto ascii7Str = AdjustString(tags->GetTag(TAG_SOFTWARE), sf_format);
868 if (ascii7Str) {
869 sf_set_string(sf, SF_STR_SOFTWARE, ascii7Str.get());
870 }
871 }
872
873 if (tags->HasTag(TAG_TRACK)) {
874 auto ascii7Str = AdjustString(tags->GetTag(TAG_TRACK), sf_format);
875 if (ascii7Str) {
876 sf_set_string(sf, SF_STR_TRACKNUMBER, ascii7Str.get());
877 }
878 }
879
880 return true;
881 }
882
883 #ifdef USE_LIBID3TAG
884 struct id3_tag_deleter {
operator ()id3_tag_deleter885 void operator () (id3_tag *p) const { if (p) id3_tag_delete(p); }
886 };
887 using id3_tag_holder = std::unique_ptr<id3_tag, id3_tag_deleter>;
888 #endif
889
AddID3Chunk(const wxFileNameWrapper & fName,const Tags * tags,int sf_format)890 bool ExportPCM::AddID3Chunk(
891 const wxFileNameWrapper &fName, const Tags *tags, int sf_format)
892 {
893 #ifdef USE_LIBID3TAG
894 id3_tag_holder tp { id3_tag_new() };
895
896 for (const auto &pair : tags->GetRange()) {
897 const auto &n = pair.first;
898 const auto &v = pair.second;
899 const char *name = "TXXX";
900
901 if (n.CmpNoCase(TAG_TITLE) == 0) {
902 name = ID3_FRAME_TITLE;
903 }
904 else if (n.CmpNoCase(TAG_ARTIST) == 0) {
905 name = ID3_FRAME_ARTIST;
906 }
907 else if (n.CmpNoCase(TAG_ALBUM) == 0) {
908 name = ID3_FRAME_ALBUM;
909 }
910 else if (n.CmpNoCase(TAG_YEAR) == 0) {
911 name = ID3_FRAME_YEAR;
912 }
913 else if (n.CmpNoCase(TAG_GENRE) == 0) {
914 name = ID3_FRAME_GENRE;
915 }
916 else if (n.CmpNoCase(TAG_COMMENTS) == 0) {
917 name = ID3_FRAME_COMMENT;
918 }
919 else if (n.CmpNoCase(TAG_TRACK) == 0) {
920 name = ID3_FRAME_TRACK;
921 }
922 else if (n.CmpNoCase(wxT("composer")) == 0) {
923 name = "TCOM";
924 }
925
926 struct id3_frame *frame = id3_frame_new(name);
927
928 if (!n.IsAscii() || !v.IsAscii()) {
929 id3_field_settextencoding(id3_frame_field(frame, 0), ID3_FIELD_TEXTENCODING_UTF_16);
930 }
931 else {
932 id3_field_settextencoding(id3_frame_field(frame, 0), ID3_FIELD_TEXTENCODING_ISO_8859_1);
933 }
934
935 MallocString<id3_ucs4_t> ucs4{
936 id3_utf8_ucs4duplicate((id3_utf8_t *) (const char *) v.mb_str(wxConvUTF8)) };
937
938 if (strcmp(name, ID3_FRAME_COMMENT) == 0) {
939 // A hack to get around iTunes not recognizing the comment. The
940 // language defaults to XXX and, since it's not a valid language,
941 // iTunes just ignores the tag. So, either set it to a valid language
942 // (which one???) or just clear it. Unfortunately, there's no supported
943 // way of clearing the field, so do it directly.
944 id3_field *f = id3_frame_field(frame, 1);
945 memset(f->immediate.value, 0, sizeof(f->immediate.value));
946 id3_field_setfullstring(id3_frame_field(frame, 3), ucs4.get());
947 }
948 else if (strcmp(name, "TXXX") == 0) {
949 id3_field_setstring(id3_frame_field(frame, 2), ucs4.get());
950
951 ucs4.reset(id3_utf8_ucs4duplicate((id3_utf8_t *) (const char *) n.mb_str(wxConvUTF8)));
952
953 id3_field_setstring(id3_frame_field(frame, 1), ucs4.get());
954 }
955 else {
956 auto addr = ucs4.get();
957 id3_field_setstrings(id3_frame_field(frame, 1), 1, &addr);
958 }
959
960 id3_tag_attachframe(tp.get(), frame);
961 }
962
963 tp->options &= (~ID3_TAG_OPTION_COMPRESSION); // No compression
964
965 // If this version of libid3tag supports it, use v2.3 ID3
966 // tags instead of the newer, but less well supported, v2.4
967 // that libid3tag uses by default.
968 #ifdef ID3_TAG_HAS_TAG_OPTION_ID3V2_3
969 tp->options |= ID3_TAG_OPTION_ID3V2_3;
970 #endif
971
972 id3_length_t len;
973
974 len = id3_tag_render(tp.get(), 0);
975 if (len == 0)
976 return true;
977
978 if ((len % 2) != 0) len++; // Length must be even.
979 ArrayOf<id3_byte_t> buffer { len, true };
980 if (buffer == NULL)
981 return false;
982
983 // Zero all locations, for ending odd UTF16 content
984 // correctly, i.e., two '\0's at the end.
985
986 id3_tag_render(tp.get(), buffer.get());
987
988 wxFFile f(fName.GetFullPath(), wxT("r+b"));
989 if (f.IsOpened()) {
990 wxUint32 sz;
991
992 sz = (wxUint32) len;
993 if (!f.SeekEnd(0))
994 return false;
995 if ((sf_format & SF_FORMAT_TYPEMASK) == SF_FORMAT_WAV)
996 {
997 if (4 != f.Write("id3 ", 4))// Must be lower case for foobar2000.
998 return false ;
999 }
1000 else {
1001 if (4 != f.Write("ID3 ", 4))
1002 return false;
1003 sz = wxUINT32_SWAP_ON_LE(sz);
1004 }
1005 if (4 != f.Write(&sz, 4))
1006 return false;
1007
1008 if (len != f.Write(buffer.get(), len))
1009 return false;
1010
1011 sz = (wxUint32) f.Tell() - 8;
1012 if ((sf_format & SF_FORMAT_TYPEMASK) == SF_FORMAT_AIFF)
1013 sz = wxUINT32_SWAP_ON_LE(sz);
1014
1015 if (!f.Seek(4))
1016 return false;
1017 if (4 != f.Write(&sz, 4))
1018 return false;
1019
1020 if (!f.Flush())
1021 return false;
1022
1023 if (!f.Close())
1024 return false;
1025 }
1026 else
1027 return false;
1028 #endif
1029 return true;
1030 }
1031
OptionsCreate(ShuttleGui & S,int format)1032 void ExportPCM::OptionsCreate(ShuttleGui &S, int format)
1033 {
1034 switch (format)
1035 {
1036 #if defined(__WXMAC__)
1037 case FMT_AIFF:
1038 #endif
1039 case FMT_WAV:
1040 case FMT_OTHER:
1041 S.AddWindow(safenew ExportPCMOptions{ S.GetParent(), format });
1042 break;
1043
1044 default:
1045 ExportPlugin::OptionsCreate(S, format);
1046 break;
1047 }
1048 }
1049
GetFormat(int index)1050 wxString ExportPCM::GetFormat(int index)
1051 {
1052 if (index != FMT_OTHER)
1053 {
1054 return ExportPlugin::GetFormat(index);
1055 }
1056
1057 // Get the saved type
1058 int typ = LoadOtherFormat() & SF_FORMAT_TYPEMASK;
1059
1060 // Return the format name for that type
1061 return sf_header_shortname(typ);
1062 }
1063
GetExtension(int index)1064 FileExtension ExportPCM::GetExtension(int index)
1065 {
1066 if (index != FMT_OTHER)
1067 {
1068 return ExportPlugin::GetExtension(index);
1069 }
1070
1071 // Get the saved type
1072 int typ = LoadOtherFormat() & SF_FORMAT_TYPEMASK;
1073
1074 // Return the extension for that type
1075 return sf_header_extension(typ);
1076 }
1077
GetMaxChannels(int index)1078 unsigned ExportPCM::GetMaxChannels(int index)
1079 {
1080 SF_INFO si = {};
1081
1082 if (index < FMT_OTHER)
1083 {
1084 si.format = kFormats[index].format;
1085 }
1086 else
1087 {
1088 // Get the saved type
1089 si.format = LoadOtherFormat() & SF_FORMAT_TYPEMASK;
1090 si.format |= LoadEncoding(si.format);
1091 }
1092
1093 for (si.channels = 1; sf_format_check(&si); si.channels++)
1094 {
1095 // just counting
1096 }
1097
1098 // Return the max number of channels
1099 return si.channels - 1;
1100 }
1101
1102 static Exporter::RegisteredExportPlugin sRegisteredPlugin{ "PCM",
__anone2fcd5cd0402null1103 []{ return std::make_unique< ExportPCM >(); }
1104 };
1105