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

Add room generator. #113

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Added
- Acoustic properties of different materials in ``pyroomacoustics.materials``
- Scattering from the wall is handled via ray tracing method, scattering coefficients are provided
in ``pyroomacoustics.materials.Material`` objects
- Room generator in ``pyroomacoustics.datasets.room``


Changed
Expand Down
7 changes: 7 additions & 0 deletions docs/pyroomacoustics.datasets.distribution.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Distribution Utilities
============================================

.. automodule:: pyroomacoustics.datasets.distribution
:members:
:undoc-members:
:show-inheritance:
10 changes: 10 additions & 0 deletions docs/pyroomacoustics.datasets.room.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Room Generation Utilities
====================================

See ``examples/generate_room_dataset.py`` for an example on generating
a dataset of randomly sampled room.

.. automodule:: pyroomacoustics.datasets.room
:members:
:undoc-members:
:show-inheritance:
2 changes: 2 additions & 0 deletions docs/pyroomacoustics.datasets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ Tools and Helpers

pyroomacoustics.datasets.base
pyroomacoustics.datasets.utils
pyroomacoustics.datasets.distribution
pyroomacoustics.datasets.room

191 changes: 191 additions & 0 deletions examples/generate_room_dataset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import numpy as np
import json
import click
import os
from glob import glob
from pprint import pprint
import random
import soundfile as sf

from pyroomacoustics.utilities import rms, sample_audio
from pyroomacoustics.datasets.room import ShoeBoxRoomGenerator


"""

Example script for:

1) Generating a dataset of random room configuration and saving their
corresponding room impulse responses.
```
python examples/generate_room_dataset.py make_dataset
```

2) Randomly selecting a room from the dataset and applying its room impulse
responses to a randomly selected speech file and (depending on the selected
room) some noise sources.
```
python examples/generate_room_dataset.py apply_rir \
--room_dataset <ROOM_DATASET_PATH>
```

"""

example_noise_files = [
'examples/input_samples/doing_the_dishes.wav',
'examples/input_samples/exercise_bike.wav',
'examples/input_samples/running_tap.wav',
]


@click.group()
def main():
pass


@main.command('make_dataset')
@click.option('--n_rooms', type=int, default=50)
def make_dataset(n_rooms):
"""

Generate a dataset of room impulse responses. A new folder will be created
with the name `pra_room_dataset_<TIMESTAMP>` with the following structure:

```
pra_room_dataset_<TIMESTAMP>/
room_metadata.json
data/
room_<uuid>.npz
room_<uuid>.npz
...
```

where `room_metadata.json` contains metadata about each room configuration
in the `data` folder.

The `apply_rir` functions shows a room can be selected at random in order
to simulate a measurement in one of the randomly generated configurations.


Parameters
-----------
n_rooms : int
Number of room configurations to generate.
"""

room_generator = ShoeBoxRoomGenerator()
room_generator.create_dataset(n_rooms)


@main.command('apply_rir')
@click.option('--room_dataset', type=str, default=None)
@click.option('--target_speech', type=str,
default='examples/input_samples/cmu_arctic_us_aew_a0001.wav')
@click.option('--noise_dir', type=str, default=None)
@click.option('--snr_db', type=float, default=5.)
@click.option('--output_file', type=str, default='simulated_output.wav')
def apply_rir(room_dataset, target_speech, noise_dir, snr_db, output_file):
"""

Randomly selecting a room from the dataset and applying its room impulse
responses to a randomly selected speech file and (depending on the selected
room) some noise sources.

Parameters
-----------
room_dataset : str
Path to room dataset from calling `make_dataset`.
target_speech : str
Path to a target speech WAV file.
noise_dir : str
Path to a directory with noise WAV files. Default is to apply the room
impulse response to WAV file(s) from `examples/input_samples`.
snr_db : float
Desired signal-to-noise ratio resulting from simulation.
output_file : str
Path of output WAV file from simulation.

"""
if room_dataset is None:
raise ValueError('Provide a path to a room dataset. You can compute '
'one with the `make_dataset` command.')

with open(os.path.join(room_dataset, 'room_metadata.json')) as json_file:
room_metadata = json.load(json_file)

# pick a room at random
random_room_key = random.choice(list(room_metadata.keys()))
_room_metadata = room_metadata[random_room_key]
print('Room metadata')
pprint(_room_metadata)

# load target audio
target_data, fs_target = sf.read(target_speech)

# load impulse responses
ir_file = os.path.join(room_dataset, 'data', _room_metadata['file'])
ir_data = np.load(ir_file)
n_noises = ir_data['n_noise']
sample_rate = ir_data['sample_rate']
assert sample_rate == fs_target, 'Target sampling rate does not match IR' \
'sampling rate.'

# apply target IR
target_ir = ir_data['target_ir']
n_mics, ir_len = target_ir.shape
output_len = ir_len + len(target_data) - 1
room_output = np.zeros((n_mics, output_len))
for n in range(n_mics):
room_output[n] = np.convolve(target_data, target_ir[n])

# apply noise IR(s) if applicable
if n_noises:

if noise_dir is None:
noise_files = example_noise_files
else:
noise_files = glob(os.path.join(noise_dir, '*.wav'))
print('\nNumber of noise files : {}'.format(len(noise_files)))

_noise_files = np.random.choice(noise_files, size=n_noises,
replace=False)
print('Selected noise file(s) : {}'.format(_noise_files))
noise_output = np.zeros_like(room_output)
for k, _file in enumerate(_noise_files):

