/*
 * mod2midi.cpp
 * ------------
 * Purpose: Module to MIDI conversion (dialog + conversion code).
 * Notes  : This code makes use of the existing MIDI plugin output functionality.
 * Authors: OpenMPT Devs
 * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
 */


#include "stdafx.h"
#include "Mptrack.h"
#include "Mainfrm.h"
#include "Moddoc.h"
#include "../common/mptStringBuffer.h"
#include "../common/mptFileIO.h"
#include "mod2midi.h"
#include "../soundlib/plugins/PlugInterface.h"
#include "../soundlib/plugins/PluginManager.h"
#include <sstream>
#include "mpt/io/io.hpp"
#include "mpt/io/io_stdstream.hpp"

#ifndef NO_PLUGINS

OPENMPT_NAMESPACE_BEGIN

namespace MidiExport
{
	// MIDI file resolution
	constexpr int32 ppq = 480;

	enum StringType : uint8
	{
		kText = 1,
		kCopyright = 2,
		kTrackName = 3,
		kInstrument = 4,
		kLyric = 5,
		kMarker = 6,
		kCue = 7,
	};

	class MidiTrack final : public IMidiPlugin
	{
		ModInstrument m_instr;
		const ModInstrument *const m_oldInstr;
		const CSoundFile &m_sndFile;
		const GetLengthType &m_songLength;
		MidiTrack *const m_tempoTrack;  // Pointer to tempo track, nullptr if this is the tempo track
		decltype(m_MidiCh) *m_lastMidiCh = nullptr;
		std::array<decltype(m_instr.midiPWD), 16> m_pitchWheelDepth = { 0 };

		std::vector<std::array<char, 4>> m_queuedEvents;

		std::ostringstream f;
		double m_tempo = 0.0;
		double m_ticks = 0.0;                           // MIDI ticks since previous event
		CSoundFile::samplecount_t m_samplePos = 0;      // Current sample position
		CSoundFile::samplecount_t m_prevEventTime = 0;  // Sample position of previous event
		uint32 m_sampleRate;
		uint32 m_oldSigNumerator = 0;
		int32 m_oldGlobalVol = -1;
		const bool m_overlappingInstruments;
		bool m_wroteLoopStart = false;

		// Calculate how many MIDI ticks have passed since the last written event
		void UpdateTicksSinceLastEvent()
		{
			m_ticks += (m_samplePos - m_prevEventTime) * m_tempo * static_cast<double>(MidiExport::ppq) / (m_sampleRate * 60);
			m_prevEventTime = m_samplePos;
		}
		
		// Write delta tick count since last event
		void WriteTicks()
		{
			uint32 ticks = (m_ticks <= 0) ? 0 : mpt::saturate_round<uint32>(m_ticks);
			mpt::IO::WriteVarInt(f, ticks);
			m_ticks -= ticks;
		}

		// Update MIDI channel states in non-overlapping export mode so that all plugins have the same view
		void SynchronizeMidiChannelState()
		{
			if(m_tempoTrack != nullptr && !m_overlappingInstruments)
			{
				if(m_tempoTrack->m_lastMidiCh != nullptr && m_tempoTrack->m_lastMidiCh != &m_MidiCh)
					m_MidiCh = *m_tempoTrack->m_lastMidiCh;
				m_tempoTrack->m_lastMidiCh = &m_MidiCh;
			}
		}

		void SynchronizeMidiPitchWheelDepth(CHANNELINDEX trackerChn)
		{
			if(trackerChn >= std::size(m_sndFile.m_PlayState.Chn))
				return;
			const auto midiCh = GetMidiChannel(m_sndFile.m_PlayState.Chn[trackerChn], trackerChn);
			if(!m_overlappingInstruments && m_tempoTrack && m_tempoTrack->m_pitchWheelDepth[midiCh] != m_instr.midiPWD)
				WritePitchWheelDepth(static_cast<MidiChannel>(midiCh + MidiFirstChannel));
		}

	public:

		operator ModInstrument& () { return m_instr; }

		MidiTrack(VSTPluginLib &factory, CSoundFile &sndFile, const GetLengthType &songLength, SNDMIXPLUGIN *mixStruct, MidiTrack *tempoTrack, const mpt::ustring &name, const ModInstrument *oldInstr, bool overlappingInstruments)
			: IMidiPlugin(factory, sndFile, mixStruct)
			, m_oldInstr(oldInstr)
			, m_sndFile(sndFile)
			, m_songLength(songLength)
			, m_tempoTrack(tempoTrack)
			, m_sampleRate(sndFile.GetSampleRate())
			, m_overlappingInstruments(overlappingInstruments)
		{
			// Write instrument / song name
			WriteString(kTrackName, name);
			m_pMixStruct->pMixPlugin = this;
		}

