NanoVNA という非常に小型で低コストのスタンドアロン VNA (ベクターネットワークアナライザ) がにわかにグローバルで流行している。VNA は高周波回路設計・実装に必須の測定器のひとつだが、もともと非常に高価なため、個人でちゃんとしたメーカーものを所有することはまずない。たとえ低価格と言われるものでも数万円〜だった。それがさらに低コストで使える性能であると評判なので、VNA の民主化だというおもむき。

NanoVNA の設計のオリジナルはTT (ttrftech) さんという日本語でブログも書かれているかたの設計のもの。自分もハムフェアかMaker Faireかで実機をちらっと見たことがある気がする (もしかするとVNAではなく後続プロジェクトのSDRのほうだったかもしれないが)。少量のキット化までされていたが、これを hugen79 さんが手を加えてしPCBアートワークなどをやりなおして売っているようだ (もともとPCBの設計はソースに含まれてない)。ファームにも独自のコードが入っている。

aliexpress などでは、hugen79 版を元にした (おそらく) さらにこれのクローンと思われるものが非常に安価に売っている。hugen79 レポジトリの README では、粗悪なものがあって性能が出ないことがある (ミキサーまわりに齟齬があるらしい? シールドの有無など違いもある) けど、本来の性能じゃないから買う側でちゃんと調べろや、ということが書いてある。

自分には技術レベル的にVNAは必要ないが、VNAとして使わなくても、これはそのままアンテナアナライザーにもなるわけで、もはや自作するより良さそうだ。

ちなみに2ポートのうちCH0だけ反射を計測できるので、繋ぎかえて測定してマージしないと s2p 相当の結果を得られない。(S11 S21 を得て、繋ぎかえて S11、S21をS22 S12 に読み替える) 入出力が全く同じで対称の受動コンポーネントなら S22=S11 S12=S21 としてもまぁいいのかも (VNA使ったことないので温度感がわからない)。

基本的な使いかた

  • 周波数の範囲を決める (範囲内の固定 101 ポイントを測定する)
  • キャリブレーションする
    • RESET してから、CAL を順番に実行していく
  • 被測定物を繋ぐ

特定のよく使う周波数範囲に関してはキャリブレーション済みの状態を保存できる。

PCソフトウェア

オリジナル版だと python による実装があり Jupyter Notebook から呼びだすような例が書いてある。

それとは別に hugen79 開発の C# ソフトウェアが存在している。ただしこれはOSSではない。

デバイスとの通信は USB CDC によるもので特に難しさはなさそう。main.c を見れば実装はわかるし、python 実装を読めばホストインターフェイスは容易に書けそう。ただ細かいことをやろうとするとホスト側で信号処理をやらないといけない。

逆に USB CDC しかないので雑に BLE シリアル化みたいなのはちょっと面倒そう。MCU から使ってないピンを引き出してUARTに割り当てたらいいのかも。

ファームウェア

販売されているものに入っているファームウェアは店によって違うっぽい。hugen79 ファームが入っているようだが、これにはいくつか種類がある。上限周波数とアンテナアライザーに特化しているかどうかの違いらしい。いまいち違いがわからないのでコードを読んでみたが、上記レポジトリには上限周波数のフラグはあってもアンテナアライザー特化版フラグが入ってなさそう。謎。

ttrftech ファームも割と最近には上限を900MHz までとする高調波拡張が入っているみたい。(ブランチには1.5GHzまでの拡張もある)


ということで何らかの変更を入れる場合どっちからフォークするかは悩ましい。github を検索すると FreeRTOS バージョンのコードもあったりする。hugen79 版はオリジナルから定期的にコードを手動でマージしているみたいなので、特に必要がなければオリジナルにPRを作ったほうが良さそうかな。

SRAM がギリギリなのであまり高級な機能は入れにくい。可能なら宣言済みのバッファをうまく利用したい。

ファームウェアの歩きかた

ブロックダイアグラムをまず見とく。全体でやってることは難しくなくて、クロックジェネレータ・ミキサ・ステレオオーディコーデック・MCU をうまく組み合わせてあまり部品数を増やさずに構成されている。

MCU は STM32F072C8T6 (Coretex-M0 48MHz / 16KB SRAM / 64KB Flash)


STM32F072CBT6 (Coretex-M0 48MHz / 16KB SRAM / 128KB Flash) でした。

