NVIDIA Jetson Nano 開発者キットで CuPy を利用して WAV ファイルを周波数解析する

# はじめに
NVIDIA Jetson Nano 開発者キットでボコーダーなどサウンド・エフェクトに挑戦しようと思い、まず USB オーディオ 変換 アダプタなるものを買いました。これで音声入出力を Jetson Nano に持たせることができます。アマゾンで適当に安いものを選びましたが、Jetson Nano 開発者キットの USB コネクタに挿すだけで無事認識されました。
![image.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384208/1813c01c-05a0-95bb-6c0e-5570b6935622.jpeg)
さて、いきなりサウンド・エフェクトに取り掛かるのは大変なので、とりあず最初の目標は **WAV ファイルを再生しながら GPU で FFT を実行してスペクトルをリアルタイム表示** としました。これ位なら簡単と軽く見ていたのですが、予想以上に時間がかかり途中でめげそうになり、かつ、（単なる一次元 FFT に使う場合は）GPU による高速化の効果が発揮できない結果となりました。ちょっと中途半端な感じではありますが、最初の目標は達成できたので記事にしてみました。

# 方針
休日の楽しみとしていろいろ実験しているので、あまり大きなプログラムは作成できません。手軽さからプログラミング言語は Python を選ぶことになります。また、私の場合、C/C++ でプログラミングするといつのまにか仕事スイッチが入ってしまい、あまり楽しくなくなってくるという理由もあります。

