Skip to content

Commit 3f6b41b

Browse files
committed
Add WAV file looping support for standalone mode
- New --wav flag: loops any .wav file through the filter - Supports 16/24/32-bit int and float WAV formats - Auto-resamples to output device sample rate - Mono WAV duplicates to all output channels - Updated run.sh and run.bat with usage examples
1 parent 884c920 commit 3f6b41b

File tree

5 files changed

+136
-38
lines changed

5 files changed

+136
-38
lines changed

plugin/Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugin/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@ required-features = ["standalone"]
2020

2121
[features]
2222
default = []
23-
standalone = ["cpal"]
23+
standalone = ["cpal", "hound"]
2424

2525
[dependencies.cpal]
2626
version = "0.15"
2727
optional = true
2828

29+
[dependencies.hound]
30+
version = "3.5"
31+
optional = true
32+
2933
[profile.release]
3034
lto = true
3135
strip = true

plugin/src/standalone.rs

Lines changed: 113 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
//! Standalone test harness: generates a test tone, runs it through the biquad LPF,
2-
//! and outputs to system audio. The Compose UI connects via TCP on localhost:9847
3-
//! exactly as it would with the DAW-hosted plugin.
1+
//! Standalone test harness: generates a test tone or loops a WAV file,
2+
//! runs it through the biquad LPF, and outputs to system audio.
3+
//! The Compose UI connects via TCP on localhost:9847.
44
//!
55
//! Usage:
66
//! cargo run --features standalone --bin compose-vst-standalone
77
//! cargo run --features standalone --bin compose-vst-standalone -- --tone noise
8-
//! cargo run --features standalone --bin compose-vst-standalone -- --freq 440 --tone sine
8+
//! cargo run --features standalone --bin compose-vst-standalone -- --tone sine --freq 440
9+
//! cargo run --features standalone --bin compose-vst-standalone -- --wav sample.wav
910
1011
#[cfg(not(feature = "standalone"))]
1112
compile_error!("Build with --features standalone");
1213

13-
// Include shared modules directly (can't link against cdylib)
1414
#[path = "filter.rs"]
1515
mod filter;
1616
#[path = "ipc_standalone.rs"]
@@ -51,6 +51,56 @@ fn main() {
5151
}
5252
}
5353

