1
#define DEBUGLVL 0
2
#include "mydebug.h"
3
4
/**
5
 * @file
6
 * @author Holger Schurig
7
 *
8
 * @section DESCRIPTION
9
 *
10
 * Generic class that
11
 * -# can translate cleartext into morse
12
 * -# can play translated morse
13
 *
14
 * Oh, err, it doesn't actually play, but instead generate appropriate
15
 * signals which other classes can then use to play, display or to control a
16
 * rig.
17
 *
18
 * @section LICENSE
19
 *
20
 * This program is free software; you can redistribute it and/or
21
 * modify it under the terms of the GNU General Public License as
22
 * published by the Free Software Foundation; either version 2 of
23
 * the License, or (at your option) any later version.
24
 *
25
 * This program is distributed in the hope that it will be useful, but
26
 * WITHOUT ANY WARRANTY; without even the implied warranty of
27
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
28
 * General Public License for more details at
29
 * http://www.gnu.org/copyleft/gpl.html
30
 */
31
32
33
#include "morse.h"
34
#include "characters.h"
35
36
#include <QTimer>
37
38
39
bool Morse::contains(const QString &clearText) const
40
{
41
	MYTRACE("Morse::contains(%s)", qPrintable(clearText));
42
	foreach(MorseCharacter m, chars) {
43
		MYVERBOSE("  at %s", qPrintable(m.sign));
44
		if (m.sign == clearText)
45
			return true;
46
	}
47
	return false;
48
}
49
50
const QString Morse::operator[] (const QString &clearText) const
51
{
52
	MYTRACE("Morse::operator[](%s)", qPrintable(clearText));
53
	foreach(MorseCharacter m, chars) {
54
		if (m.sign == clearText)
55
			return m.code;
56
	}
57
	return QString::null;
58
}
59
60
61
/*!
62
 * \brief Class to generate morse code
63
 *
64
 * Simple usage:
65
 * \code
66
 *   GenerateMorse *morse = new GenerateMorse(this);
67
 *   connect(morse, SIGNAL(playSound(int)), audio, SLOT(playSound(int)) );
68
 *   morse->append("73 de");
69
 *   morse->append("dh3hs");
70
 *   morse->play();
71
 * \endcode
72
 */
73
GenerateMorse::GenerateMorse(QObject *parent)
74
	: QObject(parent)
75
	, playElement(0)
76
	, playIdx(0)
77
	, clearIdx(0)
78
	, playLoop(false)
79
	, playWpm(5)
80
	, ditFactor(1)
81
	, dahFactor(1)
82
	, intraFactor(1)
83
	, charFactor(1)
84
	, wordFactor(1)
85
{
86
	MYTRACE("GenerateMorse::GenerateMorse");
87
88
	playTimer = new QTimer(this);
89
	playTimer->setSingleShot(true);
90
	connect(playTimer, SIGNAL(timeout()), this, SLOT(slotPlayNext()) );
91
}
92
93
94
/*
95
 * Section: adding morse characters and cleartext
96
 */
97
98
/* NOTE: all values must be different ! */
99
const int ditLength = 1;
100
const int dahLength = 3;
101
const int intraSpacing = -1;
102
const int charSpacing = -3;
103
const int wordSpacing = -7;
104
105
/*!
106
 * \brief Add morse code (in string representation) to morse storage
107
 *
108
 * The string representation will be converted into lengths of dits and
109
 * dahs, but also into spacings (intra-character spacing, between character
110
 * spacing, word spacing) and then added to \ref morse. The cleartext is also
111
 * added to \ref clearText.
112
 *
113
 * @param dahdits  string representation of morse, e.g. "-."
114
 * @param clear    clear-text of the same
115
 *
116
 * \sa append
117
 */
