<!DOCTYPE html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Morse code</title>
<style type="text/css">
<!--
BODY {
line-height: 170%;
}
-->
</style>
<script type="text/javascript">
var AudioContext = window.webkitAudioContext || window.AudioContext;
var act = new AudioContext();
// 文字間隔は短点3つ分、語間隔は短点7つ分
var mcode = {
' ':'0000',
'_':'0000',
'A':'102',
'B':'2010101',
'C':'2010201',
'D':'20101',
'E':'1',
'F':'1010201',
'G':'20201',
'H':'1010101',
'I':'101',
'J':'1020202',
'K':'20102',
'L':'1020101',
'M':'202',
'N':'201',
'O':'20202',
'P':'1020201',
'Q':'2020102',
'R':'10201',
'S':'10101',
'T':'2',
'U':'10102',
'V':'1010102',
'W':'10202',
'X':'2010102',
'Y':'2010202',
'Z':'2020101',
'0':'202020202',
'1':'102020202',
'2':'101020202',
'3':'101010202',
'4':'101010102',
'5':'101010101',
'6':'201010101',
'7':'202010101',
'8':'202020101',
'9':'202020201',
};
var Query = {};
function initPage()
{
// Get Query
if (1 < document.location.search.length) {
var parameters = document.location.search.substring(1).split('&');
for (var i = 0; i < parameters.length; i++) {
var element = parameters[i].split('=');
Query[ decodeURIComponent(element[0]).toUpperCase() ] = decodeURIComponent(element[1]);
}
}
if( Query['M'] ){
document.form001.strMorse.value = Query['M'];
}
if( Query['F'] ){
document.form001.frequency.value = parseInt( Query['F'] );
}
if( Query['V'] ){
document.form001.volume.value = parseInt( Query['V'] );
}
if( Query['W'] ){
var w = parseInt( Query['W'] );
if( w >= 1 && w <= 4 ){
var w001 = document.form001.wave;
for (var i = 0; i < w001.length; i++){
w001[i].checked = (i == w -1);
}
}
}
if( Query['S'] ){
document.form001.speed.value = parseInt( Query['S'] );
}
if( Query['C'] ){
document.form001.statusChk.checked = false;
}
}
var soundMutex = false;
var soundData = []; // [ スタート, 長さ, 文字位置, 文字 ] * n
var soundTick = 60.0/50.0/70;
var soundIndex = 0;
var soundSubIdx = 0;
var soundCh = 0;
var soundT0 = 0;
var soundT9 = 0;
function Setup() {
if( soundMutex ){
soundIndex = soundData.length + 1;
soundSubIdx = soundIndex;
soundT9 = 0;
return;
}
soundData = [];
var spd = parseInt( document.form001.speed.value );
var str = document.form001.strMorse.value.toUpperCase();
var tick = 60.0/50.0/spd;
soundTick = tick;
var tock = 0;
var offset = 0.3;
for (var i = 0; i < str.length; i++) {
var ch = str.charAt(i);
if( !mcode[ch] ){
continue;
}
var mch = mcode[ch];
for (var j = 0; j < mch.length; j++) {
switch( mch.charAt(j) ){
case '0':
tock += 1;
break;
case '1':
soundData.push( [tick*tock + offset, tick, i, ch] );
tock += 1;
break;
case '2':
soundData.push( [tick*tock + offset, 3*tick, i, ch] );
tock += 3;
break;
}
}
tock += 3;
}
soundIndex = 0;
soundSubIdx = 0;
soundCh = -1;
soundT0 = act.currentTime;
soundT9 = 0;
if( soundData.length > 0 ){
soundT9 = soundData[soundData.length-1][0] + soundData[soundData.length-1][1] + 3*tick;
}
document.form001.statusTxt.value = "";
document.form001.toggle.value = "STOP";
if( act.state == 'suspended' ){
act.resume();
}
soundMutex = true;
setTimeout( PlaySound, 0 );
}
function PlaySound()
{
var frq = document.form001.frequency.value;
var vol = parseInt( document.form001.volume.value ) / 100.0;
vol = (Math.exp(vol)-1)/(Math.exp(1)-1)
var wav = 'sine'; // sine, triangle, sawtooth, square
var w001 = document.form001.wave;
for (var i = 0; i < w001.length; i++){
if( w001[i].checked ){
wav = w001[i].value;
}
}
var dt1 = act.currentTime - soundT0;
for(var i = soundIndex; i < soundData.length; i++){
if( soundData[i][0] - soundTick*3 > dt1 ){
break;
}
var osc = act.createOscillator();
var gain = act.createGain();
osc.connect(gain);
gain.connect(act.destination);
osc.frequency.value = frq;
osc.type = wav;
osc.detune.value = 0;
gain.gain.value = vol;
osc.start(soundT0 + soundData[i][0]);
osc.stop( soundT0 + soundData[i][0] + soundData[i][1] );
soundIndex = i + 1;
}
for(var i = soundSubIdx; i < soundData.length; i++){
if( document.form001.subtitle[0].checked ){
if( soundData[i][0] - soundTick*2 > dt1 ) break;
} else {
if( soundData[i][0]+soundData[i][1] + soundTick*2 > dt1 ){
break;
}
if( i < soundData.length - 1 && soundData[i][2] == soundData[i+1][2] ){
soundSubIdx = i + 1;
break;
}
}
if( soundData[i][2] > soundCh ){
if( !document.form001.subtitle[2].checked ){
if( soundData[i][2] - soundCh > 1 ){
document.form001.statusTxt.value += " ";
}
document.form001.statusTxt.value += soundData[i][3];
}
soundCh = soundData[i][2];
}
soundSubIdx = i + 1;
}
if( soundT9 > dt1 ){
setTimeout( PlaySound, soundTick );
} else {
if( act.state == 'running' ){
act.close();
act = new AudioContext();
}
document.form001.toggle.value = "Start";
soundMutex = false;
}
}
function SetupJJY()
{
if( soundMutex ){
return;
}
var dd = new Date();
var hhmm = ('0'+dd.getHours()).substr(-2,2) + ('0'+dd.getMinutes()).substr(-2,2);
document.form001.strMorse.value = "JJY JJY " + hhmm + " N N N N N";
}
function statusCls()
{
document.form001.statusTxt.value = "";
}
</script>
</head>
<body onload='initPage()'>
<h1>Morse code</h1>
<br>
<form name="form001">
Text: <input type="text" name="strMorse" size="70">
<input type="button" name="toggle" onclick="Setup()" value="Start"><br>
Frequency: <input type="number" name="frequency" min="20" max="30000" value="440" style="text-align:right;font-family:monospace;"> [Hz]<br>
Volume: <input type="range" name="volume" min="0" max="100" value="3"><br>
Wave:
<input type="radio" name="wave" value="sine" checked>sine
<input type="radio" name="wave" value="triangle">triangle
<input type="radio" name="wave" value="sawtooth">sawtooth
<input type="radio" name="wave" value="square">square
<br>
WPM: <input type="number" name="speed" min="1" max="100" value="20" style="text-align:right;font-family:monospace;"> (作動中は変更を受け付けない)<br>
<br>
<input type="button" onclick="SetupJJY()" value="JJY"><br>
<br>
<div style="width: 40em; border:1px solid gray; padding: 10px;">
Status:
<input type="text" name="statusTxt" size="70" readonly style="border: 1px dotted gray;">
<input type="button" onclick="statusCls()" value="CLS"><br>
mode
<input type="radio" name="subtitle" value="P" checked>previous
<input type="radio" name="subtitle" value="A">after
<input type="radio" name="subtitle" value="N">none
</divn>
<br>
</form>
</body>
</html>
<!--
※お題
(1) 入力された英数字([A-Z0-9])をモールス信号を表す文字列に変換する。大文字小文字は区別しない。
記号は変換出来なくても良い。また数字の略体は不要。短点と長点には何の文字を使っても良いが、
なるべく分かり易くするために「・」、「-」等にすること。
※参照
コードを音楽にする「コードサウンド」をWebAudioAPIで作った - Qiita:
https://q...content-available-to-author-only...a.com/zaru/items/87a82a3cf6839c692972
Web Audio APIでコインの音を作る - Qiita:
https://q...content-available-to-author-only...a.com/mohayonao/items/c506f7ddcaac63694eb9
Getting Started with Web Audio API - HTML5 Rocks:
https://w...content-available-to-author-only...s.com/ja/tutorials/webaudio/intro/
Web Audio API 解説 - 03.オシレーターの使い方 | g200kg Music & Software:
https://w...content-available-to-author-only...g.com/jp/docs/webaudio/oscillator.html
>なお、このようにあらかじめタイミングを予約する事ができるからと言って、あまりに大量の予約を詰め込む事は推奨されていません。
※メモ
Pale Moon だと、実行中に他のタブに切り替えると、ほぼ確実に発声のタイミングなどがおかしくなる。
タブを切り替えなくても偶にずれてるような気がする。
Vivaldi,Kinza だと上記のようなことはないような気がする。
最近のブラウザはローカルファイルでもクエリを受け付けるとは思ってなかった。
昔やったときは失敗したと思ったけれど、それが間違いだったのかもしれない。
2018/05/19
ステータス出力のタイミングを、モールス符号出力直前と直後を選べるようにした。字幕モードと解読モード。
2018/05/21
AudioContext.resume()を追加。終了/中途終了時に AudioContext.close() & new AudioContext()。
-->