## FFT
GPU による FFT の実行は [CuPy](https://cupy.chainer.org/) を利用させていただくことにしました。このライブラリを使用すると Python で [NVIDIA CUDA](https://developer.nvidia.com/cuda-toolkit) を利用できます。CUDA プログラミングができるだけでなく、NVIDIA CUDA ライブラリも利用できます。FFT の CUDA ライブラリである [cuFFT](https://developer.nvidia.com/cufft) も、この CuPy から利用できます。また、CuPy は NumPy との互換性を重視しているので大変便利です。
CUDA の Python ライブラリにはもう一つ [PyCUDA](https://mathema.tician.de/software/pycuda/) がありますが、CuPy の方がインターネット上に情報が多かったので CuPy を選びました。

## WAV ファイルの処理
WAV ファイルの中身をデータとして取り込んで利用するには [PySoundFile](https://pysoundfile.readthedocs.io/) という便利なライブラリが存在します。しかし、解析と同時に再生するという使い方をするには不便に思えました。そのため、データの読み出しは [WAV モジュール](https://docs.python.org/ja/3/library/wave.html) で行い、再生は [PyAudio](http://people.csail.mit.edu/hubert/pyaudio/) ライブラリを利用させていただきました。

## グラフ表示
グラフ表示には、非常に有名な [Matplotlib](https://matplotlib.org/) を利用するのが妥当かと最初は思いました。しかし、いろいろ試した結果、音声のスペクトル表示をリアルタイムに行うには難しいと感じました。[matplotlib.animation](https://matplotlib.org/3.1.0/api/animation_api.html) の機能も試して高速化を試みましたが、私には無理でした。インターネットで検索すると [PyQtGraph](http://www.pyqtgraph.org/) が高速と評判でしたのでこれを利用させていただくことにしました。

# 準備

## ライブラリのインストール
CuPy のインストールは1時間位かかります。

```
$ sudo apt-get update

$ sudo apt-get install python3-pip

$ sudo pip3 install -U numpy

$ sudo pip3 install cupy

$ sudo pip3 install pyqtgraph

$ sudo apt-get install portaudio19-dev

$ sudo pip3 install pyaudio
```

## 音声出力インターフェースの選択
今回、購入した USB オーディオ 変換 アダプタから音声が出力されるように設定しました。音声出力付きの HDMI モニターがあればそちらを選択しても良いと思います。
![Screenshot from 2019-05-26 14-21-53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384208/9fe13c0c-82ac-a280-2a35-0fb749a846f6.png)

## おまじない
なぜか、今回作成したプログラムの最初の実行が失敗したとき、PyQtGraph の Example プログラムを実行すると、うまく動作するようになりました。おまじないとして最初に一回だけこの Example プログラムを動作させます。

```
$ python3
>>> import pyqtgraph.examples
>>> pyqtgraph.examples.run()
```

![Screenshot from 2019-05-26 14-51-22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384208/037ce040-3f6b-aad8-8c9b-cff923eddbf6.png)

# プログラム
Gist [tsutof/WaveReader.py](https://gist.github.com/tsutof/c7231584300e01b5b3a35b08c82a67db) で公開しています。以下二つのファイルから構成されます。エラー処理が不十分なことは予め了承ください。
- [snd_fft_plot.py](https://gist.github.com/tsutof/c7231584300e01b5b3a35b08c82a67db#file-snd_fft_plot-py)
- [WaveReader.py](https://gist.github.com/tsutof/c7231584300e01b5b3a35b08c82a67db#file-wavereader-py)

**WaveReader** クラスを作成して以下の機能を持たせました。（インスタンスを一つしか生成しないアプリケーションにわざわざクラスを作成する必要はないかも知れませんが、老化による記憶力の低下をカプセル化で補っています。）
- WAV ファイルから予め指定した個数のサンプルを読み出す。
- FFT をかける都合上、直前の FFT とデータをオーバーラップさせたいので直前に読み出したサンプル・データを保存する。
- 読みだしたサンプル・データを NumPy 配列に変換する。
- WAV ファイルを再生するため PyAudio ストリームの管理をする。

通常、PyAudio による WAV ファイルの再生ではコールバック関数を用意してサンプル・データを出力するようですが、PyQtGraph によるグラフの表示更新にもコールバック関数が必要なので、グラフ表示更新のコールバック関数内からブロッキング・コールでサンプル・データを出力しています。

FFT は **CHUNK_SIZE = (2 \*\* 11)** 二つ分（4096サンプル）のデータに対して実行しています。

# 動作確認
**本プログラムで音声が出力されます。お試しになる場合はプログラムの不具合等で耳を傷めないよう注意願います。音量調整を低くして試してください。イヤホンで聞く場合、初めはイヤホンを耳から離してお試しください。**

```
$ python3 snd_fft_plot.py WAV形式ファイル
```

たまにアンダーランのワーニングが出ますがリアルタイムに表示できます。左チャンネルと右チャンネルで色を変えています。
![Screenshot from 2019-05-25 15-20-05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384208/4c01b01d-2d57-7005-3700-88a4824ac2e4.png)

上記の表示では [BBC Sound Effects](http://bbcsfx.acropolis.org.uk/) で公開されている **Big Ben ticking.** を使わせていただきました。

以下は 8kHz のサイン波（モノラル）を入力してテストしたときの表示です。
![Screenshot from 2019-05-26 14-47-26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384208/f8ff6436-4396-0fce-d553-c0c4c68a7d82.png)

16ビットPCMのステレオとモノラルで動作確認しました。他の形式ではうまく動作しない可能性があります。

# GPU による FFT の高速化について
Jetson Nano の内蔵 GPU により FFT が高速化されているか気になり調べてみました。まだ検証が不十分なのでデータは出しませんが、この実験の場合は残念ながら高速化の効果は出ませんでした。NumPy で FFT を実行した方が速いということです。
以下のように GPU による FFT は、CPU から GPU へデータ入力、GPU で FFT 処理、GPU から CPU へデータ出力という流れで計算されるのですが、この GPU <-> CPU のデータ転送に（実際の計算時間と比較して）かなり大きな時間を要します。つまり、データ転送がオーバーヘッドになるので GPU での計算量がデータ転送に比べて十分大きい場合に GPU による高速化を発揮します。[^1]
**FFT の計算自体は NumPy で行うよりも CuPy で行う方がずっと高速です。あくまで、データ転送のオーバーヘッドが問題です。**

[^1]: まだ、十分な実験を行っていないので、実際の処理時間をここで示すことはしませんが、今回の実験では FFT の入力点数が 256k 個以上あると、データ転送時間を含めても、GPU で処理した方が速くなりました。

```
def fft_gpu(x_cpu):
    x_gpu = cp.asarray(x_cpu)   # CPU mem -> GPU mem
    y_gpu = cp.fft.rfft(x_gpu)  # FFT
    y_cpu = cp.asnumpy(y_gpu)   # GPU mem -> CPU mem
    return abs(y_cpu)
```

また、GPU による計算処理のバックグランドでデータ転送を行う CUDA Stream という効率的な仕組みがあり、CuPyでも利用できる [^2] ようなのでこれを使う必要がありそうです。

[^2]: [Cupy を用いた 非同期メモリ転送](https://qiita.com/fujiisoup/items/92f4b4f4c7948fabdd60)　で詳しく解説されています。

しかし、GPU が専用メモリを持っている Tesla/Quadro/GeForce などの GPU カードと異なり、**CPU と GPU でメモリを共有する Jetson にデータ転送が果たして必要なのか？**は今後の研究課題とさせていただきます。

# まとめ

音声を再生しながら、そのデータを周波数解析し、その結果をリアルタイムでグラフ表示する方法の一例を示し、NVIDIA Jetson Nano 開発者キットで動作確認を行いました。但し、Jetson Nano の GPU でその解析を高速化するには実装面での工夫が必要であることが分かりました。

以上です。