pythonによるビームフォーマの応答

#Delay and Sumのビームパターンの描画

車輪の再開発だけど、ドンピシャで使いたいのがないので、書いてみた。
アレイ・マニフォールド・ベクトル(array manifold vector)の部分が怪しいけど、それっぽい図形が描けた。

それにしても、plot_surface の3Dプロットで、アスペクト比が調整できなくて断念。
```ax.get_proj```で変わるけど、描画領域が変わらず、うれしくない。

あまり意味はないけどL2ノルムをnumbaで定義。

```python
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numba
@numba.vectorize([numba.float64(numba.complex128),numba.float32(numba.complex64)])
def abs2(x):
    return x.real**2 + x.imag**2
```

直線状等間隔アレイ。X軸上に並ぶ。

```python
def linear_mic_array(
    num     = 4,      # Number of array elements
    spacing = 0.2,    # Element separation in metres
    Is3D    = True,
):
    PX = np.arange(num) * spacing # マイク位置
    if Is3D:
        return np.array([PX, np.zeros(len(PX)), np.zeros(len(PX))])
    else:
        return np.array([PX, np.zeros(len(PX))])
```


アレイ・マニフォールド・ベクトル(array manifold vector)の計算。
平面波は大丈夫だと思うけど、球面波はよくわからん。

```python
def calc_array_manifold_vector_from_point(mic_layout, source_from_mic, nfft=512, sampling_rate = 16000,
                                          c=343, attn=False, ff=True):
    p = np.array(source_from_mic)
    if p.ndim == 1:
        p = source[:, np.newaxis]
    
    frequencies = np.linspace(0, sampling_rate // 2, nfft // 2 + 1)
    omega = 2 * np.pi * frequencies
    if ff:
        p /= np.linalg.norm(p)
        D = -1 * np.einsum("im,i->m", mic_layout, p.squeeze())
        D -= D.min()
    else:
        D = np.sqrt(((mic_layout - source_from_mic) ** 2).sum(0))
    
    phase = np.exp(np.einsum("k,m->km", -1j * omega / c, D))
    
    if attn:
        return 1.0 / (4 * np.pi) / D * phase
    else:
        return phase
```


音源の方向の位置を計算。単位ベクトルの極座標から直行座標に変えているだけ。
向きから、source_from_mic の引数生成用。

```python
def get_look_direction_loc(deg_phi, deg_theta):
    phi   = np.deg2rad(deg_phi)
    theta = np.deg2rad(deg_theta)
    return np.array([[
        np.cos(phi) * np.cos(theta),
        np.cos(phi) * np.sin(theta),
        np.sin(phi),
    ]]).T
```

アレイ・マニフォールド・ベクトル(array manifold vector)からDelay and Sumのフィルタを生成。

```python
def calc_delay_and_sum_weights(mic_layout, source_from_mic, nfft=512, sampling_rate = 16000, 
                                          c=343, attn=False, ff=True):
    a = calc_array_manifold_vector_from_point(mic_layout, source_from_mic, nfft, sampling_rate, c, attn, ff)
    return a / np.einsum("km,km->k", a.conj(), a)[:, None]
```

ビームフォーマのフィルタに対するビームパターンを描画。MVDRとか他のビームフォーマにも使えるはず。

```python
def plot_freq_resp(
    w,
    nfft             = 512,    # Number of fft
    angle_resolution = 500,    # Number of angle points to calculate
    mic_layout       = linear_mic_array(),
    sampling_rate    = 16000,  # Hz 
    speedSound       = 343.0,  # m/s
    plt3D            = False, 
    vmin             = -50,
    vmax             = None,
):
    n_freq, n_ch = w.shape
    if n_freq != nfft // 2 + 1:
        raise ValueError("invalid n_freq")
    if n_ch != mic_layout.shape[1]:
        raise ValueError("invalid n_ch")
    
    freqs      = np.linspace(0, sampling_rate // 2, nfft // 2 + 1)
    angles     = np.linspace(0, 180, angle_resolution)
    angleRads  = np.deg2rad(angles)
    loc        = np.array([[np.cos(theta), np.sin(theta), 0] for theta in angleRads]).T
    phases     = np.einsum('k,ai,am->kim', 2j * np.pi * freqs / speedSound, loc, mic_layout) 
    psi        = np.einsum('km,kim->ki', w.conj(), np.exp(phases))
    outputs    = np.sqrt(abs2(psi))
    logOutputs = 20 * np.log10(outputs)
    if vmin is not None:
        logOutputs = np.maximum(logOutputs, vmin)
    if vmax is not None:
        logOutputs = np.minimum(logOutputs, vmax)
    
    plt.figure(figsize=(10,8))
    if plt3D:
        ax = plt.subplot(projection='3d')
        surf = ax.plot_surface(*np.meshgrid(angles, freqs), logOutputs, rstride=1, cstride=1, cmap='hsv', linewidth=0.3)
        #ax.get_proj = lambda: np.dot(Axes3D.get_proj(ax), np.diag([1.5, 1.5, 0.3, 1]))
        ax.view_init(elev=30, azim=-45)
    else:
        ax = plt.subplot()
        surf = ax.pcolormesh(*np.meshgrid(angles, freqs), logOutputs, cmap="inferno", shading='gouraud')
    plt.colorbar(surf)
    plt.xlabel("Arrival Angle[degrees]")
    plt.ylabel("Frequency[Hz]")
    plt.title("Frequency Response")
    plt.show()
```

