津波警報などが発表された際のピロピロ音を解析するまで

# はじめに

津波警報などが発表された際にNHKなどでよく流される[^1]ピロピロ音ですが、これは低い音と高い音がなっていて、その高い方の音が1、低い方の音が0になっていて現在発生している事象についての情報が送信されているらしいです。
世間一般ではFSK(周波数偏移変調)と呼ばれているらしいです。
仕様としては、

|状態|周波数|
|:--:|:---:|
|0|640Hz|
|1|1024Hz|

で64bpsで送信されているようです。

この音自体は[Wikipedia先生](https://ja.wikipedia.org/wiki/%E7%B7%8A%E6%80%A5%E8%AD%A6%E5%A0%B1%E6%94%BE%E9%80%81)によると緊急警報放送(EWS)を開始するための信号で、この音に反応して対応している機器を起動させたりする(していた)ようです[^2]。

信号処理などの知識は全くありませんが、昔から実際にリアルタイムにビット列を見ながら解析してみたいと思っていたのもあり、この度チャレンジしてみました。大変でした。

# 完成したもの

ソース: https://github.com/ingen084/EwsDemodulator
開発言語はC#(.NET Core)でNAudioを使用して`WasapiLoopbackCapture`でPCの鳴っている音を解析します。
解析部分は現状完全にNAudioに頼らない作りになりました。
WASAPIは音を鳴らしてるアプリケーションがない場合キャプチャデータが送られてこなくなるのですが、今回は考慮していません。

動画(音量注意): https://streamable.com/4quqw
音源はWikipediaにあるものを使用しました。

# 実際に作っている最中に困ったこと

## YouTubeの動画の音声を使用してはいけない

実際の放送で動くか検証するために、Youtubeの動画を使用しようとしていましたが、音声の加工がされているため波形レベルでは全く異なる音声になっている動画がありました。[^8]

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">初回の第2種開始信号がなんかおかしい…(EWS) <a href="https://t.co/XtyQwJo85J">pic.twitter.com/XtyQwJo85J</a></p>&mdash; ingen084 (@ingen084) <a href="https://twitter.com/ingen084/status/1201765944424452097?ref_src=twsrc%5Etfw">December 3, 2019</a></blockquote>

ニコニコ動画の音声を利用しました。

## プレイヤーにも気をつけよう

これは当たり前というか、自分の落ち度なのですがPCの音をキャプチャしながらテストしたりする場合、プレイヤーの設定によっては波形などがカットされてしまったりするため注意しておきましょう。

今回はfoobar2000のSoundTouch DSPを有効にしていたのですが、(実際に再生速度を変更する設定は有効にしていない)メチャクチャな波形が出力されていました。[^9]

# 試行

## とりあえず区切ってFFT

私の知識では波形から周波数を特定するにはフーリエ変換という思い込みがあった[^3]ので、そのまま波形を区切ってFFTにかけようと考えました。
64bpsで送られてくるということはサンプリングレートからサンプルの数が割り出せるので、そこから計算しようと考えました。(画像は波形を表してるつもり。)
![fft](https://gyazo.ingen084.net/data/419a54cbdde664dd3112eede377d88cf.png)

ですが、まずきれいにサンプルを区切ることはできませんでした。
44100/64 = 689.0625 なので当たり前です。
また、無音検出のずれやそもそものサンプル数の少なさもあり成分が混ざり全くうまく判断することができませんでした[^4]。

## Goertzelアルゴリズムを使用してみる

FFT以外の検出方法を探していると、[DTMF](https://ja.wikipedia.org/wiki/DTMF)[^5]の検出などに使用されるGoertzelアルゴリズムというものを発見しました。
これはイケるかもと思い組み込んでみましたが、やはりサンプルがきれいに取れないということで精度は変わらず、散々でした。

## 常に一定幅で区切ってみる

FFTでは精度の確保のために1ビット分(15ミリ秒ぐらい)をまとめて処理させる必要がありましたが、Goertzelの場合ある程度短くても問題ないだろうと無音区間が終わってからのサンプルを5msごとに区切った上で0か1を判断し、だいたい1ビットの長さになればそのビットを確定するというような手法にしてみました。
![image](https://gyazo.ingen084.net/data/4a7a87986fa36f3513a5a9bafcb4e636.png)

動画: https://streamable.com/bdtdw
音源はNHKFMの試験放送
精度はだいぶ改善され、体感では6~70%ぐらいにはなっていたと思います。

しかし、この手法でもかなり問題点がありました。

一定間隔ごとで区切ってチェックしないといけないためビットを確定する際は1/64秒よりも少ないサンプル数で確定しないといけませんでした。
ですが高頻度で信号が変化する場合うまく捉えることができず、そのために確定するサンプル数を減らすと同じ値が4~5連続した際に検出数が増えてしまうことがありました。
解決のために1msで区切ったりしようとしましたが、さすがにアルゴリズムの検出の限界もあり精度を上げることはできませんでした[^10]。

## ゼロクロッシング検出

もうだめだと思っていたのですが、よく考えてみれば単音なので波形を見るだけで周波数を特定することが可能ということがわかりました。(手書きなのでめっちゃ汚いのは許してください…)
![zx](https://gyazo.ingen084.net/data/921ae86ff8bbd3c44eaf5792013d7417.png)
実際に手法としてゼロクロッシング検出[^6]という名前がついていたので、実装してみることにしました。
当時とりあえず実装してみた手法としてはこんな感じです。

1. (実際に入力された波形を舐めながら)波形がマイナスからプラスに0と交差している点を探す。
2. 前回交差した時間からの経過時間(サンプルのインデックス)とサンプリングレートから周波数を推定する。
3. **50Hz**以上ずれているようであればカウンタをリセット。
4. 一定時間以上周波数が安定していれば周波数からビットの割り出す。

50Hzというのは640Hzあたりにおいてかなり大きな周波数の違いになってしまっていたと思います。
ですが1周期だけでの推定はかなりズレが大きく、実際に安定させるにはこれぐらいの遊びが必要でした。

結果として精度はさらに向上し8~90%ぐらいにはなっていたと思います。
ですがいくつかの問題は克服できておらず、実際の80%ぐらいのサンプル数で確定しないといけない点など前回の問題点をそのまま引き継ぐ形となってしまいました。

## 断定方法の改善

これが現在の方法です。

まずは無駄な点を排除していきました。
周波数ベースで計算するのも誤差が大きい上に640Hz or 1024Hz のものしか必要ないのでその周波数に必要なサンプル数を事前に計算しておきその2つの周波数に近いかだけチェックするようにしました。(3値化)
周波数が変わった際にもすぐに反応するわけではなくしばらく待ってから変更を確定するようにしました。
さらに次のビットが来るであろう時間を予測しその時間が経過後現在のビットとして確定するようにしました。

流れとしては

```
生のサンプル -> 1周期の波形 -> 3値 -> 1ビット
```
![image](https://gyazo.ingen084.net/data/6e6c486b199e666a7206a0db008c5eab.png)

となりました。

動画はこちら

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">(音量注意)解析してる感があってとてもいい<br>2011/3/11に実際に放送されたデータを参考にしています <a href="https://t.co/JBAYoU1a3Z">pic.twitter.com/JBAYoU1a3Z</a></p>&mdash; ingen084 (@ingen084) <a href="https://twitter.com/ingen084/status/1202050345951588352?ref_src=twsrc%5Etfw">December 4, 2019</a></blockquote>

精度はこの時点でほぼ100%になり、十分実用圏内に入りました。

# パーサの作成

デコードが正常に動くようになったテンションでさらっと書いたのもあり、かなり適当に作りました。

EWSのフォーマットについては昭和60年に郵政省が告示している [無線設備規則第九条の三第五号の規定に基づく緊急警報信号の構成](https://www.tele.soumu.go.jp/horei/reiki_honbun/a721030001.html) に記載があります。
地域コードなどはあまりにも面倒だったので正規表現で一括変換しました。

## 処理の流れ

パーサの主なステートは2つあり、**前置符号解析**と**ブロック解析**です。

### 前置符号解析

1. 最初の4ビットまで待って信号の種類を検出する
2. 信号の種類が検出できたら種別に応じたブロック解析モードへ

### ブロック解析

1. 32ビット受信するまで待つ
2. 先頭の16ビットが固定符号に一致しているかチェックする
3. 処理中のブロックステージ･処理中のブロック数からパースする内容を決める
4. 現在処理している信号の種類やブロックの数で終了させるかなどを決定する

## 注意点

年符号が昭和ベースなので西暦に変換する必要があります。
事前の設定等なしに違和感なく表示させるには±10年の中で一番近い年を選ぶ形になると思います。
なのでいま2011年のものを解析すると2021年と表示されます。

ビット抜けした場合の対処などきれいに判定できるようにするにはかなり処理が大きくなると思うのですが、もっといい感じに処理できる方法があったらおしえてください…。

# さいごに

正直なところEWSというシステム自体どこをどうターゲットにしたものなのかがわからないという事がわかります[^7]。
わざわざ日付などの情報が入っていますが実際に現在の日付とチェックしている機器は無かったらしく、テレビ/ラジオの電源をつけるだけなら前置符号、固定符号、地域区分符号を受信した時点で起動(アラート)してよさそうです。

全く知識がありませんでしたが最初の頃からするとかなり安定したものが完成できたのではないかと思います。
もっといい方法があるとかこいつアホみたいなことしてるなと思った人がいたらぜひ後学のためにも教えていただけると嬉しいです！

[^1]: 現在TV放送でその音がなるのはNHKだけという意味。
[^2]: Wikipediaでも言及されている通り、現在はデジタル放送になっているためNHK以外のTV局をソースとするのであればPT3などで受信したTSを直接解析する形になる。
[^3]: FFTは周波数成分の取り出し(のはず)なので特定の単音の周波数が鳴っているか判断するのには適切ではない。
[^4]: 雑に使ってたのもあって検出率は10%ぐらいだった気がする…
[^5]: 電話のピポパ音
[^6]: 波形が0と交差する点を見つけるため`ゼロクロッシング`と呼ばれている。らしい。
[^8]: もしかすると元のデータ自体に問題があったのかもしれないので、YouTubeが悪いと決めつけるのは早計かもしれない。
[^7]: 休日等に試験放送がかぶると昼間からテレビでゲームしてる人などが急にチャンネル切り替わってびっくりしてる様子などが見受けられた。
[^9]: 本当に恐ろしいのはその状態の波形を普段から違和感なく聴いていた自分がいること
[^10]: 自分の使い方が悪いだけかも。音量によって検出後の値が変わってしまう問題の解決もできていなかったので…。