README.md 24.6 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
# What is this?

Koch method with real words.  Prepare a list of words to learn Morse
code by the Koch method, using real words.

## TL;DR

If you just want to learn Morse code:

* Find a programm that can play text files as Morse code.

* Choose your language. We have [German](de_DE), [British
  English](en_GB) and [American English](en_US) on offer.

* Let your program play `lesson_01_0.txt` to you at speed 12 wpm = 60
  cpm.  There are several blanks after each word, so you'll get a
  small break after each word to comprehend it.  You may need to copy
  to paper at first, but try to copy in your head only.

* Do not set up your program to introduce additional gaps between
  letters of the same word, so as to slow down the speeds of the words
  below 60 cpm.

* Next, let your program play `lesson_01_1.txt`, then
  `lesson_01_2.txt` and so on.  Keep track how many of the words you
  can copy.  If you are down to five non-copies per lesson file, you
  are done with lesson 01 and can continue to lesson 02.

* The initial lesson 01 teaches you the three letters.  All other
  lessons introduce one new letter per lesson.  Which letters these
  are depends on the language.  You can read up `learning_order.json`
  to find out the details.

* Skip to the next lesson whenever you are down to five or fewer
  non-copies per lesson file.  No reason to listen to all files from 0
  to 9 just because they are there.

* It is recommended to practice for 30 minutes per day.

* For the practice frequency, it is recommended to practice anything
  between daily down to twice a week.  This depending on how much in a
  hurry you are to learn Morse code and how much time you're willing
  to spend per week.

* On each new learning session (day), start by listening again to one
  file (or more, as needed) of the lesson you stopped with at the end
  of the previous learning session.  In that sense, it is not quite
  "one lesson per day".  Proceed further when you have five non-copies
  in that file or less.

With this system, you can expect to learn receiving the 26 letters of
the alphabet as Morse code within some 25 to 30 half-hour learning
sessions.  As you have always practiced at 12 wpm or 60 cpm, that is
the speed you will have mastered.

This speed is already above the dreaded 50 cpm "plateau of unlearning
of thinking", so you'll never get stuck there.

## What is that "Koch method"?

Ludwig Koch was a German scientist who developed, in his research, a
method of teaching Morse code.  This was published in February 1936:

> Ludwig Koch (Braunschweig): "Arbeitspsychologische Untersuchung der
> Tätigkeit bei der Aufnahme von Morsezeichen, zugleich ein neues
> Anlernverfahren für Funker" (Dissertation der Technischen Hochschule
> Braunschweig), Zeitschrift für angewandte Psychologie und
> Charakterkunde, Band 50 Heft 1 u. 2, Februar 1936.

The main results of his research and key points of his method are
(crudely) summarized as follows (all speeds in characters per minute):

* Speeds below 50 invite people to think while listening to Morse
  code.
  
* As long as thinking is involved, speeds above about 50 cannot be
  reached.

* It is important to hear Morse code as characters "automatically"
  without thinking.

* Thinking doesn't work at speeds above 50.  So all Morse code
  training should be at speeds above that, from the very beginning.

* Koch's research established his method as an improvement over the
  Fansworth method of sending individual letters at high speed and
  leaving gaps between them to reduce speed much below 50, which again
  facilitates thinking. For the record: At the time, the method wasn't
  called by its now popular name "Fansworth" yet, Koch calls it
  "Klangbildverfahren", but it was commonly used.

* A moderate speed of 60 is ideal for learning Morse code.  (He also
  experimented with higher initial speeds, but that lead to slower
  learning.)

* One should start initial Morse code practice by being offered two
  different characters only, at speed 60.

* During the initial minutes of the original Koch-method course, one
  listens to those Morse characters, but does not yet know which
  letters they represent.  One simply writes a dot for each letter
  heard.

* Then the characters are disclosed and are copied on paper as heard.

* Practice is always in groups of five letters, as common for the
  encrypted traffic of that day.

* When 90 % of the characters are copied correctly by the learning
  group as a whole, the next (third) letter is introduced.

* This continues throughout the course: Each time the group as a whole
  copies the letters learned thus far 90 % correctly, a new letter is
  added.

* Each new letter is introduced as sound only.  It is not initially
  disclosed which character the letter stands for.  At first, students
  copy a dot for each new letter heard.

* Throughout the course, it is stressed to always put a dot for a
  letter not immediately copied.  Trying to think when a letter is not
  immediately copied is discouraged: Thinking is not likely to help,
  but likely to hinder copying the next letter.