Delay and Sumビームフォーマ専用のビームパターン生成。Y軸方向(90度)に指向性が向いている想定。

```python
def plot_ds_freq_resp(
    nfft             = 512,    # Number of fft
    angle_resolution = 500,    # Number of angle points to calculate
    mic_layout       = linear_mic_array(),
    sampling_rate    = 16000,  # Hz 
    speedSound       = 343.0,  # m/s
    plt3D            = False, 
):
    freqs      = np.linspace(0, sampling_rate // 2, nfft // 2 + 1)
    angles     = np.linspace(0, 180, angle_resolution)
    angleRads  = np.deg2rad(angles)
    loc        = np.array([[np.cos(theta), np.sin(theta), 0] for theta in angleRads]).T
    phases     = np.einsum('k,ai,am->kim', 2j * np.pi * freqs / speedSound, loc, mic_layout) 
    outputs    = np.sqrt(abs2(np.exp(phases).sum(-1))) / mic_layout.shape[1]
    logOutputs = np.maximum(20 * np.log10(outputs), -50)
    
    plt.figure(figsize=(10,8))
    if plt3D:
        ax = plt.subplot(projection='3d')
        surf = ax.plot_surface(*np.meshgrid(angles, freqs), logOutputs, rstride=1, cstride=1, cmap='hsv', linewidth=0.3)
        #ax.get_proj = lambda: np.dot(Axes3D.get_proj(ax), np.diag([1.5, 1.5, 0.3, 1]))
        ax.view_init(elev=30, azim=-45)
    else:
        ax = plt.subplot()
        surf = ax.pcolormesh(*np.meshgrid(angles, freqs), logOutputs, cmap="inferno", shading='gouraud')
    plt.colorbar(surf)
    plt.xlabel("Arrival Angle[degrees]")
    plt.ylabel("Frequency[Hz]")
    plt.title("Frequency Response")
    plt.show()
```

Delay and Sumビームフォーマのビームパターン(2D)をプロット。
```python
plot_ds_freq_resp()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455423/50e4c333-49e4-e85f-67f8-144235c6891e.png)


Delay and Sumビームフォーマのビームパターン(3D)をプロット。

```python
plot_ds_freq_resp(plt3D=True)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455423/a4ade352-96e0-1f06-830d-79e28d96dc1e.png)


マイク間隔を変更してプロット。メインローブの太さが変わっていたり、折り返し歪が発生しているのがわかる。

```python
plot_ds_freq_resp(plt3D=True, mic_layout=linear_mic_array(num=8, spacing=0.06))
plot_ds_freq_resp(plt3D=True, mic_layout=linear_mic_array(num=8, spacing=0.03))
plot_ds_freq_resp(plt3D=True, mic_layout=linear_mic_array(num=8, spacing=0.015))
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455423/aebae7be-8ce9-166d-a8fe-8e4f8819bbce.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455423/c516299d-d1fa-f3c1-da10-32d8ad438cd2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455423/535a4f32-73db-c4a2-8a77-d87ec0bb6e33.png)


指向性の向きを変えてビームパターンをプロット。

```pyhon
mic_layout = linear_mic_array()
source_from_mic = get_look_direction_loc(0, 120)
Wds = calc_delay_and_sum_weights(mic_layout, source_from_mic)
plot_freq_resp(Wds, mic_layout=mic_layout)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455423/0c54118f-f15c-fcc9-d07f-04176a0014e0.png)