View Issue Details

IDProjectCategoryView StatusLast Update
0001497OpenMPTPlayback Compatibilitypublic2022-01-28 19:30
ReporterSaga Musix Assigned ToSaga Musix  
PrioritynormalSeverityfeatureReproducibilityN/A
Status resolvedResolutionfixed 
Target VersionOpenMPT 1.30.02.00 / libopenmpt 0.6.1 (upgrade first)Fixed in VersionOpenMPT 1.30.02.00 / libopenmpt 0.6.1 (upgrade first) 
Summary0001497: Zxx Macro support for seeking
Description

CSoundFile::GetLength currently doesn't support Zxx MIDI macros at all, mostly for technical reasons. While it might not be possible or desirable to add full support for any kind of MIDI macros to this function, it should at the very least support local MIDI macros (i.e. anything that's not passed on to plugins as raw MIDI messages).

This might be done by splitting the parsing of the MIDI text macros and evaluation of the resulting MIDI data into two steps that can be carried out independently, and allow the second step to only handle local macros. But even with local macros there needs to be a distinction between macros that just modify channel state (such as filters, which are arguably the most useful macros for this use case) and macros that are local but still modify the module state (such as plugin dry/wet ratio) - the latter may only be evaluated if GetLength succeeds with seeking. When GetLength isn't used for seeking, macros should not be parsed at all to save time.

TagsNo tags attached.
Has the bug occurred in previous versions?
Tested code revision (in case you know it)

Activities

Saga Musix

Saga Musix

2022-01-24 22:42

administrator   ~0005006

Filter macros are now applied on the PlayState allocated by GetLength, however for plugin stuff this is not possible yet. At least for dry/wet ratio this would be great to have, but also nice for plugin parameters because with PC events that's already possible.

MacroRewrite.patch (52,659 bytes)   
Index: mptrack/EffectInfo.cpp
===================================================================
--- mptrack/EffectInfo.cpp	(revision 16687)
+++ 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 16687)
+++ mptrack/MIDIMacroDialog.cpp	(working copy)
@@ -310,7 +310,7 @@
 		{
 			CString s;
 			m_EditSFx.GetWindowText(s);
-			mpt::String::WriteAutoBuf(m_MidiCfg.szMidiSFXExt[sfx]) = mpt::ToCharset(mpt::Charset::ASCII, s);
+			m_MidiCfg.szMidiSFXExt[sfx] = mpt::ToCharset(mpt::Charset::ASCII, s);
 
 			int sfx_preset = m_MidiCfg.GetParameteredMacroType(sfx);
 			m_CbnSFxPreset.SetCurSel(sfx_preset);
@@ -330,7 +330,7 @@
 		{
 			CString s;
 			m_EditZxx.GetWindowText(s);
-			mpt::String::WriteAutoBuf(m_MidiCfg.szMidiZXXExt[zxx]) = mpt::ToCharset(mpt::Charset::ASCII, s);
+			m_MidiCfg.szMidiZXXExt[zxx] = mpt::ToCharset(mpt::Charset::ASCII, s);
 			m_CbnZxxPreset.SetCurSel(m_MidiCfg.GetFixedMacroType());
 		}
 	}
@@ -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 16687)
+++ mptrack/MIDIMacroDialog.h	(working copy)
@@ -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 16687)
+++ 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 16687)
+++ 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;
Index: mptrack/TrackerSettings.cpp
===================================================================
--- mptrack/TrackerSettings.cpp	(revision 16687)
+++ mptrack/TrackerSettings.cpp	(working copy)
@@ -443,11 +443,11 @@
 	theApp.GetDefaultMidiMacro(macros);
 	for(int isfx = 0; isfx < 16; isfx++)
 	{
-		mpt::String::WriteAutoBuf(macros.szMidiSFXExt[isfx]) = conf.Read<std::string>(U_("Zxx Macros"), MPT_UFORMAT("SF{}")(mpt::ufmt::HEX(isfx)), macros.szMidiSFXExt[isfx]);
+		macros.szMidiSFXExt[isfx] = conf.Read<std::string>(U_("Zxx Macros"), MPT_UFORMAT("SF{}")(mpt::ufmt::HEX(isfx)), macros.szMidiSFXExt[isfx]);
 	}
 	for(int izxx = 0; izxx < 128; izxx++)
 	{
-		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.szMidiZXXExt[izxx] = conf.Read<std::string>(U_("Zxx Macros"), MPT_UFORMAT("Z{}")(mpt::ufmt::HEX0<2>(izxx | 0x80)), macros.szMidiZXXExt[izxx]);
 	}
 
 
Index: soundlib/Fastmix.cpp
===================================================================
--- soundlib/Fastmix.cpp	(revision 16687)
+++ 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 16687)
+++ 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.szMidiZXXExt[i     ] = MPT_AFORMAT("F0F080{}")(mpt::afmt::HEX0<2>(param));
+			m_MidiCfg.szMidiZXXExt[i + 32] = MPT_AFORMAT("F0F081{}")(mpt::afmt::HEX0<2>(param));
+			m_MidiCfg.szMidiZXXExt[i + 64] = MPT_AFORMAT("F0F082{}")(mpt::afmt::HEX0<2>(param));
+			m_MidiCfg.szMidiZXXExt[i + 96] = MPT_AFORMAT("F0F083{}")(mpt::afmt::HEX0<2>(param));
 		}
 	}
 #endif // NO_PLUGINS
Index: soundlib/Load_med.cpp
===================================================================
--- soundlib/Load_med.cpp	(revision 16687)
+++ 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.szMidiSFXExt[0] = "Cc z";
 
 	file.Rewind();
 	PATTERNINDEX basePattern = 0;
@@ -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.szMidiZXXExt[dump] = macro;
 				}
 			}
 		}
Index: soundlib/Load_mo3.cpp
===================================================================
--- soundlib/Load_mo3.cpp	(revision 16687)
+++ 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.szMidiSFXExt[i] = MPT_AFORMAT("F0F0{}z")(mpt::afmt::HEX0<2>(fileHeader.sfxMacros[i] - 1));
 			else
