aboutsummaryrefslogtreecommitdiff
path: root/Src/external_dependencies/openmpt-trunk/mptrack/mod2midi.cpp
diff options
context:
space:
mode:
authorJef <jef@targetspot.com>2024-09-24 08:54:57 -0400
committerJef <jef@targetspot.com>2024-09-24 08:54:57 -0400
commit20d28e80a5c861a9d5f449ea911ab75b4f37ad0d (patch)
tree12f17f78986871dd2cfb0a56e5e93b545c1ae0d0 /Src/external_dependencies/openmpt-trunk/mptrack/mod2midi.cpp
parent537bcbc86291b32fc04ae4133ce4d7cac8ebe9a7 (diff)
downloadwinamp-20d28e80a5c861a9d5f449ea911ab75b4f37ad0d.tar.gz
Initial community commit
Diffstat (limited to 'Src/external_dependencies/openmpt-trunk/mptrack/mod2midi.cpp')
-rw-r--r--Src/external_dependencies/openmpt-trunk/mptrack/mod2midi.cpp863
1 files changed, 863 insertions, 0 deletions
diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/mod2midi.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/mod2midi.cpp
new file mode 100644
index 00000000..aad37810
--- /dev/null
+++ b/Src/external_dependencies/openmpt-trunk/mptrack/mod2midi.cpp
@@ -0,0 +1,863 @@
+/*
+ * 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