TinyModem
Header-only software audio modem library
Loading...
Searching...
No Matches
MorseDecoder.h
Go to the documentation of this file.
1#pragma once
2
3#include <math.h>
4#include <string.h>
5
6#include "AudioTools.h"
7#include "ModemBase.h"
8#include "Morse.h"
9
10namespace tinymodem {
11
32class MorseDecoder : public ModemDecoder {
33 public:
34 MorseDecoder() = default;
35 MorseDecoder(Print& out) { setOutput(out); }
36
39
41 void setAudioInfo(audio_tools::AudioInfo info) override {
42 if (info.sample_rate == cfg.sample_rate && info.channels == cfg.channels &&
43 info.bits_per_sample == cfg.bits_per_sample)
44 return;
45 cfg.sample_rate = info.sample_rate;
46 cfg.channels = info.channels;
47 cfg.bits_per_sample = info.bits_per_sample;
48 if (is_active) begin();
49 }
50
51 audio_tools::AudioInfo audioInfo() override { return cfg; }
52
53 using audio_tools::AudioDecoder::begin;
54
57 cfg = config;
58 return begin();
59 }
60
61 bool begin() override {
62 is_active = false;
63 if (!cfg.isValid()) return false;
64 if (!input.begin(cfg.bits_per_sample, cfg.channels, cfg.channel))
65 return false;
66 block = lroundf(cfg.sample_rate * BLOCK_SEC);
67 if (block < 1) block = 1;
68 // window of half a dot (at least one block)
69 window = lroundf(cfg.unitTime() * cfg.sample_rate * WINDOW_UNITS);
70 if (window < block) window = block;
71 history.resize(2 * window);
72 double w = 2.0 * M_PI * cfg.frequency / cfg.sample_rate;
73 step_re = cos(w);
74 step_im = sin(w);
75 resetState();
76 is_active = true;
77 return true;
78 }
79
81 void end() override {
82 if (!is_active) return;
83 flush();
84 is_active = false;
85 }
86
88 void flush() override {
89 if (!is_active) return;
90 // feed silence, so that the last character is completed
91 size_t n = (size_t)(cfg.rx_timeout_units + 2) * cfg.unitTime() *
92 cfg.sample_rate;
93 for (size_t j = 0; j < n && !is_finished && carrier; j++)
94 addSample(0.0f, false);
95 if (carrier) endOfCarrier();
96 bool finished = is_finished;
97 resetState();
98 is_finished = finished;
99 }
100
102 size_t write(const uint8_t* data, size_t len) override {
103 if (!is_active) return 0;
104 input.write(data, len, [this](float x) { addSample(x, true); });
105 return len;
106 }
107
109 bool isCarrier() override { return carrier; }
110
112 bool isFinished() override { return is_finished; }
113
115 float wpm() { return unit > 0 ? 1.2f / unit : cfg.wpm; }
116
118 MorseConfig& config() { return cfg; }
119
120 operator bool() override { return is_active; }
121
122 protected:
123 static constexpr float BLOCK_SEC = 0.002f;
125 static constexpr float WINDOW_UNITS = 0.5f;
127 static constexpr float DEBOUNCE_UNITS = 0.3f;
128 static constexpr int MAX_CODE = 16;
130 static constexpr float MIN_SNR = 5.0f;
131
133 bool is_active = false;
134 bool is_finished = false;
136 uint32_t noise_seed = 1;
137 // level measurement
138 int block = 96, window = 480, count = 0;
139 audio_tools::Vector<float> history; // i, q
140 int pos = 0;
141 float sum_i = 0, sum_q = 0;
142 float re = 1, im = 0, step_re = 1, step_im = 0;
143 uint32_t sample_count = 0;
144 float signal_level = 0, noise_level = 0;
145 float noise_mean = 0; // average level without tone
146 bool first_block = true;
147 // on/off detection
148 bool tone_on = false; // debounced state
149 int pending = 0; // blocks with a different raw state
150 float duration = 0; // seconds in the current state
151 float unit = 0.06f; // seconds per unit
152 float bias = 0; // marks are measured shorter by this (seconds)
153 float last_mark = 0; // length of the last mark
154 char last_element = 0;
155 bool carrier = false;
156 bool word_pending = false;
157 char code[MAX_CODE + 1];
158 int code_len = 0;
159 unsigned ndata = 0;
160
161 void resetState() {
162 for (size_t j = 0; j < history.size(); j++) history[j] = 0;
163 pos = count = 0;
164 sum_i = sum_q = 0;
165 re = 1;
166 im = 0;
168 first_block = true;
169 tone_on = false;
170 pending = 0;
171 duration = 0;
172 unit = cfg.unitTime();
173 bias = 0;
174 last_element = 0;
175 carrier = false;
176 word_pending = false;
177 code_len = 0;
178 is_finished = false;
179 input.reset();
180 }
181
183 float noise() {
184 noise_seed = noise_seed * 1103515245u + 12345u;
185 return ((noise_seed >> 8) & 0xFFFF) / 65535.0f - 0.5f;
186 }
187
188 void addSample(float x, bool add_noise) {
189 if (is_finished) return;
190 if (add_noise && cfg.rx_noise != 0.0f) x += noise() * cfg.rx_noise * 2.0f;
191 // mix to the baseband and sum over the window
192 float vi = x * re, vq = -x * im;
193 float* old = history.data() + pos * 2;
194 sum_i += vi - old[0];
195 sum_q += vq - old[1];
196 old[0] = vi;
197 old[1] = vq;
198 if (++pos == window) pos = 0;
199 float r = re * step_re - im * step_im;
200 im = re * step_im + im * step_re;
201 re = r;
202 if (++sample_count % 1024 == 0) recalculate();
203 if (++count == block) {
204 count = 0;
205 processBlock(2.0f * sqrtf(sum_i * sum_i + sum_q * sum_q) / window);
206 }
207 }
208
210 void recalculate() {
211 sum_i = sum_q = 0;
212 for (int k = 0; k < window; k++) {
213 sum_i += history[k * 2];
214 sum_q += history[k * 2 + 1];
215 }
216 float mag = sqrtf(re * re + im * im);
217 re /= mag;
218 im /= mag;
219 }
220
222 void processBlock(float level) {
223 float block_sec = (float)block / cfg.sample_rate;
224 // signal level: fast attack, slow decay; noise level: while off
225 if (level > signal_level)
226 signal_level += 0.5f * (level - signal_level);
227 else
228 signal_level += 0.002f * (level - signal_level);
229 float threshold = noise_level + 0.5f * (signal_level - noise_level);
230 float hysteresis = 0.1f * (signal_level - noise_level);
231 // a tone must be clearly above the noise (noise alone has peaks)
232 bool raw = tone_on ? level > threshold - hysteresis
233 : level > threshold + hysteresis &&
234 level >= cfg.min_amplitude &&
235 level > MIN_SNR * noise_mean;
236 // the noise level follows the levels without tone (also not while a new
237 // tone is confirmed): it falls fast and rises slowly, so that the end of
238 // a tone hardly raises it
239 if (first_block) {
240 noise_mean = level;
241 first_block = false;
242 }
243 if (!tone_on && !raw) {
244 noise_level += (level < noise_level ? 0.1f : 0.01f) * (level - noise_level);
245 // the average excludes the end and the start of a tone: the window
246 // must have left the last tone and the level must be close to the
247 // noise floor
248 if (duration >= WINDOW_UNITS * unit && level < 2.5f * noise_level)
249 noise_mean += 0.02f * (level - noise_mean);
250 }
251
252 duration += block_sec;
253 if (raw != tone_on) {
254 // the change must last for a while
255 if (++pending * block_sec >= DEBOUNCE_UNITS * unit) {
256 float changed = pending * block_sec;
257 float length = duration - changed;
258 tone_on = raw;
259 pending = 0;
260 duration = changed;
261 if (tone_on)
262 endOfSpace(length);
263 else
264 endOfMark(length);
265 }
266 } else {
267 pending = 0;
268 }
269 // the end of a character, a word and the transmission is detected
270 // during the space
272 }
273
274 void endOfMark(float raw_length) {
275 const MorseCode::Timing& t = MorseCode::timing(cfg.alphabet);
276 last_mark = raw_length;
277 float length = raw_length + bias;
278 float units = length / unit;
279 char element;
280 float expected;
281 if (units < (t.dot + t.dash) / 2) {
282 element = '.';
283 expected = t.dot;
284 } else if (cfg.alphabet == MorseAlphabet::INTERNATIONAL ||
285 units < (t.dash + t.long_dash) / 2) {
286 element = '-';
287 expected = t.dash;
288 } else if (units < (t.long_dash + t.extra_long_dash) / 2) {
289 element = '_';
290 expected = t.long_dash;
291 } else {
292 element = '=';
293 expected = t.extra_long_dash;
294 }
295 // follow the speed of the sender with the dots and dashes
296 if (cfg.adaptive_speed && (element == '.' || element == '-'))
297 unit += 0.2f * (length / expected - unit);
298 if (code_len < MAX_CODE) code[code_len++] = element;
299 last_element = element;
300 }
301
302 void endOfSpace(float raw_length) {
303 if (!carrier) {
305 return;
306 }
307 const MorseCode::Timing& t = MorseCode::timing(cfg.alphabet);
308 float length = raw_length - bias;
309 float units = length / unit;
310 // a dot and an element space have the same length: the difference is
311 // twice the bias
312 if (last_element == '.' && code_len > 0 &&
313 units < (t.element_gap + t.inner_gap + 1) / 2) {
314 bias += 0.2f * ((raw_length - last_mark) / 2 - bias);
315 float max_bias = 0.3f * unit;
316 if (bias > max_bias) bias = max_bias;
317 if (bias < -max_bias) bias = -max_bias;
318 }
319 // a space within an American character
320 if (cfg.alphabet == MorseAlphabet::AMERICAN && code_len > 0 &&
321 units >= (t.element_gap + t.inner_gap) / 2 &&
322 units < (t.inner_gap + t.letter_gap) / 2 && code_len < MAX_CODE)
323 code[code_len++] = ' ';
324 if (word_pending) {
325 output(' ');
326 word_pending = false;
327 }
328 }
329
331 void checkSpace(float raw_length) {
332 const MorseCode::Timing& t = MorseCode::timing(cfg.alphabet);
333 float units = (raw_length - bias) / unit;
334 float letter = cfg.alphabet == MorseAlphabet::AMERICAN
335 ? (t.inner_gap + t.letter_gap) / 2
336 : (t.element_gap + t.letter_gap) / 2;
337 if (code_len > 0 && units >= letter) finishCharacter();
338 if (units >= (t.letter_gap + t.word_gap) / 2 && ndata > 0)
339 word_pending = true;
340 if (units >= cfg.rx_timeout_units) endOfCarrier();
341 }
342
344 code[code_len] = 0;
345 char ch = MorseCode::decode(cfg.alphabet, code);
346 code_len = 0;
347 output(ch == 0 ? '?' : ch); // '?' for an unknown code
348 }
349
350 void output(char ch) {
351 ndata++;
352 if (p_print != nullptr) p_print->write((uint8_t)ch);
353 }
354
356 carrier = true;
357 ndata = 0;
358 code_len = 0;
359 word_pending = false;
360 status("### CARRIER morse %s %.1f wpm @ %.1f Hz ###\n",
361 cfg.alphabet == MorseAlphabet::AMERICAN ? "american"
362 : "international",
363 (double)cfg.wpm, (double)cfg.frequency);
364 }
365
367 if (code_len > 0) finishCharacter();
368 carrier = false;
369 word_pending = false;
370 status("\n### NOCARRIER ndata=%u wpm=%.1f ###\n", ndata, (double)wpm());
371 if (cfg.rx_one) is_finished = true;
372 }
373};
374
375} // namespace tinymodem
Common API of the demodulators (FSKDecoder, MFSKDecoder): PCM audio data is written to the decoder an...
Definition ModemBase.h:344
void status(const char *fmt,...)
Definition ModemBase.h:367
static const Timing & timing(MorseAlphabet alphabet)
Definition Morse.h:153
static char decode(MorseAlphabet alphabet, const char *code)
Character of a code: returns 0 if the code is unknown.
Definition Morse.h:169
void endOfSpace(float raw_length)
Definition MorseDecoder.h:302
float last_mark
Definition MorseDecoder.h:153
void flush() override
Processes the remaining samples (e.g. at the end of the input).
Definition MorseDecoder.h:88
void endOfMark(float raw_length)
Definition MorseDecoder.h:274
MorseConfig defaultConfig()
Provides a default configuration: International Morse code, 20 wpm.
Definition MorseDecoder.h:38
void processBlock(float level)
On/off decision of one block with the level of the tone.
Definition MorseDecoder.h:222
uint32_t sample_count
Definition MorseDecoder.h:143
char last_element
Definition MorseDecoder.h:154
float step_re
Definition MorseDecoder.h:142
bool isCarrier() override
true while a transmission is received
Definition MorseDecoder.h:109
float noise_level
Definition MorseDecoder.h:144
void endOfCarrier()
Definition MorseDecoder.h:366
float noise_mean
Definition MorseDecoder.h:145
int window
Definition MorseDecoder.h:138
bool is_active
Definition MorseDecoder.h:133
int pos
Definition MorseDecoder.h:140
void end() override
Processes the remaining samples and reports the end of the carrier.
Definition MorseDecoder.h:81
void finishCharacter()
Definition MorseDecoder.h:343
MorseDecoder(Print &out)
Definition MorseDecoder.h:35
float step_im
Definition MorseDecoder.h:142
float re
Definition MorseDecoder.h:142
bool isFinished() override
true if the decoding was stopped (rx_one)
Definition MorseDecoder.h:112
MorseConfig & config()
The configuration which was provided in begin().
Definition MorseDecoder.h:118
char code[MAX_CODE+1]
Definition MorseDecoder.h:157
float im
Definition MorseDecoder.h:142
float sum_i
Definition MorseDecoder.h:141
float signal_level
Definition MorseDecoder.h:144
static constexpr float DEBOUNCE_UNITS
changes which are shorter than this (in units) are ignored
Definition MorseDecoder.h:127
MorseConfig cfg
Definition MorseDecoder.h:132
void setAudioInfo(audio_tools::AudioInfo info) override
Defines the format of the input audio data.
Definition MorseDecoder.h:41
audio_tools::Vector< float > history
Definition MorseDecoder.h:139
float duration
Definition MorseDecoder.h:150
static constexpr float WINDOW_UNITS
length of the measurement window in units
Definition MorseDecoder.h:125
static constexpr float BLOCK_SEC
Definition MorseDecoder.h:123
PcmInput input
Definition MorseDecoder.h:135
float noise()
uniform random value in the range -0.5 to 0.5
Definition MorseDecoder.h:183
uint32_t noise_seed
Definition MorseDecoder.h:136
bool first_block
Definition MorseDecoder.h:146
void recalculate()
Avoids the accumulation of rounding errors.
Definition MorseDecoder.h:210
static constexpr float MIN_SNR
min ratio of the tone level to the average noise level
Definition MorseDecoder.h:130
unsigned ndata
Definition MorseDecoder.h:159
bool is_finished
Definition MorseDecoder.h:134
size_t write(const uint8_t *data, size_t len) override
Provide PCM data to the decoder.
Definition MorseDecoder.h:102
int pending
Definition MorseDecoder.h:149
bool carrier
Definition MorseDecoder.h:155
int code_len
Definition MorseDecoder.h:158
static constexpr int MAX_CODE
Definition MorseDecoder.h:128
int count
Definition MorseDecoder.h:138
float unit
Definition MorseDecoder.h:151
float wpm()
Current speed of the sender in words per minute.
Definition MorseDecoder.h:115
bool word_pending
Definition MorseDecoder.h:156
int block
Definition MorseDecoder.h:138
void resetState()
Definition MorseDecoder.h:161
void addSample(float x, bool add_noise)
Definition MorseDecoder.h:188
bool begin() override
Definition MorseDecoder.h:61
bool tone_on
Definition MorseDecoder.h:148
void checkSpace(float raw_length)
Ends the character, the word or the transmission during a space.
Definition MorseDecoder.h:331
float bias
Definition MorseDecoder.h:152
float sum_q
Definition MorseDecoder.h:141
bool begin(MorseConfig config)
Starts the processing with the indicated configuration.
Definition MorseDecoder.h:56
void output(char ch)
Definition MorseDecoder.h:350
void startOfCarrier()
Definition MorseDecoder.h:355
audio_tools::AudioInfo audioInfo() override
Definition MorseDecoder.h:51
Assembles the PCM frames from the written bytes and provides the samples of the selected channel as f...
Definition ModemBase.h:170
TinyModem: software audio modems and packets with error correction.
Definition AX25.h:11
@ AMERICAN
Definition Morse.h:18
@ INTERNATIONAL
International Morse code (ITU-R M.1677): dots and dashes of 3 units.
Definition Morse.h:15
Lengths in units.
Definition Morse.h:146
float extra_long_dash
Definition Morse.h:147
float dot
Definition Morse.h:147
float letter_gap
Definition Morse.h:148
float element_gap
Definition Morse.h:148
float dash
Definition Morse.h:147
float inner_gap
Definition Morse.h:148
float long_dash
Definition Morse.h:147
float word_gap
Definition Morse.h:148
Configuration of the Morse code modem (MorseEncoder, MorseDecoder): a tone which is switched on and o...
Definition Morse.h:31