-				mpt::String::WriteAutoBuf(m_MidiCfg.szMidiSFXExt[i]) = "";
+				m_MidiCfg.szMidiSFXExt[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.szMidiZXXExt[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.szMidiZXXExt[i] = "";
 		}
 	}
 
Index: soundlib/Load_symmod.cpp
===================================================================
--- soundlib/Load_symmod.cpp	(revision 16687)
+++ 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)
 	{
Index: soundlib/MIDIMacros.cpp
===================================================================
--- soundlib/MIDIMacros.cpp	(revision 16687)
+++ soundlib/MIDIMacros.cpp	(working copy)
@@ -9,9 +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
@@ -60,7 +59,7 @@
 			bool found = true;
 			for(uint32 j = 0; j < 128; j++)
 			{
-				if(strncmp(macros[j], szMidiZXXExt[j], MACRO_LENGTH))
+				if(macros[j] != szMidiZXXExt[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();
@@ -100,7 +99,7 @@
 {
 	Macro parameteredMacro;
 	CreateParameteredMacro(parameteredMacro, macroType, subType);
-	return mpt::String::ReadAutoBuf(parameteredMacro);
+	return parameteredMacro;
 }
 
 
@@ -113,44 +112,44 @@
 		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;
@@ -344,11 +343,11 @@
 	MemsetZero(szMidiSFXExt);
 	MemsetZero(szMidiZXXExt);
 
-	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");
+	szMidiGlb[MIDIOUT_START] = "FF";
+	szMidiGlb[MIDIOUT_STOP] = "FC";
+	szMidiGlb[MIDIOUT_NOTEON] = "9c n v";
+	szMidiGlb[MIDIOUT_NOTEOFF] = "9c n 0";
+	szMidiGlb[MIDIOUT_PROGRAM] = "Cc p";
 	// SF0: Z00-Z7F controls cutoff
 	CreateParameteredMacro(0, kSFxCutoff);
 	// Z80-Z8F controls resonance
@@ -369,16 +368,15 @@
 {
 	for(auto &macro : *this)
 	{
-		macro[MACRO_LENGTH - 1] = '\0';
-		std::fill(std::find(std::begin(macro), std::end(macro), '\0'), std::end(macro), '\0');
+		macro.Sanitize();
 	}
 }
 
 
 // Helper function for UpgradeMacros()
-void MIDIMacroConfig::UpgradeMacroString(Macro &macro) const
+void MIDIMacroConfig::Macro::UpgradeLegacyMacro() noexcept
 {
-	for(auto &c : macro)
+	for(auto &c : *this)
 	{
 		if(c >= 'a' && c <= 'f') // Both A-F and a-f were treated as hex constants
 		{
@@ -399,7 +397,7 @@
 {
 	for(auto &macro : *this)
 	{
-		UpgradeMacroString(macro);
+		macro.UpgradeLegacyMacro();
 	}
 }
 
Index: soundlib/MIDIMacros.h
===================================================================
--- soundlib/MIDIMacros.h	(revision 16687)
+++ soundlib/MIDIMacros.h	(working copy)
@@ -86,19 +86,93 @@
 
 struct MIDIMacroConfigData
 {
-	typedef char Macro[MACRO_LENGTH];
+	struct Macro
+	{
+	public:
+		using RawType = std::array<char, MACRO_LENGTH>;
+		constexpr auto begin() const noexcept { return m_data.begin(); }
+		constexpr auto cbegin() const noexcept { return m_data.cbegin(); }
+		constexpr auto end() const noexcept { return m_data.end(); }
+		constexpr auto cend() const noexcept { return m_data.cend(); }
+		constexpr auto data() const noexcept { return m_data.data(); }
+		constexpr auto size() const noexcept { return m_data.size(); }
+	private:
+		constexpr auto begin() noexcept { return m_data.begin(); }
+		constexpr auto end() noexcept { return m_data.end(); }
+		constexpr auto data() noexcept { return m_data.data(); }
+
+	public:
+		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, begin());
+			std::fill(begin() + copyLength, end(), '\0');
+			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);
+		}
+
+		operator mpt::span<const char>() const noexcept
+		{
+			return {data(), length()};
+		}
+		operator std::string_view() const noexcept
+		{
+			return {data(), length()};
+		}
+		operator std::string() const
+		{
+			return {data(), length()};
+		}
+
+		size_t length() const noexcept
+		{
+			return static_cast<size_t>(std::distance(begin(), std::find(begin(), end(), '\0')));
+		}
+
+		void clear() noexcept
+		{
+			m_data.fill('\0');
+		}
+
+		void Sanitize() noexcept
+		{
+			m_data.back() = '\0';
+			std::fill(begin() + length(), end(), '\0');
+		}
+
+		void UpgradeLegacyMacro() noexcept;
+
+	private:
+		RawType m_data;
+	};
+
 	// encoding is ASCII
-	Macro szMidiGlb[9];      // Global MIDI macros
-	Macro szMidiSFXExt[16];  // Parametric MIDI macros
-	Macro szMidiZXXExt[128]; // Fixed MIDI macros
+	Macro szMidiGlb[9];              // Global MIDI macros
+	Macro szMidiSFXExt[NUM_MACROS];  // Parametric MIDI macros
+	Macro szMidiZXXExt[128];         // Fixed MIDI macros
 
-	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 *begin() noexcept { return std::begin(szMidiGlb); }
+	const Macro *begin() const noexcept { return std::begin(szMidiGlb); }
+	Macro *end() noexcept { return std::end(szMidiZXXExt); }
+	const Macro *end() const noexcept { return std::end(szMidiZXXExt); }
 };
 
-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
 {
Index: soundlib/ModInstrument.cpp
===================================================================
--- soundlib/ModInstrument.cpp	(revision 16687)
+++ 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 16687)
+++ 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/PlugInterface.cpp
===================================================================
--- soundlib/plugins/PlugInterface.cpp	(revision 16687)
+++ 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;
 }
Index: soundlib/plugins/PlugInterface.h
===================================================================
--- soundlib/plugins/PlugInterface.h	(revision 16687)
+++ 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 16687)
+++ soundlib/Snd_fx.cpp	(working copy)
@@ -828,6 +828,13 @@
 			case CMD_PANBRELLO:
 				Panbrello(chn, param);
 				break;
+
+			case CMD_MIDI:
+			case CMD_SMOOTHMIDI:
+				if(param < 0x80)
+					ProcessMIDIMacro(playState, nChn, false, m_MidiCfg.szMidiSFXExt[chn.nActiveMacro], chn.rowCommand.param, 0, true);
+				else
+					ProcessMIDIMacro(playState, nChn, false, m_MidiCfg.szMidiZXXExt[param & 0x7F], chn.rowCommand.param, 0, true);
 			default:
 				break;
 			}
@@ -2268,7 +2275,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 +3378,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 +3867,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 +4818,104 @@
 
 // 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)
+// localOnly: Do not execute macros that would modify state outside of playState (e.g. sending messages to plugins)
+void CSoundFile::ProcessMIDIMacro(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const MIDIMacroConfigData::Macro &macro, uint8 param, PLUGINDEX plugin, bool localOnly)
 {
-	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, 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 = mpt::as_span(out.data() + sendPos, sendLen);
+		uint32 bytesSent = 0;
+		// Local-only messages are messages that can be processed directly in the replay routines without sending stuff to plugins.
+		bytesSent = SendMIDIData(playState, nChn, isSmooth, midiMsg, plugin, localOnly);
+		// 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;
+	}
+}
+
+
+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
+	size_t pos = 0;
+	for(; 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 +4925,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 +4935,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 +4947,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 +5023,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 +5033,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 +5064,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 +5080,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,12 +5103,10 @@
 
 
 // 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)
+uint32 CSoundFile::SendMIDIData(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const mpt::span<const unsigned char> macro, PLUGINDEX plugin, bool localOnly)
 {
-	if(macroLen < 1)
-	{
+	if(macro.size() < 1)
 		return 0;
-	}
 
 	if(macro[0] == 0xFA || macro[0] == 0xFC || macro[0] == 0xFF)
 	{
@@ -5092,16 +5113,16 @@
 		// 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];
+	ModChannel &chn = playState.Chn[nChn];
 	if(macro[0] == 0xF0 && (macro[1] == 0xF0 || macro[1] == 0xF1))
 	{
 		// Internal device.
-		if(macroLen < 4)
+		if(macro.size() < 4)
 		{
 			return 0;
 		}
@@ -5113,16 +5134,13 @@
 		{
 			// 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);
@@ -5133,12 +5151,9 @@
 		{
 			// 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]);
 
@@ -5157,38 +5172,32 @@
 		} else if(macroCode == 0x03 && !isExtended)
 		{
 			// F0.F0.03.xx: Set plug dry/wet
-			const PLUGINDEX plug = (plugin != 0) ? plugin : GetBestPlugin(nChn, PrioritiseChannel, EvenIfMuted);
-			if(plug > 0 && plug <= MAX_MIXPLUGINS && param < 0x80)
+			const PLUGINDEX plug = (plugin != 0) ? plugin : GetBestPlugin(playState, nChn, PrioritiseChannel, EvenIfMuted);
+			if(plug > 0 && plug <= MAX_MIXPLUGINS && param < 0x80 && !localOnly)
 			{
-				const float newRatio = (0x7F - (param & 0x7F)) / 127.0f;
+				const float newRatio = (127 - param) / 127.0f;
 				if(!isSmooth)
-				{
 					m_MixPlugins[plug - 1].fDryRatio = newRatio;
-				} else
-				{
-					m_MixPlugins[plug - 1].fDryRatio = CalculateSmoothParamChange(m_MixPlugins[plug - 1].fDryRatio, newRatio);
-				}
+				else
+					m_MixPlugins[plug - 1].fDryRatio = CalculateSmoothParamChange(playState, m_MixPlugins[plug - 1].fDryRatio, newRatio);
 			}
 
 			return 4;
-		} else if((macroCode & 0x80) || isExtended)
+		} 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);
-			if(plug > 0 && plug <= MAX_MIXPLUGINS)
+			const PLUGINDEX plug = (plugin != 0) ? plugin : GetBestPlugin(playState, nChn, PrioritiseChannel, EvenIfMuted);
+			if(plug > 0 && plug <= MAX_MIXPLUGINS && !localOnly)
 			{
 				IMixPlugin *pPlugin = m_MixPlugins[plug - 1].pMixPlugin;
 				if(pPlugin && param < 0x80)
 				{
+					const uint32 plugParam = isExtended ? (0x80 + macroCode) : (macroCode & 0x7F);
 					const float fParam = param / 127.0f;
 					if(!isSmooth)
-					{
 						pPlugin->SetParameter(plugParam, fParam);
-					} else
-					{
-						pPlugin->SetParameter(plugParam, CalculateSmoothParamChange(pPlugin->GetParameter(plugParam), fParam));
-					}
+					else
+						pPlugin->SetParameter(plugParam, CalculateSmoothParamChange(playState, pPlugin->GetParameter(plugParam), fParam));
 				}
 			}
 
@@ -5198,7 +5207,7 @@
 
 		// 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 +5217,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 +5227,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);
 					}
 				}
@@ -5233,7 +5242,7 @@
 		MPT_UNREFERENCED_PARAMETER(plugin);
 #endif // NO_PLUGINS
 
-		return macroLen;
+		return static_cast<uint32>(macro.size());
 	}
 
 	return 0;
