Add Strum Tool to Piano Roll (#7725)
Adds a complex strum tool to the Piano Roll, allowing the user to take a selection of chords, and drag around the notes to shape the strum exactly how they want it. --------- Co-authored-by: szeli1 <143485814+szeli1@users.noreply.github.com> Co-authored-by: Sotonye Atemie <satemiej@gmail.com>
This commit is contained in:
@@ -112,7 +112,8 @@ public:
|
||||
Erase,
|
||||
Select,
|
||||
Detuning,
|
||||
Knife
|
||||
Knife,
|
||||
Strum
|
||||
};
|
||||
|
||||
/*! \brief Resets settings to default when e.g. creating a new project */
|
||||
@@ -268,7 +269,8 @@ private:
|
||||
SelectNotes,
|
||||
ChangeNoteProperty,
|
||||
ResizeNoteEditArea,
|
||||
Knife
|
||||
Knife,
|
||||
Strum
|
||||
};
|
||||
|
||||
enum class NoteEditMode
|
||||
@@ -324,6 +326,9 @@ private:
|
||||
void setKnifeAction();
|
||||
void cancelKnifeAction();
|
||||
|
||||
void setStrumAction();
|
||||
void cancelStrumAction();
|
||||
|
||||
void updateScrollbars();
|
||||
void updatePositionLineHeight();
|
||||
|
||||
@@ -347,6 +352,7 @@ private:
|
||||
QPixmap m_toolMove = embed::getIconPixmap("edit_move");
|
||||
QPixmap m_toolOpen = embed::getIconPixmap("automation");
|
||||
QPixmap m_toolKnife = embed::getIconPixmap("edit_knife");
|
||||
QPixmap m_toolStrum = embed::getIconPixmap("arp_free");
|
||||
|
||||
static std::array<KeyType, 12> prKeyOrder;
|
||||
|
||||
@@ -437,6 +443,7 @@ private:
|
||||
EditMode m_editMode;
|
||||
EditMode m_ctrlMode; // mode they were in before they hit ctrl
|
||||
EditMode m_knifeMode; // mode they where in before entering knife mode
|
||||
EditMode m_strumMode; //< mode they where in before entering strum mode
|
||||
|
||||
bool m_mouseDownRight; //true if right click is being held down
|
||||
|
||||
@@ -465,6 +472,21 @@ private:
|
||||
|
||||
void updateKnifePos(QMouseEvent* me, bool initial);
|
||||
|
||||
//! Stores the chords for the strum tool
|
||||
std::vector<NoteVector> m_selectedChords;
|
||||
//! Computes which notes belong to which chords from the selection
|
||||
void setupSelectedChords();
|
||||
|
||||
TimePos m_strumStartTime;
|
||||
TimePos m_strumCurrentTime;
|
||||
int m_strumStartVertical = 0;
|
||||
int m_strumCurrentVertical = 0;
|
||||
float m_strumHeightRatio = 0.0f;
|
||||
bool m_strumEnabled = false;
|
||||
//! Handles updating all of the note positions when performing a strum
|
||||
void updateStrumPos(QMouseEvent* me, bool initial, bool warp);
|
||||
|
||||
|
||||
friend class PianoRollWindow;
|
||||
|
||||
StepRecorderWidget m_stepRecorderWidget;
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
#include "FontHelper.h"
|
||||
#include "InstrumentTrack.h"
|
||||
#include "KeyboardShortcuts.h"
|
||||
#include "lmms_math.h"
|
||||
#include "MainWindow.h"
|
||||
#include "MidiClip.h"
|
||||
#include "PatternStore.h"
|
||||
@@ -1396,7 +1397,7 @@ void PianoRoll::keyPressEvent(QKeyEvent* ke)
|
||||
}
|
||||
|
||||
case Qt::Key_A:
|
||||
if( ke->modifiers() & Qt::ControlModifier )
|
||||
if (ke->modifiers() & Qt::ControlModifier && m_editMode != EditMode::Strum && m_editMode != EditMode::Knife)
|
||||
{
|
||||
ke->accept();
|
||||
if (ke->modifiers() & Qt::ShiftModifier)
|
||||
@@ -1414,11 +1415,15 @@ void PianoRoll::keyPressEvent(QKeyEvent* ke)
|
||||
break;
|
||||
|
||||
case Qt::Key_Escape:
|
||||
// On the Knife mode, ESC cancels it
|
||||
// On the Knife mode or Strum mode, ESC cancels it
|
||||
if (m_editMode == EditMode::Knife)
|
||||
{
|
||||
cancelKnifeAction();
|
||||
}
|
||||
else if (m_editMode == EditMode::Strum)
|
||||
{
|
||||
cancelStrumAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Same as Ctrl + Shift + A
|
||||
@@ -1519,6 +1524,7 @@ void PianoRoll::keyReleaseEvent(QKeyEvent* ke )
|
||||
}
|
||||
computeSelectedNotes( ke->modifiers() & Qt::ShiftModifier);
|
||||
m_editMode = m_ctrlMode;
|
||||
if (m_editMode == EditMode::Strum) { setupSelectedChords(); }
|
||||
update();
|
||||
break;
|
||||
|
||||
@@ -1614,6 +1620,19 @@ void PianoRoll::mousePressEvent(QMouseEvent * me )
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_editMode == EditMode::Strum && me->button() == Qt::LeftButton)
|
||||
{
|
||||
// Only strum if the user is dragging a selected note
|
||||
const auto& selectedNotes = getSelectedNotes();
|
||||
if (std::find(selectedNotes.begin(), selectedNotes.end(), noteUnderMouse()) != selectedNotes.end())
|
||||
{
|
||||
updateStrumPos(me, true, me->modifiers() & Qt::ShiftModifier);
|
||||
m_strumEnabled = true;
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if( m_editMode == EditMode::Detuning && noteUnderMouse() )
|
||||
{
|
||||
static QPointer<AutomationClip> detuningClip = nullptr;
|
||||
@@ -2141,7 +2160,27 @@ void PianoRoll::cancelKnifeAction()
|
||||
update();
|
||||
}
|
||||
|
||||
void PianoRoll::setStrumAction()
|
||||
{
|
||||
if (m_editMode != EditMode::Strum)
|
||||
{
|
||||
m_strumMode = m_editMode;
|
||||
m_editMode = EditMode::Strum;
|
||||
m_action = Action::Strum;
|
||||
m_strumEnabled = false;
|
||||
setupSelectedChords();
|
||||
setCursor(Qt::ArrowCursor);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void PianoRoll::cancelStrumAction()
|
||||
{
|
||||
m_editMode = m_strumMode;
|
||||
m_action = Action::None;
|
||||
m_strumEnabled = false;
|
||||
update();
|
||||
}
|
||||
|
||||
void PianoRoll::testPlayKey( int key, int velocity, int pan )
|
||||
{
|
||||
@@ -2241,11 +2280,15 @@ void PianoRoll::mouseReleaseEvent( QMouseEvent * me )
|
||||
|
||||
s_textFloat->hide();
|
||||
|
||||
// Quit knife mode if we pressed and released the right mouse button
|
||||
// Quit knife mode or strum mode if we pressed and released the right mouse button
|
||||
if (m_editMode == EditMode::Knife && me->button() == Qt::RightButton)
|
||||
{
|
||||
cancelKnifeAction();
|
||||
}
|
||||
else if (m_editMode == EditMode::Strum && me->button() == Qt::RightButton)
|
||||
{
|
||||
cancelStrumAction();
|
||||
}
|
||||
|
||||
if( me->button() & Qt::LeftButton )
|
||||
{
|
||||
@@ -2266,6 +2309,10 @@ void PianoRoll::mouseReleaseEvent( QMouseEvent * me )
|
||||
m_midiClip->rearrangeAllNotes();
|
||||
|
||||
}
|
||||
else if (m_action == Action::Strum || m_strumEnabled)
|
||||
{
|
||||
m_strumEnabled = false;
|
||||
}
|
||||
else if (m_action == Action::Knife && hasValidMidiClip())
|
||||
{
|
||||
bool deleteShortEnds = me->modifiers() & Qt::ShiftModifier;
|
||||
@@ -2313,7 +2360,7 @@ void PianoRoll::mouseReleaseEvent( QMouseEvent * me )
|
||||
|
||||
m_currentNote = nullptr;
|
||||
|
||||
if (m_action != Action::Knife)
|
||||
if (m_action != Action::Knife && m_action != Action::Strum)
|
||||
{
|
||||
m_action = Action::None;
|
||||
}
|
||||
@@ -2379,6 +2426,12 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me )
|
||||
updateKnifePos(me, false);
|
||||
}
|
||||
|
||||
// Update Strum position if we are on knife mode
|
||||
if (m_editMode == EditMode::Strum && m_strumEnabled)
|
||||
{
|
||||
updateStrumPos(me, false, me->modifiers() & Qt::ShiftModifier);
|
||||
}
|
||||
|
||||
if( me->y() > PR_TOP_MARGIN || m_action != Action::None )
|
||||
{
|
||||
bool edit_note = ( me->y() > noteEditTop() )
|
||||
@@ -2661,7 +2714,7 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me )
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (me->buttons() == Qt::NoButton && m_editMode != EditMode::Draw && m_editMode != EditMode::Knife)
|
||||
else if (me->buttons() == Qt::NoButton && m_editMode != EditMode::Draw && m_editMode != EditMode::Knife && m_editMode != EditMode::Strum)
|
||||
{
|
||||
// Is needed to restore cursor when it previously was set to
|
||||
// Qt::SizeVerCursor (between keyAreaBottom and noteEditTop)
|
||||
@@ -2780,6 +2833,120 @@ void PianoRoll::updateKnifePos(QMouseEvent* me, bool initial)
|
||||
m_knifeEndKey = mouseKey;
|
||||
}
|
||||
|
||||
/*
|
||||
* Setup chords
|
||||
*
|
||||
* A chord is an island of notes--as the loop goes over the notes, if the notes overlap,
|
||||
* they are part of the same chord. Else, they are part of a new chord.
|
||||
*/
|
||||
void PianoRoll::setupSelectedChords()
|
||||
{
|
||||
if (!hasValidMidiClip()) { return; }
|
||||
m_selectedChords.clear();
|
||||
m_midiClip->rearrangeAllNotes();
|
||||
|
||||
const NoteVector& selectedNotes = getSelectedNotes();
|
||||
if (selectedNotes.empty()) { return; }
|
||||
|
||||
int maxTime = -1;
|
||||
NoteVector currentChord;
|
||||
for (Note* note: selectedNotes)
|
||||
{
|
||||
// If the note is not in the current chord range (and this isn't the first chord), start a new chord.
|
||||
if (note->pos() >= maxTime && maxTime != -1)
|
||||
{
|
||||
// Sort the notes by key before adding the chord to the vector
|
||||
std::sort(currentChord.begin(), currentChord.end(), [](Note* a, Note* b){ return a->key() < b->key(); });
|
||||
m_selectedChords.push_back(currentChord);
|
||||
currentChord.clear();
|
||||
maxTime = note->endPos();
|
||||
}
|
||||
maxTime = std::max(maxTime, static_cast<int>(note->endPos()));
|
||||
currentChord.push_back(note);
|
||||
}
|
||||
// Add final chord
|
||||
std::sort(currentChord.begin(), currentChord.end(), [](Note* a, Note* b){ return a->key() < b->key(); });
|
||||
m_selectedChords.push_back(currentChord);
|
||||
}
|
||||
|
||||
/*
|
||||
* Perform the Strum
|
||||
*
|
||||
* Notes above the clicked note (relative to each chord) will be strummed down, notes below will be strummed up.
|
||||
* Holding shift raises the amount of movement to a power, causing the strum to be curved/warped.
|
||||
*/
|
||||
void PianoRoll::updateStrumPos(QMouseEvent* me, bool initial, bool warp)
|
||||
{
|
||||
if (!hasValidMidiClip()) { return; }
|
||||
// Calculate the TimePos from the mouse
|
||||
int mouseViewportPos = me->x() - m_whiteKeyWidth;
|
||||
int mouseTickPos = mouseViewportPos * TimePos::ticksPerBar() / m_ppb + m_currentPosition;
|
||||
// Should we add quantization? probably not?
|
||||
if (initial)
|
||||
{
|
||||
m_strumStartTime = mouseTickPos;
|
||||
m_strumStartVertical = me->y();
|
||||
}
|
||||
m_strumCurrentTime = mouseTickPos;
|
||||
m_strumCurrentVertical = me->y();
|
||||
int strumTicksHorizontal = m_strumCurrentTime - m_strumStartTime;
|
||||
float strumPower = fastPow10f(0.01f * (m_strumCurrentVertical - m_strumStartVertical));
|
||||
|
||||
if (initial)
|
||||
{
|
||||
m_midiClip->addJournalCheckPoint();
|
||||
|
||||
Note* clickedNote = noteUnderMouse();
|
||||
if (clickedNote == nullptr) { return; }
|
||||
|
||||
for (NoteVector chord: m_selectedChords)
|
||||
{
|
||||
for (Note* note: chord)
|
||||
{
|
||||
// Save the current note position
|
||||
note->setOldPos(note->pos());
|
||||
// if this is the clicked note, calculate it's ratio up the chord
|
||||
if (note == clickedNote && chord.size() > 1)
|
||||
{
|
||||
m_strumHeightRatio = 1.f * std::distance(chord.begin(), std::find(chord.begin(), chord.end(), clickedNote)) / (chord.size() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (NoteVector chord: m_selectedChords)
|
||||
{
|
||||
// Don't strum a chord with only one note
|
||||
if (chord.size() <= 1) { continue; }
|
||||
for (size_t i = 0; i < chord.size(); ++i)
|
||||
{
|
||||
float heightRatio = 1.f * i / (chord.size() - 1);
|
||||
float ratio = 0.0f;
|
||||
|
||||
if (heightRatio == m_strumHeightRatio)
|
||||
{
|
||||
ratio = 1.f;
|
||||
}
|
||||
else if (heightRatio < m_strumHeightRatio)
|
||||
{
|
||||
ratio = heightRatio / m_strumHeightRatio;
|
||||
}
|
||||
else
|
||||
{
|
||||
ratio = (1.f - heightRatio) / (1.f - m_strumHeightRatio);
|
||||
}
|
||||
|
||||
if (warp)
|
||||
{
|
||||
ratio = std::pow(ratio, strumPower);
|
||||
}
|
||||
chord.at(i)->setPos(std::max(0, static_cast<tick_t>(chord.at(i)->oldPos() + ratio * strumTicksHorizontal)));
|
||||
}
|
||||
}
|
||||
m_midiClip->rearrangeAllNotes();
|
||||
m_midiClip->updateLength();
|
||||
m_midiClip->dataChanged();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3682,6 +3849,9 @@ void PianoRoll::paintEvent(QPaintEvent * pe )
|
||||
case EditMode::Knife:
|
||||
cursor = &m_toolKnife;
|
||||
break;
|
||||
case EditMode::Strum:
|
||||
cursor = &m_toolStrum;
|
||||
break;
|
||||
}
|
||||
QPoint mousePosition = mapFromGlobal( QCursor::pos() );
|
||||
if( cursor != nullptr && mousePosition.y() > keyAreaTop() && mousePosition.x() > noteEditLeft())
|
||||
@@ -3896,7 +4066,14 @@ void PianoRoll::focusOutEvent( QFocusEvent * )
|
||||
if (m_editMode == EditMode::Knife) {
|
||||
m_editMode = m_knifeMode;
|
||||
m_action = Action::None;
|
||||
} else {
|
||||
}
|
||||
else if (m_editMode == EditMode::Strum)
|
||||
{
|
||||
m_editMode = m_strumMode;
|
||||
m_action = Action::None;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_editMode = m_ctrlMode;
|
||||
}
|
||||
update();
|
||||
@@ -4841,6 +5018,10 @@ PianoRollWindow::PianoRollWindow() :
|
||||
connect(knifeAction, &QAction::triggered, m_editor, &PianoRoll::setKnifeAction);
|
||||
knifeAction->setShortcut(combine(Qt::SHIFT, Qt::Key_K));
|
||||
|
||||
auto strumAction = new QAction(embed::getIconPixmap("arp_free"), tr("Strum"), noteToolsButton);
|
||||
connect(strumAction, &QAction::triggered, m_editor, &PianoRoll::setStrumAction);
|
||||
strumAction->setShortcut(combine(Qt::SHIFT, Qt::Key_J));
|
||||
|
||||
auto fillAction = new QAction(embed::getIconPixmap("fill"), tr("Fill"), noteToolsButton);
|
||||
connect(fillAction, &QAction::triggered, [this](){ m_editor->fitNoteLengths(true); });
|
||||
fillAction->setShortcut(combine(Qt::SHIFT, Qt::Key_F));
|
||||
@@ -4857,6 +5038,7 @@ PianoRollWindow::PianoRollWindow() :
|
||||
|
||||
noteToolsButton->addAction(glueAction);
|
||||
noteToolsButton->addAction(knifeAction);
|
||||
noteToolsButton->addAction(strumAction);
|
||||
noteToolsButton->addAction(fillAction);
|
||||
noteToolsButton->addAction(cutOverlapsAction);
|
||||
noteToolsButton->addAction(minLengthAction);
|
||||
|
||||
Reference in New Issue
Block a user