標本化定理と量子化(PCM)を導出して実装する

アナログ信号をディジタル信号に変換する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$ は次の近似が成り立ちます。

  1. $e$ は $[-\Delta/2, \, \Delta/2]$ の一様分布に従う
  2. $e$ は入力信号と無相関
  3. $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段階で構成されます。

  1. 標本化: アナログ信号をサンプリング周波数 $f_s$ でサンプリング
  2. 量子化: サンプル値を $L = 2^B$ レベルに量子化
  3. 符号化: 量子化レベルを $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()

シミュレーション結果の考察

  1. サンプリングとエイリアシング: サンプリング周波数が不十分($f_s < 2f_{\max}$)な場合、3 Hz のエイリアス成分が現れ、元の5 Hz信号と区別できなくなります。

  2. sinc補間: ホイッタカー-シャノンの補間公式により、各サンプル点を中心としたsinc関数の重ね合わせで元の連続信号が忠実に復元されています。

  3. 量子化SNR: 理論値 $6.02B + 1.76$ dB と実測値がほぼ一致しており、ビット数1増加ごとに約6 dBのSNR改善が確認できます。

  4. 圧伸の効果: 一様量子化では入力レベルが低下すると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を実現

次のステップとして、以下の記事も参考にしてください。