winamp/Src/external_dependencies/openmpt-trunk/soundlib/Load_symmod.cpp
2024-09-24 14:54:57 +02:00

1948 lines
62 KiB
C++

/*
* Load_symmod.cpp
* ---------------
* Purpose: SymMOD (Symphonie / Symphonie Pro) module loader
* Notes : Based in part on Patrick Meng's Java-based Symphonie player and its source.
* Some effect behaviour and other things are based on the original Amiga assembly source.
* Symphonie is an interesting beast, with a surprising combination of features and lack thereof.
* It offers advanced DSPs (for its time) but has a fixed track tempo. It can handle stereo samples
* but free panning support was only added in one of the very last versions. Still, a good number
* of high-quality modules were made with it despite (or because of) its lack of features.
* Authors: Devin Acker
* OpenMPT Devs
* The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
*/
#include "stdafx.h"
#include "Loaders.h"
#include "Mixer.h"
#include "MixFuncTable.h"
#include "modsmp_ctrl.h"
#include "openmpt/soundbase/SampleConvert.hpp"
#include "openmpt/soundbase/SampleConvertFixedPoint.hpp"
#include "openmpt/soundbase/SampleDecode.hpp"
#include "SampleCopy.h"
#ifdef MPT_EXTERNAL_SAMPLES
#include "../common/mptPathString.h"
#endif // MPT_EXTERNAL_SAMPLES
#include "mpt/base/numbers.hpp"
#include <map>
OPENMPT_NAMESPACE_BEGIN
struct SymFileHeader
{
char magic[4]; // "SymM"
uint32be version;
bool Validate() const
{
return !std::memcmp(magic, "SymM", 4) && version == 1;
}
};
MPT_BINARY_STRUCT(SymFileHeader, 8)
struct SymEvent
{
enum Command : uint8
{
KeyOn = 0,
VolSlideUp,
VolSlideDown,
PitchSlideUp,
PitchSlideDown,
ReplayFrom,
FromAndPitch,
SetFromAdd,
FromAdd,
SetSpeed,
AddPitch,
AddVolume,
Tremolo,
Vibrato,
SampleVib,
PitchSlideTo,
Retrig,
Emphasis,
AddHalfTone,
CV,
CVAdd,
Filter = 23,
DSPEcho,
DSPDelay,
};
enum Volume : uint8
{
VolCommand = 200,
StopSample = 254,
ContSample = 253,
StartSample = 252, // unused
KeyOff = 251,
SpeedDown = 250,
SpeedUp = 249,
SetPitch = 248,
PitchUp = 247,
PitchDown = 246,
PitchUp2 = 245,
PitchDown2 = 244,
PitchUp3 = 243,
PitchDown3 = 242
};
uint8be command; // See Command enum
int8be note;
uint8be param; // Volume if <= 100, see Volume enum otherwise
uint8be inst;
bool IsGlobal() const
{
if(command == SymEvent::SetSpeed || command == SymEvent::DSPEcho || command == SymEvent::DSPDelay)
return true;
if(command == SymEvent::KeyOn && (param == SymEvent::SpeedUp || param == SymEvent::SpeedDown))
return true;
return false;
}
// used to compare DSP events for mapping them to MIDI macro numbers
bool operator<(const SymEvent &other) const
{
return std::tie(command, note, param, inst) < std::tie(other.command, other.note, other.param, other.inst);
}
};
MPT_BINARY_STRUCT(SymEvent, 4)
struct SymVirtualHeader
{
char id[4]; // "ViRT"
uint8be zero;
uint8be filler1;
uint16be version; // 0 = regular, 1 = transwave
uint16be mixInfo; // unused, but not 0 in all modules
uint16be filler2;
uint16be eos; // 0
uint16be numEvents;
uint16be maxEvents; // always 20
uint16be eventSize; // 4 for virtual instruments, 10 for transwave instruments (number of cycles, not used)
bool IsValid() const
{
return !memcmp(id, "ViRT", 4) && zero == 0 && version <= 1 && eos == 0 && maxEvents == 20;
}
bool IsVirtual() const
{
return IsValid() && version == 0 && numEvents <= 20 && eventSize == sizeof(SymEvent);
}
bool IsTranswave() const
{
return IsValid() && version == 1 && numEvents == 2 && eventSize == 10;
}
};
MPT_BINARY_STRUCT(SymVirtualHeader, 20)
// Virtual instrument info
// This allows instruments to be created based on a mix of other instruments.
// The sample mixing is done at load time.
struct SymVirtualInst
{
SymVirtualHeader header;
SymEvent noteEvents[20];
char padding[28];
bool Render(CSoundFile &sndFile, const bool asQueue, ModSample &target, uint16 sampleBoost) const
{
if(header.numEvents < 1 || header.numEvents > std::size(noteEvents) || noteEvents[0].inst >= sndFile.GetNumSamples())
return false;
target.Initialize(MOD_TYPE_IT);
target.uFlags = CHN_16BIT;
const auto events = mpt::as_span(noteEvents).subspan(0, header.numEvents);
const double rateFactor = 1.0 / std::max(sndFile.GetSample(events[0].inst + 1).nC5Speed, uint32(1));
for(const auto &event : events.subspan(0, asQueue ? events.size() : 1u))
{
if(event.inst >= sndFile.GetNumSamples() || event.note < 0)
continue;
const ModSample &sourceSmp = sndFile.GetSample(event.inst + 1);
const double length = sourceSmp.nLength * std::pow(2.0, (event.note - events[0].note) / -12.0) * sourceSmp.nC5Speed * rateFactor;
target.nLength += mpt::saturate_round<SmpLength>(length);
}
if(!target.AllocateSample())
return false;
std::vector<ModChannel> channels(events.size());
SmpLength lastSampleOffset = 0;
for(size_t ev = 0; ev < events.size(); ev++)
{
const SymEvent &event = events[ev];
ModChannel &chn = channels[ev];
if(event.inst >= sndFile.GetNumSamples() || event.note < 0)
continue;
int8 finetune = 0;
if(event.param >= SymEvent::PitchDown3 && event.param <= SymEvent::PitchUp)
{
static constexpr int8 PitchTable[] = {-4, 4, -2, 2, -1, 1};
static_assert(mpt::array_size<decltype(PitchTable)>::size == SymEvent::PitchUp - SymEvent::PitchDown3 + 1);
finetune = PitchTable[event.param - SymEvent::PitchDown3];
}
const ModSample &sourceSmp = sndFile.GetSample(event.inst + 1);
const double increment = std::pow(2.0, (event.note - events[0].note) / 12.0 + finetune / 96.0) * sourceSmp.nC5Speed * rateFactor;
if(increment <= 0)
continue;
chn.increment = SamplePosition::FromDouble(increment);
chn.pCurrentSample = sourceSmp.samplev();
chn.nLength = sourceSmp.nLength;
chn.dwFlags = sourceSmp.uFlags & CHN_SAMPLEFLAGS;
if(asQueue)
{
// This determines when the queued sample will be played
chn.oldOffset = lastSampleOffset;
lastSampleOffset += mpt::saturate_round<SmpLength>(chn.nLength / chn.increment.ToDouble());
}
int32 volume = 4096 * sampleBoost / 10000; // avoid clipping the filters if the virtual sample is later also filtered (see e.g. 303 emulator.symmod)
if(!asQueue)
volume /= header.numEvents;
chn.leftVol = chn.rightVol = volume;
}
SmpLength writeOffset = 0;
while(writeOffset < target.nLength)
{
std::array<mixsample_t, MIXBUFFERSIZE * 2> buffer{};
const SmpLength writeCount = std::min(static_cast<SmpLength>(MIXBUFFERSIZE), target.nLength - writeOffset);
for(auto &chn : channels)
{
if(!chn.pCurrentSample)
continue;
// Should queued sample be played yet?
if(chn.oldOffset >= writeCount)
{
chn.oldOffset -= writeCount;
continue;
}
uint32 functionNdx = MixFuncTable::ndxLinear;
if(chn.dwFlags[CHN_16BIT])
functionNdx |= MixFuncTable::ndx16Bit;
if(chn.dwFlags[CHN_STEREO])
functionNdx |= MixFuncTable::ndxStereo;
const SmpLength procCount = std::min(writeCount - chn.oldOffset, mpt::saturate_round<SmpLength>((chn.nLength - chn.position.ToDouble()) / chn.increment.ToDouble()));
MixFuncTable::Functions[functionNdx](chn, sndFile.m_Resampler, buffer.data() + chn.oldOffset * 2, procCount);
chn.oldOffset = 0;
if(chn.position.GetUInt() >= chn.nLength)
chn.pCurrentSample = nullptr;
}
CopySample<SC::ConversionChain<SC::ConvertFixedPoint<int16, mixsample_t, 27>, SC::DecodeIdentity<mixsample_t>>>(target.sample16() + writeOffset, writeCount, 1, buffer.data(), sizeof(buffer), 2);
writeOffset += writeCount;
}
return true;
}
};
MPT_BINARY_STRUCT(SymVirtualInst, 128)
// Transwave instrument info
// Similar to virtual instruments, allows blending between two sample loops
struct SymTranswaveInst
{
struct Transwave
{
uint16be sourceIns;
uint16be volume; // According to source label - but appears to be unused
uint32be loopStart;
uint32be loopLen;
uint32be padding;
std::pair<SmpLength, SmpLength> ConvertLoop(const ModSample &mptSmp) const
{
const double loopScale = static_cast<double>(mptSmp.nLength) / (100 << 16);
const SmpLength start = mpt::saturate_cast<SmpLength>(loopScale * std::min(uint32(100 << 16), loopStart.get()));
const SmpLength length = mpt::saturate_cast<SmpLength>(loopScale * std::min(uint32(100 << 16), loopLen.get()));
return {start, std::min(mptSmp.nLength - start, length)};
}
};
SymVirtualHeader header;
Transwave points[2];
char padding[76];
// Morph between two sample loops
bool Render(const ModSample &smp1, const ModSample &smp2, ModSample &target) const
{
target.Initialize(MOD_TYPE_IT);
const auto [loop1Start, loop1Len] = points[0].ConvertLoop(smp1);
const auto [loop2Start, loop2Len] = points[1].ConvertLoop(smp2);
if(loop1Len < 1 || loop1Len > MAX_SAMPLE_LENGTH / (4u * 80u))
return false;
const SmpLength cycleLength = loop1Len * 4u;
const double cycleFactor1 = loop1Len / static_cast<double>(cycleLength);
const double cycleFactor2 = loop2Len / static_cast<double>(cycleLength);
target.uFlags = CHN_16BIT;
target.nLength = cycleLength * 80u;
if(!target.AllocateSample())
return false;
const double ampFactor = 1.0 / target.nLength;
for(SmpLength i = 0; i < cycleLength; i++)
{
const double v1 = TranswaveInterpolate(smp1, loop1Start + i * cycleFactor1);
const double v2 = TranswaveInterpolate(smp2, loop2Start + i * cycleFactor2);
SmpLength writeOffset = i;
for(int cycle = 0; cycle < 80; cycle++, writeOffset += cycleLength)
{
const double amp = writeOffset * ampFactor;
target.sample16()[writeOffset] = mpt::saturate_round<int16>(v1 * (1.0 - amp) + v2 * amp);
}
}
return true;
}
static MPT_FORCEINLINE double TranswaveInterpolate(const ModSample &smp, double offset)
{
if(!smp.HasSampleData())
return 0.0;
SmpLength intOffset = static_cast<SmpLength>(offset);
const double fractOffset = offset - intOffset;
const uint8 numChannels = smp.GetNumChannels();
intOffset *= numChannels;
int16 v1, v2;
if(smp.uFlags[CHN_16BIT])
{
v1 = smp.sample16()[intOffset];
v2 = smp.sample16()[intOffset + numChannels];
} else
{
v1 = smp.sample8()[intOffset] * 256;
v2 = smp.sample8()[intOffset + numChannels] * 256;
}
return (v1 * (1.0 - fractOffset) + v2 * fractOffset);
}
};
MPT_BINARY_STRUCT(SymTranswaveInst, 128)
// Instrument definition
struct SymInstrument
{
using SymInstrumentName = std::array<char, 128>;
SymVirtualInst virt; // or SymInstrumentName, or SymTranswaveInst
enum Type : int8
{
Silent = -8,
Kill = -4,
Normal = 0,
Loop = 4,
Sustain = 8
};
enum Channel : uint8
{
Mono,
StereoL,
StereoR,
LineSrc // virtual mix instrument
};
enum SampleFlags : uint8
{
PlayReverse = 1, // reverse sample
AsQueue = 2, // "queue" virtual instrument (rendereds samples one after another rather than simultaneously)
MirrorX = 4, // invert sample phase
Is16Bit = 8, // not used, we already know the bit depth of the samples
NewLoopSystem = 16, // use fine loop start/len values
MakeNewSample = (PlayReverse | MirrorX)
};
enum InstFlags : uint8
{
NoTranspose = 1, // don't apply sequence/position transpose
NoDSP = 2, // don't apply DSP effects
SyncPlay = 4 // play a stereo instrument pair (or two copies of the same mono instrument) on consecutive channels
};
int8be type; // see Type enum
uint8be loopStartHigh;
uint8be loopLenHigh;
uint8be numRepetitions; // for "sustain" instruments
uint8be channel; // see Channel enum
uint8be dummy1; // called "automaximize" (normalize?) in Amiga source, but unused
uint8be volume; // 0-199
uint8be dummy2[3]; // info about "parent/child" and sample format
int8be finetune; // -128..127 ~= 2 semitones
int8be transpose;
uint8be sampleFlags; // see SampleFlags enum
int8be filter; // negative: highpass, positive: lowpass
uint8be instFlags; // see InstFlags enum
uint8be downsample; // downsample factor; affects sample tuning
uint8be dummy3[2]; // resonance, "loadflags" (both unused)
uint8be info; // bit 0 should indicate that rangeStart/rangeLen are valid, but they appear to be unused
uint8be rangeStart; // ditto
uint8be rangeLen; // ditto
uint8be dummy4;
uint16be loopStartFine;
uint16be loopLenFine;
uint8be dummy5[6];
uint8be filterFlags; // bit 0 = enable, bit 1 = highpass
uint8be numFilterPoints; // # of filter envelope points (up to 4, possibly only 1-2 ever actually used)
struct SymFilterSetting
{
uint8be cutoff;
uint8be resonance;
} filterPoint[4];
uint8be volFadeFlag;
uint8be volFadeFrom;
uint8be volFadeTo;
uint8be padding[83];
bool IsVirtual() const
{
return virt.header.IsValid();
}
// Valid instrument either is virtual or has a name
bool IsEmpty() const
{
return virt.header.id[0] == 0 || type < 0;
}
std::string GetName() const
{
return mpt::String::ReadBuf(mpt::String::maybeNullTerminated, mpt::bit_cast<SymInstrumentName>(virt));
}
SymTranswaveInst GetTranswave() const
{
return mpt::bit_cast<SymTranswaveInst>(virt);
}
void ConvertToMPT(ModInstrument &mptIns, ModSample &mptSmp, CSoundFile &sndFile) const
{
if(!IsVirtual())
mptIns.name = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, mpt::bit_cast<SymInstrumentName>(virt));
mptSmp.uFlags.reset(CHN_LOOP | CHN_PINGPONGLOOP | CHN_SUSTAINLOOP | CHN_PANNING); // Avoid these coming in from sample files
const auto [loopStart, loopLen] = GetSampleLoop(mptSmp);
if(type == Loop && loopLen > 0)
{
mptSmp.uFlags.set(CHN_LOOP);
mptSmp.nLoopStart = loopStart;
mptSmp.nLoopEnd = loopStart + loopLen;
}
// volume (0-199, default 100)
// Symphonie actually compresses the sample data if the volume is above 100 (see end of function)
// We spread the volume between sample and instrument global volume if it's below 100 for the best possible resolution.
// This can be simplified if instrument volume ever gets adjusted to 0...128 range like in IT.
uint8 effectiveVolume = (volume > 0 && volume < 200) ? static_cast<uint8>(std::min(volume.get(), uint8(100)) * 128u / 100) : 128;
mptSmp.nGlobalVol = std::max(effectiveVolume, uint8(64)) / 2u;
mptIns.nGlobalVol = std::min(effectiveVolume, uint8(64));
// Tuning info (we'll let our own mixer take care of the downsampling instead of doing it at load time)
mptSmp.nC5Speed = 40460;
mptSmp.Transpose(-downsample + (transpose / 12.0) + (finetune / (128.0 * 12.0)));
// DSP settings
mptIns.nMixPlug = (instFlags & NoDSP) ? 2 : 1;
if(instFlags & NoDSP)
{
// This is not 100% correct: An instrument playing after this one should pick up previous filter settings.
mptIns.SetCutoff(127, true);
mptIns.SetResonance(0, true);
}
// Various sample processing follows
if(!mptSmp.HasSampleData())
return;
if(sampleFlags & PlayReverse)
ctrlSmp::ReverseSample(mptSmp, 0, 0, sndFile);
if(sampleFlags & MirrorX)
ctrlSmp::InvertSample(mptSmp, 0, 0, sndFile);
// Always use 16-bit data to help with heavily filtered 8-bit samples (like in Future_Dream.SymMOD)
const bool doVolFade = (volFadeFlag == 2) && (volFadeFrom <= 100) && (volFadeTo <= 100);
if(!mptSmp.uFlags[CHN_16BIT] && (filterFlags || doVolFade || filter))
{
int16 *newSample = static_cast<int16 *>(ModSample::AllocateSample(mptSmp.nLength, 2 * mptSmp.GetNumChannels()));
if(!newSample)
return;
CopySample<SC::ConversionChain<SC::Convert<int16, int8>, SC::DecodeIdentity<int8>>>(newSample, mptSmp.nLength * mptSmp.GetNumChannels(), 1, mptSmp.sample8(), mptSmp.GetSampleSizeInBytes(), 1);
mptSmp.uFlags.set(CHN_16BIT);
ctrlSmp::ReplaceSample(mptSmp, newSample, mptSmp.nLength, sndFile);
}
// Highpass
if(filter < 0)
{
auto sampleData = mpt::as_span(mptSmp.sample16(), mptSmp.nLength * mptSmp.GetNumChannels());
for(int i = 0; i < -filter; i++)
{
int32 mix = sampleData[0];
for(auto &sample : sampleData)
{
mix = mpt::rshift_signed(sample - mpt::rshift_signed(mix, 1), 1);
sample = static_cast<int16>(mix);
}
}
}
// Volume Fade
if(doVolFade)
{
auto sampleData = mpt::as_span(mptSmp.sample16(), mptSmp.nLength * mptSmp.GetNumChannels());
int32 amp = volFadeFrom << 24, inc = Util::muldivr(volFadeTo - volFadeFrom, 1 << 24, static_cast<SmpLength>(sampleData.size()));
for(auto &sample : sampleData)
{
sample = static_cast<int16>(Util::muldivr(sample, amp, 100 << 24));
amp += inc;
}
}
// Resonant Filter Sweep
if(filterFlags != 0)
{
auto sampleData = mpt::as_span(mptSmp.sample16(), mptSmp.nLength * mptSmp.GetNumChannels());
int32 cutoff = filterPoint[0].cutoff << 23, resonance = filterPoint[0].resonance << 23;
const int32 cutoffStep = numFilterPoints > 1 ? Util::muldivr(filterPoint[1].cutoff - filterPoint[0].cutoff, 1 << 23, static_cast<SmpLength>(sampleData.size())) : 0;
const int32 resoStep = numFilterPoints > 1 ? Util::muldivr(filterPoint[1].resonance - filterPoint[0].resonance, 1 << 23, static_cast<SmpLength>(sampleData.size())) : 0;
const uint8 highpass = filterFlags & 2;
int32 filterState[3]{};
for(auto &sample : sampleData)
{
const int32 currentCutoff = cutoff / (1 << 23), currentReso = resonance / (1 << 23);
cutoff += cutoffStep;
resonance += resoStep;
filterState[2] = mpt::rshift_signed(sample, 1) - filterState[0];
filterState[1] += mpt::rshift_signed(currentCutoff * filterState[2], 8);
filterState[0] += mpt::rshift_signed(currentCutoff * filterState[1], 6);
filterState[0] += mpt::rshift_signed(currentReso * filterState[0], 6);
filterState[0] = mpt::rshift_signed(filterState[0], 2);
sample = mpt::saturate_cast<int16>(filterState[highpass]);
}
}
// Lowpass
if(filter > 0)
{
auto sampleData = mpt::as_span(mptSmp.sample16(), mptSmp.nLength * mptSmp.GetNumChannels());
for(int i = 0; i < filter; i++)
{
int32 mix = sampleData[0];
for(auto &sample : sampleData)
{
mix = (sample + sample + mix) / 3;
sample = static_cast<int16>(mix);
}
}
}
// Symphonie normalizes samples at load time (it normalizes them to the sample boost value - but we will use the full 16-bit range)
// Indeed, the left and right channel instruments are normalized separately.
const auto Normalize = [](auto sampleData)
{
const auto scale = Util::MaxValueOfType(sampleData[0]);
const auto [minElem, maxElem] = std::minmax_element(sampleData.begin(), sampleData.end());
const int max = std::max(-*minElem, +*maxElem);
if(max >= scale || max == 0)
return;
for(auto &v : sampleData)
{
v = static_cast<typename std::remove_reference<decltype(v)>::type>(static_cast<int>(v) * scale / max);
}
};
if(mptSmp.uFlags[CHN_16BIT])
Normalize(mpt::as_span(mptSmp.sample16(), mptSmp.nLength * mptSmp.GetNumChannels()));
else
Normalize(mpt::as_span(mptSmp.sample8(), mptSmp.nLength * mptSmp.GetNumChannels()));
// "Non-destructive" over-amplification with hard knee compression
if(volume > 100 && volume < 200)
{
const auto Amplify = [](auto sampleData, const uint8 gain)
{
const int32 knee = 16384 * (200 - gain) / 100, kneeInv = 32768 - knee;
constexpr int32 scale = 1 << (16 - (sizeof(sampleData[0]) * 8));
for(auto &sample : sampleData)
{
int32 v = sample * scale;
if(v > knee)
v = (v - knee) * knee / kneeInv + kneeInv;
else if(v < -knee)
v = (v + knee) * knee / kneeInv - kneeInv;
else
v = v * kneeInv / knee;
sample = mpt::saturate_cast<typename std::remove_reference<decltype(sample)>::type>(v / scale);
}
};
const auto length = mptSmp.nLength * mptSmp.GetNumChannels();
if(mptSmp.uFlags[CHN_16BIT])
Amplify(mpt::span(mptSmp.sample16(), mptSmp.sample16() + length), volume);
else
Amplify(mpt::span(mptSmp.sample8(), mptSmp.sample8() + length), volume);
}
// This must be applied last because some sample processors are time-dependent and Symphonie would be doing this during playback instead
mptSmp.RemoveAllCuePoints();
if(type == Sustain && numRepetitions > 0 && loopLen > 0)
{
mptSmp.cues[0] = loopStart + loopLen * (numRepetitions + 1u);
mptSmp.nSustainStart = loopStart; // This is of purely informative value and not used for playback
mptSmp.nSustainEnd = loopStart + loopLen;
if(MAX_SAMPLE_LENGTH / numRepetitions < loopLen)
return;
if(MAX_SAMPLE_LENGTH - numRepetitions * loopLen < mptSmp.nLength)
return;
const uint8 bps = mptSmp.GetBytesPerSample();
SmpLength loopEnd = loopStart + loopLen * (numRepetitions + 1);
SmpLength newLength = mptSmp.nLength + loopLen * numRepetitions;
std::byte *newSample = static_cast<std::byte *>(ModSample::AllocateSample(newLength, bps));
if(!newSample)
return;
mptSmp.nLength = newLength;
std::memcpy(newSample, mptSmp.sampleb(), (loopStart + loopLen) * bps);
for(uint8 i = 0; i < numRepetitions; i++)
{
std::memcpy(newSample + (loopStart + loopLen * (i + 1)) * bps, mptSmp.sampleb() + loopStart * bps, loopLen * bps);
}
std::memcpy(newSample + loopEnd * bps, mptSmp.sampleb() + (loopStart + loopLen) * bps, (newLength - loopEnd) * bps);
ctrlSmp::ReplaceSample(mptSmp, newSample, mptSmp.nLength, sndFile);
}
}
std::pair<SmpLength, SmpLength> GetSampleLoop(const ModSample &mptSmp) const
{
if(type != Loop && type != Sustain)
return {0, 0};
SmpLength loopStart = static_cast<SmpLength>(std::min(loopStartHigh.get(), uint8(100)));
SmpLength loopLen = static_cast<SmpLength>(std::min(loopLenHigh.get(), uint8(100)));
if(sampleFlags & NewLoopSystem)
{
loopStart = (loopStart << 16) + loopStartFine;
loopLen = (loopLen << 16) + loopLenFine;
const double loopScale = static_cast<double>(mptSmp.nLength) / (100 << 16);
loopStart = mpt::saturate_cast<SmpLength>(loopStart * loopScale);
loopLen = std::min(mptSmp.nLength - loopStart, mpt::saturate_cast<SmpLength>(loopLen * loopScale));
} else if(mptSmp.HasSampleData())
{
// The order of operations here may seem weird as it reduces precision, but it's taken directly from the original assembly source (UpdateRecalcLoop)
loopStart = ((loopStart << 7) / 100u) * (mptSmp.nLength >> 7);
loopLen = std::min(mptSmp.nLength - loopStart, ((loopLen << 7) / 100u) * (mptSmp.nLength >> 7));
const auto FindLoopEnd = [](auto sampleData, const uint8 numChannels, SmpLength loopStart, SmpLength loopLen, const int threshold)
{
const auto valAtStart = sampleData.data()[loopStart * numChannels];
auto *endPtr = sampleData.data() + (loopStart + loopLen) * numChannels;
while(loopLen)
{
if(std::abs(*endPtr - valAtStart) < threshold)
return loopLen;
endPtr -= numChannels;
loopLen--;
}
return loopLen;
};
if(mptSmp.uFlags[CHN_16BIT])
loopLen = FindLoopEnd(mpt::as_span(mptSmp.sample16(), mptSmp.nLength * mptSmp.GetNumChannels()), mptSmp.GetNumChannels(), loopStart, loopLen, 6 * 256);
else
loopLen = FindLoopEnd(mpt::as_span(mptSmp.sample8(), mptSmp.nLength * mptSmp.GetNumChannels()), mptSmp.GetNumChannels(), loopStart, loopLen, 6);
}
return {loopStart, loopLen};
}
};
MPT_BINARY_STRUCT(SymInstrument, 256)
struct SymSequence
{
uint16be start;
uint16be length;
uint16be loop;
int16be info;
int16be transpose;
uint8be padding[6];
};
MPT_BINARY_STRUCT(SymSequence, 16)
struct SymPosition
{
uint8be dummy[4];
uint16be loopNum;
uint16be loopCount; // Only used during playback
uint16be pattern;
uint16be start;
uint16be length;
uint16be speed;
int16be transpose;
uint16be eventsPerLine; // Unused
uint8be padding[12];
// Used to compare position entries for mapping them to OpenMPT patterns
bool operator<(const SymPosition &other) const
{
return std::tie(pattern, start, length, transpose, speed) < std::tie(other.pattern, other.start, other.length, other.transpose, other.speed);
}
};
MPT_BINARY_STRUCT(SymPosition, 32)
static std::vector<std::byte> DecodeSymChunk(FileReader &file)
{
std::vector<std::byte> data;
const uint32 packedLength = file.ReadUint32BE();
if(!file.CanRead(packedLength))
{
file.Skip(file.BytesLeft());
return data;
}
FileReader chunk = file.ReadChunk(packedLength);
if(packedLength >= 10 && chunk.ReadMagic("PACK\xFF\xFF"))
{
// RLE-compressed chunk
uint32 unpackedLength = chunk.ReadUint32BE();
// The best compression ratio can be achieved with type 1, where six bytes turn into up to 255*4 bytes, a ratio of 1:170.
uint32 maxLength = packedLength - 10;
if(Util::MaxValueOfType(maxLength) / 170 >= maxLength)
maxLength *= 170;
else
maxLength = Util::MaxValueOfType(maxLength);
LimitMax(unpackedLength, maxLength);
data.resize(unpackedLength);
bool done = false;
uint32 offset = 0, remain = unpackedLength;
while(!done && !chunk.EndOfFile())
{
uint8 len;
std::array<std::byte, 4> dword;
const int8 type = chunk.ReadInt8();
switch(type)
{
case 0:
// Copy raw bytes
len = chunk.ReadUint8();
if(remain >= len && chunk.CanRead(len))
{
chunk.ReadRaw(mpt::as_span(data).subspan(offset, len));
offset += len;
remain -= len;
} else
{
done = true;
}
break;
case 1:
// Copy a dword multiple times
len = chunk.ReadUint8();
if(remain >= (len * 4u) && chunk.ReadArray(dword))
{
remain -= len * 4u;
while(len--)
{
std::copy(dword.begin(), dword.end(), data.begin() + offset);
offset += 4;
}
} else
{
done = true;
}
break;
case 2:
// Copy a dword twice
if(remain >= 8 && chunk.ReadArray(dword))
{
std::copy(dword.begin(), dword.end(), data.begin() + offset);
std::copy(dword.begin(), dword.end(), data.begin() + offset + 4);
offset += 8;
remain -= 8;
} else
{
done = true;
}
break;
case 3:
// Zero bytes
len = chunk.ReadUint8();
if(remain >= len)
{
// vector is already initialized to zero
offset += len;
remain -= len;
} else
{
done = true;
}
break;
case -1:
done = true;
break;
default:
// error
done = true;
break;
}
}
#ifndef MPT_BUILD_FUZZER
// When using a fuzzer, we should not care if the decompressed buffer has the correct size.
// This makes finding new interesting test cases much easier.
if(remain)
std::vector<std::byte>{}.swap(data);
#endif
} else
{
// Uncompressed chunk
chunk.ReadVector(data, packedLength);
}
return data;
}
template<typename T>
static std::vector<T> DecodeSymArray(FileReader &file)
{
const auto data = DecodeSymChunk(file);
FileReader chunk(mpt::as_span(data));
std::vector<T> retVal;
chunk.ReadVector(retVal, data.size() / sizeof(T));
return retVal;
}
static bool ReadRawSymSample(ModSample &sample, FileReader &file)
{
SampleIO sampleIO(SampleIO::_16bit, SampleIO::mono, SampleIO::bigEndian, SampleIO::signedPCM);
SmpLength nullBytes = 0;
sample.Initialize();
file.Rewind();
if(file.ReadMagic("MAESTRO"))
{
file.Seek(12);
if(file.ReadUint32BE() == 0)
sampleIO |= SampleIO::stereoInterleaved;
file.Seek(24);
} else if(file.ReadMagic("16BT"))
{
file.Rewind();
nullBytes = 4; // In Symphonie, the anti-click would take care of those...
} else
{
sampleIO |= SampleIO::_8bit;
}
sample.nLength = mpt::saturate_cast<SmpLength>(file.BytesLeft() / (sampleIO.GetNumChannels() * sampleIO.GetBitDepth() / 8u));
const bool ok = sampleIO.ReadSample(sample, file) > 0;
if(ok && nullBytes)
std::memset(sample.samplev(), 0, std::min(nullBytes, sample.GetSampleSizeInBytes()));
return ok;
}
static std::vector<std::byte> DecodeSample8(FileReader &file)
{
auto data = DecodeSymChunk(file);
uint8 lastVal = 0;
for(auto &val : data)
{
lastVal += mpt::byte_cast<uint8>(val);
val = mpt::byte_cast<std::byte>(lastVal);
}
return data;
}
static std::vector<std::byte> DecodeSample16(FileReader &file)
{
auto data = DecodeSymChunk(file);
std::array<std::byte, 4096> buf;
constexpr size_t blockSize = buf.size() / 2; // Size of block in 16-bit samples
for(size_t block = 0; block < data.size() / buf.size(); block++)
{
const size_t offset = block * sizeof(buf);
uint8 lastVal = 0;
// Decode LSBs
for(size_t i = 0; i < blockSize; i++)
{
lastVal += mpt::byte_cast<uint8>(data[offset + i]);
buf[i * 2 + 1] = mpt::byte_cast<std::byte>(lastVal);
}
// Decode MSBs
for(size_t i = 0; i < blockSize; i++)
{
lastVal += mpt::byte_cast<uint8>(data[offset + i + blockSize]);
buf[i * 2] = mpt::byte_cast<std::byte>(lastVal);
}
std::copy(buf.begin(), buf.end(), data.begin() + offset);
}
return data;
}
static bool ConvertDSP(const SymEvent event, MIDIMacroConfigData::Macro &macro, const CSoundFile &sndFile)
{
if(event.command == SymEvent::Filter)
{
// Symphonie practically uses the same filter for this as for the sample processing.
// The cutoff and resonance are an approximation.
const uint8 type = event.note % 5u;
const uint8 cutoff = sndFile.FrequencyToCutOff(event.param * 10000.0 / 240.0);
const uint8 reso = static_cast<uint8>(std::min(127, event.inst * 127 / 185));
if(type == 1) // lowpass filter
macro = MPT_AFORMAT("F0F000{} F0F001{} F0F00200")(mpt::afmt::HEX0<2>(cutoff), mpt::afmt::HEX0<2>(reso));
else if(type == 2) // highpass filter
macro = MPT_AFORMAT("F0F000{} F0F001{} F0F00210")(mpt::afmt::HEX0<2>(cutoff), mpt::afmt::HEX0<2>(reso));
else // no filter or unsupported filter type
macro = "F0F0007F F0F00100";
return true;
} else if(event.command == SymEvent::DSPEcho)
{
const uint8 type = (event.note < 5) ? event.note : 0;
const uint8 length = (event.param < 128) ? event.param : 127;
const uint8 feedback = (event.inst < 128) ? event.inst : 127;
macro = MPT_AFORMAT("F0F080{} F0F081{} F0F082{}")(mpt::afmt::HEX0<2>(type), mpt::afmt::HEX0<2>(length), mpt::afmt::HEX0<2>(feedback));
return true;
} else if(event.command == SymEvent::DSPDelay)
{
// DSP first has to be turned on from the Symphonie GUI before it can be used in a track (unlike Echo),
// so it's not implemented for now.
return false;
}
return false;
}
CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderSymMOD(MemoryFileReader file, const uint64 *pfilesize)
{
MPT_UNREFERENCED_PARAMETER(pfilesize);
SymFileHeader fileHeader;
if(!file.ReadStruct(fileHeader))
return ProbeWantMoreData;
if(!fileHeader.Validate())
return ProbeFailure;
if(!file.CanRead(sizeof(uint32be)))
return ProbeWantMoreData;
if(file.ReadInt32BE() >= 0)
return ProbeFailure;
return ProbeSuccess;
}
bool CSoundFile::ReadSymMOD(FileReader &file, ModLoadingFlags loadFlags)
{
file.Rewind();
SymFileHeader fileHeader;
if(!file.ReadStruct(fileHeader) || !fileHeader.Validate())
return false;
if(file.ReadInt32BE() >= 0)
return false;
else if(loadFlags == onlyVerifyHeader)
return true;
InitializeGlobals(MOD_TYPE_MPT);
m_SongFlags.set(SONG_LINEARSLIDES | SONG_EXFILTERRANGE | SONG_IMPORTED);
m_playBehaviour = GetDefaultPlaybackBehaviour(MOD_TYPE_IT);
m_playBehaviour.reset(kITShortSampleRetrig);
enum class ChunkType : int32
{
NumChannels = -1,
TrackLength = -2,
PatternSize = -3,
NumInstruments = -4,
EventSize = -5,
Tempo = -6,
ExternalSamples = -7,
PositionList = -10,
SampleFile = -11,
EmptySample = -12,
PatternEvents = -13,
InstrumentList = -14,
Sequences = -15,
InfoText = -16,
SamplePacked = -17,
SamplePacked16 = -18,
InfoType = -19,
InfoBinary = -20,
InfoString = -21,
SampleBoost = 10, // All samples will be normalized to this value
StereoDetune = 11, // Note: Not affected by no-DSP flag in instrument! So this would need to have its own plugin...
StereoPhase = 12,
};
uint32 trackLen = 0;
uint16 sampleBoost = 2500;
bool isSymphoniePro = false;
bool externalSamples = false;
std::vector<SymPosition> positions;
std::vector<SymSequence> sequences;
std::vector<SymEvent> patternData;
std::vector<SymInstrument> instruments;
file.SkipBack(sizeof(int32));
while(file.CanRead(sizeof(int32)))
{
const ChunkType chunkType = static_cast<ChunkType>(file.ReadInt32BE());
switch(chunkType)
{
// Simple values
case ChunkType::NumChannels:
if(auto numChannels = static_cast<CHANNELINDEX>(file.ReadUint32BE()); !m_nChannels && numChannels > 0 && numChannels <= MAX_BASECHANNELS)
{
m_nChannels = numChannels;
m_nSamplePreAmp = Clamp(512 / m_nChannels, 16, 128);
}
break;
case ChunkType::TrackLength:
trackLen = file.ReadUint32BE();
if(trackLen > 1024)
return false;
break;
case ChunkType::EventSize:
if(auto eventSize = (file.ReadUint32BE() & 0xFFFF); eventSize != sizeof(SymEvent))
return false;
break;
case ChunkType::Tempo:
m_nDefaultTempo = TEMPO(1.24 * std::min(file.ReadUint32BE(), uint32(800)));
break;
// Unused values
case ChunkType::NumInstruments: // determined from # of instrument headers instead
case ChunkType::PatternSize:
file.Skip(4);
break;
case ChunkType::SampleBoost:
sampleBoost = static_cast<uint16>(Clamp(file.ReadUint32BE(), 0u, 10000u));
isSymphoniePro = true;
break;
case ChunkType::StereoDetune:
case ChunkType::StereoPhase:
isSymphoniePro = true;
if(uint32 val = file.ReadUint32BE(); val != 0)
AddToLog(LogWarning, U_("Stereo Detune / Stereo Phase is not supported"));
break;
case ChunkType::ExternalSamples:
file.Skip(4);
if(!m_nSamples)
externalSamples = true;
break;
// Binary chunk types
case ChunkType::PositionList:
if((loadFlags & loadPatternData) && positions.empty())
positions = DecodeSymArray<SymPosition>(file);
else
file.Skip(file.ReadUint32BE());
break;
case ChunkType::SampleFile:
case ChunkType::SamplePacked:
case ChunkType::SamplePacked16:
if(m_nSamples >= instruments.size())
break;
if(!externalSamples && (loadFlags & loadSampleData) && CanAddMoreSamples())
{
const SAMPLEINDEX sample = ++m_nSamples;
std::vector<std::byte> unpackedSample;
FileReader chunk;
if(chunkType == ChunkType::SampleFile)
{
chunk = file.ReadChunk(file.ReadUint32BE());
} else if(chunkType == ChunkType::SamplePacked)
{
unpackedSample = DecodeSample8(file);
chunk = FileReader(mpt::as_span(unpackedSample));
} else // SamplePacked16
{
unpackedSample = DecodeSample16(file);
chunk = FileReader(mpt::as_span(unpackedSample));
}
if(!ReadIFFSample(sample, chunk)
&& !ReadWAVSample(sample, chunk)
&& !ReadAIFFSample(sample, chunk)
&& !ReadRawSymSample(Samples[sample], chunk))
{
AddToLog(LogWarning, U_("Unknown sample format."));
}
// Symphonie represents stereo instruments as two consecutive mono instruments which are
// automatically played at the same time. If this one uses a stereo sample, split it
// and map two OpenMPT instruments to the stereo halves to ensure correct playback
if(Samples[sample].uFlags[CHN_STEREO] && CanAddMoreSamples())
{
const SAMPLEINDEX sampleL = ++m_nSamples;
ctrlSmp::SplitStereo(Samples[sample], Samples[sampleL], Samples[sample], *this);
Samples[sampleL].filename = "Left";
Samples[sample].filename = "Right";
} else if(sample < instruments.size() && instruments[sample].channel == SymInstrument::StereoR && CanAddMoreSamples())
{
// Prevent misalignment of samples in exit.symmod (see condition in MoveNextMonoInstrument in Symphonie source)
m_nSamples++;
}
} else
{
// Skip sample
file.Skip(file.ReadUint32BE());
}
break;
case ChunkType::EmptySample:
if(CanAddMoreSamples())
m_nSamples++;
break;
case ChunkType::PatternEvents:
if((loadFlags & loadPatternData) && patternData.empty())
patternData = DecodeSymArray<SymEvent>(file);
else
file.Skip(file.ReadUint32BE());
break;
case ChunkType::InstrumentList:
if(instruments.empty())
instruments = DecodeSymArray<SymInstrument>(file);
else
file.Skip(file.ReadUint32BE());
break;
case ChunkType::Sequences:
if((loadFlags & loadPatternData) && sequences.empty())
sequences = DecodeSymArray<SymSequence>(file);
else
file.Skip(file.ReadUint32BE());
break;
case ChunkType::InfoText:
if(const auto text = DecodeSymChunk(file); !text.empty())
m_songMessage.Read(text.data(), text.size(), SongMessage::leLF);
break;
// Unused binary chunks
case ChunkType::InfoType:
case ChunkType::InfoBinary:
case ChunkType::InfoString:
file.Skip(file.ReadUint32BE());
break;
// Unrecognized chunk/value type
default:
return false;
}
}
if(!m_nChannels || !trackLen || instruments.empty())
return false;
if((loadFlags & loadPatternData) && (positions.empty() || patternData.empty() || sequences.empty()))
return false;
// Let's hope noone is going to use the 256th instrument ;)
if(instruments.size() >= MAX_INSTRUMENTS)
instruments.resize(MAX_INSTRUMENTS - 1u);
m_nInstruments = static_cast<INSTRUMENTINDEX>(instruments.size());
static_assert(MAX_SAMPLES >= MAX_INSTRUMENTS);
m_nSamples = std::max(m_nSamples, m_nInstruments);
// Supporting this is probably rather useless, as the paths will always be full Amiga paths. We just take the filename without path for now.
if(externalSamples)
{
#ifdef MPT_EXTERNAL_SAMPLES
m_nSamples = m_nInstruments;
for(SAMPLEINDEX sample = 1; sample <= m_nSamples; sample++)
{
const SymInstrument &symInst = instruments[sample - 1];
if(symInst.IsEmpty() || symInst.IsVirtual())
continue;
auto filename = mpt::PathString::FromUnicode(mpt::ToUnicode(mpt::Charset::Amiga_no_C1, symInst.GetName()));
if(file.GetOptionalFileName())
filename = file.GetOptionalFileName()->GetPath() + filename.GetFullFileName();
if(!LoadExternalSample(sample, filename))
AddToLog(LogError, MPT_UFORMAT("Unable to load sample {}: {}")(sample, filename));
else
ResetSamplePath(sample);
if(Samples[sample].uFlags[CHN_STEREO] && sample < m_nSamples)
{
const SAMPLEINDEX sampleL = sample + 1;
ctrlSmp::SplitStereo(Samples[sample], Samples[sampleL], Samples[sample], *this);
Samples[sampleL].filename = "Left";
Samples[sample].filename = "Right";
sample++;
}
}
#else
AddToLog(LogWarning, U_("External samples are not supported."));
#endif // MPT_EXTERNAL_SAMPLES
}
// Convert instruments
for(int pass = 0; pass < 2; pass++)
{
for(INSTRUMENTINDEX ins = 1; ins <= m_nInstruments; ins++)
{
SymInstrument &symInst = instruments[ins - 1];
if(symInst.IsEmpty())
continue;
// First load all regular instruments, and when we have the required information, render the virtual ones
if(symInst.IsVirtual() != (pass == 1))
continue;
SAMPLEINDEX sample = ins;
if(symInst.virt.header.IsVirtual())
{
const uint8 firstSource = symInst.virt.noteEvents[0].inst;
ModSample &target = Samples[sample];
if(symInst.virt.Render(*this, symInst.sampleFlags & SymInstrument::AsQueue, target, sampleBoost))
{
m_szNames[sample] = "Virtual";
if(firstSource < instruments.size())
symInst.downsample += instruments[firstSource].downsample;
} else
{
sample = firstSource + 1;
}
} else if(symInst.virt.header.IsTranswave())
{
const SymTranswaveInst transwaveInst = symInst.GetTranswave();
const auto &trans1 = transwaveInst.points[0], &trans2 = transwaveInst.points[1];
if(trans1.sourceIns < m_nSamples)
{
const ModSample emptySample;
const ModSample &smp1 = Samples[trans1.sourceIns + 1];
const ModSample &smp2 = trans2.sourceIns < m_nSamples ? Samples[trans2.sourceIns + 1] : emptySample;
ModSample &target = Samples[sample];
if(transwaveInst.Render(smp1, smp2, target))
{
m_szNames[sample] = "Transwave";
// Transwave instruments play an octave lower than the original source sample, but are 4x oversampled,
// so effectively they play an octave higher
symInst.transpose += 12;
}
}
}
if(ModInstrument *instr = AllocateInstrument(ins, sample); instr != nullptr && sample <= m_nSamples)
symInst.ConvertToMPT(*instr, Samples[sample], *this);
}
}
// Convert patterns
// map Symphonie positions to converted patterns
std::map<SymPosition, PATTERNINDEX> patternMap;
// map DSP commands to MIDI macro numbers
std::map<SymEvent, uint8> macroMap;
bool useDSP = false;
const uint32 patternSize = m_nChannels * trackLen;
const PATTERNINDEX numPatterns = mpt::saturate_cast<PATTERNINDEX>(patternData.size() / patternSize);
Patterns.ResizeArray(numPatterns);
Order().clear();
struct ChnState
{
float curVolSlide = 0; // Current volume slide factor of a channel
float curVolSlideAmt = 0; // Cumulative volume slide amount
float curPitchSlide = 0; // Current pitch slide factor of a channel
float curPitchSlideAmt = 0; // Cumulative pitch slide amount
bool stopped = false; // Sample paused or not (affects volume and pitch slides)
uint8 lastNote = 0; // Last note played on a channel
uint8 lastInst = 0; // Last instrument played on a channel
uint8 lastVol = 64; // Last specified volume of a channel (to avoid excessive Mxx commands)
uint8 channelVol = 100; // Volume multiplier, 0...100
uint8 calculatedVol = 64; // Final channel volume
uint8 fromAdd = 0; // Base sample offset for FROM and FR&P effects
uint8 curVibrato = 0;
uint8 curTremolo = 0;
uint8 sampleVibSpeed = 0;
uint8 sampleVibDepth = 0;
uint8 tonePortaAmt = 0;
uint16 sampleVibPhase = 0;
uint16 retriggerRemain = 0;
uint16 tonePortaRemain = 0;
};
std::vector<ChnState> chnStates(m_nChannels);
// In Symphonie, sequences represent the structure of a song, and not separate songs like in OpenMPT. Hence they will all be loaded into the same ModSequence.
for(SymSequence &seq : sequences)
{
if(seq.info == 1)
continue;
if(seq.info == -1)
break;
if(seq.start >= positions.size()
|| seq.length > positions.size()
|| seq.length == 0
|| positions.size() - seq.length < seq.start)
continue;
auto seqPositions = mpt::as_span(positions).subspan(seq.start, seq.length);
// Sequences are all part of the same song, just add a skip index as a divider
ModSequence &order = Order();
if(!order.empty())
order.push_back(ModSequence::GetIgnoreIndex());
for(auto &pos : seqPositions)
{
// before checking the map, apply the sequence transpose value
pos.transpose += seq.transpose;
// pattern already converted?
PATTERNINDEX patternIndex = 0;
if(patternMap.count(pos))
{
patternIndex = patternMap[pos];
} else if(loadFlags & loadPatternData)
{
// Convert pattern now
patternIndex = Patterns.InsertAny(pos.length);
if(patternIndex == PATTERNINDEX_INVALID)
break;
patternMap[pos] = patternIndex;
if(pos.pattern >= numPatterns || pos.start >= trackLen)
continue;
uint8 patternSpeed = static_cast<uint8>(pos.speed);
// This may intentionally read into the next pattern
auto srcEvent = patternData.cbegin() + (pos.pattern * patternSize) + (pos.start * m_nChannels);
const SymEvent emptyEvent{};
ModCommand syncPlayCommand;
for(ROWINDEX row = 0; row < pos.length; row++)
{
ModCommand *rowBase = Patterns[patternIndex].GetpModCommand(row, 0);
bool applySyncPlay = false;
for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++)
{
ModCommand &m = rowBase[chn];
const SymEvent &event = (srcEvent != patternData.cend()) ? *srcEvent : emptyEvent;
if(srcEvent != patternData.cend())
srcEvent++;
int8 note = (event.note >= 0 && event.note <= 84) ? event.note + 25 : -1;
uint8 origInst = event.inst;
uint8 mappedInst = 0;
if(origInst < instruments.size())
{
mappedInst = static_cast<uint8>(origInst + 1);
if(!(instruments[origInst].instFlags & SymInstrument::NoTranspose) && note >= 0)
note = Clamp(static_cast<int8>(note + pos.transpose), NOTE_MIN, NOTE_MAX);
}
// If we duplicated a stereo channel to this cell but the event is non-empty, remove it again.
if(m.note != NOTE_NONE && (event.command != SymEvent::KeyOn || event.note != -1 || event.inst != 0 || event.param != 0)
&& m.instr > 0 && m.instr <= instruments.size() && instruments[m.instr - 1].channel == SymInstrument::StereoR)
{
m.Clear();
}
auto &chnState = chnStates[chn];
if(applySyncPlay)
{
applySyncPlay = false;
m = syncPlayCommand;
if(m.command == CMD_NONE && chnState.calculatedVol != chnStates[chn - 1].calculatedVol)
{
m.command = CMD_CHANNELVOLUME;
m.param = chnState.calculatedVol = chnStates[chn - 1].calculatedVol;
}
if(!event.IsGlobal())
continue;
}
bool applyVolume = false;
switch(static_cast<SymEvent::Command>(event.command.get()))
{
case SymEvent::KeyOn:
if(event.param > SymEvent::VolCommand)
{
switch(event.param)
{
case SymEvent::StopSample:
m.volcmd = VOLCMD_PLAYCONTROL;
m.vol = 0;
chnState.stopped = true;
break;
case SymEvent::ContSample:
m.volcmd = VOLCMD_PLAYCONTROL;
m.vol = 1;
chnState.stopped = false;
break;
case SymEvent::KeyOff:
if(m.note == NOTE_NONE)
m.note = chnState.lastNote;
m.volcmd = VOLCMD_OFFSET;
m.vol = 1;
break;
case SymEvent::SpeedDown:
if(patternSpeed > 1)
{
m.command = CMD_SPEED;
m.param = --patternSpeed;
}
break;
case SymEvent::SpeedUp:
if(patternSpeed < 0xFF)
{
m.command = CMD_SPEED;
m.param = ++patternSpeed;
}
break;
case SymEvent::SetPitch:
chnState.lastNote = note;
if(mappedInst != chnState.lastInst)
break;
m.note = note;
m.command = CMD_TONEPORTAMENTO;
m.param = 0xFF;
chnState.curPitchSlide = 0;
chnState.tonePortaRemain = 0;
break;
// fine portamentos with range up to half a semitone
case SymEvent::PitchUp:
m.command = CMD_PORTAMENTOUP;
m.param = 0xF2;
break;
case SymEvent::PitchDown:
m.command = CMD_PORTAMENTODOWN;
m.param = 0xF2;
break;
case SymEvent::PitchUp2:
m.command = CMD_PORTAMENTOUP;
m.param = 0xF4;
break;
case SymEvent::PitchDown2:
m.command = CMD_PORTAMENTODOWN;
m.param = 0xF4;
break;
case SymEvent::PitchUp3:
m.command = CMD_PORTAMENTOUP;
m.param = 0xF8;
break;
case SymEvent::PitchDown3:
m.command = CMD_PORTAMENTODOWN;
m.param = 0xF8;
break;
}
} else
{
if(event.note >= 0 || event.param < 100)
{
if(event.note >= 0)
{
m.note = chnState.lastNote = note;
m.instr = chnState.lastInst = mappedInst;
chnState.curPitchSlide = 0;
chnState.tonePortaRemain = 0;
}
if(event.param > 0)
{
chnState.lastVol = mpt::saturate_round<uint8>(event.param * 0.64);
if(chnState.curVolSlide != 0)
applyVolume = true;
chnState.curVolSlide = 0;
}
}
}
if(const uint8 newVol = static_cast<uint8>(Util::muldivr_unsigned(chnState.lastVol, chnState.channelVol, 100));
applyVolume || chnState.calculatedVol != newVol)
{
chnState.calculatedVol = newVol;
m.command = CMD_CHANNELVOLUME;
m.param = newVol;
}
// Key-On commands with stereo instruments are played on both channels - unless there's already some sort of event
if(event.note > 0 && (chn < m_nChannels - 1) && !(chn % 2u)
&& origInst < instruments.size() && instruments[origInst].channel == SymInstrument::StereoL)
{
ModCommand &next = rowBase[chn + 1];
next = m;
next.instr++;
chnStates[chn + 1].lastVol = chnState.lastVol;
chnStates[chn + 1].curVolSlide = chnState.curVolSlide;
chnStates[chn + 1].curVolSlideAmt = chnState.curVolSlideAmt;
chnStates[chn + 1].curPitchSlide = chnState.curPitchSlide;
chnStates[chn + 1].curPitchSlideAmt = chnState.curPitchSlideAmt;
chnStates[chn + 1].retriggerRemain = chnState.retriggerRemain;
}
break;
// volume effects
// Symphonie has very fine fractional volume slides which are applied at the output sample rate,
// rather than per tick or per row, so instead let's simulate it based on the pattern speed
// by keeping track of the volume and using normal volume commands
// the math here is an approximation which works fine for most songs
case SymEvent::VolSlideUp:
chnState.curVolSlideAmt = 0;
chnState.curVolSlide = event.param * 0.0333f;
break;
case SymEvent::VolSlideDown:
chnState.curVolSlideAmt = 0;
chnState.curVolSlide = event.param * -0.0333f;
break;
case SymEvent::AddVolume:
m.command = m.param = 0;
break;
case SymEvent::Tremolo:
{
// both tremolo speed and depth can go much higher than OpenMPT supports,
// but modules will probably use pretty sane, supportable values anyway
// TODO: handle very small nonzero params
uint8 speed = std::min<uint8>(15, event.inst >> 3);
uint8 depth = std::min<uint8>(15, event.param >> 3);
chnState.curTremolo = (speed << 4) | depth;
}
break;
// pitch effects
// Pitch slides have a similar granularity to volume slides, and are approximated
// the same way here based on a rough comparison against Exx/Fxx slides
case SymEvent::PitchSlideUp:
chnState.curPitchSlideAmt = 0;
chnState.curPitchSlide = event.param * 0.0333f;
chnState.tonePortaRemain = 0;
break;
case SymEvent::PitchSlideDown:
chnState.curPitchSlideAmt = 0;
chnState.curPitchSlide = event.param * -0.0333f;
chnState.tonePortaRemain = 0;
break;
case SymEvent::PitchSlideTo:
if(note >= 0 && event.param > 0)
{
const int distance = std::abs((note - chnState.lastNote) * 32);
chnState.curPitchSlide = 0;
m.note = chnState.lastNote = note;
m.command = CMD_TONEPORTAMENTO;
chnState.tonePortaAmt = m.param = mpt::saturate_cast<ModCommand::PARAM>(distance / (2 * event.param));
chnState.tonePortaRemain = static_cast<uint16>(distance - std::min(distance, chnState.tonePortaAmt * (patternSpeed - 1)));
}
break;
case SymEvent::AddPitch:
// "The range (-128...127) is about 4 half notes."
m.command = m.param = 0;
break;
case SymEvent::Vibrato:
{
// both vibrato speed and depth can go much higher than OpenMPT supports,
// but modules will probably use pretty sane, supportable values anyway
// TODO: handle very small nonzero params
uint8 speed = std::min<uint8>(15, event.inst >> 3);
uint8 depth = std::min<uint8>(15, event.param);
chnState.curVibrato = (speed << 4) | depth;
}
break;
case SymEvent::AddHalfTone:
m.note = chnState.lastNote = Clamp(static_cast<uint8>(chnState.lastNote + event.param), NOTE_MIN, NOTE_MAX);
m.command = CMD_TONEPORTAMENTO;
m.param = 0xFF;
chnState.tonePortaRemain = 0;
break;
// DSP effects
case SymEvent::Filter:
#ifndef NO_PLUGINS
case SymEvent::DSPEcho:
case SymEvent::DSPDelay:
#endif
if(macroMap.count(event))
{
m.command = CMD_MIDI;
m.param = macroMap[event];
} else if(macroMap.size() < m_MidiCfg.Zxx.size())
{
uint8 param = static_cast<uint8>(macroMap.size());
if(ConvertDSP(event, m_MidiCfg.Zxx[param], *this))
{
m.command = CMD_MIDI;
m.param = macroMap[event] = 0x80 | param;
if(event.command == SymEvent::DSPEcho || event.command == SymEvent::DSPDelay)
useDSP = true;
}
}
break;
// other effects
case SymEvent::Retrig:
// This plays the note <param> times every <inst>+1 ticks.
// The effect continues on the following rows until the correct amount is reached.
if(event.param < 1)
break;
m.command = CMD_RETRIG;
m.param = static_cast<uint8>(std::min(15, event.inst + 1));
chnState.retriggerRemain = event.param * (event.inst + 1u);
break;
case SymEvent::SetSpeed:
m.command = CMD_SPEED;
m.param = patternSpeed = event.param ? event.param : 4u;
break;
// TODO this applies a fade on the sample level
case SymEvent::Emphasis:
m.command = CMD_NONE;
break;
case SymEvent::CV:
if(event.note == 0 || event.note == 4)
{
uint8 pan = (event.note == 4) ? event.inst : 128;
uint8 vol = std::min<uint8>(event.param, 100);
uint8 volL = static_cast<uint8>(vol * std::min(128, 256 - pan) / 128);
uint8 volR = static_cast<uint8>(vol * std::min(uint8(128), pan) / 128);
if(volL != chnState.channelVol)
{
chnState.channelVol = volL;
m.command = CMD_CHANNELVOLUME;
m.param = chnState.calculatedVol = static_cast<uint8>(Util::muldivr_unsigned(chnState.lastVol, chnState.channelVol, 100));
}
if(event.note == 4 && chn < (m_nChannels - 1) && chnStates[chn + 1].channelVol != volR)
{
chnStates[chn + 1].channelVol = volR;
ModCommand &next = rowBase[chn + 1];
next.command = CMD_CHANNELVOLUME;
next.param = chnState.calculatedVol = static_cast<uint8>(Util::muldivr_unsigned(chnState.lastVol, chnState.channelVol, 100));
}
}
break;
case SymEvent::CVAdd:
// Effect doesn't seem to exist in UI and code looks like a no-op
m.command = CMD_NONE;
break;
case SymEvent::SetFromAdd:
chnState.fromAdd = event.param;
chnState.sampleVibSpeed = 0;
chnState.sampleVibDepth = 0;
break;
case SymEvent::FromAdd:
// TODO need to verify how signedness of this value is treated
// C = -128...+127
//FORMEL: Neuer FADD := alter FADD + C* Samplelaenge/16384
chnState.fromAdd += event.param;
break;
case SymEvent::SampleVib:
chnState.sampleVibSpeed = event.inst;
chnState.sampleVibDepth = event.param;
break;
// sample effects
case SymEvent::FromAndPitch:
chnState.lastNote = note;
m.instr = chnState.lastInst = mappedInst;
[[fallthrough]];
case SymEvent::ReplayFrom:
m.note = chnState.lastNote;
if(note >= 0)
m.instr = chnState.lastInst = mappedInst;
if(event.command == SymEvent::ReplayFrom)
{
m.volcmd = VOLCMD_TONEPORTAMENTO;
m.vol = 1;
}
// don't always add the command, because often FromAndPitch is used with offset 0
// to act as a key-on which doesn't cancel volume slides, etc
if(event.param || chnState.fromAdd || chnState.sampleVibDepth)
{
double sampleVib = 0.0;
if(chnState.sampleVibDepth)
sampleVib = chnState.sampleVibDepth * (std::sin(chnState.sampleVibPhase * (mpt::numbers::pi * 2.0 / 1024.0) + 1.5 * mpt::numbers::pi) - 1.0) / 4.0;
m.command = CMD_OFFSETPERCENTAGE;
m.param = mpt::saturate_round<ModCommand::PARAM>(event.param + chnState.fromAdd + sampleVib);
}
chnState.tonePortaRemain = 0;
break;
}
// Any event which plays a note should re-enable continuous effects
if(m.note != NOTE_NONE)
chnState.stopped = false;
else if(chnState.stopped)
continue;
if(chnState.retriggerRemain)
{
chnState.retriggerRemain = std::max(chnState.retriggerRemain, static_cast<uint16>(patternSpeed)) - patternSpeed;
if(m.command == CMD_NONE)
{
m.command = CMD_RETRIG;
m.param = 0;
}
}
// Handle fractional volume slides
if(chnState.curVolSlide != 0)
{
chnState.curVolSlideAmt += chnState.curVolSlide * patternSpeed;
if(m.command == CMD_NONE)
{
if(patternSpeed > 1 && chnState.curVolSlideAmt >= (patternSpeed - 1))
{
uint8 slideAmt = std::min<uint8>(15, mpt::saturate_round<uint8>(chnState.curVolSlideAmt / (patternSpeed - 1)));
chnState.curVolSlideAmt -= slideAmt * (patternSpeed - 1);
// normal slide up
m.command = CMD_CHANNELVOLSLIDE;
m.param = slideAmt << 4;
} else if(chnState.curVolSlideAmt >= 1.0f)
{
uint8 slideAmt = std::min<uint8>(15, mpt::saturate_round<uint8>(chnState.curVolSlideAmt));
chnState.curVolSlideAmt -= slideAmt;
// fine slide up
m.command = CMD_CHANNELVOLSLIDE;
m.param = (slideAmt << 4) | 0x0F;
} else if(patternSpeed > 1 && chnState.curVolSlideAmt <= -(patternSpeed - 1))
{
uint8 slideAmt = std::min<uint8>(15, mpt::saturate_round<uint8>(-chnState.curVolSlideAmt / (patternSpeed - 1)));
chnState.curVolSlideAmt += slideAmt * (patternSpeed - 1);
// normal slide down
m.command = CMD_CHANNELVOLSLIDE;
m.param = slideAmt;
} else if(chnState.curVolSlideAmt <= -1.0f)
{
uint8 slideAmt = std::min<uint8>(14, mpt::saturate_round<uint8>(-chnState.curVolSlideAmt));
chnState.curVolSlideAmt += slideAmt;
// fine slide down
m.command = CMD_CHANNELVOLSLIDE;
m.param = slideAmt | 0xF0;
}
}
}
// Handle fractional pitch slides
if(chnState.curPitchSlide != 0)
{
chnState.curPitchSlideAmt += chnState.curPitchSlide * patternSpeed;
if(m.command == CMD_NONE)
{
if(patternSpeed > 1 && chnState.curPitchSlideAmt >= (patternSpeed - 1))
{
uint8 slideAmt = std::min<uint8>(0xDF, mpt::saturate_round<uint8>(chnState.curPitchSlideAmt / (patternSpeed - 1)));
chnState.curPitchSlideAmt -= slideAmt * (patternSpeed - 1);
// normal slide up
m.command = CMD_PORTAMENTOUP;
m.param = slideAmt;
} else if(chnState.curPitchSlideAmt >= 1.0f)
{
uint8 slideAmt = std::min<uint8>(15, mpt::saturate_round<uint8>(chnState.curPitchSlideAmt));
chnState.curPitchSlideAmt -= slideAmt;
// fine slide up
m.command = CMD_PORTAMENTOUP;
m.param = slideAmt | 0xF0;
} else if(patternSpeed > 1 && chnState.curPitchSlideAmt <= -(patternSpeed - 1))
{
uint8 slideAmt = std::min<uint8>(0xDF, mpt::saturate_round<uint8>(-chnState.curPitchSlideAmt / (patternSpeed - 1)));
chnState.curPitchSlideAmt += slideAmt * (patternSpeed - 1);
// normal slide down
m.command = CMD_PORTAMENTODOWN;
m.param = slideAmt;
} else if(chnState.curPitchSlideAmt <= -1.0f)
{
uint8 slideAmt = std::min<uint8>(14, mpt::saturate_round<uint8>(-chnState.curPitchSlideAmt));
chnState.curPitchSlideAmt += slideAmt;
// fine slide down
m.command = CMD_PORTAMENTODOWN;
m.param = slideAmt | 0xF0;
}
}
// TODO: use volume column if effect column is occupied
else if(m.volcmd == VOLCMD_NONE)
{
if(patternSpeed > 1 && chnState.curPitchSlideAmt / 4 >= (patternSpeed - 1))
{
uint8 slideAmt = std::min<uint8>(9, mpt::saturate_round<uint8>(chnState.curPitchSlideAmt / (patternSpeed - 1)) / 4);
chnState.curPitchSlideAmt -= slideAmt * (patternSpeed - 1) * 4;
m.volcmd = VOLCMD_PORTAUP;
m.vol = slideAmt;
} else if(patternSpeed > 1 && chnState.curPitchSlideAmt / 4 <= -(patternSpeed - 1))
{
uint8 slideAmt = std::min<uint8>(9, mpt::saturate_round<uint8>(-chnState.curPitchSlideAmt / (patternSpeed - 1)) / 4);
chnState.curPitchSlideAmt += slideAmt * (patternSpeed - 1) * 4;
m.volcmd = VOLCMD_PORTADOWN;
m.vol = slideAmt;
}
}
}
// Vibrato and Tremolo
if(m.command == CMD_NONE && chnState.curVibrato != 0)
{
m.command = CMD_VIBRATO;
m.param = chnState.curVibrato;
}
if(m.command == CMD_NONE && chnState.curTremolo != 0)
{
m.command = CMD_TREMOLO;
m.param = chnState.curTremolo;
}
// Tone Portamento
if(m.command != CMD_TONEPORTAMENTO && chnState.tonePortaRemain)
{
if(m.command == CMD_NONE)
m.command = CMD_TONEPORTAMENTO;
else
m.volcmd = VOLCMD_TONEPORTAMENTO;
chnState.tonePortaRemain -= std::min(chnState.tonePortaRemain, static_cast<uint16>(chnState.tonePortaAmt * (patternSpeed - 1)));
}
chnState.sampleVibPhase = (chnState.sampleVibPhase + chnState.sampleVibSpeed * patternSpeed) & 1023;
if(!(chn % 2u) && chnState.lastInst && chnState.lastInst <= instruments.size()
&& (instruments[chnState.lastInst - 1].instFlags & SymInstrument::SyncPlay))
{
syncPlayCommand = m;
applySyncPlay = true;
if(syncPlayCommand.instr && instruments[chnState.lastInst - 1].channel == SymInstrument::StereoL)
syncPlayCommand.instr++;
}
}
}
Patterns[patternIndex].WriteEffect(EffectWriter(CMD_SPEED, static_cast<uint8>(pos.speed)).Row(0).RetryNextRow());
}
order.insert(order.GetLength(), std::max(pos.loopNum.get(), uint16(1)), patternIndex);
// Undo transpose tweak
pos.transpose -= seq.transpose;
}
}
#ifndef NO_PLUGINS
if(useDSP)
{
SNDMIXPLUGIN &plugin = m_MixPlugins[0];
plugin.Destroy();
memcpy(&plugin.Info.dwPluginId1, "SymM", 4);
memcpy(&plugin.Info.dwPluginId2, "Echo", 4);
plugin.Info.routingFlags = SNDMIXPLUGININFO::irAutoSuspend;
plugin.Info.mixMode = 0;
plugin.Info.gain = 10;
plugin.Info.reserved = 0;
plugin.Info.dwOutputRouting = 0;
std::fill(plugin.Info.dwReserved, plugin.Info.dwReserved + std::size(plugin.Info.dwReserved), 0);
plugin.Info.szName = "Echo";
plugin.Info.szLibraryName = "SymMOD Echo";
m_MixPlugins[1].Info.szName = "No Echo";
}
#endif // NO_PLUGINS
// Channel panning
for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++)
{
InitChannel(chn);
ChnSettings[chn].nPan = (chn & 1) ? 256 : 0;
ChnSettings[chn].nMixPlugin = useDSP ? 1 : 0; // For MIDI macros controlling the echo DSP
}
m_modFormat.formatName = U_("Symphonie");
m_modFormat.type = U_("symmod");
if(!isSymphoniePro)
m_modFormat.madeWithTracker = U_("Symphonie"); // or Symphonie Jr
else if(instruments.size() <= 128)
m_modFormat.madeWithTracker = U_("Symphonie Pro");
else
m_modFormat.madeWithTracker = U_("Symphonie Pro 256");
m_modFormat.charset = mpt::Charset::Amiga_no_C1;
return true;
}
OPENMPT_NAMESPACE_END