Generate a synthetic vowel using additive synthesis with Gaussian formant envelopes Simplest physically-motivated voice model: harmonic series with 1/h rolloff shaped by two resonance peaks
| 225 | /// Generate a synthetic vowel using additive synthesis with Gaussian formant envelopes |
| 226 | /// Simplest physically-motivated voice model: harmonic series with 1/h rolloff shaped by two resonance peaks |
| 227 | inline Sample makeSyntheticVowel(float f0, float f1Freq, float f2Freq, |
| 228 | u32 timestamp, float amplitude = 16000.0f, |
| 229 | int count = 512, float sampleRate = 44100.0f) { |
| 230 | fl::vector<fl::i16> data(count, 0); |
| 231 | const float f1Bw = 150.0f; // F1 bandwidth |
| 232 | const float f2Bw = 200.0f; // F2 bandwidth |
| 233 | const float maxFreq = fl::min(4000.0f, sampleRate / 2.0f); |
| 234 | |
| 235 | for (int h = 1; h * f0 < maxFreq; ++h) { |
| 236 | float freq = f0 * h; |
| 237 | // Natural 1/h rolloff |
| 238 | float naturalAmp = 1.0f / static_cast<float>(h); |
| 239 | // Gaussian formant envelopes |
| 240 | float f1Gain = fl::expf(-0.5f * (freq - f1Freq) * (freq - f1Freq) / (f1Bw * f1Bw)); |
| 241 | float f2Gain = fl::expf(-0.5f * (freq - f2Freq) * (freq - f2Freq) / (f2Bw * f2Bw)); |
| 242 | float formantGain = fl::max(f1Gain, f2Gain); |
| 243 | float harmonicAmp = amplitude * naturalAmp * fl::max(0.05f, formantGain); |
| 244 | |
| 245 | for (int i = 0; i < count; ++i) { |
| 246 | float phase = 2.0f * FL_M_PI * freq * i / sampleRate; |
| 247 | data[i] += static_cast<fl::i16>(harmonicAmp * fl::sinf(phase)); |
| 248 | } |
| 249 | } |
| 250 | return Sample(data, timestamp); |
| 251 | } |
| 252 | |
| 253 | /// Generate a jittered vowel: synthetic vowel with per-period amplitude and timing jitter |
| 254 | /// Simulates real vocal cord irregularity — higher envelope jitter and lower autocorrelation |