/*
 * SampleFormatSFZ.cpp
 * -------------------
 * Purpose: Loading and saving SFZ instruments.
 * Notes  : (currently none)
 * Authors: OpenMPT Devs
 * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
 */


#include "stdafx.h"
#include "Sndfile.h"
#ifdef MODPLUG_TRACKER
#include "../mptrack/TrackerSettings.h"
#endif // MODPLUG_TRACKER
#ifndef MODPLUG_NO_FILESAVE
#include "../common/mptFileIO.h"
#endif // !MODPLUG_NO_FILESAVE
#include "modsmp_ctrl.h"
#include "mpt/base/numbers.hpp"

#include <functional>

OPENMPT_NAMESPACE_BEGIN

#ifdef MPT_EXTERNAL_SAMPLES

template<size_t N>
static bool SFZStartsWith(const std::string_view &l, const char(&r)[N])
{
	return l.substr(0, N - 1) == r;
}

template <size_t N>
static bool SFZEndsWith(const std::string_view &l, const char (&r)[N])
{
	return l.size() >= (N - 1) && l.substr(l.size() - (N - 1), N - 1) == r;
}

static bool SFZIsNumeric(const std::string_view &str)
{
	return std::find_if(str.begin(), str.end(), [](char c) { return c < '0' || c > '9'; }) == str.end();
}

struct SFZControl
{
	std::string defaultPath;
	int8 octaveOffset = 0, noteOffset = 0;

	void Parse(const std::string_view key, const std::string &value)
	{
		if(key == "default_path")
			defaultPath = value;
		else if(key == "octave_offset")
			octaveOffset = ConvertStrTo<int8>(value);
		else if(key == "note_offset")
			noteOffset = ConvertStrTo<int8>(value);
	}
};

struct SFZFlexEG
{
	using PointIndex = decltype(InstrumentEnvelope().nLoopStart);

	std::vector<std::pair<double, double>> points;
	double amplitude = 0;  // percentage (100 = full volume range)
	double pan = 0;        // percentage (100 = full pan range)
	double pitch = 0;      // in cents
	double cutoff = 0;     // in cents
	PointIndex sustain = 0;

	void Parse(std::string_view key, const std::string &value)
	{
		key = key.substr(key.find('_') + 1);
		const double v = ConvertStrTo<double>(value);

		const bool isTime = SFZStartsWith(key, "time"), isLevel = SFZStartsWith(key, "level");
		std::string_view pointStr;
		if(isTime)
			pointStr = key.substr(4);
		else if(isLevel)
			pointStr = key.substr(5);

		if(!pointStr.empty() && SFZIsNumeric(pointStr))
		{
			PointIndex point = ConvertStrTo<PointIndex>(std::string(pointStr));
			if(point >= points.size() && point < MAX_ENVPOINTS)
				points.resize(point + 1);

			if(point < points.size())
			{
				if(isTime)
					points[point].first = v;
				else
					points[point].second = v;
			}
			return;
		}

		if(key == "points")
			points.resize(std::min(static_cast<PointIndex>(v), static_cast<PointIndex>(MAX_ENVPOINTS)));
		else if(key == "sustain")
			sustain = mpt::saturate_round<PointIndex>(v);
		else if(key == "amplitude" || key == "ampeg")
			amplitude = v;
		else if(key == "pan")
			pan = v;
		else if(key == "pitch")
			pitch = v;
		else if(key == "cutoff")
			cutoff = v;
	}

	void ConvertToMPT(ModInstrument *ins, const CSoundFile &sndFile) const
	{
		if(amplitude)
			ConvertToMPT(ins, sndFile, ENV_VOLUME, amplitude / 100.0, 0.0, 1.0);
		if(pan)
			ConvertToMPT(ins, sndFile, ENV_PANNING, pan / 100.0, -1.0, 1.0);
		if(pitch)
			ConvertToMPT(ins, sndFile, ENV_PITCH, pitch / 1600.0, -1.0, 1.0);
		if(cutoff)
			ConvertToMPT(ins, sndFile, ENV_PITCH, cutoff, 0.0, 1.0, true);
	}

	void ConvertToMPT(ModInstrument *ins, const CSoundFile &sndFile, EnvelopeType envType, double scale, double minVal, double maxVal, bool forceFilter = false) const
	{
		const double tickDuration = sndFile.m_PlayState.m_nSamplesPerTick / static_cast<double>(sndFile.GetSampleRate());
		if(tickDuration <= 0 || points.empty() || scale == 0.0)
			return;

		auto &env = ins->GetEnvelope(envType);
		std::function<double(double)> conversionFunc = Identity;
		if(forceFilter && envType == ENV_PITCH)
		{
			env.dwFlags.set(ENV_FILTER);
			conversionFunc = FilterConversionFunc(*ins, sndFile);
		}

		env.clear();
		env.reserve(points.size());

		const auto ToValue = std::bind(SFZFlexEG::ToValue, std::placeholders::_1, scale, minVal, maxVal, conversionFunc);

		int32 prevTick = -1;
		// If the first envelope point's time is greater than 0, we fade in from a neutral value
		if(points.front().first > 0)
		{
			env.push_back({0, ToValue(0.0)});
			prevTick = 0;
		}

		for(const auto &point : points)
		{
			const auto tick = mpt::saturate_cast<EnvelopeNode::tick_t>(prevTick + ToTicks(point.first, tickDuration));
			const auto value = ToValue(point.second);
			env.push_back({tick, value});
			prevTick = tick;
			if(tick == Util::MaxValueOfType(tick))
				break;
		}

		if(sustain < env.size())
		{
			env.nSustainStart = env.nSustainEnd = sustain;
			env.dwFlags.set(ENV_SUSTAIN);
		} else
		{
			env.dwFlags.reset(ENV_SUSTAIN);
		}
		env.dwFlags.set(ENV_ENABLED);

		if(envType == ENV_VOLUME && env.nSustainEnd > 0)
			env.nReleaseNode = env.nSustainEnd;
	}

protected:
	static EnvelopeNode::tick_t ToTicks(double duration, double tickDuration)
	{
		return std::max(EnvelopeNode::tick_t(1), mpt::saturate_round<EnvelopeNode::tick_t>(duration / tickDuration));
	}

