Skip to content

Commit 0d3ffe5

Browse files
committed
Added 'Chase MIDI notes' global setting (true by default)
1 parent 4661e65 commit 0d3ffe5

4 files changed

Lines changed: 99 additions & 53 deletions

File tree

plugin/include/XenRoll/data/GlobalSettings.h

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ class GlobalSettings {
2222
propsFile->saveIfNeeded();
2323
} // =========================================
2424

25+
// ============ chaseMIDINotes ============
26+
static constexpr bool chaseMIDINotes_default = true;
27+
bool getChaseMIDINotes() const { return chaseMIDINotes_.load(std::memory_order_relaxed); }
28+
void setChaseMIDINotes(bool chaseMIDINotes) {
29+
chaseMIDINotes_.store(chaseMIDINotes, std::memory_order_relaxed);
30+
propsFile->setValue("chaseMIDINotes", chaseMIDINotes);
31+
propsFile->saveIfNeeded();
32+
} // =========================================
33+
2534
// ============ horZoomOnCursor ============
2635
static constexpr bool horZoomOnCursor_default = true;
2736
bool getHorZoomOnCursor() const { return horZoomOnCursor_.load(std::memory_order_relaxed); }
@@ -88,9 +97,11 @@ class GlobalSettings {
8897
propsFile->getBoolValue("playDraggedNotes", playDraggedNotes_default),
8998
std::memory_order_relaxed);
9099

91-
horZoomOnCursor_.store(
92-
propsFile->getBoolValue("horZoomOnCursor", horZoomOnCursor_default),
93-
std::memory_order_relaxed);
100+
chaseMIDINotes_.store(propsFile->getBoolValue("chaseMIDINotes", chaseMIDINotes_default),
101+
std::memory_order_relaxed);
102+
103+
horZoomOnCursor_.store(propsFile->getBoolValue("horZoomOnCursor", horZoomOnCursor_default),
104+
std::memory_order_relaxed);
94105

