Skip to content
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