	static EnvelopeNode::value_t ToValue(double value, double scale, double minVal, double maxVal, const std::function<double(double)> &conversionFunc)
	{
		value = conversionFunc((value * scale - minVal) / (maxVal - minVal)) * ENVELOPE_MAX + ENVELOPE_MIN;
		Limit<double, double>(value, ENVELOPE_MIN, ENVELOPE_MAX);
		return mpt::saturate_round<EnvelopeNode::value_t>(value);
	}

	static double Identity(double v) noexcept { return v; }

	static double CentsToFilterCutoff(double v, const CSoundFile &sndFile, int envBaseCutoff, uint32 envBaseFreq)
	{
		const auto freq = envBaseFreq * std::pow(2.0, v / 1200.0);
		return Util::muldivr(sndFile.FrequencyToCutOff(freq), 127, envBaseCutoff) / 127.0;
	}

	static std::function<double(double)> FilterConversionFunc(const ModInstrument &ins, const CSoundFile &sndFile)
	{
		const auto envBaseCutoff = ins.IsCutoffEnabled() ? ins.GetCutoff() : 127;
		const auto envBaseFreq = sndFile.CutOffToFrequency(envBaseCutoff);
		return std::bind(CentsToFilterCutoff, std::placeholders::_1, std::cref(sndFile), envBaseCutoff, envBaseFreq);
	}
};

struct SFZEnvelope
{
	double startLevel = 0, delay = 0, attack = 0, hold = 0;
	double decay = 0, sustainLevel = 100, release = 0, depth = 0;

	void Parse(std::string_view key, const std::string &value)
	{
		key = key.substr(key.find('_') + 1);
		double v = ConvertStrTo<double>(value);
		if(key == "depth")
			Limit(v, -12000.0, 12000.0);
		else if(key == "start" || key == "sustain")
			Limit(v, -100.0, 100.0);
		else
			Limit(v, 0.0, 100.0);

		if(key == "start")
			startLevel = v;
		else if(key == "delay")
			delay = v;
		else if(key == "attack")
			attack = v;
		else if(key == "hold")
			hold = v;
		else if(key == "decay")
			decay = v;
		else if(key == "sustain")
			sustainLevel = v;
		else if(key == "release")
			release = v;
		else if(key == "depth")
			depth = v;
	}

	void ConvertToMPT(ModInstrument *ins, const CSoundFile &sndFile, EnvelopeType envType, bool forceFilter = false) const
	{
		SFZFlexEG eg;
		if(envType == ENV_VOLUME)
			eg.amplitude = 1.0;
		else if(envType == ENV_PITCH && !forceFilter)
			eg.pitch = depth / 100.0;
		else if(envType == ENV_PITCH && forceFilter)
			eg.cutoff = depth / 100.0;

		auto &env = eg.points;
		if(attack > 0 || delay > 0)
		{
			env.push_back({0.0, startLevel});
			if(delay > 0)
				env.push_back({delay, env.back().second});
			env.push_back({attack, 100.0});
		}
		if(hold > 0)
		{
			if(env.empty())
				env.push_back({0.0, 100.0});
			env.push_back({hold, env.back().second});
		}
		if(env.empty())
			env.push_back({0.0, 100.0});
		if(env.back().second != sustainLevel)
			env.push_back({decay, sustainLevel});
		if(sustainLevel != 0)
		{
			eg.sustain = static_cast<SFZFlexEG::PointIndex>(env.size() - 1);
			env.push_back({release, 0.0});
		} else
		{
			eg.sustain = std::numeric_limits<SFZFlexEG::PointIndex>::max();
		}

		eg.ConvertToMPT(ins, sndFile);
	}
};


struct SFZRegion
{
	enum class LoopMode
	{
		kUnspecified,
		kContinuous,
		kOneShot,
		kSustain,
		kNoLoop
	};

	enum class LoopType
	{
		kUnspecified,
		kForward,
		kBackward,
		kAlternate,
	};

	size_t filenameOffset = 0;
	std::string filename, name;
	SFZEnvelope ampEnv, pitchEnv, filterEnv;
	std::vector<SFZFlexEG> flexEGs;
	SmpLength loopStart = 0, loopEnd = 0;
	SmpLength end = MAX_SAMPLE_LENGTH, offset = 0;
	LoopMode loopMode = LoopMode::kUnspecified;
	LoopType loopType = LoopType::kUnspecified;
	double loopCrossfade = 0.0;
	double cutoff = 0;         // in Hz
	double resonance = 0;      // 0...40dB
	double filterRandom = 0;   // 0...9600 cents
	double volume = 0;         // -144dB...+6dB
	double amplitude = 100.0;  // 0...100
	double pitchBend = 200;    // -9600...9600 cents
	double pitchLfoFade = 0;   // 0...100 seconds
	double pitchLfoDepth = 0;  // -1200...12000
	double pitchLfoFreq = 0;   // 0...20 Hz
	double panning = -128;     // -100...+100
	double finetune = 0;       // in cents
	int8 transpose = 0;
	uint8 keyLo = 0, keyHi = 127, keyRoot = 60;
	FilterMode filterType = FilterMode::Unchanged;
	uint8 polyphony = 255;
	bool useSampleKeyRoot = false;
	bool invertPhase = false;

	template<typename T, typename Tc>
	static void Read(const std::string &valueStr, T &value, Tc valueMin = std::numeric_limits<T>::min(), Tc valueMax = std::numeric_limits<T>::max())
	{
		double valueF = ConvertStrTo<double>(valueStr);
		if constexpr(std::numeric_limits<T>::is_integer)
		{
			valueF = mpt::round(valueF);
		}
		Limit(valueF, static_cast<double>(valueMin), static_cast<double>(valueMax));
		value = static_cast<T>(valueF);
	}

	static uint8 ReadKey(const std::string &value, const SFZControl &control)
	{
		if(value.empty())
			return 0;

		int key = 0;
		if(value[0] >= '0' && value[0] <= '9')
		{
			// MIDI key
			key = ConvertStrTo<uint8>(value);
		} else if(value.length() < 2)
		{
			return 0;
		} else
		{
			// Scientific pitch
			static constexpr int8 keys[] = { 9, 11, 0, 2, 4, 5, 7 };
			static_assert(std::size(keys) == 'g' - 'a' + 1);
			auto keyC = value[0];
			if(keyC >= 'A' && keyC <= 'G')
				key = keys[keyC - 'A'];
			if(keyC >= 'a' && keyC <= 'g')
				key = keys[keyC - 'a'];
			else
				return 0;

			uint8 octaveOffset = 1;
			if(value[1] == '#')
			{
				key++;
				octaveOffset = 2;
			} else if(value[1] == 'b' || value[1] == 'B')
			{
				key--;
				octaveOffset = 2;
			}
			if(octaveOffset >= value.length())
				return 0;

			int8 octave = ConvertStrTo<int8>(value.c_str() + octaveOffset);
			key += (octave + 1) * 12;
		}
		key += control.octaveOffset * 12 + control.noteOffset;
		return static_cast<uint8>(Clamp(key, 0, 127));
	}

