1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270 |
- /*
- * 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 ®ion : 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
|