vapylib 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vapylib-0.1.0/LICENSE +1 -0
- vapylib-0.1.0/PKG-INFO +13 -0
- vapylib-0.1.0/README.md +16 -0
- vapylib-0.1.0/pyproject.toml +12 -0
- vapylib-0.1.0/setup.cfg +4 -0
- vapylib-0.1.0/vapylib/__init__.py +1 -0
- vapylib-0.1.0/vapylib/parser.py +27 -0
- vapylib-0.1.0/vapylib/synthesizer.py +107 -0
- vapylib-0.1.0/vapylib.egg-info/PKG-INFO +13 -0
- vapylib-0.1.0/vapylib.egg-info/SOURCES.txt +11 -0
- vapylib-0.1.0/vapylib.egg-info/dependency_links.txt +1 -0
- vapylib-0.1.0/vapylib.egg-info/requires.txt +5 -0
- vapylib-0.1.0/vapylib.egg-info/top_level.txt +1 -0
vapylib-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
my vim doesnt have clipboard support so i cant copy gplv3 license text to here but it's a strong copyleft license. you can edit this program however you want, but you have to publish it with the same license.
|
vapylib-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vapylib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: a python voice synthesis lib
|
|
5
|
+
License-Expression: GPL-3.0-only
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: scipy
|
|
10
|
+
Requires-Dist: sounddevice
|
|
11
|
+
Requires-Dist: soundfile
|
|
12
|
+
Requires-Dist: PyYAML
|
|
13
|
+
Dynamic: license-file
|
vapylib-0.1.0/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# What is VAPyLib?
|
|
2
|
+
VAPyLib (**V**oice **A**cting **Py**thon **Lib**rary) is a library that utilizes sine waves and filters to create singing voices using formant synthesis.
|
|
3
|
+
# How to make songs for VAPyLib?
|
|
4
|
+
To make a song, you need to create a `.yaml` or `.yml` file, and parse the song using the `parse_song(filepath)` function. Note that pitch can only be between `0.0` and `1.0`
|
|
5
|
+
### Example `song.yaml`:
|
|
6
|
+
```yaml
|
|
7
|
+
version: 1
|
|
8
|
+
syllables:
|
|
9
|
+
- "ma"
|
|
10
|
+
- "na"
|
|
11
|
+
pitch_maps:
|
|
12
|
+
- 0.5,
|
|
13
|
+
- 0.7
|
|
14
|
+
```
|
|
15
|
+
## Note:
|
|
16
|
+
VAPyLib is version 0.1.0, but put **1** instead.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
[project]
|
|
5
|
+
license = "GPL-3.0-only"
|
|
6
|
+
name = "vapylib"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "a python voice synthesis lib"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = ["numpy", "scipy", "sounddevice", "soundfile", "PyYAML"]
|
|
11
|
+
[tool.setuptools]
|
|
12
|
+
packages = ["vapylib"]
|
vapylib-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .parser import parse_song
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
from . import synthesizer
|
|
3
|
+
class Song:
|
|
4
|
+
def __init__(self, syllables, pitch_maps):
|
|
5
|
+
self.syllables = syllables
|
|
6
|
+
self.pitch_maps = pitch_maps
|
|
7
|
+
def play(self):
|
|
8
|
+
synthesizer.play(self)
|
|
9
|
+
def save(self, filename):
|
|
10
|
+
synthesizer.save_wav(self, filename)
|
|
11
|
+
def parse_song(filepath):
|
|
12
|
+
with open(filepath, "r", encoding="utf8") as file:
|
|
13
|
+
data = yaml.safe_load(file)
|
|
14
|
+
if data.get("version") != 1:
|
|
15
|
+
raise ValueError("unsupported vapylib version")
|
|
16
|
+
syllables = data.get("syllables")
|
|
17
|
+
pitch_maps = data.get("pitch_maps")
|
|
18
|
+
if not isinstance(syllables, list):
|
|
19
|
+
raise ValueError("'syllables' must be a list")
|
|
20
|
+
if not isinstance(pitch_maps, list):
|
|
21
|
+
raise ValueError("'pitch_maps' must be a list")
|
|
22
|
+
if len(syllables) != len(pitch_maps):
|
|
23
|
+
raise ValueError("syllables and pitch_maps need to have equal amount of values")
|
|
24
|
+
for pitch in pitch_maps:
|
|
25
|
+
if not isinstance(pitch, (int, float)) or not 0 <= pitch <= 1:
|
|
26
|
+
raise ValueError(f"not good pitch ({pitch}), needs to be int or float and be between 0 and 1")
|
|
27
|
+
return Song(syllables, pitch_maps)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy import signal
|
|
3
|
+
import sounddevice as sd
|
|
4
|
+
import soundfile as sf
|
|
5
|
+
def _pitch_to_frequency(pitch, min_hz=100, max_hz=800):
|
|
6
|
+
return min_hz + pitch * (max_hz - min_hz)
|
|
7
|
+
_phonemes_list = [
|
|
8
|
+
"a", "e", "i", "o", "u",
|
|
9
|
+
"f", "s", "m", "n"
|
|
10
|
+
]
|
|
11
|
+
def _split_syllable(syllable):
|
|
12
|
+
phonemes = sorted(_phonemes_list, key=len, reverse=True)
|
|
13
|
+
result = []
|
|
14
|
+
position = 0
|
|
15
|
+
while position < len(syllable):
|
|
16
|
+
for phoneme in phonemes:
|
|
17
|
+
if syllable.startswith(phoneme, position):
|
|
18
|
+
result.append(phoneme)
|
|
19
|
+
position += len(phoneme)
|
|
20
|
+
break
|
|
21
|
+
else:
|
|
22
|
+
raise ValueError(f"not known phoneme at {syllable[position:]}")
|
|
23
|
+
return result
|
|
24
|
+
#a = 800 1200 2300
|
|
25
|
+
#e = 500 1500 2600
|
|
26
|
+
#i = 250 2750 3195
|
|
27
|
+
#o = 515 960 2695
|
|
28
|
+
#u = 255 1200 1935
|
|
29
|
+
_vowels = {
|
|
30
|
+
"a": [800, 1200, 2300],
|
|
31
|
+
"e": [500, 1500, 2600],
|
|
32
|
+
"i": [250, 2750, 3195],
|
|
33
|
+
"o": [515, 960, 2695],
|
|
34
|
+
"u": [255, 1200, 1935]
|
|
35
|
+
}
|
|
36
|
+
_fricatives = {
|
|
37
|
+
"f": {
|
|
38
|
+
"low": 1000,
|
|
39
|
+
"high": 8000
|
|
40
|
+
},
|
|
41
|
+
"s": {
|
|
42
|
+
"low": 4000,
|
|
43
|
+
"high": 12000
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
_nasals = {
|
|
47
|
+
"m": [250, 1000, 2200],
|
|
48
|
+
"n": [300, 1500, 2500]
|
|
49
|
+
}
|
|
50
|
+
_nasal_antiresonances = {
|
|
51
|
+
"m": [750, 1800],
|
|
52
|
+
"n": [1200, 2500]
|
|
53
|
+
}
|
|
54
|
+
def _apply_resonances(source, frequencies, sample_rate):
|
|
55
|
+
result = source
|
|
56
|
+
for frequency in frequencies:
|
|
57
|
+
if frequency >= sample_rate / 2:
|
|
58
|
+
continue
|
|
59
|
+
b, a = signal.iirpeak(frequency, Q=4, fs=sample_rate)
|
|
60
|
+
result = signal.lfilter(b, a, result)
|
|
61
|
+
return result
|
|
62
|
+
def _apply_antiresonances(source, frequencies, sample_rate):
|
|
63
|
+
result = source
|
|
64
|
+
for frequency in frequencies:
|
|
65
|
+
if frequency >= sample_rate / 2:
|
|
66
|
+
continue
|
|
67
|
+
b, a = signal.iirnotch(frequency, Q=4, fs=sample_rate)
|
|
68
|
+
result = signal.lfilter(b, a, result)
|
|
69
|
+
def _generate_source(pitch, sample_rate=44100):
|
|
70
|
+
frequency = _pitch_to_frequency(pitch)
|
|
71
|
+
sample_count = int(sample_rate * 0.5)
|
|
72
|
+
t = np.arange(sample_count) / sample_rate
|
|
73
|
+
return np.sin(2 * np.pi * frequency * t)
|
|
74
|
+
def _generate_vowel(phoneme, pitch, sample_rate=44100):
|
|
75
|
+
source = _generate_source(pitch, 0.5, sample_rate)
|
|
76
|
+
return _apply_resonances(source, _vowels[phoneme], sample_rate)
|
|
77
|
+
def _generate_fricative(phoneme, pitch, sample_rate=44100):
|
|
78
|
+
sample_count = int(sample_rate * 0.5)
|
|
79
|
+
source = np.random.normal(0, 1, sample_count)
|
|
80
|
+
return _apply_resonances(source, _fricatives[phoneme], sample_rate)
|
|
81
|
+
def _generate_nasal(phoneme, pitch, sample_rate=44100):
|
|
82
|
+
source = _generate_source(pitch, 0.5, sample_rate)
|
|
83
|
+
return _apply_antiresonances(_apply_resonances(source, _nasals[phoneme], sample_rate), _nasal_antiresonances[phoneme], sample_rate)
|
|
84
|
+
def _generate_audio(song, sample_rate=44100):
|
|
85
|
+
audio = []
|
|
86
|
+
for syllable, pitch in zip(song.syllables, song.pitch_maps):
|
|
87
|
+
syllable_audio = []
|
|
88
|
+
for phoneme in _split_syllable(syllable):
|
|
89
|
+
if phoneme in _vowels:
|
|
90
|
+
sound = _generate_vowel(phoneme, pitch, sample_rate=sample_rate)
|
|
91
|
+
elif phoneme in _fricatives:
|
|
92
|
+
sound = _generate_fricative(phoneme, pitch, sample_rate=sample_rate)
|
|
93
|
+
elif phoneme in _nasals:
|
|
94
|
+
sound = _generate_nasal(phoneme, pitch, sample_rate=sample_rate)
|
|
95
|
+
else:
|
|
96
|
+
continue
|
|
97
|
+
syllable_audio.append(sound)
|
|
98
|
+
if syllable_audio:
|
|
99
|
+
audio.extend(np.concatenate(syllable_audio))
|
|
100
|
+
return np.array(audio, dtype=np.float32)
|
|
101
|
+
def play(song):
|
|
102
|
+
audio = _generate_audio(song)
|
|
103
|
+
sd.play(audio, samplerate=44100)
|
|
104
|
+
sd.wait()
|
|
105
|
+
def save_wav(song, filename):
|
|
106
|
+
audio = _generate_audio(song)
|
|
107
|
+
sf.write(filename, audio, 44100)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vapylib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: a python voice synthesis lib
|
|
5
|
+
License-Expression: GPL-3.0-only
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: scipy
|
|
10
|
+
Requires-Dist: sounddevice
|
|
11
|
+
Requires-Dist: soundfile
|
|
12
|
+
Requires-Dist: PyYAML
|
|
13
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
vapylib/__init__.py
|
|
5
|
+
vapylib/parser.py
|
|
6
|
+
vapylib/synthesizer.py
|
|
7
|
+
vapylib.egg-info/PKG-INFO
|
|
8
|
+
vapylib.egg-info/SOURCES.txt
|
|
9
|
+
vapylib.egg-info/dependency_links.txt
|
|
10
|
+
vapylib.egg-info/requires.txt
|
|
11
|
+
vapylib.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vapylib
|