Support polyphonic aftertouch basics

This commit is contained in:
Paul Fd 2021-04-01 23:40:47 +02:00
parent 9c3b31030f
commit 993620252b
31 changed files with 430 additions and 24 deletions

View file

@ -86,7 +86,7 @@ int process(jack_nframes_t numFrames, void* arg)
synth->noteOn(event.time, event.buffer[1], event.buffer[2]);
break;
case midi::polyphonicPressure:
// Not implemented
synth->polyAftertouch(event.time, event.buffer[1], event.buffer[2]);
break;
case midi::controlChange:
synth->cc(event.time, event.buffer[1], event.buffer[2]);

View file

@ -59,6 +59,7 @@ SFIZZ_SOURCES = \
src/sfizz/modulations/ModMatrix.cpp \
src/sfizz/modulations/sources/ADSREnvelope.cpp \
src/sfizz/modulations/sources/ChannelAftertouch.cpp \
src/sfizz/modulations/sources/PolyAftertouch.cpp \
src/sfizz/modulations/sources/Controller.cpp \
src/sfizz/modulations/sources/FlexEnvelope.cpp \
src/sfizz/modulations/sources/LFO.cpp \

View file

@ -640,6 +640,12 @@ sfizz_lv2_process_midi_event(sfizz_plugin_t *self, const LV2_Atom_Event *ev)
(int)ev->time.frames,
msg[1]);
break;
case LV2_MIDI_MSG_NOTE_PRESSURE:
sfizz_send_poly_aftertouch(self->synth,
(int)ev->time.frames,
(int)msg[1],
msg[2]);
break;
case LV2_MIDI_MSG_BENDER:
sfizz_send_pitch_wheel(self->synth,
(int)ev->time.frames,

View file

@ -465,9 +465,13 @@ void SfizzVstProcessor::processEvents(Vst::IEventList& events)
_noteEventsCurrentCycle[pitch] = 0.0f;
break;
}
// case Vst::Event::kPolyPressureEvent:
// synth.aftertouch(e.sampleOffset, convertVelocityFromFloat(e.polyPressure.pressure));
// break;
case Vst::Event::kPolyPressureEvent: {
int pitch = e.polyPressure.pitch;
if (pitch < 0 || pitch >= 128)
break;
synth.polyAftertouch(e.sampleOffset, pitch, convertVelocityFromFloat(e.polyPressure.pressure));
break;
}
}
}
}

View file

