アナログ信号をディジタル信号に変換するA/D変換は、現代のディジタル通信の出発点です。A/D変換は「標本化(サンプリング)→ 量子化 → 符号化」の3段階で行われ、これらを組み合わせた方式をPCM(パルス符号変調)と呼びます。
標本化定理は、「帯域制限されたアナログ信号は、一定の条件を満たすサンプリングレートで完全に復元できる」という情報理論の根幹をなす定理です。本記事では、この定理の厳密な証明から量子化雑音の解析、PCMの実装まで丁寧に解説します。
本記事の内容
- 標本化の数学的モデル(インパルス列との乗算)
- 標本化定理の証明(周波数領域でのスペクトル解析)
- エイリアシングの条件と防止
- 理想的復元(sinc関数補間)の導出
- 量子化の数学(一様量子化・量子化雑音)
- 量子化SNR = $6.02B + 1.76$ dB の導出
- PCM(パルス符号変調)の仕組み
- $\mu$-law / A-law 圧伸
- Pythonによるシミュレーション
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
標本化の数学的モデル
インパルス列によるサンプリング
連続時間信号 $x(t)$ を周期 $T_s$ でサンプリングする操作は、数学的にはインパルス列(くし型関数)との乗算で表されます。
サンプリング周波数を $f_s = 1/T_s$ とし、理想的なインパルス列を
$$ p(t) = \sum_{n=-\infty}^{\infty} \delta(t – nT_s) $$
とします。サンプリングされた信号は
$$ x_s(t) = x(t) \cdot p(t) = \sum_{n=-\infty}^{\infty} x(nT_s) \, \delta(t – nT_s) $$
です。各インパルスの「重み」が元信号のサンプル値 $x(nT_s)$ になっています。
周波数領域での解析
インパルス列 $p(t)$ のフーリエ変換を導出します。$p(t)$ は周期 $T_s$ の周期関数なので、フーリエ級数展開できます。
$$ p(t) = \sum_{n=-\infty}^{\infty} \delta(t – nT_s) = \sum_{k=-\infty}^{\infty} c_k \, e^{j 2\pi k f_s t} $$
フーリエ係数 $c_k$ は
$$ \begin{align} c_k &= \frac{1}{T_s} \int_{-T_s/2}^{T_s/2} p(t) \, e^{-j 2\pi k f_s t} \, dt \\ &= \frac{1}{T_s} \int_{-T_s/2}^{T_s/2} \delta(t) \, e^{-j 2\pi k f_s t} \, dt \\ &= \frac{1}{T_s} \cdot e^{0} \\ &= \frac{1}{T_s} = f_s \end{align} $$
全ての $k$ について $c_k = f_s$ なので
$$ p(t) = f_s \sum_{k=-\infty}^{\infty} e^{j 2\pi k f_s t} $$
フーリエ変換すると
$$ P(f) = f_s \sum_{k=-\infty}^{\infty} \delta(f – k f_s) $$
つまり、インパルス列のフーリエ変換もまたインパルス列(間隔 $f_s$)です。
サンプリング信号のスペクトル
時間領域での乗算は周波数領域での畳み込みに対応するため
$$ \begin{align} X_s(f) &= X(f) * P(f) \\ &= X(f) * f_s \sum_{k=-\infty}^{\infty} \delta(f – kf_s) \\ &= f_s \sum_{k=-\infty}^{\infty} X(f – kf_s) \end{align} $$
最後の等式はデルタ関数の性質 $X(f) * \delta(f – kf_s) = X(f – kf_s)$ を用いました。
この結果は非常に重要です。サンプリング信号のスペクトルは、元信号のスペクトル $X(f)$ が周波数軸上で $f_s$ 間隔で周期的に繰り返された(レプリカ)ものになります。
標本化定理の証明
定理の主張
標本化定理(ナイキスト-シャノンの定理): 帯域制限信号 $x(t)$($|f| > f_{\max}$ で $X(f) = 0$)は、サンプリング周波数が
$$ f_s > 2 f_{\max} $$
を満たすとき、サンプル値 $\{x(nT_s)\}_{n=-\infty}^{\infty}$ から完全に復元できる。
$f_N = 2f_{\max}$ をナイキストレートと呼びます。
証明
帯域制限信号のスペクトル $X(f)$ は $|f| \leq f_{\max}$ の範囲にのみ存在します。サンプリング信号のスペクトルは
$$ X_s(f) = f_s \sum_{k=-\infty}^{\infty} X(f – kf_s) $$
です。各レプリカ $X(f – kf_s)$ は $|f – kf_s| \leq f_{\max}$ の範囲に存在します。
$f_s > 2f_{\max}$ のとき、隣接するレプリカ間のギャップは
$$ f_s – 2f_{\max} > 0 $$
となり、レプリカ同士が重なりません。したがって、$|f| \leq f_s/2$ の帯域に理想低域通過フィルタ(LPF)を適用すれば、$k = 0$ のレプリカのみを取り出せます。
理想LPFの伝達関数を
$$ H_{\text{LP}}(f) = \begin{cases} \frac{1}{f_s} & |f| \leq \frac{f_s}{2} \\ 0 & |f| > \frac{f_s}{2} \end{cases} $$
とすると、フィルタ出力のスペクトルは
$$ \hat{X}(f) = X_s(f) \cdot H_{\text{LP}}(f) = f_s \cdot X(f) \cdot \frac{1}{f_s} = X(f) $$
元のスペクトルが完全に復元されました。$\square$
エイリアシング
$f_s \leq 2f_{\max}$ の場合、隣接するレプリカが重なり合います。この現象をエイリアシング(折り返し)と呼びます。エイリアシングが発生すると、重なった部分の情報が失われるため、元信号を完全に復元することは原理的に不可能です。
具体的には、周波数 $f_0 > f_s/2$ の成分は、$f_0 – f_s$ の周波数に「折り返されて」見えます。これがエイリアスです。
理想的復元(sinc関数補間)
復元公式の導出
理想LPFの時間領域でのインパルス応答を求めます。
$$ \begin{align} h_{\text{LP}}(t) &= \mathcal{F}^{-1}\{H_{\text{LP}}(f)\} \\ &= \int_{-f_s/2}^{f_s/2} \frac{1}{f_s} e^{j 2\pi f t} \, df \\ &= \frac{1}{f_s} \left[ \frac{e^{j 2\pi f t}}{j 2\pi t} \right]_{-f_s/2}^{f_s/2} \\ &= \frac{1}{f_s} \cdot \frac{e^{j \pi f_s t} – e^{-j \pi f_s t}}{j 2\pi t} \\ &= \frac{1}{f_s} \cdot \frac{2j \sin(\pi f_s t)}{j 2\pi t} \\ &= \frac{\sin(\pi f_s t)}{\pi f_s t} \\ &= \text{sinc}(f_s t) \end{align} $$
ここで $\text{sinc}(x) = \sin(\pi x) / (\pi x)$ です。
復元信号は、サンプリング信号と理想LPFインパルス応答の畳み込みです。
$$ \begin{align} \hat{x}(t) &= x_s(t) * h_{\text{LP}}(t) \\ &= \sum_{n=-\infty}^{\infty} x(nT_s) \, \delta(t – nT_s) * \text{sinc}(f_s t) \\ &= \sum_{n=-\infty}^{\infty} x(nT_s) \, \text{sinc}\left(\frac{t – nT_s}{T_s}\right) \end{align} $$
最後の等式では $\text{sinc}(f_s(t – nT_s)) = \text{sinc}((t – nT_s)/T_s)$ としました。これがホイッタカー-シャノンの補間公式です。
$$ \hat{x}(t) = \sum_{n=-\infty}^{\infty} x(nT_s) \, \text{sinc}\left(\frac{t – nT_s}{T_s}\right) $$
sinc関数の性質 $\text{sinc}(0) = 1$ かつ $\text{sinc}(m) = 0$($m$ が非零整数)により、$t = mT_s$ では
$$ \hat{x}(mT_s) = x(mT_s) $$
が成り立ち、サンプル点での値が一致することも確認できます。
量子化の数学
一様量子化
量子化は、連続値の振幅を離散的なレベルに丸める操作です。$B$ ビットの一様量子化器では、入力のダイナミックレンジ $[-V_{\max}, V_{\max}]$ を $L = 2^B$ 個の等間隔な量子化レベルに分割します。
量子化ステップサイズ(量子化幅)は
$$ \Delta = \frac{2V_{\max}}{L} = \frac{2V_{\max}}{2^B} $$
量子化レベルの中点を $q_i$ とし、入力 $x$ を最も近い $q_i$ にマッピングします。量子化出力は
$$ Q(x) = q_i \quad \text{if } x \in \left[q_i – \frac{\Delta}{2}, \, q_i + \frac{\Delta}{2}\right) $$
量子化誤差
量子化誤差(量子化雑音)は
$$ e = x – Q(x) $$
です。一様量子化では $e$ は $[-\Delta/2, \, \Delta/2]$ の範囲に収まります。
量子化雑音のモデル
入力信号のダイナミックレンジが十分に活用されており、$B$ が十分大きい場合、量子化誤差 $e$ は次の近似が成り立ちます。
- $e$ は $[-\Delta/2, \, \Delta/2]$ の一様分布に従う
- $e$ は入力信号と無相関
- $e$ は白色雑音(スペクトルが平坦)
一様分布の平均と分散を計算します。
平均:
$$ E[e] = \int_{-\Delta/2}^{\Delta/2} e \cdot \frac{1}{\Delta} \, de = \frac{1}{\Delta} \left[\frac{e^2}{2}\right]_{-\Delta/2}^{\Delta/2} = \frac{1}{\Delta} \cdot 0 = 0 $$
分散(量子化雑音電力):
$$ \begin{align} \sigma_e^2 = E[e^2] &= \int_{-\Delta/2}^{\Delta/2} e^2 \cdot \frac{1}{\Delta} \, de \\ &= \frac{1}{\Delta} \left[\frac{e^3}{3}\right]_{-\Delta/2}^{\Delta/2} \\ &= \frac{1}{\Delta} \cdot \frac{2}{3} \cdot \left(\frac{\Delta}{2}\right)^3 \\ &= \frac{1}{\Delta} \cdot \frac{2}{3} \cdot \frac{\Delta^3}{8} \\ &= \frac{\Delta^2}{12} \end{align} $$
量子化SNRの導出
$6.02B + 1.76$ dB の法則
信号が $[-V_{\max}, V_{\max}]$ を一様に使う正弦波の場合を考えます。振幅 $V_{\max}$ の正弦波 $x(t) = V_{\max} \sin(2\pi f_0 t)$ の平均電力は
$$ P_x = \frac{V_{\max}^2}{2} $$
量子化ステップサイズは $\Delta = 2V_{\max}/2^B$ なので、量子化雑音電力は
$$ P_e = \frac{\Delta^2}{12} = \frac{(2V_{\max}/2^B)^2}{12} = \frac{4V_{\max}^2}{12 \cdot 2^{2B}} = \frac{V_{\max}^2}{3 \cdot 2^{2B}} $$
量子化SNRは
$$ \begin{align} \text{SNR}_q &= \frac{P_x}{P_e} \\ &= \frac{V_{\max}^2 / 2}{V_{\max}^2 / (3 \cdot 2^{2B})} \\ &= \frac{3 \cdot 2^{2B}}{2} \\ &= \frac{3}{2} \cdot 4^B \end{align} $$
デシベル表記に変換します。
$$ \begin{align} \text{SNR}_q \, [\text{dB}] &= 10 \log_{10}\left(\frac{3}{2} \cdot 4^B\right) \\ &= 10 \log_{10}\left(\frac{3}{2}\right) + 10 \log_{10}(4^B) \\ &= 10 \log_{10}\left(\frac{3}{2}\right) + 10B \log_{10}(4) \\ &= 10 \log_{10}(1.5) + 10B \cdot 2 \log_{10}(2) \\ &= 1.761 + 20B \cdot 0.3010 \\ &= 1.76 + 6.02 B \end{align} $$
したがって
$$ \boxed{\text{SNR}_q \, [\text{dB}] = 6.02 B + 1.76} $$
これが量子化SNRの基本公式です。ビット数 $B$ を1ビット増やすごとに、SNRが約6 dB 改善することを意味します。
具体例
| ビット数 $B$ | 量子化レベル数 $L$ | SNR [dB] |
|---|---|---|
| 8 | 256 | 49.9 |
| 12 | 4096 | 74.0 |
| 16 | 65536 | 98.1 |
| 24 | 16777216 | 146.2 |
電話音声(8ビット)では約50 dBのSNR、CDオーディオ(16ビット)では約98 dBのSNRが得られます。
PCM(パルス符号変調)
PCMの概要
PCM(Pulse Code Modulation)は、アナログ信号をディジタル信号に変換する最も基本的な方式です。以下の3段階で構成されます。
- 標本化: アナログ信号をサンプリング周波数 $f_s$ でサンプリング
- 量子化: サンプル値を $L = 2^B$ レベルに量子化
- 符号化: 量子化レベルを $B$ ビットの二進符号に変換
PCMのビットレートは
$$ R_b = f_s \cdot B \quad [\text{bit/s}] $$
例えば、電話音声($f_s = 8\,\text{kHz}$, $B = 8$)では $R_b = 64\,\text{kbit/s}$、CDオーディオ($f_s = 44.1\,\text{kHz}$, $B = 16$, ステレオ)では $R_b = 1.411\,\text{Mbit/s}$ です。
PCMの信号処理フロー
アナログ信号 x(t)
↓ [アンチエイリアシングフィルタ] (f_max 以上をカット)
↓ [標本化] (f_s > 2*f_max)
x[n] = x(n*T_s)
↓ [量子化] (B ビット一様量子化)
x_q[n] = Q(x[n])
↓ [符号化] (自然二進符号 or グレイ符号)
ビット列 b_0, b_1, ..., b_{B-1}
↓ [送信]
$\mu$-law / A-law 圧伸
圧伸の必要性
音声信号のような動的範囲の広い信号を一様量子化すると、小振幅の信号に対するSNRが著しく低下します。これは、量子化ステップ $\Delta$ が一定であるため、小振幅信号の量子化誤差の相対比率が大きくなるからです。
圧伸(companding = compression + expanding)は、信号を量子化する前に非線形圧縮し、復元時に逆の伸張を行う技術です。小振幅の信号を拡大し、大振幅の信号を圧縮することで、実効的に非一様量子化を実現します。
$\mu$-law 圧伸
北米・日本で使用される圧伸方式です。圧縮特性は
$$ F_\mu(x) = \frac{\ln(1 + \mu |x|)}{\ln(1 + \mu)} \cdot \text{sgn}(x), \quad -1 \leq x \leq 1 $$
ここで $\mu$ は圧縮パラメータ(通常 $\mu = 255$)です。
$\mu = 0$ のとき $F_\mu(x) = x$(一様量子化)に退化します。$\mu$ が大きいほど小振幅の圧縮が強くなります。$\mu = 255$ の場合を展開すると
$$ F_{255}(x) = \frac{\ln(1 + 255|x|)}{\ln(256)} \cdot \text{sgn}(x) $$
A-law 圧伸
欧州で使用される圧伸方式です。
$$ F_A(x) = \begin{cases} \frac{A|x|}{1 + \ln A} \cdot \text{sgn}(x) & 0 \leq |x| \leq \frac{1}{A} \\ \frac{1 + \ln(A|x|)}{1 + \ln A} \cdot \text{sgn}(x) & \frac{1}{A} < |x| \leq 1 \end{cases} $$
通常 $A = 87.6$ が使用されます。
圧伸によるSNR改善
一様量子化では信号振幅に依存してSNRが変動しますが、圧伸を用いると広い入力レベル範囲にわたってほぼ一定のSNRが得られます。これは、小振幅信号では量子化ステップが細かく、大振幅信号では粗くなるためです。
Pythonによるシミュレーション
サンプリングとエイリアシングの可視化
import numpy as np
import matplotlib.pyplot as plt
# ===== サンプリングとエイリアシングの可視化 =====
# 元の信号: 5 Hz の正弦波
f_signal = 5 # 信号周波数 [Hz]
t_cont = np.linspace(0, 1, 10000) # 連続時間(近似)
x_cont = np.sin(2 * np.pi * f_signal * t_cont)
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# (1) 適切なサンプリング (fs = 40 Hz > 2*5 = 10 Hz)
fs1 = 40
ts1 = np.arange(0, 1, 1/fs1)
xs1 = np.sin(2 * np.pi * f_signal * ts1)
ax = axes[0, 0]
ax.plot(t_cont, x_cont, 'b-', alpha=0.5, label=f'Original ({f_signal} Hz)')
ax.stem(ts1, xs1, linefmt='r-', markerfmt='ro', basefmt='k-', label=f'Sampled ($f_s$={fs1} Hz)')
ax.set_title(f'Proper Sampling ($f_s$={fs1} Hz > $2f_{{max}}$={2*f_signal} Hz)')
ax.legend()
ax.grid(True, alpha=0.3)
ax.set_xlabel('Time [s]')
ax.set_ylabel('Amplitude')
# (2) ナイキストぎりぎり (fs = 12 Hz)
fs2 = 12
ts2 = np.arange(0, 1, 1/fs2)
xs2 = np.sin(2 * np.pi * f_signal * ts2)
ax = axes[0, 1]
ax.plot(t_cont, x_cont, 'b-', alpha=0.5, label=f'Original ({f_signal} Hz)')
ax.stem(ts2, xs2, linefmt='r-', markerfmt='ro', basefmt='k-', label=f'Sampled ($f_s$={fs2} Hz)')
ax.set_title(f'Marginal Sampling ($f_s$={fs2} Hz)')
ax.legend()
ax.grid(True, alpha=0.3)
ax.set_xlabel('Time [s]')
ax.set_ylabel('Amplitude')
# (3) エイリアシング発生 (fs = 8 Hz < 2*5 = 10 Hz)
fs3 = 8
ts3 = np.arange(0, 1, 1/fs3)
xs3 = np.sin(2 * np.pi * f_signal * ts3)
# エイリアス周波数: f_alias = f_s - f_signal = 3 Hz
f_alias = fs3 - f_signal
x_alias = np.sin(2 * np.pi * f_alias * t_cont)
ax = axes[1, 0]
ax.plot(t_cont, x_cont, 'b-', alpha=0.5, label=f'Original ({f_signal} Hz)')
ax.plot(t_cont, x_alias, 'g--', alpha=0.7, label=f'Alias ({f_alias} Hz)')
ax.stem(ts3, xs3, linefmt='r-', markerfmt='ro', basefmt='k-', label=f'Sampled ($f_s$={fs3} Hz)')
ax.set_title(f'Aliasing ($f_s$={fs3} Hz < $2f_{{max}}$={2*f_signal} Hz)')
ax.legend()
ax.grid(True, alpha=0.3)
ax.set_xlabel('Time [s]')
ax.set_ylabel('Amplitude')
# (4) スペクトルの比較
ax = axes[1, 1]
# 各サンプリングレートでのスペクトル(レプリカ)を描画
for fs, color, label in [(40, 'blue', '$f_s$=40Hz'),
(12, 'green', '$f_s$=12Hz'),
(8, 'red', '$f_s$=8Hz')]:
freqs = np.linspace(-60, 60, 1000)
spectrum = np.zeros_like(freqs)
for k in range(-3, 4):
center = k * fs
# 三角形で信号スペクトルを近似表現
spectrum += np.maximum(0, 1 - np.abs(freqs - center) / f_signal)
spectrum += np.maximum(0, 1 - np.abs(freqs + center) / f_signal)
ax.plot(freqs, spectrum, color=color, alpha=0.6, label=label)
ax.set_title('Spectrum Replicas')
ax.set_xlabel('Frequency [Hz]')
ax.set_ylabel('Amplitude')
ax.legend()
ax.grid(True, alpha=0.3)
ax.set_xlim([-30, 30])
plt.tight_layout()
plt.savefig('sampling_aliasing.png', dpi=150, bbox_inches='tight')
plt.show()
sinc関数による信号復元
import numpy as np
import matplotlib.pyplot as plt
# ===== sinc補間による信号復元 =====
f_signal = 5 # 信号周波数 [Hz]
fs = 20 # サンプリング周波数 [Hz]
Ts = 1 / fs
# 元の連続信号
t_cont = np.linspace(0, 1, 10000)
x_cont = np.sin(2 * np.pi * f_signal * t_cont)
# サンプリング
n_samples = int(1 * fs)
t_samples = np.arange(n_samples) * Ts
x_samples = np.sin(2 * np.pi * f_signal * t_samples)
# sinc補間による復元
def sinc_interpolation(t, t_samples, x_samples, Ts):
"""ホイッタカー-シャノンの補間公式"""
x_reconstructed = np.zeros_like(t)
for n in range(len(t_samples)):
x_reconstructed += x_samples[n] * np.sinc((t - t_samples[n]) / Ts)
return x_reconstructed
x_reconstructed = sinc_interpolation(t_cont, t_samples, x_samples, Ts)
# プロット
fig, axes = plt.subplots(2, 1, figsize=(12, 8))
# 上段: 復元結果
ax = axes[0]
ax.plot(t_cont, x_cont, 'b-', linewidth=2, label='Original signal', alpha=0.7)
ax.plot(t_cont, x_reconstructed, 'r--', linewidth=2, label='Reconstructed (sinc)')
ax.stem(t_samples, x_samples, linefmt='g-', markerfmt='go', basefmt='k-',
label='Samples')
ax.set_xlabel('Time [s]')
ax.set_ylabel('Amplitude')
ax.set_title(f'Sinc Interpolation ($f_s$={fs} Hz, $f_{{signal}}$={f_signal} Hz)')
ax.legend()
ax.grid(True, alpha=0.3)
# 下段: 各sinc関数の寄与
ax = axes[1]
for n in range(len(t_samples)):
sinc_contribution = x_samples[n] * np.sinc((t_cont - t_samples[n]) / Ts)
ax.plot(t_cont, sinc_contribution, alpha=0.4, linewidth=0.8)
ax.plot(t_cont, x_reconstructed, 'k-', linewidth=2, label='Sum of sinc functions')
ax.stem(t_samples, x_samples, linefmt='r-', markerfmt='ro', basefmt='k-')
ax.set_xlabel('Time [s]')
ax.set_ylabel('Amplitude')
ax.set_title('Individual sinc contributions')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('sinc_interpolation.png', dpi=150, bbox_inches='tight')
plt.show()
量子化シミュレーション
import numpy as np
import matplotlib.pyplot as plt
# ===== 量子化シミュレーション =====
# 1kHz正弦波を量子化
f0 = 1000 # 信号周波数 [Hz]
fs = 44100 # サンプリング周波数 [Hz]
duration = 0.01 # 表示期間 [s]
t = np.arange(0, duration, 1/fs)
x = np.sin(2 * np.pi * f0 * t)
# 量子化関数
def uniform_quantize(x, B, V_max=1.0):
"""一様量子化"""
L = 2**B
Delta = 2 * V_max / L
# 量子化
x_clipped = np.clip(x, -V_max, V_max - Delta)
x_q = Delta * np.floor(x_clipped / Delta + 0.5)
return x_q, Delta
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
bit_depths = [2, 4, 8, 16]
for idx, B in enumerate(bit_depths):
ax = axes[idx // 2, idx % 2]
x_q, Delta = uniform_quantize(x, B)
error = x - x_q
ax.plot(t * 1000, x, 'b-', linewidth=1.5, label='Original', alpha=0.7)
ax.step(t * 1000, x_q, 'r-', linewidth=1.5, label=f'Quantized ({B} bit)',
where='mid')
ax.set_xlabel('Time [ms]')
ax.set_ylabel('Amplitude')
ax.set_title(f'{B}-bit Quantization ($\\Delta$={Delta:.4f}, L={2**B})')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('quantization_demo.png', dpi=150, bbox_inches='tight')
plt.show()
import numpy as np
import matplotlib.pyplot as plt
# ===== 量子化SNRの理論値と実測値の比較 =====
f0 = 1000
fs = 44100
duration = 1.0 # 1秒間
t = np.arange(0, duration, 1/fs)
x = np.sin(2 * np.pi * f0 * t)
def uniform_quantize(x, B, V_max=1.0):
"""一様量子化"""
L = 2**B
Delta = 2 * V_max / L
x_clipped = np.clip(x, -V_max, V_max - Delta)
x_q = Delta * np.floor(x_clipped / Delta + 0.5)
return x_q, Delta
bit_range = np.arange(2, 25)
snr_theory = 6.02 * bit_range + 1.76 # 理論値 [dB]
snr_measured = []
for B in bit_range:
x_q, _ = uniform_quantize(x, B)
error = x - x_q
P_signal = np.mean(x**2)
P_noise = np.mean(error**2)
if P_noise > 0:
snr_measured.append(10 * np.log10(P_signal / P_noise))
else:
snr_measured.append(np.inf)
snr_measured = np.array(snr_measured)
plt.figure(figsize=(10, 6))
plt.plot(bit_range, snr_theory, 'b-', linewidth=2, label='Theory: $6.02B + 1.76$ dB')
plt.plot(bit_range, snr_measured, 'ro', markersize=6, label='Measured (sine wave)')
plt.xlabel('Number of Bits $B$', fontsize=12)
plt.ylabel('SNR [dB]', fontsize=12)
plt.title('Quantization SNR vs Number of Bits', fontsize=13)
plt.legend(fontsize=11)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('quantization_snr.png', dpi=150, bbox_inches='tight')
plt.show()
for B in [8, 12, 16, 24]:
idx = B - 2
print(f"B={B:2d}: Theory={snr_theory[idx]:.1f} dB, Measured={snr_measured[idx]:.1f} dB")
$\mu$-law 圧伸の可視化
import numpy as np
import matplotlib.pyplot as plt
# ===== μ-law 圧伸の特性と効果 =====
def mu_law_compress(x, mu=255):
"""μ-law 圧縮"""
return np.sign(x) * np.log(1 + mu * np.abs(x)) / np.log(1 + mu)
def mu_law_expand(y, mu=255):
"""μ-law 伸張(逆変換)"""
return np.sign(y) * (1/mu) * ((1 + mu)**np.abs(y) - 1)
def a_law_compress(x, A=87.6):
"""A-law 圧縮"""
y = np.zeros_like(x)
mask_small = np.abs(x) <= 1/A
mask_large = np.abs(x) > 1/A
y[mask_small] = A * np.abs(x[mask_small]) / (1 + np.log(A)) * np.sign(x[mask_small])
y[mask_large] = (1 + np.log(A * np.abs(x[mask_large]))) / (1 + np.log(A)) * np.sign(x[mask_large])
return y
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# (1) μ-law 圧縮特性
ax = axes[0, 0]
x_range = np.linspace(-1, 1, 1000)
for mu_val in [0, 5, 50, 255]:
if mu_val == 0:
y = x_range # 線形(一様量子化)
label = '$\\mu$=0 (linear)'
else:
y = mu_law_compress(x_range, mu_val)
label = f'$\\mu$={mu_val}'
ax.plot(x_range, y, linewidth=2, label=label)
ax.set_xlabel('Input $x$')
ax.set_ylabel('Output $F_\\mu(x)$')
ax.set_title('$\\mu$-law Compression Characteristics')
ax.legend()
ax.grid(True, alpha=0.3)
# (2) A-law 圧縮特性
ax = axes[0, 1]
ax.plot(x_range, x_range, 'b-', linewidth=2, label='Linear')
ax.plot(x_range, a_law_compress(x_range), 'r-', linewidth=2, label='A-law (A=87.6)')
ax.plot(x_range, mu_law_compress(x_range), 'g--', linewidth=2, label='$\\mu$-law ($\\mu$=255)')
ax.set_xlabel('Input $x$')
ax.set_ylabel('Compressed Output')
ax.set_title('Comparison of Companding Laws')
ax.legend()
ax.grid(True, alpha=0.3)
# (3) 量子化SNRの比較(一様 vs μ-law)
ax = axes[1, 0]
B = 8 # 8ビット量子化
input_levels_dB = np.linspace(-50, 0, 50) # 入力レベル [dB]
snr_uniform = []
snr_mulaw = []
for level_dB in input_levels_dB:
amplitude = 10**(level_dB / 20)
# テスト信号
t = np.linspace(0, 0.1, 10000)
x = amplitude * np.sin(2 * np.pi * 100 * t)
# 一様量子化
L = 2**B
Delta = 2.0 / L
x_q_uniform = Delta * np.floor(np.clip(x, -1, 1-Delta) / Delta + 0.5)
e_uniform = x - x_q_uniform
P_s = np.mean(x**2)
P_e_uniform = np.mean(e_uniform**2)
snr_uniform.append(10*np.log10(P_s / P_e_uniform) if P_e_uniform > 0 else 0)
# μ-law 圧伸量子化
x_compressed = mu_law_compress(x, mu=255)
x_compressed_q = Delta * np.floor(np.clip(x_compressed, -1, 1-Delta) / Delta + 0.5)
x_expanded = mu_law_expand(x_compressed_q, mu=255)
e_mulaw = x - x_expanded
P_e_mulaw = np.mean(e_mulaw**2)
snr_mulaw.append(10*np.log10(P_s / P_e_mulaw) if P_e_mulaw > 0 else 0)
ax.plot(input_levels_dB, snr_uniform, 'b-', linewidth=2, label='Uniform')
ax.plot(input_levels_dB, snr_mulaw, 'r-', linewidth=2, label='$\\mu$-law ($\\mu$=255)')
ax.set_xlabel('Input Level [dB]')
ax.set_ylabel('SNR [dB]')
ax.set_title(f'Quantization SNR vs Input Level ({B}-bit)')
ax.legend()
ax.grid(True, alpha=0.3)
# (4) PCM符号化の例
ax = axes[1, 1]
B_demo = 3
t_demo = np.linspace(0, 1, 1000)
x_demo = 0.8 * np.sin(2 * np.pi * 2 * t_demo)
L_demo = 2**B_demo
Delta_demo = 2.0 / L_demo
x_q_demo = Delta_demo * np.floor(np.clip(x_demo, -1, 1-Delta_demo) / Delta_demo + 0.5)
# サンプリング
fs_demo = 20
t_s_demo = np.arange(0, 1, 1/fs_demo)
x_s_demo = 0.8 * np.sin(2 * np.pi * 2 * t_s_demo)
x_sq_demo = Delta_demo * np.floor(np.clip(x_s_demo, -1, 1-Delta_demo) / Delta_demo + 0.5)
ax.plot(t_demo, x_demo, 'b-', alpha=0.5, label='Original')
ax.step(t_s_demo, x_sq_demo, 'r-', where='mid', linewidth=2, label=f'PCM ({B_demo}-bit)')
ax.stem(t_s_demo, x_sq_demo, linefmt='r-', markerfmt='ro', basefmt='k-')
# 量子化レベルを点線で表示
levels = np.arange(-1 + Delta_demo/2, 1, Delta_demo)
for lv in levels:
ax.axhline(y=lv, color='gray', linestyle=':', alpha=0.3)
ax.set_xlabel('Time [s]')
ax.set_ylabel('Amplitude')
ax.set_title(f'PCM Encoding ({B_demo}-bit, $f_s$={fs_demo} Hz)')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('pcm_companding.png', dpi=150, bbox_inches='tight')
plt.show()
シミュレーション結果の考察
-
サンプリングとエイリアシング: サンプリング周波数が不十分($f_s < 2f_{\max}$)な場合、3 Hz のエイリアス成分が現れ、元の5 Hz信号と区別できなくなります。
-
sinc補間: ホイッタカー-シャノンの補間公式により、各サンプル点を中心としたsinc関数の重ね合わせで元の連続信号が忠実に復元されています。
-
量子化SNR: 理論値 $6.02B + 1.76$ dB と実測値がほぼ一致しており、ビット数1増加ごとに約6 dBのSNR改善が確認できます。
-
圧伸の効果: 一様量子化では入力レベルが低下するとSNRも急激に低下しますが、$\mu$-law 圧伸では広い入力レベル範囲にわたってほぼ一定のSNRを維持しています。
まとめ
本記事では、標本化定理と量子化(PCM)について解説しました。
- 標本化定理: 帯域制限信号 $x(t)$(最高周波数 $f_{\max}$)は、$f_s > 2f_{\max}$ でサンプリングすれば完全に復元可能。これはスペクトルのレプリカが重ならない条件から導かれる
- sinc補間: 復元はホイッタカー-シャノンの補間公式 $\hat{x}(t) = \sum_n x(nT_s)\text{sinc}((t-nT_s)/T_s)$ で実現される
- 量子化雑音: 一様量子化の雑音電力は $\sigma_e^2 = \Delta^2/12$
- 量子化SNR: 正弦波に対して $\text{SNR}_q = 6.02B + 1.76$ dB。ビット数1増加で約6 dB改善
- PCM: 標本化・量子化・符号化の3段階でA/D変換を行う基本方式
- 圧伸: $\mu$-law / A-law により、広い入力範囲で一定のSNRを実現
次のステップとして、以下の記事も参考にしてください。