/***************************************************************************
* Copyright (C) 2017, Paul Lutus *
* *
* 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. *
***************************************************************************/
var Morse = Morse || {}
Morse.id = function(ss) {
return document.getElementById(ss);
}
Morse.letters = {
"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" : "--..",
}
Morse.numbers = {
"0" : "-----",
"1" : ".----",
"2" : "..---",
"3" : "...--",
"4" : "....-",
"5" : ".....",
"6" : "-....",
"7" : "--...",
"8" : "---..",
"9" : "----.",
}
Morse.common = {
"," : "--..--",
"." : ".-.-.-",
"?" : "..--..",
"=" : "-...-",
}
Morse.exotic = {
"!" : "-.-.--",
"\"" : ".-..-.",
"$" : "...-..-",
"'" : ".----.",
"/" : "-..-.",
"(" : "-.--.",
")" : "-.--.-",
"[" : "-.--.",
"]" : "-.--.-",
"+" : ".-.-.",
"_" : "..--.-",
"/" : "-..-.",
":" : "---...",
";" : "-.-.-.",
"-" : "-....-",
"@" : ".--.-.",
}
// translate Unicode and other
// characters into common ones
Morse.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
}
Morse.write_cookie = function() {
var cvalue = Morse.create_cookie();
var d = new Date();
d.setTime(d.getTime() + (90*24*60*60*1000));
var expires = "expires="+ d.toUTCString();
document.cookie = "Morse=" + cvalue + ";" + expires + ";path=/";
}
Morse.read_cookie = function() {
var decoded_cookie = decodeURIComponent(document.cookie);
var result = decoded_cookie.replace(/[\s\S]*?Morse=([^;]*)[\s\S]*/m,"$1");
Morse.decode_cookie(result);
}
Morse.loadContext = function() {
if (typeof Morse.audio !== 'undefined') {
Morse.audio.close();
}
if (typeof AudioContext !== "undefined") {
return new AudioContext();
} else if (typeof webkitAudioContext !== "undefined") {
return new webkitAudioContext();
} else if (typeof mozAudioContext !== "undefined") {
return new mozAudioContext();
}
else {
alert("Sorry! This browser doesn't support the audio\n\nfeatures this program requires.\n\nPlease update or replace your browser.");
return null;
}
}
Morse.reset_string = function() {
if(Morse.target_div != null) {
Morse.target_div.innerHTML = Morse.string;
Morse.target_div = null;
}
}
Morse.generate_code_char = function(i,key,output) {
var result = '';
if(key != '') {
var value = Morse.mdict[key];
skey = key.toUpperCase();
var skey = skey.replace(/"/g,""");
skey = skey.replace(/'/g,"'");
result = '
\n';
keys = Object.keys(Morse.mdict);
var klen = keys.length;
Morse.char_keys = [];
var j = keys.indexOf('a');
for (var i = 0;i < klen;i++) {
var k = (i+j) % klen;
Morse.char_keys.push(keys[k]);
}
var cols = 5
var rows = 13;
for (var y = 0; y < rows;y++) {
output += '
';
for (var x = 0; x < cols;x++) {
var k = x * rows + y;
var key = '';
if(k < klen) {
key = Morse.char_keys[k];
}
output = Morse.generate_code_char(k,key,output);
}
output += '
';
}
output += '
\n';
Morse.id('code_list').innerHTML = output;
}
Morse.set_defaults = function() {
// These values are derived from
// https://en.wikipedia.org/wiki/Morse_code
Morse.dot_constant = 1.2; // units seconds
// NOTE: these timings are additive
Morse.dot_time_between_dots_dashes = 1;
Morse.dot_time_between_characters = 2;
// The result value between characters
// is the sum of the above values (2+1) = 3
Morse.dot_time_between_words = 4;
// It's the same here -- the result
// is (4+2+1) = 7
Morse.speed_wpm = 13;
Morse.frequency = 750;
Morse.volume = 0.5;
Morse.slope_constant = .005;
Morse.binary_letters = true;
Morse.binary_numbers = false;
Morse.binary_common = false;
Morse.binary_exotic = false;
}
Morse.reset_all = function() {
Morse.set_defaults();
Morse.write_controls();
Morse.setup_all();
}
Morse.setup_all = function() {
Morse.stop_process();
Morse.read_controls();
Morse.write_controls();
Morse.setup_oscillators();
}
Morse.check_input = function() {
if(Morse.new_input) {
Morse.setup_all();
}
}
Morse.append = function(array,add) {
for(key in add) {
array[key] = add[key];
}
}
Morse.init = function() {
Morse.audio = Morse.loadContext();
if(!Morse.audio) {
return;
}
Morse.COSINE = 0;
Morse.NOISE = 1;
Morse.ERROR = 2;
Morse_running = false;
Morse.practice = false;
Morse.new_input = false;
Morse.set_practice_string();
Morse.errormult = 1.0;
Morse.wait_time = 100;
Morse.mdict = {};
Morse.append(Morse.mdict,Morse.letters);
Morse.append(Morse.mdict,Morse.numbers);
Morse.append(Morse.mdict,Morse.common);
Morse.append(Morse.mdict,Morse.exotic);
Morse.generate_code_list();
Morse.control_strings = [
'dot_constant',
'dot_time_between_dots_dashes',
'dot_time_between_characters',
'dot_time_between_words',
'speed_wpm',
'frequency',
'volume',
'slope_constant',
'binary_letters',
'binary_numbers',
'binary_common',
'binary_exotic',
];
// default values
Morse.set_defaults();
Morse.high_volume = 1;
Morse.low_volume = 0;
// this time constant suppresses keying clicks
Morse.record_button = Morse.id('record');
Morse.target_div = null;
Morse.sanitize_default_text();
Morse.setup_control_panel();
Morse.read_cookie();
Morse.populate_span(Morse.common,'common_punct');
Morse.populate_span(Morse.exotic,'exotic_punct');
Morse.setup_oscillators();
Morse.stop_process();
}
Morse.populate_span = function(code,target) {
var s = '';
for(var v in code) {
s += ' ' + v;
}
Morse.id(target).innerHTML = s.trim();
}
Morse.read_controls = function() {
var pn = Object.getOwnPropertyNames(Morse);
for (var i in Morse.control_strings) {
var tag = Morse.control_strings[i];
var control = Morse.id(tag);
if(tag.match(/binary_.*/)) {
Morse[tag] = control.checked;
}
else {
var v = control.value;
v = parseFloat(v);
v = (isNaN(v))?0:v;
if(tag == 'slope_constant') {
// this value cannot be zero
v = Math.max(v,1e-10);
}
Morse[tag] = v;
}
}
Morse.new_input = false;
}
Morse.write_controls = function() {
var sum = 0;
var pn = Object.getOwnPropertyNames(Morse);
for (var i in Morse.control_strings) {
var tag = Morse.control_strings[i];
var control = Morse.id(tag);
if(tag.match(/binary_.*/)) {
control.checked = Morse[tag];
}
else {
control.value = Morse[tag];
if(i >= 1 && i <= 3) {
sum += parseFloat(control.value);
expl = Morse.id('ex_' + tag);
expl.innerHTML = ' = ' + sum;
}
}
}
Morse.new_input = false;
}
Morse.create_cookie = function() {
Morse.read_controls();
var pn = Object.getOwnPropertyNames(Morse);
var output = [];
for (var i in Morse.control_strings) {
var tag = Morse.control_strings[i];
output[i] = Morse[tag];
}
return output;
}
Morse.decode_cookie = function(str) {
var values = str.split(",");
// test: valid cookie?
if(values.length == Morse.control_strings.length) {
var pn = Object.getOwnPropertyNames(Morse);
for (var i in Morse.control_strings) {
var tag = Morse.control_strings[i];
if(tag.match(/binary_.*/)) {
Morse[tag] = values[i] == 'true';
}
else {
Morse[tag] = parseFloat(values[i]);
}
}
Morse.write_controls();
}
}
Morse.make_control = function(id) {
return '
' + id + '
';
}
Morse.setup_control_panel = function() {
var tags = [];
var output = '
';
for(var i in Morse.control_strings) {
tag = Morse.control_strings[i];
if(tag.match(/binary_.*/)) {
break;
}
tags.push(tag);
output += Morse.make_control(tag);
}
output += '
';
Morse.id('controlpanel').innerHTML = output;
for(var i in tags) {
var tag = tags[i];
Morse.id(tag).addEventListener("keydown", function(e) {
Morse.new_input = true;
if (!e) { var e = window.event; }
if (e.keyCode == 13) {
Morse.setup_all();
}
}, false);
}
Morse.write_controls();
}
Morse.setup = function() {
Morse.times = [];
Morse.index = 0;
Morse.reset_string();
}
Morse.setup_noise_oscillator = function() {
var bufferSize = 4 * Morse.audio.sampleRate;
Morse.noiseBuffer = Morse.audio.createBuffer(1, bufferSize, Morse.audio.sampleRate),
output = Morse.noiseBuffer.getChannelData(0);
for (var i = 0; i < bufferSize; i++) {
output[i] = Math.random() * 2 - 1;
}
Morse.noise = Morse.audio.createBufferSource();
Morse.noise.buffer = Morse.noiseBuffer;
Morse.noise.loop = true;
}
Morse.reset_volume = function() {
Morse.vol.gain.cancelScheduledValues( Morse.audio.currentTime );
Morse.time = Morse.audio.currentTime;
Morse.vol.gain.setTargetAtTime(0,0,0);
Morse.vol.connect(Morse.audio.destination);
Morse.vol.gain.setTargetAtTime(0.0, Morse.time, Morse.slope_constant);
}
Morse.setup_oscillators = function() {
Morse.setup();
Morse.record_button.disabled = true;
Morse.read_controls();
Morse.time = Morse.audio.currentTime;
Morse.start_time = Morse.time;
Morse.times.push(Morse.time);
Morse.vol = Morse.audio.createGain();
Morse.vol.connect(Morse.audio.destination);
Morse.vol.gain.setTargetAtTime(0,0,0);
Morse.setup_noise_oscillator();
var real = new Float32Array(2);
var imag = new Float32Array(2);
// set up a cosine waveform
real[0] = 0;
imag[0] = 0;
real[1] = 1;
imag[1] = 0;
Morse.cosine = Morse.audio.createOscillator();
Morse.errortone = Morse.audio.createOscillator();
var wave = Morse.audio.createPeriodicWave(real, imag);
Morse.cosine.setPeriodicWave(wave);
//Morse.cosine.detune.value = 0;
Morse.cosine.frequency.setTargetAtTime(Morse.frequency,0,0);
Morse.cosine.start(0);
Morse.errortone.setPeriodicWave(wave);
Morse.errortone.detune.setTargetAtTime(0,0,0);
Morse.errortone.frequency.setTargetAtTime(Morse.frequency * 1.5,0,0);
Morse.errortone.start(0);
Morse.noise.start();
Morse.dot_time = Morse.dot_constant / Morse.speed_wpm;
Morse.osc_array = [Morse.cosine, Morse.noise, Morse.errortone];
}
Morse.init_recording = function() {
Morse.recording = Morse.record_button.checked;
if(Morse.recording) {
if(typeof Morse.audio.createMediaStreamDestination === 'undefined') {
alert("Sorry! Your browser doesn't support audio recording.\n\nConsider replacing your browser.");
Morse.recording = false;
Morse.record_button.checked = false;
}
else {
var dest = Morse.audio.createMediaStreamDestination();
// connect to the recording buffer in parallel with ordinary playing
Morse.vol.connect(dest);
// the data buffer
Morse.chunks = [];
Morse.mediaRecorder = new MediaRecorder(dest.stream);
Morse.mediaRecorder.ondataavailable = function(evt) {
Morse.chunks.push(evt.data);
};
Morse.mediaRecorder.onstop = function(evt) {
var blob = new Blob(Morse.chunks, { 'type' : 'audio/ogg' });
document.querySelector("audio").src = URL.createObjectURL(blob);
};
}
}
}
Morse.choose_oscillator = function(index) {
Morse.reset_volume();
for (var i in Morse.osc_array) {
osc = Morse.osc_array[i];
if(i == index) {
osc.connect(Morse.vol);
}
else {
try {
osc.disconnect(Morse.vol);
}
catch (e) {
}
}
}
Morse.init_recording();
}
Morse.gen_bit = function(delay,s) {
// durations are additive:
// dot-time between dots/dashes : 1
// added dot-times between characters : 2 , total 3
// added dot-times between words : 4 , total 7
var vs = 0.0;
if(s == null) {
// volume for a silent pause
// caller-provided delay
var duration = delay;
}
else {
// normal volume for dot or dash
vs = Morse.volume;
// dot = 1, dash = 3
var duration = (s == '-')?3:1;
}
var hv = (Morse.high_volume-Morse.low_volume) * vs + Morse.low_volume;
Morse.vol.gain.setTargetAtTime(hv, Morse.time, Morse.slope_constant);
Morse.time += duration * Morse.dot_time;
Morse.vol.gain.setTargetAtTime(Morse.low_volume, Morse.time, Morse.slope_constant);
}
Morse.send_char = function(c) {
c = c.toLowerCase();
var data = '';
var chl = c.toLowerCase();
if(chl == ' ') {
// inter-word pause
Morse.gen_bit(Morse.dot_time_between_words, null);
}
else {
if(chl in Morse.translate) {
// translate Unicode char to common
chl = Morse.translate[chl];
}
if(chl in Morse.mdict) {
var cs = Morse.mdict[chl];
if(cs.length > 0) {
for (var i in cs) {
var tok = cs[i];
// send a dot or dash
Morse.gen_bit(Morse.dot_time_between_dots_dashes,tok);
// send a dot-length pause
Morse.gen_bit(Morse.dot_time_between_dots_dashes,null);
}
// send an inter-character pause
Morse.gen_bit(Morse.dot_time_between_characters,null);
}
}
}
}
Morse.audio_busy = function() {
return Morse.audio && Morse.time > Morse.audio.currentTime;
}
Morse.highlight_current_char = function() {
if(Morse.target_div != null && Morse.audio_busy() && Morse.running) {
var sa = Morse.string.substring(0,Morse.index);
var sb = Morse.string.substring(Morse.index,Morse.index+1);
var sc = Morse.string.substring(Morse.index+1,Morse.string.length);
result = sa + '' + sb + '' + sc;
Morse.target_div.innerHTML = result;
if(Morse.scroll) {
var hlp = Morse.id('highlight');
hlp.parentNode.scrollTop = hlp.offsetTop - hlp.parentNode.offsetTop;
}
Morse.index += 1;
if(Morse.index <= Morse.string.length && Morse.running) {
var t = Morse.times[Morse.index];
Morse.char_timer = setTimeout(Morse.highlight_current_char,(t-Morse.start_time) * 1000);
Morse.start_time = t;
return;
}
}
Morse.stop_process();
}
Morse.send_highlighted_string = function(s,index) {
Morse.time = Morse.audio.currentTime;
Morse.start_time = Morse.time;
Morse.running = true;
Morse.choose_oscillator(index);
if(Morse.recording) {
Morse.mediaRecorder.start();
}
Morse.send_char(' ');
Morse.times = [];
Morse.times.push(Morse.time);
Morse.string = s;
for (var i in s) {
if(!Morse.running) {
break;
}
var c = s[i];
Morse.send_char(c);
Morse.times.push(Morse.time);
if(i == 0) {
window.clearTimeout(Morse.char_timer);
Morse.index = 0;
// only call this function once
Morse.highlight_current_char();
}
}
}
Morse.send_single_char = function(k) {
Morse.stop_process();
if(Morse.audio_busy()) {
setTimeout(function() { Morse.send_single_char(c);},Morse_wait_time,false);
}
else{
Morse.check_input();
Morse.choose_oscillator(Morse.COSINE);
var c = Morse.char_keys[k];
Morse.time = Morse.audio.currentTime;
Morse.send_char(c);
}
}
Morse.stop_process = function() {
Morse.running = false;
Morse.practice = false;
Morse.set_practice_string();
window.clearTimeout(Morse.char_timer);
Morse.set_practice_string();
if(Morse.recording) {
Morse.mediaRecorder.stop();
}
Morse.reset_volume();
Morse.record_button.disabled = false;
Morse.setup();
Morse.low_volume = 0;
}
Morse.sanitize_string = function(str) {
//console.log('sanitize: [' + str + ']')
str = str.trim();
str = str.replace(/ /g,' ');
str = str.replace(//g,'\n');
str = str.replace(/<\/p.*?\>/g,'\n\n');
str = str.replace(/<.*?\>/g,' ');
str = str.replace(/\r/g,'\n');
str = str.replace(/\n[ \t]+/g,'\n');
// obligatory space with linefeed
str = str.replace(/\n/g,' \n');
// collapse multiple spaces
str = str.replace(/ +/g,' ');
return str;
}
Morse.sanitize_default_text = function() {
var div = Morse.id('practicearea');
str = div.innerHTML;
str = Morse.sanitize_string(str);
div.innerHTML = str;
}
Morse.process_div = function(div,osc_type,hivol,lovol,scroll) {
if(Morse.running) {
Morse.stop_process();
setTimeout(function() { Morse.process_div(div,osc_type,hivol,lovol,scroll);},Morse.wait_time,false);
return;
}
Morse.check_input();
Morse.target_div = div;
var str = Morse.target_div.innerHTML.trim();
str = Morse.sanitize_string(str);
Morse.target_div.innerHTML = str;
Morse.high_volume = (typeof lovol === 'undefined')?1:parseFloat(hivol);
Morse.low_volume = (typeof lovol === 'undefined')?0:parseFloat(lovol);
Morse.scroll = (typeof scroll === 'undefined')?false:scroll;
Morse.osc_type = (typeof osc_type === 'undefined')?Morse.COSINE:osc_type;
Morse.send_highlighted_string(str,Morse.osc_type);
}
Morse.process_divname = function(divname,osc_type,hivol,lovol,scroll) {
// this fixes an issue in Google Chrome that requires a
// user interaction to commence audio 03.03.2020
Morse.audio.resume();
if(Morse.running) {
Morse.stop_process();
}
else {
var div = Morse.id(divname);
Morse.process_div(div,osc_type,hivol,lovol,scroll);
}
}
Morse.erase_default_text = function() {
Morse.stop_process();
Morse.id('practicearea').innerHTML = '';
}
Morse.code_practice = function(state) {
if(Morse.running) {
Morse.stop_process();
setTimeout(function() { Morse.code_practice(state);},Morse.wait_time,false);
return;
}
Morse.check_input();
Morse.practice = state;
if(state) {
Morse.running = true;
Morse.choose_oscillator(Morse.COSINE);
Morse.id('practice_result').innerHTML = 'Initializing code practice.';
var pn = Object.getOwnPropertyNames(Morse);
Morse.practice_errorflag = false;
Morse.practice_total = 0;
Morse.practice_errors = 0;
Morse.practice_oldch = -1;
Morse.practice_dict = {};
Morse.practice_index = [];
var ckbox = ['letters','numbers','common','exotic']
for (var i in ckbox) {
var tag = ckbox[i];
if (Morse.id('binary_'+tag).checked) {
Morse.append(Morse.practice_dict,Morse[tag]);
}
}
for(var i in Morse.practice_dict) {
Morse.practice_index += i;
}
if(Morse.practice_index.length == 0) {
// force use of letter set
Morse.id('binary_letters').checked = true;
setTimeout(function() { Morse.code_practice(state);},Morse.wait_time,false);
return;
}
else {
Morse.practice_emit_random();
}
}
Morse.set_practice_string();
}
Morse.set_practice_string = function() {
tag_string = (Morse.practice)?'Practice session running.':'Practice session stopped.';
Morse.id('practice_tag').innerHTML = tag_string;
}
Morse.practice_emit_random = function() {
var sz = Morse.practice_index.length;
var duplicated = true;
while(duplicated) {
var n = Math.floor(Math.random() * sz);
Morse.practice_char = Morse.practice_index[n];
duplicated = Morse.practice_char == Morse.practice_oldch;
}
Morse.practice_oldch = Morse.practice_char;
Morse.send_practice_char(Morse.practice_char);
}
Morse.send_practice_char = function(c,index = Morse.COSINE) {
if(Morse.audio_busy()) {
setTimeout(function() { Morse.send_practice_char(c,index);},Morse.wait_time,false);
}
else {
Morse.choose_oscillator(index);
Morse.time = Morse.audio.currentTime;
Morse.send_char(c);
}
}
Morse.practice_keypress = function(e) {
if(Morse.practice) {
var ch = String.fromCharCode(e.charCode).toLowerCase();
Morse.practice_total += 1;
Morse.practice_errorflag = ch != Morse.practice_char;
if(Morse.practice_errorflag) {
Morse.practice_errors += 1;
Morse.send_practice_char('e',Morse.ERROR);
Morse.send_practice_char(Morse.practice_char);
}
else {
Morse.practice_emit_random();
}
var pct = Math.floor(100 * (Morse.practice_total-Morse.practice_errors)/Morse.practice_total);
var s = "Total: " + Morse.practice_total + ", Errors: " + Morse.practice_errors + ", score: " + pct + "%";
Morse.id('practice_result').innerHTML = s;
}
return false;
}
if(typeof AudioContext != "undefined" || typeof webkitAudioContext != "undefined") {
var resumeAudio = function() {
if(typeof g_WebAudioContext == "undefined" || g_WebAudioContext == null) return;
if(g_WebAudioContext.state == "suspended") g_WebAudioContext.resume();
document.removeEventListener("click", resumeAudio);
};
document.addEventListener("click", resumeAudio);
}
window.addEventListener("keypress", Morse.practice_keypress, false);
window.addEventListener('load', function() {
Morse.init();
});
window.addEventListener('unload', function() {
Morse.write_cookie();
});