概要

無線機の出す 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)