winamp/Src/external_dependencies/openmpt-trunk/mptrack/mod2midi.cpp
2024-09-24 14:54:57 +02:00

863 lines
26 KiB
C++

/*
* 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