Add loprog/hiprob in the API and engine

This commit is contained in:
Paul Fd 2021-12-30 20:23:47 +01:00
parent 3d50afc794
commit ff0257dc07
18 changed files with 203 additions and 23 deletions

View file

@ -445,6 +445,23 @@ SFIZZ_EXPORTED_API void sfizz_send_cc(sfizz_synth_t* synth, int delay, int cc_nu
*/ */
SFIZZ_EXPORTED_API void sfizz_send_hdcc(sfizz_synth_t* synth, int delay, int cc_number, float norm_value); SFIZZ_EXPORTED_API void sfizz_send_hdcc(sfizz_synth_t* synth, int delay, int cc_number, float norm_value);
/**
* @brief Send a program change event to the synth.
* @since 1.1.2
*
* This command should be delay-ordered with all other midi-type events
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
* synth is undefined.
*
* @param synth The synth.
* @param delay The delay of the event in the block, in samples.
* @param program The program number, in domain 0 to 127.
*
* @par Thread-safety constraints
* - @b RT: the function must be invoked from the Real-time thread
*/
SFIZZ_EXPORTED_API void sfizz_send_program_change(sfizz_synth_t* synth, int delay, int program);
/** /**
* @brief Send a high precision CC automation to the synth. * @brief Send a high precision CC automation to the synth.
* @since 1.0.0 * @since 1.0.0

View file

@ -495,6 +495,23 @@ public:
*/ */
void hdcc(int delay, int ccNumber, float normValue) noexcept; void hdcc(int delay, int ccNumber, float normValue) noexcept;
/**
* @brief Send a program change event to the synth
* @since 1.1.2
*
* This command should be delay-ordered with all other midi-type events
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
* synth is undefined.
*
* @param delay the delay at which the event occurs; this should be lower
* than the size of the block in the next call to renderBlock().
* @param program the cc number, in domain 0 to 127.
*
* @par Thread-safety constraints
* - @b RT: the function must be invoked from the Real-time thread
*/
void programChange(int delay, int program) noexcept;
/** /**
* @brief Send a high precision CC automation to the synth * @brief Send a high precision CC automation to the synth
* *

View file

@ -152,6 +152,12 @@ public:
* @return float * @return float
*/ */
double getBeatsPerFrame() const { return beatsPerSecond_ * samplePeriod_; } double getBeatsPerFrame() const { return beatsPerSecond_ * samplePeriod_; }
/**
* @brief Get beats per second
*
* @return float
*/
double getBeatsPerSecond() const { return beatsPerSecond_; }
/** /**
* @brief Create a normalized phase signal for LFO which completes a * @brief Create a normalized phase signal for LFO which completes a
* period every N-th beat. * period every N-th beat.

View file

@ -62,6 +62,8 @@ FloatSpec loPolyAftertouch { 0, {0, 127}, kNormalizeMidi|kPermissiveBounds };
FloatSpec hiPolyAftertouch { 127, {0, 127}, kNormalizeMidi|kFillGap|kPermissiveBounds }; FloatSpec hiPolyAftertouch { 127, {0, 127}, kNormalizeMidi|kFillGap|kPermissiveBounds };
FloatSpec loBend { -8191, {-8192.0f, 8191.0f}, kNormalizeBend|kPermissiveBounds }; FloatSpec loBend { -8191, {-8192.0f, 8191.0f}, kNormalizeBend|kPermissiveBounds };
FloatSpec hiBend { 8191, {-8192.0f, 8191.0f}, kNormalizeBend|kFillGap|kPermissiveBounds }; FloatSpec hiBend { 8191, {-8192.0f, 8191.0f}, kNormalizeBend|kFillGap|kPermissiveBounds };
UInt8Spec loProgram { 0, {0, 127}, 0 };
UInt8Spec hiProgram { 127, {0, 127}, 0 };
FloatSpec loNormalized { 0.0f, {0.0f, 1.0f}, kPermissiveBounds }; FloatSpec loNormalized { 0.0f, {0.0f, 1.0f}, kPermissiveBounds };
FloatSpec hiNormalized { 1.0f, {0.0f, 1.0f}, kPermissiveBounds }; FloatSpec hiNormalized { 1.0f, {0.0f, 1.0f}, kPermissiveBounds };
FloatSpec loBipolar { -1.0f, {-1.0f, 1.0f}, kPermissiveBounds }; FloatSpec loBipolar { -1.0f, {-1.0f, 1.0f}, kPermissiveBounds };

View file

@ -171,6 +171,8 @@ namespace Default
extern const OpcodeSpec<float> xfinLo; extern const OpcodeSpec<float> xfinLo;
extern const OpcodeSpec<float> loBend; extern const OpcodeSpec<float> loBend;
extern const OpcodeSpec<float> hiBend; extern const OpcodeSpec<float> hiBend;
extern const OpcodeSpec<uint8_t> loProgram;
extern const OpcodeSpec<uint8_t> hiProgram;
extern const OpcodeSpec<float> loNormalized; extern const OpcodeSpec<float> loNormalized;
extern const OpcodeSpec<float> hiNormalized; extern const OpcodeSpec<float> hiNormalized;
extern const OpcodeSpec<float> loBipolar; extern const OpcodeSpec<float> loBipolar;

View file

@ -38,12 +38,14 @@ void Layer::initializeActivations()
pitchSwitched_ = true; pitchSwitched_ = true;
bpmSwitched_ = true; bpmSwitched_ = true;
aftertouchSwitched_ = true; aftertouchSwitched_ = true;
programSwitched_ = true;
ccSwitched_.set(); ccSwitched_.set();
} }
bool Layer::isSwitchedOn() const noexcept bool Layer::isSwitchedOn() const noexcept
{ {
return keySwitched_ && previousKeySwitched_ && sequenceSwitched_ && pitchSwitched_ && bpmSwitched_ && aftertouchSwitched_ && ccSwitched_.all(); return keySwitched_ && previousKeySwitched_ && sequenceSwitched_ && pitchSwitched_
&& programSwitched_ && bpmSwitched_ && aftertouchSwitched_ && ccSwitched_.all();
} }
bool Layer::registerNoteOn(int noteNumber, float velocity, float randValue) noexcept bool Layer::registerNoteOn(int noteNumber, float velocity, float randValue) noexcept
@ -152,10 +154,7 @@ void Layer::updateCCState(int ccNumber, float ccValue) noexcept
if (!conditions) if (!conditions)
return; return;
if (conditions->containsWithEnd(ccValue)) ccSwitched_.set(ccNumber, conditions->containsWithEnd(ccValue));
ccSwitched_.set(ccNumber, true);
else
ccSwitched_.set(ccNumber, false);
} }
bool Layer::registerCC(int ccNumber, float ccValue, float randValue) noexcept bool Layer::registerCC(int ccNumber, float ccValue, float randValue) noexcept
@ -189,30 +188,23 @@ bool Layer::registerCC(int ccNumber, float ccValue, float randValue) noexcept
void Layer::registerPitchWheel(float pitch) noexcept void Layer::registerPitchWheel(float pitch) noexcept
{ {
const Region& region = region_; pitchSwitched_ = region_.bendRange.containsWithEnd(pitch);
if (region.bendRange.containsWithEnd(pitch)) }
pitchSwitched_ = true;
else void Layer::registerProgramChange(int program) noexcept
pitchSwitched_ = false; {
programSwitched_ = region_.programRange.containsWithEnd(program);
} }
void Layer::registerAftertouch(float aftertouch) noexcept void Layer::registerAftertouch(float aftertouch) noexcept
{ {
const Region& region = region_; aftertouchSwitched_ = region_.aftertouchRange.containsWithEnd(aftertouch);
if (region.aftertouchRange.containsWithEnd(aftertouch))
aftertouchSwitched_ = true;
else
aftertouchSwitched_ = false;
} }
void Layer::registerTempo(float secondsPerQuarter) noexcept void Layer::registerTempo(float secondsPerQuarter) noexcept
{ {
const Region& region = region_;
const float bpm = 60.0f / secondsPerQuarter; const float bpm = 60.0f / secondsPerQuarter;
if (region.bpmRange.containsWithEnd(bpm)) bpmSwitched_ = region_.bpmRange.containsWithEnd(bpm);
bpmSwitched_ = true;
else
bpmSwitched_ = false;
} }
void Layer::delaySustainRelease(int noteNumber, float velocity) noexcept void Layer::delaySustainRelease(int noteNumber, float velocity) noexcept

View file

@ -118,6 +118,12 @@ public:
* @param secondsPerQuarter * @param secondsPerQuarter
*/ */
void registerTempo(float secondsPerQuarter) noexcept; void registerTempo(float secondsPerQuarter) noexcept;
/**
* @brief Register program change
*
* @param secondsPerQuarter
*/
void registerProgramChange(int program) noexcept;
// Started notes // Started notes
bool sustainPressed_ { false }; bool sustainPressed_ { false };
@ -136,6 +142,7 @@ public:
bool previousKeySwitched_ {}; bool previousKeySwitched_ {};
bool sequenceSwitched_ {}; bool sequenceSwitched_ {};
bool pitchSwitched_ {}; bool pitchSwitched_ {};
bool programSwitched_ {};
bool bpmSwitched_ {}; bool bpmSwitched_ {};
bool aftertouchSwitched_ {}; bool aftertouchSwitched_ {};
std::bitset<config::numCCs> ccSwitched_; std::bitset<config::numCCs> ccSwitched_;

View file

@ -292,3 +292,14 @@ const sfz::EventVector& sfz::MidiState::getPolyAftertouchEvents(int noteNumber)
return polyAftertouchEvents[noteNumber]; return polyAftertouchEvents[noteNumber];
} }
int sfz::MidiState::getProgram() const noexcept
{
return currentProgram;
}
void sfz::MidiState::programChangeEvent(int delay, int program) noexcept
{
ASSERT(program >= 0 && program <= 127);
currentProgram = program;
}

