Generate a synthetic audio stem using ffmpeg's signal generators.
(ffmpeg: str, stem_type: str, output: Path)
| 158 | |
| 159 | |
| 160 | def generate_synthetic_stem(ffmpeg: str, stem_type: str, output: Path) -> None: |
| 161 | """Generate a synthetic audio stem using ffmpeg's signal generators.""" |
| 162 | if stem_type == "voice_male": |
| 163 | # Synthetic male voice: F0=130Hz with formants at F1=700Hz, F2=1200Hz |
| 164 | # Uses multiple harmonics shaped by formant envelopes |
| 165 | expr_parts: list[str] = [] |
| 166 | f0 = 130.0 |
| 167 | for h in range(1, 30): |
| 168 | freq = f0 * h |
| 169 | if freq > SAMPLE_RATE / 2: |
| 170 | break |
| 171 | # Natural 1/h rolloff |
| 172 | amp = 1.0 / h |
| 173 | # Gaussian formant shaping (F1=700, F2=1200, F3=2600) |
| 174 | import math |
| 175 | |
| 176 | f1_gain = math.exp(-0.5 * ((freq - 700) / 150) ** 2) |
| 177 | f2_gain = math.exp(-0.5 * ((freq - 1200) / 200) ** 2) |
| 178 | f3_gain = math.exp(-0.5 * ((freq - 2600) / 250) ** 2) |
| 179 | formant = max(f1_gain, f2_gain, f3_gain, 0.02) |
| 180 | final_amp = amp * formant * 0.3 |
| 181 | expr_parts.append(f"{final_amp:.4f}*sin(2*PI*{freq:.1f}*t)") |
| 182 | expr = "+".join(expr_parts) |
| 183 | # Add slight amplitude modulation for naturalness |
| 184 | expr = f"({expr})*( 0.8 + 0.2*sin(2*PI*5.5*t) )" |
| 185 | run_ffmpeg( |
| 186 | ffmpeg, |
| 187 | [ |
| 188 | "-f", |
| 189 | "lavfi", |
| 190 | "-i", |
| 191 | f"aevalsrc={expr}:s={SAMPLE_RATE}:d={DURATION_S}", |
| 192 | "-ac", |
| 193 | str(CHANNELS), |
| 194 | "-c:a", |
| 195 | "libvorbis", |
| 196 | "-b:a", |
| 197 | OGG_BITRATE, |
| 198 | str(output), |
| 199 | ], |
| 200 | f"Synthesizing {stem_type}", |
| 201 | ) |
| 202 | |
| 203 | elif stem_type == "voice_female": |
| 204 | # Synthetic female voice: F0=220Hz with formants at F1=800, F2=1500 |
| 205 | expr_parts: list[str] = [] |
| 206 | f0 = 220.0 |
| 207 | for h in range(1, 20): |
| 208 | freq = f0 * h |
| 209 | if freq > SAMPLE_RATE / 2: |
| 210 | break |
| 211 | amp = 1.0 / h |
| 212 | import math |
| 213 | |
| 214 | f1_gain = math.exp(-0.5 * ((freq - 800) / 160) ** 2) |
| 215 | f2_gain = math.exp(-0.5 * ((freq - 1500) / 220) ** 2) |
| 216 | f3_gain = math.exp(-0.5 * ((freq - 2800) / 280) ** 2) |
| 217 | formant = max(f1_gain, f2_gain, f3_gain, 0.02) |
no test coverage detected