IQファイルの記録と読み込み
生のIQサンプル形式(cf32からcu8まで)、SigMFメタデータ、numpyによる合成キャプチャの書き込みと読み出し、スペクトログラムでのバースト検出、SoapySDRによる実際のSDRからの記録
このレッスンで学ぶこと
- 一般的な生IQ形式(インターリーブされたfloat32、int16、int8、オフセット付きuint8)を挙げ、複素1サンプルあたりのバイト数を示し、それぞれをnumpyでcomplex64に変換する
- 必須のglobal・captures・annotationsオブジェクトを持つSigMF記録を書き込み、メタデータだけを使って読み戻す
- 16 ビットおよび8 ビットでの保存がキャプチャに加える量子化雑音を測定し、スペクトラム上で確認する
- スペクトログラムでバーストの位置を特定し、その時間と周波数の範囲をSigMFアノテーションとして保存する
- SoapySDRでSDRからサンプルを記録し、SigMF記録として保存する
ソフトウェア無線(SDR)はどれも、いずれはサンプルをディスクに書き込みます。現場でキャプチャを記録し、机で解析し、同僚と共有し、あるいはデバッグ中の受信機に再生して入力します。生のサンプルのファイルは、3つの事実がなければ役に立ちません。各サンプルの格納方法、サンプルレート、中心周波数です。このレッスンは、レッスン9のフローグラフを動かし、無線機が出力したものを保存したい読者を対象としています。よく出会う生の形式、欠けている事実を記録するメタデータ規格SigMF、numpyで書き込んで読み戻す合成キャプチャ、そしてSoapySDRによる実際の記録を扱います。
生のIQファイルの中身
IQファイルは複素ベースバンドサンプルの並びで、それ以外は何も含みません。ヘッダも、サンプルレートも、周波数もありません。各サンプルは同相(I)と直交(Q)の2つの数値で、順に格納されます。普遍的な慣例はIが先、次にQです。I0、Q0、I1、Q1という具合です。異なるのは数値の型です。
| 形式 | 一般的な名称 | 複素1サンプルあたりのバイト数 | 値 | 備考 |
|---|---|---|---|---|
| 32 ビット浮動小数点 | cf32、SigMF cf32_le、SoapySDR CF32、numpy complex64 |
8 | フルスケールで約 ±1.0 | 量子化を気にする必要がない。ファイルが大きい |
| 16 ビット符号付き整数 | cs16、SigMF ci16_le、SoapySDR CS16 |
4 | −32768〜32767 | 12 ビット・14 ビットの変換器には十分 |
| 8 ビット符号付き整数 | cs8、SigMF ci8、SoapySDR CS8 |
2 | −128〜127 | ファイルが小さい。量子化雑音はフルスケールの約 50 dB 下 |
| 8 ビット符号なし(オフセットバイナリ) | cu8、SigMF cu8、SoapySDR CU8 |
2 | 0〜255、ゼロは 127.5 | 使用前にオフセットを引く |
つまずきやすい点が3つあります。第一に、numpyは複素数型をペア全体の幅で命名するため、complex64は2つのfloat32値です。SigMFとSoapySDRは各成分の幅で命名するため、cf32が同じものを指します。SigMFの仕様はこの違いを明示的に指摘しています。第二に、マルチバイト形式にはエンディアンがあります。使う機会のあるほぼすべてのマシンはリトルエンディアンで、SigMFは型名の中でそれを明記します(_le)。第三に、整数形式にはフルスケールの定義がありません。32767が1.0を意味するか0.5を意味するかは、ファイルを書いたソフトウェアの慣例です。以下のコードでは最大の整数を1.0に対応させます。これが一般的な選択です。
SigMF:ファイルを使えるものにするメタデータ
信号メタデータ形式(Signal Metadata Format、SigMF)は、GitHubで管理されているオープンな仕様です。記録を、同じベース名を持つ2つのファイルとして保存します。拡張子.sigmf-dataのデータセットファイルは、生のサンプルだけを保持します。拡張子.sigmf-metaのメタデータファイルはJSONです。本稿執筆時点で、仕様の最新リリースはv1.2.6です。
メタデータには3つのトップレベルオブジェクトがあり、いずれもスキーマで必須です。
globalは記録全体を記述します。core:datatype(例:cf32_le)とcore:version(使用したSigMFのバージョン)を含む必要があります。通常は1秒あたりのサンプル数で表したcore:sample_rateを含み、core:description、core:author、ハードウェアを表すcore:hw、ソフトウェアを表すcore:recorderを加えることもできます。capturesはセグメントのリストです。各セグメントには、そのセグメントが始まるサンプル番号core:sample_startが必要です。最初のセグメントには通常、Hz単位の中心周波数core:frequencyと、Zで終わるUTCのRFC 3339タイムスタンプcore:datetimeを加えます。annotationsは注目する領域を示すリストで、空でもかまいません。各要素にはcore:sample_startが必要で、通常はcore:sample_count、core:freq_lower_edge、core:freq_upper_edge、core:labelを持ちます。2つの周波数端は必ず組で指定します。また仕様では、中心周波数がわかっている場合はRF上の値で指定するよう求めています。
データ型の文字列は小さな文法に従います。複素数か実数かを表すcまたはr、続いてf32、f64、i32、i16、u32、u16のいずれかと_leまたは_be、あるいはエンディアンを持たないバイト型i8・u8のいずれかです。したがってcf32_le、ci16_le、cu8はいずれも有効です。一方、SoapySDRが同じ配置にCS16を使っていても、cs16はSigMFの名前ではありません。
合成キャプチャを3通りの形式で
このブロックは、2 MHzの複素キャプチャを65.5 ms分作ります。内容は+300 kHzのトーン、キャプチャの30%から70%の区間でオンになる−500 kHzのQPSKバースト、低い雑音フロアです。同じ信号をcf32_le、ci16_le、cu8の3形式で、それぞれ専用のメタデータファイルとともに書き込みます。読み出し側はデータ型とサンプルレートをメタデータだけから取得し、complex64に変換してスペクトラムを推定します。
import json
from pathlib import Path
import numpy as np
from scipy import signal
rng = np.random.default_rng(10)
fs = 2.0e6 # sample rate, 2 MHz complex
fc = 100.0e6 # centre frequency we record in the metadata (the signal is synthetic)
n = 2**17 # 131 072 samples, 65.5 ms
t = np.arange(n) / fs
# A synthetic capture (complex128 while we build it): a tone, a QPSK burst and a little noise.
tone = 0.3 * np.exp(2j * np.pi * 300e3 * t)
sps = 20 # 100 kbaud at 2 MHz
n_sym = -(-n // sps) # enough symbols to cover n samples
syms = (rng.choice([-1, 1], n_sym) + 1j * rng.choice([-1, 1], n_sym)) / np.sqrt(2)
lowpass = signal.firwin(401, 70e3, window=("kaiser", 10.0), fs=fs) # sharp, low-sidelobe filter
shaped = signal.lfilter(lowpass, 1.0, np.repeat(syms, sps)[:n])
burst = 0.4 * shaped / np.std(shaped) * np.exp(-2j * np.pi * 500e3 * t)
gate = np.zeros(n)
gate[int(0.3 * n) : int(0.7 * n)] = 1.0 # burst on from 30 % to 70 % of the capture
burst *= np.convolve(gate, np.hanning(401) / np.hanning(401).sum(), mode="same") # soft edges
noise = 1e-4 * (rng.normal(size=n) + 1j * rng.normal(size=n)) / np.sqrt(2)
x = tone + burst + noise
print(f"peak |I| or |Q|: {max(np.abs(x.real).max(), np.abs(x.imag).max()):.3f} (full scale is 1.0)")
# How each SigMF datatype is stored: numpy dtype per rail, scale to full scale, offset of zero.
FORMATS = {
"cf32_le": ("<f4", 1.0, 0.0),
"ci16_le": ("<i2", 32767.0, 0.0),
"cu8": ("u1", 127.5, 127.5),
}
def write_sigmf(base: str, z: np.ndarray, datatype: str) -> None:
dt, scale, offset = FORMATS[datatype]
iq = np.empty(2 * z.size)
iq[0::2], iq[1::2] = z.real, z.imag # interleave, in-phase first
raw = iq * scale + offset
if dt != "<f4":
info = np.iinfo(np.dtype(dt))
raw = np.clip(np.round(raw), info.min, info.max)
raw.astype(dt).tofile(f"{base}.sigmf-data")
meta = {
"global": {"core:datatype": datatype, "core:sample_rate": fs, "core:version": "1.2.6",
"core:description": "Synthetic tone and QPSK burst for Wireless Field Guide"},
"captures": [{"core:sample_start": 0, "core:frequency": fc, "core:datetime": "2026-11-15T09:00:00Z"}],
"annotations": [],
}
Path(f"{base}.sigmf-meta").write_text(json.dumps(meta, indent=2))
def read_sigmf(base: str) -> tuple[np.ndarray, dict]:
meta = json.loads(Path(f"{base}.sigmf-meta").read_text())
dt, scale, offset = FORMATS[meta["global"]["core:datatype"]]
raw = np.fromfile(f"{base}.sigmf-data", dtype=dt).astype(np.float32)
iq = (raw - offset) / scale
return (iq[0::2] + 1j * iq[1::2]).astype(np.complex64), meta
psd = {}
for dtype in FORMATS:
base = f"capture_{dtype}"
write_sigmf(base, x, dtype)
y, meta = read_sigmf(base)
size_kib = Path(f"{base}.sigmf-data").stat().st_size / 1024
f, p = signal.welch(y, fs=meta["global"]["core:sample_rate"], window="blackmanharris", nperseg=1250, detrend=False, return_onesided=False,
scaling="spectrum")
f, p = np.fft.fftshift(f), np.fft.fftshift(p)
psd[dtype] = 10 * np.log10(p.reshape(50, 25).mean(axis=1)) # 50 points, 40 kHz apart
quiet = (f > -200e3) & (f < 100e3) # a band with nothing in it
err = 20 * np.log10(np.sqrt(np.mean(np.abs(y - x) ** 2)))
print(f"{dtype:8s} {size_kib:7.0f} KiB, rms error vs original {err:6.1f} dB, "
f"mean level, -200 to +100 kHz: {10 * np.log10(np.mean(p[quiet])):6.1f} dB")
fc_bins = f.reshape(50, 25).mean(axis=1)
print("freq kHz, cf32_le dB, ci16_le dB, cu8 dB")
for i in range(50):
print(f"{fc_bins[i] / 1e3:.0f}, {psd['cf32_le'][i]:.1f}, {psd['ci16_le'][i]:.1f}, {psd['cu8'][i]:.1f}")
peak |I| or |Q|: 0.712 (full scale is 1.0)
cf32_le 1024 KiB, rms error vs original -159.6 dB, mean level, -200 to +100 kHz: -108.0 dB
ci16_le 512 KiB, rms error vs original -98.1 dB, mean level, -200 to +100 kHz: -107.9 dB
cu8 256 KiB, rms error vs original -49.6 dB, mean level, -200 to +100 kHz: -76.3 dB
freq kHz, cf32_le dB, ci16_le dB, cu8 dB
-981, -108.0, -107.9, -80.0
-941, -108.0, -107.9, -80.3
-901, -108.2, -108.1, -67.8
-861, -108.0, -108.0, -80.1
-821, -107.9, -107.8, -80.2
-781, -107.8, -107.7, -80.1
-741, -107.9, -107.8, -80.2
-701, -107.9, -107.8, -80.1
-661, -107.8, -107.7, -80.2
-621, -107.9, -107.8, -80.0
-581, -41.6, -41.6, -41.6
-541, -28.8, -28.8, -28.8
-501, -26.5, -26.5, -26.5
-461, -28.5, -28.5, -28.5
-421, -40.2, -40.2, -40.2
-381, -107.7, -107.7, -80.1
-341, -107.8, -107.8, -80.2
-301, -107.8, -107.8, -80.3
-261, -107.8, -107.8, -80.3
-221, -108.1, -108.0, -80.2
-181, -108.0, -107.9, -80.1
-141, -107.9, -107.8, -80.4
-101, -107.9, -107.8, -69.5
-61, -108.0, -108.0, -80.1
-21, -108.2, -108.1, -80.3
19, -107.9, -107.9, -80.2
59, -107.9, -107.8, -80.0
99, -107.8, -107.8, -80.1
139, -108.1, -108.0, -80.1
179, -107.8, -107.7, -80.2
219, -107.9, -107.8, -80.2
259, -107.9, -107.9, -80.3
299, -21.4, -21.4, -21.4
339, -107.8, -107.7, -80.3
379, -107.9, -107.8, -80.1
419, -107.9, -107.8, -80.2
459, -107.8, -107.8, -80.2
499, -107.8, -107.7, -80.3
539, -108.1, -108.1, -80.1
579, -107.9, -107.9, -80.3
619, -107.8, -107.8, -80.0
659, -108.0, -107.9, -80.3
699, -107.8, -107.7, -69.6
739, -108.0, -107.9, -80.2
779, -107.9, -107.8, -80.1
819, -107.8, -107.7, -80.1
859, -107.9, -107.9, -80.2
899, -107.9, -107.9, -80.1
939, -108.1, -108.1, -80.2
979, -108.0, -107.9, -80.0
ファイルサイズは1段階下げるごとに半分になります。131 072サンプルで1024 KiB、512 KiB、256 KiBです。代償は量子化雑音です。ステップΔの一様量子化器は、各レールに分散Δ²/12の雑音を加えます。±1にスケーリングした16 ビットでは、これはキャプチャ内の他のどの成分よりもはるかに小さくなります。int16のコピーと原信号の差は−98.1 dBで、そのスペクトラムは浮動小数点版と区別がつきません。8 ビットでは誤差は−49.6 dBです。帯域内の何もない部分、−200 kHzから+100 kHzの間では、平均レベルが−108.0 dBから−76.3 dBに上昇します。
cu8のトレースが示すのは、フロアの上昇だけではありません。−901 kHz、−101 kHz、+699 kHz付近にスプリアスが現れ、最大−67.8 dBに達します。256段階への丸めは非線形な操作であり、入力に強いトーンがあると誤差は純粋なランダムではなくなります。誤差には信号の高調波や相互変調積が含まれます。実際の8 ビット受信機では、これらのスプリアスを本物の信号と見誤りやすくなります。役立つ習慣が2つあります。1つは、最も強い信号がクリップせずにレンジの大部分を使うようにゲインを保つことです。もう1つは、スプリアスが現れたらゲインやチューニングを変え、本物の信号と同じように動くかを確かめることです。
バーストを見つけてアノテーションを付ける
スペクトログラムは、時間スライスごとの短いスペクトラムを並べたものです。信号がいつ存在するかを見る最も手早い方法です。次のブロックは同じキャプチャを作り直し(当サイトの各ブロックは単独で実行されます)、cf32_leとして書き込みます。続いて256点のスライスでスペクトログラムを計算し、中心より400 kHzから600 kHz下の帯域の電力を合計して、それが立ち上がる位置を見つけます。最後に、キャプチャのメタデータに中心周波数があるので、周波数端をRF上の値として、結果をSigMFアノテーションに記録します。
import json
from pathlib import Path
import numpy as np
from scipy import signal
# Rebuild the same capture as before and save it as cf32_le SigMF.
rng = np.random.default_rng(10)
fs, fc, n = 2.0e6, 100.0e6, 2**17
t = np.arange(n) / fs
sps = 20
n_sym = -(-n // sps)
syms = (rng.choice([-1, 1], n_sym) + 1j * rng.choice([-1, 1], n_sym)) / np.sqrt(2)
lowpass = signal.firwin(401, 70e3, window=("kaiser", 10.0), fs=fs)
shaped = signal.lfilter(lowpass, 1.0, np.repeat(syms, sps)[:n])
gate = np.zeros(n)
gate[int(0.3 * n) : int(0.7 * n)] = 1.0
gate = np.convolve(gate, np.hanning(401) / np.hanning(401).sum(), mode="same")
x = (0.3 * np.exp(2j * np.pi * 300e3 * t)
+ 0.4 * shaped / np.std(shaped) * np.exp(-2j * np.pi * 500e3 * t) * gate
+ 1e-4 * (rng.normal(size=n) + 1j * rng.normal(size=n)) / np.sqrt(2))
x.astype(np.complex64).tofile("burst.sigmf-data") # complex64 is interleaved float32, I first
meta = {
"global": {"core:datatype": "cf32_le", "core:sample_rate": fs, "core:version": "1.2.6"},
"captures": [{"core:sample_start": 0, "core:frequency": fc}],
"annotations": [],
}
# Read it back the way any tool would: datatype and sample rate come from the metadata.
y = np.fromfile("burst.sigmf-data", dtype=np.complex64)
fs_r = meta["global"]["core:sample_rate"]
f, tt, s = signal.spectrogram(y, fs=fs_r, window="blackmanharris", nperseg=256, noverlap=0,
detrend=False, return_onesided=False, scaling="spectrum")
f = np.fft.fftshift(f)
s = np.fft.fftshift(s, axes=0)
print(f"spectrogram: {f.size} frequency bins of {fs_r / f.size / 1e3:.2f} kHz, {tt.size} time slices of {256 / fs_r * 1e6:.0f} us")
# Find the burst: power in the 400-600 kHz band below the centre, slice by slice.
band = (f >= -600e3) & (f <= -400e3)
p_band = 10 * np.log10(s[band].sum(axis=0))
on = p_band > p_band.max() - 20 # within 20 dB of the strongest slice
first, last = np.argmax(on), on.size - 1 - np.argmax(on[::-1])
start = int(first * 256)
count = int((last + 1) * 256) - start
meta["annotations"].append({
"core:sample_start": start,
"core:sample_count": count,
"core:freq_lower_edge": fc - 600e3, # at RF, because the capture has a known centre
"core:freq_upper_edge": fc - 400e3,
"core:label": "QPSK burst",
})
Path("burst.sigmf-meta").write_text(json.dumps(meta, indent=2))
print(f"burst found from sample {start} to {start + count} ({start / fs_r * 1e3:.2f} ms to {(start + count) / fs_r * 1e3:.2f} ms)")
print(f"true burst edges: {0.3 * n / fs_r * 1e3:.2f} ms to {0.7 * n / fs_r * 1e3:.2f} ms")
print("band power over time (every 10th slice): t ms, dB")
for k in range(0, tt.size, 10):
print(f"{tt[k] * 1e3:.2f}, {p_band[k]:.1f}")
print(Path("burst.sigmf-meta").read_text())
spectrogram: 256 frequency bins of 7.81 kHz, 512 time slices of 128 us
burst found from sample 39168 to 91904 (19.58 ms to 45.95 ms)
true burst edges: 19.66 ms to 45.88 ms
band power over time (every 10th slice): t ms, dB
0.06, -87.7
1.34, -88.7
2.62, -86.3
3.90, -87.5
5.18, -86.1
6.46, -86.2
7.74, -89.3
9.02, -87.9
10.30, -86.5
11.58, -88.7
12.86, -87.2
14.14, -85.0
15.42, -86.0
16.70, -87.6
17.98, -86.0
19.26, -87.2
20.54, -5.3
21.82, -5.1
23.10, -5.1
24.38, -5.0
25.66, -4.8
26.94, -4.7
28.22, -4.7
29.50, -4.9
30.78, -4.5
32.06, -5.0
33.34, -5.1
34.62, -5.1
35.90, -4.8
37.18, -4.8
38.46, -4.9
39.74, -5.0
41.02, -5.1
42.30, -5.0
43.58, -4.8
44.86, -5.2
46.14, -88.0
47.42, -91.3
48.70, -87.1
49.98, -86.9
51.26, -86.0
52.54, -87.7
53.82, -86.8
55.10, -88.9
56.38, -86.1
57.66, -86.8
58.94, -87.8
60.22, -88.6
61.50, -86.9
62.78, -86.9
64.06, -88.0
65.34, -86.3
{
"global": {
"core:datatype": "cf32_le",
"core:sample_rate": 2000000.0,
"core:version": "1.2.6"
},
"captures": [
{
"core:sample_start": 0,
"core:frequency": 100000000.0
}
],
"annotations": [
{
"core:sample_start": 39168,
"core:sample_count": 52736,
"core:freq_lower_edge": 99400000.0,
"core:freq_upper_edge": 99600000.0,
"core:label": "QPSK burst"
}
]
}
検出器は、バーストを19.58 msから45.95 msの区間に見つけます。適用したソフトなランプを除いた真のバーストの端は、19.66 msと45.88 msです。検出領域がわずかに広いのは、しきい値がピークより20 dB下にあり、128 µsのスライス単位で外側に丸められるためです。アノテーションには、これがサンプル番号39 168、サンプル数52 736として、周波数端は99.4 MHzと99.6 MHzとして記録されます。
共有された記録を役立つものにするのがアノテーションです。同僚やスクリプトは、ファイル全体を走査せずに、ラベル付きのバーストへ直接移動できます。
実際の無線機から記録する
最後のブロックは、SoapySDRを通じてSDRから記録します。SoapySDRは自らを、ベンダーやプラットフォームに依存しないSDRサポートライブラリと説明しています。Pythonバインディングを持ち、無線機の各ファミリーにはそれぞれ専用のSoapySDRサポートモジュールを通じてアクセスします。このブロックには、コンピュータに接続したSDRと、その無線機用のSoapySDRモジュールのインストールが必要です。これらがなければ実行できません。そのため、このブロックは# needs-hardwareで始まり、当サイトのチェッカーは構文だけを検査します。driverを、使用する無線機のSoapySDRドライバキーに変更してください。当サイトのハードウェア表に、一般的な受信機とその周波数範囲・変換器のビット幅を掲載しています。
呼び出しは、SoapySDRのPythonサンプルに従っています。デバイスを開き、受信チャネル0のサンプルレートと中心周波数を設定し、選んだサンプル形式でストリームをセットアップして有効化し、バッファを渡してreadStreamを繰り返し呼び出します。getStreamFormatsは、ドライバが出力できる形式を列挙します。このブロックはインターリーブされたint16であるCS16を要求し、サンプルをSigMFのci16_leとして、開始時刻をcore:datetimeに入れて保存します。
# needs-hardware
import json
from datetime import datetime, timezone
from pathlib import Path
import numpy as np
import SoapySDR
from SoapySDR import SOAPY_SDR_RX, SOAPY_SDR_CS16
driver = "rtlsdr" # change to the SoapySDR driver key of your radio
fs = 2.0e6 # sample rate, Hz
fc = 100.0e6 # centre frequency, Hz (pick a band you may legally receive)
n = 2**18 # samples to record
sdr = SoapySDR.Device(dict(driver=driver))
print("formats offered:", sdr.getStreamFormats(SOAPY_SDR_RX, 0))
sdr.setSampleRate(SOAPY_SDR_RX, 0, fs)
sdr.setFrequency(SOAPY_SDR_RX, 0, fc)
stream = sdr.setupStream(SOAPY_SDR_RX, SOAPY_SDR_CS16) # interleaved int16, I first
start_time = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
sdr.activateStream(stream)
buf = np.empty(2 * n, dtype=np.int16) # two int16 values per complex sample
got = 0
while got < n:
chunk = np.empty(2 * 8192, dtype=np.int16)
sr = sdr.readStream(stream, [chunk], 8192) # count is in complex samples
if sr.ret > 0:
k = min(sr.ret, n - got)
buf[2 * got : 2 * (got + k)] = chunk[: 2 * k]
got += k
elif sr.ret < 0:
print(f"readStream error {sr.ret}")
break
sdr.deactivateStream(stream)
sdr.closeStream(stream)
buf[: 2 * got].astype("<i2").tofile("radio.sigmf-data")
meta = {
"global": {"core:datatype": "ci16_le", "core:sample_rate": fs, "core:version": "1.2.6",
"core:hw": f"SoapySDR driver {driver}"},
"captures": [{"core:sample_start": 0, "core:frequency": fc, "core:datetime": start_time}],
"annotations": [],
}
Path("radio.sigmf-meta").write_text(json.dumps(meta, indent=2))
print(f"recorded {got} samples at {fs / 1e6:.1f} MHz around {fc / 1e6:.3f} MHz")
実用上の注意をいくつか挙げます。readStreamは要求より少ないサンプルを返すことがあるため、ループで個数を数えています。負の戻り値はエラーコードです。SOAPY_SDR_OVERFLOW(−4)は、ホストの読み出しが間に合わず、サンプルが欠落したことを意味します。オーバーフローは記録に欠落を残します。高速なローカルディスクに記録し、サンプルレートは必要以上に上げないでください。受信は多くの地域・多くの帯域で合法ですが、どこでも、何についても合法というわけではありません。他者の送信を記録する前に、自国の規則を確認してください。GNU Radio FM受信機プロジェクトでは、これを発展させてライブ受信機を作ります。
演習
- 1つ目のブロックに
ci8形式(符号付き8 ビット、スケール127、オフセット0)を追加してください。誤差とフロアをcu8と比較し、両者がほぼ同じになる理由を説明してください。 cu8を書き込む前に、1つ目のブロックのすべての信号の振幅を半分にしてください。トーンに対して、スプリアスとフロアはどう変化しますか。次に振幅を2倍にして、クリッピングを探してください。- 2つ目のブロックで、キャプチャ全体にわたる+300 kHzのトーンに対して、2つ目のアノテーションを追加してください。メタデータを読み戻し、すべてのアノテーションのラベルと、ミリ秒単位の継続時間を表示してください。
まとめ
- 生のIQファイルは、ヘッダを持たず、IとQを同相成分を先にしてインターリーブしたサンプルの並びです。一般的な型はcf32、cs16(SigMFの
ci16_le)、cs8、cu8で、複素1サンプルあたりそれぞれ8、4、2、2バイトです。 - SigMFは、
.sigmf-dataファイルと、global・captures・annotationsを保持するJSONの.sigmf-metaファイルを組にします。core:datatypeとcore:versionは必須で、core:sample_rateとcore:frequencyがあることでファイルが使えるものになります。 - 当サイトのテストでは、16 ビットでの保存が加えた誤差は−98.1 dBで、スペクトラム上では見えませんでした。8 ビットでの保存は−49.6 dBの誤差を加え、フロアを約30 dB押し上げ、スプリアスを生じさせました。
- スペクトログラムと帯域電力のしきい値により、128 µsのスライス1つ以内の精度でバーストを見つけました。SigMFアノテーションには、それを時間とRF周波数で記録しました。
- 無線機を開くコードには、ハードウェアの接続が必要です。SoapySDRは、多くの無線機に対して1つのAPIを提供します。
さらに読む
- SigMF仕様リポジトリ:スキーマと仕様のソースです。レンダリング済みの仕様はsigmf.orgにあります。
- SoapySDRのPythonサポート:Pythonからデバイスを開いてストリームを読む公式の例です。
- GNU Radio Manual and C++ API Reference:フローグラフ内で生のサンプルストリームを読み書きする、file sourceブロック・file sinkブロックのリファレンスです。