54+
/// Load a WAV file into interleaved f32 samples, resampled to target_sr if needed.
55+
/// Returns (samples, channels).
56+
fn load_wav(path: &str, target_sr: f32) -> (Vec<f32>, usize) {
57+
let reader = hound::WavReader::open(path)
58+
.unwrap_or_else(|e| panic!("Failed to open WAV file '{}': {}", path, e));
59+
60+
let spec = reader.spec();
61+
let wav_sr = spec.sample_rate as f32;
62+
let wav_channels = spec.channels as usize;
63+
64+
println!(" WAV: {}Hz, {} channels, {:?} {}bit",
65+
spec.sample_rate, spec.channels, spec.sample_format, spec.bits_per_sample);
66+
67+
// Read all samples as f32
68+
let raw_samples: Vec<f32> = match spec.sample_format {
69+
hound::SampleFormat::Int => {
70+
let max_val = (1u32 << (spec.bits_per_sample - 1)) as f32;
71+
reader.into_samples::<i32>()
72+
.map(|s| s.unwrap() as f32 / max_val)
73+
.collect()
74+
}
75+
hound::SampleFormat::Float => {
76+
reader.into_samples::<f32>()
77+
.map(|s| s.unwrap())
78+
.collect()
79+
}
80+
};
81+
82+
let num_frames = raw_samples.len() / wav_channels;
83+
println!(" Duration: {:.2}s ({} frames)", num_frames as f32 / wav_sr, num_frames);
84+
85+
// Simple nearest-neighbor resample if sample rates differ
86+
if (wav_sr - target_sr).abs() > 1.0 {
87+
println!(" Resampling {}Hz → {}Hz", wav_sr, target_sr);
88+
let ratio = wav_sr as f64 / target_sr as f64;
89+
let new_frames = (num_frames as f64 / ratio) as usize;
90+
let mut resampled = Vec::with_capacity(new_frames * wav_channels);
91+
92+
for i in 0..new_frames {
93+
let src_frame = ((i as f64 * ratio) as usize).min(num_frames - 1);
94+
for ch in 0..wav_channels {
95+
resampled.push(raw_samples[src_frame * wav_channels + ch]);
96+
}
97+
}
98+
(resampled, wav_channels)
99+
} else {
100+
(raw_samples, wav_channels)
101+
}
102+
}
103+
54104
fn parse_arg_str(args: &[String], flag: &str) -> Option<String> {
55105
args.iter().position(|a| a == flag).and_then(|i| args.get(i + 1).cloned())
56106
}
@@ -60,16 +110,18 @@ fn main() {
60110

61111
let args: Vec<String> = std::env::args().collect();
62112
let tone_freq = parse_arg_f32(&args, "--freq").unwrap_or(440.0);
63-
let use_noise = matches!(
64-
parse_arg_str(&args, "--tone").as_deref(),
65-
Some("noise") | Some("white-noise")
66-
);
67-
let use_sweep = parse_arg_str(&args, "--tone").as_deref() == Some("sweep");
113+
let wav_path = parse_arg_str(&args, "--wav");
114+
let tone_str = parse_arg_str(&args, "--tone");
115+
let use_noise = matches!(tone_str.as_deref(), Some("noise") | Some("white-noise"));
116+
let use_sweep = tone_str.as_deref() == Some("sweep");
117+
let use_wav = wav_path.is_some();
68118

69119
println!("╔══════════════════════════════════════════╗");
70120
println!("║ Compose VST - Standalone Test Mode ║");
71121
println!("╠══════════════════════════════════════════╣");
72-
if use_noise {
122+
if use_wav {
123+
println!("║ Source: WAV file (looping) ║");
124+
} else if use_noise {
73125
println!("║ Tone: white noise ║");
74126
} else if use_sweep {
75127
println!("║ Tone: sweep 20Hz-20kHz ║");
@@ -103,6 +155,12 @@ fn main() {
103155
let channels = config.channels() as usize;
104156
println!("Sample rate: {}Hz, Channels: {}", sample_rate, channels);
105157

158+
// Load WAV if specified
159+
let wav_data: Option<(Vec<f32>, usize)> = wav_path.as_ref().map(|p| {
160+
println!("Loading WAV: {}", p);
161+
load_wav(p, sample_rate)
162+
});
163+
106164
let state_audio = state.clone();
107165
let mut phase: f64 = 0.0;
108166
let mut sweep_freq: f64 = 20.0;
@@ -111,6 +169,12 @@ fn main() {
111169
let mut sample_count: u64 = 0;
112170
let send_interval = (sample_rate as u64 / 30).max(1);
113171

172+
// WAV playback state
173+
let mut wav_pos: usize = 0;
174+
let wav_samples = wav_data.as_ref().map(|(s, _)| s.clone());
175+
let wav_channels = wav_data.as_ref().map(|(_, c)| *c).unwrap_or(1);
176+
let wav_total_frames = wav_samples.as_ref().map(|s| s.len() / wav_channels).unwrap_or(0);
177+
114178
let stream = device.build_output_stream(
115179
&config.into(),
116180
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
@@ -121,30 +185,46 @@ fn main() {
121185
f.set_params(cutoff, resonance);
122186
}
123187

188+
let num_filters = filters.len();
189+
124190
for frame in data.chunks_mut(channels) {
125-
let raw = if use_noise {
126-
rng_state ^= rng_state << 13;
127-
rng_state ^= rng_state >> 17;
128-
rng_state ^= rng_state << 5;
129-
(rng_state as f32 / u32::MAX as f32 * 2.0 - 1.0) * 0.3
130-
} else if use_sweep {
131-
let s = (phase * 2.0 * std::f64::consts::PI).sin() as f32 * 0.3;
132-
phase += sweep_freq / sample_rate as f64;
133-
if phase >= 1.0 { phase -= 1.0; }
134-
sweep_freq = 20.0 * (20000.0_f64 / 20.0).powf(
135-
(sample_count as f64 % (sample_rate as f64 * 5.0)) / (sample_rate as f64 * 5.0)
136-
);
137-
s
191+
if let Some(ref wav) = wav_samples {
192+
// WAV looping playback
193+
for (ch, sample) in frame.iter_mut().enumerate() {
194+
// Map output channel to WAV channel (mono WAV → duplicate to all channels)
195+
let wav_ch = if ch < wav_channels { ch } else { ch % wav_channels };
196+
let raw = wav[wav_pos * wav_channels + wav_ch];
197+
*sample = filters[ch % num_filters].process(raw);
198+
}
199+
wav_pos += 1;
200+
if wav_pos >= wav_total_frames {
201+
wav_pos = 0; // loop
202+
}
138203
} else {
139-
let s = (phase * 2.0 * std::f64::consts::PI).sin() as f32 * 0.3;
140-
phase += tone_freq as f64 / sample_rate as f64;
141-
if phase >= 1.0 { phase -= 1.0; }
142-
s
143-
};
144-
145-
let num_filters = filters.len();
146-
for (ch, sample) in frame.iter_mut().enumerate() {
147-
*sample = filters[ch % num_filters].process(raw);
204+
// Generated tone
205+
let raw = if use_noise {
206+
rng_state ^= rng_state << 13;
207+
rng_state ^= rng_state >> 17;
208+
rng_state ^= rng_state << 5;
209+
(rng_state as f32 / u32::MAX as f32 * 2.0 - 1.0) * 0.3
210+
} else if use_sweep {
211+
let s = (phase * 2.0 * std::f64::consts::PI).sin() as f32 * 0.3;
212+
phase += sweep_freq / sample_rate as f64;
213+
if phase >= 1.0 { phase -= 1.0; }
214+
sweep_freq = 20.0 * (20000.0_f64 / 20.0).powf(
215+
(sample_count as f64 % (sample_rate as f64 * 5.0)) / (sample_rate as f64 * 5.0)
216+
);
217+
s
218+
} else {
219+
let s = (phase * 2.0 * std::f64::consts::PI).sin() as f32 * 0.3;
220+
phase += tone_freq as f64 / sample_rate as f64;
221+
if phase >= 1.0 { phase -= 1.0; }
222+
s
223+
};
224+
225+
for (ch, sample) in frame.iter_mut().enumerate() {
226+
*sample = filters[ch % num_filters].process(raw);
227+
}
148228
}
149229
sample_count += 1;
150230
}

run.bat

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
@echo off
22
REM Launch Compose VST standalone (Rust audio + Compose UI)
3-
REM Usage: run.bat [--tone sine|noise|sweep] [--freq 440]
3+
REM Usage:
4+
REM run.bat default 440Hz sine
5+
REM run.bat --tone noise white noise
6+
REM run.bat --tone sweep frequency sweep
7+
REM run.bat --wav sample.wav loop a WAV file
48
REM Close this window to shut everything down.
59

610
setlocal
@@ -25,5 +29,4 @@ echo.
2529
echo ✅ Both running. Close this window to stop.
2630
echo.
2731

28-
REM Wait for user to close — on window close, child processes terminate
2932
pause >nul

run.sh

100644100755
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
#!/usr/bin/env bash
22
# Launch Compose VST standalone (Rust audio + Compose UI)
3-
# Usage: ./run.sh [--tone sine|noise|sweep] [--freq 440]
3+
# Usage:
4+
# ./run.sh # default 440Hz sine
5+
# ./run.sh --tone noise # white noise
6+
# ./run.sh --tone sweep # frequency sweep
7+
# ./run.sh --wav sample.wav # loop a WAV file
8+
# ./run.sh --wav sample.wav --freq 440 # (--freq ignored with --wav)
49
# Press Ctrl+C or close the terminal to shut everything down.
510

611
set -e
@@ -10,7 +15,6 @@ RUST_ARGS=("$@")
1015
cleanup() {
1116
echo ""
1217
echo "Shutting down..."
13-
# Kill child processes
1418
[[ -n "$RUST_PID" ]] && kill "$RUST_PID" 2>/dev/null
1519
[[ -n "$UI_PID" ]] && kill "$UI_PID" 2>/dev/null
1620
wait 2>/dev/null

0 commit comments

Comments
 (0)