@ozdemirburak/morse-code-translator 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/dist/morse-code-translator.min.js +1 -0
- package/package.json +56 -0
- package/src/audio.ts +197 -0
- package/src/characters.ts +138 -0
- package/src/index.d.ts +26 -0
- package/src/index.js +348 -0
- package/src/index.ts +58 -0
- package/src/options.ts +24 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Burak Özdemir
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# [Morse Code Translator](https://morsecodetranslator.com)
|
|
2
|
+
|
|
3
|
+
[![npm-version]][npm] [![npm-downloads]][npm]
|
|
4
|
+
|
|
5
|
+
Morse code encoder and Morse decoder with no dependencies. It supports Latin, Cyrillic, Greek, Hebrew, Arabic,
|
|
6
|
+
Persian, Japanese, Korean, and Thai, with audio-generation functionality using the [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API).
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
### npm
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
$ npm install @ozdemirburak/morse-code-translator --save
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
### Common JS
|
|
19
|
+
```js
|
|
20
|
+
const morse = require('morse-code-translator');
|
|
21
|
+
const encoded = morse.encode('SOS'); // ... --- ...
|
|
22
|
+
const decoded = morse.decode('... --- ...'); // SOS
|
|
23
|
+
const characters = morse.characters(); // {'1': {'A': '.-', ...}, ..., '11': {'ㄱ': '.-..', ...}}
|
|
24
|
+
const audio = morse.audio('SOS');
|
|
25
|
+
audio.play(); // play audio
|
|
26
|
+
audio.stop(); // stop audio (cannot resume)
|
|
27
|
+
audio.exportWave(); // download audio wave file (promise)
|
|
28
|
+
const url = await audio.getWaveUrl(); // get audio wave url (promise)
|
|
29
|
+
const blob = await audio.getWaveBlob(); // get audio wave blob (promise)
|
|
30
|
+
```
|
|
31
|
+
### Browser
|
|
32
|
+
|
|
33
|
+
Morse decoder exports a global object named 'morse-code-translator';
|
|
34
|
+
```js
|
|
35
|
+
const morse = window['morse-code-translator']
|
|
36
|
+
const audioElement = document.querySelector('audio');
|
|
37
|
+
const helloAudio = morse.audio('Hello world');
|
|
38
|
+
helloAudio.getWaveUrl().then((url) => {
|
|
39
|
+
document.querySelector('#morse')
|
|
40
|
+
.setAttribute('src', url);
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Options and localization
|
|
45
|
+
|
|
46
|
+
You can customize the dash, dot, or space characters and specify the alphabet with the priority option for
|
|
47
|
+
an accurate encoding and decoding.
|
|
48
|
+
|
|
49
|
+
The priority option gives direction to the plugin to start searching for the given character set first.
|
|
50
|
+
|
|
51
|
+
Set the priority option according to the list below.
|
|
52
|
+
|
|
53
|
+
- 1 => ASCII (Default)
|
|
54
|
+
- 2 => Numbers
|
|
55
|
+
- 3 => Punctuation
|
|
56
|
+
- 4 => Latin Extended (Turkish, Polish etc.)
|
|
57
|
+
- 5 => Cyrillic
|
|
58
|
+
- 6 => Greek
|
|
59
|
+
- 7 => Hebrew
|
|
60
|
+
- 8 => Arabic
|
|
61
|
+
- 9 => Persian
|
|
62
|
+
- 10 => Japanese
|
|
63
|
+
- 11 => Korean
|
|
64
|
+
- 12 => Thai
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
const cyrillic = morse.encode('Ленинград', { priority: 5 }); // .-.. . -. .. -. --. .-. .- -..
|
|
68
|
+
const greek = morse.decode('... .- --. .- .--. .--', { priority: 6 }); // ΣΑΓΑΠΩ
|
|
69
|
+
const hebrew = morse.decode('.. ––– . –––', { dash: '–', dot: '.', priority: 7 }); // יהוה
|
|
70
|
+
const japanese = morse.encode('NEWS', { priority: 10, dash: '-', dot: '・', separator: ' ' }); // -・ ・ ・-- ・・・
|
|
71
|
+
const characters = morse.characters({ dash: '–', dot: '•' }); // {'1': {'A': '•–', ...}, ..., '11': {'ㄱ': '•–••', ...}}
|
|
72
|
+
const arabicAudio = morse.audio('البراق', { // generates the Morse .- .-.. -... .-. .- --.- then generates the audio from it
|
|
73
|
+
wpm: 18, // sets the wpm based on the Paris method, note that setting this will override and set the unit and fwUnits accordingly
|
|
74
|
+
unit: 0.1, // period of one unit, in seconds, 1.2 / c where c is speed of transmission, in words per minute
|
|
75
|
+
fwUnit: 0.1, // period of one Farnsworth unit to control intercharacter and interword gaps
|
|
76
|
+
volume: 100, // the volume in percent (0-100)
|
|
77
|
+
oscillator: {
|
|
78
|
+
type: 'sine', // sine, square, sawtooth, triangle
|
|
79
|
+
frequency: 500, // value in hertz
|
|
80
|
+
onended: function () { // event that fires when the tone stops playing
|
|
81
|
+
console.log('ended');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const oscillator = arabicAudio.oscillator; // OscillatorNode
|
|
86
|
+
const context = arabicAudio.context; // AudioContext;
|
|
87
|
+
const gainNode = arabicAudio.gainNode; // GainNode
|
|
88
|
+
arabicAudio.play(); // will start playing Morse audio
|
|
89
|
+
arabicAudio.stop(); // will stop playing Morse audio
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Contributions
|
|
93
|
+
|
|
94
|
+
[morse-code-translator](https://github.com/ozdemirburak/morse-code-translator) has been developed
|
|
95
|
+
with extensive feedback and contributions from [numerous developers](https://github.com/ozdemirburak/morse-code-translator/graphs/contributors).
|
|
96
|
+
Special thanks to [Chris Jones](https://github.com/chris--jones), who added many great features.
|
|
97
|
+
|
|
98
|
+
## References
|
|
99
|
+
|
|
100
|
+
Please consider referencing the website of this project, if you find this package useful for your work.
|
|
101
|
+
|
|
102
|
+
```html
|
|
103
|
+
<a href="https://morsecodetranslator.com">Morse Code Translator</a>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
The MIT License (MIT). Please see [License File](LICENSE) for more information.
|
|
109
|
+
|
|
110
|
+
[npm-version]: https://img.shields.io/npm/v/morse-code-translator.svg?style=flat-square
|
|
111
|
+
[npm-downloads]: https://img.shields.io/npm/dm/morse-code-translator.svg?style=flat-square
|
|
112
|
+
[npm]: https://www.npmjs.com/package/morse-code-translator
|
|
113
|
+
|
|
114
|
+
## Generating Minified Files
|
|
115
|
+
|
|
116
|
+
Install node and npm and run the commands below.
|
|
117
|
+
|
|
118
|
+
``` bash
|
|
119
|
+
$ npm install
|
|
120
|
+
# generate `index.js` inside `src`
|
|
121
|
+
$ npm run build
|
|
122
|
+
# generate `morse-code-translator.min.js` (minified) inside `dist`
|
|
123
|
+
$ npm run build-rel
|
|
124
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const e={1:{A:"01",B:"1000",C:"1010",D:"100",E:"0",F:"0010",G:"110",H:"0000",I:"00",J:"0111",K:"101",L:"0100",M:"11",N:"10",O:"111",P:"0110",Q:"1101",R:"010",S:"000",T:"1",U:"001",V:"0001",W:"011",X:"1001",Y:"1011",Z:"1100"},2:{0:"11111",1:"01111",2:"00111",3:"00011",4:"00001",5:"00000",6:"10000",7:"11000",8:"11100",9:"11110"},3:{".":"010101",",":"110011","?":"001100","'":"011110","!":"101011","/":"10010","(":"10110",")":"101101","&":"01000",":":"111000",";":"101010","=":"10001","+":"01010","-":"100001",_:"001101",'"':"010010",$:"0001001","@":"011010","¿":"00101","¡":"110001"},4:{"Ã":"01101","Á":"01101","Å":"01101","À":"01101","Â":"01101","Ä":"0101","Ą":"0101","Æ":"0101","Ç":"10100","Ć":"10100","Ĉ":"10100","Č":"110","Ð":"00110","È":"01001","Ę":"00100","Ë":"00100","É":"00100","Ê":"10010","Ğ":"11010","Ĝ":"11010","Ĥ":"1111","İ":"01001","Ï":"10011","Ì":"01110","Ĵ":"01110","Ł":"01001","Ń":"11011","Ñ":"11011","Ó":"1110","Ò":"1110","Ö":"1110","Ô":"1110","Ø":"1110","Ś":"0001000","Ş":"01100","Ș":"1111","Š":"1111","Ŝ":"00010","ß":"000000","Þ":"01100","Ü":"0011","Ù":"0011","Ŭ":"0011","Ž":"11001","Ź":"110010","Ż":"11001"},5:{"А":"01","Б":"1000","В":"011","Г":"110","Д":"100","Е":"0","Ж":"0001","З":"1100","И":"00","Й":"0111","К":"101","Л":"0100","М":"11","Н":"10","О":"111","П":"0110","Р":"010","С":"000","Т":"1","У":"001","Ф":"0010","Х":"0000","Ц":"1010","Ч":"1110","Ш":"1111","Щ":"1101","Ъ":"11011","Ы":"1011","Ь":"1001","Э":"00100","Ю":"0011","Я":"0101","Ї":"01110","Є":"00100","І":"00","Ґ":"110"},6:{"Α":"01","Β":"1000","Γ":"110","Δ":"100","Ε":"0","Ζ":"1100","Η":"0000","Θ":"1010","Ι":"00","Κ":"101","Λ":"0100","Μ":"11","Ν":"10","Ξ":"1001","Ο":"111","Π":"0110","Ρ":"010","Σ":"000","Τ":"1","Υ":"1011","Φ":"0010","Χ":"1111","Ψ":"1101","Ω":"011"},7:{"א":"01","ב":"1000","ג":"110","ד":"100","ה":"111","ו":"0","ז":"1100","ח":"0000","ט":"001","י":"00","כ":"101","ל":"0100","מ":"11","נ":"10","ס":"1010","ע":"0111","פ":"0110","צ":"011","ק":"1101","ר":"010","ש":"000","ת":"1"},8:{"ا":"01","ب":"1000","ت":"1","ث":"1010","ج":"0111","ح":"0000","خ":"111","د":"100","ذ":"1100","ر":"010","ز":"1110","س":"000","ش":"1111","ص":"1001","ض":"0001","ط":"001","ظ":"1011","ع":"0101","غ":"110","ف":"0010","ق":"1101","ك":"101","ل":"0100","م":"11","ن":"10","ه":"00100","و":"011","ي":"00","ﺀ":"0"},9:{"ا":"01","ب":"1000","پ":"0110","ت":"1","ث":"1010","ج":"0111","چ":"1110","ح":"0000","خ":"1001","د":"100","ذ":"0001","ر":"010","ز":"1100","ژ":"110","س":"000","ش":"1111","ص":"0101","ض":"00100","ط":"001","ظ":"1011","ع":"111","غ":"0011","ف":"0010","ق":"111000","ک":"101","گ":"1101","ل":"0100","م":"11","ن":"10","و":"011","ه":"0","ی":"00"},10:{"ア":"11011","カ":"0100","サ":"10101","タ":"10","ナ":"010","ハ":"1000","マ":"1001","ヤ":"011","ラ":"000","ワ":"101","イ":"01","キ":"10100","シ":"11010","チ":"0010","ニ":"1010","ヒ":"11001","ミ":"00101","リ":"110","ヰ":"01001","ウ":"001","ク":"0001","ス":"11101","ツ":"0110","ヌ":"0000","フ":"1100","ム":"1","ユ":"10011","ル":"10110","ン":"01010","エ":"10111","ケ":"1011","セ":"01110","テ":"01011","ネ":"1101","ヘ":"0","メ":"10001","レ":"111","ヱ":"01100","オ":"01000","コ":"1111","ソ":"1110","ト":"00100","ノ":"0011","ホ":"100","モ":"10010","ヨ":"11","ロ":"0101","ヲ":"0111","゛":"00","゜":"00110","。":"010100","ー":"01101","、":"010101","(":"101101",")":"010010"},11:{"ㄱ":"0100","ㄴ":"0010","ㄷ":"1000","ㄹ":"0001","ㅁ":"11","ㅂ":"011","ㅅ":"110","ㅇ":"101","ㅈ":"0110","ㅊ":"1010","ㅋ":"1001","ㅌ":"1100","ㅍ":"111","ㅎ":"0111","ㅏ":"0","ㅑ":"00","ㅓ":"1","ㅕ":"000","ㅗ":"01","ㅛ":"10","ㅜ":"0000","ㅠ":"010","ㅡ":"100","ㅣ":"001"},12:{"ก":"110","ข":"1010","ค":"101","ง":"10110","จ":"10010","ฉ":"1111","ช":"1001","ซ":"1100","ญ":"0111","ด":"100","ต":"1","ถ":"10100","ท":"10011","น":"10","บ":"1000","ป":"0110","ผ":"1101","ฝ":"10101","พ":"01100","ฟ":"0010","ม":"11","ย":"1011","ร":"010","ล":"0100","ว":"011","ส":"000","ห":"0000","อ":"10001","ฮ":"11011","ฤ":"01011","ะ":"01000","า":"01","ิ":"00100","ี":"00","ึ":"00110","ื":"0011","ุ":"00101","ู":"1110","เ":"0","แ":"0101","ไ":"01001","โ":"111","ำ":"00010","่":"001","้":"0001","๊":"11000","๋":"01010","ั":"01101","็":"11100","์":"11001","ๆ":"10111","ฯ":"11010"}},t=t=>Object.assign(Object.assign({},e),{0:e[t.priority],1:Object.assign(Object.assign({},e[1]),{[t.separator]:t.space})}),n=(e,n)=>{const o={},r=t(e);for(const t in r){o[t]={};for(const n in r[t])o[t][n]=r[t][n].replace(/0/g,e.dot).replace(/1/g,e.dash)}return n||delete o[0],o},o=(e={})=>{var t,n,o;return Object.assign(Object.assign({},e),{dash:e.dash||"-",dot:e.dot||".",space:e.space||"/",separator:e.separator||" ",invalid:e.invalid||"#",priority:e.priority||1,wpm:e.wpm,unit:e.unit||.08,fwUnit:e.fwUnit||e.unit||.08,volume:e.volume||100,oscillator:Object.assign(Object.assign({},e.oscillator),{type:(null===(t=e.oscillator)||void 0===t?void 0:t.type)||"sine",frequency:(null===(n=e.oscillator)||void 0===n?void 0:n.frequency)||500,onended:(null===(o=e.oscillator)||void 0===o?void 0:o.onended)||null})})},r=(e,t)=>{let n,o=null,r=null,i=null,a=null;const[s,c]=((e,t,n=0)=>{const o=[];let{unit:r,fwUnit:i}=t,a=0;t.wpm&&(r=i=60/(50*t.wpm)),o.push([0,a]);const s=e=>{o.push([t.volume/100*1,n+a]),a+=e*r},c=e=>{o.push([0,n+a]),a+=e*r},l=e=>{o.push([0,n+a]),a+=e*i};for(let n=0,o=!1;n<=e.length;n++)e[n]===t.space?(l(7),o=!1):e[n]===t.dot?(o?c(1):o=!0,s(1)):e[n]===t.dash?(o?c(1):o=!0,s(3)):void 0!==e[n+1]&&e[n+1]!==t.space&&void 0!==e[n-1]&&e[n-1]!==t.space&&(l(3),o=!1);return[o,a]})(e,t);if(null===o&&"undefined"!=typeof window&&(o=window.AudioContext||window.webkitAudioContext,i=new o),null===r&&"undefined"!=typeof window&&(r=window.OfflineAudioContext||window.webkitOfflineAudioContext,a=new r(1,44100*c,44100)),!i||!a)throw new Error("Web Audio API is not supported in this browser");n=i.createBufferSource(),n.connect(i.destination);const l=a.createOscillator(),d=a.createGain();l.type=t.oscillator.type,l.frequency.value=t.oscillator.frequency,s.forEach((([e,t])=>d.gain.setValueAtTime(e,t))),l.connect(d),d.connect(a.destination),n.onended=t.oscillator.onended;const u=new Promise((e=>{l.start(0),a.startRendering(),a.oncomplete=t=>{n.buffer=t.renderedBuffer,e()}}));let p;const f=()=>{clearTimeout(p),p=0,n.stop(0)},w=async()=>{await u;const e=((e,t)=>{const n=new ArrayBuffer(44+2*t.length),o=new DataView(n),r=(e,t,n)=>{for(let o=0;o<n.length;o++)e.setUint8(t+o,n.charCodeAt(o))};return r(o,0,"RIFF"),o.setUint32(4,36+2*t.length,!0),r(o,8,"WAVE"),r(o,12,"fmt "),o.setUint32(16,16,!0),o.setUint16(20,1,!0),o.setUint16(22,1,!0),o.setUint32(24,e,!0),o.setUint32(28,4*e,!0),o.setUint16(32,2,!0),o.setUint16(34,16,!0),r(o,36,"data"),o.setUint32(40,2*t.length,!0),((e,t,n)=>{for(let o=0;o<n.length;o++,t+=2){const r=Math.max(-1,Math.min(1,n[o]));e.setInt16(t,r<0?32768*r:32767*r,!0)}})(o,44,t),o})(a.sampleRate,n.buffer.getChannelData(0));return new Blob([e],{type:"audio/wav"})},g=async()=>{const e=await w();return URL.createObjectURL(e)};return{play:async()=>{await u,n.start(i.currentTime),p=setTimeout((()=>{f()}),1e3*c)},stop:f,getWaveBlob:w,getWaveUrl:g,exportWave:async e=>{const t=await g(),n=document.createElement("a");n.href=t,n.target="_blank",n.download=e||"morse.wav",n.click()},context:i,oscillator:l,gainNode:d}};var i,a;i=globalThis,a=()=>{const e=(e,n)=>{const r=o(n),i=t(r);return[...e.replace(/\s+/g,r.separator).trim().toLocaleUpperCase()].map((function(e){for(const t in i)if(void 0!==i[t]&&void 0!==i[t][e])return i[t][e];return r.invalid})).join(r.separator).replace(/0/g,r.dot).replace(/1/g,r.dash)};return{characters:(e,t)=>n(o(e),t),decode:(e,t)=>{const r=o(t),i=(e=>{const t={},o=n(e,!0);for(const e in o)for(const n in o[e])void 0===t[o[e][n]]&&(t[o[e][n]]=n);return t})(r);return e.replace(/\s+/g,r.separator).trim().split(r.separator).map((function(e){return void 0!==i[e]?i[e]:r.invalid})).join("")},encode:e,audio:(t,n,i)=>{const a=i||e(t,n),s=o(n);return r(a,s)}}},"object"==typeof exports?module.exports=a():"function"==typeof define&&define.amd?define(a):void 0!==i&&(i["morse-code-translator"]=a());
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ozdemirburak/morse-code-translator",
|
|
3
|
+
"version": "5.0.0",
|
|
4
|
+
"description": "Morse code translator helps you convert text to Morse code and vice versa, with the option to play Morse code audio.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"morse",
|
|
7
|
+
"morse alphabet",
|
|
8
|
+
"morse code",
|
|
9
|
+
"morse decoder",
|
|
10
|
+
"morse encoder",
|
|
11
|
+
"morse translator",
|
|
12
|
+
"morse code translator"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"homepage": "https://morsecodetranslator.com",
|
|
16
|
+
"author": "Burak Özdemir",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/ozdemirburak/morse-code-translator.git"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
23
|
+
"@rollup/plugin-typescript": "^11.1.5",
|
|
24
|
+
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
|
25
|
+
"eslint": "^8.51.0",
|
|
26
|
+
"eslint-config-standard-with-typescript": "^39.1.0",
|
|
27
|
+
"eslint-plugin-import": "^2.28.1",
|
|
28
|
+
"eslint-plugin-n": "^16.1.0",
|
|
29
|
+
"eslint-plugin-promise": "^6.1.1",
|
|
30
|
+
"mocha": "10.2.0",
|
|
31
|
+
"nyc": "^15.1.0",
|
|
32
|
+
"rollup": "^4.0.2",
|
|
33
|
+
"tslib": "^2.6.2",
|
|
34
|
+
"typescript": "^5.2.2"
|
|
35
|
+
},
|
|
36
|
+
"types": "./src/index.d.ts",
|
|
37
|
+
"main": "./src/index.js",
|
|
38
|
+
"files": [
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"README.md",
|
|
41
|
+
"src",
|
|
42
|
+
"dist",
|
|
43
|
+
"package.json"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "rollup -c",
|
|
47
|
+
"build-rel": "rollup -c -p @rollup/plugin-terser -o ./dist/morse-code-translator.min.js",
|
|
48
|
+
"tsc": "tsc --noEmit",
|
|
49
|
+
"test": "mocha",
|
|
50
|
+
"test-cov": "nyc mocha",
|
|
51
|
+
"lint": "eslint ./src/index.ts"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=4.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/audio.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
const getGainTimings = (morse: string, opts: Options, currentTime = 0): [[[number, number]?], number] => {
|
|
2
|
+
const timings: [[number, number]?] = [];
|
|
3
|
+
let {unit, fwUnit} = opts;
|
|
4
|
+
let time = 0;
|
|
5
|
+
|
|
6
|
+
if (opts.wpm) {
|
|
7
|
+
// wpm mode uses standardised units
|
|
8
|
+
unit = fwUnit = 60 / (opts.wpm * 50)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
timings.push([0, time]);
|
|
12
|
+
|
|
13
|
+
const tone = (i: number) => {
|
|
14
|
+
timings.push([1 * (opts.volume / 100.0), currentTime + time]);
|
|
15
|
+
time += i * unit;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const silence = (i: number) => {
|
|
19
|
+
timings.push([0, currentTime + time]);
|
|
20
|
+
time += i * unit;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const gap = (i: number) => {
|
|
24
|
+
timings.push([0, currentTime + time]);
|
|
25
|
+
time += i * fwUnit;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
for (let i = 0, addSilence = false; i <= morse.length; i++) {
|
|
29
|
+
if (morse[i] === opts.space) {
|
|
30
|
+
gap(7);
|
|
31
|
+
addSilence = false;
|
|
32
|
+
} else if (morse[i] === opts.dot) {
|
|
33
|
+
if (addSilence) silence(1); else addSilence = true;
|
|
34
|
+
tone(1);
|
|
35
|
+
} else if (morse[i] === opts.dash) {
|
|
36
|
+
if (addSilence) silence(1); else addSilence = true;
|
|
37
|
+
tone(3);
|
|
38
|
+
} else if (
|
|
39
|
+
(typeof morse[i + 1] !== 'undefined' && morse[i + 1] !== opts.space) &&
|
|
40
|
+
(typeof morse[i - 1] !== 'undefined' && morse[i - 1] !== opts.space)
|
|
41
|
+
) {
|
|
42
|
+
gap(3);
|
|
43
|
+
addSilence = false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return [timings, time];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Source: https://github.com/mattdiamond/Recorderjs/blob/master/src/recorder.js#L155
|
|
51
|
+
const encodeWAV = (sampleRate: number, samples: Float32Array) => {
|
|
52
|
+
const buffer = new ArrayBuffer(44 + samples.length * 2);
|
|
53
|
+
const view = new DataView(buffer);
|
|
54
|
+
const writeString = (view: DataView, offset: number, string: string) => {
|
|
55
|
+
for (let i = 0; i < string.length; i++) {
|
|
56
|
+
view.setUint8(offset + i, string.charCodeAt(i));
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
// RIFF identifier
|
|
60
|
+
writeString(view, 0, 'RIFF');
|
|
61
|
+
// RIFF chunk length
|
|
62
|
+
view.setUint32(4, 36 + samples.length * 2, true);
|
|
63
|
+
// RIFF type
|
|
64
|
+
writeString(view, 8, 'WAVE');
|
|
65
|
+
// format chunk identifier
|
|
66
|
+
writeString(view, 12, 'fmt ');
|
|
67
|
+
// format chunk length
|
|
68
|
+
view.setUint32(16, 16, true);
|
|
69
|
+
// sample format (raw)
|
|
70
|
+
view.setUint16(20, 1, true);
|
|
71
|
+
// channel count
|
|
72
|
+
view.setUint16(22, 1, true);
|
|
73
|
+
// sample rate
|
|
74
|
+
view.setUint32(24, sampleRate, true);
|
|
75
|
+
// byte rate (sample rate * block align)
|
|
76
|
+
view.setUint32(28, sampleRate * 4, true);
|
|
77
|
+
// block align (channel count * bytes per sample)
|
|
78
|
+
view.setUint16(32, 2, true);
|
|
79
|
+
// bits per sample
|
|
80
|
+
view.setUint16(34, 16, true);
|
|
81
|
+
// data chunk identifier
|
|
82
|
+
writeString(view, 36, 'data');
|
|
83
|
+
// data chunk length
|
|
84
|
+
view.setUint32(40, samples.length * 2, true);
|
|
85
|
+
// to PCM
|
|
86
|
+
const floatTo16BitPCM = (output: DataView, offset: number, input: Float32Array) => {
|
|
87
|
+
for (let i = 0; i < input.length; i++, offset += 2) {
|
|
88
|
+
const s = Math.max(-1, Math.min(1, input[i]));
|
|
89
|
+
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
floatTo16BitPCM(view, 44, samples);
|
|
93
|
+
return view;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const audio = (morse: string, options: Options) => {
|
|
97
|
+
let AudioContext: {
|
|
98
|
+
new(contextOptions?: AudioContextOptions): AudioContext;
|
|
99
|
+
prototype: AudioContext;
|
|
100
|
+
} = null;
|
|
101
|
+
let OfflineAudioContext: {
|
|
102
|
+
new(contextOptions: OfflineAudioContextOptions): OfflineAudioContext;
|
|
103
|
+
new(numberOfChannels: number, length: number, sampleRate: number): OfflineAudioContext;
|
|
104
|
+
prototype: OfflineAudioContext;
|
|
105
|
+
} = null;
|
|
106
|
+
let context: AudioContext = null;
|
|
107
|
+
let offlineContext: OfflineAudioContext = null;
|
|
108
|
+
let source: AudioBufferSourceNode;
|
|
109
|
+
|
|
110
|
+
const [gainValues, totalTime] = getGainTimings(morse, options);
|
|
111
|
+
|
|
112
|
+
if (AudioContext === null && typeof window !== 'undefined') {
|
|
113
|
+
AudioContext = window.AudioContext || (window as any).webkitAudioContext;
|
|
114
|
+
context = new AudioContext();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (OfflineAudioContext === null && typeof window !== 'undefined') {
|
|
118
|
+
OfflineAudioContext = window.OfflineAudioContext || (window as any).webkitOfflineAudioContext;
|
|
119
|
+
offlineContext = new OfflineAudioContext(1, 44100 * totalTime, 44100);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!context || !offlineContext) {
|
|
123
|
+
throw new Error('Web Audio API is not supported in this browser');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
source = context.createBufferSource();
|
|
127
|
+
source.connect(context.destination);
|
|
128
|
+
|
|
129
|
+
const oscillator = offlineContext.createOscillator();
|
|
130
|
+
const gainNode = offlineContext.createGain();
|
|
131
|
+
|
|
132
|
+
oscillator.type = options.oscillator.type as OscillatorType;
|
|
133
|
+
oscillator.frequency.value = options.oscillator.frequency;
|
|
134
|
+
|
|
135
|
+
gainValues.forEach(([value, time]) => gainNode.gain.setValueAtTime(value, time));
|
|
136
|
+
|
|
137
|
+
oscillator.connect(gainNode);
|
|
138
|
+
gainNode.connect(offlineContext.destination);
|
|
139
|
+
source.onended = options.oscillator.onended;
|
|
140
|
+
|
|
141
|
+
// Inspired by: http://joesul.li/van/tale-of-no-clocks/
|
|
142
|
+
const render = new Promise<void>(resolve => {
|
|
143
|
+
oscillator.start(0);
|
|
144
|
+
offlineContext.startRendering();
|
|
145
|
+
offlineContext.oncomplete = (e) => {
|
|
146
|
+
source.buffer = e.renderedBuffer;
|
|
147
|
+
resolve();
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
let timeout: number;
|
|
152
|
+
|
|
153
|
+
const play = async () => {
|
|
154
|
+
await render;
|
|
155
|
+
source.start(context.currentTime);
|
|
156
|
+
timeout = setTimeout(() => { stop() }, totalTime * 1000);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const stop = () => {
|
|
160
|
+
clearTimeout(timeout);
|
|
161
|
+
timeout = 0;
|
|
162
|
+
source.stop(0);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const getWaveBlob = async () => {
|
|
166
|
+
await render;
|
|
167
|
+
const waveData = encodeWAV(offlineContext.sampleRate, source.buffer.getChannelData(0));
|
|
168
|
+
return new Blob([waveData], { 'type': 'audio/wav' });
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const getWaveUrl = async () => {
|
|
172
|
+
const audioBlob = await getWaveBlob();
|
|
173
|
+
return URL.createObjectURL(audioBlob);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const exportWave = async (filename) => {
|
|
177
|
+
const waveUrl = await getWaveUrl();
|
|
178
|
+
const anchor = document.createElement('a');
|
|
179
|
+
anchor.href = waveUrl;
|
|
180
|
+
anchor.target = '_blank';
|
|
181
|
+
anchor.download = filename || 'morse.wav';
|
|
182
|
+
anchor.click();
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
play,
|
|
187
|
+
stop,
|
|
188
|
+
getWaveBlob,
|
|
189
|
+
getWaveUrl,
|
|
190
|
+
exportWave,
|
|
191
|
+
context,
|
|
192
|
+
oscillator,
|
|
193
|
+
gainNode
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export default audio;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const baseCharacters: Characters = {
|
|
2
|
+
'1': { // Latin => https://en.wikipedia.org/wiki/Morse_code
|
|
3
|
+
'A': '01', 'B': '1000', 'C': '1010', 'D': '100', 'E': '0', 'F': '0010',
|
|
4
|
+
'G': '110', 'H': '0000', 'I': '00', 'J': '0111', 'K': '101', 'L': '0100',
|
|
5
|
+
'M': '11', 'N': '10', 'O': '111', 'P': '0110', 'Q': '1101', 'R': '010',
|
|
6
|
+
'S': '000', 'T': '1', 'U': '001', 'V': '0001', 'W': '011', 'X': '1001',
|
|
7
|
+
'Y': '1011', 'Z': '1100'
|
|
8
|
+
},
|
|
9
|
+
'2': { // Numbers
|
|
10
|
+
'0': '11111', '1': '01111', '2': '00111', '3': '00011', '4': '00001',
|
|
11
|
+
'5': '00000', '6': '10000', '7': '11000', '8': '11100', '9': '11110'
|
|
12
|
+
},
|
|
13
|
+
'3': { // Punctuation
|
|
14
|
+
'.': '010101', ',': '110011', '?': '001100', '\'': '011110', '!': '101011', '/': '10010',
|
|
15
|
+
'(': '10110', ')': '101101', '&': '01000', ':': '111000', ';': '101010', '=': '10001',
|
|
16
|
+
'+': '01010', '-': '100001', '_': '001101', '"': '010010', '$': '0001001', '@': '011010',
|
|
17
|
+
'¿': '00101', '¡': '110001'
|
|
18
|
+
},
|
|
19
|
+
'4': { // Latin Extended => https://ham.stackexchange.com/questions/1379/international-characters-in-morse-code
|
|
20
|
+
'Ã': '01101', 'Á': '01101', 'Å': '01101', 'À': '01101', 'Â': '01101', 'Ä': '0101',
|
|
21
|
+
'Ą': '0101', 'Æ': '0101', 'Ç': '10100', 'Ć': '10100', 'Ĉ': '10100', 'Č': '110',
|
|
22
|
+
'Ð': '00110', 'È': '01001', 'Ę': '00100', 'Ë': '00100', 'É': '00100',
|
|
23
|
+
'Ê': '10010', 'Ğ': '11010', 'Ĝ': '11010', 'Ĥ': '1111', 'İ': '01001', 'Ï': '10011',
|
|
24
|
+
'Ì': '01110', 'Ĵ': '01110', 'Ł': '01001', 'Ń': '11011', 'Ñ': '11011', 'Ó': '1110',
|
|
25
|
+
'Ò': '1110', 'Ö': '1110', 'Ô': '1110', 'Ø': '1110', 'Ś': '0001000', 'Ş': '01100',
|
|
26
|
+
'Ș': '1111', 'Š': '1111', 'Ŝ': '00010', 'ß': '000000', 'Þ': '01100', 'Ü': '0011',
|
|
27
|
+
'Ù': '0011', 'Ŭ': '0011', 'Ž': '11001', 'Ź': '110010', 'Ż': '11001'
|
|
28
|
+
},
|
|
29
|
+
'5': { // Cyrillic Alphabet => https://en.wikipedia.org/wiki/Russian_Morse_code
|
|
30
|
+
'А': '01', 'Б': '1000', 'В': '011', 'Г': '110', 'Д': '100', 'Е': '0',
|
|
31
|
+
'Ж': '0001', 'З': '1100', 'И': '00', 'Й': '0111', 'К': '101', 'Л': '0100',
|
|
32
|
+
'М': '11', 'Н': '10', 'О': '111', 'П': '0110', 'Р': '010', 'С': '000',
|
|
33
|
+
'Т': '1', 'У': '001', 'Ф': '0010', 'Х': '0000', 'Ц': '1010', 'Ч': '1110',
|
|
34
|
+
'Ш': '1111', 'Щ': '1101', 'Ъ': '11011', 'Ы': '1011', 'Ь': '1001', 'Э': '00100',
|
|
35
|
+
'Ю': '0011', 'Я': '0101', 'Ї': '01110', 'Є': '00100', 'І': '00', 'Ґ': '110'
|
|
36
|
+
},
|
|
37
|
+
'6': { // Greek Alphabet => https://en.wikipedia.org/wiki/Morse_code_for_non-Latin_alphabets
|
|
38
|
+
'Α': '01', 'Β': '1000', 'Γ': '110', 'Δ': '100', 'Ε': '0', 'Ζ': '1100',
|
|
39
|
+
'Η': '0000', 'Θ': '1010', 'Ι': '00', 'Κ': '101', 'Λ': '0100', 'Μ': '11',
|
|
40
|
+
'Ν': '10', 'Ξ': '1001', 'Ο': '111', 'Π': '0110', 'Ρ': '010', 'Σ': '000',
|
|
41
|
+
'Τ': '1', 'Υ': '1011', 'Φ': '0010', 'Χ': '1111', 'Ψ': '1101', 'Ω': '011'
|
|
42
|
+
},
|
|
43
|
+
'7': { // Hebrew Alphabet => https://en.wikipedia.org/wiki/Morse_code_for_non-Latin_alphabets
|
|
44
|
+
'א': '01', 'ב': '1000', 'ג': '110', 'ד': '100', 'ה': '111', 'ו': '0',
|
|
45
|
+
'ז': '1100', 'ח': '0000', 'ט': '001', 'י': '00', 'כ': '101', 'ל': '0100',
|
|
46
|
+
'מ': '11', 'נ': '10', 'ס': '1010', 'ע': '0111', 'פ': '0110', 'צ': '011',
|
|
47
|
+
'ק': '1101', 'ר': '010', 'ש': '000', 'ת': '1'
|
|
48
|
+
},
|
|
49
|
+
'8': { // Arabic Alphabet => https://en.wikipedia.org/wiki/Morse_code_for_non-Latin_alphabets
|
|
50
|
+
'ا': '01', 'ب': '1000', 'ت': '1', 'ث': '1010', 'ج': '0111', 'ح': '0000',
|
|
51
|
+
'خ': '111', 'د': '100', 'ذ': '1100', 'ر': '010', 'ز': '1110', 'س': '000',
|
|
52
|
+
'ش': '1111', 'ص': '1001', 'ض': '0001', 'ط': '001', 'ظ': '1011', 'ع': '0101',
|
|
53
|
+
'غ': '110', 'ف': '0010', 'ق': '1101', 'ك': '101', 'ل': '0100', 'م': '11',
|
|
54
|
+
'ن': '10', 'ه': '00100', 'و': '011', 'ي': '00', 'ﺀ': '0'
|
|
55
|
+
},
|
|
56
|
+
'9': { // Persian Alphabet => https://en.wikipedia.org/wiki/Morse_code_for_non-Latin_alphabets
|
|
57
|
+
'ا': '01', 'ب': '1000', 'پ': '0110', 'ت': '1', 'ث': '1010', 'ج': '0111',
|
|
58
|
+
'چ': '1110', 'ح': '0000', 'خ': '1001', 'د': '100', 'ذ': '0001', 'ر': '010',
|
|
59
|
+
'ز': '1100', 'ژ': '110', 'س': '000', 'ش': '1111', 'ص': '0101', 'ض': '00100',
|
|
60
|
+
'ط': '001', 'ظ': '1011', 'ع': '111', 'غ': '0011', 'ف': '0010', 'ق': '111000',
|
|
61
|
+
'ک': '101', 'گ': '1101', 'ل': '0100', 'م': '11', 'ن': '10', 'و': '011',
|
|
62
|
+
'ه': '0', 'ی': '00'
|
|
63
|
+
},
|
|
64
|
+
'10': { // Japanese Alphabet => https://ja.wikipedia.org/wiki/%E3%83%A2%E3%83%BC%E3%83%AB%E3%82%B9%E7%AC%A6%E5%8F%B7#%E5%92%8C%E6%96%87%E3%83%A2%E3%83%BC%E3%83%AB%E3%82%B9%E7%AC%A6%E5%8F%B7
|
|
65
|
+
'ア': '11011', 'カ': '0100', 'サ': '10101', 'タ': '10', 'ナ': '010', 'ハ': '1000',
|
|
66
|
+
'マ': '1001', 'ヤ': '011', 'ラ': '000', 'ワ': '101', 'イ': '01', 'キ': '10100',
|
|
67
|
+
'シ': '11010', 'チ': '0010', 'ニ': '1010', 'ヒ': '11001', 'ミ': '00101', 'リ': '110',
|
|
68
|
+
'ヰ': '01001', 'ウ': '001', 'ク': '0001', 'ス': '11101', 'ツ': '0110', 'ヌ': '0000',
|
|
69
|
+
'フ': '1100', 'ム': '1', 'ユ': '10011', 'ル': '10110', 'ン': '01010', 'エ': '10111',
|
|
70
|
+
'ケ': '1011', 'セ': '01110', 'テ': '01011', 'ネ': '1101', 'ヘ': '0', 'メ': '10001',
|
|
71
|
+
'レ': '111', 'ヱ': '01100', 'オ': '01000', 'コ': '1111', 'ソ': '1110', 'ト': '00100',
|
|
72
|
+
'ノ': '0011', 'ホ': '100', 'モ': '10010', 'ヨ': '11', 'ロ': '0101', 'ヲ': '0111',
|
|
73
|
+
'゛': '00', '゜': '00110', '。': '010100', 'ー': '01101', '、': '010101',
|
|
74
|
+
'(': '101101', ')': '010010'
|
|
75
|
+
},
|
|
76
|
+
'11': { // Korean Alphabet => https://en.wikipedia.org/wiki/SKATS
|
|
77
|
+
'ㄱ': '0100', 'ㄴ': '0010', 'ㄷ': '1000', 'ㄹ': '0001', 'ㅁ': '11', 'ㅂ': '011',
|
|
78
|
+
'ㅅ': '110', 'ㅇ': '101', 'ㅈ': '0110', 'ㅊ': '1010', 'ㅋ': '1001', 'ㅌ': '1100',
|
|
79
|
+
'ㅍ': '111', 'ㅎ': '0111', 'ㅏ': '0', 'ㅑ': '00', 'ㅓ': '1', 'ㅕ': '000',
|
|
80
|
+
'ㅗ': '01', 'ㅛ': '10', 'ㅜ': '0000', 'ㅠ': '010', 'ㅡ': '100', 'ㅣ': '001'
|
|
81
|
+
},
|
|
82
|
+
'12': { // Thai Alphabet => https://th.wikipedia.org/wiki/รหัสมอร์ส
|
|
83
|
+
'ก': '110', 'ข': '1010', 'ค': '101', 'ง': '10110', 'จ': '10010',
|
|
84
|
+
'ฉ': '1111', 'ช': '1001', 'ซ': '1100', 'ญ': '0111', 'ด': '100',
|
|
85
|
+
'ต': '1', 'ถ': '10100', 'ท': '10011', 'น': '10', 'บ': '1000',
|
|
86
|
+
'ป': '0110', 'ผ': '1101', 'ฝ': '10101', 'พ': '01100', 'ฟ': '0010',
|
|
87
|
+
'ม': '11', 'ย': '1011', 'ร': '010', 'ล': '0100', 'ว': '011',
|
|
88
|
+
'ส': '000', 'ห': '0000', 'อ': '10001', 'ฮ': '11011', 'ฤ': '01011',
|
|
89
|
+
'ะ': '01000', 'า': '01', 'ิ': '00100', 'ี': '00', 'ึ': '00110',
|
|
90
|
+
'ื': '0011', 'ุ': '00101', 'ู': '1110', 'เ': '0', 'แ': '0101',
|
|
91
|
+
'ไ': '01001', 'โ': '111', 'ำ': '00010', '่': '001', '้': '0001',
|
|
92
|
+
'๊': '11000', '๋': '01010', 'ั': '01101', '็': '11100', '์': '11001',
|
|
93
|
+
'ๆ': '10111', 'ฯ': '11010'
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const getCharacters = (options: Options) => ({
|
|
98
|
+
...baseCharacters,
|
|
99
|
+
'0': baseCharacters[options.priority],
|
|
100
|
+
'1': {
|
|
101
|
+
...baseCharacters['1'],
|
|
102
|
+
[options.separator]: options.space
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const getMappedCharacters = (options: Options, usePriority: boolean) => {
|
|
107
|
+
const mapped: Characters = {};
|
|
108
|
+
const characters = getCharacters(options);
|
|
109
|
+
for (const set in characters) {
|
|
110
|
+
mapped[set] = {};
|
|
111
|
+
for (const key in characters[set]) {
|
|
112
|
+
mapped[set][key] = characters[set][key].replace(/0/g, options.dot).replace(/1/g, options.dash);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!usePriority) {
|
|
116
|
+
delete mapped['0'];
|
|
117
|
+
}
|
|
118
|
+
return mapped;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const swapCharacters = (options: Options) => {
|
|
122
|
+
const swapped: Record<string, string> = {};
|
|
123
|
+
const mappedCharacters = getMappedCharacters(options, true);
|
|
124
|
+
for (const set in mappedCharacters) {
|
|
125
|
+
for (const key in mappedCharacters[set]) {
|
|
126
|
+
if (typeof swapped[mappedCharacters[set][key]] === 'undefined') {
|
|
127
|
+
swapped[mappedCharacters[set][key]] = key;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return swapped;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export {
|
|
135
|
+
getCharacters,
|
|
136
|
+
getMappedCharacters,
|
|
137
|
+
swapCharacters
|
|
138
|
+
};
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
interface Window {
|
|
2
|
+
webkitAudioContext: typeof AudioContext
|
|
3
|
+
webkitOfflineAudioContext: typeof OfflineAudioContext
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
type Characters = Record<string, Record<string, string>>
|
|
7
|
+
|
|
8
|
+
interface Oscillator {
|
|
9
|
+
type?: OscillatorType;
|
|
10
|
+
frequency?: number;
|
|
11
|
+
onended?: ((this: AudioScheduledSourceNode, ev: Event) => any) | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Options {
|
|
15
|
+
dash: string;
|
|
16
|
+
dot: string;
|
|
17
|
+
space: string;
|
|
18
|
+
separator: string;
|
|
19
|
+
invalid: string;
|
|
20
|
+
priority: number;
|
|
21
|
+
wpm?: number;
|
|
22
|
+
unit: number;
|
|
23
|
+
fwUnit: number;
|
|
24
|
+
volume: number;
|
|
25
|
+
oscillator: Oscillator
|
|
26
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const baseCharacters = {
|
|
4
|
+
'1': {
|
|
5
|
+
'A': '01', 'B': '1000', 'C': '1010', 'D': '100', 'E': '0', 'F': '0010',
|
|
6
|
+
'G': '110', 'H': '0000', 'I': '00', 'J': '0111', 'K': '101', 'L': '0100',
|
|
7
|
+
'M': '11', 'N': '10', 'O': '111', 'P': '0110', 'Q': '1101', 'R': '010',
|
|
8
|
+
'S': '000', 'T': '1', 'U': '001', 'V': '0001', 'W': '011', 'X': '1001',
|
|
9
|
+
'Y': '1011', 'Z': '1100'
|
|
10
|
+
},
|
|
11
|
+
'2': {
|
|
12
|
+
'0': '11111', '1': '01111', '2': '00111', '3': '00011', '4': '00001',
|
|
13
|
+
'5': '00000', '6': '10000', '7': '11000', '8': '11100', '9': '11110'
|
|
14
|
+
},
|
|
15
|
+
'3': {
|
|
16
|
+
'.': '010101', ',': '110011', '?': '001100', '\'': '011110', '!': '101011', '/': '10010',
|
|
17
|
+
'(': '10110', ')': '101101', '&': '01000', ':': '111000', ';': '101010', '=': '10001',
|
|
18
|
+
'+': '01010', '-': '100001', '_': '001101', '"': '010010', '$': '0001001', '@': '011010',
|
|
19
|
+
'¿': '00101', '¡': '110001'
|
|
20
|
+
},
|
|
21
|
+
'4': {
|
|
22
|
+
'Ã': '01101', 'Á': '01101', 'Å': '01101', 'À': '01101', 'Â': '01101', 'Ä': '0101',
|
|
23
|
+
'Ą': '0101', 'Æ': '0101', 'Ç': '10100', 'Ć': '10100', 'Ĉ': '10100', 'Č': '110',
|
|
24
|
+
'Ð': '00110', 'È': '01001', 'Ę': '00100', 'Ë': '00100', 'É': '00100',
|
|
25
|
+
'Ê': '10010', 'Ğ': '11010', 'Ĝ': '11010', 'Ĥ': '1111', 'İ': '01001', 'Ï': '10011',
|
|
26
|
+
'Ì': '01110', 'Ĵ': '01110', 'Ł': '01001', 'Ń': '11011', 'Ñ': '11011', 'Ó': '1110',
|
|
27
|
+
'Ò': '1110', 'Ö': '1110', 'Ô': '1110', 'Ø': '1110', 'Ś': '0001000', 'Ş': '01100',
|
|
28
|
+
'Ș': '1111', 'Š': '1111', 'Ŝ': '00010', 'ß': '000000', 'Þ': '01100', 'Ü': '0011',
|
|
29
|
+
'Ù': '0011', 'Ŭ': '0011', 'Ž': '11001', 'Ź': '110010', 'Ż': '11001'
|
|
30
|
+
},
|
|
31
|
+
'5': {
|
|
32
|
+
'А': '01', 'Б': '1000', 'В': '011', 'Г': '110', 'Д': '100', 'Е': '0',
|
|
33
|
+
'Ж': '0001', 'З': '1100', 'И': '00', 'Й': '0111', 'К': '101', 'Л': '0100',
|
|
34
|
+
'М': '11', 'Н': '10', 'О': '111', 'П': '0110', 'Р': '010', 'С': '000',
|
|
35
|
+
'Т': '1', 'У': '001', 'Ф': '0010', 'Х': '0000', 'Ц': '1010', 'Ч': '1110',
|
|
36
|
+
'Ш': '1111', 'Щ': '1101', 'Ъ': '11011', 'Ы': '1011', 'Ь': '1001', 'Э': '00100',
|
|
37
|
+
'Ю': '0011', 'Я': '0101', 'Ї': '01110', 'Є': '00100', 'І': '00', 'Ґ': '110'
|
|
38
|
+
},
|
|
39
|
+
'6': {
|
|
40
|
+
'Α': '01', 'Β': '1000', 'Γ': '110', 'Δ': '100', 'Ε': '0', 'Ζ': '1100',
|
|
41
|
+
'Η': '0000', 'Θ': '1010', 'Ι': '00', 'Κ': '101', 'Λ': '0100', 'Μ': '11',
|
|
42
|
+
'Ν': '10', 'Ξ': '1001', 'Ο': '111', 'Π': '0110', 'Ρ': '010', 'Σ': '000',
|
|
43
|
+
'Τ': '1', 'Υ': '1011', 'Φ': '0010', 'Χ': '1111', 'Ψ': '1101', 'Ω': '011'
|
|
44
|
+
},
|
|
45
|
+
'7': {
|
|
46
|
+
'א': '01', 'ב': '1000', 'ג': '110', 'ד': '100', 'ה': '111', 'ו': '0',
|
|
47
|
+
'ז': '1100', 'ח': '0000', 'ט': '001', 'י': '00', 'כ': '101', 'ל': '0100',
|
|
48
|
+
'מ': '11', 'נ': '10', 'ס': '1010', 'ע': '0111', 'פ': '0110', 'צ': '011',
|
|
49
|
+
'ק': '1101', 'ר': '010', 'ש': '000', 'ת': '1'
|
|
50
|
+
},
|
|
51
|
+
'8': {
|
|
52
|
+
'ا': '01', 'ب': '1000', 'ت': '1', 'ث': '1010', 'ج': '0111', 'ح': '0000',
|
|
53
|
+
'خ': '111', 'د': '100', 'ذ': '1100', 'ر': '010', 'ز': '1110', 'س': '000',
|
|
54
|
+
'ش': '1111', 'ص': '1001', 'ض': '0001', 'ط': '001', 'ظ': '1011', 'ع': '0101',
|
|
55
|
+
'غ': '110', 'ف': '0010', 'ق': '1101', 'ك': '101', 'ل': '0100', 'م': '11',
|
|
56
|
+
'ن': '10', 'ه': '00100', 'و': '011', 'ي': '00', 'ﺀ': '0'
|
|
57
|
+
},
|
|
58
|
+
'9': {
|
|
59
|
+
'ا': '01', 'ب': '1000', 'پ': '0110', 'ت': '1', 'ث': '1010', 'ج': '0111',
|
|
60
|
+
'چ': '1110', 'ح': '0000', 'خ': '1001', 'د': '100', 'ذ': '0001', 'ر': '010',
|
|
61
|
+
'ز': '1100', 'ژ': '110', 'س': '000', 'ش': '1111', 'ص': '0101', 'ض': '00100',
|
|
62
|
+
'ط': '001', 'ظ': '1011', 'ع': '111', 'غ': '0011', 'ف': '0010', 'ق': '111000',
|
|
63
|
+
'ک': '101', 'گ': '1101', 'ل': '0100', 'م': '11', 'ن': '10', 'و': '011',
|
|
64
|
+
'ه': '0', 'ی': '00'
|
|
65
|
+
},
|
|
66
|
+
'10': {
|
|
67
|
+
'ア': '11011', 'カ': '0100', 'サ': '10101', 'タ': '10', 'ナ': '010', 'ハ': '1000',
|
|
68
|
+
'マ': '1001', 'ヤ': '011', 'ラ': '000', 'ワ': '101', 'イ': '01', 'キ': '10100',
|
|
69
|
+
'シ': '11010', 'チ': '0010', 'ニ': '1010', 'ヒ': '11001', 'ミ': '00101', 'リ': '110',
|
|
70
|
+
'ヰ': '01001', 'ウ': '001', 'ク': '0001', 'ス': '11101', 'ツ': '0110', 'ヌ': '0000',
|
|
71
|
+
'フ': '1100', 'ム': '1', 'ユ': '10011', 'ル': '10110', 'ン': '01010', 'エ': '10111',
|
|
72
|
+
'ケ': '1011', 'セ': '01110', 'テ': '01011', 'ネ': '1101', 'ヘ': '0', 'メ': '10001',
|
|
73
|
+
'レ': '111', 'ヱ': '01100', 'オ': '01000', 'コ': '1111', 'ソ': '1110', 'ト': '00100',
|
|
74
|
+
'ノ': '0011', 'ホ': '100', 'モ': '10010', 'ヨ': '11', 'ロ': '0101', 'ヲ': '0111',
|
|
75
|
+
'゛': '00', '゜': '00110', '。': '010100', 'ー': '01101', '、': '010101',
|
|
76
|
+
'(': '101101', ')': '010010'
|
|
77
|
+
},
|
|
78
|
+
'11': {
|
|
79
|
+
'ㄱ': '0100', 'ㄴ': '0010', 'ㄷ': '1000', 'ㄹ': '0001', 'ㅁ': '11', 'ㅂ': '011',
|
|
80
|
+
'ㅅ': '110', 'ㅇ': '101', 'ㅈ': '0110', 'ㅊ': '1010', 'ㅋ': '1001', 'ㅌ': '1100',
|
|
81
|
+
'ㅍ': '111', 'ㅎ': '0111', 'ㅏ': '0', 'ㅑ': '00', 'ㅓ': '1', 'ㅕ': '000',
|
|
82
|
+
'ㅗ': '01', 'ㅛ': '10', 'ㅜ': '0000', 'ㅠ': '010', 'ㅡ': '100', 'ㅣ': '001'
|
|
83
|
+
},
|
|
84
|
+
'12': {
|
|
85
|
+
'ก': '110', 'ข': '1010', 'ค': '101', 'ง': '10110', 'จ': '10010',
|
|
86
|
+
'ฉ': '1111', 'ช': '1001', 'ซ': '1100', 'ญ': '0111', 'ด': '100',
|
|
87
|
+
'ต': '1', 'ถ': '10100', 'ท': '10011', 'น': '10', 'บ': '1000',
|
|
88
|
+
'ป': '0110', 'ผ': '1101', 'ฝ': '10101', 'พ': '01100', 'ฟ': '0010',
|
|
89
|
+
'ม': '11', 'ย': '1011', 'ร': '010', 'ล': '0100', 'ว': '011',
|
|
90
|
+
'ส': '000', 'ห': '0000', 'อ': '10001', 'ฮ': '11011', 'ฤ': '01011',
|
|
91
|
+
'ะ': '01000', 'า': '01', 'ิ': '00100', 'ี': '00', 'ึ': '00110',
|
|
92
|
+
'ื': '0011', 'ุ': '00101', 'ู': '1110', 'เ': '0', 'แ': '0101',
|
|
93
|
+
'ไ': '01001', 'โ': '111', 'ำ': '00010', '่': '001', '้': '0001',
|
|
94
|
+
'๊': '11000', '๋': '01010', 'ั': '01101', '็': '11100', '์': '11001',
|
|
95
|
+
'ๆ': '10111', 'ฯ': '11010'
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const getCharacters = (options) => (Object.assign(Object.assign({}, baseCharacters), { '0': baseCharacters[options.priority], '1': Object.assign(Object.assign({}, baseCharacters['1']), { [options.separator]: options.space }) }));
|
|
99
|
+
const getMappedCharacters = (options, usePriority) => {
|
|
100
|
+
const mapped = {};
|
|
101
|
+
const characters = getCharacters(options);
|
|
102
|
+
for (const set in characters) {
|
|
103
|
+
mapped[set] = {};
|
|
104
|
+
for (const key in characters[set]) {
|
|
105
|
+
mapped[set][key] = characters[set][key].replace(/0/g, options.dot).replace(/1/g, options.dash);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!usePriority) {
|
|
109
|
+
delete mapped['0'];
|
|
110
|
+
}
|
|
111
|
+
return mapped;
|
|
112
|
+
};
|
|
113
|
+
const swapCharacters = (options) => {
|
|
114
|
+
const swapped = {};
|
|
115
|
+
const mappedCharacters = getMappedCharacters(options, true);
|
|
116
|
+
for (const set in mappedCharacters) {
|
|
117
|
+
for (const key in mappedCharacters[set]) {
|
|
118
|
+
if (typeof swapped[mappedCharacters[set][key]] === 'undefined') {
|
|
119
|
+
swapped[mappedCharacters[set][key]] = key;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return swapped;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const getOptions = (opts = {}) => {
|
|
127
|
+
var _a, _b, _c;
|
|
128
|
+
const options = Object.assign(Object.assign({}, opts), { dash: opts.dash || '-', dot: opts.dot || '.', space: opts.space || '/', separator: opts.separator || ' ', invalid: opts.invalid || '#', priority: opts.priority || 1, wpm: opts.wpm, unit: opts.unit || 0.08, fwUnit: opts.fwUnit || opts.unit || 0.08, volume: opts.volume || 100, oscillator: Object.assign(Object.assign({}, opts.oscillator), { type: ((_a = opts.oscillator) === null || _a === void 0 ? void 0 : _a.type) || 'sine', frequency: ((_b = opts.oscillator) === null || _b === void 0 ? void 0 : _b.frequency) || 500, onended: ((_c = opts.oscillator) === null || _c === void 0 ? void 0 : _c.onended) || null // event that fires when the tone has stopped playing
|
|
129
|
+
}) });
|
|
130
|
+
return options;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const getGainTimings = (morse, opts, currentTime = 0) => {
|
|
134
|
+
const timings = [];
|
|
135
|
+
let { unit, fwUnit } = opts;
|
|
136
|
+
let time = 0;
|
|
137
|
+
if (opts.wpm) {
|
|
138
|
+
// wpm mode uses standardised units
|
|
139
|
+
unit = fwUnit = 60 / (opts.wpm * 50);
|
|
140
|
+
}
|
|
141
|
+
timings.push([0, time]);
|
|
142
|
+
const tone = (i) => {
|
|
143
|
+
timings.push([1 * (opts.volume / 100.0), currentTime + time]);
|
|
144
|
+
time += i * unit;
|
|
145
|
+
};
|
|
146
|
+
const silence = (i) => {
|
|
147
|
+
timings.push([0, currentTime + time]);
|
|
148
|
+
time += i * unit;
|
|
149
|
+
};
|
|
150
|
+
const gap = (i) => {
|
|
151
|
+
timings.push([0, currentTime + time]);
|
|
152
|
+
time += i * fwUnit;
|
|
153
|
+
};
|
|
154
|
+
for (let i = 0, addSilence = false; i <= morse.length; i++) {
|
|
155
|
+
if (morse[i] === opts.space) {
|
|
156
|
+
gap(7);
|
|
157
|
+
addSilence = false;
|
|
158
|
+
}
|
|
159
|
+
else if (morse[i] === opts.dot) {
|
|
160
|
+
if (addSilence)
|
|
161
|
+
silence(1);
|
|
162
|
+
else
|
|
163
|
+
addSilence = true;
|
|
164
|
+
tone(1);
|
|
165
|
+
}
|
|
166
|
+
else if (morse[i] === opts.dash) {
|
|
167
|
+
if (addSilence)
|
|
168
|
+
silence(1);
|
|
169
|
+
else
|
|
170
|
+
addSilence = true;
|
|
171
|
+
tone(3);
|
|
172
|
+
}
|
|
173
|
+
else if ((typeof morse[i + 1] !== 'undefined' && morse[i + 1] !== opts.space) &&
|
|
174
|
+
(typeof morse[i - 1] !== 'undefined' && morse[i - 1] !== opts.space)) {
|
|
175
|
+
gap(3);
|
|
176
|
+
addSilence = false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return [timings, time];
|
|
180
|
+
};
|
|
181
|
+
// Source: https://github.com/mattdiamond/Recorderjs/blob/master/src/recorder.js#L155
|
|
182
|
+
const encodeWAV = (sampleRate, samples) => {
|
|
183
|
+
const buffer = new ArrayBuffer(44 + samples.length * 2);
|
|
184
|
+
const view = new DataView(buffer);
|
|
185
|
+
const writeString = (view, offset, string) => {
|
|
186
|
+
for (let i = 0; i < string.length; i++) {
|
|
187
|
+
view.setUint8(offset + i, string.charCodeAt(i));
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
// RIFF identifier
|
|
191
|
+
writeString(view, 0, 'RIFF');
|
|
192
|
+
// RIFF chunk length
|
|
193
|
+
view.setUint32(4, 36 + samples.length * 2, true);
|
|
194
|
+
// RIFF type
|
|
195
|
+
writeString(view, 8, 'WAVE');
|
|
196
|
+
// format chunk identifier
|
|
197
|
+
writeString(view, 12, 'fmt ');
|
|
198
|
+
// format chunk length
|
|
199
|
+
view.setUint32(16, 16, true);
|
|
200
|
+
// sample format (raw)
|
|
201
|
+
view.setUint16(20, 1, true);
|
|
202
|
+
// channel count
|
|
203
|
+
view.setUint16(22, 1, true);
|
|
204
|
+
// sample rate
|
|
205
|
+
view.setUint32(24, sampleRate, true);
|
|
206
|
+
// byte rate (sample rate * block align)
|
|
207
|
+
view.setUint32(28, sampleRate * 4, true);
|
|
208
|
+
// block align (channel count * bytes per sample)
|
|
209
|
+
view.setUint16(32, 2, true);
|
|
210
|
+
// bits per sample
|
|
211
|
+
view.setUint16(34, 16, true);
|
|
212
|
+
// data chunk identifier
|
|
213
|
+
writeString(view, 36, 'data');
|
|
214
|
+
// data chunk length
|
|
215
|
+
view.setUint32(40, samples.length * 2, true);
|
|
216
|
+
// to PCM
|
|
217
|
+
const floatTo16BitPCM = (output, offset, input) => {
|
|
218
|
+
for (let i = 0; i < input.length; i++, offset += 2) {
|
|
219
|
+
const s = Math.max(-1, Math.min(1, input[i]));
|
|
220
|
+
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
floatTo16BitPCM(view, 44, samples);
|
|
224
|
+
return view;
|
|
225
|
+
};
|
|
226
|
+
const audio = (morse, options) => {
|
|
227
|
+
let AudioContext = null;
|
|
228
|
+
let OfflineAudioContext = null;
|
|
229
|
+
let context = null;
|
|
230
|
+
let offlineContext = null;
|
|
231
|
+
let source;
|
|
232
|
+
const [gainValues, totalTime] = getGainTimings(morse, options);
|
|
233
|
+
if (AudioContext === null && typeof window !== 'undefined') {
|
|
234
|
+
AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
235
|
+
context = new AudioContext();
|
|
236
|
+
}
|
|
237
|
+
if (OfflineAudioContext === null && typeof window !== 'undefined') {
|
|
238
|
+
OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
|
|
239
|
+
offlineContext = new OfflineAudioContext(1, 44100 * totalTime, 44100);
|
|
240
|
+
}
|
|
241
|
+
if (!context || !offlineContext) {
|
|
242
|
+
throw new Error('Web Audio API is not supported in this browser');
|
|
243
|
+
}
|
|
244
|
+
source = context.createBufferSource();
|
|
245
|
+
source.connect(context.destination);
|
|
246
|
+
const oscillator = offlineContext.createOscillator();
|
|
247
|
+
const gainNode = offlineContext.createGain();
|
|
248
|
+
oscillator.type = options.oscillator.type;
|
|
249
|
+
oscillator.frequency.value = options.oscillator.frequency;
|
|
250
|
+
gainValues.forEach(([value, time]) => gainNode.gain.setValueAtTime(value, time));
|
|
251
|
+
oscillator.connect(gainNode);
|
|
252
|
+
gainNode.connect(offlineContext.destination);
|
|
253
|
+
source.onended = options.oscillator.onended;
|
|
254
|
+
// Inspired by: http://joesul.li/van/tale-of-no-clocks/
|
|
255
|
+
const render = new Promise(resolve => {
|
|
256
|
+
oscillator.start(0);
|
|
257
|
+
offlineContext.startRendering();
|
|
258
|
+
offlineContext.oncomplete = (e) => {
|
|
259
|
+
source.buffer = e.renderedBuffer;
|
|
260
|
+
resolve();
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
let timeout;
|
|
264
|
+
const play = async () => {
|
|
265
|
+
await render;
|
|
266
|
+
source.start(context.currentTime);
|
|
267
|
+
timeout = setTimeout(() => { stop(); }, totalTime * 1000);
|
|
268
|
+
};
|
|
269
|
+
const stop = () => {
|
|
270
|
+
clearTimeout(timeout);
|
|
271
|
+
timeout = 0;
|
|
272
|
+
source.stop(0);
|
|
273
|
+
};
|
|
274
|
+
const getWaveBlob = async () => {
|
|
275
|
+
await render;
|
|
276
|
+
const waveData = encodeWAV(offlineContext.sampleRate, source.buffer.getChannelData(0));
|
|
277
|
+
return new Blob([waveData], { 'type': 'audio/wav' });
|
|
278
|
+
};
|
|
279
|
+
const getWaveUrl = async () => {
|
|
280
|
+
const audioBlob = await getWaveBlob();
|
|
281
|
+
return URL.createObjectURL(audioBlob);
|
|
282
|
+
};
|
|
283
|
+
const exportWave = async (filename) => {
|
|
284
|
+
const waveUrl = await getWaveUrl();
|
|
285
|
+
const anchor = document.createElement('a');
|
|
286
|
+
anchor.href = waveUrl;
|
|
287
|
+
anchor.target = '_blank';
|
|
288
|
+
anchor.download = filename || 'morse.wav';
|
|
289
|
+
anchor.click();
|
|
290
|
+
};
|
|
291
|
+
return {
|
|
292
|
+
play,
|
|
293
|
+
stop,
|
|
294
|
+
getWaveBlob,
|
|
295
|
+
getWaveUrl,
|
|
296
|
+
exportWave,
|
|
297
|
+
context,
|
|
298
|
+
oscillator,
|
|
299
|
+
gainNode
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
(((name, root, factory) => {
|
|
304
|
+
if (typeof exports === 'object') {
|
|
305
|
+
module.exports = factory();
|
|
306
|
+
}
|
|
307
|
+
else if (typeof define === 'function' && define.amd) {
|
|
308
|
+
define(factory);
|
|
309
|
+
}
|
|
310
|
+
else if (root !== undefined) {
|
|
311
|
+
root[name] = factory();
|
|
312
|
+
}
|
|
313
|
+
})('morse-code-translator', globalThis, () => {
|
|
314
|
+
const encode = (text, opts) => {
|
|
315
|
+
const options = getOptions(opts);
|
|
316
|
+
const characters = getCharacters(options);
|
|
317
|
+
return [...text.replace(/\s+/g, options.separator).trim().toLocaleUpperCase()].map(function (character) {
|
|
318
|
+
for (const set in characters) {
|
|
319
|
+
if (typeof characters[set] !== 'undefined' && typeof characters[set][character] !== 'undefined') {
|
|
320
|
+
return characters[set][character];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return options.invalid;
|
|
324
|
+
}).join(options.separator).replace(/0/g, options.dot).replace(/1/g, options.dash);
|
|
325
|
+
};
|
|
326
|
+
const decode = (morse, opts) => {
|
|
327
|
+
const options = getOptions(opts);
|
|
328
|
+
const swapped = swapCharacters(options);
|
|
329
|
+
return morse.replace(/\s+/g, options.separator).trim().split(options.separator).map(function (characters) {
|
|
330
|
+
if (typeof swapped[characters] !== 'undefined') {
|
|
331
|
+
return swapped[characters];
|
|
332
|
+
}
|
|
333
|
+
return options.invalid;
|
|
334
|
+
}).join('');
|
|
335
|
+
};
|
|
336
|
+
const characters = (options, usePriority) => getMappedCharacters(getOptions(options), usePriority);
|
|
337
|
+
const audio$1 = (text, opts, morseString) => {
|
|
338
|
+
const morse = morseString || encode(text, opts);
|
|
339
|
+
const options = getOptions(opts);
|
|
340
|
+
return audio(morse, options);
|
|
341
|
+
};
|
|
342
|
+
return {
|
|
343
|
+
characters,
|
|
344
|
+
decode,
|
|
345
|
+
encode,
|
|
346
|
+
audio: audio$1
|
|
347
|
+
};
|
|
348
|
+
}));
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getCharacters, getMappedCharacters, swapCharacters } from './characters';
|
|
2
|
+
import getOptions from './options';
|
|
3
|
+
import getAudio from './audio';
|
|
4
|
+
|
|
5
|
+
declare let exports;
|
|
6
|
+
declare let module;
|
|
7
|
+
declare let define;
|
|
8
|
+
|
|
9
|
+
;(((name, root, factory) => {
|
|
10
|
+
if (typeof exports === 'object') {
|
|
11
|
+
module.exports = factory();
|
|
12
|
+
} else if (typeof define === 'function' && define.amd) {
|
|
13
|
+
define(factory);
|
|
14
|
+
} else if (root !== undefined) {
|
|
15
|
+
root[name] = factory();
|
|
16
|
+
}
|
|
17
|
+
})('morse-code-translator', globalThis, () => {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const encode = (text: string, opts: Options) => {
|
|
21
|
+
const options = getOptions(opts);
|
|
22
|
+
const characters = getCharacters(options);
|
|
23
|
+
return [...text.replace(/\s+/g, options.separator).trim().toLocaleUpperCase()].map(function (character) {
|
|
24
|
+
for (const set in characters) {
|
|
25
|
+
if (typeof characters[set] !== 'undefined' && typeof characters[set][character] !== 'undefined') {
|
|
26
|
+
return characters[set][character];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return options.invalid;
|
|
30
|
+
}).join(options.separator).replace(/0/g, options.dot).replace(/1/g, options.dash);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const decode = (morse: string, opts: Options) => {
|
|
34
|
+
const options = getOptions(opts);
|
|
35
|
+
const swapped = swapCharacters(options);
|
|
36
|
+
return morse.replace(/\s+/g, options.separator).trim().split(options.separator).map(function (characters) {
|
|
37
|
+
if (typeof swapped[characters] !== 'undefined') {
|
|
38
|
+
return swapped[characters];
|
|
39
|
+
}
|
|
40
|
+
return options.invalid;
|
|
41
|
+
}).join('');
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const characters = (options, usePriority) => getMappedCharacters(getOptions(options), usePriority);
|
|
45
|
+
|
|
46
|
+
const audio = (text: string, opts: Options, morseString: string) => {
|
|
47
|
+
const morse = morseString || encode(text, opts);
|
|
48
|
+
const options = getOptions(opts);
|
|
49
|
+
return getAudio(morse, options);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
characters,
|
|
54
|
+
decode,
|
|
55
|
+
encode,
|
|
56
|
+
audio
|
|
57
|
+
};
|
|
58
|
+
}));
|
package/src/options.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const getOptions = (opts: Partial<Options> = {}): Options => {
|
|
2
|
+
const options: Options = {
|
|
3
|
+
...opts,
|
|
4
|
+
dash: opts.dash || '-',
|
|
5
|
+
dot: opts.dot || '.',
|
|
6
|
+
space: opts.space || '/',
|
|
7
|
+
separator: opts.separator || ' ',
|
|
8
|
+
invalid: opts.invalid || '#',
|
|
9
|
+
priority: opts.priority || 1,
|
|
10
|
+
wpm: opts.wpm, // words per minute - PARIS method used in favour of unit/fwUnit options
|
|
11
|
+
unit: opts.unit || 0.08, // period of one unit, in seconds, 1.2 / c where c is speed of transmission, in words per minute
|
|
12
|
+
fwUnit: opts.fwUnit || opts.unit || 0.08, // Farnsworth unit to control intercharacter and interword gaps
|
|
13
|
+
volume: opts.volume || 100,
|
|
14
|
+
oscillator: {
|
|
15
|
+
...opts.oscillator,
|
|
16
|
+
type: opts.oscillator?.type || 'sine', // sine, square, sawtooth, triangle
|
|
17
|
+
frequency: opts.oscillator?.frequency || 500, // value in hertz
|
|
18
|
+
onended: opts.oscillator?.onended || null // event that fires when the tone has stopped playing
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
return options;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default getOptions;
|