		void WritePitchWheelDepth(MidiChannel midiChOverride = MidiNoChannel)
		{
			// Set up MIDI pitch wheel depth
			uint8 firstCh = 0, lastCh = 15;
			if(midiChOverride != MidiNoChannel)
				firstCh = lastCh = midiChOverride - MidiFirstChannel;
			else if(m_instr.nMidiChannel != MidiMappedChannel && m_instr.nMidiChannel != MidiNoChannel)
				firstCh = lastCh = m_instr.nMidiChannel - MidiFirstChannel;
			
			for(uint8 i = firstCh; i <= lastCh; i++)
			{
				const uint8 ch = 0xB0 | i;
				const uint8 msg[] = { ch, 0x64, 0x00, 0x00, ch, 0x65, 0x00, 0x00, ch, 0x06, static_cast<uint8>(std::abs(m_instr.midiPWD)) };
				WriteTicks();
				mpt::IO::WriteRaw(f, msg, sizeof(msg));
				if(m_tempoTrack)
					m_tempoTrack->m_pitchWheelDepth[i] = m_instr.midiPWD;
			}
		}

		void UpdateGlobals()
		{
			m_samplePos = m_sndFile.GetTotalSampleCount();
			m_sampleRate = m_sndFile.GetSampleRate();

			const double curTempo = m_sndFile.GetCurrentBPM();
			const ROWINDEX rpb = std::max(m_sndFile.m_PlayState.m_nCurrentRowsPerBeat, ROWINDEX(1));
			const uint32 timeSigNumerator = std::max(m_sndFile.m_PlayState.m_nCurrentRowsPerMeasure, rpb) / rpb;

			const bool tempoChanged = curTempo != m_tempo;
			const bool sigChanged = timeSigNumerator != m_oldSigNumerator;
			const bool volChanged = m_sndFile.m_PlayState.m_nGlobalVolume != m_oldGlobalVol;

			if(curTempo > 0.0)
				m_tempo = curTempo;
			m_oldSigNumerator = timeSigNumerator;
			m_oldGlobalVol = m_sndFile.m_PlayState.m_nGlobalVolume;

			if(m_tempoTrack != nullptr)
				return;

			// This is the tempo track
			if(tempoChanged && curTempo > 0.0)
			{
				// Write MIDI tempo
				WriteTicks();

				uint32 mspq = mpt::saturate_round<uint32>(60000000.0 / curTempo);
				uint8 msg[6] = { 0xFF, 0x51, 0x03, static_cast<uint8>(mspq >> 16), static_cast<uint8>(mspq >> 8), static_cast<uint8>(mspq) };
				mpt::IO::WriteRaw(f, msg, 6);
			}

			if(sigChanged)
			{
				// Write MIDI time signature
				WriteTicks();

				uint8 msg[7] = { 0xFF, 0x58, 0x04, static_cast<uint8>(timeSigNumerator), 2, 24, 8 };
				mpt::IO::WriteRaw(f, msg, 7);
			}

			if(volChanged)
			{
				// Write MIDI master volume
				WriteTicks();

				int32 midiVol = Util::muldiv(m_oldGlobalVol, 0x3FFF, MAX_GLOBAL_VOLUME);
				uint8 msg[9] = { 0xF0, 0x07, 0x7F, 0x7F, 0x04, 0x01, static_cast<uint8>(midiVol & 0x7F), static_cast<uint8>((midiVol >> 7) & 0x7F), 0xF7 };
				mpt::IO::WriteRaw(f, msg, 9);
			}

			if(!m_tempoTrack && !m_wroteLoopStart && m_sndFile.m_PlayState.m_nRow == m_songLength.lastRow && m_sndFile.m_PlayState.m_nCurrentOrder == m_songLength.lastOrder)
			{
				WriteString(kCue, U_("loopStart"));
				m_wroteLoopStart = true;
			}
		}

