#!/usr/bin/env python3 # -*- coding: utf-8 -*- # NEED_SYMLINK # ************************************************************************* # Copyright (C) 2017 by Paul Lutus * # lutusp@arachnoid.com * # * # This program is free software; you can redistribute it and/or modify * # it under the terms of the GNU General Public License as published by * # the Free Software Foundation; either version 2 of the License, or * # (at your option) any later version. * # * # This program is distributed in the hope that it will be useful, * # but WITHOUT ANY WARRANTY; without even the implied warranty of * # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * # GNU General Public License for more details. * # * # You should have received a copy of the GNU General Public License * # along with this program; if not, write to the * # Free Software Foundation, Inc., * # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * # ************************************************************************* import os import sys import math import random import wave import re import platform import argparse import time import struct import numpy import pyaudio import tty import termios class MorseSender: # create a signal-shaping envelope to suppress clicks @staticmethod def envelope(a, b, t, tc): return ((b - t) * (-a + t))/((b - t + tc) * (-a + t + tc)) # generate a dot, dash, or space def gen_bit(self, delay, mc): # default durations: # dot-time between dots/dashes : 1 # added dot-times between characters : 2 , total 3 # added dot-times between words : 4 , total 7 if mc == None: vs = 0 # volume for a silent pause duration = delay else: # dot-dash time selection vs = 1 # emit sound duration = (1, 3)[mc == '-'] dd = duration * self.dot_time vl = [] for tt in range(int(dd * self.options.rate)): t = tt / self.options.rate v = math.sin(2.0 * math.pi * self.options.freq * t) * self.options.level * vs v *= self.envelope(0, dd, t, self.slope_constant) vl += [v] return [vl] # generate a complete Morse character def gen_char(self, ch,show=False): data = [] chl = ch.lower() if chl == ' ': data += self.gen_bit(self.options.word_delay, None) else: # translate Unicode and other exotics to common if chl in self.translate.keys(): chl = self.translate[chl] if chl in self.mdict.keys(): cs = self.mdict[chl] if len(cs) > 0: for tok in cs: data += self.gen_bit(1, tok) data += self.gen_bit(1, None) # code for time between dots/dashes data += self.gen_bit(self.options.char_delay, None) # code for space between completed characters if(show): sys.stdout.write(ch) sys.stdout.flush() return data # generate audio code def sound_char(self, ch, stream, show=False): data = self.gen_char(ch,show) nc = numpy.concatenate(data) stream.write(nc.astype(numpy.float32).tostring()) # generate morse characters from string def gen_code(self, s): if self.options.opath: wf = wave.open(self.options.opath, 'w') wf.setparams((1, 2, self.options.rate, 0, 'NONE', 'no compression')) for ch in s: data = self.gen_char(ch) for dd in data: dd = [struct.pack('h', int(c * 32767)) for c in dd] wf.writeframes(b''.join(dd)) wf.close() else: p = pyaudio.PyAudio() stream = p.open( format=pyaudio.paFloat32, channels=1, rate=int(self.options.rate), output=True, ) for ch in s: self.sound_char(ch,stream,True) time.sleep(1) stream.stop_stream() stream.close() p.terminate() # unbuffered keyboard input for code practice responses # keep this function for reference def getch(self): fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) try: tty.setraw(fd) ch = sys.stdin.read(1) finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) return ch # note for future Windows version # import msvcrt # return msvcrt.getch() # code listening practice session def learn_code(self): if not self.windows: # set up raw terminal mode fd = sys.stdin.fileno() old_term = termios.tcgetattr(fd) tty.setraw(fd) sys.stdout.write('Listen to code, enter guess:') sys.stdout.flush() dic = self.common top = len(dic) p = pyaudio.PyAudio() stream = p.open( format=pyaudio.paFloat32, channels=1, rate=int(self.options.rate), output=True, ) total = int(self.options.learn_count) errors = 0 och = -1 for t in range(total): # don't send the same character twice repeat = True while repeat: n = int(random.random() * top) ch = list(dic.keys())[n] repeat = och==ch och = ch matched = False while not matched: self.sound_char(ch,stream) # read raw terminal character if not self.windows: tc = sys.stdin.read(1).lower() else: tc = msvcrt.getch().lower() matched = tc==ch if(not matched): errors += 1 # make error sound at 2x frequency origf = self.options.freq self.options.freq = origf * 1.5 self.sound_char('e',stream) self.options.freq = origf if not self.windows: # restore original terminal mode termios.tcsetattr(fd, termios.TCSADRAIN, old_term) print('\nErrors: %d, score: %d%%' % (errors,100 * (total-errors)/total)) time.sleep(1) stream.stop_stream() stream.close() p.terminate() def __init__(self): # Morse code dictionary # rarely heard punctuations self.punctsa = { "\"" : ".-..-.", "$" : "...-..-", "'" : ".----.", "/" : "-..-.", "(" : "-.--.", ")" : "-.--.-", "[" : "-.--.", "]" : "-.--.-", "_" : "..--.-", "/" : "-..-." } # more common punctuations self.punctsb = { ":" : "---...", ";" : "-.-.-.", "@" : ".--.-.", "=" : "-...-", "+" : ".-.-.", "-" : "-....-", "!" : "-.-.--", } # most commonly heard characters self.common = { "." : ".-.-.-", "," : "--..--", "?" : "..--..", "0" : "-----", "1" : ".----", "2" : "..---", "3" : "...--", "4" : "....-", "5" : ".....", "6" : "-....", "7" : "--...", "8" : "---..", "9" : "----.", "a" : ".-", "b" : "-...", "c" : "-.-.", "d" : "-..", "e" : ".", "f" : "..-.", "g" : "--.", "h" : "....", "i" : "..", "j" : ".---", "k" : "-.-", "l" : ".-..", "m" : "--", "n" : "-.", "o" : "---", "p" : ".--.", "q" : "--.-", "r" : ".-.", "s" : "...", "t" : "-", "u" : "..-", "v" : "...-", "w" : ".--", "x" : "-..-", "y" : "-.--", "z" : "--..", } # translate Unicode and other # characters into common ones self.translate = { "“" : "\"", # smart quote to quote "”" : "\"", # smart quote to quote "’" : "'", # smart apostrophe to apostrophe "‘" : "'", # Unicode smart apostrophe to apostrophe "`" : "'", # accent to apostrophe "—" : "-", # Unicode dash to dash } # master list for text-to-code translation self.mdict = {} self.mdict.update(self.punctsa) self.mdict.update(self.punctsb) self.mdict.update(self.common) # import a windows-only library # for the code practice module self.windows = False try: import msvcrt self.windows = True except: None # this value controls the code waveform envelope (ascent and descent) self.slope_constant = .005 # http://www.nu-ware.com/NuCode%20Help/index.html?morse_code_structure_and_timing_.htm # http://www.arrl.org/code-practice-files (for samples of good sending) # a dot's duration is a dot-time tone followed by a dot-time of silence # therefore the theoretical time duration in seconds for one dot is 1.2 / wpm # this differs slightly from the ARRL morse code practice recordings self.dot_constant = 1.2 parser = argparse.ArgumentParser() parser.add_argument( "-s", "--speed", dest="speed", help="Code sending speed WPM, default 13.", default=13, type=float ) parser.add_argument( "-f", "--freq", dest="freq", help="Tone frequency Hz, default 750.", default=750, type=float ) parser.add_argument( "-o", "--output", dest="opath", help="File path to write WAV file, default none." ) parser.add_argument( "-i", "--input", dest="ipath", help="File path to read plain-text data." ) parser.add_argument( "-r", "--rate", dest="rate", help="Audio system data rate, default 44100 samples per second.", default=44100, type=float ) parser.add_argument( "-v", "--volume", dest="level", help="Volume level, default 0.5.", default=0.5, type=float ) parser.add_argument( "-p", "--pipe", dest="pipe", action="store_true", help="Read data from standard input." ) parser.add_argument( "-c", "--char", dest="char_delay", default = 2, type=float, help="Delay between characters in dot-times, default 2 (result 3)." ) parser.add_argument( "-w", "--word", dest="word_delay", default = 4, type=float, help="Delay between words in dot-times, default 4 (result 7)." ) parser.add_argument( "-n", "--number", dest="learn_count", default=10, type=float, help="Length of learn-code sequence, default 10." ) parser.add_argument( "-l", "--learn", dest="learn_code", action="store_true", help="Start code learning session." ) # will print help if no input args if len(sys.argv) < 2: sys.argv.append('-h') (self.options, args) = parser.parse_known_args() # this is used by all the code generator functions self.dot_time = self.dot_constant / self.options.speed if(self.options.learn_code): self.learn_code() else: if self.options.ipath: with open(self.options.ipath) as f: s = f.read() elif self.options.pipe: s = sys.stdin.read() else: s = ' '.join(args) self.gen_code(s) print('') restart_flag = '-RESTART-' if __name__ == "__main__": # a dumb hack to suppress ALSA error messages on Linux if re.search('(?i)linux', platform.system()): # suppress ALSA error messages if restart_flag not in sys.argv: os.system('exec %s %s 2> /dev/null' % (' '.join(sys.argv), restart_flag)) else: sys.argv.remove(restart_flag) MorseSender() else: MorseSender()