@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 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;