1use std::f64::consts::{PI, TAU};
2
3use sim_lib_numbers_signal::{
4 Direction, SignalBuffer, SignalError, SignalView, SpectrumPacking, TransformKind,
5 TransformPlan, transform, unwrap_phase,
6};
7use sim_lib_sound_audio_lift::{AudioTransformError, StftPlan, stft};
8use thiserror::Error;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum PhaseLockPolicy {
13 Independent,
15 Identity {
17 radius_bins: usize,
19 },
20}
21
22#[derive(Clone, Copy, Debug, PartialEq)]
24pub enum TransientPolicy {
25 Smear,
27 Reset {
29 spectral_flux_threshold: f64,
31 },
32}
33
34#[derive(Clone, Copy, Debug, PartialEq)]
36pub enum PhaseUnwrapPolicy {
37 ExpectedAdvance {
39 discontinuity_radians: f64,
41 },
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum InstantaneousFrequencyPolicy {
47 BinCenter,
49 PhaseDerivative,
51}
52
53#[derive(Clone, Debug, PartialEq)]
55pub struct VocoderPolicy {
56 pub sample_rate_hz: u32,
58 pub stretch_ratio: f64,
60 pub pitch_ratio: f64,
62 pub stft: StftPlan,
64 pub phase_lock: PhaseLockPolicy,
66 pub transient: TransientPolicy,
68 pub phase_unwrap: PhaseUnwrapPolicy,
70 pub instantaneous_frequency: InstantaneousFrequencyPolicy,
72 pub max_output_samples: usize,
74}
75
76impl Default for VocoderPolicy {
77 fn default() -> Self {
78 Self {
79 sample_rate_hz: 48_000,
80 stretch_ratio: 1.0,
81 pitch_ratio: 1.0,
82 stft: StftPlan::default(),
83 phase_lock: PhaseLockPolicy::Identity { radius_bins: 3 },
84 transient: TransientPolicy::Reset {
85 spectral_flux_threshold: 0.35,
86 },
87 phase_unwrap: PhaseUnwrapPolicy::ExpectedAdvance {
88 discontinuity_radians: PI,
89 },
90 instantaneous_frequency: InstantaneousFrequencyPolicy::PhaseDerivative,
91 max_output_samples: 16_777_216,
92 }
93 }
94}
95
96#[derive(Clone, Debug, PartialEq)]
98pub struct PhaseVocoderReport {
99 pub samples: Vec<f32>,
101 pub input_samples: usize,
103 pub output_samples: usize,
105 pub analysis_frames: usize,
107 pub synthesis_hop: f64,
109 pub transient_resets: usize,
111 pub peak: f32,
113 pub clipped_samples: usize,
115 pub policy: VocoderPolicy,
117}
118
119#[derive(Clone, Debug, Error, PartialEq)]
121pub enum VocoderError {
122 #[error("invalid vocoder {field}: {reason}")]
124 InvalidPolicy {
125 field: &'static str,
127 reason: &'static str,
129 },
130 #[error("vocoder output needs {required} samples, exceeding {maximum}")]
132 OutputLimit {
133 required: usize,
135 maximum: usize,
137 },
138 #[error("vocoder output coordinate arithmetic overflowed")]
140 SizeOverflow,
141 #[error(transparent)]
143 Stft(#[from] AudioTransformError),
144 #[error(transparent)]
146 Signal(#[from] SignalError),
147}
148
149pub fn phase_vocode(
152 input: &[f32],
153 policy: VocoderPolicy,
154) -> Result<PhaseVocoderReport, VocoderError> {
155 validate_policy(&policy)?;
156 let output_samples = scaled_len(input.len(), policy.stretch_ratio)?;
157 if output_samples > policy.max_output_samples {
158 return Err(VocoderError::OutputLimit {
159 required: output_samples,
160 maximum: policy.max_output_samples,
161 });
162 }
163 if input.is_empty() {
164 return Ok(empty_report(policy));
165 }
166
167 let analysis = stft(input, policy.sample_rate_hz, &policy.stft)?;
168 let bins = policy.stft.frame / 2 + 1;
169 let synthesis_hop = policy.stft.hop as f64 * policy.stretch_ratio;
170 let synthesis_window = policy.stft.synthesis_window.generate(policy.stft.frame)?;
171 let analysis_window = policy.stft.analysis_window.generate(policy.stft.frame)?;
172 let synthesis_len =
173 synthesis_storage_len(analysis.frames.len(), synthesis_hop, policy.stft.frame)?;
174 let mut output = vec![0.0f64; synthesis_len];
175 let mut gain = vec![0.0f64; synthesis_len];
176 let mut previous_phase = vec![0.0f64; bins];
177 let mut synthesis_phase = vec![0.0f64; bins];
178 let mut previous_magnitude = vec![0.0f64; bins];
179 let mut transient_resets = 0usize;
180
181 for (frame_index, frame) in analysis.frames.iter().enumerate() {
182 let magnitudes = frame
183 .bins
184 .iter()
185 .map(|(real, imaginary)| real.hypot(*imaginary))
186 .collect::<Vec<_>>();
187 let phases = frame
188 .bins
189 .iter()
190 .map(|(real, imaginary)| imaginary.atan2(*real))
191 .collect::<Vec<_>>();
192 let reset =
193 frame_index > 0 && transient_reset(&magnitudes, &previous_magnitude, policy.transient);
194 transient_resets += usize::from(reset);
195
196 for bin in 0..bins {
197 if frame_index == 0 || reset {
198 synthesis_phase[bin] = phases[bin];
199 } else {
200 let expected = TAU * policy.stft.hop as f64 * bin as f64 / policy.stft.frame as f64;
201 let residual = unwrap_residual(
202 phases[bin] - previous_phase[bin] - expected,
203 policy.phase_unwrap,
204 )?;
205 let radians_per_sample = match policy.instantaneous_frequency {
206 InstantaneousFrequencyPolicy::BinCenter => {
207 TAU * bin as f64 / policy.stft.frame as f64
208 }
209 InstantaneousFrequencyPolicy::PhaseDerivative => {
210 (expected + residual) / policy.stft.hop as f64
211 }
212 };
213 synthesis_phase[bin] += radians_per_sample * synthesis_hop;
214 }
215 }
216 apply_phase_lock(
217 &magnitudes,
218 &phases,
219 &mut synthesis_phase,
220 policy.phase_lock,
221 );
222 let shifted = shift_bins(&magnitudes, &synthesis_phase, policy.pitch_ratio, bins);
223 overlap_add(
224 &shifted,
225 frame_index,
226 synthesis_hop,
227 &policy,
228 &analysis_window.samples,
229 &synthesis_window.samples,
230 &mut output,
231 &mut gain,
232 )?;
233 previous_phase.copy_from_slice(&phases);
234 previous_magnitude.copy_from_slice(&magnitudes);
235 }
236
237 let trim_start = scaled_len(analysis.left_padding, policy.stretch_ratio)?;
238 let samples = trim_output(&output, &gain, trim_start, output_samples);
239 let peak = samples.iter().copied().map(f32::abs).fold(0.0, f32::max);
240 let clipped_samples = samples.iter().filter(|sample| sample.abs() > 1.0).count();
241 Ok(PhaseVocoderReport {
242 samples,
243 input_samples: input.len(),
244 output_samples,
245 analysis_frames: analysis.frames.len(),
246 synthesis_hop,
247 transient_resets,
248 peak,
249 clipped_samples,
250 policy,
251 })
252}
253
254fn validate_policy(policy: &VocoderPolicy) -> Result<(), VocoderError> {
255 if policy.sample_rate_hz == 0 {
256 return Err(invalid("sample rate", "must be positive"));
257 }
258 if !policy.stretch_ratio.is_finite() || policy.stretch_ratio <= 0.0 {
259 return Err(invalid("stretch ratio", "must be finite and positive"));
260 }
261 if !policy.pitch_ratio.is_finite() || policy.pitch_ratio <= 0.0 {
262 return Err(invalid("pitch ratio", "must be finite and positive"));
263 }
264 if policy.max_output_samples == 0 {
265 return Err(invalid("output bound", "must be positive"));
266 }
267 if let PhaseLockPolicy::Identity { radius_bins } = policy.phase_lock
268 && radius_bins == 0
269 {
270 return Err(invalid("phase lock radius", "must be positive"));
271 }
272 if let TransientPolicy::Reset {
273 spectral_flux_threshold,
274 } = policy.transient
275 && (!spectral_flux_threshold.is_finite() || spectral_flux_threshold < 0.0)
276 {
277 return Err(invalid(
278 "transient threshold",
279 "must be finite and nonnegative",
280 ));
281 }
282 let PhaseUnwrapPolicy::ExpectedAdvance {
283 discontinuity_radians,
284 } = policy.phase_unwrap;
285 if !discontinuity_radians.is_finite() || !(PI..=TAU).contains(&discontinuity_radians) {
286 return Err(invalid(
287 "phase unwrap discontinuity",
288 "must lie between pi and one turn",
289 ));
290 }
291 Ok(())
292}
293
294fn scaled_len(len: usize, ratio: f64) -> Result<usize, VocoderError> {
295 let scaled = len as f64 * ratio;
296 if !scaled.is_finite() || scaled > usize::MAX as f64 {
297 return Err(VocoderError::SizeOverflow);
298 }
299 Ok(scaled.round() as usize)
300}
301
302fn synthesis_storage_len(frames: usize, hop: f64, frame_len: usize) -> Result<usize, VocoderError> {
303 let last = frames.saturating_sub(1) as f64 * hop;
304 if !last.is_finite() || last > usize::MAX as f64 {
305 return Err(VocoderError::SizeOverflow);
306 }
307 (last.round() as usize)
308 .checked_add(frame_len)
309 .ok_or(VocoderError::SizeOverflow)
310}
311
312fn transient_reset(current: &[f64], previous: &[f64], policy: TransientPolicy) -> bool {
313 let TransientPolicy::Reset {
314 spectral_flux_threshold,
315 } = policy
316 else {
317 return false;
318 };
319 let positive_flux = current
320 .iter()
321 .zip(previous)
322 .map(|(current, previous)| (current - previous).max(0.0))
323 .sum::<f64>();
324 let energy = current.iter().sum::<f64>().max(f64::EPSILON);
325 positive_flux / energy >= spectral_flux_threshold
326}
327
328fn unwrap_residual(residual: f64, policy: PhaseUnwrapPolicy) -> Result<f64, SignalError> {
329 let PhaseUnwrapPolicy::ExpectedAdvance {
330 discontinuity_radians,
331 } = policy;
332 let unwrapped = unwrap_phase(&[0.0, residual], discontinuity_radians)?;
333 Ok(unwrapped[1])
334}
335
336fn apply_phase_lock(
337 magnitudes: &[f64],
338 analysis_phase: &[f64],
339 synthesis_phase: &mut [f64],
340 policy: PhaseLockPolicy,
341) {
342 let PhaseLockPolicy::Identity { radius_bins } = policy else {
343 return;
344 };
345 let peaks = (0..magnitudes.len())
346 .filter(|index| {
347 let left = index
348 .checked_sub(1)
349 .map(|left| magnitudes[left])
350 .unwrap_or(f64::NEG_INFINITY);
351 let right = magnitudes
352 .get(index + 1)
353 .copied()
354 .unwrap_or(f64::NEG_INFINITY);
355 magnitudes[*index] >= left && magnitudes[*index] >= right
356 })
357 .collect::<Vec<_>>();
358 let independent = synthesis_phase.to_vec();
359 for bin in 0..synthesis_phase.len() {
360 let owner = peaks
361 .iter()
362 .copied()
363 .filter(|peak| peak.abs_diff(bin) <= radius_bins)
364 .min_by_key(|peak| peak.abs_diff(bin));
365 if let Some(peak) = owner {
366 synthesis_phase[bin] = independent[peak] + analysis_phase[bin] - analysis_phase[peak];
367 }
368 }
369}
370
371fn shift_bins(
372 magnitudes: &[f64],
373 phases: &[f64],
374 pitch_ratio: f64,
375 output_bins: usize,
376) -> Vec<(f64, f64)> {
377 let mut shifted = vec![(0.0, 0.0); output_bins];
378 for source in 0..magnitudes.len() {
379 let target = source as f64 * pitch_ratio;
380 let lower = target.floor() as usize;
381 let fraction = target - target.floor();
382 add_polar(
383 &mut shifted,
384 lower,
385 magnitudes[source] * (1.0 - fraction),
386 phases[source] * pitch_ratio,
387 );
388 add_polar(
389 &mut shifted,
390 lower.saturating_add(1),
391 magnitudes[source] * fraction,
392 phases[source] * pitch_ratio,
393 );
394 }
395 if let Some(dc) = shifted.first_mut() {
396 dc.1 = 0.0;
397 }
398 if let Some(nyquist) = shifted.last_mut() {
399 nyquist.1 = 0.0;
400 }
401 shifted
402}
403
404fn add_polar(target: &mut [(f64, f64)], bin: usize, magnitude: f64, phase: f64) {
405 if let Some((real, imaginary)) = target.get_mut(bin) {
406 *real += magnitude * phase.cos();
407 *imaginary += magnitude * phase.sin();
408 }
409}
410
411#[allow(clippy::too_many_arguments)]
412fn overlap_add(
413 bins: &[(f64, f64)],
414 frame_index: usize,
415 synthesis_hop: f64,
416 policy: &VocoderPolicy,
417 analysis_window: &[f64],
418 synthesis_window: &[f64],
419 output: &mut [f64],
420 gain: &mut [f64],
421) -> Result<(), VocoderError> {
422 let mut inverse = TransformPlan::new(TransformKind::RealFft, policy.stft.frame);
423 inverse.direction = Direction::Inverse;
424 inverse.normalization = policy.stft.normalization;
425 inverse.sign = policy.stft.phase;
426 inverse.packing = SpectrumPacking::HermitianHalf;
427 let SignalBuffer::Real(frame) = transform(&inverse, SignalView::Complex(bins))? else {
428 unreachable!("inverse real FFT returns real samples")
429 };
430 let start = (frame_index as f64 * synthesis_hop).round() as usize;
431 for offset in 0..policy.stft.frame {
432 let at = start
433 .checked_add(offset)
434 .ok_or(VocoderError::SizeOverflow)?;
435 output[at] += frame.as_slice()[offset] * synthesis_window[offset];
436 gain[at] += analysis_window[offset] * synthesis_window[offset];
437 }
438 Ok(())
439}
440
441fn trim_output(output: &[f64], gain: &[f64], start: usize, len: usize) -> Vec<f32> {
442 (0..len)
443 .map(|offset| {
444 let at = start.saturating_add(offset);
445 let divisor = gain.get(at).copied().unwrap_or(0.0);
446 if divisor.abs() <= 1e-12 {
447 0.0
448 } else {
449 (output.get(at).copied().unwrap_or(0.0) / divisor) as f32
450 }
451 })
452 .collect()
453}
454
455fn empty_report(policy: VocoderPolicy) -> PhaseVocoderReport {
456 PhaseVocoderReport {
457 samples: Vec::new(),
458 input_samples: 0,
459 output_samples: 0,
460 analysis_frames: 0,
461 synthesis_hop: policy.stft.hop as f64 * policy.stretch_ratio,
462 transient_resets: 0,
463 peak: 0.0,
464 clipped_samples: 0,
465 policy,
466 }
467}
468
469fn invalid(field: &'static str, reason: &'static str) -> VocoderError {
470 VocoderError::InvalidPolicy { field, reason }
471}