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