Skip to content

Commit ed7b6fe

Browse files
committed
feat: Add phaser effect implementation and UI controls for real-time audio processing
1 parent d70abbf commit ed7b6fe

File tree

3 files changed

+207
-2
lines changed

3 files changed

+207
-2
lines changed

examples/phaser_example.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
//! Phaser Example
2+
//!
3+
//! This example demonstrates how to create a phaser effect using
4+
//! all-pass filters.
5+
6+
use cute_dsp::delay::{Delay, InterpolatorLinear};
7+
8+
struct AllPassFilter {
9+
delay: Delay<f32, InterpolatorLinear<f32>>,
10+
feedback: f32,
11+
}
12+
13+
impl AllPassFilter {
14+
fn new(max_delay_samples: usize, feedback: f32) -> Self {
15+
Self {
16+
delay: Delay::new(InterpolatorLinear::new(), max_delay_samples),
17+
feedback,
18+
}
19+
}
20+
21+
fn process(&mut self, input: f32) -> f32 {
22+
let delayed = self.delay.read(5.0); // Fixed delay of 5 samples for simplicity
23+
let output = input + delayed * self.feedback;
24+
self.delay.write(output);
25+
delayed - output * self.feedback
26+
}
27+
}
28+
29+
struct Phaser {
30+
allpass_filters: Vec<AllPassFilter>,
31+
mix: f32,
32+
}
33+
34+
impl Phaser {
35+
fn new() -> Self {
36+
let mut allpass_filters = Vec::new();
37+
38+
// Create 4 all-pass filters with different delays
39+
for i in 0..4 {
40+
let max_delay = 10 + i * 2; // Different delay lengths
41+
allpass_filters.push(AllPassFilter::new(max_delay, 0.7));
42+
}
43+
44+
Self {
45+
allpass_filters,
46+
mix: 0.5,
47+
}
48+
}
49+
50+
fn set_mix(&mut self, mix: f32) {
51+
self.mix = mix;
52+
}
53+
54+
fn process(&mut self, input: f32) -> f32 {
55+
// Process through all-pass filters
56+
let mut output = input;
57+
for filter in &mut self.allpass_filters {
58+
output = filter.process(output);
59+
}
60+
61+
// Mix dry/wet
62+
input * (1.0 - self.mix) + output * self.mix
63+
}
64+
}
65+
66+
fn main() {
67+
println!("Phaser Example");
68+
println!("==============");
69+
70+
const BUFFER_SIZE: usize = 100;
71+
72+
// Create phaser
73+
let mut phaser = Phaser::new();
74+
75+
// Test with impulse
76+
let mut impulse = vec![0.0; BUFFER_SIZE];
77+
impulse[0] = 1.0;
78+
79+
println!("Phaser impulse response (first 20 samples):");
80+
for i in 0..20 {
81+
let output = phaser.process(impulse[i]);
82+
println!("Sample {}: {:.6}", i, output);
83+
}
84+
85+
// Test with sine wave
86+
println!("\nPhaser on sine wave (first 20 samples):");
87+
phaser = Phaser::new(); // Reset
88+
for i in 0..20 {
89+
let input = (i as f32 * 440.0 * 2.0 * std::f32::consts::PI / 44100.0).sin() * 0.5;
90+
let output = phaser.process(input);
91+
println!("Sample {}: {:.6}", i, output);
92+
}
93+
}

web_example/audio-processor.js

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,69 @@ class SimpleChorus {
272272
}
273273
}
274274