@@ -6165,7 +6174,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 +6186,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 +6211,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 +6223,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 +6240,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 +6264,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 +6275,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.h
===================================================================
--- soundlib/Sndfile.h	(revision 16687)
+++ soundlib/Sndfile.h	(working copy)
@@ -583,10 +583,13 @@
 		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)!
 
+		std::vector<uint8> m_MidiMacroScratchSpace;
+
 	public:
 		PlayState()
 		{
 			std::fill(std::begin(Chn), std::end(Chn), ModChannel());
+			m_MidiMacroScratchSpace.reserve(MACRO_LENGTH);
 		}
 
 		void ResetGlobalVolumeRamping()
@@ -1115,9 +1118,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, bool localOnly = false);
+	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);
+	uint32 SendMIDIData(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const mpt::span<const unsigned char> macro, PLUGINDEX plugin, bool localOnly);
 	void SendMIDINote(CHANNELINDEX chn, uint16 note, uint16 volume);
 
 	int SetupChannelFilter(ModChannel &chn, bool bReset, int envModifier = 256) const;
@@ -1243,12 +1247,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 16687)
+++ 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.szMidiGlb[MIDIOUT_PAN]);
+		//ProcessMIDIMacro(m_PlayState, nChn, false, m_MidiCfg.szMidiGlb[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.szMidiSFXExt[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.szMidiZXXExt[(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)
 	{
MacroRewrite.patch (52,659 bytes)   
Saga Musix

Saga Musix

2022-01-25 22:04

administrator   ~0005011

Now also works with plugin parameters and dry/wet ratio changes.

MacroRewrite-2.patch (57,344 bytes)   
Index: mptrack/EffectInfo.cpp
===================================================================
--- mptrack/EffectInfo.cpp	(revision 16693)
+++ 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 16693)
+++ mptrack/MIDIMacroDialog.cpp	(working copy)
@@ -310,7 +310,7 @@
 		{
 			CString s;
 			m_EditSFx.GetWindowText(s);
-			mpt::String::WriteAutoBuf(m_MidiCfg.szMidiSFXExt[sfx]) = mpt::ToCharset(mpt::Charset::ASCII, s);
+			m_MidiCfg.szMidiSFXExt[sfx] = mpt::ToCharset(mpt::Charset::ASCII, s);
 
 			int sfx_preset = m_MidiCfg.GetParameteredMacroType(sfx);
 			m_CbnSFxPreset.SetCurSel(sfx_preset);
@@ -330,7 +330,7 @@
 		{
 			CString s;
 			m_EditZxx.GetWindowText(s);
-			mpt::String::WriteAutoBuf(m_MidiCfg.szMidiZXXExt[zxx]) = mpt::ToCharset(mpt::Charset::ASCII, s);
+			m_MidiCfg.szMidiZXXExt[zxx] = mpt::ToCharset(mpt::Charset::ASCII, s);
 			m_CbnZxxPreset.SetCurSel(m_MidiCfg.GetFixedMacroType());
 		}
 	}
