Add CC modifiers to end, loop_start, loop_end

The end cc modifiers are computed at voice start like the offset
The loop cc modifiers are updated in each block
This commit is contained in:
Paul Fd 2021-03-28 23:37:38 +02:00
parent 689bb99e40
commit 10c5e64ba1
11 changed files with 187 additions and 79 deletions

View file

@ -23,10 +23,12 @@ FloatSpec delayMod { 0.0f, {0.0f, 100.0f}, kPermissiveBounds };
Int64Spec offset { 0, {0, uint32_t_max}, kPermissiveBounds };
Int64Spec offsetMod { 0, {0, uint32_t_max}, kPermissiveBounds };
Int64Spec offsetRandom { 0, {0, uint32_t_max}, kPermissiveBounds };
UInt32Spec sampleEnd { uint32_t_max, {0, uint32_t_max}, kEnforceBounds };
Int64Spec sampleEnd { uint32_t_max, {0, uint32_t_max}, kEnforceBounds };
Int64Spec sampleEndMod { 0, {-uint32_t_max, uint32_t_max}, kPermissiveBounds };
UInt32Spec sampleCount { 0, {0, uint32_t_max}, kEnforceUpperBound };
UInt32Spec loopStart { 0, {0, uint32_t_max}, kEnforceUpperBound };
UInt32Spec loopEnd { uint32_t_max, {0, uint32_t_max}, kEnforceUpperBound };
Int64Spec loopStart { 0, {0, uint32_t_max}, kEnforceUpperBound };
Int64Spec loopEnd { uint32_t_max, {0, uint32_t_max}, kEnforceUpperBound };
Int64Spec loopMod { 0, {-uint32_t_max, uint32_t_max}, kPermissiveBounds };
UInt32Spec loopCount { 0, {0, uint32_t_max}, kEnforceUpperBound };
FloatSpec loopCrossfade { 1e-3, {1e-3, 1.0f}, kEnforceLowerBound|kPermissiveUpperBound };
ESpec<OscillatorEnabled> oscillator { OscillatorEnabled::Auto, {OscillatorEnabled::Auto, OscillatorEnabled::On}, 0 };

View file

@ -123,10 +123,12 @@ namespace Default
extern const OpcodeSpec<int64_t> offset;
extern const OpcodeSpec<int64_t> offsetMod;
extern const OpcodeSpec<int64_t> offsetRandom;
extern const OpcodeSpec<uint32_t> sampleEnd;
extern const OpcodeSpec<int64_t> sampleEnd;
extern const OpcodeSpec<int64_t> sampleEndMod;
extern const OpcodeSpec<uint32_t> sampleCount;
extern const OpcodeSpec<uint32_t> loopStart;
extern const OpcodeSpec<uint32_t> loopEnd;
extern const OpcodeSpec<int64_t> loopStart;
extern const OpcodeSpec<int64_t> loopEnd;
extern const OpcodeSpec<int64_t> loopMod;
extern const OpcodeSpec<uint32_t> loopCount;
extern const OpcodeSpec<float> loopCrossfade;
extern const OpcodeSpec<float> oscillatorPhase;

View file