95106
double db = propsFile->getDoubleValue("micGain_dB", micGain_dB_default);
96107
micGain_dB_.store(db, std::memory_order_relaxed);
@@ -105,6 +116,7 @@ class GlobalSettings {
105116

106117
// ============ Cached values (atomic, lock-free) ============
107118
std::atomic<bool> playDraggedNotes_;
119+
std::atomic<bool> chaseMIDINotes_;
108120
std::atomic<bool> horZoomOnCursor_;
109121
std::atomic<double> micGain_dB_;
110122
std::atomic<float> micGainLinear_;

plugin/include/XenRoll/editor/panels/SettingsPanel.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class SettingsViewport : public juce::Viewport {
1515
}
1616

1717
void visibilityChanged() override {
18-
auto* settingsPanel = getViewedComponent();
18+
auto *settingsPanel = getViewedComponent();
1919
if (settingsPanel) {
2020
settingsPanel->visibilityChanged();
2121
}
@@ -33,6 +33,8 @@ class SettingsPanel : public juce::Component {
3333
if (isVisible()) {
3434
playDraggedNotesCheckbox->setToggleState(
3535
GlobalSettings::getInstance().getPlayDraggedNotes(), juce::dontSendNotification);
36+
chaseMIDINotesCheckbox->setToggleState(
37+
GlobalSettings::getInstance().getChaseMIDINotes(), juce::dontSendNotification);
3638
horZoomOnCursorCheckbox->setToggleState(
3739
GlobalSettings::getInstance().getHorZoomOnCursor(), juce::dontSendNotification);
3840
noteRectRoundingSlider->setValue(GlobalSettings::getInstance().getNoteRectRounding(),
@@ -60,6 +62,9 @@ class SettingsPanel : public juce::Component {
6062
std::unique_ptr<juce::Label> playDraggedNotesLabel;
6163
std::unique_ptr<juce::ToggleButton> playDraggedNotesCheckbox;
6264

65+
std::unique_ptr<juce::Label> chaseMIDINotesLabel;
66+
std::unique_ptr<juce::ToggleButton> chaseMIDINotesCheckbox;
67+
6368
std::unique_ptr<juce::Label> horZoomOnCursorLabel;
6469
std::unique_ptr<juce::ToggleButton> horZoomOnCursorCheckbox;
6570

plugin/source/editor/panels/SettingsPanel.cpp

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,23 @@ SettingsPanel::SettingsPanel(Parameters &params, AudioPluginAudioProcessorEditor
6868
playDraggedNotesCheckbox->setSize(rowHeight, rowHeight);
6969
addAndMakeVisible(playDraggedNotesCheckbox.get());
7070

71+
chaseMIDINotesLabel = std::make_unique<juce::Label>();
72+
chaseMIDINotesLabel->setText("Chase MIDI notes:", juce::dontSendNotification);
73+
chaseMIDINotesLabel->setFont(settingFont);
74+
chaseMIDINotesLabel->setTooltip(
75+
"Notes will be played even if you start playback in the middle of them");
76+
addAndMakeVisible(chaseMIDINotesLabel.get());
77+
78+
chaseMIDINotesCheckbox = std::make_unique<juce::ToggleButton>();
79+
chaseMIDINotesLabel->attachToComponent(chaseMIDINotesCheckbox.get(), true);
80+
chaseMIDINotesCheckbox->setToggleState(GlobalSettings::getInstance().getChaseMIDINotes(),
81+
juce::dontSendNotification);
82+
chaseMIDINotesCheckbox->onStateChange = [this]() {
83+
GlobalSettings::getInstance().setChaseMIDINotes(chaseMIDINotesCheckbox->getToggleState());
84+
};
85+
chaseMIDINotesCheckbox->setSize(rowHeight, rowHeight);
86+
addAndMakeVisible(chaseMIDINotesCheckbox.get());
87+
7188
horZoomOnCursorLabel = std::make_unique<juce::Label>();
7289
horZoomOnCursorLabel->setText("Horizontal zoom on cursor:", juce::dontSendNotification);
7390
horZoomOnCursorLabel->setFont(settingFont);
@@ -145,8 +162,7 @@ SettingsPanel::SettingsPanel(Parameters &params, AudioPluginAudioProcessorEditor
145162

146163
heightCoefSlider = std::make_unique<juce::Slider>();
147164
heightCoefLabel->attachToComponent(heightCoefSlider.get(), true);
148-
heightCoefSlider->setRange(params.min_noteRectHeightCoef, params.max_noteRectHeightCoef,
149-
0.001);
165+
heightCoefSlider->setRange(params.min_noteRectHeightCoef, params.max_noteRectHeightCoef, 0.001);
150166
heightCoefSlider->setValue(params.noteRectHeightCoef);
151167
heightCoefSlider->setTextBoxStyle(juce::Slider::TextBoxLeft, false, 60, rowHeight);
152168
heightCoefSlider->setSliderStyle(juce::Slider::LinearHorizontal);
@@ -259,6 +275,10 @@ void SettingsPanel::resized() {
259275
playDraggedNotesCheckbox->setBounds(playRow.withTrimmedLeft(labelWidth));
260276
area.removeFromTop(padding);
261277

278+
auto noteTrigRow = area.removeFromTop(rowHeight);
279+
chaseMIDINotesCheckbox->setBounds(noteTrigRow.withTrimmedLeft(labelWidth));
280+
area.removeFromTop(padding);
281+
262282
auto horZoomRow = area.removeFromTop(rowHeight);
263283
horZoomOnCursorCheckbox->setBounds(horZoomRow.withTrimmedLeft(labelWidth));
264284
area.removeFromTop(padding + sectionSpacing);
@@ -303,7 +323,7 @@ void SettingsPanel::resized() {
303323

304324
int SettingsPanel::getRequiredHeight() const {
305325
return padding + // top padding
306-
headerRowHeight + padding + 4 * (rowHeight + padding) +
326+
headerRowHeight + padding + 5 * (rowHeight + padding) +
307327
sectionSpacing + // Basic Settings
308328
headerRowHeight + padding + 4 * (rowHeight + padding) +
309329
sectionSpacing + // Visual Settings

plugin/source/processor/PluginProcessor.cpp

Lines changed: 55 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -683,7 +683,7 @@ void AudioPluginAudioProcessor::processBlock(juce::AudioBuffer<float> &buffer,
683683
}
684684

685685
{
686-
// TODO: if it will cause audio glitches,
686+
// TODO: if it will cause audio glitches,
687687
// change the approach to an intermediate buffer for notes
688688
std::scoped_lock lock(notesMutex);
689689
// Stop playing unexisting notes (from piano roll)
@@ -854,39 +854,28 @@ void AudioPluginAudioProcessor::processBlock(juce::AudioBuffer<float> &buffer,
854854
}
855855
}
856856
}
857-
// ===================== Note bend =====================
858-
for (int i = 0; i < notes.size(); ++i) {
859-
const Note &note = notes[i];
860-
if ((note.bend != 0) && (note.time < playHeadTime) &&
861-
(playHeadTime <= note.time + note.duration)) {
862-
int totalCents = note.octave * 1200 + note.cents;
863-
const auto &itNote = noteToChAndMidiNoteMPE.find({i, totalCents});
864-
if (itNote != noteToChAndMidiNoteMPE.end()) {
865-
const auto &chAndMidiNote = itNote->second;
866-
int bendMPE = calcBendMPE(note, playHeadTime);
867-
juce::MidiMessage pitchBend =
868-
juce::MidiMessage::pitchWheel(chAndMidiNote.first, bendMPE);
869-
midiMessages.addEvent(pitchBend, 0);
870-
}
871-
}
872-
}
873857
// ===================== Note on =====================
858+
const bool chaseMIDINotes =
859+
GlobalSettings::getInstance().getChaseMIDINotes();
874860
for (int i = 0; i < notes.size(); ++i) {
875861
const Note &note = notes[i];
876862
int totalCents = note.octave * 1200 + note.cents;
877-
if ((note.time >= playHeadTime) &&
878-
(note.time < playHeadTime + barsInBlock) &&
879-
// need to check it so there will be no bug when you turn on/off
880-
// playback so fast that you can't catch the moment isPlaying=false
881-
// to make noteOff (so just don't make unnecassry noteOn)
863+
bool rightBorderCond =
864+
chaseMIDINotes
865+
? playHeadTime <= note.time + note.duration - barsInBlock
866+
: playHeadTime <= note.time;
867+
if ((note.time - barsInBlock < playHeadTime) && rightBorderCond &&
882868
(!noteToChAndMidiNoteMPE.contains({i, totalCents}))) {
883869
std::tie(midiNote, bendMPE) = calcMidiNoteAndBendMPE(totalCents);
884870
int ch =
885871
channelsManagerMPE->allocateChannelMPE(bendMPE, note.bend != 0);
886872
if (ch != -1) {
887873
pitchesOverflow = false;
888-
int noteOnSample = static_cast<int>(ceil(
889-
numSamples * (note.time - playHeadTime) / barsInBlock));
874+
int noteOnSample = 0;
875+
if (playHeadTime < note.time) {
876+
noteOnSample = static_cast<int>(ceil(
877+
numSamples * (note.time - playHeadTime) / barsInBlock));
878+
}
890879
juce::MidiMessage pitchBend =
891880
juce::MidiMessage::pitchWheel(ch, bendMPE);
892881
midiMessages.addEvent(pitchBend, noteOnSample);
@@ -901,6 +890,22 @@ void AudioPluginAudioProcessor::processBlock(juce::AudioBuffer<float> &buffer,
901890
}
902891
}
903892
}
893+
// ===================== Note bend =====================
894+
for (int i = 0; i < notes.size(); ++i) {
895+
const Note &note = notes[i];
896+
if ((note.bend != 0) && (note.time < playHeadTime) &&
897+
(playHeadTime <= note.time + note.duration)) {
898+
int totalCents = note.octave * 1200 + note.cents;
899+
const auto &itNote = noteToChAndMidiNoteMPE.find({i, totalCents});
900+
if (itNote != noteToChAndMidiNoteMPE.end()) {
901+
const auto &chAndMidiNote = itNote->second;
902+
int bendMPE = calcBendMPE(note, playHeadTime);
903+
juce::MidiMessage pitchBend =
904+
juce::MidiMessage::pitchWheel(chAndMidiNote.first, bendMPE);
905+
midiMessages.addEvent(pitchBend, 0);
906+
}
907+
}
908+
}
904909
}
905910
}
906911

@@ -936,9 +941,9 @@ void AudioPluginAudioProcessor::processBlock(juce::AudioBuffer<float> &buffer,
936941
// = USING MTS-ESP =
937942
// ================================================================================
938943

939-
// TODO: if it causes glitches,
940-
// change the approach to an intermediate buffers for things that are
941-
// changed in prepareToPlay(). Also then use buffers for notes and
944+
// TODO: if it causes glitches,
945+
// change the approach to an intermediate buffers for things that are
946+
// changed in prepareToPlay(). Also then use buffers for notes and
942947
// manPlNotes, so all data will be consistent
943948
std::scoped_lock lock(prepareNotesMutex);
944949

@@ -1073,16 +1078,6 @@ void AudioPluginAudioProcessor::processBlock(juce::AudioBuffer<float> &buffer,
10731078
}
10741079
bool needUpdateFreqs = false;
10751080
// =======================================
1076-
for (int i = 0; i < notes.size(); ++i) {
1077-
const Note &note = notes[i];
1078-
// Note bend
1079-
if ((note.bend != 0) && (note.time < playHeadTime) &&
1080-
(playHeadTime <= note.time + note.duration)) {
1081-
freqs[notesIndexes[i]] = getNoteFreq(note);
1082-
needUpdateFreqs = true;
1083-
}
1084-
}
1085-
// =======================================
10861081
for (int i = 0; i < notes.size(); ++i) {
10871082
const Note &note = notes[i];
10881083
// PRE Note on
@@ -1099,23 +1094,27 @@ void AudioPluginAudioProcessor::processBlock(juce::AudioBuffer<float> &buffer,
10991094
}
11001095
}
11011096
// =======================================
1097+
const bool chaseMIDINotes =
1098+
GlobalSettings::getInstance().getChaseMIDINotes();
11021099
for (int i = 0; i < notes.size(); ++i) {
11031100
const Note &note = notes[i];
11041101
// Note on
1105-
if ((note.time >= playHeadTime) &&
1106-
(note.time < playHeadTime + barsInBlock) &&
1107-
// need to check it so there will be no bug when you turn on/off
1108-
// playback so fast that you can't catch the moment isPlaying=false
1109-
// to make noteOff (so just don't make unnecassry noteOn)
1102+
bool rightBorderCond =
1103+
chaseMIDINotes
1104+
? playHeadTime <= note.time + note.duration - barsInBlock
1105+
: playHeadTime <= note.time;
1106+
if ((note.time - barsInBlock < playHeadTime) && rightBorderCond &&
11101107
!currPlayedNotesTotalCents.contains(note.octave * 1200 +
11111108
note.cents)) {
11121109
const int noteInd = notesIndexes[i];
11131110
juce::MidiMessage noteOn = juce::MidiMessage::noteOn(
11141111
params.channelIndex + 1, noteInd, note.velocity);
1115-
midiMessages.addEvent(
1116-
noteOn,
1117-
static_cast<int>(ceil(numSamples * (note.time - playHeadTime) /
1118-
barsInBlock)));
1112+
int noteOnSample = 0;
1113+
if (playHeadTime < note.time) {
1114+
noteOnSample = static_cast<int>(ceil(
1115+
numSamples * (note.time - playHeadTime) / barsInBlock));
1116+
}
1117+
midiMessages.addEvent(noteOn, noteOnSample);
11191118
currPlayedNotesTotalCents.insert(note.octave * 1200 + note.cents);
11201119
currPlayedNotesIndexes.insert(noteInd);
11211120
if (note.bend != 0) {
@@ -1125,6 +1124,16 @@ void AudioPluginAudioProcessor::processBlock(juce::AudioBuffer<float> &buffer,
11251124
}
11261125
}
11271126
}
1127+
// =======================================
1128+
for (int i = 0; i < notes.size(); ++i) {
1129+
const Note &note = notes[i];
1130+
// Note bend
1131+
if ((note.bend != 0) && (note.time < playHeadTime) &&
1132+
(playHeadTime <= note.time + note.duration)) {
1133+
freqs[notesIndexes[i]] = getNoteFreq(note);
1134+
needUpdateFreqs = true;
1135+
}
1136+
}
11281137
if (needUpdateFreqs) {
11291138
pluginInstanceManager->updateFreqs(freqs);
11301139
}

0 commit comments

Comments
 (0)