@@ -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 16693)
+++ mptrack/MIDIMacroDialog.h	(working copy)
@@ -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 16693)
+++ 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 16693)
+++ 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;
Index: mptrack/TrackerSettings.cpp
===================================================================
--- mptrack/TrackerSettings.cpp	(revision 16693)
+++ mptrack/TrackerSettings.cpp	(working copy)
@@ -443,11 +443,11 @@
 	theApp.GetDefaultMidiMacro(macros);
 	for(int isfx = 0; isfx < 16; isfx++)
 	{
-		mpt::String::WriteAutoBuf(macros.szMidiSFXExt[isfx]) = conf.Read<std::string>(U_("Zxx Macros"), MPT_UFORMAT("SF{}")(mpt::ufmt::HEX(isfx)), macros.szMidiSFXExt[isfx]);
+		macros.szMidiSFXExt[isfx] = conf.Read<std::string>(U_("Zxx Macros"), MPT_UFORMAT("SF{}")(mpt::ufmt::HEX(isfx)), macros.szMidiSFXExt[isfx]);
 	}
 	for(int izxx = 0; izxx < 128; izxx++)
 	{
-		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.szMidiZXXExt[izxx] = conf.Read<std::string>(U_("Zxx Macros"), MPT_UFORMAT("Z{}")(mpt::ufmt::HEX0<2>(izxx | 0x80)), macros.szMidiZXXExt[izxx]);
 	}
 
 
Index: soundlib/Fastmix.cpp
===================================================================
--- soundlib/Fastmix.cpp	(revision 16693)
+++ 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 16693)
+++ 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.szMidiZXXExt[i     ] = MPT_AFORMAT("F0F080{}")(mpt::afmt::HEX0<2>(param));
+			m_MidiCfg.szMidiZXXExt[i + 32] = MPT_AFORMAT("F0F081{}")(mpt::afmt::HEX0<2>(param));
+			m_MidiCfg.szMidiZXXExt[i + 64] = MPT_AFORMAT("F0F082{}")(mpt::afmt::HEX0<2>(param));
+			m_MidiCfg.szMidiZXXExt[i + 96] = MPT_AFORMAT("F0F083{}")(mpt::afmt::HEX0<2>(param));
 		}
 	}
 #endif // NO_PLUGINS
Index: soundlib/Load_med.cpp
===================================================================
--- soundlib/Load_med.cpp	(revision 16693)
+++ 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.szMidiSFXExt[0] = "Cc z";
 
 	file.Rewind();
 	PATTERNINDEX basePattern = 0;
@@ -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.szMidiZXXExt[dump] = macro;
 				}
 			}
 		}
Index: soundlib/Load_mo3.cpp
===================================================================
--- soundlib/Load_mo3.cpp	(revision 16693)
+++ 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.szMidiSFXExt[i] = MPT_AFORMAT("F0F0{}z")(mpt::afmt::HEX0<2>(fileHeader.sfxMacros[i] - 1));
 			else
-				mpt::String::WriteAutoBuf(m_MidiCfg.szMidiSFXExt[i]) = "";
+				m_MidiCfg.szMidiSFXExt[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.szMidiZXXExt[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.szMidiZXXExt[i] = "";
 		}
 	}
 