		void Process(float *, float *, uint32 numFrames) override
		{
			UpdateGlobals();
			if(m_tempoTrack != nullptr)
				m_tempoTrack->UpdateGlobals();

			for(const auto &midiData : m_queuedEvents)
			{
				WriteTicks();
				mpt::IO::WriteRaw(f, midiData.data(), MIDIEvents::GetEventLength(midiData[0]));
			}
			m_queuedEvents.clear();

			m_samplePos += numFrames;
			if (m_tempoTrack != nullptr)
			{
				m_tempoTrack->m_samplePos = std::max(m_tempoTrack->m_samplePos, m_samplePos);
				m_tempoTrack->UpdateTicksSinceLastEvent();
			}
			UpdateTicksSinceLastEvent();
		}

		// Write end marker and return the stream
		const std::ostringstream& Finalise()
		{
			HardAllNotesOff();
			UpdateTicksSinceLastEvent();

			if(!m_tempoTrack)
				WriteString(kCue, U_("loopEnd"));

			WriteTicks();
			uint8 msg[3] = { 0xFF, 0x2F, 0x00 };
			mpt::IO::WriteRaw(f, msg, 3);

			return f;
		}

		void WriteString(StringType strType, const mpt::ustring &ustr)
		{
			std::string str = mpt::ToCharset(mpt::Charset::Locale, ustr);
			if(!str.empty())
			{
				WriteTicks();
				uint8 msg[2] = { 0xFF, strType };
				mpt::IO::WriteRaw(f, msg, 2);
				mpt::IO::WriteVarInt(f, str.length());
				mpt::IO::WriteRaw(f, str.data(), str.length());
			}
		}

		void Release() override { }
		int32 GetUID() const override { return 0; }
		int32 GetVersion() const override { return 0; }
		void Idle() override { }
		uint32 GetLatency() const override { return 0; }

		int32 GetNumPrograms() const override { return 0; }
		int32 GetCurrentProgram() override { return 0; }
		void SetCurrentProgram(int32) override { }

		PlugParamIndex GetNumParameters() const override { return 0; }
		PlugParamValue GetParameter(PlugParamIndex) override { return 0; }
		void SetParameter(PlugParamIndex, PlugParamValue) override { }

		float RenderSilence(uint32) override { return 0.0f; }

		bool MidiSend(uint32 midiCode) override
		{
			std::array<char, 4> midiData;
			memcpy(midiData.data(), &midiCode, 4);

			// Note-On events go last to prevent early note-off in a situation like this:
			// ... ..|C-5 01
			// C-5 01|=== ..
			if(MIDIEvents::GetTypeFromEvent(midiCode) == MIDIEvents::evNoteOn)
			{
				m_queuedEvents.push_back(midiData);
				return true;
			}
			WriteTicks();
			mpt::IO::WriteRaw(f, midiData.data(), MIDIEvents::GetEventLength(midiData[0]));
			return true;
		}

		bool MidiSysexSend(mpt::const_byte_span sysex) override
		{
			if(sysex.size() > 1)
			{
				WriteTicks();
				mpt::IO::WriteIntBE<uint8>(f, 0xF0);
				mpt::IO::WriteVarInt(f, mpt::saturate_cast<uint32>(sysex.size() - 1));
				mpt::IO::WriteRaw(f, sysex.data() + 1, sysex.size() - 1);
			}
			return true;
		}

		uint8 GetMidiChannel(const ModChannel &chn, CHANNELINDEX trackChannel) const override
		{
			if(m_instr.nMidiChannel == MidiMappedChannel && trackChannel < std::size(m_sndFile.m_PlayState.Chn))
			{
				// For mapped channels, distribute tracker channels evenly over MIDI channels, but avoid channel 10 (drums)
				uint8 midiCh = trackChannel % 15u;
				if(midiCh >= 9)
					midiCh++;
				return midiCh;
			}
			return IMidiPlugin::GetMidiChannel(chn, trackChannel);
		}

		void MidiCommand(const ModInstrument &instr, uint16 note, uint16 vol, CHANNELINDEX trackChannel) override
		{
			if(note == NOTE_NOTECUT && (m_oldInstr == nullptr || !(m_oldInstr->nMixPlug != 0 && m_oldInstr->HasValidMIDIChannel())))
			{
				// The default implementation does things with Note Cut that we don't want here: it cuts all notes.
				note = NOTE_KEYOFF;
			}
			SynchronizeMidiChannelState();
			IMidiPlugin::MidiCommand(instr, note, vol, trackChannel);
		}

		void MidiPitchBendRaw(int32 pitchbend, CHANNELINDEX trackerChn) override
		{
			SynchronizeMidiChannelState();
			SynchronizeMidiPitchWheelDepth(trackerChn);
			IMidiPlugin::MidiPitchBendRaw(pitchbend, trackerChn);
		}

