/********************************************************************** Audacity: A Digital Audio Editor ImportQT.cpp Joshua Haberman On OS X, handles import of MPEG-4 audio including AAC and Apple Lossless, import of audio from MPEG-4 video files, and import of AIF(F)/AIFC files (AIF(F) might contain content libsndfile can't handle). Could be modified to support QuickTime import on Windows. **********************************************************************/ #include "../Audacity.h" #include "ImportQT.h" #include "ImportPlugin.h" #include "../widgets/ErrorDialog.h" #define DESC _("QuickTime files") static const wxChar *exts[] = { wxT("aif"), wxT("aifc"), wxT("aiff"), wxT("mov"), wxT("aac"), wxT("m4a"), wxT("mp4") }; #if defined(__WXMAC__) #undef USE_QUICKTIME #endif #ifndef USE_QUICKTIME void GetQTImportPlugin(ImportPluginList &importPluginList, UnusableImportPluginList &unusableImportPluginList) { unusableImportPluginList.push_back( std::make_unique (DESC, wxArrayString(WXSIZEOF(exts), exts)) ); } #else /* USE_QUICKTIME */ // There's a name collision between our Track and QuickTime's...workaround it #define Track XTrack #if defined(__WXMAC__) // These get used when building under OSX #include #include #include #else // These get used when building under Windows #include #include #include #include #include #include #include #include #endif // There's a name collision between our Track and QuickTime's...workaround it #undef Track #include "../Internat.h" #include "../Tags.h" #include "../WaveTrack.h" #define kQTAudioPropertyID_MaxAudioSampleSize 'mssz' class QTImportPlugin final : public ImportPlugin { public: QTImportPlugin() : ImportPlugin(wxArrayString(WXSIZEOF(exts), exts)), mInitialized(false) { OSErr err = noErr; #if defined(__WXMSW__) err = ::InitializeQTML(0); #endif if (err == noErr) { err = ::EnterMovies(); if (err == noErr) { mInitialized = true; } else { #if defined(__WXMSW__) TerminateQTML(); #endif } } } ~QTImportPlugin() { if (mInitialized) { ExitMovies(); #if defined(__WXMSW__) TerminateQTML(); #endif } mInitialized = false; } wxString GetPluginStringID() { return wxT("quicktime"); } wxString GetPluginFormatDescription(); std::unique_ptr Open(const wxString & Filename) override; private: bool mInitialized; }; class QTImportFileHandle final : public ImportFileHandle { public: QTImportFileHandle(const wxString & name, Movie movie) : ImportFileHandle(name) { mMovie = movie; } virtual ~QTImportFileHandle() { if (mMovie) { DisposeMovie(mMovie); mMovie = NULL; } } wxString GetFileDescription() override; ByteCount GetFileUncompressedBytes() override; wxInt32 GetStreamCount() override { return 1; } const wxArrayString &GetStreamInfo() override { static wxArrayString empty; return empty; } void SetStreamUsage(wxInt32 StreamID, bool Use) override { } ProgressResult Import(TrackFactory *trackFactory, TrackHolders &outTracks, Tags *tags) override; private: void AddMetadata(Tags *tags); Movie mMovie; }; void GetQTImportPlugin(ImportPluginList &importPluginList, UnusableImportPluginList &unusableImportPluginList) { importPluginList.push_back( std::make_unique() ); } wxString QTImportPlugin::GetPluginFormatDescription() { return DESC; } std::unique_ptr QTImportPlugin::Open(const wxString & Filename) { OSErr err; FSRef inRef; Movie theMovie = NULL; Handle dataRef = NULL; OSType dataRefType = 0; short resID = 0; #if defined(__WXMAC__) err = wxMacPathToFSRef(Filename, &inRef); #else // LLL: This will not work for pathnames with Unicode characters...find // another method. err = FSPathMakeRef((UInt8 *)OSFILENAME(Filename), &inRef, NULL); #endif if (err != noErr) { return nullptr; } err = QTNewDataReferenceFromFSRef(&inRef, 0, &dataRef, &dataRefType); if (err != noErr) { return nullptr; } // instantiate the movie err = NewMovieFromDataRef(&theMovie, newMovieActive | newMovieDontAskUnresolvedDataRefs, &resID, dataRef, dataRefType); DisposeHandle(dataRef); if (err != noErr) { return nullptr; } return std::make_unique(Filename, theMovie); } wxString QTImportFileHandle::GetFileDescription() { return DESC; } auto QTImportFileHandle::GetFileUncompressedBytes() -> ByteCount { return 0; } ProgressResult QTImportFileHandle::Import(TrackFactory *trackFactory, TrackHolders &outTracks, Tags *tags) { outTracks.clear(); OSErr err = noErr; MovieAudioExtractionRef maer = NULL; auto updateResult = ProgressResult::Success; auto totSamples = (sampleCount) GetMovieDuration(mMovie); // convert from TimeValue decltype(totSamples) numSamples = 0; Boolean discrete = true; UInt32 quality = kQTAudioRenderQuality_Max; AudioStreamBasicDescription desc; UInt32 maxSampleSize; bool res = false; auto cleanup = finally( [&] { if (maer) { MovieAudioExtractionEnd(maer); } } ); CreateProgress(); do { err = MovieAudioExtractionBegin(mMovie, 0, &maer); if (err != noErr) { AudacityMessageBox(_("Unable to start QuickTime extraction")); break; } err = MovieAudioExtractionSetProperty(maer, kQTPropertyClass_MovieAudioExtraction_Audio, kQTMovieAudioExtractionAudioPropertyID_RenderQuality, sizeof(quality), &quality); if (err != noErr) { AudacityMessageBox(_("Unable to set QuickTime render quality")); break; } err = MovieAudioExtractionSetProperty(maer, kQTPropertyClass_MovieAudioExtraction_Movie, kQTMovieAudioExtractionMoviePropertyID_AllChannelsDiscrete, sizeof(discrete), &discrete); if (err != noErr) { AudacityMessageBox(_("Unable to set QuickTime discrete channels property")); break; } err = MovieAudioExtractionGetProperty(maer, kQTPropertyClass_MovieAudioExtraction_Audio, kQTAudioPropertyID_MaxAudioSampleSize, sizeof(maxSampleSize), &maxSampleSize, NULL); if (err != noErr) { AudacityMessageBox(_("Unable to get QuickTime sample size property")); break; } err = MovieAudioExtractionGetProperty(maer, kQTPropertyClass_MovieAudioExtraction_Audio, kQTMovieAudioExtractionAudioPropertyID_AudioStreamBasicDescription, sizeof(desc), &desc, NULL); if (err != noErr) { AudacityMessageBox(_("Unable to retrieve stream description")); break; } auto numchan = desc.mChannelsPerFrame; const size_t bufsize = 5 * desc.mSampleRate; // determine sample format sampleFormat format; switch (maxSampleSize) { case 16: format = int16Sample; break; case 24: format = int24Sample; break; default: format = floatSample; break; } // Allocate an array of pointers, whose size is not known statically, // and prefixed with the AudioBufferList structure. MallocPtr< AudioBufferList > abl{ static_cast< AudioBufferList * >( calloc( 1, offsetof( AudioBufferList, mBuffers ) + (sizeof(AudioBuffer) * numchan))) }; abl->mNumberBuffers = numchan; NewChannelGroup channels{ numchan }; const auto size = sizeof(float) * bufsize; ArraysOf holders{ numchan, size }; for (size_t c = 0; c < numchan; c++) { auto &buffer = abl->mBuffers[c]; auto &holder = holders[c]; auto &channel = channels[c]; buffer.mNumberChannels = 1; buffer.mDataByteSize = size; buffer.mData = holder.get(); channel = trackFactory->NewWaveTrack( format ); channel->SetRate( desc.mSampleRate ); } do { UInt32 flags = 0; UInt32 numFrames = bufsize; err = MovieAudioExtractionFillBuffer(maer, &numFrames, abl.get(), &flags); if (err != noErr) { AudacityMessageBox(_("Unable to get fill buffer")); break; } for (size_t c = 0; c < numchan; c++) { channels[c]->Append((char *) abl->mBuffers[c].mData, floatSample, numFrames); } numSamples += numFrames; updateResult = mProgress->Update( numSamples.as_long_long(), totSamples.as_long_long() ); if (numFrames == 0 || flags & kQTMovieAudioExtractionComplete) { break; } } while (updateResult == ProgressResult::Success); res = (updateResult == ProgressResult::Success && err == noErr); if (res) { for (auto &channel: channels) channel->Flush(); if (!channels.empty()) outTracks.push_back(std::move(channels)); } // // Extract any metadata // if (res) { AddMetadata(tags); } } while (false); // done: return (res ? ProgressResult::Success : ProgressResult::Failed); } static const struct { const OSType key; const wxChar *name; } names[] = { { kQTMetaDataCommonKeyAuthor, wxT("Author") }, { kQTMetaDataCommonKeyComment, TAG_COMMENTS }, { kQTMetaDataCommonKeyCopyright, TAG_COPYRIGHT }, { kQTMetaDataCommonKeyDirector, wxT("Director") }, { kQTMetaDataCommonKeyDisplayName, wxT("Full Name") }, { kQTMetaDataCommonKeyInformation, wxT("Information") }, { kQTMetaDataCommonKeyKeywords, wxT("Keywords") }, { kQTMetaDataCommonKeyProducer, wxT("Producer") }, { kQTMetaDataCommonKeyAlbum, TAG_ALBUM }, { kQTMetaDataCommonKeyArtist, TAG_ARTIST }, { kQTMetaDataCommonKeyChapterName, wxT("Chapter") }, { kQTMetaDataCommonKeyComposer, wxT("Composer") }, { kQTMetaDataCommonKeyDescription, wxT("Description") }, { kQTMetaDataCommonKeyGenre, TAG_GENRE }, { kQTMetaDataCommonKeyOriginalFormat, wxT("Original Format") }, { kQTMetaDataCommonKeyOriginalSource, wxT("Original Source") }, { kQTMetaDataCommonKeyPerformers, wxT("Performers") }, { kQTMetaDataCommonKeySoftware, TAG_SOFTWARE }, { kQTMetaDataCommonKeyWriter, wxT("Writer") }, }; void QTImportFileHandle::AddMetadata(Tags *tags) { QTMetaDataRef metaDataRef = NULL; auto cleanup = finally( [&] { // we are done so release our metadata object if ( metaDataRef ) QTMetaDataRelease(metaDataRef); } ); OSErr err; err = QTCopyMovieMetaData(mMovie, &metaDataRef); if (err != noErr) { return; } for (int i = 0; i < WXSIZEOF(names); i++) { QTMetaDataItem item = kQTMetaDataItemUninitialized; // OSType key = names[i].key; err = QTMetaDataGetNextItem(metaDataRef, kQTMetaDataStorageFormatWildcard, kQTMetaDataItemUninitialized, kQTMetaDataKeyFormatCommon, (const UInt8 *) &names[i].key, sizeof(names[i].key), &item); if (err != noErr) { continue; } if (item == kQTMetaDataItemUninitialized) { continue; } QTPropertyValueType outPropType; ::ByteCount outPropValueSize; ::ByteCount outPropValueSizeUsed = 0; UInt32 outPropFlags; UInt32 dataType; // Get data type err = QTMetaDataGetItemProperty(metaDataRef, item, kPropertyClass_MetaDataItem, kQTMetaDataItemPropertyID_DataType, sizeof(dataType), &dataType, &outPropValueSizeUsed); if (err != noErr) { continue; } // Get the data length err = QTMetaDataGetItemPropertyInfo(metaDataRef, item, kPropertyClass_MetaDataItem, kQTMetaDataItemPropertyID_Value, &outPropType, &outPropValueSize, &outPropFlags ); if (err != noErr) { continue; } // Alloc memory for it ArrayOf outVals{ outPropValueSize }; // Retrieve the data err = QTMetaDataGetItemProperty(metaDataRef, item, kPropertyClass_MetaDataItem, kQTMetaDataItemPropertyID_Value, outPropValueSize, outVals.get(), &outPropValueSizeUsed); if (err != noErr) continue; wxString v; switch (dataType) { case kQTMetaDataTypeUTF8: v = wxString(outVals.get(), wxConvUTF8); break; case kQTMetaDataTypeUTF16BE: { wxMBConvUTF16BE conv; v = wxString(outVals.get(), conv); } break; } if (!v.empty()) { tags->SetTag(names[i].name, v); } } return; } #endif