275+
// Simple Phaser Effect
276+
class SimplePhaser {
277+
constructor(sampleRate) {
278+
this.sampleRate = sampleRate;
279+
this.allpassFilters = [];
280+
281+
// Create 4 all-pass filters for the phaser
282+
for (let i = 0; i < 4; i++) {
283+
this.allpassFilters.push({
284+
delay: Math.floor((0.005 + i * 0.002) * sampleRate), // 5ms to 11ms delays
285+
feedback: 0.7,
286+
buffer: new Float32Array(Math.ceil(0.015 * sampleRate)), // 15ms max
287+
writeIndex: 0
288+
});
289+
}
290+
291+
this.lfo = new SimpleLFO();
292+
this.lfo.set_frequency(0.5, sampleRate); // 0.5 Hz modulation
293+
294+
this.mix = 0.5;
295+
this.depth = 0.5;
296+
}
297+
298+
set_mix(value) {
299+
this.mix = value;
300+
}
301+
302+
set_depth(value) {
303+
this.depth = value;
304+
}
305+
306+
process(input, output) {
307+
// Start with input
308+
for (let i = 0; i < input.length; i++) {
309+
output[i] = input[i];
310+
}
311+
312+
// Get LFO value for modulation
313+
const lfoValue = this.lfo.process();
314+
315+
// Process through all-pass filters with modulated feedback
316+
for (let filter of this.allpassFilters) {
317+
const modulatedFeedback = filter.feedback * (1 + lfoValue * this.depth);
318+
319+
for (let i = 0; i < input.length; i++) {
320+
const readIndex = (filter.writeIndex - filter.delay + filter.buffer.length) % filter.buffer.length;
321+
const delayed = filter.buffer[readIndex];
322+
323+
const filtered = output[i] + delayed * modulatedFeedback;
324+
filter.buffer[filter.writeIndex] = filtered;
325+
filter.writeIndex = (filter.writeIndex + 1) % filter.buffer.length;
326+
327+
output[i] = filtered * -modulatedFeedback + delayed;
328+
}
329+
}
330+
331+
// Mix dry/wet
332+
for (let i = 0; i < input.length; i++) {
333+
output[i] = input[i] * (1 - this.mix) + output[i] * this.mix;
334+
}
335+
}
336+
}
337+
275338
class CuteDSPProcessor extends AudioWorkletProcessor {
276339
constructor() {
277340
super();
@@ -281,6 +344,7 @@ class CuteDSPProcessor extends AudioWorkletProcessor {
281344
this.distortion = null;
282345
this.reverb = null;
283346
this.chorus = null;
347+
this.phaser = null;
284348
this.sampleRate = sampleRate;
285349
this.isInitialized = false;
286350

@@ -290,7 +354,8 @@ class CuteDSPProcessor extends AudioWorkletProcessor {
290354
delay: true,
291355
distortion: false,
292356
reverb: false,
293-
chorus: false
357+
chorus: false,
358+
phaser: false
294359
};
295360

296361
// Pre-allocate buffers to avoid allocations during processing
@@ -343,6 +408,12 @@ class CuteDSPProcessor extends AudioWorkletProcessor {
343408
this.chorus.set_depth(data.depth);
344409
}
345410
break;
411+
case 'updatePhaser':
412+
if (this.phaser) {
413+
this.phaser.set_mix(data.mix);
414+
this.phaser.set_depth(data.depth);
415+
}
416+
break;
346417
case 'toggleEffect':
347418
if (this.effectsEnabled.hasOwnProperty(data.effect)) {
348419
this.effectsEnabled[data.effect] = data.enabled;
@@ -378,6 +449,10 @@ class CuteDSPProcessor extends AudioWorkletProcessor {
378449
this.chorus.set_mix(0.4);
379450
this.chorus.set_depth(0.005);
380451

452+
this.phaser = new SimplePhaser(this.sampleRate);
453+
this.phaser.set_mix(0.5);
454+
this.phaser.set_depth(0.5);
455+
381456
this.isInitialized = true;
382457
this.port.postMessage({ type: 'ready' });
383458

@@ -476,6 +551,12 @@ class CuteDSPProcessor extends AudioWorkletProcessor {
476551
this.reverb.process(output, this.tempBuffer1);
477552
[output, this.tempBuffer1] = [this.tempBuffer1, output]; // Swap buffers
478553
}
554+
555+
// Apply phaser (if enabled)
556+
if (this.effectsEnabled.phaser) {
557+
this.phaser.process(output, this.tempBuffer1);
558+
[output, this.tempBuffer1] = [this.tempBuffer1, output]; // Swap buffers
559+
}
479560
}
480561
}
481562

web_example/index.html

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@
159159
}
160160

161161
.status.error {
162-
background: rgba(231, 76, 60, 0.2);
162+
background: rgba(
163163
border: 1px solid rgba(231, 76, 60, 0.3);
164164
}
165165

@@ -291,6 +291,21 @@ <h3>🌊 Chorus</h3>
291291
<label><input type="checkbox" id="chorus-enabled"> Enable Chorus</label>
292292
</div>
293293
</div>
294+
295+
<div class="control-group">
296+
<h3>🌈 Phaser</h3>
297+
<div class="slider-container">
298+
<label for="phaser-mix">Mix: <span id="phaser-mix-value">0.5</span></label>
299+
<input type="range" id="phaser-mix" min="0.0" max="1.0" value="0.5" step="0.01">
300+
</div>
301+
<div class="slider-container">
302+
<label for="phaser-depth">Depth: <span id="phaser-depth-value">0.5</span></label>
303+
<input type="range" id="phaser-depth" min="0.0" max="1.0" value="0.5" step="0.01">
304+
</div>
305+
<div style="margin-top: 10px;">
306+
<label><input type="checkbox" id="phaser-enabled"> Enable Phaser</label>
307+
</div>
308+
</div>
294309
</div>
295310

296311
<div style="text-align: center; margin: 30px 0;">
@@ -313,6 +328,7 @@ <h3>ℹ️ About This Demo</h3>
313328
<li><strong>Distortion:</strong> Soft-clipping distortion with drive and amount controls</li>
314329
<li><strong>Reverb:</strong> Schroeder reverb with adjustable mix and decay</li>
315330
<li><strong>Chorus:</strong> Multi-voice chorus effect with modulation depth control</li>
331+
<li><strong>Phaser:</strong> All-pass filter phaser with LFO modulation and adjustable mix/depth</li>
316332
</ul>
317333
<p><strong>Features:</strong></p>
318334
<ul>
@@ -577,6 +593,17 @@ <h3>ℹ️ About This Demo</h3>
577593
}
578594
}
579595

596+
updatePhaser() {
597+
if (this.worklet && this.isProcessing) {
598+
const mix = this.getPhaserMix();
599+
const depth = this.getPhaserDepth();
600+
this.worklet.port.postMessage({
601+
type: 'updatePhaser',
602+
data: { mix, depth }
603+
});
604+
}
605+
}
606+
580607
toggleEffect(effect, enabled) {
581608
if (this.worklet && this.isProcessing) {
582609
this.worklet.port.postMessage({
@@ -600,6 +627,8 @@ <h3>ℹ️ About This Demo</h3>
600627
getReverbDecay() { return parseFloat(document.getElementById('reverb-decay').value); }
601628
getChorusMix() { return parseFloat(document.getElementById('chorus-mix').value); }
602629
getChorusDepth() { return parseFloat(document.getElementById('chorus-depth').value); }
630+
getPhaserMix() { return parseFloat(document.getElementById('phaser-mix').value); }
631+
getPhaserDepth() { return parseFloat(document.getElementById('phaser-depth').value); }
603632
}
604633

605634
// Initialize the application
@@ -662,6 +691,8 @@ <h3>ℹ️ About This Demo</h3>
662691
processor.updateReverb();
663692
} else if (slider.id === 'chorus-mix' || slider.id === 'chorus-depth') {
664693
processor.updateChorus();
694+
} else if (slider.id === 'phaser-mix' || slider.id === 'phaser-depth') {
695+
processor.updatePhaser();
665696
}
666697
delete updateThrottle[slider.id];
667698
}, 16); // ~60fps throttling (16ms)

0 commit comments

Comments
 (0)