Skip to main content

sioul_core/
sounds.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! Sounds to rest by or to focus with (docs/sounds.md), all made here, nothing
5//! downloaded, nothing to license, each looping without a seam (its end faded
6//! into its start):
7//! - **noise** for focus: white, pink, brown; a minute each, mono;
8//! - **nature** to rest by: waves on a beach, rain, wind in the trees, crickets
9//!   at night, a distant storm; one to three minutes each, in stereo at
10//!   22 kHz (what they hold lies below 10 kHz, and the files stay small).
11//!   Filtered noise shaped by waves, gusts and drops, and crickets' chirps as
12//!   pulsed tones, each sound seeded the same every time.
13//!
14//! Recordings of your own are your files, in the notes' `sounds` folder.
15
16use std::path::{Path, PathBuf};
17
18/// The noises made here.
19pub const NOISES: &[&str] = &["white", "pink", "brown"];
20/// The sounds of nature made here.
21pub const NATURE: &[&str] = &["waves", "rain", "wind", "crickets", "storm"];
22
23const RATE: u32 = 44_100;
24const SECONDS: usize = 60;
25/// The seam's crossfade, in samples: two seconds.
26const FADE: usize = 2 * RATE as usize;
27
28/// A small, seeded random source: the same noise each time.
29struct Random(u64);
30
31impl Random {
32    /// A number in [-1, 1).
33    fn next(&mut self) -> f32 {
34        self.0 ^= self.0 << 13;
35        self.0 ^= self.0 >> 7;
36        self.0 ^= self.0 << 17;
37        // The top 24 bits, as [0, 1), then centred.
38        ((self.0 >> 40) as f32 / (1u64 << 24) as f32) * 2.0 - 1.0
39    }
40
41    /// A number in [0, 1).
42    fn unit(&mut self) -> f32 {
43        (self.next() + 1.0) * 0.5
44    }
45
46    /// A number in [low, high).
47    fn between(&mut self, low: f32, high: f32) -> f32 {
48        low + (high - low) * self.unit()
49    }
50}
51
52/// `count` samples of a noise: white (flat), pink (−3 dB per octave, Paul
53/// Kellett's filter), brown (−6 dB per octave, a leaky integration).
54fn noise(kind: &str, count: usize) -> Vec<f32> {
55    let mut random = Random(0x5105_1d00_c0ff_ee11);
56    let (mut b0, mut b1, mut b2, mut b3, mut b4, mut b5, mut b6) = (0f32, 0f32, 0f32, 0f32, 0f32, 0f32, 0f32);
57    let mut last = 0f32;
58    (0..count)
59        .map(|_| {
60            let white = random.next();
61            match kind {
62                "pink" => {
63                    b0 = 0.99886 * b0 + white * 0.0555179;
64                    b1 = 0.99332 * b1 + white * 0.0750759;
65                    b2 = 0.96900 * b2 + white * 0.1538520;
66                    b3 = 0.86650 * b3 + white * 0.3104856;
67                    b4 = 0.55000 * b4 + white * 0.5329522;
68                    b5 = -0.7616 * b5 - white * 0.0168980;
69                    let pink = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362;
70                    b6 = white * 0.115926;
71                    pink * 0.11
72                }
73                "brown" => {
74                    last = (last + 0.02 * white) / 1.02;
75                    last * 3.5
76                }
77                _ => white,
78            }
79        })
80        .collect()
81}
82
83/// A noise of `seconds` that loops: its last two seconds faded into its first,
84/// with equal power, so the end flows into the start. Quiet: about −20 dBFS.
85pub fn looped(kind: &str, seconds: usize) -> Vec<i16> {
86    let length = seconds * RATE as usize;
87    let raw = noise(kind, length + FADE);
88    let mut out: Vec<f32> = raw[..length].to_vec();
89    for (i, sample) in out.iter_mut().take(FADE).enumerate() {
90        let t = i as f32 / FADE as f32;
91        *sample = raw[i] * t.sqrt() + raw[length + i] * (1.0 - t).sqrt();
92    }
93    let rms = (out.iter().map(|s| s * s).sum::<f32>() / out.len() as f32).sqrt().max(1e-6);
94    let gain = 0.1 / rms;
95    out.iter().map(|s| (s * gain).clamp(-1.0, 1.0)).map(|s| (s * f32::from(i16::MAX)) as i16).collect()
96}
97
98/// 16-bit mono WAV bytes, at the noises' rate.
99fn wav(samples: &[i16]) -> Vec<u8> {
100    wav_of(samples, 1, RATE)
101}
102
103/// 16-bit WAV bytes: `channels` interleaved, at `rate`.
104fn wav_of(samples: &[i16], channels: u16, rate: u32) -> Vec<u8> {
105    let data = u32::try_from(samples.len() * 2).unwrap_or(u32::MAX);
106    let mut out = Vec::with_capacity(44 + samples.len() * 2);
107    out.extend(b"RIFF");
108    out.extend((36 + data).to_le_bytes());
109    out.extend(b"WAVEfmt ");
110    out.extend(16u32.to_le_bytes());
111    out.extend(1u16.to_le_bytes());
112    out.extend(channels.to_le_bytes());
113    out.extend(rate.to_le_bytes());
114    out.extend((rate * 2 * u32::from(channels)).to_le_bytes());
115    out.extend((2 * channels).to_le_bytes());
116    out.extend(16u16.to_le_bytes());
117    out.extend(b"data");
118    out.extend(data.to_le_bytes());
119    for sample in samples {
120        out.extend(sample.to_le_bytes());
121    }
122    out
123}
124
125/// The file of a noise or a sound of nature, made the first time in `folder`.
126pub fn noise_file(folder: &Path, kind: &str) -> Result<PathBuf, String> {
127    let (name, bytes): (String, fn(&str) -> Vec<u8>) = if NOISES.contains(&kind) {
128        (format!("{kind}-noise-{NATURE_VERSION}.wav"), |kind| wav(&looped(kind, SECONDS)))
129    } else if NATURE.contains(&kind) {
130        (format!("{kind}-{NATURE_VERSION}.wav"), |kind| {
131            let (left, right) = nature(kind);
132            let interleaved: Vec<i16> = left.iter().zip(&right).flat_map(|(l, r)| [*l, *r]).collect();
133            wav_of(&interleaved, 2, NATURE_RATE)
134        })
135    } else {
136        return Err(format!("{kind}: no such sound"));
137    };
138    let path = folder.join(name);
139    if !path.exists() {
140        std::fs::create_dir_all(folder).map_err(|e| format!("{}: {e}", folder.display()))?;
141        let temporary = path.with_extension("wav.new");
142        std::fs::write(&temporary, bytes(kind)).map_err(|e| format!("{}: {e}", temporary.display()))?;
143        std::fs::rename(&temporary, &path).map_err(|e| format!("{}: {e}", path.display()))?;
144    }
145    Ok(path)
146}
147
148// Nature.
149
150/// The sounds of nature's rate: what they hold lies below 10 kHz.
151const NATURE_RATE: u32 = 22_050;
152/// Raised when the recipes change, so the files are made again (2: the noises
153/// were offset, their random source not centred; 3: crickets held in tune;
154/// 4: warmer waves).
155const NATURE_VERSION: u32 = 4;
156
157/// A one-pole low-pass filter, its cutoff free to move each sample.
158#[derive(Default, Clone, Copy)]
159struct LowPass(f32);
160
161impl LowPass {
162    fn run(&mut self, x: f32, cutoff: f32) -> f32 {
163        let a = 1.0 - (-2.0 * std::f32::consts::PI * cutoff / NATURE_RATE as f32).exp();
164        self.0 += a * (x - self.0);
165        self.0
166    }
167}
168
169/// A band-pass filter (RBJ's cookbook, constant peak gain), its centre free to move.
170#[derive(Default, Clone, Copy)]
171struct BandPass {
172    x1: f32,
173    x2: f32,
174    y1: f32,
175    y2: f32,
176}
177
178impl BandPass {
179    fn run(&mut self, x: f32, centre: f32, q: f32) -> f32 {
180        let w = 2.0 * std::f32::consts::PI * centre / NATURE_RATE as f32;
181        let alpha = w.sin() / (2.0 * q);
182        let a0 = 1.0 + alpha;
183        let (b0, b2, a1, a2) = (alpha / a0, -alpha / a0, -2.0 * w.cos() / a0, (1.0 - alpha) / a0);
184        let y = b0 * x + b2 * self.x2 - a1 * self.y1 - a2 * self.y2;
185        (self.x2, self.x1, self.y2, self.y1) = (self.x1, x, self.y1, y);
186        y
187    }
188}
189
190/// A slow, smooth random curve in [0, 1]: a new point every `every` seconds,
191/// joined by cosines; the same for every sample count.
192fn slow_curve(random: &mut Random, count: usize, every: f32) -> Vec<f32> {
193    let step = (every * NATURE_RATE as f32).max(1.0) as usize;
194    let points: Vec<f32> = (0..count / step + 3).map(|_| random.unit()).collect();
195    (0..count)
196        .map(|i| {
197            let (k, t) = (i / step, (i % step) as f32 / step as f32);
198            let w = (1.0 - (t * std::f32::consts::PI).cos()) * 0.5;
199            points[k] * (1.0 - w) + points[k + 1] * w
200        })
201        .collect()
202}
203
204/// A sound that fades out quickly: a tone of `frequency` decaying in `decay`
205/// seconds, added at `at` with `amplitude`.
206fn add_ping(out: &mut [f32], at: usize, frequency: f32, decay: f32, amplitude: f32, phase: f32) {
207    let length = (decay * 5.0 * NATURE_RATE as f32) as usize;
208    let w = 2.0 * std::f32::consts::PI * frequency / NATURE_RATE as f32;
209    for (n, sample) in out.iter_mut().skip(at).take(length).enumerate() {
210        let t = n as f32 / NATURE_RATE as f32;
211        *sample += amplitude * (-t / decay).exp() * (w * n as f32 + phase).sin();
212    }
213}
214
215/// Times of events, `rate` a second on average, at random (a Poisson process).
216fn events(random: &mut Random, count: usize, rate: f32) -> Vec<usize> {
217    let mut at = 0.0f32;
218    let mut out = Vec::new();
219    loop {
220        at += -(1.0 - random.unit()).max(1e-6).ln() / rate * NATURE_RATE as f32;
221        if at as usize >= count {
222            return out;
223        }
224        out.push(at as usize);
225    }
226}
227
228/// Waves on a beach: each swells (a few seconds), breaks (brighter, louder),
229/// then withdraws in a fizz of foam; a far surf under them all.
230fn waves(count: usize, seed: u64, schedule: &[(f32, f32, f32, f32)]) -> Vec<f32> {
231    let mut random = Random(seed);
232    let rate = NATURE_RATE as f32;
233    let (mut body, mut foam, mut foam_top, mut surf) = (LowPass::default(), LowPass::default(), LowPass::default(), LowPass::default());
234    (0..count)
235        .map(|i| {
236            let t = i as f32 / rate;
237            let (mut swell, mut fizz) = (0.0f32, 0.0f32);
238            for &(start, rise, fall, strength) in schedule {
239                let since = t - start;
240                if since < 0.0 || since > rise + fall * 4.0 {
241                    continue;
242                }
243                if since < rise {
244                    swell += strength * (since / rise).powi(2);
245                } else {
246                    let after = since - rise;
247                    swell += strength * (-after / fall).exp();
248                    fizz += strength * (1.0 - (-after / 0.4).exp()) * (-after / (fall * 0.7)).exp();
249                }
250            }
251            let white = random.next();
252            let crash = body.run(white, 150.0 + 1500.0 * swell.min(1.2));
253            // The foam: a band of hiss, 2.5 to 6 kHz, as the water withdraws.
254            let hiss = foam_top.run(white - foam.run(white, 2500.0), 6000.0);
255            let far = surf.run(random.next(), 90.0);
256            crash * (0.15 + swell) * 1.8 + hiss * fizz * 0.25 + far * 0.9
257        })
258        .collect()
259}
260
261/// Rain: a soft hiss, many small drops, a few larger ones on leaves and stones.
262fn rain(count: usize, seed: u64, heavy: f32) -> Vec<f32> {
263    let mut random = Random(seed);
264    let mut out = vec![0.0f32; count];
265    let (mut low, mut high) = (LowPass::default(), LowPass::default());
266    let level = slow_curve(&mut random, count, 6.0);
267    let mut pink = Pink::default();
268    for (i, sample) in out.iter_mut().enumerate() {
269        let p = pink.next(random.next());
270        let band = high.run(p, 6500.0) - low.run(p, 450.0);
271        *sample = band * (0.75 + 0.25 * level[i]) * 0.9 * heavy;
272    }
273    for at in events(&mut random, count, 160.0 * heavy) {
274        let (frequency, decay, amplitude, phase) = (random.between(2400.0, 7000.0), random.between(0.0012, 0.004), 0.08 * -(1.0 - random.unit()).max(1e-4).ln().min(4.0), random.between(0.0, 6.28));
275        add_ping(&mut out, at, frequency, decay, amplitude, phase);
276    }
277    for at in events(&mut random, count, 5.0 * heavy) {
278        let (frequency, decay, amplitude, phase) = (random.between(700.0, 2400.0), random.between(0.008, 0.022), random.between(0.05, 0.14), random.between(0.0, 6.28));
279        add_ping(&mut out, at, frequency, decay, amplitude, phase);
280    }
281    out
282}
283
284/// Pink noise, one channel: Paul Kellett's filter, as `noise` uses it.
285#[derive(Default)]
286struct Pink([f32; 7]);
287
288impl Pink {
289    fn next(&mut self, white: f32) -> f32 {
290        let b = &mut self.0;
291        b[0] = 0.99886 * b[0] + white * 0.0555179;
292        b[1] = 0.99332 * b[1] + white * 0.0750759;
293        b[2] = 0.96900 * b[2] + white * 0.1538520;
294        b[3] = 0.86650 * b[3] + white * 0.3104856;
295        b[4] = 0.55000 * b[4] + white * 0.5329522;
296        b[5] = -0.7616 * b[5] - white * 0.0168980;
297        let pink = b[0] + b[1] + b[2] + b[3] + b[4] + b[5] + b[6] + white * 0.5362;
298        b[6] = white * 0.115926;
299        pink * 0.11
300    }
301}
302
303/// Wind in the trees: gusts that rise and fall (a moving band of noise), and
304/// the leaves rustling with them.
305fn wind(count: usize, seed: u64, gusts: &[f32]) -> Vec<f32> {
306    let mut random = Random(seed);
307    let mut out = vec![0.0f32; count];
308    let (mut whoosh, mut leaves, mut flutter) = (BandPass::default(), LowPass::default(), LowPass::default());
309    for (i, sample) in out.iter_mut().enumerate() {
310        let gust = 0.2 + 0.8 * gusts[i].powf(1.6);
311        let white = random.next();
312        let air = whoosh.run(white, 220.0 + 900.0 * gust, 0.9);
313        let rustle = white - leaves.run(white, 2400.0);
314        let shiver = flutter.run(random.next().abs(), 25.0) * 6.0;
315        *sample = air * gust * 1.4 + rustle * gust.powi(2) * shiver.min(1.5) * 0.18;
316    }
317    let crackles: Vec<usize> = events(&mut random, count, 140.0);
318    for at in crackles {
319        let gust = gusts[at.min(count - 1)];
320        if random.unit() < gust.powi(2) {
321            let (frequency, decay, amplitude, phase) = (random.between(2800.0, 7500.0), random.between(0.0004, 0.0012), random.between(0.02, 0.07), random.between(0.0, 6.28));
322            add_ping(&mut out, at, frequency, decay, amplitude, phase);
323        }
324    }
325    out
326}
327
328/// One cricket of a night: where it sits (its weight in this ear), its pitch,
329/// its rhythm.
330struct Cricket {
331    gain: f32,
332    pitch: f32,
333    period: f32,
334    pulses: usize,
335    offset: f32,
336}
337
338/// Crickets at night: a few field crickets chirping, each its pitch and
339/// rhythm, near and far, one tree cricket trilling softly; the night's air under them.
340fn crickets(count: usize, seed: u64, band: &[Cricket], trill_gain: f32) -> Vec<f32> {
341    let mut random = Random(seed);
342    let rate = NATURE_RATE as f32;
343    let mut air = LowPass::default();
344    let mut out: Vec<f32> = (0..count).map(|_| air.run(random.next(), 120.0) * 0.25).collect();
345    let pulse = 0.014f32;
346    let gap = 0.016f32;
347    for cricket in band {
348        let mut chirp = cricket.offset;
349        while ((chirp * rate) as usize) < count {
350            for k in 0..cricket.pulses {
351                let start = chirp + k as f32 * (pulse + gap);
352                let first = (start * rate) as usize;
353                let length = (pulse * rate) as usize;
354                // The phase counted from the pulse's start: exact in single precision.
355                let mut phase = 0.0f32;
356                for n in 0..length {
357                    let Some(sample) = out.get_mut(first + n) else { break };
358                    let t = n as f32 / length as f32;
359                    let window = (std::f32::consts::PI * t).sin().powi(2);
360                    let pitch = cricket.pitch * (1.0 - 0.015 * t);
361                    phase += 2.0 * std::f32::consts::PI * pitch / rate;
362                    *sample += cricket.gain * window * phase.sin();
363                }
364            }
365            chirp += cricket.period * random.between(0.95, 1.05);
366        }
367    }
368    // A tree cricket: a soft trill at 2.8 kHz, 40 pulses a second, swelling and resting.
369    let swell = slow_curve(&mut random, count, 3.5);
370    for (i, sample) in out.iter_mut().enumerate() {
371        // In double precision: a minute of a 2.8 kHz tone is a million radians.
372        let t = i as f64 / f64::from(NATURE_RATE);
373        let beat = ((2.0 * std::f64::consts::PI * 40.0 * t).sin().max(0.0).powi(2)) as f32;
374        let on = (swell[i] - 0.35).max(0.0) / 0.65;
375        *sample += trill_gain * on * beat * (2.0 * std::f64::consts::PI * 2800.0 * t).sin() as f32;
376    }
377    out
378}
379
380/// Thunder far away: a low rumble rolling for seconds, every half minute or so.
381fn thunder(count: usize, seed: u64, strikes: &[(f32, f32, f32)]) -> Vec<f32> {
382    let mut random = Random(seed);
383    let rate = NATURE_RATE as f32;
384    let (mut low, mut roll) = (LowPass::default(), LowPass::default());
385    let mut brown = 0.0f32;
386    (0..count)
387        .map(|i| {
388            let t = i as f32 / rate;
389            brown = (brown + 0.02 * random.next()) / 1.02;
390            let rumble = low.run(brown * 3.5, 110.0);
391            let rolling = 0.5 + 0.5 * roll.run(random.next(), 4.0) * 8.0;
392            let mut level = 0.0f32;
393            for &(start, rise, length) in strikes {
394                let since = t - start;
395                if since >= 0.0 && since < rise + length * 4.0 {
396                    level += if since < rise { since / rise } else { (-(since - rise) / length).exp() };
397                }
398            }
399            rumble * level * rolling.clamp(0.2, 1.4) * 4.0
400        })
401        .collect()
402}
403
404/// A sound of nature in stereo: its two channels, each looping, about −22 dBFS.
405pub fn nature(kind: &str) -> (Vec<i16>, Vec<i16>) {
406    let rate = NATURE_RATE as f32;
407    let seconds: usize = match kind {
408        "waves" => 120,
409        "storm" => 180,
410        "wind" => 90,
411        _ => 60,
412    };
413    let fade = 4 * NATURE_RATE as usize;
414    let count = seconds * NATURE_RATE as usize + fade;
415    let mut plan = Random(0x5105_7a7e_0000_0000 ^ kind.bytes().fold(0u64, |h, b| h.wrapping_mul(31).wrapping_add(u64::from(b))));
416    let (left, right) = match kind {
417        "waves" => {
418            // The same waves for both ears, the right a little later: they come from one sea.
419            let mut schedule = Vec::new();
420            let mut start = -2.0f32;
421            while start < (count as f32 / rate) {
422                let rise = plan.between(2.0, 3.8);
423                let fall = plan.between(1.6, 2.6);
424                schedule.push((start, rise, fall, plan.between(0.55, 1.0)));
425                start += plan.between(6.5, 11.5);
426            }
427            let late: Vec<(f32, f32, f32, f32)> = schedule.iter().map(|&(s, r, f, k)| (s + 0.25, r, f, k * 0.95)).collect();
428            (waves(count, 11, &schedule), waves(count, 12, &late))
429        }
430        "rain" => (rain(count, 21, 1.0), rain(count, 22, 1.0)),
431        "wind" => {
432            let gusts = slow_curve(&mut plan, count, 4.5);
433            let later: Vec<f32> = (0..count).map(|i| gusts[i.saturating_sub((0.3 * rate) as usize)]).collect();
434            (wind(count, 31, &gusts), wind(count, 32, &later))
435        }
436        "crickets" => {
437            let band: Vec<(f32, Cricket)> = (0..6)
438                .map(|_| {
439                    let pan = plan.between(-0.85, 0.85);
440                    let near = plan.between(0.15, 1.0);
441                    (pan, Cricket { gain: near * 0.35, pitch: plan.between(4100.0, 4900.0), period: plan.between(0.42, 0.95), pulses: if plan.unit() < 0.5 { 3 } else { 4 }, offset: plan.between(0.0, 1.0) })
442                })
443                .collect();
444            let side = |left: bool| -> Vec<Cricket> {
445                band.iter()
446                    .map(|(pan, c)| {
447                        let angle = (pan + 1.0) * std::f32::consts::FRAC_PI_4;
448                        let weight = if left { angle.cos() } else { angle.sin() };
449                        Cricket { gain: c.gain * weight, pitch: c.pitch, period: c.period, pulses: c.pulses, offset: c.offset }
450                    })
451                    .collect()
452            };
453            (crickets(count, 41, &side(true), 0.025), crickets(count, 42, &side(false), 0.035))
454        }
455        _ => {
456            // A distant storm: lighter rain, and thunder far away.
457            let mut strikes = Vec::new();
458            let mut at = plan.between(4.0, 14.0);
459            while at < (count as f32 / rate) {
460                strikes.push((at, plan.between(0.4, 1.1), plan.between(1.8, 3.5)));
461                at += plan.between(18.0, 42.0);
462            }
463            let far: Vec<(f32, f32, f32)> = strikes.iter().map(|&(s, r, l)| (s + 0.04, r, l)).collect();
464            let mix = |a: Vec<f32>, b: Vec<f32>| a.iter().zip(&b).map(|(x, y)| x * 0.8 + y).collect::<Vec<f32>>();
465            (mix(rain(count, 51, 0.6), thunder(count, 53, &strikes)), mix(rain(count, 52, 0.6), thunder(count, 54, &far)))
466        }
467    };
468    let target = if kind == "crickets" { 0.05 } else { 0.08 };
469    let looped = |raw: &[f32]| -> Vec<f32> {
470        let length = raw.len() - fade;
471        let mut out: Vec<f32> = raw[..length].to_vec();
472        for (i, sample) in out.iter_mut().take(fade).enumerate() {
473            let t = i as f32 / fade as f32;
474            *sample = raw[i] * t.sqrt() + raw[length + i] * (1.0 - t).sqrt();
475        }
476        out
477    };
478    let (left, right) = (looped(&left), looped(&right));
479    let rms = (left.iter().chain(&right).map(|s| s * s).sum::<f32>() / (left.len() + right.len()) as f32).sqrt().max(1e-6);
480    let gain = target / rms;
481    let to_pcm = |channel: &[f32]| channel.iter().map(|s| (s * gain).tanh()).map(|s| (s * f32::from(i16::MAX)) as i16).collect::<Vec<i16>>();
482    (to_pcm(&left), to_pcm(&right))
483}
484
485#[cfg(test)]
486mod tests {
487    use super::*;
488
489    #[test]
490    fn the_random_source_is_centred() {
491        let mut random = Random(7);
492        let values: Vec<f32> = (0..100_000).map(|_| random.next()).collect();
493        let mean = values.iter().sum::<f32>() / values.len() as f32;
494        assert!(mean.abs() < 0.01, "mean {mean}");
495        assert!(values.iter().all(|v| (-1.0..1.0).contains(v)));
496    }
497
498    #[test]
499    fn noises_loop_without_a_seam() {
500        for kind in NOISES {
501            let samples = looped(kind, 3);
502            assert_eq!(samples.len(), 3 * RATE as usize);
503            // The jump from the end to the start is no larger than the noise's own steps.
504            let step = |a: i16, b: i16| (i32::from(a) - i32::from(b)).abs();
505            let seam = step(samples[samples.len() - 1], samples[0]);
506            let typical = samples.windows(2).map(|w| step(w[0], w[1])).max().unwrap_or(0);
507            assert!(seam <= typical, "{kind}: seam {seam}, steps up to {typical}");
508        }
509        // Brown is darker than white: smaller steps between samples.
510        let mean_step = |s: &[i16]| s.windows(2).map(|w| (i32::from(w[0]) - i32::from(w[1])).abs() as i64).sum::<i64>() / s.len() as i64;
511        assert!(mean_step(&looped("brown", 1)) * 4 < mean_step(&looped("white", 1)));
512    }
513
514    /// Energy around a frequency, per sample and per hertz of the band (Q = 2).
515    fn energy(samples: &[i16], frequency: f32) -> f32 {
516        let mut band = BandPass::default();
517        let power = samples.iter().map(|&x| band.run(f32::from(x) / 32768.0, frequency, 2.0).powi(2)).sum::<f32>() / samples.len() as f32;
518        power / (frequency / 2.0)
519    }
520
521    #[test]
522    fn nature_loops_and_sounds_like_itself() {
523        for kind in NATURE {
524            let (left, right) = nature(kind);
525            assert_eq!(left.len(), right.len());
526            assert!(left.len() >= 60 * NATURE_RATE as usize, "{kind}: a minute at least");
527            // The jump from the end to the start is no larger than the sound's own steps.
528            let step = |a: i16, b: i16| (i32::from(a) - i32::from(b)).abs();
529            let seam = step(left[left.len() - 1], left[0]);
530            let largest = left.windows(2).map(|w| step(w[0], w[1])).max().unwrap_or(0);
531            assert!(seam <= largest, "{kind}: seam {seam}, steps up to {largest}");
532            // Quiet, never clipped.
533            assert!(left.iter().all(|s| s.unsigned_abs() < 32000), "{kind}: clipped");
534        }
535        // Crickets sing near 4.5 kHz, far above their night's air; waves rumble low.
536        let (crickets, _) = nature("crickets");
537        if let Some(folder) = std::env::var_os("SIOUL_SOUNDS_OUT") {
538            for kind in NATURE {
539                let _ = noise_file(std::path::Path::new(&folder), kind);
540            }
541        }
542        assert!(energy(&crickets, 4500.0) > 5.0 * energy(&crickets, 1000.0));
543        let (waves, _) = nature("waves");
544        assert!(energy(&waves, 150.0) > energy(&waves, 5000.0));
545    }
546
547    #[test]
548    fn a_wav_header_says_what_follows() {
549        let bytes = wav(&[0, 1, -1]);
550        assert_eq!(&bytes[..4], b"RIFF");
551        let stereo = wav_of(&[0, 1, -1, 2], 2, NATURE_RATE);
552        assert_eq!(u16::from_le_bytes(stereo[22..24].try_into().unwrap()), 2);
553        assert_eq!(u32::from_le_bytes(stereo[28..32].try_into().unwrap()), NATURE_RATE * 4);
554        assert_eq!(u32::from_le_bytes(bytes[40..44].try_into().unwrap()), 6);
555        assert_eq!(bytes.len(), 44 + 6);
556    }
557}
558