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