/* * 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 OPENMPT_NAMESPACE_BEGIN #ifdef MPT_EXTERNAL_SAMPLES template static bool SFZStartsWith(const std::string_view &l, const char(&r)[N]) { return l.substr(0, N - 1) == r; } template 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(value); else if(key == "note_offset") noteOffset = ConvertStrTo(value); } }; struct SFZFlexEG { using PointIndex = decltype(InstrumentEnvelope().nLoopStart); std::vector> 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(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(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(v), static_cast(MAX_ENVPOINTS))); else if(key == "sustain") sustain = mpt::saturate_round(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(sndFile.GetSampleRate()); if(tickDuration <= 0 || points.empty() || scale == 0.0) return; auto &env = ins->GetEnvelope(envType); std::function 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(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(duration / tickDuration)); } static EnvelopeNode::value_t ToValue(double value, double scale, double minVal, double maxVal, const std::function &conversionFunc) { value = conversionFunc((value * scale - minVal) / (maxVal - minVal)) * ENVELOPE_MAX + ENVELOPE_MIN; Limit(value, ENVELOPE_MIN, ENVELOPE_MAX); return mpt::saturate_round(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 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(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(env.size() - 1); env.push_back({release, 0.0}); } else { eg.sustain = std::numeric_limits::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 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 static void Read(const std::string &valueStr, T &value, Tc valueMin = std::numeric_limits::min(), Tc valueMax = std::numeric_limits::max()) { double valueF = ConvertStrTo(valueStr); if constexpr(std::numeric_limits::is_integer) { valueF = mpt::round(valueF); } Limit(valueF, static_cast(valueMin), static_cast(valueMax)); value = static_cast(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(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(value.c_str() + octaveOffset); key += (octave + 1) * 12; } key += control.octaveOffset * 12 + control.noteOffset; return static_cast(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(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; // FileReader has pointers into this so its address must not change std::string remain; SFZInputFile(FileReader f = {}, std::unique_ptr 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 regions; std::map macros; std::vector 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 // sample=piano C3.wav key=48 ampeg_release=0.7 // a comment here // key = 49 sample = piano Db3.wav // // 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(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(keyLo, 0, NOTE_MAX - NOTE_MIN); Clamp(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(sample.nLength * 261.6255653); sample.uFlags.set(CHN_16BIT); std::function generator; if(synthSample == "*sine") generator = [](int32 i) { return mpt::saturate_round(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(i < 128 ? ((63 - i) * 512) : ((i - 192) * 512)); }; else if(synthSample == "*saw") generator = [](int32 i) { return static_cast((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(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(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(region.resonance * 128.0 / 24.0), true); pIns->nCutSwing = mpt::saturate_round(region.filterRandom * (m_SongFlags[SONG_EXFILTERRANGE] ? 20 : 24) / 1200.0); pIns->midiPWD = mpt::saturate_round(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(GetSampleRate()); pIns->nFadeOut = std::min(mpt::saturate_cast(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(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((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(255.0 / region.pitchLfoFade); sample.nVibDepth = mpt::saturate_round(region.pitchLfoDepth * 32.0 / 100.0); sample.nVibRate = mpt::saturate_round(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(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 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(env.nLoopStart) << "-" << static_cast(env.nLoopEnd); if(env.dwFlags[ENV_SUSTAIN] && env.nSustainEnd > env.nSustainStart) f << "\n// Sustain Loop: " << static_cast(env.nSustainStart) << "-" << static_cast(env.nSustainEnd); if(env.nReleaseNode != ENV_RELEASE_NODE_UNSET) f << "\n// Release Node: " << static_cast(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(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\ndefault_path=" << sampleDirName.ToUTF8() << "\n\n"; f << ""; 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(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(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(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(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"; 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