@ -39,6 +39,7 @@ set(SFIZZ_HEADERS
sfizz/modulations/ModGenerator.h
sfizz/modulations/sources/ADSREnvelope.h
sfizz/modulations/sources/ChannelAftertouch.h
sfizz/modulations/sources/PolyAftertouch.h
sfizz/modulations/sources/Controller.h
sfizz/modulations/sources/FlexEnvelope.h
sfizz/modulations/sources/LFO.h
@ -166,6 +167,7 @@ set(SFIZZ_SOURCES
sfizz/modulations/ModMatrix.cpp
sfizz/modulations/sources/ADSREnvelope.cpp
sfizz/modulations/sources/ChannelAftertouch.cpp
sfizz/modulations/sources/PolyAftertouch.cpp
sfizz/modulations/sources/Controller.cpp
sfizz/modulations/sources/FlexEnvelope.cpp
sfizz/modulations/sources/LFO.cpp

View file

@ -430,7 +430,7 @@ SFIZZ_EXPORTED_API void sfizz_automate_hdcc(sfizz_synth_t* synth, int delay, int
SFIZZ_EXPORTED_API void sfizz_send_pitch_wheel(sfizz_synth_t* synth, int delay, int pitch);
/**
* @brief Send an aftertouch event. (CURRENTLY UNIMPLEMENTED)
* @brief Send an aftertouch event.
* @since 0.2.0
*
* @param synth The synth.
@ -443,6 +443,22 @@ SFIZZ_EXPORTED_API void sfizz_send_pitch_wheel(sfizz_synth_t* synth, int delay,
*/
SFIZZ_EXPORTED_API void sfizz_send_aftertouch(sfizz_synth_t* synth, int delay, char aftertouch);
/**
* @brief Send a polyphonic aftertouch event. This feature is experimental and needs more testing
* in the internal engine.
* @since 0.6.0
*
* @param synth 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 note_number The note number.
* @param aftertouch The aftertouch value.
*
* @par Thread-safety constraints
* - @b RT: the function must be invoked from the Real-time thread
*/
SFIZZ_EXPORTED_API void sfizz_send_poly_aftertouch(sfizz_synth_t* synth, int delay, int note_number, char aftertouch);
/**
* @brief Send a tempo event.
* @since 0.2.0
@ -823,7 +839,7 @@ SFIZZ_EXPORTED_API void sfizz_clear_external_definitions(sfizz_synth_t* synth);
* @brief Index out of bound error for the requested CC/key label.
* @since 0.4.0
*/
#define SFIZZ_OUT_OF_BOUNDS_LABEL_INDEX -1
#define SFIZZ_OUT_OF_BOUNDS_LABEL_INDEX (-1)
/**
* @brief Get the number of key labels registered in the current sfz file.

View file

@ -425,7 +425,7 @@ public:
void pitchWheel(int delay, int pitch) noexcept;
/**
* @brief Send a aftertouch event to the synth. (CURRENTLY UNIMPLEMENTED)
* @brief Send an aftertouch event to the synth.
*
* @since 0.2.0
*
@ -438,6 +438,22 @@ public:
*/
void aftertouch(int delay, uint8_t aftertouch) noexcept;
/**
* @brief Send a polyphonic aftertouch event to the synth. This feature is
* experimental and needs more testing in the internal engine.
*
* @since 0.6.0
*
* @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 noteNumber the note number.
* @param aftertouch the aftertouch value.
*
* @par Thread-safety constraints
* - @b RT: the function must be invoked from the Real-time thread
*/
void polyAftertouch(int delay, int noteNumber, uint8_t aftertouch) noexcept;
/**
* @brief Send a tempo event to the synth.
*

View file

@ -53,6 +53,8 @@ FloatSpec loVel { 0, {0.0f, 127.0f}, kNormalizeMidi|kPermissiveBounds };
FloatSpec hiVel { 127, {0.0f, 127.0f}, kNormalizeMidi|kPermissiveBounds };
FloatSpec loChannelAftertouch { 0, {0, 127}, kNormalizeMidi|kPermissiveBounds };
FloatSpec hiChannelAftertouch { 127, {0, 127}, kNormalizeMidi|kPermissiveBounds };
FloatSpec loPolyAftertouch { 0, {0, 127}, kNormalizeMidi|kPermissiveBounds };
FloatSpec hiPolyAftertouch { 127, {0, 127}, kNormalizeMidi|kPermissiveBounds };
FloatSpec loBend { -8191, {-8192.0f, 8191.0f}, kNormalizeBend|kPermissiveBounds };
FloatSpec hiBend { 8191, {-8192.0f, 8191.0f}, kNormalizeBend|kPermissiveBounds };
FloatSpec loNormalized { 0.0f, {0.0f, 1.0f}, kPermissiveBounds };

View file

@ -163,6 +163,8 @@ namespace Default
extern const OpcodeSpec<float> hiBipolar;
extern const OpcodeSpec<float> loChannelAftertouch;
extern const OpcodeSpec<float> hiChannelAftertouch;
extern const OpcodeSpec<float> loPolyAftertouch;
extern const OpcodeSpec<float> hiPolyAftertouch;
extern const OpcodeSpec<uint16_t> ccNumber;
extern const OpcodeSpec<uint8_t> curveCC;
extern const OpcodeSpec<uint16_t> smoothCC;

View file

@ -59,7 +59,10 @@ bool Layer::registerNoteOn(int noteNumber, float velocity, float randValue) noex
((sequenceCounter_++ % region.sequenceLength) == region.sequencePosition - 1);
}
if (!isSwitchedOn())
const bool polyAftertouchActive =
region.polyAftertouchRange.containsWithEnd(midiState_.getPolyAftertouch(noteNumber));
if (!isSwitchedOn() || !polyAftertouchActive)
return false;
if (!region.triggerOnNote)
@ -83,7 +86,10 @@ bool Layer::registerNoteOff(int noteNumber, float velocity, float randValue) noe
const Region& region = region_;
if (!isSwitchedOn())
const bool polyAftertouchActive =
region.polyAftertouchRange.containsWithEnd(midiState_.getPolyAftertouch(noteNumber));
if (!isSwitchedOn() || !polyAftertouchActive)
return false;
if (!region.triggerOnNote)

View file

@ -73,8 +73,11 @@ void sfz::MidiState::flushEvents() noexcept
events.resize(1);
};
for (auto& ccEvents : cc)
flushEventVector(ccEvents);
for (auto& events : ccEvents)
flushEventVector(events);
for (auto& events: polyAftertouchEvents)
flushEventVector(events);
flushEventVector(pitchEvents);
flushEventVector(channelAftertouchEvents);
@ -88,8 +91,11 @@ void sfz::MidiState::setSamplesPerBlock(int samplesPerBlock) noexcept
events.reserve(samplesPerBlock);
};
this->samplesPerBlock = samplesPerBlock;
for (auto& ccEvents : cc)
updateEventBufferSize(ccEvents);
for (auto& events: ccEvents)
updateEventBufferSize(events);
for (auto& events: polyAftertouchEvents)
updateEventBufferSize(events);
updateEventBufferSize(pitchEvents);
updateEventBufferSize(channelAftertouchEvents);
@ -147,21 +153,39 @@ void sfz::MidiState::channelAftertouchEvent(int delay, float aftertouch) noexcep
insertEventInVector(channelAftertouchEvents, delay, aftertouch);
}
void sfz::MidiState::polyAftertouchEvent(int delay, int noteNumber, float aftertouch) noexcept
{
ASSERT(aftertouch >= 0.0f && aftertouch <= 1.0f);
if (noteNumber < 0 || noteNumber >= static_cast<int>(polyAftertouchEvents.size()))
return;
insertEventInVector(polyAftertouchEvents[noteNumber], delay, aftertouch);
}
float sfz::MidiState::getChannelAftertouch() const noexcept
{
ASSERT(channelAftertouchEvents.size() > 0);
return channelAftertouchEvents.back().value;
}
float sfz::MidiState::getPolyAftertouch(int noteNumber) const noexcept
{
if (noteNumber < 0 || noteNumber > 127)
return 0.0f;
ASSERT(polyAftertouchEvents[noteNumber].size() > 0);
return polyAftertouchEvents[noteNumber].back().value;
}
void sfz::MidiState::ccEvent(int delay, int ccNumber, float ccValue) noexcept
{
insertEventInVector(cc[ccNumber], delay, ccValue);
insertEventInVector(ccEvents[ccNumber], delay, ccValue);
}
float sfz::MidiState::getCCValue(int ccNumber) const noexcept
{
ASSERT(ccNumber >= 0 && ccNumber < config::numCCs);
return cc[ccNumber].back().value;
return ccEvents[ccNumber].back().value;
}
void sfz::MidiState::reset() noexcept
@ -174,8 +198,11 @@ void sfz::MidiState::reset() noexcept
events.push_back({ 0, 0.0f });
};
for (auto& ccEvents : cc)
clearEvents(ccEvents);
for (auto& events : ccEvents)
clearEvents(events);
for (auto& events : polyAftertouchEvents)
clearEvents(events);
clearEvents(pitchEvents);
clearEvents(channelAftertouchEvents);
@ -201,7 +228,7 @@ const sfz::EventVector& sfz::MidiState::getCCEvents(int ccIdx) const noexcept
if (ccIdx < 0 || ccIdx >= config::numCCs)
return nullEvent;
return cc[ccIdx];
return ccEvents[ccIdx];
}
const sfz::EventVector& sfz::MidiState::getPitchEvents() const noexcept
@ -213,3 +240,11 @@ const sfz::EventVector& sfz::MidiState::getChannelAftertouchEvents() const noexc
{
return channelAftertouchEvents;
}
const sfz::EventVector& sfz::MidiState::getPolyAftertouchEvents(int noteNumber) const noexcept
{
if (noteNumber < 0 || noteNumber > 127)
return nullEvent;
return polyAftertouchEvents[noteNumber];
}

View file

@ -113,6 +113,13 @@ public:
*/
void channelAftertouchEvent(int delay, float aftertouch) noexcept;
/**
* @brief Register a channel aftertouch event
*
* @param aftertouch
*/
void polyAftertouchEvent(int delay, int noteNumber, float aftertouch) noexcept;
/**
* @brief Get the channel aftertouch status
@ -120,6 +127,13 @@ public:
*/
float getChannelAftertouch() const noexcept;
/**
* @brief Get the polyphonic aftertouch status
* @return int
*/
float getPolyAftertouch(int noteNumber) const noexcept;
/**
* @brief Register a CC event
*
@ -172,6 +186,7 @@ public:
void resetAllControllers(int delay) noexcept;
const EventVector& getCCEvents(int ccIdx) const noexcept;
const EventVector& getPolyAftertouchEvents(int noteNumber) const noexcept;
const EventVector& getPitchEvents() const noexcept;
const EventVector& getChannelAftertouchEvents() const noexcept;
@ -230,7 +245,7 @@ private:
* @brief Current known values for the CCs.
*
*/
std::array<EventVector, config::numCCs> cc;
std::array<EventVector, config::numCCs> ccEvents;
/**
* @brief Null event
@ -248,6 +263,11 @@ private:
*/
EventVector channelAftertouchEvents;
/**
* @brief Polyphonic aftertouch status.
*/
std::array<EventVector, 128> polyAftertouchEvents;
float sampleRate { config::defaultSampleRate };
int samplesPerBlock { config::defaultSamplesPerBlock };
float alternate { 0.0f };

View file

@ -348,6 +348,12 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
case hash("hichanaft"):
aftertouchRange.setEnd(opcode.read(Default::hiChannelAftertouch));
break;
case hash("lopolyaft"):
polyAftertouchRange.setStart(opcode.read(Default::loPolyAftertouch));
break;
case hash("hipolyaft"):
polyAftertouchRange.setEnd(opcode.read(Default::hiPolyAftertouch));
break;
case hash("lobpm"):
bpmRange.setStart(opcode.read(Default::loBPM));
break;
@ -926,7 +932,9 @@ bool sfz::Region::parseLFOOpcode(const Opcode& opcode, LFODescription& lfo)
= opcode.read(depthModSpec);
break;
case_any_lfo("depthpolyaft"): // NOLINT bugprone-branch-clone
// TODO(jpc) LFO v1
getOrCreateConnection(sourceKey, targetKey).sourceDepthMod = sourceDepthKey;
getOrCreateConnection(ModKey::createNXYZ(ModId::PolyAftertouch, id), sourceDepthKey).sourceDepth
= opcode.read(depthModSpec);
break;
case_any_lfo("fade"):
lfo.fade = opcode.read(Default::lfoFade);
@ -942,7 +950,8 @@ bool sfz::Region::parseLFOOpcode(const Opcode& opcode, LFODescription& lfo)
= opcode.read(Default::lfoFreqMod);
break;
case_any_lfo("freqpolyaft"): // NOLINT bugprone-branch-clone
// TODO(jpc) LFO v1
getOrCreateConnection(ModKey::createNXYZ(ModId::PolyAftertouch, id), lfo.freqKey).sourceDepth
= opcode.read(Default::lfoFreqMod);
break;
// sfizz extension

View file

@ -362,6 +362,7 @@ struct Region {
// Region logic: internal conditions
UncheckedRange<float> aftertouchRange { Default::loChannelAftertouch, Default::hiChannelAftertouch }; // hichanaft and lochanaft
UncheckedRange<float> polyAftertouchRange { Default::loPolyAftertouch, Default::hiPolyAftertouch }; // hipolyaft and lopolyaft
UncheckedRange<float> bpmRange { Default::loBPM, Default::hiBPM }; // hibpm and lobpm
UncheckedRange<float> randRange { Default::loNormalized, Default::hiNormalized }; // hirand and lorand
uint8_t sequenceLength { Default::sequence }; // seq_length

View file

@ -64,6 +64,7 @@ Synth::Impl::Impl()
genFlexEnvelope_.reset(new FlexEnvelopeSource(voiceManager_));
genADSREnvelope_.reset(new ADSREnvelopeSource(voiceManager_, resources_.midiState));
genChannelAftertouch_.reset(new ChannelAftertouchSource(voiceManager_, resources_.midiState));
genPolyAftertouch_.reset(new PolyAftertouchSource(voiceManager_, resources_.midiState));
}
Synth::Impl::~Impl()
@ -1337,6 +1338,26 @@ void Synth::hdAftertouch(int delay, float normAftertouch) noexcept
impl.performHdcc(delay, ExtendedCCs::channelAftertouch, normAftertouch, false);
}
void Synth::polyAftertouch(int delay, int noteNumber, uint8_t aftertouch) noexcept
{
const float normalizedAftertouch = normalize7Bits(aftertouch);
hdPolyAftertouch(delay, noteNumber, normalizedAftertouch);
}
void Synth::hdPolyAftertouch(int delay, int noteNumber, float normAftertouch) noexcept
{
Impl& impl = *impl_;
ScopedTiming logger { impl.dispatchDuration_, ScopedTiming::Operation::addToDuration };
impl.resources_.midiState.polyAftertouchEvent(delay, noteNumber, normAftertouch);
for (auto& voice : impl.voiceManager_)
voice.registerPolyAftertouch(delay, noteNumber, normAftertouch);
// Note information is lost on this CC
impl.performHdcc(delay, ExtendedCCs::polyphonicAftertouch, normAftertouch, false);
}
void Synth::tempo(int delay, float secondsPerBeat) noexcept
{
Impl& impl = *impl_;
@ -1721,6 +1742,9 @@ void Synth::Impl::setupModMatrix()
case ModId::ChannelAftertouch:
gen = genChannelAftertouch_.get();
break;
case ModId::PolyAftertouch:
gen = genPolyAftertouch_.get();
break;
default:
DBG("[sfizz] Have unknown type of source generator");
break;

View file

@ -421,6 +421,22 @@ public:
* @param secondsPerQuarter the new period of the quarter note
*/
void tempo(int delay, float secondsPerQuarter) noexcept;
/**
* @brief Send a polyphonic aftertouch event to the synth
*
* @param delay
* @param noteNumber
* @param normAftertouch
*/
void polyAftertouch(int delay, int noteNumber, uint8_t aftertouch) noexcept;
/**
* @brief Send a polyphonic aftertouch event to the synth
*
* @param delay
* @param noteNumber
* @param normAftertouch
*/
void hdPolyAftertouch(int delay, int noteNumber, float normAftertouch) noexcept;
/**
* @brief Send the time signature.
*

View file

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

View file

@ -11,6 +11,7 @@
#include "modulations/sources/Controller.h"
#include "modulations/sources/FlexEnvelope.h"
#include "modulations/sources/ChannelAftertouch.h"
#include "modulations/sources/PolyAftertouch.h"
#include "modulations/sources/LFO.h"
namespace sfz {
@ -303,6 +304,7 @@ struct Synth::Impl final: public Parser::Listener {
std::unique_ptr<FlexEnvelopeSource> genFlexEnvelope_;
std::unique_ptr<ADSREnvelopeSource> genADSREnvelope_;
std::unique_ptr<ChannelAftertouchSource> genChannelAftertouch_;
std::unique_ptr<PolyAftertouchSource> genPolyAftertouch_;
// Settings per voice
struct {

View file

@ -16,6 +16,7 @@
#include "LFO.h"
#include "MathHelpers.h"
#include "ModifierHelpers.h"
#include "TriggerEvent.h"
#include "modulations/ModId.h"
#include "modulations/ModKey.h"
#include "modulations/ModMatrix.h"
@ -658,13 +659,28 @@ void Voice::registerPitchWheel(int delay, float pitch) noexcept
UNUSED(pitch);
}
void Voice::registerAftertouch(int delay, uint8_t aftertouch) noexcept
void Voice::registerAftertouch(int delay, float aftertouch) noexcept
{
// TODO
UNUSED(delay);
UNUSED(aftertouch);
}
void Voice::registerPolyAftertouch(int delay, int noteNumber, float aftertouch) noexcept
{
Impl& impl = *impl_;
if (impl.state_ != State::playing)
return;
if (!(impl.triggerEvent_.type == TriggerEventType::NoteOn || impl.triggerEvent_.type == TriggerEventType::NoteOff)
|| impl.triggerEvent_.number != noteNumber)
return;
// TODO
UNUSED(delay);
UNUSED(aftertouch);
}
void Voice::registerTempo(int delay, float secondsPerQuarter) noexcept
{
// TODO

View file

@ -153,7 +153,17 @@ public:
* @param delay
* @param aftertouch
*/
void registerAftertouch(int delay, uint8_t aftertouch) noexcept;
void registerAftertouch(int delay, float aftertouch) noexcept;
/**
* @brief Register a polyphonic aftertouch event; for now this does nothing
*
* @param delay
* @param noteNumber
* @param aftertouch
*/
void registerPolyAftertouch(int delay, int noteNumber, float aftertouch) noexcept;
/**
* @brief Register a tempo event; for now this does nothing
*

View file

@ -44,6 +44,8 @@ int ModIds::flags(ModId id) noexcept
return kModIsPerVoice;
case ModId::ChannelAftertouch:
return kModIsPerCycle;
case ModId::PolyAftertouch:
return kModIsPerVoice;
case ModId::PerVoiceController:
return kModIsPerVoice;

View file

@ -30,6 +30,7 @@ enum class ModId : int {
PitchEG,
FilEG,
ChannelAftertouch,
PolyAftertouch,
PerVoiceController,
_SourcesEnd,

View file

@ -113,6 +113,8 @@ std::string ModKey::toString() const
return absl::StrCat("FilterEG {", region_.number(), "}");
case ModId::ChannelAftertouch:
return absl::StrCat("ChannelAftertouch");
case ModId::PolyAftertouch:
return absl::StrCat("PolyAftertouch");
case ModId::PerVoiceController:
return absl::StrCat("PerVoiceController ", params_.cc,
" {curve=", params_.curve, ", smooth=", params_.smooth,

View file

@ -106,6 +106,16 @@ void ControllerSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceI
};
switch(p.cc) {
case ExtendedCCs::polyphonicAftertouch: {
const auto voice = impl_->voiceManager_->getVoiceById(voiceId);
const float fillValue =
voice && voice->getTriggerEvent().type == TriggerEventType::NoteOn ?
impl_->res_->midiState.getPolyAftertouch(voice->getTriggerEvent().number) : 0.0f;
sfz::fill(buffer, quantize(fillValue));
canShortcut = true;
break;
}
case ExtendedCCs::noteOnVelocity: {
const auto voice = impl_->voiceManager_->getVoiceById(voiceId);
const float fillValue =

View file

@ -0,0 +1,48 @@
// SPDX-License-Identifier: BSD-2-Clause
// This code is part of the sfizz library and is licensed under a BSD 2-clause
// license. You should have receive a LICENSE.md file along with the code.
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#include "PolyAftertouch.h"
#include "../../ModifierHelpers.h"
#include "../../ADSREnvelope.h"
namespace sfz {
PolyAftertouchSource::PolyAftertouchSource(VoiceManager& manager, MidiState& state)
: midiState_(state), manager_(manager)
{
}
void PolyAftertouchSource::init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay)
{
UNUSED(sourceKey);
UNUSED(voiceId);
UNUSED(delay);
}
void PolyAftertouchSource::release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay)
{
UNUSED(sourceKey);
UNUSED(voiceId);
UNUSED(delay);
}
void PolyAftertouchSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer)
{
UNUSED(sourceKey);
Voice* voice = manager_.getVoiceById(voiceId);
if (!voice || voice->getTriggerEvent().type == TriggerEventType::CC) {
fill(buffer, 0.0f);
return;
}
const int noteNumber = voice->getTriggerEvent().number;
const EventVector& events = midiState_.getPolyAftertouchEvents(noteNumber);
linearEnvelope(events, buffer, [](float x) { return x; });
}
} // namespace sfz

View file

@ -0,0 +1,27 @@
// SPDX-License-Identifier: BSD-2-Clause
// This code is part of the sfizz library and is licensed under a BSD 2-clause
// license. You should have receive a LICENSE.md file along with the code.
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#pragma once
#include "../ModGenerator.h"
#include "../../VoiceManager.h"
#include "../../MidiState.h"
namespace sfz {
class Synth;
class PolyAftertouchSource : public ModGenerator {
public:
explicit PolyAftertouchSource(VoiceManager &manager, MidiState& state);
void init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
void release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override;
private:
MidiState& midiState_;
VoiceManager& manager_;
};
} // namespace sfz

View file

@ -185,6 +185,11 @@ void sfz::Sfizz::aftertouch(int delay, uint8_t aftertouch) noexcept
synth->synth.aftertouch(delay, aftertouch);
}
void sfz::Sfizz::polyAftertouch(int delay, int noteNumber, uint8_t aftertouch) noexcept
{
synth->synth.polyAftertouch(delay, noteNumber, aftertouch);
}
void sfz::Sfizz::tempo(int delay, float secondsPerBeat) noexcept
{
synth->synth.tempo(delay, secondsPerBeat);

View file

@ -142,6 +142,10 @@ void sfizz_send_aftertouch(sfizz_synth_t* synth, int delay, char aftertouch)
{
synth->synth.aftertouch(delay, aftertouch);
}
void sfizz_send_poly_aftertouch(sfizz_synth_t* synth, int delay, int note_number, char aftertouch)
{
synth->synth.polyAftertouch(delay, note_number, aftertouch);
}
void sfizz_send_tempo(sfizz_synth_t* synth, int delay, float seconds_per_quarter)
{
synth->synth.tempo(delay, seconds_per_quarter);

View file

@ -430,7 +430,6 @@ TEST_CASE("[Modulations] LFO v1 aftertouch connections")
}, 3));
}
TEST_CASE("[Modulations] LFO v1 aftertouch frequency connections")
{
sfz::Synth synth;
@ -451,6 +450,46 @@ TEST_CASE("[Modulations] LFO v1 aftertouch frequency connections")
}, 3));
}
TEST_CASE("[Modulations] LFO v1 poly aftertouch connections")
{
sfz::Synth synth;
synth.loadSfzString("/modulation.sfz", R"(
<region> sample=*sine amplfo_depthpolyaft=10
<region> sample=*sine pitchlfo_depthpolyaft=1200
<region> sample=*sine fillfo_depthpolyaft=-3600
)");
const std::string graph = synth.getResources().modMatrix.toDotGraph();
REQUIRE(graph == createDefaultGraph({
R"("PolyAftertouch" -> "AmplitudeLFODepth {0}")",
R"("PolyAftertouch" -> "PitchLFODepth {1}")",
R"("PolyAftertouch" -> "FilterLFODepth {2}")",
R"("AmplitudeLFO {0}" -> "Volume {0}")",
R"("PitchLFO {1}" -> "Pitch {1}")",
R"("FilterLFO {2}" -> "FilterCutoff {2, N=1}")",
}, 3));
}
TEST_CASE("[Modulations] LFO v1 poly aftertouch frequency connections")
{
sfz::Synth synth;
synth.loadSfzString("/modulation.sfz", R"(
<region> sample=*sine amplfo_freqpolyaft=10
<region> sample=*sine pitchlfo_freqpolyaft=1200
<region> sample=*sine fillfo_freqpolyaft=-3600
)");
const std::string graph = synth.getResources().modMatrix.toDotGraph();
REQUIRE(graph == createDefaultGraph({
R"("PolyAftertouch" -> "AmplitudeLFOFrequency {0}")",
R"("PolyAftertouch" -> "PitchLFOFrequency {1}")",
R"("PolyAftertouch" -> "FilterLFOFrequency {2}")",
R"("AmplitudeLFO {0}" -> "Volume {0}")",
R"("PitchLFO {1}" -> "Pitch {1}")",
R"("FilterLFO {2}" -> "FilterCutoff {2, N=1}")",
}, 3));
}
TEST_CASE("[Modulations] EG v1 CC connections")
{
sfz::Synth synth;

View file

@ -537,3 +537,47 @@ TEST_CASE("[Keyswitches] Multiple sw_default, in region")
REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
}
TEST_CASE("[Region activation] Polyphonic aftertouch")
{
sfz::Synth synth;
SECTION("Basic sequence, note on")
{
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyaft.sfz", R"(
<region> sample=*saw lokey=48 hikey=60
<region> lopolyaft=50 hipolyaft=100 sample=*sine lokey=36 hikey=47
)");
synth.noteOn(0, 50, 100);
REQUIRE( synth.getNumActiveVoices() == 1);
synth.noteOn(1, 40, 100);
REQUIRE( synth.getNumActiveVoices() == 1); // no notes playing
synth.polyAftertouch(2, 40, 80);
synth.noteOn(3, 40, 100);
REQUIRE( synth.getNumActiveVoices() == 2);
}
SECTION("Basic sequence, note off, no polyaft set")
{
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyaft.sfz", R"(
<region> sample=*saw
<region> lopolyaft=50 hipolyaft=100 sample=*sine trigger=release
)");
synth.noteOn(0, 50, 100);
REQUIRE( synth.getNumActiveVoices() == 1);
synth.noteOff(1, 50, 0);
REQUIRE( synth.getNumActiveVoices() == 1); // no note off playing
}
SECTION("Basic sequence, note off")
{
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyaft.sfz", R"(
<region> sample=*saw
<region> lopolyaft=50 hipolyaft=100 sample=*sine trigger=release
)");
synth.noteOn(0, 50, 100);
REQUIRE( synth.getNumActiveVoices() == 1);
synth.polyAftertouch(2, 50, 80);
synth.noteOff(3, 50, 0);
REQUIRE( synth.getNumActiveVoices() == 2); // no note off playing
}
}

View file

@ -922,6 +922,34 @@ TEST_CASE("[Values] Aftertouch range")
REQUIRE(messageList == expected);
}
TEST_CASE("[Values] Polyaftertouch 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 lopolyaft=34 hipolyaft=60
<region> sample=kick.wav lopolyaft=-3 hipolyaft=60
<region> sample=kick.wav lopolyaft=20 hipolyaft=-1
<region> sample=kick.wav lopolyaft=20 hipolyaft=10
)");
synth.dispatchMessage(client, 0, "/region0/polyaft_range", "", nullptr);
synth.dispatchMessage(client, 0, "/region1/polyaft_range", "", nullptr);
synth.dispatchMessage(client, 0, "/region2/polyaft_range", "", nullptr);
synth.dispatchMessage(client, 0, "/region3/polyaft_range", "", nullptr);
synth.dispatchMessage(client, 0, "/region4/polyaft_range", "", nullptr);
std::vector<std::string> expected {
"/region0/polyaft_range,ff : { 0, 1 }",
"/region1/polyaft_range,ff : { 0.267717, 0.472441 }",
"/region2/polyaft_range,ff : { -0.023622, 0.472441 }",
"/region3/polyaft_range,ff : { 0.15748, -0.00787402 }",
"/region4/polyaft_range,ff : { 0.15748, 0.0787402 }",
};
REQUIRE(messageList == expected);
}
TEST_CASE("[Values] BPM range")
{
Synth synth;