		void MidiPitchBend(int32 increment, int8 pwd, CHANNELINDEX trackerChn) override
		{
			SynchronizeMidiChannelState();
			SynchronizeMidiPitchWheelDepth(trackerChn);
			IMidiPlugin::MidiPitchBend(increment, pwd, trackerChn);
		}

		void MidiVibrato(int32 depth, int8 pwd, CHANNELINDEX trackerChn) override
		{
			SynchronizeMidiChannelState();
			SynchronizeMidiPitchWheelDepth(trackerChn);
			IMidiPlugin::MidiVibrato(depth, pwd, trackerChn);
		}

		bool IsNotePlaying(uint8 note, CHANNELINDEX trackerChn) override
		{
			SynchronizeMidiChannelState();
			return IMidiPlugin::IsNotePlaying(note, trackerChn);
		}

		void HardAllNotesOff() override
		{
			for(uint8 mc = 0; mc < m_MidiCh.size(); mc++)
			{
				PlugInstrChannel &channel = m_MidiCh[mc];
				for(size_t i = 0; i < std::size(channel.noteOnMap); i++)
				{
					for(auto &c : channel.noteOnMap[i])
					{
						while(c != 0)
						{
							MidiSend(MIDIEvents::NoteOff(mc, static_cast<uint8>(i), 0));
							c--;
						}
					}
				}
			}
		}

		void Resume() override { }
		void Suspend() override { }
		void PositionChanged() override { }

		bool IsInstrument() const override { return true; }
		bool CanRecieveMidiEvents() override { return true; }
		bool ShouldProcessSilence() override { return true; }
#ifdef MODPLUG_TRACKER
		CString GetDefaultEffectName() override { return {}; }
		CString GetParamName(PlugParamIndex) override { return {}; }
		CString GetParamLabel(PlugParamIndex) override { return {}; }
		CString GetParamDisplay(PlugParamIndex) override { return {}; }
		CString GetCurrentProgramName() override { return {}; }
		void SetCurrentProgramName(const CString &) override { }
		CString GetProgramName(int32) override { return {}; }
		bool HasEditor() const override { return false; }
#endif // MODPLUG_TRACKER
		int GetNumInputChannels() const override { return 0; }
		int GetNumOutputChannels() const override { return 0; }
	};


	class Conversion
	{
		std::vector<ModInstrument *> m_oldInstruments;
		std::vector<MidiTrack *> m_tracks;
		std::vector<SNDMIXPLUGIN> m_oldPlugins;
		SNDMIXPLUGIN tempoTrackPlugin;
		VSTPluginLib m_plugFactory;
		CSoundFile &m_sndFile;
		mpt::ofstream &m_file;
		const GetLengthType m_songLength;
		const bool m_wasInstrumentMode;

