641 lines
19 KiB
C++
641 lines
19 KiB
C++
|
/*
|
||
|
* OPLExport.cpp
|
||
|
* -------------
|
||
|
* Purpose: Export of OPL register dumps as VGM/VGZ or DRO files
|
||
|
* Notes : (currently none)
|
||
|
* Authors: OpenMPT Devs
|
||
|
* The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
|
||
|
*/
|
||
|
|
||
|
|
||
|
#include "stdafx.h"
|
||
|
#include "FileDialog.h"
|
||
|
#include "InputHandler.h"
|
||
|
#include "Mainfrm.h"
|
||
|
#include "Moddoc.h"
|
||
|
#include "ProgressDialog.h"
|
||
|
#include "../soundlib/OPL.h"
|
||
|
#include "../soundlib/Tagging.h"
|
||
|
|
||
|
#include <zlib/zlib.h>
|
||
|
|
||
|
OPENMPT_NAMESPACE_BEGIN
|
||
|
|
||
|
// DRO file header
|
||
|
struct DROHeaderV1
|
||
|
{
|
||
|
static constexpr char droMagic[] = "DBRAWOPL";
|
||
|
|
||
|
char magic[8];
|
||
|
uint16le verHi;
|
||
|
uint16le verLo;
|
||
|
uint32le lengthMs;
|
||
|
uint32le lengthBytes;
|
||
|
uint32le hardwareType;
|
||
|
};
|
||
|
|
||
|
MPT_BINARY_STRUCT(DROHeaderV1, 24);
|
||
|
|
||
|
|
||
|
// VGM file header
|
||
|
struct VGMHeader
|
||
|
{
|
||
|
static constexpr char VgmMagic[] = "Vgm ";
|
||
|
|
||
|
char magic[4];
|
||
|
uint32le eofOffset;
|
||
|
uint32le version;
|
||
|
uint32le sn76489clock;
|
||
|
uint32le ym2413clock;
|
||
|
uint32le gd3Offset;
|
||
|
uint32le totalNumSamples;
|
||
|
uint32le loopOffset;
|
||
|
uint32le loopNumSamples;
|
||
|
uint32le rate;
|
||
|
uint32le someChipClocks[3];
|
||
|
uint32le vgmDataOffset;
|
||
|
uint32le variousChipClocks[9];
|
||
|
uint32le ymf262clock; // 14318180
|
||
|
uint32le evenMoreChipClocks[7];
|
||
|
uint8 volumeModifier;
|
||
|
uint8 reserved[131]; // Various other fields we're not interested in
|
||
|
};
|
||
|
|
||
|
MPT_BINARY_STRUCT(VGMHeader, 256);
|
||
|
|
||
|
|
||
|
// VGM metadata header
|
||
|
struct Gd3Header
|
||
|
{
|
||
|
static constexpr char Gd3Magic[] = "Gd3 ";
|
||
|
|
||
|
char magic[4];
|
||
|
uint32le version;
|
||
|
uint32le size;
|
||
|
};
|
||
|
|
||
|
MPT_BINARY_STRUCT(Gd3Header, 12);
|
||
|
|
||
|
|
||
|
// The OPL register logger and serializer for VGM/VGZ/DRO files
|
||
|
class OPLCapture final : public OPL::IRegisterLogger
|
||
|
{
|
||
|
struct RegisterDump
|
||
|
{
|
||
|
CSoundFile::samplecount_t sampleOffset;
|
||
|
uint8 regLo;
|
||
|
uint8 regHi;
|
||
|
uint8 value;
|
||
|
};
|
||
|
|
||
|
public:
|
||
|
OPLCapture(CSoundFile &sndFile) : m_sndFile{sndFile} {}
|
||
|
|
||
|
void Reset()
|
||
|
{
|
||
|
m_registerDump.clear();
|
||
|
m_prevRegisters.clear();
|
||
|
}
|
||
|
|
||
|
void CaptureAllVoiceRegisters()
|
||
|
{
|
||
|
for(const auto reg : OPL::AllVoiceRegisters())
|
||
|
{
|
||
|
uint8 value = 0;
|
||
|
if(const auto prevValue = m_prevRegisters.find(reg); prevValue != m_prevRegisters.end())
|
||
|
value = prevValue->second;
|
||
|
m_registerDumpAtLoopStart[reg] = value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void WriteDRO(std::ostream &f) const
|
||
|
{
|
||
|
DROHeaderV1 header{};
|
||
|
memcpy(header.magic, DROHeaderV1::droMagic, 8);
|
||
|
header.verHi = 0;
|
||
|
header.verLo = 1;
|
||
|
header.lengthMs = Util::muldivr_unsigned(m_sndFile.GetTotalSampleCount(), 1000, m_sndFile.GetSampleRate());
|
||
|
header.lengthBytes = 0;
|
||
|
header.hardwareType = 1; // OPL3
|
||
|
|
||
|
mpt::IO::Write(f, header);
|
||
|
|
||
|
CSoundFile::samplecount_t prevOffset = 0, prevOffsetMs = 0;
|
||
|
bool prevHigh = false;
|
||
|
for(const auto ® : m_registerDump)
|
||
|
{
|
||
|
if(reg.sampleOffset > prevOffset)
|
||
|
{
|
||
|
uint32 offsetMs = Util::muldivr_unsigned(reg.sampleOffset, 1000, m_sndFile.GetSampleRate());
|
||
|
header.lengthBytes += WriteDRODelay(f, offsetMs - prevOffsetMs);
|
||
|
prevOffset = reg.sampleOffset;
|
||
|
prevOffsetMs = offsetMs;
|
||
|
}
|
||
|
if(const bool isHigh = (reg.regHi == 1); isHigh != prevHigh)
|
||
|
{
|
||
|
prevHigh = isHigh;
|
||
|
mpt::IO::Write(f, mpt::as_byte(2 + reg.regHi));
|
||
|
header.lengthBytes++;
|
||
|
}
|
||
|
if(reg.regLo <= 4)
|
||
|
{
|
||
|
mpt::IO::Write(f, mpt::as_byte(4));
|
||
|
header.lengthBytes++;
|
||
|
}
|
||
|
const uint8 regValue[] = {reg.regLo, reg.value};
|
||
|
mpt::IO::Write(f, regValue);
|
||
|
header.lengthBytes += 2;
|
||
|
}
|
||
|
if(header.lengthMs > prevOffsetMs)
|
||
|
header.lengthBytes += WriteDRODelay(f, header.lengthMs - prevOffsetMs);
|
||
|
|
||
|
MPT_ASSERT(mpt::IO::TellWrite(f) == static_cast<mpt::IO::Offset>(header.lengthBytes + sizeof(header)));
|
||
|
// AdPlug can read some metadata following the register dump, but DroTrimmer panics if it see that data.
|
||
|
// As the metadata is very limited (40 characters per field, unknown 8-bit encoding) we'll leave that feature to the VGM export.
|
||
|
#if 0
|
||
|
mpt::IO::Write(f, mpt::as_byte(0xFF));
|
||
|
mpt::IO::Write(f, mpt::as_byte(0xFF));
|
||
|
|
||
|
char name[40];
|
||
|
mpt::String::WriteBuf(mpt::String::maybeNullTerminated, name) = m_sndFile.m_songName;
|
||
|
mpt::IO::Write(f, mpt::as_byte(0x1A));
|
||
|
mpt::IO::Write(f, name);
|
||
|
|
||
|
if(!m_sndFile.m_songArtist.empty())
|
||
|
{
|
||
|
mpt::String::WriteBuf(mpt::String::maybeNullTerminated, name) = mpt::ToCharset(mpt::Charset::ISO8859_1, m_sndFile.m_songArtist);
|
||
|
mpt::IO::Write(f, mpt::as_byte(0x1B));
|
||
|
mpt::IO::Write(f, name);
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
mpt::IO::SeekAbsolute(f, 0);
|
||
|
mpt::IO::Write(f, header);
|
||
|
}
|
||
|
|
||
|
void WriteVGZ(std::ostream &f, const CSoundFile::samplecount_t loopStart, const FileTags &fileTags, const mpt::ustring &filename) const
|
||
|
{
|
||
|
std::ostringstream outStream;
|
||
|
WriteVGM(outStream, loopStart, fileTags);
|
||
|
|
||
|
std::string outData = std::move(outStream).str();
|
||
|
z_stream strm{};
|
||
|
strm.avail_in = static_cast<uInt>(outData.size());
|
||
|
strm.next_in = reinterpret_cast<Bytef *>(outData.data());
|
||
|
if(deflateInit2(&strm, Z_BEST_COMPRESSION, Z_DEFLATED, 15 | 16, 9, Z_DEFAULT_STRATEGY) != Z_OK)
|
||
|
throw std::runtime_error{"zlib init failed"};
|
||
|
gz_header gzHeader{};
|
||
|
gzHeader.time = static_cast<uLong>(time(nullptr));
|
||
|
std::string filenameISO = mpt::ToCharset(mpt::Charset::ISO8859_1, filename);
|
||
|
gzHeader.name = reinterpret_cast<Bytef *>(filenameISO.data());
|
||
|
deflateSetHeader(&strm, &gzHeader);
|
||
|
do
|
||
|
{
|
||
|
std::array<Bytef, mpt::IO::BUFFERSIZE_TINY> buffer;
|
||
|
strm.avail_out = static_cast<uInt>(buffer.size());
|
||
|
strm.next_out = buffer.data();
|
||
|
deflate(&strm, Z_FINISH);
|
||
|
mpt::IO::WritePartial(f, buffer, buffer.size() - strm.avail_out);
|
||
|
} while(strm.avail_out == 0);
|
||
|
deflateEnd(&strm);
|
||
|
}
|
||
|
|
||
|
void WriteVGM(std::ostream &f, const CSoundFile::samplecount_t loopStart, const FileTags &fileTags) const
|
||
|
{
|
||
|
VGMHeader header{};
|
||
|
memcpy(header.magic, VGMHeader::VgmMagic, 4);
|
||
|
header.version = 0x160;
|
||
|
header.vgmDataOffset = sizeof(header) - offsetof(VGMHeader, vgmDataOffset);
|
||
|
header.ymf262clock = 14318180;
|
||
|
header.totalNumSamples = static_cast<uint32>(m_sndFile.GetTotalSampleCount());
|
||
|
if(loopStart != Util::MaxValueOfType(loopStart))
|
||
|
header.loopNumSamples = static_cast<uint32>(m_sndFile.GetTotalSampleCount() - loopStart);
|
||
|
|
||
|
mpt::IO::Write(f, header);
|
||
|
|
||
|
bool wroteLoopStart = (header.loopNumSamples == 0);
|
||
|
CSoundFile::samplecount_t prevOffset = 0;
|
||
|
for(const auto ® : m_registerDump)
|
||
|
{
|
||
|
if(reg.sampleOffset >= loopStart && !wroteLoopStart)
|
||
|
{
|
||
|
WriteVGMDelay(f, loopStart - prevOffset);
|
||
|
prevOffset = loopStart;
|
||
|
header.loopOffset = static_cast<uint32>(mpt::IO::TellWrite(f) - 0x1C);
|
||
|
wroteLoopStart = true;
|
||
|
for(const auto & [loopReg, value] : m_registerDumpAtLoopStart)
|
||
|
{
|
||
|
if(m_prevRegisters.count(loopReg))
|
||
|
{
|
||
|
const uint8 data[] = {static_cast<uint8>(0x5E + (loopReg >> 8)), static_cast<uint8>(loopReg & 0xFF), value};
|
||
|
mpt::IO::Write(f, data);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
WriteVGMDelay(f, reg.sampleOffset - prevOffset);
|
||
|
prevOffset = reg.sampleOffset;
|
||
|
const uint8 data[] = {static_cast<uint8>(0x5E + reg.regHi), reg.regLo, reg.value};
|
||
|
mpt::IO::Write(f, data);
|
||
|
}
|
||
|
WriteVGMDelay(f, m_sndFile.GetTotalSampleCount() - prevOffset);
|
||
|
mpt::IO::Write(f, mpt::as_byte(0x66));
|
||
|
|
||
|
header.gd3Offset = static_cast<uint32>(mpt::IO::TellWrite(f) - offsetof(VGMHeader, gd3Offset));
|
||
|
|
||
|
const mpt::ustring tags[] =
|
||
|
{
|
||
|
fileTags.title,
|
||
|
{}, // Song name JP
|
||
|
{}, // Game name EN
|
||
|
{}, // Game name JP
|
||
|
Version::Current().GetOpenMPTVersionString(),
|
||
|
{}, // System name JP
|
||
|
fileTags.artist,
|
||
|
{}, // Author name JP
|
||
|
fileTags.year,
|
||
|
{}, // Person who created the VGM file
|
||
|
mpt::String::Replace(fileTags.comments, U_("\r\n"), U_("\n")),
|
||
|
};
|
||
|
std::ostringstream tagStream;
|
||
|
for(const auto &tag : tags)
|
||
|
{
|
||
|
WriteVGMString(tagStream, mpt::ToWide(tag));
|
||
|
}
|
||
|
const auto tagsData = std::move(tagStream).str();
|
||
|
|
||
|
Gd3Header gd3Header{};
|
||
|
memcpy(gd3Header.magic, Gd3Header::Gd3Magic, 4);
|
||
|
gd3Header.version = 0x100;
|
||
|
gd3Header.size = static_cast<uint32>(tagsData.size());
|
||
|
mpt::IO::Write(f, gd3Header);
|
||
|
mpt::IO::WriteRaw(f, mpt::as_span(tagsData));
|
||
|
|
||
|
header.eofOffset = static_cast<uint32>(mpt::IO::TellWrite(f) - offsetof(VGMHeader, eofOffset));
|
||
|
|
||
|
mpt::IO::SeekAbsolute(f, 0);
|
||
|
mpt::IO::Write(f, header);
|
||
|
}
|
||
|
|
||
|
private:
|
||
|
static uint32 WriteDRODelay(std::ostream &f, uint32 delay)
|
||
|
{
|
||
|
uint32 bytesWritten = 0;
|
||
|
while(delay > 256)
|
||
|
{
|
||
|
uint32 subDelay = std::min(delay, 65536u);
|
||
|
mpt::IO::Write(f, mpt::as_byte(1));
|
||
|
mpt::IO::WriteIntLE(f, static_cast<uint16>(subDelay - 1));
|
||
|
bytesWritten += 3;
|
||
|
delay -= subDelay;
|
||
|
}
|
||
|
if(delay)
|
||
|
{
|
||
|
mpt::IO::Write(f, mpt::as_byte(0));
|
||
|
mpt::IO::WriteIntLE(f, static_cast<uint8>(delay - 1));
|
||
|
bytesWritten += 2;
|
||
|
}
|
||
|
return bytesWritten;
|
||
|
}
|
||
|
|
||
|
static void WriteVGMDelay(std::ostream &f, CSoundFile::samplecount_t delay)
|
||
|
{
|
||
|
while(delay)
|
||
|
{
|
||
|
uint16 subDelay = mpt::saturate_cast<uint16>(delay);
|
||
|
if(subDelay <= 16)
|
||
|
{
|
||
|
mpt::IO::Write(f, mpt::as_byte(0x6F + subDelay));
|
||
|
} else if(subDelay == 735)
|
||
|
{
|
||
|
mpt::IO::Write(f, mpt::as_byte(0x62)); // 1/60th of a second
|
||
|
} else if(subDelay == 882)
|
||
|
{
|
||
|
mpt::IO::Write(f, mpt::as_byte(0x63)); // 1/50th of a second
|
||
|
} else
|
||
|
{
|
||
|
mpt::IO::Write(f, mpt::as_byte(0x61));
|
||
|
mpt::IO::WriteIntLE(f, subDelay);
|
||
|
}
|
||
|
delay -= subDelay;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static void WriteVGMString(std::ostream &f, const std::wstring &s)
|
||
|
{
|
||
|
std::vector<uint16le> s16le(s.length() + 1);
|
||
|
for(size_t i = 0; i < s.length(); i++)
|
||
|
{
|
||
|
s16le[i] = s[i] ? s[i] : L' ';
|
||
|
}
|
||
|
mpt::IO::Write(f, s16le);
|
||
|
}
|
||
|
|
||
|
void Port(CHANNELINDEX, uint16 reg, uint8 value) override
|
||
|
{
|
||
|
if(const auto prevValue = m_prevRegisters.find(reg); prevValue != m_prevRegisters.end() && prevValue->second == value)
|
||
|
return;
|
||
|
m_registerDump.push_back({m_sndFile.GetTotalSampleCount(), static_cast<uint8>(reg & 0xFF), static_cast<uint8>(reg >> 8), value});
|
||
|
m_prevRegisters[reg] = value;
|
||
|
}
|
||
|
|
||
|
std::vector<RegisterDump> m_registerDump;
|
||
|
std::map<uint16, uint8> m_prevRegisters, m_registerDumpAtLoopStart;
|
||
|
CSoundFile &m_sndFile;
|
||
|
};
|
||
|
|
||
|
|
||
|
class OPLExportDlg : public CProgressDialog
|
||
|
{
|
||
|
private:
|
||
|
enum class ExportFormat
|
||
|
{
|
||
|
VGZ = IDC_RADIO1,
|
||
|
VGM = IDC_RADIO2,
|
||
|
DRO = IDC_RADIO3,
|
||
|
};
|
||
|
|
||
|
static ExportFormat s_format;
|
||
|
|
||
|
OPLCapture m_oplLogger;
|
||
|
CSoundFile &m_sndFile;
|
||
|
CModDoc &m_modDoc;
|
||
|
|
||
|
std::vector<SubSong> m_subSongs;
|
||
|
size_t m_selectedSong = 0;
|
||
|
bool m_conversionRunning = false;
|
||
|
bool m_locked = true;
|
||
|
|
||
|
public:
|
||
|
OPLExportDlg(CModDoc &modDoc, CWnd *parent = nullptr)
|
||
|
: CProgressDialog{parent, IDD_OPLEXPORT}
|
||
|
, m_oplLogger{modDoc.GetSoundFile()}
|
||
|
, m_sndFile{modDoc.GetSoundFile()}
|
||
|
, m_modDoc{modDoc}
|
||
|
, m_subSongs{modDoc.GetSoundFile().GetAllSubSongs()}
|
||
|
{
|
||
|
}
|
||
|
|
||
|
BOOL OnInitDialog() override
|
||
|
{
|
||
|
CProgressDialog::OnInitDialog();
|
||
|
|
||
|
CheckRadioButton(IDC_RADIO1, IDC_RADIO3, static_cast<int>(s_format));
|
||
|
CheckRadioButton(IDC_RADIO4, IDC_RADIO5, IDC_RADIO4);
|
||
|
|
||
|
static_cast<CSpinButtonCtrl *>(GetDlgItem(IDC_SPIN1))->SetRange32(1, static_cast<int>(m_subSongs.size()));
|
||
|
SetDlgItemInt(IDC_EDIT1, static_cast<UINT>(m_selectedSong + 1), FALSE);
|
||
|
if(m_subSongs.size() <= 1)
|
||
|
{
|
||
|
const int controls[] = {IDC_RADIO4, IDC_RADIO5, IDC_EDIT1, IDC_SPIN1};
|
||
|
for(int control : controls)
|
||
|
GetDlgItem(control)->EnableWindow(FALSE);
|
||
|
}
|
||
|
UpdateSubsongName();
|
||
|
OnFormatChanged();
|
||
|
|
||
|
SetDlgItemText(IDC_EDIT2, mpt::ToWin(m_sndFile.GetCharsetFile(), m_sndFile.GetTitle()).c_str());
|
||
|
SetDlgItemText(IDC_EDIT3, mpt::ToWin(m_sndFile.m_songArtist).c_str());
|
||
|
if(!m_sndFile.GetFileHistory().empty())
|
||
|
SetDlgItemText(IDC_EDIT4, mpt::ToWin(mpt::String::Replace(m_sndFile.GetFileHistory().back().AsISO8601().substr(0, 10), U_("-"), U_("/"))).c_str());
|
||
|
SetDlgItemText(IDC_EDIT5, mpt::ToWin(m_sndFile.GetCharsetFile(), m_sndFile.m_songMessage.GetFormatted(SongMessage::leCRLF)).c_str());
|
||
|
|
||
|
m_locked = false;
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
void OnOK() override
|
||
|
{
|
||
|
mpt::PathString extension = P_("vgz");
|
||
|
s_format = static_cast<ExportFormat>(GetCheckedRadioButton(IDC_RADIO1, IDC_RADIO3));
|
||
|
if(s_format == ExportFormat::DRO)
|
||
|
extension = P_("dro");
|
||
|
else if(s_format == ExportFormat::VGM)
|
||
|
extension = P_("vgm");
|
||
|
|
||
|
FileDialog dlg = SaveFileDialog()
|
||
|
.DefaultExtension(extension)
|
||
|
.DefaultFilename(m_modDoc.GetPathNameMpt().GetFileName().ReplaceExt(P_(".") + extension))
|
||
|
.ExtensionFilter(MPT_UFORMAT("{} Files|*.{}||")(mpt::ToUpperCase(extension.ToUnicode()), extension))
|
||
|
.WorkingDirectory(TrackerSettings::Instance().PathExport.GetWorkingDir());
|
||
|
if(!dlg.Show())
|
||
|
{
|
||
|
OnCancel();
|
||
|
return;
|
||
|
}
|
||
|
TrackerSettings::Instance().PathExport.SetWorkingDir(dlg.GetWorkingDirectory());
|
||
|
|
||
|
DoConversion(dlg.GetFirstFile());
|
||
|
|
||
|
CProgressDialog::OnOK();
|
||
|
}
|
||
|
|
||
|
void OnCancel() override
|
||
|
{
|
||
|
if(m_conversionRunning)
|
||
|
CProgressDialog::OnCancel();
|
||
|
else
|
||
|
CDialog::OnCancel();
|
||
|
}
|
||
|
|
||
|
void Run() override {}
|
||
|
|
||
|
afx_msg void OnFormatChanged()
|
||
|
{
|
||
|
const int controls[] = {IDC_EDIT2, IDC_EDIT3, IDC_EDIT4, IDC_EDIT5};
|
||
|
for(int control : controls)
|
||
|
GetDlgItem(control)->EnableWindow(GetCheckedRadioButton(IDC_RADIO1, IDC_RADIO3) == static_cast<int>(ExportFormat::DRO) ? FALSE : TRUE);
|
||
|
}
|
||
|
|
||
|
afx_msg void OnSubsongChanged()
|
||
|
{
|
||
|
if(m_locked)
|
||
|
return;
|
||
|
CheckRadioButton(IDC_RADIO4, IDC_RADIO5, IDC_RADIO5);
|
||
|
BOOL ok = FALSE;
|
||
|
const auto newSubSong = std::clamp(static_cast<size_t>(GetDlgItemInt(IDC_EDIT1, &ok, FALSE)), size_t(1), m_subSongs.size()) - 1;
|
||
|
if(m_selectedSong == newSubSong || !ok)
|
||
|
return;
|
||
|
m_selectedSong = newSubSong;
|
||
|
UpdateSubsongName();
|
||
|
}
|
||
|
|
||
|
void UpdateSubsongName()
|
||
|
{
|
||
|
const auto subsongText = GetDlgItem(IDC_SUBSONG);
|
||
|
if(subsongText == nullptr || m_selectedSong >= m_subSongs.size())
|
||
|
return;
|
||
|
const auto &song = m_subSongs[m_selectedSong];
|
||
|
const auto sequenceName = m_sndFile.Order(song.sequence).GetName();
|
||
|
const auto startPattern = m_sndFile.Order(song.sequence).PatternAt(song.startOrder);
|
||
|
const auto orderName = startPattern ? startPattern->GetName() : std::string{};
|
||
|
subsongText->SetWindowText(MPT_TFORMAT("Sequence {}{}\nOrder {} to {}{}")(
|
||
|
song.sequence + 1,
|
||
|
sequenceName.empty() ? mpt::tstring{} : MPT_TFORMAT(" ({})")(sequenceName),
|
||
|
song.startOrder,
|
||
|
song.endOrder,
|
||
|
orderName.empty() ? mpt::tstring{} : MPT_TFORMAT(" ({})")(mpt::ToWin(m_sndFile.GetCharsetInternal(), orderName)))
|
||
|
.c_str());
|
||
|
}
|
||
|
|
||
|
void DoConversion(const mpt::PathString &fileName)
|
||
|
{
|
||
|
const int controls[] = {IDC_RADIO1, IDC_RADIO2, IDC_RADIO3, IDC_RADIO4, IDC_RADIO5, IDC_EDIT1, IDC_EDIT2, IDC_EDIT3, IDC_EDIT4, IDC_EDIT5, IDC_SPIN1, IDOK};
|
||
|
for(int control : controls)
|
||
|
GetDlgItem(control)->EnableWindow(FALSE);
|
||
|
|
||
|
BypassInputHandler bih;
|
||
|
CMainFrame::GetMainFrame()->StopMod(&m_modDoc);
|
||
|
|
||
|
FileTags fileTags;
|
||
|
{
|
||
|
CString title, artist, date, notes;
|
||
|
GetDlgItemText(IDC_EDIT2, title);
|
||
|
GetDlgItemText(IDC_EDIT3, artist);
|
||
|
GetDlgItemText(IDC_EDIT4, date);
|
||
|
GetDlgItemText(IDC_EDIT5, notes);
|
||
|
fileTags.title = mpt::ToUnicode(title);
|
||
|
fileTags.artist = mpt::ToUnicode(artist);
|
||
|
fileTags.year = mpt::ToUnicode(date);
|
||
|
fileTags.comments = mpt::ToUnicode(notes);
|
||
|
}
|
||
|
|
||
|
if(IsDlgButtonChecked(IDC_RADIO5))
|
||
|
m_subSongs = {m_subSongs[m_selectedSong]};
|
||
|
|
||
|
SetRange(0, mpt::saturate_round<uint64>(std::accumulate(m_subSongs.begin(), m_subSongs.end(), 0.0, [](double acc, const auto &song) { return acc + song.duration; }) * m_sndFile.GetSampleRate()));
|
||
|
GetDlgItem(IDC_PROGRESS1)->ShowWindow(SW_SHOW);
|
||
|
|
||
|
m_sndFile.m_bIsRendering = true;
|
||
|
|
||
|
const auto origSettings = m_sndFile.m_MixerSettings;
|
||
|
auto newSettings = m_sndFile.m_MixerSettings;
|
||
|
if(s_format != ExportFormat::DRO)
|
||
|
newSettings.gdwMixingFreq = 44100; // required for VGM, DRO doesn't care
|
||
|
m_sndFile.SetMixerSettings(newSettings);
|
||
|
|
||
|
const auto origSequence = m_sndFile.Order.GetCurrentSequenceIndex();
|
||
|
const auto origRepeatCount = m_sndFile.GetRepeatCount();
|
||
|
m_sndFile.SetRepeatCount(0);
|
||
|
|
||
|
auto opl = std::move(m_sndFile.m_opl);
|
||
|
|
||
|
const auto songIndexFmt = mpt::FormatSpec{}.Dec().FillNul().Width(1 + static_cast<int>(std::log10(m_subSongs.size())));
|
||
|
|
||
|
size_t totalSamples = 0;
|
||
|
for(size_t i = 0; i < m_subSongs.size() && !m_abort; i++)
|
||
|
{
|
||
|
const auto &song = m_subSongs[i];
|
||
|
|
||
|
m_sndFile.ResetPlayPos();
|
||
|
m_sndFile.GetLength(eAdjust, GetLengthTarget(song.startOrder, song.startRow).StartPos(song.sequence, 0, 0));
|
||
|
m_sndFile.m_SongFlags.reset(SONG_PLAY_FLAGS);
|
||
|
|
||
|
m_oplLogger.Reset();
|
||
|
m_sndFile.m_opl = std::make_unique<OPL>(m_oplLogger);
|
||
|
|
||
|
auto prevTime = timeGetTime();
|
||
|
CSoundFile::samplecount_t loopStart = std::numeric_limits<CSoundFile::samplecount_t>::max(), subsongSamples = 0;
|
||
|
while(!m_abort)
|
||
|
{
|
||
|
auto count = m_sndFile.ReadOneTick();
|
||
|
if(count == 0)
|
||
|
break;
|
||
|
|
||
|
if(loopStart == Util::MaxValueOfType(loopStart)
|
||
|
&& m_sndFile.m_PlayState.m_nCurrentOrder == song.loopStartOrder && m_sndFile.m_PlayState.m_nRow == song.loopStartRow
|
||
|
&& (song.loopStartOrder != song.startOrder || song.loopStartRow != song.startRow))
|
||
|
{
|
||
|
loopStart = subsongSamples;
|
||
|
m_oplLogger.CaptureAllVoiceRegisters(); // Make sure all registers are in the correct state when looping back
|
||
|
}
|
||
|
|
||
|
totalSamples += count;
|
||
|
subsongSamples += count;
|
||
|
|
||
|
auto currentTime = timeGetTime();
|
||
|
if(currentTime - prevTime >= 16)
|
||
|
{
|
||
|
prevTime = currentTime;
|
||
|
auto timeSec = subsongSamples / m_sndFile.GetSampleRate();
|
||
|
SetWindowText(MPT_TFORMAT("Exporting Song {} / {}... {}:{}:{}")(i + 1, m_subSongs.size(), timeSec / 3600, mpt::cfmt::dec0<2>((timeSec / 60) % 60), mpt::cfmt::dec0<2>(timeSec % 60)).c_str());
|
||
|
|
||
|
SetProgress(totalSamples);
|
||
|
ProcessMessages();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(m_sndFile.m_SongFlags[SONG_BREAKTOROW] && loopStart == Util::MaxValueOfType(loopStart) && song.loopStartOrder == song.startOrder && song.loopStartRow == song.startRow)
|
||
|
loopStart = 0;
|
||
|
|
||
|
mpt::PathString currentFileName = fileName;
|
||
|
if(m_subSongs.size() > 1)
|
||
|
currentFileName = fileName.ReplaceExt(mpt::PathString::FromNative(MPT_TFORMAT(" ({})")(mpt::ufmt::fmt(i + 1, songIndexFmt))) + fileName.GetFileExt());
|
||
|
mpt::SafeOutputFile sf(currentFileName, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave));
|
||
|
mpt::ofstream &f = sf;
|
||
|
try
|
||
|
{
|
||
|
if(!f)
|
||
|
throw std::exception{};
|
||
|
f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit);
|
||
|
if(s_format == ExportFormat::DRO)
|
||
|
m_oplLogger.WriteDRO(f);
|
||
|
else if(s_format == ExportFormat::VGM)
|
||
|
m_oplLogger.WriteVGM(f, loopStart, fileTags);
|
||
|
else
|
||
|
m_oplLogger.WriteVGZ(f, loopStart, fileTags, currentFileName.ReplaceExt(P_(".vgm")).GetFullFileName().ToUnicode());
|
||
|
} catch(const std::exception &)
|
||
|
{
|
||
|
Reporting::Error(MPT_UFORMAT("Unable to write to file {}!")(currentFileName));
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Reset globals to previous values
|
||
|
m_sndFile.m_opl = std::move(opl);
|
||
|
m_sndFile.SetRepeatCount(origRepeatCount);
|
||
|
m_sndFile.Order.SetSequence(origSequence);
|
||
|
m_sndFile.ResetPlayPos();
|
||
|
m_sndFile.SetMixerSettings(origSettings);
|
||
|
m_sndFile.m_bIsRendering = false;
|
||
|
}
|
||
|
|
||
|
DECLARE_MESSAGE_MAP()
|
||
|
};
|
||
|
|
||
|
OPLExportDlg::ExportFormat OPLExportDlg::s_format = OPLExportDlg::ExportFormat::VGZ;
|
||
|
|
||
|
|
||
|
BEGIN_MESSAGE_MAP(OPLExportDlg, CDialog)
|
||
|
//{{AFX_MSG_MAP(OPLExportDlg)
|
||
|
ON_COMMAND(IDC_RADIO1, &OPLExportDlg::OnFormatChanged)
|
||
|
ON_COMMAND(IDC_RADIO2, &OPLExportDlg::OnFormatChanged)
|
||
|
ON_COMMAND(IDC_RADIO3, &OPLExportDlg::OnFormatChanged)
|
||
|
ON_EN_CHANGE(IDC_EDIT1, &OPLExportDlg::OnSubsongChanged)
|
||
|
//}}AFX_MSG_MAP
|
||
|
END_MESSAGE_MAP()
|
||
|
|
||
|
|
||
|
void CModDoc::OnFileOPLExport()
|
||
|
{
|
||
|
bool anyOPL = false;
|
||
|
for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++)
|
||
|
{
|
||
|
if(m_SndFile.GetSample(smp).uFlags[CHN_ADLIB])
|
||
|
{
|
||
|
anyOPL = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if(!anyOPL)
|
||
|
{
|
||
|
Reporting::Information(_T("This module does not use any OPL instruments."), _T("No OPL Instruments Found"));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
OPLExportDlg dlg{*this, CMainFrame::GetMainFrame()};
|
||
|
dlg.DoModal();
|
||
|
}
|
||
|
|
||
|
OPENMPT_NAMESPACE_END
|