/* * 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 #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 m_pitchWheelDepth = { 0 }; std::vector> 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(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(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(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(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(60000000.0 / curTempo); uint8 msg[6] = { 0xFF, 0x51, 0x03, static_cast(mspq >> 16), static_cast(mspq >> 8), static_cast(mspq) }; mpt::IO::WriteRaw(f, msg, 6); } if(sigChanged) { // Write MIDI time signature WriteTicks(); uint8 msg[7] = { 0xFF, 0x58, 0x04, static_cast(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(midiVol & 0x7F), static_cast((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 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(f, 0xF0); mpt::IO::WriteVarInt(f, mpt::saturate_cast(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(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 m_oldInstruments; std::vector m_tracks; std::vector 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(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(m_file, 6); mpt::IO::WriteIntBE(m_file, 1); // Type 1 MIDI - multiple simultaneous tracks mpt::IO::WriteIntBE(m_file, static_cast(m_tracks.size())); // Number of tracks mpt::IO::WriteIntBE(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(data.size()); mpt::IO::WriteRaw(m_file, "MTrk", 4); mpt::IO::WriteIntBE(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) override { } void Process(mpt::audio_span_interleaved) 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(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(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(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(duration * m_sndFile.m_MixerSettings.gdwMixingFreq); SetRange(0, totalSamples); auto conv = std::make_unique(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(curSamples / m_sndFile.m_MixerSettings.gdwMixingFreq); uint32 timeRemaining = 0; if(curSamples > 0 && curSamples < totalSamples) { timeRemaining = static_cast(((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