Merge pull request #1058 from paulfd/lohiprog

Lohiprog
This commit is contained in:
Paul Ferrand 2022-01-05 11:44:13 +01:00 committed by GitHub
commit cca66847ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 208 additions and 23 deletions

View file

@ -911,6 +911,11 @@ sfizz_lv2_process_midi_event(sfizz_plugin_t *self, const LV2_Atom_Event *ev)
(int)ev->time.frames,
PITCH_BUILD_AND_CENTER(msg[1], msg[2]));
break;
case LV2_MIDI_MSG_PGM_CHANGE:
sfizz_send_program_change(self->synth,
(int)ev->time.frames,
msg[1]);
break;
default:
break;
}

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);
/**
* @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.
* @since 1.0.0

View file

@ -495,6 +495,23 @@ public:
*/
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
*

View file

@ -152,6 +152,12 @@ public:
* @return float
*/
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
* 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 loBend { -8191, {-8192.0f, 8191.0f}, kNormalizeBend|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 hiNormalized { 1.0f, {0.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> loBend;
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> hiNormalized;
extern const OpcodeSpec<float> loBipolar;

View file

@ -38,12 +38,14 @@ void Layer::initializeActivations()
pitchSwitched_ = true;
bpmSwitched_ = true;
aftertouchSwitched_ = true;
programSwitched_ = true;
ccSwitched_.set();
}
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
@ -152,10 +154,7 @@ void Layer::updateCCState(int ccNumber, float ccValue) noexcept
if (!conditions)
return;
if (conditions->containsWithEnd(ccValue))
ccSwitched_.set(ccNumber, true);
else
ccSwitched_.set(ccNumber, false);
ccSwitched_.set(ccNumber, conditions->containsWithEnd(ccValue));
}
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
{
const Region& region = region_;
if (region.bendRange.containsWithEnd(pitch))
pitchSwitched_ = true;
else
pitchSwitched_ = false;
pitchSwitched_ = region_.bendRange.containsWithEnd(pitch);
}
void Layer::registerProgramChange(int program) noexcept
{
programSwitched_ = region_.programRange.containsWithEnd(program);
}
void Layer::registerAftertouch(float aftertouch) noexcept
{
const Region& region = region_;
if (region.aftertouchRange.containsWithEnd(aftertouch))
aftertouchSwitched_ = true;
else
aftertouchSwitched_ = false;
aftertouchSwitched_ = region_.aftertouchRange.containsWithEnd(aftertouch);
}
void Layer::registerTempo(float secondsPerQuarter) noexcept
{
const Region& region = region_;
const float bpm = 60.0f / secondsPerQuarter;
if (region.bpmRange.containsWithEnd(bpm))
bpmSwitched_ = true;
else
bpmSwitched_ = false;
bpmSwitched_ = region_.bpmRange.containsWithEnd(bpm);
}
void Layer::delaySustainRelease(int noteNumber, float velocity) noexcept

View file

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

View file

@ -292,3 +292,14 @@ const sfz::EventVector& sfz::MidiState::getPolyAftertouchEvents(int 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;
/**
* @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
*
@ -275,6 +289,11 @@ private:
*/
std::array<EventVector, 128> polyAftertouchEvents;
/**
* @brief Current midi program
*/
int currentProgram { 0 };
float sampleRate { config::defaultSampleRate };
int samplesPerBlock { config::defaultSamplesPerBlock };
float alternate { 0.0f };

View file

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

View file

@ -278,6 +278,7 @@ struct Region {
// Region logic: MIDI conditions
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 }};
absl::optional<uint8_t> lastKeyswitch {}; // 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.velocityPoints, Curve::Interpolator::Linear);
layer.registerPitchWheel(0);
layer.registerAftertouch(0);
layer.registerTempo(2.0f);
layer.registerPitchWheel(midiState.getPitchBend());
layer.registerAftertouch(midiState.getChannelAftertouch());
layer.registerTempo(static_cast<float>(resources_.getBeatClock().getBeatsPerSecond()));
layer.registerProgramChange(midiState.getProgram());
maxFilters = max(maxFilters, region.filters.size());
maxEQs = max(maxEQs, region.equalizers.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);
}
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
{
const float normalizedAftertouch = normalize7Bits(aftertouch);

View file

@ -408,6 +408,14 @@ public:
* @param normValue the normalized cc value, in domain 0 to 1
*/
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
*

View file

@ -435,6 +435,14 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co
client.receive(delay, path, "ff", args);
} 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&", "") {
GET_REGION_OR_BREAK(indices[0])
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);
}
void sfz::Sfizz::programChange(int delay, int program) noexcept
{
synth->synth.programChange(delay, program);
}
void sfz::Sfizz::pitchWheel(int delay, int pitch) noexcept
{
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);
}
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)
{
synth->synth.channelAftertouch(delay, aftertouch);

View file

@ -706,3 +706,43 @@ TEST_CASE("[Keyswitches] sw_default with octave_offset")
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);
}
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")
{
Synth synth;