diff options
Diffstat (limited to 'xbmc/cdrip')
-rw-r--r-- | xbmc/cdrip/CDDARipJob.cpp | 259 | ||||
-rw-r--r-- | xbmc/cdrip/CDDARipJob.h | 92 | ||||
-rw-r--r-- | xbmc/cdrip/CDDARipper.cpp | 326 | ||||
-rw-r--r-- | xbmc/cdrip/CDDARipper.h | 102 | ||||
-rw-r--r-- | xbmc/cdrip/CMakeLists.txt | 17 | ||||
-rw-r--r-- | xbmc/cdrip/Encoder.cpp | 159 | ||||
-rw-r--r-- | xbmc/cdrip/Encoder.h | 67 | ||||
-rw-r--r-- | xbmc/cdrip/EncoderAddon.cpp | 95 | ||||
-rw-r--r-- | xbmc/cdrip/EncoderAddon.h | 43 | ||||
-rw-r--r-- | xbmc/cdrip/EncoderFFmpeg.cpp | 413 | ||||
-rw-r--r-- | xbmc/cdrip/EncoderFFmpeg.h | 74 | ||||
-rw-r--r-- | xbmc/cdrip/IEncoder.h | 46 |
12 files changed, 1693 insertions, 0 deletions
diff --git a/xbmc/cdrip/CDDARipJob.cpp b/xbmc/cdrip/CDDARipJob.cpp new file mode 100644 index 0000000..d8cbab9 --- /dev/null +++ b/xbmc/cdrip/CDDARipJob.cpp @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "CDDARipJob.h" + +#include "Encoder.h" +#include "EncoderAddon.h" +#include "EncoderFFmpeg.h" +#include "FileItem.h" +#include "ServiceBroker.h" +#include "Util.h" +#include "addons/AddonManager.h" +#include "addons/addoninfo/AddonType.h" +#include "dialogs/GUIDialogExtendedProgressBar.h" +#include "filesystem/File.h" +#include "filesystem/SpecialProtocol.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "storage/MediaManager.h" +#include "utils/StringUtils.h" +#include "utils/SystemInfo.h" +#include "utils/log.h" + +#if defined(TARGET_WINDOWS) +#include "platform/win32/CharsetConverter.h" +#endif + +using namespace ADDON; +using namespace MUSIC_INFO; +using namespace XFILE; +using namespace KODI::CDRIP; + +CCDDARipJob::CCDDARipJob(const std::string& input, + const std::string& output, + const CMusicInfoTag& tag, + int encoder, + bool eject, + unsigned int rate, + unsigned int channels, + unsigned int bps) + : m_rate(rate), + m_channels(channels), + m_bps(bps), + m_tag(tag), + m_input(input), + m_output(CUtil::MakeLegalPath(output)), + m_eject(eject), + m_encoder(encoder) +{ +} + +CCDDARipJob::~CCDDARipJob() = default; + +bool CCDDARipJob::DoWork() +{ + CLog::Log(LOGINFO, "CCDDARipJob::{} - Start ripping track {} to {}", __func__, m_input, m_output); + + // if we are ripping to a samba share, rip it to hd first and then copy it to the share + CFileItem file(m_output, false); + if (file.IsRemote()) + m_output = SetupTempFile(); + + if (m_output.empty()) + { + CLog::Log(LOGERROR, "CCDDARipJob::{} - Error opening file", __func__); + return false; + } + + // init ripper + CFile reader; + std::unique_ptr<CEncoder> encoder{}; + if (!reader.Open(m_input, READ_CACHED) || !(encoder = SetupEncoder(reader))) + { + CLog::Log(LOGERROR, "CCDDARipJob::{} - Opening failed", __func__); + return false; + } + + // setup the progress dialog + CGUIDialogExtendedProgressBar* pDlgProgress = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>( + WINDOW_DIALOG_EXT_PROGRESS); + CGUIDialogProgressBarHandle* handle = pDlgProgress->GetHandle(g_localizeStrings.Get(605)); + + int iTrack = atoi(m_input.substr(13, m_input.size() - 13 - 5).c_str()); + std::string strLine0 = + StringUtils::Format("{:02}. {} - {}", iTrack, m_tag.GetArtistString(), m_tag.GetTitle()); + handle->SetText(strLine0); + + // start ripping + int percent = 0; + int oldpercent = 0; + bool cancelled(false); + int result; + while (!cancelled && (result = RipChunk(reader, encoder, percent)) == 0) + { + cancelled = ShouldCancel(percent, 100); + if (percent > oldpercent) + { + oldpercent = percent; + handle->SetPercentage(static_cast<float>(percent)); + } + } + + // close encoder ripper + encoder->EncoderClose(); + encoder.reset(); + reader.Close(); + + if (file.IsRemote() && !cancelled && result == 2) + { + // copy the ripped track to the share + if (!CFile::Copy(m_output, file.GetPath())) + { + CLog::Log(LOGERROR, "CCDDARipJob::{} - Error copying file from {} to {}", __func__, m_output, + file.GetPath()); + CFile::Delete(m_output); + return false; + } + // delete cached file + CFile::Delete(m_output); + } + + if (cancelled) + { + CLog::Log(LOGWARNING, "CCDDARipJob::{} - User Cancelled CDDA Rip", __func__); + CFile::Delete(m_output); + } + else if (result == 1) + CLog::Log(LOGERROR, "CCDDARipJob::{} - Error ripping {}", __func__, m_input); + else if (result < 0) + CLog::Log(LOGERROR, "CCDDARipJob::{} - Error encoding {}", __func__, m_input); + else + { + CLog::Log(LOGINFO, "CCDDARipJob::{} - Finished ripping {}", __func__, m_input); + if (m_eject) + { + CLog::Log(LOGINFO, "CCDDARipJob::{} - Ejecting CD", __func__); + CServiceBroker::GetMediaManager().EjectTray(); + } + } + + handle->MarkFinished(); + + return !cancelled && result == 2; +} + +int CCDDARipJob::RipChunk(CFile& reader, const std::unique_ptr<CEncoder>& encoder, int& percent) +{ + percent = 0; + + uint8_t stream[1024]; + + // get data + ssize_t result = reader.Read(stream, 1024); + + // return if rip is done or on some kind of error + if (result <= 0) + return 1; + + // encode data + ssize_t encres = encoder->EncoderEncode(stream, result); + + // Get progress indication + percent = static_cast<int>(reader.GetPosition() * 100 / reader.GetLength()); + + if (reader.GetPosition() == reader.GetLength()) + return 2; + + return -(1 - encres); +} + +std::unique_ptr<CEncoder> CCDDARipJob::SetupEncoder(CFile& reader) +{ + std::unique_ptr<CEncoder> encoder; + const std::string audioEncoder = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString( + CSettings::SETTING_AUDIOCDS_ENCODER); + if (audioEncoder == "audioencoder.kodi.builtin.aac" || + audioEncoder == "audioencoder.kodi.builtin.wma") + { + encoder = std::make_unique<CEncoderFFmpeg>(); + } + else + { + const AddonInfoPtr addonInfo = + CServiceBroker::GetAddonMgr().GetAddonInfo(audioEncoder, AddonType::AUDIOENCODER); + if (addonInfo) + { + encoder = std::make_unique<CEncoderAddon>(addonInfo); + } + } + if (!encoder) + return std::unique_ptr<CEncoder>{}; + + // we have to set the tags before we init the Encoder + const std::string strTrack = StringUtils::Format( + "{}", std::stol(m_input.substr(13, m_input.size() - 13 - 5), nullptr, 10)); + + const std::string itemSeparator = + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator; + + encoder->SetComment(std::string("Ripped with ") + CSysInfo::GetAppName()); + encoder->SetArtist(StringUtils::Join(m_tag.GetArtist(), itemSeparator)); + encoder->SetTitle(m_tag.GetTitle()); + encoder->SetAlbum(m_tag.GetAlbum()); + encoder->SetAlbumArtist(StringUtils::Join(m_tag.GetAlbumArtist(), itemSeparator)); + encoder->SetGenre(StringUtils::Join(m_tag.GetGenre(), itemSeparator)); + encoder->SetTrack(strTrack); + encoder->SetTrackLength(static_cast<int>(reader.GetLength())); + encoder->SetYear(m_tag.GetYearString()); + + // init encoder + if (!encoder->EncoderInit(m_output, m_channels, m_rate, m_bps)) + encoder.reset(); + + return encoder; +} + +std::string CCDDARipJob::SetupTempFile() +{ + char tmp[MAX_PATH + 1]; +#if defined(TARGET_WINDOWS) + using namespace KODI::PLATFORM::WINDOWS; + wchar_t tmpW[MAX_PATH]; + GetTempFileName(ToW(CSpecialProtocol::TranslatePath("special://temp/")).c_str(), L"riptrack", 0, + tmpW); + auto tmpString = FromW(tmpW); + strncpy_s(tmp, tmpString.length(), tmpString.c_str(), MAX_PATH); +#else + int fd; + strncpy(tmp, CSpecialProtocol::TranslatePath("special://temp/riptrackXXXXXX").c_str(), MAX_PATH); + if ((fd = mkstemp(tmp)) == -1) + tmp[0] = '\0'; + if (fd != -1) + close(fd); +#endif + return tmp; +} + +bool CCDDARipJob::operator==(const CJob* job) const +{ + if (strcmp(job->GetType(), GetType()) == 0) + { + const CCDDARipJob* rjob = dynamic_cast<const CCDDARipJob*>(job); + if (rjob) + { + return m_input == rjob->m_input && m_output == rjob->m_output; + } + } + return false; +} diff --git a/xbmc/cdrip/CDDARipJob.h b/xbmc/cdrip/CDDARipJob.h new file mode 100644 index 0000000..11d92eb --- /dev/null +++ b/xbmc/cdrip/CDDARipJob.h @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "music/tags/MusicInfoTag.h" +#include "utils/Job.h" + +namespace XFILE +{ +class CFile; +} + +namespace KODI +{ +namespace CDRIP +{ + +class CEncoder; + +class CCDDARipJob : public CJob +{ +public: + /*! + * \brief Construct a ripper job + * + * \param[in] input The input file url + * \param[in] output The output file url + * \param[in] tag The music tag to attach to track + * \param[in] encoder The encoder to use. See Encoder.h + * \param[in] eject Should we eject tray on finish? + * \param[in] rate The sample rate of the input + * \param[in] channels Number of audio channels in input + * \param[in] bps The bits per sample for input + */ + CCDDARipJob(const std::string& input, + const std::string& output, + const MUSIC_INFO::CMusicInfoTag& tag, + int encoder, + bool eject = false, + unsigned int rate = 44100, + unsigned int channels = 2, + unsigned int bps = 16); + + ~CCDDARipJob() override; + + const char* GetType() const override { return "cdrip"; } + bool operator==(const CJob* job) const override; + bool DoWork() override; + std::string GetOutput() const { return m_output; } + +protected: + /*! + * \brief Setup the audio encoder + */ + std::unique_ptr<CEncoder> SetupEncoder(XFILE::CFile& reader); + + /*! + * \brief Helper used if output is a remote url + */ + std::string SetupTempFile(); + + /*! + * \brief Rip a chunk of audio + * + * \param[in] reader The input reader + * \param[in] encoder The audio encoder + * \param[out] percent The percentage completed on return + * \return 0 (CDDARIP_OK) if everything went okay, or + * a positive error code from the reader, or + * -1 if the encoder failed + * \sa CCDDARipper::GetData, CEncoder::Encode + */ + int RipChunk(XFILE::CFile& reader, const std::unique_ptr<CEncoder>& encoder, int& percent); + + unsigned int m_rate; //< The sample rate of the input file + unsigned int m_channels; //< The number of channels in input file + unsigned int m_bps; //< The bits per sample of input + MUSIC_INFO::CMusicInfoTag m_tag; //< Music tag to attach to output file + std::string m_input; //< The input url + std::string m_output; //< The output url + bool m_eject; //< Should we eject tray when we are finished? + int m_encoder; //< The audio encoder +}; + +} /* namespace CDRIP */ +} /* namespace KODI */ diff --git a/xbmc/cdrip/CDDARipper.cpp b/xbmc/cdrip/CDDARipper.cpp new file mode 100644 index 0000000..fdea7b1 --- /dev/null +++ b/xbmc/cdrip/CDDARipper.cpp @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "CDDARipper.h" + +#include "CDDARipJob.h" +#include "FileItem.h" +#include "ServiceBroker.h" +#include "Util.h" +#include "addons/AddonManager.h" +#include "addons/addoninfo/AddonInfo.h" +#include "addons/addoninfo/AddonType.h" +#include "filesystem/CDDADirectory.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "music/MusicDatabase.h" +#include "music/MusicDbUrl.h" +#include "music/MusicLibraryQueue.h" +#include "music/infoscanner/MusicInfoScanner.h" +#include "music/tags/MusicInfoTag.h" +#include "music/tags/MusicInfoTagLoaderFactory.h" +#include "settings/AdvancedSettings.h" +#include "settings/MediaSourceSettings.h" +#include "settings/SettingPath.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/windows/GUIControlSettings.h" +#include "storage/MediaManager.h" +#include "utils/LabelFormatter.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" + +using namespace ADDON; +using namespace XFILE; +using namespace MUSIC_INFO; +using namespace KODI::MESSAGING; +using namespace KODI::CDRIP; + +CCDDARipper& CCDDARipper::GetInstance() +{ + static CCDDARipper sRipper; + return sRipper; +} + +CCDDARipper::CCDDARipper() : CJobQueue(false, 1) //enforce fifo and non-parallel processing +{ +} + +CCDDARipper::~CCDDARipper() = default; + +// rip a single track from cd +bool CCDDARipper::RipTrack(CFileItem* pItem) +{ + // don't rip non cdda items + if (!URIUtils::HasExtension(pItem->GetPath(), ".cdda")) + { + CLog::Log(LOGDEBUG, "CCDDARipper::{} - File '{}' is not a cdda track", __func__, + pItem->GetPath()); + return false; + } + + // construct directory where the track is stored + std::string strDirectory; + int legalType; + if (!CreateAlbumDir(*pItem->GetMusicInfoTag(), strDirectory, legalType)) + return false; + + std::string strFile = URIUtils::AddFileToFolder( + strDirectory, CUtil::MakeLegalFileName(GetTrackName(pItem), legalType)); + + AddJob(new CCDDARipJob(pItem->GetPath(), strFile, *pItem->GetMusicInfoTag(), + CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_AUDIOCDS_ENCODER))); + + return true; +} + +bool CCDDARipper::RipCD() +{ + // return here if cd is not a CDDA disc + MEDIA_DETECT::CCdInfo* pInfo = CServiceBroker::GetMediaManager().GetCdInfo(); + if (pInfo == nullptr || !pInfo->IsAudio(1)) + { + CLog::Log(LOGDEBUG, "CCDDARipper::{} - CD is not an audio cd", __func__); + return false; + } + + // get cd cdda contents + CFileItemList vecItems; + XFILE::CCDDADirectory directory; + directory.GetDirectory(CURL("cdda://local/"), vecItems); + + // get cddb info + for (int i = 0; i < vecItems.Size(); ++i) + { + CFileItemPtr pItem = vecItems[i]; + CMusicInfoTagLoaderFactory factory; + std::unique_ptr<IMusicInfoTagLoader> pLoader(factory.CreateLoader(*pItem)); + if (nullptr != pLoader) + { + pLoader->Load(pItem->GetPath(), *pItem->GetMusicInfoTag()); // get tag from file + if (!pItem->GetMusicInfoTag()->Loaded()) + break; // No CDDB info available + } + } + + // construct directory where the tracks are stored + std::string strDirectory; + int legalType; + if (!CreateAlbumDir(*vecItems[0]->GetMusicInfoTag(), strDirectory, legalType)) + return false; + + // rip all tracks one by one + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + for (int i = 0; i < vecItems.Size(); i++) + { + CFileItemPtr item = vecItems[i]; + + // construct filename + std::string strFile = URIUtils::AddFileToFolder( + strDirectory, CUtil::MakeLegalFileName(GetTrackName(item.get()), legalType)); + + // don't rip non cdda items + if (item->GetPath().find(".cdda") == std::string::npos) + continue; + + bool eject = + settings->GetBool(CSettings::SETTING_AUDIOCDS_EJECTONRIP) && i == vecItems.Size() - 1; + AddJob(new CCDDARipJob(item->GetPath(), strFile, *item->GetMusicInfoTag(), + settings->GetInt(CSettings::SETTING_AUDIOCDS_ENCODER), eject)); + } + + return true; +} + +bool CCDDARipper::CreateAlbumDir(const MUSIC_INFO::CMusicInfoTag& infoTag, + std::string& strDirectory, + int& legalType) +{ + std::shared_ptr<CSettingPath> recordingpathSetting = std::static_pointer_cast<CSettingPath>( + CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting( + CSettings::SETTING_AUDIOCDS_RECORDINGPATH)); + if (recordingpathSetting != nullptr) + { + strDirectory = recordingpathSetting->GetValue(); + if (strDirectory.empty()) + { + if (CGUIControlButtonSetting::GetPath(recordingpathSetting, &g_localizeStrings)) + strDirectory = recordingpathSetting->GetValue(); + } + } + URIUtils::AddSlashAtEnd(strDirectory); + + if (strDirectory.size() < 3) + { + // no rip path has been set, show error + CLog::Log(LOGERROR, "CCDDARipper::{} - Required path has not been set", __func__); + HELPERS::ShowOKDialogText(CVariant{257}, CVariant{608}); + return false; + } + + legalType = LEGAL_NONE; + CFileItem ripPath(strDirectory, true); + if (ripPath.IsSmb()) + legalType = LEGAL_WIN32_COMPAT; +#ifdef TARGET_WINDOWS + if (ripPath.IsHD()) + legalType = LEGAL_WIN32_COMPAT; +#endif + + std::string strAlbumDir = GetAlbumDirName(infoTag); + + if (!strAlbumDir.empty()) + { + strDirectory = URIUtils::AddFileToFolder(strDirectory, strAlbumDir); + URIUtils::AddSlashAtEnd(strDirectory); + } + + strDirectory = CUtil::MakeLegalPath(strDirectory, legalType); + + // Create directory if it doesn't exist + if (!CUtil::CreateDirectoryEx(strDirectory)) + { + CLog::Log(LOGERROR, "CCDDARipper::{} - Unable to create directory '{}'", __func__, + strDirectory); + return false; + } + + return true; +} + +std::string CCDDARipper::GetAlbumDirName(const MUSIC_INFO::CMusicInfoTag& infoTag) +{ + std::string strAlbumDir; + + // use audiocds.trackpathformat setting to format + // directory name where CD tracks will be stored, + // use only format part ending at the last '/' + strAlbumDir = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString( + CSettings::SETTING_AUDIOCDS_TRACKPATHFORMAT); + size_t pos = strAlbumDir.find_last_of("/\\"); + if (pos == std::string::npos) + return ""; // no directory + + strAlbumDir = strAlbumDir.substr(0, pos); + + // replace %A with album artist name + if (strAlbumDir.find("%A") != std::string::npos) + { + std::string strAlbumArtist = infoTag.GetAlbumArtistString(); + if (strAlbumArtist.empty()) + strAlbumArtist = infoTag.GetArtistString(); + if (strAlbumArtist.empty()) + strAlbumArtist = "Unknown Artist"; + else + StringUtils::Replace(strAlbumArtist, '/', '_'); + StringUtils::Replace(strAlbumDir, "%A", strAlbumArtist); + } + + // replace %B with album title + if (strAlbumDir.find("%B") != std::string::npos) + { + std::string strAlbum = infoTag.GetAlbum(); + if (strAlbum.empty()) + strAlbum = StringUtils::Format("Unknown Album {}", + CDateTime::GetCurrentDateTime().GetAsLocalizedDateTime()); + else + StringUtils::Replace(strAlbum, '/', '_'); + StringUtils::Replace(strAlbumDir, "%B", strAlbum); + } + + // replace %G with genre + if (strAlbumDir.find("%G") != std::string::npos) + { + std::string strGenre = StringUtils::Join( + infoTag.GetGenre(), + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator); + if (strGenre.empty()) + strGenre = "Unknown Genre"; + else + StringUtils::Replace(strGenre, '/', '_'); + StringUtils::Replace(strAlbumDir, "%G", strGenre); + } + + // replace %Y with year + if (strAlbumDir.find("%Y") != std::string::npos) + { + std::string strYear = infoTag.GetYearString(); + if (strYear.empty()) + strYear = "Unknown Year"; + else + StringUtils::Replace(strYear, '/', '_'); + StringUtils::Replace(strAlbumDir, "%Y", strYear); + } + + return strAlbumDir; +} + +std::string CCDDARipper::GetTrackName(CFileItem* item) +{ + // get track number from "cdda://local/01.cdda" + int trackNumber = atoi(item->GetPath().substr(13, item->GetPath().size() - 13 - 5).c_str()); + + // Format up our ripped file label + CFileItem destItem(*item); + destItem.SetLabel(""); + + // get track file name format from audiocds.trackpathformat setting, + // use only format part starting from the last '/' + std::string strFormat = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString( + CSettings::SETTING_AUDIOCDS_TRACKPATHFORMAT); + size_t pos = strFormat.find_last_of("/\\"); + if (pos != std::string::npos) + strFormat.erase(0, pos + 1); + + CLabelFormatter formatter(strFormat, ""); + formatter.FormatLabel(&destItem); + + // grab the label to use it as our ripped filename + std::string track = destItem.GetLabel(); + if (track.empty()) + track = StringUtils::Format("{}{:02}", "Track-", trackNumber); + + const std::string encoder = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString( + CSettings::SETTING_AUDIOCDS_ENCODER); + const AddonInfoPtr addonInfo = + CServiceBroker::GetAddonMgr().GetAddonInfo(encoder, AddonType::AUDIOENCODER); + if (addonInfo) + track += addonInfo->Type(AddonType::AUDIOENCODER)->GetValue("@extension").asString(); + + return track; +} + +void CCDDARipper::OnJobComplete(unsigned int jobID, bool success, CJob* job) +{ + if (success) + { + if (CJobQueue::QueueEmpty()) + { + std::string dir = URIUtils::GetDirectory(static_cast<CCDDARipJob*>(job)->GetOutput()); + bool unimportant; + int source = CUtil::GetMatchingSource( + dir, *CMediaSourceSettings::GetInstance().CMediaSourceSettings::GetSources("music"), + unimportant); + + CMusicDatabase database; + database.Open(); + if (source >= 0 && database.InsideScannedPath(dir)) + CMusicLibraryQueue::GetInstance().ScanLibrary( + dir, MUSIC_INFO::CMusicInfoScanner::SCAN_NORMAL, false); + + database.Close(); + } + return CJobQueue::OnJobComplete(jobID, success, job); + } + + CancelJobs(); +} diff --git a/xbmc/cdrip/CDDARipper.h b/xbmc/cdrip/CDDARipper.h new file mode 100644 index 0000000..0f61b7c --- /dev/null +++ b/xbmc/cdrip/CDDARipper.h @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "utils/JobManager.h" + +#include <string> + +class CFileItem; + +namespace MUSIC_INFO +{ +class CMusicInfoTag; +} + +namespace KODI +{ +namespace CDRIP +{ + +/*! + * \brief Rip an entire CD or a single track + * + * The CCDDARipper class is used to rip an entire CD or just a single track. + * Tracks are stored in a folder constructed from two user settings: audiocds.recordingpath and + * audiocds.trackpathformat. The former is the absolute file system path for the root folder + * where ripped music is stored, and the latter specifies the format for the album subfolder and + * for the track file name. + * Format used to encode ripped tracks is defined by the audiocds.encoder user setting, and + * there are several choices: wav, ogg vorbis and mp3. + */ +class CCDDARipper : public CJobQueue +{ +public: + /*! + * \brief The only way through which the global instance of the CDDARipper should be accessed. + * + * \return the global instance. + */ + static CCDDARipper& GetInstance(); + + /*! + * \brief Rip a single track + * + * \param[in] pItem CFileItem representing a track to rip + * \return true if success, false if failure + */ + bool RipTrack(CFileItem* pItem); + + /*! + * \brief Rip an entire CD + * + * \return true if success, false if failure + */ + bool RipCD(); + + void OnJobComplete(unsigned int jobID, bool success, CJob* job) override; + +private: + // private construction and no assignments + CCDDARipper(); + CCDDARipper(const CCDDARipper&) = delete; + ~CCDDARipper() override; + CCDDARipper const& operator=(CCDDARipper const&) = delete; + + /*! + * \brief Create folder where CD tracks will be stored + * + * \param[in] infoTag music info tags for the CD, used to format album name + * \param[out] strDirectory full path of the created folder + * \param[out] legalType created directory type (see LEGAL_... constants) + * \return true if success, false if failure + */ + bool CreateAlbumDir(const MUSIC_INFO::CMusicInfoTag& infoTag, + std::string& strDirectory, + int& legalType); + + /*! + * \brief Return formatted album subfolder for rip path + * + * \param infoTag music info tags for the CD, used to format album name + * \return album subfolder path name + */ + std::string GetAlbumDirName(const MUSIC_INFO::CMusicInfoTag& infoTag); + + /*! + * \brief Return file name for the track + * + * \param[in] item CFileItem representing a track + * \return track file name + */ + std::string GetTrackName(CFileItem* item); +}; + +} /* namespace CDRIP */ +} /* namespace KODI */ diff --git a/xbmc/cdrip/CMakeLists.txt b/xbmc/cdrip/CMakeLists.txt new file mode 100644 index 0000000..4e591d2 --- /dev/null +++ b/xbmc/cdrip/CMakeLists.txt @@ -0,0 +1,17 @@ +set(SOURCES CDDARipJob.cpp + Encoder.cpp + EncoderAddon.cpp + EncoderFFmpeg.cpp) + +set(HEADERS CDDARipJob.h + Encoder.h + EncoderAddon.h + EncoderFFmpeg.h + IEncoder.h) + +if(ENABLE_OPTICAL) + list(APPEND SOURCES CDDARipper.cpp) + list(APPEND HEADERS CDDARipper.h) +endif() + +core_add_library(cdrip) diff --git a/xbmc/cdrip/Encoder.cpp b/xbmc/cdrip/Encoder.cpp new file mode 100644 index 0000000..fa78f84 --- /dev/null +++ b/xbmc/cdrip/Encoder.cpp @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "Encoder.h" + +#include "filesystem/File.h" +#include "utils/log.h" + +#include <string.h> +#include <utility> + +using namespace KODI::CDRIP; + +CEncoder::CEncoder() = default; + +CEncoder::~CEncoder() +{ + FileClose(); +} + +bool CEncoder::EncoderInit(const std::string& strFile, int iInChannels, int iInRate, int iInBits) +{ + m_dwWriteBufferPointer = 0; + m_strFile = strFile; + m_iInChannels = iInChannels; + m_iInSampleRate = iInRate; + m_iInBitsPerSample = iInBits; + + if (!FileCreate(strFile)) + { + CLog::Log(LOGERROR, "CEncoder::{} - Cannot open file: {}", __func__, strFile); + return false; + } + + return Init(); +} + +ssize_t CEncoder::EncoderEncode(uint8_t* pbtStream, size_t nNumBytesRead) +{ + const int iBytes = Encode(pbtStream, nNumBytesRead); + if (iBytes < 0) + { + CLog::Log(LOGERROR, "CEncoder::{} - Internal encoder error: {}", __func__, iBytes); + return 0; + } + return 1; +} + +bool CEncoder::EncoderClose() +{ + if (!Close()) + return false; + + FlushStream(); + FileClose(); + + return true; +} + +bool CEncoder::FileCreate(const std::string& filename) +{ + m_file = std::make_unique<XFILE::CFile>(); + if (m_file) + return m_file->OpenForWrite(filename, true); + return false; +} + +bool CEncoder::FileClose() +{ + if (m_file) + { + m_file->Close(); + m_file.reset(); + } + return true; +} + +// return total bytes written, or -1 on error +ssize_t CEncoder::FileWrite(const uint8_t* pBuffer, size_t iBytes) +{ + if (!m_file) + return -1; + + const ssize_t dwBytesWritten = m_file->Write(pBuffer, iBytes); + if (dwBytesWritten <= 0) + return -1; + + return dwBytesWritten; +} + +ssize_t CEncoder::Seek(ssize_t iFilePosition, int iWhence) +{ + if (!m_file) + return -1; + FlushStream(); + return m_file->Seek(iFilePosition, iWhence); +} + +// write the stream to our writebuffer, and write the buffer to disk if it's full +ssize_t CEncoder::Write(const uint8_t* pBuffer, size_t iBytes) +{ + if ((WRITEBUFFER_SIZE - m_dwWriteBufferPointer) > iBytes) + { + // writebuffer is big enough to fit data + memcpy(m_btWriteBuffer + m_dwWriteBufferPointer, pBuffer, iBytes); + m_dwWriteBufferPointer += iBytes; + return iBytes; + } + else + { + // buffer is not big enough to fit data + if (m_dwWriteBufferPointer == 0) + { + // nothing in our buffer, just write the entire pBuffer to disk + return FileWrite(pBuffer, iBytes); + } + + const size_t dwBytesRemaining = iBytes - (WRITEBUFFER_SIZE - m_dwWriteBufferPointer); + // fill up our write buffer and write it to disk + memcpy(m_btWriteBuffer + m_dwWriteBufferPointer, pBuffer, + (WRITEBUFFER_SIZE - m_dwWriteBufferPointer)); + FileWrite(m_btWriteBuffer, WRITEBUFFER_SIZE); + m_dwWriteBufferPointer = 0; + + // pbtRemaining = pBuffer + bytesWritten + const uint8_t* pbtRemaining = pBuffer + (iBytes - dwBytesRemaining); + if (dwBytesRemaining > WRITEBUFFER_SIZE) + { + // data is not going to fit in our buffer, just write it to disk + if (FileWrite(pbtRemaining, dwBytesRemaining) == -1) + return -1; + return iBytes; + } + else + { + // copy remaining bytes to our currently empty writebuffer + memcpy(m_btWriteBuffer, pbtRemaining, dwBytesRemaining); + m_dwWriteBufferPointer = dwBytesRemaining; + return iBytes; + } + } +} + +// flush the contents of our writebuffer +ssize_t CEncoder::FlushStream() +{ + if (m_dwWriteBufferPointer == 0) + return 0; + + const ssize_t iResult = FileWrite(m_btWriteBuffer, m_dwWriteBufferPointer); + m_dwWriteBufferPointer = 0; + + return iResult; +} diff --git a/xbmc/cdrip/Encoder.h b/xbmc/cdrip/Encoder.h new file mode 100644 index 0000000..644296d --- /dev/null +++ b/xbmc/cdrip/Encoder.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IEncoder.h" + +#include <memory> +#include <stdint.h> +#include <stdio.h> +#include <string> + +namespace XFILE +{ +class CFile; +} + +namespace KODI +{ +namespace CDRIP +{ + +constexpr size_t WRITEBUFFER_SIZE = 131072; // 128k buffer + +class CEncoder : public IEncoder +{ +public: + CEncoder(); + virtual ~CEncoder(); + + bool EncoderInit(const std::string& strFile, int iInChannels, int iInRate, int iInBits); + ssize_t EncoderEncode(uint8_t* pbtStream, size_t nNumBytesRead); + bool EncoderClose(); + + void SetComment(const std::string& str) { m_strComment = str; } + void SetArtist(const std::string& str) { m_strArtist = str; } + void SetTitle(const std::string& str) { m_strTitle = str; } + void SetAlbum(const std::string& str) { m_strAlbum = str; } + void SetAlbumArtist(const std::string& str) { m_strAlbumArtist = str; } + void SetGenre(const std::string& str) { m_strGenre = str; } + void SetTrack(const std::string& str) { m_strTrack = str; } + void SetTrackLength(int length) { m_iTrackLength = length; } + void SetYear(const std::string& str) { m_strYear = str; } + +protected: + virtual ssize_t Write(const uint8_t* pBuffer, size_t iBytes); + virtual ssize_t Seek(ssize_t iFilePosition, int iWhence); + +private: + bool FileCreate(const std::string& filename); + bool FileClose(); + ssize_t FileWrite(const uint8_t* pBuffer, size_t iBytes); + ssize_t FlushStream(); + + std::unique_ptr<XFILE::CFile> m_file; + + uint8_t m_btWriteBuffer[WRITEBUFFER_SIZE]; // 128k buffer for writing to disc + size_t m_dwWriteBufferPointer{0}; +}; + +} /* namespace CDRIP */ +} /* namespace KODI */ diff --git a/xbmc/cdrip/EncoderAddon.cpp b/xbmc/cdrip/EncoderAddon.cpp new file mode 100644 index 0000000..4d250ad --- /dev/null +++ b/xbmc/cdrip/EncoderAddon.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2013 Arne Morten Kvarving + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "EncoderAddon.h" + +using namespace ADDON; +using namespace KODI::CDRIP; + +CEncoderAddon::CEncoderAddon(const AddonInfoPtr& addonInfo) + : IAddonInstanceHandler(ADDON_INSTANCE_AUDIOENCODER, addonInfo) +{ + // Create "C" interface structures, used as own parts to prevent API problems on update + m_ifc.audioencoder = new AddonInstance_AudioEncoder(); + m_ifc.audioencoder->toAddon = new KodiToAddonFuncTable_AudioEncoder(); + m_ifc.audioencoder->toKodi = new AddonToKodiFuncTable_AudioEncoder(); + m_ifc.audioencoder->toKodi->kodiInstance = this; + m_ifc.audioencoder->toKodi->write = cb_write; + m_ifc.audioencoder->toKodi->seek = cb_seek; +} + +CEncoderAddon::~CEncoderAddon() +{ + // Delete "C" interface structures + delete m_ifc.audioencoder->toKodi; + delete m_ifc.audioencoder->toAddon; + delete m_ifc.audioencoder; +} + +bool CEncoderAddon::Init() +{ + if (CreateInstance() != ADDON_STATUS_OK || !m_ifc.audioencoder->toAddon->start) + return false; + + KODI_ADDON_AUDIOENCODER_INFO_TAG tag{}; + tag.channels = m_iInChannels; + tag.samplerate = m_iInSampleRate; + tag.bits_per_sample = m_iInBitsPerSample; + tag.track_length = m_iTrackLength; + tag.title = m_strTitle.c_str(); + tag.artist = m_strArtist.c_str(); + tag.album_artist = m_strAlbumArtist.c_str(); + tag.album = m_strAlbum.c_str(); + tag.release_date = m_strYear.c_str(); + tag.track = atoi(m_strTrack.c_str()); + tag.genre = m_strGenre.c_str(); + tag.comment = m_strComment.c_str(); + + return m_ifc.audioencoder->toAddon->start(m_ifc.hdl, &tag); +} + +ssize_t CEncoderAddon::Encode(uint8_t* pbtStream, size_t nNumBytesRead) +{ + if (m_ifc.audioencoder->toAddon->encode) + return m_ifc.audioencoder->toAddon->encode(m_ifc.hdl, pbtStream, nNumBytesRead); + return 0; +} + +bool CEncoderAddon::Close() +{ + bool ret = false; + if (m_ifc.audioencoder->toAddon->finish) + ret = m_ifc.audioencoder->toAddon->finish(m_ifc.hdl); + + DestroyInstance(); + + return ret; +} + +ssize_t CEncoderAddon::Write(const uint8_t* data, size_t len) +{ + return CEncoder::Write(data, len); +} + +ssize_t CEncoderAddon::Seek(ssize_t pos, int whence) +{ + return CEncoder::Seek(pos, whence); +} + +ssize_t CEncoderAddon::cb_write(KODI_HANDLE kodiInstance, const uint8_t* data, size_t len) +{ + if (!kodiInstance || !data) + return -1; + return static_cast<CEncoderAddon*>(kodiInstance)->Write(data, len); +} + +ssize_t CEncoderAddon::cb_seek(KODI_HANDLE kodiInstance, ssize_t pos, int whence) +{ + if (!kodiInstance) + return -1; + return static_cast<CEncoderAddon*>(kodiInstance)->Seek(pos, whence); +} diff --git a/xbmc/cdrip/EncoderAddon.h b/xbmc/cdrip/EncoderAddon.h new file mode 100644 index 0000000..6721d28 --- /dev/null +++ b/xbmc/cdrip/EncoderAddon.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2013 Arne Morten Kvarving + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "Encoder.h" +#include "addons/binary-addons/AddonInstanceHandler.h" +#include "addons/kodi-dev-kit/include/kodi/addon-instance/AudioEncoder.h" + +namespace KODI +{ +namespace CDRIP +{ + +class CEncoderAddon : public CEncoder, public ADDON::IAddonInstanceHandler +{ +public: + explicit CEncoderAddon(const ADDON::AddonInfoPtr& addonInfo); + ~CEncoderAddon() override; + + // Child functions related to IEncoder within CEncoder + bool Init() override; + ssize_t Encode(uint8_t* pbtStream, size_t nNumBytesRead) override; + bool Close() override; + + // Addon callback functions + ssize_t Write(const uint8_t* data, size_t len) override; + ssize_t Seek(ssize_t pos, int whence) override; + +private: + // Currently needed addon interface parts + //@{ + static ssize_t cb_write(KODI_HANDLE kodiInstance, const uint8_t* data, size_t len); + static ssize_t cb_seek(KODI_HANDLE kodiInstance, ssize_t pos, int whence); + //@} +}; + +} /* namespace CDRIP */ +} /* namespace KODI */ diff --git a/xbmc/cdrip/EncoderFFmpeg.cpp b/xbmc/cdrip/EncoderFFmpeg.cpp new file mode 100644 index 0000000..2867b7c --- /dev/null +++ b/xbmc/cdrip/EncoderFFmpeg.cpp @@ -0,0 +1,413 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "EncoderFFmpeg.h" + +#include "ServiceBroker.h" +#include "addons/Addon.h" +#include "addons/AddonManager.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/SystemInfo.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +using namespace KODI::CDRIP; + +namespace +{ + +struct EncoderException : public std::exception +{ + std::string s; + template<typename... Args> + EncoderException(const std::string& fmt, Args&&... args) + : s(StringUtils::Format(fmt, std::forward<Args>(args)...)) + { + } + ~EncoderException() throw() {} // Updated + const char* what() const throw() { return s.c_str(); } +}; + +} /* namespace */ + +bool CEncoderFFmpeg::Init() +{ + try + { + ADDON::AddonPtr addon; + const std::string addonId = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString( + CSettings::SETTING_AUDIOCDS_ENCODER); + bool success = + CServiceBroker::GetAddonMgr().GetAddon(addonId, addon, ADDON::OnlyEnabled::CHOICE_YES); + int bitrate; + if (success && addon) + { + addon->GetSettingInt("bitrate", bitrate); + bitrate *= 1000; /* Multiply as on settings as kbps */ + } + else + { + throw EncoderException("Could not get add-on: {}", addonId); + } + + // Hack fix about PTS on generated files. + // - AAC need to multiply with sample rate + // - Note: Within Kodi it can still played without use of sample rate, only becomes by VLC the problem visible, + // - WMA need only the multiply with 1000 + if (addonId == "audioencoder.kodi.builtin.aac") + m_samplesCountMultiply = m_iInSampleRate; + else if (addonId == "audioencoder.kodi.builtin.wma") + m_samplesCountMultiply = 1000; + else + throw EncoderException("Internal add-on id \"{}\" not known as usable", addonId); + + const std::string filename = URIUtils::GetFileName(m_strFile); + + m_formatCtx = avformat_alloc_context(); + if (!m_formatCtx) + throw EncoderException("Could not allocate output format context"); + + m_bcBuffer = static_cast<uint8_t*>(av_malloc(BUFFER_SIZE + AV_INPUT_BUFFER_PADDING_SIZE)); + if (!m_bcBuffer) + throw EncoderException("Could not allocate buffer"); + + m_formatCtx->pb = avio_alloc_context(m_bcBuffer, BUFFER_SIZE, AVIO_FLAG_WRITE, this, nullptr, + avio_write_callback, avio_seek_callback); + if (!m_formatCtx->pb) + throw EncoderException("Failed to allocate ByteIOContext"); + + /* Guess the desired container format based on the file extension. */ + m_formatCtx->oformat = av_guess_format(nullptr, filename.c_str(), nullptr); + if (!m_formatCtx->oformat) + throw EncoderException("Could not find output file format"); + + m_formatCtx->url = av_strdup(filename.c_str()); + if (!m_formatCtx->url) + throw EncoderException("Could not allocate url"); + + /* Find the encoder to be used by its name. */ + AVCodec* codec = avcodec_find_encoder(m_formatCtx->oformat->audio_codec); + if (!codec) + throw EncoderException("Unable to find a suitable FFmpeg encoder"); + + /* Create a new audio stream in the output file container. */ + m_stream = avformat_new_stream(m_formatCtx, nullptr); + if (!m_stream) + throw EncoderException("Failed to allocate AVStream context"); + + m_codecCtx = avcodec_alloc_context3(codec); + if (!m_codecCtx) + throw EncoderException("Failed to allocate the encoder context"); + + /* Set the basic encoder parameters. + * The input file's sample rate is used to avoid a sample rate conversion. */ + m_codecCtx->channels = m_iInChannels; + m_codecCtx->channel_layout = av_get_default_channel_layout(m_iInChannels); + m_codecCtx->sample_rate = m_iInSampleRate; + m_codecCtx->sample_fmt = codec->sample_fmts[0]; + m_codecCtx->bit_rate = bitrate; + + /* Allow experimental encoders (like FFmpeg builtin AAC encoder) */ + m_codecCtx->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL; + + /* Set the sample rate for the container. */ + m_codecCtx->time_base.num = 1; + m_codecCtx->time_base.den = m_iInSampleRate; + + /* Some container formats (like MP4) require global headers to be present. + * Mark the encoder so that it behaves accordingly. */ + if (m_formatCtx->oformat->flags & AVFMT_GLOBALHEADER) + m_codecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + + int err = avcodec_open2(m_codecCtx, codec, nullptr); + if (err < 0) + throw EncoderException("Failed to open the codec {} (error '{}')", + codec->long_name ? codec->long_name : codec->name, + FFmpegErrorToString(err)); + + err = avcodec_parameters_from_context(m_stream->codecpar, m_codecCtx); + if (err < 0) + throw EncoderException("Failed to copy encoder parameters to output stream (error '{}')", + FFmpegErrorToString(err)); + + m_inFormat = GetInputFormat(m_iInBitsPerSample); + m_outFormat = m_codecCtx->sample_fmt; + m_needConversion = (m_outFormat != m_inFormat); + + /* calculate how many bytes we need per frame */ + m_neededFrames = m_codecCtx->frame_size; + m_neededBytes = + av_samples_get_buffer_size(nullptr, m_iInChannels, m_neededFrames, m_inFormat, 0); + m_buffer = static_cast<uint8_t*>(av_malloc(m_neededBytes)); + m_bufferSize = 0; + + m_bufferFrame = av_frame_alloc(); + if (!m_bufferFrame || !m_buffer) + throw EncoderException("Failed to allocate necessary buffers"); + + m_bufferFrame->nb_samples = m_codecCtx->frame_size; + m_bufferFrame->format = m_inFormat; + m_bufferFrame->channel_layout = m_codecCtx->channel_layout; + m_bufferFrame->sample_rate = m_codecCtx->sample_rate; + + err = av_frame_get_buffer(m_bufferFrame, 0); + if (err < 0) + throw EncoderException("Could not allocate output frame samples (error '{}')", + FFmpegErrorToString(err)); + + avcodec_fill_audio_frame(m_bufferFrame, m_iInChannels, m_inFormat, m_buffer, m_neededBytes, 0); + + if (m_needConversion) + { + m_swrCtx = swr_alloc_set_opts(nullptr, m_codecCtx->channel_layout, m_outFormat, + m_codecCtx->sample_rate, m_codecCtx->channel_layout, m_inFormat, + m_codecCtx->sample_rate, 0, nullptr); + if (!m_swrCtx || swr_init(m_swrCtx) < 0) + throw EncoderException("Failed to initialize the resampler"); + + m_resampledBufferSize = + av_samples_get_buffer_size(nullptr, m_iInChannels, m_neededFrames, m_outFormat, 0); + m_resampledBuffer = static_cast<uint8_t*>(av_malloc(m_resampledBufferSize)); + m_resampledFrame = av_frame_alloc(); + if (!m_resampledBuffer || !m_resampledFrame) + throw EncoderException("Failed to allocate a frame for resampling"); + + m_resampledFrame->nb_samples = m_neededFrames; + m_resampledFrame->format = m_outFormat; + m_resampledFrame->channel_layout = m_codecCtx->channel_layout; + m_resampledFrame->sample_rate = m_codecCtx->sample_rate; + + err = av_frame_get_buffer(m_resampledFrame, 0); + if (err < 0) + throw EncoderException("Could not allocate output resample frame samples (error '{}')", + FFmpegErrorToString(err)); + + avcodec_fill_audio_frame(m_resampledFrame, m_iInChannels, m_outFormat, m_resampledBuffer, + m_resampledBufferSize, 0); + } + + /* set the tags */ + SetTag("album", m_strAlbum); + SetTag("album_artist", m_strArtist); + SetTag("genre", m_strGenre); + SetTag("title", m_strTitle); + SetTag("track", m_strTrack); + SetTag("encoder", CSysInfo::GetAppName() + " FFmpeg Encoder"); + + /* write the header */ + err = avformat_write_header(m_formatCtx, nullptr); + if (err != 0) + throw EncoderException("Failed to write the header (error '{}')", FFmpegErrorToString(err)); + + CLog::Log(LOGDEBUG, "CEncoderFFmpeg::{} - Successfully initialized with muxer {} and codec {}", + __func__, + m_formatCtx->oformat->long_name ? m_formatCtx->oformat->long_name + : m_formatCtx->oformat->name, + codec->long_name ? codec->long_name : codec->name); + } + catch (EncoderException& caught) + { + CLog::Log(LOGERROR, "CEncoderFFmpeg::{} - {}", __func__, caught.what()); + + av_freep(&m_buffer); + av_frame_free(&m_bufferFrame); + swr_free(&m_swrCtx); + av_frame_free(&m_resampledFrame); + av_freep(&m_resampledBuffer); + av_free(m_bcBuffer); + avcodec_free_context(&m_codecCtx); + if (m_formatCtx) + { + av_freep(&m_formatCtx->pb); + avformat_free_context(m_formatCtx); + } + return false; + } + + return true; +} + +void CEncoderFFmpeg::SetTag(const std::string& tag, const std::string& value) +{ + av_dict_set(&m_formatCtx->metadata, tag.c_str(), value.c_str(), 0); +} + +int CEncoderFFmpeg::avio_write_callback(void* opaque, uint8_t* buf, int buf_size) +{ + CEncoderFFmpeg* enc = static_cast<CEncoderFFmpeg*>(opaque); + if (enc->Write(buf, buf_size) != buf_size) + { + CLog::Log(LOGERROR, "CEncoderFFmpeg::{} - Error writing FFmpeg buffer to file", __func__); + return -1; + } + return buf_size; +} + +int64_t CEncoderFFmpeg::avio_seek_callback(void* opaque, int64_t offset, int whence) +{ + CEncoderFFmpeg* enc = static_cast<CEncoderFFmpeg*>(opaque); + return enc->Seek(offset, whence); +} + +ssize_t CEncoderFFmpeg::Encode(uint8_t* pbtStream, size_t nNumBytesRead) +{ + while (nNumBytesRead > 0) + { + size_t space = m_neededBytes - m_bufferSize; + size_t copy = nNumBytesRead > space ? space : nNumBytesRead; + + memcpy(&m_buffer[m_bufferSize], pbtStream, copy); + m_bufferSize += copy; + pbtStream += copy; + nNumBytesRead -= copy; + + /* only write full packets */ + if (m_bufferSize == m_neededBytes) + { + if (!WriteFrame()) + return 0; + } + } + + return 1; +} + +bool CEncoderFFmpeg::WriteFrame() +{ + int err = AVERROR_UNKNOWN; + AVPacket* pkt = av_packet_alloc(); + if (!pkt) + { + CLog::Log(LOGERROR, "CEncoderFFmpeg::{} - av_packet_alloc failed: {}", __func__, + strerror(errno)); + return false; + } + + try + { + AVFrame* frame; + if (m_needConversion) + { + //! @bug libavresample isn't const correct + if (swr_convert(m_swrCtx, m_resampledFrame->data, m_neededFrames, + const_cast<const uint8_t**>(m_bufferFrame->extended_data), + m_neededFrames) < 0) + throw EncoderException("Error resampling audio"); + + frame = m_resampledFrame; + } + else + frame = m_bufferFrame; + + if (frame) + { + /* To fix correct length on wma files */ + frame->pts = m_samplesCount; + m_samplesCount += frame->nb_samples * m_samplesCountMultiply / m_codecCtx->time_base.den; + } + + m_bufferSize = 0; + err = avcodec_send_frame(m_codecCtx, frame); + if (err < 0) + throw EncoderException("Error sending a frame for encoding (error '{}')", __func__, + FFmpegErrorToString(err)); + + while (err >= 0) + { + err = avcodec_receive_packet(m_codecCtx, pkt); + if (err == AVERROR(EAGAIN) || err == AVERROR_EOF) + { + av_packet_free(&pkt); + return (err == AVERROR(EAGAIN)) ? false : true; + } + else if (err < 0) + { + throw EncoderException("Error during encoding (error '{}')", __func__, + FFmpegErrorToString(err)); + } + + err = av_write_frame(m_formatCtx, pkt); + if (err < 0) + throw EncoderException("Failed to write the frame data (error '{}')", __func__, + FFmpegErrorToString(err)); + + av_packet_unref(pkt); + } + } + catch (EncoderException& caught) + { + CLog::Log(LOGERROR, "CEncoderFFmpeg::{} - {}", __func__, caught.what()); + } + + av_packet_free(&pkt); + + return (err) ? false : true; +} + +bool CEncoderFFmpeg::Close() +{ + if (m_formatCtx) + { + /* if there is anything still in the buffer */ + if (m_bufferSize > 0) + { + /* zero the unused space so we dont encode random junk */ + memset(&m_buffer[m_bufferSize], 0, m_neededBytes - m_bufferSize); + /* write any remaining data */ + WriteFrame(); + } + + /* Flush if needed */ + av_freep(&m_buffer); + av_frame_free(&m_bufferFrame); + swr_free(&m_swrCtx); + av_frame_free(&m_resampledFrame); + av_freep(&m_resampledBuffer); + m_needConversion = false; + + WriteFrame(); + + /* write the trailer */ + av_write_trailer(m_formatCtx); + + /* cleanup */ + av_free(m_bcBuffer); + avcodec_free_context(&m_codecCtx); + av_freep(&m_formatCtx->pb); + avformat_free_context(m_formatCtx); + } + + m_bufferSize = 0; + + return true; +} + +AVSampleFormat CEncoderFFmpeg::GetInputFormat(int inBitsPerSample) +{ + switch (inBitsPerSample) + { + case 8: + return AV_SAMPLE_FMT_U8; + case 16: + return AV_SAMPLE_FMT_S16; + case 32: + return AV_SAMPLE_FMT_S32; + default: + throw EncoderException("Invalid input bits per sample"); + } +} + +std::string CEncoderFFmpeg::FFmpegErrorToString(int err) +{ + std::string text; + text.reserve(AV_ERROR_MAX_STRING_SIZE); + av_strerror(err, text.data(), AV_ERROR_MAX_STRING_SIZE); + return text; +} diff --git a/xbmc/cdrip/EncoderFFmpeg.h b/xbmc/cdrip/EncoderFFmpeg.h new file mode 100644 index 0000000..39112ba --- /dev/null +++ b/xbmc/cdrip/EncoderFFmpeg.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "Encoder.h" + +extern "C" +{ +#include <libavcodec/avcodec.h> +#include <libavformat/avformat.h> +#include <libswresample/swresample.h> +} + +namespace KODI +{ +namespace CDRIP +{ + +class CEncoderFFmpeg : public CEncoder +{ +public: + CEncoderFFmpeg() = default; + ~CEncoderFFmpeg() override = default; + + bool Init() override; + ssize_t Encode(uint8_t* pbtStream, size_t nNumBytesRead) override; + bool Close() override; + +private: + static int avio_write_callback(void* opaque, uint8_t* buf, int buf_size); + static int64_t avio_seek_callback(void* opaque, int64_t offset, int whence); + + void SetTag(const std::string& tag, const std::string& value); + bool WriteFrame(); + AVSampleFormat GetInputFormat(int inBitsPerSample); + std::string FFmpegErrorToString(int err); + + AVFormatContext* m_formatCtx{nullptr}; + AVCodecContext* m_codecCtx{nullptr}; + SwrContext* m_swrCtx{nullptr}; + AVStream* m_stream{nullptr}; + AVSampleFormat m_inFormat; + AVSampleFormat m_outFormat; + + /* From libavformat/avio.h: + * The buffer size is very important for performance. + * For protocols with fixed blocksize it should be set to this + * blocksize. + * For others a typical size is a cache page, e.g. 4kb. + */ + static constexpr size_t BUFFER_SIZE = 4096; + uint8_t* m_bcBuffer{nullptr}; + + unsigned int m_neededFrames{0}; + size_t m_neededBytes{0}; + uint8_t* m_buffer{nullptr}; + size_t m_bufferSize{0}; + AVFrame* m_bufferFrame{nullptr}; + uint8_t* m_resampledBuffer{nullptr}; + size_t m_resampledBufferSize{0}; + AVFrame* m_resampledFrame{nullptr}; + bool m_needConversion{false}; + int64_t m_samplesCount{0}; + int64_t m_samplesCountMultiply{1000}; +}; + +} /* namespace CDRIP */ +} /* namespace KODI */ diff --git a/xbmc/cdrip/IEncoder.h b/xbmc/cdrip/IEncoder.h new file mode 100644 index 0000000..de484aa --- /dev/null +++ b/xbmc/cdrip/IEncoder.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <stdint.h> +#include <string> + +#include "PlatformDefs.h" // for ssize_t + +namespace KODI +{ +namespace CDRIP +{ + +class IEncoder +{ +public: + virtual ~IEncoder() = default; + virtual bool Init() = 0; + virtual ssize_t Encode(uint8_t* pbtStream, size_t nNumBytesRead) = 0; + virtual bool Close() = 0; + + // tag info + std::string m_strComment; + std::string m_strArtist; + std::string m_strAlbumArtist; + std::string m_strTitle; + std::string m_strAlbum; + std::string m_strGenre; + std::string m_strTrack; + std::string m_strYear; + std::string m_strFile; + int m_iTrackLength = 0; + int m_iInChannels = 0; + int m_iInSampleRate = 0; + int m_iInBitsPerSample = 0; +}; + +} /* namespace CDRIP */ +} /* namespace KODI */ |