Skip to content
Merged
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "mbrola"
version = "0.3.28"
version = "0.4.0"
description = "A Python front-end for the MBROLA speech synthesizer"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
91 changes: 84 additions & 7 deletions src/mbrola.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ class MBROLA:
outer_silences (Sequence[int, int], optional): duration in milliseconds of the silence interval to be inserted at onset and offset. Defaults to (1, 1).

Examples:
>>> house = mb.MBROLA(
phonemes = ["h", "a", "U", "s"],
>>> house = MBROLA(
phon = ["h", "a", "U", "s"],
durations = 100,
pitch = 200
)
Expand All @@ -45,6 +45,8 @@ def __init__(
pitch: utils.PITCH_TYPE_INPUT = 200,
outer_silences: tuple[int, int] = (1, 1),
):
"""Initiate MBROLA instance."""

if isinstance(phon, str):
if len(phon) > 1:
phon = list(phon)
Expand All @@ -57,26 +59,93 @@ def __init__(
self.outer_silences = utils._validate_outer_silences(outer_silences)
self.pho = _make_pho(self)

def __len__(self):
def __len__(self) -> int:
"""Get number of phonemes in MBROLA instance.

Returns:
int: Number of phonemes in MBROLA instance.

Examples:
>>> house = MBROLA(phon = ["h", "a", "U", "s"])
>>> len(house)
4
"""
return len(self.phon)

def __eq__(self, other):
def __eq__(self, other) -> bool:
"""Check if two MBROLA instances are equal.

Args:
other (MBROLA): Another MBROLA instance to compare.
Returns:
bool: True if both MBROLA instances are equal.

Examples:
>>> house = MBROLA(phon = ["h", "a", "U", "s"])
>>> house_1 = MBROLA(phon = ["h", "a", "U", "s"])
>>> house==house_1
True
>>> house = MBROLA(phon = ["h", "a", "U", "s"])
>>> caffe = MBROLA(phon = ["k", "a", "f", "f", "E"])
>>> house_0==house_1
False
>>> house = MBROLA(phon = ["h", "a", "U", "s"])
>>> house_300 = MBROLA(phon = ["h", "a", "U", "s"], duration=300)
>>> house==house_300
True
"""
return self.pho == other.pho

def __add__(self, other):
"""Concatenate phonemes from two MBROLA instances.

Args:
other (MBROLA): Another MBROLA instance to concatenate.

Returns:
MBROLA: Concatenated MBROLA instance.

Examples:
>>> house = MBROLA(phon = ["h", "a", "U", "s"])
>>> len(house)
4
>>> house_1 = MBROLA(phon = ["h", "a", "U", "s"])
>>> len(house_1)
4
>>> len(house + house_1)
8
"""
new = self.copy()
new.phon = self.phon + other.phon
new.pho = self.pho + other.pho

return new

def copy(self):
"""Make deep copy of MBROLA instance.

Returns:
MBROLA: Deep copy of original MBROLA instance.

Examples:
>>> house = MBROLA(phon = ["h", "a", "U", "s"])
>>> house_1 = house.copy()
>>> house == house_1
True
>>> house is house_1
False
"""
return deepcopy(self)

def export_pho(self, file: str | Path) -> None:
"""Save PHO file.

Args:
file (str): Path of the output PHO file.

Examples:
>>> house = MBROLA(phon = ["h", "a", "U", "s"])
>>> house.export_pho("sample.pho")
"""
with Path(file).open("w", encoding="utf-8") as f:
f.write("\n".join(self.pho))
Expand All @@ -88,7 +157,7 @@ def make_sound(
f0_ratio: float = 1.0,
dur_ratio: float = 1.0,
remove_pho: bool = True,
):
) -> None:
"""Generate MBROLA sound WAV file.

Args:
Expand All @@ -97,13 +166,21 @@ def make_sound(
f0_ratio (float, optional): Constant to multiply the fundamental frequency of the whole sound by. Defaults to 1.0 (same fundamental frequency).
dur_ratio (float, optional): Constant to multiply the duration of the whole sound by. Defaults to 1.0 (same duration).
remove_pho (bool, optional): Should the intermediate PHO file be deleted after the sound is created? Defaults to True.

Examples:
>>> house = MBROLA(phon = ["h", "a", "U", "s"])
>>> house.make_sound("sound.wav", voice="en1")
>>> house.make_sound("sound.wav", f0_ratio=0.5, voice="en1") # reduce F0 to half the original Hz.
>>> house.make_sound("sound.wav", dur_ratio=2.0, voice="en1") # make audio double as fast
>>> house.make_sound("sound.wav", remove_pho=False, voice="en1") # keep pho file in same directory
"""
pho = Path("tmp.pho")
file = Path(file)
pho = file.with_suffix(".pho")

with Path(pho).open(mode="w", encoding="utf-8") as f:
f.write("\n".join(self.pho))

cmd_str = f"{utils._mbrola_cmd()} -f {f0_ratio} -t {dur_ratio} /usr/share/mbrola/{voice}/{voice} {pho} {str(Path(file))}"
cmd_str = f"{utils._mbrola_cmd()} -f {f0_ratio} -t {dur_ratio} /usr/share/mbrola/{voice}/{voice} {pho} {str(file)}"

try:
sp.check_output(cmd_str, shell=True)
Expand Down
19 changes: 18 additions & 1 deletion tests/test_mbrola.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@

@pytest.fixture
def mb_fix():
"""MBROLA fixture instance."""
return mb.MBROLA(["k", "a", "f", "f", "E1"], 100, 200, (1, 1))


class TestAttr:
def test_mbrola(self):
"""Test MBROLA initialization."""
x = mb.MBROLA(["k", "a", "f", "f", "E1"], 100, 200, (1, 1))
assert isinstance(x, mb.MBROLA)

def test_string_phon(self):
"""Test MBROLA initialization with string of phonemes."""
phon = "kaffe"
x = mb.MBROLA(phon, 100, 200, (1, 1))
assert isinstance(x.phon, list)
Expand Down Expand Up @@ -53,22 +56,26 @@ def test_attr_type(self, mb_fix):
assert callable(mb_fix.make_sound)

def test_eq(self, mb_fix):
"""Test `==` method."""
mb2 = mb.MBROLA(["k", "a", "f", "f", "E1"], 100, 200, (1, 1))
assert mb_fix == mb2

def test_add(self, mb_fix):
"""Test `+` method."""
mb2 = mb.MBROLA(["k", "a", "f", "f", "E1"], 100, 200, (1, 1))
added = mb_fix + mb2
assert isinstance(added, mb.MBROLA)
assert len(added) == len(mb_fix) + len(mb2)
assert len(added.pho) == len(mb_fix.pho) + len(mb2.pho)

def test_copy(self, mb_fix):
"""Test copy method."""
copy = mb_fix.copy()
assert copy == mb_fix
assert copy is not mb_fix

def test_len(self, mb_fix):
"""Test len method."""
assert len(mb_fix) == 5
assert len(mb_fix) == len(mb_fix.phon)

Expand Down Expand Up @@ -121,7 +128,17 @@ def test_make_sound(self, mb_fix):
assert file.exists()
os.unlink(file)

def test_make_sound_remove_pho(self, mb_fix):
"""Test MBROLA.make_sound method."""
file = Path("tests", "mb_fix.wav")
mb_fix.make_sound(file=file, remove_pho=False)
assert file.exists()
assert file.with_suffix(".pho").exists()

os.unlink(file)

def test_sp_error(self, mb_fix):
with pytest.raises(RuntimeError):
"""Test that subprocess errors are raised."""
with pytest.raises(FileNotFoundError):
file = Path("bad_path", "mb_fix.wav")
mb_fix.make_sound(file=file)
Loading