diff --git a/data/themes/classic/record_step_off.png b/data/themes/classic/record_step_off.png new file mode 100644 index 00000000000..8da17a91009 Binary files /dev/null and b/data/themes/classic/record_step_off.png differ diff --git a/data/themes/classic/record_step_on.png b/data/themes/classic/record_step_on.png new file mode 100644 index 00000000000..700ba97f305 Binary files /dev/null and b/data/themes/classic/record_step_on.png differ diff --git a/data/themes/default/record_step_off.png b/data/themes/default/record_step_off.png new file mode 100644 index 00000000000..8da17a91009 Binary files /dev/null and b/data/themes/default/record_step_off.png differ diff --git a/data/themes/default/record_step_on.png b/data/themes/default/record_step_on.png new file mode 100644 index 00000000000..700ba97f305 Binary files /dev/null and b/data/themes/default/record_step_on.png differ diff --git a/include/Editor.h b/include/Editor.h index 4b9017e9eb5..ca4f7415e0b 100644 --- a/include/Editor.h +++ b/include/Editor.h @@ -51,6 +51,7 @@ protected slots: virtual void play() {} virtual void record() {} virtual void recordAccompany() {} + virtual void toggleStepRecording() {} virtual void stop() {} private slots: @@ -64,7 +65,7 @@ private slots: /// /// \param record If set true, the editor's toolbar will contain record /// buttons in addition to the play and stop buttons. - Editor(bool record = false); + Editor(bool record = false, bool record_step = false); virtual ~Editor(); @@ -73,6 +74,7 @@ private slots: QAction* m_playAction; QAction* m_recordAction; QAction* m_recordAccompanyAction; + QAction* m_toggleStepRecordingAction; QAction* m_stopAction; }; diff --git a/include/PianoRoll.h b/include/PianoRoll.h index 168036245ad..f3b5440e7f6 100644 --- a/include/PianoRoll.h +++ b/include/PianoRoll.h @@ -38,6 +38,8 @@ #include "lmms_basics.h" #include "Song.h" #include "ToolTip.h" +#include "StepRecorder.h" +#include "StepRecorderWidget.h" class QPainter; class QPixmap; @@ -103,6 +105,11 @@ class PianoRoll : public QWidget return m_recording; } + inline bool isStepRecording() const + { + return m_stepRecorder.isRecording(); + } + const Pattern* currentPattern() const { return m_pattern; @@ -188,6 +195,7 @@ protected slots: void play(); void record(); void recordAccompany(); + bool toggleStepRecording(); void stop(); void startRecordNote( const Note & n ); @@ -205,9 +213,11 @@ protected slots: void updatePosition(const MidiTime & t ); void updatePositionAccompany(const MidiTime & t ); + void updatePositionStepRecording(const MidiTime & t ); void zoomingChanged(); void quantizeChanged(); + void noteLengthChanged(); void quantizeNotes(); void updateSemiToneMarkerMenu(); @@ -398,6 +408,9 @@ protected slots: friend class PianoRollWindow; + StepRecorderWidget m_stepRecorderWidget; + StepRecorder m_stepRecorder; + // qproperty fields QColor m_barLineColor; QColor m_beatLineColor; @@ -442,6 +455,7 @@ class PianoRollWindow : public Editor, SerializingObject void stop(); void record(); void recordAccompany(); + void toggleStepRecording(); void stopRecording(); bool isRecording() const; @@ -466,11 +480,14 @@ class PianoRollWindow : public Editor, SerializingObject private slots: - void patternRenamed(); + void updateAfterPatternChange(); void ghostPatternSet( bool state ); private: + void patternRenamed(); void focusInEvent(QFocusEvent * event); + void stopStepRecording(); + void updateStepRecordingIcon(); PianoRoll* m_editor; diff --git a/include/StepRecorder.h b/include/StepRecorder.h new file mode 100644 index 00000000000..b9653b1bbea --- /dev/null +++ b/include/StepRecorder.h @@ -0,0 +1,143 @@ +/* + * This file is part of LMMS - https://lmms.io + * + * This program 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 2 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 (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef STEP_RECORDER_H +#define STEP_RECORDER_H + +#include +#include +#include +#include + +#include "Note.h" +#include "lmms_basics.h" +#include "Pattern.h" + +class PianoRoll; +class StepRecorderWidget; + +class StepRecorder : public QObject +{ + Q_OBJECT + + public: + StepRecorder(PianoRoll& pianoRoll, StepRecorderWidget& stepRecorderWidget); + + void initialize(); + void start(const MidiTime& currentPosition,const MidiTime& stepLength); + void stop(); + void notePressed(const Note & n); + void noteReleased(const Note & n); + bool keyPressEvent(QKeyEvent* ke); + bool mousePressEvent(QMouseEvent* ke); + void setCurrentPattern(Pattern* newPattern); + void setStepsLength(const MidiTime& newLength); + + QVector getCurStepNotes(); + + bool isRecording() const + { + return m_isRecording; + } + + QColor curStepNoteColor() const + { + return QColor(245,3,139); // radiant pink + } + + private slots: + void removeNotesReleasedForTooLong(); + + private: + void stepForwards(); + void stepBackwards(); + + void applyStep(); + void dismissStep(); + void prepareNewStep(); + + MidiTime getCurStepEndPos(); + + void updateCurStepNotes(); + void updateWidget(); + + bool allCurStepNotesReleased(); + + PianoRoll& m_pianoRoll; + StepRecorderWidget& m_stepRecorderWidget; + + bool m_isRecording = false; + MidiTime m_curStepStartPos = 0; + MidiTime m_curStepEndPos = 0; + + MidiTime m_stepsLength; + MidiTime m_curStepLength; // current step length refers to the step currently recorded. it may defer from m_stepsLength + // since the user can make current step larger + + QTimer m_updateReleasedTimer; + + Pattern* m_pattern; + + class StepNote + { + public: + StepNote(const Note & note) : m_note(note), m_pressed(true) {}; + + void setPressed() + { + m_pressed = true; + } + + void setReleased() + { + m_pressed = false; + releasedTimer.start(); + } + + int timeSinceReleased() + { + return releasedTimer.elapsed(); + } + + bool isPressed() const + { + return m_pressed; + } + + bool isReleased() const + { + return !m_pressed; + } + + Note m_note; + + private: + bool m_pressed; + QTime releasedTimer; + } ; + + QVector m_curStepNotes; // contains the current recorded step notes (i.e. while user still press the notes; before they are applied to the pattern) + + StepNote* findCurStepNote(const int key); + + bool m_isStepInProgress = false; +}; + +#endif //STEP_RECORDER_H \ No newline at end of file diff --git a/include/StepRecorderWidget.h b/include/StepRecorderWidget.h new file mode 100644 index 00000000000..0e45121698b --- /dev/null +++ b/include/StepRecorderWidget.h @@ -0,0 +1,92 @@ +/* + * StepRecorderWidget.h - widget that provide gui markers for step recording + * + * This file is part of LMMS - https://lmms.io + * + * This program 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 2 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 (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ +#ifndef STEP_RECOREDER_WIDGET_H +#define STEP_RECOREDER_WIDGET_H + +#include "lmms_basics.h" +#include "Note.h" + +#include +#include +#include + +class StepRecorderWidget : public QWidget +{ + Q_OBJECT + +public: + StepRecorderWidget( + QWidget * parent, + const int ppt, + const int marginTop, + const int marginBottom, + const int marginLeft, + const int marginRight); + + //API used by PianoRoll + void setPixelsPerTact(int ppt); + void setCurrentPosition(MidiTime currentPosition); + void setBottomMargin(const int marginBottom); + + //API used by StepRecorder + void setStepsLength(MidiTime stepsLength); + void setStartPosition(MidiTime pos); + void setEndPosition(MidiTime pos); + + void showHint(); + +private: + virtual void paintEvent(QPaintEvent * pe); + + int xCoordOfTick(int tick); + + void drawVerLine(QPainter* painter, int x, const QColor& color, int top, int bottom); + void drawVerLine(QPainter* painter, const MidiTime& pos, const QColor& color, int top, int bottom); + + void updateBoundaries(); + + MidiTime m_stepsLength; + MidiTime m_curStepStartPos; + MidiTime m_curStepEndPos; + + int m_ppt; // pixels per tact + MidiTime m_currentPosition; // current position showed by on PianoRoll + + QColor m_colorLineStart; + QColor m_colorLineEnd; + + // boundaries within piano roll window + int m_top; + int m_bottom; + int m_left; + int m_right; + + const int m_marginTop; + int m_marginBottom; // not const since can change on resize of edit-note area + const int m_marginLeft; + const int m_marginRight; + +signals: + void positionChanged(const MidiTime & t); +} ; + +#endif //STEP_RECOREDER_WIDGET_H diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 85a00780b10..7870415f971 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -67,6 +67,7 @@ set(LMMS_SRCS core/TrackContainer.cpp core/ValueBuffer.cpp core/VstSyncController.cpp + core/StepRecorder.cpp core/audio/AudioAlsa.cpp core/audio/AudioDevice.cpp diff --git a/src/core/StepRecorder.cpp b/src/core/StepRecorder.cpp new file mode 100644 index 00000000000..7a63e88e26e --- /dev/null +++ b/src/core/StepRecorder.cpp @@ -0,0 +1,366 @@ +/* + * This file is part of LMMS - https://lmms.io + * + * This program 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 2 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 (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "StepRecorder.h" +#include "StepRecorderWidget.h" +#include "PianoRoll.h" + +#include + +#include +using std::min; +using std::max; + +const int REMOVE_RELEASED_NOTE_TIME_THRESHOLD_MS = 70; + +StepRecorder::StepRecorder(PianoRoll& pianoRoll, StepRecorderWidget& stepRecorderWidget): + m_pianoRoll(pianoRoll), + m_stepRecorderWidget(stepRecorderWidget) +{ + m_stepRecorderWidget.hide(); +} + +void StepRecorder::initialize() +{ + connect(&m_updateReleasedTimer, SIGNAL(timeout()), this, SLOT(removeNotesReleasedForTooLong())); +} + +void StepRecorder::start(const MidiTime& currentPosition, const MidiTime& stepLength) +{ + m_isRecording = true; + + setStepsLength(stepLength); + + // quantize current position to get start recording position + const int q = m_pianoRoll.quantization(); + const int curPosTicks = currentPosition.getTicks(); + const int QuantizedPosTicks = (curPosTicks / q) * q; + const MidiTime& QuantizedPos = MidiTime(QuantizedPosTicks); + + m_curStepStartPos = QuantizedPos; + m_curStepLength = 0; + + m_stepRecorderWidget.show(); + + m_stepRecorderWidget.showHint(); + + prepareNewStep(); +} + +void StepRecorder::stop() +{ + m_stepRecorderWidget.hide(); + m_isRecording = false; +} + +void StepRecorder::notePressed(const Note & n) +{ + //if this is the first pressed note in step, advance position + if(!m_isStepInProgress) + { + m_isStepInProgress = true; + + //move curser one step forwards + stepForwards(); + } + + StepNote* stepNote = findCurStepNote(n.key()); + if(stepNote == nullptr) + { + m_curStepNotes.append(new StepNote(Note(m_curStepLength, m_curStepStartPos, n.key(), n.getVolume(), n.getPanning()))); + m_pianoRoll.update(); + } + else if (stepNote->isReleased()) + { + stepNote->setPressed(); + } +} + +void StepRecorder::noteReleased(const Note & n) +{ + StepNote* stepNote = findCurStepNote(n.key()); + + if(stepNote != nullptr && stepNote->isPressed()) + { + stepNote->setReleased(); + + //if m_updateReleasedTimer is not already active, activate it + //(when activated, the timer will re-set itself as long as there are released notes) + if(!m_updateReleasedTimer.isActive()) + { + m_updateReleasedTimer.start(REMOVE_RELEASED_NOTE_TIME_THRESHOLD_MS); + } + + //check if all note are released, apply notes to pattern(or dimiss if length is zero) and prepare to record next step + if(allCurStepNotesReleased()) + { + if(m_curStepLength > 0) + { + applyStep(); + } + else + { + dismissStep(); + } + } + } +} + +bool StepRecorder::keyPressEvent(QKeyEvent* ke) +{ + bool event_handled = false; + + switch(ke->key()) + { + case Qt::Key_Right: + { + if(!ke->isAutoRepeat()) + { + stepForwards(); + } + event_handled = true; + break; + } + + case Qt::Key_Left: + { + if(!ke->isAutoRepeat()) + { + stepBackwards(); + } + event_handled = true; + break; + } + } + + return event_handled; +} + +void StepRecorder::setStepsLength(const MidiTime& newLength) +{ + if(m_isStepInProgress) + { + //update current step length by the new amount : (number_of_steps * newLength) + m_curStepLength = (m_curStepLength / m_stepsLength) * newLength; + + updateCurStepNotes(); + } + + m_stepsLength = newLength; + + updateWidget(); +} + +QVector StepRecorder::getCurStepNotes() +{ + QVector notes; + + if(m_isStepInProgress) + { + for(StepNote* stepNote: m_curStepNotes) + { + notes.append(&stepNote->m_note); + } + } + + return notes; +} + +void StepRecorder::stepForwards() +{ + if(m_isStepInProgress) + { + m_curStepLength += m_stepsLength; + + updateCurStepNotes(); + } + else + { + m_curStepStartPos += m_stepsLength; + } + + updateWidget(); +} + +void StepRecorder::stepBackwards() +{ + if(m_isStepInProgress) + { + if(m_curStepLength > 0) + { + m_curStepLength = max(m_curStepLength - m_stepsLength, 0); + } + else + { + //if length is already zero - move starting position backwards + m_curStepStartPos = max(m_curStepStartPos - m_stepsLength, 0); + } + + updateCurStepNotes(); + } + else + { + m_curStepStartPos = max(m_curStepStartPos - m_stepsLength, 0); + } + + updateWidget(); +} + +void StepRecorder::applyStep() +{ + m_pattern->addJournalCheckPoint(); + + for (const StepNote* stepNote : m_curStepNotes) + { + m_pattern->addNote(stepNote->m_note, false); + } + + m_pattern->rearrangeAllNotes(); + m_pattern->updateLength(); + m_pattern->dataChanged(); + Engine::getSong()->setModified(); + + prepareNewStep(); +} + +void StepRecorder::dismissStep() +{ + if(!m_isStepInProgress) + { + return; + } + + prepareNewStep(); +} + +void StepRecorder::prepareNewStep() +{ + for(StepNote* stepNote : m_curStepNotes) + { + delete stepNote; + } + m_curStepNotes.clear(); + + m_isStepInProgress = false; + + m_curStepStartPos = getCurStepEndPos(); + m_curStepLength = 0; + + updateWidget(); +} + +void StepRecorder::setCurrentPattern( Pattern* newPattern ) +{ + if(m_pattern != NULL && m_pattern != newPattern) + { + dismissStep(); + } + + m_pattern = newPattern; +} + +void StepRecorder::removeNotesReleasedForTooLong() +{ + int nextTimout = std::numeric_limits::max(); + bool notesRemoved = false; + + QMutableVectorIterator itr(m_curStepNotes); + while (itr.hasNext()) + { + StepNote* stepNote = itr.next(); + + if(stepNote->isReleased()) + { + const int timeSinceReleased = stepNote->timeSinceReleased(); // capture value to avoid wraparound when calculting nextTimout + if (timeSinceReleased >= REMOVE_RELEASED_NOTE_TIME_THRESHOLD_MS) + { + delete stepNote; + itr.remove(); + notesRemoved = true; + } + else + { + nextTimout = min(nextTimout, REMOVE_RELEASED_NOTE_TIME_THRESHOLD_MS - timeSinceReleased); + } + } + } + + if(notesRemoved) + { + m_pianoRoll.update(); + } + + if(nextTimout != std::numeric_limits::max()) + { + m_updateReleasedTimer.start(nextTimout); + } + else + { + // no released note found for next timout, stop timer + m_updateReleasedTimer.stop(); + } +} + +MidiTime StepRecorder::getCurStepEndPos() +{ + return m_curStepStartPos + m_curStepLength; +} + +void StepRecorder::updateCurStepNotes() +{ + for (StepNote* stepNote : m_curStepNotes) + { + stepNote->m_note.setLength(m_curStepLength); + stepNote->m_note.setPos(m_curStepStartPos); + } +} + +void StepRecorder::updateWidget() +{ + m_stepRecorderWidget.setStartPosition(m_curStepStartPos); + m_stepRecorderWidget.setEndPosition(getCurStepEndPos()); + m_stepRecorderWidget.setStepsLength(m_stepsLength); +} + +bool StepRecorder::allCurStepNotesReleased() +{ + for (const StepNote* stepNote : m_curStepNotes) + { + if(stepNote->isPressed()) + { + return false; + } + } + + return true; +} + +StepRecorder::StepNote* StepRecorder::findCurStepNote(const int key) +{ + for (StepNote* stepNote : m_curStepNotes) + { + if(stepNote->m_note.key() == key) + { + return stepNote; + } + } + + return nullptr; +} diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 5b4050bca70..d5ff6461237 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -87,6 +87,7 @@ SET(LMMS_SRCS gui/widgets/TrackLabelButton.cpp gui/widgets/TrackRenameLineEdit.cpp gui/widgets/VisualizationWidget.cpp + gui/widgets/StepRecorderWidget.cpp PARENT_SCOPE ) diff --git a/src/gui/editors/Editor.cpp b/src/gui/editors/Editor.cpp index bdc3e55d4bb..b82453acf03 100644 --- a/src/gui/editors/Editor.cpp +++ b/src/gui/editors/Editor.cpp @@ -73,11 +73,12 @@ void Editor::togglePlayStop() play(); } -Editor::Editor(bool record) : +Editor::Editor(bool record, bool stepRecord) : m_toolBar(new DropToolBar(this)), m_playAction(nullptr), m_recordAction(nullptr), m_recordAccompanyAction(nullptr), + m_toggleStepRecordingAction(nullptr), m_stopAction(nullptr) { m_toolBar = addDropToolBarToTop(tr("Transport controls")); @@ -93,11 +94,13 @@ Editor::Editor(bool record) : m_recordAction = new QAction(embed::getIconPixmap("record"), tr("Record"), this); m_recordAccompanyAction = new QAction(embed::getIconPixmap("record_accompany"), tr("Record while playing"), this); + m_toggleStepRecordingAction = new QAction(embed::getIconPixmap("record_step_off"), tr("Toggle Step Recording"), this); // Set up connections connect(m_playAction, SIGNAL(triggered()), this, SLOT(play())); connect(m_recordAction, SIGNAL(triggered()), this, SLOT(record())); connect(m_recordAccompanyAction, SIGNAL(triggered()), this, SLOT(recordAccompany())); + connect(m_toggleStepRecordingAction, SIGNAL(triggered()), this, SLOT(toggleStepRecording())); connect(m_stopAction, SIGNAL(triggered()), this, SLOT(stop())); new QShortcut(Qt::Key_Space, this, SLOT(togglePlayStop())); @@ -108,6 +111,10 @@ Editor::Editor(bool record) : addButton(m_recordAction, "recordButton"); addButton(m_recordAccompanyAction, "recordAccompanyButton"); } + if(stepRecord) + { + addButton(m_toggleStepRecordingAction, "stepRecordButton"); + } addButton(m_stopAction, "stopButton"); } diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 07309ec1cc6..063f65c210b 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -62,6 +62,7 @@ #include "stdshims.h" #include "TextFloat.h" #include "TimeLineWidget.h" +#include "StepRecorderWidget.h" using std::move; @@ -177,6 +178,8 @@ PianoRoll::PianoRoll() : m_ctrlMode( ModeDraw ), m_mouseDownRight( false ), m_scrollBack( false ), + m_stepRecorderWidget(this, DEFAULT_PR_PPT, PR_TOP_MARGIN, PR_BOTTOM_MARGIN + m_notesEditHeight, WHITE_KEY_WIDTH, 0), + m_stepRecorder(*this, m_stepRecorderWidget), m_barLineColor( 0, 0, 0 ), m_beatLineColor( 0, 0, 0 ), m_lineColor( 0, 0, 0 ), @@ -323,6 +326,10 @@ PianoRoll::PianoRoll() : connect( m_timeLine, SIGNAL( positionChanged( const MidiTime & ) ), this, SLOT( updatePosition( const MidiTime & ) ) ); + //update timeline when in step-recording mode + connect( &m_stepRecorderWidget, SIGNAL( positionChanged( const MidiTime & ) ), + this, SLOT( updatePositionStepRecording( const MidiTime & ) ) ); + // update timeline when in record-accompany mode connect( Engine::getSong()->getPlayPos( Song::Mode_PlaySong ).m_timeLine, SIGNAL( positionChanged( const MidiTime & ) ), @@ -395,7 +402,7 @@ PianoRoll::PianoRoll() : // Note length change can cause a redraw if Q is set to lock connect( &m_noteLenModel, SIGNAL( dataChanged() ), - this, SLOT( quantizeChanged() ) ); + this, SLOT( noteLengthChanged() ) ); // Set up scale model const InstrumentFunctionNoteStacking::ChordTable& chord_table = @@ -444,6 +451,8 @@ PianoRoll::PianoRoll() : //connection for selecion from timeline connect( m_timeLine, SIGNAL( regionSelectedFromPixels( int, int ) ), this, SLOT( selectRegionFromPixels( int, int ) ) ); + + m_stepRecorder.initialize(); } @@ -637,12 +646,19 @@ void PianoRoll::setCurrentPattern( Pattern* newPattern ) Engine::getSong()->playPattern( NULL ); } + if(m_stepRecorder.isRecording()) + { + m_stepRecorder.stop(); + } + // set new data m_pattern = newPattern; m_currentPosition = 0; m_currentNote = NULL; m_startKey = INITIAL_START_KEY; + m_stepRecorder.setCurrentPattern(newPattern); + if( ! hasValidPattern() ) { //resizeEvent( NULL ); @@ -1130,8 +1146,19 @@ int PianoRoll::selectionCount() const // how many notes are selected? -void PianoRoll::keyPressEvent(QKeyEvent* ke ) +void PianoRoll::keyPressEvent(QKeyEvent* ke) { + if(m_stepRecorder.isRecording()) + { + bool handled = m_stepRecorder.keyPressEvent(ke); + if(handled) + { + ke->accept(); + update(); + return; + } + } + if( hasValidPattern() && ke->modifiers() == Qt::NoModifier ) { const int key_num = PianoView::getKeyFromKeyEvent( ke ) + ( DefaultOctave - 1 ) * KeysPerOctave; @@ -1880,7 +1907,7 @@ void PianoRoll::testPlayNote( Note * n ) { m_lastKey = n->key(); - if( ! n->isPlaying() && ! m_recording ) + if( ! n->isPlaying() && ! m_recording && ! m_stepRecorder.isRecording()) { n->setIsPlaying( true ); @@ -2113,6 +2140,8 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me ) NOTE_EDIT_MIN_HEIGHT, height() - PR_TOP_MARGIN - NOTE_EDIT_RESIZE_BAR - PR_BOTTOM_MARGIN - KEY_AREA_MIN_HEIGHT ); + + m_stepRecorderWidget.setBottomMargin(PR_BOTTOM_MARGIN + m_notesEditHeight); repaint(); return; } @@ -3203,6 +3232,41 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) } } + //draw current step recording notes + for( const Note *note : m_stepRecorder.getCurStepNotes() ) + { + int len_ticks = note->length(); + + if( len_ticks == 0 ) + { + continue; + } + + const int key = note->key() - m_startKey + 1; + + int pos_ticks = note->pos(); + + int note_width = len_ticks * m_ppt / MidiTime::ticksPerTact(); + const int x = ( pos_ticks - m_currentPosition ) * + m_ppt / MidiTime::ticksPerTact(); + // skip this note if not in visible area at all + if( !( x + note_width >= 0 && x <= width() - WHITE_KEY_WIDTH ) ) + { + continue; + } + + // is the note in visible area? + if( key > 0 && key <= visible_keys ) + { + + // we've done and checked all, let's draw the note + drawNoteRect( p, x + WHITE_KEY_WIDTH, + y_base - key * KEY_LINE_HEIGHT, + note_width, note, m_stepRecorder.curStepNoteColor(), noteTextColor(), selectedNoteColor(), + noteOpacity(), noteBorders(), drawNoteNames ); + } + } + p.setPen( QPen( noteColor(), NOTE_EDIT_LINE_WIDTH + 2 ) ); p.drawPoints( editHandles ); @@ -3602,6 +3666,34 @@ void PianoRoll::recordAccompany() +bool PianoRoll::toggleStepRecording() +{ + if(m_stepRecorder.isRecording()) + { + m_stepRecorder.stop(); + } + else + { + if(hasValidPattern()) + { + if(Engine::getSong()->isPlaying()) + { + m_stepRecorder.start(0, newNoteLen()); + } + else + { + m_stepRecorder.start( + Engine::getSong()->getPlayPos( + Song::Mode_PlayPattern), newNoteLen()); + } + } + } + + return m_stepRecorder.isRecording();; +} + + + void PianoRoll::stop() { @@ -3615,22 +3707,29 @@ void PianoRoll::stop() void PianoRoll::startRecordNote(const Note & n ) { - if( m_recording && hasValidPattern() && + if(hasValidPattern()) + { + if( m_recording && Engine::getSong()->isPlaying() && (Engine::getSong()->playMode() == desiredPlayModeForAccompany() || - Engine::getSong()->playMode() == Song::Mode_PlayPattern )) - { - MidiTime sub; - if( Engine::getSong()->playMode() == Song::Mode_PlaySong ) + Engine::getSong()->playMode() == Song::Mode_PlayPattern )) { - sub = m_pattern->startPosition(); + MidiTime sub; + if( Engine::getSong()->playMode() == Song::Mode_PlaySong ) + { + sub = m_pattern->startPosition(); + } + Note n1( 1, Engine::getSong()->getPlayPos( + Engine::getSong()->playMode() ) - sub, + n.key(), n.getVolume(), n.getPanning() ); + if( n1.pos() >= 0 ) + { + m_recordingNotes << n1; + } } - Note n1( 1, Engine::getSong()->getPlayPos( - Engine::getSong()->playMode() ) - sub, - n.key(), n.getVolume(), n.getPanning() ); - if( n1.pos() >= 0 ) + else if (m_stepRecorder.isRecording()) { - m_recordingNotes << n1; + m_stepRecorder.notePressed(n); } } } @@ -3640,28 +3739,35 @@ void PianoRoll::startRecordNote(const Note & n ) void PianoRoll::finishRecordNote(const Note & n ) { - if( m_recording && hasValidPattern() && - Engine::getSong()->isPlaying() && - ( Engine::getSong()->playMode() == - desiredPlayModeForAccompany() || - Engine::getSong()->playMode() == - Song::Mode_PlayPattern ) ) - { - for( QList::Iterator it = m_recordingNotes.begin(); - it != m_recordingNotes.end(); ++it ) + if(hasValidPattern()) + { + if( m_recording && + Engine::getSong()->isPlaying() && + ( Engine::getSong()->playMode() == + desiredPlayModeForAccompany() || + Engine::getSong()->playMode() == + Song::Mode_PlayPattern ) ) { - if( it->key() == n.key() ) + for( QList::Iterator it = m_recordingNotes.begin(); + it != m_recordingNotes.end(); ++it ) { - Note n1( n.length(), it->pos(), - it->key(), it->getVolume(), - it->getPanning() ); - n1.quantizeLength( quantization() ); - m_pattern->addNote( n1 ); - update(); - m_recordingNotes.erase( it ); - break; + if( it->key() == n.key() ) + { + Note n1( n.length(), it->pos(), + it->key(), it->getVolume(), + it->getPanning() ); + n1.quantizeLength( quantization() ); + m_pattern->addNote( n1 ); + update(); + m_recordingNotes.erase( it ); + break; + } } } + else if (m_stepRecorder.isRecording()) + { + m_stepRecorder.noteReleased(n); + } } } @@ -3671,6 +3777,7 @@ void PianoRoll::finishRecordNote(const Note & n ) void PianoRoll::horScrolled(int new_pos ) { m_currentPosition = new_pos; + m_stepRecorderWidget.setCurrentPosition(m_currentPosition); emit positionChanged( m_currentPosition ); update(); } @@ -4041,6 +4148,13 @@ void PianoRoll::updatePositionAccompany( const MidiTime & t ) } +void PianoRoll::updatePositionStepRecording( const MidiTime & t ) +{ + if( m_stepRecorder.isRecording() ) + { + autoScroll( t ); + } +} void PianoRoll::zoomingChanged() @@ -4050,6 +4164,8 @@ void PianoRoll::zoomingChanged() assert( m_ppt > 0 ); m_timeLine->setPixelsPerTact( m_ppt ); + m_stepRecorderWidget.setPixelsPerTact( m_ppt ); + update(); } @@ -4061,7 +4177,11 @@ void PianoRoll::quantizeChanged() update(); } - +void PianoRoll::noteLengthChanged() +{ + m_stepRecorder.setStepsLength(newNoteLen()); + update(); +} int PianoRoll::quantization() const @@ -4198,7 +4318,7 @@ Note * PianoRoll::noteUnderMouse() PianoRollWindow::PianoRollWindow() : - Editor(true), + Editor(true, true), m_editor(new PianoRoll()) { setCentralWidget( m_editor ); @@ -4206,6 +4326,7 @@ PianoRollWindow::PianoRollWindow() : m_playAction->setToolTip(tr( "Play/pause current pattern (Space)" ) ); m_recordAction->setToolTip(tr( "Record notes from MIDI-device/channel-piano" ) ); m_recordAccompanyAction->setToolTip( tr( "Record notes from MIDI-device/channel-piano while playing song or BB track" ) ); + m_toggleStepRecordingAction->setToolTip( tr( "Record notes from MIDI-device/channel-piano, one step at the time" ) ); m_stopAction->setToolTip( tr( "Stop playing of current pattern (Space)" ) ); DropToolBar *notesActionsToolBar = addDropToolBarToTop( tr( "Edit actions" ) ); @@ -4352,7 +4473,7 @@ PianoRollWindow::PianoRollWindow() : // Connections connect( m_editor, SIGNAL( currentPatternChanged() ), this, SIGNAL( currentPatternChanged() ) ); - connect( m_editor, SIGNAL( currentPatternChanged() ), this, SLOT( patternRenamed() ) ); + connect( m_editor, SIGNAL( currentPatternChanged() ), this, SLOT( updateAfterPatternChange() ) ); } @@ -4381,8 +4502,8 @@ void PianoRollWindow::setCurrentPattern( Pattern* pattern ) if ( pattern ) { setWindowTitle( tr( "Piano-Roll - %1" ).arg( pattern->name() ) ); - connect( pattern->instrumentTrack(), SIGNAL( nameChanged() ), this, SLOT( patternRenamed()) ); - connect( pattern, SIGNAL( dataChanged() ), this, SLOT( patternRenamed() ) ); + connect( pattern->instrumentTrack(), SIGNAL( nameChanged() ), this, SLOT( updateAfterPatternChange()) ); + connect( pattern, SIGNAL( dataChanged() ), this, SLOT( updateAfterPatternChange() ) ); } else { @@ -4427,6 +4548,8 @@ void PianoRollWindow::stop() void PianoRollWindow::record() { + stopStepRecording(); //step recording mode is mutually exclusive with other record modes + m_editor->record(); } @@ -4435,11 +4558,25 @@ void PianoRollWindow::record() void PianoRollWindow::recordAccompany() { + stopStepRecording(); //step recording mode is mutually exclusive with other record modes + m_editor->recordAccompany(); } +void PianoRollWindow::toggleStepRecording() +{ + if(isRecording()) + { + // step recording mode is mutually exclusive with other record modes + // stop them before starting step recording + stop(); + } + m_editor->toggleStepRecording(); + + updateStepRecordingIcon(); +} void PianoRollWindow::stopRecording() { @@ -4480,6 +4617,11 @@ QSize PianoRollWindow::sizeHint() const +void PianoRollWindow::updateAfterPatternChange() +{ + patternRenamed(); + updateStepRecordingIcon(); //pattern change turn step recording OFF - update icon accordingly +} void PianoRollWindow::patternRenamed() { @@ -4509,3 +4651,24 @@ void PianoRollWindow::focusInEvent( QFocusEvent * event ) // when the window is given focus, also give focus to the actual piano roll m_editor->setFocus( event->reason() ); } + +void PianoRollWindow::stopStepRecording() +{ + if(m_editor->isStepRecording()) + { + m_editor->toggleStepRecording(); + updateStepRecordingIcon(); + } +} + +void PianoRollWindow::updateStepRecordingIcon() +{ + if(m_editor->isStepRecording()) + { + m_toggleStepRecordingAction->setIcon(embed::getIconPixmap("record_step_on")); + } + else + { + m_toggleStepRecordingAction->setIcon(embed::getIconPixmap("record_step_off")); + } +} diff --git a/src/gui/editors/SongEditor.cpp b/src/gui/editors/SongEditor.cpp index a2e52e200a6..92a5c5fa5b4 100644 --- a/src/gui/editors/SongEditor.cpp +++ b/src/gui/editors/SongEditor.cpp @@ -654,7 +654,7 @@ ComboBoxModel *SongEditor::zoomingModel() const SongEditorWindow::SongEditorWindow(Song* song) : - Editor(Engine::mixer()->audioDev()->supportsCapture()), + Editor(Engine::mixer()->audioDev()->supportsCapture(), false), m_editor(new SongEditor(song)), m_crtlAction( NULL ) { diff --git a/src/gui/widgets/StepRecorderWidget.cpp b/src/gui/widgets/StepRecorderWidget.cpp new file mode 100644 index 00000000000..f59e235fc80 --- /dev/null +++ b/src/gui/widgets/StepRecorderWidget.cpp @@ -0,0 +1,155 @@ +/* + * StepRecoderWidget.cpp - widget that provide gui markers for step recording + * + * This file is part of LMMS - https://lmms.io + * + * This program 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 2 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 (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "StepRecorderWidget.h" +#include "TextFloat.h" +#include "embed.h" + +StepRecorderWidget::StepRecorderWidget( + QWidget * parent, + const int ppt, + const int marginTop, + const int marginBottom, + const int marginLeft, + const int marginRight) : + QWidget(parent), + m_marginTop(marginTop), + m_marginBottom(marginBottom), + m_marginLeft(marginLeft), + m_marginRight(marginRight) +{ + const QColor baseColor = QColor(255, 0, 0);// QColor(204, 163, 0); // Orange + m_colorLineEnd = baseColor.lighter(150); + m_colorLineStart = baseColor.darker(120); + + setAttribute(Qt::WA_NoSystemBackground, true); + setPixelsPerTact(ppt); + + m_top = m_marginTop; + m_left = m_marginLeft; +} + +void StepRecorderWidget::setPixelsPerTact(int ppt) +{ + m_ppt = ppt; +} + +void StepRecorderWidget::setCurrentPosition(MidiTime currentPosition) +{ + m_currentPosition = currentPosition; +} + +void StepRecorderWidget::setBottomMargin(const int marginBottom) +{ + m_marginBottom = marginBottom; +} + +void StepRecorderWidget::setStartPosition(MidiTime pos) +{ + m_curStepStartPos = pos; +} + +void StepRecorderWidget::setEndPosition(MidiTime pos) +{ + m_curStepEndPos = pos; + emit positionChanged(m_curStepEndPos); +} + +void StepRecorderWidget::showHint() +{ + TextFloat::displayMessage(tr( "Hint" ), tr("Move recording curser using arrows"), + embed::getIconPixmap("hint")); +} + +void StepRecorderWidget::setStepsLength(MidiTime stepsLength) +{ + m_stepsLength = stepsLength; +} + +void StepRecorderWidget::paintEvent(QPaintEvent * pe) +{ + QPainter painter(this); + + updateBoundaries(); + + move(0, 0); + + //draw steps ruler + painter.setPen(m_colorLineEnd); + + MidiTime curPos = m_curStepEndPos; + int x = xCoordOfTick(curPos); + while(x <= m_right) + { + const int w = 2; + const int h = 4; + painter.drawRect(x - 1, m_top, w, h); + curPos += m_stepsLength; + x = xCoordOfTick(curPos); + } + + //draw current step start/end position lines + if(m_curStepStartPos != m_curStepEndPos) + { + drawVerLine(&painter, m_curStepStartPos, m_colorLineStart, m_top, m_bottom); + } + + drawVerLine(&painter, m_curStepEndPos, m_colorLineEnd, m_top, m_bottom); + + //if the line is adjacent to the keyboard at the left - it cannot be seen. + //add another line to make it clearer + if(m_curStepEndPos == 0) + { + drawVerLine(&painter, xCoordOfTick(m_curStepEndPos) + 1, m_colorLineEnd, m_top, m_bottom); + } +} + +int StepRecorderWidget::xCoordOfTick(int tick) +{ + return m_marginLeft + ((tick - m_currentPosition) * m_ppt / MidiTime::ticksPerTact()); +} + + +void StepRecorderWidget::drawVerLine(QPainter* painter, int x, const QColor& color, int top, int bottom) +{ + if(x >= m_marginLeft && x <= (width() - m_marginRight)) + { + painter->setPen(color); + painter->drawLine( x, top, x, bottom ); + } +} + +void StepRecorderWidget::drawVerLine(QPainter* painter, const MidiTime& pos, const QColor& color, int top, int bottom) +{ + drawVerLine(painter, xCoordOfTick(pos), color, top, bottom); +} + +void StepRecorderWidget::updateBoundaries() +{ + setFixedSize(parentWidget()->size()); + + m_bottom = height() - m_marginBottom; + m_right = width() - m_marginTop; + + //(no need to change top and left as they are static) +} +