tunes 1.1.0

A music composition, synthesis, and audio generation library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
//! Spectral freeze effect - capture and hold spectrum

use super::*;
use rustfft::num_complex::Complex;

/// Spectral freeze effect - captures and holds a frequency spectrum snapshot
///
/// This effect captures the current frequency content and holds it indefinitely,
/// creating a sustained "frozen" sound. Perfect for ambient textures, drones,
/// and creating evolving soundscapes from transient sounds.
///
/// Uses SIMD-accelerated STFT for efficient real-time processing.
///
/// # Example
/// ```
/// # use tunes::synthesis::spectral::{SpectralFreeze, WindowType};
/// // Create spectral freeze with 2048 FFT, 512 hop
/// let mut freeze = SpectralFreeze::new(2048, 512, WindowType::Hann);
///
/// // Capture and freeze the spectrum
/// freeze.freeze();
///
/// // Set mix to 100% frozen (0% live)
/// freeze.set_mix(1.0);
///
/// // Process audio - will hold frozen spectrum
/// let input = vec![0.0; 512];
/// let mut output = vec![0.0; 512];
/// freeze.process(&mut output, &input);
///
/// // Unfreeze to return to normal processing
/// freeze.unfreeze();
/// ```
#[derive(Clone)]
pub struct SpectralFreeze {
    /// STFT processor for analysis/synthesis
    stft: STFT,

    /// Frozen spectrum (captured when freeze is enabled)
    frozen_spectrum: Vec<Complex<f32>>,

    /// FFT size
    fft_size: usize,

    /// Freeze enabled flag
    is_frozen: bool,

    /// Mix amount (0.0 = all live signal, 1.0 = all frozen spectrum)
    mix: f32,

    /// Whether we've captured a spectrum yet
    has_captured: bool,
}

impl SpectralFreeze {
    /// Create a new spectral freeze effect
    ///
    /// # Arguments
    /// * `fft_size` - FFT size (must be power of 2, typically 2048 or 4096)
    /// * `hop_size` - Hop size in samples (typically fft_size/4 for 75% overlap)
    /// * `window_type` - Window function type
    ///
    /// # Example
    /// ```
    /// # use tunes::synthesis::spectral::{SpectralFreeze, WindowType};
    /// let freeze = SpectralFreeze::new(2048, 512, WindowType::Hann);
    /// ```
    pub fn new(fft_size: usize, hop_size: usize, window_type: WindowType) -> Self {
        assert!(fft_size.is_power_of_two(), "FFT size must be power of 2");
        assert!(hop_size <= fft_size, "Hop size must be <= FFT size");

        let stft = STFT::new(fft_size, hop_size, window_type);

        Self {
            stft,
            frozen_spectrum: vec![Complex::new(0.0, 0.0); fft_size],
            fft_size,
            is_frozen: false,
            mix: 1.0, // Default to 100% frozen when active
            has_captured: false,
        }
    }

    /// Enable freeze and start capturing spectrum
    ///
    /// The next processed frame will be captured and held indefinitely.
    ///
    /// # Example
    /// ```
    /// # use tunes::synthesis::spectral::{SpectralFreeze, WindowType};
    /// let mut freeze = SpectralFreeze::new(2048, 512, WindowType::Hann);
    /// freeze.freeze();  // Start freezing
    /// assert!(freeze.is_frozen());
    /// ```
    pub fn freeze(&mut self) {
        self.is_frozen = true;
    }

    /// Disable freeze and return to normal processing
    ///
    /// # Example
    /// ```
    /// # use tunes::synthesis::spectral::{SpectralFreeze, WindowType};
    /// let mut freeze = SpectralFreeze::new(2048, 512, WindowType::Hann);
    /// freeze.freeze();
    /// freeze.unfreeze();  // Stop freezing
    /// assert!(!freeze.is_frozen());
    /// ```
    pub fn unfreeze(&mut self) {
        self.is_frozen = false;
    }

    /// Set mix amount between live and frozen signals
    ///
    /// # Arguments
    /// * `mix` - Mix amount (0.0 = all live, 1.0 = all frozen, clamped to [0.0, 1.0])
    ///
    /// # Example
    /// ```
    /// # use tunes::synthesis::spectral::{SpectralFreeze, WindowType};
    /// let mut freeze = SpectralFreeze::new(2048, 512, WindowType::Hann);
    /// freeze.freeze();
    /// freeze.set_mix(0.5);  // 50% live, 50% frozen
    /// ```
    pub fn set_mix(&mut self, mix: f32) {
        self.mix = mix.clamp(0.0, 1.0);
    }

    /// Get whether freeze is currently enabled
    pub fn is_frozen(&self) -> bool {
        self.is_frozen
    }