	void Parse(const std::string_view key, const std::string &value, const SFZControl &control)
	{
		if(key == "sample")
		{
			filename = control.defaultPath + value;
			filenameOffset = control.defaultPath.size();
		}
		else if(key == "region_label")
			name = value;
		else if(key == "lokey")
			keyLo = ReadKey(value, control);
		else if(key == "hikey")
			keyHi = ReadKey(value, control);
		else if(key == "pitch_keycenter")
		{
			keyRoot = ReadKey(value, control);
			useSampleKeyRoot = (value == "sample");
		}
		else if(key == "key")
		{
			keyLo = keyHi = keyRoot = ReadKey(value, control);
			useSampleKeyRoot = false;
		}
		else if(key == "bend_up" || key == "bendup")
			Read(value, pitchBend, -9600.0, 9600.0);
		else if(key == "pitchlfo_fade")
			Read(value, pitchLfoFade, 0.0, 100.0);
		else if(key == "pitchlfo_depth")
			Read(value, pitchLfoDepth, -12000.0, 12000.0);
		else if(key == "pitchlfo_freq")
			Read(value, pitchLfoFreq, 0.0, 20.0);
		else if(key == "volume")
			Read(value, volume, -144.0, 6.0);
		else if(key == "amplitude")
			Read(value, amplitude, 0.0, 100.0);
		else if(key == "pan")
			Read(value, panning, -100.0, 100.0);
		else if(key == "transpose")
			Read(value, transpose, -127, 127);
		else if(key == "tune")
			Read(value, finetune, -100.0, 100.0);
		else if(key == "end")
			Read(value, end, SmpLength(0), MAX_SAMPLE_LENGTH);
		else if(key == "offset")
			Read(value, offset, SmpLength(0), MAX_SAMPLE_LENGTH);
		else if(key == "loop_start" || key == "loopstart")
			Read(value, loopStart, SmpLength(0), MAX_SAMPLE_LENGTH);
		else if(key == "loop_end" || key == "loopend")
			Read(value, loopEnd, SmpLength(0), MAX_SAMPLE_LENGTH);
		else if(key == "loop_crossfade" || key == "loopcrossfade")
			Read(value, loopCrossfade, 0.0, DBL_MAX);
		else if(key == "loop_mode" || key == "loopmode")
		{
			if(value == "loop_continuous")
				loopMode = LoopMode::kContinuous;
			else if(value == "one_shot")
				loopMode = LoopMode::kOneShot;
			else if(value == "loop_sustain")
				loopMode = LoopMode::kSustain;
			else if(value == "no_loop")
				loopMode = LoopMode::kNoLoop;
		}
		else if(key == "loop_type" || key == "looptype")
		{
			if(value == "forward")
				loopType = LoopType::kForward;
			else if(value == "backward")
				loopType = LoopType::kBackward;
			else if(value == "alternate")
				loopType = LoopType::kAlternate;
		}
		else if(key == "cutoff")
			Read(value, cutoff, 0.0, 96000.0);
		else if(key == "fil_random")
			Read(value, filterRandom, 0.0, 9600.0);
		else if(key == "resonance")
			Read(value, resonance, 0.0, 40.0);
		else if(key == "polyphony")
			Read(value, polyphony, 0, 255);
		else if(key == "phase")
			invertPhase = (value == "invert");
		else if(key == "fil_type" || key == "filtype")
		{
			if(value == "lpf_1p" || value == "lpf_2p" || value == "lpf_4p" || value == "lpf_6p")
				filterType = FilterMode::LowPass;
			else if(value == "hpf_1p" || value == "hpf_2p" || value == "hpf_4p" || value == "hpf_6p")
				filterType = FilterMode::HighPass;
			// Alternatives: bpf_2p, brf_2p
		}
		else if(SFZStartsWith(key, "ampeg_"))
			ampEnv.Parse(key, value);
		else if(SFZStartsWith(key, "fileg_"))
			filterEnv.Parse(key, value);
		else if(SFZStartsWith(key, "pitcheg_"))
			pitchEnv.Parse(key, value);
		else if(SFZStartsWith(key, "eg") && SFZIsNumeric(key.substr(2, 2)) && key.substr(4, 1) == "_")
		{
			uint8 eg = ConvertStrTo<uint8>(std::string(key.substr(2, 2)));
			if(eg >= flexEGs.size())
				flexEGs.resize(eg + 1);
			flexEGs[eg].Parse(key, value);
		}
	}
};

struct SFZInputFile
{
	FileReader file;
	std::unique_ptr<InputFile> inputFile;  // FileReader has pointers into this so its address must not change
	std::string remain;

	SFZInputFile(FileReader f = {}, std::unique_ptr<InputFile> i = {}, std::string r = {})
		: file{std::move(f)}, inputFile{std::move(i)}, remain{std::move(r)} {}
	SFZInputFile(SFZInputFile &&) = default;
};