* Course lectures are given in half-hour units.  Koch gives reasons
  why this is the optimal time frame.

* Koch recommends against binge learning.  Even in the common military
  setting of the day, Koch recommends half-hour Morse code sessions
  should not be held more frequently than twice per day.  In his own
  experiments, volunteers from all walks of life were given half-hour
  sessions two or three times a week.

* When the first two half-hour sessions have been completed and the
  90 % rule has been followed, the typical course will have picked up
  4 different characters.  Some courses manage 5.

* Koch observes that people who have not picked up 4 characters after
  two half-hour sessions of learning, fairly typically do not manage
  to learn Morse code at all, even when putting in a lot of effort.
  He recommends using these first two sessions as a sort of entry
  exam.

* It should be mentioned that, in his day, the qualification target
  was speed 100 in copying cipher text and speed 125 in copying plain
  text.

* Each half-hour session starts with a repetition of the previously
  learned material.  Later in the course, in most session, only a
  single new character is introduced.

* The letters p x v y q z tend to be particularly difficult to pick
  up.  Some of these characters may require two sessions before the
  course copies them 90 % and the next character can be introduced.

* Koch gives no clear recommendation in which order to learn the
  letters.  The first letters introduced should be quite
  different-sounding from each other, that much is clear.  One of his
  courses used: h f a b g c d e ch l k i o t m p q r s n u w v y x z.
  This contradicts a recommendation he also gives: The more difficult
  letters should be introduced somewhat early in the course, after
  about a third or so, so they see some repetition later.

* Koch recommends dual pitch: Sending "dits" in a slightly lower pitch
  than "dahs", at first.  The different in pitch should be "quite low"
  (Koch is not specific here). The general impression is that
  difference was not rigorously controlled and may have varied
  somewhat from course session to course session. This dual pitch is
  held up only for the first half or third or so of the course.
  Thereafter, the pitch difference is gradually reduced; so normal
  single pitch is reached some time before the end of the course.
  Koch does not claim dual pitch to be essential, but offers a
  comparison: A dual-pitch course finished three sessions earlier than
  a single-pitch companion course.

