summaryrefslogtreecommitdiffstats
path: root/xbmc/cdrip
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/cdrip')
-rw-r--r--xbmc/cdrip/CDDARipJob.cpp259
-rw-r--r--xbmc/cdrip/CDDARipJob.h92
-rw-r--r--xbmc/cdrip/CDDARipper.cpp326
-rw-r--r--xbmc/cdrip/CDDARipper.h102
-rw-r--r--xbmc/cdrip/CMakeLists.txt17
-rw-r--r--xbmc/cdrip/Encoder.cpp159
-rw-r--r--xbmc/cdrip/Encoder.h67
-rw-r--r--xbmc/cdrip/EncoderAddon.cpp95
-rw-r--r--xbmc/cdrip/EncoderAddon.h43
-rw-r--r--xbmc/cdrip/EncoderFFmpeg.cpp413
-rw-r--r--xbmc/cdrip/EncoderFFmpeg.h74
-rw-r--r--xbmc/cdrip/IEncoder.h46
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 */