118
void GenerateMorse::appendMorse(const QString &dahdits, const QString &clear)
119
{
120
	MYTRACE("GenerateMorse::appendMorse('%s', '%s')",
121
	        qPrintable(dahdits), qPrintable(clear) );
122
123
	int last = 0;
124
125
	morse.append(0);
126
	clearText.append(clear);
127
128
	for (int i = 0; i < dahdits.size(); ++i) {
129
		char c = dahdits.at(i).toAscii();
130
131
		if (!morse.isEmpty()) {
132
			last = morse.last();
133
			//MYVERBOSE("last %d", last);
134
		}
135
136
		MYVERBOSE("  append '%c'", c);
137
		switch (c) {
138
		case '.':
139
			if (last > 0) {
140
				MYVERBOSE("  DIT %d", intraSpacing);
141
				morse.append(intraSpacing);
142
			}
143
			MYVERBOSE("  dit %d, last %d", ditLength, last);
144
			morse.append(ditLength);
145
			break;
146
		case '-':
147
			if (last > 0) {
148
				MYVERBOSE("  DAH %d", intraSpacing);
149
				morse.append(intraSpacing);
150
			}
151
			MYVERBOSE("  dah %d, last %d", dahLength, last);
152
			morse.append(dahLength);
153
			break;
154
		case ' ':
155
			if (last == charSpacing) {
156
				MYVERBOSE("  spc %d, last %d (promoted from charSpacing)", wordSpacing, last);
157
				morse.last() = wordSpacing;
158
			} else {
159
				MYVERBOSE("  spc %d, last %d", wordSpacing, last);
160
				morse.append(wordSpacing);
161
			}
162
			break;
163
		}
164
	}
165
	if (!morse.isEmpty())
166
		last = morse.last();
167
	if (last != wordSpacing) {
168
		MYVERBOSE("  chr %d, last %d", charSpacing, last);
169
		morse.append(charSpacing);
170
	}
171
}
172
173
174
/*!
175
 * \brief Add clear-text to morse storage
176
 *
177
 * @param str       clear text, e.g. "73 de dh3hs". \a str can be a single
178
 *                  character, or it can be a character sequence. In all
179
 *                  cases should it be lower-case! \n
180
 *                  Use upper-case only to add morse pro-signs. Currently
181
 *                  the program understands
182
 *                  - AR: Stop (end of message)
183
 *                  - AS: Wait (for 10 seconds)
184
 *                  - BK: Break
185
 *                  - BT: Separator within message
186
 *                  - CL: Going off the air
187
 *                  - CT: Start (beginning of message)
188
 *                  - DO: Shift to japanese wabun code
189
 *                  - HH: Error
190
 *                  - KA: same as CT
191
 *                  - KN: Invitation to a specific named station to transmit
192
 *                  - SK: End (end of contact)
193
 *                  - SN: Understood
194
 *                  - SO: SOS without word spacings
195
 *                  - TV: same as BT
196
 *                  - VA: sama as SK
197
 *                  - VE: same as SN
198
 * @param addSpace  optional flag that tells us if a space (as a means to
199
 *                  delimit words) should be added. This flag is by default
200
 *                  on, you'd want to set it to falls if you characters
201
 *                  one-by-one, e.g. when directly feeding typed characters
202
 *                  into the class.
203
 *
204
 * \sa appendMorse
205
 */
206
void GenerateMorse::append(const QString &str, bool addSpace)
207
{
208
	MYTRACE("GenerateMorse::append('%s')", qPrintable(str) );
209
210
	if (codes.contains(str)) {
211
		appendMorse(codes[str], str);
212
		return;
213
	}
214
215
	for (int i=0; i<str.count(); i++) {
216
		QString c = str.mid(i,1);
217
		MYVERBOSE("processing '%s'", qPrintable(c));
218
		if (c.at(0).isUpper()) { // && i < str.count()-1) {
219
			c = str.mid(i,2);
220
			MYVERBOSE("  special '%s'", qPrintable(c));
221
			i++;
222
		}
223
		if (codes.contains(c)) {
224
			appendMorse(codes[c], c);
225
		} else {
226
			qFatal("no morse code for '%s' known", qPrintable(c));
227
		}
228
	}
229
	if (addSpace)
230
		appendMorse(" ", " ");
231
}
232
233
234
/*!
235
 * \brief Clear the morse storage
236
 *
237
 * Clears \ref morse, \ref clearText and resets the replay indexes \ref playIdx
238
 * and \ref clearIdx.
239
 */
240
void GenerateMorse::clear()
241
{
242
	MYTRACE("GenerateMorse::clear");
243
244
	morse.clear();
245
	clearText.clear();
246
	playElement = 0;
247
	playIdx = 0;
248
	clearIdx = 0;
249
	emit currElement(playElement);
250
}
251
252
253
int GenerateMorse::totalElements(int from) const
254
{
255
	MYTRACE("GenerateMorse::totalElements");
256
257
	int dits = 0;
258
	int dahs = 0;
259
	int intras = 0;
260
	int chars = 0;
261
	int words = 0;
262
263
	int sum = 0;
264
	for (int i=from; i < morse.count(); i++) {
265
		int elem = morse.at(i);
266
		switch (elem) {
267
		case ditLength: dits++; break;
268
		case dahLength: dahs++; break;
269
		case intraSpacing: intras++; break;
270
		case charSpacing: chars++; break;
271
		case wordSpacing: words++; break;
272
		}
273
		//MYVERBOSE("%d: %d", i, elem );
274
		sum += qAbs(elem);
275
	}
276
	MYVERBOSE("  elements %d, dits %d, dahs %d, intras %d, chars %d, words %d",
277
	          sum, dits, dahs, intras, chars, words);
278
	return sum;
279
}
280
281
282
283
/*
284
 * Section: playing
285
 */