	public:
		Conversion(CSoundFile &sndFile, const InstrMap &instrMap, mpt::ofstream &file, bool overlappingInstruments, const GetLengthType &songLength)
			: m_oldInstruments(sndFile.GetNumInstruments())
			, m_plugFactory(nullptr, true, {}, {}, {})
			, m_sndFile(sndFile)
			, m_file(file)
			, m_songLength(songLength)
			, m_wasInstrumentMode(sndFile.GetNumInstruments() > 0)
		{
			m_oldPlugins.assign(std::begin(m_sndFile.m_MixPlugins), std::end(m_sndFile.m_MixPlugins));
			std::fill(std::begin(m_sndFile.m_MixPlugins), std::end(m_sndFile.m_MixPlugins), SNDMIXPLUGIN());
			for(INSTRUMENTINDEX i = 1; i <= m_sndFile.GetNumInstruments(); i++)
			{
				m_oldInstruments[i - 1] = m_sndFile.Instruments[i];
			}
			if(!m_wasInstrumentMode)
			{
				m_sndFile.m_nInstruments = std::min<INSTRUMENTINDEX>(m_sndFile.m_nSamples, MAX_INSTRUMENTS - 1u);
			}

			m_tracks.reserve(m_sndFile.GetNumInstruments() + 1);
			MidiTrack &tempoTrack = *(new MidiTrack(m_plugFactory, m_sndFile, m_songLength, &tempoTrackPlugin, nullptr, mpt::ToUnicode(m_sndFile.GetCharsetInternal(), m_sndFile.m_songName), nullptr, overlappingInstruments));
			tempoTrack.WriteString(kText, mpt::ToUnicode(m_sndFile.GetCharsetInternal(), m_sndFile.m_songMessage));
			tempoTrack.WriteString(kCopyright, m_sndFile.m_songArtist);
			m_tracks.push_back(&tempoTrack);

			PLUGINDEX nextPlug = 0;
			for(INSTRUMENTINDEX i = 1; i <= m_sndFile.GetNumInstruments(); i++)
			{
				m_sndFile.Instruments[i] = nullptr;
				if(!m_sndFile.GetpModDoc()->IsInstrumentUsed(i) || (m_wasInstrumentMode && m_oldInstruments[i - 1] == nullptr) || nextPlug >= MAX_MIXPLUGINS)
					continue;

				// FIXME: Having > MAX_MIXPLUGINS used instruments won't work! So in MPTM, you can only use 250 out of 255 instruments...
				SNDMIXPLUGIN &mixPlugin = m_sndFile.m_MixPlugins[nextPlug++];

				ModInstrument *oldInstr = m_wasInstrumentMode ? m_oldInstruments[i - 1] : nullptr;
				MidiTrack &midiInstr = *(new MidiTrack(m_plugFactory, m_sndFile, m_songLength, &mixPlugin, &tempoTrack, m_wasInstrumentMode ? mpt::ToUnicode(m_sndFile.GetCharsetInternal(), oldInstr->name) : mpt::ToUnicode(m_sndFile.GetCharsetInternal(), m_sndFile.GetSampleName(i)), oldInstr, overlappingInstruments));
				ModInstrument &instr = midiInstr;
				mixPlugin.pMixPlugin = &midiInstr;
				
				m_sndFile.Instruments[i] = &instr;
				m_tracks.push_back(&midiInstr);

				if(m_wasInstrumentMode) instr = *oldInstr;
				instr.nMixPlug = nextPlug;
				if((oldInstr != nullptr && oldInstr->nMixPlug == 0) || instr.nMidiChannel == MidiNoChannel)
				{
					instr.midiPWD = 12;
				}

				instr.nMidiChannel = instrMap[i].channel;
				if(instrMap[i].channel != MidiFirstChannel + 9)
				{
					// Melodic instrument
					instr.nMidiProgram = instrMap[i].program;
				} else
				{
					// Drums
					if(oldInstr != nullptr && oldInstr->nMidiChannel != MidiFirstChannel + 9)
						instr.nMidiProgram = 0;
					if(instrMap[i].program > 0)
					{
						for(auto &key : instr.NoteMap)
						{
							key = instrMap[i].program + NOTE_MIN - 1;
						}
					}
				}
				midiInstr.WritePitchWheelDepth();
			}

			mpt::IO::WriteRaw(m_file, "MThd", 4);
			mpt::IO::WriteIntBE<uint32>(m_file, 6);
			mpt::IO::WriteIntBE<uint16>(m_file, 1);	// Type 1 MIDI - multiple simultaneous tracks
			mpt::IO::WriteIntBE<uint16>(m_file, static_cast<uint16>(m_tracks.size()));	// Number of tracks
			mpt::IO::WriteIntBE<uint16>(m_file, MidiExport::ppq);
		}

		void Finalise()
		{
			for(auto track : m_tracks)
			{
				std::string data = track->Finalise().str();
				if(!data.empty())
				{
					const uint32 len = mpt::saturate_cast<uint32>(data.size());
					mpt::IO::WriteRaw(m_file, "MTrk", 4);
					mpt::IO::WriteIntBE<uint32>(m_file, len);
					mpt::IO::WriteRaw(m_file, data.data(), len);
				}
			}
		}

		~Conversion()
		{
			for(INSTRUMENTINDEX i = 1; i <= m_sndFile.GetNumInstruments(); i++)
			{
				m_sndFile.Instruments[i] = m_wasInstrumentMode ? m_oldInstruments[i - 1] : nullptr;
			}

			if(!m_wasInstrumentMode)
			{
				m_sndFile.m_nInstruments = 0;
			}

			for(auto &plug : m_sndFile.m_MixPlugins)
			{
				plug.Destroy();
			}
			for(auto &track : m_tracks)
			{
				delete track;	// Resets m_MixPlugins[i].pMixPlugin, so do it before copying back the old structs
			}
			std::move(m_oldPlugins.cbegin(), m_oldPlugins.cend(), std::begin(m_sndFile.m_MixPlugins));

			// Be sure that instrument pointers to our faked instruments are gone.
			const auto muteFlag = CSoundFile::GetChannelMuteFlag();
			for(CHANNELINDEX i = 0; i < MAX_CHANNELS; i++)
			{
				m_sndFile.m_PlayState.Chn[i].Reset(ModChannel::resetTotal, m_sndFile, i, muteFlag);
			}
		}
	};


