Preliminary work for aftertouch handling

- Add the aftertouch modulation source
- Dispatch aftertouch events and track the aftertouch status in the midi state
- Parse cutoff_chanaft
- Convert aftertouch messages to an extended CC message
This commit is contained in:
Paul Fd 2021-02-05 00:39:07 +01:00 committed by Paul Ferrand
parent 02c5de1237
commit 6deb3d37d7
19 changed files with 249 additions and 47 deletions

View file

@ -78,34 +78,30 @@ int process(jack_nframes_t numFrames, void* arg)
switch (midi::status(event.buffer[0])) {
case midi::noteOff: noteoff:
// DBG("[MIDI] Note " << +event.buffer[1] << " OFF at time " << event.time);
synth->noteOff(event.time, event.buffer[1], event.buffer[2]);
break;
case midi::noteOn:
if (event.buffer[2] == 0)
goto noteoff;
// DBG("[MIDI] Note " << +event.buffer[1] << " ON at time " << event.time);
synth->noteOn(event.time, event.buffer[1], event.buffer[2]);
break;
case midi::polyphonicPressure:
// DBG("[MIDI] Polyphonic pressure on at time " << event.time);
// Not implemented
break;
case midi::controlChange:
// DBG("[MIDI] CC " << +event.buffer[1] << " at time " << event.time);
synth->cc(event.time, event.buffer[1], event.buffer[2]);
break;
case midi::programChange:
// DBG("[MIDI] Program change at time " << event.time);
// Not implemented
break;
case midi::channelPressure:
// DBG("[MIDI] Channel pressure at time " << event.time);
synth->aftertouch(event.time, event.buffer[1]);
break;
case midi::pitchBend:
synth->pitchWheel(event.time, midi::buildAndCenterPitch(event.buffer[1], event.buffer[2]));
// DBG("[MIDI] Pitch bend at time " << event.time);
break;
case midi::systemMessage:
// DBG("[MIDI] System message at time " << event.time);
// Not implemented
break;
}
}

View file

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

View file

@ -131,7 +131,7 @@ protected:
// The special member functions are not thread-safe.
AtomicQueueCommon() noexcept = default;
AtomicQueueCommon() = default;
AtomicQueueCommon(AtomicQueueCommon const& b) noexcept
: head_(b.head_.load(X))
@ -403,7 +403,7 @@ class AtomicQueue2 : public AtomicQueueCommon<AtomicQueue2<T, SIZE, MINIMIZE_CON
public:
using value_type = T;
AtomicQueue2() noexcept = default;
AtomicQueue2() = default;
AtomicQueue2(AtomicQueue2 const&) = delete;
AtomicQueue2& operator=(AtomicQueue2 const&) = delete;
};

View file