286
287
/*!
288
 * \brief Play morse
289
 *
290
 * Used to actually play morse. As this class doesn't play anything at all,
291
 * it just emits signals, so other class(es) can visualize / audiolize
292
 * things.
293
 *
294
 * Internally \ref playTimer get's started and whenever something has to be
295
 * done, one of the signals are emitted. This happens in \ref slotPlayNext().
296
 *
297
 * \sa stop(), slotPlayNext(), setLoop()
298
 */
299
300
void GenerateMorse::play()
301
{
302
	MYTRACE("GenerateMorse::play");
303
304
	if (!playLoop) {
305
		// Remove all trailing silence. Note that we don't do this
306
		// in loop mode, otherwise we'd jam the end of the text to
307
		// the start of the text with no pause at all.
308
		while (!morse.isEmpty() && morse.last() < 0)
309
			morse.removeLast();
310
		while (!clearText.isEmpty() && clearText.last().isEmpty())
311
			clearText.removeLast();
312
	}
313
314
	emit maxElements(totalElements());
315
316
	// Nothing left?  Bail out!
317
	if (morse.isEmpty()) {
318
		emit hasStopped();
319
		return;
320
	}
321
322
	// One one small silence back, to stop the sound
323
	morse.append(intraSpacing);
324
325
	playElement = 0;
326
	playIdx = 0;
327
	clearIdx = 0;
328
	playTimer->start(0);
329
}
330
331
332
/*!
333
 * \brief Stop playing morse
334
 *
335
 * \sa start()
336
 */
337
void GenerateMorse::stop()
338
{
339
	MYTRACE("GenerateMorse::stop");
340
341
	playTimer->stop();
342
}
343
344
345
/*!
346
 * \brief Handle next morse event
347
 *
348
 * Called from \ref playTimer. \ref playIdx is used to step throught \ref morse
349
 * and \ref clearIdx is used to step throught \ref clearText. The contents of
350
 * both lists are then used to emit various signals.
351
 *
352
 * Any class (or classes) receiving those signals can then generate sound
353
 * or controll the PTT of your rig and similar things.
354
 *
355
 * As \ref morse contains times in "elements" units, the actual timing
356
 * is controlled by \ref setWpm() and the other \c setXFactor() functions.
357
 *
358
 * \sa play(), playSound(bool), playSound(unsigned int), hasStopped(),
359
 * charChanged() symbolChanged()
360
 */
361
void GenerateMorse::slotPlayNext()
362
{
363
	MYTRACE("GenerateMorse::slotPlayNext");
364
365
	MYVERBOSE("playIdx %d, count %d", playIdx, morse.count());
366
	if (playIdx >= morse.count() ) {
367
		if (playLoop) {
368
			playElement = 0;
369
			playIdx = 0;
370
			clearIdx = 0;
371
			emit currElement(0);
372
		} else {
373
			emit hasStopped();
374
			return;
375
		}
376
	}
377
378
	int t = morse[playIdx];
379
	MYVERBOSE("playIdx %d, play %d", playIdx, t);
380
	playElement += qAbs(t);
381
	emit currElement(playElement);
382
383
	float factor = 1;
384
	switch (t) {
385
	case 0:
386
		{
387
			QString clear = clearText[clearIdx++];
388
			MYVERBOSE("  cleartext '%s'", qPrintable(clear));
389
			emit charChanged(clear);
390
		}
391
		break;
392
	case ditLength:
393
		emit symbolChanged(".");
394
		emit playSound(true);
395
		factor = ditFactor;
396
		break;
397
	case dahLength:
398
		emit symbolChanged("-");
399
		emit playSound(true);
400
		factor = dahFactor;
401
		break;
402
	case intraSpacing:
403
		emit symbolChanged(" ");
404
		emit playSound(false);
405
		factor = intraFactor;
406
		break;
407
	case charSpacing:
408
		emit charChanged(" ");
409
		emit symbolChanged(" ");
410
		emit playSound(false);
411
		factor = charFactor;
412
		break;
413
	case wordSpacing:
414
		emit charChanged(" ");
415
		emit symbolChanged(" ");
416
		emit playSound(false);
417
		factor = wordFactor;
418
		break;
419
	}
420
421
	playIdx++;
422
423
424
	// http://forums.qrz.com/showthread.php?t=178795
425
426
	// paris = 50 elements -> 1 word per minuts = 50 elements per minute
427
	//
428
	//        60s / wpm * 50 elements = x  s/element
429
	// 1000 * 60s / wpm * 50 elements = x ms/element
430
	//     60000s / wpm * 50 elements = x ms/element
431
	//      1200  / wpm               = x ms/element
432
433
	float length = 1200 / playWpm;
434
	//MYVERBOSE("elem length: %f ms", length);
435
	length *= qAbs(t) * factor;
436
437
	if (t > 0)
438
		emit playSound((unsigned int)length);
439
440
	//MYVERBOSE("delay: %f ms", length);
441
	playTimer->start(length);
442
}
443
444
445
/*!
446
 * \brief return current WPM (words per minute) speed
447
 *
448
 * The returned number is based on the word "paris " and the current
449
 * settings of \ref playWpm, \ref ditFactor, \ref dahFactor, \ref intraFactor,
450
 * \ref charFactor and \ref wordFactor.
451
 *
452
 * \sa setWpm
453
 */