	class DummyAudioTarget : public IAudioTarget
	{
	public:
		void Process(mpt::audio_span_interleaved<MixSampleInt>) override { }
		void Process(mpt::audio_span_interleaved<MixSampleFloat>) override { }
	};
}


////////////////////////////////////////////////////////////////////////////////////
//
// CModToMidi dialog implementation
//

bool CModToMidi::s_overlappingInstruments = false;

BEGIN_MESSAGE_MAP(CModToMidi, CDialog)
	ON_CBN_SELCHANGE(IDC_COMBO1,	&CModToMidi::UpdateDialog)
	ON_CBN_SELCHANGE(IDC_COMBO2,	&CModToMidi::OnChannelChanged)
	ON_CBN_SELCHANGE(IDC_COMBO3,	&CModToMidi::OnProgramChanged)
	ON_COMMAND(IDC_CHECK1,			&CModToMidi::OnOverlapChanged)
	ON_WM_VSCROLL()
END_MESSAGE_MAP()


void CModToMidi::DoDataExchange(CDataExchange *pDX)
{
	CDialog::DoDataExchange(pDX);
	DDX_Control(pDX, IDC_COMBO1,	m_CbnInstrument);
	DDX_Control(pDX, IDC_COMBO2,	m_CbnChannel);
	DDX_Control(pDX, IDC_COMBO3,	m_CbnProgram);
	DDX_Control(pDX, IDC_SPIN1,		m_SpinInstrument);
}


CModToMidi::CModToMidi(CSoundFile &sndFile, CWnd *pWndParent)
	: CDialog(IDD_MOD2MIDI, pWndParent)
	, m_sndFile(sndFile)
	, m_instrMap((sndFile.GetNumInstruments() ? sndFile.GetNumInstruments() : sndFile.GetNumSamples()) + 1)
{
	for (INSTRUMENTINDEX i = 1; i <= m_sndFile.GetNumInstruments(); i++)
	{
		ModInstrument *pIns = m_sndFile.Instruments[i];
		if(pIns != nullptr)
		{
			m_instrMap[i].channel = pIns->nMidiChannel;
			if(m_instrMap[i].channel != MidiFirstChannel + 9)
			{
				if(!pIns->HasValidMIDIChannel())
					m_instrMap[i].channel = MidiMappedChannel;
				m_instrMap[i].program = pIns->nMidiProgram;
			}
		}
	}
}


BOOL CModToMidi::OnInitDialog()
{
	CString s;

	CDialog::OnInitDialog();

	// Fill instruments box
	m_SpinInstrument.SetRange(-1, 1);
	m_SpinInstrument.SetPos(0);
	m_currentInstr = 1;
	m_CbnInstrument.SetRedraw(FALSE);
	if(m_sndFile.GetNumInstruments())
	{
		for(INSTRUMENTINDEX nIns = 1; nIns <= m_sndFile.GetNumInstruments(); nIns++)
		{
			ModInstrument *pIns = m_sndFile.Instruments[nIns];
			if(pIns && m_sndFile.GetpModDoc()->IsInstrumentUsed(nIns, false))
			{
				const CString name = m_sndFile.GetpModDoc()->GetPatternViewInstrumentName(nIns);
				m_CbnInstrument.SetItemData(m_CbnInstrument.AddString(name), nIns);
			}
		}
	} else
	{
		for(SAMPLEINDEX nSmp = 1; nSmp <= m_sndFile.GetNumSamples(); nSmp++)
		{
			if(m_sndFile.GetpModDoc()->IsSampleUsed(nSmp, false))
			{
				s.Format(_T("%02d: "), nSmp);
				s += mpt::ToCString(m_sndFile.GetCharsetInternal(), m_sndFile.m_szNames[nSmp]);
				m_CbnInstrument.SetItemData(m_CbnInstrument.AddString(s), nSmp);
			}
		}
	}
	m_CbnInstrument.SetRedraw(TRUE);
	m_CbnInstrument.SetCurSel(0);

	// Fill channels box
	m_CbnChannel.SetRedraw(FALSE);
	m_CbnChannel.SetItemData(m_CbnChannel.AddString(_T("Don't Export")), MidiNoChannel);
	m_CbnChannel.SetItemData(m_CbnChannel.AddString(_T("Melodic (any)")), MidiMappedChannel);
	m_CbnChannel.SetItemData(m_CbnChannel.AddString(_T("Percussions")), MidiFirstChannel + 9);
	for(uint32 chn = 1; chn <= 16; chn++)
	{
		if(chn == 10)
			continue;
		s.Format(_T("Melodic %u"), chn);
		m_CbnChannel.SetItemData(m_CbnChannel.AddString(s), MidiFirstChannel - 1 + chn);
	}
	m_CbnChannel.SetRedraw(TRUE);
	m_CbnChannel.SetCurSel(1);

	m_currentInstr = 1;
	m_percussion = true;
	FillProgramBox(false);
	m_CbnProgram.SetCurSel(0);
	UpdateDialog();
	CheckDlgButton(IDC_CHECK1, s_overlappingInstruments ? BST_CHECKED : BST_UNCHECKED);
	return TRUE;
}


