POORLY WORKING Opus export
This commit is contained in:
parent
e485c05a80
commit
1bf5e8baaa
5 changed files with 243 additions and 20 deletions
|
|
@ -103,27 +103,43 @@ enum DivMIDIModes {
|
|||
|
||||
enum DivAudioExportFormats {
|
||||
DIV_EXPORT_FORMAT_S16=0,
|
||||
DIV_EXPORT_FORMAT_F32
|
||||
DIV_EXPORT_FORMAT_F32,
|
||||
DIV_EXPORT_FORMAT_OPUS,
|
||||
DIV_EXPORT_FORMAT_FLAC,
|
||||
DIV_EXPORT_FORMAT_VORBIS,
|
||||
DIV_EXPORT_FORMAT_MPEG_L3
|
||||
};
|
||||
|
||||
enum DivAudioExportBitrateModes {
|
||||
DIV_EXPORT_BITRATE_CONSTANT=0,
|
||||
DIV_EXPORT_BITRATE_VARIABLE,
|
||||
DIV_EXPORT_BITRATE_AVERAGE,
|
||||
};
|
||||
|
||||
struct DivAudioExportOptions {
|
||||
DivAudioExportModes mode;
|
||||
DivAudioExportFormats format;
|
||||
DivAudioExportBitrateModes bitRateMode;
|
||||
int sampleRate;
|
||||
int chans;
|
||||
int loops;
|
||||
double fadeOut;
|
||||
int orderBegin, orderEnd;
|
||||
bool channelMask[DIV_MAX_CHANS];
|
||||
int bitRate;
|
||||
float vbrQuality;
|
||||
DivAudioExportOptions():
|
||||
mode(DIV_EXPORT_MODE_ONE),
|
||||
format(DIV_EXPORT_FORMAT_S16),
|
||||
bitRateMode(DIV_EXPORT_BITRATE_CONSTANT),
|
||||
sampleRate(44100),
|
||||
chans(2),
|
||||
loops(0),
|
||||
fadeOut(0.0),
|
||||
orderBegin(-1),
|
||||
orderEnd(-1) {
|
||||
orderEnd(-1),
|
||||
bitRate(128000),
|
||||
vbrQuality(6.0f) {
|
||||
for (int i=0; i<DIV_MAX_CHANS; i++) {
|
||||
channelMask[i]=true;
|
||||
}
|
||||
|
|
@ -498,9 +514,12 @@ class DivEngine {
|
|||
DivAudioEngines audioEngine;
|
||||
DivAudioExportModes exportMode;
|
||||
DivAudioExportFormats exportFormat;
|
||||
DivAudioExportBitrateModes exportBitRateMode;
|
||||
double exportFadeOut;
|
||||
bool isFadingOut;
|
||||
int exportOutputs;
|
||||
int exportBitRate;
|
||||
float exportVBRQuality;
|
||||
bool exportChannelMask[DIV_MAX_CHANS];
|
||||
DivConfig conf;
|
||||
FixedQueue<DivNoteEvent,8192> pendingNotes;
|
||||
|
|
@ -1488,9 +1507,12 @@ class DivEngine {
|
|||
audioEngine(DIV_AUDIO_NULL),
|
||||
exportMode(DIV_EXPORT_MODE_ONE),
|
||||
exportFormat(DIV_EXPORT_FORMAT_S16),
|
||||
exportBitRateMode(DIV_EXPORT_BITRATE_CONSTANT),
|
||||
exportFadeOut(0.0),
|
||||
isFadingOut(false),
|
||||
exportOutputs(2),
|
||||
exportBitRate(128000),
|
||||
exportVBRQuality(6.0f),
|
||||
cmdStreamInt(NULL),
|
||||
midiBaseChan(0),
|
||||
midiPoly(true),
|
||||
|
|
|
|||
|
|
@ -123,10 +123,25 @@ void DivEngine::runExportThread() {
|
|||
SFWrapper sfWrap;
|
||||
si.samplerate=got.rate;
|
||||
si.channels=exportOutputs;
|
||||
if (exportFormat==DIV_EXPORT_FORMAT_S16) {
|
||||
si.format=SF_FORMAT_WAV|SF_FORMAT_PCM_16;
|
||||
} else {
|
||||
si.format=SF_FORMAT_WAV|SF_FORMAT_FLOAT;
|
||||
switch (exportFormat) {
|
||||
case DIV_EXPORT_FORMAT_S16:
|
||||
si.format=SF_FORMAT_WAV|SF_FORMAT_PCM_16;
|
||||
break;
|
||||
case DIV_EXPORT_FORMAT_F32:
|
||||
si.format=SF_FORMAT_WAV|SF_FORMAT_FLOAT;
|
||||
break;
|
||||
case DIV_EXPORT_FORMAT_OPUS:
|
||||
si.format=SF_FORMAT_OGG|SF_FORMAT_OPUS;
|
||||
break;
|
||||
case DIV_EXPORT_FORMAT_FLAC:
|
||||
si.format=SF_FORMAT_FLAC;
|
||||
break;
|
||||
case DIV_EXPORT_FORMAT_VORBIS:
|
||||
si.format=SF_FORMAT_OGG|SF_FORMAT_VORBIS;
|
||||
break;
|
||||
case DIV_EXPORT_FORMAT_MPEG_L3:
|
||||
si.format=SF_FORMAT_MPEG|SF_FORMAT_MPEG_LAYER_III;
|
||||
break;
|
||||
}
|
||||
|
||||
sf=sfWrap.doOpen(exportPath.c_str(),SFM_WRITE,&si);
|
||||
|
|
@ -136,6 +151,59 @@ void DivEngine::runExportThread() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (exportFormat!=DIV_EXPORT_FORMAT_S16 && exportFormat!=DIV_EXPORT_FORMAT_F32) {
|
||||
float mappedLevel=0.0f;
|
||||
|
||||
switch (exportFormat) {
|
||||
case DIV_EXPORT_FORMAT_OPUS:
|
||||
mappedLevel=(float)(256000-exportBitRate)/250000.0;
|
||||
break;
|
||||
case DIV_EXPORT_FORMAT_FLAC:
|
||||
mappedLevel=exportVBRQuality*0.125;
|
||||
break;
|
||||
case DIV_EXPORT_FORMAT_VORBIS:
|
||||
mappedLevel=10.0-exportVBRQuality*0.1;
|
||||
break;
|
||||
case DIV_EXPORT_FORMAT_MPEG_L3: {
|
||||
int mappedBitRateMode=SF_BITRATE_MODE_CONSTANT;
|
||||
switch (exportBitRateMode) {
|
||||
case DIV_EXPORT_BITRATE_CONSTANT:
|
||||
mappedBitRateMode=SF_BITRATE_MODE_CONSTANT;
|
||||
break;
|
||||
case DIV_EXPORT_BITRATE_VARIABLE:
|
||||
mappedBitRateMode=SF_BITRATE_MODE_VARIABLE;
|
||||
break;
|
||||
case DIV_EXPORT_BITRATE_AVERAGE:
|
||||
mappedBitRateMode=SF_BITRATE_MODE_AVERAGE;
|
||||
break;
|
||||
}
|
||||
if (exportBitRateMode==DIV_EXPORT_BITRATE_VARIABLE) {
|
||||
mappedLevel=exportVBRQuality*0.1;
|
||||
} else {
|
||||
// a bit complicated
|
||||
if (got.rate>=32000) {
|
||||
mappedLevel=(320000.0f-(float)exportBitRate)/288000.0f;
|
||||
} else if (got.rate>=16000) {
|
||||
mappedLevel=(160000.0f-(float)exportBitRate)/152000.0f;
|
||||
} else {
|
||||
mappedLevel=(64000.0f-(float)exportBitRate)/56000.0f;
|
||||
}
|
||||
}
|
||||
|
||||
if (sf_command(sf,SFC_SET_BITRATE_MODE,&mappedBitRateMode,sizeof(mappedBitRateMode))!=SF_TRUE) {
|
||||
logE("could not set bit rate mode! (%s)",sf_strerror(sf));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (sf_command(sf,SFC_SET_COMPRESSION_LEVEL,&mappedLevel,sizeof(mappedLevel))!=SF_TRUE) {
|
||||
logE("could not set compression level! (%s)",sf_strerror(sf));
|
||||
}
|
||||
}
|
||||
|
||||
float* outBuf[DIV_MAX_OUTPUTS];
|
||||
float* outBufFinal;
|
||||
for (int i=0; i<exportOutputs; i++) {
|
||||
|
|
@ -349,10 +417,25 @@ void DivEngine::runExportThread() {
|
|||
logI("- %s",fname.c_str());
|
||||
si.samplerate=got.rate;
|
||||
si.channels=exportOutputs;
|
||||
if (exportFormat==DIV_EXPORT_FORMAT_S16) {
|
||||
si.format=SF_FORMAT_WAV|SF_FORMAT_PCM_16;
|
||||
} else {
|
||||
si.format=SF_FORMAT_WAV|SF_FORMAT_FLOAT;
|
||||
switch (exportFormat) {
|
||||
case DIV_EXPORT_FORMAT_S16:
|
||||
si.format=SF_FORMAT_WAV|SF_FORMAT_PCM_16;
|
||||
break;
|
||||
case DIV_EXPORT_FORMAT_F32:
|
||||
si.format=SF_FORMAT_WAV|SF_FORMAT_FLOAT;
|
||||
break;
|
||||
case DIV_EXPORT_FORMAT_OPUS:
|
||||
si.format=SF_FORMAT_OGG|SF_FORMAT_OPUS;
|
||||
break;
|
||||
case DIV_EXPORT_FORMAT_FLAC:
|
||||
si.format=SF_FORMAT_FLAC;
|
||||
break;
|
||||
case DIV_EXPORT_FORMAT_VORBIS:
|
||||
si.format=SF_FORMAT_OGG|SF_FORMAT_VORBIS;
|
||||
break;
|
||||
case DIV_EXPORT_FORMAT_MPEG_L3:
|
||||
si.format=SF_FORMAT_MPEG|SF_FORMAT_MPEG_LAYER_III;
|
||||
break;
|
||||
}
|
||||
|
||||
sf=sfWrap.doOpen(fname.c_str(),SFM_WRITE,&si);
|
||||
|
|
@ -490,6 +573,9 @@ bool DivEngine::saveAudio(const char* path, DivAudioExportOptions options) {
|
|||
exportPath=path;
|
||||
exportMode=options.mode;
|
||||
exportFormat=options.format;
|
||||
exportBitRate=options.bitRate;
|
||||
exportBitRateMode=options.bitRateMode;
|
||||
exportVBRQuality=options.vbrQuality;
|
||||
exportFadeOut=options.fadeOut;
|
||||
memcpy(exportChannelMask,options.channelMask,DIV_MAX_CHANS*sizeof(bool));
|
||||
if (exportMode!=DIV_EXPORT_MODE_ONE) {
|
||||
|
|
|
|||
|
|
@ -34,28 +34,59 @@ void FurnaceGUI::drawExportAudio(bool onWindow) {
|
|||
}
|
||||
if (ImGui::RadioButton(_("multiple files (one per chip)"),audioExportOptions.mode==DIV_EXPORT_MODE_MANY_SYS)) {
|
||||
audioExportOptions.mode=DIV_EXPORT_MODE_MANY_SYS;
|
||||
}
|
||||
audioExportOptions.format=DIV_EXPORT_FORMAT_S16;
|
||||
}
|
||||
if (ImGui::RadioButton(_("multiple files (one per channel)"),audioExportOptions.mode==DIV_EXPORT_MODE_MANY_CHAN)) {
|
||||
audioExportOptions.mode=DIV_EXPORT_MODE_MANY_CHAN;
|
||||
}
|
||||
ImGui::Unindent();
|
||||
|
||||
if (audioExportOptions.mode!=DIV_EXPORT_MODE_MANY_SYS) {
|
||||
ImGui::Text(_("Bit depth:"));
|
||||
ImGui::Text(_("File format:"));
|
||||
ImGui::Indent();
|
||||
if (ImGui::RadioButton(_("16-bit integer"),audioExportOptions.format==DIV_EXPORT_FORMAT_S16)) {
|
||||
if (ImGui::RadioButton(_("Wave (16-bit integer)"),audioExportOptions.format==DIV_EXPORT_FORMAT_S16)) {
|
||||
audioExportOptions.format=DIV_EXPORT_FORMAT_S16;
|
||||
}
|
||||
if (ImGui::RadioButton(_("32-bit float"),audioExportOptions.format==DIV_EXPORT_FORMAT_F32)) {
|
||||
if (ImGui::RadioButton(_("Wave (32-bit float)"),audioExportOptions.format==DIV_EXPORT_FORMAT_F32)) {
|
||||
audioExportOptions.format=DIV_EXPORT_FORMAT_F32;
|
||||
}
|
||||
#ifdef HAVE_OGG
|
||||
if (ImGui::RadioButton(_("Opus (lossy compression)"),audioExportOptions.format==DIV_EXPORT_FORMAT_OPUS)) {
|
||||
audioExportOptions.format=DIV_EXPORT_FORMAT_OPUS;
|
||||
}
|
||||
if (ImGui::RadioButton(_("FLAC (Free Lossless Audio Codec)"),audioExportOptions.format==DIV_EXPORT_FORMAT_FLAC)) {
|
||||
audioExportOptions.format=DIV_EXPORT_FORMAT_FLAC;
|
||||
}
|
||||
if (ImGui::RadioButton(_("Vorbis (lossy compression)"),audioExportOptions.format==DIV_EXPORT_FORMAT_VORBIS)) {
|
||||
audioExportOptions.format=DIV_EXPORT_FORMAT_VORBIS;
|
||||
}
|
||||
#endif
|
||||
#ifdef HAVE_MP3_EXPORT
|
||||
if (ImGui::RadioButton(_("MP3 (lossy compression)"),audioExportOptions.format==DIV_EXPORT_FORMAT_MPEG_L3)) {
|
||||
audioExportOptions.format=DIV_EXPORT_FORMAT_MPEG_L3;
|
||||
}
|
||||
#endif
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
bool rateCheck=(
|
||||
audioExportOptions.format==DIV_EXPORT_FORMAT_OPUS && (
|
||||
audioExportOptions.sampleRate!=8000 &&
|
||||
audioExportOptions.sampleRate!=12000 &&
|
||||
audioExportOptions.sampleRate!=16000 &&
|
||||
audioExportOptions.sampleRate!=24000 &&
|
||||
audioExportOptions.sampleRate!=48000
|
||||
)
|
||||
);
|
||||
pushWarningColor(false,rateCheck);
|
||||
if (ImGui::InputInt(_("Sample rate"),&audioExportOptions.sampleRate,100,10000)) {
|
||||
if (audioExportOptions.sampleRate<8000) audioExportOptions.sampleRate=8000;
|
||||
if (audioExportOptions.sampleRate>384000) audioExportOptions.sampleRate=384000;
|
||||
}
|
||||
if (rateCheck) {
|
||||
ImGui::SetItemTooltip(_("Opus only supports the following sample rates: 8000, 12000, 16000, 24000 and 48000."));
|
||||
}
|
||||
popWarningColor();
|
||||
|
||||
if (audioExportOptions.mode!=DIV_EXPORT_MODE_MANY_SYS) {
|
||||
if (ImGui::InputInt(_("Channels in file"),&audioExportOptions.chans,1,1)) {
|
||||
|
|
@ -64,6 +95,56 @@ void FurnaceGUI::drawExportAudio(bool onWindow) {
|
|||
}
|
||||
}
|
||||
|
||||
if (audioExportOptions.format==DIV_EXPORT_FORMAT_MPEG_L3) {
|
||||
ImGui::Text(_("Bit rate mode:"));
|
||||
ImGui::Indent();
|
||||
if (ImGui::RadioButton(_("Constant"),audioExportOptions.bitRateMode==DIV_EXPORT_BITRATE_CONSTANT)) {
|
||||
audioExportOptions.bitRateMode=DIV_EXPORT_BITRATE_CONSTANT;
|
||||
}
|
||||
if (ImGui::RadioButton(_("Variable"),audioExportOptions.bitRateMode==DIV_EXPORT_BITRATE_VARIABLE)) {
|
||||
audioExportOptions.bitRateMode=DIV_EXPORT_BITRATE_VARIABLE;
|
||||
}
|
||||
if (ImGui::RadioButton(_("Average"),audioExportOptions.bitRateMode==DIV_EXPORT_BITRATE_AVERAGE)) {
|
||||
audioExportOptions.bitRateMode=DIV_EXPORT_BITRATE_AVERAGE;
|
||||
}
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
int minBitRate=6000;
|
||||
int maxBitRate=256000;
|
||||
|
||||
if (audioExportOptions.format==DIV_EXPORT_FORMAT_MPEG_L3) {
|
||||
if (audioExportOptions.sampleRate>=32000) {
|
||||
minBitRate=32000;
|
||||
maxBitRate=320000;
|
||||
} else if (audioExportOptions.sampleRate>=16000) {
|
||||
minBitRate=8000;
|
||||
maxBitRate=160000;
|
||||
} else {
|
||||
minBitRate=8000;
|
||||
maxBitRate=64000;
|
||||
}
|
||||
}
|
||||
|
||||
if (audioExportOptions.format!=DIV_EXPORT_FORMAT_S16 && audioExportOptions.format!=DIV_EXPORT_FORMAT_F32) {
|
||||
if (audioExportOptions.format==DIV_EXPORT_FORMAT_FLAC) {
|
||||
if (ImGui::SliderFloat(_("Compression level"),&audioExportOptions.vbrQuality,0,8)) {
|
||||
if (audioExportOptions.vbrQuality<0) audioExportOptions.vbrQuality=0;
|
||||
if (audioExportOptions.vbrQuality>8) audioExportOptions.vbrQuality=8;
|
||||
}
|
||||
} else if (audioExportOptions.format==DIV_EXPORT_FORMAT_VORBIS || (audioExportOptions.format==DIV_EXPORT_FORMAT_MPEG_L3 && audioExportOptions.bitRateMode==DIV_EXPORT_BITRATE_VARIABLE)) {
|
||||
if (ImGui::SliderFloat(_("Quality"),&audioExportOptions.vbrQuality,0,10)) {
|
||||
if (audioExportOptions.vbrQuality<0) audioExportOptions.vbrQuality=0;
|
||||
if (audioExportOptions.vbrQuality>10) audioExportOptions.vbrQuality=10;
|
||||
}
|
||||
} else {
|
||||
if (ImGui::InputInt(_("Bit rate"),&audioExportOptions.bitRate,1000,10000)) {
|
||||
}
|
||||
if (audioExportOptions.bitRate<minBitRate) audioExportOptions.bitRate=minBitRate;
|
||||
if (audioExportOptions.bitRate>maxBitRate) audioExportOptions.bitRate=maxBitRate;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::InputInt(_("Loops"),&audioExportOptions.loops,1,2)) {
|
||||
if (audioExportOptions.loops<0) audioExportOptions.loops=0;
|
||||
}
|
||||
|
|
@ -123,8 +204,33 @@ void FurnaceGUI::drawExportAudio(bool onWindow) {
|
|||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
if (isOneOn) {
|
||||
if (isOneOn && !rateCheck) {
|
||||
if (ImGui::Button(_("Export"),ImVec2(200.0f*dpiScale,0))) {
|
||||
switch (audioExportOptions.format) {
|
||||
case DIV_EXPORT_FORMAT_S16:
|
||||
case DIV_EXPORT_FORMAT_F32:
|
||||
audioExportFilterName=_("Wave file");
|
||||
audioExportFilterExt=".wav";
|
||||
break;
|
||||
case DIV_EXPORT_FORMAT_OPUS:
|
||||
case DIV_EXPORT_FORMAT_VORBIS:
|
||||
audioExportFilterName=_("Ogg files");
|
||||
audioExportFilterExt=".ogg";
|
||||
break;
|
||||
case DIV_EXPORT_FORMAT_FLAC:
|
||||
audioExportFilterName=_("FLAC files");
|
||||
audioExportFilterExt=".flac";
|
||||
break;
|
||||
case DIV_EXPORT_FORMAT_MPEG_L3:
|
||||
audioExportFilterName=_("MPEG Layer 3 files");
|
||||
audioExportFilterExt=".mp3";
|
||||
break;
|
||||
default:
|
||||
audioExportFilterName=_("all files");
|
||||
audioExportFilterExt="*";
|
||||
break;
|
||||
}
|
||||
|
||||
switch (audioExportOptions.mode) {
|
||||
case DIV_EXPORT_MODE_ONE:
|
||||
openFileDialog(GUI_FILE_EXPORT_AUDIO_ONE);
|
||||
|
|
@ -139,7 +245,11 @@ void FurnaceGUI::drawExportAudio(bool onWindow) {
|
|||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
} else {
|
||||
ImGui::Text(_("select at least one channel"));
|
||||
if (rateCheck) {
|
||||
ImGui::Text(_("check sample rate"));
|
||||
} else {
|
||||
ImGui::Text(_("select at least one channel"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2097,7 +2097,7 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) {
|
|||
if (!dirExists(workingDirAudioExport)) workingDirAudioExport=getHomeDir();
|
||||
hasOpened=fileDialog->openSave(
|
||||
_("Export Audio"),
|
||||
{_("Wave file"), "*.wav"},
|
||||
{audioExportFilterName, "*"+audioExportFilterExt},
|
||||
workingDirAudioExport,
|
||||
dpiScale,
|
||||
(settings.autoFillSave)?shortName:""
|
||||
|
|
@ -2107,7 +2107,7 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) {
|
|||
if (!dirExists(workingDirAudioExport)) workingDirAudioExport=getHomeDir();
|
||||
hasOpened=fileDialog->openSave(
|
||||
_("Export Audio"),
|
||||
{_("Wave file"), "*.wav"},
|
||||
{audioExportFilterName, "*"+audioExportFilterExt},
|
||||
workingDirAudioExport,
|
||||
dpiScale,
|
||||
(settings.autoFillSave)?shortName:""
|
||||
|
|
@ -2117,7 +2117,7 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) {
|
|||
if (!dirExists(workingDirAudioExport)) workingDirAudioExport=getHomeDir();
|
||||
hasOpened=fileDialog->openSave(
|
||||
_("Export Audio"),
|
||||
{_("Wave file"), "*.wav"},
|
||||
{audioExportFilterName, "*"+audioExportFilterExt},
|
||||
workingDirAudioExport,
|
||||
dpiScale,
|
||||
(settings.autoFillSave)?shortName:""
|
||||
|
|
@ -5267,7 +5267,9 @@ bool FurnaceGUI::loop() {
|
|||
curFileDialog==GUI_FILE_EXPORT_AUDIO_ONE ||
|
||||
curFileDialog==GUI_FILE_EXPORT_AUDIO_PER_SYS ||
|
||||
curFileDialog==GUI_FILE_EXPORT_AUDIO_PER_CHANNEL) {
|
||||
checkExtension(".wav");
|
||||
if (audioExportFilterExt!="*") {
|
||||
checkExtension(audioExportFilterExt.c_str());
|
||||
}
|
||||
}
|
||||
if (curFileDialog==GUI_FILE_INS_SAVE) {
|
||||
checkExtension(".fui");
|
||||
|
|
@ -9090,6 +9092,8 @@ FurnaceGUI::FurnaceGUI():
|
|||
csExportResult(NULL),
|
||||
csExportTarget(false),
|
||||
csExportDone(false),
|
||||
audioExportFilterName("???"),
|
||||
audioExportFilterExt("*"),
|
||||
dmfExportVersion(0),
|
||||
curExportType(GUI_EXPORT_NONE),
|
||||
romTarget(DIV_ROM_ABSTRACT),
|
||||
|
|
|
|||
|
|
@ -2811,6 +2811,7 @@ class FurnaceGUI {
|
|||
|
||||
// export options
|
||||
DivAudioExportOptions audioExportOptions;
|
||||
String audioExportFilterName, audioExportFilterExt;
|
||||
int dmfExportVersion;
|
||||
FurnaceGUIExportTypes curExportType;
|
||||
DivCSOptions csExportOptions;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue