1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
2 
3 /*
4     Rosegarden
5     A MIDI and audio sequencer and musical notation editor.
6     Copyright 2000-2021 the Rosegarden development team.
7 
8     Other copyrights also apply to some parts of this work.  Please
9     see the AUTHORS file and individual file headers for details.
10 
11     This program is free software; you can redistribute it and/or
12     modify it under the terms of the GNU General Public License as
13     published by the Free Software Foundation; either version 2 of the
14     License, or (at your option) any later version.  See the file
15     COPYING included with this distribution for more information.
16 */
17 
18 #define RG_MODULE_STRING "[TempoDialog]"
19 
20 #include "TempoDialog.h"
21 #include "misc/Debug.h"
22 #include "base/Composition.h"
23 #include "base/NotationTypes.h"
24 #include "base/RealTime.h"
25 #include "document/RosegardenDocument.h"
26 #include "gui/editors/notation/NotePixmapFactory.h"
27 #include "gui/widgets/TimeWidget.h"
28 
29 #include <QDialog>
30 #include <QDialogButtonBox>
31 #include <QGroupBox>
32 #include <QDoubleSpinBox>
33 #include <QCheckBox>
34 #include <QFrame>
35 #include <QLabel>
36 #include <QRadioButton>
37 #include <QPushButton>
38 #include <QString>
39 #include <QWidget>
40 #include <QVBoxLayout>
41 #include <QHBoxLayout>
42 #include <QLayout>
43 #include <QUrl>
44 #include <QDesktopServices>
45 
46 
47 
48 namespace Rosegarden
49 {
50 
TempoDialog(QWidget * parent,RosegardenDocument * doc,bool timeEditable)51 TempoDialog::TempoDialog(QWidget *parent, RosegardenDocument *doc,
52                          bool timeEditable):
53         QDialog(parent),
54         m_doc(doc),
55         m_tempoTime(0)
56 {
57     setModal(true);
58     setWindowTitle(tr("Insert Tempo Change"));
59     setObjectName("MinorDialog");
60 
61     QWidget* vbox = dynamic_cast<QWidget*>(this);
62     QVBoxLayout *vboxLayout = new QVBoxLayout;
63     vbox->setLayout(vboxLayout);
64 
65     // group box for tempo
66     QGroupBox *frame = new QGroupBox(tr("Tempo"), vbox);
67     frame->setContentsMargins(5, 5, 5, 5);
68     QGridLayout *layout = new QGridLayout;
69     layout->setSpacing(5);
70     vboxLayout->addWidget(frame);
71 
72     // Set tempo
73     layout->addWidget(new QLabel(tr("New tempo:"), frame), 0, 1);
74     m_tempoValueSpinBox = new QDoubleSpinBox(frame);
75     m_tempoValueSpinBox->setDecimals(3);
76     m_tempoValueSpinBox->setMaximum(1000);
77     m_tempoValueSpinBox->setMinimum(1);
78     m_tempoValueSpinBox->setSingleStep(1);
79     m_tempoValueSpinBox->setValue(120); // will set properly below
80     layout->addWidget(m_tempoValueSpinBox, 0, 2);
81 
82     connect(m_tempoValueSpinBox, SIGNAL(valueChanged(double)),
83             SLOT(slotTempoChanged(double)));
84 
85     m_tempoTap= new QPushButton(tr("Tap"), frame);
86     layout->addWidget(m_tempoTap, 0, 3);
87     connect(m_tempoTap, &QAbstractButton::clicked, this, &TempoDialog::slotTapClicked);
88 
89 
90     m_tempoConstant = new QRadioButton(tr("Tempo is fixed until the following tempo change"), frame);
91     m_tempoRampToNext = new QRadioButton(tr("Tempo ramps to the following tempo"), frame);
92     m_tempoRampToTarget = new QRadioButton(tr("Tempo ramps to:"), frame);
93 
94     //    m_tempoTargetCheckBox = new QCheckBox(tr("Ramping to:"), frame);
95     m_tempoTargetSpinBox = new QDoubleSpinBox(frame);
96     m_tempoTargetSpinBox->setDecimals(3);
97     m_tempoTargetSpinBox->setMaximum(1000);
98     m_tempoTargetSpinBox->setMinimum(1);
99     m_tempoTargetSpinBox->setSingleStep(1);
100     m_tempoTargetSpinBox->setValue(120);
101 
102     //    layout->addWidget(m_tempoTargetCheckBox, 1, 0, 0+1, 1- 1, AlignRight);
103     //    layout->addWidget(m_tempoTargetSpinBox, 1, 2);
104 
105     layout->addWidget(m_tempoConstant, 1, 1, 1, 2);
106     layout->addWidget(m_tempoRampToNext, 2, 1, 1, 2);
107     layout->addWidget(m_tempoRampToTarget, 3, 1);
108     layout->addWidget(m_tempoTargetSpinBox, 3, 2);
109 
110     //    connect(m_tempoTargetCheckBox, SIGNAL(clicked()),
111     //            SLOT(slotTargetCheckBoxClicked()));
112     connect(m_tempoConstant, &QAbstractButton::clicked,
113             this, &TempoDialog::slotTempoConstantClicked);
114     connect(m_tempoRampToNext, &QAbstractButton::clicked,
115             this, &TempoDialog::slotTempoRampToNextClicked);
116     connect(m_tempoRampToTarget, &QAbstractButton::clicked,
117             this, &TempoDialog::slotTempoRampToTargetClicked);
118     connect(m_tempoTargetSpinBox, SIGNAL(valueChanged(double)),
119             SLOT(slotTargetChanged(double)));
120 
121     m_tempoBeatLabel = new QLabel(frame);
122     layout->addWidget(m_tempoBeatLabel, 0, 4);
123 
124     m_tempoBeat = new QLabel(frame);
125     layout->addWidget(m_tempoBeat, 0, 5);
126 
127     m_tempoBeatsPerMinute = new QLabel(frame);
128     layout->addWidget(m_tempoBeatsPerMinute, 0, 6);
129 
130     frame->setLayout(layout);
131 
132     m_timeEditor = nullptr;
133 
134     if (timeEditable) {
135 
136         m_timeEditor = new TimeWidget
137             (tr("Time of tempo change"),
138              vbox, &m_doc->getComposition(), 0, true);
139         vboxLayout->addWidget(m_timeEditor);
140 
141     } else {
142 
143         // group box for scope (area)
144         QGroupBox *scopeGroup = new QGroupBox(tr("Scope"), vbox);
145         vboxLayout->addWidget(scopeGroup);
146 
147         QVBoxLayout * scopeBoxLayout = new QVBoxLayout(scopeGroup);
148         scopeBoxLayout->setSpacing(5);
149         scopeBoxLayout->setContentsMargins(5, 5, 5, 5);
150 
151         QVBoxLayout * currentBoxLayout = scopeBoxLayout;
152         QWidget * currentBox = scopeGroup;
153 
154         QLabel *child_15 = new QLabel(tr("The pointer is currently at "), currentBox);
155         currentBoxLayout->addWidget(child_15);
156         m_tempoTimeLabel = new QLabel(currentBox);
157         currentBoxLayout->addWidget(m_tempoTimeLabel);
158         m_tempoBarLabel = new QLabel(currentBox);
159         currentBoxLayout->addWidget(m_tempoBarLabel);
160         QLabel *spare = new QLabel(currentBox);
161         currentBoxLayout->addWidget(spare);
162         currentBox->setLayout(currentBoxLayout);
163         currentBoxLayout->setStretchFactor(spare, 20);
164 
165         m_tempoStatusLabel = new QLabel(scopeGroup);
166         scopeBoxLayout->addWidget(m_tempoStatusLabel);
167         scopeGroup->setLayout(scopeBoxLayout);
168 
169         QWidget * changeWhereBox = scopeGroup;
170         QVBoxLayout * changeWhereBoxLayout = scopeBoxLayout;
171 
172         spare = new QLabel("      ", changeWhereBox);
173         changeWhereBoxLayout->addWidget(spare);
174 
175         QWidget *changeWhereVBox = new QWidget(changeWhereBox);
176         QVBoxLayout *changeWhereVBoxLayout = new QVBoxLayout;
177         changeWhereBoxLayout->addWidget(changeWhereVBox);
178         changeWhereBox->setLayout(changeWhereBoxLayout);
179         changeWhereBoxLayout->setStretchFactor(changeWhereVBox, 20);
180 
181         m_tempoChangeHere = new QRadioButton(tr("Apply this tempo from here onwards"), changeWhereVBox);
182         changeWhereVBoxLayout->addWidget(m_tempoChangeHere);
183 
184         m_tempoChangeBefore = new QRadioButton(tr("Replace the last tempo change"), changeWhereVBox);
185         changeWhereVBoxLayout->addWidget(m_tempoChangeBefore);
186         m_tempoChangeBeforeAt = new QLabel(changeWhereVBox);
187         changeWhereVBoxLayout->addWidget(m_tempoChangeBeforeAt);
188         m_tempoChangeBeforeAt->hide();
189 
190         m_tempoChangeStartOfBar = new QRadioButton(tr("Apply this tempo from the start of this bar"), changeWhereVBox);
191         changeWhereVBoxLayout->addWidget(m_tempoChangeStartOfBar);
192 
193         m_tempoChangeGlobal = new QRadioButton(tr("Apply this tempo to the whole composition"), changeWhereVBox);
194         changeWhereVBoxLayout->addWidget(m_tempoChangeGlobal);
195 
196         QWidget *optionHBox = new QWidget(changeWhereVBox);
197         changeWhereVBoxLayout->addWidget(optionHBox);
198         changeWhereVBox->setLayout(changeWhereVBoxLayout);
199         QHBoxLayout *optionHBoxLayout = new QHBoxLayout;
200         QLabel *child_6 = new QLabel("         ", optionHBox);
201         optionHBoxLayout->addWidget(child_6);
202         m_defaultBox = new QCheckBox(tr("Also make this the default tempo"), optionHBox);
203         optionHBoxLayout->addWidget(m_defaultBox);
204         spare = new QLabel(optionHBox);
205         optionHBoxLayout->addWidget(spare);
206         optionHBox->setLayout(optionHBoxLayout);
207         optionHBoxLayout->setStretchFactor(spare, 20);
208 
209         connect(m_tempoChangeHere, &QAbstractButton::clicked,
210                 this, &TempoDialog::slotActionChanged);
211         connect(m_tempoChangeBefore, &QAbstractButton::clicked,
212                 this, &TempoDialog::slotActionChanged);
213         connect(m_tempoChangeStartOfBar, &QAbstractButton::clicked,
214                 this, &TempoDialog::slotActionChanged);
215         connect(m_tempoChangeGlobal, &QAbstractButton::clicked,
216                 this, &TempoDialog::slotActionChanged);
217 
218         m_tempoChangeBefore->setChecked(true);
219 
220         // disable initially
221         m_defaultBox->setEnabled(false);
222     }
223 
224     QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Help);
225     vboxLayout->addWidget(buttonBox);
226 
227     connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
228     connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
229     connect(buttonBox, &QDialogButtonBox::helpRequested, this, &TempoDialog::slotHelpRequested);
230 
231     populateTempo();
232 }
233 
~TempoDialog()234 TempoDialog::~TempoDialog()
235 {}
236 
237 void
setTempoPosition(timeT time)238 TempoDialog::setTempoPosition(timeT time)
239 {
240     m_tempoTime = time;
241     populateTempo();
242 }
243 
244 void
populateTempo()245 TempoDialog::populateTempo()
246 {
247     Composition &comp = m_doc->getComposition();
248     tempoT tempo = comp.getTempoAtTime(m_tempoTime);
249     std::pair<bool, tempoT> ramping(false, tempo);
250 
251     int tempoChangeNo = comp.getTempoChangeNumberAt(m_tempoTime);
252     if (tempoChangeNo >= 0) {
253         tempo = comp.getTempoChange(tempoChangeNo).second;
254         ramping = comp.getTempoRamping(tempoChangeNo, false);
255     }
256 
257     m_tempoValueSpinBox->setValue(comp.getTempoQpm(tempo));
258 
259     if (ramping.first) {
260         if (ramping.second) {
261             m_tempoTargetSpinBox->setEnabled(true);
262             m_tempoTargetSpinBox->setValue(comp.getTempoQpm(ramping.second));
263             m_tempoConstant->setChecked(false);
264             m_tempoRampToNext->setChecked(false);
265             m_tempoRampToTarget->setChecked(true);
266         } else {
267             ramping = comp.getTempoRamping(tempoChangeNo, true);
268             m_tempoTargetSpinBox->setEnabled(false);
269             m_tempoTargetSpinBox->setValue(comp.getTempoQpm(ramping.second));
270             m_tempoConstant->setChecked(false);
271             m_tempoRampToNext->setChecked(true);
272             m_tempoRampToTarget->setChecked(false);
273         }
274     } else {
275         m_tempoTargetSpinBox->setEnabled(false);
276         m_tempoTargetSpinBox->setValue(comp.getTempoQpm(tempo));
277         m_tempoConstant->setChecked(true);
278         m_tempoRampToNext->setChecked(false);
279         m_tempoRampToTarget->setChecked(false);
280     }
281 
282     //    m_tempoTargetCheckBox->setChecked(ramping.first);
283     m_tempoTargetSpinBox->setEnabled(ramping.first);
284 
285     updateBeatLabels(comp.getTempoQpm(tempo));
286 
287     if (m_timeEditor) {
288         m_timeEditor->slotSetTime(m_tempoTime);
289         return ;
290     }
291 
292     RealTime tempoTime = comp.getElapsedRealTime(m_tempoTime);
293     const QString milliSeconds = QString::asprintf("%03d", tempoTime.msec());
294     m_tempoTimeLabel->setText(tr("%1.%2 s,").arg(tempoTime.sec)
295                                .arg(milliSeconds));
296 
297     int barNo = comp.getBarNumber(m_tempoTime);
298     if (comp.getBarStart(barNo) == m_tempoTime) {
299         m_tempoBarLabel->setText
300         (tr("at the start of measure %1.").arg(barNo + 1));
301         m_tempoChangeStartOfBar->setEnabled(false);
302     } else {
303         m_tempoBarLabel->setText(
304             tr("in the middle of measure %1.").arg(barNo + 1));
305         m_tempoChangeStartOfBar->setEnabled(true);
306     }
307 
308     m_tempoChangeBefore->setEnabled(false);
309     m_tempoChangeBeforeAt->setEnabled(false);
310 
311     bool havePrecedingTempo = false;
312 
313     if (tempoChangeNo >= 0) {
314 
315         timeT lastTempoTime = comp.getTempoChange(tempoChangeNo).first;
316         if (lastTempoTime < m_tempoTime) {
317 
318             RealTime lastRT = comp.getElapsedRealTime(lastTempoTime);
319             const QString lastms = QString::asprintf("%03d", lastRT.msec());
320             int lastBar = comp.getBarNumber(lastTempoTime);
321             m_tempoChangeBeforeAt->setText
322             (tr("        (at %1.%2 s, in measure %3)").arg(lastRT.sec)
323               .arg(lastms).arg(lastBar + 1));
324             m_tempoChangeBeforeAt->show();
325 
326             m_tempoChangeBefore->setEnabled(true);
327             m_tempoChangeBeforeAt->setEnabled(true);
328 
329             havePrecedingTempo = true;
330         }
331     }
332 
333     if (comp.getTempoChangeCount() > 0) {
334 
335         if (havePrecedingTempo) {
336             m_tempoStatusLabel->hide();
337         } else {
338             m_tempoStatusLabel->setText
339             (tr("There are no preceding tempo changes."));
340         }
341 
342         m_tempoChangeGlobal->setEnabled(true);
343 
344     } else {
345 
346         m_tempoStatusLabel->setText
347         (tr("There are no other tempo changes."));
348 
349         m_tempoChangeGlobal->setEnabled(false);
350     }
351 
352     m_defaultBox->setEnabled(false);
353 }
354 
355 void
updateBeatLabels(double qpm)356 TempoDialog::updateBeatLabels(double qpm)
357 {
358     Composition &comp = m_doc->getComposition();
359 
360     // If the time signature's beat is not a crotchet, need to show
361     // bpm separately
362 
363     timeT beat = comp.getTimeSignatureAt(m_tempoTime).getBeatDuration();
364     if (beat == Note(Note::Crotchet).getDuration()) {
365         m_tempoBeatLabel->setText(tr(" bpm"));
366         m_tempoBeatLabel->show();
367         m_tempoBeat->hide();
368         m_tempoBeatsPerMinute->hide();
369     } else {
370         m_tempoBeatLabel->setText("  ");
371 
372         timeT error = 0;
373         m_tempoBeat->setPixmap(NotePixmapFactory::makeNoteMenuPixmap(beat, error));
374         m_tempoBeat->setMaximumWidth(25);
375         if (error) {
376             m_tempoBeat->setPixmap
377                 (NotePixmapFactory::makeToolbarPixmap("menu-no-note"));
378         }
379 
380         m_tempoBeatsPerMinute->setText
381             (QString("= %1 ").arg
382              (int(qpm * Note(Note::Crotchet).getDuration() / beat)));
383         m_tempoBeatLabel->show();
384         m_tempoBeat->show();
385         m_tempoBeatsPerMinute->show();
386     }
387 }
388 
389 void
slotTempoChanged(double val)390 TempoDialog::slotTempoChanged(double val)
391 {
392     updateBeatLabels(val);
393 }
394 
395 void
slotTargetChanged(double)396 TempoDialog::slotTargetChanged(double)
397 {
398     //...
399 }
400 
401 void
slotTempoConstantClicked()402 TempoDialog::slotTempoConstantClicked()
403 {
404     m_tempoRampToNext->setChecked(false);
405     m_tempoRampToTarget->setChecked(false);
406     m_tempoTargetSpinBox->setEnabled(false);
407 }
408 
409 void
slotTempoRampToNextClicked()410 TempoDialog::slotTempoRampToNextClicked()
411 {
412     m_tempoConstant->setChecked(false);
413     m_tempoRampToTarget->setChecked(false);
414     m_tempoTargetSpinBox->setEnabled(false);
415 }
416 
417 void
slotTempoRampToTargetClicked()418 TempoDialog::slotTempoRampToTargetClicked()
419 {
420     m_tempoConstant->setChecked(false);
421     m_tempoRampToNext->setChecked(false);
422     m_tempoTargetSpinBox->setEnabled(true);
423 }
424 
425 void
slotActionChanged()426 TempoDialog::slotActionChanged()
427 {
428     m_defaultBox->setEnabled(m_tempoChangeGlobal->isChecked());
429 }
430 
431 void
accept()432 TempoDialog::accept()
433 {
434     tempoT tempo = Composition::getTempoForQpm(m_tempoValueSpinBox->value());
435     RG_DEBUG << "Tempo is " << tempo;
436 
437     tempoT target = -1;
438     if (m_tempoRampToNext->isChecked()) {
439         target = 0;
440     } else if (m_tempoRampToTarget->isChecked()) {
441         target = Composition::getTempoForQpm(m_tempoTargetSpinBox->value());
442     }
443 
444     RG_DEBUG << "Target is " << target;
445 
446     if (m_timeEditor) {
447 
448         emit changeTempo(m_timeEditor->getTime(),
449                          tempo,
450                          target,
451                          AddTempo);
452 
453     } else {
454 
455         TempoDialogAction action = AddTempo;
456 
457         if (m_tempoChangeBefore->isChecked()) {
458             action = ReplaceTempo;
459         } else if (m_tempoChangeStartOfBar->isChecked()) {
460             action = AddTempoAtBarStart;
461         } else if (m_tempoChangeGlobal->isChecked()) {
462             action = GlobalTempo;
463             if (m_defaultBox->isChecked()) {
464                 action = GlobalTempoWithDefault;
465             }
466         }
467 
468         emit changeTempo(m_tempoTime,
469                          tempo,
470                          target,
471                          action);
472     }
473 
474     QDialog::accept();
475 }
476 
477 void
slotTapClicked()478 TempoDialog::slotTapClicked()
479 {
480     QTime now = QTime::currentTime();
481 
482     if (m_tapMinusOne != QTime()) {
483 
484         int ms1 = m_tapMinusOne.msecsTo(now);
485 
486         if (ms1 < 10000) {
487 
488             int msec = ms1;
489 
490             if (m_tapMinusTwo != QTime()) {
491                 int ms2 = m_tapMinusTwo.msecsTo(m_tapMinusOne);
492                 if (ms2 < 10000) {
493                     msec = (ms1 + ms2) / 2;
494                 }
495             }
496 
497             int bpm = 60000 / msec;
498             m_tempoValueSpinBox->setValue(bpm);
499         }
500     }
501 
502     m_tapMinusTwo = m_tapMinusOne;
503     m_tapMinusOne = now;
504 }
505 
506 
507 
508 void
slotHelpRequested()509 TempoDialog::slotHelpRequested()
510 {
511     // TRANSLATORS: if the manual is translated into your language, you can
512     // change the two-letter language code in this URL to point to your language
513     // version, eg. "http://rosegardenmusic.com/wiki/doc:tempoDialog-es" for the
514     // Spanish version. If your language doesn't yet have a translation, feel
515     // free to create one.
516     QString helpURL = tr("http://rosegardenmusic.com/wiki/doc:tempoDialog-en");
517     QDesktopServices::openUrl(QUrl(helpURL));
518 }
519 }
520 
521