ファームウェア側は RTOS として ChibiOS が採用されている。

シェルまわり

USB CDC のシェルは ChibiOS の shell 機能で実装されている。main.c に定義がある。

すべての機能がシェルから呼べるようになっているので、機能から見るなら基本は main.c を起点にして該当コードを探すのが読みやすいと思う。

信号入力まわり
  1. TLV320AIC3204 (ADC) を初期化
  2. STM32 の I2S は常時起動しており i2s_end_callback() が呼ばれている
  3. dsp_process() で現在の値をある程度計算してグローバルに保持する

があったうえで、別スレッドで

  1. オシレータの周波数を設定
  2. ADC のチャンネルを選択して DSP が安定するのを待つ
  3. calculate_gamma() でΓを求める

がスイープにあわせて実行されている。

レンダリングまわり

ベクトルネットワークアナライザNanoVNAの液晶画面を実装する このエントリにおおまかな実装概要が書いてある。コードは plot.c だがメモリが少ないのを工夫して実装してあるので難しい。ちゃんと読んでない。トレース結果とかの状態をグローバルに保持しつつ、再描画が必要な領域に dirty フラグ (mark) を立てて、draw_cell() が実際に特定の領域のピクセル情報をつくってディスプレイに送っているみたい。

ref

  1. トップ
  2. tech
  3. 中華 NanoVNA ってなんなのか? またはファームの歩きかた
  1. トップ
  2. nanovna
  3. 中華 NanoVNA ってなんなのか? またはファームの歩きかた

NanoVNA の実装を軽く読んだ感じでは画面のキャプチャをとる機能がなさそうだったので、実装を書いてみた。測定器はとにかくキャプチャをとりたいし、せっかく綺麗にレンダリングされているので、できれば保存したい。

中華 NanoVNA ってなんなのか? またはファームの歩きかた | tech | nanovna - 氾濫原 にも少し書いたけど、画面描画はメモリ消費を抑える実装になっており、MCU 側で画面バッファを全て持っているわけではない。

そこで、使っているLCDドライバの ILI9341 のデータシートを見てみたところ、ドライバ側で持っているメモリ内容を SPI 経由で読み出せそうであったので、そちらを利用した。

メモリ内容を読めるといっても、MCU 側のメモリ容量的に全てを一気に読むことはできないので、一部読んではUSBに流し、一部読んではUSBに流すというのを繰り返す実装にした。

ハマったところ

SPIを受信するコードから書く必要があったけど、STM32 に慣れていないせいでかなりハマってしまった。

まず FRXTH フラグを適切にセットしていないと RXNE フラグがセットされないので RXNE を見て無限ループさせるとそこでスタックする。

あとは (おそらく) 受信バッファ溢れ (オーバーフロー OVR )の場合で、受信バッファが溢れた場合、あとからきたデータで上書きされるのではなく、単に全て捨てられるようなので、執拗に OVR をクリアするような実装を書く必要があった。もっとスマートに書けるのかもしれない。

  1. トップ
  2. tech
  3. NanoVNA に capture コマンドを追加してみる
  1. トップ
  2. nanovna
  3. NanoVNA に capture コマンドを追加してみる

HackRF One 用の sweep mode を利用するスペクトラムアナライザーで、いまいち安定して動くものが見つからなかった。WebUSB で書いてブラウザだけあれば動くようにしたら便利かと思い書いてみた。

余計なものをインストールせず、ブラウザで上のURLを開くだけで使えるので普通に便利。だけど WebUSB は今のところ Google Chrome でしか使えない。

実装

HackRF との通信

元々 libhackrf (Cのライブラリ) は libusb を使って実装されている。WebUSB も libusb のほぼそのままの API のラッパーなので、読みかえて実装すれば簡単に移植できる。面倒なのは実際に transferIn() するところぐらい。

USB 帯域を使いきる

20MHz サンプルで 8bit I/Q 信号を得るので、コンスタントに40MB/sec の転送帯域を使用する。これは USB 2.0 の実効速度のほぼ上限にあたるため、気をつけて実装しないと抜けが発生する。

追記:20MB/sec 使いきるのは通常のサンプリングの場合で、スイープモードの場合はブロックサイズの関係上 13.3MB/sec ぐらいになる。