Index: soundlib/Load_symmod.cpp
===================================================================
--- soundlib/Load_symmod.cpp	(revision 16693)
+++ 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)
 	{
Index: soundlib/MIDIMacros.cpp
===================================================================
--- soundlib/MIDIMacros.cpp	(revision 16693)
+++ soundlib/MIDIMacros.cpp	(working copy)
@@ -9,9 +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
@@ -60,7 +59,7 @@
 			bool found = true;
 			for(uint32 j = 0; j < 128; j++)
 			{
-				if(strncmp(macros[j], szMidiZXXExt[j], MACRO_LENGTH))
+				if(macros[j] != szMidiZXXExt[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();
@@ -100,7 +99,7 @@
 {
 	Macro parameteredMacro;
 	CreateParameteredMacro(parameteredMacro, macroType, subType);
-	return mpt::String::ReadAutoBuf(parameteredMacro);
+	return parameteredMacro;
 }
 
 
@@ -113,44 +112,44 @@
 		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;
@@ -344,11 +343,11 @@
 	MemsetZero(szMidiSFXExt);
 	MemsetZero(szMidiZXXExt);
 
-	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");
+	szMidiGlb[MIDIOUT_START] = "FF";
+	szMidiGlb[MIDIOUT_STOP] = "FC";
+	szMidiGlb[MIDIOUT_NOTEON] = "9c n v";
+	szMidiGlb[MIDIOUT_NOTEOFF] = "9c n 0";
+	szMidiGlb[MIDIOUT_PROGRAM] = "Cc p";
 	// SF0: Z00-Z7F controls cutoff
 	CreateParameteredMacro(0, kSFxCutoff);
 	// Z80-Z8F controls resonance
@@ -369,16 +368,15 @@
 {
 	for(auto &macro : *this)
 	{
-		macro[MACRO_LENGTH - 1] = '\0';
-		std::fill(std::find(std::begin(macro), std::end(macro), '\0'), std::end(macro), '\0');
+		macro.Sanitize();
 	}
 }
 
 
 // Helper function for UpgradeMacros()
-void MIDIMacroConfig::UpgradeMacroString(Macro &macro) const
+void MIDIMacroConfig::Macro::UpgradeLegacyMacro() noexcept
 {
-	for(auto &c : macro)
+	for(auto &c : *this)
 	{
 		if(c >= 'a' && c <= 'f') // Both A-F and a-f were treated as hex constants
 		{
@@ -399,7 +397,7 @@
 {
 	for(auto &macro : *this)
 	{
-		UpgradeMacroString(macro);
+		macro.UpgradeLegacyMacro();
 	}
 }
 
Index: soundlib/MIDIMacros.h
===================================================================
--- soundlib/MIDIMacros.h	(revision 16693)
+++ soundlib/MIDIMacros.h	(working copy)
@@ -86,19 +86,93 @@
 
 struct MIDIMacroConfigData
 {
-	typedef char Macro[MACRO_LENGTH];
+	struct Macro
+	{
+	public:
+		using RawType = std::array<char, MACRO_LENGTH>;
+		constexpr auto begin() const noexcept { return m_data.begin(); }
+		constexpr auto cbegin() const noexcept { return m_data.cbegin(); }
+		constexpr auto end() const noexcept { return m_data.end(); }
+		constexpr auto cend() const noexcept { return m_data.cend(); }
+		constexpr auto data() const noexcept { return m_data.data(); }
+		constexpr auto size() const noexcept { return m_data.size(); }
+	private:
+		constexpr auto begin() noexcept { return m_data.begin(); }
+		constexpr auto end() noexcept { return m_data.end(); }
+		constexpr auto data() noexcept { return m_data.data(); }
+
+	public:
+		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, begin());
+			std::fill(begin() + copyLength, end(), '\0');
+			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);
+		}
+
+		operator mpt::span<const char>() const noexcept
+		{
+			return {data(), length()};
+		}
+		operator std::string_view() const noexcept
+		{
+			return {data(), length()};
+		}
+		operator std::string() const
+		{
+			return {data(), length()};
+		}
+
+		size_t length() const noexcept
+		{
+			return static_cast<size_t>(std::distance(begin(), std::find(begin(), end(), '\0')));
+		}
+
+		void clear() noexcept
+		{
+			m_data.fill('\0');
+		}
+
+		void Sanitize() noexcept
+		{
+			m_data.back() = '\0';
+			std::fill(begin() + length(), end(), '\0');
+		}
+
+		void UpgradeLegacyMacro() noexcept;
+
+	private:
+		RawType m_data;
+	};
+
 	// encoding is ASCII
-	Macro szMidiGlb[9];      // Global MIDI macros
-	Macro szMidiSFXExt[16];  // Parametric MIDI macros
-	Macro szMidiZXXExt[128]; // Fixed MIDI macros
+	Macro szMidiGlb[9];              // Global MIDI macros
+	Macro szMidiSFXExt[NUM_MACROS];  // Parametric MIDI macros
+	Macro szMidiZXXExt[128];         // Fixed MIDI macros
 
-	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 *begin() noexcept { return std::begin(szMidiGlb); }
+	const Macro *begin() const noexcept { return std::begin(szMidiGlb); }
+	Macro *end() noexcept { return std::end(szMidiZXXExt); }
+	const Macro *end() const noexcept { return std::end(szMidiZXXExt); }
 };
 
-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
 {
Index: soundlib/ModInstrument.cpp
===================================================================
--- soundlib/ModInstrument.cpp	(revision 16693)
+++ 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 16693)
+++ 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 16693)
+++ 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 16693)
+++ 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;
 }
Index: soundlib/plugins/PlugInterface.h
===================================================================
--- soundlib/plugins/PlugInterface.h	(revision 16693)
+++ 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 16693)
+++ 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.szMidiSFXExt[chn.nActiveMacro], chn.rowCommand.param, 0);
+				else
+					ProcessMIDIMacro(playState, nChn, false, m_MidiCfg.szMidiZXXExt[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,103 @@
 
 // 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, 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 = mpt::as_span(out.data() + sendPos, sendLen);
+		uint32 bytesSent = 0;
+		// Local-only messages are messages that can be processed directly in the replay routines without sending stuff to plugins.
+		bytesSent = SendMIDIData(playState, nChn, isSmooth, midiMsg, 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;
+	}
+}
+
+
+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
+	size_t pos = 0;
+	for(; 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 +4929,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 +4939,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 +4951,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 +5027,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 +5037,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 +5068,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 +5084,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,28 +5107,29 @@
 
 
 // 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)
+uint32 CSoundFile::SendMIDIData(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const mpt::span<const unsigned char> macro, PLUGINDEX plugin)
 {
-	if(macroLen < 1)
-	{
+	if(macro.size() < 1)
 		return 0;
-	}
 
+	// 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];
+	ModChannel &chn = playState.Chn[nChn];
 	if(macro[0] == 0xF0 && (macro[1] == 0xF0 || macro[1] == 0xF1))
 	{
 		// Internal device.
-		if(macroLen < 4)
+		if(macro.size() < 4)
 		{
 			return 0;
 		}
@@ -5113,16 +5141,13 @@
 		{
 			// 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);
@@ -5133,12 +5158,9 @@
 		{
 			// 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]);
 
@@ -5157,17 +5179,17 @@
 		} 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;
@@ -5174,21 +5196,21 @@
 		} 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;
+					const PlugParamIndex plugParam = isExtended ? (0x80 + macroCode) : (macroCode & 0x7F);
+					const PlugParamValue fParam = param / 127.0f;
+					if(localOnly)
+						playState.m_midiMacroEvaluationResults->pluginParameter[{plug, plugParam}] = fParam;
 					if(!isSmooth)
-					{
 						pPlugin->SetParameter(plugParam, fParam);
-					} else
-					{
-						pPlugin->SetParameter(plugParam, CalculateSmoothParamChange(pPlugin->GetParameter(plugParam), fParam));
-					}
+					else
+						pPlugin->SetParameter(plugParam, CalculateSmoothParamChange(playState, pPlugin->GetParameter(plugParam), fParam));
 				}
 			}
 
@@ -5198,7 +5220,7 @@
 
 		// 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 +5230,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 +5240,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);
 					}
 				}