    /// Get current mix amount
    pub fn mix(&self) -> f32 {
        self.mix
    }

    /// Process audio through the spectral freeze
    ///
    /// When frozen, captures and holds the spectrum. The mix parameter controls
    /// the blend between live and frozen signals.
    ///
    /// # Arguments
    /// * `output` - Output buffer (will be filled with processed audio)
    /// * `input` - Input audio buffer
    ///
    /// # Example
    /// ```
    /// # use tunes::synthesis::spectral::{SpectralFreeze, WindowType};
    /// let mut freeze = SpectralFreeze::new(2048, 512, WindowType::Hann);
    /// freeze.freeze();
    ///
    /// let input = vec![0.0; 512];
    /// let mut output = vec![0.0; 512];
    /// freeze.process(&mut output, &input);
    /// ```
    pub fn process(&mut self, output: &mut [f32], _input: &[f32]) {
        let is_frozen = self.is_frozen;
        let mix = self.mix;
        let frozen_spectrum = &mut self.frozen_spectrum;
        let has_captured = &mut self.has_captured;

        self.stft.process(output, |spectrum| {
            if is_frozen {
                if !*has_captured {
                    // Capture the current spectrum
                    frozen_spectrum.copy_from_slice(spectrum);
                    *has_captured = true;
                }

                // Mix frozen and live spectrums using SIMD
                Self::mix_spectrums_simd(spectrum, frozen_spectrum, mix);
            } else {
                // Not frozen: pass through live signal (no modification)
                *has_captured = false;
            }
        });
    }

    /// Mix two spectrums together using SIMD operations
    ///
    /// output = live * (1 - mix) + frozen * mix
    ///
    /// # Arguments
    /// * `live` - Live spectrum (will be modified in-place)
    /// * `frozen` - Frozen spectrum to mix in
    /// * `mix` - Mix amount (0.0 = all live, 1.0 = all frozen)
    #[inline]
    fn mix_spectrums_simd(live: &mut [Complex<f32>], frozen: &[Complex<f32>], mix: f32) {
        let len = live.len().min(frozen.len());
        let live_gain = 1.0 - mix;

        // Extract to separate buffers for SIMD processing
        let mut live_re = vec![0.0f32; len];
        let mut live_im = vec![0.0f32; len];
        let mut frozen_re = vec![0.0f32; len];
        let mut frozen_im = vec![0.0f32; len];

        for i in 0..len {
            live_re[i] = live[i].re;
            live_im[i] = live[i].im;
            frozen_re[i] = frozen[i].re;
            frozen_im[i] = frozen[i].im;
        }

        // Scale both signals using TRUE SIMD
        SIMD.multiply_const(&mut live_re, live_gain);
        SIMD.multiply_const(&mut live_im, live_gain);
        SIMD.multiply_const(&mut frozen_re, mix);
        SIMD.multiply_const(&mut frozen_im, mix);

        // Add them together using TRUE SIMD
        for i in 0..len {
            live_re[i] += frozen_re[i];
            live_im[i] += frozen_im[i];
        }

        // Write back
        for i in 0..len {
            live[i] = Complex::new(live_re[i], live_im[i]);
        }
    }

    /// Reset the spectral freeze state
    ///
    /// Clears the frozen spectrum and STFT buffers.
    pub fn reset(&mut self) {
        self.stft.reset();
        self.frozen_spectrum.fill(Complex::new(0.0, 0.0));
        self.is_frozen = false;
        self.has_captured = false;
    }

    /// Get the FFT size
    pub fn fft_size(&self) -> usize {
        self.fft_size
    }

