cog/Frameworks/OpenMPT/OpenMPT/soundlib/SampleFormatSFZ.cpp

1271 lines
40 KiB
C++

/*
* 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:
sample.uFlags.set(CHN_LOOP);
break;
case SFZRegion::LoopMode::kSustain:
sample.uFlags.set(CHN_SUSTAINLOOP);
break;
case SFZRegion::LoopMode::kNoLoop:
case SFZRegion::LoopMode::kOneShot:
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