@@ -5233,7 +5255,7 @@
 		MPT_UNREFERENCED_PARAMETER(plugin);
 #endif // NO_PLUGINS
 
-		return macroLen;
+		return static_cast<uint32>(macro.size());
 	}
 
 	return 0;
@@ -6165,7 +6187,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 +6199,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 +6224,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 +6236,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 +6253,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 +6277,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 +6288,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 16693)
+++ soundlib/Sndfile.cpp	(working copy)
@@ -70,6 +70,13 @@
 }
 
 
+CSoundFile::PlayState::PlayState()
+{
+	std::fill(std::begin(Chn), std::end(Chn), ModChannel{});
+	m_midiMacroScratchSpace.reserve(MACRO_LENGTH);  // 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 16693)
+++ soundlib/Sndfile.h	(working copy)
@@ -531,6 +531,12 @@
 	MixLevels m_nMixLevels;
 
 public:
+	struct MIDIMacroEvaluationResults
+	{
+		std::map<PLUGINDEX, float> pluginDryWetRatio;
+		std::map<std::pair<PLUGINDEX, PlugParamIndex>, PlugParamValue> pluginParameter;
+	};
+
 	struct PlayState
 	{
 		friend class CSoundFile;
@@ -583,11 +589,11 @@
 		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)!
 
+		std::vector<uint8> m_midiMacroScratchSpace;
+		std::optional<MIDIMacroEvaluationResults> m_midiMacroEvaluationResults;
+
 	public:
-		PlayState()
-		{
-			std::fill(std::begin(Chn), std::end(Chn), ModChannel());
-		}
+		PlayState();
 
 		void ResetGlobalVolumeRamping()
 		{
@@ -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);
+	uint32 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 16693)
+++ 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.szMidiGlb[MIDIOUT_PAN]);
+		//ProcessMIDIMacro(m_PlayState, nChn, false, m_MidiCfg.szMidiGlb[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.szMidiSFXExt[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.szMidiZXXExt[(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)
 	{
MacroRewrite-2.patch (57,344 bytes)   
Saga Musix

Saga Musix

2022-01-25 22:43

administrator   ~0005012

MacroRewrite-3.patch (59,991 bytes)   
Index: mptrack/EffectInfo.cpp
===================================================================
--- mptrack/EffectInfo.cpp	(revision 16693)
+++ 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 16693)
+++ mptrack/MIDIMacroDialog.cpp	(working copy)
@@ -310,7 +310,7 @@
 		{
 			CString s;
 			m_EditSFx.GetWindowText(s);
-			mpt::String::WriteAutoBuf(m_MidiCfg.szMidiSFXExt[sfx]) = mpt::ToCharset(mpt::Charset::ASCII, s);
+			m_MidiCfg.szMidiSFXExt[sfx] = mpt::ToCharset(mpt::Charset::ASCII, s);
 
 			int sfx_preset = m_MidiCfg.GetParameteredMacroType(sfx);
 			m_CbnSFxPreset.SetCurSel(sfx_preset);
@@ -330,7 +330,7 @@
 		{
 			CString s;
 			m_EditZxx.GetWindowText(s);
-			mpt::String::WriteAutoBuf(m_MidiCfg.szMidiZXXExt[zxx]) = mpt::ToCharset(mpt::Charset::ASCII, s);
+			m_MidiCfg.szMidiZXXExt[zxx] = mpt::ToCharset(mpt::Charset::ASCII, s);
 			m_CbnZxxPreset.SetCurSel(m_MidiCfg.GetFixedMacroType());
 		}
 	}
@@ -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 16693)
+++ mptrack/MIDIMacroDialog.h	(working copy)
@@ -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 16693)
+++ 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 16693)
+++ 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;
Index: mptrack/MPTHacks.cpp
===================================================================
--- mptrack/MPTHacks.cpp	(revision 16693)
+++ 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 16693)
+++ mptrack/TrackerSettings.cpp	(working copy)
@@ -443,11 +443,11 @@
 	theApp.GetDefaultMidiMacro(macros);
 	for(int isfx = 0; isfx < 16; isfx++)
 	{
-		mpt::String::WriteAutoBuf(macros.szMidiSFXExt[isfx]) = conf.Read<std::string>(U_("Zxx Macros"), MPT_UFORMAT("SF{}")(mpt::ufmt::HEX(isfx)), macros.szMidiSFXExt[isfx]);
+		macros.szMidiSFXExt[isfx] = conf.Read<std::string>(U_("Zxx Macros"), MPT_UFORMAT("SF{}")(mpt::ufmt::HEX(isfx)), macros.szMidiSFXExt[isfx]);
 	}
 	for(int izxx = 0; izxx < 128; izxx++)
 	{
-		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.szMidiZXXExt[izxx] = conf.Read<std::string>(U_("Zxx Macros"), MPT_UFORMAT("Z{}")(mpt::ufmt::HEX0<2>(izxx | 0x80)), macros.szMidiZXXExt[izxx]);
 	}
 
 
