mirror of
https://codeberg.org/tenacityteam/tenacity
synced 2025-10-11 03:02:46 +02:00
Some of our basic changes remained, but a lot of work still needs to be done. At least it builds though! I have a few notes off the top of my head writing this: - Our build system was kept and modified to work with the new codebase. Similarly, the codebase was modified to work with our build system. - We don't support Audacity's full wxBase restrictions, so I just replaced it with wxWidgets::wxWidgets for now. (See above). - There are still networking features, which are to be removed in the next commit. - pffft was added as a library in lib-src, and I wrote a new CMakeLists.txt for it. - I modified WrapAllegro.h to use our allegro.h shim instead. Signed-off-by: Avery King <avery98@pm.me>
1837 lines
56 KiB
C++
1837 lines
56 KiB
C++
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
ExportFFmpeg.cpp
|
|
|
|
Audacity(R) is copyright (c) 1999-2009 Audacity Team.
|
|
License: GPL v2 or later. See License.txt.
|
|
|
|
LRN
|
|
|
|
******************************************************************//**
|
|
|
|
\class ExportFFmpeg
|
|
\brief Controlling class for FFmpeg exporting. Creates the options
|
|
dialog of the appropriate type, adds tags and invokes the export
|
|
function.
|
|
|
|
*//*******************************************************************/
|
|
|
|
|
|
#include "../FFmpeg.h"
|
|
#include "FFmpegFunctions.h"
|
|
#include "FifoBuffer.h"
|
|
|
|
#include <wx/app.h>
|
|
#include <wx/log.h>
|
|
|
|
#include <wx/window.h>
|
|
#include <wx/button.h>
|
|
#include <wx/textctrl.h>
|
|
|
|
#include "BasicSettings.h"
|
|
#include "Mix.h"
|
|
#include "Tags.h"
|
|
#include "Track.h"
|
|
#include "wxFileNameWrapper.h"
|
|
|
|
#include "ExportFFmpegOptions.h"
|
|
#include "SelectFile.h"
|
|
#include "ShuttleGui.h"
|
|
|
|
#include "ExportPluginHelpers.h"
|
|
#include "PlainExportOptionsEditor.h"
|
|
#include "FFmpegDefines.h"
|
|
#include "ExportOptionsUIServices.h"
|
|
#include "ExportPluginRegistry.h"
|
|
|
|
#if defined(WIN32) && _MSC_VER < 1900
|
|
#define snprintf _snprintf
|
|
#endif
|
|
|
|
// Define this to automatically resample audio to the nearest supported sample rate
|
|
#define FFMPEG_AUTO_RESAMPLE 1
|
|
|
|
static int AdjustFormatIndex(int format)
|
|
{
|
|
int subFormat = -1;
|
|
for (int i = 0; i <= FMT_OTHER; i++)
|
|
{
|
|
if (ExportFFmpegOptions::fmts[i].compiledIn) subFormat++;
|
|
if (subFormat == format || i == FMT_OTHER)
|
|
{
|
|
subFormat = i;
|
|
break;
|
|
}
|
|
}
|
|
return subFormat;
|
|
}
|
|
|
|
namespace
|
|
{
|
|
|
|
const int iAC3SampleRates[] =
|
|
{ 32000, 44100, 48000, 0 };
|
|
|
|
const int iWMASampleRates[] =
|
|
{ 8000, 11025, 16000, 22050, 44100, 0};
|
|
|
|
///\param rates 0-terminated array
|
|
ExportOptionsEditor::SampleRateList ToSampleRateList(const int* rates)
|
|
{
|
|
ExportOptionsEditor::SampleRateList list;
|
|
int index = 0;
|
|
while(rates[index] != 0)
|
|
list.push_back(rates[index++]);
|
|
return list;
|
|
}
|
|
|
|
// i18n-hint kbps abbreviates "thousands of bits per second"
|
|
TranslatableString n_kbps(int n) { return XO("%d kbps").Format( n ); }
|
|
TranslatableString f_kbps( double d ) { return XO("%.2f kbps").Format( d ); }
|
|
|
|
enum : int
|
|
{
|
|
AC3OptionIDBitRate = 0
|
|
};
|
|
|
|
const std::initializer_list<PlainExportOptionsEditor::OptionDesc> AC3Options {
|
|
{
|
|
{
|
|
AC3OptionIDBitRate, XO("Bit Rate"),
|
|
160000,
|
|
ExportOption::TypeEnum,
|
|
{
|
|
32000,
|
|
40000,
|
|
48000,
|
|
56000,
|
|
64000,
|
|
80000,
|
|
96000,
|
|
112000,
|
|
128000,
|
|
160000,
|
|
192000,
|
|
224000,
|
|
256000,
|
|
320000,
|
|
384000,
|
|
448000,
|
|
512000,
|
|
576000,
|
|
640000
|
|
},
|
|
{
|
|
n_kbps( 32 ),
|
|
n_kbps( 40 ),
|
|
n_kbps( 48 ),
|
|
n_kbps( 56 ),
|
|
n_kbps( 64 ),
|
|
n_kbps( 80 ),
|
|
n_kbps( 96 ),
|
|
n_kbps( 112 ),
|
|
n_kbps( 128 ),
|
|
n_kbps( 160 ),
|
|
n_kbps( 192 ),
|
|
n_kbps( 224 ),
|
|
n_kbps( 256 ),
|
|
n_kbps( 320 ),
|
|
n_kbps( 384 ),
|
|
n_kbps( 448 ),
|
|
n_kbps( 512 ),
|
|
n_kbps( 576 ),
|
|
n_kbps( 640 ),
|
|
}
|
|
}, wxT("/FileFormats/AC3BitRate")
|
|
}
|
|
};
|
|
|
|
enum : int
|
|
{
|
|
AACOptionIDQuality = 0
|
|
};
|
|
|
|
//NB: user-entered values for AAC are not always followed; mono is clamped to 98-160, stereo 196-320
|
|
const std::initializer_list<PlainExportOptionsEditor::OptionDesc> AACOptions {
|
|
{
|
|
{
|
|
AACOptionIDQuality, XO("Quality (kbps)"),
|
|
256,
|
|
ExportOption::TypeRange,
|
|
{98, 320}
|
|
}, wxT("/FileFormats/AACQuality")
|
|
}
|
|
};
|
|
|
|
enum : int
|
|
{
|
|
AMRNBOptionIDBitRate = 0
|
|
};
|
|
|
|
const std::initializer_list<PlainExportOptionsEditor::OptionDesc> AMRNBOptions {
|
|
{
|
|
{
|
|
AMRNBOptionIDBitRate, XO("Bit Rate"),
|
|
12200,
|
|
ExportOption::TypeEnum,
|
|
{
|
|
4750,
|
|
5150,
|
|
5900,
|
|
6700,
|
|
7400,
|
|
7950,
|
|
10200,
|
|
12200,
|
|
},
|
|
{
|
|
f_kbps( 4.75 ),
|
|
f_kbps( 5.15 ),
|
|
f_kbps( 5.90 ),
|
|
f_kbps( 6.70 ),
|
|
f_kbps( 7.40 ),
|
|
f_kbps( 7.95 ),
|
|
f_kbps( 10.20 ),
|
|
f_kbps( 12.20 ),
|
|
}
|
|
}, wxT("/FileFormats/AMRNBBitRate")
|
|
}
|
|
};
|
|
|
|
#ifdef SHOW_FFMPEG_OPUS_EXPORT
|
|
enum : int
|
|
{
|
|
OPUSOptionIDBitRate = 0,
|
|
OPUSOptionIDCompression,
|
|
OPUSOptionIDFrameDuration,
|
|
OPUSOptionIDVBRMode,
|
|
OPUSOptionIDApplication,
|
|
OPUSOptionIDCutoff
|
|
};
|
|
|
|
const std::initializer_list<PlainExportOptionsEditor::OptionDesc> OPUSOptions {
|
|
{
|
|
{
|
|
OPUSOptionIDBitRate, XO("Bit Rate"),
|
|
128000,
|
|
ExportOption::TypeEnum,
|
|
{
|
|
6000,
|
|
8000,
|
|
16000,
|
|
24000,
|
|
32000,
|
|
40000,
|
|
48000,
|
|
64000,
|
|
80000,
|
|
96000,
|
|
128000,
|
|
160000,
|
|
192000,
|
|
256000
|
|
},
|
|
{
|
|
n_kbps( 6 ),
|
|
n_kbps( 8 ),
|
|
n_kbps( 16 ),
|
|
n_kbps( 24 ),
|
|
n_kbps( 32 ),
|
|
n_kbps( 40 ),
|
|
n_kbps( 48 ),
|
|
n_kbps( 64 ),
|
|
n_kbps( 80 ),
|
|
n_kbps( 96 ),
|
|
n_kbps( 128 ),
|
|
n_kbps( 160 ),
|
|
n_kbps( 192 ),
|
|
n_kbps( 256 ),
|
|
}
|
|
}, wxT("/FileFormats/OPUSBitrate")
|
|
},
|
|
{
|
|
{
|
|
OPUSOptionIDCompression, XO("Compression"),
|
|
10,
|
|
ExportOption::TypeRange,
|
|
{ 0, 10 }
|
|
}, wxT("/FileFormats/OPUSCompression")
|
|
},
|
|
{
|
|
{
|
|
OPUSOptionIDFrameDuration, XO("Frame Duration"),
|
|
std::string("20"),
|
|
ExportOption::TypeEnum,
|
|
{
|
|
std::string("2.5"),
|
|
std::string("5"),
|
|
std::string("10"),
|
|
std::string("20"),
|
|
std::string("40"),
|
|
std::string("60")
|
|
},
|
|
{
|
|
XO("2.5 ms"),
|
|
XO("5 ms"),
|
|
XO("10 ms"),
|
|
XO("20 ms"),
|
|
XO("40 ms"),
|
|
XO("60 ms"),
|
|
}
|
|
}, wxT("/FileFormats/OPUSFrameDuration")
|
|
},
|
|
{
|
|
{
|
|
OPUSOptionIDVBRMode, XO("Vbr Mode"),
|
|
std::string("on"),
|
|
ExportOption::TypeEnum,
|
|
{ std::string("off"), std::string("on"), std::string("constrained") },
|
|
{ XO("Off"), XO("On"), XO("Constrained") }
|
|
}, wxT("/FileFormats/OPUSVbrMode")
|
|
},
|
|
{
|
|
{
|
|
OPUSOptionIDApplication, XO("Application"),
|
|
std::string("audio"),
|
|
ExportOption::TypeEnum,
|
|
{ std::string("voip"), std::string("audio"), std::string("lowdelay") },
|
|
{ XO("VOIP"), XO("Audio"), XO("Low Delay") }
|
|
}, wxT("/FileFormats/OPUSApplication")
|
|
},
|
|
{
|
|
{
|
|
OPUSOptionIDCutoff, XO("Cutoff"),
|
|
std::string("0"),
|
|
ExportOption::TypeEnum,
|
|
{
|
|
std::string("0"),
|
|
std::string("4000"),
|
|
std::string("6000"),
|
|
std::string("8000"),
|
|
std::string("12000"),
|
|
std::string("20000")
|
|
},
|
|
{
|
|
XO("Disabled"),
|
|
XO("Narrowband"),
|
|
XO("Mediumband"),
|
|
XO("Wideband"),
|
|
XO("Super Wideband"),
|
|
XO("Fullband")
|
|
}
|
|
}, wxT("/FileFormats/OPUSCutoff")
|
|
},
|
|
};
|
|
#endif
|
|
|
|
enum : int
|
|
{
|
|
WMAOptionIDBitRate = 0
|
|
};
|
|
|
|
const std::initializer_list<PlainExportOptionsEditor::OptionDesc> WMAOptions {
|
|
{
|
|
{
|
|
WMAOptionIDBitRate, XO("Bit Rate"),
|
|
128000,
|
|
ExportOption::TypeEnum,
|
|
{
|
|
24000,
|
|
32000,
|
|
40000,
|
|
48000,
|
|
64000,
|
|
80000,
|
|
96000,
|
|
128000,
|
|
160000,
|
|
192000,
|
|
256000,
|
|
320000
|
|
},
|
|
{
|
|
n_kbps(24),
|
|
n_kbps(32),
|
|
n_kbps(40),
|
|
n_kbps(48),
|
|
n_kbps(64),
|
|
n_kbps(80),
|
|
n_kbps(96),
|
|
n_kbps(128),
|
|
n_kbps(160),
|
|
n_kbps(192),
|
|
n_kbps(256),
|
|
n_kbps(320),
|
|
}
|
|
}, wxT("/FileFormats/WMABitRate")
|
|
}
|
|
};
|
|
|
|
const std::vector<ExportOption> FFmpegOptions {
|
|
{ FELanguageID, {}, std::string() },
|
|
{ FESampleRateID, {}, 0 },
|
|
{ FEBitrateID, {}, 0 },
|
|
{ FETagID, {}, std::string() },
|
|
{ FEQualityID, {}, 0 },
|
|
{ FECutoffID, {}, 0},
|
|
{ FEBitReservoirID, {}, true },
|
|
{ FEVariableBlockLenID, {}, true },
|
|
{ FECompLevelID, {}, -1 },
|
|
{ FEFrameSizeID, {}, 0 },
|
|
{ FELPCCoeffsID, {}, 0 },
|
|
{ FEMinPredID, {}, -1 },
|
|
{ FEMaxPredID, {}, -1 },
|
|
{ FEMinPartOrderID, {}, -1 },
|
|
{ FEMaxPartOrderID, {}, -1 },
|
|
{ FEPredOrderID, {}, 0 },
|
|
{ FEMuxRateID, {}, 0 },
|
|
{ FEPacketSizeID, {}, 0 },
|
|
{ FECodecID, {}, std::string() },
|
|
{ FEFormatID, {}, std::string() }
|
|
};
|
|
|
|
class ExportOptionsFFmpegCustomEditor
|
|
: public ExportOptionsEditor
|
|
, public ExportOptionsUIServices
|
|
{
|
|
std::unordered_map<int, ExportValue> mValues;
|
|
std::shared_ptr<FFmpegFunctions> mFFmpeg;
|
|
ExportOptionsEditor::Listener* mListener{};
|
|
//created on-demand
|
|
mutable std::unique_ptr<AVCodecWrapper> mAVCodec;
|
|
public:
|
|
|
|
ExportOptionsFFmpegCustomEditor(ExportOptionsEditor::Listener* listener = nullptr)
|
|
: mListener(listener)
|
|
{
|
|
}
|
|
|
|
void PopulateUI(ShuttleGui& S) override
|
|
{
|
|
CheckFFmpeg(true);
|
|
//Continue anyway, as we do not need ffmpeg functions to build and fill in the UI
|
|
|
|
mParent = S.GetParent();
|
|
|
|
S.StartHorizontalLay(wxCENTER);
|
|
{
|
|
S.StartVerticalLay(wxCENTER, 0);
|
|
{
|
|
S.AddButton(XXO("Open custom FFmpeg format options"))
|
|
->Bind(wxEVT_BUTTON, &ExportOptionsFFmpegCustomEditor::OnOpen, this);
|
|
S.StartMultiColumn(2, wxCENTER);
|
|
{
|
|
S.AddPrompt(XXO("Current Format:"));
|
|
mFormat = S.Name(XXO("Current Format:"))
|
|
.Style(wxTE_READONLY).AddTextBox({}, wxT(""), 25);
|
|
S.AddPrompt(XXO("Current Codec:"));
|
|
mCodec = S.Name(XXO("Current Codec:"))
|
|
.Style(wxTE_READONLY).AddTextBox({}, wxT(""), 25);
|
|
}
|
|
S.EndMultiColumn();
|
|
}
|
|
S.EndHorizontalLay();
|
|
}
|
|
S.EndHorizontalLay();
|
|
|
|
UpdateCodecAndFormat();
|
|
}
|
|
|
|
bool TransferDataFromWindow() override
|
|
{
|
|
Load(*gPrefs);
|
|
return true;
|
|
}
|
|
|
|
int GetOptionsCount() const override
|
|
{
|
|
return static_cast<int>(FFmpegOptions.size());
|
|
}
|
|
|
|
bool GetOption(int index, ExportOption& option) const override
|
|
{
|
|
if(index >= 0 && index < FFmpegOptions.size())
|
|
{
|
|
option = FFmpegOptions[index];
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool GetValue(int id, ExportValue& value) const override
|
|
{
|
|
auto it = mValues.find(id);
|
|
if(it != mValues.end())
|
|
{
|
|
value = it->second;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool SetValue(int id, const ExportValue& value) override
|
|
{
|
|
return false;
|
|
}
|
|
|
|
SampleRateList GetSampleRateList() const override
|
|
{
|
|
if(!mAVCodec)
|
|
{
|
|
auto it = mValues.find(FECodecID);
|
|
if(it == mValues.end())
|
|
return {};
|
|
|
|
const auto codecId = *std::get_if<std::string>(&it->second);
|
|
if (mFFmpeg) {
|
|
mAVCodec = mFFmpeg->CreateEncoder(codecId.c_str());
|
|
}
|
|
}
|
|
if(!mAVCodec)
|
|
return {};
|
|
|
|
if(const auto rates = mAVCodec->GetSupportedSamplerates())
|
|
return ToSampleRateList(rates);
|
|
return {};
|
|
}
|
|
|
|
void Load(const audacity::BasicSettings& config) override
|
|
{
|
|
mValues[FELanguageID] = std::string(config.Read(wxT("/FileFormats/FFmpegLanguage"), wxT("")).ToUTF8());
|
|
mValues[FESampleRateID] = static_cast<int>(config.Read(wxT("/FileFormats/FFmpegSampleRate"), 0L));
|
|
mValues[FEBitrateID] = static_cast<int>(config.Read(wxT("/FileFormats/FFmpegBitRate"), 0L));
|
|
mValues[FETagID] = std::string(config.Read(wxT("/FileFormats/FFmpegTag"), wxT(""))
|
|
.mb_str(wxConvUTF8));
|
|
mValues[FEQualityID] = static_cast<int>(config.Read(wxT("/FileFormats/FFmpegQuality"), -99999L));
|
|
mValues[FECutoffID] = static_cast<int>(config.Read(wxT("/FileFormats/FFmpegCutOff"), 0L));
|
|
mValues[FEBitReservoirID] = config.ReadBool(wxT("/FileFormats/FFmpegBitReservoir"), true);
|
|
mValues[FEVariableBlockLenID] = config.ReadBool(wxT("/FileFormats/FFmpegVariableBlockLen"), true);
|
|
mValues[FECompLevelID] = static_cast<int>(config.Read(wxT("/FileFormats/FFmpegCompLevel"), -1L));
|
|
mValues[FEFrameSizeID] = static_cast<int>(config.Read(wxT("/FileFormats/FFmpegFrameSize"), 0L));
|
|
|
|
mValues[FELPCCoeffsID] = static_cast<int>(config.Read(wxT("/FileFormats/FFmpegLPCCoefPrec"), 0L));
|
|
mValues[FEMinPredID] = static_cast<int>(config.Read(wxT("/FileFormats/FFmpegMinPredOrder"), -1L));
|
|
mValues[FEMaxPredID] = static_cast<int>(config.Read(wxT("/FileFormats/FFmpegMaxPredOrder"), -1L));
|
|
mValues[FEMinPartOrderID] = static_cast<int>(config.Read(wxT("/FileFormats/FFmpegMinPartOrder"), -1L));
|
|
mValues[FEMaxPartOrderID] = static_cast<int>(config.Read(wxT("/FileFormats/FFmpegMaxPartOrder"), -1L));
|
|
mValues[FEPredOrderID] = static_cast<int>(config.Read(wxT("/FileFormats/FFmpegPredOrderMethod"), 0L));
|
|
mValues[FEMuxRateID] = static_cast<int>(config.Read(wxT("/FileFormats/FFmpegMuxRate"), 0L));
|
|
mValues[FEPacketSizeID] = static_cast<int>(config.Read(wxT("/FileFormats/FFmpegPacketSize"), 0L));
|
|
mValues[FECodecID] = std::string(config.Read(wxT("/FileFormats/FFmpegCodec")));
|
|
mValues[FEFormatID] = std::string(config.Read(wxT("/FileFormats/FFmpegFormat")));
|
|
}
|
|
|
|
void Store(audacity::BasicSettings& settings) const override
|
|
{
|
|
|
|
}
|
|
|
|
private:
|
|
|
|
bool CheckFFmpeg(bool showError)
|
|
{
|
|
// Show "Locate FFmpeg" dialog
|
|
if(!mFFmpeg)
|
|
{
|
|
mFFmpeg = FFmpegFunctions::Load();
|
|
if (!mFFmpeg)
|
|
{
|
|
FindFFmpegLibs();
|
|
return LoadFFmpeg(showError);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void UpdateCodecAndFormat()
|
|
{
|
|
mFormat->SetValue(gPrefs->Read(wxT("/FileFormats/FFmpegFormat"), wxT("")));
|
|
mCodec->SetValue(gPrefs->Read(wxT("/FileFormats/FFmpegCodec"), wxT("")));
|
|
}
|
|
|
|
void OnOpen(const wxCommandEvent&)
|
|
{
|
|
if(!CheckFFmpeg(true))
|
|
return;
|
|
|
|
#ifdef __WXMAC__
|
|
// Bug 2077 Must be a parent window on OSX or we will appear behind.
|
|
auto pWin = wxGetTopLevelParent( mParent );
|
|
#else
|
|
// Use GetTopWindow on windows as there is no hWnd with top level parent.
|
|
auto pWin = wxTheApp->GetTopWindow();
|
|
#endif
|
|
|
|
ExportFFmpegOptions od(pWin);
|
|
od.ShowModal();
|
|
//ExportFFmpegOptions uses gPrefs to store options
|
|
//Instead we could provide it with instance of wxConfigBase
|
|
//constructed locally and read from it later
|
|
Load(*gPrefs);
|
|
mAVCodec.reset();
|
|
|
|
UpdateCodecAndFormat();
|
|
if(mListener)
|
|
mListener->OnSampleRateListChange();
|
|
}
|
|
|
|
wxWindow *mParent {nullptr};
|
|
wxTextCtrl *mFormat {nullptr};
|
|
wxTextCtrl *mCodec {nullptr};
|
|
};
|
|
|
|
}
|
|
|
|
///Performs actual export
|
|
class FFmpegExporter final
|
|
{
|
|
static constexpr auto MaxAudioPacketSize { 128 * 1024 };
|
|
public:
|
|
|
|
FFmpegExporter(std::shared_ptr<FFmpegFunctions> ffmpeg,
|
|
const wxFileNameWrapper& filename,
|
|
int numChannels,
|
|
int subformat);
|
|
|
|
/// Format initialization
|
|
bool Init(const char *shortname,
|
|
AudacityProject *project,
|
|
int sampleRate,
|
|
const Tags *metadata,
|
|
const ExportProcessor::Parameters& parameters);
|
|
|
|
/// Encodes audio
|
|
bool EncodeAudioFrame(int16_t *pFrame, size_t frameSize);
|
|
|
|
/// Flushes audio encoder
|
|
bool Finalize();
|
|
|
|
std::unique_ptr<Mixer> CreateMixer(
|
|
const AudacityProject& project, bool selectionOnly, double startTime,
|
|
double stopTime, MixerOptions::Downmix* mixerSpec);
|
|
|
|
private:
|
|
|
|
/// Writes metadata
|
|
bool AddTags(const Tags *metadata);
|
|
|
|
/// Sets individual metadata values
|
|
void SetMetadata(const Tags *tags, const char *name, const wxChar *tag);
|
|
|
|
/// Check whether or not current project sample rate is compatible with the export codec
|
|
bool CheckSampleRate(int rate, int lowrate, int highrate, const int *sampRates);
|
|
|
|
/// Asks user to resample the project or cancel the export procedure
|
|
int AskResample(int bitrate, int rate, int lowrate, int highrate, const int *sampRates);
|
|
|
|
/// Codec initialization
|
|
bool InitCodecs(int sampleRate,
|
|
const ExportProcessor::Parameters& parameters);
|
|
|
|
void WritePacket(AVPacketWrapper& packet);
|
|
|
|
int EncodeAudio(AVPacketWrapper& pkt,
|
|
int16_t* audio_samples,
|
|
int nb_samples);
|
|
|
|
std::shared_ptr<FFmpegFunctions> mFFmpeg;
|
|
|
|
std::unique_ptr<AVOutputFormatWrapper> mEncFormatDesc; // describes our output file to libavformat
|
|
int mDefaultFrameSize {};
|
|
std::unique_ptr<AVStreamWrapper> mEncAudioStream; // the output audio stream (may remain NULL)
|
|
int mEncAudioFifoOutBufSize {};
|
|
|
|
wxFileNameWrapper mName;
|
|
|
|
int mSubFormat{};
|
|
int mBitRate{};
|
|
int mSampleRate{};
|
|
unsigned mChannels{};
|
|
bool mSupportsUTF8{true};
|
|
|
|
// Smart pointer fields, their order is the reverse in which they are reset in FreeResources():
|
|
std::unique_ptr<FifoBuffer> mEncAudioFifo; // FIFO to write incoming audio samples into
|
|
AVDataBuffer<int16_t> mEncAudioFifoOutBuf; // buffer to read _out_ of the FIFO into
|
|
std::unique_ptr<AVFormatContextWrapper> mEncFormatCtx; // libavformat's context for our output file
|
|
std::unique_ptr<AVCodecContextWrapper> mEncAudioCodecCtx; // the encoder for the output audio stream
|
|
};
|
|
|
|
class FFmpegExportProcessor final : public ExportProcessor
|
|
{
|
|
std::shared_ptr<FFmpegFunctions> mFFmpeg;
|
|
struct
|
|
{
|
|
//same index as in GetFormatInfo, use AdjustFormatIndex to convert it to FFmpegExposedFormat
|
|
int subformat;
|
|
TranslatableString status;
|
|
double t0;
|
|
double t1;
|
|
std::unique_ptr<Mixer> mixer;
|
|
std::unique_ptr<FFmpegExporter> exporter;
|
|
} context;
|
|
|
|
public:
|
|
FFmpegExportProcessor(std::shared_ptr<FFmpegFunctions> ffmpeg, int format);
|
|
|
|
bool Initialize(AudacityProject& project,
|
|
const Parameters& parameters,
|
|
const wxFileNameWrapper& filename,
|
|
double t0, double t1, bool selectedOnly,
|
|
double sampleRate, unsigned channels,
|
|
MixerOptions::Downmix* mixerSpec,
|
|
const Tags* tags) override;
|
|
|
|
ExportResult Process(ExportProcessorDelegate& delegate) override;
|
|
|
|
};
|
|
|
|
class ExportFFmpeg final : public ExportPlugin
|
|
{
|
|
public:
|
|
|
|
ExportFFmpeg();
|
|
~ExportFFmpeg() override;
|
|
|
|
std::unique_ptr<ExportOptionsEditor>
|
|
CreateOptionsEditor(int format, ExportOptionsEditor::Listener* listener) const override;
|
|
|
|
int GetFormatCount() const override;
|
|
FormatInfo GetFormatInfo(int index) const override;
|
|
|
|
/// Callback, called from GetFilename
|
|
bool CheckFileName(wxFileName &filename, int format = 0) const override;
|
|
|
|
std::unique_ptr<ExportProcessor> CreateProcessor(int format) const override;
|
|
|
|
private:
|
|
mutable std::shared_ptr<FFmpegFunctions> mFFmpeg;
|
|
|
|
std::vector<FormatInfo> mFormatInfos;
|
|
};
|
|
|
|
FFmpegExporter::FFmpegExporter(std::shared_ptr<FFmpegFunctions> ffmpeg,
|
|
const wxFileNameWrapper& filename,
|
|
int numChannels,
|
|
int subFormat)
|
|
: mFFmpeg(std::move(ffmpeg))
|
|
, mName(filename)
|
|
, mChannels(numChannels)
|
|
, mSubFormat(subFormat)
|
|
{
|
|
if (!mFFmpeg) {
|
|
mFFmpeg = FFmpegFunctions::Load();
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<Mixer> FFmpegExporter::CreateMixer(
|
|
const AudacityProject& project, bool selectionOnly, double startTime,
|
|
double stopTime, MixerOptions::Downmix* mixerSpec)
|
|
{
|
|
return ExportPluginHelpers::CreateMixer(
|
|
project, selectionOnly, startTime, stopTime, mChannels, mDefaultFrameSize,
|
|
true, mSampleRate, int16Sample, mixerSpec);
|
|
}
|
|
|
|
|
|
ExportFFmpeg::ExportFFmpeg()
|
|
{
|
|
mFFmpeg = FFmpegFunctions::Load();
|
|
|
|
int avfver = mFFmpeg ? mFFmpeg->AVFormatVersion.GetIntVersion() : 0;
|
|
|
|
int newfmt;
|
|
// Adds export types from the export type list
|
|
for (newfmt = 0; newfmt < FMT_LAST; newfmt++)
|
|
{
|
|
wxString shortname(ExportFFmpegOptions::fmts[newfmt].shortname);
|
|
// Don't hide export types when there's no av-libs, and don't hide FMT_OTHER
|
|
if (newfmt < FMT_OTHER && mFFmpeg)
|
|
{
|
|
// Format/Codec support is compiled in?
|
|
auto avoformat = mFFmpeg->GuessOutputFormat(shortname.mb_str(), nullptr, nullptr);
|
|
auto avcodec = mFFmpeg->CreateEncoder(mFFmpeg->GetAVCodecID(ExportFFmpegOptions::fmts[newfmt].codecid));
|
|
|
|
if (avoformat == NULL || avcodec == NULL)
|
|
{
|
|
ExportFFmpegOptions::fmts[newfmt].compiledIn = false;
|
|
continue;
|
|
}
|
|
}
|
|
FormatInfo formatInfo {};
|
|
formatInfo.format = ExportFFmpegOptions::fmts[newfmt].name;
|
|
formatInfo.extensions.push_back(ExportFFmpegOptions::fmts[newfmt].extension);
|
|
// For some types add other extensions
|
|
switch(newfmt)
|
|
{
|
|
case FMT_M4A:
|
|
formatInfo.extensions.push_back(wxT("3gp"));
|
|
formatInfo.extensions.push_back(wxT("m4r"));
|
|
formatInfo.extensions.push_back(wxT("mp4"));
|
|
break;
|
|
case FMT_WMA2:
|
|
formatInfo.extensions.push_back(wxT("asf"));
|
|
formatInfo.extensions.push_back(wxT("wmv"));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
formatInfo.maxChannels = ExportFFmpegOptions::fmts[newfmt].maxchannels;
|
|
formatInfo.description = ExportFFmpegOptions::fmts[newfmt].description;
|
|
|
|
const int canmeta = ExportFFmpegOptions::fmts[newfmt].canmetadata;
|
|
formatInfo.canMetaData = canmeta && (canmeta == AV_CANMETA || canmeta <= avfver);
|
|
|
|
mFormatInfos.push_back(std::move(formatInfo));
|
|
}
|
|
}
|
|
|
|
ExportFFmpeg::~ExportFFmpeg() = default;
|
|
|
|
std::unique_ptr<ExportOptionsEditor>
|
|
ExportFFmpeg::CreateOptionsEditor(int format, ExportOptionsEditor::Listener* listener) const
|
|
{
|
|
switch(AdjustFormatIndex(format))
|
|
{
|
|
case FMT_M4A:
|
|
return std::make_unique<PlainExportOptionsEditor>(AACOptions, listener);
|
|
case FMT_AC3:
|
|
return std::make_unique<PlainExportOptionsEditor>(
|
|
AC3Options,
|
|
ToSampleRateList(iAC3SampleRates),
|
|
listener);
|
|
case FMT_AMRNB:
|
|
return std::make_unique<PlainExportOptionsEditor>(
|
|
AMRNBOptions,
|
|
ExportOptionsEditor::SampleRateList {8000},
|
|
listener);
|
|
#ifdef SHOW_FFMPEG_OPUS_EXPORT
|
|
case FMT_OPUS:
|
|
return std::make_unique<PlainExportOptionsEditor>(OPUSOptions, listener);
|
|
#endif
|
|
case FMT_WMA2:
|
|
return std::make_unique<PlainExportOptionsEditor>(
|
|
WMAOptions,
|
|
ToSampleRateList(iWMASampleRates),
|
|
listener);
|
|
case FMT_OTHER:
|
|
return std::make_unique<ExportOptionsFFmpegCustomEditor>(listener);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
int ExportFFmpeg::GetFormatCount() const
|
|
{
|
|
return static_cast<int>(mFormatInfos.size());
|
|
}
|
|
|
|
FormatInfo ExportFFmpeg::GetFormatInfo(int index) const
|
|
{
|
|
if(index >= 0 && index < mFormatInfos.size())
|
|
return mFormatInfos[index];
|
|
return mFormatInfos[FMT_OTHER];
|
|
}
|
|
|
|
bool ExportFFmpeg::CheckFileName(wxFileName & WXUNUSED(filename), int WXUNUSED(format)) const
|
|
{
|
|
bool result = true;
|
|
|
|
// Show "Locate FFmpeg" dialog
|
|
mFFmpeg = FFmpegFunctions::Load();
|
|
if (!mFFmpeg)
|
|
{
|
|
FindFFmpegLibs();
|
|
mFFmpeg = FFmpegFunctions::Load();
|
|
|
|
return LoadFFmpeg(true);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::unique_ptr<ExportProcessor> ExportFFmpeg::CreateProcessor(int format) const
|
|
{
|
|
return std::make_unique<FFmpegExportProcessor>(mFFmpeg, format);
|
|
}
|
|
|
|
|
|
bool FFmpegExporter::Init(const char *shortname,
|
|
AudacityProject *project,
|
|
int sampleRate,
|
|
const Tags *metadata,
|
|
const ExportProcessor::Parameters& parameters)
|
|
{
|
|
if (!mFFmpeg)
|
|
return false;
|
|
|
|
// See if libavformat has modules that can write our output format. If so, mEncFormatDesc
|
|
// will describe the functions used to write the format (used internally by libavformat)
|
|
// and the default video/audio codecs that the format uses.
|
|
const auto path = mName.GetFullPath();
|
|
if ((mEncFormatDesc = mFFmpeg->GuessOutputFormat(shortname, OSINPUT(path), nullptr)) == nullptr)
|
|
{
|
|
throw ExportException(_("FFmpeg : ERROR - Can't determine format description for file \"%s\".").Format(path));
|
|
}
|
|
|
|
// mEncFormatCtx is used by libavformat to carry around context data re our output file.
|
|
mEncFormatCtx = mFFmpeg->CreateAVFormatContext();
|
|
if (!mEncFormatCtx)
|
|
{
|
|
throw ExportException(_("FFmpeg : ERROR - Can't allocate output format context."));
|
|
}
|
|
|
|
// Initialise the output format context.
|
|
mEncFormatCtx->SetOutputFormat(mFFmpeg->CreateAVOutputFormatWrapper(mEncFormatDesc->GetWrappedValue()));
|
|
mEncFormatCtx->SetFilename(OSINPUT(path));
|
|
|
|
// At the moment Audacity can export only one audio stream
|
|
if ((mEncAudioStream = mEncFormatCtx->CreateStream()) == nullptr)
|
|
{
|
|
throw ExportException(_("FFmpeg : ERROR - Can't add audio stream to output file \"%s\"."));
|
|
}
|
|
|
|
// Documentation for avformat_new_stream says
|
|
// "User is required to call avcodec_close() and avformat_free_context() to clean
|
|
// up the allocation by avformat_new_stream()."
|
|
|
|
// We use smart pointers that ensure these cleanups either in their destructors or
|
|
// sooner if they are reset. These are std::unique_ptr with nondefault deleter
|
|
// template parameters.
|
|
|
|
// mEncFormatCtx takes care of avformat_free_context(), so
|
|
// mEncAudioStream can be a plain pointer.
|
|
|
|
// mEncAudioCodecCtx now becomes responsible for closing the codec:
|
|
mEncAudioCodecCtx = mEncAudioStream->GetAVCodecContext();
|
|
mEncAudioStream->SetId(0);
|
|
|
|
// Open the output file.
|
|
if (!(mEncFormatDesc->GetFlags() & AUDACITY_AVFMT_NOFILE))
|
|
{
|
|
AVIOContextWrapper::OpenResult result =
|
|
mEncFormatCtx->OpenOutputContext(path);
|
|
|
|
if (result != AVIOContextWrapper::OpenResult::Success)
|
|
{
|
|
throw ExportException(_("FFmpeg : ERROR - Can't open output file \"%s\" to write. Error code is %d.")
|
|
.Format(path, static_cast<int>(result)));
|
|
}
|
|
}
|
|
|
|
// Open the audio stream's codec and initialise any stream related data.
|
|
if(!InitCodecs(sampleRate, parameters))
|
|
return false;
|
|
|
|
if (mEncAudioStream->SetParametersFromContext(*mEncAudioCodecCtx) < 0)
|
|
return false;
|
|
|
|
if (metadata == NULL)
|
|
metadata = &Tags::Get( *project );
|
|
|
|
// Add metadata BEFORE writing the header.
|
|
// At the moment that works with ffmpeg-git and ffmpeg-0.5 for MP4.
|
|
const auto canmeta = ExportFFmpegOptions::fmts[mSubFormat].canmetadata;
|
|
const auto avfver = mFFmpeg->AVFormatVersion.GetIntVersion();
|
|
if (canmeta && (canmeta == AV_CANMETA || canmeta <= avfver))
|
|
{
|
|
mSupportsUTF8 = ExportFFmpegOptions::fmts[mSubFormat].canutf8;
|
|
AddTags(metadata);
|
|
}
|
|
|
|
// Write headers to the output file.
|
|
int err =
|
|
mFFmpeg->avformat_write_header(mEncFormatCtx->GetWrappedValue(), nullptr);
|
|
|
|
if (err < 0)
|
|
{
|
|
throw ExportException(XO("FFmpeg : ERROR - Can't write headers to output file \"%s\". Error code is %d.")
|
|
.Format( path, err )
|
|
.Translation());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FFmpegExporter::CheckSampleRate(int rate, int lowrate, int highrate, const int *sampRates)
|
|
{
|
|
if (lowrate && highrate)
|
|
{
|
|
if (rate < lowrate || rate > highrate)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (sampRates)
|
|
{
|
|
for (int i = 0; sampRates[i] > 0; i++)
|
|
{
|
|
if (rate == sampRates[i])
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool FFmpegExporter::InitCodecs(int sampleRate,
|
|
const ExportProcessor::Parameters& parameters)
|
|
{
|
|
std::unique_ptr<AVCodecWrapper> codec;
|
|
|
|
AVDictionaryWrapper options(*mFFmpeg);
|
|
|
|
// Get the sample rate from the passed settings if we haven't set it before.
|
|
// Doing this only when not set allows us to carry the sample rate from one
|
|
// iteration of ExportMultiple to the next. This prevents multiple resampling
|
|
// dialogs in the event the codec can't support the specified rate.
|
|
if (!mSampleRate)
|
|
{
|
|
//TODO: Does not work with export multiple any more...
|
|
mSampleRate = sampleRate;
|
|
}
|
|
|
|
// Configure the audio stream's codec context.
|
|
|
|
const auto codecID = ExportFFmpegOptions::fmts[mSubFormat].codecid;
|
|
|
|
mEncAudioCodecCtx->SetGlobalQuality(-99999); //quality mode is off by default;
|
|
|
|
// Each export type has its own settings
|
|
switch (mSubFormat)
|
|
{
|
|
case FMT_M4A:
|
|
{
|
|
int q = ExportPluginHelpers::GetParameterValue(parameters, AACOptionIDQuality, -99999);
|
|
|
|
q = wxClip( q, 98 * mChannels, 160 * mChannels );
|
|
// Set bit rate to between 98 kbps and 320 kbps (if two channels)
|
|
mEncAudioCodecCtx->SetBitRate(q * 1000);
|
|
mEncAudioCodecCtx->SetProfile(AUDACITY_FF_PROFILE_AAC_LOW);
|
|
mEncAudioCodecCtx->SetCutoff(0);
|
|
|
|
break;
|
|
}
|
|
case FMT_AC3:
|
|
mEncAudioCodecCtx->SetBitRate(ExportPluginHelpers::GetParameterValue(parameters, AC3OptionIDBitRate, 192000));
|
|
if (!CheckSampleRate(
|
|
mSampleRate, iAC3SampleRates[0],
|
|
iAC3SampleRates[2],
|
|
&iAC3SampleRates[0]))
|
|
{
|
|
mSampleRate = AskResample(
|
|
mEncAudioCodecCtx->GetBitRate(), mSampleRate,
|
|
iAC3SampleRates[0],
|
|
iAC3SampleRates[2],
|
|
&iAC3SampleRates[0]);
|
|
}
|
|
break;
|
|
case FMT_AMRNB:
|
|
mSampleRate = 8000;
|
|
mEncAudioCodecCtx->SetBitRate(ExportPluginHelpers::GetParameterValue(parameters, AMRNBOptionIDBitRate, 12200));
|
|
break;
|
|
#ifdef SHOW_FFMPEG_OPUS_EXPORT
|
|
case FMT_OPUS:
|
|
options.Set("b", ExportPluginHelpers::GetParameterValue<std::string>(parameters, OPUSOptionIDBitRate, "128000"), 0);
|
|
options.Set("vbr", ExportPluginHelpers::GetParameterValue<std::string>(parameters, OPUSOptionIDVBRMode, "on"), 0);
|
|
options.Set("compression_level", ExportPluginHelpers::GetParameterValue<std::string>(parameters, OPUSOptionIDCompression, "10"), 0);
|
|
options.Set("frame_duration", ExportPluginHelpers::GetParameterValue<std::string>(parameters, OPUSOptionIDFrameDuration, "20"), 0);
|
|
options.Set("application", ExportPluginHelpers::GetParameterValue<std::string>(parameters, OPUSOptionIDApplication, "audio"), 0);
|
|
options.Set("cutoff", ExportPluginHelpers::GetParameterValue<std::string>(parameters, OPUSOptionIDCutoff, "0"), 0);
|
|
options.Set("mapping_family", mChannels <= 2 ? "0" : "255", 0);
|
|
break;
|
|
#endif
|
|
case FMT_WMA2:
|
|
mEncAudioCodecCtx->SetBitRate(ExportPluginHelpers::GetParameterValue(parameters, WMAOptionIDBitRate, 198000));
|
|
if (!CheckSampleRate(
|
|
mSampleRate, iWMASampleRates[0],
|
|
iWMASampleRates[4],
|
|
&iWMASampleRates[0]))
|
|
{
|
|
mSampleRate = AskResample(
|
|
mEncAudioCodecCtx->GetBitRate(), mSampleRate,
|
|
iWMASampleRates[0],
|
|
iWMASampleRates[4],
|
|
&iWMASampleRates[0]);
|
|
}
|
|
break;
|
|
case FMT_OTHER:
|
|
{
|
|
AVDictionaryWrapper streamMetadata = mEncAudioStream->GetMetadata();
|
|
streamMetadata.Set(
|
|
"language",
|
|
ExportPluginHelpers::GetParameterValue<std::string>(parameters, FELanguageID), 0);
|
|
|
|
mEncAudioStream->SetMetadata(streamMetadata);
|
|
|
|
mEncAudioCodecCtx->SetSampleRate(
|
|
ExportPluginHelpers::GetParameterValue(parameters, FESampleRateID, 0));
|
|
|
|
if (mEncAudioCodecCtx->GetSampleRate() != 0)
|
|
mSampleRate = mEncAudioCodecCtx->GetSampleRate();
|
|
|
|
mEncAudioCodecCtx->SetBitRate(
|
|
ExportPluginHelpers::GetParameterValue(parameters, FEBitrateID, 0));
|
|
|
|
mEncAudioCodecCtx->SetCodecTagFourCC(
|
|
ExportPluginHelpers::GetParameterValue<std::string>(parameters, FETagID).c_str());
|
|
|
|
mEncAudioCodecCtx->SetGlobalQuality(
|
|
ExportPluginHelpers::GetParameterValue(parameters, FEQualityID, -99999));
|
|
mEncAudioCodecCtx->SetCutoff(
|
|
ExportPluginHelpers::GetParameterValue(parameters, FECutoffID, 0));
|
|
mEncAudioCodecCtx->SetFlags2(0);
|
|
|
|
if (ExportPluginHelpers::GetParameterValue(parameters, FEBitReservoirID, true))
|
|
options.Set("reservoir", "1", 0);
|
|
|
|
if (ExportPluginHelpers::GetParameterValue(parameters, FEVariableBlockLenID, true))
|
|
mEncAudioCodecCtx->SetFlags2(
|
|
mEncAudioCodecCtx->GetFlags2() | 0x0004); // WMA only?
|
|
|
|
mEncAudioCodecCtx->SetCompressionLevel(
|
|
ExportPluginHelpers::GetParameterValue(parameters, FECompLevelID, -1));
|
|
mEncAudioCodecCtx->SetFrameSize(
|
|
ExportPluginHelpers::GetParameterValue(parameters, FEFrameSizeID, 0));
|
|
|
|
// FIXME The list of supported options for the selected encoder should be
|
|
// extracted instead of a few hardcoded
|
|
|
|
options.Set(
|
|
"lpc_coeff_precision",
|
|
ExportPluginHelpers::GetParameterValue(parameters, FELPCCoeffsID, 0));
|
|
options.Set(
|
|
"min_prediction_order",
|
|
ExportPluginHelpers::GetParameterValue(parameters, FEMinPredID, -1));
|
|
options.Set(
|
|
"max_prediction_order",
|
|
ExportPluginHelpers::GetParameterValue(parameters, FEMaxPredID, -1));
|
|
options.Set(
|
|
"min_partition_order",
|
|
ExportPluginHelpers::GetParameterValue(parameters, FEMinPartOrderID, -1));
|
|
options.Set(
|
|
"max_partition_order",
|
|
ExportPluginHelpers::GetParameterValue(parameters, FEMaxPartOrderID, -1));
|
|
options.Set(
|
|
"prediction_order_method",
|
|
ExportPluginHelpers::GetParameterValue(parameters, FEPredOrderID, 0));
|
|
options.Set(
|
|
"muxrate",
|
|
ExportPluginHelpers::GetParameterValue(parameters, FEMuxRateID, 0));
|
|
|
|
mEncFormatCtx->SetPacketSize(
|
|
ExportPluginHelpers::GetParameterValue(parameters, FEPacketSizeID, 0));
|
|
|
|
codec = mFFmpeg->CreateEncoder(
|
|
ExportPluginHelpers::GetParameterValue<std::string>(parameters, FECodecID).c_str());
|
|
|
|
if (!codec)
|
|
codec = mFFmpeg->CreateEncoder(mEncFormatDesc->GetAudioCodec());
|
|
}
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
// This happens if user refused to resample the project
|
|
if (mSampleRate == 0) return false;
|
|
|
|
if (mEncAudioCodecCtx->GetGlobalQuality() >= 0)
|
|
{
|
|
mEncAudioCodecCtx->SetFlags(
|
|
mEncAudioCodecCtx->GetFlags() | AUDACITY_AV_CODEC_FLAG_QSCALE);
|
|
}
|
|
else
|
|
{
|
|
mEncAudioCodecCtx->SetGlobalQuality(0);
|
|
}
|
|
|
|
mEncAudioCodecCtx->SetGlobalQuality(mEncAudioCodecCtx->GetGlobalQuality() * AUDACITY_FF_QP2LAMBDA);
|
|
mEncAudioCodecCtx->SetSampleRate(mSampleRate);
|
|
mEncAudioCodecCtx->SetChannelLayout(mFFmpeg->CreateDefaultChannelLayout(mChannels).get());
|
|
mEncAudioCodecCtx->SetTimeBase({ 1, mSampleRate });
|
|
mEncAudioCodecCtx->SetSampleFmt(static_cast<AVSampleFormatFwd>(AUDACITY_AV_SAMPLE_FMT_S16));
|
|
mEncAudioCodecCtx->SetStrictStdCompliance(
|
|
AUDACITY_FF_COMPLIANCE_EXPERIMENTAL);
|
|
|
|
if (codecID == AUDACITY_AV_CODEC_ID_AC3)
|
|
{
|
|
// As of Jan 4, 2011, the default AC3 encoder only accept SAMPLE_FMT_FLT samples.
|
|
// But, currently, Audacity only supports SAMPLE_FMT_S16. So, for now, look for the
|
|
// "older" AC3 codec. this is not a proper solution, but will suffice until other
|
|
// encoders no longer support SAMPLE_FMT_S16.
|
|
codec = mFFmpeg->CreateEncoder("ac3_fixed");
|
|
}
|
|
|
|
if (!codec)
|
|
{
|
|
codec = mFFmpeg->CreateEncoder(mFFmpeg->GetAVCodecID(codecID));
|
|
}
|
|
|
|
// Is the required audio codec compiled into libavcodec?
|
|
if (codec == NULL)
|
|
{
|
|
/* i18n-hint: "codec" is short for a "coder-decoder" algorithm */
|
|
throw ExportException(XO("FFmpeg cannot find audio codec 0x%x.\nSupport for this codec is probably not compiled in.")
|
|
.Format(static_cast<const unsigned int>(codecID.value))
|
|
.Translation());
|
|
}
|
|
|
|
if (codec->GetSampleFmts()) {
|
|
for (int i = 0; codec->GetSampleFmts()[i] != AUDACITY_AV_SAMPLE_FMT_NONE; i++)
|
|
{
|
|
AVSampleFormatFwd fmt = codec->GetSampleFmts()[i];
|
|
|
|
if (
|
|
fmt == AUDACITY_AV_SAMPLE_FMT_U8 ||
|
|
fmt == AUDACITY_AV_SAMPLE_FMT_U8P ||
|
|
fmt == AUDACITY_AV_SAMPLE_FMT_S16 ||
|
|
fmt == AUDACITY_AV_SAMPLE_FMT_S16P ||
|
|
fmt == AUDACITY_AV_SAMPLE_FMT_S32 ||
|
|
fmt == AUDACITY_AV_SAMPLE_FMT_S32P ||
|
|
fmt == AUDACITY_AV_SAMPLE_FMT_FLT ||
|
|
fmt == AUDACITY_AV_SAMPLE_FMT_FLTP)
|
|
{
|
|
mEncAudioCodecCtx->SetSampleFmt(fmt);
|
|
}
|
|
|
|
if (
|
|
fmt == AUDACITY_AV_SAMPLE_FMT_S16 ||
|
|
fmt == AUDACITY_AV_SAMPLE_FMT_S16P)
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (codec->GetSupportedSamplerates())
|
|
{
|
|
// Workaround for crash in bug #2378. Proper fix is to get a newer version of FFmpeg.
|
|
if (codec->GetId() == mFFmpeg->GetAVCodecID(AUDACITY_AV_CODEC_ID_AAC))
|
|
{
|
|
std::vector<int> rates;
|
|
int i = 0;
|
|
|
|
while (codec->GetSupportedSamplerates()[i] &&
|
|
codec->GetSupportedSamplerates()[i] != 7350)
|
|
{
|
|
rates.push_back(codec->GetSupportedSamplerates()[i++]);
|
|
}
|
|
|
|
rates.push_back(0);
|
|
|
|
if (!CheckSampleRate(mSampleRate, 0, 0, rates.data()))
|
|
{
|
|
mSampleRate = AskResample(0, mSampleRate, 0, 0, rates.data());
|
|
mEncAudioCodecCtx->SetSampleRate(mSampleRate);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!CheckSampleRate(
|
|
mSampleRate, 0, 0, codec->GetSupportedSamplerates()))
|
|
{
|
|
mSampleRate = AskResample(
|
|
0, mSampleRate, 0, 0, codec->GetSupportedSamplerates());
|
|
mEncAudioCodecCtx->SetSampleRate(mSampleRate);
|
|
}
|
|
}
|
|
|
|
// This happens if user refused to resample the project
|
|
if (mSampleRate == 0)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (mEncFormatCtx->GetOutputFormat()->GetFlags() & AUDACITY_AVFMT_GLOBALHEADER)
|
|
{
|
|
mEncAudioCodecCtx->SetFlags(mEncAudioCodecCtx->GetFlags() | AUDACITY_AV_CODEC_FLAG_GLOBAL_HEADER);
|
|
mEncFormatCtx->SetFlags(mEncFormatCtx->GetFlags() | AUDACITY_AV_CODEC_FLAG_GLOBAL_HEADER);
|
|
}
|
|
|
|
// Open the codec.
|
|
int rc = mEncAudioCodecCtx->Open(codec.get(), &options);
|
|
if (rc < 0)
|
|
{
|
|
TranslatableString errmsg;
|
|
|
|
switch (rc)
|
|
{
|
|
case AUDACITY_AVERROR(EPERM):
|
|
errmsg = XO("The codec reported a generic error (EPERM)");
|
|
break;
|
|
case AUDACITY_AVERROR(EINVAL):
|
|
errmsg = XO("The codec reported an invalid parameter (EINVAL)");
|
|
break;
|
|
default:
|
|
char buf[64];
|
|
mFFmpeg->av_strerror(rc, buf, sizeof(buf));
|
|
errmsg = Verbatim(buf);
|
|
}
|
|
|
|
/* i18n-hint: "codec" is short for a "coder-decoder" algorithm */
|
|
throw ExportException(XO("Can't open audio codec \"%s\" (0x%x)\n\n%s")
|
|
.Format(codec->GetName(), codecID.value, errmsg)
|
|
.Translation());
|
|
}
|
|
|
|
mDefaultFrameSize = mEncAudioCodecCtx->GetFrameSize();
|
|
|
|
if (mDefaultFrameSize == 0)
|
|
mDefaultFrameSize = 1024; // arbitrary non zero value;
|
|
|
|
wxLogDebug(
|
|
wxT("FFmpeg : Audio Output Codec Frame Size: %d samples."),
|
|
mEncAudioCodecCtx->GetFrameSize());
|
|
|
|
// The encoder may require a minimum number of raw audio samples for each encoding but we can't
|
|
// guarantee we'll get this minimum each time an audio frame is decoded from the input file so
|
|
// we use a FIFO to store up incoming raw samples until we have enough for one call to the codec.
|
|
mEncAudioFifo = std::make_unique<FifoBuffer>(mDefaultFrameSize * mChannels * sizeof(int16_t));
|
|
|
|
mEncAudioFifoOutBufSize = 2*MaxAudioPacketSize;
|
|
// Allocate a buffer to read OUT of the FIFO into. The FIFO maintains its own buffer internally.
|
|
mEncAudioFifoOutBuf = mFFmpeg->CreateMemoryBuffer<int16_t>(mEncAudioFifoOutBufSize);
|
|
|
|
if (mEncAudioFifoOutBuf.empty())
|
|
{
|
|
throw ExportException(_("FFmpeg : ERROR - Can't allocate buffer to read into from audio FIFO."));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FFmpegExporter::WritePacket(AVPacketWrapper& pkt)
|
|
{
|
|
// Set presentation time of frame (currently in the codec's timebase) in the
|
|
// stream timebase.
|
|
if (pkt.GetPresentationTimestamp() != AUDACITY_AV_NOPTS_VALUE)
|
|
pkt.RescalePresentationTimestamp(
|
|
mEncAudioCodecCtx->GetTimeBase(), mEncAudioStream->GetTimeBase());
|
|
|
|
if (pkt.GetDecompressionTimestamp() != AUDACITY_AV_NOPTS_VALUE)
|
|
pkt.RescaleDecompressionTimestamp(
|
|
mEncAudioCodecCtx->GetTimeBase(), mEncAudioStream->GetTimeBase());
|
|
|
|
if (pkt.GetDuration() > 0)
|
|
pkt.RescaleDuration(
|
|
mEncAudioCodecCtx->GetTimeBase(), mEncAudioStream->GetTimeBase());
|
|
|
|
if (
|
|
mFFmpeg->av_interleaved_write_frame(
|
|
mEncFormatCtx->GetWrappedValue(), pkt.GetWrappedValue()) != 0)
|
|
{
|
|
throw ExportException(_("FFmpeg : ERROR - Couldn't write audio frame to output file."));
|
|
}
|
|
}
|
|
|
|
// Returns 0 if no more output, 1 if more output, negative if error
|
|
int FFmpegExporter::EncodeAudio(AVPacketWrapper& pkt, int16_t* audio_samples, int nb_samples)
|
|
{
|
|
// Assume *pkt is already initialized.
|
|
|
|
int i, ch, buffer_size, ret, got_output = 0;
|
|
AVDataBuffer<uint8_t> samples;
|
|
|
|
std::unique_ptr<AVFrameWrapper> frame;
|
|
|
|
if (audio_samples) {
|
|
frame = mFFmpeg->CreateAVFrameWrapper();
|
|
|
|
if (!frame)
|
|
return AUDACITY_AVERROR(ENOMEM);
|
|
|
|
frame->SetSamplesCount(nb_samples);
|
|
frame->SetFormat(mEncAudioCodecCtx->GetSampleFmt());
|
|
frame->SetChannelLayout(mEncAudioCodecCtx->GetChannelLayout());
|
|
|
|
buffer_size = mFFmpeg->av_samples_get_buffer_size(
|
|
NULL, mEncAudioCodecCtx->GetChannels(), nb_samples,
|
|
mEncAudioCodecCtx->GetSampleFmt(), 0);
|
|
|
|
if (buffer_size < 0) {
|
|
throw ExportException(_("FFmpeg : ERROR - Could not get sample buffer size"));
|
|
}
|
|
|
|
samples = mFFmpeg->CreateMemoryBuffer<uint8_t>(buffer_size);
|
|
|
|
if (samples.empty()) {
|
|
throw ExportException(_("FFmpeg : ERROR - Could not allocate bytes for samples buffer"));
|
|
}
|
|
/* setup the data pointers in the AVFrame */
|
|
ret = mFFmpeg->avcodec_fill_audio_frame(
|
|
frame->GetWrappedValue(), mEncAudioCodecCtx->GetChannels(),
|
|
mEncAudioCodecCtx->GetSampleFmt(), samples.data(), buffer_size, 0);
|
|
|
|
if (ret < 0) {
|
|
throw ExportException(_("FFmpeg : ERROR - Could not setup audio frame"));
|
|
}
|
|
|
|
const int channelsCount = mEncAudioCodecCtx->GetChannels();
|
|
|
|
for (ch = 0; ch < mEncAudioCodecCtx->GetChannels(); ch++)
|
|
{
|
|
for (i = 0; i < nb_samples; i++) {
|
|
switch (static_cast<AudacityAVSampleFormat>(
|
|
mEncAudioCodecCtx->GetSampleFmt()))
|
|
{
|
|
case AUDACITY_AV_SAMPLE_FMT_U8:
|
|
((uint8_t*)(frame->GetData(0)))[ch + i*channelsCount] = audio_samples[ch + i*channelsCount]/258 + 128;
|
|
break;
|
|
case AUDACITY_AV_SAMPLE_FMT_U8P:
|
|
((uint8_t*)(frame->GetData(ch)))[i] = audio_samples[ch + i*channelsCount]/258 + 128;
|
|
break;
|
|
case AUDACITY_AV_SAMPLE_FMT_S16:
|
|
((int16_t*)(frame->GetData(0)))[ch + i*channelsCount] = audio_samples[ch + i*channelsCount];
|
|
break;
|
|
case AUDACITY_AV_SAMPLE_FMT_S16P:
|
|
((int16_t*)(frame->GetData(ch)))[i] = audio_samples[ch + i*channelsCount];
|
|
break;
|
|
case AUDACITY_AV_SAMPLE_FMT_S32:
|
|
((int32_t*)(frame->GetData(0)))[ch + i*channelsCount] = audio_samples[ch + i*channelsCount]<<16;
|
|
break;
|
|
case AUDACITY_AV_SAMPLE_FMT_S32P:
|
|
((int32_t*)(frame->GetData(ch)))[i] = audio_samples[ch + i*channelsCount]<<16;
|
|
break;
|
|
case AUDACITY_AV_SAMPLE_FMT_FLT:
|
|
((float*)(frame->GetData(0)))[ch + i*channelsCount] = audio_samples[ch + i*channelsCount] / 32767.0;
|
|
break;
|
|
case AUDACITY_AV_SAMPLE_FMT_FLTP:
|
|
((float*)(frame->GetData(ch)))[i] = audio_samples[ch + i*channelsCount] / 32767.;
|
|
break;
|
|
default:
|
|
wxASSERT(false);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pkt.ResetData();
|
|
|
|
pkt.SetStreamIndex(mEncAudioStream->GetIndex());
|
|
|
|
if (mFFmpeg->avcodec_send_frame != nullptr)
|
|
{
|
|
ret = mFFmpeg->avcodec_send_frame(
|
|
mEncAudioCodecCtx->GetWrappedValue(),
|
|
frame ? frame->GetWrappedValue() : nullptr);
|
|
|
|
while (ret >= 0)
|
|
{
|
|
ret = mFFmpeg->avcodec_receive_packet(
|
|
mEncAudioCodecCtx->GetWrappedValue(), pkt.GetWrappedValue());
|
|
|
|
if (ret == AUDACITY_AVERROR(EAGAIN) || ret == AUDACITY_AVERROR_EOF)
|
|
{
|
|
ret = 0;
|
|
break;
|
|
}
|
|
else if (ret < 0)
|
|
break;
|
|
|
|
WritePacket(pkt);
|
|
|
|
got_output = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ret = mFFmpeg->avcodec_encode_audio2(
|
|
mEncAudioCodecCtx->GetWrappedValue(), pkt.GetWrappedValue(),
|
|
frame ? frame->GetWrappedValue() : nullptr, &got_output);
|
|
|
|
if (ret == 0)
|
|
{
|
|
WritePacket(pkt);
|
|
}
|
|
}
|
|
|
|
if (ret < 0 && ret != AUDACITY_AVERROR_EOF) {
|
|
|
|
char buf[64];
|
|
mFFmpeg->av_strerror(ret, buf, sizeof(buf));
|
|
wxLogDebug(buf);
|
|
|
|
throw ExportException(_("FFmpeg : ERROR - encoding frame failed"));
|
|
}
|
|
|
|
pkt.ResetTimestamps(); // We don't set frame timestamps thus don't trust the AVPacket timestamps
|
|
|
|
return got_output;
|
|
}
|
|
|
|
|
|
bool FFmpegExporter::Finalize()
|
|
{
|
|
// Flush the audio FIFO and encoder.
|
|
for (;;)
|
|
{
|
|
std::unique_ptr<AVPacketWrapper> pkt = mFFmpeg->CreateAVPacketWrapper();
|
|
|
|
const auto nFifoBytes =
|
|
mEncAudioFifo->GetAvailable(); // any bytes left in audio FIFO?
|
|
|
|
int encodeResult = 0;
|
|
|
|
// Flush the audio FIFO first if necessary. It won't contain a _full_ audio frame because
|
|
// if it did we'd have pulled it from the FIFO during the last encodeAudioFrame() call
|
|
if (nFifoBytes > 0)
|
|
{
|
|
const int nAudioFrameSizeOut = mDefaultFrameSize * mEncAudioCodecCtx->GetChannels() * sizeof(int16_t);
|
|
|
|
if (nAudioFrameSizeOut > mEncAudioFifoOutBufSize || nFifoBytes > mEncAudioFifoOutBufSize) {
|
|
throw ExportException(_("FFmpeg : ERROR - Too much remaining data."));
|
|
}
|
|
|
|
// We have an incomplete buffer of samples left, encode it.
|
|
// If codec supports CODEC_CAP_SMALL_LAST_FRAME, we can feed it with smaller frame
|
|
// Or if frame_size is 1, then it's some kind of PCM codec, they don't have frames and will be fine with the samples
|
|
// Otherwise we'll send a full frame of audio + silence padding to ensure all audio is encoded
|
|
int frame_size = mDefaultFrameSize;
|
|
if (
|
|
mEncAudioCodecCtx->GetCodec()->GetCapabilities() &
|
|
AUDACITY_AV_CODEC_CAP_SMALL_LAST_FRAME ||
|
|
frame_size == 1)
|
|
{
|
|
frame_size = nFifoBytes /
|
|
(mEncAudioCodecCtx->GetChannels() * sizeof(int16_t));
|
|
}
|
|
|
|
wxLogDebug(wxT("FFmpeg : Audio FIFO still contains %lld bytes, writing %d sample frame ..."),
|
|
nFifoBytes, frame_size);
|
|
|
|
// Fill audio buffer with zeroes. If codec tries to read the whole buffer,
|
|
// it will just read silence. If not - who cares?
|
|
memset(mEncAudioFifoOutBuf.data(), 0, mEncAudioFifoOutBufSize);
|
|
//const AVCodec *codec = mEncAudioCodecCtx->codec;
|
|
|
|
// Pull the bytes out from the FIFO and feed them to the encoder.
|
|
if (mEncAudioFifo->Read(mEncAudioFifoOutBuf.data(), nFifoBytes) == nFifoBytes)
|
|
{
|
|
encodeResult = EncodeAudio(*pkt, mEncAudioFifoOutBuf.data(), frame_size);
|
|
}
|
|
else
|
|
{
|
|
wxLogDebug(wxT("FFmpeg : Reading from Audio FIFO failed, aborting"));
|
|
// TODO: more precise message
|
|
throw ExportErrorException("FFmpeg:825");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Fifo is empty, flush encoder. May be called multiple times.
|
|
encodeResult =
|
|
EncodeAudio(*pkt.get(), nullptr, 0);
|
|
}
|
|
|
|
if (encodeResult < 0) {
|
|
// TODO: more precise message
|
|
throw ExportErrorException("FFmpeg:837");
|
|
}
|
|
else if (encodeResult == 0)
|
|
break;
|
|
}
|
|
|
|
// Write any file trailers.
|
|
if (mFFmpeg->av_write_trailer(mEncFormatCtx->GetWrappedValue()) != 0)
|
|
{
|
|
// TODO: more precise message
|
|
throw ExportErrorException("FFmpeg:868");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// All paths in this that fail must report their error to the user.
|
|
bool FFmpegExporter::EncodeAudioFrame(int16_t *pFrame, size_t numSamples)
|
|
{
|
|
const auto frameSize = numSamples * sizeof(int16_t) * mChannels;
|
|
int nBytesToWrite = 0;
|
|
uint8_t *pRawSamples = nullptr;
|
|
int nAudioFrameSizeOut = mDefaultFrameSize * mEncAudioCodecCtx->GetChannels() * sizeof(int16_t);
|
|
int ret;
|
|
|
|
nBytesToWrite = frameSize;
|
|
pRawSamples = (uint8_t*)pFrame;
|
|
|
|
// Put the raw audio samples into the FIFO.
|
|
ret = mEncAudioFifo->Write(pRawSamples, nBytesToWrite);
|
|
|
|
if (ret != nBytesToWrite) {
|
|
throw ExportErrorException("FFmpeg:913");
|
|
}
|
|
|
|
if (nAudioFrameSizeOut > mEncAudioFifoOutBufSize) {
|
|
throw ExportException(_("FFmpeg : ERROR - nAudioFrameSizeOut too large."));
|
|
}
|
|
|
|
// Read raw audio samples out of the FIFO in nAudioFrameSizeOut byte-sized groups to encode.
|
|
while (mEncAudioFifo->GetAvailable() >= nAudioFrameSizeOut)
|
|
{
|
|
mEncAudioFifo->Read(
|
|
mEncAudioFifoOutBuf.data(), nAudioFrameSizeOut);
|
|
|
|
std::unique_ptr<AVPacketWrapper> pkt = mFFmpeg->CreateAVPacketWrapper();
|
|
|
|
ret = EncodeAudio(*pkt, // out
|
|
mEncAudioFifoOutBuf.data(), // in
|
|
mDefaultFrameSize);
|
|
|
|
if (ret < 0)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
FFmpegExportProcessor::FFmpegExportProcessor(std::shared_ptr<FFmpegFunctions> ffmpeg, int subformat )
|
|
: mFFmpeg(std::move(ffmpeg))
|
|
{
|
|
context.subformat = subformat;
|
|
}
|
|
|
|
bool FFmpegExportProcessor::Initialize(AudacityProject& project,
|
|
const Parameters& parameters,
|
|
const wxFileNameWrapper& fName,
|
|
double t0, double t1, bool selectionOnly,
|
|
double sampleRate, unsigned channels,
|
|
MixerOptions::Downmix* mixerSpec,
|
|
const Tags* metadata)
|
|
{
|
|
context.t0 = t0;
|
|
context.t1 = t1;
|
|
|
|
if (!FFmpegFunctions::Load())
|
|
{
|
|
throw ExportException(_("Properly configured FFmpeg is required to proceed.\nYou can configure it at Preferences > Libraries."));
|
|
}
|
|
// subformat index may not correspond directly to fmts[] index, convert it
|
|
const auto adjustedFormatIndex = AdjustFormatIndex(context.subformat);
|
|
if (channels > ExportFFmpegOptions::fmts[adjustedFormatIndex].maxchannels)
|
|
{
|
|
throw ExportException(XO("Attempted to export %d channels, but maximum number of channels for selected output format is %d")
|
|
.Format(
|
|
channels,
|
|
ExportFFmpegOptions::fmts[adjustedFormatIndex].maxchannels )
|
|
.Translation());
|
|
}
|
|
|
|
bool ret = true;
|
|
|
|
if (adjustedFormatIndex >= FMT_LAST) {
|
|
// TODO: more precise message
|
|
throw ExportErrorException("FFmpeg:996");
|
|
}
|
|
|
|
wxString shortname(ExportFFmpegOptions::fmts[adjustedFormatIndex].shortname);
|
|
if (adjustedFormatIndex == FMT_OTHER)
|
|
shortname = ExportPluginHelpers::GetParameterValue<std::string>(parameters, FEFormatID, "matroska");
|
|
|
|
context.exporter = std::make_unique<FFmpegExporter>(mFFmpeg, fName, channels, adjustedFormatIndex);
|
|
|
|
ret = context.exporter->Init(shortname.mb_str(), &project, static_cast<int>(sampleRate), metadata, parameters);
|
|
|
|
if (!ret) {
|
|
// TODO: more precise message
|
|
throw ExportErrorException("FFmpeg:1008");
|
|
}
|
|
|
|
context.mixer =
|
|
context.exporter->CreateMixer(project, selectionOnly, t0, t1, mixerSpec);
|
|
|
|
context.status = selectionOnly
|
|
? XO("Exporting selected audio as %s")
|
|
.Format( ExportFFmpegOptions::fmts[adjustedFormatIndex].description )
|
|
: XO("Exporting the audio as %s")
|
|
.Format( ExportFFmpegOptions::fmts[adjustedFormatIndex].description );
|
|
|
|
return true;
|
|
}
|
|
|
|
ExportResult FFmpegExportProcessor::Process(ExportProcessorDelegate& delegate)
|
|
{
|
|
delegate.SetStatusString(context.status);
|
|
auto exportResult = ExportResult::Success;
|
|
{
|
|
while (exportResult == ExportResult::Success) {
|
|
auto pcmNumSamples = context.mixer->Process();
|
|
if (pcmNumSamples == 0)
|
|
break;
|
|
|
|
short *pcmBuffer = (short *)context.mixer->GetBuffer();
|
|
|
|
if (!context.exporter->EncodeAudioFrame(pcmBuffer, pcmNumSamples))
|
|
// All errors should already have been reported.
|
|
return ExportResult::Error;
|
|
|
|
if(exportResult == ExportResult::Success)
|
|
exportResult = ExportPluginHelpers::UpdateProgress(
|
|
delegate, *context.mixer, context.t0, context.t1);
|
|
}
|
|
}
|
|
|
|
if ( exportResult != ExportResult::Cancelled )
|
|
if ( !context.exporter->Finalize() ) // Finalize makes its own messages
|
|
return ExportResult::Error;
|
|
return exportResult;
|
|
}
|
|
|
|
|
|
void AddStringTagUTF8(char field[], int size, wxString value)
|
|
{
|
|
memset(field,0,size);
|
|
memcpy(field,value.ToUTF8(),(int)strlen(value.ToUTF8()) > size -1 ? size -1 : strlen(value.ToUTF8()));
|
|
}
|
|
|
|
void AddStringTagANSI(char field[], int size, wxString value)
|
|
{
|
|
memset(field,0,size);
|
|
memcpy(field,value.mb_str(),(int)strlen(value.mb_str()) > size -1 ? size -1 : strlen(value.mb_str()));
|
|
}
|
|
|
|
bool FFmpegExporter::AddTags(const Tags *tags)
|
|
{
|
|
if (tags == NULL)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
SetMetadata(tags, "album", TAG_ALBUM);
|
|
SetMetadata(tags, "comment", TAG_COMMENTS);
|
|
SetMetadata(tags, "genre", TAG_GENRE);
|
|
SetMetadata(tags, "title", TAG_TITLE);
|
|
SetMetadata(tags, "track", TAG_TRACK);
|
|
|
|
// Bug 2564: Add m4a tags
|
|
if (mEncFormatDesc->GetAudioCodec() == mFFmpeg->GetAVCodecID(AUDACITY_AV_CODEC_ID_AAC))
|
|
{
|
|
SetMetadata(tags, "artist", TAG_ARTIST);
|
|
SetMetadata(tags, "date", TAG_YEAR);
|
|
}
|
|
else
|
|
{
|
|
SetMetadata(tags, "author", TAG_ARTIST);
|
|
SetMetadata(tags, "year", TAG_YEAR);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FFmpegExporter::SetMetadata(const Tags *tags, const char *name, const wxChar *tag)
|
|
{
|
|
if (tags->HasTag(tag))
|
|
{
|
|
wxString value = tags->GetTag(tag);
|
|
|
|
AVDictionaryWrapper metadata = mEncFormatCtx->GetMetadata();
|
|
|
|
metadata.Set(name, mSupportsUTF8 ? value : value.mb_str(), 0);
|
|
mEncFormatCtx->SetMetadata(metadata);
|
|
}
|
|
}
|
|
|
|
|
|
//----------------------------------------------------------------------------
|
|
// AskResample dialog
|
|
//----------------------------------------------------------------------------
|
|
|
|
int FFmpegExporter::AskResample(int bitrate, int rate, int lowrate, int highrate, const int *sampRates)
|
|
{
|
|
#if defined(FFMPEG_AUTO_RESAMPLE)
|
|
std::vector<int> rates;
|
|
|
|
for (int i = 0; sampRates[i]; ++i)
|
|
{
|
|
rates.push_back(sampRates[i]);
|
|
}
|
|
|
|
std::sort(rates.begin(), rates.end());
|
|
|
|
int bestRate = 0;
|
|
for (auto i : rates)
|
|
{
|
|
bestRate = i;
|
|
if (i > rate)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return bestRate;
|
|
#else
|
|
wxDialogWrapper d(nullptr, wxID_ANY, XO("Invalid sample rate"));
|
|
d.SetName();
|
|
wxChoice *choice;
|
|
ShuttleGui S(&d, eIsCreating);
|
|
|
|
int selected = -1;
|
|
|
|
S.StartVerticalLay();
|
|
{
|
|
S.SetBorder(10);
|
|
S.StartStatic(XO("Resample"));
|
|
{
|
|
S.StartHorizontalLay(wxALIGN_CENTER, false);
|
|
{
|
|
S.AddTitle(
|
|
(bitrate == 0
|
|
? XO(
|
|
"The project sample rate (%d) is not supported by the current output\nfile format. ")
|
|
.Format( rate )
|
|
: XO(
|
|
"The project sample rate (%d) and bit rate (%d kbps) combination is not\nsupported by the current output file format. ")
|
|
.Format( rate, bitrate/1000))
|
|
+ XO("You may resample to one of the rates below.")
|
|
);
|
|
}
|
|
S.EndHorizontalLay();
|
|
|
|
S.StartHorizontalLay(wxALIGN_CENTER, false);
|
|
{
|
|
choice = S.AddChoice(XO("Sample Rates"),
|
|
[&]{
|
|
TranslatableStrings choices;
|
|
for (int i = 0; sampRates[i] > 0; i++)
|
|
{
|
|
int label = sampRates[i];
|
|
if ((!lowrate || label >= lowrate) && (!highrate || label <= highrate))
|
|
{
|
|
wxString name = wxString::Format(wxT("%d"),label);
|
|
choices.push_back( Verbatim( name ) );
|
|
if (label <= rate)
|
|
selected = i;
|
|
}
|
|
}
|
|
return choices;
|
|
}(),
|
|
std::max( 0, selected )
|
|
);
|
|
}
|
|
S.EndHorizontalLay();
|
|
}
|
|
S.EndStatic();
|
|
|
|
S.AddStandardButtons();
|
|
}
|
|
S.EndVerticalLay();
|
|
|
|
d.Layout();
|
|
d.Fit();
|
|
d.SetMinSize(d.GetSize());
|
|
d.Center();
|
|
|
|
if (d.ShowModal() == wxID_CANCEL) {
|
|
return 0;
|
|
}
|
|
|
|
return wxAtoi(choice->GetStringSelection());
|
|
#endif
|
|
}
|
|
|
|
static ExportPluginRegistry::RegisteredPlugin sRegisteredPlugin{ "FFmpeg",
|
|
[]{ return std::make_unique< ExportFFmpeg >(); }
|
|
};
|
|
|