bool CSoundFile::ReadSFZInstrument(INSTRUMENTINDEX nInstr, FileReader &file)
{
	file.Rewind();

	enum { kNone, kGlobal, kMaster, kGroup, kRegion, kControl, kCurve, kEffect, kUnknown } section = kNone;
	bool inMultiLineComment = false;
	SFZControl control;
	SFZRegion group, master, globals;
	std::vector<SFZRegion> regions;
	std::map<std::string, std::string> macros;
	std::vector<SFZInputFile> files;
	files.emplace_back(file);

	std::string s;
	while(!files.empty())
	{
		if(!files.back().file.ReadLine(s, 1024))
		{
			// Finished reading file, so back to remaining characters of the #include line from the previous file
			s = std::move(files.back().remain);
			files.pop_back();
		}

		if(inMultiLineComment)
		{
			if(auto commentEnd = s.find("*/"); commentEnd != std::string::npos)
			{
				s.erase(0, commentEnd + 2);
				inMultiLineComment = false;
			} else
			{
				continue;
			}
		}

		// First, terminate line at the start of a comment block
		if(auto commentPos = s.find("//"); commentPos != std::string::npos)
		{
			s.resize(commentPos);
		}

		// Now, read the tokens.
		// This format is so funky that no general tokenizer approach seems to work here...
		// Consider this jolly good example found at https://stackoverflow.com/questions/5923895/tokenizing-a-custom-text-file-format-file-using-c-sharp
		// <region>sample=piano C3.wav key=48 ampeg_release=0.7 // a comment here
		// <region>key = 49 sample = piano Db3.wav
		// <region>
		// group=1
		// key = 48
		//     sample = piano D3.ogg
		// The original sfz specification claims that spaces around = are not allowed, but a quick look into the real world tells us otherwise.

		while(!s.empty())
		{
			s.erase(0, s.find_first_not_of(" \t"));

			const bool isDefine = SFZStartsWith(s, "#define ") || SFZStartsWith(s, "#define\t");

			// Replace macros (unless this is a #define statement, to allow for macro re-definition)
			if(!isDefine)
			{
				for(const auto &[oldStr, newStr] : macros)
				{
					std::string::size_type pos = 0;
					while((pos = s.find(oldStr, pos)) != std::string::npos)
					{
						s.replace(pos, oldStr.length(), newStr);
						pos += newStr.length();
					}
				}
			}

			if(s.empty())
				break;

			std::string::size_type charsRead = 0;

			if(s[0] == '<' && (charsRead = s.find('>')) != std::string::npos)
			{
				// Section header
				const auto sec = std::string_view(s).substr(1, charsRead - 1);
				section = kUnknown;
				if(sec == "global")
				{
					section = kGlobal;
					// Reset global parameters
					globals = SFZRegion();
				} else if(sec == "master")
				{
					section = kMaster;
					// Reset master parameters
					master = globals;
				} else if(sec == "group")
				{
					section = kGroup;
					// Reset group parameters
					group = master;
				} else if(sec == "region")
				{
					section = kRegion;
					regions.push_back(group);
				} else if(sec == "control")
				{
					section = kControl;
				} else if(sec == "curve")
				{
					section = kCurve;
				} else if(sec == "effect")
				{
					section = kEffect;
				}
				charsRead++;
			} else if(isDefine)
			{
				// Macro definition
				charsRead += 8;
				auto keyStart = s.find_first_not_of(" \t", 8);
				auto keyEnd = s.find_first_of(" \t", keyStart);
				auto valueStart = s.find_first_not_of(" \t", keyEnd);
				if(keyStart != std::string::npos && valueStart != std::string::npos)
				{
					charsRead = s.find_first_of(" \t", valueStart);
					const auto key = s.substr(keyStart, keyEnd - keyStart);
					if(key.length() > 1 && key[0] == '$')
						macros[std::move(key)] = s.substr(valueStart, charsRead - valueStart);
				} else
				{
					break;
				}
			} else if(SFZStartsWith(s, "#include ") || SFZStartsWith(s, "#include\t"))
			{
				// Include other sfz file
				auto fileStart = s.find("\"", 9);  // Yes, there can be arbitrary characters before the opening quote, at least that's how sforzando does it.
				auto fileEnd = s.find("\"", fileStart + 1);
				if(fileStart != std::string::npos && fileEnd != std::string::npos)
				{
					charsRead = fileEnd + 1;
					fileStart++;
				} else
				{
					break;
				}

				std::string filenameU8 = s.substr(fileStart, fileEnd - fileStart);
				mpt::PathString filename = mpt::PathString::FromUTF8(filenameU8);
				if(!filename.empty())
				{
					if(filenameU8.find(':') == std::string::npos)
						filename = file.GetOptionalFileName().value_or(P_("")).GetPath() + filename;
					filename = filename.Simplify();
					// Avoid recursive #include
					if(std::find_if(files.begin(), files.end(), [&filename](const SFZInputFile &f) { return f.file.GetOptionalFileName().value_or(P_("")) == filename; }) == files.end())
					{
						auto f = std::make_unique<InputFile>(filename);
						if(f->IsValid())
						{
							s.erase(0, charsRead);
							files.emplace_back(GetFileReader(*f), std::move(f), std::move(s));
							break;
						} else
						{
							AddToLog(LogWarning, U_("Unable to load include file: ") + filename.ToUnicode());
						}
					} else
					{
						AddToLog(LogWarning, U_("Recursive include file ignored: ") + filename.ToUnicode());
					}
				}
			} else if(SFZStartsWith(s, "/*"))
			{
				// Multi-line comment
				if(auto commentEnd = s.find("*/", charsRead + 2); commentEnd != std::string::npos)
				{
					charsRead = commentEnd;
				} else
				{
					inMultiLineComment = true;
					charsRead = s.length();
				}
			} else if(section == kNone)
			{
				// Garbage before any section, probably not an sfz file
				return false;
			} else if(s.find('=') != std::string::npos)
			{
				// Read key=value pair
				auto keyEnd = s.find_first_of(" \t=");
				auto valueStart = s.find_first_not_of(" \t=", keyEnd);
				if(valueStart == std::string::npos)
				{
					break;
				}
				const std::string key = mpt::ToLowerCaseAscii(s.substr(0, keyEnd));
				// Currently defined *_label opcodes are global_label, group_label, master_label, region_label, sw_label
				if(key == "sample" || key == "default_path" || SFZStartsWith(key, "label_cc") || SFZStartsWith(key, "label_key") || SFZEndsWith(key, "_label"))
				{
					// Sample / CC name may contain spaces...
					charsRead = s.find_first_of("=\t<", valueStart);
					if(charsRead != std::string::npos && s[charsRead] == '=')
					{
						// Backtrack to end of key
						while(charsRead > valueStart && s[charsRead] == ' ')
							charsRead--;
						// Backtrack to start of key
						while(charsRead > valueStart && s[charsRead] != ' ')
							charsRead--;
					}
				} else
				{
					charsRead = s.find_first_of(" \t<", valueStart);
				}
				const std::string value = s.substr(valueStart, charsRead - valueStart);

				switch(section)
				{
				case kGlobal:
					globals.Parse(key, value, control);
					[[fallthrough]];
				case kMaster:
					master.Parse(key, value, control);
					[[fallthrough]];
				case kGroup:
					group.Parse(key, value, control);
					break;
				case kRegion:
					regions.back().Parse(key, value, control);
					break;
				case kControl:
					control.Parse(key, value);
					break;
				}
			} else
			{
				// Garbage, probably not an sfz file
				return false;
			}

			// Remove the token(s) we just read
			s.erase(0, charsRead);
		}
	}

	if(regions.empty())
		return false;


	ModInstrument *pIns = new (std::nothrow) ModInstrument();
	if(pIns == nullptr)
		return false;

	RecalculateSamplesPerTick();
	DestroyInstrument(nInstr, deleteAssociatedSamples);
	if(nInstr > m_nInstruments) m_nInstruments = nInstr;
	Instruments[nInstr] = pIns;

	SAMPLEINDEX prevSmp = 0;
	for(auto &region : regions)
	{
		uint8 keyLo = region.keyLo, keyHi = region.keyHi;
		if(keyLo > keyHi)
			continue;
		Clamp<uint8, uint8>(keyLo, 0, NOTE_MAX - NOTE_MIN);
		Clamp<uint8, uint8>(keyHi, 0, NOTE_MAX - NOTE_MIN);
		SAMPLEINDEX smp = GetNextFreeSample(nInstr, prevSmp + 1);
		if(smp == SAMPLEINDEX_INVALID)
			break;
		prevSmp = smp;

		ModSample &sample = Samples[smp];
		sample.Initialize(MOD_TYPE_MPT);
		if(const auto synthSample = std::string_view(region.filename).substr(region.filenameOffset); SFZStartsWith(synthSample, "*"))
		{
			sample.nLength = 256;
			sample.nC5Speed = mpt::saturate_round<uint32>(sample.nLength * 261.6255653);
			sample.uFlags.set(CHN_16BIT);
			std::function<uint16(int32)> generator;
			if(synthSample == "*sine")
				generator = [](int32 i) { return mpt::saturate_round<int16>(std::sin(i * ((2.0 * mpt::numbers::pi) / 256.0)) * int16_max); };
			else if(synthSample == "*square")
				generator = [](int32 i) { return i < 128 ? int16_max : int16_min; };
			else if(synthSample == "*triangle" || synthSample == "*tri")
				generator = [](int32 i) { return static_cast<int16>(i < 128 ? ((63 - i) * 512) : ((i - 192) * 512)); };
			else if(synthSample == "*saw")
				generator = [](int32 i) { return static_cast<int16>((i - 128) * 256); };
			else if(synthSample == "*silence")
				generator = [](int32) { return int16(0); };
			else if(synthSample == "*noise")
			{
				sample.nLength = sample.nC5Speed;
				generator = [this](int32) { return mpt::random<int16>(AccessPRNG()); };
			} else
			{
				AddToLog(LogWarning, U_("Unknown sample type: ") + mpt::ToUnicode(mpt::Charset::UTF8, std::string(synthSample)));
				prevSmp--;
				continue;
			}
			if(sample.AllocateSample())
			{
				for(SmpLength i = 0; i < sample.nLength; i++)
				{
					sample.sample16()[i] = generator(static_cast<int32>(i));
				}
				if(smp > m_nSamples)
					m_nSamples = smp;
				region.offset = 0;
				region.loopMode = SFZRegion::LoopMode::kContinuous;
				region.loopStart = 0;
				region.loopEnd = sample.nLength - 1;
				region.loopCrossfade = 0;
				region.keyRoot = 60;
			}
		} else if(auto filename = mpt::PathString::FromUTF8(region.filename); !filename.empty())
		{
			if(region.filename.find(':') == std::string::npos)
			{
				filename = file.GetOptionalFileName().value_or(P_("")).GetPath() + filename;
			}
			filename = filename.Simplify();
			SetSamplePath(smp, filename);
			InputFile f(filename, SettingCacheCompleteFileBeforeLoading());
			FileReader smpFile = GetFileReader(f);
			if(!ReadSampleFromFile(smp, smpFile, false))
			{
				AddToLog(LogWarning, U_("Unable to load sample: ") + filename.ToUnicode());
				prevSmp--;
				continue;
			}

			if(UseFinetuneAndTranspose())
				sample.TransposeToFrequency();

			sample.uFlags.set(SMP_KEEPONDISK, sample.HasSampleData());
		}

		if(!region.name.empty())
			m_szNames[smp] = mpt::ToCharset(GetCharsetInternal(), mpt::Charset::UTF8, region.name);
		if(!m_szNames[smp][0])
			m_szNames[smp] = mpt::ToCharset(GetCharsetInternal(), mpt::PathString::FromUTF8(region.filename).GetFileName().ToUnicode());

		if(region.useSampleKeyRoot)
		{
			if(sample.rootNote != NOTE_NONE)
				region.keyRoot = sample.rootNote - NOTE_MIN;
			else
				region.keyRoot = 60;
		}

		const auto origSampleRate = sample.GetSampleRate(GetType());
		int8 transp = region.transpose + (60 - region.keyRoot);
		for(uint8 i = keyLo; i <= keyHi; i++)
		{
			pIns->Keyboard[i] = smp;
			if(GetType() != MOD_TYPE_XM)
				pIns->NoteMap[i] = NOTE_MIN + i + transp;
		}
		if(GetType() == MOD_TYPE_XM)
			sample.Transpose(transp / 12.0);

		pIns->filterMode = region.filterType;
		if(region.cutoff != 0)
			pIns->SetCutoff(FrequencyToCutOff(region.cutoff), true);
		if(region.resonance != 0)
			pIns->SetResonance(mpt::saturate_round<uint8>(region.resonance * 128.0 / 24.0), true);
		pIns->nCutSwing = mpt::saturate_round<uint8>(region.filterRandom * (m_SongFlags[SONG_EXFILTERRANGE] ? 20 : 24) / 1200.0);
		pIns->midiPWD = mpt::saturate_round<int8>(region.pitchBend / 100.0);

		pIns->nNNA = NewNoteAction::NoteOff;
		if(region.polyphony == 1)
		{
			pIns->nDNA = DuplicateNoteAction::NoteCut;
			pIns->nDCT = DuplicateCheckType::Sample;
		}
		region.ampEnv.ConvertToMPT(pIns, *this, ENV_VOLUME);
		if(region.pitchEnv.depth)
			region.pitchEnv.ConvertToMPT(pIns, *this, ENV_PITCH);
		else if(region.filterEnv.depth)
			region.filterEnv.ConvertToMPT(pIns, *this, ENV_PITCH, true);

		for(const auto &flexEG : region.flexEGs)
		{
			flexEG.ConvertToMPT(pIns, *this);
		}

		if(region.ampEnv.release > 0)
		{
			const double tickDuration = m_PlayState.m_nSamplesPerTick / static_cast<double>(GetSampleRate());
			pIns->nFadeOut = std::min(mpt::saturate_cast<uint32>(32768.0 * tickDuration / region.ampEnv.release), uint32(32767));
			if(GetType() == MOD_TYPE_IT)
				pIns->nFadeOut = std::min((pIns->nFadeOut + 16u) & ~31u, uint32(8192));
		}
		
		sample.rootNote = region.keyRoot + NOTE_MIN;
		sample.nGlobalVol = mpt::saturate_round<decltype(sample.nGlobalVol)>(64.0 * Clamp(std::pow(10.0, region.volume / 20.0) * region.amplitude / 100.0, 0.0, 1.0));
		if(region.panning != -128)
		{
			sample.nPan = mpt::saturate_round<decltype(sample.nPan)>((region.panning + 100) * 256.0 / 200.0);
			sample.uFlags.set(CHN_PANNING);
		}
		sample.Transpose(region.finetune / 1200.0);

		if(region.pitchLfoDepth && region.pitchLfoFreq)
		{
			sample.nVibSweep = 255;
			if(region.pitchLfoFade > 0)
				sample.nVibSweep = mpt::saturate_round<uint8>(255.0 / region.pitchLfoFade);
			sample.nVibDepth = mpt::saturate_round<uint8>(region.pitchLfoDepth * 32.0 / 100.0);
			sample.nVibRate = mpt::saturate_round<uint8>(region.pitchLfoFreq * 4.0);
		}

		if(region.loopMode != SFZRegion::LoopMode::kUnspecified)
		{
			switch(region.loopMode)
			{
			case SFZRegion::LoopMode::kContinuous:
			case SFZRegion::LoopMode::kOneShot:
				sample.uFlags.set(CHN_LOOP);
				break;
			case SFZRegion::LoopMode::kSustain:
				sample.uFlags.set(CHN_SUSTAINLOOP);
				break;
			case SFZRegion::LoopMode::kNoLoop:
				sample.uFlags.reset(CHN_LOOP | CHN_SUSTAINLOOP);
			}
		}
		if(region.loopEnd > region.loopStart)
		{
			// Loop may also be defined in file, in which case loopStart and loopEnd are unset.
			if(region.loopMode == SFZRegion::LoopMode::kSustain)
			{
				sample.nSustainStart = region.loopStart;
				sample.nSustainEnd = region.loopEnd + 1;
			} else if(region.loopMode == SFZRegion::LoopMode::kContinuous || region.loopMode == SFZRegion::LoopMode::kOneShot)
			{
				sample.nLoopStart = region.loopStart;
				sample.nLoopEnd = region.loopEnd + 1;
			}
		} else if(sample.nLoopEnd <= sample.nLoopStart && region.loopMode != SFZRegion::LoopMode::kUnspecified && region.loopMode != SFZRegion::LoopMode::kNoLoop)
		{
			sample.nLoopEnd = sample.nLength;
		}
		switch(region.loopType)
		{
		case SFZRegion::LoopType::kUnspecified:
			break;
		case SFZRegion::LoopType::kForward:
			sample.uFlags.reset(CHN_PINGPONGLOOP | CHN_PINGPONGSUSTAIN | CHN_REVERSE);
			break;
		case SFZRegion::LoopType::kBackward:
			sample.uFlags.set(CHN_REVERSE);
			break;
		case SFZRegion::LoopType::kAlternate:
			sample.uFlags.set(CHN_PINGPONGLOOP | CHN_PINGPONGSUSTAIN);
			break;
		default:
			break;
		}
		if(sample.nSustainEnd <= sample.nSustainStart && sample.nLoopEnd > sample.nLoopStart && region.loopMode == SFZRegion::LoopMode::kSustain)
		{
			// Turn normal loop (imported from sample) into sustain loop
			std::swap(sample.nSustainStart, sample.nLoopStart);
			std::swap(sample.nSustainEnd, sample.nLoopEnd);
			sample.uFlags.set(CHN_SUSTAINLOOP);
			sample.uFlags.set(CHN_PINGPONGSUSTAIN, sample.uFlags[CHN_PINGPONGLOOP]);
			sample.uFlags.reset(CHN_LOOP | CHN_PINGPONGLOOP);
		}

		mpt::PathString filenameModifier;

		// Loop cross-fade
		SmpLength fadeSamples = mpt::saturate_round<SmpLength>(region.loopCrossfade * origSampleRate);
		LimitMax(fadeSamples, sample.uFlags[CHN_SUSTAINLOOP] ? sample.nSustainStart : sample.nLoopStart);
		if(fadeSamples > 0)
		{
			ctrlSmp::XFadeSample(sample, fadeSamples, 50000, true, sample.uFlags[CHN_SUSTAINLOOP], *this);
			sample.uFlags.set(SMP_MODIFIED);
			filenameModifier += P_(" (cross-fade)");
		}

		// Sample offset
		if(region.offset && region.offset < sample.nLength)
		{
			auto offset = region.offset * sample.GetBytesPerSample();
			memmove(sample.sampleb(), sample.sampleb() + offset, sample.nLength * sample.GetBytesPerSample() - offset);
			if(region.end > region.offset)
				region.end -= region.offset;
			sample.nLength -= region.offset;
			sample.nLoopStart -= region.offset;
			sample.nLoopEnd -= region.offset;
			sample.uFlags.set(SMP_MODIFIED);
			filenameModifier += P_(" (offset)");
		}
		LimitMax(sample.nLength, region.end);

		if(region.invertPhase)
		{
			ctrlSmp::InvertSample(sample, 0, sample.nLength, *this);
			sample.uFlags.set(SMP_MODIFIED);
			filenameModifier += P_(" (inverted)");
		}

		if(sample.uFlags.test_all(SMP_KEEPONDISK | SMP_MODIFIED))
		{
			// Avoid ruining the original samples
			if(auto filename = GetSamplePath(smp); !filename.empty())
			{
				filename = filename.GetPath() + filename.GetFileName() + filenameModifier + filename.GetFileExt();
				SetSamplePath(smp, filename);
			}
		}

		sample.PrecomputeLoops(*this, false);
		sample.Convert(MOD_TYPE_MPT, GetType());
	}

	pIns->Sanitize(MOD_TYPE_MPT);
	pIns->Convert(MOD_TYPE_MPT, GetType());
	return true;
}

