Index: mptrack/AbstractVstEditor.cpp
===================================================================
--- mptrack/AbstractVstEditor.cpp	(revision 16701)
+++ mptrack/AbstractVstEditor.cpp	(working copy)
@@ -91,7 +91,7 @@
 	ON_MESSAGE(WM_MOD_KEYCOMMAND,	&CAbstractVstEditor::OnCustomKeyMsg) //rewbs.customKeys
 	ON_COMMAND_RANGE(ID_PLUGSELECT, ID_PLUGSELECT + MAX_MIXPLUGINS, &CAbstractVstEditor::OnToggleEditor) //rewbs.patPlugName
 	ON_COMMAND_RANGE(ID_SELECTINST, ID_SELECTINST + MAX_INSTRUMENTS, &CAbstractVstEditor::OnSetInputInstrument) //rewbs.patPlugName
-	ON_COMMAND_RANGE(ID_LEARN_MACRO_FROM_PLUGGUI, ID_LEARN_MACRO_FROM_PLUGGUI + NUM_MACROS, &CAbstractVstEditor::PrepareToLearnMacro)
+	ON_COMMAND_RANGE(ID_LEARN_MACRO_FROM_PLUGGUI, ID_LEARN_MACRO_FROM_PLUGGUI + kSFxMacros, &CAbstractVstEditor::PrepareToLearnMacro)
 END_MESSAGE_MAP()
 
 
@@ -829,7 +829,7 @@
 	}
 
 	CString label, macroName;
