furnace/src/gui/exportOptions.cpp
tildearrow fe454ee2df VGM export: add speed drift compensation setting
it appears this is the only way to get exports to play at the correct
speed in DeadFish's VGM player for Genesis
2025-06-06 02:45:59 -05:00

560 lines
19 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Furnace Tracker - multi-system chiptune tracker
* Copyright (C) 2021-2025 tildearrow and contributors
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "gui.h"
#include "guiConst.h"
#include "../fileutils.h"
#include "misc/cpp/imgui_stdlib.h"
#include <imgui.h>
void FurnaceGUI::drawExportAudio(bool onWindow) {
exitDisabledTimer=1;
ImGui::Text(_("Export type:"));
ImGui::Indent();
if (ImGui::RadioButton(_("one file"),audioExportOptions.mode==DIV_EXPORT_MODE_ONE)) {
audioExportOptions.mode=DIV_EXPORT_MODE_ONE;
}
if (ImGui::RadioButton(_("multiple files (one per chip)"),audioExportOptions.mode==DIV_EXPORT_MODE_MANY_SYS)) {
audioExportOptions.mode=DIV_EXPORT_MODE_MANY_SYS;
}
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::Indent();
if (ImGui::RadioButton(_("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)) {
audioExportOptions.format=DIV_EXPORT_FORMAT_F32;
}
ImGui::Unindent();
}
if (ImGui::InputInt(_("Sample rate"),&audioExportOptions.sampleRate,100,10000)) {
if (audioExportOptions.sampleRate<8000) audioExportOptions.sampleRate=8000;
if (audioExportOptions.sampleRate>384000) audioExportOptions.sampleRate=384000;
}
if (audioExportOptions.mode!=DIV_EXPORT_MODE_MANY_SYS) {
if (ImGui::InputInt(_("Channels in file"),&audioExportOptions.chans,1,1)) {
if (audioExportOptions.chans<1) audioExportOptions.chans=1;
if (audioExportOptions.chans>16) audioExportOptions.chans=16;
}
}
if (ImGui::InputInt(_("Loops"),&audioExportOptions.loops,1,2)) {
if (audioExportOptions.loops<0) audioExportOptions.loops=0;
}
if (ImGui::InputDouble(_("Fade out (seconds)"),&audioExportOptions.fadeOut,1.0,2.0,"%.1f")) {
if (audioExportOptions.fadeOut<0.0) audioExportOptions.fadeOut=0.0;
}
bool isOneOn=false;
if (audioExportOptions.mode==DIV_EXPORT_MODE_MANY_CHAN) {
ImGui::Text(_("Channels to export:"));
ImGui::SameLine();
if (ImGui::SmallButton(_("All"))) {
for (int i=0; i<DIV_MAX_CHANS; i++) {
audioExportOptions.channelMask[i]=true;
}
}
ImGui::SameLine();
if (ImGui::SmallButton(_("None"))) {
for (int i=0; i<DIV_MAX_CHANS; i++) {
audioExportOptions.channelMask[i]=false;
}
}
ImGui::SameLine();
if (ImGui::SmallButton(_("Shown in pattern"))) {
for (int i=0; i<DIV_MAX_CHANS; i++) {
audioExportOptions.channelMask[i]=e->curSubSong->chanShow[i];
}
}
ImGui::SameLine();
if (ImGui::SmallButton(_("Shown in oscilloscope"))) {
for (int i=0; i<DIV_MAX_CHANS; i++) {
audioExportOptions.channelMask[i]=e->curSubSong->chanShowChanOsc[i];
}
}
ImGui::SameLine();
if (ImGui::SmallButton(_("Invert"))) {
for (int i=0; i<DIV_MAX_CHANS; i++) {
audioExportOptions.channelMask[i]=!audioExportOptions.channelMask[i];
}
}
if (ImGui::BeginChild("Channel Selection",ImVec2(0,200.0f*dpiScale))) {
for (int i=0; i<e->getTotalChannelCount(); i++) {
String name=fmt::sprintf("%d. %s##_CE%d",i+1,e->getChannelName(i),i);
ImGui::Checkbox(name.c_str(),&audioExportOptions.channelMask[i]);
if (audioExportOptions.channelMask[i]) isOneOn=true;
}
}
ImGui::EndChild();
} else {
isOneOn=true;
}
if (onWindow) {
ImGui::Separator();
if (ImGui::Button(_("Cancel"),ImVec2(200.0f*dpiScale,0))) ImGui::CloseCurrentPopup();
ImGui::SameLine();
}
if (isOneOn) {
if (ImGui::Button(_("Export"),ImVec2(200.0f*dpiScale,0))) {
switch (audioExportOptions.mode) {
case DIV_EXPORT_MODE_ONE:
openFileDialog(GUI_FILE_EXPORT_AUDIO_ONE);
break;
case DIV_EXPORT_MODE_MANY_SYS:
openFileDialog(GUI_FILE_EXPORT_AUDIO_PER_SYS);
break;
case DIV_EXPORT_MODE_MANY_CHAN:
openFileDialog(GUI_FILE_EXPORT_AUDIO_PER_CHANNEL);
break;
}
ImGui::CloseCurrentPopup();
}
} else {
ImGui::Text(_("select at least one channel"));
}
}
void FurnaceGUI::drawExportVGM(bool onWindow) {
exitDisabledTimer=1;
ImGui::Text(_("settings:"));
if (ImGui::BeginCombo(_("format version"),fmt::sprintf("%d.%.2x",vgmExportVersion>>8,vgmExportVersion&0xff).c_str())) {
for (int i=0; i<7; i++) {
if (ImGui::Selectable(fmt::sprintf("%d.%.2x",vgmVersions[i]>>8,vgmVersions[i]&0xff).c_str(),vgmExportVersion==vgmVersions[i])) {
vgmExportVersion=vgmVersions[i];
}
}
ImGui::EndCombo();
}
ImGui::Checkbox(_("loop"),&vgmExportLoop);
if (vgmExportLoop && e->song.loopModality==2) {
ImGui::Text(_("loop trail:"));
ImGui::Indent();
if (ImGui::RadioButton(_("auto-detect"),vgmExportTrailingTicks==-1)) {
vgmExportTrailingTicks=-1;
}
if (ImGui::RadioButton(_("add one loop"),vgmExportTrailingTicks==-2)) {
vgmExportTrailingTicks=-2;
}
if (ImGui::RadioButton(_("custom"),vgmExportTrailingTicks>=0)) {
vgmExportTrailingTicks=0;
}
if (vgmExportTrailingTicks>=0) {
ImGui::SameLine();
if (ImGui::InputInt("##TrailTicks",&vgmExportTrailingTicks,1,100)) {
if (vgmExportTrailingTicks<0) vgmExportTrailingTicks=0;
}
}
ImGui::Unindent();
}
ImGui::Checkbox(_("add pattern change hints"),&vgmExportPatternHints);
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(_(
"inserts data blocks on pattern changes.\n"
"useful if you are writing a playback routine.\n\n"
"the format of a pattern change data block is:\n"
"67 66 FE ll ll ll ll 01 oo rr pp pp pp ...\n"
"- ll: length, a 32-bit little-endian number\n"
"- oo: order\n"
"- rr: initial row (a 0Dxx effect is able to select a different row)\n"
"- pp: pattern index (one per channel)\n\n"
"pattern indexes are ordered as they appear in the song."
));
}
ImGui::Checkbox(_("direct stream mode"),&vgmExportDirectStream);
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(_(
"required for DualPCM and MSM6258 export.\n\n"
"allows for volume/direction changes when playing samples,\n"
"at the cost of a massive increase in file size."
));
}
ImGui::Text(_("chips to export:"));
bool hasOneAtLeast=false;
bool hasNES=false;
for (int i=0; i<e->song.systemLen; i++) {
int minVersion=e->minVGMVersion(e->song.system[i]);
if (e->song.system[i]==DIV_SYSTEM_NES) hasNES=true;
ImGui::BeginDisabled(minVersion>vgmExportVersion || minVersion==0);
ImGui::Checkbox(fmt::sprintf("%d. %s##_SYSV%d",i+1,getSystemName(e->song.system[i]),i).c_str(),&willExport[i]);
ImGui::EndDisabled();
if (minVersion>vgmExportVersion) {
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip(_("this chip is only available in VGM %d.%.2x and higher!"),minVersion>>8,minVersion&0xff);
}
} else if (minVersion==0) {
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip(_("this chip is not supported by the VGM format!"));
}
} else {
if (willExport[i]) hasOneAtLeast=true;
}
}
ImGui::Text(_("select the chip you wish to export, but only up to %d of each type."),(vgmExportVersion>=0x151)?2:1);
if (hasNES) {
ImGui::Text(_("NES DPCM bank switch method:"));
if (ImGui::RadioButton(_("data blocks"),!vgmExportDPCM07)) {
vgmExportDPCM07=false;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(_("67 66 C2 - writes a new data block on each bank switch.\nmay result in bigger files but is compatible with all players."));
}
if (ImGui::RadioButton(_("RAM write commands"),vgmExportDPCM07)) {
vgmExportDPCM07=true;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(_("67 66 07 - uses RAM write commands (68) to switch banks.\nnot all VGM players support this!"));
}
}
ImGui::Text(_("speed drift compensation:"));
if (ImGui::RadioButton(_("none"),vgmExportCorrectedRate==44100)) {
vgmExportCorrectedRate=44100;
}
// as tested on a Model 1 Genesis (VA6, USA):
// 0.97841613336995507871 slower, 1.02206000687632131440 longer
if (ImGui::RadioButton(_("DeadFish VgmPlay (1.02×)"),vgmExportCorrectedRate==43148)) {
vgmExportCorrectedRate=43148;
}
if (hasOneAtLeast) {
if (onWindow) {
ImGui::Separator();
if (ImGui::Button(_("Cancel"),ImVec2(200.0f*dpiScale,0))) ImGui::CloseCurrentPopup();
ImGui::SameLine();
}
if (ImGui::Button(_("Export"),ImVec2(200.0f*dpiScale,0))) {
openFileDialog(GUI_FILE_EXPORT_VGM);
ImGui::CloseCurrentPopup();
}
} else {
ImGui::Text(_("nothing to export"));
if (onWindow) {
ImGui::Separator();
if (ImGui::Button(_("Cancel"),ImVec2(400.0f*dpiScale,0))) ImGui::CloseCurrentPopup();
}
}
}
void FurnaceGUI::drawExportROM(bool onWindow) {
exitDisabledTimer=1;
const DivROMExportDef* def=e->getROMExportDef(romTarget);
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
if (ImGui::BeginCombo("##ROMTarget",def==NULL?"<select one>":def->name)) {
for (int i=0; i<DIV_ROM_MAX; i++) {
const DivROMExportDef* newDef=e->getROMExportDef((DivROMExportOptions)i);
if (newDef!=NULL) {
if (romExportAvail[i]) {
if (ImGui::Selectable(newDef->name)) {
romTarget=(DivROMExportOptions)i;
romMultiFile=newDef->multiOutput;
romConfig=DivConfig();
if (newDef->fileExt==NULL) {
romFilterName="";
romFilterExt="";
} else {
romFilterName=newDef->fileType;
romFilterExt=newDef->fileExt;
}
}
}
}
}
ImGui::EndCombo();
}
if (def!=NULL) {
ImGui::Text("by %s",def->author);
ImGui::TextWrapped("%s",def->description);
}
ImGui::Separator();
bool altered=false;
switch (romTarget) {
case DIV_ROM_TIUNA: {
String asmBaseLabel=romConfig.getString("baseLabel","twin");
int firstBankSize=romConfig.getInt("firstBankSize",1024);
int otherBankSize=romConfig.getInt("otherBankSize",4096-48);
int sysToExport=romConfig.getInt("sysToExport",-1);
// TODO; validate label
if (ImGui::InputText(_("base song label name"),&asmBaseLabel)) {
altered=true;
}
if (ImGui::InputInt(_("max size in first bank"),&firstBankSize,1,100)) {
if (firstBankSize<0) firstBankSize=0;
if (firstBankSize>4096) firstBankSize=4096;
altered=true;
}
if (ImGui::InputInt(_("max size in other banks"),&otherBankSize,1,100)) {
if (otherBankSize<16) otherBankSize=16;
if (otherBankSize>4096) otherBankSize=4096;
altered=true;
}
ImGui::Text(_("chip to export:"));
for (int i=0; i<e->song.systemLen; i++) {
DivSystem sys=e->song.system[i];
bool isTIA=(sys==DIV_SYSTEM_TIA);
ImGui::BeginDisabled(!isTIA);
if (ImGui::RadioButton(fmt::sprintf("%d. %s##_SYSV%d",i+1,getSystemName(e->song.system[i]),i).c_str(),sysToExport==i)) {
sysToExport=i;
altered=true;
}
ImGui::EndDisabled();
}
if (altered) {
romConfig.set("baseLabel",asmBaseLabel);
romConfig.set("firstBankSize",firstBankSize);
romConfig.set("otherBankSize",otherBankSize);
romConfig.set("sysToExport",sysToExport);
}
break;
}
case DIV_ROM_ZSM: {
int zsmExportTickRate=romConfig.getInt("zsmrate",60);
bool zsmExportLoop=romConfig.getBool("loop",true);
bool zsmExportOptimize=romConfig.getBool("optimize",true);
if (ImGui::InputInt(_("Tick Rate (Hz)"),&zsmExportTickRate,1,2)) {
if (zsmExportTickRate<1) zsmExportTickRate=1;
if (zsmExportTickRate>44100) zsmExportTickRate=44100;
altered=true;
}
if (ImGui::Checkbox(_("loop"),&zsmExportLoop)) {
altered=true;
}
if (ImGui::Checkbox(_("optimize size"),&zsmExportOptimize)) {
altered=true;
}
if (altered) {
romConfig.set("zsmrate",zsmExportTickRate);
romConfig.set("loop",zsmExportLoop);
romConfig.set("optimize",zsmExportOptimize);
}
break;
}
case DIV_ROM_ABSTRACT:
ImGui::TextWrapped("%s",_("select a target from the menu at the top of this dialog."));
break;
default:
ImGui::TextWrapped("%s",_("this export method doesn't offer any options."));
break;
}
/*
*/
if (onWindow) {
ImGui::Separator();
if (ImGui::Button(_("Cancel"),ImVec2(200.0f*dpiScale,0))) ImGui::CloseCurrentPopup();
ImGui::SameLine();
}
if (ImGui::Button(_("Export"),ImVec2(200.0f*dpiScale,0))) {
openFileDialog(GUI_FILE_EXPORT_ROM);
ImGui::CloseCurrentPopup();
}
}
void FurnaceGUI::drawExportText(bool onWindow) {
exitDisabledTimer=1;
ImGui::Text(
_("this option exports the song to a text file.\n")
);
if (onWindow) {
ImGui::Separator();
if (ImGui::Button(_("Cancel"),ImVec2(200.0f*dpiScale,0))) ImGui::CloseCurrentPopup();
ImGui::SameLine();
}
if (ImGui::Button(_("Export"),ImVec2(200.0f*dpiScale,0))) {
openFileDialog(GUI_FILE_EXPORT_TEXT);
ImGui::CloseCurrentPopup();
}
}
void FurnaceGUI::commandExportOptions() {
ImGui::Checkbox(_("Long pointers (use for 64K+ size streams)"),&csExportOptions.longPointers);
ImGui::Checkbox(_("Big endian mode"),&csExportOptions.bigEndian);
ImGui::Separator();
ImGui::Checkbox(_("Don't optimize command calls"),&csExportOptions.noCmdCallOpt);
ImGui::Checkbox(_("Don't condense delays"),&csExportOptions.noDelayCondense);
ImGui::Checkbox(_("Don't perform sub-block search"),&csExportOptions.noSubBlock);
}
void FurnaceGUI::drawExportCommand(bool onWindow) {
exitDisabledTimer=1;
ImGui::Text(_(
"this option exports a binary file which\n"
"contains a dump of the internal command stream\n"
"produced when playing the song.\n\n"
"technical/development use only!"
));
commandExportOptions();
if (onWindow) {
ImGui::Separator();
if (ImGui::Button(_("Cancel"),ImVec2(200.0f*dpiScale,0))) ImGui::CloseCurrentPopup();
ImGui::SameLine();
}
if (ImGui::Button(_("Export"),ImVec2(200.0f*dpiScale,0))) {
openFileDialog(GUI_FILE_EXPORT_CMDSTREAM);
ImGui::CloseCurrentPopup();
}
}
void FurnaceGUI::drawExportDMF(bool onWindow) {
exitDisabledTimer=1;
ImGui::Text(_(
"export in DefleMask module format.\n"
"only do it if you really, really need to, or are downgrading an existing .dmf."
));
ImGui::Text(_("format version:"));
ImGui::RadioButton(_("1.1.3 and higher"),&dmfExportVersion,0);
ImGui::RadioButton(_("1.0/legacy (0.12)"),&dmfExportVersion,1);
if (onWindow) {
ImGui::Separator();
if (ImGui::Button(_("Cancel"),ImVec2(200.0f*dpiScale,0))) ImGui::CloseCurrentPopup();
ImGui::SameLine();
}
if (ImGui::Button(_("Export"),ImVec2(200.0f*dpiScale,0))) {
if (dmfExportVersion==1) {
openFileDialog(GUI_FILE_SAVE_DMF_LEGACY);
} else {
openFileDialog(GUI_FILE_SAVE_DMF);
}
ImGui::CloseCurrentPopup();
}
}
void FurnaceGUI::drawExport() {
if (settings.exportOptionsLayout==1 || curExportType==GUI_EXPORT_NONE) {
if (ImGui::BeginTabBar("ExportTypes")) {
if (ImGui::BeginTabItem(_("Audio"))) {
drawExportAudio(true);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem(_("VGM"))) {
drawExportVGM(true);
ImGui::EndTabItem();
}
if (romExportExists) {
if (ImGui::BeginTabItem(_("ROM"))) {
drawExportROM(true);
ImGui::EndTabItem();
}
}
if (ImGui::BeginTabItem(_("Text"))) {
drawExportText(true);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem(_("Command Stream"))) {
drawExportCommand(true);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem(_("DMF"))) {
drawExportDMF(true);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
} else switch (curExportType) {
case GUI_EXPORT_AUDIO:
drawExportAudio(true);
break;
case GUI_EXPORT_VGM:
drawExportVGM(true);
break;
case GUI_EXPORT_ROM:
drawExportROM(true);
break;
case GUI_EXPORT_TEXT:
drawExportText(true);
break;
case GUI_EXPORT_CMD_STREAM:
drawExportCommand(true);
break;
case GUI_EXPORT_DMF:
drawExportDMF(true);
break;
default:
ImGui::Text(_("congratulations! you've unlocked a secret panel."));
if (ImGui::Button(_("Toggle hidden systems"))) {
settings.hiddenSystems=!settings.hiddenSystems;
ImGui::CloseCurrentPopup();
}
if (ImGui::Button(_("Toggle all instrument types"))) {
settings.displayAllInsTypes=!settings.displayAllInsTypes;
ImGui::CloseCurrentPopup();
}
if (ImGui::Button(_("Set pitch linearity to Partial"))) {
e->song.linearPitch=1;
ImGui::CloseCurrentPopup();
}
if (ImGui::Button(_("Set fat to max"))) {
ImGuiStyle& sty=ImGui::GetStyle();
sty.FramePadding=ImVec2(20.0f*dpiScale,20.0f*dpiScale);
sty.ItemSpacing=ImVec2(10.0f*dpiScale,10.0f*dpiScale);
sty.ItemInnerSpacing=ImVec2(10.0f*dpiScale,10.0f*dpiScale);
ImGui::CloseCurrentPopup();
}
if (ImGui::Button(_("Set muscle and fat to zero"))) {
ImGuiStyle& sty=ImGui::GetStyle();
sty.FramePadding=ImVec2(0,0);
sty.ItemSpacing=ImVec2(0,0);
sty.ItemInnerSpacing=ImVec2(0,0);
ImGui::CloseCurrentPopup();
}
if (ImGui::Button(_("Tell tildearrow this must be a mistake"))) {
showError(_("yeah, it's a bug. write a bug report in the GitHub page and tell me how did you get here."));
ImGui::CloseCurrentPopup();
}
break;
}
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
ImGui::CloseCurrentPopup();
}
}