#ifndef MODPLUG_NO_FILESAVE

static double SFZLinear2dB(double volume)
{
	return (volume > 0.0 ? 20.0 * std::log10(volume) : -144.0);
}

static void WriteSFZEnvelope(std::ostream &f, double tickDuration, int index, const InstrumentEnvelope &env, const char *type, double scale, std::function<double(int32)> convFunc)
{
	if(!env.dwFlags[ENV_ENABLED] || env.empty())
		return;

	const bool sustainAtEnd = (!env.dwFlags[ENV_SUSTAIN] || env.nSustainStart == (env.size() - 1)) && convFunc(env.back().value) != 0.0;

	const auto prefix = MPT_AFORMAT("\neg{}_")(mpt::afmt::dec0<2>(index));
	f << "\n" << prefix << type << "=" << scale;
	f << prefix << "points=" << (env.size() + (sustainAtEnd ? 1 : 0));
	EnvelopeNode::tick_t lastTick = 0;
	int nodeIndex = 0;
	for(const auto &node : env)
	{
		const double time = (node.tick - lastTick) * tickDuration;
		lastTick = node.tick;
		f << prefix << "time" << nodeIndex << "=" << time;
		f << prefix << "level" << nodeIndex << "=" << convFunc(node.value);
		nodeIndex++;
	}
	if(sustainAtEnd)
	{
		// Prevent envelope from going back to neutral
		f << prefix << "time" << nodeIndex << "=0";
		f << prefix << "level" << nodeIndex << "=" << convFunc(env.back().value);
	}
	// We always must write a sustain point, or the envelope will be sustained on the first point of the envelope
	f << prefix << "sustain=" << (env.dwFlags[ENV_SUSTAIN] ? env.nSustainStart : (env.size() - 1));

	if(env.dwFlags[ENV_LOOP])
		f << "\n// Loop: " << static_cast<uint32>(env.nLoopStart) << "-" << static_cast<uint32>(env.nLoopEnd);
	if(env.dwFlags[ENV_SUSTAIN] && env.nSustainEnd > env.nSustainStart)
		f << "\n// Sustain Loop: " << static_cast<uint32>(env.nSustainStart) << "-" << static_cast<uint32>(env.nSustainEnd);
	if(env.nReleaseNode != ENV_RELEASE_NODE_UNSET)
		f << "\n// Release Node: " << static_cast<uint32>(env.nReleaseNode);
}

