2018年09月11日

夏の工作 Raspberry Pi (欧文/和文モールス音源生成スクリプト)

留守番猫カメラからの続き。

せっかくのラズパイを何か無線関係に使うことはできないだろうか。D-starのノード局にして使う,というのは聞いたことがあるけれど,D-starが使えるリグもないし,そもそもphoneはあまり興味がない。

せっかくならCW関係の何か...といってもすぐ思いつくエレキー/メモリキーヤーなんかはありきたりだし,すでに電池駆動で使えるメモリキーヤーを持っているので,別に必要性もない。

そこで,ちょうど和文や欧文の聞き取り練習をしているところでもあるので,日本語の普通文をCWの音声ファイルに変換するスクリプトはどうだろう。

Windows用にA1A Breakerというすばらしいフリーソフトがあるけれど、確か日本語の平文(漢字仮名交じり文)を直接変換することはできなかったのでは。

というような思考を経て、次のような仕様を考えた。
1 欧文と和文を自動判定
2 和文の場合、漢字仮名交じり文をカナに変換
3 CWの音声ファイルを生成

1はまあ含まれている文字から簡単に判別できそう。
3も文字に対応した符号を生成して、sin波の入切で音声データを作るのはなんとかなりそう。

やっかいなのは2で、漢字仮名交じり文の読み仮名を解析してカナに変換する方法を考えなくてはいけない。

自力でそんなコードを書くのはほとんど無理だけど、世の中には親切な人がいるもので、MeCabというオープンソースの形態素解析エンジンがある。これは日本語の文章を単語に区切り、品詞を同定する処理をしてくれるもので、そのついでに読み仮名も出してくれる。

で、これを使って1、2、3それぞれの機能ごとに少しずつコードを書いていった。




この辺からPythonの日本語処理でハマることが多くなる。まずstrとunicodeと文字コードがごちゃごちゃになってよくわからない。出てくるエラーを見ながらググってはエラーを出し,print文を仕込んでordやtypeやencode文を使って確認しつつ,辻褄を合わせるデバッグ作業が続いた。

