デスクトップのブラウザやプレーヤーのサウンドデータを横取りしてスペアナ風の表示をしてみた

![screenshot9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2986568/8b333fb4-7622-408c-aa89-f4cc359b0de7.png)
![screenshot10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2986568/71013d02-bd2d-493b-8132-7ea70ab6f532.png)
![screenshot8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2986568/4bb962dc-974f-0482-ae66-397e21964190.png)
デモの動画が[Youtube](https://www.youtube.com/watch?v=ZRhx-_33xR8)にあります。

またしてもQiitaに数多いらっしゃるプロフェッショナルの方々には何の役にも立たない話、かつ技術文書的な体裁からかけ離れただらだらとした散文で恐縮ですが[^1]。

## 要旨
タイトルの通りですが、個人的にただPCで音楽を聴いてるだけではつまらないと常々思っていた訳で。
ふとした事から[Bass.dll](https://www.un4seen.com/bass.html)という素晴らしいサウンド関係のライブラリを見つけてしまって、これで何かバーグラフ・スペアナ風のビジュアライザー的なものが何か作れないかと思ってしまったわけです。

## 作成前の考察とか
巷にスペアナ的ビジュアライザーのDIY的なものはハードウエア・ソフトウエアにかかわらずチラホラ見かけますが、
1. 個別のプレーヤに専用で付属していて、曲ファイル毎に(手で・プレイリストで)ロードするタイプ。foobar2000のコンポーネントとかもこのタイプと言える。
2. 音楽制作者向けという意味なのかマイクなどの入力デバイスから音を入力して表示するタイプ。
3. ソフトウエアではなく実際にLine出力をArduidとか経由でLEDに表示するタイプ[^2]。(ほとんどモノラルのみ対応)
4. 外部小型LCDに表示するタイプ。(それならPCのモニターに表示すればいいじゃん?)

しか見つけられませんでした。
本当はハードウエア(上記3)がいいんですが、せめて16バンドxステレオ2chには対応したいな、と。バンドごとの表示分解能をそれなりにする、と考えるとLEDのマトリックスが16x分解能x2と大変なことになってしまう[^3]。(だからこそ4.のパターンになるんでしょうけど。)

じゃあソフトウエアで我慢するか、となるわけですが、YoutubeとかSpotifyとか、あと私は使ってませんがApple MusicとかAmazon Prime Musicとかで聴いてる時(デバイスからの出力時)なんかに汎用的に使いたい。つまり上記1とか2ではダメなのです。
幸いWindows Vista以降はWASAPI([Windows Audio Session API](https://ja.wikipedia.org/wiki/WASAPI))がふつー[^4]になっていて、これのLoopback機能を使って鳴らしている曲のデータを横取り(キャプチャ)してくる手が使えます[^5]。
標準でも"[Windows.Media.Capture 名前空間](https://learn.microsoft.com/ja-jp/uwp/api/windows.media.capture?view=winrt-22621)"というのがあるのですが、さすがにセキュリティの問題[^6]があるのかふつーは細かく使えない模様。慣れない[UWP](https://ja.wikipedia.org/wiki/%E3%83%A6%E3%83%8B%E3%83%90%E3%83%BC%E3%82%B5%E3%83%ABWindows%E3%83%97%E3%83%A9%E3%83%83%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0)もしたくない。
さすがに自前で[FFT](https://ja.wikipedia.org/wiki/%E9%AB%98%E9%80%9F%E3%83%95%E3%83%BC%E3%83%AA%E3%82%A8%E5%A4%89%E6%8F%9B)とかいろいろスクラッチで書きたくないので調べていくうちに
- [NAudio](https://github.com/naudio/NAudio)
- [Bass.dll](https://www.un4seen.com/bass.html)

というWASAPI Loopbackが使えそうな2種類のライブラリがみつかりました。
[NAudio](https://github.com/naudio/NAudio)はMIT License、[Bass.dll](https://www.un4seen.com/bass.html)は独自ライセンスで、非商用個人は無料[^7]です。
このうちBass.dllのほうに[良さそうなスペアナのサンプルが公開](https://www.instructables.com/Audio-Spectrum-Software-C/)されていたのでBass.dllを使うことにしたわけです。
並行してhvianna氏の[audioMotion-analyzer](https://github.com/hvianna/audioMotion-analyzer)の素晴らしい[デモ](https://audiomotion.dev/demo/fluid.html)を見つけてしまい、これもファイル読み込ませ型でJavascriptで書かれているのですが、[ソース](https://github.com/hvianna/audioMotion-analyzer/blob/master/src/audioMotion-analyzer.js)に良いカラースキームのサンプルが数種類あったのでこれも参考にさせてもらおうと。

## 方針とか
- [h0uri氏の](https://www.instructables.com/Audio-Spectrum-Software-C/)はスペクトラムのデータをWPFのユーザーコントロールに送ってXAMLでプログレスバー表示するとともにラインチャートを描いていますが、この表示をもう少しそれっぽく(偽LEDっぽく)したい。かつWPFは不慣れなのでなるべくやめたい
- 同じくこれは1ch(L+R Mix)64バンドですが、ステレオ2chにしたい。実用的[^8]には16バンド/chぐらいあれば十分だができれば32バンド欲しいし1chの表示もできるように残したい
- 同じく44.1Hz固定なのをハイレゾ対応(照)したい
- hvianna氏のソースを元に偽LEDカラーを切り替えられるようにしたい
- 実機っぽく?各バンドの周波数を下に表示したい
- L/Rチャンネルはその時の事情に合わせて左右と上下、両方切り替えて並べられるようにしたい
- 実機みたいに?ピークホールドの表示をしたい。かつそれがパラパラと落ちるようにすればそれっぽい
- ウィンドウ(Form)サイズは可変でサイズに合わせて画像も拡大縮小したい

## 実装時(改造時)のポイントとか
実際のソース現物は[Github](https://github.com/osamusg/SpeAnaLED)にあります。
### L・Rチャンネルの分離
元のh0uri氏のAnalyzerクラスをゴリゴリ改造していくわけですが、最初に悩んだのは元のL+R Mix 1chをどうやってLとRに分離するか。
Bass(WASAPI)のドキュメントやサポート掲示板をみると、どうやらチャンネルで分離する場合は、`BASS_WASAPI_GetData()`の`BASS_DATA_FFT8192`の定数引数に`BASS_DATA_FFT_INDIVIDUAL`をorするらしいと解りました[^9]。こうすることによりL+R、L+R、L+R、...と送られてくるデータがL,R,L,R,L,R,...[^10]になるらしい、と。
送るデータは1周波数バンド当たり2byteなので分解能としては256段階で、1回に送るデータの総量は1回に128byte(元は2byte x 64バンドモノラルだから)から2byte x 16バンド x 2chに変更、そのままＬ・R交互に送り表示側でLRを分離することにしました。
受け側でなぜか調べた話とLとRの順番が入れ替わっているような気がするので~~すが書き換え先のPictureBoxをLR入れ替えることで~~データの先頭に0を挿入して最後のデータをその分カットすることでひとまず動いているので当面このまま。
あとでシリアル通信で外部のハードウエアにデータを送信したりすることも考えて一応モノラル1chでも送信できるように元のコードも残しました。

### FFT部分の処理
Bass(wasapi).dllは`GetData()`に`BASS_DATA_FFT8192`とか指定するとデータを[FFT](https://ja.wikipedia.org/wiki/%E9%AB%98%E9%80%9F%E3%83%95%E3%83%BC%E3%83%AA%E3%82%A8%E5%A4%89%E6%8F%9B)してくれる[^11]ので便利なのですが、周波数なので当然X軸はlogスケールにしたい。がh0uri氏の元のロジック
`バンド周波数 = Power(2, バンド位置 * 10.0 / バンド数-1)`    //バンド位置は0～15、とか
をそのまま使うと周波数の範囲が$2^0$～$2^{10}$、1Hz～1KHzになるような気がします。
人間の平均的な可聴限界範囲と言われる20Hz～20KHzにするために`バンド位置 * 10.0 / バンド数`の部分をPower(2,X)の逆関数(函数)で求めて20Hzは`log(20,2)`≒4.32ぐらい、20KHzは`log(20000,2)`≒14.29ぐらいにします。計算を簡単にするためにXは4.29～14.29を16分割[^12]するような感じにしました。
音源のサンプリング周波数によってバンド位置がずれてしまうのでこれも実測[^13]を元に適当に調整します。

### 描画とピークホールド
XAMLでプログレスバーを書き換えていた部分は、ふつーにデータ更新のイベントを表示用のFormクラスで受け取って左右それぞれのPictureBoxを書き換えることで問題無し。
描画はまともにLEDを1つ1つ全部グラフィックで書くと大変なので、ぶっとい点線(デフォルト30px)で描くのがポイント。レベルによって色を変えるのもグラデーションブラシで簡単にできるし軽くなります。このへんは[hvianna氏の](https://github.com/hvianna/audioMotion-analyzer)を参考にしてます。
デフォルト(サイズ伸縮なし)は実線部分3px、ブランク部分3pxで1レベルを表現、最初に見た目から表示の高さを128pxぐらい、と適当にフィーリングで決めたので128÷6≒21.33、切り上げて(3+3)x22レベル-3[^14]でPictureBoxのデフォルトの高さは最終的に129pxにしました。
データはバンド毎に2byte、つまり256段階で送られてくるので点線の実線部分とブランク部分の境界を計算してそこまで点線を引きます[^15]。グラデーションブラシなので設定したレベルごとにグラデーションで勝手に色が変わります。Rainbow Colorの場合もグラデーションブラシの方向を90度変えることで簡単にできます。
ピークホールドは規定時間ごとの最大値を保存して1レベル分だけ描画します。規定時間の3/4が経過すると1レベルづつ落ちていきます。ホールドする時間や落ちるスピード(サイクル)を可変にするためにゴチャゴチャやってるのでこの辺は他以上にエレガントじゃないです。
最後に出来上がったBitmapをPictureBoxのサイズに合わせて伸縮貼り付けしてバーの表示は終了です。Center-HiやCenter-LowのFlip表示も文字通り貼り付け時の`Bitmap.RotateFlip()`で簡単。
タイマーは元の25msecを変えていないので理論上40FPSです。実際どうなのかは調べてませんが。
PictureBoxのレイアウトは「Formの大きさ→PictureBoxの大きさ」で決めるのと「PictureBoxの大きさ→Formの大きさ」で決めるのとが両方混在すると管理が難しいので「Formの大きさ→PictureBoxの大きさ」に統一。横レイアウトと縦レイアウトの変更も同様に変更後のFormのサイズを決めてからPictureBoxの位置を変更します。
バンドの周波数の数字表示も最大32個必要で位置合わせ・サイズ合わせに悩むところです。Center-Hi/Lowの表示でも位置が変わります。実際に描いてみない事には1つ1つのサイズもわかりませんので動的にLabelを作成して１つ１つ配置しています。PictureBoxの幅が小さくなると隣同士が重なってしまうのでその場合は2段に表示してできる限り読めるようにしています[^16]。が結局この小細工も表示を消してしまえば関係なし(笑)。

### 出力デバイスの検索と切り替え対応
h0uri氏の実装では起動するごとにデバイスを全部探して最初に見つかったWASAPI loopbackに適応するデバイスを使用するのですが、探すときに毎回Bass.dllを呼び出してデバイスを調べるようなので起動にやたらと時間がかかります。
~~PCのマザボ内蔵サウンド含め、そうそうデバイスを入れ替えたりはしないので、最初に使う際には全部調べるのは仕方ないにしても、見つけた対応サウンドデバイスは設定ファイルに保存するようにして次回以降の起動時間を短縮しました~~[^17]これはデバイス情報をローカル変数に格納することで高速化できたため、Windowsの「既定のデバイス」を起動時に調べてそのデバイスを使うようにしました[^18]。~~外部のDACを使っている場合など、複数接続されている場合は選択できるようにもしています~~[^19]。実行中にWindowsの「既定のデバイス」が切り替わった時はBass(wasapi).dllの初期化が必要なので可能であれば自動的に初期化(リロード)、そうでなくても手動で初期化(リロード)できるようにしてあります。~~ただし、実際にデータを取るためにはもちろんWindows側(サウンドコントロールパネル)も手で切り替える必要があります。~~

### ハイレゾ(48+KHz PCM)対応
BassでWASAPIを使う場合、先にBass.dllをダミー?のパラメータで初期化してからBassWASAPIの初期化をする2段階らしいのですが、Bass.dllの初期化のところでサンプリング周波数に`BASS_WASAPI_getDeviceInfo()`で取ってきた`device.mixfreq`パラメータを正しくセットしないと`result`に`false`が返ってきちゃうし、逆に`BassWasapi.BASS_WASAPI_Init()`のほうはデフォルトの0で良かったりしてそこに気づくまで悩みました[^20]。
あと当然「周波数が高くなる」=「書き換え頻度が多くなる」ので、バッファのサイズを3段階に調整。これをしないと高いサンプリング周波数で目まぐるしく(低いサンプリング周波数ではモッサリと)書き換わってしまいます。
でもこの対応でよくある44.1KHzからPCM仕様限界の384KHzまでそれほど不自然なく表示できるようになりました。
bit深度に関しては何も触ってませんが、Windows側を32bitとかに切り替えても問題なく表示できています。

### そのほか
- "Always on Top"
- ディスプレイパワーオフ抑止
- タイトルバー on/off[^21]
- スペクトラムの感度調整
- LEDタイプ・レベルメーター(オマケ)
- レベルストリームのグラフ(オマケ)
- デバイスのオートリロード[^22]
- 設定値保存

入ってます。

## おしまいに
素晴らしいライブラリと先人の知恵により、いまさらですが無事車輪を再発明することができました。個人的趣味と興味と時間つぶしで作ったのでそれでいーんです。
ここまでやって、プログラム本体100KB・ライブラリ込みで900KB、私の8年前のオンボロPCでもCPUロード0.1%、プロセスメモリ90MBぐらいの軽いプログラムで済んでます。
スクリーンショットのようにFormサイズをうんと小さくして画面の隅にアクセサリとして置いておいたりするのもいいでしょう(画面の隅でチラチラ動くのが気にならない人専用ですが)。

最後にお手軽にさっと見てみたい奇特な方むけにReleaseビルドのバイナリのリンクも置いておきます。
[https://github.com/osamusg/SpeAnaLED/releases](https://github.com/osamusg/SpeAnaLED/releases)

以上です。

[^1]:こういう文章を書きたくて何か作ってるまである。全部読む人がほとんどいなくても。自分の外部記憶の意味もあるのでいいんです。
[^2]:AliExpressとかBangoodとか中華でよく見かける。
[^3]:あと費用も。
[^4]:<strike>マニアや</strike>クリエータ向けにはASIOとかDSDとかもありますが...
[^5]:XBOX Game BarとかGForce Experienceのビデオキャプチャもこの方法?
[^6]:盗聴とかスパイウエアとかされちゃうからねぇ。
[^7]:"BASS is free for non-commercial use. If you are a non-commercial entity (eg. an individual) and you are not making any money from your product (through sales, advertising, etc) then you can use BASS in it for free. Otherwise, one of the following licences will be required."
[^8]:自分的な、「見て楽しむ」という実用、という意味で。間違ってもこれを見て実際に何か判断をしてはいけない。
[^9]:併せて元のバッファサイズの定数は`BASS_DATA_FFT4096`に。
[^10]:インターリーブ(interleave)と言ってました。
[^11]:窓関数はおそらくhan窓。
[^12]:もちろん8バンド表示の時は8分割。
[^13]:[WaveGene](http://web.archive.org/web/20171102031127/http://efu.jp.net/soft/wg/wg.html)で確認しながら。
[^14]:最後のブランクはいらないので-3。
[^15]:オマケのレベルメーターも同じ方法。
[^16]:それでもまだ右がはみ出たり、センター位置とズレたりしています。
[^17]:WindowsからはそのデバイスがWASAPIデバイスかどうか事前に知る方法がないから? LCDモニタにイヤフォンジャックが付いてたりしたらHDMIのサウンドデバイスとかだし。
[^18]:~~稀にWindowsがデバイス番号を振り直すことがあるみたいなのでその場合は設定ファイルの該当行を消すか、おもいっきり設定ファイルごと削除する必要がある。~~
その時に調べた対応サンプリング周波数も表示するのでハイレゾ民は悦に入ることができるオマケつき(笑)。
[^19]:考えてみたらどのみちWindowsので「既定のデバイス」になっていないと音は出ないのでデバイスのマニュアル選択は意味がないことにいまさら気づいたのでこの機能は削除。
[^20]:よく見たらドキュメントにそう書いてある。
[^21]:ちなみに[先日の記事](https://qiita.com/osamus/items/ce52bd3ffc5082468553)はこの話。
[^22]:タマに行儀の悪いデバイスがあったり、出力レベルの設定自体を自分で間違えて音声のレベルが高すぎたりして、リセットがかかりまくることがるのでその場合はオートリロードを切る必要がある。