WebUSB だと transferIn() を使って転送するが、素朴に await transferIn() を繰替えすと必ず取りこぼしが発生した。このため、2つの await transferIn() を同時にループさせて、いずれかの IO で常にデータを受信できるようにしてほぼ解決した。

これでもマシン負荷によっては時々とりこぼす (とりこぼしたところは白くなる)

WebWorker で WebUSB を使う

WebWorker 内で WebUSB は使える。使えるけど、事前に requestDevice() してペアリング (USBにはない概念) しておく必要がある。requestDevice() はメインスレッドでユーザインタラクションがないと呼べない。

  1. メインスレッドで requestDevice() して device を取得してペアリングする
  2. 取得した device 情報を worker に postMessage (device を直接 postMessage することはできない)
  3. ワーカースレッドで getDevices() してペアリング済み device 一覧を取得して、マッチするものを選ぶ

という手順が必要だった。

FFT する

FFT は RustFFT の実装を WebAssembly で呼んでいる。ComplexAnalyserNode (WebAudio) を作った (IQ信号のFFT) | tech - 氾濫原 とほぼ似たような実装。

ウォーターフォール表示

2D 描画でも WebGL を使うべきか? スペクトラムウォーターフォール最速決定戦 | tech - 氾濫原 というのを前に書いたが、現時点だと WebGL 実装が一番負荷が少なかったので、WebGL を使って実装した。

一応 WebGL 使わないバージョンと切り替えて使えるようにしている。

  1. トップ
  2. tech
  3. WebUSB HackRF One スペクトラムアナライザー

hackrf_sweep コマンドの動作を調べたので記録しておく。このコマンドは HackRF One のファームウェアと協調して、うまく広い帯域をスイープできるように作られている。

まずいくつか定数がある

  • サンプリング周波数は 20MHz
  • フィルタ周波数は 15MHz
  • オフセット周波数は 7.5MHz (設定した周波数の7.5MHz上の周波数になる)
  • step_width は 20MHz
  • interleaved モード

とにかく interleaved モードを理解しないといけない。interleaved ではないモード (linear) は単純に step_width を足していくだけなので単純明快だが、interleaved ではファームウェア側は +5MHz、+15Mhz を繰替えしてスイープする動作をする。

これで何が嬉しいか?というと、いくらか効率を犠牲にして (スキャン時間や計算量が2倍) フィルタの減衰がかかる両端の領域や、DCオフセットがある中央の領域を捨てて綺麗な帯域だけからスペクトラムを合成できる。

詳しい動作

グラフをかいてみた。中央が0Hzで±10MHz (サンプリング周波数20MHz) の周波数領域を示している。全体の赤い領域が1回でとれる 20MHz サンプル、15MHz フィルタされた信号の全体で、青の領域と紫の領域だけを採用してさらにズラしていく。次は5MHzずらせば真ん中にある隙間と次の5MHz分が埋まり、その次は15MHzずらせば……と続く。

見ての通り帯域の端のほうではローパスフィルタ(アンチエイリアシングフィルタなので必須)による減衰の影響をうける ので避けたいし、0HzにはどうしてもDCオフセットが乗るので避けたい。

一回のサンプルで20MHzの帯域にうち、10MHzぶんの帯域しか取得しないが、全領域でできるだけフラットでノイズのない結果を得られる。

  1. トップ
  2. tech
  3. HackRF One の Sweep Mode の動作


HackRF という SDR 用の無線フロントエンドがある。オープンソースハードウェア。この手のものは他に LimeSDR (高い) や RTL-SDR (安い) などがあるが、HackRF は最高周波数が 6GHz までという点とコスパが良い点が大きな特徴。

アルミケース・TCXOモジュール・ロッドアンテナ付きで $125 ほど。

TCXO モジュールはインストールされていないので、一旦ケースを分解して装着する必要があった。適当にググって解決。

brew でツールが入る

brew install hackrf

