diff --git a/CHANGELOG.md b/CHANGELOG.md
index b66b206f78..246a690804 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -106,6 +106,7 @@ at velocity 0 it would look the same as its tail (but you can't have 0 velocity)
##### Synth/Kit Clips
- Added Auto-Load feature to sample browser, so you can load the sounds to the instrument as you preview them. Auto-Load can be engaged while in sample browser, if you press the `Load` button.
+- Sounds have now the ability to send MIDI notes at the same time as they play a sample. This will allow your synths and drums to trigger external devices. A new menu `MIDI` has been added at the bottom of the `SOUND` menu to set the MIDI channel and the note (in case of drum sounds).
##### CV Clips
- Added the ability to set a CV instrument to use both 1 and 2 channels, which makes the cv2 source selectable between mod wheel, velocity, and aftertouch
diff --git a/docs/community_features.md b/docs/community_features.md
index ea17fe4e70..64863b7fa6 100644
--- a/docs/community_features.md
+++ b/docs/community_features.md
@@ -1085,6 +1085,13 @@ as an oscillator type within the subtractive engine, so it can be combined with
- ([#3279]) Added two more envelopes (Envelope 3 and Envelope 4), which you can access from the sound editor menu.
+#### 4.5.9 - Send Midi
+
+- ([#3313]) There is a new submenu `MIDI` added to the `SOUND` menu for synths and sound drums, where you can select the MIDI channel
+ (and also base note for drums) that will be sent at the same time as the sound triggers.
+ In case of drums, it is like having a Sound row + a Midi row together triggering at the same time. And in case of synths, it is like
+ having a Synth clip + a Midi clip together triggering at the same time. This feature is limited to regular MIDI (that is, not for MPE).
+
### 4.6 - Instrument Clip View - Kit Clip Features
#### 4.6.1 - Keyboard View
@@ -1592,6 +1599,8 @@ different firmware
[#3291]: https://github.com/SynthstromAudible/DelugeFirmware/pull/3291
+[#3313]: https://github.com/SynthstromAudible/DelugeFirmware/pull/3313
+
[Automation View Documentation]: features/automation_view.md
[Arpeggiator Documentation]: features/arpeggiator.md
diff --git a/docs/menu_hierarchies.md b/docs/menu_hierarchies.md
index 09dd6a56df..4f8902bb4e 100644
--- a/docs/menu_hierarchies.md
+++ b/docs/menu_hierarchies.md
@@ -1121,6 +1121,11 @@ The Sound menu contains the following menu hierarchy:
- Reversed
- Ping-Pong
+MIDI
+
+ - Channel
+ - Note (only available for Kits)
+
diff --git a/src/definitions_cxx.hpp b/src/definitions_cxx.hpp
index d9c4ec1686..d04760dcb5 100644
--- a/src/definitions_cxx.hpp
+++ b/src/definitions_cxx.hpp
@@ -906,6 +906,7 @@ constexpr int32_t MIDI_CHANNEL_MPE_LOWER_ZONE = 16;
constexpr int32_t MIDI_CHANNEL_MPE_UPPER_ZONE = 17;
constexpr int32_t NUM_CHANNELS = 18;
constexpr int32_t MIDI_CHANNEL_NONE = 255;
+constexpr int32_t MIDI_NOTE_NONE = 255;
constexpr int32_t MIDI_CC_NONE = 255;
constexpr int32_t NUM_INTERNAL_DESTS = 1;
@@ -950,6 +951,7 @@ constexpr int32_t kSubmenuIconSpacingX = 7;
// For kits
constexpr int32_t kNoteForDrum = 60;
+constexpr int32_t kDefaultNoteOffVelocity = 64;
enum BendRange {
BEND_RANGE_MAIN,
diff --git a/src/deluge/gui/l10n/english.json b/src/deluge/gui/l10n/english.json
index 2055637972..5830fe1d4b 100644
--- a/src/deluge/gui/l10n/english.json
+++ b/src/deluge/gui/l10n/english.json
@@ -837,6 +837,7 @@
"STRING_FOR_MPE_LOWER_ZONE": "MPE lower zone",
"STRING_FOR_MPE_UPPER_ZONE": "MPE upper zone",
"STRING_FOR_CHANNEL": "Channel",
+ "STRING_FOR_NOTE": "Note",
"STRING_FOR_SET": "SET",
"STRING_FOR_UNLEARNED": "UNLEARNED",
"STRING_FOR_LEARNED": "LEARNED",
diff --git a/src/deluge/gui/l10n/g_english.cpp b/src/deluge/gui/l10n/g_english.cpp
index e73ab1705f..5b25720c97 100644
--- a/src/deluge/gui/l10n/g_english.cpp
+++ b/src/deluge/gui/l10n/g_english.cpp
@@ -783,6 +783,7 @@ PLACE_SDRAM_DATA Language english{
{STRING_FOR_MPE_LOWER_ZONE, "MPE lower zone"},
{STRING_FOR_MPE_UPPER_ZONE, "MPE upper zone"},
{STRING_FOR_CHANNEL, "Channel"},
+ {STRING_FOR_NOTE, "Note"},
{STRING_FOR_SET, "SET"},
{STRING_FOR_UNLEARNED, "UNLEARNED"},
{STRING_FOR_LEARNED, "LEARNED"},
diff --git a/src/deluge/gui/l10n/strings.h b/src/deluge/gui/l10n/strings.h
index 4afe6330fd..4b769b5347 100644
--- a/src/deluge/gui/l10n/strings.h
+++ b/src/deluge/gui/l10n/strings.h
@@ -845,6 +845,7 @@ enum class String : size_t {
STRING_FOR_MPE_LOWER_ZONE,
STRING_FOR_MPE_UPPER_ZONE,
STRING_FOR_CHANNEL,
+ STRING_FOR_NOTE,
STRING_FOR_SET,
STRING_FOR_UNLEARNED,
STRING_FOR_LEARNED,
diff --git a/src/deluge/gui/menu_item/midi/sound/channel.h b/src/deluge/gui/menu_item/midi/sound/channel.h
new file mode 100644
index 0000000000..9ec998d719
--- /dev/null
+++ b/src/deluge/gui/menu_item/midi/sound/channel.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2014-2023 Synthstrom Audible Limited
+ *
+ * This file is part of The Synthstrom Audible Deluge Firmware.
+ *
+ * The Synthstrom Audible Deluge Firmware is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see .
+ */
+#pragma once
+#include "definitions_cxx.hpp"
+#include "gui/menu_item/integer.h"
+#include "gui/ui/sound_editor.h"
+#include "hid/display/oled.h"
+#include "processing/sound/sound.h"
+
+namespace deluge::gui::menu_item::midi::sound {
+
+class OutputMidiChannel final : public Integer {
+public:
+ using Integer::Integer;
+ [[nodiscard]] int32_t getMinValue() const override { return 0; }
+ [[nodiscard]] int32_t getMaxValue() const override { return 16; }
+ void readCurrentValue() override {
+ int32_t value = soundEditor.currentSound->outputMidiChannel;
+ if (value == MIDI_CHANNEL_NONE) {
+ value = 0;
+ }
+ else {
+ value = value + 1;
+ }
+ this->setValue(value);
+ }
+ void writeCurrentValue() override {
+ int32_t value = this->getValue();
+ if (value == 0) {
+ value = MIDI_CHANNEL_NONE;
+ }
+ else {
+ value = value - 1;
+ }
+ soundEditor.currentSound->outputMidiChannel = value;
+ }
+
+ void drawValue() override {
+ int32_t value = this->getValue();
+ if (value == 0) {
+ display->setScrollingText(l10n::get(l10n::String::STRING_FOR_OFF));
+ }
+ else {
+ char name[12];
+ snprintf(name, sizeof(name), "%d", value);
+ display->setScrollingText(name);
+ }
+ }
+
+ void drawInteger(int32_t textWidth, int32_t textHeight, int32_t yPixel) override {
+ deluge::hid::display::oled_canvas::Canvas& canvas = hid::display::OLED::main;
+ int32_t value = this->getValue();
+ if (value == 0) {
+ canvas.drawStringCentred(l10n::get(l10n::String::STRING_FOR_OFF), yPixel + OLED_MAIN_TOPMOST_PIXEL,
+ textWidth, textHeight);
+ }
+ else {
+ char name[12];
+ snprintf(name, sizeof(name), "%d", value);
+ canvas.drawStringCentred(name, yPixel + OLED_MAIN_TOPMOST_PIXEL, textWidth, textHeight);
+ }
+ }
+};
+} // namespace deluge::gui::menu_item::midi::sound
diff --git a/src/deluge/gui/menu_item/midi/sound/note_for_drum.h b/src/deluge/gui/menu_item/midi/sound/note_for_drum.h
new file mode 100644
index 0000000000..053f1e790b
--- /dev/null
+++ b/src/deluge/gui/menu_item/midi/sound/note_for_drum.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2014-2023 Synthstrom Audible Limited
+ *
+ * This file is part of The Synthstrom Audible Deluge Firmware.
+ *
+ * The Synthstrom Audible Deluge Firmware is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see .
+ */
+#pragma once
+#include "definitions_cxx.hpp"
+#include "gui/menu_item/integer.h"
+#include "gui/ui/sound_editor.h"
+#include "hid/display/oled.h"
+#include "processing/sound/sound.h"
+#include
+
+namespace deluge::gui::menu_item::midi::sound {
+
+class OutputMidiNoteForDrum final : public Integer {
+public:
+ using Integer::Integer;
+ [[nodiscard]] int32_t getMinValue() const override { return 0; }
+ [[nodiscard]] int32_t getMaxValue() const override { return kMaxMIDIValue; }
+ bool isRelevant(ModControllableAudio* modControllable, int32_t whichThing) override {
+ return soundEditor.editingKit() && !soundEditor.editingNonAudioDrumRow();
+ }
+ void readCurrentValue() override {
+ int32_t value = soundEditor.currentSound->outputMidiNoteForDrum;
+ if (value == MIDI_NOTE_NONE) {
+ value = kNoteForDrum;
+ }
+ this->setValue(value);
+ }
+ void writeCurrentValue() override { soundEditor.currentSound->outputMidiNoteForDrum = this->getValue(); }
+};
+} // namespace deluge::gui::menu_item::midi::sound
diff --git a/src/deluge/gui/ui/menus.cpp b/src/deluge/gui/ui/menus.cpp
index eb9f5173aa..38a1e1d0d4 100644
--- a/src/deluge/gui/ui/menus.cpp
+++ b/src/deluge/gui/ui/menus.cpp
@@ -100,6 +100,8 @@
#include "gui/menu_item/midi/follow/follow_kit_root_note.h"
#include "gui/menu_item/midi/mpe_to_mono.h"
#include "gui/menu_item/midi/pgm.h"
+#include "gui/menu_item/midi/sound/channel.h"
+#include "gui/menu_item/midi/sound/note_for_drum.h"
#include "gui/menu_item/midi/sub.h"
#include "gui/menu_item/midi/takeover.h"
#include "gui/menu_item/midi/transpose.h"
@@ -546,6 +548,11 @@ Submenu soundDistortionMenu{
},
};
+// Output MIDI for sound drums --------------------------------------------------------------
+midi::sound::OutputMidiChannel outputMidiChannelMenu{STRING_FOR_CHANNEL, STRING_FOR_CHANNEL};
+midi::sound::OutputMidiNoteForDrum outputMidiNoteForDrumMenu{STRING_FOR_NOTE, STRING_FOR_NOTE};
+Submenu outputMidiSubmenu{STRING_FOR_MIDI, {&outputMidiChannelMenu, &outputMidiNoteForDrumMenu}};
+
// MIDIInstrument menu ----------------------------------------------------------------------
midi::device_definition::Linked midiDeviceLinkedMenu{STRING_FOR_MIDI_DEVICE_DEFINITION_LINKED,
STRING_FOR_MIDI_DEVICE_DEFINITION_LINKED};
@@ -1336,6 +1343,7 @@ menu_item::Submenu soundEditorRootMenu{
&drumBendRangeMenu,
&patchCablesMenu,
&sequenceDirectionMenu,
+ &outputMidiSubmenu,
},
};
diff --git a/src/deluge/io/midi/midi_engine.h b/src/deluge/io/midi/midi_engine.h
index c81ed6c29c..41936683fb 100644
--- a/src/deluge/io/midi/midi_engine.h
+++ b/src/deluge/io/midi/midi_engine.h
@@ -42,10 +42,12 @@ struct MIDISource {
MIDISource(PlaybackHandler const* handler) : source_(handler) {};
MIDISource(MidiFollow const* follow) : source_(follow) {};
MIDISource(MIDIDrum const* drum) : source_(drum) {};
+ MIDISource(Sound const* sound) : source_(sound) {};
MIDISource(MIDICable const& cable) : source_(&cable) {};
MIDISource(MIDIInstrument const& instrument) : source_(&instrument) {};
MIDISource(MIDIDrum const& drum) : source_(&drum) {};
+ MIDISource(Sound const& sound) : source_(&sound) {};
MIDISource(MidiFollow const& follow) : source_(&follow) {};
MIDISource(PlaybackHandler const& handler) : source_(&handler) {};
diff --git a/src/deluge/model/drum/midi_drum.cpp b/src/deluge/model/drum/midi_drum.cpp
index 787997d3e1..d20e6196e9 100644
--- a/src/deluge/model/drum/midi_drum.cpp
+++ b/src/deluge/model/drum/midi_drum.cpp
@@ -64,7 +64,7 @@ void MIDIDrum::noteOnPostArp(int32_t noteCodePostArp, ArpNote* arpNote, int32_t
}
void MIDIDrum::noteOffPostArp(int32_t noteCodePostArp) {
- midiEngine.sendNote(this, false, noteCodePostArp, 64, channel, kMIDIOutputFilterNoMPE);
+ midiEngine.sendNote(this, false, noteCodePostArp, kDefaultNoteOffVelocity, channel, kMIDIOutputFilterNoMPE);
state = false;
}
@@ -156,7 +156,10 @@ void MIDIDrum::expressionEvent(int32_t newValue, int32_t expressionDimension) {
// Aftertouch only
if (expressionDimension == 2) {
int32_t value7 = newValue >> 24;
- midiEngine.sendPolyphonicAftertouch(this, channel, value7, note, kMIDIOutputFilterNoMPE);
+ // Note: use the note code currently on post-arp, because this drum supports "Chord Simulator" and "Octaves" and
+ // the note code could be different
+ midiEngine.sendPolyphonicAftertouch(this, channel, value7, arpeggiator.arpNote.noteCodeOnPostArp[0],
+ kMIDIOutputFilterNoMPE);
}
}
diff --git a/src/deluge/model/instrument/non_audio_instrument.cpp b/src/deluge/model/instrument/non_audio_instrument.cpp
index f0adaf4cb3..85ca6c2f8a 100644
--- a/src/deluge/model/instrument/non_audio_instrument.cpp
+++ b/src/deluge/model/instrument/non_audio_instrument.cpp
@@ -123,8 +123,6 @@ void NonAudioInstrument::sendNote(ModelStackWithThreeMainThings* modelStack, boo
void NonAudioInstrument::polyphonicExpressionEventOnChannelOrNote(int32_t newValue, int32_t expressionDimension,
int32_t channelOrNoteNumber,
MIDICharacteristic whichCharacteristic) {
- ArpeggiatorSettings* settings = getArpSettings();
-
int32_t n;
int32_t nEnd;
diff --git a/src/deluge/modulation/arpeggiator.cpp b/src/deluge/modulation/arpeggiator.cpp
index 5ba27a1266..285deba434 100644
--- a/src/deluge/modulation/arpeggiator.cpp
+++ b/src/deluge/modulation/arpeggiator.cpp
@@ -166,10 +166,14 @@ void ArpeggiatorForDrum::noteOff(ArpeggiatorSettings* settings, int32_t noteCode
if ((settings == nullptr) || settings->mode == ArpMode::OFF) {
instruction->noteCodeOffPostArp[0] = noteCodePreArp;
instruction->outputMIDIChannelOff[0] = arpNote.outputMemberChannel[0];
+ noteCodeCurrentlyOnPostArp[0] = ARP_NOTE_NONE;
+ outputMIDIChannelForNoteCurrentlyOnPostArp[0] = MIDI_CHANNEL_NONE;
for (int32_t n = 1; n < ARP_MAX_INSTRUCTION_NOTES; n++) {
// If no arp, rest of chord notes are for sure disabled
instruction->noteCodeOffPostArp[n] = ARP_NOTE_NONE;
instruction->outputMIDIChannelOff[n] = MIDI_CHANNEL_NONE;
+ noteCodeCurrentlyOnPostArp[n] = ARP_NOTE_NONE;
+ outputMIDIChannelForNoteCurrentlyOnPostArp[n] = MIDI_CHANNEL_NONE;
}
}
@@ -180,6 +184,9 @@ void ArpeggiatorForDrum::noteOff(ArpeggiatorSettings* settings, int32_t noteCode
// Set all chord notes
instruction->noteCodeOffPostArp[n] = noteCodeCurrentlyOnPostArp[n];
instruction->outputMIDIChannelOff[n] = outputMIDIChannelForNoteCurrentlyOnPostArp[n];
+ // Clean the temp state
+ noteCodeCurrentlyOnPostArp[n] = ARP_NOTE_NONE;
+ outputMIDIChannelForNoteCurrentlyOnPostArp[n] = MIDI_CHANNEL_NONE;
}
}
}
@@ -317,10 +324,14 @@ void Arpeggiator::noteOff(ArpeggiatorSettings* settings, int32_t noteCodePreArp,
if (arpOff) {
instruction->noteCodeOffPostArp[0] = noteCodePreArp;
instruction->outputMIDIChannelOff[0] = arpNote->outputMemberChannel[0];
+ noteCodeCurrentlyOnPostArp[0] = ARP_NOTE_NONE;
+ outputMIDIChannelForNoteCurrentlyOnPostArp[0] = MIDI_CHANNEL_NONE;
for (int32_t n = 1; n < ARP_MAX_INSTRUCTION_NOTES; n++) {
// If no arp, rest of chord notes are for sure disabled
instruction->noteCodeOffPostArp[n] = ARP_NOTE_NONE;
instruction->outputMIDIChannelOff[n] = MIDI_CHANNEL_NONE;
+ noteCodeCurrentlyOnPostArp[n] = ARP_NOTE_NONE;
+ outputMIDIChannelForNoteCurrentlyOnPostArp[n] = MIDI_CHANNEL_NONE;
}
}
@@ -333,6 +344,8 @@ void Arpeggiator::noteOff(ArpeggiatorSettings* settings, int32_t noteCodePreArp,
// Set all chord notes
instruction->noteCodeOffPostArp[n] = noteCodeCurrentlyOnPostArp[n];
instruction->outputMIDIChannelOff[n] = outputMIDIChannelForNoteCurrentlyOnPostArp[n];
+ noteCodeCurrentlyOnPostArp[n] = ARP_NOTE_NONE;
+ outputMIDIChannelForNoteCurrentlyOnPostArp[n] = MIDI_CHANNEL_NONE;
}
}
}
@@ -397,6 +410,9 @@ void ArpeggiatorBase::switchAnyNoteOff(ArpReturnInstruction* instruction) {
// Set all chord notes
instruction->noteCodeOffPostArp[n] = noteCodeCurrentlyOnPostArp[n];
instruction->outputMIDIChannelOff[n] = outputMIDIChannelForNoteCurrentlyOnPostArp[n];
+ // Clean the temp state
+ noteCodeCurrentlyOnPostArp[n] = ARP_NOTE_NONE;
+ outputMIDIChannelForNoteCurrentlyOnPostArp[n] = MIDI_CHANNEL_NONE;
}
gateCurrentlyActive = false;
}
diff --git a/src/deluge/modulation/arpeggiator.h b/src/deluge/modulation/arpeggiator.h
index 0340717772..7e61f39fb4 100644
--- a/src/deluge/modulation/arpeggiator.h
+++ b/src/deluge/modulation/arpeggiator.h
@@ -36,6 +36,11 @@ constexpr uint32_t PATTERN_MAX_BUFFER_SIZE = 16;
constexpr uint32_t ARP_NOTE_NONE = 32767;
+enum class ArpType : uint8_t {
+ SYNTH,
+ DRUM,
+};
+
class ArpeggiatorSettings {
public:
ArpeggiatorSettings();
@@ -173,6 +178,7 @@ class ArpeggiatorBase {
}
virtual void noteOn(ArpeggiatorSettings* settings, int32_t noteCode, int32_t velocity,
ArpReturnInstruction* instruction, int32_t fromMIDIChannel, int16_t const* mpeValues) = 0;
+ virtual void noteOff(ArpeggiatorSettings* settings, int32_t noteCodePreArp, ArpReturnInstruction* instruction) = 0;
void render(ArpeggiatorSettings* settings, ArpReturnInstruction* instruction, int32_t numSamples,
uint32_t gateThreshold, uint32_t phaseIncrement);
int32_t doTickForward(ArpeggiatorSettings* settings, ArpReturnInstruction* instruction, uint32_t ClipCurrentPos,
@@ -180,6 +186,7 @@ class ArpeggiatorBase {
void calculateRandomizerAmounts(ArpeggiatorSettings* settings);
virtual bool hasAnyInputNotesActive() = 0;
virtual void reset() = 0;
+ virtual ArpType getArpType() = 0;
bool gateCurrentlyActive = false;
uint32_t gatePos = 0;
@@ -266,8 +273,9 @@ class ArpeggiatorForDrum final : public ArpeggiatorBase {
ArpeggiatorForDrum();
void noteOn(ArpeggiatorSettings* settings, int32_t noteCode, int32_t velocity, ArpReturnInstruction* instruction,
int32_t fromMIDIChannel, int16_t const* mpeValues) override;
- void noteOff(ArpeggiatorSettings* settings, int32_t noteCodePreArp, ArpReturnInstruction* instruction);
+ void noteOff(ArpeggiatorSettings* settings, int32_t noteCodePreArp, ArpReturnInstruction* instruction) override;
void reset() override;
+ ArpType getArpType() override { return ArpType::DRUM; }
ArpNote arpNote; // For the one note. noteCode will always be 60. velocity will be 0 if off.
int16_t noteForDrum;
@@ -281,10 +289,11 @@ class Arpeggiator final : public ArpeggiatorBase {
Arpeggiator();
void reset() override;
+ ArpType getArpType() override { return ArpType::SYNTH; }
void noteOn(ArpeggiatorSettings* settings, int32_t noteCode, int32_t velocity, ArpReturnInstruction* instruction,
int32_t fromMIDIChannel, int16_t const* mpeValues) override;
- void noteOff(ArpeggiatorSettings* settings, int32_t noteCodePreArp, ArpReturnInstruction* instruction);
+ void noteOff(ArpeggiatorSettings* settings, int32_t noteCodePreArp, ArpReturnInstruction* instruction) override;
bool hasAnyInputNotesActive() override;
// This array tracks the notes ordered by noteCode
diff --git a/src/deluge/processing/sound/sound.cpp b/src/deluge/processing/sound/sound.cpp
index 9944941ef8..548c069f4d 100644
--- a/src/deluge/processing/sound/sound.cpp
+++ b/src/deluge/processing/sound/sound.cpp
@@ -26,7 +26,7 @@
#include "hid/display/display.h"
#include "hid/led/indicator_leds.h"
#include "hid/matrix/matrix_driver.h"
-#include "io/debug/log.h"
+#include "io/midi/midi_engine.h"
#include "memory/general_memory_allocator.h"
#include "model/action/action.h"
#include "model/action/action_logger.h"
@@ -1286,6 +1286,23 @@ Error Sound::readTagFromFileOrError(Deserializer& reader, char const* tagName, P
patchedParams->readParam(reader, patchedParamsSummary, params::LOCAL_FOLD, readAutomationUpToPos);
reader.exitTag("waveFold");
}
+ else if (!strcmp(tagName, "midiOutput")) {
+ reader.match('{');
+ while (*(tagName = reader.readNextTagOrAttributeName())) {
+ if (!strcmp(tagName, "channel")) {
+ outputMidiChannel = reader.readTagOrAttributeValueInt();
+ reader.exitTag("channel");
+ }
+ else if (!strcmp(tagName, "noteForDrum")) {
+ outputMidiNoteForDrum = reader.readTagOrAttributeValueInt();
+ reader.exitTag("noteForDrum");
+ }
+ else {
+ reader.exitTag(tagName);
+ }
+ }
+ reader.exitTag("midiOutput", true);
+ }
else {
Error result =
@@ -1533,12 +1550,29 @@ void Sound::noteOn(ModelStackWithThreeMainThings* modelStack, ArpeggiatorBase* a
if (noNoteOn) {
// in the case of the arpeggiator not returning a note On (could happen if Note Probability evaluates to "don't
// play") we must at least evaluate the render-skipping if the arpeggiator is ON
- if (arpSettings != nullptr && getArp()->hasAnyInputNotesActive() && arpSettings->mode != ArpMode::OFF) {
+ if (arpSettings != nullptr && arpeggiator->hasAnyInputNotesActive() && arpSettings->mode != ArpMode::OFF) {
reassessRenderSkippingStatus(modelStackWithSoundFlags);
}
}
}
+void Sound::noteOff(ModelStackWithThreeMainThings* modelStack, ArpeggiatorBase* arpeggiator, int32_t noteCode) {
+ ModelStackWithSoundFlags* modelStackWithSoundFlags = modelStack->addSoundFlags();
+ ArpeggiatorSettings* arpSettings = getArpSettings();
+
+ ArpReturnInstruction instruction;
+ arpeggiator->noteOff(arpSettings, noteCode, &instruction);
+
+ for (int32_t n = 0; n < ARP_MAX_INSTRUCTION_NOTES; n++) {
+ if (instruction.noteCodeOffPostArp[n] == ARP_NOTE_NONE) {
+ break;
+ }
+ noteOffPostArpeggiator(modelStackWithSoundFlags, instruction.noteCodeOffPostArp[n]);
+ }
+
+ reassessRenderSkippingStatus(modelStackWithSoundFlags);
+}
+
void Sound::noteOnPostArpeggiator(ModelStackWithSoundFlags* modelStack, int32_t noteCodePreArp, int32_t noteCodePostArp,
int32_t velocity, int16_t const* mpeValues, uint32_t sampleSyncLength,
int32_t ticksLate, uint32_t samplesLate, int32_t fromMIDIChannel) {
@@ -1664,26 +1698,150 @@ void Sound::noteOnPostArpeggiator(ModelStackWithSoundFlags* modelStack, int32_t
}
lastNoteCode = noteCodePostArp; // Store for porta. We store that at both note-on and note-off.
-}
-void Sound::allNotesOff(ModelStackWithThreeMainThings* modelStack, ArpeggiatorBase* arpeggiator) {
- arpeggiator->reset();
+ // Send midi out for sound drums
+ if (outputMidiChannel != MIDI_CHANNEL_NONE) {
+ int32_t outputNoteCode = noteCodePostArp;
+ if (outputMidiNoteForDrum != MIDI_NOTE_NONE) {
+ int32_t noteCodeDiff = noteCodePostArp - kNoteForDrum;
+ outputNoteCode = outputMidiNoteForDrum + noteCodeDiff;
+ if (outputNoteCode < 0) {
+ outputNoteCode = 0;
+ }
+ else if (outputNoteCode > 127) {
+ outputNoteCode = 127;
+ }
+ }
+ midiEngine.sendNote(this, true, outputNoteCode, velocity, outputMidiChannel, 0);
-#if ALPHA_OR_BETA_VERSION
- if (!modelStack->paramManager) {
- // Previously we were allowed to receive a NULL paramManager, then would just crudely do an unassignAllVoices().
- // But I'm pretty sure this doesn't exist anymore?
- FREEZE_WITH_ERROR("E403");
+ // If the note doesn't have a tail (for ONCE samples for example and if ARP is OFF), we will never get a noteOff
+ // event to be called by the sequencer, so we need to "off" the note right now
+ if (!allowNoteTails(modelStack, true)) {
+ midiEngine.sendNote(this, false, outputNoteCode, kDefaultNoteOffVelocity, outputMidiChannel, 0);
+ }
}
-#endif
+}
+void Sound::polyphonicExpressionEventOnChannelOrNote(int32_t newValue, int32_t expressionDimension,
+ int32_t channelOrNoteNumber,
+ MIDICharacteristic whichCharacteristic) {
+ // Send midi if midi output enabled
+ if (outputMidiChannel == MIDI_CHANNEL_NONE) {
+ return;
+ }
+ // We only support mono or poly aftertouch at the moment (regular MIDI), not full MPE
+ if (expressionDimension != 2) {
+ return;
+ }
+ int32_t value7 = newValue >> 24;
+ if (whichCharacteristic == MIDICharacteristic::CHANNEL) {
+ // Channel aftertouch
+ midiEngine.sendChannelAftertouch(this, outputMidiChannel, value7, kMIDIOutputFilterNoMPE);
+ }
+ // whichCharacteristic == MIDICharacteristic::NOTE
+ else {
+ // Polyphonic aftertouch
+ if (getArp()->getArpType() == ArpType::DRUM) {
+ // This is a sound drum (kit)
+ ArpeggiatorForDrum* arpeggiator = (ArpeggiatorForDrum*)getArp();
+ // Just one note is possible
+ ArpNote arpNote = arpeggiator->arpNote;
+ for (int32_t n = 0; n < ARP_MAX_INSTRUCTION_NOTES; n++) {
+ if (arpNote.noteCodeOnPostArp[n] == ARP_NOTE_NONE) {
+ break;
+ }
+ midiEngine.sendPolyphonicAftertouch(this, outputMidiChannel, value7, arpNote.noteCodeOnPostArp[n],
+ kMIDIOutputFilterNoMPE);
+ }
+ }
+ else if (getArp()->getArpType() == ArpType::SYNTH) {
+ // This is a sound instrument (synth)
+ Arpeggiator* arpeggiator = (Arpeggiator*)getArp();
+ // Search for the note
+ int32_t i = arpeggiator->notes.search(channelOrNoteNumber, GREATER_OR_EQUAL);
+ if (i < arpeggiator->notes.getNumElements()) {
+ ArpNote* arpNote = (ArpNote*)arpeggiator->notes.getElementAddress(i);
+ for (int32_t n = 0; n < ARP_MAX_INSTRUCTION_NOTES; n++) {
+ if (arpNote->noteCodeOnPostArp[n] == ARP_NOTE_NONE) {
+ break;
+ }
+ midiEngine.sendPolyphonicAftertouch(this, outputMidiChannel, value7, arpNote->noteCodeOnPostArp[n],
+ kMIDIOutputFilterNoMPE);
+ }
+ }
+ }
+ }
+}
+
+void Sound::allNotesOff(ModelStackWithThreeMainThings* modelStack, ArpeggiatorBase* arpeggiator) {
ModelStackWithSoundFlags* modelStackWithSoundFlags = modelStack->addSoundFlags();
+ noteOffPostArpeggiator(modelStackWithSoundFlags, ALL_NOTES_OFF);
- noteOffPostArpeggiator(modelStackWithSoundFlags, -32768);
+ arpeggiator->reset();
}
-// noteCode = -32768 (default) means stop *any* voice, regardless of noteCode
+// noteCode = ALL_NOTES_OFF (default) means stop *any* voice, regardless of noteCode
void Sound::noteOffPostArpeggiator(ModelStackWithSoundFlags* modelStack, int32_t noteCode) {
+ ArpeggiatorSettings* arpSettings = getArpSettings();
+
+ // Send midi note offs out for specific notes,
+ // but only if the type of sound allows note tails (if not, note off was already sent right after its note on)
+ if (outputMidiChannel != MIDI_CHANNEL_NONE && allowNoteTails(modelStack, true)) {
+ if (noteCode == ALL_NOTES_OFF) {
+ // We must send note offs for all active notes
+ // so we will search for the current notes on postArp phase, if any
+ for (int32_t n = 0; n < ARP_MAX_INSTRUCTION_NOTES; n++) {
+ if (getArp()->noteCodeCurrentlyOnPostArp[n] == ARP_NOTE_NONE) {
+ break;
+ }
+ int32_t outputNoteCode = getArp()->noteCodeCurrentlyOnPostArp[n];
+ if (outputMidiNoteForDrum != MIDI_NOTE_NONE) {
+ // If note for drums is set then this is a SoundDrum and we must use the relative note code
+ // (relative to kNoteForDrum)
+ int32_t noteCodeDiff = outputNoteCode - kNoteForDrum;
+ outputNoteCode = outputMidiNoteForDrum + noteCodeDiff;
+ // Correct if out of bounds
+ if (outputNoteCode < 0) {
+ outputNoteCode = 0;
+ }
+ else if (outputNoteCode > 127) {
+ outputNoteCode = 127;
+ }
+ }
+ midiEngine.sendNote(this, false, outputNoteCode, kDefaultNoteOffVelocity, outputMidiChannel, 0);
+
+ // The "voice" related code below will switch off the voice anyway, so it is safe to clean this flag so
+ // we don't send two note offs if a normal noteOff or playback stop is received later
+ getArp()->noteCodeCurrentlyOnPostArp[n] = ARP_NOTE_NONE;
+ }
+ }
+ else {
+ // We have an specific note code, so we'll directly use that.
+ // This method has been called from the arp's noteOff so the handling of "noteCodeCurrentlyOnPostArp" has
+ // already been done there
+ int32_t outputNoteCode = noteCode;
+ if (outputMidiNoteForDrum != MIDI_NOTE_NONE) {
+ // If note for drums is set then this is a SoundDrum and we must use the relative note code
+ // (relative to kNoteForDrum)
+ int32_t noteCodeDiff = outputNoteCode - kNoteForDrum;
+ outputNoteCode = outputMidiNoteForDrum + noteCodeDiff;
+ // Correct if out of bounds
+ if (outputNoteCode < 0) {
+ outputNoteCode = 0;
+ }
+ else if (outputNoteCode > 127) {
+ outputNoteCode = 127;
+ }
+ }
+ midiEngine.sendNote(this, false, outputNoteCode, kDefaultNoteOffVelocity, outputMidiChannel, 0);
+ }
+ }
+ if (outputMidiChannel != MIDI_CHANNEL_NONE && noteCode == ALL_NOTES_OFF) {
+ // Besides all the previous specific note offs already sent, send also this special MIDI message,
+ // just in case some other note is still playing and we didn't have track of it
+ midiEngine.sendAllNotesOff(this, outputMidiChannel, kMIDIOutputFilterNoMPE);
+ }
+
if (!numVoicesAssigned) {
return;
}
@@ -1695,8 +1853,6 @@ void Sound::noteOffPostArpeggiator(ModelStackWithSoundFlags* modelStack, int32_t
if ((thisVoice->noteCodeAfterArpeggiation == noteCode || noteCode == ALL_NOTES_OFF)
&& thisVoice->envelopes[0].state < EnvelopeStage::RELEASE) { // Don't bother if it's already "releasing"
- ArpeggiatorSettings* arpSettings = getArpSettings();
-
ModelStackWithVoice* modelStackWithVoice = modelStack->addVoice(thisVoice);
// If we have actual arpeggiation, just switch off.
@@ -4137,6 +4293,12 @@ void Sound::writeToFile(Serializer& writer, bool savingSong, ParamManager* param
}
writer.writeArrayEnding("modKnobs");
+ // Output MIDI note for Drums
+ writer.writeOpeningTagBeginning("midiOutput");
+ writer.writeAttribute("channel", outputMidiChannel);
+ writer.writeAttribute("noteForDrum", outputMidiNoteForDrum);
+ writer.closeTag();
+
ModControllableAudio::writeTagsToFile(writer);
}
diff --git a/src/deluge/processing/sound/sound.h b/src/deluge/processing/sound/sound.h
index 01ec464ffb..4bdd5bba7c 100644
--- a/src/deluge/processing/sound/sound.h
+++ b/src/deluge/processing/sound/sound.h
@@ -116,6 +116,10 @@ class Sound : public ModControllableAudio {
int8_t unisonDetune;
uint8_t unisonStereoSpread;
+ // For sending MIDI notes for SoundDrums
+ uint8_t outputMidiChannel{MIDI_CHANNEL_NONE};
+ uint8_t outputMidiNoteForDrum{MIDI_NOTE_NONE};
+
int16_t modulatorTranspose[kNumModulators];
int8_t modulatorCents[kNumModulators];
@@ -131,6 +135,8 @@ class Sound : public ModControllableAudio {
int32_t lastNoteCode;
+ // int32_t lastMidiNoteOffSent;
+
bool oscillatorSync;
VoicePriority voicePriority;
@@ -205,6 +211,7 @@ class Sound : public ModControllableAudio {
void noteOn(ModelStackWithThreeMainThings* modelStack, ArpeggiatorBase* arpeggiator, int32_t noteCode,
int16_t const* mpeValues, uint32_t sampleSyncLength = 0, int32_t ticksLate = 0,
uint32_t samplesLate = 0, int32_t velocity = 64, int32_t fromMIDIChannel = 16);
+ void noteOff(ModelStackWithThreeMainThings* modelStack, ArpeggiatorBase* arpeggiator, int32_t noteCode);
void allNotesOff(ModelStackWithThreeMainThings* modelStack, ArpeggiatorBase* arpeggiator);
void noteOffPostArpeggiator(ModelStackWithSoundFlags* modelStack, int32_t noteCode = -32768);
@@ -212,6 +219,9 @@ class Sound : public ModControllableAudio {
int32_t newNoteCodeAfterArpeggiation, int32_t velocity, int16_t const* mpeValues,
uint32_t sampleSyncLength, int32_t ticksLate, uint32_t samplesLate,
int32_t fromMIDIChannel = 16);
+ void polyphonicExpressionEventOnChannelOrNote(int32_t newValue, int32_t expressionDimension,
+ int32_t channelOrNoteNumber,
+ MIDICharacteristic whichCharacteristic) override;
int16_t getMaxOscTranspose(InstrumentClip* clip);
int16_t getMinOscTranspose();
diff --git a/src/deluge/processing/sound/sound_drum.cpp b/src/deluge/processing/sound/sound_drum.cpp
index d048cc07f5..3ef4a0b654 100644
--- a/src/deluge/processing/sound/sound_drum.cpp
+++ b/src/deluge/processing/sound/sound_drum.cpp
@@ -102,7 +102,7 @@ void SoundDrum::noteOn(ModelStackWithThreeMainThings* modelStack, uint8_t veloci
fromMIDIChannel);
}
void SoundDrum::noteOff(ModelStackWithThreeMainThings* modelStack, int32_t velocity) {
- Sound::allNotesOff(modelStack, &arpeggiator);
+ Sound::noteOff(modelStack, &arpeggiator, kNoteForDrum);
}
extern bool expressionValueChangesMustBeDoneSmoothly;
@@ -137,6 +137,11 @@ void SoundDrum::polyphonicExpressionEventOnChannelOrNote(int32_t newValue, int32
// Because this is a Drum, we disregard the noteCode (which is what channelOrNoteNumber always is in our case - but
// yeah, that's all irrelevant.
expressionEvent(newValue, expressionDimension);
+
+ // Let the Sound know about this polyphonic expression event
+ // The Sound class will use it to send MIDI out (if enabled in the sound config)
+ Sound::polyphonicExpressionEventOnChannelOrNote(newValue, expressionDimension, channelOrNoteNumber,
+ whichCharacteristic);
}
void SoundDrum::unassignAllVoices() {
diff --git a/src/deluge/processing/sound/sound_instrument.cpp b/src/deluge/processing/sound/sound_instrument.cpp
index aa54508574..508a7acf0c 100644
--- a/src/deluge/processing/sound/sound_instrument.cpp
+++ b/src/deluge/processing/sound/sound_instrument.cpp
@@ -369,6 +369,11 @@ void SoundInstrument::polyphonicExpressionEventOnChannelOrNote(int32_t newValue,
arpNote->mpeValues[expressionDimension] = newValue >> 16;
}
}
+
+ // Let the Sound know about this polyphonic expression event
+ // The Sound class will use it to send MIDI out (if enabled in the sound config)
+ Sound::polyphonicExpressionEventOnChannelOrNote(newValue, expressionDimension, channelOrNoteNumber,
+ whichCharacteristic);
}
void SoundInstrument::sendNote(ModelStackWithThreeMainThings* modelStack, bool isOn, int32_t noteCode,
@@ -384,29 +389,7 @@ void SoundInstrument::sendNote(ModelStackWithThreeMainThings* modelStack, bool i
fromMIDIChannel);
}
else {
- ArpeggiatorSettings* arpSettings = getArpSettings();
-
- ArpReturnInstruction instruction;
-
- arpeggiator.noteOff(arpSettings, noteCode, &instruction);
-
- for (int32_t n = 0; n < ARP_MAX_INSTRUCTION_NOTES; n++) {
- if (instruction.noteCodeOffPostArp[n] == ARP_NOTE_NONE) {
- break;
- }
-#if ALPHA_OR_BETA_VERSION
- if (!modelStack->paramManager) {
- // Previously we were allowed to receive a NULL paramManager, then would just crudely do an
- // unassignAllVoices(). But I'm pretty sure this doesn't exist anymore?
- FREEZE_WITH_ERROR("E402");
- }
-#endif
- ModelStackWithSoundFlags* modelStackWithSoundFlags = modelStack->addSoundFlags();
-
- noteOffPostArpeggiator(modelStackWithSoundFlags, instruction.noteCodeOffPostArp[n]);
-
- reassessRenderSkippingStatus(modelStackWithSoundFlags);
- }
+ noteOff(modelStack, &arpeggiator, noteCode);
}
}
diff --git a/tests/unit/scheduler_tests.cpp b/tests/unit/scheduler_tests.cpp
index 26b626bd2b..d7d5d5f345 100644
--- a/tests/unit/scheduler_tests.cpp
+++ b/tests/unit/scheduler_tests.cpp
@@ -83,8 +83,8 @@ TEST(Scheduler, schedule) {
TEST(Scheduler, remove) {
static SelfRemoving selfRemoving;
- TaskID id =
- addRepeatingTask([]() { selfRemoving.runFiveTimes(); }, 0, 0.001, 0.001, 0.001, "run five times", RESOURCE_NONE);
+ TaskID id = addRepeatingTask([]() { selfRemoving.runFiveTimes(); }, 0, 0.001, 0.001, 0.001, "run five times",
+ RESOURCE_NONE);
selfRemoving.id = id;
mock().clear();
// will be called one less time due to the time the sleep takes not being zero