void CModToMidi::FillProgramBox(bool percussion)
{
	if(m_percussion == percussion)
		return;
	m_CbnProgram.SetRedraw(FALSE);
	m_CbnProgram.ResetContent();
	if(percussion)
	{
		m_CbnProgram.SetItemData(m_CbnProgram.AddString(_T("Mapped")), 0);
		for(ModCommand::NOTE i = 0; i < 61; i++)
		{
			ModCommand::NOTE note = i + 24;
			auto s = MPT_CFORMAT("{} ({}): {}")(
				note,
				mpt::ToCString(m_sndFile.GetNoteName(note + NOTE_MIN)),
				mpt::ToCString(mpt::Charset::ASCII, szMidiPercussionNames[i]));
			m_CbnProgram.SetItemData(m_CbnProgram.AddString(s), note);
		}
	} else
	{
		m_CbnProgram.SetItemData(m_CbnProgram.AddString(_T("No Program Change")), 0);
		for(int i = 1; i <= 128; i++)
		{
			auto s = MPT_CFORMAT("{}: {}")(
				mpt::cfmt::dec0<3>(i),
				mpt::ToCString(mpt::Charset::ASCII, szMidiProgramNames[i - 1]));
			m_CbnProgram.SetItemData(m_CbnProgram.AddString(s), i);
		}
	}
	m_CbnProgram.SetRedraw(TRUE);
	m_CbnProgram.Invalidate(FALSE);
	m_percussion = percussion;
}


void CModToMidi::UpdateDialog()
{
	m_currentInstr = static_cast<UINT>(m_CbnInstrument.GetItemData(m_CbnInstrument.GetCurSel()));
	const bool validInstr = (m_currentInstr > 0 && m_currentInstr < m_instrMap.size());

	m_CbnProgram.EnableWindow(validInstr && m_instrMap[m_currentInstr].channel != MidiNoChannel);

	if(!validInstr)
		return;

	uint8 nMidiCh = m_instrMap[m_currentInstr].channel;
	int sel;
	switch(nMidiCh)
	{
	case MidiNoChannel:
		sel = 0;
		break;
	case MidiMappedChannel:
		sel = 1;
		break;
	case MidiFirstChannel + 9:
		sel = 2;
		break;
	default:
		sel = nMidiCh - MidiFirstChannel + 2;
		if(nMidiCh < MidiFirstChannel + 9)
			sel++;
	}
	if(!m_percussion && (nMidiCh == MidiFirstChannel + 9))
	{
		FillProgramBox(true);
	} else if(m_percussion && (nMidiCh != MidiFirstChannel + 9))
	{
		FillProgramBox(false);
	}
	m_CbnChannel.SetCurSel(sel);
	UINT nMidiProgram = m_instrMap[m_currentInstr].program;
	if(m_percussion)
	{
		if(nMidiProgram >= 24 && nMidiProgram <= 84)
			nMidiProgram -= 23;
		else
			nMidiProgram = 0;
	} else
	{
		if(nMidiProgram > 127)
			nMidiProgram = 0;
	}
	m_CbnProgram.SetCurSel(nMidiProgram);
}


void CModToMidi::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
	CDialog::OnVScroll(nSBCode, nPos, pScrollBar);
	int pos = m_SpinInstrument.GetPos32();
	if(pos)
	{
		m_SpinInstrument.SetPos(0);
		int numIns = m_CbnInstrument.GetCount();
		int ins = m_CbnInstrument.GetCurSel() + pos;
		if(ins < 0)
			ins = numIns - 1;
		if(ins >= numIns)
			ins = 0;
		m_CbnInstrument.SetCurSel(ins);
		UpdateDialog();
	}
}