@ -256,7 +256,8 @@ absl::optional<sfz::FileInformation> sfz::FilePool::getFileInformation(const Fil
if (haveInstrumentInfo && instrumentInfo.loop_count > 0) {
returnedValue.hasLoop = true;
returnedValue.loopStart = instrumentInfo.loops[0].start;
returnedValue.loopEnd = min(returnedValue.end, instrumentInfo.loops[0].end - 1);
returnedValue.loopEnd =
min(returnedValue.end, static_cast<int64_t>(instrumentInfo.loops[0].end - 1));
}
} else {
// TODO loops ignored when reversed

View file

@ -51,10 +51,10 @@ using FileAudioBuffer = AudioBuffer<float, 2, config::defaultAlignment,
using FileAudioBufferPtr = std::shared_ptr<FileAudioBuffer>;
struct FileInformation {
uint32_t end { Default::sampleEnd };
uint32_t maxOffset { 0 };
uint32_t loopStart { Default::loopStart };
uint32_t loopEnd { Default::loopEnd };
int64_t end { Default::sampleEnd };
int64_t maxOffset { 0 };
int64_t loopStart { Default::loopStart };
int64_t loopEnd { Default::loopEnd };
bool hasLoop { false };
double sampleRate { config::defaultSampleRate };
int numChannels { 0 };

View file

@ -108,6 +108,12 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
case hash("end"):
sampleEnd = opcode.read(Default::sampleEnd);
break;
case hash("end_oncc&"): // also end_cc&
if (opcode.parameters.back() > config::numCCs)
return false;
endCC[opcode.parameters.back()] = opcode.read(Default::sampleEndMod);
break;
case hash("count"):
sampleCount = opcode.readOptional(Default::sampleCount);
loopMode = LoopMode::one_shot;
@ -124,6 +130,18 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
case hash("loop_start"): // also loopstart
loopRange.setStart(opcode.read(Default::loopStart));
break;
case hash("loop_start_oncc&"): // also loop_start_cc&
if (opcode.parameters.back() > config::numCCs)
return false;
loopStartCC[opcode.parameters.back()] = opcode.read(Default::loopMod);
break;
case hash("loop_end_oncc&"): // also loop_end_cc&
if (opcode.parameters.back() > config::numCCs)
return false;
loopEndCC[opcode.parameters.back()] = opcode.read(Default::loopMod);
break;
case hash("loop_crossfade"):
loopCrossfade = opcode.read(Default::loopCrossfade);
break;
@ -1552,6 +1570,7 @@ uint64_t sfz::Region::getOffset(const MidiState& midiState, Oversampling factor)
uint64_t finalOffset = offset + offsetDistribution(Random::randomGenerator);
for (const auto& mod: offsetCC)
finalOffset += static_cast<uint64_t>(mod.data * midiState.getCCValue(mod.cc));
return Default::offset.bounds.clamp(finalOffset) * static_cast<uint64_t>(factor);
}
@ -1562,25 +1581,38 @@ float sfz::Region::getDelay(const MidiState& midiState) const noexcept
finalDelay += delayDistribution(Random::randomGenerator);
for (const auto& mod: delayCC)
finalDelay += mod.data * midiState.getCCValue(mod.cc);
return Default::delay.bounds.clamp(finalDelay);
}
uint32_t sfz::Region::trueSampleEnd(Oversampling factor) const noexcept
uint32_t sfz::Region::getSampleEnd(MidiState& midiState, Oversampling factor) const noexcept
{
if (sampleEnd <= 0)
return 0;
int64_t end = sampleEnd;
for (const auto& mod: endCC)
end += static_cast<int64_t>(mod.data * midiState.getCCValue(mod.cc));
return static_cast<uint32_t>(sampleEnd) * static_cast<uint32_t>(factor);
end = clamp(end, int64_t { 0 }, sampleEnd);
return static_cast<uint32_t>(end) * static_cast<uint32_t>(factor);
}
uint32_t sfz::Region::loopStart(Oversampling factor) const noexcept
uint32_t sfz::Region::loopStart(MidiState& midiState, Oversampling factor) const noexcept
{
return loopRange.getStart() * static_cast<uint32_t>(factor);
auto start = loopRange.getStart();
for (const auto& mod: loopStartCC)
start += static_cast<int64_t>(mod.data * midiState.getCCValue(mod.cc));
start = clamp(start, int64_t { 0 }, sampleEnd);
return static_cast<uint32_t>(start) * static_cast<uint32_t>(factor);
}
uint32_t sfz::Region::loopEnd(Oversampling factor) const noexcept
uint32_t sfz::Region::loopEnd(MidiState& midiState, Oversampling factor) const noexcept
{
return loopRange.getEnd() * static_cast<uint32_t>(factor);
auto end = loopRange.getEnd();
for (const auto& mod: loopEndCC)
end += static_cast<int64_t>(mod.data * midiState.getCCValue(mod.cc));
end = clamp(end, int64_t { 0 }, sampleEnd);
return static_cast<uint32_t>(end) * static_cast<uint32_t>(factor);
}
float sfz::Region::getNoteGain(int noteNumber, float velocity) const noexcept