文字コード(ASCIIコード,UTF-8コード)に対応したモールス符号の生成(半角"ア" -> UTF-8"FF67 -> モールス符号"11011")は以下のサイトを参考にした。欧文は普通にASCIIコードで変換できる(ord("A")=65)んだけど,半角カナはord("ア")=177とはならず,ord("ア")=65393(FF71)で,これはUTF-8コード。この辺も最初はよくわからずハマった。

参考サイト:JH7UBCブログ ラズパイ Python モールス符号練習機

MeCabの読み変換はもちろん完璧ではなく,「大喜び」を「だいよろこび」と変換するなど,細かいミスはあるものの,一般的な文章なら相当精度良く変換できる。これは本当に素晴らしいなぁ。

和文符号だと濁点や半濁点を分離しないといけない(「ザ」は「サ゛」にする)必要があり,このために全角カナを半角カナにさらに変換。半角カナには濁点・半濁点付きの文字がないので,強制的に分離される。




sin波をつなげて音声を作っていくのは,以下のサイトを参考にした。信号ありをレベル1,無音部分を0として,ドットを基準の長さにして,ダッシュは3倍の長さ,というようにつなげていく。

参考サイト:[python] sin波の音をWAV形式で出力する

サンプリング周波数は最初サンプルと同じくCD音質の44100Hzとしていたけれど,これだと2分を超えるような符号列だと変換途中にメモリーエラーで落ちてしまうことが判明。よく考えたらCWのトーン周波数は700Hzなので,標本化定理に従えば1400Hzで充分再現できるはず。一応さらに倍の2800Hzとした。これで15分以上の長い文章でも一気に変換できるようになったと思う。

まだまだおかしいところはあると思うけれど,現状のコードを載せておく。いろいろなライブラリを読み込んでいるので,importに書かれているライブラリを事前にインストールしておく必要がある。

速度は変数[sec]に短点の秒数で指定。現状だと総通試験の和文の速度(1分間75字程度)になっていると思う。適宜調整してほしい。総通の試験で出ないと思われる符号はバッサリ削除している。音源はそのままでは完成度が低くて使えないかもしれないので,カナ変換した時点で生成されるout.txtをA1A Breakerなりに読み込ませて使うのも良いと思う。

実行方法は下記。引数に変換元のテキストファイルを入れておく。
python txt2cw.py momotaro.txt


ソースコード txt2cw.py
※「動きません」とか質問されても,当方もラズパイ,Pythonビギナーなのでお答えできない可能性が高いです。アドバイスは歓迎いたします。

#!/usr/bin/env python
# coding: utf-8

import numpy as np
import wave
import struct
import pydub
import sys
import MeCab
import mojimoji
import subprocess
import re
import codecs
import jaconv
import textwrap
import gc

#文字コードーモールス符号の変換テーブル
Morse_Code =[63,62,60,56,48,32,33,35,39,47,1,1,1,1,1,1,1,6,17,21,9,2,20,
11,16,4,30,13,18,7,5,15,22,27,10,8,3,12,24,14,25,29,19]
Wabun_Code =[30,59,6,12,61,34,14,57,7,22,54,59,6,12,61,34,18,37,24,29,31, #ヲ〜コ
53,43,55,46,23,5,20,22,58,36,10,21,16,27,28,17,51,19,2,9, #サ〜ホ
25,52,3,49,41,14,57,7,8,11,45,15,26,13,42,4,44, #マ〜ン
109,82,121,40,74,106] # ()ホレ ラタ 段落 区切り点

m = MeCab.Tagger('-Oyomi')

# 原文の読込み(第1引数にファイル名を指定)
f = codecs.open(sys.argv[1], 'r', 'utf-8')
lines = f.readlines()
f.close
bunsho = '\n'.join(lines)
print('*** 原文 ***')
print(bunsho)

# 試験で出ない符号は削除,またはスペースに置換する
bunsho = re.sub('[&%#+*/@$=],', '', bunsho)
bunsho = re.sub('[-\'\",.()]', ' ', bunsho)

#複数の連続するスペースは一つにする
bunsho = re.sub(r"\s+", " ", bunsho)

yomi = m.parse(bunsho.encode('utf-8'))
print('*** MeCab 読み仮名取得***')
print(yomi)
#一般名詞のひらがなで残ったものをカタカナにする
yomi2 = jaconv.hira2hkata(unicode(yomi,'utf-8'))
print('*** jaconv ひらがなtoカタカナ***')
print(yomi2)

#A1A Breaker用にテキストファイルを出力
with open('./out.txt', mode='w') as f:
s_wrap_list = textwrap.wrap(yomi2,20)
f.write('\n'.join(s_wrap_list))

han = mojimoji.zen_to_han(yomi2.decode('utf-8'))
han = han[:len(han)-1]
print('*** mojimoji 全角to半角 ***')
print(han)

message=han
message_len=len(message)

#不要な変数を削除してメモリ解放
del lines
del bunsho
del yomi
del yomi2
del han
gc.collect()

A = 1 # 振幅
fs = 2800 #サンプリング周波数
f0 = 700 #基本周波数 CW信号のトーン
sec = 0.055 #秒 短点の基準

cw_tone = []
# アルファベットは全て大文字に変換
message = message.upper()

# 欧文/和文の判定
#regexp = re.compile(r'[^\x20-\x7E]')
regexp = re.compile(r'(?:\xEF\xBD[\xA1-\xBF]|\xEF\xBE[\x80-\x9F])')
result = regexp.search(message.encode('utf-8'))
if result != None:
wabun = 1 # 和文のときは1 欧文は0
else:
wabun = 0

print(wabun)

if wabun ==1:
message = u'<' + message + u'>' # 和文の場合はホレ,ラタを追加

#和文中のアルファベットは削除(総通の問題には出ないので)
message = re.sub(r'[a-zA-Z]+', r'', message)
message_len=len(message)

#和文中の試験に出ない符号は削除する
message = re.sub('[?!()]', '', message)
message_len=len(message)
print(message)

#sin波
def create_wave(A,f0,fs,t): #A:振幅,f0:基本周波数,fs:サンプリング周波数,再生時間[s]
#nポイント
point = np.arange(0,fs*t)
sin_wave =A* np.sin(2*np.pi*f0*point/fs)

sin_wave = [int(x * 32767.0) for x in sin_wave] #16bit符号付き整数に変換

return sin_wave

def create_cwfile(data): # wavファイルの作成と書き出し
#バイナリ化
binwave = struct.pack("h" * len(data), *data)

#サイン波をwavファイルとして書き出し
w = wave.Wave_write("cw.wav")
p = (1, 2, fs, len(binwave), 'NONE', 'not compressed')
w.setparams(p)
w.writeframes(binwave)
w.close()
print(len(data))

#mp3に変換
sound = pydub.AudioSegment.from_wav("cw.wav")
sound.export("cw.mp3", format="mp3")

def dot(): #短点
cw_tone.extend(create_wave(A,f0,fs,sec))

def dash(): #長点
cw_tone.extend(create_wave(A,f0,fs,3*sec))

def space(): #文字間隔
cw_tone.extend(create_wave(0,f0,fs,2*sec))

#文字コードを対応するモールス符号に変換
try:
while True:
for i in range(message_len):
char_code = ord(message[i])
print(char_code)
if char_code == 0x20: #space code
space()
elif wabun == 0: # 欧文
Mcode = Morse_Code[char_code - 48]
while Mcode != 1:
mark = Mcode & 1
if mark == 0:
dot()
else:
dash()
cw_tone.extend(create_wave(0,f0,fs,sec)) # 符号間のスペース(1点)
Mcode >>=1
cw_tone.extend(create_wave(0,f0,fs,2*sec)) # 文字間のスペース(3点分)
elif wabun == 1: # 和文
if char_code >= 48 and char_code <= 57: # 数字
Mcode = Morse_Code[char_code - 48]
elif char_code >= 65 and char_code <= 90: # 和文中のアルファベット
Mcode = Morse_Code[char_code - 48]
elif char_code == 60: #ホレ
Mcode =121
elif char_code == 62: #ラタ
Mcode =40
elif char_code == 40 or char_code == 65378: #下向きカッコ
Mcode =109
elif char_code == 41 or char_code == 65379: #上向きカッコ
Mcode =82
elif char_code == 65377: #段落(。)
Mcode =74
elif char_code == 10 or char_code == 13: #単なる改行は無視
Mcode =1
elif char_code == 65380 or char_code == 65381: #区切り点,中黒
Mcode =106
else:
Mcode = Wabun_Code[char_code - 65382]
while Mcode != 1:
mark = Mcode & 1
if mark == 0:
dot()
else:
dash()
cw_tone.extend(create_wave(0,f0,fs,sec)) # 符号間のスペース(1点)
Mcode >>=1
cw_tone.extend(create_wave(0,f0,fs,2*sec)) # 文字間のスペース(3点分)
create_cwfile(cw_tone)
break

except KeyboardInterrupt:
pass


出力されたmp3をffmpegで動画にしたもの。
原文はももたろうの前半部分。
【パソコン・インターネットの最新記事】
posted by ソウヘイ at 20:34| Comment(0) | パソコン・インターネット
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント: