Based on the tutorial for a linear array here, I've written the following code for a 6 microphone circular array. The array has a radius of 5cm. A beam pattern is generated but it doesn't look right at all.

This code generates the signals (I think properly?):

import numpy as np
import matplotlib.pyplot as plt

# Recording/positioning properties of the array
sample_rate = 16000
N = 10000  # number of samples to simulate
radius = 0.0463 
num_mics = 6

# Microphone positions in a circular array
theta_mics = np.linspace(0, 2 * np.pi, num_mics, endpoint=False)
mic_positions = radius * np.exp(1j * theta_mics)

# Find microphone angles
angles_deg = np.linspace(0, 360, num_mics, endpoint=False)  # Angles for each microphone
angles_rad = np.deg2rad(angles_deg)

# Create tone
t = np.arange(N) / sample_rate
f_tone = 2000
tx = np.exp(2j * np.pi * f_tone * t).reshape(1,-1)

# Define onset angles - here we're using just 1 
theta_deg = 45
theta1 = np.deg2rad(theta_deg)  # convert to radians

# Compute phase shifts for each microphone
phase_shifts = np.exp(-2j * np.pi * radius * np.sin(angles_rad - theta1))

# Construct signal vector
s1 = phase_shifts.reshape(-1, 1) 

# Combine for a single tone
r = s1 @ tx

# Add noise
n = np.random.randn(num_mics, N) + 1j*np.random.randn(num_mics, N)
r = r + 0.05*n

Plotting the signals looks like: signal at each mic

Then I'm using MUSIC to beamform the signal, and this is the code I've tried for that:

# For the MUSIC analysis
num_expected_signals = 1    

# Construct the covariance matrix 
R = r @ r.conj().T 

# Do eigenvalue decomposition
w, v = np.linalg.eig(R)

# Find and sort first order of magnitude of eigenvalues
eig_val_order = np.argsort(np.abs(w))
v = v[:, eig_val_order]

# Noise subspace is the rest of the eigenvalues
V = np.zeros((num_mics, num_mics - num_expected_signals), dtype=np.complex64) 

for i in range(num_mics - num_expected_signals):
    V[:, i] = v[:, i]

# Perform MUSIC analysis
# between -180 and +180
theta_scan = np.linspace(-1*np.pi, np.pi, 1000)

results = []

for theta_i in theta_scan:
    s = np.exp(-2j * np.pi * np.arange(num_mics) * np.sin(theta_i)).reshape(-1, 1)
    metric = 1 / (s.conj().T @ V @ V.conj().T @ s) 
    metric = np.abs(metric.squeeze())
    metric = 10 * np.log10(metric)
results -= np.max(results)

Which gives the following beam pattern. I know experimentally that the array can localise this kind of signal with high accuracy, so the additional lobes here feel incorrect.

Beam pattern with 4 major lobes

Glad to hear pointers on any part of the code !


1 Answer 1


My guess (it's before my coffee has kicked in) is that this line in the signal generation:

phase_shifts = np.exp(-2j * np.pi * radius * np.sin(angles_rad - theta1))

doesn't take account of the 2D location of the circular array microphone locations.

See, for example, this derivation which says that the steering vector needs to incorporate both $\sin$ and $\cos$ terms:

$$ ka\left(\cos(2\pi n/N ) \sin \theta \cos \phi + \sin(2\pi n/N ) \sin \theta \sin \phi\right) $$

(though I believe that equation (54) there incorrectly adds an extra $a$ in the second term).

I believe the steering vector in the code means the generated signals are assumed to come from a non-uniformly distributed array centered on the look direction.

Be gentle if my lack-of-coffee is causing AI-level hallucinations.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.