/*** morse - an automatic morse code teaching machine /* /* Copyright (c) 1978 Howard Cunningham all rights reserved /* Copyright (c) 1988 Jim Wilson /* /* Revision History: 04/18/78 initial pascal version adapted from 8080 /* 12/04/81 corrected typo (div for mod in select) /* 06/18/88 translated to C /* 07/14/98 corrected accelerated character introduction /* omitted in translation to C /* /* Reference: "A Fully Automatic Morse Code Teaching Machine", /* QST, (May 1977), ARRL, Newington CT */ #include /*** tweekable parameters and magic numbers */ #define WPM 15 /* Initial codespeed in words-per-minute */ #define isdah(c) (c&1) /* Mask l.s.b. to test dot or dash */ #define TONE_ON 1162 /* Period in microseconds (1Khz) */ #define TONE_OFF 0 /* Special value for silent period */ #define NLINE 25 /* Number of lines on PC screen and */ #define LINLEN 72 /* number of characters on each line */ #define BARHT (NLINE-6) /* Maximum height of error rate bars */ #define GOOD 0 /* The best one can do */ #define BAD 255 /* The worst one can do */ #define DIT(wpm) (1395/(wpm)) /* Dit length for beep() - roughly in mS */ #define BRGRY 176 /* Bargraph character for inactive letters */ #define BRCHR 219 /* Bargraph character for active latters */ /*** display character attributes for various screen regions * * These are 8-bit values left-justified into a 16-bit field (as * required by pc(), prc() in beep.s). For example, with a color- * capable display in one of its text modes: * * FrgbIRGB 00000000 * * where 'f' is set to flash (or blink) the character. * 'rgb' are set to specify the presense of red, * green and/or blue in the background. * 'I' is set to "intensify" the character * 'RGB' are set to specify the presense of red, * green and/or blue in the forground. */ #define NORM 0x1E00 /* Bargraph, herald: Intensified Y/Blue */ #define HLGT 0x1F00 /* Active characters: intensified W/Blue */ #define GREY 0x1700 /* Inactive characters, W/Blue */ #define CWIN 0x0700 /* Code window, white on black */ /*** external functions from beep.s or the C runtime library */ extern srand(), rand(); /* seed, return random number (in C runtime) */ extern beep(); /* Sound tone for an interval (in beep.s) */ extern unsigned ticks(); /* Return mS since last call (in beep.s) */ extern char resp(); /* nonblocking keyboard input (in beep.s) */ extern unsigned tod(); /* DOS raw time-of-day counter (for srand()) */ unsigned num = 2; /* Characters introduced so far (start with 2) */ unsigned dit = DIT(20); /* Default to 20 wpm */ /*** Letter[] holds character codes and error info for each letter /* MAXNUM is the largest size of the active alphabet. /* The 0th element is not part of the alphabet. It is used to /* store an overall error indication for convenient display. */ #define MAXNUM (sizeof(letter)/sizeof(struct lt)-1) /* maximum alphabet */ #define OVERALL letter[0] /* for easy graphing!! */ struct lt {char ascii; char morse; unsigned char error;} letter[] = { {'*', 0, GOOD}, {'Q', 033, BAD}, {'7', 043, BAD}, {'Z', 023, BAD}, {'G', 013, BAD}, {'0', 077, BAD}, {'9', 057, BAD}, {'8', 047, BAD}, {'O', 017, BAD}, {'1', 076, BAD}, {'J', 036, BAD}, {'P', 026, BAD}, {'W', 016, BAD}, {'L', 022, BAD}, {'R', 012, BAD}, {'A', 006, BAD}, {'M', 007, BAD}, {'6', 041, BAD}, {'B', 021, BAD}, {'X', 031, BAD}, {'D', 011, BAD}, {'Y', 035, BAD}, {'C', 025, BAD}, {'K', 015, BAD}, {'N', 005, BAD}, {'2', 074, BAD}, {'3', 070, BAD}, {'F', 024, BAD}, {'U', 014, BAD}, {'4', 060, BAD}, {'5', 040, BAD}, {'V', 030, BAD}, {'H', 020, BAD}, {'S', 010, BAD}, {'I', 004, BAD}, {'T', 003, BAD}, {'E', 002, BAD}, }; /*** main - teach morse code /* /* This program repeatedly selects a letter and teaches it to the /* student. The student can request an evaluation and/or terminate /* the session. */ main(int argc, char *argv[]) { herald(); /* Print program herald */ srand(tod()); /* seed random number generator */ bgs(); /* Show bargraph on screen */ do { pms(CWIN, "\013\n\n\n\n"); /* Clear code window */ beep(TONE_OFF, 600); /* Take a breath before we start */ do ; while (teach(select())); /* teach letters */ } while (menu()); /* grade and perhaps quit */ } /*** herald - emit program proclamation /* /* Herald() displays a copyright message and pauses long enough for /* the student to copy down an important URL (if she wants). */ herald() { pms(NORM, "\f\n\n\n\n\n\n\n\n" "\230Morse Code Training Program\n" "\222(c) 1998 Ward Cunningham and Jim Wilson\n" "\222Permission granted to distribute freely\n" "\227without profit or modification\n\n" "\227See http://c2.com/~ward/morse/\n\n\n\n\n\n" "\217Try to type the character before the computer.\n" "\226Or, press Enter to take a break."); beep(TONE_OFF, 3000); } /*** weight - return weighted sum of two values /* /* Weight() is passed two unsigned character parameters. /* It returns a weighted average of the two values. /* /* Calling Sequence: average = weight(v1, v2); /* /* where (unsigned char) "average", "v1" and "v2" are related: /* /* average = .875 * v1 + .125 * v2 */ unsigned weight(unsigned v1, unsigned v2) { return ((7*v1 + v2 + 4) / 8); /* "+ 4" forces rounding */ } /*** teach - teach a morse letter /* /* Teach() is passed an index to a letter. It sends the letter /* in Morse on the PC's speaker and patiently waits for the /* student to press the corresponding key. If too much time /* is taken, teach() gives a hint and resends the character /* until the student finally gets the answer right. The student /* is graded on his performance. /* /* Teach() returns a flag indicating that the student wants more. /* /* Calling Sequence: if (teach(lesson)) ...; /* /* where (int) "lesson" is an index into the array letters[]. /* "..." only if the student want's another character. */ teach(int lesson) { /* letter[] index */ register struct lt *l; /* Pointer into letter[] */ unsigned time; /* time required for answer */ unsigned score; /* GOOD or BAD for this letter */ int guess, answer; /* student's answer, real solution */ static int column = 0; /* Characters remaining on line */ static unsigned give = 3500; /* Milliseconds to wait for answer */ l = &letter[lesson]; /* Point register at lesson */ answer = l->ascii; /* Get solution to problem */ score = GOOD; /* Assume the best */ do { if (column <= 0) { /* If at end-of-line */ pc('\n'+CWIN); /* output newline and */ column = LINLEN; /* rearm counter */ } send(l->morse); /* Send Morse character. */ while (guess = resp()) /* Flush typeahead, but */ if (guess == '\r') /* if student want's a break, */ return column = 0; /* grant the wish */ ticks(); time = 0; /* Reset stopwatch. */ do { /* Loop to wait for answer: */ guess = resp(); /* Get student response (if any) */ if (guess == answer) /* If correct answer, */ break; /* stop and update gradebook */ if (guess == '\r') /* If student wants break, reset column */ return column = 0; /* count (for next lesson), return */ time += ticks(); /* Add elapsed time */ } while (time <= give); /* Stop if student too slow */ if (guess != answer) /* If ever time out w.o. correct */ score = BAD; /* answer, deduct some points */ pc(answer+CWIN); /* Echo correct answer */ pc(' '+CWIN); column -= 2; /* and adorn it with a blank */ give = weight(give, 2*time);/* Update response time */ if (give>6000) give = 6000; /* (but limit to 5-6 S.) */ beep(TONE_OFF, 250); /* Wait a brief interval */ } while (guess != answer); grade(lesson, score); /* Update gradebook */ /* Student has answered correctly, so we'll give her some more. * Grade() has updated the overall and specific error estimates. * If the overall rate is low, and no specific character is too * bad, we will add another character to the training alphabet. */ if (OVERALL.error > BAD * 3/10) return 1; for (l = &letter[num]; l > &OVERALL; l--) if (l->error > BAD * 4/10) return 1; add_ltr(); return 1; } /*** send - send charcter in morse code /* /* Send() is passed a morse coded letter (see reference). /* It sends the character at speed "WPM" on the PC's speaker. /* /* Calling Sequence: send(morse); /* /* where (char) "morse" is a stop-bit-prepended morse character. */ send(char code) { register element; do { element = dit; if (isdah(code)) element *= 3; beep(TONE_ON, element); beep(TONE_OFF, dit); } while ((code >>= 1) != 1); } /*** select - choose a letter from the current alphabet /* /* Select() returns the index of a letter chosen from the /* currently active alphabet. The probability of chosing /* a letter is proportional to the estimated error rate /* for that letter. /* /* Calling Sequence: new_letter = select(); /* /* where (int) "new_letter" is the index of the chosen element /* from letter[]. */ select() { register int sum; /* error probability accumulator */ register int l; /* working index into letter[] */ sum = 0; l = 1; do { sum += letter[l++].error + 1; } while (l <= num); sum = rand() % sum; do { sum -= letter[--l].error + 1; } while (sum > 0); return (l); } /*** menu - display 4-line menu, one-line prompt, and get choice /* /* Menu() displays a simple 4-line menu, and prompts for a selection. /* It awaits a valid choice and acts on it. It returns nonzero or /* zero when "C(ontinue)" or "Q(uit)" are selected (respectively). /* /* Calling Sequence: int menu(); */ menu() { pms(CWIN, /* NOTE: Change showspd(), below, if you change menu!!! */ "\nCharacter Code Speed:\203Practice Alphabet:\206Training:\n" "\202S(low --- 10 wpm)\207A(dd another letter)\203C(ontinue training)\n" "\202M(edium - 15 wpm)\207R(emove last letter)\203Q(uit program)\n" "\202F(ast --- 20 wpm)\nYour choice? (SMFARCQ): "); showspd(); /* Show codespeed (see NOTE, above) */ for (;;) switch (resp()) { case 'S': dit = DIT(10); showspd(); break; case 'M': dit = DIT(15); showspd(); break; case 'F': dit = DIT(20); showspd(); break; case 'A': add_ltr(); break; case 'R': rem_ltr(); break; case 'C': return 1; case 'Q': return 0; } } /*** showspd - show current codespeed on menu /* /* Showspd() erases column 19 on rows 21, 22, and 23 of the display, /* and then rewrites a '<' in one of the erased positions to show /* the code speed. */ showspd() { register int i; for (i = 21; i < 24; i++) prc(' '+CWIN, i, 19); switch (dit) { case DIT(10): i = 21; break; case DIT(15): i = 22; break; case DIT(20): i = 23; break; } prc('<'+CWIN, i, 19); } /*** pcs - put character and a space /* /* Pcs() is passed a character to display (converted to an integer). It /* displays the character (via putchar()) and then emits a trailing blank. /* /* This dates back to the old BRB video terminal where displaying the /* characters with intervening blanks made the upper-case much more /* readable. /* /* Calling Sequence: pcs(int c); */ pcs(int c) { putchar(c); putchar(' '); } /*** pms - put message to screen /* /* Pms() is passed a integer character "attribute" (that controls the /* character color, blinking, etc.) and a string that can contain: /* /* 1. '\f' to clear the screen (using the attribute as fill) and /* move the cursor to the leftmost column in the first line. /* 2. '\n' to advance the cursor to first column of the next line /* (scrolling if necessary). /* 3. 0x80-flagged characters which represent sequences of blanks /* (after stripping the leftmost bit). /* 4. Normal ASCII characters (<= 0x7F) which will be displayed /* on the screen. /* /* Calling Sequence: pms(int attribute, char *message); */ pms(int attr, char *msg) { register int c; register int i; while (c = *msg++) { /* While more message left to display, */ if (c & 0x80) { /* If compressed blank, */ for (c &= 0x7F; c; c--)/* display a sequence of blanks */ pc(attr+' '); } /* If other character, */ else pc(attr+c); /* Let the low-level code handle it */ } } /*** barht - convert score (in [0,BAD]) to bar graph height /* /* Barht() is passed an integer error rate in [0,BAD]. It linearly /* scales this value to [0,BARHT], the number of illuminated lines /* in a crude bar graph displayed in a column on the screen. /* /* Calling Sequence: int barht(int score); */ barht(int score) { return (score*BARHT + BAD/2)/BAD; } /*** grade - update error estimates /* /* Grade() is passed an index to the current letter and GOOD /* if the student got the right answer or BAD if she had to /* be told the answer. Grade() updates the particular and /* overall error rate estimates and revises the displayed /* bargraph to display the change. /* /* Calling sequence: grade(ltr, grade); /* /* where (int) "ltr" is the index of the current letter /* and (int) "grade" is GOOD or BAD. */ grade(unsigned ltr, unsigned g) { /* Index to lesson, test results */ register struct lt *l; /* Pointer to element being dated */ int old, new; /* Old, bar first display row */ l = &letter[ltr]; /* Aim register into letter[] */ ltr = (ltr-1)*2; /* Ltr = column in bargraph */ old = BARHT - barht(l->error); /* 1st row that now has a bar */ update(l, g); /* Update specific error rate */ if (update(&OVERALL, g) < BAD * 1/10) /* If overall error rate low, */ update(l, g); /* accelerate specific rate */ new = BARHT - barht(l->error); /* 1st row that SHOULD have bar */ while (new != old) { /* Until graph bar is right sized, */ if (new > old) /* If too tall, */ prc(' ' +NORM, old++, ltr); /* chop it down to size */ else /* If too short, */ prc(BRCHR+NORM, --old, ltr); /* build it up a bit */ } } /*** update - update error rate /* /* Update() is passed the address of an element of letter[], and a grade /* (either GOOD or BAD). It updates the .error probability estimate /* field, and returns the new value for this field. /* /* Calling sequence: int update(struct lt *ltr, int grade); */ update(struct lt *l, unsigned g) { return l->error = weight(l->error, g); } /*** bgs - display (initial) bargraph on screen /* /* Bgs() erases the herald display (see herald(), above), and replaces /* it with a crude bargraph of the student's error rate (i.e. the /* letter[].error fields). Below each bar, the corresponding letter /* (i.e. the letter[].ascii) is displayed. Active letters are dis- /* tinguished by displaying their bargraph as a solid bar; inactive /* letters' bars are "greyed". /* /* Calling Sequence: bgs(); */ bgs() { register int i = 0; /* Index into letter[] */ pc('\f'+NORM); /* Clear screen */ do drwbar(++i); /* Draw bargraph */ while (i < MAXNUM); } /*** add_ltr, rem_ltr - add letter/remove letter from training aphabet /* /* Add_ltr() checks "num", the number of letters in the training alphabet /* and increases it (assuming it is not maxed out already). The appear- /* ance of the new character (its bargraph bar) is redrawn as solid to /* reflect its new status. /* /* Rem_ltr() decreases the number of letters, (but not less than 1) and /* "greys" the bargraph appropriately. /* /* Calling Sequence: add_ltr(); ... rem_ltr(); */ add_ltr() { if (num < MAXNUM) drwbar(++num); } /* Room to add? redraw bar */ rem_ltr() { if (num > 1) drwbar(num--); } /* More than 1? redraw bar */ /*** drwbar - draw bargraph bar /* /* Drw_bar() is passed an index into letter[] (>= 1). It draws a /* a "greyed" or "solid" (depending upon whether the corresponding /* letter is part of the current trainin alphabet) whose height is /* proportional to letter[].error. /* /* Calling Sequence: drwbar(int index); */ drwbar(int c) { /* Column (nee index) for display */ register int r = letter[c].ascii; /* Letter, later its bargraph row */ int s = BARHT-barht(letter[c].error); /* Number of spaces ABOVE bar */ int bc = BRCHR; /* Bargraph char or greyed version */ if (c <= num) r += HLGT; /* If part of training set, highlite on */ else r += GREY, bc = BRGRY; /* Otherwise, deemphasize and grey its bar */ c = c-1 << 1; /* C = column for letter and its bar */ prc(r, BARHT, c); /* Display annotation */ r = BARHT; do { /* Loop to draw a bar (and space above) */ if (r-- == s) bc = ' '; /* If time to switch from draw to erase */ prc(bc+NORM, r, c); /* Put (char) at (screen) row, column */ } while (r); /* Until reach top of screen */ }