@ -761,6 +761,11 @@ sfizz_lv2_process_midi_event(sfizz_plugin_t *self, const LV2_Atom_Event *ev)
(int)msg[1],
msg[2]);
break;
case LV2_MIDI_MSG_CHANNEL_PRESSURE:
sfizz_send_aftertouch(self->synth,
(int)ev->time.frames,
msg[1]);
break;
case LV2_MIDI_MSG_BENDER:
sfizz_send_pitch_wheel(self->synth,
(int)ev->time.frames,

View file

@ -33,6 +33,7 @@ set(SFIZZ_HEADERS
sfizz/modulations/ModMatrix.h
sfizz/modulations/ModGenerator.h
sfizz/modulations/sources/ADSREnvelope.h
sfizz/modulations/sources/ChannelAftertouch.h
sfizz/modulations/sources/Controller.h
sfizz/modulations/sources/FlexEnvelope.h
sfizz/modulations/sources/LFO.h
@ -157,9 +158,10 @@ set(SFIZZ_SOURCES
sfizz/modulations/ModKey.cpp
sfizz/modulations/ModKeyHash.cpp
sfizz/modulations/ModMatrix.cpp
sfizz/modulations/sources/ADSREnvelope.cpp
sfizz/modulations/sources/ChannelAftertouch.cpp
sfizz/modulations/sources/Controller.cpp
sfizz/modulations/sources/FlexEnvelope.cpp
sfizz/modulations/sources/ADSREnvelope.cpp
sfizz/modulations/sources/LFO.cpp
sfizz/effects/Nothing.cpp
sfizz/effects/Filter.cpp

View file

@ -24,6 +24,19 @@ enum class Oversampling: int {
x8 = 8
};
enum ExtendedCCs {
pitchBend = 128,
channelAftertouch,
polyphonicAftertouch,
noteOnVelocity,
noteOffVelocity,
keyboardNoteNumber,
keyboardNoteGate,
unipolarRandom,
bipolarRandom,
alternate
};
namespace config {
constexpr float defaultSampleRate { 48000 };
constexpr float maxSampleRate { 192000 };

View file

@ -56,28 +56,33 @@ void sfz::MidiState::setSampleRate(float sampleRate) noexcept
void sfz::MidiState::advanceTime(int numSamples) noexcept
{
auto clearEvents = [] (EventVector& events) {
ASSERT(!events.empty()); // CC event vectors should never be empty
events.front().value = events.back().value;
events.front().delay = 0;
events.resize(1);
};
internalClock += numSamples;
for (auto& ccEvents : cc) {
ASSERT(!ccEvents.empty()); // CC event vectors should never be empty
ccEvents.front().value = ccEvents.back().value;
ccEvents.front().delay = 0;
ccEvents.resize(1);
}
ASSERT(!pitchEvents.empty());
pitchEvents.front().value = pitchEvents.back().value;
pitchEvents.front().delay = 0;
pitchEvents.resize(1);
for (auto& ccEvents : cc)
clearEvents(ccEvents);
clearEvents(pitchEvents);
clearEvents(channelAftertouchEvents);
}
void sfz::MidiState::setSamplesPerBlock(int samplesPerBlock) noexcept
{
auto updateEventBufferSize = [=] (EventVector& events) {
events.shrink_to_fit();
events.reserve(samplesPerBlock);
};
this->samplesPerBlock = samplesPerBlock;
for (auto& ccEvents : cc) {
ccEvents.shrink_to_fit();
ccEvents.reserve(samplesPerBlock);
}
pitchEvents.shrink_to_fit();
pitchEvents.reserve(samplesPerBlock);
for (auto& ccEvents : cc)
updateEventBufferSize(ccEvents);
updateEventBufferSize(pitchEvents);
updateEventBufferSize(channelAftertouchEvents);
}
float sfz::MidiState::getNoteDuration(int noteNumber, int delay) const
@ -100,15 +105,19 @@ float sfz::MidiState::getNoteVelocity(int noteNumber) const noexcept
return lastNoteVelocities[noteNumber];
}
void sfz::MidiState::insertEventInVector(EventVector& events, int delay, float value)
{
const auto insertionPoint = absl::c_upper_bound(events, delay, MidiEventDelayComparator {});
if (insertionPoint == events.end() || insertionPoint->delay != delay)
events.insert(insertionPoint, { delay, value });
else
insertionPoint->value = value;
}
void sfz::MidiState::pitchBendEvent(int delay, float pitchBendValue) noexcept
{
ASSERT(pitchBendValue >= -1.0f && pitchBendValue <= 1.0f);
const auto insertionPoint = absl::c_upper_bound(pitchEvents, delay, MidiEventDelayComparator {});
if (insertionPoint == pitchEvents.end() || insertionPoint->delay != delay)
pitchEvents.insert(insertionPoint, { delay, pitchBendValue });
else
insertionPoint->value = pitchBendValue;
insertEventInVector(pitchEvents, delay, pitchBendValue);
}
float sfz::MidiState::getPitchBend() const noexcept
@ -117,20 +126,27 @@ float sfz::MidiState::getPitchBend() const noexcept
return pitchEvents.back().value;
}
void sfz::MidiState::channelAftertouchEvent(int delay, float aftertouch) noexcept
{
ASSERT(aftertouch >= -1.0f && aftertouch <= 1.0f);
insertEventInVector(channelAftertouchEvents, delay, aftertouch);
}
float sfz::MidiState::getChannelAftertouch() const noexcept
{
ASSERT(channelAftertouchEvents.size() > 0);
return channelAftertouchEvents.back().value;
}
void sfz::MidiState::ccEvent(int delay, int ccNumber, float ccValue) noexcept
{
ASSERT(ccValue >= 0.0 && ccValue <= 1.0);
const auto insertionPoint = absl::c_upper_bound(cc[ccNumber], delay, MidiEventDelayComparator {});
if (insertionPoint == cc[ccNumber].end() || insertionPoint->delay != delay)
cc[ccNumber].insert(insertionPoint, { delay, ccValue });
else
insertionPoint->value = ccValue;
insertEventInVector(cc[ccNumber], delay, ccValue);
}
float sfz::MidiState::getCCValue(int ccNumber) const noexcept
{
ASSERT(ccNumber >= 0 && ccNumber < config::numCCs);
return cc[ccNumber].back().value;
}
@ -139,13 +155,16 @@ void sfz::MidiState::reset() noexcept
for (auto& velocity: lastNoteVelocities)
velocity = 0;
for (auto& ccEvents : cc) {
ccEvents.clear();
ccEvents.push_back({ 0, 0.0f });
}
auto clearEvents = [] (EventVector& events) {
events.clear();
events.push_back({ 0, 0.0f });
};
pitchEvents.clear();
pitchEvents.push_back({ 0, 0.0f });
for (auto& ccEvents : cc)
clearEvents(ccEvents);
clearEvents(pitchEvents);
clearEvents(channelAftertouchEvents);
activeNotes = 0;
internalClock = 0;
@ -173,3 +192,8 @@ const sfz::EventVector& sfz::MidiState::getPitchEvents() const noexcept
{
return pitchEvents;
}
const sfz::EventVector& sfz::MidiState::getChannelAftertouchEvents() const noexcept
{
return channelAftertouchEvents;
}

View file

@ -98,6 +98,20 @@ public:
*/
float getPitchBend() const noexcept;
/**
* @brief Register a channel aftertouch event
*
* @param aftertouch
*/
void channelAftertouchEvent(int delay, float aftertouch) noexcept;
/**
* @brief Get the channel aftertouch status
* @return int
*/
float getChannelAftertouch() const noexcept;
/**
* @brief Register a CC event
*
@ -135,8 +149,19 @@ public:
const EventVector& getCCEvents(int ccIdx) const noexcept;
const EventVector& getPitchEvents() const noexcept;
const EventVector& getChannelAftertouchEvents() const noexcept;
private:
/**
* @brief Insert events in a sorted event vector.
*
* @param events
* @param delay
* @param value
*/
void insertEventInVector(EventVector& events, int delay, float value);
int activeNotes { 0 };
/**
@ -175,6 +200,12 @@ private:
* @brief Pitch bend status
*/
EventVector pitchEvents;
/**
* @brief Aftertouch status
*/
EventVector channelAftertouchEvents;
float sampleRate { config::defaultSampleRate };
int samplesPerBlock { config::defaultSamplesPerBlock };
unsigned internalClock { 0 };

View file

@ -652,6 +652,19 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
processGenericCc(opcode, Default::filterResonanceModRange, ModKey::createNXYZ(ModId::FilResonance, id, filterIndex));
}
break;
case hash("cutoff&_chanaft"):
{
const auto filterIndex = opcode.parameters.front() - 1;
if (!extendIfNecessary(filters, filterIndex + 1, Default::numFilters))
return false;
if (auto value = readOpcode(opcode.value, Default::filterCutoffModRange)) {
const ModKey source = ModKey::createNXYZ(ModId::ChannelAftertouch);
const ModKey target = ModKey::createNXYZ(ModId::FilCutoff, id, filterIndex);
getOrCreateConnection(source, target).sourceDepth = *value;
}
}
break;
case hash("fil&_keytrack"): // also fil_keytrack
{
const auto filterIndex = opcode.parameters.front() - 1;

View file

@ -54,6 +54,7 @@ struct MidiEvent {
int delay;
float value;
};
using EventVector = std::vector<MidiEvent>;
struct MidiEventDelayComparator {

View file

@ -63,6 +63,7 @@ Synth::Impl::Impl()
genLFO_.reset(new LFOSource(voiceManager_));
genFlexEnvelope_.reset(new FlexEnvelopeSource(voiceManager_));
genADSREnvelope_.reset(new ADSREnvelopeSource(voiceManager_, resources_.midiState));
genChannelAftertouch_.reset(new ChannelAftertouchSource(voiceManager_, resources_.midiState));
}
Synth::Impl::~Impl()
@ -1214,11 +1215,26 @@ void Synth::pitchWheel(int delay, int pitch) noexcept
voice.registerPitchWheel(delay, normalizedPitch);
}
}
void Synth::aftertouch(int /* delay */, uint8_t /* aftertouch */) noexcept
void Synth::aftertouch(int delay, uint8_t aftertouch) noexcept
{
Impl& impl = *impl_;
ScopedTiming logger { impl.dispatchDuration_, ScopedTiming::Operation::addToDuration };
const auto normalizedAftertouch = normalize7Bits(aftertouch);
impl.resources_.midiState.channelAftertouchEvent(delay, normalizedAftertouch);
impl.resources_.midiState.ccEvent(delay, ExtendedCCs::channelAftertouch, normalizedAftertouch);
for (auto& region : impl.regions_) {
region->registerAftertouch(aftertouch);
}
for (auto& voice : impl.voiceManager_) {
voice.registerAftertouch(delay, aftertouch);
}
}
void Synth::tempo(int delay, float secondsPerBeat) noexcept
{
Impl& impl = *impl_;
@ -1226,6 +1242,7 @@ void Synth::tempo(int delay, float secondsPerBeat) noexcept
impl.resources_.beatClock.setTempo(delay, secondsPerBeat);
}
void Synth::timeSignature(int delay, int beatsPerBar, int beatUnit)
{
Impl& impl = *impl_;
@ -1233,6 +1250,7 @@ void Synth::timeSignature(int delay, int beatsPerBar, int beatUnit)
impl.resources_.beatClock.setTimeSignature(delay, TimeSignature(beatsPerBar, beatUnit));
}
void Synth::timePosition(int delay, int bar, double barBeat)
{
Impl& impl = *impl_;
@ -1240,6 +1258,7 @@ void Synth::timePosition(int delay, int bar, double barBeat)
impl.resources_.beatClock.setTimePosition(delay, BBT(bar, barBeat));
}
void Synth::playbackState(int delay, int playbackState)
{
Impl& impl = *impl_;
@ -1253,16 +1272,19 @@ int Synth::getNumRegions() const noexcept
Impl& impl = *impl_;
return static_cast<int>(impl.regions_.size());
}
int Synth::getNumGroups() const noexcept
{
Impl& impl = *impl_;
return impl.numGroups_;
}
int Synth::getNumMasters() const noexcept
{
Impl& impl = *impl_;
return impl.numMasters_;
}
int Synth::getNumCurves() const noexcept
{
Impl& impl = *impl_;
@ -1563,6 +1585,9 @@ void Synth::Impl::setupModMatrix()
case ModId::FilEG:
gen = genADSREnvelope_.get();
break;
case ModId::ChannelAftertouch:
gen = genChannelAftertouch_.get();
break;
default:
DBG("[sfizz] Have unknown type of source generator");
break;

View file

@ -8,6 +8,7 @@
#include "modulations/sources/ADSREnvelope.h"
#include "modulations/sources/Controller.h"
#include "modulations/sources/FlexEnvelope.h"
#include "modulations/sources/ChannelAftertouch.h"
#include "modulations/sources/LFO.h"
#include "utility/BitArray.h"
@ -265,6 +266,7 @@ struct Synth::Impl final: public Parser::Listener {
std::unique_ptr<LFOSource> genLFO_;
std::unique_ptr<FlexEnvelopeSource> genFlexEnvelope_;
std::unique_ptr<ADSREnvelopeSource> genADSREnvelope_;
std::unique_ptr<ChannelAftertouchSource> genChannelAftertouch_;
// Settings per voice
struct {

View file

@ -36,6 +36,8 @@ int ModIds::flags(ModId id) noexcept
return kModIsPerVoice;
case ModId::FilEG:
return kModIsPerVoice;
case ModId::ChannelAftertouch:
return kModIsPerCycle;
// targets
case ModId::MasterAmplitude:

View file

@ -26,6 +26,7 @@ enum class ModId : int {
AmpEG,
PitchEG,
FilEG,
ChannelAftertouch,
_SourcesEnd,

View file

@ -105,6 +105,8 @@ std::string ModKey::toString() const
return absl::StrCat("PitchEG {", region_.number(), "}");
case ModId::FilEG:
return absl::StrCat("FilterEG {", region_.number(), "}");
case ModId::ChannelAftertouch:
return absl::StrCat("ChannelAftertouch");
case ModId::MasterAmplitude:
return absl::StrCat("MasterAmplitude {", region_.number(), "}");

View file

@ -29,7 +29,7 @@ public:
: id_(id), region_(region), params_(params), flags_(ModIds::flags(id_)) {}
static ModKey createCC(uint16_t cc, uint8_t curve, uint8_t smooth, float step);
static ModKey createNXYZ(ModId id, NumericId<Region> region, uint8_t N = 0, uint8_t X = 0, uint8_t Y = 0, uint8_t Z = 0);
static ModKey createNXYZ(ModId id, NumericId<Region> region = {}, uint8_t N = 0, uint8_t X = 0, uint8_t Y = 0, uint8_t Z = 0);
explicit operator bool() const noexcept { return id_ != ModId(); }

View file

@ -0,0 +1,42 @@
// 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 "ChannelAftertouch.h"
#include "../../ModifierHelpers.h"
#include "../../ADSREnvelope.h"
// TODO(jpc): also matrix the ampeg
namespace sfz {
ChannelAftertouchSource::ChannelAftertouchSource(VoiceManager& manager, MidiState& state)
: voiceManager_(manager), midiState_(state)
{
}
void ChannelAftertouchSource::init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay)
{
UNUSED(sourceKey);
UNUSED(voiceId);
UNUSED(delay);
}
void ChannelAftertouchSource::release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay)
{
UNUSED(sourceKey);
UNUSED(voiceId);
UNUSED(delay);
}
void ChannelAftertouchSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer)
{
UNUSED(sourceKey);
UNUSED(voiceId);
const EventVector& events = midiState_.getChannelAftertouchEvents();
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 ChannelAftertouchSource : public ModGenerator {
public:
explicit ChannelAftertouchSource(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:
VoiceManager& voiceManager_;
MidiState& midiState_;
};
} // namespace sfz

View file

@ -339,3 +339,18 @@ TEST_CASE("[Modulations] Override the default pan controller")
R"("Controller 7 {curve=4, smooth=10, step=0}" -> "Amplitude {0}")",
}));
}
TEST_CASE("[Modulations] Aftertouch connections")
{
sfz::Synth synth;
synth.loadSfzString("/modulation.sfz", R"(
<region> sample=*sine cutoff_chanaft=1000
<region> sample=*sine cutoff2_chanaft=1000
)");
const std::string graph = synth.getResources().modMatrix.toDotGraph();
REQUIRE(graph == createDefaultGraph({
R"("ChannelAftertouch" -> "FilterCutoff {0, N=1}")",
R"("ChannelAftertouch" -> "FilterCutoff {1, N=2}")",
}, 2));
}