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" ) ) ]
1112compile_error ! ( "Build with --features standalone" ) ;
1213
13- // Include shared modules directly (can't link against cdylib)
1414#[ path = "filter.rs" ]
1515mod 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 }
0 commit comments