bool CSoundFile::SaveSFZInstrument(INSTRUMENTINDEX nInstr, std::ostream &f, const mpt::PathString &filename, bool useFLACsamples) const
{
#ifdef MODPLUG_TRACKER
	const mpt::FlushMode flushMode = mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave);
#else
	const mpt::FlushMode flushMode = mpt::FlushMode::Full;
#endif
	const ModInstrument *ins = Instruments[nInstr];
	if(ins == nullptr)
		return false;

	// Creating directory names with trailing spaces or dots is a bad idea, as they are difficult to remove in Windows.
	const mpt::RawPathString whitespaceDirName = PL_(" \n\r\t.");
	const mpt::PathString sampleBaseName = mpt::PathString::FromNative(mpt::trim(filename.GetFileName().AsNative(), whitespaceDirName));
	const mpt::PathString sampleDirName = (sampleBaseName.empty() ? P_("Samples") : sampleBaseName)  + P_("/");
	const mpt::PathString sampleBasePath = filename.GetPath() + sampleDirName;
	if(!sampleBasePath.IsDirectory() && !::CreateDirectory(sampleBasePath.AsNative().c_str(), nullptr))
		return false;

	const double tickDuration = m_PlayState.m_nSamplesPerTick / static_cast<double>(m_MixerSettings.gdwMixingFreq);

	f << std::setprecision(10);
	if(!ins->name.empty())
	{
		f << "// Name: " << mpt::ToCharset(mpt::Charset::UTF8, GetCharsetInternal(), ins->name) << "\n";
	}
	f << "// Created with " << mpt::ToCharset(mpt::Charset::UTF8, Version::Current().GetOpenMPTVersionString()) << "\n";
	f << "// Envelope tempo base: tempo " << m_PlayState.m_nMusicTempo.ToDouble();
	switch(m_nTempoMode)
	{
	case TempoMode::Classic:
		f << " (classic tempo mode)";
		break;
	case TempoMode::Alternative:
		f << " (alternative tempo mode)";
		break;
	case TempoMode::Modern:
		f << ", " << m_PlayState.m_nMusicSpeed << " ticks per row, " << m_PlayState.m_nCurrentRowsPerBeat << " rows per beat (modern tempo mode)";
		break;
	default:
		MPT_ASSERT_NOTREACHED();
		break;
	}

	f << "\n\n<control>\ndefault_path=" << sampleDirName.ToUTF8() << "\n\n";
	f << "<group>";
	f << "\nbend_up=" << ins->midiPWD * 100;
	f << "\nbend_down=" << -ins->midiPWD * 100;
	const uint32 cutoff = ins->IsCutoffEnabled() ? ins->GetCutoff() : 127;
	// If filter envelope is active but cutoff is not set, we still need to set the base cutoff frequency to be modulated by the envelope.
	if(ins->IsCutoffEnabled() || ins->PitchEnv.dwFlags[ENV_FILTER])
		f << "\ncutoff=" << CSoundFile::CutOffToFrequency(cutoff) << " // " << cutoff;
	if(ins->IsResonanceEnabled())
		f << "\nresonance=" << Util::muldivr_unsigned(ins->GetResonance(), 24, 128) << " // " << static_cast<int>(ins->GetResonance());
	if(ins->IsCutoffEnabled() || ins->IsResonanceEnabled())
		f << "\nfil_type=" << (ins->filterMode == FilterMode::HighPass ? "hpf_2p" : "lpf_2p");
	if(ins->dwFlags[INS_SETPANNING])
		f << "\npan=" << (Util::muldivr_unsigned(ins->nPan, 200, 256) - 100) << " // " << ins->nPan;
	if(ins->nGlobalVol != 64)
		f << "\nvolume=" << SFZLinear2dB(ins->nGlobalVol / 64.0) << " // " << ins->nGlobalVol;
	if(ins->nFadeOut)
	{
		f << "\nampeg_release=" << (32768.0 * tickDuration / ins->nFadeOut) << " // " << ins->nFadeOut;
		f << "\nampeg_release_shape=0";
	}

	if(ins->nDNA == DuplicateNoteAction::NoteCut && ins->nDCT != DuplicateCheckType::None)
		f << "\npolyphony=1";

	WriteSFZEnvelope(f, tickDuration, 1, ins->VolEnv, "amplitude", 100.0, [](int32 val) { return val / static_cast<double>(ENVELOPE_MAX); });
	WriteSFZEnvelope(f, tickDuration, 2, ins->PanEnv, "pan", 100.0, [](int32 val) { return 2.0 * (val - ENVELOPE_MID) / (ENVELOPE_MAX - ENVELOPE_MIN); });
	if(ins->PitchEnv.dwFlags[ENV_FILTER])
	{
		const auto envScale = 1200.0 * std::log(CutOffToFrequency(127, 256) / static_cast<double>(CutOffToFrequency(0, -256))) / mpt::numbers::ln2;
		const auto cutoffNormal = CutOffToFrequency(cutoff);
		WriteSFZEnvelope(f, tickDuration, 3, ins->PitchEnv, "cutoff", envScale, [this, cutoff, cutoffNormal, envScale](int32 val) {
			// Convert interval between center frequency and envelope into cents
			const auto freq = CutOffToFrequency(cutoff, (val - ENVELOPE_MID) * 256 / (ENVELOPE_MAX - ENVELOPE_MID));
			return 1200.0 * std::log(freq / static_cast<double>(cutoffNormal)) / mpt::numbers::ln2 / envScale;
		});
	} else
	{
		WriteSFZEnvelope(f, tickDuration, 3, ins->PitchEnv, "pitch", 1600.0, [](int32 val) { return 2.0 * (val - ENVELOPE_MID) / (ENVELOPE_MAX - ENVELOPE_MIN); });
	}

	size_t numSamples = 0;
	for(size_t i = 0; i < std::size(ins->Keyboard); i++)
	{
		if(ins->Keyboard[i] < 1 || ins->Keyboard[i] > GetNumSamples())
			continue;

		size_t endOfRegion = i + 1;
		while(endOfRegion < std::size(ins->Keyboard))
		{
			if(ins->Keyboard[endOfRegion] != ins->Keyboard[i] || ins->NoteMap[endOfRegion] != (ins->NoteMap[i] + endOfRegion - i))
				break;
			endOfRegion++;
		}
		endOfRegion--;

		const ModSample &sample = Samples[ins->Keyboard[i]];
		const bool isAdlib = sample.uFlags[CHN_ADLIB];

		if(!sample.HasSampleData())
		{
			i = endOfRegion;
			continue;
		}

		numSamples++;
		mpt::PathString sampleName = sampleBasePath + (sampleBaseName.empty() ? P_("Sample") : sampleBaseName) + P_(" ") + mpt::PathString::FromUnicode(mpt::ufmt::val(numSamples));
		if(isAdlib)
			sampleName += P_(".s3i");
		else if(useFLACsamples)
			sampleName += P_(".flac");
		else
			sampleName += P_(".wav");

		bool success = false;
		try
		{
			mpt::SafeOutputFile sfSmp(sampleName, std::ios::binary, flushMode);
			if(sfSmp)
			{
				mpt::ofstream &fSmp = sfSmp;
				fSmp.exceptions(fSmp.exceptions() | std::ios::badbit | std::ios::failbit);

				if(isAdlib)
					success = SaveS3ISample(ins->Keyboard[i], fSmp);
				else if(useFLACsamples)
					success = SaveFLACSample(ins->Keyboard[i], fSmp);
				else
					success = SaveWAVSample(ins->Keyboard[i], fSmp);
			}
		} catch(const std::exception &)
		{
			success = false;
		}
		if(!success)
		{
			AddToLog(LogError, MPT_USTRING("Unable to save sample: ") + sampleName.ToUnicode());
		}


		f << "\n\n<region>";
		if(!m_szNames[ins->Keyboard[i]].empty())
		{
			f << "\nregion_label=" << mpt::ToCharset(mpt::Charset::UTF8, GetCharsetInternal(), m_szNames[ins->Keyboard[i]]);
		}
		f << "\nsample=" << sampleName.GetFullFileName().ToUTF8();
		f << "\nlokey=" << i;
		f << "\nhikey=" << endOfRegion;
		if(sample.rootNote != NOTE_NONE)
			f << "\npitch_keycenter=" << sample.rootNote - NOTE_MIN;
		else
			f << "\npitch_keycenter=" << NOTE_MIDDLEC + i - ins->NoteMap[i];
		if(sample.uFlags[CHN_PANNING])
			f << "\npan=" << (Util::muldivr_unsigned(sample.nPan, 200, 256) - 100) << " // " << sample.nPan;
		if(sample.nGlobalVol != 64)
			f << "\nvolume=" << SFZLinear2dB((ins->nGlobalVol * sample.nGlobalVol) / 4096.0) << " // " << sample.nGlobalVol;
		const char *loopMode = "no_loop", *loopType = "forward";
		SmpLength loopStart = 0, loopEnd = 0;
		if(sample.uFlags[CHN_SUSTAINLOOP])
		{
			loopMode = "loop_sustain";
			loopStart = sample.nSustainStart;
			loopEnd = sample.nSustainEnd;
			if(sample.uFlags[CHN_PINGPONGSUSTAIN])
				loopType = "alternate";
		} else if(sample.uFlags[CHN_LOOP])
		{
			loopMode = "loop_continuous";
			loopStart = sample.nLoopStart;
			loopEnd = sample.nLoopEnd;
			if(sample.uFlags[CHN_PINGPONGLOOP])
				loopType = "alternate";
			else if(sample.uFlags[CHN_REVERSE])
				loopType = "backward";
		}
		f << "\nloop_mode=" << loopMode;
		if(loopStart < loopEnd)
		{
			f << "\nloop_start=" << loopStart;
			f << "\nloop_end=" << (loopEnd - 1);
			f << "\nloop_type=" << loopType;
		}
		if(sample.uFlags.test_all(CHN_SUSTAINLOOP | CHN_LOOP))
		{
			f << "\n// Warning: Only sustain loop was exported!";
		}
		i = endOfRegion;
	}

	return true;
}

#endif // MODPLUG_NO_FILESAVE

#else
bool CSoundFile::ReadSFZInstrument(INSTRUMENTINDEX, FileReader &)
{
	return false;
}
#endif // MPT_EXTERNAL_SAMPLES

OPENMPT_NAMESPACE_END