454
float GenerateMorse::getWpm() const
455
{
456
	MYTRACE("GenerateMorse::getWpm");
457
458
	// "paris " has exactly
459
	//    10 dits
460
	//     4 dahs
461
	//     9 intra-character spaces
462
	//     4 character spaces
463
	//     1 word space
464
465
	float length = 0;
466
	float ms = 1200 / playWpm;
467
	length += ms * 10 * ditLength * ditFactor;
468
	length += ms *  4 * dahLength * dahFactor;
469
	length += ms * -9 * intraSpacing * intraFactor;
470
	length += ms * -4 * charSpacing * charFactor;
471
	length += ms * -1 * wordSpacing * wordFactor;
472
	length /= 1000; // convert von ms to s
473
	return 60 / length;
474
}
475
476
477
/*!
478
 * \brief Makes the play() function loop endlessly
479
 *
480
 * @param loop   boolean if play() should loop endlessly
481
 * \sa play()
482
 */
483
void GenerateMorse::setLoop(bool loop)
484
{
485
	// First make sure that we have a pause at the end if we're in loop
486
	// mode, but no pause if not.
487
	while (!morse.isEmpty() && morse.last() < 0)
488
		morse.removeLast();
489
	while (!clearText.isEmpty() && clearText.last().isEmpty())
490
		clearText.removeLast();
491
	if (loop)
492
		append(" ");
493
494
	playLoop = loop;
495
}
496
497
498
499
/*!
500
 * \brief Set replay speed in words per minute
501
 *
502
 * Setting 5 wpm means that playing "paris " 5 times will take exactly
503
 * one minute.
504
 *
505
 * \note The current implementation won't play the last " " word spacing,
506
 * so if you time this function, you'll measure slightly less than one
507
 * minute. However, if you \ref setLoop() on, you'll get exactly one minute.
508
 *
509
 * \sa getWpm()
510
 */
511
void GenerateMorse::setWpm(float wpm)
512
{
513
	playWpm = wpm;
514
};
515
516
517
/*!
518
 * \brief Set factor to make dits longer or shorter
519
 *
520
 * Normally set to 1.0. If you call \ref setWpm() with high speeds, then
521
 * dits might no longer become audioble. So you can artificially make
522
 * them longer. \ref setDitFactor(2.0) makes them twice as long as normally.
523
 *
524
 * You can direcly wire this slot to the \c valueChanged() signal of a
525
 * \c QDoubleSpinBox.
526
 */
527
void GenerateMorse::setDitFactor(float factor)
528
{
529
	ditFactor = factor;
530
};
531
532
533
/*!
534
 * \brief Set factor to make dahs longer or shorter
535
 */
536
void GenerateMorse::setDahFactor(float factor)
537
{
538
	dahFactor = factor;
539
};
540
541
542
/*!
543
 * \brief Set factor to make the silence between dits and dahs longer or
544
 * shorter
545
 *
546
 * Here the dits and dahs inside a morse sign are meant. E.g. for the morse
547
 * code of the character "n" the duration between the "-" and "." can be
548
 * made shorter when calling \ref setIntraFactor(0.8).
549
 */
550
void GenerateMorse::setIntraFactor(float factor)
551
{
552
	intraFactor = factor;
553
};
554
555
556
/*!
557
 * \brief Set factor to make the silence between characters longer or
558
 * shorter
559
 *
560
 * This can be used for the Koch/Farnsworth learning method, e.g. you can
561
 * have a high \ref setWpm(), but a low \ref setCharFactor().
562
 */
563
void GenerateMorse::setCharFactor(float factor)
564
{
565
	charFactor = factor;
566
};
567
568
569
/*!
570
 * \brief Set factor to make the silence between words longer or
571
 * shorter
572
 *
573
 * This can be used for the Koch/Farnsworth learning method, e.g. you can
574
 * have a high \ref setWpm(), but a low \ref setCharFactor().
575
 */
576
void GenerateMorse::setWordFactor(float factor)
577
{
578
	wordFactor = factor;
579
};