Index: soundlib/Fastmix.cpp
===================================================================
--- soundlib/Fastmix.cpp	(revision 16693)
+++ 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 16693)
+++ 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.szMidiZXXExt[i     ] = MPT_AFORMAT("F0F080{}")(mpt::afmt::HEX0<2>(param));
+			m_MidiCfg.szMidiZXXExt[i + 32] = MPT_AFORMAT("F0F081{}")(mpt::afmt::HEX0<2>(param));
+			m_MidiCfg.szMidiZXXExt[i + 64] = MPT_AFORMAT("F0F082{}")(mpt::afmt::HEX0<2>(param));
+			m_MidiCfg.szMidiZXXExt[i + 96] = MPT_AFORMAT("F0F083{}")(mpt::afmt::HEX0<2>(param));
 		}
 	}
 #endif // NO_PLUGINS
Index: soundlib/Load_med.cpp
===================================================================
--- soundlib/Load_med.cpp	(revision 16693)
+++ 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.szMidiSFXExt[0] = "Cc z";
 
 	file.Rewind();
 	PATTERNINDEX basePattern = 0;
@@ -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.szMidiZXXExt[dump] = macro;
 				}
 			}
 		}
Index: soundlib/Load_mo3.cpp
===================================================================
--- soundlib/Load_mo3.cpp	(revision 16693)
+++ 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.szMidiSFXExt[i] = MPT_AFORMAT("F0F0{}z")(mpt::afmt::HEX0<2>(fileHeader.sfxMacros[i] - 1));
 			else
-				mpt::String::WriteAutoBuf(m_MidiCfg.szMidiSFXExt[i]) = "";
+				m_MidiCfg.szMidiSFXExt[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.szMidiZXXExt[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.szMidiZXXExt[i] = "";
 		}
 	}
 
Index: soundlib/Load_symmod.cpp
===================================================================
--- soundlib/Load_symmod.cpp	(revision 16693)
+++ 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)
 	{
Index: soundlib/MIDIMacros.cpp
===================================================================
--- soundlib/MIDIMacros.cpp	(revision 16693)
+++ soundlib/MIDIMacros.cpp	(working copy)
@@ -9,9 +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
@@ -23,7 +22,7 @@
 
 ParameteredMacro MIDIMacroConfig::GetParameteredMacroType(uint32 macroIndex) const
 {
-	const std::string macro = GetSafeMacro(szMidiSFXExt[macroIndex]);
+	const std::string macro = szMidiSFXExt[macroIndex].NormalizedString();
 
 	for(uint32 i = 0; i < kSFxMax; i++)
 	{
@@ -60,7 +59,7 @@
 			bool found = true;
 			for(uint32 j = 0; j < 128; j++)
 			{
-				if(strncmp(macros[j], szMidiZXXExt[j], MACRO_LENGTH))
+				if(macros[j] != szMidiZXXExt[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();
@@ -100,7 +99,7 @@
 {
 	Macro parameteredMacro;
 	CreateParameteredMacro(parameteredMacro, macroType, subType);
-	return mpt::String::ReadAutoBuf(parameteredMacro);
+	return parameteredMacro;
 }
 
 
@@ -113,44 +112,44 @@
 		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 = szMidiSFXExt[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 = szMidiSFXExt[macroIndex].NormalizedString();
 
 	int code = 0;
 	const char *param = macro.c_str();
@@ -344,11 +343,11 @@
 	MemsetZero(szMidiSFXExt);
 	MemsetZero(szMidiZXXExt);
 
-	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");
+	szMidiGlb[MIDIOUT_START] = "FF";
+	szMidiGlb[MIDIOUT_STOP] = "FC";
+	szMidiGlb[MIDIOUT_NOTEON] = "9c n v";
+	szMidiGlb[MIDIOUT_NOTEOFF] = "9c n 0";
+	szMidiGlb[MIDIOUT_PROGRAM] = "Cc p";
 	// SF0: Z00-Z7F controls cutoff
 	CreateParameteredMacro(0, kSFxCutoff);
 	// Z80-Z8F controls resonance
@@ -369,45 +368,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');
+		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)
@@ -419,4 +398,38 @@
 }
 
 
+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 16693)
+++ soundlib/MIDIMacros.h	(working copy)
@@ -86,19 +86,81 @@
 
 struct MIDIMacroConfigData
 {
-	typedef char Macro[MACRO_LENGTH];
+	struct Macro
+	{
+	public:
+		using RawType = std::array<char, MACRO_LENGTH>;
+
+		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()};
+		}
+
+		size_t Length() const noexcept
+		{
+			return static_cast<size_t>(std::distance(m_data.begin(), std::find(m_data.begin(), m_data.end(), '\0')));
+		}
+
+		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;
+	};
+
 	// encoding is ASCII
-	Macro szMidiGlb[9];      // Global MIDI macros
-	Macro szMidiSFXExt[16];  // Parametric MIDI macros
-	Macro szMidiZXXExt[128]; // Fixed MIDI macros
+	Macro szMidiGlb[9];              // Global MIDI macros
+	Macro szMidiSFXExt[NUM_MACROS];  // Parametric MIDI macros
+	Macro szMidiZXXExt[128];         // Fixed MIDI macros
 
-	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 *begin() noexcept { return std::begin(szMidiGlb); }
+	const Macro *begin() const noexcept { return std::begin(szMidiGlb); }
+	Macro *end() noexcept { return std::end(szMidiZXXExt); }
+	const Macro *end() const noexcept { return std::end(szMidiZXXExt); }
 };
 
-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
 {
@@ -162,15 +224,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 16693)
+++ 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 16693)
+++ 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 16693)
+++ 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 16693)
+++ 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;
 }