    /// Get the hop size
    pub fn hop_size(&self) -> usize {
        self.stft.hop_size()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_spectral_freeze_creation() {
        let freeze = SpectralFreeze::new(2048, 512, WindowType::Hann);
        assert_eq!(freeze.fft_size(), 2048);
        assert_eq!(freeze.hop_size(), 512);
        assert!(!freeze.is_frozen());
        assert_eq!(freeze.mix(), 1.0);
    }

    #[test]
    #[should_panic(expected = "FFT size must be power of 2")]
    fn test_spectral_freeze_requires_power_of_two() {
        SpectralFreeze::new(1000, 250, WindowType::Hann);
    }

    #[test]
    #[should_panic(expected = "Hop size must be <= FFT size")]
    fn test_spectral_freeze_hop_validation() {
        SpectralFreeze::new(1024, 2048, WindowType::Hann);
    }

    #[test]
    fn test_spectral_freeze_freeze_unfreeze() {
        let mut freeze = SpectralFreeze::new(1024, 256, WindowType::Hann);

        assert!(!freeze.is_frozen());

        freeze.freeze();
        assert!(freeze.is_frozen());

        freeze.unfreeze();
        assert!(!freeze.is_frozen());
    }

    #[test]
    fn test_spectral_freeze_set_mix() {
        let mut freeze = SpectralFreeze::new(1024, 256, WindowType::Hann);

        freeze.set_mix(0.5);
        assert_eq!(freeze.mix(), 0.5);

        freeze.set_mix(0.0);
        assert_eq!(freeze.mix(), 0.0);

        freeze.set_mix(1.0);
        assert_eq!(freeze.mix(), 1.0);
    }

    #[test]
    fn test_spectral_freeze_mix_clamping() {
        let mut freeze = SpectralFreeze::new(1024, 256, WindowType::Hann);

        // Should clamp to [0.0, 1.0]
        freeze.set_mix(1.5);
        assert_eq!(freeze.mix(), 1.0);

        freeze.set_mix(-0.5);
        assert_eq!(freeze.mix(), 0.0);
    }

    #[test]
    fn test_spectral_freeze_process_silent() {
        let mut freeze = SpectralFreeze::new(1024, 256, WindowType::Hann);
        let input = vec![0.0; 512];
        let mut output = vec![0.0; 512];

        // Process silence without freezing (should remain silent)
        freeze.process(&mut output, &input);

        for &sample in &output {
            assert!(sample.abs() < 0.001, "Expected silence, got {}", sample);
        }
    }

    #[test]
    fn test_spectral_freeze_process_frozen() {
        let mut freeze = SpectralFreeze::new(512, 128, WindowType::Hann);
        freeze.freeze();
        freeze.set_mix(1.0); // 100% frozen

        let input = vec![0.0; 256];
        let mut output = vec![0.0; 256];

        // Should process without crashing
        freeze.process(&mut output, &input);
        assert_eq!(output.len(), 256);
    }

    #[test]
    fn test_spectral_freeze_process_live() {
        let mut freeze = SpectralFreeze::new(512, 128, WindowType::Hann);
        freeze.set_mix(0.0); // 100% live (no freeze effect)

        let input = vec![0.0; 256];
        let mut output = vec![0.0; 256];

        // Should process without crashing
        freeze.process(&mut output, &input);
        assert_eq!(output.len(), 256);
    }

    #[test]
    fn test_spectral_freeze_process_mixed() {
        let mut freeze = SpectralFreeze::new(512, 128, WindowType::Hann);
        freeze.freeze();
        freeze.set_mix(0.5); // 50% live, 50% frozen

        let input = vec![0.0; 256];
        let mut output = vec![0.0; 256];

        // Should process without crashing
        freeze.process(&mut output, &input);
        assert_eq!(output.len(), 256);
    }

    #[test]
    fn test_spectral_freeze_reset() {
        let mut freeze = SpectralFreeze::new(512, 128, WindowType::Hann);

        // Enable freeze and process
        freeze.freeze();
        let input = vec![0.0; 256];
        let mut output = vec![0.0; 256];
        freeze.process(&mut output, &input);

        // Reset should clear state
        freeze.reset();
        assert!(!freeze.is_frozen());

        // Should still work after reset
        freeze.process(&mut output, &input);
        assert_eq!(output.len(), 256);
    }

    #[test]
    fn test_spectral_freeze_all_window_types() {
        for window_type in [
            WindowType::Rectangular,
            WindowType::Hann,
            WindowType::Hamming,
            WindowType::Blackman,
            WindowType::BlackmanHarris,
        ] {
            let mut freeze = SpectralFreeze::new(512, 128, window_type);
            freeze.freeze();

            let input = vec![0.0; 256];
            let mut output = vec![0.0; 256];

            freeze.process(&mut output, &input);
            assert_eq!(output.len(), 256);
        }
    }

    #[test]
    fn test_spectral_freeze_various_fft_sizes() {
        for fft_size in [512, 1024, 2048, 4096] {
            let hop_size = fft_size / 4;
            let mut freeze = SpectralFreeze::new(fft_size, hop_size, WindowType::Hann);
            freeze.freeze();

            let input = vec![0.0; 512];
            let mut output = vec![0.0; 512];

            freeze.process(&mut output, &input);
            assert_eq!(output.len(), 512);
        }
    }

    #[test]
    fn test_spectral_freeze_toggle() {
        let mut freeze = SpectralFreeze::new(512, 128, WindowType::Hann);
        let input = vec![0.0; 256];
        let mut output = vec![0.0; 256];

        // Process normally
        freeze.process(&mut output, &input);

        // Freeze
        freeze.freeze();
        freeze.process(&mut output, &input);

        // Unfreeze
        freeze.unfreeze();
        freeze.process(&mut output, &input);

        // Should complete without errors
        assert_eq!(output.len(), 256);
    }
}