TCXO がちゃんと認識されているかはこれでわかる (ref. HackRF One · mossmann/hackrf Wiki · GitHub

hackrf_debug --si5351c -n 0 -r 
[  0] -> 0x01 # 認識されている場合
[ 0] -> 0x51 # 認識されていない場合

これらのツールはあくまでCUIツールなので、GUI で何かするためには他ものがいる。Mac だと Gqrx はデフォルトで HackRF の入力に対応しており復調もできる。

スペクトラムアナライザーっぽく使えるのだとqspectrumanalyzerが一応使える。ただし非常に重く、よく落ちてしまう。

アンテナポートへの電源供給

GPS アクティブアンテナのように電源供給が必要な場合、アンテナポートに電源供給することができる。関連ツールにはコマンドオプションがある。

スイープモード

スイープモードというのが実装されている。HackRF One のハードウェア的には 20MHz サンプルなので、20Mhz 分の帯域しか同時に見ることはできないが、連続して局発周波数などを動かしてサンプリングしていくことで、擬似的に広い範囲の帯域を見ることができる。広域スペアナ的に使えるといえばわかりやすいか。

詳しい動作はまた今度説明したい。

TX

送信もできるけど HackRF One 本体からの出力は最大でも 15dBm (0.03W) なので、外部にリニアアンプなどが必要になる。

が、そもそもこの機械の仕様でアマチュア免許などは下りないと思うので、ダミーロードつけて微弱無線局として遊ぶしかないと思う。とはいえ、微弱無線局の定義も非常に厳しいので、基本的にはもったいないけど、国内ではおかざりと思ったほうが良い。

ソースコードの歩きかた

HackRF はオープンソースなのですべてのコードを読むことができる。かなり綺麗に書かれているほうだと思うので、すんなり読みくだせるものが多い。

基本的にツール側から攻めていくのがわかりやすそう

これ以下に hackrf_info などのツールのソースコードがある。

ツール類はほぼ libhackrf を呼んでいて、こちらも同じレポジトリ内にある。難しいことはあまりしておらず、素直に libusb の関数を呼びだしているので読みやすい。機能もたくさんあるわけではない。

ファームウェアは若干複雑ではあるが、USB の API から攻めていくのが読みやすい気がする。

cpld (FPGA の弟みたいなやつ) まわりはまだちゃんと読めてない。

  1. トップ
  2. tech
  3. HackRF One で遊ぶ

「設定」→「共有」→「画面共有」という項目がでるはずだが、ない場合、vino (gnome desktop 環境の vnc サーバコンポーネント) がインストールされていない。おそらく初回セットアップ時の構成によるのだろう。

sudo apt install vino

して「設定」を再起動すれば項目が出現する。

また、このままだと macOS から接続できない (互換性がないと言われる)。encryption を切ってやれば接続できる。SSH でポートフォワードする場合 vnc レイヤーでの暗号化は特に必要ないので無効にしても良いだろう。

export GIO_EXTRA_MODULES=/usr/lib/x86_64-linux-gnu/gio/modules/ # Using the 'memory' GSettings backend.  Your settings will not be saved or shared with other applications.

sudo  gsettings set org.gnome.Vino require-encryption false
sudo  gsettings   get org.gnome.Vino require-encryption # 確認
  1. トップ
  2. tech
  3. Ubuntu 18.04 の VNC (画面共有)




set-option -g status-left "#[fg=colour255,bg=colour23]#{?client_prefix,#[bg=colour202],}#{?#{==:#{pane_mode},copy-mode},#[bg=colour208] COPY , [#I #D] }"

こんな感じで設定すると、通常は [4 %7] (4はwindow番号、%7はpane識別子)みたいになり、prefix 入力中は色が変化し、copy モードに入ると、さらに別の色で COPY という表示がでるようになる。

man tmux すると #{?foo,true,false} みたいな記法とか #{==:#{var},foo} みたいな記法について記述があり割と柔軟に設定できた。

  1. トップ
  2. tech
  3. tmux で prefix 入力中や copy モードをわかりやすくする

GNU screen をずっと使っていたけど、なんとなく tmux に移行した。256色カラーとかがデフォルトで有効なのがとりあえず良いかな。

ずっと screen を使っていたといっても特に使いこなしまくっているわけではないので、普通に使う分には多少の設定でほぼ違和感なくすることができた。2重 tmux も大丈夫。

前に tmux いいよみたいな流行がでたとき移行しなかったのはなんか不愉快な人が使っていたからで特に tmux に罪はなかったが、そういう記憶も忘れたので、そろそろいいかという気がする。

  1. トップ
  2. tech
  3. tmux に移行した

GNU screen のとき定義していた cdd という、既に開いている別ウィンドウのディレクトリにcdするコマンドがあった(ref. cdd とお別れして、別の cdd を定義した | tech - 氾濫原 )。このままでは tmux で動かないので書きかえた。

function cdd() {
	typeset -A mapping
	local window=$1
	mapping=($(tmux list-windows -F '#{window_index} #{pane_current_path}'))
	local dir=$mapping[$window]
	if [[ $dir == "" ]]; then
		echo "window not found"
	else
		cd "$dir"
	fi
}

GNU screen のときはややトリッキーなことをしていたけど、tmux だとlist-windows の出力を工夫するとマッピングを直接得られるので、zsh の連想配列に入れてそのまま取得できた。(逆に、tmux だと環境変数にウィンドウ番号は入っていないので、screen のときの方法だとうまくできない)

今のところ自分は window 内を複数 pane に分割しないのでこれでうまくいくんだけど、分割すると結果が不定になりそう。

元々書いてた cdd は引数なしで実行するとリストを表示して選択して cd できるようにしていたけど、全く使わなかったので機能自体をなくした。

なぜか list-windows ではなく list-panes していたので、list-windows を使うように変更した

  1. トップ
  2. tech
  3. tmux 用の cdd (ウィンドウ番号を指定して cd)

概要

無線機の出す I/Q 信号をサンプリングして 2ch (ステレオ) としてコンピュータに入力し、これを直接 WebAudio から扱って音声までデコードする。ソフトウェア側はブラウザだけで完結する。

実用レベルではないが SSB の復調まではできたので一旦記録しておく。

前提条件

入力

無線フロントエンド(アンテナや局発ミキサなどのハードウェア)が必要になるが、それは用意されていて、PCへの入力は2chのステレオマイク入力として行われているものとする。

自分の環境では KX3(無線機) のアナログ I/Q 出力を USB Audio Device のマイク入力に入れてサンプリングしている。

何を実現できればいいか

最初の要件を定義する。

  • SSBモードをUSB/LSB 指定で音声をデコードできること

シンプルにまずこれだけを目標にする。

SSB モードが実装できれば、単純に全く同じ実装で AM と CW もひとまず復調できるし、サイドバンドの選択ができるというところに信号処理の面白さがある (と今のところは思っている) ので、ちょうど良い目標だと思う。

IQ信号とは何か

無線フロントエンド側で、特定の周波数のcos/sinをそれぞれミックスした信号のこと。

例えば 7020kHz の cos/sin 信号をアンテナからのRF信号にミックスすると、7020kHz を 0Hz として周波数変換 (ヘテロダイン)される。cos波だけだと 7030kHz も 7010kHz も同様に 10Hz に周波数変換されてしまう (イメージがでる) が、直交位相 (sin波) でも周波数変換することで、負の周波数と正周波数の情報を信号処理で分けることができるようになる。

何を作ればいいか

  • IQ シグナルを受けとって復調周波数にバンドパスフィルタをかける
  • フィルタした信号を音声周波数に周波数変換をする
    • USB なら 0Hz〜nHz、LSB なら -nHz〜0Hz に変換する
  • 実信号に変換する
  • オーディオ信号として出力

どちらのサイドバンドを復調するかはバンドパスフィルタの時点で選択して抽出する。その後適切に周波数変換して、real だけとれば音声として聞こえる。

デバッグのために

音声信号はそこそこ流量があるので、print デバッグみたいなことは難しい。せっかく WebAudio なので、適時ブラウザ上に可視化しながら実装をすすめる。問題に気付きやすいし、なによりそのほうが楽しい。

FFTウォーターフォール表示

https://lowreal.net/2019/07/17/1 に書いたが、実は最初にやると一番簡単なので、まずこれを実装しておく。

入力のIQ信号をそのまま複素数として扱い、FFT にかけるだけで良い。実数のFFTは出力の上半分を無視するが、複素数FFTの場合、出力の上半分は負の周波数領域の解析結果となっており、適切にシフトさえすれば難しいことなしにそのまま可視化できる。

複素バンドパスフィルタ

複素バンドパス フィルター設計
- MATLAB & Simulink
- MathWorks 日本
この記事がわかりやすい。

  • 実数ローパスフィルタは、負の周波数まで考慮すると0Hzを中心としたバンドパスフィルタとみなせる (実数フィルタは0Hzで折り返す)
  • 実数ローパスフィルタに周波数変換をほどこすことで任意の周波数の複素バンドパスフィルタにできる

これによって -nyquistFreq 〜 +nyquistFreq の任意の範囲を取り出すことができる。

周波数変換 (複素ミキサー)

既に複素数の信号をさらに周波数変換する。LO (ローカルオシレータ) 周波数の cos/sin を複素数で元の信号に乗算する。

複素ミキサーの良いところは原理的にはイメージが発生しないというところ。普通のミキサーでは LO に対して -LO と +LO の信号が発生するが、それがない。

実信号に変換

周波数変換を経て ±n Hz 〜 0Hz の信号になっているので、real だけとって実数にする。これを出力すれば音声が出てくる。

  1. トップ
  2. tech
  3. WebAudio でブラウザで動く SDR をつくる

ComplexAnalyserNode (WebAudio) を作った (IQ信号のFFT) | tech - 氾濫原 に続き、WebAssembly を使って複素 FIR Filter を行う AudioWorkletNode の実装を書いてみた。前回と同様 Rust と wasm_bindgen を使っている。Rust 側の実装はとても素朴。

Analyser と違うのは、直接信号に手を加える必要があるというところ。つまり AudioWorklet 内で wasm の実装を呼ぶ必要がある。

wasm_bindgen を使おうと思ったが…

wasm_bindgen の JS 側の実装は Uint8Array を文字列化するために TextEncoder を使っている。しかし TextEncoder は AudioWorkletGlobalScope には(今のところ)存在しておらず、エラーになってしまった。

自動生成されたコードに手を入れて使うのはあまりやりたくないのと、どっちにしろメモリ管理を自力でやる必要はあるので、wasm_bindgen の生成するJSコードは使わず、直接 wasm を呼びだすようにした。

なおエラーメッセージの転送などで、どうしても Uint8Array から文字列を生成したいケースはある。今回は ASCII 以外の文字が入ることは想定していないので TextEncoder の代わりに String.fromCharCode() でお茶を濁した。

wasm module を AudioWorkletGlobalScope に受け渡す

AudioWorkletGlobalScope にはそもそも fetch もないため、wasm のコードを AudioWorkletGlobalScope 内で直接読みこんでインスタンス化することができない。

どうするかというと、メインスレッド(など)で fetch を行い、wasm のモジュールを得てから、これを postMessage で transfer するという余計な手順が必要になる。

wasm module は postMessage ができるが、wasm の instance はできないので、instance 化は AudioWorkletGlobalScope 側でやる必要がある。

  1. トップ
  2. tech
  3. WebAudio ComplexFirFilterNode AudioWorklet

https://github.com/cho45/complex-analyser-node

WebAudio の AnalyserNode は実数計算しかしないタイプ (仕様でそう決まっている) ですが、IQの2ch入力の信号を FFT して表示したいので、ほぼ類似のAPIを持つComplexAnalyserNodeを作ってみました。

簡単な割に IQ 信号を WebAudio で処理する際のデバッグに便利です。

FFT部分はwasmにコンパイルしたrustfftを呼んでおり、自分では実装していません。

AudioWorkletNode と AudioWorkletProcessorの使いわけ

AudioWorkletNode と AudioWorkletProcessor をうまく使いわける必要があるのですが、今回は以下のようにしています

  • AudioWorkletProcessor (audio スレッドで動く)
    • 入力をそのまま出力にコピーするだけ
    • 入力をバッファとして貯めこみ、port 経由で AudioWorkletNode にそのまま転送する
  • AudioWorkletNode (メインスレッドで動く)
    • AudioWorkletProcessor からくるバッファを管理し、FFT 用のバッファを保持する
    • getFloatFrequencyData() に応じてバッファの内容をFFTして返す
    • 対数変換やUSB/LSBの並び換え(fftshift)も rust でやってます

こういう構成なので、wasm は普通にメインスレッドで読みこんでメインスレッドで使っています。Analyser の場合は audio スレッドで信号内容に手を加えるということはしないので、余計なことを audio スレッドでやらせたくないという気持ちがあります。

もともと WebAudio にある AnalyserNode とインターフェイスを似せようとすると同期的にしなくてはいけないので若干制約があります。全部非同期にすればもうちょいやりようがある気はします。

  1. トップ
  2. tech
  3. ComplexAnalyserNode (WebAudio) を作った (IQ信号のFFT)