Index: soundlib/plugins/PlugInterface.h
===================================================================
--- soundlib/plugins/PlugInterface.h	(revision 16693)
+++ 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 16693)
+++ 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.szMidiSFXExt[chn.nActiveMacro], chn.rowCommand.param, 0);
+				else
+					ProcessMIDIMacro(playState, nChn, false, m_MidiCfg.szMidiZXXExt[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,103 @@
 
 // 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 = mpt::as_span(out.data() + sendPos, sendLen);
+		uint32 bytesSent = 0;
+		// Local-only messages are messages that can be processed directly in the replay routines without sending stuff to plugins.
+		bytesSent = SendMIDIData(playState, nChn, isSmooth, midiMsg, 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;
+	}
+}
+
+
+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
+	size_t pos = 0;
+	for(; 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 +4929,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 +4939,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 +4951,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 +5027,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 +5037,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 +5068,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 +5084,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,28 +5107,29 @@
 
 
 // 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)
+uint32 CSoundFile::SendMIDIData(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const mpt::span<const unsigned char> macro, PLUGINDEX plugin)
 {
-	if(macroLen < 1)
-	{
+	if(macro.size() < 1)
 		return 0;
-	}
 
+	// 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];
+	ModChannel &chn = playState.Chn[nChn];
 	if(macro[0] == 0xF0 && (macro[1] == 0xF0 || macro[1] == 0xF1))
 	{
 		// Internal device.
-		if(macroLen < 4)
+		if(macro.size() < 4)
 		{
 			return 0;
 		}
@@ -5113,16 +5141,13 @@
 		{
 			// 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);
@@ -5133,12 +5158,9 @@
 		{
 			// 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]);
 
@@ -5157,17 +5179,17 @@
 		} 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;
@@ -5174,21 +5196,21 @@
 		} 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;
+					const PlugParamIndex plugParam = isExtended ? (0x80 + macroCode) : (macroCode & 0x7F);
+					const PlugParamValue fParam = param / 127.0f;
+					if(localOnly)
+						playState.m_midiMacroEvaluationResults->pluginParameter[{plug, plugParam}] = fParam;
 					if(!isSmooth)
-					{
 						pPlugin->SetParameter(plugParam, fParam);
-					} else
-					{
-						pPlugin->SetParameter(plugParam, CalculateSmoothParamChange(pPlugin->GetParameter(plugParam), fParam));
-					}
+					else
+						pPlugin->SetParameter(plugParam, CalculateSmoothParamChange(playState, pPlugin->GetParameter(plugParam), fParam));
 				}
 			}
 
@@ -5198,7 +5220,7 @@
 
 		// 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 +5230,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 +5240,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);
 					}
 				}
@@ -5233,7 +5255,7 @@
 		MPT_UNREFERENCED_PARAMETER(plugin);
 #endif // NO_PLUGINS
 
-		return macroLen;
+		return static_cast<uint32>(macro.size());
 	}
 
 	return 0;
@@ -6165,7 +6187,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 +6199,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 +6224,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 +6236,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 +6253,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 +6277,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 +6288,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 16693)
+++ soundlib/Sndfile.cpp	(working copy)
@@ -70,6 +70,13 @@
 }
 
 
+CSoundFile::PlayState::PlayState()
+{
+	std::fill(std::begin(Chn), std::end(Chn), ModChannel{});
+	m_midiMacroScratchSpace.reserve(MACRO_LENGTH);  // 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 16693)
+++ 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);
+	uint32 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 16693)
+++ 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.szMidiGlb[MIDIOUT_PAN]);
+		//ProcessMIDIMacro(m_PlayState, nChn, false, m_MidiCfg.szMidiGlb[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.szMidiSFXExt[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.szMidiZXXExt[(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)
 	{
MacroRewrite-3.patch (59,991 bytes)   
Saga Musix

Saga Musix

2022-01-26 22:58

administrator   ~0005017

MacroRewrite-4.patch (76,121 bytes)   
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));
 	}
 
MacroRewrite-4.patch (76,121 bytes)   
Saga Musix

Saga Musix

2022-01-27 17:56

administrator   ~0005019

MacroRewrite-5.patch (76,028 bytes)   
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);
+					std::array<char, kMacroLength> 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] = std::string_view{macro.data(), length * 2};
 				}
 			}
 		}
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,76 @@
 
 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 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 +176,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 +222,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));
 	}
 
MacroRewrite-5.patch (76,028 bytes)   
Saga Musix

Saga Musix

2022-01-27 19:44

administrator   ~0005020

Implemented in r16716.

Issue History

Date Modified Username Field Change
2021-08-31 20:37 Saga Musix New Issue
2021-08-31 20:37 Saga Musix Assigned To => Saga Musix
2021-08-31 20:37 Saga Musix Status new => assigned
2022-01-24 22:42 Saga Musix Note Added: 0005006
2022-01-24 22:42 Saga Musix File Added: MacroRewrite.patch
2022-01-24 22:43 Saga Musix Target Version => OpenMPT 1.31 / libopenmpt 0.7 (goals)
2022-01-25 22:04 Saga Musix Note Added: 0005011
2022-01-25 22:04 Saga Musix File Added: MacroRewrite-2.patch
2022-01-25 22:43 Saga Musix Note Added: 0005012
2022-01-25 22:43 Saga Musix File Added: MacroRewrite-3.patch
2022-01-26 22:58 Saga Musix Note Added: 0005017
2022-01-26 22:58 Saga Musix File Added: MacroRewrite-4.patch
2022-01-27 17:56 Saga Musix Note Added: 0005019
2022-01-27 17:56 Saga Musix File Added: MacroRewrite-5.patch
2022-01-27 19:44 Saga Musix Status assigned => resolved
2022-01-27 19:44 Saga Musix Resolution open => fixed
2022-01-27 19:44 Saga Musix Fixed in Version => OpenMPT 1.31.00.* (current testing)
2022-01-27 19:44 Saga Musix Note Added: 0005020
2022-01-28 19:30 Saga Musix Fixed in Version OpenMPT 1.31.00.* (current testing) => OpenMPT 1.30.02.00 / libopenmpt 0.6.1 (upgrade first)
2022-01-28 19:30 Saga Musix Target Version OpenMPT 1.31 / libopenmpt 0.7 (goals) => OpenMPT 1.30.02.00 / libopenmpt 0.6.1 (upgrade first)