Skip to main content

sound_to_patch/render/
fake.rs

1//! A fake renderer for tests: a seeded additive synth with no plug-in.
2//!
3//! Its patch is JSON in the preset state, under `settings` as in a Vital
4//! state, so the load check reads it back the same way. Each note sums the patch's
5//! harmonics under a linear attack and an exponential release, and a
6//! render draws a random phase per harmonic and a small level jitter, so
7//! repeat renders vary the way a synth with random unison phases does.
8//! Faults stand in for a plug-in that crashes, hangs or stays silent. A
9//! hang is simulated: the render returns [`RenderError::TimedOut`] at once
10//! and never waits.
11//!
12//! The fake keeps no sound between renders, so the pre-roll changes
13//! nothing in its clip. It still steps through the state load and the
14//! pre-roll of each render, and [`FakeRenderer::with_log`] records the
15//! steps. It remembers the last state it loaded, which
16//! [`Renderer::save_state`] returns; before any load it holds
17//! [`FakePatch::init`].
18
19use std::f64::consts::TAU;
20use std::sync::{Arc, Mutex};
21
22use serde::{Deserialize, Serialize};
23
24use super::{
25    EventKind, HostIdentity, PreRoll, PresetState, RenderError, RenderJob, RenderRecord, Rendered,
26    Renderer, note_events,
27};
28use crate::audio::Audio;
29use crate::cast;
30use crate::synth::Synth;
31use crate::units::Seconds;
32
33/// The level jitter of a render: each note's level varies by up to this
34/// fraction.
35const LEVEL_JITTER: f64 = 0.005;
36/// The release falls 60 dB over the release time.
37const RELEASE_DB: f64 = 60.0;
38
39/// The sound of the fake synth.
40#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
41pub struct FakePatch {
42    /// The amplitude of each harmonic, the fundamental first.
43    pub harmonics: Vec<f64>,
44    /// The attack time.
45    pub attack: Seconds,
46    /// The time the release takes to fall 60 dB.
47    pub release: Seconds,
48}
49
50impl FakePatch {
51    /// The fake's init patch: a bright tone with a short release.
52    #[must_use]
53    pub fn init() -> Self {
54        Self {
55            harmonics: vec![1.0, 0.5, 0.33, 0.25, 0.2],
56            attack: Seconds::from_const(0.005),
57            release: Seconds::from_const(0.2),
58        }
59    }
60
61    /// Returns the patch as the state of `synth`.
62    ///
63    /// # Errors
64    ///
65    /// Returns the serializer's error. A patch of finite numbers always
66    /// serializes.
67    pub fn state(&self, synth: Synth) -> Result<PresetState, serde_json::Error> {
68        let root = serde_json::json!({ "preset_name": "fake", "settings": self });
69        Ok(PresetState::new(synth, serde_json::to_vec(&root)?))
70    }
71
72    /// Reads a patch from a state.
73    fn of_state(state: &[u8]) -> Result<Self, RenderError> {
74        let not_fake = |cause: String| RenderError::State(format!("not a fake patch ({cause})"));
75        let root: serde_json::Value =
76            serde_json::from_slice(state).map_err(|source| not_fake(source.to_string()))?;
77        serde_json::from_value(root["settings"].clone())
78            .map_err(|source| not_fake(source.to_string()))
79    }
80}
81
82/// A fault the fake renderer injects into one render.
83#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84pub enum Fault {
85    /// The host process stops.
86    Crash,
87    /// The host process stops answering. Simulated: the render fails at
88    /// once with a time-out.
89    Hang,
90    /// The plug-in renders silence.
91    Silence,
92    /// The state does not load: the plug-in keeps and saves its init
93    /// patch.
94    KeepInit,
95    /// The state loads, but the state the plug-in saves names another
96    /// preset.
97    Rename,
98}
99
100/// A step the fake played, in the order a plug-in host plays them.
101#[derive(Clone, Copy, Debug, PartialEq)]
102pub enum FakeStep {
103    /// The job's state loaded.
104    Load,
105    /// A step of the recipe's pre-roll.
106    PreRoll(PreRoll),
107    /// The clip of a job, named by the end of its last note.
108    Clip(Seconds),
109}
110
111/// The steps that renderers played, shared between renderers.
112pub type FakeLog = Arc<Mutex<Vec<FakeStep>>>;
113
114/// A seeded additive synth behind the [`Renderer`] trait.
115#[derive(Clone, Debug)]
116pub struct FakeRenderer {
117    synth: Synth,
118    seed: u64,
119    renders: u64,
120    faults: Vec<(u64, Fault)>,
121    loaded: Option<Vec<u8>>,
122    fixed: Option<FakePatch>,
123    log: Option<FakeLog>,
124    last_state_load: Option<Seconds>,
125}
126
127impl FakeRenderer {
128    /// Builds a renderer that claims to be `synth`. Two renderers with one
129    /// seed give bit-identical renders in the same order.
130    #[must_use]
131    pub fn new(synth: Synth, seed: u64) -> Self {
132        Self {
133            synth,
134            seed,
135            renders: 0,
136            faults: Vec::new(),
137            loaded: None,
138            fixed: None,
139            log: None,
140            last_state_load: None,
141        }
142    }
143
144    /// Plays `patch` whatever state a job holds, as a plug-in that loads
145    /// any preset of its synth. The saved state is still the loaded one.
146    #[must_use]
147    pub fn with_patch(mut self, patch: FakePatch) -> Self {
148        self.fixed = Some(patch);
149        self
150    }
151
152    /// Records each step it plays in `log`.
153    #[must_use]
154    pub fn with_log(mut self, log: FakeLog) -> Self {
155        self.log = Some(log);
156        self
157    }
158
159    /// Injects `fault` into the render with index `at`, counted from 0.
160    #[must_use]
161    pub fn with_fault(mut self, at: u64, fault: Fault) -> Self {
162        self.faults.push((at, fault));
163        self
164    }
165
166    /// Returns how many renders were asked for, failed ones included.
167    #[must_use]
168    pub fn renders(&self) -> u64 {
169        self.renders
170    }
171}
172
173impl Renderer for FakeRenderer {
174    fn identity(&self) -> HostIdentity {
175        HostIdentity {
176            synth: self.synth,
177            build: "fake".to_owned(),
178            host: "fake".to_owned(),
179            arch: "fake".to_owned(),
180        }
181    }
182
183    fn render(&mut self, job: &RenderJob) -> Result<Rendered, RenderError> {
184        self.play_job(job, true)
185    }
186
187    fn render_init(&mut self, job: &RenderJob) -> Result<Rendered, RenderError> {
188        if self.loaded.is_some() {
189            return Err(RenderError::Host {
190                cause: "the fake already holds a loaded state".to_owned(),
191                fix: "Render the init patch first".to_owned(),
192            });
193        }
194        self.play_job(job, false)
195    }
196
197    fn last_state_load(&self) -> Option<Seconds> {
198        self.last_state_load
199    }
200
201    fn save_state(&mut self) -> Result<Vec<u8>, RenderError> {
202        match &self.loaded {
203            Some(state) => Ok(state.clone()),
204            None => Ok(init_state(self.synth)?.bytes().to_vec()),
205        }
206    }
207}
208
209fn init_state(synth: Synth) -> Result<PresetState, RenderError> {
210    FakePatch::init()
211        .state(synth)
212        .map_err(|source| RenderError::State(source.to_string()))
213}
214
215impl FakeRenderer {
216    fn play_job(&mut self, job: &RenderJob, load: bool) -> Result<Rendered, RenderError> {
217        let index = self.renders;
218        self.renders += 1;
219        let fault = self
220            .faults
221            .iter()
222            .find(|(at, _)| *at == index)
223            .map(|(_, fault)| *fault);
224        match fault {
225            Some(Fault::Crash) => return Err(RenderError::Crashed("fake crash".to_owned())),
226            Some(Fault::Hang) => {
227                return Err(RenderError::TimedOut {
228                    after: Seconds::from_const(30.0),
229                    output: "fake hang".to_owned(),
230                });
231            }
232            _ => {}
233        }
234        if job.state().synth() != self.synth {
235            return Err(RenderError::WrongSynth {
236                state: job.state().synth(),
237                renderer: self.synth,
238            });
239        }
240        self.last_state_load = None;
241        let loading = std::time::Instant::now();
242        let patch = if load && fault != Some(Fault::KeepInit) {
243            let patch = match &self.fixed {
244                Some(patch) => patch.clone(),
245                None => FakePatch::of_state(job.state().bytes())?,
246            };
247            let mut saved = job.state().bytes().to_vec();
248            if fault == Some(Fault::Rename) {
249                let mut root: serde_json::Value = serde_json::from_slice(&saved)
250                    .map_err(|source| RenderError::State(source.to_string()))?;
251                root["preset_name"] = "renamed".into();
252                saved = serde_json::to_vec(&root)
253                    .map_err(|source| RenderError::State(source.to_string()))?;
254            }
255            self.loaded = Some(saved);
256            patch
257        } else {
258            if load {
259                self.loaded = Some(init_state(self.synth)?.bytes().to_vec());
260            }
261            FakePatch::init()
262        };
263        if load {
264            self.last_state_load = Seconds::new(loading.elapsed().as_secs_f64()).ok();
265        }
266        if let Some(log) = &self.log {
267            let mut log = log
268                .lock()
269                .unwrap_or_else(std::sync::PoisonError::into_inner);
270            if load {
271                log.push(FakeStep::Load);
272            }
273            log.extend(
274                job.recipe()
275                    .pre_roll()
276                    .iter()
277                    .copied()
278                    .map(FakeStep::PreRoll),
279            );
280            log.push(FakeStep::Clip(job.last_note_off()));
281        }
282        let mut rng = SplitMix::for_render(self.seed, index);
283        let (mono, end) = play(&patch, job, &mut rng, fault == Some(Fault::Silence))?;
284        let audio = Audio::new(job.recipe().sample_rate(), vec![mono.clone(), mono])?;
285        Ok(Rendered {
286            audio,
287            record: RenderRecord::new(self.identity(), job, end),
288        })
289    }
290}
291
292/// One sounding note: its key, frequency, gain, phases and frames.
293struct Voice {
294    key: u8,
295    hz: f64,
296    gain: f64,
297    phases: Vec<f64>,
298    on: usize,
299    off: Option<usize>,
300}
301
302/// Renders the mono clip and where it ends.
303fn play(
304    patch: &FakePatch,
305    job: &RenderJob,
306    rng: &mut SplitMix,
307    silent: bool,
308) -> Result<(Vec<f64>, super::ClipEnd), RenderError> {
309    let recipe = job.recipe();
310    let rate = f64::from(recipe.sample_rate().get());
311    let mut voices: Vec<Voice> = Vec::new();
312    let events = note_events(job.notes(), recipe.sample_rate());
313    let mut next = 0;
314    let mut watch = recipe.tail_watch(job.notes());
315    let attack = patch.attack.get() * rate;
316    let release = patch.release.get() * rate;
317    let mut out = Vec::new();
318    loop {
319        let frame = out.len();
320        while let Some(event) = events.get(next).filter(|e| e.frame <= frame) {
321            match event.kind {
322                EventKind::On => voices.push(Voice {
323                    key: event.key,
324                    hz: 440.0 * 2_f64.powf((f64::from(event.key) - 69.0) / 12.0),
325                    gain: f64::from(event.velocity.get()) / 127.0
326                        * (1.0 + LEVEL_JITTER * (2.0 * rng.unit() - 1.0)),
327                    phases: patch.harmonics.iter().map(|_| TAU * rng.unit()).collect(),
328                    on: frame,
329                    off: None,
330                }),
331                EventKind::Off => {
332                    if let Some(voice) = voices
333                        .iter_mut()
334                        .find(|v| v.off.is_none() && v.key == event.key)
335                    {
336                        voice.off = Some(frame);
337                    }
338                }
339            }
340            next += 1;
341        }
342        let t = cast::usize_to_f64(frame) / rate;
343        let sample: f64 = if silent {
344            0.0
345        } else {
346            voices
347                .iter()
348                .map(|voice| {
349                    let age = cast::usize_to_f64(frame - voice.on);
350                    let rise = if attack > 0.0 {
351                        (age / attack).min(1.0)
352                    } else {
353                        1.0
354                    };
355                    let fall = voice.off.map_or(1.0, |off| {
356                        let since = cast::usize_to_f64(frame - off);
357                        10_f64.powf(-RELEASE_DB / 20.0 * since / release.max(1.0))
358                    });
359                    let tone: f64 = patch
360                        .harmonics
361                        .iter()
362                        .zip(&voice.phases)
363                        .enumerate()
364                        .map(|(k, (amp, phase))| {
365                            amp * (TAU * voice.hz * cast::usize_to_f64(k + 1) * t + phase).sin()
366                        })
367                        .sum();
368                    voice.gain * rise * fall * tone
369                })
370                .sum()
371        };
372        out.push(sample);
373        if let Some(end) = watch.push(&[sample])? {
374            return Ok((out, end));
375        }
376    }
377}
378
379/// The SplitMix64 generator: small, seeded and the same on every platform.
380struct SplitMix(u64);
381
382impl SplitMix {
383    /// Seeds the generator of one render from the pair (seed, render
384    /// index): each part goes through the mixer, so nearby pairs give
385    /// unrelated streams.
386    fn for_render(seed: u64, index: u64) -> Self {
387        let first = Self(seed).next();
388        Self(first ^ Self(index).next().rotate_left(32))
389    }
390
391    /// Returns the next 64 random bits.
392    fn next(&mut self) -> u64 {
393        self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
394        let mut z = self.0;
395        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
396        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
397        z ^ (z >> 31)
398    }
399
400    /// Returns a uniform value in `[0, 1)`.
401    fn unit(&mut self) -> f64 {
402        cast::unit_interval(self.next())
403    }
404}