Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-Studio-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2021 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 :
23 : #include "notationmidiinput.h"
24 :
25 : #include <QGuiApplication>
26 :
27 : #include "containers.h"
28 : #include "defer.h"
29 :
30 : #include "midi/miditypes.h"
31 :
32 : #include "engraving/dom/factory.h"
33 : #include "engraving/dom/note.h"
34 : #include "engraving/dom/score.h"
35 : #include "engraving/dom/segment.h"
36 : #include "engraving/editing/noteinput.h"
37 : #include "engraving/editing/transaction/transaction.h"
38 :
39 : #include "notation/inotationnoteinput.h"
40 : #include "notation/inotationselection.h" // IWYU pragma: keep
41 :
42 : #include "playback/playbackcommands.h"
43 : #include "notationscene/notationcommands.h"
44 :
45 : #include "log.h"
46 :
47 : using namespace mu::notation;
48 : using namespace mu::engraving;
49 :
50 : static constexpr int PROCESS_INTERVAL = 20;
51 :
52 0 : static mu::playback::IPlaybackController::PlayParams makeNoteOnParams(bool infiniteDuration)
53 : {
54 0 : mu::playback::IPlaybackController::PlayParams params;
55 0 : params.flushSound = !infiniteDuration;
56 :
57 0 : if (infiniteDuration) {
58 0 : params.duration = muse::mpe::INFINITE_DURATION; // play note on only
59 : }
60 :
61 0 : return params;
62 : }
63 :
64 0 : static mu::playback::IPlaybackController::PlayParams makeNoteOffParams()
65 : {
66 0 : mu::playback::IPlaybackController::PlayParams params;
67 0 : params.flushSound = false;
68 0 : params.duration = 0; // note off only
69 :
70 0 : return params;
71 : }
72 :
73 0 : NotationMidiInput::NotationMidiInput(IGetScore* getScore, INotationInteractionPtr notationInteraction,
74 0 : INotationUndoStackPtr undoStack, const muse::modularity::ContextPtr& iocCtx)
75 0 : : muse::Contextable(iocCtx), m_getScore(getScore),
76 0 : m_notationInteraction(notationInteraction), m_undoStack(undoStack)
77 : {
78 0 : QObject::connect(&m_processTimer, &QTimer::timeout, [this]() { doProcessEvents(); });
79 :
80 0 : m_realtimeTimer.setTimerType(Qt::PreciseTimer);
81 0 : QObject::connect(&m_realtimeTimer, &QTimer::timeout, [this]() { doRealtimeAdvance(); });
82 :
83 0 : m_extendNoteTimer.setTimerType(Qt::PreciseTimer);
84 0 : m_extendNoteTimer.setSingleShot(true);
85 0 : QObject::connect(&m_extendNoteTimer, &QTimer::timeout, [this]() { doExtendCurrentNote(); });
86 0 : }
87 :
88 0 : void NotationMidiInput::onMidiEventReceived(const muse::midi::Event& event)
89 : {
90 0 : if (event.isChannelVoice20()) {
91 0 : auto events = event.toMIDI10();
92 0 : for (auto& midi10event : events) {
93 0 : onMidiEventReceived(midi10event);
94 : }
95 :
96 0 : return;
97 0 : }
98 :
99 : const static std::unordered_set<muse::midi::Event::Opcode> ACCEPTED_OPCODES {
100 : muse::midi::Event::Opcode::NoteOn,
101 : muse::midi::Event::Opcode::NoteOff,
102 : muse::midi::Event::Opcode::ControlChange,
103 : muse::midi::Event::Opcode::PitchBend,
104 0 : };
105 :
106 0 : if (muse::contains(ACCEPTED_OPCODES, event.opcode())) {
107 0 : m_eventsQueue.push_back(event);
108 :
109 0 : if (!m_processTimer.isActive()) {
110 0 : m_processTimer.start(PROCESS_INTERVAL);
111 : }
112 : }
113 : }
114 :
115 0 : muse::async::Channel<std::vector<const Note*> > NotationMidiInput::notesReceived() const
116 : {
117 0 : return m_notesReceivedChannel;
118 : }
119 :
120 0 : void NotationMidiInput::onRealtimeAdvance()
121 : {
122 0 : if (!isNoteInputMode()) {
123 0 : return;
124 : }
125 :
126 0 : if (isRealtimeManual()) {
127 0 : m_allowRealtimeRests = true;
128 0 : enableMetronome();
129 0 : doRealtimeAdvance();
130 0 : } else if (isRealtimeAuto()) {
131 0 : if (m_realtimeTimer.isActive()) {
132 0 : stopRealtime();
133 0 : disableMetronome();
134 : } else {
135 0 : m_allowRealtimeRests = true;
136 0 : enableMetronome();
137 0 : runRealtime();
138 : }
139 : }
140 : }
141 :
142 0 : Score* NotationMidiInput::score() const
143 : {
144 0 : IF_ASSERT_FAILED(m_getScore) {
145 0 : return nullptr;
146 : }
147 :
148 0 : return m_getScore->score();
149 : }
150 :
151 0 : void NotationMidiInput::doProcessEvents()
152 : {
153 0 : DEFER {
154 0 : m_eventsQueue.clear();
155 0 : m_processTimer.stop();
156 0 : };
157 :
158 0 : if (m_eventsQueue.empty()) {
159 0 : return;
160 : }
161 :
162 0 : const Score* sc = score();
163 0 : if (!sc || sc->noStaves()) {
164 0 : return;
165 : }
166 :
167 0 : std::vector<const Note*> notesOn;
168 0 : std::vector<int> notesOff;
169 0 : ControllerEventMap controllers;
170 :
171 0 : startNoteInputIfNeed();
172 0 : bool isNoteInput = isNoteInputMode();
173 0 : bool isSoundPreview = !isNoteInput;
174 :
175 0 : if (isNoteInput && isInputByDuration()) {
176 0 : addNoteEventsToInputState();
177 0 : return;
178 : }
179 :
180 0 : const bool useDurationAndVelocity = isSoundPreview || configuration()->useMidiVelocityAndDurationDuringNoteInput();
181 :
182 0 : for (size_t i = 0; i < m_eventsQueue.size(); ++i) {
183 0 : const muse::midi::Event& event = m_eventsQueue.at(i);
184 0 : const muse::midi::Event::Opcode opcode = event.opcode();
185 :
186 0 : if (opcode == muse::midi::Event::Opcode::ControlChange || opcode == muse::midi::Event::Opcode::PitchBend) {
187 0 : controllers[opcode] = event; // keep only last received to prevent spam
188 0 : continue;
189 : }
190 :
191 0 : Note* note = isNoteInput ? addNoteToScore(event) : makePreviewNote(event);
192 0 : if (note) {
193 0 : if (useDurationAndVelocity) {
194 0 : note->setUserVelocity(event.velocity7());
195 0 : m_playingNotes[note->pitch()] = { isSoundPreview, note };
196 : }
197 0 : notesOn.push_back(note);
198 : }
199 :
200 0 : const bool chord = i != 0;
201 0 : const bool noteOn = opcode == muse::midi::Event::Opcode::NoteOn;
202 0 : if (!chord && noteOn && !m_realtimeTimer.isActive() && isRealtimeAuto()) {
203 0 : m_extendNoteTimer.start(configuration()->delayBetweenNotesInRealTimeModeMilliseconds());
204 0 : enableMetronome();
205 0 : doRealtimeAdvance();
206 : }
207 :
208 0 : const bool noteOff = opcode == muse::midi::Event::Opcode::NoteOff || event.velocity7() == 0;
209 0 : if (useDurationAndVelocity && noteOff) {
210 0 : notesOff.push_back(event.note());
211 : }
212 : }
213 :
214 0 : if (!controllers.empty()) {
215 0 : triggerControllers(controllers);
216 : }
217 :
218 0 : if (!notesOn.empty()) {
219 0 : if (isNoteInput) {
220 0 : playbackController()->seekElement(notesOn.front(), !useDurationAndVelocity /*flushSound*/);
221 : }
222 :
223 0 : const std::vector<const EngravingItem*> elements(notesOn.begin(), notesOn.end());
224 0 : playbackController()->playElements(elements, makeNoteOnParams(useDurationAndVelocity), true);
225 0 : m_notesReceivedChannel.send(notesOn);
226 0 : }
227 :
228 0 : if (!notesOff.empty()) {
229 0 : releasePlayingNotes(notesOff);
230 : }
231 0 : }
232 :
233 0 : void NotationMidiInput::startNoteInputIfNeed()
234 : {
235 0 : if (isNoteInputMode()) {
236 0 : return;
237 : }
238 :
239 0 : const auto containsNoteOn = [this]() -> bool {
240 0 : return std::any_of(m_eventsQueue.begin(), m_eventsQueue.end(), [](const muse::midi::Event& e) {
241 0 : return e.opcode() == muse::midi::Event::Opcode::NoteOn;
242 0 : });
243 0 : };
244 :
245 0 : if (configuration()->startNoteInputAtSelectedNoteRestWhenPressingMidiKey()) {
246 0 : if (m_notationInteraction->selection()->elementsSelected(NOTE_REST_TYPES) && containsNoteOn()) {
247 0 : commandsDispatcher()->dispatch(TOGGLE_NOTE_INPUT_COMMAND);
248 : }
249 : }
250 : }
251 :
252 0 : void NotationMidiInput::addNoteEventsToInputState()
253 : {
254 0 : INotationNoteInputPtr noteInput = m_notationInteraction->noteInput();
255 0 : const NoteInputState& state = noteInput->state();
256 0 : const staff_idx_t staffIdx = state.staffIdx();
257 0 : const bool useWrittenPitch = configuration()->midiUseWrittenPitch().val;
258 0 : const bool playPreviewNotes = configuration()->isPlayPreviewNotesInInputByDuration();
259 0 : const bool useVelocityAndDuration = playPreviewNotes && configuration()->useMidiVelocityAndDurationDuringNoteInput();
260 :
261 0 : NoteValList notesOn;
262 0 : NoteValList notesOff;
263 :
264 0 : if (m_holdingNotesInInputByDuration) {
265 0 : notesOn = state.notes();
266 : }
267 :
268 0 : ControllerEventMap controllers;
269 :
270 0 : for (const muse::midi::Event& event : m_eventsQueue) {
271 0 : const muse::midi::Event::Opcode opcode = event.opcode();
272 :
273 0 : if (opcode == muse::midi::Event::Opcode::NoteOn) {
274 0 : NoteVal nval = NoteInput::noteVal(score(), event.note(), staffIdx, useWrittenPitch);
275 0 : nval.velocityOverride = event.velocity7();
276 0 : notesOn.push_back(nval);
277 0 : m_holdingNotesInInputByDuration = true;
278 0 : } else if (opcode == muse::midi::Event::Opcode::NoteOff) {
279 0 : if (useVelocityAndDuration) {
280 0 : notesOff.push_back(NoteInput::noteVal(score(), event.note(), staffIdx, useWrittenPitch));
281 : }
282 0 : m_holdingNotesInInputByDuration = false;
283 0 : } else if (opcode == muse::midi::Event::Opcode::ControlChange || opcode == muse::midi::Event::Opcode::PitchBend) {
284 0 : if (playPreviewNotes) {
285 0 : controllers[opcode] = event; // keep only last received to prevent spam
286 : }
287 : }
288 : }
289 :
290 0 : if (!controllers.empty()) {
291 0 : triggerControllers(controllers);
292 : }
293 :
294 0 : if (!notesOff.empty()) {
295 0 : playbackController()->playNotes(notesOff, staffIdx, state.segment(), makeNoteOffParams());
296 : }
297 :
298 0 : if (!notesOn.empty() && notesOn != state.notes()) {
299 0 : noteInput->setRestMode(false);
300 0 : noteInput->setInputNotes(notesOn);
301 :
302 0 : if (playPreviewNotes) {
303 0 : playbackController()->playNotes(notesOn, staffIdx, state.segment(), makeNoteOnParams(useVelocityAndDuration));
304 : }
305 : }
306 0 : }
307 :
308 0 : Note* NotationMidiInput::addNoteToScore(const muse::midi::Event& e)
309 : {
310 0 : Score* sc = score();
311 :
312 0 : const int pitch = e.note();
313 :
314 0 : muse::remove(m_activeMidiPitches, pitch);
315 :
316 0 : const InputState& is = sc->inputState();
317 0 : if (!is.noteEntryMode()) {
318 0 : return nullptr;
319 : }
320 :
321 0 : if (!is.isValid()) {
322 0 : return nullptr;
323 : }
324 :
325 0 : DEFER {
326 0 : m_undoStack->commitChanges();
327 0 : };
328 :
329 0 : m_undoStack->prepareChanges(muse::TranslatableString("undoableAction", "Enter note"));
330 :
331 0 : if (e.opcode() == muse::midi::Event::Opcode::NoteOff) {
332 0 : if (isRealtime()) {
333 0 : if (!is.cr()) {
334 0 : return nullptr;
335 : }
336 :
337 0 : const Chord* chord = is.cr()->isChord() ? engraving::toChord(is.cr()) : nullptr;
338 0 : if (chord) {
339 0 : Note* n = chord->findNote(pitch);
340 0 : if (n) {
341 0 : sc->deleteItem(n->tieBack());
342 0 : sc->deleteItem(n);
343 : }
344 : }
345 : }
346 :
347 0 : return nullptr;
348 : }
349 :
350 0 : bool addToChord = !m_activeMidiPitches.empty();
351 :
352 : // holding shift while inputting midi will add the new pitch to the prior existing chord
353 0 : if (QGuiApplication::queryKeyboardModifiers() & Qt::ShiftModifier) {
354 0 : EngravingItem* cr = is.lastSegment()->element(is.track());
355 0 : if (cr && cr->isChord()) {
356 0 : addToChord = true;
357 : }
358 : }
359 :
360 0 : Note* note = NoteInput::addMidiPitch(sc->transactionManager()->currentOrDummyTransaction(), sc, pitch,
361 0 : addToChord, configuration()->midiUseWrittenPitch().val);
362 :
363 0 : m_activeMidiPitches.push_back(pitch);
364 :
365 0 : if (is.cr()) {
366 0 : m_notationInteraction->showItem(is.cr());
367 : }
368 :
369 0 : return note;
370 0 : }
371 :
372 0 : Note* NotationMidiInput::makePreviewNote(const muse::midi::Event& e)
373 : {
374 0 : if (e.opcode() == muse::midi::Event::Opcode::NoteOff || e.velocity7() == 0) {
375 0 : return nullptr;
376 : }
377 :
378 0 : Score* score = this->score();
379 0 : const InputState& inputState = score->inputState();
380 0 : const engraving::DummyParentOr<Segment> seg = engraving::parentOrDummy(inputState.lastSegment(), score->dummy());
381 :
382 0 : Chord* chord = engraving::Factory::createChord(seg);
383 :
384 0 : const ChordRest* cr = inputState.cr();
385 0 : const staff_idx_t staffIdx = cr ? engraving::track2staff(cr->track()) : 0;
386 :
387 0 : Note* note = engraving::Factory::createNote(chord);
388 0 : note->setStaffIdx(staffIdx);
389 :
390 0 : engraving::NoteVal nval = NoteInput::noteVal(score, e.note(), staffIdx, configuration()->midiUseWrittenPitch().val);
391 0 : note->setNval(nval);
392 :
393 0 : return note;
394 : }
395 :
396 0 : void NotationMidiInput::triggerControllers(const ControllerEventMap& events)
397 : {
398 0 : muse::mpe::ControllerChangeEventList controllers;
399 :
400 : static const std::unordered_map<int, muse::mpe::ControllerChangeEvent::Type> MIDI_CC_TO_EVENT_TYPE {
401 : { muse::midi::MODWHEEL_CONTROLLER, muse::mpe::ControllerChangeEvent::Modulation },
402 : { muse::midi::SUSTAIN_PEDAL_CONTROLLER, muse::mpe::ControllerChangeEvent::SustainPedalOnOff },
403 0 : };
404 :
405 0 : const InputState& is = score()->inputState();
406 :
407 0 : for (const auto& pair : events) {
408 0 : const muse::midi::Event& e = pair.second;
409 0 : muse::mpe::ControllerChangeEvent cc;
410 :
411 0 : if (pair.first == muse::midi::Event::Opcode::PitchBend) {
412 0 : cc.type = muse::mpe::ControllerChangeEvent::PitchBend;
413 0 : cc.val = static_cast<float>(e.pitchBend14()) / 16383.f;
414 : } else {
415 0 : cc.type = muse::value(MIDI_CC_TO_EVENT_TYPE, e.index(), muse::mpe::ControllerChangeEvent::Undefined);
416 0 : cc.val = static_cast<float>(e.data()) / 127.f;
417 : }
418 :
419 0 : if (cc.type != muse::mpe::ControllerChangeEvent::Undefined) {
420 0 : cc.layerIdx = static_cast<muse::mpe::layer_idx_t>(is.track());
421 0 : controllers.push_back(cc);
422 : }
423 : }
424 :
425 0 : playbackController()->triggerControllers(controllers, is.staffIdx(), is.tick().ticks());
426 0 : }
427 :
428 0 : void NotationMidiInput::releasePlayingNotes(const std::vector<int>& pitches)
429 : {
430 0 : std::vector<const EngravingItem*> notesOff;
431 0 : std::vector<Note*> notesToDelete;
432 :
433 0 : const staff_idx_t staffIdx = score()->inputState().staffIdx();
434 0 : const bool useWrittenPitch = configuration()->midiUseWrittenPitch().val;
435 :
436 0 : for (int pitch : pitches) {
437 0 : const NoteVal nval = NoteInput::noteVal(score(), pitch, staffIdx, useWrittenPitch);
438 :
439 0 : auto it = m_playingNotes.find(nval.pitch);
440 0 : if (it == m_playingNotes.end()) {
441 0 : continue;
442 : }
443 :
444 0 : Note* note = it->second.note;
445 0 : note->setUserVelocity(0);
446 :
447 0 : notesOff.push_back(note);
448 :
449 0 : if (it->second.isPreview) {
450 0 : notesToDelete.push_back(note);
451 : }
452 :
453 0 : m_playingNotes.erase(it);
454 : }
455 :
456 0 : playbackController()->playElements(notesOff, makeNoteOffParams(), true /*isMidi*/);
457 0 : muse::DeleteAll(notesToDelete);
458 0 : }
459 :
460 0 : void NotationMidiInput::enableMetronome()
461 : {
462 0 : bool metronomeEnabled = configuration()->isMetronomeEnabled();
463 0 : if (metronomeEnabled) {
464 0 : return;
465 : }
466 :
467 0 : commandsDispatcher()->dispatch(mu::playback::METRONOME_TOGGLE_COMMAND);
468 0 : m_shouldDisableMetronome = true;
469 : }
470 :
471 0 : void NotationMidiInput::disableMetronome()
472 : {
473 0 : if (!m_shouldDisableMetronome) {
474 0 : return;
475 : }
476 :
477 0 : commandsDispatcher()->dispatch(mu::playback::METRONOME_TOGGLE_COMMAND);
478 :
479 0 : m_shouldDisableMetronome = false;
480 : }
481 :
482 0 : void NotationMidiInput::runRealtime()
483 : {
484 0 : m_realtimeTimer.start(configuration()->delayBetweenNotesInRealTimeModeMilliseconds());
485 0 : }
486 :
487 0 : void NotationMidiInput::stopRealtime()
488 : {
489 0 : m_realtimeTimer.stop();
490 0 : }
491 :
492 0 : void NotationMidiInput::doRealtimeAdvance()
493 : {
494 0 : if (!isRealtime() || !isNoteInputMode() || (!m_allowRealtimeRests && m_activeMidiPitches.empty())) {
495 0 : if (m_realtimeTimer.isActive()) {
496 0 : stopRealtime();
497 : }
498 :
499 0 : disableMetronome();
500 :
501 0 : m_allowRealtimeRests = true;
502 0 : return;
503 : }
504 :
505 0 : const InputState& is = m_getScore->score()->inputState();
506 0 : playbackController()->playMetronome(is.tick().ticks());
507 :
508 0 : QTimer::singleShot(100, Qt::PreciseTimer, [this]() {
509 0 : m_undoStack->transaction(muse::TranslatableString("undoableAction", "Realtime advance"), [this](Transaction& tx) {
510 0 : NoteInput::realtimeAdvance(tx, score(), configuration()->midiUseWrittenPitch().val, m_activeMidiPitches);
511 0 : });
512 0 : });
513 :
514 0 : if (isRealtimeManual()) {
515 0 : int metronomeDuration = 500;
516 0 : QTimer::singleShot(metronomeDuration, Qt::PreciseTimer, [this]() {
517 0 : disableMetronome();
518 0 : });
519 : }
520 : }
521 :
522 0 : void NotationMidiInput::doExtendCurrentNote()
523 : {
524 0 : if (!isNoteInputMode() || m_realtimeTimer.isActive()) {
525 0 : return;
526 : }
527 :
528 0 : m_allowRealtimeRests = false;
529 0 : runRealtime();
530 0 : doRealtimeAdvance();
531 : }
532 :
533 0 : NoteInputMethod NotationMidiInput::noteInputMethod() const
534 : {
535 0 : return m_notationInteraction->noteInput()->state().noteEntryMethod();
536 : }
537 :
538 0 : bool NotationMidiInput::isRealtime() const
539 : {
540 0 : return isRealtimeAuto() || isRealtimeManual();
541 : }
542 :
543 0 : bool NotationMidiInput::isRealtimeAuto() const
544 : {
545 0 : return noteInputMethod() == NoteInputMethod::REALTIME_AUTO;
546 : }
547 :
548 0 : bool NotationMidiInput::isRealtimeManual() const
549 : {
550 0 : return noteInputMethod() == NoteInputMethod::REALTIME_MANUAL;
551 : }
552 :
553 0 : bool NotationMidiInput::isInputByDuration() const
554 : {
555 0 : return noteInputMethod() == NoteInputMethod::BY_DURATION;
556 : }
557 :
558 0 : bool NotationMidiInput::isNoteInputMode() const
559 : {
560 0 : return m_notationInteraction->noteInput()->isNoteInputMode();
561 : }
|