Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

moving sound source #335

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions pyroomacoustics/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -2080,6 +2080,115 @@ def add_microphone_array(self, mic_array, directivity=None):

return self.add(mic_array)

def simulate_moving_sound(
self,
position,
signal=None,
delay=0,
fs=None,
stept=200,
speed=5,
x_direction=True,
y_direction=True,
):
"""
Adds a moving sound source given by its position in the room.
Simulate all the locations the sound source is moving to
determining the RIR.
Perform time-varying convolution on the output signal.

Parameters
-----------
position: ndarray, shape: (2,) or (3,)
The starting location of the moving source in the room.
signal: ndarray, shape: (n_samples,), optional
The signal played by the source.
delay: float, optional
A time delay until the source signal starts
in the simulation.
fs: float, optional
The sampling frequency of the microphone, if different from that of the room.
stept: float, (The default is 200).
The step duration, measured in milliseconds.
It is used to determine the duration of each simulation
step in the virtual room.
speed: float, (The default is 5).
Speed of the moving sound source in the virtual room.
It is measured in meters per second.
x_direction, y_direction: bool, optional
Flags indicating the direction of movement. True for forward, False for backward.

Returns
-------
movemix: ndarray
audio signal obtained from the simulation.
filter_kernels: ndarray
Room impulse responses at each step.
"""

# Validate parameters

if signal is None:
raise ValueError("Please provide a signal for the sound source.")

if fs is None or fs <= 0:
raise ValueError("Invalid sampling frequency (fs).")

# Calculate the number of samples in each step
stepn = int(stept * fs / 1000)
# Calculate the distance traveled in a step
stepd = speed * stept / 1000
# The number of simulation steps needed to process the entire audio signal.
n = int(len(signal) / stepn)
# Store RIR of the simulated audio
# Initialize lists to store results
movemix_list = []
movemix = np.array([])
filter_kernels = np.array([])

for i in range(n):
# Reshape position to handle both (2,) and (2, 1) cases
position = np.array(position).reshape(-1)

# Update the position of the sound source based on movement direction
if x_direction:
x_position = [position[0] + stepd * i, position[1]]
else:
x_position = [position[0] - stepd * i, position[1]]

# Update the position of the sound source based on movement direction
if y_direction:
y_position = [position[0], position[1] + stepd * i]
else:
y_position = [position[0], position[1] - stepd * i]

# Create a numpy array for the source location
source_location = np.array([x_position[0], y_position[1]])
self.sources = []
self.add_source(
position=source_location,
delay=delay,
signal=signal[i * stepn : (i + 1) * stepn],
)
# Simulate the room and obtain the room impulse response
self.simulate()

# only use the duration of original, to avoid echo
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the time varying convolution, the tail (i.e., the samples beyond the original length) need to be kept and added to the head of the following segment.
Kind of like overlap add for long filtering (except the filter changes everytime).
You can think about it this way: after the source has moved, you still hear echoes generated by the source at the previous position.

# set data type as 16bit, ref:https://docs.scipy.org/doc/scipy/reference/generated/scipy.io.wavfile.write.html
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not do this here.
The scaling, normalization, and data type casting should happen only when saving to file, which is independent from the simulation.

record_ = self.mic_array.signals[:, :stepn].astype("int16")

if i == 0:
movemix = record_
filter_kernels = self.rir.copy()
else:
movemix = np.concatenate((movemix, record_), axis=1)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very inefficient as a copy of the array occurs at every loop, and at each loop the array becomes longer. In the end this incurs a cost quadratic in the length of the array.
The correct way to do it is to append to a list in the loop and concatenate all the bits at once at the end.

filter_kernels = np.concatenate(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

(filter_kernels, self.rir.copy()), axis=1
)

return movemix, filter_kernels


def add_source(self, position, signal=None, delay=0, directivity=None):
"""
Adds a sound source given by its position in the room. Optionally
Expand Down
42 changes: 42 additions & 0 deletions pyroomacoustics/tests/test_moving_sound.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import numpy as np

import pyroomacoustics as pra


def test_simulation_output_shape():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good as a basic test, but it would be nice to have some tests for the correctness of the time-varying convolution too.
One test for that is that the result of a time-varying convolution where all the filters are the same should be the same as a single long convolution.

# Set up input parameters for the test
position = np.array([0, 0])
signal = np.random.rand(44100) # Replace with an actual signal
fs = 44100
stept = 200
speed = 5
x_direction = True
y_direction = True

# Create an instance of the Room class
room = pra.ShoeBox([100, 100]) # Adjust the room dimensions as needed

# Microphone parameters
# Two microphones at different locations
mic_locations = np.array([[1, 1], [3, 1]])
room.add_microphone_array(pra.MicrophoneArray(mic_locations, room.fs))

# Set the source location as a 1D or 2D array with the correct shape
position = np.array([[2, 3]]) # 2D array with shape (1, 2)

# Call the simulate_moving_sound function
movemix, filter_kernels = room.simulate_moving_sound(
position=position,
signal=signal,
fs=fs,
stept=stept,
speed=speed,
x_direction=x_direction,
y_direction=y_direction,
)

print(movemix, filter_kernels)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be some assert at the end of the test that fails is something is wrong with the code.



if __name__ == "__main__":
test_simulation_output_shape()
Loading