void CModToMidi::OnChannelChanged()
{
	uint8 midiCh = static_cast<uint8>(m_CbnChannel.GetItemData(m_CbnChannel.GetCurSel()));
	if(m_currentInstr >= m_instrMap.size())
		return;
	const auto oldCh = m_instrMap[m_currentInstr].channel;
	m_instrMap[m_currentInstr].channel = midiCh;
	if(midiCh == MidiNoChannel
	   || oldCh == MidiNoChannel
	   || (!m_percussion && midiCh == MidiFirstChannel + 9)
	   || (m_percussion && midiCh != MidiFirstChannel + 9))
	{
		UpdateDialog();
	}
}


void CModToMidi::OnProgramChanged()
{
	DWORD_PTR nProgram = m_CbnProgram.GetItemData(m_CbnProgram.GetCurSel());
	if (nProgram == CB_ERR) return;
	if ((m_currentInstr > 0) && (m_currentInstr < MAX_SAMPLES))
	{
		m_instrMap[m_currentInstr].program = static_cast<uint8>(nProgram);
	}
}


void CModToMidi::OnOverlapChanged()
{
	s_overlappingInstruments = IsDlgButtonChecked(IDC_CHECK1) != BST_UNCHECKED;
}


void CModToMidi::OnOK()
{
	for(size_t i = 1; i < m_instrMap.size(); i++)
	{
		if(m_instrMap[i].channel != MidiNoChannel)
		{
			CDialog::OnOK();
			return;
		}
	}

	auto choice = Reporting::Confirm(_T("No instruments have been selected for export. Would you still like to export the file?"), true, true);
	if(choice == cnfYes)
		CDialog::OnOK();
	else if(choice == cnfNo)
		CDialog::OnCancel();
}


void CDoMidiConvert::Run()
{
	CMainFrame::GetMainFrame()->PauseMod(m_sndFile.GetpModDoc());

	const auto songLength = m_sndFile.GetLength(eNoAdjust).front();
	const double duration = songLength.duration;
	const uint64 totalSamples = mpt::saturate_round<uint64>(duration * m_sndFile.m_MixerSettings.gdwMixingFreq);
	SetRange(0, totalSamples);

	auto conv = std::make_unique<MidiExport::Conversion>(m_sndFile, m_instrMap, m_file, CModToMidi::s_overlappingInstruments, songLength);

	auto startTime = timeGetTime(), prevTime = startTime;

	m_sndFile.SetCurrentOrder(0);
	m_sndFile.GetLength(eAdjust, GetLengthTarget(0, 0));
	m_sndFile.m_SongFlags.reset(SONG_PATTERNLOOP);
	int oldRepCount = m_sndFile.GetRepeatCount();
	m_sndFile.SetRepeatCount(0);
	m_sndFile.m_bIsRendering = true;

	EnableTaskbarProgress();

	MidiExport::DummyAudioTarget target;
	UINT ok = IDOK;
	const auto fmt = MPT_TFORMAT("Rendering file... ({}mn{}s, {}mn{}s remaining)");
	while(m_sndFile.Read(MIXBUFFERSIZE, target) > 0)
	{
		auto currentTime = timeGetTime();
		if(currentTime - prevTime >= 16)
		{
			prevTime = currentTime;
			uint64 curSamples = m_sndFile.GetTotalSampleCount();
			uint32 curTime = static_cast<uint32>(curSamples / m_sndFile.m_MixerSettings.gdwMixingFreq);
			uint32 timeRemaining = 0;
			if(curSamples > 0 && curSamples < totalSamples)
			{
				timeRemaining = static_cast<uint32>(((currentTime - startTime) * (totalSamples - curSamples) / curSamples) / 1000u);
			}
			SetText(fmt(curTime / 60u, mpt::tfmt::dec0<2>(curTime % 60u), timeRemaining / 60u, mpt::tfmt::dec0<2>(timeRemaining % 60u)).c_str());
			SetProgress(curSamples);
			ProcessMessages();

			if(m_abort)
			{
				ok = IDCANCEL;
				break;
			}
		}
	}

	conv->Finalise();

	m_sndFile.m_bIsRendering = false;
	m_sndFile.SetRepeatCount(oldRepCount);

	EndDialog(ok);
}

OPENMPT_NAMESPACE_END

#endif // NO_PLUGINS