-	for(int nMacro = 0; nMacro < NUM_MACROS; nMacro++)
+	for(int nMacro = 0; nMacro < kSFxMacros; nMacro++)
 	{
 		int action = 0;
 		UINT greyed = MF_GRAYED;
@@ -965,7 +965,7 @@
 
 void CAbstractVstEditor::SetLearnMacro(int inMacro)
 {
-	if (inMacro < NUM_MACROS)
+	if (inMacro < kSFxMacros)
 	{
 		m_nLearnMacro=inMacro;
 	}
Index: mptrack/EffectInfo.cpp
===================================================================
--- mptrack/EffectInfo.cpp	(revision 16701)
+++ mptrack/EffectInfo.cpp	(working copy)
@@ -640,7 +640,7 @@
 			if(chn != CHANNELINDEX_INVALID)
 			{
 				const uint8 macroIndex = sndFile.m_PlayState.Chn[chn].nActiveMacro;
-				const PLUGINDEX plugin = sndFile.GetBestPlugin(chn, PrioritiseChannel, EvenIfMuted) - 1;
+				const PLUGINDEX plugin = sndFile.GetBestPlugin(sndFile.m_PlayState, chn, PrioritiseChannel, EvenIfMuted) - 1;
 				IMixPlugin *pPlugin = (plugin < MAX_MIXPLUGINS ? sndFile.m_MixPlugins[plugin].pMixPlugin : nullptr);
 				pszName.Format(_T("SFx MIDI Macro z=%d (SF%X: %s)"), param, macroIndex, sndFile.m_MidiCfg.GetParameteredMacroName(macroIndex, pPlugin).GetString());
 			} else
Index: mptrack/MIDIMacroDialog.cpp
===================================================================
--- mptrack/MIDIMacroDialog.cpp	(revision 16701)
+++ mptrack/MIDIMacroDialog.cpp	(working copy)
@@ -35,8 +35,8 @@
 	ON_CBN_SELCHANGE(IDC_MACROCC,	&CMidiMacroSetup::OnCCChanged)
 	ON_EN_CHANGE(IDC_EDIT1,			&CMidiMacroSetup::OnSFxEditChanged)
 	ON_EN_CHANGE(IDC_EDIT2,			&CMidiMacroSetup::OnZxxEditChanged)
-	ON_COMMAND_RANGE(ID_PLUGSELECT, ID_PLUGSELECT + NUM_MACROS - 1, &CMidiMacroSetup::OnViewAllParams)
-	ON_COMMAND_RANGE(ID_PLUGSELECT + NUM_MACROS, ID_PLUGSELECT + NUM_MACROS + NUM_MACROS - 1, &CMidiMacroSetup::OnSetSFx)
+	ON_COMMAND_RANGE(ID_PLUGSELECT, ID_PLUGSELECT + kSFxMacros - 1, &CMidiMacroSetup::OnViewAllParams)
+	ON_COMMAND_RANGE(ID_PLUGSELECT + kSFxMacros, ID_PLUGSELECT + kSFxMacros + kSFxMacros - 1, &CMidiMacroSetup::OnSetSFx)
 END_MESSAGE_MAP()
 
 
@@ -59,8 +59,8 @@
 {
 	CString s;
 	CDialog::OnInitDialog();
-	m_EditSFx.SetLimitText(MACRO_LENGTH - 1);
-	m_EditZxx.SetLimitText(MACRO_LENGTH - 1);
+	m_EditSFx.SetLimitText(kMacroLength - 1);
+	m_EditZxx.SetLimitText(kMacroLength - 1);
 
 	// Parametered macro selection
 	for(int i = 0; i < 16; i++)
@@ -106,18 +106,18 @@
 	int offsetx = ScalePixels(19), offsety = ScalePixels(30), separatorx = ScalePixels(4), separatory = ScalePixels(2);
 	int height = ScalePixels(18), widthMacro = ScalePixels(30), widthVal = ScalePixels(179), widthType = ScalePixels(135), widthBtn = ScalePixels(70);
 
-	for(UINT m = 0; m < NUM_MACROS; m++)
+	for(UINT m = 0; m < kSFxMacros; m++)
 	{
 		m_EditMacro[m].Create(_T(""), WS_CHILD | WS_VISIBLE | WS_TABSTOP,
-			CRect(offsetx, offsety + m * (separatory + height), offsetx + widthMacro, offsety + m * (separatory + height) + height), this, ID_PLUGSELECT + NUM_MACROS + m);
+			CRect(offsetx, offsety + m * (separatory + height), offsetx + widthMacro, offsety + m * (separatory + height) + height), this, ID_PLUGSELECT + kSFxMacros + m);
 		m_EditMacro[m].SetFont(GetFont());
 
 		m_EditMacroType[m].Create(ES_READONLY | WS_CHILD| WS_VISIBLE | WS_TABSTOP | WS_BORDER, 
-			CRect(offsetx + separatorx + widthMacro, offsety + m * (separatory + height), offsetx + widthMacro + widthType, offsety + m * (separatory + height) + height), this, ID_PLUGSELECT + NUM_MACROS + m);
+			CRect(offsetx + separatorx + widthMacro, offsety + m * (separatory + height), offsetx + widthMacro + widthType, offsety + m * (separatory + height) + height), this, ID_PLUGSELECT + kSFxMacros + m);
 		m_EditMacroType[m].SetFont(GetFont());
 
 		m_EditMacroValue[m].Create(ES_CENTER | ES_READONLY | WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER, 
-			CRect(offsetx + separatorx + widthType + widthMacro, offsety + m * (separatory + height), offsetx + widthMacro + widthType + widthVal, offsety + m * (separatory + height) + height), this, ID_PLUGSELECT + NUM_MACROS + m);
+			CRect(offsetx + separatorx + widthType + widthMacro, offsety + m * (separatory + height), offsetx + widthMacro + widthType + widthVal, offsety + m * (separatory + height) + height), this, ID_PLUGSELECT + kSFxMacros + m);
 		m_EditMacroValue[m].SetFont(GetFont());
 
 		m_BtnMacroShowAll[m].Create(_T("Show All..."), WS_CHILD | WS_TABSTOP | WS_VISIBLE,
@@ -156,13 +156,13 @@
 
 	int start, end;
 
-	if(macro >= 0 && macro < 16)
+	if(macro >= 0 && macro < kSFxMacros)
 	{
 		start = end = macro;
 	} else
 	{
 		start = 0;
-		end = NUM_MACROS - 1;
+		end = kSFxMacros - 1;
 	}
 
 	CString s;
@@ -175,7 +175,7 @@
 		m_EditMacro[m].SetWindowText(s);
 
 		// Macro value:
-		m_EditMacroValue[m].SetWindowText(mpt::ToCString(mpt::Charset::ASCII, m_MidiCfg.szMidiSFXExt[m]));
+		m_EditMacroValue[m].SetWindowText(mpt::ToCString(mpt::Charset::ASCII, m_MidiCfg.SFx[m]));
 		m_EditMacroValue[m].SetBackColor(m == selectedMacro ? RGB(200, 200, 225) : RGB(245, 245, 245));
 
 		// Macro Type:
@@ -203,10 +203,10 @@
 {
 	UINT sfx = m_CbnSFx.GetCurSel();
 	UINT sfx_preset = static_cast<UINT>(m_CbnSFxPreset.GetItemData(m_CbnSFxPreset.GetCurSel()));
-	if(sfx < std::size(m_MidiCfg.szMidiSFXExt))
+	if(sfx < m_MidiCfg.SFx.size())
 	{
 		ToggleBoxes(sfx_preset, sfx);
-		m_EditSFx.SetWindowText(mpt::ToCString(mpt::Charset::ASCII, m_MidiCfg.szMidiSFXExt[sfx]));
+		m_EditSFx.SetWindowText(mpt::ToCString(mpt::Charset::ASCII, m_MidiCfg.SFx[sfx]));
 	}
 
 	UpdateZxxSelection();
@@ -268,7 +268,7 @@
 	UINT sfx = m_CbnSFx.GetCurSel();
 	ParameteredMacro sfx_preset = static_cast<ParameteredMacro>(m_CbnSFxPreset.GetItemData(m_CbnSFxPreset.GetCurSel()));
 
-	if (sfx < std::size(m_MidiCfg.szMidiSFXExt))
+	if (sfx < kSFxMacros)
 	{
 		if(sfx_preset != kSFxCustom)
 		{
@@ -294,9 +294,9 @@
 void CMidiMacroSetup::UpdateZxxSelection()
 {
 	UINT zxx = m_CbnZxx.GetCurSel();
-	if(zxx < std::size(m_MidiCfg.szMidiZXXExt))
+	if(zxx < m_MidiCfg.Zxx.size())
 	{
-		m_EditZxx.SetWindowText(mpt::ToCString(mpt::Charset::ASCII, m_MidiCfg.szMidiZXXExt[zxx]));
+		m_EditZxx.SetWindowText(mpt::ToCString(mpt::Charset::ASCII, m_MidiCfg.Zxx[zxx]));
 	}
 }
 
@@ -304,13 +304,13 @@
 void CMidiMacroSetup::OnSFxEditChanged()
 {
 	UINT sfx = m_CbnSFx.GetCurSel();
-	if (sfx < std::size(m_MidiCfg.szMidiSFXExt))
+	if(sfx < m_MidiCfg.SFx.size())
 	{
-		if(ValidateMacroString(m_EditSFx, m_MidiCfg.szMidiSFXExt[sfx], true))
+		if(ValidateMacroString(m_EditSFx, m_MidiCfg.SFx[sfx], true))
 		{
 			CString s;
 			m_EditSFx.GetWindowText(s);
-			mpt::String::WriteAutoBuf(m_MidiCfg.szMidiSFXExt[sfx]) = mpt::ToCharset(mpt::Charset::ASCII, s);
+			m_MidiCfg.SFx[sfx] = mpt::ToCharset(mpt::Charset::ASCII, s);
 
 			int sfx_preset = m_MidiCfg.GetParameteredMacroType(sfx);
 			m_CbnSFxPreset.SetCurSel(sfx_preset);
@@ -324,13 +324,13 @@
 void CMidiMacroSetup::OnZxxEditChanged()
 {
 	UINT zxx = m_CbnZxx.GetCurSel();
-	if (zxx < std::size(m_MidiCfg.szMidiZXXExt))
+	if(zxx < m_MidiCfg.Zxx.size())
 	{
-		if(ValidateMacroString(m_EditZxx, m_MidiCfg.szMidiZXXExt[zxx], false))
+		if(ValidateMacroString(m_EditZxx, m_MidiCfg.Zxx[zxx], false))
 		{
 			CString s;
 			m_EditZxx.GetWindowText(s);
-			mpt::String::WriteAutoBuf(m_MidiCfg.szMidiZXXExt[zxx]) = mpt::ToCharset(mpt::Charset::ASCII, s);
+			m_MidiCfg.Zxx[zxx] = mpt::ToCharset(mpt::Charset::ASCII, s);
 			m_CbnZxxPreset.SetCurSel(m_MidiCfg.GetFixedMacroType());
 		}
 	}
@@ -338,7 +338,7 @@
 
 void CMidiMacroSetup::OnSetSFx(UINT id)
 {
-	m_CbnSFx.SetCurSel(id - (ID_PLUGSELECT + NUM_MACROS));
+	m_CbnSFx.SetCurSel(id - (ID_PLUGSELECT + kSFxMacros));
 	OnSFxChanged();
 }
 
@@ -442,7 +442,7 @@
 }
 
 
-bool CMidiMacroSetup::ValidateMacroString(CEdit &wnd, char *lastMacro, bool isParametric)
+bool CMidiMacroSetup::ValidateMacroString(CEdit &wnd, const MIDIMacroConfig::Macro &lastMacro, bool isParametric)
 {
 	CString macroStrT;
 	wnd.GetWindowText(macroStrT);
@@ -451,11 +451,11 @@
 	bool allowed = true, caseChange = false;
 	for(char &c : macroStr)
 	{
-		if(c == 'k' || c == 'K')		// Previously, 'K' was used for MIDI channel
+		if(c == 'k' || c == 'K')  // Previously, 'K' was used for MIDI channel
 		{
 			caseChange = true;
 			c = 'c';
-		} else if(c >= 'd' && c <= 'f')	// abc have special meanings, but def can be fixed
+		} else if(c >= 'd' && c <= 'f')  // abc have special meanings, but def can be fixed
 		{
 			caseChange = true;
 			c = c - 'a' + 'A';
@@ -476,7 +476,7 @@
 	if(!allowed)
 	{
 		// Replace text and keep cursor position if we just typed in an invalid character
-		if(lastMacro != macroStr)
+		if(lastMacro != std::string_view{macroStr})
 		{
 			int start, end;
 			wnd.GetSel(start, end);
Index: mptrack/MIDIMacroDialog.h
===================================================================
--- mptrack/MIDIMacroDialog.h	(revision 16701)
+++ mptrack/MIDIMacroDialog.h	(working copy)
@@ -24,8 +24,8 @@
 protected:
 	CComboBox m_CbnSFx, m_CbnSFxPreset, m_CbnZxx, m_CbnZxxPreset, m_CbnMacroPlug, m_CbnMacroParam, m_CbnMacroCC;
 	CEdit m_EditSFx, m_EditZxx;
-	CColourEdit m_EditMacroValue[NUM_MACROS], m_EditMacroType[NUM_MACROS];
-	CButton m_EditMacro[NUM_MACROS], m_BtnMacroShowAll[NUM_MACROS];
+	CColourEdit m_EditMacroValue[kSFxMacros], m_EditMacroType[kSFxMacros];
+	CButton m_EditMacro[kSFxMacros], m_BtnMacroShowAll[kSFxMacros];
 
 	CSoundFile &m_SndFile;
 
@@ -40,7 +40,7 @@
 	BOOL OnInitDialog() override;
 	void DoDataExchange(CDataExchange* pDX) override;
 
-	bool ValidateMacroString(CEdit &wnd, char *lastMacro, bool isParametric);
+	bool ValidateMacroString(CEdit &wnd, const MIDIMacroConfig::Macro &lastMacro, bool isParametric);
 
 	void UpdateMacroList(int macro=-1);
 	void ToggleBoxes(UINT preset, UINT sfx);
Index: mptrack/mod2midi.cpp
===================================================================
--- mptrack/mod2midi.cpp	(revision 16701)
+++ mptrack/mod2midi.cpp	(working copy)
@@ -94,7 +94,9 @@
 
 		void SynchronizeMidiPitchWheelDepth(CHANNELINDEX trackerChn)
 		{
-			const auto midiCh = GetMidiChannel(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));
 		}
@@ -306,7 +308,7 @@
 			return true;
 		}
 
-		uint8 GetMidiChannel(CHANNELINDEX trackChannel) const override
+		uint8 GetMidiChannel(const ModChannel &chn, CHANNELINDEX trackChannel) const override
 		{
 			if(m_instr.nMidiChannel == MidiMappedChannel && trackChannel < std::size(m_sndFile.m_PlayState.Chn))
 			{
@@ -316,7 +318,7 @@
 					midiCh++;
 				return midiCh;
 			}
-			return IMidiPlugin::GetMidiChannel(trackChannel);
+			return IMidiPlugin::GetMidiChannel(chn, trackChannel);
 		}
 
 		void MidiCommand(const ModInstrument &instr, uint16 note, uint16 vol, CHANNELINDEX trackChannel) override
@@ -327,8 +329,8 @@
 				note = NOTE_KEYOFF;
 			}
 			SynchronizeMidiChannelState();
-			if(trackChannel < MAX_CHANNELS)
-				m_lastModChannel[GetMidiChannel(trackChannel)] = trackChannel;
+			if(trackChannel < std::size(m_sndFile.m_PlayState.Chn))
+				m_lastModChannel[GetMidiChannel(m_sndFile.m_PlayState.Chn[trackChannel], trackChannel)] = trackChannel;
 			IMidiPlugin::MidiCommand(instr, note, vol, trackChannel);
 		}
 
Index: mptrack/Moddoc.cpp
===================================================================
--- mptrack/Moddoc.cpp	(revision 16701)
+++ mptrack/Moddoc.cpp	(working copy)
@@ -1290,7 +1290,7 @@
 		m_SndFile.m_PlayState.Chn[nChn].dwFlags.set(muteType);
 		if(m_SndFile.m_opl) m_SndFile.m_opl->NoteCut(nChn);
 		// Kill VSTi notes on muted channel.
-		PLUGINDEX nPlug = m_SndFile.GetBestPlugin(nChn, PrioritiseInstrument, EvenIfMuted);
+		PLUGINDEX nPlug = m_SndFile.GetBestPlugin(m_SndFile.m_PlayState, nChn, PrioritiseInstrument, EvenIfMuted);
 		if ((nPlug) && (nPlug<=MAX_MIXPLUGINS))
 		{
 			IMixPlugin *pPlug = m_SndFile.m_MixPlugins[nPlug - 1].pMixPlugin;
@@ -2864,22 +2864,18 @@
 
 void CModDoc::LearnMacro(int macroToSet, PlugParamIndex paramToUse)
 {
-	if (macroToSet < 0 || macroToSet > NUM_MACROS)
+	if(macroToSet < 0 || macroToSet > kSFxMacros)
 	{
 		return;
 	}
 
 	// If macro already exists for this param, inform user and return
-	for (int checkMacro = 0; checkMacro < NUM_MACROS; checkMacro++)
+	if(auto macro = m_SndFile.m_MidiCfg.FindMacroForParam(paramToUse); macro >= 0)
 	{
-		if (m_SndFile.m_MidiCfg.GetParameteredMacroType(checkMacro) == kSFxPlugParam
-			&& m_SndFile.m_MidiCfg.MacroToPlugParam(checkMacro) == paramToUse)
-		{
-			CString message;
-			message.Format(_T("Parameter %i can already be controlled with macro %X."), static_cast<int>(paramToUse), checkMacro);
-			Reporting::Information(message, _T("Macro exists for this parameter"));
-			return;
-		}
+		CString message;
+		message.Format(_T("Parameter %i can already be controlled with macro %X."), static_cast<int>(paramToUse), macro);
+		Reporting::Information(message, _T("Macro exists for this parameter"));
+		return;
 	}
 
 	// Set new macro
Index: mptrack/MPTHacks.cpp
===================================================================
--- mptrack/MPTHacks.cpp	(revision 16701)
+++ mptrack/MPTHacks.cpp	(working copy)
@@ -460,7 +460,7 @@
 	{
 		for(const auto &macro : m_SndFile.m_MidiCfg)
 		{
-			for(const auto c : macro)
+			for(const auto c : macro.Span())
 			{
 				if(c == 's')
 				{
Index: mptrack/TrackerSettings.cpp
===================================================================
--- mptrack/TrackerSettings.cpp	(revision 16701)
+++ mptrack/TrackerSettings.cpp	(working copy)
@@ -441,13 +441,13 @@
 	// Zxx Macros
 	MIDIMacroConfig macros;
 	theApp.GetDefaultMidiMacro(macros);
-	for(int isfx = 0; isfx < 16; isfx++)
+	for(int i = 0; i < kSFxMacros; i++)
 	{
-		mpt::String::WriteAutoBuf(macros.szMidiSFXExt[isfx]) = conf.Read<std::string>(U_("Zxx Macros"), MPT_UFORMAT("SF{}")(mpt::ufmt::HEX(isfx)), macros.szMidiSFXExt[isfx]);
+		macros.SFx[i] = conf.Read<std::string>(U_("Zxx Macros"), MPT_UFORMAT("SF{}")(mpt::ufmt::HEX(i)), macros.SFx[i]);
 	}
-	for(int izxx = 0; izxx < 128; izxx++)
+	for(int i = 0; i < kZxxMacros; i++)
 	{
-		mpt::String::WriteAutoBuf(macros.szMidiZXXExt[izxx]) = conf.Read<std::string>(U_("Zxx Macros"), MPT_UFORMAT("Z{}")(mpt::ufmt::HEX0<2>(izxx | 0x80)), macros.szMidiZXXExt[izxx]);
+		macros.Zxx[i] = conf.Read<std::string>(U_("Zxx Macros"), MPT_UFORMAT("Z{}")(mpt::ufmt::HEX0<2>(i | 0x80)), macros.Zxx[i]);
 	}
 
 
@@ -1342,13 +1342,13 @@
 	// Save default macro configuration
 	MIDIMacroConfig macros;
 	theApp.GetDefaultMidiMacro(macros);
-	for(int isfx = 0; isfx < 16; isfx++)
+	for(int isfx = 0; isfx < kSFxMacros; isfx++)
 	{
-		conf.Write<std::string>(U_("Zxx Macros"), MPT_UFORMAT("SF{}")(mpt::ufmt::HEX(isfx)), macros.szMidiSFXExt[isfx]);
+		conf.Write<std::string>(U_("Zxx Macros"), MPT_UFORMAT("SF{}")(mpt::ufmt::HEX(isfx)), macros.SFx[isfx]);
 	}
-	for(int izxx = 0; izxx < 128; izxx++)
+	for(int izxx = 0; izxx < kZxxMacros; izxx++)
 	{
-		conf.Write<std::string>(U_("Zxx Macros"), MPT_UFORMAT("Z{}")(mpt::ufmt::HEX0<2>(izxx | 0x80)), macros.szMidiZXXExt[izxx]);
+		conf.Write<std::string>(U_("Zxx Macros"), MPT_UFORMAT("Z{}")(mpt::ufmt::HEX0<2>(izxx | 0x80)), macros.Zxx[izxx]);
 	}
 
 	// MRU list
Index: soundlib/Fastmix.cpp
===================================================================
--- soundlib/Fastmix.cpp	(revision 16701)
+++ soundlib/Fastmix.cpp	(working copy)
@@ -345,7 +345,7 @@
 
 		//Look for plugins associated with this implicit tracker channel.
 #ifndef NO_PLUGINS
-		PLUGINDEX nMixPlugin = GetBestPlugin(m_PlayState.ChnMix[nChn], PrioritiseInstrument, RespectMutes);
+		PLUGINDEX nMixPlugin = GetBestPlugin(m_PlayState, m_PlayState.ChnMix[nChn], PrioritiseInstrument, RespectMutes);
 
 		if ((nMixPlugin > 0) && (nMixPlugin <= MAX_MIXPLUGINS) && m_MixPlugins[nMixPlugin - 1].pMixPlugin != nullptr)
 		{
Index: soundlib/Load_dbm.cpp
===================================================================
--- soundlib/Load_dbm.cpp	(revision 16701)
+++ soundlib/Load_dbm.cpp	(working copy)
@@ -623,10 +623,10 @@
 		for(uint32 i = 0; i < 32; i++)
 		{
 			uint32 param = (i * 127u) / 32u;
-			mpt::String::WriteAutoBuf(m_MidiCfg.szMidiZXXExt[i     ]) = MPT_AFORMAT("F0F080{}")(mpt::afmt::HEX0<2>(param));
-			mpt::String::WriteAutoBuf(m_MidiCfg.szMidiZXXExt[i + 32]) = MPT_AFORMAT("F0F081{}")(mpt::afmt::HEX0<2>(param));
-			mpt::String::WriteAutoBuf(m_MidiCfg.szMidiZXXExt[i + 64]) = MPT_AFORMAT("F0F082{}")(mpt::afmt::HEX0<2>(param));
-			mpt::String::WriteAutoBuf(m_MidiCfg.szMidiZXXExt[i + 96]) = MPT_AFORMAT("F0F083{}")(mpt::afmt::HEX0<2>(param));
+			m_MidiCfg.Zxx[i     ] = MPT_AFORMAT("F0F080{}")(mpt::afmt::HEX0<2>(param));
+			m_MidiCfg.Zxx[i + 32] = MPT_AFORMAT("F0F081{}")(mpt::afmt::HEX0<2>(param));
+			m_MidiCfg.Zxx[i + 64] = MPT_AFORMAT("F0F082{}")(mpt::afmt::HEX0<2>(param));
+			m_MidiCfg.Zxx[i + 96] = MPT_AFORMAT("F0F083{}")(mpt::afmt::HEX0<2>(param));
 		}
 	}
 #endif // NO_PLUGINS
Index: soundlib/Load_med.cpp
===================================================================
--- soundlib/Load_med.cpp	(revision 16701)
+++ soundlib/Load_med.cpp	(working copy)
@@ -1042,7 +1042,7 @@
 
 	// Setup a program change macro for command 1C (even if MIDI plugin is disabled, as otherwise these commands may act as filter commands)
 	m_MidiCfg.ClearZxxMacros();
-	strcpy(m_MidiCfg.szMidiSFXExt[0], "Cc z");
+	m_MidiCfg.SFx[0] = "Cc z";
 
 	file.Rewind();
 	PATTERNINDEX basePattern = 0;
@@ -1202,7 +1202,7 @@
 		// Read MIDI messages
 		if(expData.midiDumpOffset && file.Seek(expData.midiDumpOffset) && file.CanRead(8))
 		{
-			uint16 numDumps = std::min(file.ReadUint16BE(), static_cast<uint16>(std::size(m_MidiCfg.szMidiZXXExt)));
+			uint16 numDumps = std::min(file.ReadUint16BE(), static_cast<uint16>(m_MidiCfg.Zxx.size()));
 			file.Skip(6);
 			if(file.CanRead(numDumps * 4))
 			{
@@ -1216,8 +1216,8 @@
 					file.ReadStruct(dumpHeader);
 					if(!file.Seek(dumpHeader.dataPointer) || !file.CanRead(dumpHeader.length))
 						continue;
-					auto &macro = m_MidiCfg.szMidiZXXExt[dump];
-					auto length = std::min(static_cast<size_t>(dumpHeader.length), std::size(macro) / 2u);
+					MIDIMacroConfig::Macro::RawType macro{};
+					auto length = std::min(static_cast<size_t>(dumpHeader.length), macro.size() / 2u);
 					for(size_t i = 0; i < length; i++)
 					{
 						const uint8 byte = file.ReadUint8(), high = byte >> 4, low = byte & 0x0F;
@@ -1224,6 +1224,7 @@
 						macro[i * 2] = high + (high < 0x0A ? '0' : 'A' - 0x0A);
 						macro[i * 2 + 1] = low + (low < 0x0A ? '0' : 'A' - 0x0A);
 					}
+					m_MidiCfg.Zxx[dump] = macro;
 				}
 			}
 		}
Index: soundlib/Load_mo3.cpp
===================================================================
--- soundlib/Load_mo3.cpp	(revision 16701)
+++ soundlib/Load_mo3.cpp	(working copy)
@@ -907,16 +907,16 @@
 		for(uint32 i = 0; i < 16; i++)
 		{
 			if(fileHeader.sfxMacros[i])
-				mpt::String::WriteAutoBuf(m_MidiCfg.szMidiSFXExt[i]) = MPT_AFORMAT("F0F0{}z")(mpt::afmt::HEX0<2>(fileHeader.sfxMacros[i] - 1));
+				m_MidiCfg.SFx[i] = MPT_AFORMAT("F0F0{}z")(mpt::afmt::HEX0<2>(fileHeader.sfxMacros[i] - 1));
 			else
-				mpt::String::WriteAutoBuf(m_MidiCfg.szMidiSFXExt[i]) = "";
+				m_MidiCfg.SFx[i] = "";
 		}
 		for(uint32 i = 0; i < 128; i++)
 		{
 			if(fileHeader.fixedMacros[i][1])
-				mpt::String::WriteAutoBuf(m_MidiCfg.szMidiZXXExt[i]) = MPT_AFORMAT("F0F0{}{}")(mpt::afmt::HEX0<2>(fileHeader.fixedMacros[i][1] - 1), mpt::afmt::HEX0<2>(fileHeader.fixedMacros[i][0].get()));
+				m_MidiCfg.Zxx[i] = MPT_AFORMAT("F0F0{}{}")(mpt::afmt::HEX0<2>(fileHeader.fixedMacros[i][1] - 1), mpt::afmt::HEX0<2>(fileHeader.fixedMacros[i][0].get()));
 			else
-				mpt::String::WriteAutoBuf(m_MidiCfg.szMidiZXXExt[i]) = "";
+				m_MidiCfg.Zxx[i] = "";
 		}
 	}
 
Index: soundlib/Load_symmod.cpp
===================================================================
--- soundlib/Load_symmod.cpp	(revision 16701)
+++ soundlib/Load_symmod.cpp	(working copy)
@@ -955,11 +955,11 @@
 		const uint8 reso = static_cast<uint8>(std::min(127, event.inst * 127 / 185));
 
 		if(type == 1)  // lowpass filter
-			mpt::String::WriteAutoBuf(macro) = MPT_AFORMAT("F0F000{} F0F001{} F0F00200")(mpt::afmt::HEX0<2>(cutoff), mpt::afmt::HEX0<2>(reso));
+			macro = MPT_AFORMAT("F0F000{} F0F001{} F0F00200")(mpt::afmt::HEX0<2>(cutoff), mpt::afmt::HEX0<2>(reso));
 		else if(type == 2)  // highpass filter
-			mpt::String::WriteAutoBuf(macro) = MPT_AFORMAT("F0F000{} F0F001{} F0F00210")(mpt::afmt::HEX0<2>(cutoff), mpt::afmt::HEX0<2>(reso));
+			macro = MPT_AFORMAT("F0F000{} F0F001{} F0F00210")(mpt::afmt::HEX0<2>(cutoff), mpt::afmt::HEX0<2>(reso));
 		else  // no filter or unsupported filter type
-			mpt::String::WriteAutoBuf(macro) = "F0F0007F F0F00100";
+			macro = "F0F0007F F0F00100";
 		return true;
 	} else if(event.command == SymEvent::DSPEcho)
 	{
@@ -966,7 +966,7 @@
 		const uint8 type = (event.note < 5) ? event.note : 0;
 		const uint8 length = (event.param < 128) ? event.param : 127;
 		const uint8 feedback = (event.inst < 128) ? event.inst : 127;
-		mpt::String::WriteAutoBuf(macro) = MPT_AFORMAT("F0F080{} F0F081{} F0F082{}")(mpt::afmt::HEX0<2>(type), mpt::afmt::HEX0<2>(length), mpt::afmt::HEX0<2>(feedback));
+		macro = MPT_AFORMAT("F0F080{} F0F081{} F0F082{}")(mpt::afmt::HEX0<2>(type), mpt::afmt::HEX0<2>(length), mpt::afmt::HEX0<2>(feedback));
 		return true;
 	} else if(event.command == SymEvent::DSPDelay)
 	{
@@ -1642,10 +1642,10 @@
 							{
 								m.command = CMD_MIDI;
 								m.param = macroMap[event];
-							} else if(macroMap.size() < std::size(m_MidiCfg.szMidiZXXExt))
+							} else if(macroMap.size() < m_MidiCfg.Zxx.size())
 							{
 								uint8 param = static_cast<uint8>(macroMap.size());
-								if(ConvertDSP(event, m_MidiCfg.szMidiZXXExt[param], *this))
+								if(ConvertDSP(event, m_MidiCfg.Zxx[param], *this))
 								{
 									m.command = CMD_MIDI;
 									m.param = macroMap[event] = 0x80 | param;
Index: soundlib/MIDIMacros.cpp
===================================================================
--- soundlib/MIDIMacros.cpp	(revision 16701)
+++ soundlib/MIDIMacros.cpp	(working copy)
@@ -9,10 +9,8 @@
 
 
 #include "stdafx.h"
+#include "MIDIMacros.h"
 #include "../soundlib/MIDIEvents.h"
-#include "MIDIMacros.h"
-#include "../common/mptStringBuffer.h"
-#include "../common/misc_util.h"
 
 #ifdef MODPLUG_TRACKER
 #include "Sndfile.h"
@@ -23,7 +21,7 @@
 
 ParameteredMacro MIDIMacroConfig::GetParameteredMacroType(uint32 macroIndex) const
 {
-	const std::string macro = GetSafeMacro(szMidiSFXExt[macroIndex]);
+	const std::string macro = SFx[macroIndex].NormalizedString();
 
 	for(uint32 i = 0; i < kSFxMax; i++)
 	{
@@ -30,17 +28,18 @@
 		ParameteredMacro sfx = static_cast<ParameteredMacro>(i);
 		if(sfx != kSFxCustom)
 		{
-			if(macro.compare(CreateParameteredMacro(sfx)) == 0) return sfx;
+			if(macro == CreateParameteredMacro(sfx))
+				return sfx;
 		}
 	}
 
 	// Special macros with additional "parameter":
-	if (macro.compare(CreateParameteredMacro(kSFxCC, MIDIEvents::MIDICC_start)) >= 0 && macro.compare(CreateParameteredMacro(kSFxCC, MIDIEvents::MIDICC_end)) <= 0 && macro.size() == 5)
+	if(macro.size() == 5 && macro.compare(CreateParameteredMacro(kSFxCC, MIDIEvents::MIDICC_start)) >= 0 && macro.compare(CreateParameteredMacro(kSFxCC, MIDIEvents::MIDICC_end)) <= 0)
 		return kSFxCC;
-	if (macro.compare(CreateParameteredMacro(kSFxPlugParam, 0)) >= 0 && macro.compare(CreateParameteredMacro(kSFxPlugParam, 0x17F)) <= 0 && macro.size() == 7)
+	if(macro.size() == 7 && macro.compare(CreateParameteredMacro(kSFxPlugParam, 0)) >= 0 && macro.compare(CreateParameteredMacro(kSFxPlugParam, 0x17F)) <= 0)
 		return kSFxPlugParam; 
 
-	return kSFxCustom;	// custom / unknown
+	return kSFxCustom;  // custom / unknown
 }
 
 
@@ -54,13 +53,13 @@
 		if(zxx != kZxxCustom)
 		{
 			// Prepare macro pattern to compare
-			Macro macros[128];
-			CreateFixedMacro(macros, zxx);
+			decltype(Zxx) fixedMacro;
+			CreateFixedMacro(fixedMacro, zxx);
 
 			bool found = true;
-			for(uint32 j = 0; j < 128; j++)
+			for(uint32 j = 0; j < kZxxMacros; j++)
 			{
-				if(strncmp(macros[j], szMidiZXXExt[j], MACRO_LENGTH))
+				if(fixedMacro[j] != Zxx[j])
 				{
 					found = false;
 					break;
@@ -77,17 +76,17 @@
 {
 	switch(macroType)
 	{
-	case kSFxUnused:     mpt::String::WriteAutoBuf(parameteredMacro) = ""; break;
-	case kSFxCutoff:     mpt::String::WriteAutoBuf(parameteredMacro) = "F0F000z"; break;
-	case kSFxReso:       mpt::String::WriteAutoBuf(parameteredMacro) = "F0F001z"; break;
-	case kSFxFltMode:    mpt::String::WriteAutoBuf(parameteredMacro) = "F0F002z"; break;
-	case kSFxDryWet:     mpt::String::WriteAutoBuf(parameteredMacro) = "F0F003z"; break;
-	case kSFxCC:         mpt::String::WriteAutoBuf(parameteredMacro) = MPT_AFORMAT("Bc{}z")(mpt::afmt::HEX0<2>(subType & 0x7F)); break;
-	case kSFxPlugParam:  mpt::String::WriteAutoBuf(parameteredMacro) = MPT_AFORMAT("F0F{}z")(mpt::afmt::HEX0<3>(std::min(subType, 0x17F) + 0x80)); break;
-	case kSFxChannelAT:  mpt::String::WriteAutoBuf(parameteredMacro) = "Dcz"; break;
-	case kSFxPolyAT:     mpt::String::WriteAutoBuf(parameteredMacro) = "Acnz"; break;
-	case kSFxPitch:      mpt::String::WriteAutoBuf(parameteredMacro) = "Ec00z"; break;
-	case kSFxProgChange: mpt::String::WriteAutoBuf(parameteredMacro) = "Ccz"; break;
+	case kSFxUnused:     parameteredMacro = ""; break;
+	case kSFxCutoff:     parameteredMacro = "F0F000z"; break;
+	case kSFxReso:       parameteredMacro = "F0F001z"; break;
+	case kSFxFltMode:    parameteredMacro = "F0F002z"; break;
+	case kSFxDryWet:     parameteredMacro = "F0F003z"; break;
+	case kSFxCC:         parameteredMacro = MPT_AFORMAT("Bc{}z")(mpt::afmt::HEX0<2>(subType & 0x7F)); break;
+	case kSFxPlugParam:  parameteredMacro = MPT_AFORMAT("F0F{}z")(mpt::afmt::HEX0<3>(std::min(subType, 0x17F) + 0x80)); break;
+	case kSFxChannelAT:  parameteredMacro = "Dcz"; break;
+	case kSFxPolyAT:     parameteredMacro = "Acnz"; break;
+	case kSFxPitch:      parameteredMacro = "Ec00z"; break;
+	case kSFxProgChange: parameteredMacro = "Ccz"; break;
 	case kSFxCustom:
 	default:
 		MPT_ASSERT_NOTREACHED();
@@ -98,59 +97,59 @@
 
 std::string MIDIMacroConfig::CreateParameteredMacro(ParameteredMacro macroType, int subType) const
 {
-	Macro parameteredMacro;
+	Macro parameteredMacro{};
 	CreateParameteredMacro(parameteredMacro, macroType, subType);
-	return mpt::String::ReadAutoBuf(parameteredMacro);
+	return parameteredMacro;
 }
 
 
 // Create Zxx (Z80 - ZFF) from preset
-void MIDIMacroConfig::CreateFixedMacro(Macro (&fixedMacros)[128], FixedMacro macroType) const
+void MIDIMacroConfig::CreateFixedMacro(std::array<Macro, kZxxMacros> fixedMacros, FixedMacro macroType) const
 {
-	for(uint32 i = 0; i < 128; i++)
+	for(uint32 i = 0; i < kZxxMacros; i++)
 	{
 		uint32 param = i;
 		switch(macroType)
 		{
 		case kZxxUnused:
-			mpt::String::WriteAutoBuf(fixedMacros[i]) = "";
+			fixedMacros[i] = "";
 			break;
 		case kZxxReso4Bit:
 			param = i * 8;
 			if(i < 16)
-				mpt::String::WriteAutoBuf(fixedMacros[i]) = MPT_AFORMAT("F0F001{}")(mpt::afmt::HEX0<2>(param));
+				fixedMacros[i] = MPT_AFORMAT("F0F001{}")(mpt::afmt::HEX0<2>(param));
 			else
-				mpt::String::WriteAutoBuf(fixedMacros[i]) = "";
+				fixedMacros[i] = "";
 			break;
 		case kZxxReso7Bit:
-			mpt::String::WriteAutoBuf(fixedMacros[i]) = MPT_AFORMAT("F0F001{}")(mpt::afmt::HEX0<2>(param));
+			fixedMacros[i] = MPT_AFORMAT("F0F001{}")(mpt::afmt::HEX0<2>(param));
 			break;
 		case kZxxCutoff:
-			mpt::String::WriteAutoBuf(fixedMacros[i]) = MPT_AFORMAT("F0F000{}")(mpt::afmt::HEX0<2>(param));
+			fixedMacros[i] = MPT_AFORMAT("F0F000{}")(mpt::afmt::HEX0<2>(param));
 			break;
 		case kZxxFltMode:
-			mpt::String::WriteAutoBuf(fixedMacros[i]) = MPT_AFORMAT("F0F002{}")(mpt::afmt::HEX0<2>(param));
+			fixedMacros[i] = MPT_AFORMAT("F0F002{}")(mpt::afmt::HEX0<2>(param));
 			break;
 		case kZxxResoFltMode:
 			param = (i & 0x0F) * 8;
 			if(i < 16)
-				mpt::String::WriteAutoBuf(fixedMacros[i]) = MPT_AFORMAT("F0F001{}")(mpt::afmt::HEX0<2>(param));
+				fixedMacros[i] = MPT_AFORMAT("F0F001{}")(mpt::afmt::HEX0<2>(param));
 			else if(i < 32)
-				mpt::String::WriteAutoBuf(fixedMacros[i]) = MPT_AFORMAT("F0F002{}")(mpt::afmt::HEX0<2>(param));
+				fixedMacros[i] = MPT_AFORMAT("F0F002{}")(mpt::afmt::HEX0<2>(param));
 			else
-				mpt::String::WriteAutoBuf(fixedMacros[i]) = "";
+				fixedMacros[i] = "";
 			break;
 		case kZxxChannelAT:
-			mpt::String::WriteAutoBuf(fixedMacros[i]) = MPT_AFORMAT("Dc{}")(mpt::afmt::HEX0<2>(param));
+			fixedMacros[i] = MPT_AFORMAT("Dc{}")(mpt::afmt::HEX0<2>(param));
 			break;
 		case kZxxPolyAT:
-			mpt::String::WriteAutoBuf(fixedMacros[i]) = MPT_AFORMAT("Acn{}")(mpt::afmt::HEX0<2>(param));
+			fixedMacros[i] = MPT_AFORMAT("Acn{}")(mpt::afmt::HEX0<2>(param));
 			break;
 		case kZxxPitch:
-			mpt::String::WriteAutoBuf(fixedMacros[i]) = MPT_AFORMAT("Ec00{}")(mpt::afmt::HEX0<2>(param));
+			fixedMacros[i] = MPT_AFORMAT("Ec00{}")(mpt::afmt::HEX0<2>(param));
 			break;
 		case kZxxProgChange:
-			mpt::String::WriteAutoBuf(fixedMacros[i]) = MPT_AFORMAT("Cc{}")(mpt::afmt::HEX0<2>(param));
+			fixedMacros[i] = MPT_AFORMAT("Cc{}")(mpt::afmt::HEX0<2>(param));
 			break;
 		case kZxxCustom:
 		default:
@@ -167,7 +166,7 @@
 {
 	for(auto left = begin(), right = other.begin(); left != end(); left++, right++)
 	{
-		if(strncmp(*left, *right, MACRO_LENGTH))
+		if(*left != *right)
 			return false;
 	}
 	return true;
@@ -262,7 +261,7 @@
 
 int MIDIMacroConfig::MacroToPlugParam(uint32 macroIndex) const
 {
-	const std::string macro = GetSafeMacro(szMidiSFXExt[macroIndex]);
+	const std::string macro = SFx[macroIndex].NormalizedString();
 
 	int code = 0;
 	const char *param = macro.c_str();
@@ -281,7 +280,7 @@
 
 int MIDIMacroConfig::MacroToMidiCC(uint32 macroIndex) const
 {
-	const std::string macro = GetSafeMacro(szMidiSFXExt[macroIndex]);
+	const std::string macro = SFx[macroIndex].NormalizedString();
 
 	int code = 0;
 	const char *param = macro.c_str();
@@ -297,7 +296,7 @@
 
 int MIDIMacroConfig::FindMacroForParam(PlugParamIndex param) const
 {
-	for(int macroIndex = 0; macroIndex < NUM_MACROS; macroIndex++)
+	for(int macroIndex = 0; macroIndex < kSFxMacros; macroIndex++)
 	{
 		if(GetParameteredMacroType(macroIndex) == kSFxPlugParam && MacroToPlugParam(macroIndex) == param)
 		{
@@ -314,26 +313,7 @@
 // i.e. the configuration that is assumed when loading a file that has no macros embedded.
 bool MIDIMacroConfig::IsMacroDefaultSetupUsed() const
 {
-	const MIDIMacroConfig defaultConfig;
-
-	// TODO - Global macros (currently not checked because they are not editable)
-
-	// SF0: Z00-Z7F controls cutoff, all other parametered macros are unused
-	for(uint32 i = 0; i < NUM_MACROS; i++)
-	{
-		if(GetParameteredMacroType(i) != defaultConfig.GetParameteredMacroType(i))
-		{
-			return false;
-		}
-	}
-
-	// Z80-Z8F controls resonance
-	if(GetFixedMacroType() != defaultConfig.GetFixedMacroType())
-	{
-		return false;
-	}
-
-	return true;
+	return *this == MIDIMacroConfig{};
 }
 
 
@@ -340,15 +320,13 @@
 // Reset MIDI macro config to default values.
 void MIDIMacroConfig::Reset()
 {
-	MemsetZero(szMidiGlb);
-	MemsetZero(szMidiSFXExt);
-	MemsetZero(szMidiZXXExt);
+	std::fill(begin(), end(), Macro{});
 
-	strcpy(szMidiGlb[MIDIOUT_START], "FF");
-	strcpy(szMidiGlb[MIDIOUT_STOP], "FC");
-	strcpy(szMidiGlb[MIDIOUT_NOTEON], "9c n v");
-	strcpy(szMidiGlb[MIDIOUT_NOTEOFF], "9c n 0");
-	strcpy(szMidiGlb[MIDIOUT_PROGRAM], "Cc p");
+	Global[MIDIOUT_START] = "FF";
+	Global[MIDIOUT_STOP] = "FC";
+	Global[MIDIOUT_NOTEON] = "9c n v";
+	Global[MIDIOUT_NOTEOFF] = "9c n 0";
+	Global[MIDIOUT_PROGRAM] = "Cc p";
 	// SF0: Z00-Z7F controls cutoff
 	CreateParameteredMacro(0, kSFxCutoff);
 	// Z80-Z8F controls resonance
@@ -359,8 +337,8 @@
 // Clear all Zxx macros so that they do nothing.
 void MIDIMacroConfig::ClearZxxMacros()
 {
-	MemsetZero(szMidiSFXExt);
-	MemsetZero(szMidiZXXExt);
+	std::fill(SFx.begin(), SFx.end(), Macro{});
+	std::fill(Zxx.begin(), Zxx.end(), Macro{});
 }
 
 
@@ -369,50 +347,25 @@
 {
 	for(auto &macro : *this)
 	{
-		macro[MACRO_LENGTH - 1] = '\0';
-		std::fill(std::find(std::begin(macro), std::end(macro), '\0'), std::end(macro), '\0');
-		for(auto &c : macro)
-		{
-			if(c && (c < 32 || c >= 127))
-				c = ' ';
-		}
+		macro.Sanitize();
 	}
 }
 
 
-// Helper function for UpgradeMacros()
-void MIDIMacroConfig::UpgradeMacroString(Macro &macro) const
-{
-	for(auto &c : macro)
-	{
-		if(c >= 'a' && c <= 'f') // Both A-F and a-f were treated as hex constants
-		{
-			c = c - 'a' + 'A';
-		} else if(c == 'K' || c == 'k') // Channel was K or k
-		{
-			c = 'c';
-		} else if(c == 'X' || c == 'x' || c == 'Y' || c == 'y') // Those were pointless
-		{
-			c = 'z';
-		}
-	}
-}
-
-
 // Fix old-format (not conforming to IT's MIDI macro definitions) MIDI config strings.
 void MIDIMacroConfig::UpgradeMacros()
 {
 	for(auto &macro : *this)
 	{
-		UpgradeMacroString(macro);
+		macro.UpgradeLegacyMacro();
 	}
 }
 
 
 // Normalize by removing blanks and other unwanted characters from macro strings for internal usage.
-std::string MIDIMacroConfig::GetSafeMacro(const Macro &macro) const
+std::string MIDIMacroConfig::Macro::NormalizedString() const
 {
-	std::string sanitizedMacro = macro;
+	std::string sanitizedMacro = *this;
 
 	std::string::size_type pos;
 	while((pos = sanitizedMacro.find_first_not_of("0123456789ABCDEFabchmnopsuvxyz")) != std::string::npos)
@@ -424,4 +377,35 @@
 }
 
 
+void MIDIMacroConfig::Macro::Sanitize() noexcept
+{
+	m_data.back() = '\0';
+	const auto length = Length();
+	std::fill(m_data.begin() + length, m_data.end(), '\0');
+	for(size_t i = 0; i < length; i++)
+	{
+		if(m_data[i] < 32 || m_data[i] >= 127)
+			m_data[i] = ' ';
+	}
+}
+
+
+void MIDIMacroConfig::Macro::UpgradeLegacyMacro() noexcept
+{
+	for(auto &c : m_data)
+	{
+		if(c >= 'a' && c <= 'f')  // Both A-F and a-f were treated as hex constants
+		{
+			c = c - 'a' + 'A';
+		} else if(c == 'K' || c == 'k')  // Channel was K or k
+		{
+			c = 'c';
+		} else if(c == 'X' || c == 'x' || c == 'Y' || c == 'y')  // Those were pointless
+		{
+			c = 'z';
+		}
+	}
+}
+
+
 OPENMPT_NAMESPACE_END
Index: soundlib/MIDIMacros.h
===================================================================
--- soundlib/MIDIMacros.h	(revision 16701)
+++ soundlib/MIDIMacros.h	(working copy)
@@ -18,8 +18,10 @@
 
 enum
 {
-	NUM_MACROS = 16,	// number of parametered macros
-	MACRO_LENGTH = 32,	// max number of chars per macro
+	kGlobalMacros = 9,    // Number of global macros
+	kSFxMacros    = 16,   // Number of parametered macros
+	kZxxMacros    = 128,  // Number of fixed macros
+	kMacroLength  = 32,   // Max number of chars per macro
 };
 
 OPENMPT_NAMESPACE_END
@@ -70,7 +72,7 @@
 
 
 // Global macro types
-enum
+enum GlobalMacro
 {
 	MIDIOUT_START = 0,
 	MIDIOUT_STOP,
@@ -86,19 +88,80 @@
 
 struct MIDIMacroConfigData
 {
-	typedef char Macro[MACRO_LENGTH];
-	// encoding is ASCII
-	Macro szMidiGlb[9];      // Global MIDI macros
-	Macro szMidiSFXExt[16];  // Parametric MIDI macros
-	Macro szMidiZXXExt[128]; // Fixed MIDI macros
+	struct Macro
+	{
+	public:
+		using RawType = std::array<char, kMacroLength>;
 
-	Macro *begin() { return std::begin(szMidiGlb); }
-	const Macro *begin() const { return std::begin(szMidiGlb); }
-	Macro *end() { return std::end(szMidiZXXExt); }
-	const Macro *end() const { return std::end(szMidiZXXExt); }
+		Macro &operator=(const Macro &other) = default;
+		Macro &operator=(const RawType &other) noexcept
+		{
+			return (*this = std::string_view{other.data(), other.size()});
+		}
+		Macro &operator=(const std::string_view &other) noexcept
+		{
+			const size_t copyLength = std::min({m_data.size() - 1u, other.size(), other.find('\0')});
+			std::copy(other.begin(), other.begin() + copyLength, m_data.begin());
+			m_data[copyLength] = '\0';
+			Sanitize();
+			return *this;
+		}
+
+		bool operator==(const Macro &other) const noexcept
+		{
+			return m_data == other.m_data;  // Don't care about data past null-terminator as operator= and Sanitize() ensure there is no data behind it.
+		}
+		bool operator!=(const Macro &other) const noexcept
+		{
+			return !(*this == other);
+		}
+
+		mpt::span<const char> Span() const noexcept
+		{
+			return {m_data.data(), Length()};
+		}
+		operator std::string_view() const noexcept
+		{
+			return {m_data.data(), Length()};
+		}
+		operator std::string() const
+		{
+			return {m_data.data(), Length()};
+		}
+
+		MPT_CONSTEXPR20_FUN size_t Length() const noexcept
+		{
+			return static_cast<size_t>(std::distance(m_data.begin(), std::find(m_data.begin(), m_data.end(), '\0')));
+		}
+
+		MPT_CONSTEXPR20_FUN void Clear() noexcept
+		{
+			m_data.fill('\0');
+		}
+
+		// Remove blanks and other unwanted characters from macro strings for internal usage.
+		std::string NormalizedString() const;
+
+		void Sanitize() noexcept;
+		void UpgradeLegacyMacro() noexcept;
+
+	private:
+		RawType m_data;
+	};
+
+	std::array<Macro, kGlobalMacros> Global;
+	std::array<Macro, kSFxMacros> SFx;
+	std::array<Macro, kZxxMacros> Zxx;
+
+	constexpr Macro *begin() noexcept {return Global.data(); }
+	constexpr const Macro *begin() const noexcept { return Global.data(); }
+	constexpr Macro *end() noexcept { return Zxx.data() + Zxx.size(); }
+	constexpr const Macro *end() const noexcept { return Zxx.data() + Zxx.size(); }
 };
 
-MPT_BINARY_STRUCT(MIDIMacroConfigData, 4896) // this is directly written to files, so the size must be correct!
+// This is directly written to files, so the size must be correct!
+MPT_BINARY_STRUCT(MIDIMacroConfigData::Macro, 32)
+MPT_BINARY_STRUCT(MIDIMacroConfigData, 4896)
 
 class MIDIMacroConfig : public MIDIMacroConfigData
 {
@@ -117,16 +180,17 @@
 public:
 	void CreateParameteredMacro(uint32 macroIndex, ParameteredMacro macroType, int subType = 0)
 	{
-		CreateParameteredMacro(szMidiSFXExt[macroIndex], macroType, subType);
+		if(macroIndex < std::size(SFx))
+			CreateParameteredMacro(SFx[macroIndex], macroType, subType);
 	}
 	std::string CreateParameteredMacro(ParameteredMacro macroType, int subType = 0) const;
 
 protected:
-	void CreateFixedMacro(Macro (&fixedMacros)[128], FixedMacro macroType) const;
+	void CreateFixedMacro(std::array<Macro, kZxxMacros> fixedMacros, FixedMacro macroType) const;
 public:
 	void CreateFixedMacro(FixedMacro macroType)
 	{
-		CreateFixedMacro(szMidiZXXExt, macroType);
+		CreateFixedMacro(Zxx, macroType);
 	}
 
 #ifdef MODPLUG_TRACKER
@@ -162,15 +226,6 @@
 
 	// Fix old-format (not conforming to IT's MIDI macro definitions) MIDI config strings.
 	void UpgradeMacros();
-
-protected:
-
-	// Helper function for FixMacroFormat()
-	void UpgradeMacroString(Macro &macro) const;
-
-	// Remove blanks and other unwanted characters from macro strings for internal usage.
-	std::string GetSafeMacro(const Macro &macro) const;
-
 };
 
 static_assert(sizeof(MIDIMacroConfig) == sizeof(MIDIMacroConfigData)); // this is directly written to files, so the size must be correct!
Index: soundlib/ModInstrument.cpp
===================================================================
--- soundlib/ModInstrument.cpp	(revision 16701)
+++ soundlib/ModInstrument.cpp	(working copy)
@@ -314,13 +314,9 @@
 }
 
 
-uint8 ModInstrument::GetMIDIChannel(const CSoundFile &sndFile, CHANNELINDEX chn) const
+uint8 ModInstrument::GetMIDIChannel(const ModChannel &channel, CHANNELINDEX chn) const
 {
-	if(chn >= std::size(sndFile.m_PlayState.Chn))
-		return 0;
-
 	// For mapped channels, return their pattern channel, modulo 16 (because there are only 16 MIDI channels)
-	const ModChannel &channel = sndFile.m_PlayState.Chn[chn];
 	if(nMidiChannel == MidiMappedChannel)
 		return static_cast<uint8>((channel.nMasterChn ? (channel.nMasterChn - 1u) : chn) % 16u);
 	else if(HasValidMIDIChannel())
Index: soundlib/ModInstrument.h
===================================================================
--- soundlib/ModInstrument.h	(revision 16701)
+++ soundlib/ModInstrument.h	(working copy)
@@ -21,7 +21,7 @@
 
 OPENMPT_NAMESPACE_BEGIN
 
-class CSoundFile;
+struct ModChannel;
 
 // Instrument Nodes
 struct EnvelopeNode
@@ -150,7 +150,7 @@
 	void SetResonance(uint8 resonance, bool enable) { nIFR = std::min(resonance, uint8(0x7F)) | (enable ? 0x80 : 0x00); }
 
 	bool HasValidMIDIChannel() const { return (nMidiChannel >= 1 && nMidiChannel <= 17); }
-	uint8 GetMIDIChannel(const CSoundFile &sndFile, CHANNELINDEX chn) const;
+	uint8 GetMIDIChannel(const ModChannel &channel, CHANNELINDEX chn) const;
 
 	void SetTuning(CTuning *pT)
 	{
Index: soundlib/plugins/PluginStructs.h
===================================================================
--- soundlib/plugins/PluginStructs.h	(revision 16701)
+++ soundlib/plugins/PluginStructs.h	(working copy)
@@ -22,8 +22,8 @@
 ////////////////////////////////////////////////////////////////////
 // Mix Plugins
 
-typedef int32 PlugParamIndex;
-typedef float PlugParamValue;
+using PlugParamIndex = int32;
+using PlugParamValue = float;
 
 struct SNDMIXPLUGINSTATE;
 struct SNDMIXPLUGIN;
Index: soundlib/plugins/PlugInterface.cpp
===================================================================
--- soundlib/plugins/PlugInterface.cpp	(revision 16701)
+++ soundlib/plugins/PlugInterface.cpp	(working copy)
@@ -775,13 +775,19 @@
 
 
 // Get the MIDI channel currently associated with a given tracker channel
-uint8 IMidiPlugin::GetMidiChannel(CHANNELINDEX trackChannel) const
+uint8 IMidiPlugin::GetMidiChannel(const ModChannel &chn, CHANNELINDEX trackChannel) const
 {
-	if(trackChannel >= std::size(m_SndFile.m_PlayState.Chn))
+	if(auto ins = chn.pModInstrument; ins != nullptr)
+		return ins->GetMIDIChannel(chn, trackChannel);
+	else
 		return 0;
+}
 
-	if(auto ins = m_SndFile.m_PlayState.Chn[trackChannel].pModInstrument; ins != nullptr)
-		return ins->GetMIDIChannel(m_SndFile, trackChannel);
+
+uint8 IMidiPlugin::GetMidiChannel(CHANNELINDEX trackChannel) const
+{
+	if(trackChannel < std::size(m_SndFile.m_PlayState.Chn))
+		return GetMidiChannel(m_SndFile.m_PlayState.Chn[trackChannel], trackChannel);
 	else
 		return 0;
 }
@@ -884,7 +890,7 @@
 		uint8 high = static_cast<uint8>(midiBank >> 7);
 		uint8 low = static_cast<uint8>(midiBank & 0x7F);
 
-		//m_SndFile.ProcessMIDIMacro(trackChannel, false, m_SndFile.m_MidiCfg.szMidiGlb[MIDIOUT_BANKSEL], 0, m_nSlot + 1);
+		//m_SndFile.ProcessMIDIMacro(trackChannel, false, m_SndFile.m_MidiCfg.Global[MIDIOUT_BANKSEL], 0, m_nSlot + 1);
 		MidiSend(MIDIEvents::CC(MIDIEvents::MIDICC_BankSelect_Coarse, midiCh, high));
 		MidiSend(MIDIEvents::CC(MIDIEvents::MIDICC_BankSelect_Fine, midiCh, low));
 
@@ -897,7 +903,7 @@
 	if(progChanged || (midiProg < 0x80 && bankChanged))
 	{
 		channel.currentProgram = midiProg;
-		//m_SndFile.ProcessMIDIMacro(trackChannel, false, m_SndFile.m_MidiCfg.szMidiGlb[MIDIOUT_PROGRAM], 0, m_nSlot + 1);
+		//m_SndFile.ProcessMIDIMacro(trackChannel, false, m_SndFile.m_MidiCfg.Global[MIDIOUT_PROGRAM], 0, m_nSlot + 1);
 		MidiSend(MIDIEvents::ProgramChange(midiCh, midiProg));
 	}
 
Index: soundlib/plugins/PlugInterface.h
===================================================================
--- soundlib/plugins/PlugInterface.h	(revision 16701)
+++ soundlib/plugins/PlugInterface.h	(working copy)
@@ -25,6 +25,7 @@
 struct VSTPluginLib;
 struct SNDMIXPLUGIN;
 struct ModInstrument;
+struct ModChannel;
 class CSoundFile;
 class CModDoc;
 class CAbstractVstEditor;
@@ -275,9 +276,11 @@
 	bool IsNotePlaying(uint8 note, CHANNELINDEX trackerChn) override;
 
 	// Get the MIDI channel currently associated with a given tracker channel
-	virtual uint8 GetMidiChannel(CHANNELINDEX trackChannel) const;
+	virtual uint8 GetMidiChannel(const ModChannel &chn, CHANNELINDEX trackChannel) const;
 
 protected:
+	uint8 GetMidiChannel(CHANNELINDEX trackChannel) const;
+
 	// Plugin wants to send MIDI to OpenMPT
 	virtual void ReceiveMidi(uint32 midiCode);
 	virtual void ReceiveSysex(mpt::const_byte_span sysex);
Index: soundlib/Snd_fx.cpp
===================================================================
--- soundlib/Snd_fx.cpp	(revision 16701)
+++ soundlib/Snd_fx.cpp	(working copy)
@@ -64,10 +64,6 @@
 		uint8 vol = 0xFF;
 	};
 
-#ifndef NO_PLUGINS
-	typedef std::map<std::pair<ModCommand::INSTR, uint16>, uint16> PlugParamMap;
-	PlugParamMap plugParams;
-#endif
 	std::vector<ChnSettings> chnSettings;
 	double elapsedTime;
 	static constexpr uint32 IGNORE_CHANNEL = uint32_max;
@@ -81,9 +77,8 @@
 
 	void Reset()
 	{
-#ifndef NO_PLUGINS
-		plugParams.clear();
-#endif
+		if(state->m_midiMacroEvaluationResults)
+			state->m_midiMacroEvaluationResults.emplace();
 		elapsedTime = 0.0;
 		state->m_lTotalSampleCount = 0;
 		state->m_nMusicSpeed = sndFile.m_nDefaultSpeed;
@@ -295,6 +290,9 @@
 		}
 	}
 
+	if(adjustMode & eAdjust)
+		playState.m_midiMacroEvaluationResults.emplace();
+
 	// If samples are being synced, force them to resync if tick duration changes
 	uint32 oldTickDuration = 0;
 	bool breakToRow = false;
@@ -469,9 +467,9 @@
 			if(p->IsPcNote())
 			{
 #ifndef NO_PLUGINS
-				if((adjustMode & eAdjust) && p->instr > 0 && p->instr <= MAX_MIXPLUGINS)
+				if(playState.m_midiMacroEvaluationResults && p->instr > 0 && p->instr <= MAX_MIXPLUGINS)
 				{
-					memory.plugParams[std::make_pair(p->instr, p->GetValueVolCol())] = p->GetValueEffectCol();
+					playState.m_midiMacroEvaluationResults->pluginParameter[{static_cast<PLUGINDEX>(p->instr - 1), p->GetValueVolCol()}] = p->GetValueEffectCol() / PlugParamValue(ModCommand::maxColumnValue);
 				}
 #endif // NO_PLUGINS
 				chn.rowCommand.Clear();
@@ -828,6 +826,13 @@
 			case CMD_PANBRELLO:
 				Panbrello(chn, param);
 				break;
+
+			case CMD_MIDI:
+			case CMD_SMOOTHMIDI:
+				if(param < 0x80)
+					ProcessMIDIMacro(playState, nChn, false, m_MidiCfg.SFx[chn.nActiveMacro], chn.rowCommand.param, 0);
+				else
+					ProcessMIDIMacro(playState, nChn, false, m_MidiCfg.Zxx[param & 0x7F], chn.rowCommand.param, 0);
 			default:
 				break;
 			}
@@ -1186,6 +1191,8 @@
 	{
 		if(retval.targetReached || target.mode == GetLengthTarget::NoTarget)
 		{
+			const auto midiMacroEvaluationResults = std::move(playState.m_midiMacroEvaluationResults);
+			playState.m_midiMacroEvaluationResults.reset();
 			// Target found, or there is no target (i.e. play whole song)...
 			m_PlayState = std::move(playState);
 			m_PlayState.ResetGlobalVolumeRamping();
@@ -1215,11 +1222,11 @@
 			}
 
 #ifndef NO_PLUGINS
-			// If there were any PC events, update plugin parameters to their latest value.
+			// If there were any PC events or MIDI macros updating plugin parameters, update plugin parameters to their latest value.
 			std::bitset<MAX_MIXPLUGINS> plugSetProgram;
-			for(const auto &param : memory.plugParams)
+			for(const auto [plugParam, value] : midiMacroEvaluationResults->pluginParameter)
 			{
-				PLUGINDEX plug = param.first.first - 1;
+				PLUGINDEX plug = plugParam.first;
 				IMixPlugin *plugin = m_MixPlugins[plug].pMixPlugin;
 				if(plugin != nullptr)
 				{
@@ -1229,7 +1236,7 @@
 						plugSetProgram.set(plug);
 						plugin->BeginSetProgram();
 					}
-					plugin->SetParameter(param.first.second, param.second / PlugParamValue(ModCommand::maxColumnValue));
+					plugin->SetParameter(plugParam.second, value);
 				}
 			}
 			if(plugSetProgram.any())
@@ -1242,6 +1249,11 @@
 					}
 				}
 			}
+			// Do the same for dry/wet ratios
+			for(const auto [plug, dryWetRatio] : midiMacroEvaluationResults->pluginDryWetRatio)
+			{
+				m_MixPlugins[plug].fDryRatio = dryWetRatio;
+			}
 #endif // NO_PLUGINS
 		} else if(adjustMode != eAdjustOnSuccess)
 		{
@@ -2268,7 +2280,7 @@
 	IMixPlugin *pPlugin = nullptr;
 	if(srcChn.HasMIDIOutput() && ModCommand::IsNote(srcChn.nNote)) // instro sends to a midi chan
 	{
-		PLUGINDEX plugin = GetBestPlugin(nChn, PrioritiseInstrument, RespectMutes);
+		PLUGINDEX plugin = GetBestPlugin(m_PlayState, nChn, PrioritiseInstrument, RespectMutes);
 
 		if(plugin > 0 && plugin <= MAX_MIXPLUGINS)
 		{
@@ -3371,7 +3383,7 @@
 			{
 				SetFinetune(nChn, m_PlayState, cmd == CMD_FINETUNE_SMOOTH);
 #ifndef NO_PLUGINS
-				if(IMixPlugin *plugin = GetChannelInstrumentPlugin(nChn); plugin != nullptr)
+				if(IMixPlugin *plugin = GetChannelInstrumentPlugin(m_PlayState.Chn[nChn]); plugin != nullptr)
 					plugin->MidiPitchBendRaw(chn.GetMIDIPitchBend(), nChn);
 #endif  // NO_PLUGINS
 			}
@@ -3860,7 +3872,7 @@
 	if(pitchBend)
 	{
 #ifndef NO_PLUGINS
-		IMixPlugin *plugin = GetChannelInstrumentPlugin(nChn);
+		IMixPlugin *plugin = GetChannelInstrumentPlugin(m_PlayState.Chn[nChn]);
 		if(plugin != nullptr)
 		{
 			int8 pwd = 13;	// Early OpenMPT legacy... Actually it's not *exactly* 13, but close enough...
@@ -4811,23 +4823,96 @@
 
 // Process a MIDI Macro.
 // Parameters:
+// playState: The playback state to operate on.
 // nChn: Mod channel to apply macro on
 // isSmooth: If true, internal macros are interpolated between two rows
-// macro: Actual MIDI Macro string
-// param: Parameter for parametric macros (Z00 - Z7F)
+// macro: MIDI Macro string to process
+// param: Parameter for parametric macros (Zxx / \xx parameter)
 // plugin: Plugin to send MIDI message to (if not specified but needed, it is autodetected)
-void CSoundFile::ProcessMIDIMacro(CHANNELINDEX nChn, bool isSmooth, const char *macro, uint8 param, PLUGINDEX plugin)
+void CSoundFile::ProcessMIDIMacro(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const MIDIMacroConfigData::Macro &macro, uint8 param, PLUGINDEX plugin)
 {
-	ModChannel &chn = m_PlayState.Chn[nChn];
-	const ModInstrument *pIns = GetNumInstruments() ? chn.pModInstrument : nullptr;
+	playState.m_midiMacroScratchSpace.resize(macro.Length() + 1);
+	auto out = mpt::as_span(playState.m_midiMacroScratchSpace);
 
-	uint8 out[MACRO_LENGTH];
-	uint32 outPos = 0;  // output buffer position, which also equals the number of complete bytes
+	ParseMIDIMacro(playState, nChn, isSmooth, macro.Span(), out, param, plugin);
+
+	// Macro string has been parsed and translated, now send the message(s)...
+	uint32 outSize = static_cast<uint32>(out.size());
+	uint32 sendPos = 0;
+	uint8 runningStatus = 0;
+	while(sendPos < out.size())
+	{
+		uint32 sendLen = 0;
+		if(out[sendPos] == 0xF0)
+		{
+			// SysEx start
+			if((outSize - sendPos >= 4) && (out[sendPos + 1] == 0xF0 || out[sendPos + 1] == 0xF1))
+			{
+				// Internal macro (normal (F0F0) or extended (F0F1)), 4 bytes long
+				sendLen = 4;
+			} else
+			{
+				// SysEx message, find end of message
+				for(uint32 i = sendPos + 1; i < outSize; i++)
+				{
+					if(out[i] == 0xF7)
+					{
+						// Found end of SysEx message
+						sendLen = i - sendPos + 1;
+						break;
+					}
+				}
+				if(sendLen == 0)
+				{
+					// Didn't find end, so "invent" end of SysEx message
+					out[outSize++] = 0xF7;
+					sendLen = outSize - sendPos;
+				}
+			}
+		} else if(!(out[sendPos] & 0x80))
+		{
+			// Missing status byte? Try inserting running status
+			if(runningStatus != 0)
+			{
+				sendPos--;
+				out[sendPos] = runningStatus;
+			} else
+			{
+				// No running status to re-use; skip this byte
+				sendPos++;
+			}
+			continue;
+		} else
+		{
+			// Other MIDI messages
+			sendLen = std::min(static_cast<uint32>(MIDIEvents::GetEventLength(out[sendPos])), outSize - sendPos);
+		}
+
+		if(sendLen == 0)
+			break;
+
+		if(out[sendPos] < 0xF0)
+		{
+			runningStatus = out[sendPos];
+		}
+		const auto midiMsg = out.subspan(sendPos, sendLen);
+		SendMIDIData(playState, nChn, isSmooth, midiMsg, plugin);
+		sendPos += sendLen;
+	}
+}
+
+
+void CSoundFile::ParseMIDIMacro(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const mpt::span<const char> macro, mpt::span<uint8> &out, uint8 param, PLUGINDEX plugin) const
+{
+	ModChannel &chn = playState.Chn[nChn];
+	const ModInstrument *pIns = chn.pModInstrument;
+
 	const uint8 lastZxxParam = chn.lastZxxParam;  // always interpolate based on original value in case z appears multiple times in macro string
 	uint8 updateZxxParam = 0xFF;                  // avoid updating lastZxxParam immediately if macro contains both internal and external MIDI message
+
 	bool firstNibble = true;
-
-	for(uint32 pos = 0; pos < (MACRO_LENGTH - 1) && macro[pos]; pos++)
+	size_t outPos = 0;  // output buffer position, which also equals the number of complete bytes
+	for(size_t pos = 0; pos < macro.size() && outPos < out.size(); pos++)
 	{
 		bool isNibble = false;  // did we parse a nibble or a byte value?
 		uint8 data = 0;         // data that has just been parsed
@@ -4837,8 +4922,7 @@
 		{
 			isNibble = true;
 			data = static_cast<uint8>(macro[pos] - '0');
-		}
-		else if(macro[pos] >= 'A' && macro[pos] <= 'F')
+		} else if(macro[pos] >= 'A' && macro[pos] <= 'F')
 		{
 			isNibble = true;
 			data = static_cast<uint8>(macro[pos] - 'A' + 0x0A);
@@ -4848,12 +4932,12 @@
 			isNibble = true;
 			data = 0xFF;
 #ifndef NO_PLUGINS
-			const PLUGINDEX plug = (plugin != 0) ? plugin : GetBestPlugin(nChn, PrioritiseChannel, EvenIfMuted);
+			const PLUGINDEX plug = (plugin != 0) ? plugin : GetBestPlugin(playState, nChn, PrioritiseChannel, EvenIfMuted);
 			if(plug > 0 && plug <= MAX_MIXPLUGINS)
 			{
 				auto midiPlug = dynamic_cast<const IMidiPlugin *>(m_MixPlugins[plug - 1u].pMixPlugin);
 				if(midiPlug)
-					data = midiPlug->GetMidiChannel(nChn);
+					data = midiPlug->GetMidiChannel(playState.Chn[nChn], nChn);
 			}
 #endif // NO_PLUGINS
 			if(data == 0xFF)
@@ -4860,7 +4944,7 @@
 			{
 				// Fallback if no plugin was found
 				if(pIns)
-					data = pIns->GetMIDIChannel(*this, nChn);
+					data = pIns->GetMIDIChannel(playState.Chn[nChn], nChn);
 				else
 					data = 0;
 			}
@@ -4936,7 +5020,7 @@
 			{
 				// Interpolation for external MIDI messages - interpolation for internal messages
 				// is handled separately to allow for more than 7-bit granularity where it's possible
-				data = static_cast<uint8>(CalculateSmoothParamChange(lastZxxParam, data));
+				data = static_cast<uint8>(CalculateSmoothParamChange(playState, lastZxxParam, data));
 				chn.lastZxxParam = data;
 				updateZxxParam = 0x80;
 			} else if(updateZxxParam == 0xFF)
@@ -4946,13 +5030,13 @@
 		} else if(macro[pos] == 's')
 		{
 			// SysEx Checksum (not an original Impulse Tracker macro variable, but added for convenience)
-			uint32 startPos = outPos;
+			auto startPos = outPos;
 			while(startPos > 0 && out[--startPos] != 0xF0);
 			if(outPos - startPos < 5 || out[startPos] != 0xF0)
 			{
 				continue;
 			}
-			for(uint32 p = startPos + 5; p != outPos; p++)
+			for(auto p = startPos + 5u; p != outPos; p++)
 			{
 				data += out[p];
 			}
@@ -4977,7 +5061,7 @@
 			firstNibble = !firstNibble;
 		} else  // parsed a byte (variable)
 		{
-			if(!firstNibble)	// From MIDI.TXT: '9n' is exactly the same as '09 n' or '9 n' -- so finish current byte first
+			if(!firstNibble)  // From MIDI.TXT: '9n' is exactly the same as '09 n' or '9 n' -- so finish current byte first
 			{
 				outPos++;
 			}
@@ -4993,83 +5077,19 @@
 	if(updateZxxParam < 0x80)
 		chn.lastZxxParam = updateZxxParam;
 
-	// Macro string has been parsed and translated, now send the message(s)...
-	uint32 sendPos = 0;
-	uint8 runningStatus = 0;
-	while(sendPos < outPos)
-	{
-		uint32 sendLen = 0;
-		if(out[sendPos] == 0xF0)
-		{
-			// SysEx start
-			if((outPos - sendPos >= 4) && (out[sendPos + 1] == 0xF0 || out[sendPos + 1] == 0xF1))
-			{
-				// Internal macro (normal (F0F0) or extended (F0F1)), 4 bytes long
-				sendLen = 4;
-			} else
-			{
-				// SysEx message, find end of message
-				for(uint32 i = sendPos + 1; i < outPos; i++)
-				{
-					if(out[i] == 0xF7)
-					{
-						// Found end of SysEx message
-						sendLen = i - sendPos + 1;
-						break;
-					}
-				}
-				if(sendLen == 0)
-				{
-					// Didn't find end, so "invent" end of SysEx message
-					out[outPos++] = 0xF7;
-					sendLen = outPos - sendPos;
-				}
-			}
-		} else if(!(out[sendPos] & 0x80))
-		{
-			// Missing status byte? Try inserting running status
-			if(runningStatus != 0)
-			{
-				sendPos--;
-				out[sendPos] = runningStatus;
-			} else
-			{
-				// No running status to re-use; skip this byte
-				sendPos++;
-			}
-			continue;
-		} else
-		{
-			// Other MIDI messages
-			sendLen = std::min(static_cast<uint32>(MIDIEvents::GetEventLength(out[sendPos])), outPos - sendPos);
-		}
-
-		if(sendLen == 0)
-			break;
-
-		if(out[sendPos] < 0xF0)
-		{
-			runningStatus = out[sendPos];
-		}
-		uint32 bytesSent = SendMIDIData(nChn, isSmooth, out + sendPos, sendLen, plugin);
-		// If there's no error in the macro data (e.g. unrecognized internal MIDI macro), we have sendLen == bytesSent.
-		if(bytesSent > 0)
-			sendPos += bytesSent;
-		else
-			sendPos += sendLen;
-	}
+	out = out.first(outPos);
 }
 
 
 // Calculate smooth MIDI macro slide parameter for current tick.
-float CSoundFile::CalculateSmoothParamChange(float currentValue, float param) const
+float CSoundFile::CalculateSmoothParamChange(const PlayState &playState, float currentValue, float param)
 {
-	MPT_ASSERT(m_PlayState.TicksOnRow() > m_PlayState.m_nTickCount);
-	const uint32 ticksLeft = m_PlayState.TicksOnRow() - m_PlayState.m_nTickCount;
+	MPT_ASSERT(playState.TicksOnRow() > playState.m_nTickCount);
+	const uint32 ticksLeft = playState.TicksOnRow() - playState.m_nTickCount;
 	if(ticksLeft > 1)
 	{
 		// Slide param
-		const float step = (param - currentValue) / (float)ticksLeft;
+		const float step = (param - currentValue) / static_cast<float>(ticksLeft);
 		return (currentValue + step);
 	} else
 	{
@@ -5080,31 +5100,28 @@
 
 
 // Process exactly one MIDI message parsed by ProcessMIDIMacro. Returns bytes sent on success, 0 on (parse) failure.
-uint32 CSoundFile::SendMIDIData(CHANNELINDEX nChn, bool isSmooth, const unsigned char *macro, uint32 macroLen, PLUGINDEX plugin)
+void CSoundFile::SendMIDIData(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const mpt::span<const unsigned char> macro, PLUGINDEX plugin)
 {
-	if(macroLen < 1)
-	{
-		return 0;
-	}
+	if(macro.size() < 1)
+		return;
 
+	// Don't do anything that modifies state outside of the playState itself.
+	const bool localOnly = playState.m_midiMacroEvaluationResults.has_value();
+
 	if(macro[0] == 0xFA || macro[0] == 0xFC || macro[0] == 0xFF)
 	{
 		// Start Song, Stop Song, MIDI Reset - both interpreted internally and sent to plugins
 		for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++)
 		{
-			m_PlayState.Chn[chn].nCutOff = 0x7F;
-			m_PlayState.Chn[chn].nResonance = 0x00;
+			playState.Chn[chn].nCutOff = 0x7F;
+			playState.Chn[chn].nResonance = 0x00;
 		}
 	}
 
-	ModChannel &chn = m_PlayState.Chn[nChn];
-	if(macro[0] == 0xF0 && (macro[1] == 0xF0 || macro[1] == 0xF1))
+	ModChannel &chn = playState.Chn[nChn];
+	if(macro.size() == 4 && macro[0] == 0xF0 && (macro[1] == 0xF0 || macro[1] == 0xF1))
 	{
 		// Internal device.
-		if(macroLen < 4)
-		{
-			return 0;
-		}
 		const bool isExtended = (macro[1] == 0xF1);
 		const uint8 macroCode = macro[2];
 		const uint8 param = macro[3];
@@ -5113,36 +5130,26 @@
 		{
 			// F0.F0.00.xx: Set CutOff
 			if(!isSmooth)
-			{
 				chn.nCutOff = param;
-			} else
-			{
-				chn.nCutOff = mpt::saturate_round<uint8>(CalculateSmoothParamChange(chn.nCutOff, param));
-			}
+			else
+				chn.nCutOff = mpt::saturate_round<uint8>(CalculateSmoothParamChange(playState, chn.nCutOff, param));
 			chn.nRestoreCutoffOnNewNote = 0;
 			int cutoff = SetupChannelFilter(chn, !chn.dwFlags[CHN_FILTER]);
 
-			if(cutoff >= 0 && chn.dwFlags[CHN_ADLIB] && m_opl)
+			if(cutoff >= 0 && chn.dwFlags[CHN_ADLIB] && m_opl && !localOnly)
 			{
 				// Cutoff doubles as modulator intensity for FM instruments
 				m_opl->Volume(nChn, static_cast<uint8>(cutoff / 4), true);
 			}
-
-			return 4;
 		} else if(macroCode == 0x01 && !isExtended && param < 0x80)
 		{
 			// F0.F0.01.xx: Set Resonance
 			if(!isSmooth)
-			{
 				chn.nResonance = param;
-			} else
-			{
-				chn.nResonance = (uint8)CalculateSmoothParamChange((float)chn.nResonance, (float)param);
-			}
+			else
+				chn.nResonance = mpt::saturate_round<uint8>(CalculateSmoothParamChange(playState, chn.nResonance, param));
 			chn.nRestoreResonanceOnNewNote = 0;
 			SetupChannelFilter(chn, !chn.dwFlags[CHN_FILTER]);
-
-			return 4;
 		} else if(macroCode == 0x02 && !isExtended)
 		{
 			// F0.F0.02.xx: Set filter mode (high nibble determines filter mode)
@@ -5151,54 +5158,45 @@
 				chn.nFilterMode = static_cast<FilterMode>(param >> 4);
 				SetupChannelFilter(chn, !chn.dwFlags[CHN_FILTER]);
 			}
-
-			return 4;
 #ifndef NO_PLUGINS
 		} else if(macroCode == 0x03 && !isExtended)
 		{
 			// F0.F0.03.xx: Set plug dry/wet
-			const PLUGINDEX plug = (plugin != 0) ? plugin : GetBestPlugin(nChn, PrioritiseChannel, EvenIfMuted);
+			PLUGINDEX plug = (plugin != 0) ? plugin : GetBestPlugin(playState, nChn, PrioritiseChannel, EvenIfMuted);
 			if(plug > 0 && plug <= MAX_MIXPLUGINS && param < 0x80)
 			{
-				const float newRatio = (0x7F - (param & 0x7F)) / 127.0f;
-				if(!isSmooth)
-				{
-					m_MixPlugins[plug - 1].fDryRatio = newRatio;
-				} else
-				{
-					m_MixPlugins[plug - 1].fDryRatio = CalculateSmoothParamChange(m_MixPlugins[plug - 1].fDryRatio, newRatio);
-				}
+				plug--;
+				const float newRatio = (127 - param) / 127.0f;
+				if(localOnly)
+					playState.m_midiMacroEvaluationResults->pluginDryWetRatio[plug] = newRatio;
+				else if(!isSmooth)
+					m_MixPlugins[plug].fDryRatio = newRatio;
+				else
+					m_MixPlugins[plug].fDryRatio = CalculateSmoothParamChange(playState, m_MixPlugins[plug].fDryRatio, newRatio);
 			}
-
-			return 4;
 		} else if((macroCode & 0x80) || isExtended)
 		{
 			// F0.F0.{80|n}.xx / F0.F1.n.xx: Set VST effect parameter n to xx
-			const PLUGINDEX plug = (plugin != 0) ? plugin : GetBestPlugin(nChn, PrioritiseChannel, EvenIfMuted);
-			const uint32 plugParam = isExtended ? (0x80 + macroCode) : (macroCode & 0x7F);
+			PLUGINDEX plug = (plugin != 0) ? plugin : GetBestPlugin(playState, nChn, PrioritiseChannel, EvenIfMuted);
 			if(plug > 0 && plug <= MAX_MIXPLUGINS)
 			{
-				IMixPlugin *pPlugin = m_MixPlugins[plug - 1].pMixPlugin;
+				plug--;
+				IMixPlugin *pPlugin = m_MixPlugins[plug].pMixPlugin;
 				if(pPlugin && param < 0x80)
 				{
-					const float fParam = param / 127.0f;
-					if(!isSmooth)
-					{
-						pPlugin->SetParameter(plugParam, fParam);
-					} else
-					{
-						pPlugin->SetParameter(plugParam, CalculateSmoothParamChange(pPlugin->GetParameter(plugParam), fParam));
-					}
+					const PlugParamIndex plugParam = isExtended ? (0x80 + macroCode) : (macroCode & 0x7F);
+					const PlugParamValue value = param / 127.0f;
+					if(localOnly)
+						playState.m_midiMacroEvaluationResults->pluginParameter[{plug, plugParam}] = value;
+					else if(!isSmooth)
+						pPlugin->SetParameter(plugParam, value);
+					else
+						pPlugin->SetParameter(plugParam, CalculateSmoothParamChange(playState, pPlugin->GetParameter(plugParam), value));
 				}
 			}
-
-			return 4;
 #endif // NO_PLUGINS
 		}
-
-		// If we reach this point, the internal macro was invalid.
-
-	} else
+	} else if(!localOnly)
 	{
 #ifndef NO_PLUGINS
 		// Not an internal device. Pass on to appropriate plugin.
@@ -5208,7 +5206,7 @@
 			PLUGINDEX plug = 0;
 			if(!chn.dwFlags[CHN_NOFX])
 			{
-				plug = (plugin != 0) ? plugin : GetBestPlugin(nChn, PrioritiseChannel, EvenIfMuted);
+				plug = (plugin != 0) ? plugin : GetBestPlugin(playState, nChn, PrioritiseChannel, EvenIfMuted);
 			}
 
 			if(plug > 0 && plug <= MAX_MIXPLUGINS)
@@ -5218,12 +5216,12 @@
 				{
 					if(macro[0] == 0xF0)
 					{
-						pPlugin->MidiSysexSend(mpt::as_span(mpt::byte_cast<const std::byte*>(macro), macroLen));
+						pPlugin->MidiSysexSend(mpt::byte_cast<mpt::const_byte_span>(macro));
 					} else
 					{
-						uint32 len = std::min(static_cast<uint32>(MIDIEvents::GetEventLength(macro[0])), macroLen);
+						size_t len = std::min(static_cast<size_t>(MIDIEvents::GetEventLength(macro[0])), macro.size());
 						uint32 curData = 0;
-						memcpy(&curData, macro, len);
+						memcpy(&curData, macro.data(), len);
 						pPlugin->MidiSend(curData);
 					}
 				}
@@ -5232,11 +5230,7 @@
 #else
 		MPT_UNREFERENCED_PARAMETER(plugin);
 #endif // NO_PLUGINS
-
-		return macroLen;
 	}
-
-	return 0;
 }
 
 
@@ -6165,7 +6159,7 @@
 }
 
 
-PLUGINDEX CSoundFile::GetBestPlugin(CHANNELINDEX nChn, PluginPriority priority, PluginMutePriority respectMutes) const
+PLUGINDEX CSoundFile::GetBestPlugin(const PlayState &playState, CHANNELINDEX nChn, PluginPriority priority, PluginMutePriority respectMutes) const
 {
 	if (nChn >= MAX_CHANNELS)		//Check valid channel number
 	{
@@ -6177,23 +6171,23 @@
 	switch (priority)
 	{
 		case ChannelOnly:
-			plugin = GetChannelPlugin(nChn, respectMutes);
+			plugin = GetChannelPlugin(playState, nChn, respectMutes);
 			break;
 		case InstrumentOnly:
-			plugin  = GetActiveInstrumentPlugin(nChn, respectMutes);
+			plugin  = GetActiveInstrumentPlugin(playState.Chn[nChn], respectMutes);
 			break;
 		case PrioritiseInstrument:
-			plugin  = GetActiveInstrumentPlugin(nChn, respectMutes);
+			plugin  = GetActiveInstrumentPlugin(playState.Chn[nChn], respectMutes);
 			if(!plugin || plugin > MAX_MIXPLUGINS)
 			{
-				plugin = GetChannelPlugin(nChn, respectMutes);
+				plugin = GetChannelPlugin(playState, nChn, respectMutes);
 			}
 			break;
 		case PrioritiseChannel:
-			plugin  = GetChannelPlugin(nChn, respectMutes);
+			plugin  = GetChannelPlugin(playState, nChn, respectMutes);
 			if(!plugin || plugin > MAX_MIXPLUGINS)
 			{
-				plugin = GetActiveInstrumentPlugin(nChn, respectMutes);
+				plugin = GetActiveInstrumentPlugin(playState.Chn[nChn], respectMutes);
 			}
 			break;
 	}
@@ -6202,9 +6196,9 @@
 }
 
 
-PLUGINDEX CSoundFile::GetChannelPlugin(CHANNELINDEX nChn, PluginMutePriority respectMutes) const
+PLUGINDEX CSoundFile::GetChannelPlugin(const PlayState &playState, CHANNELINDEX nChn, PluginMutePriority respectMutes) const
 {
-	const ModChannel &channel = m_PlayState.Chn[nChn];
+	const ModChannel &channel = playState.Chn[nChn];
 
 	PLUGINDEX plugin;
 	if((respectMutes == RespectMutes && channel.dwFlags[CHN_MUTE | CHN_SYNCMUTE]) || channel.dwFlags[CHN_NOFX])
@@ -6214,8 +6208,7 @@
 	{
 		// If it looks like this is an NNA channel, we need to find the master channel.
 		// This ensures we pick up the right ChnSettings.
-		// NB: nMasterChn == 0 means no master channel, so we need to -1 to get correct index.
-		if (nChn >= m_nChannels && channel.nMasterChn > 0)
+		if(channel.nMasterChn > 0)
 		{
 			nChn = channel.nMasterChn - 1;
 		}
@@ -6232,20 +6225,21 @@
 }
 
 
-PLUGINDEX CSoundFile::GetActiveInstrumentPlugin(CHANNELINDEX nChn, PluginMutePriority respectMutes) const
+PLUGINDEX CSoundFile::GetActiveInstrumentPlugin(const ModChannel &chn, PluginMutePriority respectMutes)
 {
 	// Unlike channel settings, pModInstrument is copied from the original chan to the NNA chan,
 	// so we don't need to worry about finding the master chan.
 
 	PLUGINDEX plug = 0;
-	if(m_PlayState.Chn[nChn].pModInstrument != nullptr)
+	if(chn.pModInstrument != nullptr)
 	{
-		if(respectMutes == RespectMutes && m_PlayState.Chn[nChn].pModSample && m_PlayState.Chn[nChn].pModSample->uFlags[CHN_MUTE])
+		// TODO this looks fishy. Shouldn't it check the mute status of the instrument itself?!
+		if(respectMutes == RespectMutes && chn.pModSample && chn.pModSample->uFlags[CHN_MUTE])
 		{
 			plug = 0;
 		} else
 		{
-			plug = m_PlayState.Chn[nChn].pModInstrument->nMixPlug;
+			plug = chn.pModInstrument->nMixPlug;
 		}
 	}
 	return plug;
@@ -6255,10 +6249,10 @@
 // Retrieve the plugin that is associated with the channel's current instrument.
 // No plugin is returned if the channel is muted or if the instrument doesn't have a MIDI channel set up,
 // As this is meant to be used with instrument plugins.
-IMixPlugin *CSoundFile::GetChannelInstrumentPlugin(CHANNELINDEX chn) const
+IMixPlugin *CSoundFile::GetChannelInstrumentPlugin(const ModChannel &chn) const
 {
 #ifndef NO_PLUGINS
-	if(m_PlayState.Chn[chn].dwFlags[CHN_MUTE | CHN_SYNCMUTE])
+	if(chn.dwFlags[CHN_MUTE | CHN_SYNCMUTE])
 	{
 		// Don't process portamento on muted channels. Note that this might have a side-effect
 		// on other channels which trigger notes on the same MIDI channel of the same plugin,
@@ -6266,9 +6260,9 @@
 		return nullptr;
 	}
 
-	if(m_PlayState.Chn[chn].HasMIDIOutput())
+	if(chn.HasMIDIOutput())
 	{
-		const ModInstrument *pIns = m_PlayState.Chn[chn].pModInstrument;
+		const ModInstrument *pIns = chn.pModInstrument;
 		// Instrument sends to a MIDI channel
 		if(pIns->nMixPlug != 0 && pIns->nMixPlug <= MAX_MIXPLUGINS)
 		{
Index: soundlib/Sndfile.cpp
===================================================================
--- soundlib/Sndfile.cpp	(revision 16701)
+++ soundlib/Sndfile.cpp	(working copy)
@@ -70,6 +70,13 @@
 }
 
 
+CSoundFile::PlayState::PlayState()
+{
+	std::fill(std::begin(Chn), std::end(Chn), ModChannel{});
+	m_midiMacroScratchSpace.reserve(kMacroLength);  // Note: If macros ever become variable-length, the scratch space needs to be at least one byte longer than the longest macro in the file for end-of-SysEx insertion!
+}
+
+
 //////////////////////////////////////////////////////////
 // CSoundFile
 
Index: soundlib/Sndfile.h
===================================================================
--- soundlib/Sndfile.h	(revision 16701)
+++ soundlib/Sndfile.h	(working copy)
@@ -583,12 +583,18 @@
 		CHANNELINDEX ChnMix[MAX_CHANNELS]; // Index of channels in Chn to be actually mixed
 		ModChannel Chn[MAX_CHANNELS];      // Mixing channels... First m_nChannels channels are master channels (i.e. they are never NNA channels)!
 
-	public:
-		PlayState()
+		struct MIDIMacroEvaluationResults
 		{
-			std::fill(std::begin(Chn), std::end(Chn), ModChannel());
-		}
+			std::map<PLUGINDEX, float> pluginDryWetRatio;
+			std::map<std::pair<PLUGINDEX, PlugParamIndex>, PlugParamValue> pluginParameter;
+		};
 
+		std::vector<uint8> m_midiMacroScratchSpace;
+		std::optional<MIDIMacroEvaluationResults> m_midiMacroEvaluationResults;
+
+	public:
+		PlayState();
+
 		void ResetGlobalVolumeRamping()
 		{
 			m_lHighResRampingGlobalVolume = m_nGlobalVolume << VOLUMERAMPPRECISION;
@@ -1115,9 +1121,10 @@
 	void GlobalVolSlide(ModCommand::PARAM param, uint8 &nOldGlobalVolSlide);
 
 	void ProcessMacroOnChannel(CHANNELINDEX nChn);
-	void ProcessMIDIMacro(CHANNELINDEX nChn, bool isSmooth, const char *macro, uint8 param = 0, PLUGINDEX plugin = 0);
-	float CalculateSmoothParamChange(float currentValue, float param) const;
-	uint32 SendMIDIData(CHANNELINDEX nChn, bool isSmooth, const unsigned char *macro, uint32 macroLen, PLUGINDEX plugin);
+	void ProcessMIDIMacro(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const MIDIMacroConfigData::Macro &macro, uint8 param = 0, PLUGINDEX plugin = 0);
+	void ParseMIDIMacro(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const mpt::span<const char> macro, mpt::span<uint8> &out, uint8 param = 0, PLUGINDEX plugin = 0) const;
+	static float CalculateSmoothParamChange(const PlayState &playState, float currentValue, float param);
+	void SendMIDIData(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const mpt::span<const unsigned char> macro, PLUGINDEX plugin);
 	void SendMIDINote(CHANNELINDEX chn, uint16 note, uint16 volume);
 
 	int SetupChannelFilter(ModChannel &chn, bool bReset, int envModifier = 256) const;
@@ -1243,12 +1250,12 @@
 	void ProcessStereoSeparation(long countChunk);
 
 private:
-	PLUGINDEX GetChannelPlugin(CHANNELINDEX nChn, PluginMutePriority respectMutes) const;
-	PLUGINDEX GetActiveInstrumentPlugin(CHANNELINDEX, PluginMutePriority respectMutes) const;
-	IMixPlugin *GetChannelInstrumentPlugin(CHANNELINDEX chn) const;
+	PLUGINDEX GetChannelPlugin(const PlayState &playState, CHANNELINDEX nChn, PluginMutePriority respectMutes) const;
+	static PLUGINDEX GetActiveInstrumentPlugin(const ModChannel &chn, PluginMutePriority respectMutes);
+	IMixPlugin *GetChannelInstrumentPlugin(const ModChannel &chn) const;
 
 public:
-	PLUGINDEX GetBestPlugin(CHANNELINDEX nChn, PluginPriority priority, PluginMutePriority respectMutes) const;
+	PLUGINDEX GetBestPlugin(const PlayState &playState, CHANNELINDEX nChn, PluginPriority priority, PluginMutePriority respectMutes) const;
 
 };
 
Index: soundlib/Sndmix.cpp
===================================================================
--- soundlib/Sndmix.cpp	(revision 16701)
+++ soundlib/Sndmix.cpp	(working copy)
@@ -1705,7 +1705,7 @@
 
 			// Process MIDI vibrato for plugins:
 #ifndef NO_PLUGINS
-			IMixPlugin *plugin = GetChannelInstrumentPlugin(nChn);
+			IMixPlugin *plugin = GetChannelInstrumentPlugin(m_PlayState.Chn[nChn]);
 			if(plugin != nullptr)
 			{
 				// If the Pitch Wheel Depth is configured correctly (so it's the same as the plugin's PWD),
@@ -1728,7 +1728,7 @@
 	{
 		// Stop MIDI vibrato for plugins:
 #ifndef NO_PLUGINS
-		IMixPlugin *plugin = GetChannelInstrumentPlugin(nChn);
+		IMixPlugin *plugin = GetChannelInstrumentPlugin(m_PlayState.Chn[nChn]);
 		if(plugin != nullptr)
 		{
 			plugin->MidiVibrato(0, 0, nChn);
@@ -2528,15 +2528,15 @@
 	if(nChn < GetNumChannels())
 	{
 		// TODO evaluate per-plugin macros here
-		//ProcessMIDIMacro(nChn, false, m_MidiCfg.szMidiGlb[MIDIOUT_PAN]);
-		//ProcessMIDIMacro(nChn, false, m_MidiCfg.szMidiGlb[MIDIOUT_VOLUME]);
+		//ProcessMIDIMacro(m_PlayState, nChn, false, m_MidiCfg.Global[MIDIOUT_PAN]);
+		//ProcessMIDIMacro(m_PlayState, nChn, false, m_MidiCfg.Global[MIDIOUT_VOLUME]);
 
 		if((chn.rowCommand.command == CMD_MIDI && m_SongFlags[SONG_FIRSTTICK]) || chn.rowCommand.command == CMD_SMOOTHMIDI)
 		{
 			if(chn.rowCommand.param < 0x80)
-				ProcessMIDIMacro(nChn, (chn.rowCommand.command == CMD_SMOOTHMIDI), m_MidiCfg.szMidiSFXExt[chn.nActiveMacro], chn.rowCommand.param);
+				ProcessMIDIMacro(m_PlayState, nChn, (chn.rowCommand.command == CMD_SMOOTHMIDI), m_MidiCfg.SFx[chn.nActiveMacro], chn.rowCommand.param);
 			else
-				ProcessMIDIMacro(nChn, (chn.rowCommand.command == CMD_SMOOTHMIDI), m_MidiCfg.szMidiZXXExt[(chn.rowCommand.param & 0x7F)], 0);
+				ProcessMIDIMacro(m_PlayState, nChn, (chn.rowCommand.command == CMD_SMOOTHMIDI), m_MidiCfg.Zxx[chn.rowCommand.param & 0x7F], 0);
 		}
 	}
 }
@@ -2562,7 +2562,7 @@
 	}
 
 	// Check instrument plugins
-	const PLUGINDEX nPlugin = GetBestPlugin(nChn, PrioritiseInstrument, RespectMutes);
+	const PLUGINDEX nPlugin = GetBestPlugin(m_PlayState, nChn, PrioritiseInstrument, RespectMutes);
 	IMixPlugin *pPlugin = nullptr;
 	if(nPlugin > 0 && nPlugin <= MAX_MIXPLUGINS)
 	{
@@ -2623,7 +2623,7 @@
 		if(ModCommand::IsNote(note))
 			realNote = pIns->NoteMap[note - NOTE_MIN];
 		// Experimental VST panning
-		//ProcessMIDIMacro(nChn, false, m_MidiCfg.szMidiGlb[MIDIOUT_PAN], 0, nPlugin);
+		//ProcessMIDIMacro(nChn, false, m_MidiCfg.Global[MIDIOUT_PAN], 0, nPlugin);
 		SendMIDINote(nChn, realNote, static_cast<uint16>(velocity));
 	}
 
