今年に入ってから国信大の事務室の電話がIP化したそうです。(新しい国信大の電話番号は050-5438-6933)
IP化により、ただ電話機で受けるだけではない高度な電話サービスを簡単に実現することができます。自動音声応答もその一つですよね。
そこで、久々にちょっと電話ハックということ自動音声応答(Interactive Voice Response, IVR)を自作してみようと思います。
IVRの構築に必要なものは3つあります。1つは当然ですが電話回線が必要ですよね。2つ目も当然ですが自動音声応答で流れる音声です。そして3つ目がIVRを実装するPBXサーバーです。(PBXっていうのは構内交換機のことです。)
電話回線についてはSIPでレジストできる回線が必要です。NTTのひかり電話は固定電話の番号のままIP化(VoIP)できるので有名ですね。また、050番号のIP電話は多数の会社が提供していると思います。個人趣味でちょっと電話回線が欲しい場合は楽天コミュニケーションズか提供しているFUSION IP-Phone SMARTとかいいんじゃないかと思います。スマホの通話料金を節約するのに便利なサービスですが、番号が月額無料で発行できるのでありがたいです。無料で050番号が持てるサービスは他には050 Freeとかもあります。
自動音声応答で流す音声ですが、21世紀ですから声優やアナウンサーの友達がいなくても適当な方法で音声合成すればいいですよね。ゆっくりに喋らせてもいいですし、Google翻訳の読み上げ機能を使うこともできます。AWSの音声合成サービスのPollyなんかは完全ではないですが自然に近い音声を合成できるのでおすすめです。
そして本題のIVRを構築する方法ですが世の中にはTwillioなど便利なサービスがありますが有償で企業向けだったりするので個人では使いにくいです。無料で済ませるには自前のサーバーでオープンソースのIP-PBXであるAsteriskを動かすのが鉄板です。
Asteriskの導入
まず、サーバーにAsteriskを導入します。debianなら# apt-get install asteriskで一発でインスコできると思います。
次にSIPの設定です。/etc/asterisk/sip.confを編集します。INFファイルの要領で設定を書き換えます。
まず、忘れずに行いたいのは外部から勝手に発信されないようゲスト発信を許可しないことです。[general]セクションにallowguest=noを追加しましょう。
次にSIPアカウントの設定をします。SIPアカウントのログイン情報を用意して[general]セクションに以下を追記します。
register => ユーザー:パスワード@ドメイン
次にsip.confの末尾にもアカウント情報を記載します。
type=friend
username=ユーザー
fromuser=ユーザー
secret=パスワード
host=ドメイン
fromdomain=ドメイン
context=default
insecure=port,invite
canreinvite=no
disallow=all
allow=ulaw
allow=alaw
dtmfmode=inband
nat=yes
こんな感じで記載します。これでSIPアカウントの設定ができました。
次はいよいよ着信後の動作の設定をします。ここでIVRを組み立てます。/etc/asterisk/extensions.confを編集します。
IVR用のセクションの名前を仮にmy-ivrとでもしておきましょう。
まず、[default]セクションに着信したらmy-ivrセクションに飛ぶように以下の記述をします。
exten => SIPのユーザー,1,Goto(my-ivr,s,1)
これは(SIPのユーザー)への着信は1番目にmy-ivrセクションのsコンテキストの1番目に飛ぶという意味です。
my-ivrセクションを書いていきます。
IVRの流れは例として以下の流れで構築するとします。
- 音声ファイルの場所は/home/asteriskとする
- 着信したら5秒間呼び出し音を鳴らした後応答する
- 最初に音声ファイルhelloを流す
- 次に音声ファイルmenuを流すと同時に数字入力を受け付ける
- 1の場合、message1-1、message1-2を流して終了
- 2の場合、message2-1、message2-2を流して最初の選択に戻る
- 3の場合、message3を流して1の場合に続ける
- 無操作は1の場合に続ける
- 誤った番号は音声ファイルinvalidを流して選択に戻る
上記のIVRの流れの設定をコメント付きで載せてみますのでこれを参考にいじれば自分だけの独自のIVRが構築できると思います。基本的に「exten => コンテキスト,順番,コマンド」を並べて記述します。コンテキストは最初はs、番号別の場合はその番号です。順番は同一コンテキスト内でのコマンドの実行順です。1からスタートして2,3,4…と続いていきますが番号を決め打ちすると後から編集する時に番号の振り直しが発生して大変なので2番目以降はnと書くことで前のコマンドの次と扱われます。
[my-ivr];exten => コンテキスト,順番,コマンド
;コンテキストsからスタートします
exten => s,1,Set(dir=/home/asterisk/) ;変数定義(音声ファイルのディレクトリ)
exten => s,n,Ringing() ;呼び出し中であることを明示する
exten => s,n,Wait(3) ;3秒待つ
exten => s,n,Answer() ;応答する
exten => s,n,Playback(${dir}hello) ;音声ファイル/home/asterisk/helloを再生
exten => s,n(menu),Background(${dir}menu) ;音声ファイルmenuを再生しつつ番号入力を受け付ける 後でここに飛べるようにn(名前)という感じで順番に名前をつけることができます。
exten => s,n,WaitExten(10) ;番号が入力されるまで待つ
exten => s,n,Goto(1,1) ;番号が入力され無かった場合コンテキスト1の1番に飛ぶ
;誤った番号(その番号のコンテキストが定義されてない)が入力されるとコンテキストiに飛びます。
exten => i,1,Playback(${dir}invalid) ;音声ファイルinvalidを再生
exten => i,2,Goto(s,menu) ;コンテキストsの順番n(menu)に飛ぶ
;番号選択で1が押されたらコンテキスト1に飛びます
exten => 1,1,Playback(${dir}message1-1)
exten => 1,n,Playback(${dir}message1-2)
;番号選択で2が押されたらコンテキスト2に飛びます
exten => 2,1,Playback(${dir}message2-1&${dir}message2-2) ; &で挟んで複数のファイルを指定できます
exten => 2,n,Goto(s,menu)
;番号選択で3がry)
exten => 3,1,Playback(${dir}message3)
exten => 3,n,Goto(1,1) ;コンテキスト1の順番1に飛ぶ
上記で音声ファイルを指定しましたが、音声ファイルはmp3とかはそのまま使えずgsmやulawでエンコードしたものを用意する必要があります。基本gsm形式で用意して、余裕があったらulawでも用意しておくのが良いと思います。(gsm形式だけでも十分です。)
音声ファイルの変換はsoxコマンドが使えます。
gsm形式に変換(拡張子は.gsm)
$ sox sound.mp3 -r 8000 -c 1 sound.gsm
ulawに変換(拡張子は.ulaw)
$ sox sound.mp3 -r 8000 -c 1 -e u-law -t wav sound.ulaw
extensions.confで音声ファイルを指定するときは拡張子を除いたファイル名を指定します。(sound.gsmファイルを再生したいときはPlayback(sound)って感じ)
設定ができたらasteriskを再読み込みすれば反映されます。(# systemctl reload asterisk)
asteriskが起動していなければ起動します。(# systemctl start asterisk)
# asterisk -vvvcrでasteriskのcliを起動できます。cliを起動後に表示されるログを見ながら電話をかけて動作を確認すればデバッグしやすいと思います。
いかがでしたか?
asteriskにはたくさんの機能がありますので色々組み合わせたら結構高度なことができるんじゃないかと思います。
21世紀は電話も情報化している時代です。ご家庭にある自宅サーバーにasteriskを導入すれば誰でも企業レベルの電話システムをおうちで実現することできます。
くれぐれもセキュリティには十分注意しましょう。勝手に発信されて国際電話されまくって高額な電話代を請求される被害が多いです。(僕も高校生の時、家電をasteriskにつなげて遊んでたら中東方面に国際電話されまくって涙目になったことがあります。数日で気がついたので諭吉数人の被害で済みました。諭吉数人が「これくらいで済んで良かった」と思うくらい国際電話の被害は恐ろしいです。昔のダイヤルQ2みたいな感じで途上国に国際電話して現地の電話会社からキックバックをもらうという手口らしいです。)
被害にあったからと言って電話会社は一切電話代を免除してくれないと思います。
asteriskに収容する電話回線は国際電話できない設定にする、サーバーのファイヤーウォールで外部からSIP接続は遮断するくらいは必要です。allowguest=noの設定も絶対に忘れないでください。
突然 の質問をお許し下さい
外線着信時のIVR入力ができません
内線時のは正常に動いてくれます
※電話の受信・発信は問題ありません
お知恵を貸して頂けないでしょうか
宜しくお願い致します
○内線1234正常に1を入力できます
gvc-asterisk*CLI>
== Using SIP RTP CoS mark 5
— Executing [1234@default:1] Goto(“SIP/512-000001e3”, “incoming,s,1”) in new stack
— Goto (incoming,s,1)
— Executing [s@incoming:1] NoOp(“SIP/512-000001e3”, “”*———-* 時間外 incoming Menu *———-*””) in new stack
— Executing [s@incoming:2] Answer(“SIP/512-000001e3”, “”) in new stack
— Executing [s@incoming:3] BackGround(“SIP/512-000001e3”, “info-1”) in new stack
— Playing ‘info-1.gsm’ (language ‘ja’)
— Executing [1@incoming:1] NoOp(“SIP/512-000001e3”, “”*———-* 時間外 折り返 *———-*””) in new stack
— Executing [1@incoming:2] SayNumber(“SIP/512-000001e3”, “1”) in new stack
— Playing ‘digits/1.ulaw’ (language ‘ja’)
— Executing [1@incoming:3] NoOp(“SIP/512-000001e3”, “”*——* Input 1 *———*””) in new stack
— Auto fallthrough, channel ‘SIP/512-000001e3’ status is ‘UNKNOWN’
— Executing [h@incoming:1] NoOp(“SIP/512-000001e3”, “”*—- h ——*””) in new stack
— Executing [h@incoming:2] Hangup(“SIP/512-000001e3”, “”) in new stack
○外線へ着信した時メニューへ分岐しますが1を入力できません
— Executing [4000@incoming:1] NoOp(“SIP/GVC-000001e4”, “0908888888”) in new stack
— Executing [4000@incoming:3] GotoIf(“SIP/GVC-000001e4”, “1?JIKANGAI”) in new stack
— Goto (incoming,4000,15)
— Executing [4000@incoming:15] Goto(“SIP/GVC-000001e4”, “incoming,s,1”) in new stack
— Goto (incoming,s,1)
— Executing [s@incoming:1] NoOp(“SIP/GVC-000001e4”, “”*———-* 時間外 incoming Menu *———-*””) in new stack
— Executing [s@incoming:2] Answer(“SIP/GVC-000001e4”, “”) in new stack
— Executing [s@incoming:3] BackGround(“SIP/GVC-000001e4”, “info-1”) in new stack
— Playing ‘info-1.gsm’ (language ‘ja’)
— Executing [s@incoming:4] WaitExten(“SIP/GVC-000001e4”, “2”) in new stack
== Spawn extension (incoming, s, 4) exited non-zero on ‘SIP/GVC-000001e4’
— Executing [h@incoming:1] NoOp(“SIP/GVC-000001e4”, “”*—- h ——*””) in new stack
— Executing [h@incoming:2] Hangup(“SIP/GVC-000001e4”, “”) in new stack
; Asterisk 13.14.1~dfsg-2+deb9u4
;—————
; sip.conf
;—————
[general]
context=incoming
register => xxxxx:password@CALL_01/4000
[CALL_01]
type=friend
secret=password
Port=5060
username=xxxxx
fromuser=xxxxx
host=192.168.11.1
context=incoming
insecure=invite
canreinvite=no
dtmfmode=inband
;—————
; extensions.conf
;—————
[general]
writeprotect=no
priorityjumping=no
[incoming]
exten => s,1,NoOp(“*———-* 時間外 incoming Menu *———-*”)
same => n,Answer()
same => n(tryagain),Background(info-1)
same => n,WaitExten(2)
same => n,NoOp(“*———-* 時間外 Menu End *———-*”)
exten => 1,1,NoOp(“*———-* 時間外 折り返 *———-*”)
exten => 1,n,SayNumber(1)
exten => 1,n,NoOp(“*——* Input 1 OK OK OK OK *———*”)
exten => t,1,NoOp(“*—- t ——*”)
exten => t,n,Goto(10,tryagain)
exten => h,1,NoOp(“*—- h ——*”)
exten => h,n,Hangup
exten => i,1,NoOp(“*—- i ——*”)
exten => i,n,Goto(10,tryagain)
;——————————
; GVC着信
;——————————
exten => 4000,1,NoOp(${CALLERID(num)})
exten => 4000,n,NoOp(“*——–* 4000 *———*”)
exten => 4000,n,GotoIf($[“${CALLERID(num)}” = “0908888888”]?JIKANGAI)
exten => 4000,n,GotoIfTime(00:00-08:59,*,*,*?JIKANGAI)
exten => 4000,n,GotoIfTime(18:00-23:59,*,*,*?JIKANGAI)
exten => 4000,n,Wait(1)
exten => 4000,n,Dial(${CALL_CENTER_GROUP},60,rtT)
exten => 4000,n,Hangup
exten => 4000,n(JIKANGAI),Goto(incoming,s,1)
[default]
exten => 1234,1,Goto(incoming,s,1)
;————-
; 発信
;————-
exten => _0.,1,Set(CALLFILENAME=./${CALLERID(num)}/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-OUT-${EXTEN})
exten => _0.,n,Set(CALLERID(num)=${MYNUMBER2})
exten => _0.,n,Set(CALLERID(name)=${MYNUMBER2})
exten => _0.,n,Dial(SIP/${EXTEN}@${SR-7100VN},30,rtT)
こんにちは
外線着信時にDTMF音が認識されない感じですね。(外線→asteriskへ音声がちゃんと届いているならば)
軽く調べたところsip.confの外線アカウント(ここでは[CALL_01])のところで
dtmfmode=auto
にするか、disallow=all
allow=ulaw
allow=alaw
dtmfmode=inband
とコーデックをulawかalawに制限した上でdtmfmode=inbandにすると動作しそうな感じがします。
管理者様
返信ありがとうございます
感謝感謝です
通話は、正常に出来ております
>dtmfmode=autoにするか、
>disallow=all
>allow=ulaw
>allow=alaw
>dtmfmode=inband
どちらも 結果は、同じでした
logger.conf へconsole => notice,warning,error,dtmf
を追加して
確認したところ
入力は受け取れているように見えます
しかし 分岐してくれません
内線経由で同じメニューを通すと正常に動作します
もうお手上げ状態です
[May 28 11:32:57] DTMF[2241][C-0000002a]: channel.c:4103 __ast_read: DTMF begin ‘1’ received on SIP/512-000000a2
[May 28 11:32:57] DTMF[2241][C-0000002a]: channel.c:4114 __ast_read: DTMF begin passthrough ‘1’ on SIP/512-000000a2
[May 28 11:32:57] DTMF[2241][C-0000002a]: channel.c:4017 __ast_read: DTMF end ‘1’ received on SIP/512-000000a2, duration 200 ms
[May 28 11:32:57] DTMF[2241][C-0000002a]: channel.c:4058 __ast_read: DTMF end accepted with begin ‘1’ on SIP/512-000000a2
[May 28 11:32:57] DTMF[2241][C-0000002a]: channel.c:4087 __ast_read: DTMF end passthrough ‘1’ on SIP/512-000000a2
[May 28 11:32:59] DTMF[2241][C-0000002a]: channel.c:4103 __ast_read: DTMF begin ‘1’ received on SIP/512-000000a2
[May 28 11:32:59] DTMF[2241][C-0000002a]: channel.c:4114 __ast_read: DTMF begin passthrough ‘1’ on SIP/512-000000a2
[May 28 11:32:59] DTMF[2241][C-0000002a]: channel.c:4017 __ast_read: DTMF end ‘1’ received on SIP/512-000000a2, duration 200 ms
[May 28 11:32:59] DTMF[2241][C-0000002a]: channel.c:4058 __ast_read: DTMF end accepted with begin ‘1’ on SIP/512-000000a2
[May 28 11:32:59] DTMF[2241][C-0000002a]: channel.c:4087 __ast_read: DTMF end passthrough ‘1’ on SIP/512-000000a2
管理者様
出来ました!!
下記で正常に動作しました
本当にありがとうございました
[general]
dtmfmode=auto
[CALL_01]
;dtmfmode=auto
管理者様
回答頂きありがとうございます
感謝感謝です
CALL_01のdtmfmode=autoへ変更し
/etc/asterisk/logger.conf
console => notice,warning,error,dtmf
で実行してみました
入力は受け取れているみたですが分岐してくれません
[May 28 09:43:19] DTMF[2005][C-00000019]: channel.c:4103 __ast_read: DTMF begin ‘1’ received on SIP/512-0000006b
[May 28 09:43:19] DTMF[2005][C-00000019]: channel.c:4114 __ast_read: DTMF begin passthrough ‘1’ on SIP/512-0000006b
[May 28 09:43:19] DTMF[2005][C-00000019]: channel.c:4017 __ast_read: DTMF end ‘1’ received on SIP/512-0000006b, duration 200 ms
[May 28 09:43:19] DTMF[2005][C-00000019]: channel.c:4058 __ast_read: DTMF end accepted with begin ‘1’ on SIP/512-0000006b
[May 28 09:43:19] DTMF[2005][C-00000019]: channel.c:4087 __ast_read: DTMF end passthrough ‘1’ on SIP/512-0000006b
[May 28 09:43:22] DTMF[2005][C-00000019]: channel.c:4103 __ast_read: DTMF begin ‘2’ received on SIP/512-0000006b
[May 28 09:43:22] DTMF[2005][C-00000019]: channel.c:4114 __ast_read: DTMF begin passthrough ‘2’ on SIP/512-0000006b
[May 28 09:43:22] DTMF[2005][C-00000019]: channel.c:4017 __ast_read: DTMF end ‘2’ received on SIP/512-0000006b, duration 200 ms
[May 28 09:43:22] DTMF[2005][C-00000019]: channel.c:4058 __ast_read: DTMF end accepted with begin ‘2’ on SIP/512-0000006b
[May 28 09:43:22] DTMF[2005][C-00000019]: channel.c:4087 __ast_read: DTMF end passthrough ‘2’ on SIP/512-0000006b
[May 28 09:43:24] DTMF[2005][C-00000019]: channel.c:4103 __ast_read: DTMF begin ‘3’ received on SIP/512-0000006b
[May 28 09:43:24] DTMF[2005][C-00000019]: channel.c:4114 __ast_read: DTMF begin passthrough ‘3’ on SIP/512-0000006b
[May 28 09:43:25] DTMF[2005][C-00000019]: channel.c:4017 __ast_read: DTMF end ‘3’ received on SIP/512-0000006b, duration 200 ms
[May 28 09:43:25] DTMF[2005][C-00000019]: channel.c:4058 __ast_read: DTMF end accepted with begin ‘3’ on SIP/512-0000006b
[May 28 09:43:25] DTMF[2005][C-00000019]: channel.c:4087 __ast_read: DTMF end passthrough ‘3’ on SIP/512-0000006b
下記も同じ結果でした
disallow=all
allow=ulaw
allow=alaw
dtmfmode=inband
— Executing [4000@incoming:1] NoOp(“SIP/GVC-00000072”, “0900000”) in new stack
— Executing [4000@incoming:2] NoOp(“SIP/GVC-00000072”, “”*——–* 4000 *———*””) in new stack
— Executing [4000@incoming:3] GotoIf(“SIP/GVC-00000072”, “0?JIKANGAI”) in new stack
— Executing [4000@incoming:6] GotoIfTime(“SIP/GVC-00000072”, “00:00-08:59,mon-sun,29-31,dec?JIKANGAI”) in new stack
— Executing [4000@incoming:7] GotoIfTime(“SIP/GVC-00000072”, “00:00-23:59,mon-sun,1-3,jan?JIKANGAI”) in new stack
— Executing [4000@incoming:8] GotoIfTime(“SIP/GVC-00000072”, “00:00-08:59,*,*,*?JIKANGAI”) in new stack
— Executing [4000@incoming:9] GotoIfTime(“SIP/GVC-00000072”, “09:00-23:59,*,*,*?JIKANGAI”) in new stack
— Goto (incoming,4000,15)
— Executing [4000@incoming:15] Goto(“SIP/GVC-00000072”, “incoming,s,1”) in new stack
— Goto (incoming,s,1)
— Executing [s@incoming:1] NoOp(“SIP/GVC-00000072”, “”*———-* 時間外 incoming Menu *———-*””) in new stack
— Executing [s@incoming:2] Answer(“SIP/GVC-00000072”, “”) in new stack
> 0x73f20aa0 — Strict RTP switching to RTP target address 192.168.11.1:5208 as source
— Executing [s@incoming:3] BackGround(“SIP/GVC-00000072”, “info-1”) in new stack
— Playing ‘info-1.gsm’ (language ‘ja’)
> 0x73b29558 — Strict RTP learning after remote address set to: 192.168.11.1:5212
— SIP/192.168.11.1-00000071 answered SIP/512-00000070
— Channel SIP/192.168.11.1-00000071 joined ‘simple_bridge’ basic-bridge
— Channel SIP/512-00000070 joined ‘simple_bridge’ basic-bridge
> 0x73b29558 — Strict RTP switching to RTP target address 192.168.11.1:5212 as source
> 0x73f07148 — Strict RTP switching to RTP target address 192.168.11.130:4002 as source
> 0x73f07148 — Strict RTP learning complete – Locking on source address 192.168.11.130:4002
> 0x73f20aa0 — Strict RTP learning complete – Locking on source address 192.168.11.1:5208
> 0x73b29558 — Strict RTP learning complete – Locking on source address 192.168.11.1:5212
[May 28 09:50:46] DTMF[2042][C-0000001d]: channel.c:4103 __ast_read: DTMF begin ‘1’ received on SIP/512-00000070
[May 28 09:50:46] DTMF[2042][C-0000001d]: channel.c:4114 __ast_read: DTMF begin passthrough ‘1’ on SIP/512-00000070
[May 28 09:50:46] DTMF[2042][C-0000001d]: channel.c:4017 __ast_read: DTMF end ‘1’ received on SIP/512-00000070, duration 200 ms
[May 28 09:50:46] DTMF[2042][C-0000001d]: channel.c:4058 __ast_read: DTMF end accepted with begin ‘1’ on SIP/512-00000070
[May 28 09:50:46] DTMF[2042][C-0000001d]: channel.c:4087 __ast_read: DTMF end passthrough ‘1’ on SIP/512-00000070
[May 28 09:50:52] DTMF[2042][C-0000001d]: channel.c:4103 __ast_read: DTMF begin ‘1’ received on SIP/512-00000070
[May 28 09:50:52] DTMF[2042][C-0000001d]: channel.c:4114 __ast_read: DTMF begin passthrough ‘1’ on SIP/512-00000070
[May 28 09:50:52] DTMF[2042][C-0000001d]: channel.c:4017 __ast_read: DTMF end ‘1’ received on SIP/512-00000070, duration 200 ms
[May 28 09:50:52] DTMF[2042][C-0000001d]: channel.c:4058 __ast_read: DTMF end accepted with begin ‘1’ on SIP/512-00000070
[May 28 09:50:52] DTMF[2042][C-0000001d]: channel.c:4087 __ast_read: DTMF end passthrough ‘1’ on SIP/512-00000070
— Executing [s@incoming:4] WaitExten(“SIP/GVC-00000072”, “2”) in new stack
— Timeout on SIP/GVC-00000072, continuing…
— Executing [s@incoming:5] NoOp(“SIP/GVC-00000072”, “”*———-* 時間外 Menu End *———-*””) in new stack
— Auto fallthrough, channel ‘SIP/GVC-00000072’ status is ‘UNKNOWN’
— Executing [h@incoming:1] NoOp(“SIP/GVC-00000072”, “”*—- h ——*””) in new stack
— Executing [h@incoming:2] Hangup(“SIP/GVC-00000072”, “”) in new stack
== Spawn extension (incoming, h, 2) exited non-zero on ‘SIP/GVC-00000072’