Skip to content

Commit d5a81e5

Browse files
committed
Sequencer MIDI control.
1 parent d410df0 commit d5a81e5

File tree

11 files changed

+126
-45
lines changed

11 files changed

+126
-45
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ From the original implementation this project takes only `addsynth` and `rankwav
2020
The rest and the most of the code (including voicing, spatialisation, reverb, etc.) is all new, and it is not based on the original Aeolus, so the sound this plugin produces is different.
2121

2222
The convolution reverb uses IRs from the [Open AIR](https://www.openair.hosted.york.ac.uk/) project database.
23+
24+
## MIDI Control
25+
Sequencer steps are controlled via the program change messages sent on the control MIDI channel (program 0 corresponds to the first step of the sequencer, 1 - to the second and so on).

Source/PluginEditor.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ AeolusAudioProcessorEditor::AeolusAudioProcessorEditor (AeolusAudioProcessor& p)
4848
, _volumeLevelR{p.getEngine().getVolumeLevel().right, ui::LevelIndicator::Orientation::Horizontal}
4949
, _panicButton{"PANIC"}
5050
, _cancelButton{"Cancel"}
51+
, _midiControlChannelLabel{{}, {"Conrol channel"}}
52+
, _midiControlChannelComboBox{}
5153
{
5254
setLookAndFeel(&ui::CustomLookAndFeel::getInstance());
5355

@@ -132,6 +134,21 @@ AeolusAudioProcessorEditor::AeolusAudioProcessorEditor (AeolusAudioProcessor& p)
132134

133135
addAndMakeVisible(_sequencerView);
134136

137+
_midiControlChannelLabel.setColour(Label::textColourId, Colour(0x99, 0x99, 0x99));
138+
addAndMakeVisible(_midiControlChannelLabel);
139+
140+
_midiControlChannelComboBox.addItem("All", 1);
141+
for (int i = 1; i <= 16; ++i) {
142+
_midiControlChannelComboBox.addItem(String(i), i + 1);
143+
}
144+
145+
_midiControlChannelComboBox.setSelectedId(1 + _audioProcessor.getEngine().getMIDIControlChannel(), juce::dontSendNotification);
146+
_midiControlChannelComboBox.onChange = [this]() {
147+
_audioProcessor.getEngine().setMIDIControlChannel(_midiControlChannelComboBox.getSelectedId() - 1);
148+
};
149+
150+
addAndMakeVisible(_midiControlChannelComboBox);
151+
135152
resized();
136153

137154
startTimerHz(10);
@@ -196,6 +213,10 @@ void AeolusAudioProcessorEditor::resized()
196213

197214
_cancelButton.setColour(TextButton::buttonColourId, Colour(0x66, 0x66, 0x33));
198215
_cancelButton.setBounds((_midiKeyboard.getX() - 60)/2, getHeight() - 60, 60, 35);
216+
217+
int x = _midiKeyboard.getRight() + (getWidth() - _midiKeyboard.getRight() - 100) / 2;
218+
_midiControlChannelLabel.setBounds(x, _midiKeyboard.getY(), 100, 20);
219+
_midiControlChannelComboBox.setBounds(x, _midiControlChannelLabel.getBottom() + 5, 100, 20);
199220
}
200221

201222
void AeolusAudioProcessorEditor::timerCallback()
@@ -218,7 +239,9 @@ void AeolusAudioProcessorEditor::refresh()
218239
{
219240
updateMeters();
220241
updateDivisionViews();
242+
updateSequencerView();
221243
updateMidiKeyboardRange();
244+
updateMidiControlChannel();
222245
}
223246

224247
void AeolusAudioProcessorEditor::updateMeters()
@@ -245,3 +268,14 @@ void AeolusAudioProcessorEditor::updateDivisionViews()
245268
for (auto* dv : _divisionViews)
246269
dv->update();
247270
}
271+
272+
void AeolusAudioProcessorEditor::updateSequencerView()
273+
{
274+
_sequencerView.update();
275+
}
276+
277+
void AeolusAudioProcessorEditor::updateMidiControlChannel()
278+
{
279+
const int ch = _audioProcessor.getEngine().getMIDIControlChannel();
280+
_midiControlChannelComboBox.setSelectedId(1 + ch, juce::dontSendNotification);
281+
}

Source/PluginEditor.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ class AeolusAudioProcessorEditor : public juce::AudioProcessorEditor,
5454
void updateMeters();
5555
void updateMidiKeyboardRange();
5656
void updateDivisionViews();
57+
void updateSequencerView();
58+
void updateMidiControlChannel();
5759

5860
AeolusAudioProcessor& _audioProcessor;
5961

@@ -85,5 +87,9 @@ class AeolusAudioProcessorEditor : public juce::AudioProcessorEditor,
8587
/// Organ cancel button
8688
juce::TextButton _cancelButton;
8789

90+
/// MIDI control channel selection
91+
juce::Label _midiControlChannelLabel;
92+
juce::ComboBox _midiControlChannelComboBox;
93+
8894
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AeolusAudioProcessorEditor)
8995
};

Source/PluginProcessor.cpp

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,27 +78,30 @@ bool AeolusAudioProcessor::isMidiEffect() const
7878

7979
double AeolusAudioProcessor::getTailLengthSeconds() const
8080
{
81-
return 0.0;
81+
return _engine.getReverbLengthInSeconds();
8282
}
8383

8484
int AeolusAudioProcessor::getNumPrograms()
8585
{
86-
return 1; // NB: some hosts don't cope very well if you tell them there are 0 programs,
87-
// so this should be at least 1, even if you're not really implementing programs.
86+
// NB: some hosts don't cope very well if you tell them there are 0 programs,
87+
// so this should be at least 1, even if you're not really implementing programs.
88+
return jmax(1, _engine.getSequencer()->getStepsCount());
8889
}
8990

9091
int AeolusAudioProcessor::getCurrentProgram()
9192
{
92-
return 0;
93+
return _engine.getSequencer()->getCurrentStep();
9394
}
9495

95-
void AeolusAudioProcessor::setCurrentProgram (int /* index */)
96+
void AeolusAudioProcessor::setCurrentProgram (int index)
9697
{
98+
if (index >= 0 && index < _engine.getSequencer()->getStepsCount())
99+
_engine.getSequencer()->setStep(index);
97100
}
98101

99-
const juce::String AeolusAudioProcessor::getProgramName (int /* index */)
102+
const juce::String AeolusAudioProcessor::getProgramName (int index)
100103
{
101-
return {};
104+
return juce::String("Sequencer step ") + juce::String(index + 1);
102105
}
103106

104107
void AeolusAudioProcessor::changeProgramName (int /* index */, const juce::String& /* newName */)
@@ -196,7 +199,7 @@ void AeolusAudioProcessor::processMidi (juce::MidiBuffer& midiMessages)
196199
for (auto msgIter : midiMessages) {
197200
const auto msg = msgIter.getMessage();
198201

199-
_engine.getMidiKeyboardState().processNextMidiEvent(msg);
202+
_engine.processMIDIMessage(msg);
200203
}
201204

202205
}

Source/aeolus/engine.cpp

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ Engine::Engine()
204204
, _interpolator{1.0f}
205205
, _midiKeyboardState{}
206206
, _volumeLevel{}
207+
, _midiControlChannel{0}
207208
{
208209
populateDivisions();
209210

@@ -254,6 +255,11 @@ void Engine::postReverbIR(int num)
254255
_irSwitchEvents.send({num});
255256
}
256257

258+
float Engine::getReverbLengthInSeconds() const
259+
{
260+
return float(_convolver.length()) * SAMPLE_RATE_R;
261+
}
262+
257263
void Engine::setReverbWet(float v)
258264
{
259265
_convolver.setDryWet(1.0f, v);
@@ -328,6 +334,17 @@ void Engine::process(float* outL, float* outR, int numFrames, bool isNonRealtime
328334
_volumeLevel.right.process(origOutR, origNumFrames);
329335
}
330336

337+
void Engine::processMIDIMessage(const MidiMessage& message)
338+
{
339+
const int ch = getMIDIControlChannel();
340+
341+
if (ch == 0 || message.getChannel() == 0 || message.getChannel() == getMIDIControlChannel())
342+
processControlMIDIMessage(message);
343+
344+
if (message.isNoteOnOrOff())
345+
_midiKeyboardState.processNextMidiEvent(message);
346+
}
347+
331348
void Engine::noteOn(int note, int midiChannel)
332349
{
333350
clearDivisionsTriggerFlag();
@@ -388,6 +405,9 @@ var Engine::getPersistentState() const
388405
{
389406
auto* obj = new DynamicObject();
390407

408+
// Save control channel
409+
obj->setProperty("midi_ctrl_channel", getMIDIControlChannel());
410+
391411
// Save the IR.
392412
int irNum = _selectedIR;
393413
obj->setProperty("ir", irNum);
@@ -410,6 +430,9 @@ var Engine::getPersistentState() const
410430
void Engine::setPersistentState(const var& state)
411431
{
412432
if (const auto* obj = state.getDynamicObject()) {
433+
// Restore control channel
434+
setMIDIControlChannel(obj->getProperty("midi_ctrl_channel"));
435+
413436
// Restore the IR
414437
int irNum = obj->getProperty("ir");
415438

@@ -580,4 +603,20 @@ void Engine::applyVolume(float* outL, float* outR, int numFrames)
580603
}
581604
}
582605

606+
void Engine::processControlMIDIMessage(const MidiMessage& message)
607+
{
608+
// NOTE: VST3 will not pass through the program change MIDI messages.
609+
// Instead program change must be handled at the processor level
610+
// via the setCurrentProgram() method.
611+
612+
// Here we handle the program change message nevertheless
613+
// in case of a non-VST3 or stand-alone plugin.
614+
if (message.isProgramChange()) {
615+
int step = message.getProgramChangeNumber();
616+
617+
if (step >= 0 && step < _sequencer->getStepsCount())
618+
_sequencer->setStep(step);
619+
}
620+
}
621+
583622
AEOLUS_NAMESPACE_END

Source/aeolus/engine.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ class Engine
161161
*/
162162
int getReverbIR() const noexcept { return _selectedIR; }
163163

164+
float getReverbLengthInSeconds() const;
165+
164166
/**
165167
* Set reverb wet output level (linear).
166168
* @note This must be called on the audio thread.
@@ -178,11 +180,26 @@ class Engine
178180
*/
179181
Level& getVolumeLevel() noexcept { return _volumeLevel; }
180182

183+
/**
184+
* Assign MIDI channel to be used to control the organ stops and sequencer.
185+
*/
186+
int getMIDIControlChannel() const noexcept { return _midiControlChannel; }
187+
188+
/**
189+
* Returns currently set MIDI control channel.
190+
*/
191+
void setMIDIControlChannel(int c) noexcept { _midiControlChannel = c; }
192+
181193
/**
182194
* Generate audio.
183195
*/
184196
void process(float* outL, float* outR, int numFrames, bool isNonRealtime = false);
185197

198+
/**
199+
* Process incoming MIDI messages.
200+
*/
201+
void processMIDIMessage(const juce::MidiMessage& message);
202+
186203
/**
187204
* Handle note-on events.
188205
*/
@@ -233,6 +250,8 @@ class Engine
233250
/// Apply the gloval volume.
234251
void applyVolume(float* outL, float* outR, int numFrames);
235252

253+
void processControlMIDIMessage(const juce::MidiMessage& message);
254+
236255
float _sampleRate;
237256

238257
RingBuffer<NoteEvent, 1024> _pendingNoteEvents;
@@ -266,6 +285,8 @@ class Engine
266285

267286
Level _volumeLevel;
268287

288+
std::atomic<int> _midiControlChannel;
289+
269290
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Engine)
270291
};
271292

Source/aeolus/sequencer.cpp

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ Sequencer::Sequencer(Engine& engine, int numSteps)
9090
: _engine{engine}
9191
, _steps(numSteps)
9292
, _currentStep{0}
93-
, _listeners{}
9493
{
9594
jassert(_steps.size() > 0);
9695

@@ -107,7 +106,7 @@ var Sequencer::getPersistentState() const
107106
stepsArr.add(_steps[stepIdx].getPersistentState());
108107

109108
sequencerObj->setProperty("steps", stepsArr);
110-
sequencerObj->setProperty("current_step", _currentStep);
109+
sequencerObj->setProperty("current_step", getCurrentStep());
111110

112111
return var{sequencerObj};
113112
}
@@ -148,10 +147,6 @@ void Sequencer::setStep(int index, bool captureCurrentState)
148147

149148
_currentStep = index;
150149
recallState(_steps[_currentStep]);
151-
152-
_listeners.call([index](Listener& listener) {
153-
listener.sequencerStepChanged(index);
154-
});
155150
}
156151
}
157152

Source/aeolus/sequencer.h

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
#pragma once
2121

2222
#include "aeolus/globals.h"
23-
23+
#include <atomic>
2424
#include <vector>
2525

2626
AEOLUS_NAMESPACE_BEGIN
@@ -51,22 +51,12 @@ class Sequencer
5151
void setPersistentState(const juce::var& v);
5252
};
5353

54-
class Listener
55-
{
56-
public:
57-
virtual ~Listener() {};
58-
virtual void sequencerStepChanged(int step) = 0;
59-
};
60-
6154
Sequencer() = delete;
6255
Sequencer(Engine& engine, int numSteps);
6356

6457
int getStepsCount() const noexcept { return (int)_steps.size(); }
6558
int getCurrentStep() const noexcept { return _currentStep; }
6659

67-
void addListener(Listener* listener) { _listeners.add(listener); }
68-
void removeListener(Listener* listener) { _listeners.remove(listener); }
69-
7060
juce::var getPersistentState() const;
7161
void setPersistentState(const juce::var& v);
7262

@@ -87,9 +77,7 @@ class Sequencer
8777

8878
Engine& _engine;
8979
std::vector<OrganState> _steps;
90-
int _currentStep;
91-
92-
juce::ListenerList<Listener> _listeners;
80+
std::atomic<int> _currentStep;
9381

9482
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Sequencer)
9583
};

Source/ui/DivisionControlPanel.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ DivisionControlPanel::DivisionControlPanel(aeolus::Division* division)
4646
_midiChannelComboBox.addItem(String(i), i + 1);
4747
}
4848

49-
_midiChannelComboBox.setSelectedId(1 + _division->getMIDIChannel(), true);
49+
_midiChannelComboBox.setSelectedId(1 + _division->getMIDIChannel(), juce::dontSendNotification);
5050
_midiChannelComboBox.onChange = [this]() {
5151
_division->setMIDIChannel(_midiChannelComboBox.getSelectedId() - 1);
5252
};

Source/ui/SequencerView.cpp

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,14 @@ SequencerView::SequencerView(aeolus::Sequencer* sequencer)
3737
_sequencer->stepForward();
3838
};
3939
addAndMakeVisible(_advanceButton);
40-
41-
_sequencer->addListener(this);
4240
}
4341

44-
SequencerView::~SequencerView()
42+
void SequencerView::update()
4543
{
46-
_sequencer->removeListener(this);
44+
const auto currentStep = _sequencer->getCurrentStep();
45+
46+
for (int i = 0; i < _stepButtons.size(); ++i)
47+
_stepButtons[i]->setToggleState(i == currentStep, juce::dontSendNotification);
4748
}
4849

4950
void SequencerView::resized()
@@ -69,12 +70,6 @@ void SequencerView::resized()
6970
_advanceButton.setBounds(x, buttonPadding, 2 * buttonWidth, getHeight() - 2 * buttonPadding);
7071
}
7172

72-
void SequencerView::sequencerStepChanged(int step)
73-
{
74-
if (step >= 0 && step < _stepButtons.size())
75-
_stepButtons[step]->setToggleState(true, juce::dontSendNotification);
76-
}
77-
7873
void SequencerView::populateStepButtons()
7974
{
8075
const auto numSteps = _sequencer->getStepsCount();

0 commit comments

Comments
 (0)