View file

@ -180,7 +180,7 @@ struct Region {
*
* @return uint32_t
*/
uint32_t trueSampleEnd(Oversampling factor = Oversampling::x1) const noexcept;
uint32_t getSampleEnd(MidiState& midiState, Oversampling factor = Oversampling::x1) const noexcept;
/**
* @brief Parse a new opcode into the region to fill in the proper parameters.
* This must be called multiple times for each opcode applying to this region.
@ -259,8 +259,8 @@ struct Region {
void offsetAllKeys(int offset) noexcept;
uint32_t loopStart(Oversampling factor = Oversampling::x1) const noexcept;
uint32_t loopEnd(Oversampling factor = Oversampling::x1) const noexcept;
uint32_t loopStart(MidiState& midiState, Oversampling factor = Oversampling::x1) const noexcept;
uint32_t loopEnd(MidiState& midiState, Oversampling factor = Oversampling::x1) const noexcept;
/**
* @brief Get the gain this region contributes into the input of the Nth
@ -305,10 +305,13 @@ struct Region {
int64_t offset { Default::offset }; // offset
int64_t offsetRandom { Default::offsetRandom }; // offset_random
CCMap<int64_t> offsetCC { Default::offsetMod };
uint32_t sampleEnd { Default::sampleEnd }; // end
int64_t sampleEnd { Default::sampleEnd }; // end
CCMap<int64_t> endCC { Default::sampleEndMod };
absl::optional<uint32_t> sampleCount {}; // count
absl::optional<LoopMode> loopMode {}; // loopmode
UncheckedRange<uint32_t> loopRange { Default::loopStart, Default::loopEnd }; //loopstart and loopend
UncheckedRange<int64_t> loopRange { Default::loopStart, Default::loopEnd }; //loopstart and loopend
CCMap<int64_t> loopStartCC { Default::sampleEndMod };
CCMap<int64_t> loopEndCC { Default::sampleEndMod };
absl::optional<uint32_t> loopCount {}; // count
float loopCrossfade { Default::loopCrossfade }; // loop_crossfade

View file

@ -590,7 +590,7 @@ void Synth::Impl::finalizeSfzLoad()
}
if (!region.isOscillator()) {
region.sampleEnd = std::min(region.sampleEnd, fileInformation->end);
region.sampleEnd = min(region.sampleEnd, fileInformation->end);
if (fileInformation->hasLoop) {
if (region.loopRange.getStart() == Default::loopStart)

View file

@ -165,6 +165,11 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co
client.receive<'h'>(delay, path, region.sampleEnd);
} break;
MATCH("/region&/end_cc&", "") {
GET_REGION_OR_BREAK(indices[0])
client.receive<'h'>(delay, path, region.endCC.getWithDefault(indices[1]));
} break;
MATCH("/region&/enabled", "") {
GET_REGION_OR_BREAK(indices[0])
if (region.disabled()) {
@ -208,6 +213,16 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co
client.receive(delay, path, "hh", args);
} break;
MATCH("/region&/loop_start_cc&", "") {
GET_REGION_OR_BREAK(indices[0])
client.receive<'h'>(delay, path, region.loopStartCC.getWithDefault(indices[1]));
} break;
MATCH("/region&/loop_end_cc&", "") {
GET_REGION_OR_BREAK(indices[0])
client.receive<'h'>(delay, path, region.loopEndCC.getWithDefault(indices[1]));
} break;
MATCH("/region&/loop_mode", "") {
GET_REGION_OR_BREAK(indices[0])
if (!region.loopMode) {

View file

@ -459,7 +459,7 @@ bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexc
impl.triggerDelay_ = delay;
impl.initialDelay_ = delay + static_cast<int>(region.getDelay(resources.midiState) * impl.sampleRate_);
impl.baseFrequency_ = resources.tuning.getFrequencyOfKey(impl.triggerEvent_.number);
impl.sampleSize_ = region.trueSampleEnd(resources.filePool.getOversamplingFactor()) - impl.sourcePosition_ - 1;
impl.sampleSize_ = region.getSampleEnd(resources.midiState, resources.filePool.getOversamplingFactor()) - impl.sourcePosition_ - 1;
impl.bendStepFactor_ = centsFactor(region.bendStep);
impl.bendSmoother_.setSmoothing(region.bendSmooth, impl.sampleRate_);
impl.bendSmoother_.reset(centsFactor(region.getBendInCents(resources.midiState.getPitchBend())));
@ -578,7 +578,6 @@ void Voice::registerNoteOff(int delay, int noteNumber, float velocity) noexcept
void Voice::registerCC(int delay, int ccNumber, float ccValue) noexcept
{
ASSERT(ccValue >= 0.0 && ccValue <= 1.0);
Impl& impl = *impl_;
if (impl.region_ == nullptr)
return;
@ -986,7 +985,8 @@ void Voice::Impl::fillWithData(AudioSpan<float> buffer) noexcept
add1<int>(sourcePosition_, *indices);
}
// calculate loop characteristics
// Update loop characteristics with the current CC state
updateLoopInformation();
const auto loop = this->loop_;
// Looping logic
@ -1035,7 +1035,7 @@ void Voice::Impl::fillWithData(AudioSpan<float> buffer) noexcept
}
const auto sampleEnd = min(
int(region_->trueSampleEnd(resources_.filePool.getOversamplingFactor())),
int(sampleSize_),
int(currentPromise_->information.end),
int(source.getNumFrames()))
- 1;
@ -1591,12 +1591,14 @@ void Voice::Impl::updateLoopInformation() noexcept
if (!region_->shouldLoop())
return;
const auto& info = currentPromise_->information;
const auto factor = resources_.filePool.getOversamplingFactor();
const auto rate = info.sampleRate;
loop_.end = static_cast<int>(region_->loopEnd(factor));
loop_.start = static_cast<int>(region_->loopStart(factor));
Resources& resources = resources_;
const FileInformation& info = currentPromise_->information;
const Oversampling factor = resources_.filePool.getOversamplingFactor();
const double rate = info.sampleRate;
loop_.start = static_cast<int>(region_->loopStart(resources.midiState, factor));
loop_.end = max(static_cast<int>(region_->loopEnd(resources.midiState, factor)), loop_.start);
loop_.size = loop_.end + 1 - loop_.start;
loop_.xfSize = static_cast<int>(lroundPositive(region_->loopCrossfade * rate));
// Clamp the crossfade to the part available before the loop starts

View file

@ -535,9 +535,9 @@ TEST_CASE("[Files] Looped regions taken from files and possibly overriden")
REQUIRE( synth.getRegionView(1)->loopMode == LoopMode::no_loop );
REQUIRE( synth.getRegionView(2)->loopMode == LoopMode::loop_continuous );
REQUIRE(synth.getRegionView(0)->loopRange == Range<uint32_t> { 77554, 186581 });
REQUIRE(synth.getRegionView(1)->loopRange == Range<uint32_t> { 77554, 186581 });
REQUIRE(synth.getRegionView(2)->loopRange == Range<uint32_t> { 4, 124 });
REQUIRE(synth.getRegionView(0)->loopRange == Range<int64_t> { 77554, 186581 });
REQUIRE(synth.getRegionView(1)->loopRange == Range<int64_t> { 77554, 186581 });
REQUIRE(synth.getRegionView(2)->loopRange == Range<int64_t> { 4, 124 });
}
TEST_CASE("[Files] Looped regions can start at 0")
@ -548,7 +548,7 @@ TEST_CASE("[Files] Looped regions can start at 0")
)");
REQUIRE( synth.getNumRegions() == 1 );
REQUIRE( synth.getRegionView(0)->loopMode == LoopMode::loop_continuous );
REQUIRE( synth.getRegionView(0)->loopRange == Range<uint32_t> { 0, synth.getRegionView(0)->sampleEnd } );
REQUIRE( synth.getRegionView(0)->loopRange == Range<int64_t> { 0, synth.getRegionView(0)->sampleEnd } );
}
TEST_CASE("[Synth] Release triggers automatically sets the loop mode")

View file

@ -189,30 +189,55 @@ TEST_CASE("[Values] End")
std::vector<std::string> messageList;
Client client(&messageList);
client.setReceiveCallback(&simpleMessageReceiver);
synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"(
<region> sample=kick.wav end=194
<region> sample=kick.wav end=-1
<region> sample=kick.wav end=0
<region> sample=kick.wav end=194 end=-1
<region> sample=kick.wav end=0 end=194
)");
synth.dispatchMessage(client, 0, "/region0/end", "", nullptr);
synth.dispatchMessage(client, 0, "/region0/enabled", "", nullptr);
synth.dispatchMessage(client, 0, "/region1/enabled", "", nullptr);
synth.dispatchMessage(client, 0, "/region2/enabled", "", nullptr);
synth.dispatchMessage(client, 0, "/region3/enabled", "", nullptr);
synth.dispatchMessage(client, 0, "/region4/enabled", "", nullptr);
synth.dispatchMessage(client, 0, "/region4/end", "", nullptr);
std::vector<std::string> expected {
"/region0/end,h : { 194 }",
"/region0/enabled,T : { }",
"/region1/enabled,F : { }",
"/region2/enabled,F : { }",
"/region3/enabled,F : { }",
"/region4/enabled,T : { }",
"/region4/end,h : { 194 }",
};
REQUIRE(messageList == expected);
SECTION("Basic")
{
synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"(
<region> sample=kick.wav end=194
<region> sample=kick.wav end=-1
<region> sample=kick.wav end=0
<region> sample=kick.wav end=194 end=-1
<region> sample=kick.wav end=0 end=194
)");
synth.dispatchMessage(client, 0, "/region0/end", "", nullptr);
synth.dispatchMessage(client, 0, "/region0/enabled", "", nullptr);
synth.dispatchMessage(client, 0, "/region1/enabled", "", nullptr);
synth.dispatchMessage(client, 0, "/region2/enabled", "", nullptr);
synth.dispatchMessage(client, 0, "/region3/enabled", "", nullptr);
synth.dispatchMessage(client, 0, "/region4/enabled", "", nullptr);
synth.dispatchMessage(client, 0, "/region4/end", "", nullptr);
std::vector<std::string> expected {
"/region0/end,h : { 194 }",
"/region0/enabled,T : { }",
"/region1/enabled,F : { }",
"/region2/enabled,F : { }",
"/region3/enabled,F : { }",
"/region4/enabled,T : { }",
"/region4/end,h : { 194 }",
};
REQUIRE(messageList == expected);
}
SECTION("CC")
{
synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"(
<region> sample=kick.wav
<region> sample=kick.wav end_cc12=12
<region> sample=kick.wav end_oncc12=-12
<region> sample=kick.wav end_cc14=14 end_cc12=12 end_oncc12=-12
)");
synth.dispatchMessage(client, 0, "/region0/end_cc12", "", nullptr);
synth.dispatchMessage(client, 0, "/region1/end_cc12", "", nullptr);
synth.dispatchMessage(client, 0, "/region2/end_cc12", "", nullptr);
synth.dispatchMessage(client, 0, "/region3/end_cc14", "", nullptr);
synth.dispatchMessage(client, 0, "/region3/end_cc12", "", nullptr);
std::vector<std::string> expected {
"/region0/end_cc12,h : { 0 }",
"/region1/end_cc12,h : { 12 }",
"/region2/end_cc12,h : { -12 }",
"/region3/end_cc14,h : { 14 }",
"/region3/end_cc12,h : { -12 }",
};
REQUIRE(messageList == expected);
}
}
TEST_CASE("[Values] Count")
@ -305,23 +330,49 @@ TEST_CASE("[Values] Loop range")
std::vector<std::string> messageList;
Client client(&messageList);
client.setReceiveCallback(&simpleMessageReceiver);
synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"(
<region> sample=kick.wav
<region> sample=kick.wav loop_start=10 loop_end=100
<region> sample=kick.wav loopstart=10 loopend=100
<region> sample=kick.wav loop_start=-1 loopend=-100
)");
synth.dispatchMessage(client, 0, "/region0/loop_range", "", nullptr);
synth.dispatchMessage(client, 0, "/region1/loop_range", "", nullptr);
synth.dispatchMessage(client, 0, "/region2/loop_range", "", nullptr);
synth.dispatchMessage(client, 0, "/region3/loop_range", "", nullptr);
std::vector<std::string> expected {
"/region0/loop_range,hh : { 0, 44011 }", // Default loop points in the file
"/region1/loop_range,hh : { 10, 100 }",
"/region2/loop_range,hh : { 10, 100 }",
"/region3/loop_range,hh : { 0, 44011 }",
};
REQUIRE(messageList == expected);
SECTION("Basic")
{
synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"(
<region> sample=kick.wav
<region> sample=kick.wav loop_start=10 loop_end=100
<region> sample=kick.wav loopstart=10 loopend=100
<region> sample=kick.wav loop_start=-1 loopend=-100
)");
synth.dispatchMessage(client, 0, "/region0/loop_range", "", nullptr);
synth.dispatchMessage(client, 0, "/region1/loop_range", "", nullptr);
synth.dispatchMessage(client, 0, "/region2/loop_range", "", nullptr);
synth.dispatchMessage(client, 0, "/region3/loop_range", "", nullptr);
std::vector<std::string> expected {
"/region0/loop_range,hh : { 0, 44011 }", // Default loop points in the file
"/region1/loop_range,hh : { 10, 100 }",
"/region2/loop_range,hh : { 10, 100 }",
"/region3/loop_range,hh : { 0, 44011 }",
};
REQUIRE(messageList == expected);
}
SECTION("CC")
{
synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"(
<region> sample=kick.wav
<region> sample=kick.wav loop_start_cc12=10 loop_end_cc14=-100
<region> sample=kick.wav loop_start_oncc12=-10 loop_end_oncc14=100
)");
synth.dispatchMessage(client, 0, "/region0/loop_start_cc12", "", nullptr);
synth.dispatchMessage(client, 0, "/region0/loop_end_cc14", "", nullptr);
synth.dispatchMessage(client, 0, "/region1/loop_start_cc12", "", nullptr);
synth.dispatchMessage(client, 0, "/region1/loop_end_cc14", "", nullptr);
synth.dispatchMessage(client, 0, "/region2/loop_start_cc12", "", nullptr);
synth.dispatchMessage(client, 0, "/region2/loop_end_cc14", "", nullptr);
std::vector<std::string> expected {
"/region0/loop_start_cc12,h : { 0 }",
"/region0/loop_end_cc14,h : { 0 }",
"/region1/loop_start_cc12,h : { 10 }",
"/region1/loop_end_cc14,h : { -100 }",
"/region2/loop_start_cc12,h : { -10 }",
"/region2/loop_end_cc14,h : { 100 }",
};
REQUIRE(messageList == expected);
}
}
TEST_CASE("[Values] Loop crossfade")