* How long does it take to learn Morse code by the Koch method?
  During his research, Koch took the shortcut of teaching only the 26
  letters of the alphabet.  As his courses were taught to conduct
  research, not to train radio people, digits, punctuation, or pro
  signs were not introduced.  Teaching 26 alphabet characters usually
  took 24 to 28 half-hour sessions, apparently spread over some 8 to
  14 weeks (depending on twice or thrice a week schedule).  For the
  record: The text mentions teaching "ch" (no longer a character in
  today's international Morse code as specified by the ITU), but all
  the graphs show 26 characters being taught; the publication is not
  entirely clear here.

It strikes me that in his 70 page paper, Koch never even mentions
Morse code keying training.  Sending does not seem to be an issue that
needs a lot of concern, once reception is mastered.

That rather fits my own experience and that of many other radio
amateurs: Once solid code copy is mastered, keying is reduced to a
mere mechanical task.  There is some concern about avoiding cramping
and "glass fist", yes.  But Morse code knowledge straightforwardly
transfers from copy to keying.

## Koch and today's radio amateur

The speeds are comparable.  In Koch's age, the top-notch professional
radio men on board of ships or airplanes had passed exams demanding
solid copy of 100 characters per minute cipher text or 125 plain, for
five minutes.  Today's radio amateur "high speed club" demands those
same 125 characters per minute, plain text, but for half an hour,
mixed reception and sending.  In contests, many CW stations use
similar speed ranges.

Koch mentions less demanding environments of his days, where average
communication speeds of 60-90 were common.  Today, we have a lot of
Morse code communication going on in these slower 60+ speed ranges, in
particular on the lower bands.  Quite a few radio amateurs need such
relatively slow speeds in order to be able to join the party.

Unfortunately, many have learned to copy Morse code in a way that
involves some kind of thinking or mental processing, rather than
automatism.  Trying to get faster, they have thoroughly practiced, but
what they practice is exactly such mental processing. In the end, they
find themselves stuck at a speed level of maybe 50 or, with lots of
more practice, 60 characters per minute.

That's the end speed that can be reached with mental processing, with
thinking.  In contrast, higher speed requires getting rid of thinking,
replacing the processing with automatism.  This is by no means
impossible, but hard to do.

Many never break that barrier.

That same barrier at about speed 50 cpm was well-known in Koch's days.
His brilliant idea was to sneak new learners around it.

But Koch's method can also be used to break the barrier in those that
already struggle with it.  I happen to know from first-hand personal
experience.

Important differences between then and now exist.  In Koch's days,
radio operators copied to paper all they heard, to be forwarded to the
intended recipients.  Today's Morse code users, we radio amateurs, are
typically ourselves the intended recipients of messages we receive.
There is no need for paper copy of every character received, if we
manage to manage to comprehend the messages directly.

The traditional training back then involved copying five-letter groups
of random text to paper.  If comprehension independent of paper copy
is our goal, this might not be optimal.

I therefore propose doing away with meaningless five-letter groups. If
comprehension is to be trained, let us use comprehensible words.

Can we combine Koch' method with real words?  That's exactly what this
software is about.

## Koch with real words

Pretty much same as Koch, but with real words from your mother tongue
replacing traditional meaningless five-letter groups.

You may choose to copy them on paper.  But I recommend to simply try
to understand them in your head.  You'll know the word once you "have"
it.

Koch starts initial training with two characters.  We have to bite the
lemmon and start with three characters instead.  There is hope that we
can find a few meaningful words that can be spelled with some chosen
three letters.

Which initial three letters allow at least some words to be spelled?
How to choose?

That's where software comes in.

Given our alphabet of 26 letters, there are 2600 choices of three
different letters from that alphabet.  Software can simply and
systematically try out all these 2600 choices, and find the choice
that allows the most words to be spelled with just those three letters
chosen.

In German, those three letters are "a", "p", and "s".  21 short German
words can be spelled with just these three letters. In alphabetic
order, these are:

> aa aas app apps ass papa papas papp pass passa passas ppp pps ps sap
> saps sas sass spa spas spass

21 different words is not a whole lot.  But for the first 20 minutes
of practice or so, such a restricted set of words may just be good
enough.  Then, a new, fourth letter can be added.

Again, software to the rescue. The "t" is a good choice here, adding
it to gives 29 new German words. And so on.  For details, see
`learning_order.json` (of your chosen language).

## Disadvantages

* If you follow these suggestion, you'll learn only the letters and
  have to deal with numbers and other characters separately later.

* The present implementation is based on a dictionary.  So odd, funny
  words are practiced.  We might want an improvement that takes word
  frequency into account.

# How to run this?

## Installation 

Have `docker` installed.

## Preparation

Run, in this directory (that contains this `README.md` file):

```
docker build -t registry.invalid/kmrw:latest .
```

_Beware! There's a `.` at the end of that line.  It is needed._

This prepares the software and a spell checking dictionary and packs
it all into the Docker image.

If you want to develop the code and rebuild frequently, it is
convenient to have a local HTTP proxy, in which case you'd use
something like

```
docker build --build-arg=http_proxy=http://172.18.0.1:3128/ -t registry.invalid/kmrw:latest .
```

If you don't know what a local HTTP proxy is, you can safely ignore
this.

### What's this `registry.invalid`?

(Skip this section if you only want the recipe, not the explanation.)

Docker uses a registry, which is a place in the cloud where Docker
images are served (and, more likely than not, the company that
operates it spies on you).  Out of the box, the `docker` command uses
`registry-1.docker.io` when you don't mention another one.

I would like a way to say "cut out this registry stuff, this
particular Docker image lives locally on my computer only and goes
nowhere".  I would much prefer a Docker image without a registry would
be just that, a Docker image without a registry, but that's not how
the `docker` command has been designed.  But I can achieve the same
effect by using a bogus registry name.

The `.invalid` top level domain has been set aside officially to be
just that - invalid.  A registry ending with ".invalid" will be
nowhere.  (Technically, DNS will not yield an IP for any hostname in
the `.invalid` domain.)

## Run

This is for the people who want to play with the system and create
their own lesson files.

I run the following in a `bash`.

The `docker build ...` command needs to happen in the directory that
has this `README.md` file and, more to the point, the `Dockerfile`.

In contrast, you can run everything that follows in any (one)
directory of your choosing.

Why "(one)"?  Later commands generally depend on files produced by
earlier ones.  So, for smoothest sailing, don't switch directories in
between calls.

Unless you want to switch languages. One directory per language is an
excellent idea.

Some of the reasonably fast running commands produce no output.  This
follows the time-honored "no news is good news" command line
tradition.  If you know about exit values, check those.

I run the commands in a `bash` on a Linux system.  If you are in a
non-Linux environment, you can probably remove the `--user="$(id
--user):$(id --group)"` stances.  If you remove those, there is some
concern: Both the user inside the Docker container and you on the
outside want access to the files.  But then, rumors have it that on
some non-Linux operating systems, the Docker installation handles this
for you automatically.  I don't know; in my spare time, I'm not
polyglot, generally, I never leave Linuxland unless I have to.

### All in one swoop

If you are like me and want to run the whole thing in one swoop, for
all three languages supported (which takes some 4,5 hours on my
laptop):

```
docker run --rm -ti --mount=type=bind,src="$(pwd)",dst=/fromhost --user="$(id --user):$(id --group)" --workdir=/fromhost registry.invalid/kmrw:latest invoke all
```

Or you can do stuff for only one language: Replace the `all`
in the above by `de-de`, or `en-gb`, or `en-us`.  This cuts the
time needed down to about 1,5 hours on my laptop.

### A word on my invoke tasks

Those invoke tasks tend to be conservative.  What one invocation
constructed, the next will not knowingly overwrite.  Even `invoke
--list` will show fewer and fewer possible tasks.

If you actually do want to run stuff again, delete the file that was
produced.

### Wordlist generation

This step generates a list of words, only ASCII characters, one word
per line, from the material of spell checking dictionaries.

Those spell checking dictionaries and accompanying software is in the
Docker container we built. Currently it has German, and the two
English flavors GB and US.

The German command line is:

```
docker run --rm -ti --mount=type=bind,src="$(pwd)",dst=/fromhost --user="$(id --user):$(id --group)" --workdir=/fromhost registry.invalid/kmrw:latest invoke de-de.mk-wordlist-de-de
```

For US-English, replace the `invoke` target `de-de.mk-wordlist-de-de`
with `en-us.mk-wordlist-en-us`, for British English, with
`en-gb.mk-wordlist-en-gb`.


This spills out a bit of output from the spell check software we use.

If all is well, you'd find a file `wordlist.txt` in the language
sub-directory that contains, one word a line, all words the spell
checker knows that are at most 6 characters long.

Reading words in your head gets much harder with increasing word
length.  Hence the restriction to at most 6 characters.

### Letter count file generation

Next, I use that to generate a count, for each letter, in how many
words that letter occurs.  The invoke targets are
`de-de.mk-lettercount-de-de`, `en-gb.mk-lettercount-en-gb`, and
`en-us.mk-lettercount-en-us`.

Each of these results in a plain text file `lettercount.txt`.  Have a
look, if you want to.

### Database generation

Now, the fun stuff with "databases" starts.  We generate a database
`letterset2count.kmrw`.  (The abbreviation `kmrw` is intended to stand
for "Koch method - real words".)  This database contains, for each set
of letters, the number of words that can be build from (spelled with)
those letters.

That file `letterset2count.kmrw` will be 268,435,456 bytes long,
2 ** 26 * 4.

An earlier version took roughly 10 hours on my laptop. After some
optimization, that's down to about 1 hour 20 minutes.

This program tries to keep you entertained by occasionally posting
estimates for when it'll finish. The time printed is (probably) UTC.
With each estimate, it also posts a number, counting down from 2**26 =
67108864. As the hard work is done first, all estimates tend to be
(decreasingly) pessimistic.  The initial estimate is outright
ridiculous.

The invoke targets are `de-de.mk-lettercount-de-de`,
`en-gb.mk-lettercount-en-gb`, and `en-us.mk-lettercount-en-us`.

Upon completion, this process leaves a database file `letterset2count.kmrw` in
your current directory.  For any possible choice of a handful of
letters from the alphabet a...z, that database gives you the number of
words that can be spelled with only the letters from your chosen
handful.

To be interpreted, that database requires the `lettercount.txt` to be
available as well.  The details:

## What's the deal with these database files?

You can skip this section if you are only interested in the results,
not the details.

### Database logic

The databases can be thought of as key/value maps.

The key is a set of letters a-z.  Order of letters is ignored, as is
repetition, so "aab" and "ba" come down to the same set of letters,
hence the same key.

The value is simply an integral number between 0 (inclusive) and
2^{32} (exclusive): The numbers of words than can be spelled with just
the letters from the set.

### Database file layout

Every individual letter is associated with a power of 2.

Which power of 2 depends in the number of words containing that
letter, hence on the language.  Common letters get higher powers of
two, less common letters get lower ones.  To see for yourself, change
to the language directory that has the `lettercount.txt` file and
there, execute this:

```
docker run --rm -ti --mount=type=bind,src=$(pwd),dst=/fromhost --user="$(id --user):$(id --group)" --workdir=/fromhost  registry.invalid/kmrw:latest letter2bitmask.py --lettercount lettercount.txt
```

Given a set of these letters, I simply "bitwise or" the corresponding
powers of two.  (If that sounds like a weird or interesting idea to
you, I recommend you research "bitmasks".)  (If you don't understand
"bitwise or", you can think "add".  As long as the same number isn't
added twice, it boils down to the same thing.)

The result is a number.  That number completely determines the set of
letters, hence the key into the dictionary.

If you have a `lettercount` file in your current directory, you can
explore this mapping from sets of letters to numbers and reverse at
the interactive `python3` prompt.  Here is one sample session.

Gory details:

* The stuff following ">>>" or "..." is what I typed at the Python
  prompt.  I typed two spaces before `map = ...`, and the next line
  was completely empty.  (If you know Python, this will not surprise
  you.)

* You may get different numbers if you use a `letterfile` different
  from mine.  (This particular example was done with the en-US
  wordlist.)

```
andreas@meise:~/amateurfunk/floss/python/koch-method-real-words/en_US
$ docker run --rm -ti --mount=type=bind,src=$(pwd),dst=/fromhost -e HOME=/tmp --user="$(id --user):$(id --group)" --workdir=/fromhost  registry.invalid/kmrw:latest python3
Python 3.7.3 (default, Jul 25 2020, 13:03:44) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from letters_rare_first import from_lettercount_file
from letters_rare_first import from_lettercount_file
>>> from letter2bitmask import Letter2Bitmask
from letter2bitmask import Letter2Bitmask
>>> with open('lettercount.txt', 'r') as file:
with open('lettercount.txt', 'r') as file:
...   map = Letter2Bitmask(from_lettercount_file(file))
  map = Letter2Bitmask(from_lettercount_file(file))
... 

>>> map.number("kochmethod")
map.number("kochmethod")
34826368
>>> map.chars(34826368)
map.chars(34826368)
['c', 'd', 'e', 'h', 'k', 'm', 'o', 't']
>>> map.number(['c', 'd', 'e', 'h', 'k', 'm', 'o', 't'])
map.number(['c', 'd', 'e', 'h', 'k', 'm', 'o', 't'])
34826368
>>> [map.number(ch) for ch in ['c', 'd', 'e', 'h', 'k', 'm', 'o', 't']]
[map.number(ch) for ch in ['c', 'd', 'e', 'h', 'k', 'm', 'o', 't']]
[16384, 65536, 33554432, 2048, 128, 8192, 1048576, 131072]
>>> 16384 | 65536 | 33554432 | 2048 | 128 | 8192 | 1048576 | 131072
16384 | 65536 | 33554432 | 2048 | 128 | 8192 | 1048576 | 131072
34826368
>>> 16384 + 65536 + 33554432 + 2048 + 128 + 8192 + 1048576 + 131072
16384 + 65536 + 33554432 + 2048 + 128 + 8192 + 1048576 + 131072
34826368
>>> exit(0)
exit(0)
```

Letters common in the language are coded to larger numbers.  This
tremendously helps speed up the database generation and also makes
`kmrw` files more easily compressible (with `xz`, `zip`, and the
like).

So, what we now have is a key that still _represents_  a set of
letters, but actually _is_ a mere integral number.

The database format is rather simple.  Here is how it works:

* Multiply that number by 4 to get at a position.  E.g., the number
34826368 would yield a position of 139305472.

* Go to that position in the file, and read the next four bytes.

* Interpret those four bytes as a 32 bit integer - and voila, that is
the value.

In my example, that value happened to be 270:

```
$ docker run --rm -ti --mount=type=bind,src=$(pwd),dst=/fromhost --user="$(id --user):$(id --group)" --workdir=/fromhost  registry.invalid/kmrw:latest /bin/bash -c "dd if=letterset2count.kmrw bs=1 skip=139305472 count=4 | od -td"
4+0 records in
4+0 records out
0000000         270
0000004
4 bytes copied, 0.0213296 s, 0.2 kB/s
```

That can be verified by straightforwardly counting all words in the
`wordlist.txt` file that consist of those letters:

```
$ docker run --rm -ti --mount=type=bind,src=$(pwd),dst=/fromhost --user="$(id --user):$(id --group)" --workdir=/fromhost  registry.invalid/kmrw:latest /bin/bash -c "grep -P '^[cdehkmot]+$' wordlist.txt | wc -l"
270
```

So that's what a `.kmrw` file is.  (If you know what "memory mapped"
means: Internally in my Python program, it's a memory mapped array of
integers. That's all.)

Don't open a `.kmrw` file with standard tools (unless for you, a hex
viewer or similar qualifies as "standard tool").