# load audio
noise_data, fs_noise = sf.read(_file)
assert fs_noise == sample_rate, 'Noise sampling rate {} does ' \
'not match IR sampling rate.' \
''.format(_file)

# load impulse response
noise_ir = ir_data['noise_ir_{}'.format(k)]

# sample segment of noise and normalize so each source has
# roughly similar amplitude
# take a bit more audio than target audio so we are sure to fill
# up the end with noise (end of IR is sparse)
_noise = sample_audio(noise_data, int(1.1*output_len))
_noise /= _noise.max()

# apply impulse response
for n in range(n_mics):
noise_output[n] = np.convolve(_noise, noise_ir[n])[:output_len]

# rescale noise according to specified SNR, add to target signal
noise_rms = rms(noise_output[0])
signal_rms = rms(room_output[0])
noise_fact = signal_rms / noise_rms * 10 ** (-snr_db / 20.)
room_output += (noise_output * noise_fact)

else:
print('\nNo noise source in selected room!')

# write output to file
sf.write(output_file, np.squeeze(room_output), sample_rate)
print('\nOutput written to : {}'.format(output_file))


if __name__ == '__main__':
main()
4 changes: 3 additions & 1 deletion examples/input_samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ corpus and are included for testing purposes.
* cmu_arctic_us_aew_a0003.wav
* cmu_arctic_us_axb_a0005.wav

The following noise sample was taken from Google's [Speech Commands Dataset](https://research.googleblog.com/2017/08/launching-speech-commands-dataset.html)
The following noise samples were taken from Google's [Speech Commands Dataset](https://research.googleblog.com/2017/08/launching-speech-commands-dataset.html)

* doing_the_dishes.wav
* exercise_bike.wav
* running_tap.wav

The following two samples are from unknown origin and were found online.

Expand Down
Binary file added examples/input_samples/exercise_bike.wav
Binary file not shown.
Binary file added examples/input_samples/running_tap.wav
Binary file not shown.
3 changes: 3 additions & 0 deletions pyroomacoustics/datasets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,6 @@
from .timit import Word, Sentence, TimitCorpus
from .cmu_arctic import CMUArcticCorpus, CMUArcticSentence, cmu_arctic_speakers
from .google_speech_commands import GoogleSpeechCommands, GoogleSample
from .room import ShoeBoxRoomGenerator
from .distribution import UniformDistribution, MultiUniformDistribution, \
DiscreteDistribution, MultiDiscreteDistribution
140 changes: 140 additions & 0 deletions pyroomacoustics/datasets/distribution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Utility functions generating a dataset of room impulse responses.
# Copyright (C) 2019 Eric Bezzam
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# You should have received a copy of the MIT License along with this program. If
# not, see <https://opensource.org/licenses/MIT>.

from abc import ABCMeta, abstractmethod
import numpy as np


class Distribution(metaclass=ABCMeta):
"""

Abstract class for distributions.

"""
@abstractmethod
def __init__(self):
pass

@abstractmethod
def sample(self):
pass


class UniformDistribution(Distribution):
"""

Create a uniform distribution between two values.

Parameters
-------------
vals_range : tuple / list
Tuple or list of two values, (lower bound, upper bound).

"""
def __init__(self, vals_range):
super(UniformDistribution, self).__init__()
assert len(vals_range) == 2, 'Length of `vals_range` must be 2.'
assert vals_range[0] <= vals_range[1], '`vals_range[0]` must be ' \
'less than or equal to ' \
'`vals_range[1]`.'
self.vals_range = vals_range

def sample(self):
return np.random.uniform(self.vals_range[0], self.vals_range[1])


class MultiUniformDistribution(Distribution):
"""

Sample from multiple uniform distributions.

Parameters
------------
ranges : list of tuples / lists
List of tuples / lists, each with two values.

"""
def __init__(self, ranges):
super(MultiUniformDistribution, self).__init__()
self.distributions = [UniformDistribution(r) for r in ranges]

def sample(self):
return [d.sample() for d in self.distributions]


class DiscreteDistribution(Distribution):
"""

Create a discrete distribution which samples from a given set of values
and (optionally) a given set of probabilities.

Parameters
------------
values : list
List of values to sample from.
prob : list
Corresponding list of probabilities. Default to equal probability for
all values.

"""
def __init__(self, values, prob=None):
super(DiscreteDistribution, self).__init__()
if prob is None:
prob = np.ones_like(values)
assert len(values) == len(prob), \
'len(values)={}, len(prob)={}'.format(len(values), len(prob))
self.values = values
self.prob = np.array(prob) / float(sum(prob))

def sample(self):
return np.random.choice(self.values, p=self.prob)


class MultiDiscreteDistribution(Distribution):
"""

Sample from multiple discrete distributions.

Parameters
------------
ranges : list of tuples / lists
List of tuples / lists, each with two values.

"""
def __init__(self, values_list, prob_list=None):
super(MultiDiscreteDistribution, self).__init__()
if prob_list is not None:
assert len(values_list) == len(prob_list), \
'Lengths of `values_list` and `prob_list` must match.'
else:
prob_list = [None] * len(values_list)
self.distributions = [
DiscreteDistribution(values=tup[0], prob=tup[1])
for tup in zip(values_list, prob_list)
]

def sample(self):
return [d.sample() for d in self.distributions]


Loading