1 /*
2 Standard MIDI simple metronome
3 Copyright (C) 2006-2021, Pedro Lopez-Cabanillas <plcl@users.sf.net>
4
5 This library is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 3 of the License, or
8 (at your option) any later version.
9
10 This library is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include "metronome.h"
20 #include <QCommandLineParser>
21 #include <QCoreApplication>
22 #include <QDebug>
23 #include <QReadLocker>
24 #include <QStringList>
25 #include <QTextStream>
26 #include <QIODevice>
27 #include <QWriteLocker>
28 #include <QtAlgorithms>
29 #include <csignal>
30 #include <drumstick/alsatimer.h>
31 #include <drumstick/sequencererror.h>
32
33 #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
34 #define right Qt::right
35 #define left Qt::left
36 #define endl Qt::endl
37 #endif
38
39 QTextStream cout(stdout, QIODevice::WriteOnly);
40 QTextStream cerr(stderr, QIODevice::WriteOnly);
41
42 using namespace drumstick::ALSA;
43
44 /* *************** *
45 * Metronome class *
46 * *************** */
47
Metronome(QObject * parent)48 Metronome::Metronome(QObject *parent) : QObject(parent),
49 m_weak_note(METRONOME_WEAK_NOTE),
50 m_strong_note(METRONOME_STRONG_NOTE),
51 m_weak_velocity(METRONOME_VELOCITY),
52 m_strong_velocity(METRONOME_VELOCITY),
53 m_program(METRONOME_PROGRAM),
54 m_channel(METRONOME_CHANNEL),
55 m_volume(METRONOME_VOLUME),
56 m_pan(METRONOME_PAN),
57 m_resolution(METRONOME_RESOLUTION),
58 m_bpm(TEMPO_DEFAULT),
59 m_ts_num(RHYTHM_TS_NUM),
60 m_ts_div(RHYTHM_TS_DEN),
61 m_noteDuration(NOTE_DURATION),
62 m_portId(-1),
63 m_queueId(-1),
64 m_clientId(-1),
65 m_Stopped(true)
66 {
67 QString name{QStringLiteral("Metronome")};
68 m_Client = new MidiClient(this);
69 m_Client->open();
70 m_Client->setClientName(name);
71 m_Client->setHandler(this);
72 m_Port = new MidiPort(this);
73 m_Port->attach( m_Client );
74 m_Port->setPortName(name);
75 m_Port->setCapability( SND_SEQ_PORT_CAP_READ |
76 SND_SEQ_PORT_CAP_SUBS_READ |
77 SND_SEQ_PORT_CAP_WRITE );
78 m_Port->setPortType( SND_SEQ_PORT_TYPE_MIDI_GENERIC |
79 SND_SEQ_PORT_TYPE_APPLICATION );
80 m_Queue = m_Client->createQueue(name);
81 m_clientId = m_Client->getClientId();
82 m_queueId = m_Queue->getId();
83 m_portId = m_Port->getPortId();
84 m_Port->setTimestamping(true);
85 m_Port->setTimestampQueue(m_queueId);
86 // Get and apply the best available timer
87 TimerId best = Timer::bestGlobalTimerId();
88 QueueTimer qtimer;
89 qtimer.setId(best);
90 m_Queue->setTimer(qtimer);
91 // Start sequencer input
92 m_Client->setRealTimeInput(false);
93 m_Client->startSequencerInput();
94 }
95
~Metronome()96 Metronome::~Metronome()
97 {
98 m_Port->detach();
99 m_Client->close();
100 }
101
handleSequencerEvent(SequencerEvent * ev)102 void Metronome::handleSequencerEvent( SequencerEvent *ev )
103 {
104 if (ev->getSequencerType() == SND_SEQ_EVENT_USR0)
105 metronome_pattern(static_cast<int>(ev->getTick()) + m_patternDuration);
106 delete ev;
107 }
108
metronome_event_output(SequencerEvent * ev)109 void Metronome::metronome_event_output(SequencerEvent* ev)
110 {
111 ev->setSource(static_cast<unsigned char>(m_portId));
112 ev->setSubscribers();
113 ev->setDirect();
114 m_Client->outputDirect(ev);
115 }
116
sendControlChange(int cc,int value)117 void Metronome::sendControlChange(int cc, int value)
118 {
119 ControllerEvent ev(m_channel, cc, value);
120 metronome_event_output(&ev);
121 }
122
sendInitialControls()123 void Metronome::sendInitialControls()
124 {
125 metronome_set_program();
126 metronome_set_controls();
127 metronome_set_tempo();
128 }
129
metronome_set_program()130 void Metronome::metronome_set_program()
131 {
132 ProgramChangeEvent ev(m_channel, m_program);
133 metronome_event_output(&ev);
134 }
135
metronome_schedule_event(SequencerEvent * ev,int tick,bool lb)136 void Metronome::metronome_schedule_event(SequencerEvent* ev, int tick, bool lb)
137 {
138 ev->setSource(static_cast<unsigned char>(m_portId));
139 if (lb) // loop back
140 ev->setDestination(static_cast<unsigned char>(m_clientId), static_cast<unsigned char>(m_portId));
141 else
142 ev->setSubscribers();
143 ev->scheduleTick(m_queueId, tick, false);
144 m_Client->outputDirect(ev);
145 }
146
metronome_note(int note,int tick)147 void Metronome::metronome_note(int note, int tick)
148 {
149 NoteEvent ev(m_channel, note, METRONOME_VELOCITY, m_noteDuration);
150 metronome_schedule_event(&ev, tick, false);
151 }
152
metronome_echo(int tick)153 void Metronome::metronome_echo(int tick)
154 {
155 SystemEvent ev(SND_SEQ_EVENT_USR0);
156 metronome_schedule_event(&ev, tick, true);
157 }
158
metronome_pattern(int tick)159 void Metronome::metronome_pattern(int tick)
160 {
161 int j, t, duration;
162 t = tick;
163 duration = m_resolution * 4 / m_ts_div;
164 for (j = 0; j < m_ts_num; j++) {
165 metronome_note(j ? m_weak_note : m_strong_note, t);
166 t += duration;
167 }
168 metronome_echo(t);
169 }
170
metronome_set_tempo()171 void Metronome::metronome_set_tempo()
172 {
173 QueueTempo t = m_Queue->getTempo();
174 t.setPPQ(m_resolution);
175 t.setNominalBPM(m_bpm);
176 m_Queue->setTempo(t);
177 m_Client->drainOutput();
178 }
179
metronome_set_controls()180 void Metronome::metronome_set_controls()
181 {
182 sendControlChange(MIDI_CTL_MSB_MAIN_VOLUME, m_volume);
183 sendControlChange(MIDI_CTL_MSB_PAN, m_pan);
184 }
185
subscribe(const QString & portName)186 void Metronome::subscribe(const QString& portName)
187 {
188 m_Port->subscribeTo(portName);
189 }
190
stopped()191 bool Metronome::stopped()
192 {
193 QReadLocker locker(&m_mutex);
194 return m_Stopped;
195 }
196
stop()197 void Metronome::stop()
198 {
199 QWriteLocker locker(&m_mutex);
200 m_Stopped = true;
201 m_Client->dropOutput();
202 }
203
shutupSound()204 void Metronome::shutupSound()
205 {
206 sendControlChange( MIDI_CTL_ALL_NOTES_OFF, 0 );
207 sendControlChange( MIDI_CTL_ALL_SOUNDS_OFF, 0 );
208 }
209
play(QString tempo)210 void Metronome::play(QString tempo)
211 {
212 bool ok;
213 m_Stopped = false;
214 m_patternDuration = m_resolution * 4 / m_ts_div * m_ts_num;
215 m_bpm = tempo.toInt(&ok);
216 if (!ok) m_bpm = TEMPO_DEFAULT;
217 cout << "Metronome playing. " << m_bpm << " bpm" << endl;
218 cout << "Press Ctrl+C to exit" << endl;
219 try {
220 sendInitialControls();
221 m_Queue->start();
222 metronome_pattern(0);
223 metronome_pattern(m_patternDuration);
224 while (!stopped())
225 sleep(1);
226 } catch (const SequencerError& err) {
227 cerr << "SequencerError exception. Error code: " << err.code()
228 << " (" << err.qstrError() << ")" << endl;
229 cerr << "Location: " << err.location() << endl;
230 }
231 }
232
233 static Metronome* metronome = nullptr;
234
signalHandler(int sig)235 void signalHandler(int sig)
236 {
237 if (sig == SIGINT)
238 qDebug() << "Caught a SIGINT. Exiting";
239 else if (sig == SIGTERM)
240 qDebug() << "Caught a SIGTERM. Exiting";
241 if (metronome != nullptr) {
242 metronome->stop();
243 metronome->shutupSound();
244 }
245 }
246
main(int argc,char ** argv)247 int main(int argc, char **argv)
248 {
249 const QString PGM_NAME = QStringLiteral("drumstick-metronome");
250 const QString PGM_DESCRIPTION = QStringLiteral("ALSA based command line metronome");
251 const QString ERRORSTR = QStringLiteral("Fatal error from the ALSA sequencer. "
252 "This usually happens when the kernel doesn't have ALSA support, "
253 "or the device node (/dev/snd/seq) doesn't exists, "
254 "or the kernel module (snd_seq) is not loaded. "
255 "Please check your ALSA/MIDI configuration.");
256
257 signal(SIGINT, signalHandler);
258 signal(SIGTERM, signalHandler);
259
260 QCoreApplication app(argc, argv);
261 QCoreApplication::setApplicationName(PGM_NAME);
262 QCoreApplication::setApplicationVersion(QStringLiteral(QT_STRINGIFY(VERSION)));
263
264 QCommandLineParser parser;
265 parser.setApplicationDescription(PGM_DESCRIPTION);
266 auto helpOption = parser.addHelpOption();
267 auto versionOption = parser.addVersionOption();
268 QCommandLineOption portOption({"p","port"}, "Destination, MIDI port identifier.", "client:port");
269 parser.addOption(portOption);
270 QCommandLineOption bpmOption({"b","bpm"}, "Tempo, in beats per minute (default=120).", "BPM", "120");
271 parser.addOption(bpmOption);
272 parser.process(app);
273
274 if (parser.isSet(versionOption) || parser.isSet(helpOption)) {
275 return 0;
276 }
277
278 try {
279 metronome = new Metronome();
280 if (parser.isSet(portOption)) {
281 QString port = parser.value(portOption);
282 metronome->subscribe(port);
283 } else {
284 cerr << "Destination Port is mandatory" << endl;
285 parser.showHelp();
286 }
287
288 QString bpm("120");
289 if (parser.isSet(bpmOption)) {
290 bpm = parser.value(bpmOption);
291 }
292 metronome->play(bpm);
293
294 } catch (const SequencerError& ex) {
295 cerr << ERRORSTR << " Returned error was: " << ex.qstrError() << endl;
296 } catch (...) {
297 cerr << ERRORSTR << endl;
298 }
299 delete metronome;
300 return 0;
301 }
302