View file

@ -134,6 +134,20 @@ public:
*/ */
float getPolyAftertouch(int noteNumber) const noexcept; float getPolyAftertouch(int noteNumber) const noexcept;
/**
* @brief Get the current midi program
*
* @return int
*/
int getProgram() const noexcept;
/**
* @brief Register a program change event
*
* @param delay
* @param program
*/
void programChangeEvent(int delay, int program) noexcept;
/** /**
* @brief Register a CC event * @brief Register a CC event
* *
@ -275,6 +289,11 @@ private:
*/ */
std::array<EventVector, 128> polyAftertouchEvents; std::array<EventVector, 128> polyAftertouchEvents;
/**
* @brief Current midi program
*/
int currentProgram { 0 };
float sampleRate { config::defaultSampleRate }; float sampleRate { config::defaultSampleRate };
int samplesPerBlock { config::defaultSamplesPerBlock }; int samplesPerBlock { config::defaultSamplesPerBlock };
float alternate { 0.0f }; float alternate { 0.0f };

View file

@ -243,6 +243,12 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode, bool cleanOpcode)
case hash("hibend"): case hash("hibend"):
bendRange.setEnd(opcode.read(Default::hiBend)); bendRange.setEnd(opcode.read(Default::hiBend));
break; break;
case hash("loprog"):
programRange.setStart(opcode.read(Default::loProgram));
break;
case hash("hiprog"):
programRange.setEnd(opcode.read(Default::hiProgram));
break;
case hash("locc&"): case hash("locc&"):
if (opcode.parameters.back() >= config::numCCs) if (opcode.parameters.back() >= config::numCCs)
return false; return false;

