effects.rs

crates/veilvoice-core/src/effects.rs

veilvoice-core · 245 lines · read the source here · or on GitHub

Light time-domain effects applied after resynthesis.

These run on the continuous output stream (not per FFT frame) and exist to (a) further decorrelate the signal from the original, and (b) add a few detuned "voices" so the spectrogram is densely filled rather than showing a clean harmonic stack, without harming intelligibility, so every mix defaults low. None of them are invertible in a way that recovers the source voice.

How little these contribute, said plainly

It would be easy to read a chorus and a reverb as part of the anonymity argument. They are not, and this crate should not let anybody think they are. The voiceprint is destroyed in crate::spectral -- by discarding measured phase and by mapping every speaker onto one canonical pitch register and vocal-tract scale. That has already happened before a single sample reaches this file.

What these three add is decorrelation at the margins: a denser spectrogram, a few detuned voices where there was a clean harmonic stack, and some odd harmonics smearing whatever residual cues survived. Useful, cheap, and nowhere near sufficient alone. Set all three mixes to zero and the output is exactly as unlinkable as before.

The reason to be exact about this is that a filter chain of precisely this shape -- clip, chorus, reverb -- is what a voice changer ships, and a voice changer offers no anonymity whatsoever. Everything that separates this project from that one happens upstream of this file.

Why every mix defaults low

Intelligibility is a requirement, not a preference. Each of these effects trades clarity for density, and past a fairly low mix the words start to cost more than the added decorrelation is worth. The defaults sit where a listener does not notice the effect is there at all; they are a starting point a user may raise, not a recommendation to raise them.

Real-time constraints

These run per output sample inside an audio callback, on the continuous stream rather than per FFT frame. Every buffer is allocated once at construction: process allocates nothing, takes no lock and reads no clock. Chorus and Reverb own their delay lines and index them with wrapping arithmetic, so changing sample rate means building a new one rather than resizing a live one.

In plain words

A few small finishing touches applied to the sound after the main work is done.

They do two things. They loosen what remains of the connection between the result and the original recording, and they fill in the picture a spectrogram would show, so it looks like a dense, ordinary voice rather than something obviously processed.

Every one of them is set gently by default, because all of them can hurt how clear the words are if pushed, and clear words are the point.

WHAT THIS FILE CONTAINS

245 lines defining 8 functions (6 public), 4 types and 0 constants. Everything below is read out of the source, so it cannot disagree with the code.

The types it owns.

  • struct SoftClip line 63 · Symmetric soft-clip (tanh) waveshaper.
  • struct DelayVoice line 92 · A single modulated delay line, summed into a small ensemble to create the impression of several slightly different voices.
  • struct Chorus line 139 · Detuned chorus ensemble.
  • struct Reverb line 173 · Minimal Schroeder-style reverb: one feedback comb + one all-pass.

What happens when it runs. These are the ways in: public, and nothing else in this file calls them, so they are what an outside caller reaches first.

  • SoftClip::new line 72 · Build a shaper.
  • SoftClip::process line 84 · Shape one sample.
  • Chorus::new line 147 · Build the ensemble.
  • Chorus::process line 161 · Sum the voices, average them, and blend that against the dry sample.
  • Reverb::new line 186 · Size both delay lines for this sample rate, once.
  • Reverb::process line 203 · One sample through the comb and then the all-pass, blended against the dry sample.

WHAT CALLS WHAT

SoftClip::new line 72 SoftClip::process line 84 DelayVoice::new line 105 DelayVoice::process line 122 Chorus::new line 147 Chorus::process line 161 Reverb::new line 186 Reverb::process line 203 entry: a way in: public, and nothing in this file calls it helper: private to this file dashed: a call that goes back up, or across a wrapped rank The functions this file defines, and the calls between them. An edge means the callee's name appears, called, inside the caller's body. This is a syntactic reading, not a type-resolved one.

The functions this file defines, and the calls between them. An edge means the callee's name appears, called, inside the caller's body. This is a syntactic reading, not a type-resolved one.

The same graph as Mermaid source
%%{init: {"theme":"base","themeVariables":{"background":"#1a1b26","primaryColor":"#1f2335","primaryTextColor":"#c0caf5","primaryBorderColor":"#7aa2f7","secondaryColor":"#16161e","tertiaryColor":"#16161e","lineColor":"#737aa2","textColor":"#c0caf5","mainBkg":"#1f2335","nodeBorder":"#7aa2f7","clusterBkg":"#16161e","clusterBorder":"#2f3549","fontFamily":"ui-monospace, SFMono-Regular, Consolas, monospace","fontSize":"14px"}}}%%
flowchart TD
    n_new(["SoftClip::new<br/>line 72"])
    n_process(["SoftClip::process<br/>line 84"])
    n_new["DelayVoice::new<br/>line 105"]
    n_process["DelayVoice::process<br/>line 122"]
    n_new(["Chorus::new<br/>line 147"])
    n_process(["Chorus::process<br/>line 161"])
    n_new(["Reverb::new<br/>line 186"])
    n_process(["Reverb::process<br/>line 203"])
    click n_new href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-core/src/effects.rs#L72" "open the source"
    click n_process href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-core/src/effects.rs#L84" "open the source"
    click n_new href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-core/src/effects.rs#L105" "open the source"
    click n_process href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-core/src/effects.rs#L122" "open the source"
    click n_new href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-core/src/effects.rs#L147" "open the source"
    click n_process href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-core/src/effects.rs#L161" "open the source"
    click n_new href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-core/src/effects.rs#L186" "open the source"
    click n_process href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-core/src/effects.rs#L203" "open the source"
    classDef entry fill:#1f2335,stroke:#7aa2f7,color:#c0caf5
    class n_new,n_process,n_new,n_process,n_new,n_process entry
    classDef helper fill:#1f2335,stroke:#bb9af7,color:#c0caf5
    class n_new,n_process helper

This site loads no third-party script, so it cannot run Mermaid; the diagram above is the same nodes and edges drawn by the generator instead. GitHub renders the source below directly.

ITEMS

ItemLineDocumentation
SoftClip pub struct63Symmetric soft-clip (tanh) waveshaper.
SoftClip::new pub fn72Build a shaper.
SoftClip::process pub fn84Shape one sample.
DelayVoice struct92A single modulated delay line, summed into a small ensemble to create the impression of several slightly different voices.
DelayVoice::new fn105One delay line, sized once here for the deepest sweep it can be asked for.
DelayVoice::process fn122Write one sample and read one back from where the sweep currently points, interpolating between the two neighbouring samples so the moving read position does not step audibly.
Chorus pub struct139Detuned chorus ensemble.
Chorus::new pub fn147Build the ensemble.
Chorus::process pub fn161Sum the voices, average them, and blend that against the dry sample.
Reverb pub struct173Minimal Schroeder-style reverb: one feedback comb + one all-pass.
Reverb::new pub fn186Size both delay lines for this sample rate, once.
Reverb::process pub fn203One sample through the comb and then the all-pass, blended against the dry sample.