View file

@ -278,6 +278,7 @@ struct Region {
// Region logic: MIDI conditions // Region logic: MIDI conditions
UncheckedRange<float> bendRange { Default::loBend, Default::hiBend }; // hibend and lobend UncheckedRange<float> bendRange { Default::loBend, Default::hiBend }; // hibend and lobend
UncheckedRange<uint8_t> programRange { Default::loProgram, Default::hiProgram }; // loprog and hiprog
CCMap<UncheckedRange<float>> ccConditions {{ Default::loCC, Default::hiCC }}; CCMap<UncheckedRange<float>> ccConditions {{ Default::loCC, Default::hiCC }};
absl::optional<uint8_t> lastKeyswitch {}; // sw_last absl::optional<uint8_t> lastKeyswitch {}; // sw_last
absl::optional<UncheckedRange<uint8_t>> lastKeyswitchRange {}; // sw_last absl::optional<UncheckedRange<uint8_t>> lastKeyswitchRange {}; // sw_last

View file

@ -871,9 +871,10 @@ void Synth::Impl::finalizeSfzLoad()
region.velCurve = Curve::buildFromVelcurvePoints( region.velCurve = Curve::buildFromVelcurvePoints(
region.velocityPoints, Curve::Interpolator::Linear); region.velocityPoints, Curve::Interpolator::Linear);
layer.registerPitchWheel(0); layer.registerPitchWheel(midiState.getPitchBend());
layer.registerAftertouch(0); layer.registerAftertouch(midiState.getChannelAftertouch());
layer.registerTempo(2.0f); layer.registerTempo(resources_.getBeatClock().getBeatsPerSecond());
layer.registerProgramChange(midiState.getProgram());
maxFilters = max(maxFilters, region.filters.size()); maxFilters = max(maxFilters, region.filters.size());
maxEQs = max(maxEQs, region.equalizers.size()); maxEQs = max(maxEQs, region.equalizers.size());
maxLFOs = max(maxLFOs, region.lfos.size()); maxLFOs = max(maxLFOs, region.lfos.size());
@ -1530,6 +1531,15 @@ void Synth::hdPitchWheel(int delay, float normalizedPitch) noexcept
impl.performHdcc(delay, ExtendedCCs::pitchBend, normalizedPitch, false); impl.performHdcc(delay, ExtendedCCs::pitchBend, normalizedPitch, false);
} }
void Synth::programChange(int delay, int program) noexcept
{
Impl& impl = *impl_;
impl.resources_.getMidiState().programChangeEvent(delay, program);
for (const Impl::LayerPtr& layer : impl.layers_)
layer->registerProgramChange(program);
}
void Synth::channelAftertouch(int delay, int aftertouch) noexcept void Synth::channelAftertouch(int delay, int aftertouch) noexcept
{ {
const float normalizedAftertouch = normalize7Bits(aftertouch); const float normalizedAftertouch = normalize7Bits(aftertouch);

View file

@ -408,6 +408,14 @@ public:
* @param normValue the normalized cc value, in domain 0 to 1 * @param normValue the normalized cc value, in domain 0 to 1
*/ */
void hdcc(int delay, int ccNumber, float normValue) noexcept; void hdcc(int delay, int ccNumber, float normValue) noexcept;
/**
* @brief Send a program change event to the synth
*
* @param delay the delay at which the event occurs; this should be lower than the size of
* the block in the next call to renderBlock().
* @param ccNumber the program number
*/
void programChange(int delay, int program) noexcept;
/** /**
* @brief Send a high precision CC automation to the synth * @brief Send a high precision CC automation to the synth
* *

View file

@ -435,6 +435,14 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co
client.receive(delay, path, "ff", args); client.receive(delay, path, "ff", args);
} break; } break;
MATCH("/region&/program_range", "") {
GET_REGION_OR_BREAK(indices[0])
sfizz_arg_t args[2];
args[0].i = region.programRange.getStart();
args[1].i = region.programRange.getEnd();
client.receive(delay, path, "ii", args);
} break;
MATCH("/region&/cc_range&", "") { MATCH("/region&/cc_range&", "") {
GET_REGION_OR_BREAK(indices[0]) GET_REGION_OR_BREAK(indices[0])
sfizz_arg_t args[2]; sfizz_arg_t args[2];

View file

@ -200,6 +200,11 @@ void sfz::Sfizz::automateHdcc(int delay, int ccNumber, float normValue) noexcept
synth->synth.automateHdcc(delay, ccNumber, normValue); synth->synth.automateHdcc(delay, ccNumber, normValue);
} }
void sfz::Sfizz::programChange(int delay, int program) noexcept
{
synth->synth.programChange(delay, program);
}
void sfz::Sfizz::pitchWheel(int delay, int pitch) noexcept void sfz::Sfizz::pitchWheel(int delay, int pitch) noexcept
{ {
synth->synth.pitchWheel(delay, pitch); synth->synth.pitchWheel(delay, pitch);

View file

@ -150,6 +150,10 @@ void sfizz_send_hd_pitch_wheel(sfizz_synth_t* synth, int delay, float pitch)
{ {
synth->synth.hdPitchWheel(delay, pitch); synth->synth.hdPitchWheel(delay, pitch);
} }
void sfizz_send_program_change(sfizz_synth_t* synth, int delay, int program)
{
synth->synth.programChange(delay, program);
}
void sfizz_send_aftertouch(sfizz_synth_t* synth, int delay, int aftertouch) void sfizz_send_aftertouch(sfizz_synth_t* synth, int delay, int aftertouch)
{ {
synth->synth.channelAftertouch(delay, aftertouch); synth->synth.channelAftertouch(delay, aftertouch);

View file

@ -706,3 +706,43 @@ TEST_CASE("[Keyswitches] sw_default with octave_offset")
REQUIRE(messageList == expected); REQUIRE(messageList == expected);
} }
} }
TEST_CASE("[Region activation] Program change")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
SECTION("Default value") {
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_previous.sfz", R"(
<region> sample=*saw
)");
REQUIRE(synth.getLayerView(0)->isSwitchedOn());
synth.noteOn(0, 51, 64);
synth.renderBlock(buffer);
REQUIRE(numPlayingVoices(synth) == 1);
synth.programChange(0, 45);
REQUIRE(synth.getLayerView(0)->isSwitchedOn());
synth.noteOn(0, 53, 64);
synth.renderBlock(buffer);
REQUIRE(numPlayingVoices(synth) == 2);
}
SECTION("Change range") {
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_previous.sfz", R"(
<region> sample=*saw hiprog=2
<region> sample=*sine loprog=1 hiprog=126
<region> sample=*tri loprog=-1 hiprog=200
)");
synth.noteOn(0, 51, 64);
synth.renderBlock(buffer);
REQUIRE(playingSamples(synth) == std::vector<std::string> { "*saw", "*tri" });
synth.programChange(0, 5);
synth.noteOn(0, 53, 64);
synth.renderBlock(buffer);
REQUIRE(playingSamples(synth) == std::vector<std::string> { "*saw", "*tri", "*sine", "*tri" });
synth.programChange(0, 127);
synth.noteOn(0, 54, 64);
synth.renderBlock(buffer);
REQUIRE(playingSamples(synth) == std::vector<std::string> { "*saw", "*tri", "*sine", "*tri", "*tri" });
}
}

View file

@ -706,6 +706,31 @@ TEST_CASE("[Values] Bend range")
REQUIRE(messageList == expected); REQUIRE(messageList == expected);
} }
TEST_CASE("[Values] Program range")
{
Synth synth;
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 loprog=1 hiprog=45
<region> sample=kick.wav loprog=-1 hiprog=555
<region> sample=kick.wav hiprog=-1
)");
synth.dispatchMessage(client, 0, "/region0/program_range", "", nullptr);
synth.dispatchMessage(client, 0, "/region1/program_range", "", nullptr);
synth.dispatchMessage(client, 0, "/region2/program_range", "", nullptr);
synth.dispatchMessage(client, 0, "/region3/program_range", "", nullptr);
std::vector<std::string> expected {
"/region0/program_range,ii : { 0, 127 }",
"/region1/program_range,ii : { 1, 45 }",
"/region2/program_range,ii : { 0, 127 }",
"/region3/program_range,ii : { 0, 127 }",
};
REQUIRE(messageList == expected);
}
TEST_CASE("[Values] CC condition range") TEST_CASE("[Values] CC condition range")
{ {
Synth synth; Synth synth;